refactor(brand): wire brand + new primitives through layout chrome

The layout chrome adopts the new brand module + the just-landed
app-primitives. CSS variables, the favicon, the page title, and the
vite PWA manifest all switch to the new brand assets:

- BrandBlock / NavPill / Sidebar / Layout use BrandMark / BrandWordmark
  / BRAND.name from @/lib/brand.
- ThemeContext exposes the system / dark / light options for the new
  'theme = System' setting (commit 1 of 9 in the brand refresh set;
  the actual settings UI lands in the per-page commit).
- client/index.css: 153 lines of churn — the design-token refresh
  (accent palette, neutrals, surface tokens) consumed by every
  downstream commit.
- index.html: title, favicon, apple-touch-icon, theme-color, manifest
  href all point at client/public/brand/*.
- vite.config.mjs: VitePWA manifest icons + theme/background colors
  follow the new brand.

This is commit 3 of 9 in the v0.42.0 brand refresh. Depends on
4898987 (app-primitives).
This commit is contained in:
null 2026-07-05 17:18:31 -05:00
parent 4898987a18
commit 36834e6625
8 changed files with 116 additions and 96 deletions

View File

@ -1,20 +1,17 @@
import React, { useMemo } from 'react'; import React from 'react';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { BrandMark, BrandWordmark } from '@/components/brand/Brand';
import { BRAND } from '@/lib/brand';
export const BrandBlock = React.memo(function BrandBlock({ adminMode = false }: { adminMode?: boolean }) { export const BrandBlock = React.memo(function BrandBlock({ adminMode = false }: { adminMode?: boolean }) {
const logoSrc = useMemo(() => '/img/logo.png', []);
return ( return (
<NavLink <NavLink
to={adminMode ? '/admin' : '/'} to={adminMode ? '/admin' : '/'}
aria-label="BillTracker" aria-label={BRAND.name}
className="flex items-center gap-2 rounded-xl focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50" className="group flex items-center gap-2 rounded-xl focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
> >
<img <BrandMark className="h-12 w-auto max-w-[6.75rem] drop-shadow-[0_1px_2px_rgba(0,0,0,0.24)] transition-transform duration-200 group-hover:scale-[1.015]" />
src={logoSrc} <BrandWordmark className="sr-only" />
alt="BillTracker"
className="h-16 w-auto max-w-[9rem] object-contain drop-shadow-[0_1px_2px_rgba(0,0,0,0.45)]"
/>
{adminMode && ( {adminMode && (
<span className="hidden sm:inline-flex rounded-full border border-destructive/25 bg-destructive/10 px-2 py-0.5 text-[10px] font-semibold uppercase text-destructive"> <span className="hidden sm:inline-flex rounded-full border border-destructive/25 bg-destructive/10 px-2 py-0.5 text-[10px] font-semibold uppercase text-destructive">
Admin Admin

View File

@ -28,7 +28,7 @@ export default function Layout({ mainContentId }: { mainContentId?: string }) {
const location = useLocation(); const location = useLocation();
return ( return (
<div className="min-h-screen bg-[radial-gradient(circle_at_top_left,oklch(var(--primary)/0.06),transparent_30rem),linear-gradient(180deg,oklch(var(--background)),oklch(var(--muted)/0.12))] text-foreground" <div className="min-h-screen bg-[radial-gradient(circle_at_top_left,oklch(var(--primary)/0.08),transparent_28rem),radial-gradient(circle_at_top_right,oklch(var(--accent)/0.08),transparent_24rem),linear-gradient(180deg,oklch(var(--background)),oklch(var(--muted)/0.16))] text-foreground"
role="main" role="main"
aria-labelledby={mainContentId} aria-labelledby={mainContentId}
> >

View File

@ -34,7 +34,7 @@ export const NavPill = React.memo(function NavPill({ item, onNavigate, badge = 0
'focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50', 'focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50',
isActive isActive
? 'bg-primary text-primary-foreground shadow-sm shadow-primary/20' ? 'bg-primary text-primary-foreground shadow-sm shadow-primary/20'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground hover:shadow-sm' : 'text-muted-foreground hover:bg-accent/85 hover:text-accent-foreground hover:shadow-sm'
)} )}
> >
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4" />

View File

@ -81,7 +81,7 @@ function TrackerMenu({ onNavigate, badge = 0, badgeNames = [], items = trackerIt
'focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50', 'focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50',
isTrackerActive isTrackerActive
? 'bg-primary text-primary-foreground shadow-sm shadow-primary/20' ? 'bg-primary text-primary-foreground shadow-sm shadow-primary/20'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground hover:shadow-sm', : 'text-muted-foreground hover:bg-accent/85 hover:text-accent-foreground hover:shadow-sm',
)} )}
aria-expanded={isTrackerActive} aria-expanded={isTrackerActive}
aria-haspopup="menu" aria-haspopup="menu"
@ -237,7 +237,7 @@ export default function Sidebar({ adminMode = false }: { adminMode?: boolean; ma
}, [adminMode]); }, [adminMode]);
return ( return (
<header className="sticky top-0 z-40 border-b border-border/80 bg-card/95 shadow-sm shadow-foreground/10 backdrop-blur-md supports-[backdrop-filter]:bg-card/90"> <header className="sticky top-0 z-40 border-b border-border/70 bg-card/95 shadow-sm shadow-black/10 backdrop-blur-xl supports-[backdrop-filter]:bg-card/90">
<div className="mx-auto flex h-16 w-full max-w-[1500px] items-center gap-4 px-4 sm:px-6 lg:px-8"> <div className="mx-auto flex h-16 w-full max-w-[1500px] items-center gap-4 px-4 sm:px-6 lg:px-8">
<BrandBlock adminMode={adminMode} /> <BrandBlock adminMode={adminMode} />
@ -253,7 +253,7 @@ export default function Sidebar({ adminMode = false }: { adminMode?: boolean; ma
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
className="hidden h-9 gap-2 rounded-full bg-muted px-3 text-muted-foreground shadow-sm md:inline-flex" className="hidden h-9 gap-2 rounded-full bg-card/70 px-3 text-muted-foreground shadow-sm shadow-black/5 md:inline-flex"
onClick={() => window.dispatchEvent(new Event('command-palette:open'))} onClick={() => window.dispatchEvent(new Event('command-palette:open'))}
title="Find a bill" title="Find a bill"
> >
@ -264,13 +264,13 @@ export default function Sidebar({ adminMode = false }: { adminMode?: boolean; ma
</kbd> </kbd>
</Button> </Button>
)} )}
<ThemeToggle className="rounded-full border border-border/80 bg-muted shadow-sm" /> <ThemeToggle className="rounded-full border border-border/75 bg-card/70 shadow-sm shadow-black/5" />
<UserMenu adminMode={adminMode} /> <UserMenu adminMode={adminMode} />
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
size="icon" size="icon"
className="lg:hidden rounded-full bg-muted" className="rounded-full bg-card/70 lg:hidden"
aria-label={mobileOpen ? 'Close navigation menu' : 'Open navigation menu'} aria-label={mobileOpen ? 'Close navigation menu' : 'Open navigation menu'}
aria-expanded={mobileOpen} aria-expanded={mobileOpen}
onClick={() => setMobileOpen(v => !v)} onClick={() => setMobileOpen(v => !v)}
@ -281,7 +281,7 @@ export default function Sidebar({ adminMode = false }: { adminMode?: boolean; ma
</div> </div>
{mobileOpen && ( {mobileOpen && (
<div className="max-h-[70vh] overflow-y-auto border-t border-border/70 bg-card/95 px-4 py-3 shadow-lg shadow-foreground/10 lg:hidden"> <div className="max-h-[70vh] overflow-y-auto border-t border-border/70 bg-card/95 px-4 py-3 shadow-lg shadow-black/15 lg:hidden">
<nav className="mx-auto grid max-w-[1500px] gap-1"> <nav className="mx-auto grid max-w-[1500px] gap-1">
{!adminMode && trackerMenuItems.map(item => ( {!adminMode && trackerMenuItems.map(item => (
<NavPill <NavPill

View File

@ -20,7 +20,7 @@ const VALID_THEMES: Theme[] = ['light', 'dark', 'system'];
const DEFAULT_THEME: Theme = 'dark'; const DEFAULT_THEME: Theme = 'dark';
// Keep in sync with the inline pre-bundle script in index.html. // Keep in sync with the inline pre-bundle script in index.html.
const THEME_COLORS: Record<ResolvedTheme, string> = { light: '#ffffff', dark: '#18181b' }; const THEME_COLORS: Record<ResolvedTheme, string> = { light: '#f8faf9', dark: '#101417' };
function systemPrefersDark(): boolean { function systemPrefersDark(): boolean {
return typeof window !== 'undefined' && !!window.matchMedia?.('(prefers-color-scheme: dark)').matches; return typeof window !== 'undefined' && !!window.matchMedia?.('(prefers-color-scheme: dark)').matches;

View File

@ -30,38 +30,38 @@
@layer base { @layer base {
:root { :root {
--background: 0.97 0.005 250; --background: 0.985 0.006 235;
--foreground: 0.15 0.008 250; --foreground: 0.185 0.018 238;
--card: 0.995 0.004 250; --card: 0.998 0.003 238;
--card-foreground: 0.15 0.008 250; --card-foreground: 0.185 0.018 238;
--popover: 1.00 0 0; --popover: 1.00 0 0;
--popover-foreground: 0.15 0.008 250; --popover-foreground: 0.185 0.018 238;
--primary: 0.55 0.20 276; --primary: 0.64 0.17 154;
--primary-foreground: 0.99 0.003 250; --primary-foreground: 0.985 0.006 154;
--secondary: 0.93 0.008 250; --secondary: 0.94 0.010 235;
--secondary-foreground: 0.20 0.010 250; --secondary-foreground: 0.245 0.018 238;
--muted: 0.94 0.006 250; --muted: 0.945 0.009 235;
--muted-foreground: 0.38 0.010 250; --muted-foreground: 0.44 0.016 238;
--accent: 0.92 0.012 265; --accent: 0.925 0.020 184;
--accent-foreground: 0.20 0.010 250; --accent-foreground: 0.205 0.020 190;
--destructive: 0.55 0.22 25; --destructive: 0.58 0.21 22;
--destructive-foreground: 0.99 0.003 250; --destructive-foreground: 0.99 0.003 250;
--border: 0.88 0.008 250; --border: 0.875 0.010 235;
--input: 0.88 0.008 250; --input: 0.875 0.010 235;
--ring: 0.55 0.16 276; --ring: 0.64 0.15 154;
--chart-1: 0.55 0.22 270; --chart-1: 0.62 0.17 154;
--chart-2: 0.55 0.14 150; --chart-2: 0.58 0.13 195;
--chart-3: 0.60 0.18 310; --chart-3: 0.64 0.16 65;
--chart-4: 0.58 0.16 130; --chart-4: 0.62 0.17 335;
--chart-5: 0.55 0.18 255; --chart-5: 0.55 0.11 250;
--sidebar: 0.94 0.007 250; --sidebar: 0.965 0.008 235;
--sidebar-foreground: 0.20 0.008 250; --sidebar-foreground: 0.22 0.018 238;
--sidebar-primary: 0.55 0.20 276; --sidebar-primary: 0.64 0.17 154;
--sidebar-primary-foreground: 0.99 0.003 250; --sidebar-primary-foreground: 0.985 0.006 154;
--sidebar-accent: 0.90 0.009 250; --sidebar-accent: 0.925 0.020 184;
--sidebar-accent-foreground: 0.20 0.008 250; --sidebar-accent-foreground: 0.205 0.020 190;
--sidebar-border: 0.88 0.008 250; --sidebar-border: 0.875 0.010 235;
--sidebar-ring: 0.55 0.20 276; --sidebar-ring: 0.64 0.15 154;
--font-sans: 'GeorgiaDigits', Inter, Roboto, ui-sans-serif, system-ui, sans-serif; --font-sans: 'GeorgiaDigits', Inter, Roboto, ui-sans-serif, system-ui, sans-serif;
--font-serif: Merriweather, ui-serif, serif; --font-serif: Merriweather, ui-serif, serif;
--font-mono: 'GeorgiaDigits', "Roboto Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; --font-mono: 'GeorgiaDigits', "Roboto Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
@ -69,40 +69,40 @@
} }
.dark { .dark {
--background: 0.155 0.014 245; --background: 0.145 0.018 238;
--foreground: 0.965 0.006 245; --foreground: 0.962 0.008 235;
--card: 0.235 0.016 245; --card: 0.205 0.020 238;
--card-foreground: 0.965 0.006 245; --card-foreground: 0.962 0.008 235;
--popover: 0.26 0.018 245; --popover: 0.235 0.022 238;
--popover-foreground: 0.965 0.006 245; --popover-foreground: 0.962 0.008 235;
--primary: 0.72 0.17 150; --primary: 0.72 0.15 154;
--primary-foreground: 0.14 0.018 150; --primary-foreground: 0.145 0.020 154;
--secondary: 0.275 0.018 245; --secondary: 0.275 0.020 238;
--secondary-foreground: 0.94 0.007 245; --secondary-foreground: 0.94 0.008 235;
--muted: 0.275 0.016 245; --muted: 0.275 0.018 238;
/* Secondary text. Raised 0.76 0.85 at 0.76 labels and hints sat too /* Secondary text. Raised 0.76 0.85 at 0.76 labels and hints sat too
close to the card background (L 0.235) and strained the eyes. */ close to the card background (L 0.235) and strained the eyes. */
--muted-foreground: 0.85 0.012 245; --muted-foreground: 0.85 0.012 245;
--accent: 0.32 0.045 158; --accent: 0.33 0.050 184;
--accent-foreground: 0.965 0.006 245; --accent-foreground: 0.965 0.008 235;
--destructive: 0.66 0.18 26; --destructive: 0.66 0.18 26;
--destructive-foreground: 0.98 0.004 245; --destructive-foreground: 0.98 0.004 245;
--border: 0.40 0.018 245; --border: 0.37 0.020 238;
--input: 0.40 0.018 245; --input: 0.37 0.020 238;
--ring: 0.72 0.16 150; --ring: 0.72 0.14 154;
--chart-1: 0.55 0.22 270; --chart-1: 0.72 0.15 154;
--chart-2: 0.62 0.14 150; --chart-2: 0.70 0.12 195;
--chart-3: 0.65 0.18 310; --chart-3: 0.75 0.15 68;
--chart-4: 0.62 0.16 130; --chart-4: 0.70 0.16 340;
--chart-5: 0.58 0.18 255; --chart-5: 0.68 0.10 250;
--sidebar: 0.175 0.014 245; --sidebar: 0.165 0.018 238;
--sidebar-foreground: 0.95 0.006 245; --sidebar-foreground: 0.95 0.008 235;
--sidebar-primary: 0.72 0.17 150; --sidebar-primary: 0.72 0.15 154;
--sidebar-primary-foreground: 0.14 0.018 150; --sidebar-primary-foreground: 0.145 0.020 154;
--sidebar-accent: 0.30 0.038 158; --sidebar-accent: 0.30 0.045 184;
--sidebar-accent-foreground: 0.95 0.006 245; --sidebar-accent-foreground: 0.95 0.008 235;
--sidebar-border: 0.36 0.018 245; --sidebar-border: 0.34 0.020 238;
--sidebar-ring: 0.72 0.16 150; --sidebar-ring: 0.72 0.14 154;
} }
* { * {
@ -134,21 +134,44 @@
/* Generic surface */ /* Generic surface */
.surface { .surface {
@apply rounded-2xl border border-border/80 bg-card shadow-sm shadow-black/10; @apply rounded-xl border border-border/75 bg-card/95 shadow-sm shadow-black/10;
} }
/* Elevated surface */ /* Elevated surface */
.surface-elevated { .surface-elevated {
@apply surface; @apply surface backdrop-blur-sm;
box-shadow: box-shadow:
0 1px 2px rgb(0 0 0 / 0.05), 0 1px 2px rgb(0 0 0 / 0.05),
0 6px 18px rgb(0 0 0 / 0.06); 0 10px 30px rgb(15 23 42 / 0.07);
} }
.dark .surface-elevated { .dark .surface-elevated {
box-shadow: box-shadow:
0 0 0 1px oklch(var(--border) / 0.78), 0 0 0 1px oklch(var(--border) / 0.70),
0 10px 24px rgb(0 0 0 / 0.48); 0 14px 34px rgb(0 0 0 / 0.42);
}
.surface-premium {
@apply rounded-xl border border-border/75 bg-card/95 shadow-sm shadow-black/10 backdrop-blur-sm;
}
.dark .surface-premium {
box-shadow:
0 0 0 1px oklch(var(--border) / 0.55),
0 12px 30px rgb(0 0 0 / 0.34);
}
.metric-card {
@apply relative overflow-hidden rounded-xl border border-border/75 bg-card/95 px-5 py-4 shadow-sm shadow-black/10;
}
.metric-card::before {
content: "";
position: absolute;
inset: 0 0 auto;
height: 3px;
background: linear-gradient(90deg, oklch(var(--primary) / 0.85), oklch(var(--accent) / 0.68));
opacity: 0.76;
} }
/* Stat cards */ /* Stat cards */
@ -158,7 +181,7 @@
/* Table */ /* Table */
.table-surface { .table-surface {
@apply surface overflow-hidden shadow-sm shadow-black/10; @apply surface-premium overflow-hidden;
} }
.tracker-number { .tracker-number {

View File

@ -3,13 +3,13 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="/img/logo.png"> <link rel="icon" type="image/png" href="/brand/logo.png">
<link rel="apple-touch-icon" href="/img/apple-touch-icon.png"> <link rel="apple-touch-icon" href="/brand/apple-touch-icon.png">
<meta name="theme-color" content="#18181b"> <meta name="theme-color" content="#101417">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="apple-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"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="BillTracker"> <meta name="apple-mobile-web-app-title" content="Bill Tracker">
<title>Bill Tracker</title> <title>Bill Tracker</title>
<script> <script>
// Pre-bundle theme apply — avoids a light/dark flash. Mirrors applyTheme in // Pre-bundle theme apply — avoids a light/dark flash. Mirrors applyTheme in
@ -25,7 +25,7 @@
var root = document.documentElement; var root = document.documentElement;
if (resolved === 'dark') root.classList.add('dark'); else root.classList.remove('dark'); if (resolved === 'dark') root.classList.add('dark'); else root.classList.remove('dark');
var m = document.querySelector('meta[name="theme-color"]'); var m = document.querySelector('meta[name="theme-color"]');
if (m) m.setAttribute('content', resolved === 'dark' ? '#18181b' : '#ffffff'); if (m) m.setAttribute('content', resolved === 'dark' ? '#101417' : '#f8faf9');
} catch (e) { /* localStorage/matchMedia unavailable — keep default */ } } catch (e) { /* localStorage/matchMedia unavailable — keep default */ }
})(); })();
</script> </script>

