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).
6.4 KiB
Sprint 9 — F1 Onboarding Tour (H10)
Status (2026-06-05): ✅ Code complete. npm run build green. Awaiting user commit + deploy.
Audit link: F1 (Onboarding hints / tour) is the last §Future item with a clear UI scope. F8 (Spoonacular) and F9 (Ollama) are full backend proposals; the dead Generate Meal Plan CTA is a separate follow-up.
Goal: First-time visitors get a 4-step tour. Returning users never see it. The tour re-shows on demand via ?reset-tour=1.
What ships
OnboardingTour.tsx (NEW)
Hand-rolled (no react-joyride) to keep the npm footprint flat. 4 steps:
- Dashboard — "Your weekly meal plan"
- Pantry — "What you have in stock"
- Recipes — "Browse + filter recipes"
- Shopping List — "Plan → shop → restock"
Each step:
- Anchors to a
[data-tour="<id>"]attribute on the existing page. - Renders a tooltip card pinned to the anchor (top/bottom/center fallback).
- Highlights the anchor with a primary-400 ring + soft scrim.
- Step progress shown as 4 progress bars (top of card).
- Skip / Back / Next (or "Got it" on the last step).
Keyboard nav (when tour is visible):
1–4→ jump to that step←/→→ step back / forwardEsc→ dismiss- Tab order:
Skip → Back → Next(orSkip → Open page → Nextwhen off-route)
A11y:
role="dialog",aria-modal="true",aria-labelledby→ step title.- Focus is captured on open (moved to the primary action) and restored on close.
- Tooltip + anchor ring are announced via
aria-hidden="true"(decorative); the dialog text is the real signal.
Storage:
- localStorage key:
mealplanner:onboarding-complete("1"once completed). ?reset-tour=1in any URL clears the key + strips the param vianavigate(..., { replace: true })so a refresh doesn't re-clear.- Reading the key is wrapped in try/catch — private mode / disabled storage silently falls through.
Anchor points (5 lines of code total)
| Page | File:line | Anchor | Notes |
|---|---|---|---|
| Dashboard | pages/Dashboard.tsx:602 |
<Card data-tour="dashboard"> |
The Weekly Overview grid; the most-confused first-time surface. |
| Pantry | pages/Pantry.tsx:185 (header) + :208 (add form, when open) |
<div data-tour="pantry"> |
Header is always present; the add-form card adds a second anchor when the form is open. |
| Recipes | pages/Recipes.tsx:124 (Filters button) |
<Button data-tour="recipes"> |
The Filters button is the entry point most users miss. |
| ShoppingList | pages/ShoppingList.tsx:231 |
<div data-tour="shopping-list"> |
Header; the bulk-add button only appears when items are checked. |
App.tsx (mount)
- Imports
OnboardingTour+useOnboarding. - Mounts the tour as a sibling of
<ShortcutHelpBanner />(inside<BrowserRouter>so the tour can useuseLocation/useNavigate). - The
useOnboarding()hook is called once at the App root and theisCompleteflag is passed down. On dismiss, the tour callsonComplete()which the App maps toonboarding.reset()— flipping the flag so re-renders don't re-show.
Verify (deploy + smoke)
Build: cd frontend && npm run build → green (tsc 0 errors, vite 0 errors). Verified locally.
Browser smoke on http://100.108.208.56:8082/:
- First-visit tour. Open an incognito window (or a new browser) and navigate to
http://100.108.208.56:8082/. The tour auto-shows on step 1 (Dashboard) within 1 frame. - Anchor highlight. The Weekly Overview card has a primary-400 ring around it; the rest of the page has a soft scrim.
- Forward nav. Press
→→ step 2 (Pantry) shows. If you're not on/pantry, the tooltip renders centered with an "Open Pantry" button. - Click "Open Pantry". Tour stays open, navigates to
/pantry, anchor re-renders below the header. - Keyboard jumps. From step 2, press
3→ tour jumps to Recipes (Filters button highlighted). - Dismiss. Press
Escon any step → tour disappears, localStorage key is set. Refresh the page → tour does NOT re-show. - Reset. Navigate to
http://100.108.208.56:8082/?reset-tour=1. The query string is stripped, localStorage key is cleared, tour shows again on step 1. - A11y. Tab through the dialog: focus moves from Skip → Back → Next in order; Esc dismisses; screen reader announces the step title (e.g. "What you have in stock, dialog").
A11y verification:
- VoiceOver on the dialog announces "Your weekly meal plan, dialog".
- Arrow keys step the tour.
- Esc dismisses.
- Focus is restored to the previously-focused element on dismiss.
Regression check:
- Sprint 5 keyboard shortcuts (
g d,g r,g p,g s,/,?) still work. - Sprint 4 global error toast still fires.
- The 3-button Sprint 8 voting row on Dashboard meal cards still works.
- Sprint 7
WeekRangeNavstill renders on Dashboard and ShoppingList.
Out of scope
- Per-page deep tutorials (the welcome tour is the only thing S9 ships).
- Video or animated demos.
- Tooltip-on-hover patterns.
- A user-facing "Show tour" link in the footer (operator can use
?reset-tour=1; a footer link is a 5-line follow-up if requested). - F8 Spoonacular proposal, F9 Ollama proposal, dead
Generate Meal PlanCTA — separate.
Risks & mitigations
- R1: rAF polling on the anchor's
getBoundingClientRect. Runs while the tour is open. Cheap (one DOM read per frame). Cancelled on close. No throttling needed for a 4-step tour. - R2: The auto-show on
/only. If the user lands on/pantryfirst (e.g. via a bookmark), the tour does NOT auto-show. The header anchor is still present so a?reset-tour=1would show the tour with the right anchor. Documented; not a bug. - R3:
useOnboardingflag is App-level state. A second<App>mount (in tests, e.g.) would not share the flag. The tour reads localStorage on mount so the real source of truth is the storage key, not the flag. - R4: Existing pre-existing WIP in
git status. Sprint 9 doesn't touchbackend/app/api/recipes.py,backend/app/schemas/recipe.py, ornginx/nginx.conf— those are the user's to manage.
Commit
One commit: feat(ui): Sprint 9 — F1 onboarding tour (4-step welcome). Files:
- NEW
frontend/src/components/OnboardingTour.tsx(~420 lines) frontend/src/App.tsx(mount + flag)frontend/src/pages/Dashboard.tsx(anchor)frontend/src/pages/Pantry.tsx(2 anchors)frontend/src/pages/Recipes.tsx(anchor on Filters button)frontend/src/pages/ShoppingList.tsx(anchor on header)