import { createContext, useContext, useId, type CSSProperties, type ReactNode } from "react"; import { DndContext, PointerSensor, closestCenter, pointerWithin, useSensor, useSensors } from "@dnd-kit/core"; import { SortableContext, horizontalListSortingStrategy, useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { ArrowDown, ArrowUp, Ellipsis } from "lucide-react"; import { Button } from "./ui/button"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from "./ui/dropdown-menu"; import { allTaskListColumns, defaultTaskListColumns, reorderTaskListColumns, taskListColumnStart, taskListColumns, type TaskListColumnId, type TaskListSort } from "../lib/task-list"; import { CellMarquee } from "./CellMarquee"; import { FlyoutAppearanceProvider, FlyoutHeader, FlyoutOptions } from "./ui/flyout"; import { useHorizontalScrollEdges } from "@/hooks/useHorizontalScrollEdges"; export { taskListColumns } from "../lib/task-list"; export type { TaskListColumnId, TaskListSort } from "../lib/task-list"; const ColumnContext = createContext({ columns: defaultTaskListColumns, order: allTaskListColumns }); export const useTaskListColumns = () => useContext(ColumnContext).columns; function columnLayout(visibleColumns: readonly TaskListColumnId[], hasControls: boolean): CSSProperties { const columns = visibleColumns.map(id => taskListColumns.find(column => column.id === id)!); const hasTitle = visibleColumns.includes("task"); const leadingWidth = hasTitle ? "var(--task-list-column-status)" : "var(--task-row-control-size)"; const widths = columns.flatMap(column => column.id === "task" ? [leadingWidth, "var(--task-list-column-title-min)"] : [column.width]); const tracks = columns.flatMap(column => column.id === "task" ? [leadingWidth, "minmax(var(--task-list-column-title-min), 1fr)"] : [column.width]); if (!hasTitle) { widths.unshift(leadingWidth); tracks.unshift(`minmax(${leadingWidth}, 1fr)`); } return { "--task-list-columns": tracks.join(" "), "--task-list-task-start": hasTitle ? taskListColumnStart(visibleColumns, "task") : 1, "--task-list-min-width": `calc(${widths.join(" + ")} + var(--task-row-gap) * ${widths.length - 1} + var(--task-row-padding-inline) * 2${hasControls ? " + var(--task-list-controls-width)" : ""})`, "--task-list-controls-space": hasControls ? "var(--task-list-controls-width)" : "var(--task-list-no-controls-space)", } as CSSProperties; } /** Shared collection viewport: fades only the edges with content beyond them. */ export function TaskListViewport({ children, visibleColumns = defaultTaskListColumns, columnOrder = allTaskListColumns, controls }: { children: ReactNode; visibleColumns?: readonly TaskListColumnId[]; columnOrder?: readonly TaskListColumnId[]; controls?: ReactNode; }) { const { scrollRef, contentRef, edges } = useHorizontalScrollEdges(); const orderedColumns = columnOrder.filter(id => visibleColumns.includes(id)); return (
{children}
{controls ?
{controls}
: null}
); } const detailColumns = taskListColumns.filter(column => column.id !== "task"); type DetailColumn = Exclude<(typeof detailColumns)[number]["id"], "taskId">; /** Values are resolved by the collection; identity columns intentionally accept names only. */ export type TaskRowColumnValues = Partial, ReactNode>> & { createdBy?: string | null; assignee?: string | null; }; function SortableColumnHeader({ id, label, start, sort, onSortChange, onColumnOrderChange, helpId }: { id: TaskListColumnId; label: string; start: number; sort?: TaskListSort; onSortChange?: (sort: TaskListSort) => void; onColumnOrderChange?: (columns: readonly TaskListColumnId[]) => void; helpId: string; }) { const { columns, order } = useContext(ColumnContext); const sortable = useSortable({ id, disabled: !onColumnOrderChange, transition: null }); const active = sort?.column === id; const direction = active && sort.direction === "ascending" ? "descending" : "ascending"; return ( {onSortChange || onColumnOrderChange ? ( ) : label} ); } export function TaskListColumnHeader({ sort, onSortChange, onColumnOrderChange }: { sort?: TaskListSort; onSortChange?: (sort: TaskListSort) => void; onColumnOrderChange?: (columns: readonly TaskListColumnId[]) => void; }) { const { columns, order } = useContext(ColumnContext); const helpId = useId(); const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } })); return ( { const targets = pointerWithin(args); return targets.length ? targets : closestCenter(args); }} onDragEnd={({ active, over }) => { if (over) onColumnOrderChange?.(reorderTaskListColumns(order, active.id as TaskListColumnId, over.id as TaskListColumnId)); }} accessibility={{ screenReaderInstructions: { draggable: "Click to sort. Drag horizontally to reorder, or use Alt plus Left or Right Arrow." } }}> Drag to reorder columns, or use Alt plus Left or Right Arrow. Click to sort.
{!columns.includes("task") ? : null} {columns.map(id => column.id === id)!.label} start={taskListColumnStart(columns, id)} sort={sort} onSortChange={onSortChange} onColumnOrderChange={onColumnOrderChange} helpId={helpId} />)}
); } /** Compact refresh variant; the existing inbox picker uses legacy column IDs. */ export function TaskListColumnSettings({ visibleColumns, onVisibleColumnsChange, onColumnOrderChange, onResetDefaults }: { visibleColumns: readonly TaskListColumnId[]; onVisibleColumnsChange: (columns: readonly TaskListColumnId[]) => void; onColumnOrderChange?: (columns: readonly TaskListColumnId[]) => void; onResetDefaults?: () => void; }) { const { order } = useContext(ColumnContext); return ( Columns {order.map(id => ( event.preventDefault()} onCheckedChange={checked => onVisibleColumnsChange(order.filter(column => column === id ? checked === true : visibleColumns.includes(column)))} > {taskListColumns.find(column => column.id === id)!.label} ))} { event.preventDefault(); onColumnOrderChange?.(allTaskListColumns); onVisibleColumnsChange(defaultTaskListColumns); onResetDefaults?.(); }}>Reset defaults ); } export function TaskRowColumnCells({ identifier, values }: { identifier: string; values: TaskRowColumnValues }) { const columns = useTaskListColumns(); if (!detailColumns.some(column => columns.includes(column.id))) return null; return ( {columns.filter(id => id !== "task").map(id => ( {id === "taskId" ? identifier : values[id] ?? "—"} ))} ); }