Files
stack/apps/web/src/components/kanban/kanban-column.tsx
Jason Woltje 0b330464ba feat(#17): implement Kanban board view
- 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
2026-01-29 17:55:33 -06:00

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>
);
}