diff --git a/client/components/BillsTableInner.jsx b/client/components/BillsTableInner.jsx index 260d292..e907dc0 100644 --- a/client/components/BillsTableInner.jsx +++ b/client/components/BillsTableInner.jsx @@ -22,8 +22,8 @@ function AprColor({ rate }) { const cls = rate >= 25 ? 'text-rose-400' : rate >= 15 ? 'text-amber-400' : - 'text-muted-foreground/60'; - return {rate}% APR; + 'text-muted-foreground'; + return {rate}% APR; } const ALL_ON = { @@ -50,24 +50,24 @@ function BillCard({ bill, prefs = ALL_ON, onEdit, onToggle, onDelete, onHistory, {prefs.showCategory && bill.category_name && ( - + {bill.category_name} )} {prefs.showAutopay && !!bill.autopay_enabled && ( - + Autopay )} {prefs.show2fa && !!bill.has_2fa && ( - + 2FA )} @@ -82,7 +82,7 @@ function BillCard({ bill, prefs = ALL_ON, onEdit, onToggle, onDelete, onHistory, {/* Meta row */} -
+
{prefs.showCycle && {bill.billing_cycle || 'monthly'}} {prefs.showCycle && prefs.showDueDay && ·} @@ -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)) && ·} - + ${Number(bill.current_balance).toLocaleString(undefined, { maximumFractionDigits: 0 })} balance @@ -111,11 +111,11 @@ function BillCard({ bill, prefs = ALL_ON, onEdit, onToggle, onDelete, onHistory, {/* Amount */} {prefs.showAmount && (
-

+

${Number(bill.expected_amount).toFixed(2)}

{prefs.showMinPayment && bill.minimum_payment != null && ( -

+

${Number(bill.minimum_payment).toFixed(0)} min

)} @@ -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" > @@ -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" > @@ -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" > @@ -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 ? : } @@ -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" > diff --git a/client/components/MobileBillRow.jsx b/client/components/MobileBillRow.jsx index 67eeaed..dad32c2 100644 --- a/client/components/MobileBillRow.jsx +++ b/client/components/MobileBillRow.jsx @@ -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 ( -
+
@@ -65,30 +65,30 @@ export const MobileBillRow = React.memo(function MobileBillRow({ bill, onEdit, o {bill.active ? 'Active' : 'Inactive'} {bill.autopay_enabled && ( - AP + AP )} {bill.has_2fa && ( - 2FA + 2FA )}
- + ${Number(bill.expected_amount).toFixed(2)}
-
+
-

Due

+

Due

Day {bill.due_day}

-

Category

+

Category

{bill.category_name || '—'}

-

Cycle

+

Cycle

{bill.billing_cycle || 'monthly'}

diff --git a/client/components/SummaryCard.jsx b/client/components/SummaryCard.jsx index f5af0a7..708b191 100644 --- a/client/components/SummaryCard.jsx +++ b/client/components/SummaryCard.jsx @@ -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 )} />
-

+

{def.label}

{type === 'starting' && onEdit && ( @@ -75,12 +75,12 @@ export const SummaryCard = React.memo(function SummaryCard({ type, value, onEdit )}

{fmt(value)}

- {hint &&

{hint}

} + {hint &&

{hint}

}
); }); diff --git a/client/components/data/DownloadMyDataSection.jsx b/client/components/data/DownloadMyDataSection.jsx index 7a44904..be71921 100644 --- a/client/components/data/DownloadMyDataSection.jsx +++ b/client/components/data/DownloadMyDataSection.jsx @@ -96,7 +96,7 @@ export default function DownloadMyDataSection() {
    {['Passwords','Sessions','Admin settings','Server configuration',"Other users' data",'Full system backup files'].map(i => (
  • - {i} + {i}
  • ))}
diff --git a/client/components/data/ImportSpreadsheetSection.jsx b/client/components/data/ImportSpreadsheetSection.jsx index 3e4038c..f837f30 100644 --- a/client/components/data/ImportSpreadsheetSection.jsx +++ b/client/components/data/ImportSpreadsheetSection.jsx @@ -280,8 +280,8 @@ function RowDecisionRow({ row, decision, onDecisionChange, allBills, categories, {/* Status icon */}
- {hasError ? : - isSkip ? : + {hasError ? : + isSkip ? : complete ? : action !== null ? : } @@ -740,7 +740,7 @@ function BillDetailView({ group, onBack, onImport, isImporting, importResult }) {row.detected_name ?? '—'} + row._match.match_confidence === 'medium' ? 'text-amber-500' : 'text-muted-foreground')}> {row._match.match_confidence}
diff --git a/client/components/ui/badge.jsx b/client/components/ui/badge.jsx index b0d770e..5d0af65 100644 --- a/client/components/ui/badge.jsx +++ b/client/components/ui/badge.jsx @@ -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: { diff --git a/client/components/ui/button.jsx b/client/components/ui/button.jsx index 5870106..dc8f55b 100644 --- a/client/components/ui/button.jsx +++ b/client/components/ui/button.jsx @@ -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', }, }, diff --git a/client/components/ui/card.jsx b/client/components/ui/card.jsx index 5e63640..c4c6d4c 100644 --- a/client/components/ui/card.jsx +++ b/client/components/ui/card.jsx @@ -4,7 +4,7 @@ import { cn } from '@/lib/utils'; const Card = React.forwardRef(({ className, ...props }, ref) => (
)); @@ -22,7 +22,7 @@ CardHeader.displayName = 'CardHeader'; const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
)); @@ -31,7 +31,7 @@ CardTitle.displayName = 'CardTitle'; const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
)); diff --git a/client/components/ui/input.jsx b/client/components/ui/input.jsx index caba7aa..48f75c6 100644 --- a/client/components/ui/input.jsx +++ b/client/components/ui/input.jsx @@ -6,7 +6,7 @@ const Input = React.forwardRef(({ className, type, ...props }, ref) => { 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} - + )); @@ -53,7 +53,7 @@ const SelectContent = React.forwardRef(({ className, children, position = 'poppe ( 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 diff --git a/client/index.css b/client/index.css index 809323a..0de3e56 100644 --- a/client/index.css +++ b/client/index.css @@ -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; diff --git a/client/pages/AnalyticsPage.jsx b/client/pages/AnalyticsPage.jsx index 01c1846..1f77fb8 100644 --- a/client/pages/AnalyticsPage.jsx +++ b/client/pages/AnalyticsPage.jsx @@ -762,9 +762,9 @@ export default function AnalyticsPage() { {forecastRows.map(row => ( {row.label} - {fullMoney(row.total)} - {fullMoney(row.low)} - {fullMoney(row.high)} + {fullMoney(row.total)} + {fullMoney(row.low)} + {fullMoney(row.high)} ))} diff --git a/client/pages/CategoriesPage.jsx b/client/pages/CategoriesPage.jsx index c738bb9..bfdd246 100644 --- a/client/pages/CategoriesPage.jsx +++ b/client/pages/CategoriesPage.jsx @@ -181,8 +181,8 @@ function ExpandedBills({ category }) {

Due day {bill.due_day}

- {fmt(bill.expected_amount)} - {fmt(bill.total_paid)} + {fmt(bill.expected_amount)} + {fmt(bill.total_paid)}

{plural(bill.payment_count || 0, 'payment')}

{fmtDate(bill.last_paid_date)}

@@ -206,11 +206,11 @@ function ExpandedBills({ category }) {

Expected

-

{fmt(bill.expected_amount)}

+

{fmt(bill.expected_amount)}

Paid

-

{fmt(bill.total_paid)}

+

{fmt(bill.total_paid)}

Payments

@@ -389,8 +389,8 @@ export default function CategoriesPage() { ['Payments', paymentCount], ].map(([label, value]) => (
-

{label}

-

{value}

+

{label}

+

{value}

))}
diff --git a/client/pages/HealthPage.jsx b/client/pages/HealthPage.jsx index 3e50434..c10d78b 100644 --- a/client/pages/HealthPage.jsx +++ b/client/pages/HealthPage.jsx @@ -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', )}> -

{label}

-

{value}

+

{label}

+

{value}

); } diff --git a/client/pages/SnowballPage.jsx b/client/pages/SnowballPage.jsx index a72cb91..fa08746 100644 --- a/client/pages/SnowballPage.jsx +++ b/client/pages/SnowballPage.jsx @@ -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" > @@ -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" > @@ -999,7 +999,7 @@ export default function SnowballPage() { ); })} -

+

{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'} diff --git a/client/pages/SummaryPage.jsx b/client/pages/SummaryPage.jsx index 5e4f47f..3006ef1 100644 --- a/client/pages/SummaryPage.jsx +++ b/client/pages/SummaryPage.jsx @@ -278,7 +278,7 @@ export default function SummaryPage() {

-

Starting Balance

+

Starting Balance