docs(08): create phase plan - 2 plans, 2 waves

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 18:07:29 +02:00
co-authored by Claude Opus 4.6
parent 0ef71cdcab
commit bee3bad93a
3 changed files with 492 additions and 2 deletions
@@ -0,0 +1,225 @@
---
phase: 08-theme-foundation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/index.css
- index.html
- src/components/ui/ThemeToggle.tsx
- src/components/ui/ThemeToggle.test.tsx
- src/App.tsx
autonomous: true
requirements:
- THEME-01
- THEME-02
must_haves:
truths:
- "Tailwind utility classes bg-surface, text-on-surface, bg-primary, text-on-primary, border-outline etc. are available and resolve to correct hex values"
- "Adding .dark class to html element switches all token values to dark palette"
- "ThemeToggle renders three options: Light, Dark, System"
- "Clicking a theme option applies the correct class to document.documentElement and persists to localStorage"
- "On hard reload with r2b-theme=dark in localStorage, the .dark class is present before React mounts (no flash)"
artifacts:
- path: "src/index.css"
provides: "MD3 color token definitions (light + dark) and @theme mapping"
contains: "@theme"
- path: "index.html"
provides: "Inline blocking script for flash prevention"
contains: "r2b-theme"
- path: "src/components/ui/ThemeToggle.tsx"
provides: "Segmented theme toggle component"
exports: ["ThemeToggle"]
- path: "src/components/ui/ThemeToggle.test.tsx"
provides: "Unit tests for ThemeToggle behavior"
contains: "describe"
- path: "src/App.tsx"
provides: "ThemeToggle wired into header, bg-surface applied to shell"
contains: "ThemeToggle"
key_links:
- from: "src/index.css"
to: "src/components/ui/ThemeToggle.tsx"
via: ".dark class on html toggles CSS variable cascade"
pattern: "classList\\.toggle.*dark"
- from: "index.html"
to: "localStorage"
via: "inline script reads r2b-theme before paint"
pattern: "localStorage\\.getItem.*r2b-theme"
- from: "src/App.tsx"
to: "src/components/ui/ThemeToggle.tsx"
via: "import and render in header"
pattern: "import.*ThemeToggle"
---
<objective>
Build the MD3 color token system, dark mode infrastructure, and ThemeToggle component.
Purpose: Establish the CSS custom property foundation that all components will reference instead of hardcoded Tailwind colors. This is the infrastructure layer that Plan 02 (migration) and Phases 9-11 depend on.
Output: Working token system with dark mode toggle, flash prevention, and ThemeToggle component with unit tests.
</objective>
<execution_context>
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/08-theme-foundation/08-CONTEXT.md
@.planning/phases/08-theme-foundation/08-RESEARCH.md
<interfaces>
<!-- Current src/index.css is just: @import "tailwindcss"; -->
<!-- Current index.html has no inline scripts -->
From src/App.tsx (current structure):
```tsx
import { useWizard } from './store/context';
import { WizardProvider } from './store/context';
import { StepIndicator } from './components/wizard/StepIndicator';
// ... step imports
function WizardShell() {
const { state } = useWizard();
// ...
return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center py-12 px-4">
<div className="w-full max-w-2xl">
<h1 className="text-3xl font-bold text-gray-900 mb-8 text-center">Ready2Blob</h1>
<StepIndicator />
<div className="mt-8">{CurrentStep}</div>
</div>
</div>
);
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Define MD3 color tokens and flash prevention script</name>
<files>src/index.css, index.html</files>
<action>
**src/index.css** — Replace the single `@import "tailwindcss"` line with the complete token system. Structure (in this exact order):
1. `@import "tailwindcss";`
2. `@custom-variant dark (&:where(.dark, .dark *));` — immediately after import, before any @layer or @theme
3. `@layer base` block with `:root` (light values) and `.dark` (dark values) for all tokens:
- `--r2b-primary`: light #4338CA, dark #A5B4FC
- `--r2b-on-primary`: light #FFFFFF, dark #1E1B4B
- `--r2b-surface`: light #F9FAFB, dark #111827
- `--r2b-on-surface`: light #111827, dark #F9FAFB
- `--r2b-surface-variant`: light #1F2937, dark #0F172A (code blocks)
- `--r2b-on-surface-variant`: light #F3F4F6, dark #E5E7EB
- `--r2b-surface-container`: light #FFFFFF, dark #1F2937 (cards/forms)
- `--r2b-on-surface-container`: light #374151, dark #D1D5DB
- `--r2b-outline`: light #D1D5DB, dark #4B5563
- `--r2b-error`: light #EF4444, dark #FCA5A5
- `--r2b-on-error`: light #FFFFFF, dark #7F1D1D
- Also add warning/success tokens per research recommendation:
- `--r2b-success`: light #15803D (green-700), dark #86EFAC (green-300)
- `--r2b-on-success`: light #F0FDF4 (green-50), dark #14532D (green-900)
- `--r2b-warning`: light #D97706 (amber-600), dark #FCD34D (amber-300)
- `--r2b-on-warning`: light #FFFBEB (amber-50), dark #78350F (amber-900)
Also add `body` transition in `@layer base`: `transition: background-color 200ms ease, color 200ms ease, border-color 200ms ease` with `@media (prefers-reduced-motion: reduce)` wrapper that sets `transition: none`.
4. `@theme` block mapping each `--color-*` to `var(--r2b-*)` for all 15 tokens (the 11 core + warning, on-warning, success, on-success).
**index.html** — Add inline blocking script inside `<head>`, BEFORE any stylesheet or script tags. The script:
1. Reads `localStorage.getItem('r2b-theme')`
2. If `'dark'` -> effective = dark. If `'light'` -> effective = light. Otherwise -> check `window.matchMedia('(prefers-color-scheme: dark)').matches`
3. If effective is dark, add `.dark` to `document.documentElement.classList`
4. Wrap in try/catch for private browsing mode safety
Use vanilla JS (no arrow functions) for maximum browser compat in the inline script. Use the exact pattern from 08-RESEARCH.md Pattern 2.
</action>
<verify>
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npx vite build 2>&1 | tail -5</automated>
</verify>
<done>src/index.css contains @custom-variant, @layer base with :root and .dark blocks (15 token pairs), @theme mapping all tokens. index.html has inline blocking script reading r2b-theme from localStorage. Vite build succeeds with no errors.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Create ThemeToggle component with tests and wire into App.tsx</name>
<files>src/components/ui/ThemeToggle.tsx, src/components/ui/ThemeToggle.test.tsx, src/App.tsx</files>
<behavior>
- ThemeToggle renders three buttons: Light, Dark, System
- Each button has role="button" and aria-pressed reflecting current selection
- The component group has aria-label="Theme"
- Default selection is "system" when localStorage is empty
- Clicking "Dark" adds .dark class to document.documentElement
- Clicking "Light" removes .dark class from document.documentElement
- Clicking any option writes the value to localStorage key "r2b-theme"
- Active button uses bg-primary text-on-primary classes
- Inactive buttons use bg-surface-container text-on-surface-container classes
</behavior>
<action>
**ThemeToggle.test.tsx** (RED first):
Write tests using Vitest + jsdom (add `// @vitest-environment jsdom` pragma). Test:
1. Renders three buttons with text containing "Light", "Dark", "System"
2. Default state is "System" (aria-pressed="true" on System button) when localStorage is empty
3. Clicking "Dark" -> `document.documentElement.classList.contains('dark')` is true
4. Clicking "Light" -> `document.documentElement.classList.contains('dark')` is false
5. Clicking "Dark" -> `localStorage.getItem('r2b-theme')` === `'dark'`
6. Active button has aria-pressed="true", others have aria-pressed="false"
Run tests — they must FAIL (RED).
**ThemeToggle.tsx** (GREEN):
Create the component following 08-RESEARCH.md Pattern 3:
- Type `ThemeValue = 'light' | 'dark' | 'system'`
- `STORAGE_KEY = 'r2b-theme'`
- `getStored()` reads from localStorage, returns ThemeValue (default 'system')
- `applyTheme(value)` toggles `.dark` on `document.documentElement` and writes to localStorage
- `useState<ThemeValue>(getStored)` for lazy init
- Render as a segmented control: `<div role="group" aria-label="Theme">` with three buttons
- Each button: `type="button"`, `aria-pressed={theme === v}`, `onClick={() => select(v)}`
- Active state: `bg-primary text-on-primary font-medium`
- Inactive state: `bg-surface-container text-on-surface-container hover:bg-surface`
- Icons: Sun for Light, Moon for Dark, Monitor for System (use Unicode or simple SVG inline icons)
- Outer div: `flex rounded-md border border-outline overflow-hidden text-sm`
Run tests — they must PASS (GREEN).
**App.tsx** updates:
1. Add `import { ThemeToggle } from './components/ui/ThemeToggle';`
2. Change the header from centered h1 to a flex row: `<div className="flex items-center justify-between mb-8">`
3. h1 keeps `text-3xl font-bold` but change `text-gray-900` to `text-on-surface`, remove `text-center`
4. Add `<ThemeToggle />` as second child in the flex row
5. Change outer div `bg-gray-50` to `bg-surface`
6. Change h1 `text-gray-900` to `text-on-surface`
Run full test suite to confirm no regressions.
</action>
<verify>
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npm test 2>&1 | tail -20</automated>
</verify>
<done>ThemeToggle.test.tsx has 6+ passing tests. ThemeToggle renders segmented control with Light/Dark/System. App.tsx header shows title left and ThemeToggle right. All existing tests still pass. App outer div uses bg-surface, h1 uses text-on-surface.</done>
</task>
</tasks>
<verification>
1. `npx vite build` completes without errors
2. `npm test` — all tests pass (existing + new ThemeToggle tests)
3. `grep -c "@theme" src/index.css` returns 1 (theme block exists)
4. `grep -c "r2b-theme" index.html` returns at least 1 (inline script present)
5. `grep "ThemeToggle" src/App.tsx` confirms component is wired in
</verification>
<success_criteria>
- Token system produces working Tailwind utility classes: bg-surface, text-on-surface, bg-primary, etc.
- Dark mode toggle works: clicking Dark applies .dark class, Light removes it, System checks OS preference
- Flash prevention: inline script in index.html reads localStorage before CSS paints
- ThemeToggle has passing unit tests covering all three modes and DOM class toggling
- No regression in existing test suite
</success_criteria>
<output>
After completion, create `.planning/phases/08-theme-foundation/08-01-SUMMARY.md`
</output>
@@ -0,0 +1,262 @@
---
phase: 08-theme-foundation
plan: 02
type: execute
wave: 2
depends_on:
- "08-01"
files_modified:
- src/components/ui/BackendCard.tsx
- src/components/ui/FieldRenderer.tsx
- src/components/ui/PasswordField.tsx
- src/components/wizard/AzureAuthToggle.tsx
- src/components/wizard/SftpAuthToggle.tsx
- src/components/wizard/DeploymentStep.tsx
- src/components/wizard/OutputBlock.tsx
- src/components/wizard/RemoteConfigStep.tsx
- src/components/wizard/ReviewStep.tsx
autonomous: false
requirements:
- THEME-01
- THEME-02
must_haves:
truths:
- "No hardcoded Tailwind color classes (gray-*, blue-*, red-*, green-*, yellow-*) remain in any of the 10 migrated files (App.tsx already done in Plan 01)"
- "All components render correctly using semantic token classes (bg-surface, text-on-surface, bg-primary, etc.)"
- "Components look correct in both light and dark mode without any dark: prefixes"
- "All existing tests pass without modification (tests use semantic queries, not class selectors)"
artifacts:
- path: "src/components/ui/BackendCard.tsx"
provides: "Migrated backend selection card"
min_lines: 15
- path: "src/components/ui/FieldRenderer.tsx"
provides: "Migrated form field renderer"
min_lines: 50
- path: "src/components/ui/PasswordField.tsx"
provides: "Migrated password input"
min_lines: 30
- path: "src/components/wizard/AzureAuthToggle.tsx"
provides: "Migrated Azure auth method toggle"
min_lines: 20
- path: "src/components/wizard/SftpAuthToggle.tsx"
provides: "Migrated SFTP auth method toggle"
min_lines: 25
- path: "src/components/wizard/DeploymentStep.tsx"
provides: "Migrated deployment step"
min_lines: 50
- path: "src/components/wizard/OutputBlock.tsx"
provides: "Migrated code output block"
min_lines: 20
- path: "src/components/wizard/RemoteConfigStep.tsx"
provides: "Migrated remote config step"
min_lines: 50
- path: "src/components/wizard/ReviewStep.tsx"
provides: "Migrated review step with success/warning tokens"
min_lines: 60
key_links:
- from: "src/index.css"
to: "all 9 migrated component files"
via: "CSS variable cascade through Tailwind utility classes"
pattern: "bg-surface|text-on-surface|border-outline|bg-primary|text-on-primary"
---
<objective>
Migrate all 63 hardcoded Tailwind color classes across 9 component files to semantic MD3 token classes.
Purpose: Complete THEME-01 by eliminating all hardcoded color references. After this plan, the entire UI responds to theme changes via the CSS variable cascade established in Plan 01.
Output: 9 migrated component files with zero hardcoded color classes, all tests passing.
</objective>
<execution_context>
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/08-theme-foundation/08-CONTEXT.md
@.planning/phases/08-theme-foundation/08-RESEARCH.md
@.planning/phases/08-theme-foundation/08-01-SUMMARY.md
<interfaces>
<!-- Token utility classes available after Plan 01 (from @theme in src/index.css): -->
<!-- bg-surface, text-on-surface, bg-surface-variant, text-on-surface-variant -->
<!-- bg-surface-container, text-on-surface-container -->
<!-- bg-primary, text-on-primary, text-primary, border-primary -->
<!-- border-outline, bg-error, text-error, text-on-error -->
<!-- bg-success, text-success, text-on-success -->
<!-- bg-warning, text-warning, text-on-warning -->
<!-- Opacity modifiers work: text-on-surface-container/70, border-primary/30, etc. -->
</interfaces>
<!-- Migration mapping reference (from 08-RESEARCH.md Pattern 4): -->
<!--
bg-gray-50 -> bg-surface
bg-white -> bg-surface-container
bg-gray-900 -> bg-surface-variant (code blocks)
text-gray-900 -> text-on-surface
text-gray-700 -> text-on-surface-container
text-gray-500 -> text-on-surface-container/70
text-gray-400 -> text-on-surface-container/50
text-gray-100 -> text-on-surface-variant
border-gray-300 -> border-outline
border-gray-200 -> border-outline
hover:bg-gray-50 -> hover:bg-surface
hover:border-blue-400 -> hover:border-primary
bg-blue-600 -> bg-primary
bg-blue-50 -> bg-primary/10
text-white (on blue bg) -> text-on-primary
text-blue-600/500/700 -> text-primary
hover:bg-blue-700 -> hover:bg-primary
border-blue-600 -> border-primary
border-blue-200 -> border-primary/30
text-blue-700 bg-blue-50 border-blue-200 -> text-primary bg-primary/10 border-primary/30
border-red-500 -> border-error
focus:ring-red-300 -> focus:ring-error/50
text-red-600 -> text-error
text-red-500 -> text-error
focus:ring-blue-300 -> focus:ring-primary/50
text-green-700 bg-green-50 -> text-success bg-on-success (or bg-success/10)
bg-yellow-50 border-yellow-300 text-yellow-800/900 -> bg-on-warning border-warning text-warning
-->
</context>
<tasks>
<task type="auto">
<name>Task 1: Migrate UI primitives (BackendCard, FieldRenderer, PasswordField)</name>
<files>src/components/ui/BackendCard.tsx, src/components/ui/FieldRenderer.tsx, src/components/ui/PasswordField.tsx</files>
<action>
Migrate all hardcoded color classes to semantic tokens. This is mechanical find-and-replace following the mapping. Do NOT change any DOM structure, only className strings.
**BackendCard.tsx** (5 occurrences):
- `border-blue-600 bg-blue-50` (selected) -> `border-primary bg-primary/10`
- `border-gray-200 bg-white hover:border-blue-400 hover:bg-gray-50` (unselected) -> `border-outline bg-surface-container hover:border-primary hover:bg-surface`
- `text-gray-900` -> `text-on-surface`
- `text-gray-500` -> `text-on-surface-container/70`
**FieldRenderer.tsx** (17 occurrences):
- `text-gray-700` (labels) -> `text-on-surface-container`
- `text-red-500` (required asterisk) -> `text-error`
- `text-blue-500 hover:text-blue-700` (toggle link) -> `text-primary hover:text-primary`
- `text-blue-700 bg-blue-50 border border-blue-200` (tooltip/info) -> `text-primary bg-primary/10 border border-primary/30`
- `border-red-500 focus:ring-red-300` (error state) -> `border-error focus:ring-error/50`
- `border-gray-300 focus:ring-blue-300` (normal state) -> `border-outline focus:ring-primary/50`
- `text-gray-500` (helpText) -> `text-on-surface-container/70`
- `text-red-600` (error message) -> `text-error`
- Apply the same mapping to both the text-input branch and the select branch (they have identical color patterns)
**PasswordField.tsx** (8 occurrences):
- `text-gray-700` -> `text-on-surface-container`
- `text-blue-500 hover:text-blue-700` -> `text-primary hover:text-primary`
- `text-blue-700 bg-blue-50 border border-blue-200` -> `text-primary bg-primary/10 border border-primary/30`
- `border-red-500 focus:ring-red-300` -> `border-error focus:ring-error/50`
- `border-gray-300 focus:ring-blue-300` -> `border-outline focus:ring-primary/50`
- `text-gray-400 hover:text-gray-700` (eye icon) -> `text-on-surface-container/50 hover:text-on-surface-container`
- `text-gray-500` (helpText) -> `text-on-surface-container/70`
- `text-red-600` (error message) -> `text-error`
Run `npm test` after each file to catch any regressions immediately.
</action>
<verify>
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npm test 2>&1 | tail -20</automated>
</verify>
<done>BackendCard.tsx has 0 hardcoded color classes. FieldRenderer.tsx has 0 hardcoded color classes. PasswordField.tsx has 0 hardcoded color classes. All tests pass.</done>
</task>
<task type="auto">
<name>Task 2: Migrate wizard components (toggles, steps, output block)</name>
<files>src/components/wizard/AzureAuthToggle.tsx, src/components/wizard/SftpAuthToggle.tsx, src/components/wizard/DeploymentStep.tsx, src/components/wizard/OutputBlock.tsx, src/components/wizard/RemoteConfigStep.tsx, src/components/wizard/ReviewStep.tsx</files>
<action>
Continue the mechanical migration for all wizard components. Do NOT change DOM structure, only className strings.
**AzureAuthToggle.tsx** (7 occurrences):
- `border-gray-300` -> `border-outline`
- `bg-blue-600 text-white` (active tab) -> `bg-primary text-on-primary`
- `bg-white text-gray-700 hover:bg-gray-50` (inactive tab) -> `bg-surface-container text-on-surface-container hover:bg-surface`
**SftpAuthToggle.tsx** (10 occurrences):
- `text-gray-700` (label) -> `text-on-surface-container`
- `text-blue-500 hover:text-blue-700` (toggle link) -> `text-primary hover:text-primary`
- `text-blue-700 bg-blue-50 border border-blue-200` (info) -> `text-primary bg-primary/10 border border-primary/30`
- `border-gray-300` -> `border-outline`
- `bg-blue-600 text-white` (active) -> `bg-primary text-on-primary`
- `bg-white text-gray-700 hover:bg-gray-50` (inactive) -> `bg-surface-container text-on-surface-container hover:bg-surface`
**DeploymentStep.tsx** (2 occurrences):
- `border border-gray-300 ... hover:bg-gray-50` (Back button) -> `border border-outline ... hover:bg-surface`
- `bg-blue-600 text-white ... hover:bg-blue-700` (Next button) -> `bg-primary text-on-primary ... hover:bg-primary`
**OutputBlock.tsx** (4 occurrences):
- `text-gray-700` (label) -> `text-on-surface-container`
- `border-gray-300` (action buttons, 2 occurrences) -> `border-outline`
- `bg-gray-900 text-gray-100` (code pre) -> `bg-surface-variant text-on-surface-variant`
**RemoteConfigStep.tsx** (2 occurrences):
- `border border-gray-300 ... hover:bg-gray-50` (Back button) -> `border border-outline ... hover:bg-surface`
- `bg-blue-600 text-white ... hover:bg-blue-700` (Next button) -> `bg-primary text-on-primary ... hover:bg-primary`
**ReviewStep.tsx** (6 occurrences):
- `text-green-700 bg-green-50` (client-side notice) -> `text-success bg-on-success`
- `bg-yellow-50 border border-yellow-300` (security warning box) -> `bg-on-warning border border-warning`
- `text-yellow-800` (warning title) -> `text-warning`
- `text-yellow-900` (warning label) -> `text-warning`
- `border border-gray-300 ... hover:bg-gray-50` (Back button) -> `border border-outline ... hover:bg-surface`
- `bg-blue-600 text-white ... hover:bg-blue-700` (Download button) -> `bg-primary text-on-primary ... hover:bg-primary`
After all files migrated, run verification grep to confirm zero hardcoded color classes remain.
</action>
<verify>
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npm test 2>&1 | tail -20 && echo "---GREP CHECK---" && grep -rn "text-gray-\|bg-gray-\|border-gray-\|bg-blue-\|text-blue-\|border-blue-\|text-white\|bg-white\|text-red-\|border-red-\|text-green-\|bg-green-\|bg-yellow-\|text-yellow-\|border-yellow-\|hover:bg-gray-\|hover:bg-blue-\|hover:border-blue-\|focus:ring-red-\|focus:ring-blue-" src/components/ src/App.tsx 2>/dev/null; echo "Exit: $?"</automated>
</verify>
<done>All 9 component files migrated. Grep for hardcoded color classes in src/components/ and src/App.tsx returns 0 matches. All existing tests pass without modification.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 3: Visual verification of theme system across all wizard steps</name>
<files>none</files>
<action>
Present the completed theme system for user visual verification. Start the dev server if not already running.
</action>
<what-built>Complete MD3 token system with dark mode toggle and all 63 hardcoded color classes migrated to semantic tokens across 10 files.</what-built>
<how-to-verify>
1. Run `npm run dev` and open http://localhost:5173
2. Verify the app renders correctly in light mode (indigo primary accents, gray surfaces)
3. Click "Dark" in the theme toggle (top-right, next to "Ready2Blob" title) -- entire UI should switch to dark palette instantly
4. Click "Light" -- UI returns to light palette
5. Click "System" -- follows your OS preference
6. With "Dark" selected, hard-reload the page (Ctrl+Shift+R) -- should load directly in dark mode with NO flash of light theme
7. Navigate through all 4 wizard steps checking:
- Backend cards have correct border/background colors in both themes
- Form inputs have visible borders and labels in both themes
- Code output blocks (ReviewStep) are readable in both themes
- Warning/success notices in ReviewStep are visible in both themes
- All buttons (Back/Next/Download) are visible and styled in both themes
</how-to-verify>
<verify>User confirms visual correctness</verify>
<done>User approves visual appearance of both light and dark themes across all wizard steps.</done>
<resume-signal>Type "approved" or describe any visual issues</resume-signal>
</task>
</tasks>
<verification>
1. `npm test` -- all tests pass (existing + ThemeToggle tests from Plan 01)
2. `grep -rn "text-gray-\|bg-gray-\|border-gray-\|bg-blue-\|text-blue-\|border-blue-\|text-white\|bg-white\|text-red-\|border-red-\|text-green-\|bg-green-\|bg-yellow-\|text-yellow-\|border-yellow-" src/components/ src/App.tsx` returns 0 matches
3. `npx vite build` completes without errors
</verification>
<success_criteria>
- Zero hardcoded Tailwind color classes remain in src/components/ and src/App.tsx
- All 9 component files use only semantic token classes (bg-surface, text-on-surface, bg-primary, etc.)
- No `dark:` utility prefixes used anywhere -- the CSS variable cascade handles both themes
- All existing tests pass without modification
- Visual verification confirms both light and dark mode render correctly across all wizard steps
</success_criteria>
<output>
After completion, create `.planning/phases/08-theme-foundation/08-02-SUMMARY.md`
</output>