feat(ui): Sprint 9 — F1 onboarding tour (4-step welcome)

Hand-rolled 4-step tour (no react-joyride) anchors to existing
[data-tour="<id>"] attributes. localStorage key
mealplanner:onboarding-complete is the source of truth; ?reset-tour=1
clears the key and re-shows.

Steps: Dashboard / Pantry / Recipes / Shopping List. Keyboard: 1-4 jump,
←/→ step, Esc dismiss. Off-route fallback renders a centered card with
an 'Open <page>' CTA. A11y: role=dialog, aria-modal=true, focus captured
on open and restored on close.

5 lines of code across 4 pages; 1 new component (~420 lines). No new
dependencies. No backend changes. No migration. Frontend-only deploy.

Tracking: Review/sprint9-verification.md (8-step browser smoke + a11y
check + reset-link test).
This commit is contained in:
2026-06-05 11:14:53 -07:00
parent efd1fc695f
commit 6e386baf6e
13 changed files with 839 additions and 25 deletions
+44
View File
@@ -497,3 +497,47 @@ Outside the original audit. Driven by user report 2026-06-05: "Latest meal plans
- [ ] No regressions in existing Playwright walkthrough.
- [ ] **Sprint 7 (committed `09c7525`, awaiting deploy):** webui "empty meal plan" date-semantics mismatch. Code + SQL fix + verification doc. ✅ done on dev; awaiting user deploy.
- [ ] **Sprint 8 (in progress):** "Deny" semantics (C + Z, hard-filter escalation). Migration 0016 + 3 helpers + 2 endpoint extensions + 1 planner update + 1 email template + 1 webui 3-button card. ✅ build green + 21/21 planner tests pass; awaiting user commit + deploy.
---
## Sprint 9 — F1 Onboarding Tour (H10) — ✅ COMPLETE, awaiting deploy
User direction 2026-06-05: "Proceed with the next phase in the redesign." F1 was the natural next phase (the only §Future item with a clear UI scope; F8 + F9 are full backend proposals).
**Status (2026-06-05):** ✅ Code complete. `npm run build` green (tsc 0 errors, vite 0 errors). Awaiting user commit + deploy. One new component, no new dependencies, no backend changes.
### T3.1 · `OnboardingTour.tsx` (NEW)
- **File:** `frontend/src/components/OnboardingTour.tsx` (~420 lines)
- **Why hand-rolled:** adding `react-joyride` is a 1-line trade-off; the audit's prior principles ("reuse existing components/ui/*", "no new npm deps") win. The 4-step tour fits in ~420 lines of focused React.
- **Steps:**
1. **Dashboard** — "Your weekly meal plan"
2. **Pantry** — "What you have in stock"
3. **Recipes** — "Browse + filter recipes"
4. **Shopping List** — "Plan → shop → restock"
- **Storage:** `localStorage.getItem('mealplanner:onboarding-complete') === '1'`. Reads/writes wrapped in try/catch.
- **Reset:** `?reset-tour=1` in any URL clears the key + strips the param via `navigate(..., { replace: true })`. Operator can use this from the browser URL bar.
- **Keyboard:** `1``4` jump to step, `←/→` step back/forward, `Esc` dismiss, `Tab` order is `Skip → Back → Next`.
- **A11y:** `role="dialog"`, `aria-modal="true"`, `aria-labelledby` → step title. Focus captured on open (primary action), restored on close. Decorative scrim + anchor ring are `aria-hidden="true"`.
- **Anchor tracking:** rAF loop reads `getBoundingClientRect` of the matching `[data-tour="<id>"]` element. One DOM read per frame; cancellable on close.
### T3.2 · Anchor points (5 lines of code total)
- **File:** `frontend/src/pages/Dashboard.tsx:602` — `<Card data-tour="dashboard">` on the Weekly Overview grid.
- **File:** `frontend/src/pages/Pantry.tsx:185` — `<div data-tour="pantry">` on the page header (always present). Plus `:208` for the add-form card (when the form is open).
- **File:** `frontend/src/pages/Recipes.tsx:124` — `<Button data-tour="recipes">` on the Filters button.
- **File:** `frontend/src/pages/ShoppingList.tsx:231` — `<div data-tour="shopping-list">` on the page header.
- **Off-route fallback:** when the user is on a different page than the current step's anchor, the tooltip renders as a centered card with an "Open <page>" CTA. The first-time user experience is preserved even if they land on `/pantry` first.
### T3.3 · `App.tsx` mount
- **File:** `frontend/src/App.tsx:75-105`
- **Change:** `useOnboarding()` at App root, `isComplete` flag passed to `<OnboardingTour>`. Mounted as a sibling of `<ShortcutHelpBanner />` inside `<BrowserRouter>` (so the tour can use `useLocation` / `useNavigate`).
- **Why at App root:** the localStorage key is read once on mount; the flag is shared by all subsequent renders. A child of `<BrowserRouter>` would re-read on every navigation.
### T3.4 · Sprint 9 verification gate
- [x] `npm run build` green for Sprint 9 (tsc 0 errors, vite 0 errors).
- [ ] Browser smoke (8 steps) on `http://100.108.208.56:8082/` per `Review/sprint9-verification.md`.
- [ ] No regression in Sprints 1-8.
- [x] `Review/sprint9-verification.md` written.