feat: add drag-and-drop for cards and columns with keyboard support

This commit is contained in:
Your Name
2026-02-15 18:55:35 +02:00
parent 624be051c1
commit 86de747bc4
4 changed files with 426 additions and 67 deletions

View File

@@ -1,5 +1,12 @@
import { useState } from "react";
import { Plus } from "lucide-react";
import { useDroppable } from "@dnd-kit/core";
import {
SortableContext,
verticalListSortingStrategy,
useSortable,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { ColumnHeader } from "@/components/board/ColumnHeader";
@@ -24,29 +31,66 @@ export function KanbanColumn({ column }: KanbanColumnProps) {
const width = WIDTH_MAP[column.width];
// Make the column itself sortable (for column reordering)
const {
attributes,
listeners,
setNodeRef: setSortableNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id: column.id,
data: { type: "column" },
});
// Make the column a droppable target so empty columns can receive cards
const { setNodeRef: setDroppableNodeRef } = useDroppable({
id: `column-droppable-${column.id}`,
data: { type: "column", columnId: column.id },
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : undefined,
width,
};
return (
<div
ref={setSortableNodeRef}
style={style}
className="group/column flex shrink-0 flex-col rounded-lg bg-pylon-column"
style={{ width }}
{...attributes}
>
<ColumnHeader column={column} cardCount={column.cardIds.length} />
{/* The column header is the drag handle for column reordering */}
<div {...listeners}>
<ColumnHeader column={column} cardCount={column.cardIds.length} />
</div>
{/* Card list */}
<ScrollArea className="flex-1 overflow-y-auto">
<div className="flex flex-col gap-2 p-2">
{column.cardIds.map((cardId) => {
const card = board?.cards[cardId];
if (!card) return null;
return (
<CardThumbnail
key={cardId}
card={card}
boardLabels={board?.labels ?? []}
/>
);
})}
</div>
</ScrollArea>
{/* Card list - wrapped in SortableContext for within-column sorting */}
<SortableContext
items={column.cardIds}
strategy={verticalListSortingStrategy}
>
<ScrollArea className="flex-1 overflow-y-auto">
<div ref={setDroppableNodeRef} className="flex min-h-[40px] flex-col gap-2 p-2">
{column.cardIds.map((cardId) => {
const card = board?.cards[cardId];
if (!card) return null;
return (
<CardThumbnail
key={cardId}
card={card}
boardLabels={board?.labels ?? []}
columnId={column.id}
/>
);
})}
</div>
</ScrollArea>
</SortableContext>
{/* Add card section */}
{showAddCard ? (