// Adapted from Kibo UI (MIT), with owned React/CSS APIs. See THIRD_PARTY_NOTICES.md. "use client"; import { useState, type ComponentProps, type ReactNode } from "react"; import { DndContext, PointerSensor, KeyboardSensor, useSensor, useSensors, useDroppable, closestCenter, type DragEndEvent, } from "./_ynrlib/dnd-core.js"; import { SortableContext, useSortable, verticalListSortingStrategy, sortableKeyboardCoordinates, arrayMove, } from "./_ynrlib/dnd-sortable.js"; import { CSS } from "./_ynrlib/dnd-utilities.js"; export interface KanbanItem { id: string; title: string; column: string; } export interface KanbanColumn { id: string; title: string; } export type KanbanProps = ComponentProps<"div"> & { columns: readonly KanbanColumn[]; items: T[]; onItemsChange: (items: T[]) => void; renderItem?: (item: T) => ReactNode; disabled?: boolean; }; function BoardColumn({ column, children, }: { column: KanbanColumn; children: ReactNode; }) { const { setNodeRef, isOver } = useDroppable({ id: `column:${column.id}` }); return (

{column.title}

{children}
); } function BoardCard({ item, columns, move, reorder, renderItem, disabled, first, last, }: { item: T; columns: readonly KanbanColumn[]; move: (id: string, column: string) => void; reorder: (id: string, direction: number) => void; renderItem?: (item: T) => ReactNode; disabled?: boolean; first: boolean; last: boolean; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: `item:${item.id}`, disabled }); return (
{renderItem?.(item) ?? item.title}
); } export function Kanban({ columns, items, onItemsChange, renderItem, disabled, className = "", ...props }: KanbanProps) { const [notice, setNotice] = useState(""); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ); const move = (id: string, column: string) => { if (disabled || !columns.some((c) => c.id === column)) return; onItemsChange( items.map((item) => (item.id === id ? { ...item, column } : item)), ); setNotice( `Moved ${items.find((item) => item.id === id)?.title} to ${columns.find((c) => c.id === column)?.title}`, ); }; const reorder = (id: string, direction: number) => { const old = items.findIndex((item) => item.id === id); const same = items.filter((item) => item.column === items[old]?.column); const next = same[same.findIndex((item) => item.id === id) + direction]; if (disabled || !next) return; onItemsChange( arrayMove( items, old, items.findIndex((item) => item.id === next.id), ), ); setNotice(`Reordered ${items[old].title}`); }; const end = ({ active, over }: DragEndEvent) => { if (disabled || !over || active.id === over.id) return; const id = String(active.id).slice(5); const old = items.findIndex((item) => item.id === id); if (old < 0) return; if (String(over.id).startsWith("column:")) { move(id, String(over.id).slice(7)); return; } const index = items.findIndex((item) => `item:${item.id}` === over.id); if (index < 0) return; const next = items.map((item) => item.id === id ? { ...item, column: items[index].column } : item, ); onItemsChange(arrayMove(next, old, index)); setNotice(`Moved ${items[old].title}`); }; return (
{columns.map((column) => { const cards = items.filter((item) => item.column === column.id); return ( `item:${item.id}`)} strategy={verticalListSortingStrategy} > {cards.map((item, i) => ( ))} {!cards.length &&

No items.

}
); })}
{notice}
); }