Public Access
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).
120 lines
6.4 KiB
Markdown
120 lines
6.4 KiB
Markdown
# 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):**
|
||
- `1`–`4` → 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)
|