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

@ -134,7 +134,7 @@ export default function CashFlowCard({ cashflow, onSetStartingAmounts }: { cashf
<div> <div>
<p className="text-sm font-semibold text-foreground">See your safe-to-spend</p> <p className="text-sm font-semibold text-foreground">See your safe-to-spend</p>
<p className="text-[11px] text-muted-foreground"> <p className="text-[11px] text-muted-foreground">
Add what's in your account for the 1st and 15th, and BillTracker projects what's left after bills. Add what's in your account for the 1st and 15th, and Bill Tracker projects what's left after bills.
</p> </p>
</div> </div>
</div> </div>

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">