Frontend: A3/D3 — Pantry staples component (onboarding + settings) #20
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Toggle staple ingredients by category. Ingredients marked as staples are excluded from generated shopping lists. A3 and D3 are the same component — shown during onboarding (step 2 of 3) and accessible from settings at any time.
Journey: J6 (onboarding) + J5 (shopping settings)
Role: Planner
Design rule: A3 = D3 — design and build once.
Categories
Oils & fats, Spices, Grains & pasta, Sauces, Baking, Dairy & basics.
A default list of common staples is pre-selected.
Chip Design
--green-tintbg,--green-lightborder,--green-darktext--color-surfacebg,--color-borderborderLayout
Mobile (< 768px)
Desktop — Onboarding (A3)
Desktop — Settings (D3)
Behavior
is_stapleAcceptance Criteria
Spec file:
specs/frontend/j6-household-setup.html— screen A3/D3 with mobile + desktop previews (both onboarding and settings contexts), agent table, and LLM implementation guide.👨💻 Kai — Frontend Engineer
Questions & Observations
Context switching via prop: The component must know whether it's in onboarding (2-col) or settings (3-col) context. I'd expect a
context: 'onboarding' | 'settings'prop that drives the grid class. Is that the intended API, or should the grid be purely CSS-driven via a container query?Data flow: Where does the initial staple state come from? It must be loaded server-side in
+page.server.ts(not client-side$effect) so the first render is correct with SSR. The component should receivecategories(with pre-resolvedis_stapleflags) as a prop — no fetching inside the component itself.Debounce implementation: A 300ms debounce with
$effectis the natural fit, but$effectre-runs on every state change including the debounce timer itself. I'd use a plain debounce utility called from a toggle handler, not an$effect, to avoid unexpected re-trigger loops.Optimistic vs. confirmed UI: The issue doesn't mention error handling. If the debounced PATCH fails, does the chip revert? Silently ignoring a failed save is a data loss risk. Worth deciding now — even a simple toast + revert is better than nothing.
Default staples on first load: "Default list of common staples is pre-selected" — is this a server-side concern (seeded into the DB on household creation) or does the frontend hardcode a list and mark them selected if the backend returns no data? These are very different implementations. The server-side approach is cleaner and more testable.
Suggestions
StaplesManager.svelte(layout + context prop) wrappingCategorySection.sveltewrappingStapleChip.svelte. Each stays short and focused.updateStaple(ingredientId, newValue)which updates$stateimmediately (optimistic) and fires the debounced PATCH.🏗️ Backend Engineer — Spring Boot / PostgreSQL
Questions & Observations
Household vs. global
is_staple: The spec says staples are excluded from generated shopping lists, which implies per-household staple preferences. But isis_staplea column on theingredienttable (global) or on a join table likehousehold_ingredient(per-household)? This is the most important data model question to answer before writing any code.API design for toggle: A debounced save on the frontend sending
PATCH /ingredients/{id}with{"is_staple": true}is fine, but consider:PATCH /ingredients/staples) to handle rapid toggling without hammering the DB? 300ms debounce helps, but a batch payload is safer under load.Default staples seeding: "Default list of common staples is pre-selected on first load" — this should be seeded when the household is created, not derived on the frontend. A Flyway migration can insert default ingredient rows with
is_staple = true. The frontend just reads what the server returns.Schema consideration: If
is_staplelives on a per-household table, the DDL needs aUNIQUE (household_id, ingredient_id)constraint and a proper FK. If it's on theingredienttable directly (global), aNOT NULL DEFAULT falseconstraint is sufficient — but then all households share the same staple list, which may not be the intent.Suggestions
204 No Contenton success — no need to return the full ingredient object for a boolean toggle.ingredient.householdId == currentUser.householdIdbefore persisting — don't rely only on the URL parameter.🧪 QA Engineer — Test Coverage Review
Questions & Observations
Missing error state in acceptance criteria: The ACs cover happy paths well (toggle, default selection, debounce, layouts) but there's no AC for what happens when the PATCH fails. If the save fails silently, the user thinks their staple is saved but it isn't — that's a data loss bug with no test to catch it.
"Default staples pre-selected on first load" — first load of what? First time the household is created? Every page load? What happens if new ingredients are added to a category after initial setup — are they staples by default? This boundary needs defining before I can write a test for it.
Debounce edge cases to cover:
Layout testing: The 3-column (settings) vs 2-column (onboarding) layout difference is a behaviour difference driven by a prop. This needs a component test, not just a visual check.
Test Plan I'd Write
/ingredients/{id}persistsis_stapleSuggestion
🔒 Sable — Security Engineer
Questions & Observations
IDOR risk on the toggle endpoint: The debounced
PATCH /ingredients/{id}must verify thatingredient.householdId == session.householdId. If the backend only checks "is the user authenticated?" but not "does this ingredient belong to this user's household?", a planner can toggle staples for another household by enumerating ingredient IDs. This is the most critical control to verify.Role enforcement: The issue says Role: Planner. The backend must reject
PATCH /ingredients/{id}/staplewith403if the session user is amember. Members should not be able to modify staple settings. Confirm this is enforced at the service layer, not just the frontend.Ingredient names and XSS: Category names and ingredient names rendered as chip labels — are these static/seeded data, or can a planner create custom ingredients with arbitrary names? If user-created ingredient names are rendered inside chips, they need HTML escaping. Svelte auto-escapes text nodes, so
{ingredient.name}is safe, but{@html ingredient.name}would not be. Flag any{@html}usage in the component during review.Debounce and rate limiting: 300ms debounce on the frontend reduces API calls, but doesn't prevent a malicious client from skipping the debounce entirely and flooding
PATCHcalls. Consider rate limiting on the toggle endpoint (e.g., Spring'sHandlerInterceptoror a bucket4j limiter) — especially since the endpoint writes to the DB on every call.Information leakage in error responses: If the ingredient ID doesn't exist or belongs to another household, the backend should return
404in both cases (not403). Returning403confirms the resource exists, enabling enumeration.No Concerns
{@html}risk from the chip design itself (static categories, toggle state is a boolean).🎨 Atlas — UI/UX Designer
Questions & Observations
Border-radius 20px is off-system: The design system goes
--radius-xl(16px) →--radius-full(9999px). 20px sits between them with no token. For a pill-style chip,--radius-fullis the semantically correct choice (and avoids a magic number). Was 20px intentional, or should this be--radius-full?Unselected chip text color not specified: The spec defines selected state fully (
--green-tintbg,--green-lightborder,--green-darktext) but unselected only specifies bg (--color-surface) and border (--color-border). What's the text color?--color-text?--color-text-muted? This needs to be explicit to ensure 4.5:1 contrast on--color-surface.Focus and hover states missing: WCAG 2.2 AA requires visible focus indicators. Keyboard users navigating through 30+ chips need a clear
:focus-visiblering. Hover state for pointer users should also be specified — a subtle bg shift on--color-surface-hoveris consistent with other interactive elements in the system.Category heading typography: The issue lists category names (Oils & fats, Spices, etc.) but doesn't specify their typographic treatment. Are these
text-xs uppercase tracking-wide --color-text-mutedlike section labels elsewhere, ortext-sm font-medium? Consistent with the rest of the settings page is important.Empty category state: What renders if a category has no ingredients? A hidden section? A "No items" placeholder? Worth defining so the component handles it gracefully rather than showing an empty heading.
Column count as layout context, not content: The 2-col / 3-col difference between onboarding and settings is a layout decision, not a content decision. This is well-handled by a context prop — just confirm the chip grid uses
gap: 24px(row) consistently across both layouts as specified.Suggestions
--radius-fullexplicitly in the spec to close the token gap.outline: 2px solid --green-light; outline-offset: 2pxis consistent with the green interaction palette.Implementation complete — PR #35
All 19 tasks implemented on branch
feat/issue-20-pantry-staples.What was built
StapleChip.svelte+ testsaria-pressed, focus ring, selected/unselected stylesCategorySection.svelte+ testsonToggle(id, value)StaplesManager.svelte+ tests+server.ts+ testsPATCH /v1/ingredients/{id}, validates id, 204/400/500+page.server.ts+ tests+page.svelte+ testshousehold/setupredirect/household/staples?ctx=onboardinghousehold/invitestubTest results
221 unit tests — all green. 0 type errors.
🤖 Generated with Claude Code