View File

@ -22,18 +22,18 @@ export default defineConfig({
}), }),
VitePWA({ VitePWA({
registerType: 'autoUpdate', registerType: 'autoUpdate',
includeAssets: ['img/logo.png', 'img/pwa-192.png', 'img/pwa-512.png'], includeAssets: ['brand/logo.png', 'brand/pwa-192.png', 'brand/pwa-512.png'],
manifest: { manifest: {
name: 'BillTracker', name: 'Bill Tracker',
short_name: 'BillTracker', short_name: 'Bill Tracker',
description: 'Personal bill planning and tracking', description: 'Personal bill planning and tracking',
theme_color: '#18181b', theme_color: '#101417',
background_color: '#18181b', background_color: '#101417',
display: 'standalone', display: 'standalone',
start_url: '/', start_url: '/',
icons: [ icons: [
{ src: '/img/pwa-192.png', sizes: '192x192', type: 'image/png' }, { src: '/brand/pwa-192.png', sizes: '192x192', type: 'image/png' },
{ src: '/img/pwa-512.png', sizes: '512x512', type: 'image/png', purpose: 'any maskable' }, { src: '/brand/pwa-512.png', sizes: '512x512', type: 'image/png', purpose: 'any maskable' },
], ],
}, },
workbox: { workbox: {