feat: full design system redesign — semantic tokens, dark mode, test coverage - #2
Merged
Merged
Conversation
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
- 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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
@themeblock (--color-surface,--color-text,--color-brand,--color-border,--color-danger, etc.)[data-theme='dark']override blockindex.htmlreadslocalStorage.themebefore React hydrates to prevent white flash on dark-mode reloadsrc/🌙 Theme system
ThemeProviderReact context (app/src/app/providers/theme-provider.tsx)'light'|'dark'|'system'localStoragedata-themeattribute on<html>viauseEffectsystemmode registers amatchMedialistener and reacts to OS changes liveThemeWiringcomponent +sheetly:theme-changeevent pattern⚙️ Settings page redesign
ThemePreviewcomponent that uses semantic tokens and updates with the theme automatically📊 Dashboard analytics fixes
ChartTooltipContent— dark-mode–aware tooltip (bg-surface border-border) replacing Recharts default white boxEmptyStaterendering moved outsideResponsiveContainerto fix clipping whenwidth=0during first renderCartesianGrid, axis ticks, and SVG borders now use CSS custom properties🔄 Custom Select component
isLoadingprop: shows animated spinner icon instead of chevron and prevents dropdown from opening while data loadsCurrencySelectduring currency list fetch🔬 Custom currency UI
<select>with customSelectcomponent across the whole appDatePickerreplacing native<input type="date">(fixes empty-state crash)analyticsActiveCurrencytoLocalStorageKey🧪 Test coverage
local-storage.service.test.ts—get/set/remove/exportKnownKeys/importKnownKeys/clearKnownKeys(12 tests)theme-provider.test.tsx—getEffectiveThemeresolution, DOMdata-themewiring,useThemestate + localStorage persistence,matchMedialistener registration, throw-outside-provider guard (10 tests)Select.test.tsx— rendering (label, placeholder, spinner, chevron, error, hint) + interaction (open/close,onChange, Escape,isLoadingguard,disabledguard,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)@testing-library/react,@testing-library/user-event,@testing-library/jest-domvite.config.ts: lines/statements ≥ 35%, branches/functions ≥ 33% — CI fails on regression🚫 ESLint color-ban rule
color-banineslint.config.jsbg-white,bg-zinc-*,bg-slate-*,text-zinc-*,text-red-*,text-green-*,text-indigo-*,border-zinc-*,ring-red-*,ring-indigo-*,hover:bg-zinc-*src/**/*.{ts,tsx}— any new file using raw color utilities will fail CICommits
7f7a2a2df5978b71f816d1c3097eba8bd57709411482aa2674e68c148ff429aTest plan
bg-surfacebackground in dark mode (no white box)EmptyStatewithout clippingpnpm test— all 226 tests passpnpm lint— zero errors (color-ban rule clean across allsrc/)