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:
parent
b714715ddb
commit
0eb024de74
|
|
@ -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}>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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) && (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue