--- 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`