Journey spec — Generate shopping list, shared checklist
Merge ingredients, filter staples. Shared with household.
/* Desktop: 224px sidebar + topbar (title + shared 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. * Sync model: server-authoritative — check/uncheck persists via form action, other users see changes on page refresh. * 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 with household" |
| Badge (desktop) | blue-tint pill in topbar | Compact: dot + "Shared with household" |
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). * 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. * Sync model: server-authoritative. Changes persist via form actions. * Other users see updates on page refresh (no WebSocket/SSE). */
/* Mobile layout:
* topbar (title + settings icon)
* + blue-tint shared banner ("Shared with household", 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 "Shared with household" 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 */
/* Sync rules:
* - Server-authoritative: all mutations (check, uncheck, add) go through form actions / API calls
* - No WebSocket or SSE — other users see changes on page refresh
* - Each check/uncheck is a single server round-trip (form action with use:enhance)
* - Blue accent colour for shared-state UI:
* Mobile: blue-tint banner with blue dot ("Shared with household")
* Desktop: blue-tint badge in topbar with blue dot ("Shared with household") */
/* 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 * * Add custom: * INSERT INTO shopping_list_item (name, quantity, is_custom, is_checked, shopping_list_id) * VALUES (:name, :quantity, true, false, :list_id) */
/* 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 * * 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 */