---
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)"
---
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.
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
@.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
From src/components/ui/FieldRenderer.tsx:
```typescript
interface FieldRendererProps {
field: FieldDef;
register: UseFormRegister;
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;
}
```
Task 1: Create TextFieldMD3 component with floating label + MD3 button constantssrc/components/ui/TextFieldMD3.tsx, src/components/ui/TextFieldMD3.test.tsx, src/styles/md3-buttons.ts
- 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
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).
npx vitest run src/components/ui/TextFieldMD3.test.tsx --reporter=dotTextFieldMD3 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.Task 2: Fix DEBT-01 aria-label inconsistency in FieldRenderer + add testssrc/components/ui/FieldRenderer.tsx, src/components/ui/FieldRenderer.test.tsx
- 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 })
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 `......` 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):
// After:
```
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.
npx vitest run src/components/ui/FieldRenderer.test.tsx --reporter=dot && npx vitest run --reporter=dotBoth 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.
- `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
- 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