diff --git a/specs/frontend/j1-add-recipe.html b/specs/frontend/j1-add-recipe.html new file mode 100644 index 0000000..c9d50f1 --- /dev/null +++ b/specs/frontend/j1-add-recipe.html @@ -0,0 +1,394 @@ + + +
+ + +Save recipe with ingredients, steps, tags, and hero image.
/* Desktop: 224px sidebar + topbar (search 220px + Add button) + content: filter chips row + 4-col grid. + * Desktop cards are richer: 100px image area + name (Fraunces 14px) + meta + tags row. + * Mobile: 2-col, 64px image, no tags on cards (too small). + * Filter chips: from tag table. Active: green-tint. 12px border-radius. + * Grid fills the content area directly — no wrapping card. + * Card click → B2 (recipe detail). Add button → B3 (empty form). */+
| Element | Value | Notes |
|---|---|---|
| Desktop | ||
| Grid | 4 columns, 12px gap, content padding 20px 24px | Cards: radius-lg, border-default, overflow hidden |
| Card image | 100px height. hero_image_url → object-fit:cover. NULL → tint + emoji. | Tint color based on first protein tag |
| Card content | 10px 12px padding. Name: Fraunces 14px. Meta: 10px muted. Tags: badge row. | meta shows cook time + effort + "last cooked X ago" |
| Filter chips | 11px/500, 5px 14px pad, 12px radius | Active: green-tint bg + green-dark text. Others: border-default. |
| Mobile | ||
| Grid | 2 columns, 8px gap | Cards: 64px image, 12px name, 9px meta. No tags. |
/* Desktop: 224px sidebar + topbar (breadcrumb + Save) + split content: + * Left (flex:1, page bg): hero upload + name + serves/time/prep (3-col) + ingredients + steps + * Right (280px, surface bg): effort chips + category chips + live preview card + * Form content is NOT in a card — it's directly on the page bg. + * Tags panel (right) uses surface bg as a section differentiator, not as a card. + * Mobile: single scroll, full width. Back + title + Save in topbar. + * Ingredient autocomplete: citext ILIKE from ingredient table. */+
| Element | Value | Notes |
|---|---|---|
| Desktop | ||
| Form area | flex:1, page bg, 24px padding, border-right | Contains: hero + name (16px input) + 3-col row + ingredients + steps |
| Tags panel | 280px, surface bg, 24px padding | Effort chips + category chips + divider + live preview card |
| Live preview | Mini B1 card inside the panel | Updates as user types. Shows name, time, servings, ingredient count, tags. |
B1 (Recipe library) → B3 (Add/edit form) → B1 (Recipe library). Actor: Planner only.
+ +tag table. Active chip = green-tint bg / green-dark text. Inactive chip = border-default. Style: font-size:11px; font-weight:500; padding:5px 14px; border-radius:12px.flex:1, page bg, 24px padding) + tags panel right (280px, surface bg).Form sections:
+Tags (required):
+Desktop right panel: live preview card (mini B1 card) updates as user types — shows name, time, servings, ingredient count, and selected tags.
+Ingredient autocomplete: citext ILIKE query against the ingredient table.
| Screen | Operation | Tables |
|---|---|---|
| B1 | SELECT recipes with tags | recipe JOIN recipe_tag + tag |
| B3 | INSERT / UPDATE recipe | recipe |
| B3 | INSERT / DELETE ingredients | recipe_ingredient |
| B3 | INSERT / DELETE steps | recipe_step |
| B3 | INSERT / DELETE tags | recipe_tag |
Minimum to save: name + effort tag + at least 1 category tag.
+ +surface bg as a section differentiator, not as a card wrapper.<label> elements.role="radiogroup" for single-select effort, role="group" with aria-pressed for multi-select categories.aria-live="polite" to announce updates.Journey spec · Screens C1, C2, C3 · All breakpoints
+Fill day slots, get variety-aware suggestions, review score. C1 is the app home screen.
The weekly planner is the app home screen. It lets the planner see all seven dinner slots for the current week at a glance, select any day to view the full meal, and access swap and cook actions without leaving the screen. For household members (read-only role) all edit and swap actions are hidden — the rest of the layout is identical.
+The core pain point this app solves is meal repetition across the week. The variety score is therefore always on screen on all three breakpoints — it never hides in a tab, modal, or collapsed region. Ingredient repeat warnings appear inline at the tile level on desktop and in the detail panel across all breakpoints.
+Five stacked fixed-height regions: top nav → variety banner → week strip → selected day card → scrollable remaining list. The tab bar is position: fixed at the bottom. Only the remaining days list scrolls.
+7-column grid, each chip 8px top+bottom padding. A 4px dot below the date is the only meal indicator: grey = no meal, green = meal planned, yellow-text = today, green-dark = selected. Today chip: yellow-tint bg + yellow-light border. Selected: green-tint + green-light. Tapping any chip updates the selected day panel immediately below.
+Meal name in Fraunces 20px weight 400. Tags row (time, effort, attributes) then two equal-width action buttons. shadow-card elevation. When today is selected: 2px yellow border + yellow-tint background — the only 2px border in the system.
+Always the first element below the nav. Never collapses. Score in Fraunces 28px weight 300. Warning row (yellow-text colour, yellow dot) appears when any ingredient appears in 2+ meals this week.
+Shows only days after the selected day. Date column: 36px wide. Meal name: Fraunces 14px. Meta: 11px muted DM Sans. Swap button: right-aligned 11px. Rows separated by 1px color-subtle dividers. Empty days are not shown — their absence is visible only via the dot in the strip.
+The tablet uses the same five-region stack and the same interaction model as mobile. Key differences: the selected meal card expands to a two-column layout (info left, action buttons right); the remaining days list becomes a 2-column card grid; the variety banner gains a labelled progress bar; and the bottom navigation becomes an inline horizontal pill bar rather than a fixed screen overlay. A "Today" shortcut chip appears between the prev/next week buttons.
+Two-column layout: meal info (name 22px, tags, description) on the left, three stacked full-width action buttons on the right (min-width 120px). Description line is tablet and desktop only.
2-column CSS grid with 8px gap. Each card: date column (30px) + 1px vertical divider + meal name + meta + right arrow. Empty slots: dashed border, transparent bg, + icon right-aligned.
Bottom nav becomes inline (not fixed), rendered as horizontal labelled pill items. Active item: green-tint bg + green-dark text. A "Today" shortcut chip sits between the prev/next week buttons.
Desktop is a fundamentally different layout. The screen switches from a vertical single-column stack to a three-panel horizontal model: left sidebar (224px, fixed height, sticky), main calendar area (flex 1, only panel that scrolls), and right detail panel (280px, fixed). The day strip is replaced entirely by a 7-column calendar grid. The bottom tab bar is replaced by a persistent left sidebar nav. This is a deliberate layout change — not responsive scaling.
+Sidebar 224px fixed + calendar flex 1 scrollable + detail panel 280px fixed. Sidebar and detail panel never scroll. Only the calendar area scrolls vertically. All three are 100% viewport height.
Each tile uses flex: 1 to fill its column. Column header has a 2px bottom border — yellow for today, green for selected. Tiles with repeated ingredients show a badge-warn chip (rgba(242,193,46,.25) bg). Empty slots: dashed border, centred + icon.
Bottom of the sidebar. Never scrolls out of view. Includes score, bar, message, ingredient-specific warning, and a "Review variety" button linking to screen C3.
All values are real production sizes. Previews above are scaled for visual presentation only — never measure from them. Use tokens from color-palette-spec v1.0, typography-spec v1.0, and spacing-shape-spec v1.0.
+ +/* C1 Implementation rules + * 1. Default route on first load. App home screen. + * 2. Variety score ALWAYS VISIBLE on all breakpoints. Never hidden or in a tab/modal. + * 3. Today chip: background yellow-tint (#FDF6D8), border 1px yellow-light (#F9E08A). + * 4. Selected chip: background green-tint (#E8F5EA), border 1px green-light (#AEDCB0). + * 5. Today meal card (mobile/tablet): 2px solid yellow (#F2C12E) border + yellow-tint bg. + * Today tile (desktop): same 2px yellow border. This is the ONLY 2px border in the system. + * 6. Meal names: always --font-display (Fraunces). All UI chrome: --font-sans (DM Sans). + * 7. Chip dot: --color-border = no meal. --green = meal. --yellow-text = today. --green-dark = selected. + * 8. Desktop: sidebar position sticky, height 100vh. Detail panel also sticky. Only cal-area scrolls. + * 9. Ingredient repeat warnings: badge-warn (rgba(242,193,46,.25) bg + --yellow-text color) on tiles. + * In detail panel: repeat rows use --yellow-text for name AND quantity text. + * 10. Household member role: hide swap buttons, hide "+ Add meal" button. Show read-only badge. + * 11. Empty day slot mobile: not shown in the remaining days list (only shown via dot in strip). + * Empty day slot tablet/desktop: dashed 1px --color-border, transparent bg, centred + (18px --color-border). + * 12. Meal description text (13px DM Sans muted, line-height 1.5): tablet + desktop only. Hidden on mobile. + */+ +
| Region / element | Real production value | Notes | Breakpoints |
|---|---|---|---|
| Layout & breakpoints | |||
| Mobile breakpoint | < 768px | Single-column vertical stack, fixed bottom tab bar, body full-width | mobile |
| Tablet breakpoint | 768px – 1024px | Same stack structure, wider panels, inline bottom nav. No fixed tab bar. | tablet |
| Desktop breakpoint | > 1024px | 3-panel horizontal: sidebar + calendar + detail panel | desktop |
| Desktop sidebar width | 224px fixed | position: sticky; height: 100vh. Contains logo, nav, variety score. | desktop |
| Desktop detail panel width | 280px fixed | position: sticky; height: 100vh. Hidden on mobile and tablet. | desktop |
| Desktop calendar gap | gap: 8px between columns (--space-2) | grid-template-columns: repeat(7, 1fr) | desktop |
| Desktop calendar padding | 20px all sides (--space-5) | Inside the scrollable cal-area div | desktop |
| Mobile page gutter | 16px each side (--space-4) | Applied to all content regions individually, not a wrapper | mobile |
| Tablet page gutter | 20px–24px each side (--space-5 / --space-6) | Variety banner and selected card: 20px. Week header and strip: 20px. | tablet |
| Tablet remaining grid gap | 8px (--space-2) | 2-column CSS grid | tablet |
| Navigation | |||
| Mobile top nav padding | 10px 16px (--space-2 --space-4) | position: sticky; top: 0; z-index: 10; border-bottom: --border-default | mobile |
| Mobile top nav title | Fraunces 20px weight 500, letter-spacing -0.02em | "This week" — updates to week range on scroll if implemented | mobile |
| Tablet top nav padding | 14px 20px (--space-3 --space-5) | Title: Fraunces 24px weight 500. Right side adds "+ Add meal" button. | tablet |
| Desktop topbar padding | 10px 24px (--space-2 --space-6) | Title: Fraunces 20px weight 500. Week nav inline left. Today + Add meal right. | desktop |
| Desktop sidebar nav item | padding: 7px 6px; font-size: 13px; border-radius: --radius-md (6px) | Active: green-tint bg + green-dark text + weight 500. Icon 16px, 20px wide. | desktop |
| Mobile tab bar | position: fixed; bottom: 0; padding-bottom: 20px (safe area) | 4 items. Active icon: green-tint bg. Active label: green-dark color. Label: 10px DM Sans weight 500. | mobile |
| Tablet nav bar | position: static; inline at bottom; padding: 10px 20px 14px | Horizontal labelled pills. Active: green-tint bg + green-dark text. | tablet |
| Nav prev/next buttons | DM Sans 11–12px, padding: 4–5px 8–10px, radius-sm (4px), color-subtle bg, border-default | Mobile: icon-only (⟨ ⟩) 32x32px. Tablet/desktop: text "‹ Prev" / "Next ›". | all |
| "Today" shortcut chip | DM Sans 10–11px weight 500, color-subtle bg, border-default, radius-sm | Between prev/next buttons. Tablet and desktop only. | tablet, desktop |
| "+ Add meal" button | DM Sans 12–13px weight 500, green bg (#3D8C4A), white text, radius-md (6px), padding 7–8px 14–18px | Planner role only. Hidden for household members. | tablet, desktop |
| Variety score | |||
| Component background | yellow-tint (#FDF6D8) bg, 1px yellow-light (#F9E08A) border, radius-lg (10px) | Identical on all breakpoints | all |
| Mobile position | margin: 16px 16px 0 (top + sides). First content after nav. | padding: 8px 16px (--space-2 --space-4). Never collapses. | mobile |
| Tablet position | margin: 14px 20px 0. padding: 12px 16px. | Same structure, slightly larger | tablet |
| Desktop position | Bottom of sidebar. margin: 0 8px 14px. padding: 10px 12px. | Always visible. Never scrolled by any panel. | desktop |
| Score number | Fraunces 28px mobile / 28px tablet / 40px desktop, weight 300, letter-spacing -0.02em, line-height 1 | Followed inline by denominator "/10" in DM Sans 12–16px muted | all |
| Eyebrow label | DM Sans 8–10px, weight 500, letter-spacing 0.08–0.1em, uppercase, --yellow-text (#8A6800) | Always first text inside the component | all |
| Status message | DM Sans 9–11px, --color-text-muted | E.g. "Good variety this week" | all |
| Warning row | DM Sans 9–11px, --yellow-text, flex row, 4–5px yellow dot (border-radius 50%) | Only shown when any ingredient appears in 2 or more meals | all |
| Progress bar | height: 3–5px, track: yellow-light, fill: yellow, radius: 99px | Width = variety score as percentage (score 8 = 80%) | all |
| "Review variety" button | DM Sans 10px weight 500, color-page bg, yellow-light border, yellow-text color, radius-sm, padding 5px | Desktop sidebar only. Links to screen C3 (variety review). | desktop |
| Day strip (mobile + tablet) | |||
| Strip grid | grid-template-columns: repeat(7, 1fr); gap: 2–3px mobile / 5–6px tablet | padding: 0 16px 8–12px | mobile, tablet |
| Chip padding | 5–8px top+bottom / 1–4px left+right | border-radius: --radius-md (6px) mobile / --radius-lg (10px) tablet | mobile, tablet |
| Chip abbreviation | DM Sans 7–9px, weight 500, tracking 0.05–0.06em, uppercase, --color-text-muted | 2 letters on mobile (Mo Tu), 3 letters on tablet (Mon Tue) | mobile, tablet |
| Chip date number | DM Sans 11–14px, weight 500, --color-text, line-height 1–1.1 | mobile, tablet | |
| Chip dot | 3–4px diameter circle, border-radius: 50% | No meal: --color-border. Has meal: --green (#3D8C4A). Today: --yellow-text (#8A6800). Selected: --green-dark (#2E6E39). | mobile, tablet |
| Today chip | background: --yellow-tint (#FDF6D8); border: 1px solid --yellow-light (#F9E08A) | Abbreviation colour: --yellow-text. Dot colour: --yellow-text. | mobile, tablet |
| Selected chip | background: --green-tint (#E8F5EA); border: 1px solid --green-light (#AEDCB0) | Abbreviation colour: --green-dark. Dot colour: --green-dark. | mobile, tablet |
| Calendar grid (desktop only) | |||
| Column header day name | DM Sans 9–10px, weight 500, tracking 0.08em, uppercase, --color-text-muted | margin-bottom: 3px | desktop |
| Column header date badge | 24x24px, border-radius: 4px (--radius-sm), DM Sans 12–14px weight 500, --color-text | Today col: yellow (#F2C12E) bg. Selected col: green-tint bg + green-dark text. Default: no bg. | desktop |
| Column header bottom border | 2px solid | Default: --color-border. Today column: --yellow (#F2C12E). Selected column: --green (#3D8C4A). | desktop |
| Tile padding | 8px 8px 10px (top / sides / bottom) | border-radius: --radius-lg (10px). flex: 1 to fill column height. | desktop |
| Tile eyebrow | DM Sans 8–9px, weight 500, tracking 0.06–0.08em, uppercase | Default: --color-text-muted. Today tile: --yellow-text. Selected tile: --green-dark. | desktop |
| Tile meal name | Fraunces 11–13px, weight 400, letter-spacing -0.01em, line-height 1.3 | No text truncation — tile height grows to fit | desktop |
| Tile at rest | background: --color-surface; border: 1px --color-border; box-shadow: --shadow-card | Hover: border-color --green-light; box-shadow: --shadow-raised | desktop |
| Today tile | border: 2px solid --yellow (#F2C12E); background: --yellow-tint (#FDF6D8) | ONLY 2px border in the system | desktop |
| Selected tile | border: 2px solid --green (#3D8C4A); background: --green-tint (#E8F5EA) | Also 2px — matches today tile treatment | desktop |
| Ingredient repeat badge | background: rgba(242,193,46,0.25); color: --yellow-text (#8A6800); DM Sans 10px weight 500; padding: 2px 6px; border-radius: 3px | Replaces or supplements normal tags. Shows "⚠ [ingredient]". | desktop |
| Empty day tile | border: 1px dashed --color-border; background: transparent; box-shadow: none; min-height: 60px | Centred content: + icon (16–18px --color-border) + "Add meal" label (9–10px --color-border) | desktop |
| Selected day / detail panel | |||
| Mobile: selected area padding | 16px all sides (--space-4) | Section above rule: eyebrow label left, date right | mobile |
| Mobile: meal card padding | 10–16px all sides | background: --color-surface; border: 1px --color-border; radius: --radius-lg (10px); box-shadow: --shadow-card | mobile |
| Meal name — mobile | Fraunces 20px, weight 400, letter-spacing -0.02em, line-height 1.25, margin-bottom: 8px | --color-text | mobile |
| Meal name — tablet | Fraunces 22px, weight 400, same other values | --color-text | tablet |
| Meal name — desktop detail panel | Fraunces 17px, weight 400, line-height 1.3, margin-bottom: 6px | Smaller because panel is narrower (280px) | desktop |
| Meal description | DM Sans 13px, --color-text-muted, line-height 1.5 | Tablet + desktop only. Hidden on mobile. | tablet, desktop |
| Actions — mobile | 2 buttons, flex row, equal width, DM Sans 12px weight 500, padding: 8px 12px, radius-md | "Cook now" (green primary) + "Swap meal" (ghost) | mobile |
| Actions — tablet | 3 buttons, stacked column in right half of card (min-width 120px), DM Sans 12px, padding: 7px 12px | "Cook now" (primary) + "View recipe" (outline) + "Swap meal" (ghost) | tablet |
| Actions — desktop | 3 buttons, stacked column, full panel width (280px minus padding), DM Sans 11px, padding: 6px | "View recipe" (primary) + "Cook mode" (outline) + "Swap meal" (ghost) | desktop |
| Desktop ingredient rows | padding: 4px 0 per row; border-bottom: 1px --color-subtle; DM Sans 11px name + 10px muted qty | Repeat ingredient rows: both name and qty use --yellow-text (#8A6800) | desktop |
| Remaining days list / grid | |||
| Mobile list item padding | 6–12px top + bottom (--space-2 / --space-3) | Separated by 1px --color-subtle bottom border. No left/right border. | mobile |
| Date column — mobile list | width: 26–36px, flex-shrink: 0 | Abbreviation: 7–9px DM Sans uppercase muted. Number: Fraunces 13–17px weight 300. | mobile |
| Row meal name — mobile | Fraunces 11–14px, weight 400, letter-spacing -0.01em, --color-text | white-space: nowrap; overflow: hidden; text-overflow: ellipsis | mobile |
| Row metadata — mobile | DM Sans 9–11px, --color-text-muted | Format: "{time} · {attribute}" e.g. "25 min · Easy" | mobile |
| Swap button | DM Sans 9–11px, padding: 3–4px 6–8px, radius-sm (4px), border-default, color-surface bg | Right-aligned. Planner role only — hidden for household members. | mobile, tablet |
| Tablet grid | grid-template-columns: 1fr 1fr; gap: 8px (--space-2); padding: 0 20px 16px | Each card: radius-lg, shadow-card, flex row. Date col (30px) + 1px vertical divider + content + arrow. | tablet |
| Empty slot — tablet grid | border: 1px dashed --color-border; background: transparent; box-shadow: none | Right side: + icon (16px --color-border). Left: date col + divider as normal. | tablet |
| Empty slot — mobile | Not shown in the remaining list | Absence is communicated via grey dot in the day strip only. | mobile |
/* Desktop: 224px sidebar (variety score at bottom) + topbar + split content: + * Left panel (280px, surface bg): week-so-far mini cards + filter reason bullets + * Right panel (flex:1, page bg): ranked suggestion cards with Pick action + * The context panel is a page section, not a floating card. + * Mobile: context banner (green-tint, collapsible) + ranked list below. + * Pick → writes week_plan_slot → returns to C1. + * "Browse full library" → B1 in selection mode. */+
| Element | Value | Notes |
|---|---|---|
| Desktop | ||
| Context panel | 280px, surface bg, border-right | Week meals + current slot (yellow) + filter reasons |
| Suggestions | flex:1, page bg, 20px padding | Same sg-card component as mobile but wider |
| Mobile | ||
| Context banner | green-tint, radius-lg, 10px 12px pad | Day label + filter summary. Collapsed on mobile. |
/* Desktop: sidebar + topbar (breadcrumb ← Planner / Variety review) + content: + * Top area: 2-col flex. Left: big score (72px) + progress bar + sub-score rows. Right: 7-day protein grid + effort bar. + * Bottom area: full-width warning cards. + * Content sits directly on page bg — no wrapper card. + * Mobile: stacked. Score → sub-scores → warnings. No protein grid (too small). + * Score is COMPUTED (variety CTE from data model), never stored. */+
| Element | Value | Notes |
|---|---|---|
| Desktop | ||
| Score area | flex:1. Score: Fraunces 72px/300. Bar: 200px wide, 6px, yellow. | Sub-scores: bordered rows, not in a card. |
| Protein grid | 320px. 7-col grid, 6px gap. Cells: 44px height, colored by protein. | Repeated protein: 2px yellow border on matched cells (Mon + Thu chicken). |
| Effort bar | Proportional flex row. Green/yellow/red segments. | Labels: "Easy ×3", "Medium ×3", "Hard ×1" |
| Warnings | Full width, yellow-tint, radius-lg | Title 13px/500 + body 12px. Below the 2-col area. |
C1 (planner) → C2 (suggestions) → C1 → C3 (variety review) → C1. The actor is the Planner only. Household members see C1 in read-only mode (all action buttons hidden). C1 is the app home screen and the default route.
+ +Recipe detail → cook mode → mark as cooked. Kitchen context.
/* Desktop: sidebar + topbar (breadcrumb + Edit button) + hero banner (full width, green-tint) + 2-col below. + * Hero: name Fraunces 28px + tag pills + description + Cook button on the right. + * Below hero: ingredients (left, border-right) + steps (right). Both panels scroll independently. + * Hero with image: hero_image_url as bg, 40% dark overlay, text in white. + * Mobile: hero banner → stacked ingredients → steps. + * "Start cooking" → B4. "Edit" → B3 in edit mode. */+
| Element | Value | Notes |
|---|---|---|
| Desktop hero | ||
| Layout | Full content width, green-tint bg, 32px padding | Flex: info left (flex:1) + Cook button right (flex-shrink:0) |
| Name | Fraunces 28px/500, green-deeper | With image: #fff on dark overlay |
| Desktop content below hero | ||
| Ingredients | flex:1, 24px padding, border-right | ir-row component. Scrolls independently. |
| Steps | flex:1, 24px padding | Numbered circles (28px) + 14px body text, 1.6 line-height. |
/* FULL SCREEN on all breakpoints. NO sidebar. NO tabs. NO nav chrome. + * Body text: EXACTLY 16px, line-height 1.75. NON-NEGOTIABLE. + * Max text width: 260px mobile, 320px tablet, 400px desktop. + * Step number: Fraunces 56px mobile, 72px desktop. Green-light. + * TAP ANYWHERE to advance. Wake lock on enter. Exit = top-left error red. + * Final step: "Done — mark as cooked" → cooking_log INSERT → C1. + * This is an EXCEPTION to nav-spec: no sidebar, no breadcrumbs, no tabs. */+
| Element | Value | Notes |
|---|---|---|
| Layout — IDENTICAL all breakpoints | ||
| Body text | 16px, line-height 1.75 | NON-NEGOTIABLE |
| Max text width | 260px mobile / 320px tablet / 400px desktop | Only difference between breakpoints |
| Step number | Fraunces 56px mobile / 72px desktop, weight 300, green-light | Visible from arm's length |
| Tap target | Entire body area | onclick → next step |
| Wake lock | navigator.wakeLock.request('screen') | On enter, release on exit/done |
This section provides structured guidance for code-generating LLMs implementing the J3 journey. Follow these rules exactly.
+ +C1 (today highlight) → B2 (recipe detail) → B4 (cook mode) → C1
+flex:1) and Cook button right (flex-shrink:0).flex:1, border-right) + steps right (flex:1). Both panels scroll independently.hero_image_url as background, 40% dark overlay, all text rendered in white..ir rows (quantity + name). Quantities are scaled to the saved serving count for the planned meal.This screen is the single exception to all responsive and navigation patterns in the app.
+var(--color-error)) + progress indicator (center, "Step N of M") + empty spacer (right).var(--color-subtle). Fill: var(--green). Border-radius: 2px.(currentStep / totalSteps) * 100%.var(--green-light). Size: 56px (mobile) / 72px (desktop).cooking_log INSERT with recipe_id and cooked_date set to today's date, then navigates back to C1.navigator.wakeLock.request('screen') to prevent the screen from sleeping.The cooking_log table is the data source for the variety/repetition algorithm used in J2 (meal suggestions). Meals cooked more recently are weighted more heavily in the repetition filter, causing the algorithm to deprioritize them in future suggestions. This means J3's "mark as cooked" action directly makes J2's suggestions smarter over time.
| Screen | Operation | Tables |
|---|---|---|
| B2 | READ | recipe, recipe_ingredient, ingredient, recipe_step |
| B4 | WRITE | cooking_log INSERT (recipe_id, cooked_date) |
Mid-week swap in ≤ 3 taps. Action sheet → pick replacement → done.
/* Mobile: tap meal card → bottom action sheet. Planner dims to 40%. + * Sheet: drag handle + meal name + meta + 4 action buttons stacked. + * Swap = orange-tint. Cook = green-tint. View = subtle. Cancel = no bg. + * Desktop: no action sheet. C1 detail panel has "Swap meal" ghost button (per planner-spec). + * Clicking "Swap meal" transitions the detail panel to show swap suggestions inline. + * Tap count: Mobile 3 (card → Swap → Pick). Desktop 2 (Swap → Pick). */+
/* Mobile: bottom sheet over dimmed C1. "Replacing" banner + suggestion list. + * Sorted EASIEST FIRST (effort ASC, cook_time ASC) — different from J2. + * "Pick" → UPDATE week_plan_slot. Dismiss sheet. No confirmation dialog (undo toast instead). + * Desktop: detail panel (280px) transitions in-place. Calendar grid stays visible. + * Replacing header: orange-tint, old meal struck through. + * Suggestion cards: compact, fitting panel width. Name + meta + "Pick" link. + * Tap count: Mobile 3. Desktop 2 (faster — no action sheet intermediary). */+
C1 → action sheet (mobile) or detail panel button (desktop) → swap suggestions → pick → C1. + Actor: Planner. Frequency: 1-2x/week. Urgency: HIGH.
+ +From "Swap" to updated plan in no more than 3 taps.
+var(--color-border) background.orange-tint bg / orange-dark textgreen-tint bg / green-dark textsubtle bg / muted textorange-tint background, old meal name struck through.effort ASC, cook_time ASC. This is DIFFERENT from C2 in J2, which sorts by variety score.UPDATE week_plan_slot with new recipe_id → dismiss sheet → show undo toast (NOT a confirmation dialog).effort ASC, cook_time ASC) and "Replacing" header as mobile.Mid-week swaps typically happen because the original plan was too ambitious. Sorting by effort makes the fastest, lowest-effort options most visible, matching the user's intent to simplify.
+ +week_plan_slot UPDATE — sets new recipe_id on the slot.orange-tint background, orange-dark text on banners and primary action.Journey spec — Generate shopping list, real-time shared checklist
+Merge ingredients, filter staples. Always live and shared with household.
/* Desktop: 224px sidebar + topbar (title + shared status badge) + 2-col content: + * Left (flex:1, page bg): remaining count + checklist rows + "checked off" section + add custom + * Right (280px, surface bg): recipe reference cards + filtered staples list + edit staples link + * Recipe reference panel: page-section with surface bg, not a floating card. + * Mobile: full-width checklist + shared banner + bottom tabs. + * Real-time sync: is_checked updates broadcast to all connected clients. + * Both roles: planner + member can view and check off. Only planner can regenerate. */+
| Element | Value | Notes |
|---|---|---|
| Desktop | ||
| Checklist area | flex:1, page bg, 20px 24px padding | Remaining items + divider + checked items |
| Recipe reference | 280px, surface bg, border-left | Recipe name + day + ingredient count. Filtered staples below. |
| Shared state | ||
| Banner (mobile) | blue-tint, blue dot, radius-lg | "Shared · N online" |
| Badge (desktop) | blue-tint pill in topbar | Compact: dot + "N members online" |
Authoritative implementation reference for the shopping list journey. Covers screen D1 and references A3/D3 (staples). Use this when building or modifying the shopping list feature.
+ +/* J5 flow + * C1 (week confirmed) → D1 (shopping list, always live). + * Actor: Planner generates the list. All household members shop (view, check off, add items). + * Preconditions: J1 (recipes exist) + J2 (week is planned) for generating a list. + * J6 (household setup) for shared access. + * There is NO draft/publish workflow — the list is always live. */+ +
/* Mobile layout:
+ * topbar (title + settings icon)
+ * + blue-tint shared banner ("Shared with household · N members online", blue dot)
+ * + checklist (unchecked items, then checked items below divider)
+ * + "+ Add custom item" link (blue-dark, centred)
+ * + bottom tabs (Planner | Recipes | Shopping [active] | Settings)
+ *
+ * Desktop layout:
+ * sidebar (224px, dsb) + topbar (dtb: title + blue-tint "N members online" badge)
+ * + split content area:
+ * Left: checklist (flex:1, page bg, 20px 24px padding)
+ * - eyebrow "N items remaining · N checked off"
+ * - unchecked rows
+ * - border-top divider → "Checked off" eyebrow (opacity .6) → checked rows
+ * - "+ Add custom item" link (12px, blue-dark, font-weight 500)
+ * Right: recipe reference panel (280px, surface bg, border-left, 20px padding)
+ * - eyebrow "This week's recipes"
+ * - recipe cards: page bg, border, radius-md, 10px padding
+ * - recipe name (13px/500) + day + ingredient count (10px muted)
+ * - border-top divider → "Filtered staples" eyebrow
+ * - staple names inline (11px muted, dot-separated)
+ * - "Edit staples →" link (11px, blue, font-weight 500) — links to D3
+ *
+ * Checklist row (.ck):
+ * checkbox (.ck-b, 22px, radius 4px, 2px border)
+ * + content (.ck-c): name (.ck-n, 14px) + source (.ck-s, 10px muted, "For: [recipe names]")
+ * + quantity (.ck-q, mono 12px muted, flex-shrink 0)
+ *
+ * Checked state (.ck.d):
+ * checkbox fills green with white checkmark
+ * name gets line-through + muted colour
+ * row moves below divider into "Checked off" section */
+
+ /* Real-time rules: + * - is_checked updates broadcast to ALL connected clients instantly + * - "N members online" indicator shows who is currently viewing the shopping list + * - Prevents double-buying when multiple family members shop simultaneously or at different times + * - Blue accent colour for all shared-state UI: + * Mobile: blue-tint banner with blue dot + * Desktop: blue-tint badge in topbar with blue dot + * - WebSocket or SSE for real-time — implementation choice, but must be instant */+ +
/* Generate list: + * SELECT ri.ingredient_id, i.name, SUM(ri.quantity), ri.unit + * FROM recipe_ingredient ri + * JOIN ingredient i ON ri.ingredient_id = i.id + * JOIN week_plan_slot wps ON wps.recipe_id = ri.recipe_id + * WHERE wps.week = :current_week + * AND i.is_staple = false + * GROUP BY ri.ingredient_id, i.name, ri.unit + * + * Check off: + * UPDATE shopping_list_item + * SET is_checked = true/false + * WHERE id = :item_id + * → broadcast change to all connected clients via real-time channel + * + * Add custom: + * INSERT INTO shopping_list_item (name, quantity, is_custom, is_checked, shopping_list_id) + * VALUES (:name, :quantity, true, false, :list_id) + * → broadcast new item to all connected clients */+ +
/* Precondition chain: + * J1 (recipes exist) — cannot generate a shopping list without recipes + * J2 (week is planned) — cannot generate a shopping list without planned meals + * J6 (household setup) — required for shared access (multiple members online) + * + * If no meals are planned: show empty state on D1 with prompt to plan the week first + * If no household members: list works for solo planner, shared banner is hidden */+
One-time onboarding journey · Screens A1, A2, A3/D3, A4
+One-time onboarding. Account creation → household naming → staples → invite.
/* Pre-auth. No nav chrome. Desktop: full-viewport 2-col split. Brand ~42% / Form ~58%. + * Mobile: brand as ~120px banner, form below. + * Brand section: green-dark bg. Contains logo, tagline, 3 feature icons. + * Form section: page bg. Form max-width 380px. Not a card — just content on the page. + * Writes: user_account INSERT → redirect A2 */+
| Element | Value | Notes |
|---|---|---|
| Desktop layout | ||
| Brand panel | 440px fixed, green-dark bg, content centered vertically | Logo 64px + name Fraunces 36px + tagline 15px + 3 feature icons |
| Form panel | flex:1, page bg, content centered vertically, 48px 56px padding | Form max-width 380px. Not wrapped in a card. |
| Mobile layout | ||
| Brand banner | ~120px, green-dark bg, centered | Logo 28px + name 22px + tagline 12px |
| Form | Full width, 24px 20px padding | Same fields, smaller type sizes |
| Data | ||
| Writes | user_account INSERT | system_role='user', is_active=true → A2 |
/* Desktop: progress sidebar (300px, surface bg) + form area (flex:1, page bg). + * Progress sidebar shows: app logo + 3 numbered steps (current = green circle, future = subtle). + * Form area: content centered vertically, max-width 420px. Not a card — content on page bg. + * Mobile: full-width form with step indicator text at top. No progress sidebar. + * Writes: household INSERT + household_member INSERT (role=planner) → A3 */+
| Element | Value | Notes |
|---|---|---|
| Desktop layout | ||
| Progress sidebar | 300px, surface bg, border-right | App logo top + numbered steps. Current step: green circle. |
| Form area | flex:1, page bg, centered content | max-width 420px. Larger input (16px, 12px padding). |
| Step circles | 28px diameter. Active: green bg #fff text. Future: subtle bg muted text. | Labels: 13px name + 11px description below |
| Mobile | ||
| Step indicator | "Step 1 of 3" eyebrow text | No sidebar, text-only progress |
| Form | Full width, 24px 20px padding | Same fields |
/* Desktop onboarding: progress sidebar (300px, step 2 active) + content area with 2-col category grid. + * Categories flow in a CSS grid (2 columns, 24px row gap, 32px col gap). + * Chips sit directly on the page bg — no card wrappers. + * Desktop settings (D3): same chip grid but inside sidebar+topbar layout (no progress sidebar). + * Mobile: single-column chip list, full width. + * Chip toggle: UPDATE ingredient SET is_staple. Debounced 300ms. */+
| Element | Value | Notes |
|---|---|---|
| Desktop onboarding | ||
| Progress sidebar | 300px, surface bg. Step 1 = ✓ completed. Step 2 = green active. | Same sidebar as A2, reused component. |
| Category grid | grid-template-columns: 1fr 1fr. gap: 24px 32px. | Categories from ingredient_category, sorted by sort_order. |
| Chips | 12px/500, 6px 12px pad, 20px radius. Selected: green-tint/green-light/green-dark. | Direct on page bg. No card wrapper. |
| Desktop settings (D3) | ||
| Layout | App sidebar (224px) + topbar ("Staples") + content with 3-col grid | Wider content area → 3 columns for categories. |
/* Desktop: split layout. Left (400px, green-tint bg): household identity + permissions list. + * Right (flex:1, page bg): signup form max-width 380px. + * Left panel uses green-tint (welcoming) not green-dark (brand-first). + * Mobile: green-tint banner at top + form below. + * Transaction: user_account INSERT + household_member INSERT (role=member) + household_invite UPDATE → C1 */+
| Element | Value | Notes |
|---|---|---|
| Desktop | ||
| Identity panel | 400px, green-tint bg, centered content | Logo 64px + name Fraunces 32px + inviter + permissions list |
| Form panel | flex:1, page bg, max-width 380px | Same signup fields as A1 |
This journey is completed once when the app is first used. The planner creates an account, names the household, defines pantry staples, and invites household members.
+ +user_account → redirects to A2.household + household_member (role=planner) → redirects to A3.ingredient.is_staple. Same component as D3 in settings.user_account + household_member (role=member) + updates household_invite → redirects to C1.| Role | Created at | Access |
|---|---|---|
planner | A2 (automatic) | Full access to all 15 screens |
member | A4 (on invite accept) | C1 read-only + D1 view/check/add |
--green-dark bg (brand-first). A4 identity panel: --green-tint bg (welcoming).--green-tint bg + --green-light border + --green-dark text.| Screen | Writes | Reads |
|---|---|---|
| A1 | user_account INSERT | — |
| A2 | household INSERT, household_member INSERT (role=planner), household_invite INSERT (optional) | — |
| A3/D3 | ingredient UPDATE (is_staple) — debounced 300ms | ingredient SELECT grouped by ingredient_category |
| A4 | user_account INSERT, household_member INSERT (role=member), household_invite UPDATE | household_invite SELECT (validates code), household SELECT (name, inviter) |
<form>, <label> with for, visible focus indicators.role="checkbox" or <input type="checkbox"> with visual styling.aria-current="step" on the active step.