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