Files
Meal-Planner/Review/sprint9-verification.md
T
admin 6e386baf6e 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).
2026-06-05 11:14:53 -07:00

6.4 KiB
Raw Blame History

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:

  1. Dashboard — "Your weekly meal plan"
  2. Pantry — "What you have in stock"
  3. Recipes — "Browse + filter recipes"
  4. 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):

  • 14 → jump to that step
  • ←/→ → step back / forward
  • Esc → dismiss
  • Tab order: Skip → Back → Next (or Skip → Open page → Next when 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=1 in any URL clears the key + strips the param via navigate(..., { 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 use useLocation / useNavigate).
  • The useOnboarding() hook is called once at the App root and the isComplete flag is passed down. On dismiss, the tour calls onComplete() which the App maps to onboarding.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/:

  1. 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.
  2. Anchor highlight. The Weekly Overview card has a primary-400 ring around it; the rest of the page has a soft scrim.
  3. Forward nav. Press → step 2 (Pantry) shows. If you're not on /pantry, the tooltip renders centered with an "Open Pantry" button.
  4. Click "Open Pantry". Tour stays open, navigates to /pantry, anchor re-renders below the header.
  5. Keyboard jumps. From step 2, press 3 → tour jumps to Recipes (Filters button highlighted).
  6. Dismiss. Press Esc on any step → tour disappears, localStorage key is set. Refresh the page → tour does NOT re-show.
  7. 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.
  8. 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 WeekRangeNav still 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 Plan CTA — 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 /pantry first (e.g. via a bookmark), the tour does NOT auto-show. The header anchor is still present so a ?reset-tour=1 would show the tour with the right anchor. Documented; not a bug.
  • R3: useOnboarding flag 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 touch backend/app/api/recipes.py, backend/app/schemas/recipe.py, or nginx/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)