Skip to content

Commit 7aaa36e

Browse files
authored
feat(theme): premium light theme + Light/Dark/System toggle (#79) (#199)
* feat(theme): premium light theme + Light/Dark/System toggle (closes #79) Adds a full light theme alongside the existing dark, switchable via a sidebar toggle and Settings > Appearance (Light / Dark / System). Everything is driven from ONE place -- index.css tokens -- so the whole app flips with no per-page logic. An inline script in index.html applies the saved theme before first paint (no flash); ThemeWatcher follows the OS preference in System mode. Light palette: warm off-white page background (LAB, perceptually even warmth, matching the kalchar sibling-repo approach), pure-white cards that lift off it with soft layered shadows, hairline cool-grey borders, near-black text, darkened accent colors for AA contrast on white. The real work was removing pre-existing hardcoded-color tech debt so 'one variable controls all' is actually true: - 364 text-white -> text-foreground (themed: near-white in dark, near-black in light) - 291 bg-white/N + border-white/N -> a semantic --overlay-N / --hairline-N ladder that flips per theme (white-alpha in dark, dark-alpha in light) - hardcoded dark literals (bg-[#1a1a1c] modals, bg-zinc-900 dropdowns, bg-[#0a0a0c] tab bar, bg-[#111113] sidebar, bg-[rgba(44,44,46,.6)] selects) -> existing surface tokens - new --sidebar-bg / --header-bg / --skeleton-* / --divider tokens Dark theme is byte-identical (the new tokens default to the exact prior values). New: lib/theme.ts, store/themeStore.ts, Sidebar/ThemeToggle.tsx. Verified live at desktop in both themes (sidebar, search, cards, modals, dropdowns, charts) + dark regression check; tsc + eslint + 266 tests + build all green. * fix(theme): synchronized cross-fade on theme switch Parts were easing at different speeds on switch (body 250ms, glass cards 200/300ms, others 150ms or instant). Now a short-lived 'theme-transition' class is added to <html> only during the switch (auto-removed after 400ms by the theme lib), giving EVERY element + pseudo-element one uniform 300ms background/border/color/fill/stroke/shadow transition -- so the whole UI cross-fades in sync. transform/opacity excluded so the active-tab pill and hovers aren't dragged in; respects prefers-reduced-motion; no transition on first paint. Verified live: real toggle adds the class, theme flips, class auto-clears; body/card/sidebar all report identical 0.3s timing mid-switch. * fix(theme): light demo banner + keep white text on solid accent buttons - DemoBanner: bg-black/80 -> bg-surface-dropdown/90 so it flips to light (was a dark pill with now-dark text = invisible in light mode). - Revert text-foreground -> text-white on SOLID/gradient accent buttons (bg-app-blue, from-primary, bg-app-red/90, bg-primary) where white-on-color is correct in both themes; tinted selected states (bg-*/15, /20) keep text-foreground (dark text on light tint is correct in light). Verified live: light demo banner readable, buttons correct in both themes; 266 tests + build green. * fix(theme): make charts theme-aware + reactive, fix leftover literals, restore dark fidelity - Add 20 --chart-* CSS tokens (dark defaults verbatim, AA-contrast light overrides) so chart axis/grid/tooltip/label colors flip with the theme. - chartColors.ts now sources from rawColors.chart; refreshChartConstants() rebuilds palettes (CHART_COLORS/WARM/INCOME), CHART_TEXT/SURFACE/INPUT, axis/grid, semantics in place on theme toggle. colors.ts gains refreshRawColors()/onRawColorsRefresh(). - metricColorConfig + EXPENSE_CATEGORY_COLORS rebuild in place on toggle (were frozen at module load, leaving KPI chips/category hues stale after switching). - AppLayout remounts routed content on resolved-theme change so charts recompute; applyTheme() calls refreshRawColors() first. - Fix leftover chart literals: StandardPie center label, StandardArea/StandardBar reference lines + labels, Sankey node text, EffectiveTaxRate/YearInReview dots. - Home page: header bg-black -> --header-bg, invisible from-white heading gradient -> from-foreground, add theme toggle to public header. Toast + select option + focus ring + mobile scrollbar -> theme-aware tokens. - Restore dark fidelity: --glow-base-shadow (none in dark, glass-shadow in light), distinct --glass-border-ultra/-thin/-strong-hover tiers, skeleton 0.03/0.06, --scrollbar-thumb-hover, so dark stays visually unchanged. * feat(ui): world-class UI/UX elevation pass across all pages Result of a 14-lens audit (189 findings) -> synthesis -> surgical implementation. 52 safe, behavior-preserving improvements; gates green (type-check, lint, 266 tests). - Token discipline: remaining hardcoded text-white/text-black/bg-black/N/bg-white/ placeholder-gray/SVG-hex swapped for theme-aware tokens so both themes stay readable. - tabular-nums on currency/KPI/table numeric readouts (flows, goals, projections, comparison/tax/net-worth tables, bill-calendar) so digits stop shifting width. - a11y: ariaLabel on the four StandardChart primitives + per-call-site chart labels, listbox/option roles on CurrencySwitcher/NotificationCenter, aria-label on icon-only sidebar buttons + skip link, aria-valuenow on FIRE slider. - Nav/IA: page headers now mirror the canonical sidebar labels (Expense Analysis, Cash Flow, Comparison, Budget Manager, Recurring, Income Tax, FIRE Calculator, Anomaly Review); PAGE_TITLES covers /upload, /more, /demo. - Mobile-first: 2-col KPI grids on phone with progressive sm:/md: steps, >=44px targets. - States: standardized PageSkeleton / ChartEmptyState / EmptyState usage. - Motion (kept visible): shared whileTap/hover feedback, synced collapsible timing. - Overflow safety: title attrs on truncated values, break-friendly large currency. - Removed dead getStoredTheme(); dropped erroneous static aria-current on NavLinks (NavLink sets it on the active link automatically). * chore(release): 2.19.0 -- light theme + world-class UI/UX elevation * chore(gitignore): ignore Playwright MCP + ad-hoc verification artifacts * feat(ui): implement the 12 needs-review items -- tokens, IA, structure, resilience Design-decision + shared-primitive items (stage 1): - New semantic tokens: --color-on-accent (white-on-solid-accent, both themes), --color-on-warning (near-black on yellow warning pairs), --modal-backdrop (dialog scrim). Swept the 4 modals + sidebar drawer onto --modal-backdrop, text-black warning pairs onto text-on-warning. - Removed dead --color-border-strong alias (line 41 already won the cascade). - PageContainer now mirrors PageHeader's safe-area horizontal padding so the sticky header lines up on notched iOS PWA; added optional maxWidth prop (7xl default, 5xl/4xl) so narrow pages can adopt the scaffold. - AnalyticsTimeFilter mode tabs go full-width/flex on phone (no overflow <400px). - Settings Toggle gets a 44px ::before hit area; goal-form buttons min-h-11 on mobile. Page-breadth items (stage 2, via workflow): - PageContainer adoption across hand-rolled page roots (kept deliberate centered layouts like UploadSync hand-rolled). - isError branches added where query hooks expose it but only isLoading was handled. - KPI sizing/padding converged onto --text-kpi scale where it had drifted. - Split oversized files: Sidebar -> BrandHeader + navConfig, SubscriptionTracker -> components/{AddRecurringForm,RecurringCard} + constants + types, YearInReview -> MonthlyBreakdownChart + YearInsightsPanel. Fixed regressions from the earlier text-white->text-foreground sweep: 5 solid-accent buttons (ConfirmDialog, SubscriptionTracker, Goals, HomePage, Hero) were rendering near-black text on solid accent fills in light theme; now use text-on-accent. Comparison KPI grid 3+1 orphan at md fixed (2x2 -> 1x4). Gate: type-check + lint clean, 266 tests pass. Both themes verified live. * feat(ia): information-architecture restructure (nav, Overview, settings, workflows) From a 5-lens IA audit; user-approved scope A-F + quick-wins. Navigation: - Merge Net Worth + Investments into one 'Wealth' section (D). - Fold the dead-end single-item Transactions section into a new 'Data' section with Upload & Sync (elevated, E) + Settings (C); reorder sidebar to the money decision-flow; rename Tracking -> Commitments. - Mobile parity: Transactions now on the More page; More tab shows an alert badge (unreviewed anomalies + over-budget count). MorePage groups mirror desktop. Overview page (F): - New /overview composing existing hooks (income/spend/net/savings-rate KPIs, budgets-at-risk, goals progress, top income/expense) with deep links + empty state. Route + lazy import + sidebar entry + page title wired. Settings (A) + feature-config (B): - Regroup 11 flat sections into 4 task groups (Money Setup, Categories & Classification, Profile & Display, Advanced) via a new GroupHeader primitive; essential setup expanded first. - Budget Defaults and Anomaly Detection now also render as collapsible panels on the Budget and Anomaly pages (same components/endpoints, context-of-use). Quick wins: - Dashboard shows an upload CTA empty state for zero-data users (qw2). - Income Tax <-> GST reciprocal cross-links (qw4). Tokens/cleanup: - New --color-on-accent / --color-on-warning / --modal-backdrop tokens; swept 16 files of raw text-white -> text-on-accent; modal scrims -> --modal-backdrop; warning pairs -> text-on-warning; removed dead --color-border-strong alias. - PageContainer mirrors PageHeader safe-area padding + optional maxWidth prop. - AnalyticsTimeFilter tabs full-width on phone; settings Toggle 44px hit area. - theme-color meta now tracks the active theme (pre-paint + live). Gate: type-check + lint clean, 266 tests pass. Both themes verified live (Overview, settings groups, new sidebar). * fix(a11y): WCAG 2.2 focus-not-obscured + target-size (from web.dev/WCAG research) - Focus Not Obscured (SC 2.4.11): add scroll-padding-top (sticky PageHeader) and phone-only scroll-padding-bottom (fixed MobileTabBar + safe-area) to #main-content so a keyboard-focused control near an edge is never hidden behind the chrome. - Target Size Minimum (SC 2.5.8): bump three sub-24px icon buttons to a >=24px hit area (min-w-6 min-h-6) without changing icon size -- BudgetTracker edit/delete (was ~20px), DemoBanner + NotificationCenter close (was ~22px). Added the two missing aria-labels on the BudgetTracker icon buttons. Researched current best practices (web.dev CWV/PWA, WCAG 2.2 quickref, React 19); backend security headers (CSP/HSTS preload/Permissions-Policy) and the PWA config were already best-practice, so no change needed there. CSS/attribute-only; no behavior change. type-check + lint clean, 266 tests pass. * chore(deps): refresh frontend + backend to latest compatible Stack is already on the latest majors (React 19.2, Vite 8, Tailwind 4, TS 6, Python 3.13, pandas 3, FastAPI 0.138) -- this is the routine patch/minor freshness pass, no breaking upgrades. Frontend: @tanstack/react-query 5.101.1->.2, framer-motion 12.41->12.42, eslint 10.5->10.6, @types/node 26.0.0->.1. type-check + lint + 266 tests + prod build (incl PWA SW) all green. Backend: uv lock --upgrade -- pandas 3.0.1->3.0.3, numpy 2.4.2->2.5, fastapi 0.138.0->.1, alembic/ruff/pydantic-core/anyio/requests/etc patch bumps. ruff + mypy (131 files) clean, 239 tests pass.
1 parent 635a7d0 commit 7aaa36e

210 files changed

Lines changed: 4762 additions & 3209 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,12 @@ typings/
222222
# Added for consistency
223223
.env.*.local
224224
Thumbs.db
225+
226+
# Test / verification artifacts (Playwright MCP screenshots, ad-hoc captures, traces)
227+
.playwright-mcp/
228+
playwright-report/
229+
test-results/
230+
*.local.jpeg
231+
*.local.png
232+
/*.jpeg
233+
/*.png

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,29 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

77
---
88

9+
## 2.19.0 - 2026-06-28
10+
11+
A premium **light theme**, a world-class UI/UX elevation pass, and an information-architecture restructure. Adds a Light/Dark/System toggle and makes the entire app render flawlessly in both themes, driven by live design-system research (Radix Colors, Material 3, Apple HIG), a 14-lens UI audit, and a 5-lens IA audit. Frontend-only -- no API, schema, or backend changes. Dark theme is preserved byte-for-byte.
12+
13+
### Added
14+
15+
- **Overview page (`/overview`).** A single "whole picture" view composing existing data (income/spending/net-saved/savings-rate KPIs, budgets-at-risk, goals progress, top income vs spending) with deep links to each detail page. Sits beside Dashboard, which stays the configurable widget board.
16+
- **Premium light theme + Light/Dark/System toggle.** A warm off-white palette (LAB surfaces, white cards lifting off the page, soft layered shadows, AA-contrast accents) controlled entirely from `index.css` tokens. Switch via the sidebar cycle button or Settings > Appearance; an anti-flash inline script applies the stored theme before first paint, and `System` follows the OS preference live. The public landing page gets its own toggle in the header.
17+
- **Theme-aware, reactive charts.** 20 new `--chart-*` tokens (axis, grid, tooltip, reference-line, label colors) flip with the theme; Recharts/SVG colors resolve through `rawColors` at runtime (CSS `var()` does not work in SVG attributes). All derived palettes (`CHART_COLORS`, `metricColorConfig`, category colors) rebuild in place on toggle, and the routed view remounts on theme change so charts repaint without a reload.
18+
19+
### Changed
20+
21+
- **One source of truth for color.** Swept the remaining hardcoded color literals app-wide (`text-white`/`bg-white/N`/`bg-black/N`/gray-family/SVG hex/inline rgba) onto semantic tokens (`text-foreground`, `bg-surface-*`, `bg-[var(--overlay-N)]`, `border-[var(--hairline-N)]`), so a single token edit re-themes everything. White-on-solid-accent button text is the only intentional exception.
22+
- **World-class UI/UX elevation (52 surgical improvements):** `tabular-nums` on currency/KPI/table numbers so digits stop shifting width; accessible names on chart primitives + icon-only controls (listbox/option roles on dropdowns, aria-valuenow on the FIRE slider); page headers now mirror the canonical sidebar labels (Expense Analysis, Cash Flow, Income Tax, Budget Manager, ...); 2-column KPI grids on phones with >=44px touch targets; standardized loading/empty states; shared press/hover micro-interactions (motion kept fully visible).
23+
- **Information-architecture restructure** (from a 5-lens IA audit). Sidebar reordered to the money decision-flow and regrouped: Net Worth + Investments merged into one **Wealth** section, the dead-end single-item Transactions section folded into a new **Data** section (with Upload & Sync elevated out of the icon bar), and Tracking renamed **Commitments**. **Settings** regrouped from 11 flat sections into four task groups (Money Setup, Categories & Classification, Profile & Display, Advanced) with essential setup expanded first. Budget Defaults and Anomaly Detection now also appear as collapsible panels on their own feature pages (context-of-use), Income Tax and GST cross-link, the mobile More tab shows an alert badge (anomalies + over-budget), and the Dashboard shows an upload CTA when a new user has no data. New tokens `--color-on-accent` / `--color-on-warning` / `--modal-backdrop` name the white-on-accent / black-on-warning / dialog-scrim conventions; the mobile browser chrome (`theme-color`) now tracks the active theme.
24+
25+
### Fixed
26+
27+
- **Theme switch desync** -- a short-lived `theme-transition` class gives every element one uniform 300ms cross-fade so the whole UI flips in sync instead of parts easing at different speeds.
28+
- Removed an erroneous static `aria-current="page"` on sidebar/mobile-tab `NavLink`s (every item announced as the current page; `NavLink` already sets it on the active link) and a dead `getStoredTheme()` helper.
29+
30+
---
31+
932
## 2.18.0 - 2026-06-28
1033

1134
A frontend-only UX, visualization, and mobile pass. No API, schema, or backend changes -- every commit touches `frontend/src`. Driven by a senior-design audit of the shared UI primitives and every page, a data-visualization-fit audit (~295 elements scored), and a live mobile audit at 390px. Desktop layout is preserved throughout (mobile changes are gated behind `sm:`/`lg:`/`useIsMobile`).

0 commit comments

Comments
 (0)