# Tutorial / Onboarding Mode

## Context

New parents signing up for the chore app land on an empty screen with no guidance. Many will not be especially technical — a busy parent setting things up on a phone needs to be shown where to tap, what each control does, and what states like `TOO LATE` / `PENDING` mean, without feeling badgered. Today there is no onboarding, no first-time tips, no contextual help.

The goal is a modular tutorial layer that:
- Walks users through the essentials (PIN → child → chore) so the app is never empty after first use.
- Surfaces contextual hints **just-in-time** when users reach a feature for the first time (image picker, kebab menus, scheduler, status badges, notifications, assignment, etc.).
- Persists progress per-user on the backend so it syncs across devices and is dismissed only once.
- Can be reset or disabled from the profile page.
- Is friction-light: visible Skip, one card at a time outside the intro, a permanent `?` button so users can re-trigger help themselves.
- Is extensible — adding a tutorial for a future feature is roughly a one-line call + a registry entry.

## Approach (locked decisions)

- **Cadence**: brief forced 3-step intro after first sign-in (PIN setup → first child → first chore), then everything else is just-in-time.
- **Mobile**: bottom-sheet card on screens ≤600px, floating tooltip on desktop. Spotlight cut-out is shared.
- **Skip behaviour**: a single Skip suppresses all hints for the session; user re-engages via the per-screen `?` button or by resetting from the profile.
- **State storage**: `tutorial_progress: dict[str, bool]` and `tutorial_enabled: bool` on the `User` model, broadcast via existing `PROFILE_UPDATED` SSE event.
- **Step authoring**: imperative `tutorial.maybeShow('step-id', anchorRef)` from `onMounted` / `watch` in each host component, with step copy in a single registry file.

## Architecture

### Backend
- **`backend/models/user.py`** — add `tutorial_enabled: bool = True` and `tutorial_progress: dict = field(default_factory=dict)` alongside `email_digest_enabled`. Update `from_dict` / `to_dict`.
- **`backend/api/user_api.py`** — add `PATCH /user/tutorial-progress` that accepts:
  - `{ "step_id": "...", "seen": true }` — mark one step
  - `{ "reset": true }` — clear the dict
  - `{ "enabled": false }` — toggle
  Each branch emits `EventType.PROFILE_UPDATED` (already wired) so other tabs / devices stay in sync.

### Frontend — new directory `frontend/src/tutorial/`

- **`controller.ts`** — module-level `ref()`s (matches the no-Pinia pattern from `stores/auth.ts`):
  ```ts
  export const tutorialEnabled = ref(true)
  export const tutorialProgress = ref<Record<string, boolean>>({})
  export const activeStep = ref<ActiveStep | null>(null)

  export function shouldShowStep(id: string): boolean
  export function maybeShow(id: string, anchor: HTMLElement | (() => HTMLElement | null)): void
  export async function markStepSeen(id: string): Promise<void>     // PATCH + optimistic
  export async function resetAllProgress(): Promise<void>
  export async function setTutorialEnabled(on: boolean): Promise<void>
  ```
  Hydrated from `/user/profile` (extend the existing fetch in `LoginButton.vue`); re-hydrated on `profile_updated` SSE.

- **`steps.ts`** — single registry of step copy/config:
  ```ts
  export interface StepDef {
    id: string
    title: string
    body: string
    next?: string         // chains a tour
    placement?: 'auto' | 'below' | 'above'
    ctaLabel?: string     // defaults "Got it"
  }
  export const stepRegistry: Record<string, StepDef> = { … }
  ```
  Step IDs follow flat kebab-case: `setup-parent-pin`, `create-child`, `create-chore`, `create-chore-image-photo`, `create-chore-image-camera`, `create-chore-schedule`, `chore-kebab`, `chore-kebab-extend-time`, `status-too-late`, `status-pending`, `notification-click`, `assign-chore`, `child-mode-tour-offer`, etc.

