docs(11-polish-responsiveness): create phase plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -98,7 +98,10 @@ Plans:
|
|||||||
2. Every interactive element shows a visible 3px MD3 focus outline when navigated via keyboard (using `focus-visible`)
|
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
|
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
|
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
|
## Progress
|
||||||
|
|
||||||
@@ -113,5 +116,5 @@ Plans:
|
|||||||
| 7. Validation & UX Polish | v1.1 | 3/3 | Complete | 2026-03-31 |
|
| 7. Validation & UX Polish | v1.1 | 3/3 | Complete | 2026-03-31 |
|
||||||
| 8. Theme Foundation | v1.2 | 2/2 | Complete | 2026-04-01 |
|
| 8. Theme Foundation | v1.2 | 2/2 | Complete | 2026-04-01 |
|
||||||
| 9. MD3 Components | v1.2 | 5/5 | Complete | 2026-04-01 |
|
| 9. MD3 Components | v1.2 | 5/5 | Complete | 2026-04-01 |
|
||||||
| 10. Content & Clarity | 3/3 | Complete | 2026-04-01 | - |
|
| 10. Content & Clarity | v1.2 | 3/3 | Complete | 2026-04-01 |
|
||||||
| 11. Polish & Responsiveness | v1.2 | 0/? | Not started | - |
|
| 11. Polish & Responsiveness | v1.2 | 0/2 | Not started | - |
|
||||||
|
|||||||
@@ -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"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
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.
|
||||||
|
</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
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- Key constants and component signatures the executor needs -->
|
||||||
|
|
||||||
|
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
|
||||||
|
<div className="flex gap-3 mt-6"> // → flex flex-col sm:flex-row gap-3 mt-6
|
||||||
|
<button className={MD3_BTN_OUTLINED}>Back</button> // → add w-full sm:w-auto
|
||||||
|
<button className={MD3_BTN_FILLED}>Next</button> // → add w-full sm:w-auto
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Mobile responsive layout — card grid, button rows, step indicator collapse</name>
|
||||||
|
<files>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</files>
|
||||||
|
<action>
|
||||||
|
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 `<span>` 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.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>npx vitest run</automated>
|
||||||
|
</verify>
|
||||||
|
<done>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.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: MD3 focus-visible indicators on all interactive elements</name>
|
||||||
|
<files>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</files>
|
||||||
|
<action>
|
||||||
|
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 `<span>`, 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.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>npx vitest run</automated>
|
||||||
|
</verify>
|
||||||
|
<done>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.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `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)
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- 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)
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/11-polish-responsiveness/11-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
@@ -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