docs(11-polish-responsiveness): create phase plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</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/11-polish-responsiveness/11-RESEARCH.md
|
||||
@.planning/phases/11-polish-responsiveness/11-01-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Current state of files this plan modifies -->
|
||||
|
||||
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
|
||||
<div className="mt-8">
|
||||
{CurrentStep}
|
||||
</div>
|
||||
// CHANGE TO: <div key={state.currentStep} className="mt-8 animate-step-in">
|
||||
```
|
||||
|
||||
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<typeof remoteNameSchema>;
|
||||
// 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.
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Step transition animation with reduced-motion guard</name>
|
||||
<files>src/index.css, src/App.tsx</files>
|
||||
<action>
|
||||
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: `<div className="mt-8">`
|
||||
To: `<div key={state.currentStep} className="mt-8 animate-step-in">`
|
||||
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx vitest run src/App.test.tsx</automated>
|
||||
</verify>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Auto-scroll to first error on validation failure</name>
|
||||
<files>src/components/wizard/BackendSelectionStep.tsx, src/components/wizard/BackendSelectionStep.test.tsx, src/components/wizard/RemoteConfigStep.tsx, src/components/wizard/RemoteConfigStep.test.tsx</files>
|
||||
<behavior>
|
||||
- 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
|
||||
</behavior>
|
||||
<action>
|
||||
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<string, any>` type on the errors param if FieldErrors causes issues).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx vitest run src/components/wizard/BackendSelectionStep.test.tsx src/components/wizard/RemoteConfigStep.test.tsx</automated>
|
||||
</verify>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/11-polish-responsiveness/11-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user