Kanban Board

Components

ComponentPurpose
KanbanBoard.tsxMain board with DndContext, PointerSensor (5px activation), closestCorners collision
KanbanCard.tsxCompact card (forwardRef). Title, assignee avatar, priority dot, due date, PR count, project, lock icon
KanbanColumn.tsxColumn with useDroppable, droppable ID = column-{statusId}
TaskDetailSheet.tsxSide panel for full task details

Drag-and-Drop Architecture

  • Library: @dnd-kit/core + @dnd-kit/sortable + @dnd-kit/utilities
  • Transform: CSS.Translate.toString(transform) - NOT CSS.Transform (avoids scale offset)
  • During drag: Original card opacity: 0, only DragOverlay visible
  • Droppable ref: On plain <div> wrapper, NEVER on <ScrollArea> (Radix viewport breaks coordinates)
  • Drop detection: If overId starts with column-, extract statusId; else find task’s status
  • Read-only tasks: Not draggable (disabled: isReadOnly in useSortable)

Status Update Logic

Moving to DONE category    → set completed_at = now
Moving from DONE           → clear completed_at = null
Moving between non-DONE    → preserve existing completed_at

Key Lesson

ScrollArea + DnD

Never put dnd-kit droppable refs on Radix <ScrollArea>. The ref goes to ScrollAreaPrimitive.Root but content scrolls inside nested ScrollAreaPrimitive.Viewport, causing coordinate mismatch.

Data Flow

useTasksRealtime() hook wraps task fetching with Supabase Realtime subscription (500ms debounce). On drag end: update task status + completed_at via Supabase, invalidate queries.