Skip to content

feat: full design system redesign — semantic tokens, dark mode, test coverage - #2

Merged
Ermolz69 merged 5 commits into
mainfrom
feat/redesign
Jun 24, 2026
Merged

Ermolz69 merged 5 commits into
mainfrom
feat/redesign

Conversation

@Ermolz69

@Ermolz69 Ermolz69 commented Jun 24, 2026 •

Copy link
Copy Markdown
Contributor

Overview

Full frontend redesign that introduces a semantic design token system, light/dark/system theming, a restructured Settings page, and foundational test coverage. All UI components now use design tokens instead of raw Tailwind color utilities, making the app fully dark-mode–aware and easier to maintain.


What changed

🎨 Design system — semantic color tokens

  • Added 24 semantic CSS custom properties via Tailwind v4 @theme block (--color-surface, --color-text, --color-brand, --color-border, --color-danger, etc.)
  • Light and dark theme values defined in [data-theme='dark'] override block
  • Anti-flash inline script in index.html reads localStorage.theme before React hydrates to prevent white flash on dark-mode reload
  • All shared components (Button, Card, Input, Select, Badge, Modal, AppLayout, DatePicker) fully converted to tokens — zero raw color utilities remain in src/

🌙 Theme system

  • New ThemeProvider React context (app/src/app/providers/theme-provider.tsx)
    • Three values: 'light' | 'dark' | 'system'
    • Persists choice to localStorage
    • Syncs data-theme attribute on <html> via useEffect
    • system mode registers a matchMedia listener and reacts to OS changes live
  • Replaces the old ThemeWiring component + sheetly:theme-change event pattern

⚙️ Settings page redesign

  • Appearance section: three theme cards (Light / Dark / System) with hardcoded mini-previews and a live ThemePreview component that uses semantic tokens and updates with the theme automatically
  • General: currency + language
  • Spreadsheet: merged three separate cards (connection, Google auth, setup) into one
  • Containers, Data & Sync sections preserved
  • Advanced diagnostics: collapsible — shows mini sync summary (status / cached count / pending count) when collapsed, full diagnostics panel when expanded

📊 Dashboard analytics fixes

  • Custom ChartTooltipContent — dark-mode–aware tooltip (bg-surface border-border) replacing Recharts default white box
  • EmptyState rendering moved outside ResponsiveContainer to fix clipping when width=0 during first render
  • All CartesianGrid, axis ticks, and SVG borders now use CSS custom properties

🔄 Custom Select component

  • New isLoading prop: shows animated spinner icon instead of chevron and prevents dropdown from opening while data loads
  • Fixes visual misalignment in CurrencySelect during currency list fetch

🔬 Custom currency UI

  • Replaced native <select> with custom Select component across the whole app
  • Added DatePicker replacing native <input type="date"> (fixes empty-state crash)
  • Added analyticsActiveCurrency to LocalStorageKey

🧪 Test coverage

  • local-storage.service.test.ts — get/set/remove/exportKnownKeys/importKnownKeys/clearKnownKeys (12 tests)
  • theme-provider.test.tsx — getEffectiveTheme resolution, DOM data-theme wiring, useTheme state + localStorage persistence, matchMedia listener registration, throw-outside-provider guard (10 tests)
  • Select.test.tsx — rendering (label, placeholder, spinner, chevron, error, hint) + interaction (open/close, onChange, Escape, isLoading guard, disabled guard, aria-selected) (12 tests)
  • color-ban-patterns.test.ts — 14 banned cases + 23 allowed semantic-token cases; serves as the living specification for the ESLint color-ban rule (37 tests)
  • Added @testing-library/react, @testing-library/user-event, @testing-library/jest-dom
  • Coverage thresholds enforced in vite.config.ts: lines/statements ≥ 35%, branches/functions ≥ 33% — CI fails on regression

🚫 ESLint color-ban rule

  • Inline ESLint plugin color-ban in eslint.config.js
  • Bans: bg-white, bg-zinc-*, bg-slate-*, text-zinc-*, text-red-*, text-green-*, text-indigo-*, border-zinc-*, ring-red-*, ring-indigo-*, hover:bg-zinc-*
  • Applied to src/**/*.{ts,tsx} — any new file using raw color utilities will fail CI

Commits

Hash Summary
7f7a2a2 Add Vitest test coverage for critical paths and enforce coverage thresholds
df5978b Add live theme preview in Settings and color-ban ESLint rule
71f816d Add ThemeProvider, redesign Settings page, fix loading currency select
1c3097e Add semantic color tokens, light/dark themes, and fix chart theming
ba8bd57 Replace native select with custom Select component across the project
7094114 Fix empty-state crash and replace native date input with custom DatePicker
82aa267 Add analyticsActiveCurrency to LocalStorageKey
4e68c14 Add currency-aware analytics, scoped offline cache, and spreadsheet isolation tests
8ff429a Add Currencies sheet, per-currency summary, and currency filter

