297 lines
26 KiB
Markdown
297 lines
26 KiB
Markdown
# Handoff: MealQuest — mobile-first meal planning PWA
|
||
|
||
## Overview
|
||
MealQuest helps one household plan weekly dinners. The household enters the dinners they make on repeat ("regulars"); an AI assistant suggests new dinners to try ("side quests"), which the household accepts or declines with a swipe-card UI. Accepted dinners go into a to-try pool (the "Quest log"). Each week the AI drafts a plan that mixes regulars with Quest-log picks; the household swaps days, then the plan is pushed to Google Calendar. A shopping list is generated from the planned dinners, grouped by dinner, minus anything the household unchecks as already on hand. Each week's list is archived as a "post" in a blog-style history (by week / month / year). Push notifications and Google Chat webhook messages announce key events. An admin area inside Settings configures the LLM.
|
||
|
||
### Product decisions (confirmed with the client)
|
||
| Topic | Decision |
|
||
|---|---|
|
||
| Users | One **shared household account** (no per-member profiles). Anyone in the household can edit everything, including Admin. |
|
||
| Swipe accept | Goes into the **Quest log** (to-try pool), scheduled later. |
|
||
| Suggestion card shows | Photo, **why it was suggested** (referencing regular dinners), **difficulty**. |
|
||
| Week building | **AI builds the week** mixing regulars + Quest-log meals; the user swaps/adjusts. No fixed new-vs-regular ratio. |
|
||
| Google Calendar | **One-way: app is source of truth**, pushes dinners to a Google Calendar. |
|
||
| Shopping list | All ingredients for planned dinners, **grouped by dinner**. The user **unchecks items they already have** (this is the only pantry mechanism — no persistent pantry). |
|
||
| History | **Each weekly shopping list is a new post**; filter Week / Month / Year. |
|
||
| Quick dinners | "Quick" = **crock pot** OR **≤ 30 min prep-to-table**. Shown as **two tabs** (Crock pot / 30 min or less). Reachable from a **bottom-nav tab** AND a Home shortcut, and as a "Quick only" filter in Cook-from-what-I-have. |
|
||
| Notifications (push + Google Chat) | New weekly plan ready · Shopping list ready · Day-of dinner reminder · New suggestions to swipe. Each event is toggled separately for Push and Chat. |
|
||
| Admin (LLM) | Provider + model picker · API key management · Temperature (creativity) · System prompt editor. Lives **inside Settings**. |
|
||
| Copy tone | Playful, quest-themed, but every label stays plain enough to understand. |
|
||
|
||
### Vocabulary (use consistently in UI copy)
|
||
- **Regulars** — the household's routine dinners.
|
||
- **Side quests** — AI dinner suggestions (Discover tab).
|
||
- **Quest log** — accepted suggestions waiting to be scheduled (to-try pool).
|
||
- **Supply run** — the current week's shopping list.
|
||
- **Supply archive** — shopping-list history.
|
||
|
||
## About the design files
|
||
The files in this bundle are **design references built in HTML** — static hi-fi mockups plus one interactive motion prototype. They show the intended look and behavior; they are **not production code to copy**. Recreate them in the target codebase using its established patterns. There is no existing codebase, so the recommended stack is:
|
||
|
||
- **React 18 + TypeScript + Vite**, `vite-plugin-pwa` (Workbox) for the service worker + manifest.
|
||
- Styling: CSS variables from `tokens.css` + CSS Modules or Tailwind configured to those variables. Do not hard-code hex values in components.
|
||
- Motion: **Web Animations API** (as in the prototype) or **Framer Motion** — the keyframes below translate 1:1.
|
||
- Icons: **Lucide** (`lucide-react`), `strokeWidth={2.75}` everywhere.
|
||
- Routing: React Router. Data: TanStack Query against a small API (backend out of scope for this handoff; integration points listed under *State & data*).
|
||
|
||
## Fidelity
|
||
**High-fidelity.** Colors, type, radii, spacing, copy and layout are final. Recreate pixel-accurately. Photos are placeholders (striped boxes labelled "dish photo") — real recipe photography will be supplied and should be rendered through the Organic "washed" treatment (`filter: saturate(.6) contrast(.85) brightness(1.1) opacity(.94)`), with rounded corners.
|
||
|
||
---
|
||
|
||
## Design tokens
|
||
All values are in `tokens.css`. Summary:
|
||
|
||
**Color**
|
||
| Role | Token | Hex |
|
||
|---|---|---|
|
||
| Page / phone background | `--color-bg` | `#f9f6f1` |
|
||
| Surface (cards, rows, inputs) | `--color-surface` | `#eee9de` |
|
||
| Ink | `--color-text` | `#201e1d` |
|
||
| Primary accent (Grape) | `--color-accent` | `#5a358c` |
|
||
| Text on accent | `--on-accent` | `#f9f6f1` |
|
||
| Second accent (Teal blue) | `--color-accent-2` | `#29889c` |
|
||
| Divider | `--color-divider` | `#201e1d` @ 16% |
|
||
|
||
Ramps (100→900): Grape `#f8f1ff #eedfff #dac3ff #c1a1f9 #a47ee1 #865fc0 #68449b #4a2d73 #2d1a49`; Teal `#e0fbff #bbf3ff #88e0f6 #3fc7e4 #00a9ca #008aaa #006b88 #004c63 #002f3e`; Neutral `#f9f4ed #eee7db #dcd3c4 #c0b6a5 #a19786 #82796a #645c50 #474238 #2e2b25`.
|
||
|
||
Usage rules:
|
||
- Grape fill (`--color-accent`) = primary buttons, active nav pill, selected segmented option, checked checkboxes, slider fill. **Always `--on-accent` text on it.**
|
||
- Grape small text (kickers, links) uses `--color-accent-700`.
|
||
- "Side quest"/new-dinner highlights: grape-100 fill + grape-300 inset ring (rows) or teal-200/300 fills (tiles). Tags: grape-100 bg / grape-800 text.
|
||
- Teal = positive/confirmation voice: "Why this?" boxes (teal-100 bg, teal-900 text), "Synced"/"Connected" status text (teal-800), toggles ON and "Try it" button (teal-600), Quest-log count pill (teal-200 bg, teal-800 text).
|
||
- Neutral-100 = secondary card fill; neutral-700 = muted meta text.
|
||
|
||
**Typography**
|
||
| Use | Font | Size / line-height |
|
||
|---|---|---|
|
||
| Mobile screen title | Bagel Fat One 400 | 26–30px / 1.1 |
|
||
| Card title (suggestion) | Bagel Fat One | 24px / 1.1 |
|
||
| Section/card heading | Bagel Fat One | 16–22px / 1.15 |
|
||
| Tablet/desktop page title | Bagel Fat One | 36–40px / 1.1 |
|
||
| Buttons | Bagel Fat One | 14–16px |
|
||
| Body | Figtree 400 | 13–15px / 1.45–1.55 |
|
||
| Row label | Figtree 700 | 14–15px |
|
||
| Kicker | Figtree 700, uppercase, letter-spacing .1em | 11–12px |
|
||
| Meta / tag | Figtree 700 | 10–12px |
|
||
| Mono (URLs, keys, system prompt, photo placeholder) | ui-monospace | 11–13px |
|
||
|
||
**Shape:** buttons, inputs, tags, nav, segmented controls are pills (999px). Mobile cards 24–32px radius; list rows 18–24px; inner photo 24px inset 10px from card edge. Phone frame in mocks is 36px (device chrome only).
|
||
|
||
**Elevation:** `--shadow-sm` (segmented selected), `--shadow-md` (round action buttons, tiles), `--shadow-lg` (top swipe card, dragged items).
|
||
|
||
**Icons (Lucide, stroke 2.75):** house, calendar, compass, zap, sliders-horizontal, x, heart, check, chevron-left/right/down, refresh-cw (swap), plus, shopping-cart, message-square, bell, search, archive, users. Nav icons 21–22px; inline 15–18px.
|
||
|
||
---
|
||
|
||
## Layout & navigation
|
||
|
||
### Breakpoints
|
||
- **Mobile < 768px** — design width 360–390px. Bottom nav bar.
|
||
- **Tablet 768–1199px** — design 1180×820 landscape. 96px left icon rail.
|
||
- **Desktop ≥ 1200px** — design 1440×900. 208px left sidebar.
|
||
|
||
### Primary nav (5 items, same order everywhere)
|
||
`Home · Plan · Discover · Quick · Settings`
|
||
- **Mobile bottom bar:** absolute, `left/right 12px`, `bottom 16px` (+ safe-area inset), height 62px, pill, background `--color-text`, `justify-content: space-around`, padding 0 6px. Inactive items: icon only, color `#c0b6a5` (neutral-400). Active item: icon + label (Figtree 700 13px) on a grape pill (height 42px, padding 0 12–14px, `--on-accent` text). **The grape pill is one shared element that slides between items — see Motion.**
|
||
- **Tablet rail:** 96px, `--color-surface` bg. Top: 48px grape circle with "M". Items stacked, gap 18px: 56×36 icon pill (active = grape fill) over an 11px 700 label.
|
||
- **Desktop sidebar:** 208px, `--color-surface`, padding 28px 16px. "MealQuest" wordmark (Bagel Fat One 28px, grape). Items: pill rows 12px 16px, gap 12px, Figtree 700 15px; active = grape fill + on-accent text. Footer card "Our kitchen / Shared household" with 36px teal avatar.
|
||
|
||
### Routes
|
||
| Route | Screen | Mock id |
|
||
|---|---|---|
|
||
| `/` | Home | 1g |
|
||
| `/onboarding` | Add regular dinners (step 2 of 3 shown) | 1h |
|
||
| `/plan` | Weekly plan (draft board) | **1f** (mobile), 2c (tablet), 2d (desktop) |
|
||
| `/plan/shopping` | Shopping list | 1l, 2b, 2f |
|
||
| `/plan/shopping/archive` | Supply archive | 1m, 2f (right panel) |
|
||
| `/discover` | Swipe suggestions | **1a** |
|
||
| `/quick` | Quick dinners (tabs) | 1j |
|
||
| `/cook-from-pantry` | Cook from what I have | 1i |
|
||
| `/dinner/:id` | Recipe / dinner detail | 1k, 2a (tablet reading mode) |
|
||
| `/settings` | Settings home | 1p |
|
||
| `/settings/notifications` | Notifications & Google Chat | 1n |
|
||
| `/settings/ai` | Admin · AI assistant | 1o |
|
||
|
||
Desktop settings (2e) shows Notifications, Google Chat webhook and AI assistant on one page (two columns).
|
||
|
||
**Chosen mobile directions:** Discover = **1a** (card stack), Plan = **1f** (draft board). Mocks 1b, 1c, 1d, 1e are rejected alternatives — ignore them.
|
||
|
||
---
|
||
|
||
## Screens
|
||
|
||
All mobile screens: background `--color-bg`; status-bar area 40px; content side padding 20px; bottom nav overlays the last 94px, so scrollable content needs ≥ 100px bottom padding.
|
||
|
||
### 1g · Home
|
||
- Header: "Friday, Oct 2" (12px 700 neutral-700) over title "Tonight's quest". Right: 40px round bell button (surface) with 8px grape unread dot.
|
||
- Tonight card (surface, radius 32): 150px photo inset 10px, then dinner name (Bagel 22px), meta "Regular · 40 min", right-aligned dark pill button "Recipe" (`--color-text` bg, `--color-bg` text).
|
||
- Two 104px tiles, 2-col grid gap 10: **Quick dinner** (grape fill, zap icon, on-accent text) → `/quick`; **Cook from what I have** (teal-300 fill, teal-900 text, search icon) → `/cook-from-pantry`.
|
||
- Status rows (neutral-100, radius 20, 12px 14px): "10 new side quests to swipe" → Discover; "Shopping list ready · Oct 5–11" → Shopping. Icon in grape-700, chevron right.
|
||
|
||
### 1h · Onboarding — regular dinners
|
||
3 progress bars (6px pills, filled = grape). "Step 2 of 3" kicker, title "What's on your usual rotation?" (30px), helper "Add the dinners your household makes on repeat. MealQuest builds suggestions from these." Input pill (48px, surface) with 36px grape "+" button. "Your regulars · N": chips teal-200/teal-900 with "×". "Common picks": dashed-outline chips "+ Chili" etc. Fixed bottom primary button "Next: notifications" (52px).
|
||
|
||
### 1a · Discover — swipe (CHOSEN)
|
||
- Header: kicker "Discover", title "Side quests"; right pill "Quest log · 6" (teal-200/teal-800).
|
||
- Deck area: margin 18px 20px, height 440px, `perspective: 1000px`. Three stacked cards: depth 0 `none`, depth 1 `translateY(8px) scale(.95)`, depth 2 `translateY(16px) scale(.9)`. Top card bg `#f9f4ed`, shadow-lg; depth 1 surface; depth 2 neutral-300. Radius 32.
|
||
- Card content: photo 220px inset 10px (radius 24); title (Bagel 24px); difficulty = 3 dots 8px (filled grape, empty neutral-300) + label "Easy/Medium/Hard"; "Why this?" box (teal-100, radius 18, 12px 14px, 13px/1.45) — e.g. "You make Chicken stir-fry and Beef tacos most weeks. Same flavors, sweet-savory twist."
|
||
- Stamps (hidden until gesture): "Try it!" top-left, rotate −12°, teal; "Pass" top-right, rotate 12°, neutral.
|
||
- Actions (centered, gap 28, 22px below deck): 62px round **Pass** (neutral-100, X) and **Try it** (teal-600, heart, on-accent icon), 11px labels below.
|
||
|
||
### 1f · Plan — draft board (CHOSEN)
|
||
- Header: "Oct 5 – 11" (26px) + "Draft" tag (grape-100/800). After pushing, tag becomes "Synced".
|
||
- Assistant bubble: 32px grape "Q" avatar + bubble (surface, radius 4 18 18 18): "I mixed 5 of your regulars with 2 from your Quest log. Tap any day to swap it."
|
||
- 2-col grid, gap 8, tiles 86px, radius 22, padding 12: day kicker (10px 700 .1em) + dinner (Bagel 15px). Regular = surface; Quest-log pick = teal-300 with "WED · NEW" kicker (teal-900). Swapping state = bg + 2px grape inset ring, kicker "FRI · SWAPPING…", muted name. 8th tile = dashed "+ Rebuild week".
|
||
- Primary button "Looks good — push to Calendar" (50px).
|
||
- Tapping a tile opens a swap sheet (not mocked; use Quest log + Regulars lists from 2d's right panel as a bottom sheet).
|
||
|
||
### 1j · Quick dinners
|
||
Header with 36px grape zap circle + "Quick dinners". Segmented tabs (surface track, 4px padding; selected = bg + shadow-sm): **Crock pot | 30 min or less**. Helper line per tab ("Prep to table in 30 minutes or less, picked from your tastes." / crock-pot equivalent). Rows (neutral-100, radius 24): 64px round photo, name (Bagel 16px), "25 min · Medium", 36px teal "+" (adds to Quest log). Footer link "↻ Show me more" (grape-700). Nav: Quick active.
|
||
|
||
### 1i · Cook from what I have
|
||
Back button + title. Ingredient chip field (surface, radius 26; chips bg-colored "Chicken thighs ×"; trailing "Add ingredient…"). Segmented "Any time | Quick only" + dark "Find ideas" button. Result cards (neutral-100, radius 24): name + time, teal-800 line "Uses all 5 · missing: 1 lime", buttons "Cook tonight" (grape) and "Add to Quest log" (outline).
|
||
|
||
### 1k · Recipe detail (mobile) / 2a · Kitchen reading mode (tablet)
|
||
Mobile: 250px full-bleed photo with round back button; sheet overlaps by 28px (radius 32 top). Tags (Side quest / Easy / 30 min), title 28px, "Why it was picked" teal box, Ingredients list with qty right-aligned and dividers, sticky bottom "Add to this week" + heart button.
|
||
Tablet 2a: header (back, "Saturday, Oct 10 · Side quest", title 36px) with **Keep screen on** toggle (Screen Wake Lock API) and **text size** segmented A/A/A. Two columns `400px | 1fr`: Ingredients card with 22px checkboxes (checked = struck-through, "3 of 9 set out"); Steps list — completed step muted with check, **current step** highlighted (grape-100 bg, 2px grape-300 ring, number circle grape, text 22px), others 18px. "Previous / Next step" buttons bottom-right.
|
||
|
||
### 1l · Shopping list / 2b tablet / 2f desktop
|
||
Kicker "Supply run · Oct 5–11", title "Shopping list", count "19 / 23". Helper "Uncheck anything you already have — it won't be counted." One card per dinner (surface, radius 24): dinner name (Bagel 16px) + day (11px). Items: 20–22px checkbox (checked = grape fill w/ white check = **to buy**; unchecked = 2px neutral-400 outline, struck-through name, "have it" label). Status line: "Sent to Google Chat · Sat 8:00 am" (teal-800, message icon). Tablet/desktop: CSS `column-count: 3` masonry, actions "Archive" (outline) + "Send to Google Chat" (primary).
|
||
|
||
### 1m · Supply archive (and 2f right panel)
|
||
Segmented "Week | Month | Year". Month headings (Bagel 18px). Post cards: kicker "WEEK 41 · OCT 5 – 11", AI-written title e.g. "Tacos, tikka & a salmon finale", meta "7 dinners · 23 items · 4 already on hand", dinner chips. Current week post = grape-100 + grape-300 ring, "· Current". Tapping a post opens the frozen list (read-only).
|
||
|
||
### 1p · Settings home
|
||
Title "Settings". Household banner (teal-100, 44px avatar "K", "Our kitchen / Shared household · changes apply to everyone"). Grouped list cards (surface, radius 26; rows 14px 16px; 38px round icon; chevron):
|
||
- HOUSEHOLD: Regular dinners ("7 on rotation"), Google Calendar ("Connected · Dinners calendar").
|
||
- ALERTS: Notifications & Google Chat.
|
||
- ADMIN: AI assistant ("Anthropic · Claude Sonnet") + "Admin" tag; card has grape-300 inset ring.
|
||
|
||
### 1n · Notifications & Google Chat
|
||
Back + title. Grid `1fr 46px 46px`: Event | PUSH | CHAT with toggles (40×24 pill; ON teal-600, knob right; OFF neutral-300). Events: New weekly plan ready · Shopping list ready · Day-of dinner reminder ("at 4:30 pm", tappable time) · New suggestions to swipe. Google Chat webhook card: mono URL input (ellipsis), "Connected · Dinner Crew space", "Send test" button. Message preview bubble: "MealQuest · preview — This week's plan is ready: Tacos, Bolognese, Tikka masala + 4 more. Open the plan →".
|
||
|
||
### 1o · Admin · AI assistant
|
||
Back + kicker "Settings · Admin" + title "AI assistant". Provider + Model dropdown pills (2-col). API key: masked mono "sk-••••••••3f9a" + "Replace" chip; "Verified · Oct 1" (teal-800). Temperature slider 0–1 step 0.1 (8px track, grape fill, 24px knob with 3px grape ring), labels "Familiar ↔ Adventurous", value right. System prompt textarea (mono 12px, radius 22). Bottom: "Reset" (outline) + "Save changes" (primary). Store API keys server-side only; the client only ever sees the masked tail.
|
||
|
||
### 2c tablet / 2d desktop · Plan manager
|
||
Header: kicker "Week 41", title "Oct 5 – 11", prev/next circles (+ "This week" pill on desktop), actions "Shopping list · 28", "Rebuild week", "Push to Calendar" (primary). Status "Synced to Google Calendar 2 min ago" (teal-800). 7-column grid (`repeat(7, minmax(0,1fr))`, gap 12): day label + card (photo 96–110px, name Bagel 16–17px, time, crock-pot "Start by 12:30 pm" hint, tag Regular/Side quest, swap icon). Side-quest cards teal-200.
|
||
Desktop right panel (290px, left divider): segmented "Quest log · 6 | Regulars · 9", helper "Drag a dinner onto a day to swap it in.", draggable rows (48px photo, name, difficulty, 6-dot grip). **Drag state:** source row becomes a dashed ghost at 55% opacity; target day shows dashed 2.5px grape border on grape-100 with "Drop to swap / Replaces Pizza night"; dragged card floats with shadow-lg, rotate −3°. Tablet 2c shows the Quest log as a chip strip under the grid with the same drag behavior.
|
||
|
||
### 2e · Desktop settings
|
||
Title + "Changes apply to everyone in the household." Sub-tabs: Household · Regular dinners · **Notifications & AI**. Left column: Notifications matrix card, Google Chat webhook card. Right column: AI assistant card (same fields as 1o, prompt textarea flexes to fill).
|
||
|
||
---
|
||
|
||
## Motion
|
||
A working reference is in **`MealQuest Motion.dc.html`** (open in a browser; tap buttons, drag the card, switch tabs, toggle reduced motion). Implement with WAAPI or Framer Motion using these exact values.
|
||
|
||
### 1. Bottom-nav pill slide
|
||
One absolutely positioned grape pill (`top:10px; height:42px`) lives behind the nav items (items are `position:relative; z-index:1`).
|
||
1. On tap, record each item's `getBoundingClientRect().left` and the pill's current `left/width`.
|
||
2. Commit the new active tab (active item now renders its label, so items reflow).
|
||
3. Measure the active item's `offsetLeft/offsetWidth`; set the pill to it and animate **from old → new `left` + `width`, 420ms, `cubic-bezier(.34,1.4,.64,1)`** (slight overshoot).
|
||
4. FLIP every nav item: animate `translateX(oldLeft − newLeft) → 0` with the same duration/easing so icons glide instead of jumping.
|
||
5. Label fades in: `opacity 0, translateX(-4px) → 1, none`, 220ms ease-out, **120ms delay**.
|
||
6. Icon color crossfades (inactive `#c0b6a5` → `--on-accent`) via `transition: color .2s`.
|
||
Recompute pill position on resize and after fonts load.
|
||
|
||
### 2. "Try it" (accept)
|
||
Triggered by the Try it button (gesture variant below).
|
||
1. Button bump: `scale 1 → .88 → 1.06 → 1`, 300ms ease-out.
|
||
2. "Try it!" stamp pops: `opacity 0, rotate(-12deg) scale(1.5) → opacity 1, rotate(-12deg) scale(1)`, 220ms spring, fill forwards.
|
||
3. **Wiggle:** card `rotate 0 → -4° → 4° → -3° → 2° → 0`, 420ms ease-in-out (transform-origin center).
|
||
4. **Fly off right:** keyframes `translate(0,0) rotate(0)` → at 18% `translate(-14px,4px) rotate(-3deg)` (anticipation) → `translate(150%,-60px) rotate(28deg); opacity 0`. 480ms, `cubic-bezier(.55,0,.8,.4)`, fill forwards.
|
||
5. On finish: add dinner to Quest log (optimistic), bump the "Quest log · N" pill (same bump as step 1), advance the deck (see 5).
|
||
|
||
### 3. "Pass" (decline)
|
||
1. Button bump (as above).
|
||
2. Set `transform-origin: 0% 50%` (left edge — the card folds over like a page).
|
||
3. Card keyframes (720ms total, fill forwards):
|
||
- 0%: `rotateY(0deg)` — easing to next frame `cubic-bezier(.45,0,.85,.55)`
|
||
- 45%: `rotateY(-80deg)` — easing to next frame `cubic-bezier(.4,0,.9,.6)`
|
||
- 100%: `translate(-45%,560px) rotate(-35deg) rotateY(-90deg); opacity 0` (drops and tumbles off left)
|
||
4. Shade overlay inside the card (`#201e1d`, inset 0, radius 32): opacity `0 → .4 (45%) → .55`, 720ms — sells the fold.
|
||
5. Parent deck container has `perspective: 1000px`.
|
||
6. On finish: record the decline (so the AI doesn't re-suggest), advance the deck.
|
||
|
||
### 4. Swipe gesture (top card only)
|
||
- Pointer events with `setPointerCapture`; card has `touch-action: none`.
|
||
- While dragging: `transform: translate(dx, dy*0.3) rotate(dx/18 deg)`. "Try it!" stamp opacity = `clamp(dx/100, 0, 1)`; "Pass" stamp opacity = `clamp(-dx/100, 0, 1)`.
|
||
- Release with **dx > 110px** → accept: skip the wiggle, fly from the current transform to `translate(150%,-60px) rotate(28deg); opacity 0`, 340ms, same throw easing.
|
||
- Release with **dx < −110px** → pass: run the Pass fold, prefixing the first two keyframes with the current drag transform.
|
||
- Otherwise snap back to `translate(0,0) rotate(0)`, 420ms `cubic-bezier(.34,1.4,.64,1)`; reset stamps.
|
||
- Lock input (`busy`) while any exit animation runs.
|
||
|
||
### 5. Deck advance
|
||
After an exit, cancel the exited card's animations, remove it, and promote the others: depth 1→0 and 2→1 animate from their previous depth transform to the new one, **300ms `cubic-bezier(.2,.8,.2,1)`**. Prefetch suggestions so the deck always has ≥ 3 cards.
|
||
|
||
### 6. Reduced motion
|
||
Respect `prefers-reduced-motion: reduce`: nav pill jumps instantly (no FLIP/overshoot); Try it / Pass become a 200ms opacity fade; no wiggle, fold or deck promotion.
|
||
|
||
### Other transitions (not prototyped; keep consistent)
|
||
- Screen push/pop: 260ms `cubic-bezier(.2,.8,.2,1)`, slide 24px + fade.
|
||
- Toggles: knob slides 180ms spring; track color 150ms.
|
||
- Checkbox check: scale `.8 → 1` 160ms spring; strike-through fades in 150ms.
|
||
- Bottom sheets: rise from 100% → 0, 320ms `cubic-bezier(.2,.8,.2,1)`, scrim fade to 50% neutral-900.
|
||
- Buttons: hover = one ramp step darker (`--color-accent-600`… on grape use `#4a2d73` for pressed), pressed `scale(.97)` 100ms. Focus: `outline 2px solid var(--color-accent); outline-offset 2px`.
|
||
|
||
---
|
||
|
||
## State & data
|
||
|
||
### Core entities
|
||
```ts
|
||
type Difficulty = 'easy' | 'medium' | 'hard';
|
||
interface Dinner { id: string; name: string; photoUrl?: string; minutes: number; method: 'stovetop'|'oven'|'crockpot'|'other';
|
||
difficulty: Difficulty; ingredients: Ingredient[]; steps: string[]; source: 'regular'|'suggestion'; }
|
||
interface Ingredient { name: string; qty?: string; }
|
||
interface Suggestion { dinner: Dinner; reason: string; /* "why this?" copy from the LLM, cites regulars */
|
||
status: 'pending'|'accepted'|'declined'; }
|
||
interface WeekPlan { weekStart: string /* ISO Monday */; days: { date: string; dinnerId: string; isNew: boolean }[];
|
||
status: 'draft'|'synced'; calendarSyncedAt?: string; }
|
||
interface ShoppingList { weekStart: string; groups: { dinnerId: string; day: string;
|
||
items: { name: string; qty?: string; needed: boolean /* false = "have it" */ }[] }[];
|
||
sentToChatAt?: string; title: string /* blog post title, AI-generated */; }
|
||
interface Settings { notifications: Record<'planReady'|'listReady'|'dayOfReminder'|'newSuggestions', { push: boolean; chat: boolean }>;
|
||
reminderTime: string; chatWebhookUrl?: string; calendarId?: string;
|
||
ai: { provider: string; model: string; temperature: number; systemPrompt: string; apiKeyLast4?: string; keyVerifiedAt?: string }; }
|
||
```
|
||
`isQuick(d) = d.method === 'crockpot' || d.minutes <= 30` (prep-to-table).
|
||
|
||
### Client state
|
||
- `activeTab` (nav), deck `{ queue: Suggestion[], busy: boolean, drag?: {x,y,dx} }`, `questLogCount`.
|
||
- Plan: `selectedDay` for swap sheet, drag-and-drop source/target on tablet/desktop.
|
||
- Shopping list item toggles are optimistic and persisted per week; once the week ends, the list is frozen into the archive.
|
||
|
||
### Integration points (backend — out of scope, but the UI depends on them)
|
||
- **LLM** — suggestion generation, week drafting, ingredient-based ideas, quick ideas, archive post titles. Uses Admin settings. Never expose the API key to the client.
|
||
- **Google Calendar** — OAuth once (Settings → Google Calendar); "Push to Calendar" creates/updates one event per dinner (one-way).
|
||
- **Google Chat** — server POSTs to the stored incoming-webhook URL for enabled events; "Send test" posts a sample.
|
||
- **Web Push** — VAPID; request permission during onboarding step 3 ("notifications"), not on first load. iOS requires the PWA to be installed to the home screen — show an install hint.
|
||
- **PWA** — manifest (`name: MealQuest`, `theme_color: #5a358c`, `background_color: #f9f6f1`, `display: standalone`), offline cache for the current plan, shopping list and recipes (kitchen use).
|
||
|
||
### Empty / loading / error states (not mocked — follow the system)
|
||
- Deck empty: Bagel title "All caught up", body "New side quests arrive Sunday." + "Get more now" primary.
|
||
- Loading: pill-shaped skeletons in `--color-surface` with a slow 1.2s opacity pulse.
|
||
- Errors: inline teal/grape-toned banner (neutral-100 card, grape-700 text) with a retry button; webhook test failure shows the HTTP status under the URL field.
|
||
|
||
## Accessibility
|
||
- Swipe is never the only path: Pass / Try it buttons always present; also support ← / → keys on Discover.
|
||
- All text ≥ 4.5:1 (on-accent on grape ≈ 9:1). Hit targets ≥ 44px (nav items are 42px tall inside a 62px bar — keep the full bar height tappable).
|
||
- Toggles are `role="switch"`; checkboxes native inputs styled; nav items `aria-current="page"`.
|
||
|
||
## Assets
|
||
- Fonts: Bagel Fat One, Figtree (Google Fonts).
|
||
- Icons: Lucide.
|
||
- Photos: placeholders only — supply real dinner photos and apply the washed treatment.
|
||
- Base system: Organic design system (`_ds/.../styles.css`), overridden by `tokens.css`.
|
||
|
||
## Files
|
||
- `README.md` — this document.
|
||
- `tokens.css` — final design tokens (use these).
|
||
- `MealQuest Mockups v2.dc.html` — all screens. Section 2 = tablet & desktop (2a–2f); section 1 = mobile (1a–1p). Chosen mobile directions are badged "Chosen".
|
||
- `MealQuest Motion.dc.html` — interactive motion reference (nav pill, Try it, Pass, swipe).
|
||
- `screenshots/` — PNG of every chosen screen: `mobile-1a…1p-*.png` (720×1520, 2× of a 360×760 phone), `tablet-2a…2c-*.png` (1180×820), `desktop-2d…2f-*.png` (1440×900). Rejected mobile alternatives (1b–1e) are not included. Note: the static screenshot of 1a doesn't show the two stacked cards behind the top card; see Motion → deck depth transforms.
|
||
- `support.js`, `_ds/organic-…/styles.css` — needed only to open the HTML references locally (serve the folder over a local web server, e.g. `npx serve`).
|