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:
@@ -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>
|
||||
Reference in New Issue
Block a user