Files
Ready2Blob/.planning/phases/08-theme-foundation/08-01-SUMMARY.md
T
kawa 01752a126d docs(08-01): complete theme foundation plan 01
- SUMMARY with MD3 token system, ThemeToggle, FOUC prevention
- STATE updated: decisions, session, 97% progress
- ROADMAP phase 8 marked In Progress (1/2 plans)
- REQUIREMENTS THEME-01, THEME-02 marked complete
2026-04-01 04:51:55 +02:00

7.6 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed duration completed
08-theme-foundation 01 ui
tailwindcss
css-custom-properties
dark-mode
theme
react
vitest
MD3 color token system (15 tokens
primary, surface, error, success, warning families)
Dark mode infrastructure via @custom-variant dark + .dark class on <html>
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
09-component-migration
10-content-refinement
11-polish
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 <head> for FOUC prevention
vi.stubGlobal for Node v25 localStorage/matchMedia in jsdom tests
created modified
src/components/ui/ThemeToggle.tsx
src/components/ui/ThemeToggle.test.tsx
src/index.css
index.html
src/App.tsx
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 <head> for maximum paint-blocking guarantee
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
THEME-01
THEME-02
17min 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 <head> before stylesheets; reads r2b-theme from localStorage before first CSS paint
  • ThemeToggle segmented control: Light/Dark/System, writes localStorage + toggles .dark on <html>, no React Context overhead
  • ThemeToggle wired into App.tsx header flex row; bg-gray-50bg-surface, text-gray-900text-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: <hex>; } is the only pattern that allows .dark class cascade to work in Tailwind v4. Static hex values in @theme break dark mode override.
  • 15 tokens instead of 10: Added success/on-success (green) and warning/on-warning (amber) to cover ReviewStep semantic colors. Avoids hardcoded dark: prefixes in Phase 9 migration.
  • Node v25 localStorage workaround: Node v25 ships experimental WebStorage (with --localstorage-file flag). The global localStorage in tests lacks setItem/getItem methods. Fix: vi.stubGlobal('localStorage', inMemoryMock) in test beforeEach.
  • matchMedia mock required: jsdom does not implement window.matchMedia. applyTheme() calls it for system preference. Fix: vi.stubGlobal('matchMedia', mockFn) returning { matches: false, ... }.

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] localStorage.clear() unavailable in Node v25 jsdom test environment

  • Found during: Task 2 (ThemeToggle TDD)
  • Issue: Node v25 experimental WebStorage provides a global localStorage object with no standard Storage methods (setItem, getItem, clear, removeItem all undefined). The @vitest-environment jsdom pragma cannot override this at the global level.
  • Fix: Used vi.stubGlobal('localStorage', inMemoryMock) with a hand-rolled Storage implementation in beforeEach. Added vi.stubGlobal('matchMedia', mockFn) for the same reason (jsdom missing matchMedia).
  • Files modified: src/components/ui/ThemeToggle.test.tsx
  • Verification: 7 ThemeToggle tests green; all 166 tests pass
  • Committed in: 074dc33 (Task 2 GREEN commit)

2. [Rule 1 - Bug] toBeInTheDocument() unavailable — @testing-library/jest-dom not installed

  • Found during: Task 2 (ThemeToggle TDD)
  • Issue: Tests initially used toBeInTheDocument() which requires @testing-library/jest-dom. Existing tests in the project use only standard Vitest assertions (toBeDefined(), toBeNull()).
  • Fix: Replaced all toBeInTheDocument() calls with toBeDefined(), replaced all toHaveAttribute(attr, val) with element.getAttribute(attr) + toBe(val).
  • Files modified: src/components/ui/ThemeToggle.test.tsx
  • Verification: Tests pass without adding any new dependency
  • Committed in: 074dc33 (Task 2 GREEN commit)

Total deviations: 2 auto-fixed (both Rule 1 - bugs in test infrastructure) Impact on plan: Both fixes necessary to get tests green in this environment. No scope creep. Component implementation is unaffected.

Issues Encountered

  • Node v25 experimental WebStorage is a new environment quirk not documented in the research. The workaround (vi.stubGlobal) is clean and consistent with how other projects handle this in Node 22+.

User Setup Required

None — no external service configuration required.

Next Phase Readiness

  • Token system ready: bg-surface, text-on-surface, bg-primary, text-on-primary, border-outline, etc. are available as Tailwind utility classes
  • Dark mode working: .dark class on <html> cascades all 15 token values automatically
  • FOUC prevention in place: inline script reads r2b-theme before first CSS paint
  • Phase 9 (component migration) can proceed: all 10 files with 73 hardcoded color classes ready for mechanical find-and-replace migration using Pattern 4 from 08-RESEARCH.md
  • No blockers

Phase: 08-theme-foundation Completed: 2026-04-01