style: global readability/theme pass
- Sharpened font stack in index.css, removed softer Georgia digit font for UI text/money - Tuned dark-mode tokens: clearer foreground, brighter muted text, stronger borders, defined cards - Updated UI primitives: cards, buttons, inputs, selects, tables, badges - Cleaned up bills rows, mobile bill rows, tracker dismiss, snowball icons, summary/category/health/analytics money values, import/export status icons - Reduced fuzzy uppercase label spacing globally
This commit is contained in:
parent
7a58d69c70
commit
a1f679f7b0
|
|
@ -22,8 +22,8 @@ function AprColor({ rate }) {
|
|||
const cls =
|
||||
rate >= 25 ? 'text-rose-400' :
|
||||
rate >= 15 ? 'text-amber-400' :
|
||||
'text-muted-foreground/60';
|
||||
return <span className={cn('text-[10px] tabular-nums', cls)}>{rate}% APR</span>;
|
||||
'text-muted-foreground';
|
||||
return <span className={cn('tracker-number text-[11px] font-semibold', cls)}>{rate}% APR</span>;
|
||||
}
|
||||
|
||||
const ALL_ON = {
|
||||
|
|
@ -50,24 +50,24 @@ function BillCard({ bill, prefs = ALL_ON, onEdit, onToggle, onDelete, onHistory,
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => onEdit?.(bill.id)}
|
||||
className="text-sm font-semibold text-foreground hover:text-primary transition-colors text-left truncate max-w-[240px]"
|
||||
className="max-w-[240px] truncate text-left text-sm font-semibold text-foreground transition-colors hover:text-primary"
|
||||
>
|
||||
{bill.name}
|
||||
</button>
|
||||
|
||||
{prefs.showCategory && bill.category_name && (
|
||||
<span className="text-[10px] text-muted-foreground border border-border/50 rounded px-1.5 py-0.5 shrink-0">
|
||||
<span className="shrink-0 rounded border border-border/70 bg-secondary/60 px-1.5 py-0.5 text-[11px] font-medium text-muted-foreground">
|
||||
{bill.category_name}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{prefs.showAutopay && !!bill.autopay_enabled && (
|
||||
<span className="text-[10px] font-semibold px-1.5 py-0.5 rounded bg-emerald-500/15 text-emerald-500 shrink-0">
|
||||
<span className="shrink-0 rounded bg-emerald-500/20 px-1.5 py-0.5 text-[11px] font-semibold text-emerald-300">
|
||||
Autopay
|
||||
</span>
|
||||
)}
|
||||
{prefs.show2fa && !!bill.has_2fa && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-violet-500/15 text-violet-400 shrink-0">
|
||||
<span className="shrink-0 rounded bg-violet-500/20 px-1.5 py-0.5 text-[11px] font-semibold text-violet-300">
|
||||
2FA
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -82,7 +82,7 @@ function BillCard({ bill, prefs = ALL_ON, onEdit, onToggle, onDelete, onHistory,
|
|||
</div>
|
||||
|
||||
{/* Meta row */}
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs font-medium text-muted-foreground">
|
||||
{prefs.showCycle && <span className="capitalize">{bill.billing_cycle || 'monthly'}</span>}
|
||||
|
||||
{prefs.showCycle && prefs.showDueDay && <span className="text-border">·</span>}
|
||||
|
|
@ -99,7 +99,7 @@ function BillCard({ bill, prefs = ALL_ON, onEdit, onToggle, onDelete, onHistory,
|
|||
{prefs.showBalance && isDebt && bill.current_balance != null && (
|
||||
<>
|
||||
{(prefs.showCycle || prefs.showDueDay || (prefs.showApr && bill.interest_rate != null)) && <span className="text-border">·</span>}
|
||||
<span className="text-[10px] text-muted-foreground/70 tabular-nums">
|
||||
<span className="tracker-number text-[11px] font-medium text-muted-foreground">
|
||||
${Number(bill.current_balance).toLocaleString(undefined, { maximumFractionDigits: 0 })} balance
|
||||
</span>
|
||||
</>
|
||||
|
|
@ -111,11 +111,11 @@ function BillCard({ bill, prefs = ALL_ON, onEdit, onToggle, onDelete, onHistory,
|
|||
{/* Amount */}
|
||||
{prefs.showAmount && (
|
||||
<div className="text-right shrink-0 hidden sm:block">
|
||||
<p className="font-mono text-sm font-semibold tabular-nums">
|
||||
<p className="tracker-number text-sm font-bold text-foreground">
|
||||
${Number(bill.expected_amount).toFixed(2)}
|
||||
</p>
|
||||
{prefs.showMinPayment && bill.minimum_payment != null && (
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
<p className="tracker-number text-[11px] font-medium text-muted-foreground">
|
||||
${Number(bill.minimum_payment).toFixed(0)} min
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -128,7 +128,7 @@ function BillCard({ bill, prefs = ALL_ON, onEdit, onToggle, onDelete, onHistory,
|
|||
type="button"
|
||||
onClick={() => onEdit?.(bill.id)}
|
||||
title="Edit"
|
||||
className="p-1.5 rounded-md text-muted-foreground/40 hover:text-foreground hover:bg-muted/60 transition-colors"
|
||||
className="rounded-md p-1.5 text-muted-foreground hover:bg-muted/70 hover:text-foreground transition-colors"
|
||||
>
|
||||
<PenLine className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
|
@ -137,7 +137,7 @@ function BillCard({ bill, prefs = ALL_ON, onEdit, onToggle, onDelete, onHistory,
|
|||
type="button"
|
||||
onClick={() => onDuplicate?.(bill)}
|
||||
title="Duplicate"
|
||||
className="p-1.5 rounded-md text-muted-foreground/40 hover:text-primary hover:bg-primary/10 transition-colors"
|
||||
className="rounded-md p-1.5 text-muted-foreground hover:bg-primary/10 hover:text-primary transition-colors"
|
||||
>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
|
@ -147,7 +147,7 @@ function BillCard({ bill, prefs = ALL_ON, onEdit, onToggle, onDelete, onHistory,
|
|||
type="button"
|
||||
onClick={() => onHistory?.(bill)}
|
||||
title="History visibility"
|
||||
className="p-1.5 rounded-md text-muted-foreground/40 hover:text-sky-400 hover:bg-sky-500/10 transition-colors"
|
||||
className="rounded-md p-1.5 text-muted-foreground hover:bg-sky-500/10 hover:text-sky-400 transition-colors"
|
||||
>
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
|
@ -158,10 +158,10 @@ function BillCard({ bill, prefs = ALL_ON, onEdit, onToggle, onDelete, onHistory,
|
|||
onClick={() => onToggle?.(bill)}
|
||||
title={bill.active ? 'Deactivate' : 'Activate'}
|
||||
className={cn(
|
||||
'p-1.5 rounded-md transition-colors',
|
||||
'rounded-md p-1.5 transition-colors',
|
||||
bill.active
|
||||
? 'text-muted-foreground/40 hover:text-amber-400 hover:bg-amber-500/10'
|
||||
: 'text-muted-foreground/40 hover:text-emerald-400 hover:bg-emerald-500/10',
|
||||
? 'text-muted-foreground hover:bg-amber-500/10 hover:text-amber-400'
|
||||
: 'text-muted-foreground hover:bg-emerald-500/10 hover:text-emerald-400',
|
||||
)}
|
||||
>
|
||||
{bill.active ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
||||
|
|
@ -171,7 +171,7 @@ function BillCard({ bill, prefs = ALL_ON, onEdit, onToggle, onDelete, onHistory,
|
|||
type="button"
|
||||
onClick={() => onDelete?.(bill)}
|
||||
title="Delete"
|
||||
className="p-1.5 rounded-md text-muted-foreground/40 hover:text-destructive hover:bg-destructive/10 transition-colors"
|
||||
className="rounded-md p-1.5 text-muted-foreground hover:bg-destructive/10 hover:text-destructive transition-colors"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -15,14 +15,14 @@ export const MobileBillRow = React.memo(function MobileBillRow({ bill, onEdit, o
|
|||
return cn(
|
||||
'rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
|
||||
bill.active
|
||||
? 'bg-emerald-500/15 text-emerald-500'
|
||||
? 'bg-emerald-500/20 text-emerald-300'
|
||||
: 'bg-muted text-muted-foreground',
|
||||
);
|
||||
}, [bill.active]);
|
||||
|
||||
const autopayClass = useMemo(() => {
|
||||
return cn(
|
||||
'rounded bg-emerald-500/20 px-1.5 py-0.5 text-[10px] font-semibold text-emerald-500',
|
||||
'rounded bg-emerald-500/20 px-1.5 py-0.5 text-[10px] font-semibold text-emerald-300',
|
||||
!!bill.autopay_enabled ? 'opacity-100' : 'opacity-0',
|
||||
);
|
||||
}, [bill.autopay_enabled]);
|
||||
|
|
@ -37,7 +37,7 @@ export const MobileBillRow = React.memo(function MobileBillRow({ bill, onEdit, o
|
|||
}, [bill.active]);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-border/60 bg-background/60 p-3 shadow-sm">
|
||||
<div className="rounded-xl border border-border/80 bg-card/90 p-3 shadow-sm shadow-black/10">
|
||||
<div className="flex min-w-0 items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
|
|
@ -65,30 +65,30 @@ export const MobileBillRow = React.memo(function MobileBillRow({ bill, onEdit, o
|
|||
{bill.active ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
{bill.autopay_enabled && (
|
||||
<span className="rounded bg-emerald-500/20 px-1.5 py-0.5 text-[10px] font-semibold text-emerald-500">AP</span>
|
||||
<span className="rounded bg-emerald-500/20 px-1.5 py-0.5 text-[10px] font-semibold text-emerald-300">AP</span>
|
||||
)}
|
||||
{bill.has_2fa && (
|
||||
<span className="rounded bg-violet-500/15 px-1.5 py-0.5 text-[10px] text-violet-400">2FA</span>
|
||||
<span className="rounded bg-violet-500/20 px-1.5 py-0.5 text-[10px] font-semibold text-violet-300">2FA</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span className="shrink-0 font-mono text-sm font-semibold tabular-nums text-foreground">
|
||||
<span className="tracker-number shrink-0 text-sm font-bold text-foreground">
|
||||
${Number(bill.expected_amount).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid grid-cols-3 gap-2 text-xs text-muted-foreground">
|
||||
<div className="mt-3 grid grid-cols-3 gap-2 text-xs font-medium text-muted-foreground">
|
||||
<div>
|
||||
<p className="uppercase tracking-wide text-muted-foreground/60">Due</p>
|
||||
<p className="uppercase tracking-normal text-muted-foreground">Due</p>
|
||||
<p className="mt-0.5 text-sm text-foreground">Day {bill.due_day}</p>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="uppercase tracking-wide text-muted-foreground/60">Category</p>
|
||||
<p className="uppercase tracking-normal text-muted-foreground">Category</p>
|
||||
<p className="mt-0.5 truncate text-sm text-foreground">{bill.category_name || '—'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="uppercase tracking-wide text-muted-foreground/60">Cycle</p>
|
||||
<p className="uppercase tracking-normal text-muted-foreground">Cycle</p>
|
||||
<p className="mt-0.5 text-sm capitalize text-foreground">{bill.billing_cycle || 'monthly'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const CARD_DEFS = {
|
|||
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-100',
|
||||
activateWhen: (v) => v > 0,
|
||||
},
|
||||
remaining: {
|
||||
|
|
@ -36,7 +36,7 @@ const CARD_DEFS = {
|
|||
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-100',
|
||||
activateWhen: (v) => v > 0,
|
||||
},
|
||||
};
|
||||
|
|
@ -60,7 +60,7 @@ export const SummaryCard = React.memo(function SummaryCard({ type, value, onEdit
|
|||
)} />
|
||||
<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">
|
||||
<p className="text-xs font-semibold uppercase tracking-normal text-muted-foreground">
|
||||
{def.label}
|
||||
</p>
|
||||
{type === 'starting' && onEdit && (
|
||||
|
|
@ -75,12 +75,12 @@ export const SummaryCard = React.memo(function SummaryCard({ type, value, onEdit
|
|||
)}
|
||||
</div>
|
||||
<p className={cn(
|
||||
'text-[1.75rem] font-bold tracking-tight font-mono leading-none',
|
||||
'tracker-number text-[1.75rem] font-bold leading-none',
|
||||
isActive ? def.valueClass : 'text-foreground',
|
||||
)}>
|
||||
{fmt(value)}
|
||||
</p>
|
||||
{hint && <p className="mt-2 text-[11px] text-muted-foreground">{hint}</p>}
|
||||
{hint && <p className="mt-2 text-xs font-medium text-muted-foreground">{hint}</p>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export default function DownloadMyDataSection() {
|
|||
<ul className="space-y-1.5">
|
||||
{['Passwords','Sessions','Admin settings','Server configuration',"Other users' data",'Full system backup files'].map(i => (
|
||||
<li key={i} className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<XCircle className="h-3.5 w-3.5 text-muted-foreground/40 shrink-0" />{i}
|
||||
<XCircle className="h-3.5 w-3.5 text-muted-foreground shrink-0" />{i}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -280,8 +280,8 @@ function RowDecisionRow({ row, decision, onDecisionChange, allBills, categories,
|
|||
|
||||
{/* Status icon */}
|
||||
<div className="mt-0.5 shrink-0">
|
||||
{hasError ? <XCircle className="h-4 w-4 text-muted-foreground/40" /> :
|
||||
isSkip ? <SkipForward className="h-4 w-4 text-muted-foreground/40" /> :
|
||||
{hasError ? <XCircle className="h-4 w-4 text-muted-foreground" /> :
|
||||
isSkip ? <SkipForward className="h-4 w-4 text-muted-foreground" /> :
|
||||
complete ? <CheckCircle2 className="h-4 w-4 text-emerald-500" /> :
|
||||
action !== null ? <AlertTriangle className="h-4 w-4 text-amber-500" /> :
|
||||
<AlertTriangle className="h-4 w-4 text-orange-500" />}
|
||||
|
|
@ -740,7 +740,7 @@ function BillDetailView({ group, onBack, onImport, isImporting, importResult })
|
|||
<span className="text-xs text-muted-foreground truncate flex-1">{row.detected_name ?? '—'}</span>
|
||||
<span className={cn('text-[10px] shrink-0',
|
||||
row._match.match_confidence === 'high' ? 'text-emerald-500' :
|
||||
row._match.match_confidence === 'medium' ? 'text-amber-500' : 'text-muted-foreground/40')}>
|
||||
row._match.match_confidence === 'medium' ? 'text-amber-500' : 'text-muted-foreground')}>
|
||||
{row._match.match_confidence}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ const badgeVariants = cva(
|
|||
destructive: 'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
|
||||
outline: 'text-foreground',
|
||||
// Bill status variants
|
||||
paid: 'bg-emerald-500/15 text-emerald-400 border-emerald-500/20',
|
||||
late: 'bg-orange-500/15 text-orange-400 border-orange-500/20',
|
||||
missed: 'bg-red-500/15 text-red-400 border-red-500/20',
|
||||
due_soon: 'bg-yellow-500/15 text-yellow-400 border-yellow-500/20',
|
||||
upcoming: 'bg-slate-500/15 text-slate-400 border-slate-500/20',
|
||||
autodraft: 'bg-amber-500/15 text-amber-400 border-amber-500/20',
|
||||
paid: 'bg-emerald-500/20 text-emerald-300 border-emerald-400/35',
|
||||
late: 'bg-orange-500/20 text-orange-300 border-orange-400/35',
|
||||
missed: 'bg-red-500/20 text-red-300 border-red-400/35',
|
||||
due_soon: 'bg-yellow-500/20 text-yellow-200 border-yellow-400/35',
|
||||
upcoming: 'bg-slate-400/20 text-slate-200 border-slate-300/30',
|
||||
autodraft: 'bg-amber-500/20 text-amber-200 border-amber-400/35',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
|
|
|||
|
|
@ -4,21 +4,21 @@ import { cva } from 'class-variance-authority';
|
|||
import { cn } from '@/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
'inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-semibold outline-none transition-all focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 hover:shadow-md active:scale-[0.99]',
|
||||
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90 hover:shadow-md active:scale-[0.99]',
|
||||
outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground hover:shadow',
|
||||
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/90 hover:shadow',
|
||||
outline: 'border border-input bg-card/80 shadow-sm hover:bg-accent hover:text-accent-foreground hover:shadow',
|
||||
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/90 hover:shadow-md',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground active:bg-accent/80',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
sm: 'h-8 rounded-lg px-3 text-xs',
|
||||
lg: 'h-10 rounded-lg px-8',
|
||||
icon: 'h-9 w-9',
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { cn } from '@/lib/utils';
|
|||
const Card = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('rounded-2xl border border-border/70 bg-card text-card-foreground shadow-sm transition-shadow hover:shadow', className)}
|
||||
className={cn('rounded-2xl border border-border/80 bg-card/95 text-card-foreground shadow-sm shadow-black/10 transition-shadow hover:shadow-md hover:shadow-black/10', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
|
@ -22,7 +22,7 @@ CardHeader.displayName = 'CardHeader';
|
|||
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('font-semibold leading-none tracking-tight', className)}
|
||||
className={cn('font-semibold leading-tight text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
|
@ -31,7 +31,7 @@ CardTitle.displayName = 'CardTitle';
|
|||
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
className={cn('text-sm font-medium leading-relaxed text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const Input = React.forwardRef(({ className, type, ...props }, ref) => {
|
|||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground/70 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20',
|
||||
'flex h-9 w-full rounded-lg border border-input bg-card/70 px-3 py-1 text-sm font-medium text-foreground shadow-sm shadow-black/5 transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground/80 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref)
|
|||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-all placeholder:text-muted-foreground/70 focus:outline-none focus:ring-[3px] focus:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
||||
'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-lg border border-input bg-card/70 px-3 py-2 text-sm font-medium text-foreground shadow-sm shadow-black/5 transition-all placeholder:text-muted-foreground/80 focus:outline-none focus:ring-[3px] focus:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
||||
className
|
||||
)}
|
||||
aria-haspopup="listbox"
|
||||
|
|
@ -20,7 +20,7 @@ const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref)
|
|||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
|
|
@ -53,7 +53,7 @@ const SelectContent = React.forwardRef(({ className, children, position = 'poppe
|
|||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-xl border border-border/70 bg-popover/95 text-popover-foreground shadow-xl backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-xl border border-border/80 bg-popover/95 text-popover-foreground shadow-xl shadow-black/25 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
position === 'popper' &&
|
||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
className
|
||||
|
|
@ -92,7 +92,7 @@ const SelectItem = React.forwardRef(({ className, children, ...props }, ref) =>
|
|||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex w-full cursor-pointer select-none items-center rounded-lg py-1.5 pl-2 pr-8 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',
|
||||
'relative flex w-full cursor-pointer select-none items-center rounded-lg py-1.5 pl-2 pr-8 text-sm font-medium outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
role="option"
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ const TableHead = React.forwardRef(({ className, ...props }, ref) => (
|
|||
ref={ref}
|
||||
className={cn(
|
||||
'h-10 px-4 text-left align-middle',
|
||||
'text-[11px] font-medium uppercase tracking-[0.08em]',
|
||||
'text-xs font-semibold uppercase tracking-normal',
|
||||
'text-muted-foreground',
|
||||
'[&:has([role=checkbox])]:pr-0',
|
||||
'[&>[role=checkbox]]:translate-y-[2px]',
|
||||
|
|
@ -90,7 +90,7 @@ const TableCell = React.forwardRef(({ className, ...props }, ref) => (
|
|||
ref={ref}
|
||||
className={cn(
|
||||
'px-5 py-3 align-middle',
|
||||
'text-sm',
|
||||
'text-sm font-medium text-foreground',
|
||||
'[&:has([role=checkbox])]:pr-0',
|
||||
'[&>[role=checkbox]]:translate-y-[2px]',
|
||||
className
|
||||
|
|
|
|||
|
|
@ -62,45 +62,45 @@
|
|||
--sidebar-accent-foreground: 0.20 0.008 250;
|
||||
--sidebar-border: 0.88 0.008 250;
|
||||
--sidebar-ring: 0.55 0.20 276;
|
||||
--font-sans: 'GeorgiaDigits', Roboto, Inter, ui-sans-serif, system-ui, sans-serif;
|
||||
--font-serif: 'GeorgiaDigits', Merriweather, Georgia, serif;
|
||||
--font-mono: 'GeorgiaDigits', "Roboto Mono", ui-monospace, SFMono-Regular, monospace;
|
||||
--font-sans: Inter, Roboto, ui-sans-serif, system-ui, sans-serif;
|
||||
--font-serif: Merriweather, Georgia, serif;
|
||||
--font-mono: "Roboto Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
--radius: 1rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 0.165 0.014 245;
|
||||
--foreground: 0.95 0.006 245;
|
||||
--card: 0.225 0.014 245;
|
||||
--card-foreground: 0.95 0.006 245;
|
||||
--popover: 0.25 0.016 245;
|
||||
--popover-foreground: 0.95 0.006 245;
|
||||
--primary: 0.69 0.18 150;
|
||||
--background: 0.155 0.014 245;
|
||||
--foreground: 0.965 0.006 245;
|
||||
--card: 0.235 0.016 245;
|
||||
--card-foreground: 0.965 0.006 245;
|
||||
--popover: 0.26 0.018 245;
|
||||
--popover-foreground: 0.965 0.006 245;
|
||||
--primary: 0.72 0.17 150;
|
||||
--primary-foreground: 0.14 0.018 150;
|
||||
--secondary: 0.265 0.016 245;
|
||||
--secondary-foreground: 0.92 0.007 245;
|
||||
--muted: 0.255 0.014 245;
|
||||
--muted-foreground: 0.70 0.014 245;
|
||||
--accent: 0.295 0.035 158;
|
||||
--accent-foreground: 0.95 0.006 245;
|
||||
--secondary: 0.275 0.018 245;
|
||||
--secondary-foreground: 0.94 0.007 245;
|
||||
--muted: 0.275 0.016 245;
|
||||
--muted-foreground: 0.76 0.012 245;
|
||||
--accent: 0.32 0.045 158;
|
||||
--accent-foreground: 0.965 0.006 245;
|
||||
--destructive: 0.66 0.18 26;
|
||||
--destructive-foreground: 0.98 0.004 245;
|
||||
--border: 0.36 0.018 245;
|
||||
--input: 0.36 0.018 245;
|
||||
--ring: 0.69 0.16 150;
|
||||
--border: 0.40 0.018 245;
|
||||
--input: 0.40 0.018 245;
|
||||
--ring: 0.72 0.16 150;
|
||||
--chart-1: 0.55 0.22 270;
|
||||
--chart-2: 0.62 0.14 150;
|
||||
--chart-3: 0.65 0.18 310;
|
||||
--chart-4: 0.62 0.16 130;
|
||||
--chart-5: 0.58 0.18 255;
|
||||
--sidebar: 0.18 0.014 245;
|
||||
--sidebar-foreground: 0.93 0.006 245;
|
||||
--sidebar-primary: 0.69 0.18 150;
|
||||
--sidebar: 0.175 0.014 245;
|
||||
--sidebar-foreground: 0.95 0.006 245;
|
||||
--sidebar-primary: 0.72 0.17 150;
|
||||
--sidebar-primary-foreground: 0.14 0.018 150;
|
||||
--sidebar-accent: 0.275 0.030 158;
|
||||
--sidebar-accent-foreground: 0.93 0.006 245;
|
||||
--sidebar-border: 0.32 0.018 245;
|
||||
--sidebar-ring: 0.69 0.16 150;
|
||||
--sidebar-accent: 0.30 0.038 158;
|
||||
--sidebar-accent-foreground: 0.95 0.006 245;
|
||||
--sidebar-border: 0.36 0.018 245;
|
||||
--sidebar-ring: 0.72 0.16 150;
|
||||
}
|
||||
|
||||
* {
|
||||
|
|
@ -115,6 +115,15 @@
|
|||
@apply bg-background font-sans text-foreground antialiased;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
::selection {
|
||||
color: oklch(var(--primary-foreground));
|
||||
background: oklch(var(--primary) / 0.75);
|
||||
}
|
||||
|
||||
strong {
|
||||
@apply font-semibold text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Utilities ───────────────────────────────────────────── */
|
||||
|
|
@ -123,7 +132,7 @@
|
|||
|
||||
/* Generic surface */
|
||||
.surface {
|
||||
@apply rounded-2xl border border-border/70 bg-card shadow-sm;
|
||||
@apply rounded-2xl border border-border/80 bg-card shadow-sm shadow-black/10;
|
||||
}
|
||||
|
||||
/* Elevated surface */
|
||||
|
|
@ -136,8 +145,8 @@
|
|||
|
||||
.dark .surface-elevated {
|
||||
box-shadow:
|
||||
0 0 0 1px oklch(var(--border) / 0.7),
|
||||
0 8px 22px rgb(0 0 0 / 0.55);
|
||||
0 0 0 1px oklch(var(--border) / 0.78),
|
||||
0 10px 24px rgb(0 0 0 / 0.48);
|
||||
}
|
||||
|
||||
/* Stat cards */
|
||||
|
|
@ -147,7 +156,7 @@
|
|||
|
||||
/* Table */
|
||||
.table-surface {
|
||||
@apply surface overflow-hidden shadow-sm;
|
||||
@apply surface overflow-hidden shadow-sm shadow-black/10;
|
||||
}
|
||||
|
||||
.tracker-number {
|
||||
|
|
@ -158,6 +167,35 @@
|
|||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
@apply tracker-number font-bold tracking-tight text-foreground;
|
||||
}
|
||||
|
||||
.tracking-tight,
|
||||
.tracking-wide,
|
||||
.tracking-wider,
|
||||
.tracking-widest,
|
||||
.tracking-\[0\.08em\],
|
||||
.tracking-\[0\.14em\] {
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.dark .text-muted-foreground\/40 {
|
||||
color: oklch(var(--muted-foreground) / 0.72);
|
||||
}
|
||||
|
||||
.dark .text-muted-foreground\/50 {
|
||||
color: oklch(var(--muted-foreground) / 0.78);
|
||||
}
|
||||
|
||||
.dark .text-muted-foreground\/60 {
|
||||
color: oklch(var(--muted-foreground) / 0.84);
|
||||
}
|
||||
|
||||
.dark .text-muted-foreground\/70 {
|
||||
color: oklch(var(--muted-foreground) / 0.9);
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
.scrollbar-thin {
|
||||
scrollbar-width: thin;
|
||||
|
|
|
|||
|
|
@ -762,9 +762,9 @@ export default function AnalyticsPage() {
|
|||
{forecastRows.map(row => (
|
||||
<tr key={row.month} className="text-muted-foreground hover:text-foreground transition-colors">
|
||||
<td className="py-2 pr-4 font-medium text-foreground">{row.label}</td>
|
||||
<td className="py-2 pr-4 text-right font-mono tabular-nums">{fullMoney(row.total)}</td>
|
||||
<td className="py-2 pr-4 text-right font-mono tabular-nums opacity-70">{fullMoney(row.low)}</td>
|
||||
<td className="py-2 text-right font-mono tabular-nums opacity-70">{fullMoney(row.high)}</td>
|
||||
<td className="tracker-number py-2 pr-4 text-right font-semibold">{fullMoney(row.total)}</td>
|
||||
<td className="tracker-number py-2 pr-4 text-right font-medium text-muted-foreground">{fullMoney(row.low)}</td>
|
||||
<td className="tracker-number py-2 text-right font-medium text-muted-foreground">{fullMoney(row.high)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -181,8 +181,8 @@ function ExpandedBills({ category }) {
|
|||
<p className="mt-1 text-xs text-muted-foreground">Due day {bill.due_day}</p>
|
||||
</td>
|
||||
<td className="px-4 py-3"><StatusPill active={bill.active} /></td>
|
||||
<td className="px-4 py-3 text-right font-mono">{fmt(bill.expected_amount)}</td>
|
||||
<td className="px-4 py-3 text-right font-mono">{fmt(bill.total_paid)}</td>
|
||||
<td className="tracker-number px-4 py-3 text-right font-semibold">{fmt(bill.expected_amount)}</td>
|
||||
<td className="tracker-number px-4 py-3 text-right font-semibold">{fmt(bill.total_paid)}</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<p className="tabular-nums">{plural(bill.payment_count || 0, 'payment')}</p>
|
||||
<p className="mt-1 text-xs tabular-nums text-muted-foreground">{fmtDate(bill.last_paid_date)}</p>
|
||||
|
|
@ -206,11 +206,11 @@ function ExpandedBills({ category }) {
|
|||
<div className="mt-4 grid grid-cols-2 gap-3 text-xs sm:grid-cols-4">
|
||||
<div>
|
||||
<p className="text-muted-foreground">Expected</p>
|
||||
<p className="mt-0.5 font-mono font-semibold">{fmt(bill.expected_amount)}</p>
|
||||
<p className="tracker-number mt-0.5 font-semibold">{fmt(bill.expected_amount)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Paid</p>
|
||||
<p className="mt-0.5 font-mono font-semibold">{fmt(bill.total_paid)}</p>
|
||||
<p className="tracker-number mt-0.5 font-semibold">{fmt(bill.total_paid)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Payments</p>
|
||||
|
|
@ -389,8 +389,8 @@ export default function CategoriesPage() {
|
|||
['Payments', paymentCount],
|
||||
].map(([label, value]) => (
|
||||
<div key={label} className="rounded-xl border border-border/70 bg-card/80 px-4 py-3 shadow-sm">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">{label}</p>
|
||||
<p className="mt-1 font-mono text-xl font-bold text-foreground">{value}</p>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-normal text-muted-foreground">{label}</p>
|
||||
<p className="tracker-number mt-1 text-xl font-bold text-foreground">{value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -50,8 +50,8 @@ function StatCard({ label, value, tone = 'default' }) {
|
|||
tone === 'ok' && 'border-emerald-500/20 bg-emerald-500/10',
|
||||
tone === 'default' && 'border-border/70 bg-card/80',
|
||||
)}>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">{label}</p>
|
||||
<p className="mt-1 font-mono text-xl font-bold text-foreground">{value}</p>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-normal text-muted-foreground">{label}</p>
|
||||
<p className="tracker-number mt-1 text-xl font-bold text-foreground">{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -980,7 +980,7 @@ export default function SnowballPage() {
|
|||
type="button"
|
||||
onClick={() => setEditBill({ bill })}
|
||||
title="Edit bill"
|
||||
className="p-1.5 rounded-md text-muted-foreground/40 hover:text-foreground hover:bg-muted/60 transition-colors"
|
||||
className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted/70 hover:text-foreground"
|
||||
>
|
||||
<PenLine className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
|
@ -988,7 +988,7 @@ export default function SnowballPage() {
|
|||
type="button"
|
||||
onClick={() => removeFromSnowball(bill)}
|
||||
title="Hide from Snowball"
|
||||
className="p-1.5 rounded-md text-muted-foreground/40 hover:text-amber-400 hover:bg-amber-500/10 transition-colors"
|
||||
className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-amber-500/10 hover:text-amber-400"
|
||||
>
|
||||
<EyeOff className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
|
@ -999,7 +999,7 @@ export default function SnowballPage() {
|
|||
);
|
||||
})}
|
||||
|
||||
<p className="text-xs text-muted-foreground/50 text-center pt-1">
|
||||
<p className="pt-1 text-center text-xs font-medium text-muted-foreground">
|
||||
{ramseyMode
|
||||
? 'Ramsey Mode keeps debts sorted by smallest balance · Click a balance to update it'
|
||||
: 'Drag the grip handle to reorder · Click a balance to update it · Save Order to persist'}
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@ export default function SummaryPage() {
|
|||
<CardContent className="space-y-5">
|
||||
<section className="space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h2 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Starting Balance</h2>
|
||||
<h2 className="text-xs font-semibold uppercase tracking-normal text-muted-foreground">Starting Balance</h2>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
|
|
@ -294,29 +294,29 @@ export default function SummaryPage() {
|
|||
<div className="grid gap-3 rounded-2xl bg-muted/45 p-4 sm:grid-cols-3">
|
||||
<div>
|
||||
<div className="text-xs font-medium text-muted-foreground">1st</div>
|
||||
<div className="mt-1 font-mono text-base font-bold text-foreground">{fmt(starting.first_amount)}</div>
|
||||
<div className="tracker-number mt-1 text-base font-bold text-foreground">{fmt(starting.first_amount)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-medium text-muted-foreground">15th</div>
|
||||
<div className="mt-1 font-mono text-base font-bold text-foreground">{fmt(starting.fifteenth_amount)}</div>
|
||||
<div className="tracker-number mt-1 text-base font-bold text-foreground">{fmt(starting.fifteenth_amount)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-medium text-muted-foreground">Other</div>
|
||||
<div className="mt-1 font-mono text-base font-bold text-foreground">{fmt(starting.other_amount)}</div>
|
||||
<div className="tracker-number mt-1 text-base font-bold text-foreground">{fmt(starting.other_amount)}</div>
|
||||
</div>
|
||||
<div className="border-t border-border/60 pt-3 sm:col-span-3">
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<div>
|
||||
<div className="text-xs font-medium text-muted-foreground">Total starting</div>
|
||||
<div className="mt-1 font-mono text-lg font-bold text-foreground">{fmt(starting.combined_amount)}</div>
|
||||
<div className="tracker-number mt-1 text-lg font-bold text-foreground">{fmt(starting.combined_amount)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-medium text-muted-foreground">Paid</div>
|
||||
<div className="mt-1 font-mono text-lg font-bold text-emerald-600 dark:text-emerald-400">{fmt(starting.paid_total)}</div>
|
||||
<div className="tracker-number mt-1 text-lg font-bold text-emerald-600 dark:text-emerald-300">{fmt(starting.paid_total)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-medium text-muted-foreground">Total remaining</div>
|
||||
<div className={cn('mt-1 font-mono text-lg font-bold', moneyClass(starting.combined_remaining || 0))}>
|
||||
<div className={cn('tracker-number mt-1 text-lg font-bold', moneyClass(starting.combined_remaining || 0))}>
|
||||
{fmt(starting.combined_remaining)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1193,7 +1193,7 @@ function Row({ row, year, month, refresh, index, onEditBill }) {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setShowUpdateNudge(false)}
|
||||
className="text-muted-foreground/40 transition-colors hover:text-muted-foreground"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground"
|
||||
title="Dismiss"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "bill-tracker",
|
||||
"version": "0.30.0",
|
||||
"version": "0.30.1",
|
||||
"description": "Monthly bill tracking system",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
Loading…
Reference in New Issue