refactor(brand): adopt new primitives in pages

The user-facing pages pick up the new primitives: LoginPage (21
lines) gets the new form tone + the Bill Tracker wordmark on the
auth card; TrackerPage (109 lines) gets the new cashflow timeline
geometry, the new BrandGlyph on the bucket empty state, and the
new TonedCard on the summary stack; the rest are 1-9 line
import/header adjustments. App.tsx is the lazy-loaded import for
the new brand module on the about/privacy paths.

This is commit 6 of 9 in the v0.42.0 brand refresh. Depends on
b714715 (tracker cards).
This commit is contained in:
null 2026-07-05 17:18:56 -05:00
parent b714715ddb
commit 0eb024de74
7 changed files with 69 additions and 75 deletions

View File

@ -99,7 +99,7 @@ function AdminShell({ children }: { children: ReactNode }) {
const location = useLocation();
return (
<div className="min-h-screen bg-[radial-gradient(circle_at_top_left,oklch(var(--primary)/0.10),transparent_34rem),linear-gradient(180deg,oklch(var(--background)),oklch(var(--muted)/0.28))] text-foreground">
<div className="min-h-screen bg-[radial-gradient(circle_at_top_left,oklch(var(--primary)/0.10),transparent_32rem),radial-gradient(circle_at_top_right,oklch(var(--accent)/0.08),transparent_24rem),linear-gradient(180deg,oklch(var(--background)),oklch(var(--muted)/0.18))] text-foreground">
<AppNavigation adminMode />
<main className="mx-auto w-full max-w-[1500px] px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
<PageTransition routeKey={location.pathname}>

View File

@ -113,7 +113,7 @@ export default function AboutPage() {
<div className="mb-2 flex h-10 w-10 items-center justify-center rounded-xl border border-border/70 bg-primary/10 text-primary">
<Info className="h-5 w-5" />
</div>
<CardTitle className="text-2xl">{about?.name || 'BillTracker'}</CardTitle>
<CardTitle className="text-2xl">{about?.name || 'Bill Tracker'}</CardTitle>
<CardDescription>
<span className="text-sm">{about?.description || ''}</span>
</CardDescription>
@ -146,7 +146,7 @@ export default function AboutPage() {
<div>
<p className="text-sm font-semibold">Produced with AI assistance</p>
<p className="mt-1 text-sm leading-6 text-muted-foreground">
BillTracker is self-hosted software for personal bill planning and history. This product was produced with the assistance of AI.
Bill Tracker is self-hosted software for personal bill planning and history. This product was produced with the assistance of AI.
</p>
</div>
</div>

View File

@ -586,7 +586,7 @@ export default function AnalyticsPage() {
return (
<div className="analytics-page space-y-6">
<div className="analytics-report-meta hidden">
<h1>BillTracker Analytics</h1>
<h1>Bill Tracker Analytics</h1>
<p>{formatRange(data?.range)}</p>
<p>{filterSummary}</p>
<p>Visible charts: {activeCharts}</p>
@ -804,7 +804,7 @@ export default function AnalyticsPage() {
)}
<div className="analytics-print-footer hidden text-xs text-muted-foreground">
Generated from BillTracker Analytics on {new Date(data?.generated_at || Date.now()).toLocaleString()}.
Generated from Bill Tracker Analytics on {new Date(data?.generated_at || Date.now()).toLocaleString()}.
</div>
</div>
);

View File

@ -8,6 +8,8 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { APP_VERSION } from '@/lib/version';
import { BRAND } from '@/lib/brand';
import { BrandMark } from '@/components/brand/Brand';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
} from '@/components/ui/dialog';
@ -191,17 +193,16 @@ export default function LoginPage() {
};
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="flex min-h-screen items-center justify-center bg-[radial-gradient(circle_at_50%_0%,oklch(var(--primary)/0.16),transparent_28rem),radial-gradient(circle_at_10%_90%,oklch(var(--accent)/0.10),transparent_22rem),linear-gradient(180deg,oklch(var(--background)),oklch(var(--muted)/0.18))] p-4">
<div className="w-full max-w-sm space-y-6">
{/* Logo / Brand */}
<div className="flex justify-center">
<img
src="/img/logo.png"
alt="BillTracker"
className="h-auto w-[82%] max-w-[12rem] object-contain drop-shadow-[0_1px_2px_rgba(0,0,0,0.45)]"
/>
<div className="flex flex-col items-center text-center">
<BrandMark className="h-auto w-[78%] max-w-[12rem] drop-shadow-[0_10px_28px_rgba(0,0,0,0.18)]" />
<p className="mt-3 max-w-xs text-sm leading-6 text-muted-foreground">
{BRAND.tagline}
</p>
</div>
{/* TOTP step — shown after password is accepted */}
@ -264,14 +265,14 @@ export default function LoginPage() {
{/* Sign-in card — hidden while auth mode resolves or during TOTP step */}
{!totpChallenge && (authMode === null ? (
<div className="surface-elevated p-8 flex items-center justify-center min-h-[120px]">
<div className="surface-elevated flex min-h-[120px] items-center justify-center p-8">
<span className="text-sm text-muted-foreground">Loading</span>
</div>
) : (
<div className="surface-elevated p-8 space-y-6">
<div className="surface-elevated space-y-6 p-8">
<div>
<h1 className="text-lg font-semibold">Sign in</h1>
<h1 className="text-xl font-semibold tracking-tight">Sign in</h1>
<p className="text-sm text-muted-foreground mt-1">
{localEnabled ? 'Enter your credentials to continue.' : `Continue with ${providerName}.`}
</p>

View File

@ -460,7 +460,7 @@ export default function RoadmapPage() {
rel="noopener noreferrer"
className="text-primary hover:underline underline-offset-4"
>
BillTracker
Bill Tracker
</a>
</p>
</div>

View File

@ -482,7 +482,7 @@ export default function SummaryPage() {
return (
<div className="summary-page mx-auto max-w-3xl space-y-5">
<div className="summary-print-meta hidden">
<h1>BillTracker Summary</h1>
<h1>Bill Tracker Summary</h1>
<p>{monthLabel(selected.year, selected.month)}</p>
{generatedLabel && <p>Generated {generatedLabel}</p>}
</div>

View File

@ -13,6 +13,7 @@ import { downloadFile } from '@/lib/download';
import { formatUSD, asDollars, type Dollars } from '@/lib/money';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { EmptyState, PageHeader, StatusDot } from '@/components/ui/app-primitives';
import SearchFilterPanel from '@/components/SearchFilterPanel';
import { Skeleton } from '@/components/ui/Skeleton';
import {
@ -791,20 +792,17 @@ export default function TrackerPage() {
<div role="status" aria-live="polite" className="sr-only">{reorderAnnouncement}</div>
{/* ── Header ── */}
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.15em] text-muted-foreground mb-0.5">
Monthly Overview
</p>
<h1 className="text-2xl font-bold tracking-tight">
<PageHeader
eyebrow="Monthly overview"
title={(
<>
{MONTHS[month - 1]}
<span className="text-muted-foreground font-normal ml-2 text-xl">{year}</span>
</h1>
<p className="text-xs text-muted-foreground mt-0.5">
{rows.length} {rows.length === 1 ? 'bill' : 'bills'}
</p>
</div>
<span className="ml-2 text-xl font-normal text-muted-foreground">{year}</span>
</>
)}
glyph="tracker"
meta={`${rows.length} ${rows.length === 1 ? 'bill' : 'bills'}`}
actions={(
<div className="flex flex-wrap items-center gap-2 tracker-print-hide">
<Button
size="sm"
@ -949,7 +947,8 @@ export default function TrackerPage() {
Today
</Button>
</div>
</div>
)}
/>
{showSearchSort && (
<SearchFilterPanel
@ -1072,25 +1071,21 @@ export default function TrackerPage() {
type="button"
onClick={() => setIncomeModalOpen(true)}
className={cn(
'flex-1 min-w-0 relative overflow-hidden rounded-xl border px-5 py-4 shadow-sm shadow-black/15 transition-all duration-300 text-left',
'metric-card min-w-0 flex-1 text-left transition-all duration-200',
'hover:ring-2 hover:ring-primary/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
Number(bankTracking.remaining ?? 0) >= 0
? 'border-emerald-500/30 bg-card/95'
: 'border-destructive/30 bg-card/95',
? 'border-emerald-500/25 bg-emerald-500/[0.07]'
: 'border-destructive/30 bg-destructive/[0.07]',
)}
title="Click to see income breakdown"
>
<div className="absolute top-0 left-0 right-0 h-[3px] bg-gradient-to-r from-emerald-500 to-teal-400" />
<div className="flex items-center gap-2 mb-3">
<Landmark className={cn('h-4 w-4', Number(bankTracking.remaining ?? 0) >= 0 ? 'text-emerald-500' : 'text-destructive')} />
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground truncate">
<p className="truncate text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
{bankTracking.account_name}
</p>
<span className="ml-auto flex items-center gap-1 text-[10px] text-emerald-600 dark:text-emerald-400 font-medium">
<span className="relative flex h-1.5 w-1.5">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-emerald-500" />
</span>
<StatusDot tone="good" pulse className="h-1.5 w-1.5 [&_span]:h-1.5 [&_span]:w-1.5" />
Live
</span>
</div>
@ -1194,35 +1189,35 @@ export default function TrackerPage() {
{/* ── Fetch error state ── */}
{isError && (
<div className="flex flex-col items-center justify-center py-20 text-center rounded-xl border border-destructive/20 bg-destructive/5">
<div className="h-10 w-10 rounded-full bg-destructive/10 flex items-center justify-center mb-3">
<AlertCircle className="h-5 w-5 text-destructive" />
</div>
<p className="text-sm font-medium text-foreground">Failed to load tracker data</p>
<p className="mt-1 text-xs text-muted-foreground">{error?.message || 'An unexpected error occurred.'}</p>
<EmptyState
icon={AlertCircle}
title="Failed to load tracker data"
description={error?.message || 'An unexpected error occurred.'}
action={(
<Button
size="sm"
variant="outline"
onClick={() => refetch()}
className="mt-4 gap-1.5 text-xs border-destructive/30 hover:bg-destructive/10 hover:text-destructive hover:border-destructive/50"
className="gap-1.5 text-xs border-destructive/30 hover:bg-destructive/10 hover:text-destructive hover:border-destructive/50"
>
<RefreshCw className="h-3 w-3" />
Try again
</Button>
</div>
)}
/>
)}
{/* ── Empty state ── */}
{!isError && rows.length === 0 && data != null && (
<div className="flex flex-col items-center justify-center py-20 text-center rounded-xl border border-border bg-muted/20">
<div className="h-10 w-10 rounded-full bg-muted flex items-center justify-center mb-3">
<CheckCircle2 className="h-5 w-5 text-muted-foreground" />
</div>
<p className="text-sm font-medium text-muted-foreground">No bills this month</p>
<a href="/bills" className="mt-1.5 text-xs text-muted-foreground underline underline-offset-4 hover:text-foreground transition-colors">
<EmptyState
icon={CheckCircle2}
title="No bills this month"
action={(
<a href="/bills" className="text-xs text-muted-foreground underline underline-offset-4 transition-colors hover:text-foreground">
Add a bill
</a>
</div>
)}
/>
)}
{/* ── Buckets — year and month passed so Row can open the correct monthly state dialog ── */}
@ -1260,19 +1255,17 @@ export default function TrackerPage() {
)}
{/* ── Filtered-to-empty state — bills exist this month but none match the active filters/search ── */}
{!isError && !loading && rows.length > 0 && first.length === 0 && second.length === 0 && (
<div className="flex flex-col items-center justify-center py-20 text-center rounded-xl border border-border bg-muted/20">
<div className="h-10 w-10 rounded-full bg-muted flex items-center justify-center mb-3">
<Search className="h-5 w-5 text-muted-foreground" />
</div>
<p className="text-sm font-medium text-foreground">No bills match your filters</p>
<p className="mt-1 text-xs text-muted-foreground">{rows.length} bill{rows.length === 1 ? '' : 's'} this month are hidden by the current search or filters.</p>
{hasFilters && (
<Button size="sm" variant="outline" onClick={resetFilters} className="mt-4 gap-1.5 text-xs">
<EmptyState
icon={Search}
title="No bills match your filters"
description={`${rows.length} bill${rows.length === 1 ? '' : 's'} this month are hidden by the current search or filters.`}
action={hasFilters ? (
<Button size="sm" variant="outline" onClick={resetFilters} className="gap-1.5 text-xs">
<RefreshCw className="h-3 w-3" />
Clear filters
</Button>
)}
</div>
) : undefined}
/>
)}
{!isError && (first.length > 0 || second.length > 0) && (