- Drag-and-drop with @dnd-kit - Four status columns (Not Started, In Progress, Paused, Completed) - Task cards with priority badges and due dates - PDA-friendly design (calm colors, gentle language) - 70 tests (87% coverage) - Demo page at /demo/kanban
87 lines
2.9 KiB
TypeScript
87 lines
2.9 KiB
TypeScript
"use client";
|
|
|
|
import type { Task } from "@mosaic/shared";
|
|
import { TaskStatus } from "@mosaic/shared";
|
|
import { useDroppable } from "@dnd-kit/core";
|
|
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
import { TaskCard } from "./task-card";
|
|
|
|
interface KanbanColumnProps {
|
|
status: TaskStatus;
|
|
title: string;
|
|
tasks: Task[];
|
|
}
|
|
|
|
const statusColors = {
|
|
[TaskStatus.NOT_STARTED]: "border-gray-300 dark:border-gray-600",
|
|
[TaskStatus.IN_PROGRESS]: "border-blue-300 dark:border-blue-600",
|
|
[TaskStatus.PAUSED]: "border-amber-300 dark:border-amber-600",
|
|
[TaskStatus.COMPLETED]: "border-green-300 dark:border-green-600",
|
|
[TaskStatus.ARCHIVED]: "border-gray-400 dark:border-gray-500",
|
|
};
|
|
|
|
const statusBadgeColors = {
|
|
[TaskStatus.NOT_STARTED]: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300",
|
|
[TaskStatus.IN_PROGRESS]: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400",
|
|
[TaskStatus.PAUSED]: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400",
|
|
[TaskStatus.COMPLETED]: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400",
|
|
[TaskStatus.ARCHIVED]: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400",
|
|
};
|
|
|
|
export function KanbanColumn({ status, title, tasks }: KanbanColumnProps) {
|
|
const { setNodeRef, isOver } = useDroppable({
|
|
id: status,
|
|
});
|
|
|
|
const taskIds = tasks.map((task) => task.id);
|
|
|
|
return (
|
|
<section
|
|
ref={setNodeRef}
|
|
role="region"
|
|
aria-label={`${title} tasks`}
|
|
data-testid={`column-${status}`}
|
|
className={`
|
|
flex flex-col
|
|
bg-gray-50 dark:bg-gray-900
|
|
rounded-lg border-2
|
|
p-4 space-y-4
|
|
min-h-[500px]
|
|
transition-colors duration-200
|
|
${statusColors[status]}
|
|
${isOver ? "bg-gray-100 dark:bg-gray-800 border-opacity-100" : "border-opacity-50"}
|
|
`}
|
|
>
|
|
{/* Column Header */}
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
|
{title}
|
|
</h3>
|
|
<span
|
|
className={`
|
|
inline-flex items-center justify-center
|
|
w-6 h-6 rounded-full text-xs font-medium
|
|
${statusBadgeColors[status]}
|
|
`}
|
|
>
|
|
{tasks.length}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Tasks */}
|
|
<div className="flex-1 space-y-3 overflow-y-auto">
|
|
<SortableContext items={taskIds} strategy={verticalListSortingStrategy}>
|
|
{tasks.length > 0 ? (
|
|
tasks.map((task) => <TaskCard key={task.id} task={task} />)
|
|
) : (
|
|
<div className="flex items-center justify-center h-32 text-sm text-gray-500 dark:text-gray-400">
|
|
{/* Empty state - gentle, PDA-friendly */}
|
|
<p>No tasks here yet</p>
|
|
</div>
|
|
)}
|
|
</SortableContext>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|