Files
Ready2Blob/.planning/phases/09-md3-components/09-01-PLAN.md
T
2026-04-01 09:14:15 +02:00

205 lines
9.1 KiB
Markdown

---
phase: 09-md3-components
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/components/ui/TextFieldMD3.tsx
- src/components/ui/TextFieldMD3.test.tsx
- src/styles/md3-buttons.ts
- src/components/ui/FieldRenderer.tsx
- src/components/ui/FieldRenderer.test.tsx
autonomous: true
requirements: [COMP-01, COMP-02, DEBT-01]
must_haves:
truths:
- "TextFieldMD3 renders an outlined input with a floating label that floats on focus"
- "TextFieldMD3 floating label floats when the field has a value (not just on focus)"
- "TextFieldMD3 preserves htmlFor/id pairing so getByLabelText queries work"
- "MD3 button class constants exist for filled, outlined, and text variants"
- "FieldRenderer text-branch tooltip button uses aria-label instead of sr-only span"
artifacts:
- path: "src/components/ui/TextFieldMD3.tsx"
provides: "MD3 outlined text field with CSS floating label"
exports: ["TextFieldMD3"]
- path: "src/components/ui/TextFieldMD3.test.tsx"
provides: "Unit tests for floating label behavior"
min_lines: 30
- path: "src/styles/md3-buttons.ts"
provides: "MD3 button class constants"
exports: ["MD3_BTN_FILLED", "MD3_BTN_OUTLINED", "MD3_BTN_TEXT"]
- path: "src/components/ui/FieldRenderer.test.tsx"
provides: "Tests for DEBT-01 aria-label consistency"
min_lines: 20
key_links:
- from: "src/components/ui/TextFieldMD3.tsx"
to: "react-hook-form"
via: "registration prop spread onto input"
pattern: "\\.\\.\\.(registration|register)"
---
<objective>
Create MD3 UI primitives: TextFieldMD3 component with CSS-only floating label, MD3 button class constants, and fix FieldRenderer DEBT-01 aria-label inconsistency.
Purpose: Establish the reusable building blocks that Plan 03 will wire into all wizard steps. TextFieldMD3 is the most complex new component and needs its own test coverage before integration.
Output: TextFieldMD3.tsx + tests, md3-buttons.ts constants, FieldRenderer aria-label fix + 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/ROADMAP.md
@.planning/STATE.md
@.planning/phases/09-md3-components/09-RESEARCH.md
@src/index.css
@src/components/ui/FieldRenderer.tsx
@src/components/ui/PasswordField.tsx
<interfaces>
<!-- FieldRenderer and PasswordField both use react-hook-form registration -->
From src/components/ui/FieldRenderer.tsx:
```typescript
interface FieldRendererProps {
field: FieldDef;
register: UseFormRegister<any>;
error?: FieldError;
}
```
From src/components/ui/PasswordField.tsx:
```typescript
interface PasswordFieldProps {
id: string;
label: string;
error?: FieldError;
registration: UseFormRegisterReturn;
placeholder?: string;
helpText?: string;
tooltipText?: string;
}
```
From src/schemas/registry.ts (FieldDef shape):
```typescript
interface FieldDef {
key: string;
label: string;
required?: boolean;
inputType?: 'text' | 'password' | 'select';
options?: { value: string; label: string }[];
placeholder?: string;
helpText?: string;
tooltipText?: string;
}
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Create TextFieldMD3 component with floating label + MD3 button constants</name>
<files>src/components/ui/TextFieldMD3.tsx, src/components/ui/TextFieldMD3.test.tsx, src/styles/md3-buttons.ts</files>
<behavior>
- Test 1: TextFieldMD3 renders an input with the given id and a label with matching htmlFor
- Test 2: screen.getByLabelText(label) finds the input (htmlFor/id pairing works)
- Test 3: Input receives registration props (can be queried after typing)
- Test 4: Error message renders with role="alert" when error prop is passed
- Test 5: Help text renders when helpText prop is passed and no error
- Test 6: Required asterisk renders when required=true
- Test 7: Suffix slot renders (for PasswordField show/hide button)
- Test 8: Input has placeholder=" " for CSS floating label trick
</behavior>
<action>
Create `src/components/ui/TextFieldMD3.tsx` implementing the CSS-only floating label pattern from the research doc (Pattern 1). Key details:
- Interface: `{ id, label, error?, registration, type?, helpText?, required?, suffix? }` where registration is `UseFormRegisterReturn`
- Input gets `placeholder=" "` for `:placeholder-shown` CSS detection
- Label is absolutely positioned, uses Tailwind `peer` utilities to float on focus and when input has value
- Use `peer-[:not(:placeholder-shown)]:` arbitrary variant for has-value state (Tailwind v4 supports this)
- All colors use semantic tokens: `border-outline`, `focus:border-primary`, `text-on-surface-container/60`, `text-error`
- Error state: `border-error` + error message with `role="alert"`
- Suffix slot: absolutely positioned right side (for password show/hide toggle)
- Spread `{...registration}` onto the input element
Create `src/styles/md3-buttons.ts` exporting three constants:
- `MD3_BTN_FILLED`: `px-6 py-2.5 rounded-full bg-primary text-on-primary text-sm font-medium hover:opacity-90 active:opacity-80 transition-opacity disabled:opacity-40 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50`
- `MD3_BTN_OUTLINED`: `px-6 py-2.5 rounded-full border border-outline text-on-surface text-sm font-medium hover:bg-primary/8 active:bg-primary/12 transition-colors disabled:opacity-40 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50`
- `MD3_BTN_TEXT`: `px-4 py-2.5 rounded-full text-primary text-sm font-medium hover:bg-primary/8 active:bg-primary/12 transition-colors disabled:opacity-40 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50`
Write tests FIRST (RED), then implement (GREEN).
</action>
<verify>
<automated>npx vitest run src/components/ui/TextFieldMD3.test.tsx --reporter=dot</automated>
</verify>
<done>TextFieldMD3 component renders floating label with proper htmlFor/id pairing, getByLabelText works, error/helpText/suffix/required all render correctly. MD3 button constants exported. All tests pass.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Fix DEBT-01 aria-label inconsistency in FieldRenderer + add tests</name>
<files>src/components/ui/FieldRenderer.tsx, src/components/ui/FieldRenderer.test.tsx</files>
<behavior>
- Test 1: Text-branch tooltip button has aria-label="More info about {field.label}" (not sr-only span)
- Test 2: Select-branch tooltip button has aria-label="More info about {field.label}"
- Test 3: Both branches' tooltip buttons are findable via getByRole('button', { name: /more info about/i })
</behavior>
<action>
Create `src/components/ui/FieldRenderer.test.tsx` with tests for DEBT-01 aria-label consistency. Tests render FieldRenderer with a mock field that has tooltipText, and verify the tooltip button uses `aria-label` attribute (not sr-only span) for both text-branch and select-branch.
Then fix FieldRenderer.tsx text-branch (lines 91-99): Replace the `<span className="sr-only">...</span><span aria-hidden="true">...</span>` pattern with a direct `aria-label={...}` on the button and plain `i` icon content, matching the select-branch pattern (lines 49-54).
The fix is exactly:
```tsx
// Before (text branch, ~line 91):
<button type="button" onClick={...} className="...">
<span className="sr-only">More info about {field.label}</span>
<span aria-hidden="true">ⓘ</span>
</button>
// After:
<button
type="button"
onClick={...}
aria-label={`More info about ${field.label}`}
className="..."
>
</button>
```
Also apply the same fix to PasswordField.tsx (lines 27-31) which has the identical sr-only pattern.
Run full test suite after to confirm no regressions.
</action>
<verify>
<automated>npx vitest run src/components/ui/FieldRenderer.test.tsx --reporter=dot && npx vitest run --reporter=dot</automated>
</verify>
<done>Both text-branch and select-branch tooltip buttons use aria-label attribute consistently. PasswordField tooltip also fixed. All 166+ existing tests still pass. DEBT-01 resolved.</done>
</task>
</tasks>
<verification>
- `npx vitest run --reporter=dot` — all existing tests pass (no regressions)
- `npx vitest run src/components/ui/TextFieldMD3.test.tsx` — new component tests pass
- `npx vitest run src/components/ui/FieldRenderer.test.tsx` — DEBT-01 tests pass
- `src/styles/md3-buttons.ts` exports MD3_BTN_FILLED, MD3_BTN_OUTLINED, MD3_BTN_TEXT
</verification>
<success_criteria>
- TextFieldMD3 component exists with floating label, proper htmlFor/id, and all tests passing
- MD3 button constants exported from src/styles/md3-buttons.ts
- FieldRenderer and PasswordField tooltip buttons use consistent aria-label (DEBT-01 complete)
- Zero test regressions across the full suite
</success_criteria>
<output>
After completion, create `.planning/phases/09-md3-components/09-01-SUMMARY.md`
</output>