--- phase: 08-theme-foundation plan: 01 subsystem: ui tags: [tailwindcss, css-custom-properties, dark-mode, theme, react, vitest] # Dependency graph requires: [] provides: - MD3 color token system (15 tokens: primary, surface, error, success, warning families) - Dark mode infrastructure via @custom-variant dark + .dark class on - Flash prevention inline script in index.html - ThemeToggle segmented control component (Light/Dark/System) - bg-surface, text-on-surface, bg-primary, etc. Tailwind utility classes affects: [09-component-migration, 10-content-refinement, 11-polish] # Tech tracking tech-stack: added: [] patterns: - Two-layer token architecture (@layer base :root/.dark raw values + @theme var() references) - DOM class toggle for theme (no React Context, no re-render cascade) - Inline blocking script in
for FOUC prevention - vi.stubGlobal for Node v25 localStorage/matchMedia in jsdom tests key-files: created: - src/components/ui/ThemeToggle.tsx - src/components/ui/ThemeToggle.test.tsx modified: - src/index.css - index.html - src/App.tsx key-decisions: - "Two-layer CSS token pattern: raw values in @layer base :root/.dark, @theme references via var() — required for .dark cascade to work" - "vi.stubGlobal for localStorage and matchMedia in tests — Node v25 experimental WebStorage breaks standard Storage API in jsdom" - "15 tokens (11 core + success/on-success/warning/on-warning) to cover ReviewStep semantic colors without hardcoded dark: prefixes" - "Inline FOUC script placed before stylesheets in for maximum paint-blocking guarantee" patterns-established: - "Token pattern: Always use @theme { --color-X: var(--r2b-X); } + @layer base :root/.dark for dynamic theming" - "Test pattern: vi.stubGlobal('localStorage', mock) + vi.stubGlobal('matchMedia', mock) for Node v25 jsdom compatibility" - "DOM theme toggle: applyTheme() directly mutates document.documentElement.classList — no useEffect, no Context" requirements-completed: [THEME-01, THEME-02] # Metrics duration: 17min completed: 2026-04-01 --- # Phase 8, Plan 01: Theme Foundation Summary **MD3 color token system with 15 CSS custom properties, @custom-variant dark toggle, FOUC-free flash prevention script, and ThemeToggle segmented control component with 7 passing unit tests** ## Performance - **Duration:** 17 min - **Started:** 2026-04-01T02:32:49Z - **Completed:** 2026-04-01T02:50:04Z - **Tasks:** 2 (+ TDD RED commit) - **Files modified:** 5 ## Accomplishments - Full MD3 color token system in `src/index.css`: 15 token pairs (light/dark), wired to Tailwind utility classes via `@theme { --color-*: var(--r2b-*) }` two-layer pattern - Flash prevention inline blocking script added to `index.html` `` before stylesheets; reads `r2b-theme` from localStorage before first CSS paint - ThemeToggle segmented control: Light/Dark/System, writes localStorage + toggles `.dark` on ``, no React Context overhead - ThemeToggle wired into App.tsx header flex row; `bg-gray-50` → `bg-surface`, `text-gray-900` → `text-on-surface` - All 166 tests pass (159 pre-existing + 7 new ThemeToggle unit tests) ## Task Commits Each task was committed atomically: 1. **Task 1: Define MD3 color tokens and flash prevention script** - `6027552` (feat) 2. **Task 2 RED: Add failing tests for ThemeToggle** - `fa2c0d9` (test) 3. **Task 2 GREEN: Implement ThemeToggle and wire into App.tsx** - `074dc33` (feat) **Plan metadata:** _(docs commit follows)_ _Note: TDD task split into RED (fa2c0d9) and GREEN (074dc33) commits per TDD protocol_ ## Files Created/Modified - `src/index.css` — @import + @custom-variant dark + @layer base with 15 token pairs + @theme mapping + body transition - `index.html` — Inline blocking IIFE script reading r2b-theme from localStorage before paint - `src/components/ui/ThemeToggle.tsx` — Segmented control: 3 buttons, DOM class toggle, localStorage write - `src/components/ui/ThemeToggle.test.tsx` — 7 unit tests with localStorage/matchMedia stubs for Node v25 - `src/App.tsx` — Import ThemeToggle, flex header row, bg-surface + text-on-surface token classes ## Decisions Made - **Two-layer token pattern confirmed:** `@theme { --color-X: var(--r2b-X); }` + `@layer base :root/.dark { --r2b-X: