Kanban Board
Components
| Component | Purpose |
|---|---|
KanbanBoard.tsx | Main board with DndContext, PointerSensor (5px activation), closestCorners collision |
KanbanCard.tsx | Compact card (forwardRef). Title, assignee avatar, priority dot, due date, PR count, project, lock icon |
KanbanColumn.tsx | Column with useDroppable, droppable ID = column-{statusId} |
TaskDetailSheet.tsx | Side panel for full task details |
Drag-and-Drop Architecture
- Library:
@dnd-kit/core+@dnd-kit/sortable+@dnd-kit/utilities - Transform:
CSS.Translate.toString(transform)- NOTCSS.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: isReadOnlyin 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 toScrollAreaPrimitive.Rootbut content scrolls inside nestedScrollAreaPrimitive.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.
Related
- levandor-crm - Project overview
- day-planner - Day planner system