refactor(brand): adopt new primitives in tracker cards
CashFlowCard, OverdueCommandCenter, SummaryCards, TrackerBucket, and
the shared StatusBadge move to the new TonedCard / ToneDot / BrandGlyph
primitives. The biggest single change is SummaryCards (81 lines
churn) where the per-card tone tokens now defer to the design system.
This is commit 5 of 9 in the v0.42.0 brand refresh. Depends on
c2b6a7d (UI primitives).
This commit is contained in:
parent
c2b6a7d1b4
commit
b714715ddb
|
|
@ -3,12 +3,12 @@ import { AlertCircle } from 'lucide-react';
|
|||
import { cn } from '@/lib/utils';
|
||||
|
||||
const STATUS_META = {
|
||||
paid: { label: 'Paid', cls: 'bg-emerald-500/15 text-emerald-500 border border-emerald-500/30 dark:bg-emerald-300/10 dark:text-emerald-200 dark:border-emerald-300/30' },
|
||||
paid: { label: 'Paid', cls: 'bg-emerald-500/15 text-emerald-700 border border-emerald-500/30 dark:bg-emerald-300/10 dark:text-emerald-200 dark:border-emerald-300/30' },
|
||||
upcoming: { label: 'Upcoming', cls: 'bg-secondary text-muted-foreground border border-border' },
|
||||
due_soon: { label: 'Due Soon', cls: 'bg-amber-400/15 text-amber-500 border border-amber-400/30 dark:bg-amber-300/10 dark:text-amber-200 dark:border-amber-300/28' },
|
||||
late: { label: 'Late', cls: 'bg-orange-500/30 text-orange-800 border border-orange-500/60 shadow-sm shadow-orange-950/10 dark:bg-orange-400/25 dark:text-orange-100 dark:border-orange-300/60' },
|
||||
missed: { label: 'Missed', cls: 'bg-rose-500/30 text-rose-800 border border-rose-500/70 shadow-sm shadow-rose-950/10 dark:bg-rose-400/25 dark:text-rose-100 dark:border-rose-300/60' },
|
||||
autodraft: { label: 'Autodraft', cls: 'bg-sky-400/15 text-sky-500 border border-sky-400/30 dark:bg-sky-300/10 dark:text-sky-200 dark:border-sky-300/28' },
|
||||
due_soon: { label: 'Due Soon', cls: 'bg-amber-400/15 text-amber-700 border border-amber-400/35 dark:bg-amber-300/10 dark:text-amber-200 dark:border-amber-300/28' },
|
||||
late: { label: 'Late', cls: 'bg-orange-500/20 text-orange-700 border border-orange-500/50 shadow-sm shadow-orange-950/10 dark:bg-orange-400/25 dark:text-orange-100 dark:border-orange-300/60' },
|
||||
missed: { label: 'Missed', cls: 'bg-rose-500/20 text-rose-700 border border-rose-500/55 shadow-sm shadow-rose-950/10 dark:bg-rose-400/25 dark:text-rose-100 dark:border-rose-300/60' },
|
||||
autodraft: { label: 'Autodraft', cls: 'bg-teal-400/15 text-teal-700 border border-teal-400/35 dark:bg-teal-300/10 dark:text-teal-200 dark:border-teal-300/28' },
|
||||
skipped: { label: 'Skipped', cls: 'bg-muted text-muted-foreground border border-border' },
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -93,13 +93,13 @@ function OverdueRow({ row, year, month, onPayNow, onRefresh }: OverdueRowProps)
|
|||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-rose-400 dark:text-rose-300">
|
||||
<span className="text-xs font-medium text-rose-600 dark:text-rose-300">
|
||||
{daysOverdueLabel(row.due_date)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Amount */}
|
||||
<span className="shrink-0 font-mono text-sm font-semibold text-rose-400 dark:text-rose-300">
|
||||
<span className="shrink-0 font-mono text-sm font-semibold text-rose-600 dark:text-rose-300">
|
||||
{fmt(threshold)}
|
||||
</span>
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ function OverdueRow({ row, year, month, onPayNow, onRefresh }: OverdueRowProps)
|
|||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 gap-1.5 border-rose-400/40 px-2.5 text-xs text-rose-500 hover:border-rose-400/70 hover:bg-rose-500/[0.08] hover:text-rose-400"
|
||||
className="h-7 gap-1.5 border-rose-500/40 px-2.5 text-xs text-rose-600 hover:border-rose-500/70 hover:bg-rose-500/[0.08] hover:text-rose-700 dark:text-rose-300 dark:hover:text-rose-200"
|
||||
disabled={loading}
|
||||
onClick={() => onPayNow(row)}
|
||||
>
|
||||
|
|
@ -185,17 +185,17 @@ export default function OverdueCommandCenter({ rows, year, month, refresh, onPay
|
|||
|
||||
return (
|
||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||
<div className="rounded-xl border border-rose-400/30 bg-rose-500/[0.06] shadow-sm overflow-hidden dark:bg-rose-400/[0.05]">
|
||||
<div className="surface-premium overflow-hidden border-rose-500/30 bg-rose-500/[0.055] dark:border-rose-300/25 dark:bg-rose-400/[0.05]">
|
||||
|
||||
{/* Header */}
|
||||
<CollapsibleTrigger asChild>
|
||||
<button className="flex w-full items-center justify-between px-4 py-3 transition-colors hover:bg-rose-500/[0.04] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-400/40 focus-visible:ring-inset">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<AlertCircle className="h-4 w-4 shrink-0 text-rose-400" />
|
||||
<AlertCircle className="h-4 w-4 shrink-0 text-rose-600 dark:text-rose-300" />
|
||||
<span className="text-sm font-semibold text-foreground">
|
||||
{`${overdueRows.length} overdue ${overdueRows.length === 1 ? 'bill' : 'bills'}`}
|
||||
</span>
|
||||
<span className="font-mono text-sm text-rose-400 dark:text-rose-300">
|
||||
<span className="font-mono text-sm font-semibold text-rose-600 dark:text-rose-300">
|
||||
{fmt(asDollars(totalOverdue))}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import { TrendingUp, AlertCircle, Clock, CheckCircle2, Settings2, type LucideIcon } from 'lucide-react';
|
||||
import { cn, fmt } from '@/lib/utils';
|
||||
import type { Dollars } from '@/lib/money';
|
||||
import { MetricCard } from '@/components/ui/app-primitives';
|
||||
|
||||
type CardType = 'starting' | 'paid' | 'remaining' | 'overdue';
|
||||
|
||||
interface CardDef {
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
bar: string;
|
||||
glow: string;
|
||||
borderActive?: string;
|
||||
valueClass: string;
|
||||
tone: 'neutral' | 'good' | 'warn' | 'danger' | 'info';
|
||||
activateWhen: (v: number) => boolean;
|
||||
}
|
||||
|
||||
|
|
@ -18,35 +17,29 @@ const CARD_DEFS: Record<CardType, CardDef> = {
|
|||
starting: {
|
||||
label: 'Starting',
|
||||
icon: TrendingUp,
|
||||
bar: 'from-slate-400 to-slate-300',
|
||||
glow: '',
|
||||
valueClass: 'text-foreground',
|
||||
tone: 'neutral',
|
||||
activateWhen: () => true,
|
||||
},
|
||||
paid: {
|
||||
label: 'Total Paid',
|
||||
icon: CheckCircle2,
|
||||
bar: 'from-emerald-500 to-emerald-300',
|
||||
glow: 'shadow-[0_4px_20px_rgba(16,185,129,0.15)]',
|
||||
borderActive: 'border-emerald-400/40',
|
||||
valueClass: 'text-emerald-600 dark:text-emerald-200',
|
||||
tone: 'good',
|
||||
activateWhen: (v) => v > 0,
|
||||
},
|
||||
remaining: {
|
||||
label: 'Remaining',
|
||||
icon: Clock,
|
||||
bar: 'from-blue-400 to-indigo-300',
|
||||
glow: '',
|
||||
valueClass: 'text-foreground',
|
||||
tone: 'info',
|
||||
activateWhen: () => true,
|
||||
},
|
||||
overdue: {
|
||||
label: 'Overdue',
|
||||
icon: AlertCircle,
|
||||
bar: 'from-rose-400 to-orange-300',
|
||||
glow: 'shadow-[0_4px_20px_rgba(251,113,133,0.10)]',
|
||||
borderActive: 'border-rose-400/35',
|
||||
valueClass: 'text-red-500 dark:text-rose-200',
|
||||
tone: 'danger',
|
||||
activateWhen: (v) => v > 0,
|
||||
},
|
||||
};
|
||||
|
|
@ -106,23 +99,18 @@ export function SummaryCard({ type, value, onEdit, hint, label }: SummaryCardPro
|
|||
const displayLabel = label || def.label;
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'flex-1 min-w-0 relative overflow-hidden rounded-xl border border-border/80',
|
||||
'bg-card/95 px-5 py-4 shadow-sm shadow-black/15 transition-all duration-300',
|
||||
isActive && def.glow,
|
||||
isActive && def.borderActive,
|
||||
)}>
|
||||
<div className={cn(
|
||||
'absolute top-0 left-0 right-0 h-[3px] bg-gradient-to-r',
|
||||
def.bar,
|
||||
!isActive && (type === 'paid' || type === 'overdue') && 'opacity-30',
|
||||
)} />
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Icon className={cn('h-4 w-4', isActive ? def.valueClass : 'text-muted-foreground')} />
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{displayLabel}
|
||||
</p>
|
||||
{type === 'starting' && onEdit && (
|
||||
<MetricCard
|
||||
className="min-w-0 flex-1"
|
||||
tone={isActive ? def.tone : 'neutral'}
|
||||
icon={Icon}
|
||||
label={displayLabel}
|
||||
value={(
|
||||
<span className={cn(isActive ? def.valueClass : 'text-foreground')}>
|
||||
{fmt(value)}
|
||||
</span>
|
||||
)}
|
||||
hint={hint}
|
||||
action={type === 'starting' && onEdit ? (
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="ml-auto h-4 w-4 text-muted-foreground hover:text-foreground transition-colors"
|
||||
|
|
@ -131,16 +119,8 @@ export function SummaryCard({ type, value, onEdit, hint, label }: SummaryCardPro
|
|||
>
|
||||
<Settings2 className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className={cn(
|
||||
'text-[1.75rem] font-bold tracking-tight font-mono leading-none',
|
||||
isActive ? def.valueClass : 'text-foreground',
|
||||
)}>
|
||||
{fmt(value)}
|
||||
</p>
|
||||
{hint && <p className="mt-2 text-[11px] text-muted-foreground">{hint}</p>}
|
||||
</div>
|
||||
) : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -148,17 +128,16 @@ export function TrendCard({ trend }: { trend?: TrendInfo | null }) {
|
|||
if (!trend) return null;
|
||||
|
||||
return (
|
||||
<div className="flex-1 min-w-0 relative overflow-hidden rounded-xl border border-border/80 bg-card/95 px-5 py-4 shadow-sm shadow-black/15 transition-all duration-300">
|
||||
<div className="absolute top-0 left-0 right-0 h-[3px] bg-gradient-to-r from-purple-500 to-indigo-400" />
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<TrendingUp className="h-4 w-4 text-foreground" />
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
3-Month Trend
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-center h-10">
|
||||
<MetricCard
|
||||
className="min-w-0 flex-1"
|
||||
tone="info"
|
||||
icon={TrendingUp}
|
||||
label="3-Month Trend"
|
||||
value={(
|
||||
<span className="flex min-h-10 items-center justify-center">
|
||||
<TrendIndicator trend={trend} />
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ export function TrackerBucket({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border/80 overflow-hidden bg-card/95 shadow-sm shadow-black/15">
|
||||
<div className="surface-premium overflow-hidden">
|
||||
|
||||
{/* Bucket header */}
|
||||
<div className="flex flex-col gap-2 bg-muted/35 px-3 py-3 border-b border-border/80 sm:px-5 md:flex-row md:items-center md:justify-between">
|
||||
|
|
|
|||
Loading…
Reference in New Issue