Frontend: J4 — Swap flow (action sheet + quick suggestions) #29
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
Mid-week meal swap flow. Speed is critical — must complete in ≤3 taps from "Swap" to updated plan. Different UI pattern per breakpoint: mobile uses action sheet + bottom sheet, desktop uses inline panel transitions.
Journey: J4 — Adapt on the fly
Role: Planner only
Constraint: ≤3 taps to complete swap
Mobile: Action Sheet
Bottom action sheet pulls up from bottom on meal tap:
Mobile: Swap Suggestions (Bottom Sheet)
After tapping "Swap", a bottom sheet appears:
--orange-tintbg, old meal name struck througheffort ASC, cook_time ASCweek_plan_slot+ dismiss sheet + undo toastDesktop: Inline Panel
No action sheet. The C1 detail panel (280px) has a "Swap meal" ghost button:
Tap Count
Sorting Difference from C2
In swap context (J4), suggestions sort easiest first because mid-week swaps typically happen when the original plan was too ambitious. In planning context (J2), C2 sorts by variety algorithm.
Behavior
Acceptance Criteria
Spec file:
specs/frontend/j4-adapt-on-the-fly.html— swap trigger (mobile action sheet) + C2 swap context (easiest-first suggestions) previews, agent tables, and LLM implementation guide with 3-tap constraint details.👨💻 Kai — Frontend Engineer
The ≤3-tap constraint and the two completely different interaction patterns per breakpoint (action sheet on mobile, inline panel on desktop) make this a fun but structurally tricky build. A few things to nail down.
Component split
MealActionSheet— mobile-only bottom sheet triggered by tapping a meal card on C1. Contains the 4 action buttons.SwapSuggestionsSheet— mobile-only second bottom sheet that replaces/overlays the action sheet after tapping "Swap". Contains the "Replacing" banner + suggestion cards.SwapSuggestionsPanel— desktop-only, renders inline within the existing C1 detail panel (280px). Transitions in-place.SwapSuggestionCard— shared between sheet and panel: name + time/effort/tag + "Pick" link.UndoToast— shared utility component for the post-swap confirmation.The action sheet is not a dialog — it's a sheet
transform: translateY, spring animation, drag handle that actually works with pointer events. No<dialog>element here — dialogs don't animate from the bottom naturally.bg-black/40(matches the "40%" dim in the spec) — needs to be a separate layer that traps focus and closes the sheet on click.role="dialog",aria-modal="true", focus trap, andEscapeto close.Inline panel transition on desktop
{#key activeView}block with a CSS transition for this. Simple and controllable.The undo toast timing
week_plan_slotback to the original meal, or is it a local optimistic rollback?PATCHback to original meal ID). This needs to be in scope.Questions
🛠️ Backend Engineer — Swap API & Logging
The swap flow has cleaner backend requirements than D1, but the swap logging requirement and the sorting logic for suggestions deserve careful attention.
Swap logging — what exactly gets recorded?
week_plan_slot.swap_logtable:id,week_plan_slot_id,original_recipe_id,replacement_recipe_id,swapped_by(user_id FK),swapped_at. This is separate fromadmin_audit_log— it's a domain event, not an admin action.The swap endpoint
PUT /api/week-plan-slots/{slotId}/recipe— replace the recipe in a slot. Body:{ recipeId: UUID }.plannerrole, new recipe exists in the household's library.Undo ��� needs its own endpoint or same endpoint?
recipeIdso the client can call the same endpoint again to undo within the toast window. No special undo endpoint needed.Suggestions endpoint for swap context (J4)
effort ASC, cook_time ASC— explicitly different from C2's variety-first sorting.?context=swapparameter, or a dedicated endpoint? I lean toward a query parameter to avoid duplicating business logic — but the sorting strategies are different enough that it might warrant clear separation.Variety score recalculation
Questions
effortstored as an enum (easy,medium,hard) or a numeric value? Theeffort ASCsort requires a consistent ordering.🧪 QA Engineer — Test Coverage Plan for J4 Swap Flow
The ≤3-tap constraint is testable and the swap logging requirement makes the data trail auditable. Here's what needs full coverage.
Backend unit tests
shouldReplaceRecipeInSlotAndReturnUpdatedVarietyScore()shouldRecordSwapLogWithOriginalAndReplacementRecipeIds()shouldSortSwapSuggestionsByEffortAscThenCookTimeAsc()— this is where the sorting rule gets validatedshouldExcludeCurrentSlotRecipeFromSwapSuggestions()— if that's the intended behaviorshouldThrowWhenSlotDoesNotBelongToCallerHousehold()shouldThrowWhenCallerIsNotPlanner()Backend integration tests
shouldReturn403WhenMemberAttemptsToSwapMeal()— planner-onlyshouldReturn404WhenSlotDoesNotExist()shouldReturn404WhenNewRecipeNotInHouseholdLibrary()shouldPersistSwapLogAfterSuccessfulSwap()— verify the log row was actually writtenshouldReturnUpdatedVarietyScoreInSwapResponse()— verify score is in the response body, not just in a follow-up GETshouldReturn200WithPreviousRecipeIdInResponseForUndoSupport()Frontend component tests
MealActionSheet: renders 4 buttons, tapping "Swap" transitions to suggestions, tapping "Cancel" dismisses, backdrop click dismissesSwapSuggestionsSheet: shows "Replacing" banner with struck-through meal name, suggestions sorted by effort, "Pick" triggers swapSwapSuggestionsPanel(desktop): renders inline in detail panel, panel transitions back after pickUndoToast: appears after swap, disappears after timeout, "Undo" button triggers reverse swap actionE2E tests — the tap count is auditable
Gaps / edge cases to clarify
🔐 Sable — Security Engineer
The swap flow is planner-only and involves a write operation to the week plan. The primary risks are authorization bypass and the undo pattern creating a replay window. Let me threat-model.
Planner-only authorization — verify at the right layer
PUT /api/week-plan-slots/{slotId}/recipemust receive a 403.slotIdmust be verified to belong to the caller's household before any write occurs. IDOR via slot ID enumeration is the obvious attack vector here.Swap logging and the undo window
swap_logentry should be written in the same transaction as theweek_plan_slotupdate. If logging fails, the swap should fail. An atomic write prevents log/data divergence.swap_log— which is correct and auditable. No special concern here.Suggestions endpoint — authorization
GET /api/recipes?context=swap&slotId={slotId}or equivalent: the slot ID lookup must verify household membership before returning suggestions. Even "read" endpoints can leak data.The "Swap this meal" action from the action sheet
slotIdused to open the sheet comes from the server-rendered page data (not a client-generated value). The+page.server.tsload function should provide slot IDs — the client should not construct them.Questions
swap_logrows.slot → week_plan → household? Direct FK is safer for audit queries.🎨 Atlas — UI/UX Designer
Speed is the north star for J4 — ≤3 taps — and every design decision must serve that. The two-breakpoint pattern (action sheet on mobile, inline panel on desktop) is the right call. Let me spec out the details the issue leaves open.
Action sheet anatomy — mobile
--color-mutedbg, centered,--radius-full, 8px top margin inside sheet--color-surfacebg,--radius-lgtop corners only (16px),--shadow-raised, slides up from bottom--color-text, 12px below handle--color-muted, DM Sans--color-borderdivider between each--orange-tintbg — note,--orangeis not in the current design system. I need to define--orange-tintand--orange-darktokens before this ships.--green-tintbg,--green-darktext — these exist ✓--color-surfacebg,--color-mutedtext--color-mutedtext, font-weight 400 (visually de-prioritized)Design system gap — orange tokens
--orange-tintand--orange-darkfor the swap action color. These don't exist in the current token set. I'll define them: a warm amber-orange hue that reads as "attention/change" without being alarming. Must pass 4.5:1 contrast for--orange-darktext on--orange-tintbg."Replacing" banner
--orange-tintbg, full-width inside the suggestions sheet/panel--orange-darktext,text-decoration: line-through--orange-dark, font-weight 500 as eyebrow above the struck nameSuggestions sheet — mobile scroll behavior
Undo toast
--color-surfacebg,--shadow-raised,--radius-md--green-dark, 13px/500)Questions
--orange/ amber a new brand color we're adding to the system, or should the swap action use an existing signal color differently?✅ Implementation complete — branch
feat/issue-29-swap-flowWhat was built
6 commits, all tests green (593 passing, 2 pre-existing failures in an unrelated test file).
DayMealCard— newonactionsheetprop<button>(full tap target, no inline buttons)onaddrecipeto open the RecipePicker directlysortEasiestFirstutility (week.ts)effort ASC(easy → medium → hard) thencookTimeMin ASCMealActionSheetcomponent<a>link, green-tint), 👁 View recipe (<a>link, subtle), Cancel (no bg)SwapSuggestionListcomponentPlanner page wiring
Mobile (3 taps — card → Swap → Pick):
MealActionSheetopensSwapSuggestionListin aBottomSheet(70 vh)handleRecipePick(updateSlot) + undo toastRecipePickerdirectly (unchanged)Desktop (2 taps — Swap → Pick):
recipe-pickerpanel state now checks whether the slot has a recipe:SwapSuggestionListwith "Gericht tauschen" header and easiest-first sortingRecipePickeras before (unchanged)Acceptance criteria status
invalidateAllafter slot update)swap_loginfrastructure exists yet; needs a separate backend ticket