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(); const location = useLocation();
return ( 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 /> <AppNavigation adminMode />
<main className="mx-auto w-full max-w-[1500px] px-4 py-6 sm:px-6 lg:px-8 lg:py-8"> <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}> <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"> <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" /> <Info className="h-5 w-5" />
</div> </div>
<CardTitle className="text-2xl">{about?.name || 'BillTracker'}</CardTitle> <CardTitle className="text-2xl">{about?.name || 'Bill Tracker'}</CardTitle>
<CardDescription> <CardDescription>
<span className="text-sm">{about?.description || ''}</span> <span className="text-sm">{about?.description || ''}</span>
</CardDescription> </CardDescription>
@ -146,7 +146,7 @@ export default function AboutPage() {
<div> <div>
<p className="text-sm font-semibold">Produced with AI assistance</p> <p className="text-sm font-semibold">Produced with AI assistance</p>
<p className="mt-1 text-sm leading-6 text-muted-foreground"> <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> </p>
</div> </div>
</div> </div>

View File

@ -586,7 +586,7 @@ export default function AnalyticsPage() {
return ( return (
<div className="analytics-page space-y-6"> <div className="analytics-page space-y-6">
<div className="analytics-report-meta hidden"> <div className="analytics-report-meta hidden">
<h1>BillTracker Analytics</h1> <h1>Bill Tracker Analytics</h1>
<p>{formatRange(data?.range)}</p> <p>{formatRange(data?.range)}</p>
<p>{filterSummary}</p> <p>{filterSummary}</p>
<p>Visible charts: {activeCharts}</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"> <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>
</div> </div>
); );

View File

@ -8,6 +8,8 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { APP_VERSION } from '@/lib/version'; import { APP_VERSION } from '@/lib/version';
import { BRAND } from '@/lib/brand';
import { BrandMark } from '@/components/brand/Brand';
import { import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
@ -191,17 +193,16 @@ export default function LoginPage() {
}; };
return ( 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"> <div className="w-full max-w-sm space-y-6">
{/* Logo / Brand */} {/* Logo / Brand */}
<div className="flex justify-center"> <div className="flex flex-col items-center text-center">
<img <BrandMark className="h-auto w-[78%] max-w-[12rem] drop-shadow-[0_10px_28px_rgba(0,0,0,0.18)]" />
src="/img/logo.png" <p className="mt-3 max-w-xs text-sm leading-6 text-muted-foreground">
alt="BillTracker" {BRAND.tagline}
className="h-auto w-[82%] max-w-[12rem] object-contain drop-shadow-[0_1px_2px_rgba(0,0,0,0.45)]" </p>
/>
</div> </div>
{/* TOTP step — shown after password is accepted */} {/* 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 */} {/* Sign-in card — hidden while auth mode resolves or during TOTP step */}
{!totpChallenge && (authMode === null ? ( {!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> <span className="text-sm text-muted-foreground">Loading</span>
</div> </div>
) : ( ) : (
<div className="surface-elevated p-8 space-y-6"> <div className="surface-elevated space-y-6 p-8">
<div> <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"> <p className="text-sm text-muted-foreground mt-1">
{localEnabled ? 'Enter your credentials to continue.' : `Continue with ${providerName}.`} {localEnabled ? 'Enter your credentials to continue.' : `Continue with ${providerName}.`}
</p> </p>

View File

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

View File

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

View File

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