Compare commits

..

No commits in common. "b25be070ed177b46e55238404cbd46fc3cd8cfbf" and "23f79c31e089d7e99c77fc9bc68e08888ec34be4" have entirely different histories.

25 changed files with 70 additions and 312 deletions

View File

@ -1,7 +1,7 @@
# Bill Tracker
<p align="center">
<img src="client/public/brand/social-preview.png" alt="Bill Tracker — Calm command for household money">
<img src="client/public/brand/social-preview.svg" alt="Bill Tracker — Calm command for household money">
</p>
Bill Tracker is a private, self-hosted bill planning app for households and

View File

@ -6,19 +6,12 @@ export const BRAND = {
assets: {
logo: '/brand/lockup.svg',
logoLight: '/brand/lockup-light.svg',
logoPng: '/brand/logo.png',
mark: '/brand/mark.svg',
markPng: '/brand/mark.png',
monoMark: '/brand/mark-mono.svg',
favicon: '/brand/favicon.svg',
faviconIco: '/brand/favicon.ico',
favicon: '/brand/mark.svg',
appleTouchIcon: '/brand/apple-touch-icon.png',
pwa192: '/brand/pwa-192.png',
pwa512: '/brand/pwa-512.png',
pwaMaskable512: '/brand/pwa-maskable-512.png',
socialPreview: '/brand/social-preview.svg',
socialPreviewPng: '/brand/social-preview.png',
emptyState: '/brand/empty-state.svg',
legacyLogo: '/img/logo.png',
},
colors: {
@ -41,15 +34,15 @@ export type BrandGlyphName =
| 'snowball'
| 'security';
export const BRAND_GLYPHS: Record<BrandGlyphName, { label: string; asset: string }> = {
tracker: { label: 'Tracker', asset: '/brand/glyphs/tracker.svg' },
bills: { label: 'Bills', asset: '/brand/glyphs/bills.svg' },
calendar: { label: 'Calendar', asset: '/brand/glyphs/calendar.svg' },
analytics: { label: 'Analytics', asset: '/brand/glyphs/analytics.svg' },
data: { label: 'Data', asset: '/brand/glyphs/data-banking.svg' },
banking: { label: 'Banking', asset: '/brand/glyphs/data-banking.svg' },
snowball: { label: 'Snowball', asset: '/brand/glyphs/snowball-payoff.svg' },
security: { label: 'Security', asset: '/brand/glyphs/admin-security.svg' },
export const BRAND_GLYPHS: Record<BrandGlyphName, { label: string }> = {
tracker: { label: 'Tracker' },
bills: { label: 'Bills' },
calendar: { label: 'Calendar' },
analytics: { label: 'Analytics' },
data: { label: 'Data' },
banking: { label: 'Banking' },
snowball: { label: 'Snowball' },
security: { label: 'Security' },
};
export const ADMIN_GLYPH = { label: 'Admin' } as const;

View File

@ -58,7 +58,7 @@ export const RELEASE_NOTES: ReleaseNotes = {
},
],
image: {
src: '/brand/social-preview.png',
src: '/brand/social-preview.svg',
alt: 'Bill Tracker brand preview',
},
};

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useMemo, useCallback, useDeferredValue, type ComponentType, type ReactNode } from 'react';
import { useState, useEffect, useMemo, useCallback, useDeferredValue, type ReactNode } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { ChevronLeft, ChevronRight, AlertCircle, CheckCircle2, Plus, Search, RefreshCw, Landmark, ArrowUpToLine, ArrowUp, ArrowDown, BellOff, EyeOff, Settings2, Download, Printer, FileSpreadsheet, CalendarDays, Keyboard } from 'lucide-react';
import { toast } from 'sonner';
@ -91,34 +91,6 @@ interface TrackerFilters {
type BoolFilterKey = 'autopay' | 'firstBucket' | 'fifteenthBucket' | 'unpaid' | 'overdue' | 'debt';
type Patch = Record<string, string | number | boolean | null | undefined>;
function HeaderStat({
icon: Icon,
label,
value,
tone = 'neutral',
}: {
icon: ComponentType<{ className?: string }>;
label: string;
value: ReactNode;
tone?: 'neutral' | 'good' | 'warn' | 'danger' | 'info';
}) {
const toneClass = {
neutral: 'border-border/70 bg-card/65 text-muted-foreground',
good: 'border-emerald-500/25 bg-emerald-500/[0.08] text-emerald-700 dark:text-emerald-300',
warn: 'border-amber-500/30 bg-amber-500/[0.08] text-amber-700 dark:text-amber-300',
danger: 'border-rose-500/30 bg-rose-500/[0.08] text-rose-700 dark:text-rose-300',
info: 'border-sky-500/25 bg-sky-500/[0.08] text-sky-700 dark:text-sky-300',
}[tone];
return (
<span className={cn('inline-flex items-center gap-2 rounded-full border px-3 py-1.5', toneClass)}>
<Icon className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
<span className="text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">{label}</span>
<span className="tracker-number text-xs font-semibold text-foreground">{value}</span>
</span>
);
}
function fmtBalanceAge(isoStr: string | null | undefined): string | null {
const d = parseUtc(isoStr);
if (!d) return null;
@ -799,22 +771,6 @@ export default function TrackerPage() {
const first = sortedRows.filter(r => r.bucket === '1st');
const second = sortedRows.filter(r => r.bucket === '15th');
const reorderEnabled = !hasFilters && !loading && !isError && !pinUpcoming;
const activeMonthLabel = `${MONTHS[month - 1]} ${year}`;
const monthStartLabel = new Date(year, month - 1, 1).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
const monthEndLabel = new Date(year, month, 0).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
const paidCount = Number(summary.count_paid ?? 0);
const overdueCount = Number(summary.count_late ?? 0);
const unpaidCount = rows.filter(row => !row.is_skipped && !rowIsPaid(row)).length;
const headerStatusTone = isError ? 'danger' : loading ? 'info' : overdueCount > 0 ? 'danger' : unpaidCount > 0 ? 'warn' : 'good';
const headerStatusText = isError
? 'Needs attention'
: loading
? 'Loading month'
: overdueCount > 0
? `${overdueCount} overdue`
: unpaidCount > 0
? `${unpaidCount} unpaid`
: 'All settled';
async function persistTrackerOrder(nextRows: TrackerRow[], movedBillId: number | null) {
const payload = Object.fromEntries(nextRows.map((row, index) => [row.id, index]));
@ -856,70 +812,22 @@ export default function TrackerPage() {
{/* ── Header ── */}
<PageHeader
eyebrow="Monthly command center"
title="Tracker"
description={(
eyebrow="Monthly overview"
title={(
<>
{activeMonthLabel}
<span className="mx-2 text-muted-foreground/60">·</span>
{monthStartLabel} to {monthEndLabel}
{isCurrentMonth && (
<>
<span className="mx-2 text-muted-foreground/60">·</span>
Current month
</>
)}
{MONTHS[month - 1]}
<span className="ml-2 text-xl font-normal text-muted-foreground">{year}</span>
</>
)}
glyph="tracker"
meta={(
<div className="flex flex-wrap items-center gap-2">
<HeaderStat icon={CheckCircle2} label="Paid" value={loading ? '...' : paidCount} tone={paidCount > 0 ? 'good' : 'neutral'} />
<HeaderStat icon={AlertCircle} label="Needs action" value={loading ? '...' : headerStatusText} tone={headerStatusTone} />
<HeaderStat icon={Search} label="Showing" value={`${filteredRows.length}/${rows.length}`} tone={hasFilters ? 'info' : 'neutral'} />
<HeaderStat icon={RefreshCw} label="Remaining" value={loading ? '...' : fmt(summary.remaining)} tone={Number(summary.remaining ?? 0) <= 0 ? 'good' : 'neutral'} />
</div>
)}
meta={`${rows.length} ${rows.length === 1 ? 'bill' : 'bills'}`}
actions={(
<div className="flex flex-wrap items-center gap-2 tracker-print-hide">
<div className="flex items-center gap-1 rounded-full border border-border/75 bg-card/70 p-1 shadow-sm shadow-black/5">
<Button
size="icon" variant="ghost"
onClick={() => navigate(-1)}
onMouseEnter={() => prefetchAdjacent(-1)}
onFocus={() => prefetchAdjacent(-1)}
className="h-8 w-8 rounded-full"
aria-label="Previous month"
>
<ChevronLeft className="h-4 w-4" />
</Button>
<span className="min-w-[8rem] px-1 text-center text-xs font-semibold tabular-nums select-none">
{activeMonthLabel}
</span>
<Button
size="icon" variant="ghost"
onClick={() => navigate(1)}
onMouseEnter={() => prefetchAdjacent(1)}
onFocus={() => prefetchAdjacent(1)}
className="h-8 w-8 rounded-full"
aria-label="Next month"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
<Button
size="sm" variant={isCurrentMonth ? 'secondary' : 'outline'}
onClick={goToday}
className="h-9 rounded-full px-3 text-xs"
>
Today
</Button>
<Button
size="sm"
variant={pinUpcoming ? 'default' : 'outline'}
onClick={togglePinUpcoming}
className="h-9 gap-1.5 rounded-full px-3"
className="h-9 gap-1.5 px-3"
title={pinUpcoming ? 'Showing urgent bills first — click to restore normal order' : 'Pin overdue and due-soon bills to the top'}
>
<ArrowUpToLine className="h-4 w-4" />
@ -932,7 +840,7 @@ export default function TrackerPage() {
variant="outline"
onClick={handleBankSync}
disabled={bankSyncing}
className="h-9 gap-1.5 rounded-full px-3"
className="h-9 gap-1.5 px-3"
title="Scan bank transactions and match payments"
>
{bankSyncing
@ -944,7 +852,7 @@ export default function TrackerPage() {
<Button
size="sm"
onClick={handleOpenAddBill}
className="h-9 gap-1.5 rounded-full px-3 shadow-sm"
className="h-9 gap-1.5 px-3 shadow-sm"
aria-label="Add bill"
title="Add bill"
>
@ -956,7 +864,7 @@ export default function TrackerPage() {
size="sm"
variant="outline"
onClick={goToCalendar}
className="h-9 gap-1.5 rounded-full px-3 tracker-print-hide"
className="h-9 gap-1.5 px-3 tracker-print-hide"
aria-label="Open calendar for this month"
title="View this month on the calendar"
>
@ -969,7 +877,7 @@ export default function TrackerPage() {
<Button
size="sm"
variant="outline"
className="h-9 gap-1.5 rounded-full px-3 tracker-print-hide"
className="h-9 gap-1.5 px-3 tracker-print-hide"
aria-label="Export or print this month"
title="Export or print this month"
>
@ -996,7 +904,7 @@ export default function TrackerPage() {
<Button
size="icon"
variant="ghost"
className="h-9 w-9 rounded-full tracker-print-hide"
className="h-9 w-9 tracker-print-hide"
aria-label="Keyboard shortcuts"
title="Keyboard shortcuts"
>
@ -1025,6 +933,38 @@ export default function TrackerPage() {
</DropdownMenuContent>
</DropdownMenu>
<div className="flex items-center gap-1 bg-muted/50 border border-border rounded-lg p-1">
<Button
size="icon" variant="ghost"
onClick={() => navigate(-1)}
onMouseEnter={() => prefetchAdjacent(-1)}
onFocus={() => prefetchAdjacent(-1)}
className="h-7 w-7 hover:bg-white/5"
aria-label="Previous month"
>
<ChevronLeft className="h-4 w-4" />
</Button>
<span className="min-w-[7.5rem] px-1 text-center text-xs font-semibold tabular-nums select-none">
{MONTHS[month - 1]} {year}
</span>
<Button
size="icon" variant="ghost"
onClick={() => navigate(1)}
onMouseEnter={() => prefetchAdjacent(1)}
onFocus={() => prefetchAdjacent(1)}
className="h-7 w-7 hover:bg-white/5"
aria-label="Next month"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
<Button
size="sm" variant="outline"
onClick={goToday}
className="h-9 px-3 text-xs"
>
Today
</Button>
</div>
)}
/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,31 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 360" role="img" aria-labelledby="title desc">
<title id="title">Bill Tracker empty-state illustration</title>
<desc id="desc">Quiet ledger cards, a checkmark chart, and calm status dots.</desc>
<defs>
<radialGradient id="glow" cx="50%" cy="35%" r="60%">
<stop offset="0" stop-color="#40c878" stop-opacity="0.18"/>
<stop offset="1" stop-color="#40c878" stop-opacity="0"/>
</radialGradient>
<linearGradient id="trust" x1="242" x2="392" y1="246" y2="96" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#1e9f93"/>
<stop offset="0.58" stop-color="#40c878"/>
<stop offset="1" stop-color="#96e6a1"/>
</linearGradient>
</defs>
<rect width="640" height="360" rx="38" fill="#101417"/>
<rect width="640" height="360" rx="38" fill="url(#glow)"/>
<rect x="126" y="74" width="388" height="212" rx="30" fill="#172025" stroke="#314047"/>
<rect x="158" y="108" width="122" height="18" rx="9" fill="#314047"/>
<rect x="158" y="144" width="84" height="14" rx="7" fill="#26343a"/>
<rect x="158" y="174" width="128" height="14" rx="7" fill="#26343a"/>
<rect x="158" y="204" width="102" height="14" rx="7" fill="#26343a"/>
<circle cx="438" cy="118" r="9" fill="#40c878"/>
<circle cx="438" cy="156" r="9" fill="#d99a24"/>
<circle cx="438" cy="194" r="9" fill="#23b6a8"/>
<rect x="318" y="183" width="22" height="52" rx="7" fill="url(#trust)" opacity="0.82"/>
<rect x="352" y="153" width="22" height="82" rx="7" fill="url(#trust)" opacity="0.92"/>
<rect x="386" y="122" width="22" height="113" rx="7" fill="url(#trust)"/>
<path d="m319 132 29 29 82-64" fill="none" stroke="#96e6a1" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
<path d="m319 132 29 29 82-64" fill="none" stroke="#40c878" stroke-linecap="round" stroke-linejoin="round" stroke-width="6"/>
<path d="M118 296h404" stroke="#26343a" stroke-width="4" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

View File

@ -1,21 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-labelledby="title desc">
<title id="title">Bill Tracker favicon</title>
<desc id="desc">A compact green checkmark and bar chart mark.</desc>
<defs>
<linearGradient id="trust" x1="48" x2="206" y1="210" y2="50" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#1e9f93"/>
<stop offset="0.58" stop-color="#40c878"/>
<stop offset="1" stop-color="#96e6a1"/>
</linearGradient>
<linearGradient id="ink" x1="34" x2="222" y1="34" y2="222" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#172025"/>
<stop offset="1" stop-color="#0f1417"/>
</linearGradient>
</defs>
<rect width="256" height="256" rx="58" fill="url(#ink)"/>
<rect x="61" y="123" width="28" height="59" rx="8" fill="url(#trust)" opacity="0.86"/>
<rect x="101" y="95" width="28" height="87" rx="8" fill="url(#trust)" opacity="0.94"/>
<rect x="141" y="69" width="28" height="113" rx="8" fill="url(#trust)"/>
<path d="M62 80.5 97.5 116 190 45" fill="none" stroke="#96e6a1" stroke-linecap="round" stroke-linejoin="round" stroke-width="18"/>
<path d="M62 80.5 97.5 116 190 45" fill="none" stroke="#40c878" stroke-linecap="round" stroke-linejoin="round" stroke-width="10"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" role="img" aria-labelledby="title desc">
<title id="title">Admin and security glyph</title>
<desc id="desc">A shield with a confirmation check for secure administration.</desc>
<rect width="96" height="96" rx="22" fill="#101417"/>
<path d="M48 15 73 25v19c0 17-10 29-25 37-15-8-25-20-25-37V25l25-10Z" fill="none" stroke="#40c878" stroke-width="6" stroke-linejoin="round"/>
<path d="m35 48 9 9 19-25" fill="none" stroke="#96e6a1" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="70" cy="70" r="8" fill="#23b6a8"/>
</svg>

Before

Width:  |  Height:  |  Size: 621 B

View File

@ -1,10 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" role="img" aria-labelledby="title desc">
<title id="title">Analytics glyph</title>
<desc id="desc">A calm trend line with two data points.</desc>
<rect width="96" height="96" rx="22" fill="#101417"/>
<path d="M20 72h56" stroke="#314047" stroke-width="6" stroke-linecap="round"/>
<path d="M22 63 38 46l14 10 24-34" fill="none" stroke="#40c878" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="38" cy="46" r="7" fill="#96e6a1"/>
<circle cx="76" cy="22" r="7" fill="#23b6a8"/>
<path d="M22 63 38 46l14 10" fill="none" stroke="#96e6a1" stroke-width="3" stroke-linecap="round" opacity="0.75"/>
</svg>

Before

Width:  |  Height:  |  Size: 701 B

View File

@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" role="img" aria-labelledby="title desc">
<title id="title">Bills glyph</title>
<desc id="desc">A receipt with ledger lines and a checkmark.</desc>
<rect width="96" height="96" rx="22" fill="#101417"/>
<path d="M27 17h42v62l-8-6-7 6-7-6-7 6-7-6-6 5V17Z" fill="none" stroke="#40c878" stroke-width="6" stroke-linejoin="round"/>
<path d="M36 34h20M36 46h16M36 58h12" stroke="#96e6a1" stroke-width="5" stroke-linecap="round" opacity="0.78"/>
<path d="m52 56 7 7 16-22" fill="none" stroke="#23b6a8" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 638 B

View File

@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" role="img" aria-labelledby="title desc">
<title id="title">Calendar glyph</title>
<desc id="desc">A calendar page with one quiet payment check.</desc>
<rect width="96" height="96" rx="22" fill="#101417"/>
<rect x="20" y="23" width="56" height="53" rx="10" fill="none" stroke="#40c878" stroke-width="6"/>
<path d="M31 16v15M65 16v15M22 38h52" stroke="#96e6a1" stroke-width="6" stroke-linecap="round"/>
<rect x="31" y="48" width="13" height="13" rx="4" fill="#23b6a8" opacity="0.82"/>
<path d="m49 60 7 7 17-24" fill="none" stroke="#40c878" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 686 B

View File

@ -1,10 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" role="img" aria-labelledby="title desc">
<title id="title">Data and banking glyph</title>
<desc id="desc">A banking column connected to a data store.</desc>
<rect width="96" height="96" rx="22" fill="#101417"/>
<path d="M17 35 42 21l25 14H17Z" fill="none" stroke="#40c878" stroke-width="6" stroke-linejoin="round"/>
<path d="M25 39v25M42 39v25M59 39v25M20 71h45" stroke="#96e6a1" stroke-width="6" stroke-linecap="round"/>
<ellipse cx="68" cy="49" rx="12" ry="5" fill="none" stroke="#23b6a8" stroke-width="5"/>
<path d="M56 49v14c0 3 5 6 12 6s12-3 12-6V49" fill="none" stroke="#23b6a8" stroke-width="5"/>
<path d="M56 57c0 3 5 6 12 6s12-3 12-6" stroke="#23b6a8" stroke-width="5" opacity="0.7"/>
</svg>

Before

Width:  |  Height:  |  Size: 777 B

View File

@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" role="img" aria-labelledby="title desc">
<title id="title">Snowball and payoff glyph</title>
<desc id="desc">Two growing payoff circles following an upward path.</desc>
<rect width="96" height="96" rx="22" fill="#101417"/>
<circle cx="33" cy="36" r="11" fill="none" stroke="#96e6a1" stroke-width="6" opacity="0.82"/>
<circle cx="58" cy="57" r="18" fill="none" stroke="#40c878" stroke-width="6"/>
<path d="M20 74c19-3 41-19 57-53" fill="none" stroke="#23b6a8" stroke-width="8" stroke-linecap="round"/>
<path d="M61 22h16v16" fill="none" stroke="#96e6a1" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 700 B

View File

@ -1,17 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" role="img" aria-labelledby="title desc">
<title id="title">Tracker glyph</title>
<desc id="desc">Three rising bars with a checkmark for bill tracking.</desc>
<defs>
<linearGradient id="g" x1="20" x2="76" y1="76" y2="18" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#1e9f93"/>
<stop offset="0.62" stop-color="#40c878"/>
<stop offset="1" stop-color="#96e6a1"/>
</linearGradient>
</defs>
<rect width="96" height="96" rx="22" fill="#101417"/>
<rect x="22" y="50" width="10" height="25" rx="4" fill="url(#g)" opacity="0.78"/>
<rect x="40" y="38" width="10" height="37" rx="4" fill="url(#g)" opacity="0.9"/>
<rect x="58" y="26" width="10" height="49" rx="4" fill="url(#g)"/>
<path d="m21 30 14 14 40-30" fill="none" stroke="#96e6a1" stroke-linecap="round" stroke-linejoin="round" stroke-width="8"/>
<path d="m21 30 14 14 40-30" fill="none" stroke="#40c878" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View File

@ -1,26 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-labelledby="title desc">
<title id="title">Bill Tracker maskable PWA icon</title>
<desc id="desc">A padded Bill Tracker mark for maskable app icon surfaces.</desc>
<defs>
<linearGradient id="trust" x1="142" x2="370" y1="378" y2="134" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#1e9f93"/>
<stop offset="0.58" stop-color="#40c878"/>
<stop offset="1" stop-color="#96e6a1"/>
</linearGradient>
<radialGradient id="glow" cx="44%" cy="34%" r="62%">
<stop offset="0" stop-color="#40c878" stop-opacity="0.18"/>
<stop offset="1" stop-color="#40c878" stop-opacity="0"/>
</radialGradient>
<linearGradient id="ink" x1="0" x2="512" y1="0" y2="512" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#172025"/>
<stop offset="1" stop-color="#0f1417"/>
</linearGradient>
</defs>
<rect width="512" height="512" fill="url(#ink)"/>
<rect width="512" height="512" fill="url(#glow)"/>
<rect x="118" y="256" width="44" height="92" rx="13" fill="url(#trust)" opacity="0.86"/>
<rect x="192" y="203" width="44" height="145" rx="13" fill="url(#trust)" opacity="0.94"/>
<rect x="266" y="152" width="44" height="196" rx="13" fill="url(#trust)"/>
<path d="m120 174 62 62 180-138" fill="none" stroke="#96e6a1" stroke-linecap="round" stroke-linejoin="round" stroke-width="34"/>
<path d="m120 174 62 62 180-138" fill="none" stroke="#40c878" stroke-linecap="round" stroke-linejoin="round" stroke-width="18"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

View File

@ -49,15 +49,10 @@ Primary source files live in `client/public/brand/`.
- `lockup.svg` - primary logo for dark surfaces.
- `lockup-light.svg` - primary logo for light surfaces.
- `logo.png` - raster export of the primary logo for docs and non-SVG contexts.
- `mark.svg` - compact app mark and favicon.
- `mark.png` - raster export of the compact app mark.
- `mark-mono.svg` - single-color mark for constrained contexts.
- `favicon.svg`, `favicon.ico` - browser tab and legacy favicon surfaces.
- `social-preview.svg`, `social-preview.png` - Open Graph, README, release, or marketplace preview.
- `empty-state.svg` - reusable empty-state illustration style.
- `pwa-192.png`, `pwa-512.png`, `pwa-maskable.svg`, `pwa-maskable-512.png`, `apple-touch-icon.png` - install surfaces.
- `glyphs/*.svg` - standalone product glyph source files.
- `social-preview.svg` - Open Graph, README, release, or marketplace preview.
- `pwa-192.png`, `pwa-512.png`, `apple-touch-icon.png` - install surfaces.
Rules:
@ -70,15 +65,14 @@ Rules:
Bill Tracker has a small proprietary product glyph set for page identity only:
| Product area | Asset |
| --- | --- |
| Tracker | `client/public/brand/glyphs/tracker.svg` |
| Bills | `client/public/brand/glyphs/bills.svg` |
| Calendar | `client/public/brand/glyphs/calendar.svg` |
| Analytics | `client/public/brand/glyphs/analytics.svg` |
| Data / Banking | `client/public/brand/glyphs/data-banking.svg` |
| Snowball / Payoff | `client/public/brand/glyphs/snowball-payoff.svg` |
| Admin / Security | `client/public/brand/glyphs/admin-security.svg` |
- Tracker
- Bills
- Calendar
- Analytics
- Data
- Banking
- Snowball
- Security/Admin
Rules:

View File

@ -3,21 +3,17 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/svg+xml" href="/brand/favicon.svg">
<link rel="alternate icon" href="/brand/favicon.ico">
<link rel="icon" type="image/svg+xml" href="/brand/mark.svg">
<link rel="apple-touch-icon" href="/brand/apple-touch-icon.png">
<meta name="theme-color" content="#101417">
<meta name="description" content="Calm command for household money.">
<meta property="og:title" content="Bill Tracker">
<meta property="og:description" content="Calm command for household money.">
<meta property="og:image" content="/brand/social-preview.png">
<meta property="og:image:type" content="image/png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image" content="/brand/social-preview.svg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Bill Tracker">
<meta name="twitter:description" content="Calm command for household money.">
<meta name="twitter:image" content="/brand/social-preview.png">
<meta name="twitter:image" content="/brand/social-preview.svg">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">

View File

@ -23,28 +23,13 @@ export default defineConfig({
VitePWA({
registerType: 'autoUpdate',
includeAssets: [
'brand/favicon.svg',
'brand/favicon.ico',
'brand/mark.svg',
'brand/mark.png',
'brand/mark-mono.svg',
'brand/lockup.svg',
'brand/lockup-light.svg',
'brand/logo.png',
'brand/empty-state.svg',
'brand/social-preview.svg',
'brand/social-preview.png',
'brand/pwa-maskable.svg',
'brand/glyphs/tracker.svg',
'brand/glyphs/bills.svg',
'brand/glyphs/calendar.svg',
'brand/glyphs/analytics.svg',
'brand/glyphs/data-banking.svg',
'brand/glyphs/snowball-payoff.svg',
'brand/glyphs/admin-security.svg',
'brand/pwa-192.png',
'brand/pwa-512.png',
'brand/pwa-maskable-512.png',
],
manifest: {
name: 'Bill Tracker',
@ -56,8 +41,7 @@ export default defineConfig({
start_url: '/',
icons: [
{ src: '/brand/pwa-192.png', sizes: '192x192', type: 'image/png' },
{ src: '/brand/pwa-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
{ src: '/brand/pwa-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
{ src: '/brand/pwa-512.png', sizes: '512x512', type: 'image/png', purpose: 'any maskable' },
],
},
workbox: {