Maestro task 5c2842a8-5ac1-4141-9575-c0f461d90c3e — "Drag-and-drop and keyboard swapping" (Feature 14, Weekly Plan Builder). Last implementation task in the feature.
Done-condition
On tablet/desktop, dinners can be dragged (pointer events) from the desktop right panel or the tablet chip strip onto a day: the source row becomes a dashed ghost at 55% opacity, the dragged card floats with shadow-lg rotated −3°, the hovered day shows a dashed 2.5 px grape border on grape-100 with "Drop to swap / Replaces {current dinner}", and dropping calls the swap API optimistically with rollback on error; dropping outside a day cancels. Keyboard alternative: focus a panel row, press Enter/Space to pick it up, arrow keys to choose a day, Enter to drop, Escape to cancel, with live-region announcements. Vitest tests cover pointer drop, cancel, rollback and the full keyboard flow. Typecheck, test and build pass.
Scope
Tablet/desktop only (≥ 768 px); mobile is unchanged. No new dependency, no HTML5 drag-and-drop, and no second swap path: drops go through WeekBoard.dropIn → the existing useSwapDay mutate (which owns the optimistic update, rollback and the one-swap-per-week guard). No backend, migration, skill or API-contract change.
Notes for review
New web/src/plan/drag.tsx: a page-level provider, DropBoard (the grid registers itself only while shown, so there are no targets while a draft runs or no plan exists), useDropTarget, useDraggable (pointer events with capture and a 4 px threshold, plus the keyboard handling) and a polite live region. The panel/strip sit outside the per-week board, hence the provider.
One existing assertion changed: the 6-dot grip is now read inside the row's button.
Two additions beyond the literal spec: a dinner already this week cannot be picked up (matching the swap sheet, with the reason announced), and moving focus off a keyboard drag cancels it.
Space picks up but only Enter drops, as the done-condition states. The live region is aria-live="polite" rather than role="status" so it does not collide with the app's existing status queries in tests.
The stylesheet declares the 2.5 px dashed border; headless Chrome reports 2 px at 1× and 2× because it rounds border widths.
Evidence (orchestrator-run pre-review gate at 3d6c1109c8)
gofmt -l . — empty (no Go file changed)
cd web && npm run typecheck — PASS
cd web && npm test — PASS, 45 files / 759 tests (+12), colour-literal lint clean
cd web && npm run build — PASS (includes lint:colors and check:dist-fonts)
Developer-run headless-Chrome (Playwright) verification against the Vite dev server with a mocked API at 1180 and 1440 px: pointer drop (one ghost at 0.55 opacity, floating copy at −3° with the large shadow, "Drop to swap / Replaces Pizza night" on grape-100, exactly one PUT on release, dinner removed from the Quest log); cancel over the header (no PUT, announced); rollback on a 409 (day restored with the error and Retry, rows aria-disabled while pending); and the full keyboard flow (pick up, arrow to Wednesday, Escape cancel keeping focus, Space + arrows + Enter dropping and moving focus to that day's swap button). Screenshots under /tmp/opencode/f14t7/, outside the repository.
## Task
Maestro task `5c2842a8-5ac1-4141-9575-c0f461d90c3e` — "Drag-and-drop and keyboard swapping" (Feature 14, Weekly Plan Builder). Last implementation task in the feature.
## Done-condition
On tablet/desktop, dinners can be dragged (pointer events) from the desktop right panel or the tablet chip strip onto a day: the source row becomes a dashed ghost at 55% opacity, the dragged card floats with shadow-lg rotated −3°, the hovered day shows a dashed 2.5 px grape border on grape-100 with "Drop to swap / Replaces {current dinner}", and dropping calls the swap API optimistically with rollback on error; dropping outside a day cancels. Keyboard alternative: focus a panel row, press Enter/Space to pick it up, arrow keys to choose a day, Enter to drop, Escape to cancel, with live-region announcements. Vitest tests cover pointer drop, cancel, rollback and the full keyboard flow. Typecheck, test and build pass.
## Scope
Tablet/desktop only (≥ 768 px); mobile is unchanged. No new dependency, no HTML5 drag-and-drop, and no second swap path: drops go through `WeekBoard.dropIn` → the existing `useSwapDay` mutate (which owns the optimistic update, rollback and the one-swap-per-week guard). No backend, migration, skill or API-contract change.
## Notes for review
- New `web/src/plan/drag.tsx`: a page-level provider, `DropBoard` (the grid registers itself only while shown, so there are no targets while a draft runs or no plan exists), `useDropTarget`, `useDraggable` (pointer events with capture and a 4 px threshold, plus the keyboard handling) and a polite live region. The panel/strip sit outside the per-week board, hence the provider.
- One existing assertion changed: the 6-dot grip is now read inside the row's button.
- Two additions beyond the literal spec: a dinner already this week cannot be picked up (matching the swap sheet, with the reason announced), and moving focus off a keyboard drag cancels it.
- Space picks up but only Enter drops, as the done-condition states. The live region is `aria-live="polite"` rather than `role="status"` so it does not collide with the app's existing `status` queries in tests.
- The stylesheet declares the 2.5 px dashed border; headless Chrome reports 2 px at 1× and 2× because it rounds border widths.
## Evidence (orchestrator-run pre-review gate at 3d6c1109c812fa468e2a2ea39ac39da591ba1b60)
- `gofmt -l .` — empty (no Go file changed)
- `cd web && npm run typecheck` — PASS
- `cd web && npm test` — PASS, 45 files / 759 tests (+12), colour-literal lint clean
- `cd web && npm run build` — PASS (includes `lint:colors` and `check:dist-fonts`)
Developer-run headless-Chrome (Playwright) verification against the Vite dev server with a mocked API at 1180 and 1440 px: pointer drop (one ghost at 0.55 opacity, floating copy at −3° with the large shadow, "Drop to swap / Replaces Pizza night" on grape-100, exactly one PUT on release, dinner removed from the Quest log); cancel over the header (no PUT, announced); rollback on a 409 (day restored with the error and Retry, rows aria-disabled while pending); and the full keyboard flow (pick up, arrow to Wednesday, Escape cancel keeping focus, Space + arrows + Enter dropping and moving focus to that day's swap button). Screenshots under `/tmp/opencode/f14t7/`, outside the repository.
Implementation commit: `3d6c1109c812fa468e2a2ea39ac39da591ba1b60`
From tablet up, a Quest-log or Regular dinner is dragged from desktop's
right panel or tablet's Quest-log strip onto a day of the week to swap it
in. Pointer events with capture: the source becomes a dashed ghost at 55%
opacity, a copy floats with shadow-lg at -3deg, and the day under the
pointer shows a dashed 2.5 px grape border on grape-100 with "Drop to swap /
Replaces {dinner}". Letting go over a day calls useSwapDay's mutate, so the
swap is optimistic and rolls back with the existing error; anywhere else, a
pointer cancel or Escape cancels with no request.
Keyboard alternative: Enter or Space picks a row up onto Monday, the arrow
keys choose the day, Enter drops (moving focus to that day's swap button),
Escape or leaving the row cancels. Each step is announced in a polite live
region. No drag starts while a swap of the week is pending, nor for a
dinner already this week, matching the swap sheet. Mobile is unchanged.
Task: 5c2842a8-5ac1-4141-9575-c0f461d90c3e
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The floating copy sat where it was grabbed, over the hovered day's
"Drop to swap / Replaces ..." text. It now floats 48 px below and to
the right of the pointer, flipping left or up where it would leave the
viewport, so the day's current dinner stays readable during a drag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Task
Maestro task
5c2842a8-5ac1-4141-9575-c0f461d90c3e— "Drag-and-drop and keyboard swapping" (Feature 14, Weekly Plan Builder). Last implementation task in the feature.Done-condition
On tablet/desktop, dinners can be dragged (pointer events) from the desktop right panel or the tablet chip strip onto a day: the source row becomes a dashed ghost at 55% opacity, the dragged card floats with shadow-lg rotated −3°, the hovered day shows a dashed 2.5 px grape border on grape-100 with "Drop to swap / Replaces {current dinner}", and dropping calls the swap API optimistically with rollback on error; dropping outside a day cancels. Keyboard alternative: focus a panel row, press Enter/Space to pick it up, arrow keys to choose a day, Enter to drop, Escape to cancel, with live-region announcements. Vitest tests cover pointer drop, cancel, rollback and the full keyboard flow. Typecheck, test and build pass.
Scope
Tablet/desktop only (≥ 768 px); mobile is unchanged. No new dependency, no HTML5 drag-and-drop, and no second swap path: drops go through
WeekBoard.dropIn→ the existinguseSwapDaymutate (which owns the optimistic update, rollback and the one-swap-per-week guard). No backend, migration, skill or API-contract change.Notes for review
web/src/plan/drag.tsx: a page-level provider,DropBoard(the grid registers itself only while shown, so there are no targets while a draft runs or no plan exists),useDropTarget,useDraggable(pointer events with capture and a 4 px threshold, plus the keyboard handling) and a polite live region. The panel/strip sit outside the per-week board, hence the provider.aria-live="polite"rather thanrole="status"so it does not collide with the app's existingstatusqueries in tests.Evidence (orchestrator-run pre-review gate at
3d6c1109c8)gofmt -l .— empty (no Go file changed)cd web && npm run typecheck— PASScd web && npm test— PASS, 45 files / 759 tests (+12), colour-literal lint cleancd web && npm run build— PASS (includeslint:colorsandcheck:dist-fonts)Developer-run headless-Chrome (Playwright) verification against the Vite dev server with a mocked API at 1180 and 1440 px: pointer drop (one ghost at 0.55 opacity, floating copy at −3° with the large shadow, "Drop to swap / Replaces Pizza night" on grape-100, exactly one PUT on release, dinner removed from the Quest log); cancel over the header (no PUT, announced); rollback on a 409 (day restored with the error and Retry, rows aria-disabled while pending); and the full keyboard flow (pick up, arrow to Wednesday, Escape cancel keeping focus, Space + arrows + Enter dropping and moving focus to that day's swap button). Screenshots under
/tmp/opencode/f14t7/, outside the repository.Implementation commit:
3d6c1109c812fa468e2a2ea39ac39da591ba1b60From tablet up, a Quest-log or Regular dinner is dragged from desktop's right panel or tablet's Quest-log strip onto a day of the week to swap it in. Pointer events with capture: the source becomes a dashed ghost at 55% opacity, a copy floats with shadow-lg at -3deg, and the day under the pointer shows a dashed 2.5 px grape border on grape-100 with "Drop to swap / Replaces {dinner}". Letting go over a day calls useSwapDay's mutate, so the swap is optimistic and rolls back with the existing error; anywhere else, a pointer cancel or Escape cancels with no request. Keyboard alternative: Enter or Space picks a row up onto Monday, the arrow keys choose the day, Enter drops (moving focus to that day's swap button), Escape or leaving the row cancels. Each step is announced in a polite live region. No drag starts while a swap of the week is pending, nor for a dinner already this week, matching the swap sheet. Mobile is unchanged. Task: 5c2842a8-5ac1-4141-9575-c0f461d90c3e Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Independent Codex gpt-6-sol review: APPROVED for this exact commit.
Independent Codex gpt-6-sol review: APPROVED for this exact commit.