Test plan

  • Toggle Light → Dark → System in Settings → theme switches instantly, persists across reload
  • OS dark mode toggle → System theme follows without reload
  • Chart tooltips use bg-surface background in dark mode (no white box)
  • Empty chart slots render centered EmptyState without clipping
  • CurrencySelect shows spinner while currencies load, no layout shift
  • pnpm test — all 226 tests pass
  • pnpm lint — zero errors (color-ban rule clean across all src/)

Ermolz69 added 4 commits June 24, 2026 20:54
Introduces 24 CSS custom property tokens via Tailwind v4 @theme with
light defaults and [data-theme="dark"] overrides. All shared UI
components, widgets, and pages now use semantic classes (bg-surface,
text-text, border-border, etc.) instead of hardcoded zinc/white/indigo.

Theme wiring: anti-flash inline script in index.html, useApplyTheme
hook (watches localStorage + prefers-color-scheme + sheetly:theme-change
event), ThemeWiring mounted in AppProviders. Theme selector in Settings
applies instantly without page reload.

Fixes Recharts tooltip white background in dark mode via custom
ChartTooltipContent component using theme tokens. Fixes EmptyState
clipping by rendering it outside ResponsiveContainer when chart has no
data. Also themes CartesianGrid stroke and axis ticks via CSS vars.
ThemeProvider (TASK-3): replaces ThemeWiring + event-dispatch pattern.
Theme is now React state in context; setTheme() from useTheme() persists
to localStorage and applies data-theme to <html> immediately. Watches
prefers-color-scheme only when theme === 'system'. Deleted
use-apply-theme.ts.

Settings redesign (TASK-4/5/6): Appearance section with three theme
cards (Light/Dark/System) each with a hardcoded mini-preview using fixed
hex colors so the preview always shows that theme regardless of current
mode. Sections restructured: Appearance, General, Spreadsheet (merged 3
cards into 1 with divide-y), Containers, Data & Sync, Advanced
diagnostics. Advanced is collapsible and shows a mini sync summary
(status, cached count, pending count) in the collapsed state.

Select isLoading prop (fix): CurrencySelect no longer passes 'Loading
currencies...' as hint text, which was expanding the select cell height
and misaligning the Add button. A spinner icon replaces the chevron
inside the trigger while loading.
TASK-10: ThemePreview component renders a miniature app UI using only
semantic tokens (bg-surface, text-text, bg-brand, etc.) so it instantly
reflects any theme change without extra code. Inserted below the theme
selection cards in the Appearance section.

TASK-11: Inline ESLint plugin 'color-ban' in eslint.config.js bans
hardcoded Tailwind color utilities (bg-white, bg-zinc-*, bg-slate-*,
text-zinc-*, text-red-*, text-green-*, text-indigo-*, border-zinc-*,
ring-red-*, ring-indigo-*, hover:bg-zinc-*) across src/**/*.{ts,tsx}.
Full src/ lint pass confirms zero violations — codebase is already clean.
…sholds

New test files (83 tests):
- local-storage.service.test.ts: get/set/remove/export/import/clear across all
  known keys — foundation for all persistence in the app
- theme-provider.test.tsx: getEffectiveTheme resolution (dark/light/system +
  OS matchMedia), ThemeProvider DOM wiring, useTheme hook state/localStorage
  persistence and listener registration
- Select.test.tsx: rendering (label, placeholder, selected, spinner, chevron,
  error, hint), interaction (open/close, onChange, Escape key, isLoading guard,
  disabled guard, aria-selected)
- color-ban-patterns.test.ts: 14 banned cases + 23 allowed semantic-token cases
  that act as the specification for the ESLint color-ban rule

Also:
- Export getEffectiveTheme from theme-provider.tsx for direct unit testing
- Fix Select.tsx: isLoading now prevents dropdown from opening (was missing guard)
- Add @testing-library/react + @testing-library/user-event + @testing-library/jest-dom
- Add test-setup.ts (jest-dom matchers) wired via vite.config.ts setupFiles
- Add coverage thresholds (lines/statements 35%, branches/functions 33%) so CI
  fails if coverage regresses below the current baseline
@Ermolz69 Ermolz69 changed the title Feat/redesign feat: full design system redesign — semantic tokens, dark mode, test coverage Jun 24, 2026
- DashboardAnalytics: import JSX type from 'react' (namespace not globally
  available in strict mode with React 19)
- CurrencySelect: convert hint={warning} (string|null) to hint={warning??undefined}
  to match Select prop type (string|undefined)
@Ermolz69
Ermolz69 merged commit 9aaf081 into main Jun 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant