Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
11 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | user_setup | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 04-shared-lists-live-sync | 05 | execute | 4 |
|
|
true |
|
|
MVP slice: after this plan a real user can reorder list items — the last interactive capability of the lists surface — building directly on the items rendered in Plan 04.
Purpose: Layer drag-and-drop and client-side fractional-rank computation onto the existing ItemRow/ListDetail, reusing the server-side per-field position PATCH already built in Plan 04. Output: dnd-kit DndContext/SortableContext in ListDetail; sortable ItemRow with handle-scoped listeners + sensors; client computes the new rank via generateKeyBetween and PATCHes position optimistically.
<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/04-shared-lists-live-sync/04-CONTEXT.md @.planning/phases/04-shared-lists-live-sync/04-RESEARCH.md @.planning/phases/04-shared-lists-live-sync/04-PATTERNS.md @.planning/phases/04-shared-lists-live-sync/04-UI-SPEC.md @.planning/phases/04-shared-lists-live-sync/04-VALIDATION.md Task 1: Sortable ItemRow + DndContext reorder with optimistic rank PATCH (LIST-03, D-13/D-14/D-15) apps/pwa/src/components/ItemRow.tsx, apps/pwa/src/routes/ListDetail.tsx, apps/pwa/src/api/listsClient.ts - apps/pwa/src/components/ItemRow.tsx (from Plan 04 — add useSortable; handle slot already present) - apps/pwa/src/routes/ListDetail.tsx (active-items rendering from Plan 04) - apps/pwa/src/api/listsClient.ts (patchListItem supports { position }) - .planning/phases/04-shared-lists-live-sync/04-RESEARCH.md Finding 7 (dnd-kit + handle + sensors + rank-on-drop) - .planning/phases/04-shared-lists-live-sync/04-PATTERNS.md §"ItemRow.tsx" - .planning/phases/04-shared-lists-live-sync/04-UI-SPEC.md §"Drag-to-Reorder", §"Accessibility Baseline" (keyboard reorder) Make ItemRow sortable: use useSortable({ id: item.id }) from @dnd-kit/sortable; attach setNodeRef + style (CSS.Transform.toString(transform), transition fallback 'transform 150ms ease-out' for D-14 remote animation, opacity 0.8 + slight scale-down when isDragging). Attach drag listeners to the GripVertical handle button ONLY (not the whole row) so taps on checkbox/text/delete still work. Drag handle only on active items (completed items not reorderable per UI-SPEC).In ListDetail, wrap the active-items list in DndContext (collisionDetection={closestCenter}) + SortableContext (items = active item ids, verticalListSortingStrategy). Configure sensors via useSensors: PointerSensor/MouseSensor immediate, TouchSensor with activationConstraint { delay: 200, tolerance: 5 } (no accidental drags), and KeyboardSensor for the accessibility keyboard-reorder fallback.
onDragEnd: ignore no-op (no over / same id). Compute the destination index after the move; derive prevRank/nextRank from the active list at the destination and compute newRank = generateKeyBetween(prevRank, nextRank) (fractional-indexing). Fire an optimistic reorder mutation: setQueryData(['list', listId]) to reflect the new order immediately (snap), then patchListItem(itemId, { position: newRank }); onError animate back / rollback to previous; onSettled invalidate. Only the moved item's rank is written (one-row PATCH — D-13). Concurrent same-item reorder converges by server LWW on updatedAt (D-15) — no drag-state broadcasting.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| browser → PATCH /api/list-items/:id { position } | client supplies the new rank string — untrusted |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-04-07 | Tampering | client sending position alongside other fields | mitigate | zod patchItemSchema refine (exactly one field) already enforces position-only PATCH (Plan 04); reasserted by tests |
| T-04-05 | Elevation of Privilege | reordering items in an inaccessible list | mitigate | PATCH list-items access-gated (owner OR list_shares) from Plan 04 |
| T-04-10 | Denial of Service | pathological "zipper" inserts growing rank strings | accept | VARCHAR(255) headroom; fractional-indexing degrades gracefully; rebalance available via generateNKeysBetween if ever needed (not in scope) |
</threat_model>
pnpm --filter @familysync/api exec vitest run tests/lib/rank.test.ts tests/routes/lists.test.ts && pnpm --filter @familysync/pwa exec tsc --noEmit - `playwright-cli`: drag-reorder persists across reload.<success_criteria>
- LIST-03 satisfied: drag-to-reorder works, persists as a single-row rank write.
- Touch long-press + keyboard reorder available; remote reorders animate (D-14).
- Reorder ordering + precision covered by automated tests. </success_criteria>
<artifacts_produced> Symbols/files this plan creates (exclude from drift verification):
- ItemRow gains useSortable + handle-scoped drag listeners
- ListDetail gains DndContext/SortableContext + useSensors + onDragEnd rank computation
- Additional rank/order tests in rank.test.ts and lists.test.ts (no new production endpoints — reuses Plan 04 PATCH position) </artifacts_produced>