- **`TutorialOverlay.vue`** — mounted once in `App.vue`. Watches `activeStep`. Renders:
  - Four-rectangle dim (top/left/right/bottom of the anchor's bbox) with `rgba(0,0,0,0.55)`.
  - A coach-mark card with `title`, `body`, **Skip tour** + primary action button.
  - Desktop: tooltip positioned by `getBoundingClientRect()` + viewport clamping (no `floating-ui` — same hand-rolled pattern as the existing `TimePickerPopover.vue`).
  - Mobile (`window.matchMedia('(max-width: 600px)')`): card docks as a bottom sheet with a chevron pointing toward the spotlight.
  - Re-positions on `resize`, capture-phase `scroll`, and `ResizeObserver` on the card.
  - `role="dialog"`, focus trap, Esc to dismiss, `aria-live="polite"` for announcements, `prefers-reduced-motion` kills animations.

- **`HelpButton.vue`** — a small `?` icon placed in the `ParentLayout` topbar (and `ChildLayout`). On tap, re-fires the most relevant step for the current route from a `routeToStep` map. This is the user's escape hatch — critical for the non-technical audience.

### Triggering — three sources, all routed through `maybeShow`

1. **Route entered** — `router.afterEach` in `controller.ts` consults a small `routeTriggers: Record<RouteName, string>` map (used for `tab-tasks`, `tab-rewards`, `tab-notifications`, …).
2. **Element first rendered** — host component calls `maybeShow` in `onMounted` or `watchEffect` once its anchor `ref` is bound.
3. **State-derived** — host component `watch`es a reactive condition (`isChoreLate`, `menuOpen`, `firstPendingChore`) and calls `maybeShow` when it flips true. This is how dynamic kebab options (`Extend Time` only on late chores) and badge-first-appearance (`TOO LATE`, `PENDING`) get triggered.

No global DOM scanning, no MutationObserver — every anchor is already owned by a Vue component that knows when it appears.

## Step coverage (the JIT hints)

Phase 2 wires these via `maybeShow` calls in the listed components. All copy is warm/parent-friendly, never jargon.

| Step ID | Trigger | Anchor |
|---|---|---|
| `setup-parent-pin` (intro #1) | First load after signup, no PIN set | `LoginButton.vue` avatar |
| `create-child` (intro #2) | After PIN set, 0 children | FAB in `ChildrenListView.vue` |
| `create-chore` (intro #3) | After 1+ child, 0 chores, on Tasks tab | FAB in tasks view |
| `create-chore-image-photo` | First time `ImagePicker` opens | `+` button (`addPhotoBtn` ref) in `ImagePicker.vue` |
| `create-chore-image-camera` | First time `ImagePicker` opens | Camera button (`cameraBtn` ref) in `ImagePicker.vue` |
| `create-chore-schedule` | First time `ScheduleModal` opens | day chips in `ScheduleModal.vue` |
| `create-kindness`, `create-penalty`, `create-routine` | First time landing on each create view | FAB / form |
| `create-routine-add-task` | First time inside `RoutineEditView` | `.add-task-trigger` |
| `create-reward` | First time on Rewards tab with 0 rewards | FAB |
| `notification-click` | First time on Notifications tab with 1+ notification | first notification row in `NotificationView.vue` |
| `select-child` then `assign-chore` / `assign-reward` / `assign-routine` | First time entering ParentView for a specific child | the assignment sections |
| `child-kebab` | First time `.kebab-btn` opens in `ChildrenListView.vue` | the open menu |
| `chore-kebab` | First time `.kebab-menu` opens in `ParentView.vue` | the open menu |
| `chore-kebab-extend-time` | First time the `Extend Time` menu item renders (chore is late) | that menu item |
| `chore-kebab-reset` | First time the `Reset` menu item renders | that menu item |
| `status-too-late` | First time `.chore-stamp` (TOO LATE) renders for any chore | the badge |
| `status-pending` | First time `.chore-stamp.pending-stamp` renders | the badge |

Adding a future step = one entry in `steps.ts` + one `maybeShow` call at the anchor site.

## Profile controls (`UserProfile.vue`)

Add a new "Help" section after the existing email-digest / push-notifications toggles:
- **Show tutorial tips** toggle — bound to `tutorialEnabled`, copy: *"Show helpful tips as I use the app."*
- **Restart tutorial** button — opens a `ModalDialog` confirm (*"Start the tour again from the beginning?"*), then calls `resetAllProgress()` and routes to `/parent` (a toast confirms reset).

Dev bypass: support `?tutorial=off` / `?tutorial=reset` in `controller.ts` init (gated by `import.meta.env.DEV`) for debugging without touching the backend.

## Child-mode tour (parent learns to teach)

A derived `watchEffect` in `controller.ts`:

```ts
watchEffect(() => {
  const p = tutorialProgress.value
  if (p['create-child'] && p['create-chore'] && p['create-reward']
      && !p['child-mode-tour-offer']) {
    showOfferModal()  // ModalDialog with Yes / Maybe later / No thanks
  }
})
```

- **Yes** → mark `child-mode-tour-offer` seen, navigate to `/child`, fire `child-mode-*` steps (separate IDs, same registry/controller).
- **Maybe later** → don't mark seen; reappears once next session, then auto-converts to "No thanks" (max two asks).
- **No thanks** → mark seen, never asked again (reset clears it).

## UX rules baked in (for non-technical users)

- 3-step forced intro, never longer. Single-card JIT hints elsewhere — no chained `Next → Next → Next` outside the intro.
- Skip is always visible, labeled "Skip tour", and one Skip suppresses everything for the session.
- Persistent `?` button on every screen — taps re-fire the most relevant step. This is the single most important affordance for users who don't remember the first run.
- Copy is short and warm: *"Let's add your first kiddo"*, not *"Create a child entity."*
- If an anchor element doesn't exist (e.g. 0 chores so no kebab to point at), the step silently waits — never an arrow into empty space.
- Bottom-sheet on mobile keeps text at thumb height and survives the on-screen keyboard via the `visualViewport` API.
- `prefers-reduced-motion`, focus trap, Esc-to-dismiss, ≥5:1 contrast on the highlight ring.

## Phasing

**Phase 1 — Infrastructure + one end-to-end step.**
- Backend `tutorial_enabled` / `tutorial_progress` fields, PATCH endpoint, SSE wiring.
- Frontend `controller.ts`, `steps.ts` (one entry), `TutorialOverlay.vue` mounted in `App.vue`, hydrate from profile fetch, listen for `profile_updated`.
- Ship `create-child` end-to-end (triggered from `ChildrenListView.vue` when child list is empty).
- Manual verification: sign up a fresh user, see the coach mark, dismiss, confirm it doesn't return; reload to confirm persistence; open in second tab to confirm SSE sync.

**Phase 2 — Roll out remaining steps.**
- Add the 3-step forced intro chaining (`setup-parent-pin` → `create-child` → `create-chore`).
- Add the JIT step entries from the coverage table; wire `maybeShow` calls at each anchor.
- Add `HelpButton.vue` in both layouts with a `routeToStep` map.

**Phase 3 — Profile controls + child-mode tour.**
- "Help" section in `UserProfile.vue` (toggle + restart button).
- `child-mode-tour-offer` watcher + child-mode step set.
- Dev bypass URL params.

## Critical files

**Backend**
- `backend/models/user.py` — model fields
- `backend/api/user_api.py` — PATCH endpoint
- (no event type changes — `PROFILE_UPDATED` is reused)

**Frontend (new)**
- `frontend/src/tutorial/controller.ts`
- `frontend/src/tutorial/steps.ts`
- `frontend/src/tutorial/TutorialOverlay.vue`
- `frontend/src/tutorial/HelpButton.vue`

**Frontend (modified — `maybeShow` call sites, mostly one-liners)**
- `frontend/src/App.vue` — mount `TutorialOverlay`
- `frontend/src/common/models.ts` — `User` interface additions
- `frontend/src/components/shared/LoginButton.vue` — hydrate controller from profile fetch
- `frontend/src/layout/ParentLayout.vue`, `frontend/src/layout/ChildLayout.vue` — `HelpButton`
- `frontend/src/components/shared/ChildrenListView.vue` — `create-child`, `child-kebab`
- `frontend/src/components/child/ParentView.vue` — `chore-kebab`, `chore-kebab-extend-time`, `status-too-late`, `status-pending`, `assign-*`, `select-child`
- `frontend/src/components/task/ChoreEditView.vue` + sibling create views — `create-chore`, `create-kindness`, `create-penalty`
- `frontend/src/components/utils/ImagePicker.vue` — `create-chore-image-photo`, `create-chore-image-camera`
- `frontend/src/components/routine/RoutineEditView.vue` — `create-routine`, `create-routine-add-task`
- `frontend/src/components/reward/RewardEditView.vue` — `create-reward`
- `frontend/src/components/notification/NotificationView.vue` — `notification-click`
- `frontend/src/components/shared/ScheduleModal.vue` — `create-chore-schedule`
- `frontend/src/components/profile/UserProfile.vue` — "Help" section

## Verification

- **Backend**: `pytest tests/test_user_api.py` covering the new PATCH branches (mark / reset / disable), plus a check that `PROFILE_UPDATED` is emitted.
- **Frontend unit**: `npm run test:unit` for `controller.ts` (`shouldShowStep`, `markStepSeen` optimism, reset/disable).
- **E2E** (`npx playwright test`): a new bucket `chromium-tutorial` with an isolated user that signs up fresh, walks the 3-step intro, navigates to Rewards/Notifications to hit JIT hints, then resets from the profile and confirms the intro replays.
- **Manual mobile check**: Chrome devtools at 375×667, confirm the bottom-sheet variant renders, survives the on-screen keyboard, and re-positions on rotation.
- **Manual cross-device SSE**: log in to two tabs, dismiss a step in one, confirm the second tab updates without manual refresh.
- **Accessibility**: keyboard-only run-through (Tab/Esc/Enter); VoiceOver pass on macOS to confirm `aria-live` announcements; `prefers-reduced-motion` set in devtools to confirm pulse animation is suppressed.
