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:
null 2026-07-05 17:18:49 -05:00
parent c2b6a7d1b4
commit b714715ddb
5 changed files with 43 additions and 64 deletions

View File

@ -3,12 +3,12 @@ import { AlertCircle } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
const STATUS_META = { 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' }, 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' }, 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/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' }, 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/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' }, 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-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' }, 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' }, skipped: { label: 'Skipped', cls: 'bg-muted text-muted-foreground border border-border' },
}; };

View File

@ -93,13 +93,13 @@ function OverdueRow({ row, year, month, onPayNow, onRefresh }: OverdueRowProps)
</Badge> </Badge>
)} )}
</div> </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)} {daysOverdueLabel(row.due_date)}
</span> </span>
</div> </div>
{/* Amount */} {/* 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)} {fmt(threshold)}
</span> </span>
@ -108,7 +108,7 @@ function OverdueRow({ row, year, month, onPayNow, onRefresh }: OverdueRowProps)
<Button <Button
size="sm" size="sm"
variant="outline" 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} disabled={loading}
onClick={() => onPayNow(row)} onClick={() => onPayNow(row)}
> >
@ -185,17 +185,17 @@ export default function OverdueCommandCenter({ rows, year, month, refresh, onPay
return ( return (
<Collapsible open={isOpen} onOpenChange={setIsOpen}> <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 */} {/* Header */}
<CollapsibleTrigger asChild> <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"> <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"> <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"> <span className="text-sm font-semibold text-foreground">
{`${overdueRows.length} overdue ${overdueRows.length === 1 ? 'bill' : 'bills'}`} {`${overdueRows.length} overdue ${overdueRows.length === 1 ? 'bill' : 'bills'}`}
</span> </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))} {fmt(asDollars(totalOverdue))}
</span> </span>
</div> </div>

View File

@ -1,16 +1,15 @@
import { TrendingUp, AlertCircle, Clock, CheckCircle2, Settings2, type LucideIcon } from 'lucide-react'; import { TrendingUp, AlertCircle, Clock, CheckCircle2, Settings2, type LucideIcon } from 'lucide-react';
import { cn, fmt } from '@/lib/utils'; import { cn, fmt } from '@/lib/utils';
import type { Dollars } from '@/lib/money'; import type { Dollars } from '@/lib/money';
import { MetricCard } from '@/components/ui/app-primitives';
type CardType = 'starting' | 'paid' | 'remaining' | 'overdue'; type CardType = 'starting' | 'paid' | 'remaining' | 'overdue';
interface CardDef { interface CardDef {
label: string; label: string;
icon: LucideIcon; icon: LucideIcon;
bar: string;
glow: string;
borderActive?: string;
valueClass: string; valueClass: string;
tone: 'neutral' | 'good' | 'warn' | 'danger' | 'info';
activateWhen: (v: number) => boolean; activateWhen: (v: number) => boolean;
} }
@ -18,35 +17,29 @@ const CARD_DEFS: Record<CardType, CardDef> = {
starting: { starting: {
label: 'Starting', label: 'Starting',
icon: TrendingUp, icon: TrendingUp,
bar: 'from-slate-400 to-slate-300',
glow: '',
valueClass: 'text-foreground', valueClass: 'text-foreground',
tone: 'neutral',
activateWhen: () => true, activateWhen: () => true,
}, },
paid: { paid: {
label: 'Total Paid', label: 'Total Paid',
icon: CheckCircle2, 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', valueClass: 'text-emerald-600 dark:text-emerald-200',
tone: 'good',
activateWhen: (v) => v > 0, activateWhen: (v) => v > 0,
}, },
remaining: { remaining: {
label: 'Remaining', label: 'Remaining',
icon: Clock, icon: Clock,
bar: 'from-blue-400 to-indigo-300',
glow: '',
valueClass: 'text-foreground', valueClass: 'text-foreground',
tone: 'info',
activateWhen: () => true, activateWhen: () => true,
}, },
overdue: { overdue: {
label: 'Overdue', label: 'Overdue',
icon: AlertCircle, 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', valueClass: 'text-red-500 dark:text-rose-200',
tone: 'danger',
activateWhen: (v) => v > 0, activateWhen: (v) => v > 0,
}, },
}; };
@ -106,23 +99,18 @@ export function SummaryCard({ type, value, onEdit, hint, label }: SummaryCardPro
const displayLabel = label || def.label; const displayLabel = label || def.label;
return ( return (
<div className={cn( <MetricCard
'flex-1 min-w-0 relative overflow-hidden rounded-xl border border-border/80', className="min-w-0 flex-1"
'bg-card/95 px-5 py-4 shadow-sm shadow-black/15 transition-all duration-300', tone={isActive ? def.tone : 'neutral'}
isActive && def.glow, icon={Icon}
isActive && def.borderActive, label={displayLabel}
)}> value={(
<div className={cn( <span className={cn(isActive ? def.valueClass : 'text-foreground')}>
'absolute top-0 left-0 right-0 h-[3px] bg-gradient-to-r', {fmt(value)}
def.bar, </span>
!isActive && (type === 'paid' || type === 'overdue') && 'opacity-30', )}
)} /> hint={hint}
<div className="flex items-center gap-2 mb-3"> action={type === 'starting' && onEdit ? (
<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 && (
<button <button
onClick={onEdit} onClick={onEdit}
className="ml-auto h-4 w-4 text-muted-foreground hover:text-foreground transition-colors" 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" /> <Settings2 className="h-4 w-4" />
</button> </button>
)} ) : undefined}
</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>
); );
} }
@ -148,17 +128,16 @@ export function TrendCard({ trend }: { trend?: TrendInfo | null }) {
if (!trend) return null; if (!trend) return null;
return ( 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"> <MetricCard
<div className="absolute top-0 left-0 right-0 h-[3px] bg-gradient-to-r from-purple-500 to-indigo-400" /> className="min-w-0 flex-1"
<div className="flex items-center gap-2 mb-3"> tone="info"
<TrendingUp className="h-4 w-4 text-foreground" /> icon={TrendingUp}
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground"> label="3-Month Trend"
3-Month Trend value={(
</p> <span className="flex min-h-10 items-center justify-center">
</div>
<div className="flex items-center justify-center h-10">
<TrendIndicator trend={trend} /> <TrendIndicator trend={trend} />
</div> </span>
</div> )}
/>
); );
} }

View File

@ -223,7 +223,7 @@ export function TrackerBucket({
} }
return ( 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 */} {/* 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"> <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">