diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 0271361..555db4f 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -98,7 +98,10 @@ Plans: 2. Every interactive element shows a visible 3px MD3 focus outline when navigated via keyboard (using `focus-visible`) 3. Step transitions use a subtle fade/slide animation that is suppressed when the user has `prefers-reduced-motion` enabled 4. When form validation fails, the view auto-scrolls to the first field with an error -**Plans**: TBD +**Plans:** 2 plans +Plans: +- [ ] 11-01-PLAN.md — Mobile responsive layout + MD3 focus-visible indicators +- [ ] 11-02-PLAN.md — Step transition animation + auto-scroll to first error ## Progress @@ -113,5 +116,5 @@ Plans: | 7. Validation & UX Polish | v1.1 | 3/3 | Complete | 2026-03-31 | | 8. Theme Foundation | v1.2 | 2/2 | Complete | 2026-04-01 | | 9. MD3 Components | v1.2 | 5/5 | Complete | 2026-04-01 | -| 10. Content & Clarity | 3/3 | Complete | 2026-04-01 | - | -| 11. Polish & Responsiveness | v1.2 | 0/? | Not started | - | +| 10. Content & Clarity | v1.2 | 3/3 | Complete | 2026-04-01 | +| 11. Polish & Responsiveness | v1.2 | 0/2 | Not started | - | diff --git a/.planning/phases/11-polish-responsiveness/11-01-PLAN.md b/.planning/phases/11-polish-responsiveness/11-01-PLAN.md new file mode 100644 index 0000000..743835d --- /dev/null +++ b/.planning/phases/11-polish-responsiveness/11-01-PLAN.md @@ -0,0 +1,211 @@ +--- +phase: 11-polish-responsiveness +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/styles/md3-buttons.ts + - src/components/ui/BackendCard.tsx + - src/components/ui/ThemeToggle.tsx + - src/components/ui/FieldRenderer.tsx + - src/components/wizard/BackendSelectionStep.tsx + - src/components/wizard/RemoteConfigStep.tsx + - src/components/wizard/DeploymentStep.tsx + - src/components/wizard/ReviewStep.tsx + - src/components/wizard/StepIndicator.tsx +autonomous: true +requirements: + - POLISH-01 + - POLISH-02 + +must_haves: + truths: + - "On mobile viewport (<640px), backend cards stack vertically in a single column" + - "On mobile viewport (<640px), navigation buttons stretch to full width and stack vertically" + - "On mobile viewport (<640px), StepIndicator hides label text but keeps circles and connectors" + - "On desktop viewport (>=640px), backend cards display in a 2-column grid" + - "Every button and interactive card shows a 3px MD3 focus ring when keyboard-navigated" + - "Focus ring does not appear on mouse click (uses focus-visible, not focus)" + artifacts: + - path: "src/styles/md3-buttons.ts" + provides: "MD3 button constants with focus-visible:ring-3" + contains: "focus-visible:ring-3" + - path: "src/components/ui/BackendCard.tsx" + provides: "Responsive full-width card with focus-visible ring" + contains: "focus-visible:ring-3" + - path: "src/components/wizard/StepIndicator.tsx" + provides: "Collapsed labels on mobile, upgraded focus ring" + contains: "hidden sm:block" + - path: "src/components/wizard/BackendSelectionStep.tsx" + provides: "Responsive card grid and button row" + contains: "grid grid-cols-1 sm:grid-cols-2" + key_links: + - from: "src/styles/md3-buttons.ts" + to: "all step components" + via: "MD3_BTN_FILLED / MD3_BTN_OUTLINED constants" + pattern: "focus-visible:ring-3" + - from: "src/components/ui/BackendCard.tsx" + to: "BackendSelectionStep.tsx" + via: "component usage" + pattern: "w-full.*focus-visible:ring-3" +--- + + +Make the wizard layout mobile-responsive and add MD3 focus-visible indicators to all interactive elements. + +Purpose: POLISH-01 ensures the wizard is usable on mobile devices (cards stack, buttons stretch, step labels collapse). POLISH-02 ensures keyboard navigability with visible 3px MD3 focus rings on all interactive elements. +Output: All step components, BackendCard, StepIndicator, ThemeToggle, and FieldRenderer updated with responsive classes and focus-visible rings. + + + +@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/11-polish-responsiveness/11-RESEARCH.md + + + + +From src/styles/md3-buttons.ts: +```typescript +export const MD3_BTN_FILLED = '...' // contains focus-visible:ring-2 focus-visible:ring-primary/50 — upgrade to ring-3 +export const MD3_BTN_OUTLINED = '...' // same pattern +export const MD3_BTN_TEXT = '...' // same pattern +``` + +From src/components/ui/BackendCard.tsx: +```typescript +// className array does NOT have focus-visible — must add +// className does NOT have w-full — must add for grid cell fill +export function BackendCard({ name, description, selected, onClick, ...rest }: BackendCardProps) +``` + +From src/components/wizard/StepIndicator.tsx: +```typescript +// Three label spans (completed, active, future) need hidden sm:block +// Completed step button: group-focus-visible:ring-2 → upgrade to ring-3 +``` + +From src/components/ui/ThemeToggle.tsx: +```typescript +// Buttons have NO focus-visible classes — must add +// className is conditional (selected vs not) — add focus-visible to both branches +``` + +From src/components/ui/FieldRenderer.tsx: +```typescript +// Select element: focus:ring-2 → change to focus-visible:ring-2 (inputs keep focus: per convention) +// Tooltip buttons (2 instances): NO focus-visible — must add +``` + +Button row pattern in all step components: +```tsx +
// → flex flex-col sm:flex-row gap-3 mt-6 + // → add w-full sm:w-auto + // → add w-full sm:w-auto +
+``` +
+
+ + + + + Task 1: Mobile responsive layout — card grid, button rows, step indicator collapse + src/components/wizard/BackendSelectionStep.tsx, src/components/ui/BackendCard.tsx, src/components/wizard/RemoteConfigStep.tsx, src/components/wizard/DeploymentStep.tsx, src/components/wizard/ReviewStep.tsx, src/components/wizard/StepIndicator.tsx + +POLISH-01: Make wizard layout mobile-responsive using Tailwind v4 mobile-first approach. + +1. **BackendSelectionStep.tsx** — Change the `data-testid="backend-cards"` div from bare div to responsive grid: + - Add className `grid grid-cols-1 sm:grid-cols-2 gap-3 mt-4` to the `data-testid="backend-cards"` div + - Change button row from `flex gap-3 mt-6` to `flex flex-col sm:flex-row gap-3 mt-6` + - Add `w-full sm:w-auto` to the Next button className (append after MD3_BTN_FILLED) + +2. **BackendCard.tsx** — Add `w-full` to the className array so the button fills its grid cell on all viewports. Insert at the start of the className string: `'w-full flex flex-col items-start...'` + +3. **RemoteConfigStep.tsx** — Change button row div from `flex gap-3 mt-6` to `flex flex-col sm:flex-row gap-3 mt-6`. Add `w-full sm:w-auto` to both Back and Next button classNames (use template literal: `` `${MD3_BTN_OUTLINED} w-full sm:w-auto` ``). + +4. **DeploymentStep.tsx** — Same button row changes as RemoteConfigStep: `flex flex-col sm:flex-row gap-3 mt-6` on the wrapper div, `w-full sm:w-auto` on both buttons. + +5. **ReviewStep.tsx** — Change button row div from `flex gap-3 mt-6` to `flex flex-col sm:flex-row gap-3 mt-6`. Add `w-full sm:w-auto` to the Back button. The "Download All (ZIP)" button already has `w-full` — leave as-is. + +6. **StepIndicator.tsx** — Add `hidden sm:block` to all three label `` elements: + - Completed step label: `className="text-xs text-primary hidden sm:block"` + - Active step label: `className="text-xs text-primary font-semibold hidden sm:block"` + - Future step label: `className="text-xs text-on-surface-container/40 hidden sm:block"` + +Do NOT change any DOM structure beyond adding/modifying className strings. Do NOT add wrapper elements. The `data-testid="backend-cards"` div already exists — just add classes to it. + + + npx vitest run + + Backend cards use responsive grid (1-col mobile, 2-col desktop). All step button rows stack vertically on mobile and go horizontal on desktop. StepIndicator labels hidden on mobile, visible on desktop. All existing tests pass. + + + + Task 2: MD3 focus-visible indicators on all interactive elements + src/styles/md3-buttons.ts, src/components/ui/BackendCard.tsx, src/components/wizard/StepIndicator.tsx, src/components/ui/ThemeToggle.tsx, src/components/ui/FieldRenderer.tsx + +POLISH-02: Upgrade all interactive elements to 3px MD3 focus-visible rings for keyboard navigation. + +1. **md3-buttons.ts** — In all three constants (MD3_BTN_FILLED, MD3_BTN_OUTLINED, MD3_BTN_TEXT), replace: + `focus-visible:ring-2 focus-visible:ring-primary/50` + with: + `focus-visible:ring-3 focus-visible:ring-primary` + Keep `focus-visible:outline-none` as-is. + +2. **BackendCard.tsx** — Add focus-visible ring to the button className array. Add this string to the base classes (the first string before the ternary): + `'w-full flex flex-col items-start gap-1 rounded-xl border-2 p-4 text-left transition-all focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-primary'` + Do NOT add `ring-offset-2` (omit ring-offset to avoid dark mode surface color issues per research open question #2). + +3. **StepIndicator.tsx** — On the completed-step circle ``, change: + `group-focus-visible:ring-2 group-focus-visible:ring-primary/50` + to: + `group-focus-visible:ring-3 group-focus-visible:ring-primary` + +4. **ThemeToggle.tsx** — Add focus-visible ring to BOTH branches of the ternary className on each theme button: + - Selected: `'flex-1 px-3 py-1 bg-primary text-on-primary font-medium focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-primary focus-visible:ring-inset'` + - Unselected: `'flex-1 px-3 py-1 bg-surface-container text-on-surface-container hover:bg-surface focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-primary focus-visible:ring-inset'` + Use `ring-inset` because ThemeToggle buttons share a border (overflow-hidden) — outset ring would be clipped. + +5. **FieldRenderer.tsx** — Two changes: + a. **Select element:** Change `focus:ring-2` to `focus-visible:ring-2` in the select className (keep ring-2 for form controls — inputs conventionally show focus on mouse too, but selects should match button behavior). + b. **Tooltip info buttons** (both the select-branch tooltip button and the text-branch tooltip button): Add `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded` to both tooltip button classNames. These are small buttons so ring-2 is appropriate (not ring-3). + +Note on TextFieldMD3: The input element uses `focus:ring-2` which is correct for text inputs (users expect visible focus on mouse click for inputs). Do NOT change TextFieldMD3. + + + npx vitest run + + MD3_BTN_FILLED/OUTLINED/TEXT contain focus-visible:ring-3. BackendCard has focus-visible ring. StepIndicator completed-step has upgraded group-focus-visible:ring-3. ThemeToggle buttons have focus-visible ring. FieldRenderer select uses focus-visible:ring-2, tooltip buttons have focus-visible ring. All tests pass. + + + + + +- `npx vitest run` — all tests green +- Grep for `focus-visible:ring-3` in md3-buttons.ts, BackendCard.tsx, StepIndicator.tsx, ThemeToggle.tsx — all present +- Grep for `grid grid-cols-1 sm:grid-cols-2` in BackendSelectionStep.tsx — present +- Grep for `flex-col sm:flex-row` in all step components — present +- Grep for `hidden sm:block` in StepIndicator.tsx — 3 occurrences (one per label state) + + + +- All step button rows use `flex-col sm:flex-row` (mobile stacks, desktop inline) +- Backend cards container uses `grid grid-cols-1 sm:grid-cols-2` +- BackendCard has `w-full` for grid cell fill +- StepIndicator labels have `hidden sm:block` (3 spans) +- All MD3 button constants use `focus-visible:ring-3 focus-visible:ring-primary` +- BackendCard, ThemeToggle, StepIndicator completed-step, FieldRenderer tooltip buttons all have focus-visible rings +- All existing tests pass (no regressions) + + + +After completion, create `.planning/phases/11-polish-responsiveness/11-01-SUMMARY.md` + diff --git a/.planning/phases/11-polish-responsiveness/11-02-PLAN.md b/.planning/phases/11-polish-responsiveness/11-02-PLAN.md new file mode 100644 index 0000000..91fb998 --- /dev/null +++ b/.planning/phases/11-polish-responsiveness/11-02-PLAN.md @@ -0,0 +1,244 @@ +--- +phase: 11-polish-responsiveness +plan: 02 +type: execute +wave: 2 +depends_on: ["11-01"] +files_modified: + - src/index.css + - src/App.tsx + - src/components/wizard/BackendSelectionStep.tsx + - src/components/wizard/RemoteConfigStep.tsx +autonomous: true +requirements: + - POLISH-03 + - POLISH-04 + +must_haves: + truths: + - "Changing wizard steps plays a subtle fade/slide-up animation on the new step content" + - "Users with prefers-reduced-motion enabled see no animation on step change" + - "When form validation fails, the view auto-scrolls to the first errored field" + - "scrollIntoView uses smooth behavior and centers the errored field" + artifacts: + - path: "src/index.css" + provides: "step-in keyframe animation and reduced-motion guard" + contains: "@keyframes step-in" + - path: "src/App.tsx" + provides: "Step content wrapper with key and animation class" + contains: "animate-step-in" + - path: "src/components/wizard/BackendSelectionStep.tsx" + provides: "Auto-scroll to first error on validation failure" + contains: "scrollIntoView" + - path: "src/components/wizard/RemoteConfigStep.tsx" + provides: "Auto-scroll to first error on validation failure" + contains: "scrollIntoView" + key_links: + - from: "src/index.css" + to: "src/App.tsx" + via: "animate-step-in Tailwind utility from @theme --animate-step-in" + pattern: "animate-step-in" + - from: "src/components/wizard/BackendSelectionStep.tsx" + to: "DOM element" + via: "document.getElementById(firstErrorKey)?.scrollIntoView" + pattern: "scrollIntoView" +--- + + +Add step transition animations and auto-scroll-to-error behavior to the wizard. + +Purpose: POLISH-03 creates visual continuity between wizard steps with a subtle fade/slide animation that respects accessibility preferences. POLISH-04 helps users find validation errors by auto-scrolling to the first errored field when form submission fails. +Output: CSS animation keyframes in index.css, animated step wrapper in App.tsx, scrollIntoView handlers in BackendSelectionStep and RemoteConfigStep. + + + +@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/11-polish-responsiveness/11-RESEARCH.md +@.planning/phases/11-polish-responsiveness/11-01-SUMMARY.md + + + + +From src/index.css (after @theme block, line 53-69): +```css +@theme { + --color-primary: var(--r2b-primary); + /* ... more color tokens ... */ + --color-on-warning: var(--r2b-on-warning); + /* ADD: --animate-step-in: step-in 200ms ease-out both; */ +} +``` + +From src/App.tsx (WizardShell step content wrapper, line 65): +```tsx +
+ {CurrentStep} +
+// CHANGE TO:
+``` + +From src/components/wizard/BackendSelectionStep.tsx: +```typescript +// handleSubmit(onValidSubmit) called in two places: +// 1. form onSubmit={handleSubmit(onValidSubmit)} — line 65 +// 2. void handleSubmit(onValidSubmit)() inside handleCardClick — line 55 +// ADD second arg: handleSubmit(onValidSubmit, onInvalidSubmit) +// The form's onSubmit needs the error handler. handleCardClick does NOT need it +// (card click validates remote name — if invalid, scrolls to remote-name field). + +type RemoteNameFormValues = z.infer; +// Error key will be "name" → document.getElementById("name") won't work +// because TextFieldMD3 uses id="remote-name" (passed as id prop) +// SOLUTION: use the form element to find the first [aria-invalid] or use +// a fixed ID since there's only one field. Simplest: getElementById('remote-name') +// since the only validatable field in this step is the remote name input. +``` + +From src/components/wizard/RemoteConfigStep.tsx: +```typescript +// handleSubmit(onNext) — line 63 +// ADD second arg: handleSubmit(onNext, onInvalidSubmit) +// FieldRenderer passes field.key as id to TextFieldMD3 and select +// So document.getElementById(fieldKey) will find the correct element. +``` + + + + + + + Task 1: Step transition animation with reduced-motion guard + src/index.css, src/App.tsx + +POLISH-03: Add a subtle fade/slide-up animation on wizard step transitions. + +1. **src/index.css** — Add two things: + + a. Register the animation in the existing `@theme` block. Add this line inside `@theme { ... }` after the last `--color-*` token: + ```css + --animate-step-in: step-in 200ms ease-out both; + ``` + This registers `animate-step-in` as a Tailwind utility class. + + b. Add the keyframes and reduced-motion guard AFTER the closing `}` of the `@theme` block (at the end of the file): + ```css + @keyframes step-in { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + @media (prefers-reduced-motion: reduce) { + .animate-step-in { + animation: none !important; + } + } + ``` + +2. **src/App.tsx** — In the WizardShell component, change the step content wrapper div: + From: `
` + To: `
` + + The `key={state.currentStep}` forces React to unmount/remount the div when the step changes, which triggers the CSS animation from its initial state. The animation also plays on first render — this is acceptable per research (imperceptible during page load). + +Do NOT add any JavaScript media query listener for reduced-motion. The CSS `@media (prefers-reduced-motion: reduce)` guard handles it declaratively, consistent with the existing pattern at line 45 of index.css. + + + npx vitest run src/App.test.tsx + + index.css contains @keyframes step-in with 200ms ease-out, @theme contains --animate-step-in, @media prefers-reduced-motion guard exists. App.tsx step wrapper has key={state.currentStep} and animate-step-in class. All App tests pass. + + + + Task 2: Auto-scroll to first error on validation failure + src/components/wizard/BackendSelectionStep.tsx, src/components/wizard/BackendSelectionStep.test.tsx, src/components/wizard/RemoteConfigStep.tsx, src/components/wizard/RemoteConfigStep.test.tsx + + - Test: When BackendSelectionStep form submits with empty remote name, scrollIntoView is called on the remote-name input element + - Test: When RemoteConfigStep form submits with missing required fields, scrollIntoView is called on the first errored field's element + + +POLISH-04: Auto-scroll to the first errored field when form validation fails. + +**Important prerequisite in both test files:** Add `Element.prototype.scrollIntoView = vi.fn();` in a `beforeEach` block (jsdom does not implement scrollIntoView — it will throw without this mock). + +1. **BackendSelectionStep.tsx** — Add an `onInvalidSubmit` error handler: + ```tsx + function onInvalidSubmit() { + // Only one validatable field in this step: remote-name + document.getElementById('remote-name')?.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + ``` + Update the form's onSubmit: `handleSubmit(onValidSubmit, onInvalidSubmit)` + Also update the handleCardClick call: `void handleSubmit(onValidSubmit, onInvalidSubmit)()` + +2. **BackendSelectionStep.test.tsx** — Add test: + - Setup: `beforeEach(() => { Element.prototype.scrollIntoView = vi.fn(); });` + - Test name: "scrolls to remote-name field when submitted with empty name" + - Action: render component, clear the remote name input (if it has a default), click "Next" button (type=submit) + - Assert: `expect(Element.prototype.scrollIntoView).toHaveBeenCalled()` + +3. **RemoteConfigStep.tsx** — Add an `onInvalidSubmit` error handler: + ```tsx + import type { FieldErrors } from 'react-hook-form'; + + function onInvalidSubmit(errors: FieldErrors) { + const firstKey = Object.keys(errors)[0]; + if (firstKey) { + document.getElementById(firstKey)?.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + } + ``` + Note: `FieldErrors` is already imported in RemoteConfigStep.tsx (as `FieldError` — add `FieldErrors` to the import). + Update the form's onSubmit: `handleSubmit(onNext, onInvalidSubmit)` + +4. **RemoteConfigStep.test.tsx** — Add test: + - Setup: `beforeEach(() => { Element.prototype.scrollIntoView = vi.fn(); });` + - Test name: "scrolls to first errored field when submitted with missing required fields" + - Action: render component with a backend that has required fields (e.g., azureblob), submit the form without filling required fields + - Assert: `expect(Element.prototype.scrollIntoView).toHaveBeenCalled()` + +The `FieldErrors` type import: RemoteConfigStep already imports `FieldError` from react-hook-form. Change to `import type { FieldError, FieldErrors } from 'react-hook-form';` (or just use the generic `Record` type on the errors param if FieldErrors causes issues). + + + npx vitest run src/components/wizard/BackendSelectionStep.test.tsx src/components/wizard/RemoteConfigStep.test.tsx + + BackendSelectionStep calls scrollIntoView on remote-name input when form validation fails. RemoteConfigStep calls scrollIntoView on the first errored field when form validation fails. Both behaviors verified by unit tests with mocked scrollIntoView. All tests pass. + + + + + +- `npx vitest run` — all tests green (full suite) +- Grep for `@keyframes step-in` in index.css — present +- Grep for `animate-step-in` in App.tsx — present +- Grep for `prefers-reduced-motion` in index.css — present (2 occurrences: existing theme transition + new animation guard) +- Grep for `scrollIntoView` in BackendSelectionStep.tsx and RemoteConfigStep.tsx — present in both +- Grep for `scrollIntoView` in test files — mock setup present in both test files + + + +- Step content wrapper in App.tsx has `key={state.currentStep}` and `animate-step-in` class +- index.css defines `@keyframes step-in` with opacity 0->1 and translateY 8px->0 +- index.css has `@media (prefers-reduced-motion: reduce)` guard that disables animation +- `@theme` block contains `--animate-step-in` token +- BackendSelectionStep scrolls to remote-name input on validation failure +- RemoteConfigStep scrolls to first errored field on validation failure +- Both scroll behaviors verified by passing unit tests +- Full test suite passes with no regressions + + + +After completion, create `.planning/phases/11-polish-responsiveness/11-02-SUMMARY.md` +