# 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)
