Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-168Done

3.2.4 Drag-drop transitions — dnd-kit, optimistic move, snap-back on illegal/unmapped

Estimate: 26m · Depends on: 3.2.3, 3.1.6

The heart of the story: make cards draggable, move them via the Story-3.1 transition contract, and reconcile optimistically. Introduce dnd-kit (@dnd-kit/core + @dnd-kit/sortable) — the accessible React DnD standard (the stub mandates accessible keyboard DnD; Jira/Linear ship it). pnpm add the deps (a plain library add — no dashboard/SaaS provisioning, so no manual subtask). Wire it per design/boards/.

DnD wiring. A DndContext over the board with pointer AND keyboard sensors; each BoardColumn is a droppable, each BoardCard a sortable draggable; a DragOverlay renders the lifted card; an insertion indicator marks the drop position (the 3.2.1 treatment, not colour-alone — finding #35).

Move semantics → the 3.1 contract. On drop, compute { workItemId, toColumnId, beforeId?, afterId? } from the source/target and call POST /api/projects/[key]/board/move (Story 3.1.6). A cross-column drop is a workflow transition (the server resolves the target status + validates via canTransition); an in-column drop is a rank change (work_item.position). The UI does NOT decide legality — the server does.

Optimistic update + reconcile. Apply the move to the board state immediately (card jumps to the target column/position) for snappy feedback, then:

  • 200 → reconcile to the returned BoardCardDto (confirmed status/position; update the source + target column counts).
  • 409 IllegalBoardMoveErrorsnap the card back to its origin column/position (the 3.2.1 snap-back treatment) and show a Toast explaining the rejected transition.
  • 422 UnmappedColumnTargetError → snap back + a toast (drop onto an unmapped target).
  • other / network error → snap back + a generic retry toast; never leave the card in a lying optimistic position.

Accessibility. Keyboard DnD must fully work: focus a card, pick up (Space/Enter), arrow between columns/positions, drop, and Escape-to-cancel — with aria-live announcements (dnd-kit announcements, copy from the 3.2.1 design-notes). Operable with the keyboard alone.

Out of scope here: the lazy load-more/virtualization the dragged stack lives in (3.2.5 — ensure the DnD wiring tolerates a virtualized list); WIP-limit rejection on over-limit drops (Story 3.3 — the server does not reject on WIP yet, and this story does not pre-empt it).

Acceptance criteria

  • @dnd-kit/core + @dnd-kit/sortable added; a DndContext with pointer + keyboard sensors makes cards draggable between and within columns, with a DragOverlay + an insertion indicator (not colour-alone, finding #35).
  • A drop calls POST …/board/move with the right { workItemId, toColumnId, beforeId?, afterId? }; cross-column = transition, in-column = rank — the UI defers legality to the server.
  • The move applies optimistically; on 200 it reconciles to the returned card + updates column counts; on 409 it snaps the card back + toasts; on 422 and other errors it snaps back + toasts. The card never stays in a rejected position.
  • Keyboard DnD works end-to-end (pick up / move / drop / Esc-cancel) with aria-live announcements; the flow is operable with no mouse.
  • Vitest covers the optimistic-move/reconcile reducer (confirm on 200, revert on 409/422) in isolation; colours/shape via tokens.

Context refs

  • Story 3.1.5 / 3.1.6 — boardsService.moveCard semantics + POST …/board/move + the IllegalBoardMoveError (409) / UnmappedColumnTargetError (422) typed errors this branches on
  • components/ui/Toast — the rejection feedback; app/(authed)/issues/_components/IssueInlineEdit.tsx — an existing optimistic-update precedent to mirror
  • dnd-kit docs (DndContext, useSortable, DragOverlay, keyboard sensor, announcements); design/boards/design-notes.md (3.2.1) — drag states + announcement copy
  • finding #35 (not colour-alone); motir-core/CLAUDE.md--el-* + element-shape rules