Compare commits
2 Commits
23f79c31e0
...
b25be070ed
| Author | SHA1 | Date |
|---|---|---|
|
|
b25be070ed | |
|
|
40dfd6e3c1 |
|
|
@ -1,7 +1,7 @@
|
|||
# Bill Tracker
|
||||
|
||||
<p align="center">
|
||||
<img src="client/public/brand/social-preview.svg" alt="Bill Tracker — Calm command for household money">
|
||||
<img src="client/public/brand/social-preview.png" alt="Bill Tracker — Calm command for household money">
|
||||
</p>
|
||||
|
||||
Bill Tracker is a private, self-hosted bill planning app for households and
|
||||
|
|
|
|||
|
|
@ -6,12 +6,19 @@ export const BRAND = {
|
|||
assets: {
|
||||
logo: '/brand/lockup.svg',
|
||||
logoLight: '/brand/lockup-light.svg',
|
||||
logoPng: '/brand/logo.png',
|
||||
mark: '/brand/mark.svg',
|
||||
favicon: '/brand/mark.svg',
|
||||
markPng: '/brand/mark.png',
|
||||
monoMark: '/brand/mark-mono.svg',
|
||||
favicon: '/brand/favicon.svg',
|
||||
faviconIco: '/brand/favicon.ico',
|
||||
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: {
|
||||
|
|
@ -34,15 +41,15 @@ export type BrandGlyphName =
|
|||
| 'snowball'
|
||||
| 'security';
|
||||
|
||||
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 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 ADMIN_GLYPH = { label: 'Admin' } as const;
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export const RELEASE_NOTES: ReleaseNotes = {
|
|||
},
|
||||
],
|
||||
image: {
|
||||
src: '/brand/social-preview.svg',
|
||||
src: '/brand/social-preview.png',
|
||||
alt: 'Bill Tracker brand preview',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect, useMemo, useCallback, useDeferredValue, type ReactNode } from 'react';
|
||||
import { useState, useEffect, useMemo, useCallback, useDeferredValue, type ComponentType, 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,6 +91,34 @@ 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;
|
||||
|
|
@ -771,6 +799,22 @@ 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]));
|
||||
|
|
@ -812,22 +856,70 @@ export default function TrackerPage() {
|
|||
|
||||
{/* ── Header ── */}
|
||||
<PageHeader
|
||||
eyebrow="Monthly overview"
|
||||
title={(
|
||||
eyebrow="Monthly command center"
|
||||
title="Tracker"
|
||||
description={(
|
||||
<>
|
||||
{MONTHS[month - 1]}
|
||||
<span className="ml-2 text-xl font-normal text-muted-foreground">{year}</span>
|
||||
{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
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
glyph="tracker"
|
||||
meta={`${rows.length} ${rows.length === 1 ? 'bill' : 'bills'}`}
|
||||
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>
|
||||
)}
|
||||
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 px-3"
|
||||
className="h-9 gap-1.5 rounded-full 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" />
|
||||
|
|
@ -840,7 +932,7 @@ export default function TrackerPage() {
|
|||
variant="outline"
|
||||
onClick={handleBankSync}
|
||||
disabled={bankSyncing}
|
||||
className="h-9 gap-1.5 px-3"
|
||||
className="h-9 gap-1.5 rounded-full px-3"
|
||||
title="Scan bank transactions and match payments"
|
||||
>
|
||||
{bankSyncing
|
||||
|
|
@ -852,7 +944,7 @@ export default function TrackerPage() {
|
|||
<Button
|
||||
size="sm"
|
||||
onClick={handleOpenAddBill}
|
||||
className="h-9 gap-1.5 px-3 shadow-sm"
|
||||
className="h-9 gap-1.5 rounded-full px-3 shadow-sm"
|
||||
aria-label="Add bill"
|
||||
title="Add bill"
|
||||
>
|
||||
|
|
@ -864,7 +956,7 @@ export default function TrackerPage() {
|
|||
size="sm"
|
||||
variant="outline"
|
||||
onClick={goToCalendar}
|
||||
className="h-9 gap-1.5 px-3 tracker-print-hide"
|
||||
className="h-9 gap-1.5 rounded-full px-3 tracker-print-hide"
|
||||
aria-label="Open calendar for this month"
|
||||
title="View this month on the calendar"
|
||||
>
|
||||
|
|
@ -877,7 +969,7 @@ export default function TrackerPage() {
|
|||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-9 gap-1.5 px-3 tracker-print-hide"
|
||||
className="h-9 gap-1.5 rounded-full px-3 tracker-print-hide"
|
||||
aria-label="Export or print this month"
|
||||
title="Export or print this month"
|
||||
>
|
||||
|
|
@ -904,7 +996,7 @@ export default function TrackerPage() {
|
|||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-9 w-9 tracker-print-hide"
|
||||
className="h-9 w-9 rounded-full tracker-print-hide"
|
||||
aria-label="Keyboard shortcuts"
|
||||
title="Keyboard shortcuts"
|
||||
>
|
||||
|
|
@ -933,38 +1025,6 @@ 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>
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -0,0 +1,31 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
|
@ -0,0 +1,21 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,8 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 621 B |
|
|
@ -0,0 +1,10 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 701 B |
|
|
@ -0,0 +1,8 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 638 B |
|
|
@ -0,0 +1,9 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 686 B |
|
|
@ -0,0 +1,10 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 777 B |
|
|
@ -0,0 +1,9 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 700 B |
|
|
@ -0,0 +1,17 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
|
@ -0,0 +1,26 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
|
@ -49,10 +49,15 @@ 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.
|
||||
- `social-preview.svg` - Open Graph, README, release, or marketplace preview.
|
||||
- `pwa-192.png`, `pwa-512.png`, `apple-touch-icon.png` - install surfaces.
|
||||
- `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.
|
||||
|
||||
Rules:
|
||||
|
||||
|
|
@ -65,14 +70,15 @@ Rules:
|
|||
|
||||
Bill Tracker has a small proprietary product glyph set for page identity only:
|
||||
|
||||
- Tracker
|
||||
- Bills
|
||||
- Calendar
|
||||
- Analytics
|
||||
- Data
|
||||
- Banking
|
||||
- Snowball
|
||||
- Security/Admin
|
||||
| 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` |
|
||||
|
||||
Rules:
|
||||
|
||||
|
|
|
|||
10
index.html
|
|
@ -3,17 +3,21 @@
|
|||
<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/mark.svg">
|
||||
<link rel="icon" type="image/svg+xml" href="/brand/favicon.svg">
|
||||
<link rel="alternate icon" href="/brand/favicon.ico">
|
||||
<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.svg">
|
||||
<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 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.svg">
|
||||
<meta name="twitter:image" content="/brand/social-preview.png">
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -23,13 +23,28 @@ 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',
|
||||
|
|
@ -41,7 +56,8 @@ 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 maskable' },
|
||||
{ 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' },
|
||||
],
|
||||
},
|
||||
workbox: {
|
||||
|
|
|
|||