@medalsocial/meda 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/components/ui/checkbox.d.ts +8 -0
  2. package/dist/components/ui/checkbox.js +9 -0
  3. package/dist/components/ui/collapsible.d.ts +5 -0
  4. package/dist/components/ui/collapsible.js +13 -0
  5. package/dist/kanban/index.d.ts +5 -0
  6. package/dist/kanban/index.js +4 -0
  7. package/dist/kanban/kanban-board.d.ts +8 -0
  8. package/dist/kanban/kanban-board.js +197 -0
  9. package/dist/kanban/kanban-card-wrapper.d.ts +16 -0
  10. package/dist/kanban/kanban-card-wrapper.js +20 -0
  11. package/dist/kanban/kanban-collision.d.ts +16 -0
  12. package/dist/kanban/kanban-collision.js +18 -0
  13. package/dist/kanban/kanban-column.d.ts +19 -0
  14. package/dist/kanban/kanban-column.js +19 -0
  15. package/dist/kanban/kanban-drop-handler.d.ts +16 -0
  16. package/dist/kanban/kanban-drop-handler.js +84 -0
  17. package/dist/kanban/types.d.ts +47 -0
  18. package/dist/kanban/types.js +7 -0
  19. package/dist/list/index.d.ts +2 -0
  20. package/dist/list/index.js +1 -0
  21. package/dist/list/list-row.d.ts +52 -0
  22. package/dist/list/list-row.js +37 -0
  23. package/dist/shell/drag-mode-banner.d.ts +31 -0
  24. package/dist/shell/drag-mode-banner.js +19 -0
  25. package/dist/shell/index.d.ts +6 -0
  26. package/dist/shell/index.js +4 -0
  27. package/dist/shell/rail-drop-slot.d.ts +41 -0
  28. package/dist/shell/rail-drop-slot.js +49 -0
  29. package/dist/shell/rail-drop-zones.d.ts +28 -0
  30. package/dist/shell/rail-drop-zones.js +17 -0
  31. package/dist/shell/shell-main.js +1 -1
  32. package/dist/timeline/index.d.ts +3 -0
  33. package/dist/timeline/index.js +2 -0
  34. package/dist/timeline/lane-timeline-types.d.ts +61 -0
  35. package/dist/timeline/lane-timeline-types.js +6 -0
  36. package/dist/timeline/lane-timeline.d.ts +2 -0
  37. package/dist/timeline/lane-timeline.js +85 -0
  38. package/dist/timeline/lane.d.ts +10 -0
  39. package/dist/timeline/lane.js +39 -0
  40. package/dist/timeline/public.d.ts +1 -0
  41. package/dist/timeline/time-axis.d.ts +18 -0
  42. package/dist/timeline/time-axis.js +22 -0
  43. package/package.json +24 -1
@@ -0,0 +1,8 @@
1
+ import { Checkbox as CheckboxPrimitive } from '@base-ui/react/checkbox';
2
+ interface CheckboxProps extends CheckboxPrimitive.Root.Props {
3
+ /** Show indeterminate (mixed) state - a dash instead of checkmark */
4
+ indeterminate?: boolean;
5
+ }
6
+ declare function Checkbox({ className, indeterminate, ...props }: CheckboxProps): import("react/jsx-runtime").JSX.Element;
7
+ export type { CheckboxProps };
8
+ export { Checkbox };
@@ -0,0 +1,9 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Checkbox as CheckboxPrimitive } from '@base-ui/react/checkbox';
4
+ import { CheckIcon, MinusIcon } from 'lucide-react';
5
+ import { cn } from '../../lib/utils.js';
6
+ function Checkbox({ className, indeterminate, ...props }) {
7
+ return (_jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", indeterminate: indeterminate, className: cn('peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input outline-none transition-colors after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 group-has-disabled/field:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:bg-input/30 dark:data-checked:bg-primary dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40', indeterminate && 'border-primary bg-primary text-primary-foreground', className), ...props, children: indeterminate ? (_jsx("span", { className: "grid place-content-center text-current [&>svg]:size-3.5", children: _jsx(MinusIcon, {}) })) : (_jsx(CheckboxPrimitive.Indicator, { "data-slot": "checkbox-indicator", className: "grid place-content-center text-current transition-none [&>svg]:size-3.5", children: _jsx(CheckIcon, {}) })) }));
8
+ }
9
+ export { Checkbox };
@@ -0,0 +1,5 @@
1
+ import { Collapsible as CollapsiblePrimitive } from '@base-ui/react/collapsible';
2
+ declare function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props): import("react/jsx-runtime").JSX.Element;
3
+ declare function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props): import("react/jsx-runtime").JSX.Element;
4
+ declare function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props): import("react/jsx-runtime").JSX.Element;
5
+ export { Collapsible, CollapsibleContent, CollapsibleTrigger };
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Collapsible as CollapsiblePrimitive } from '@base-ui/react/collapsible';
4
+ function Collapsible({ ...props }) {
5
+ return _jsx(CollapsiblePrimitive.Root, { "data-slot": "collapsible", ...props });
6
+ }
7
+ function CollapsibleTrigger({ ...props }) {
8
+ return _jsx(CollapsiblePrimitive.Trigger, { "data-slot": "collapsible-trigger", ...props });
9
+ }
10
+ function CollapsibleContent({ ...props }) {
11
+ return _jsx(CollapsiblePrimitive.Panel, { "data-slot": "collapsible-content", ...props });
12
+ }
13
+ export { Collapsible, CollapsibleContent, CollapsibleTrigger };
@@ -0,0 +1,5 @@
1
+ export { KanbanBoard } from './kanban-board.js';
2
+ export type { KanbanColumnDropHandlerArgs } from './kanban-drop-handler.js';
3
+ export { handleKanbanColumnDrop } from './kanban-drop-handler.js';
4
+ export type { KanbanBoardProps, KanbanColumn, KanbanItem, KanbanLabels, } from './types.js';
5
+ export { defaultKanbanLabels } from './types.js';
@@ -0,0 +1,4 @@
1
+ // open/meda/src/kanban/index.ts
2
+ export { KanbanBoard } from './kanban-board.js';
3
+ export { handleKanbanColumnDrop } from './kanban-drop-handler.js';
4
+ export { defaultKanbanLabels } from './types.js';
@@ -0,0 +1,8 @@
1
+ import { type KanbanBoardProps, type KanbanItem } from './types.js';
2
+ /**
3
+ * Generic Kanban Board Component
4
+ *
5
+ * A reusable drag-and-drop kanban board that can be used across
6
+ * different features (Deals, Ideas, etc.)
7
+ */
8
+ export declare function KanbanBoard<TItem extends KanbanItem, TStatus extends string = string>(props: KanbanBoardProps<TItem, TStatus>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,197 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { DndContext, DragOverlay, KeyboardSensor, MeasuringStrategy, PointerSensor, useDndMonitor, useSensor, useSensors, } from '@dnd-kit/core';
4
+ import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
5
+ import { ChevronRight, EyeOff, MoreHorizontal } from 'lucide-react';
6
+ import { useCallback, useDeferredValue, useMemo, useState } from 'react';
7
+ import { createPortal } from 'react-dom';
8
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '../components/ui/collapsible.js';
9
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '../components/ui/dropdown-menu.js';
10
+ import { cn } from '../lib/utils.js';
11
+ import { KanbanCardWrapper } from './kanban-card-wrapper.js';
12
+ import { kanbanCollisionDetection } from './kanban-collision.js';
13
+ import { KanbanColumn } from './kanban-column.js';
14
+ import { handleKanbanColumnDrop } from './kanban-drop-handler.js';
15
+ import { defaultKanbanLabels, } from './types.js';
16
+ const interpolate = (template, vars) => Object.entries(vars).reduce((s, [k, v]) => s.replace(new RegExp(`\\{\\{${k}\\}\\}`, 'g'), v), template);
17
+ function resolveOverColumnStatus(overId, columns, items) {
18
+ const targetColumn = columns.find((col) => col.id === overId);
19
+ if (targetColumn) {
20
+ return targetColumn.id;
21
+ }
22
+ const targetItem = items.find((item) => item.id === overId);
23
+ return targetItem?.status ?? null;
24
+ }
25
+ /**
26
+ * Inner component that always renders inside a DndContext — either the kanban's
27
+ * own (non-headless) or the consumer's outer context (headless). This means
28
+ * `useDndMonitor` is always safe to call here, enabling the DragOverlay to
29
+ * track external drags when `headless={true}`.
30
+ */
31
+ function KanbanBoardInner({ columns, items, renderCard, onReorder, onCardMove, canDropCard, onAddItem, showEmptyColumns = false, hiddenColumnIds = [], onHiddenColumnIdsChange, emptyColumnContent, labels, headless = false, className, }) {
32
+ const resolvedLabels = { ...defaultKanbanLabels, ...(labels ?? {}) };
33
+ const [activeId, setActiveId] = useState(null);
34
+ const [overColumnId, setOverColumnId] = useState(null);
35
+ // Defer item updates during drag to keep the overlay responsive
36
+ const deferredItems = useDeferredValue(items);
37
+ // Group items by status
38
+ const itemsByStatus = useMemo(() => {
39
+ const grouped = new Map();
40
+ // Initialize all columns
41
+ for (const column of columns) {
42
+ grouped.set(column.id, []);
43
+ }
44
+ // Group items
45
+ for (const item of deferredItems) {
46
+ const status = item.status;
47
+ const columnItems = grouped.get(status);
48
+ if (columnItems) {
49
+ columnItems.push(item);
50
+ }
51
+ }
52
+ // Sort each column by position
53
+ for (const [status, columnItems] of grouped) {
54
+ columnItems.sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
55
+ grouped.set(status, columnItems);
56
+ }
57
+ return grouped;
58
+ }, [columns, deferredItems]);
59
+ // Get the active item being dragged
60
+ const activeItem = useMemo(() => {
61
+ if (!activeId)
62
+ return null;
63
+ return items.find((item) => item.id === activeId) ?? null;
64
+ }, [activeId, items]);
65
+ const hiddenColumnIdSet = useMemo(() => new Set(hiddenColumnIds.map((columnId) => String(columnId))), [hiddenColumnIds]);
66
+ const hiddenColumns = useMemo(() => columns.filter((column) => hiddenColumnIdSet.has(String(column.id))), [columns, hiddenColumnIdSet]);
67
+ const explicitlyVisibleColumns = useMemo(() => columns.filter((column) => !hiddenColumnIdSet.has(String(column.id))), [columns, hiddenColumnIdSet]);
68
+ // Filter columns if not showing empty ones
69
+ const visibleColumns = useMemo(() => {
70
+ if (showEmptyColumns)
71
+ return explicitlyVisibleColumns;
72
+ return explicitlyVisibleColumns.filter((column) => {
73
+ const columnItems = itemsByStatus.get(column.id);
74
+ return columnItems && columnItems.length > 0;
75
+ });
76
+ }, [explicitlyVisibleColumns, itemsByStatus, showEmptyColumns]);
77
+ // Handle drag over (for preview) — used both by own DndContext and via useDndMonitor
78
+ const handleDragOver = useCallback((event) => {
79
+ const { over } = event;
80
+ if (!over) {
81
+ setOverColumnId(null);
82
+ return;
83
+ }
84
+ setOverColumnId(resolveOverColumnStatus(String(over.id), visibleColumns, items));
85
+ }, [items, visibleColumns]);
86
+ // Handle drag start
87
+ const handleDragStart = useCallback((event) => {
88
+ setActiveId(event.active.id);
89
+ }, []);
90
+ // Handle drag end
91
+ const handleDragEnd = useCallback((event) => {
92
+ setActiveId(null);
93
+ setOverColumnId(null);
94
+ handleKanbanColumnDrop({
95
+ event,
96
+ items,
97
+ columns: visibleColumns,
98
+ onCardMove,
99
+ onReorder,
100
+ canDropCard,
101
+ });
102
+ }, [items, visibleColumns, canDropCard, onReorder, onCardMove]);
103
+ // Always monitor the active DndContext (either the kanban's own in non-headless
104
+ // mode, or the consumer's outer context in headless mode). This is safe because
105
+ // KanbanBoardInner is always rendered inside a DndContext.
106
+ //
107
+ // In headless mode the consumer's outer onDragEnd is the single owner of drop
108
+ // routing — only update local UI state here to avoid double-firing handleKanbanColumnDrop.
109
+ useDndMonitor({
110
+ onDragStart: handleDragStart,
111
+ onDragOver: handleDragOver,
112
+ onDragEnd: headless
113
+ ? () => {
114
+ setActiveId(null);
115
+ setOverColumnId(null);
116
+ }
117
+ : handleDragEnd,
118
+ onDragCancel: () => {
119
+ setActiveId(null);
120
+ setOverColumnId(null);
121
+ },
122
+ });
123
+ const handleHideColumn = useCallback((columnId) => {
124
+ if (!onHiddenColumnIdsChange)
125
+ return;
126
+ const nextHiddenColumnIds = [...hiddenColumnIds];
127
+ if (!nextHiddenColumnIds.includes(columnId)) {
128
+ nextHiddenColumnIds.push(columnId);
129
+ onHiddenColumnIdsChange(nextHiddenColumnIds);
130
+ }
131
+ }, [hiddenColumnIds, onHiddenColumnIdsChange]);
132
+ const handleShowColumn = useCallback((columnId) => {
133
+ if (!onHiddenColumnIdsChange)
134
+ return;
135
+ onHiddenColumnIdsChange(hiddenColumnIds.filter((hiddenColumnId) => hiddenColumnId !== columnId));
136
+ }, [hiddenColumnIds, onHiddenColumnIdsChange]);
137
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { "data-slot": "kanban-board", className: cn('flex h-full gap-4 pb-4', className), children: [visibleColumns.map((column) => {
138
+ const columnItems = itemsByStatus.get(column.id) ?? [];
139
+ const canDropInColumn = !activeId ||
140
+ !canDropCard ||
141
+ activeItem?.status === column.id ||
142
+ canDropCard(activeId, column.id);
143
+ const isOver = overColumnId === column.id && canDropInColumn;
144
+ return (_jsx(SortableContext, { items: columnItems.map((item) => item.id), strategy: verticalListSortingStrategy, children: _jsx(KanbanColumn, { column: column, items: columnItems, count: columnItems.length, isOver: isOver, onAddItem: onAddItem && column.canAdd !== false
145
+ ? () => onAddItem(column.id)
146
+ : undefined, onHideColumn: onHiddenColumnIdsChange ? () => handleHideColumn(column.id) : undefined, labels: resolvedLabels, children: columnItems.length === 0 ? (
147
+ // If onAddItem is provided, show nothing (add button appears on hover)
148
+ // Otherwise show custom empty content or default "No items" message
149
+ (() => {
150
+ if (onAddItem && column.canAdd !== false)
151
+ return null;
152
+ return (emptyColumnContent || (_jsx("div", { className: "py-8 text-center text-muted-foreground text-sm", children: resolvedLabels.noItems })));
153
+ })()) : (_jsx("div", { className: "space-y-2", children: columnItems.map((item) => (_jsx(KanbanCardWrapper, { id: item.id, isDragging: activeId === item.id, children: renderCard(item) }, item.id))) })) }) }, column.id));
154
+ }), hiddenColumns.length > 0 && (_jsx(HiddenColumnsRail, { hiddenColumns: hiddenColumns, itemsByStatus: itemsByStatus, onShowColumn: handleShowColumn, labels: resolvedLabels }))] }), typeof document !== 'undefined'
155
+ ? createPortal(_jsx(DragOverlay, { dropAnimation: null, children: activeItem ? (_jsx("div", { className: "cursor-grabbing opacity-95 shadow-xl", children: renderCard(activeItem) })) : null }), document.body)
156
+ : null] }));
157
+ }
158
+ /**
159
+ * Generic Kanban Board Component
160
+ *
161
+ * A reusable drag-and-drop kanban board that can be used across
162
+ * different features (Deals, Ideas, etc.)
163
+ */
164
+ export function KanbanBoard(props) {
165
+ const { columns, onReorder, onCardMove, hiddenColumnIds = [], isLoading = false, headless = false, } = props;
166
+ // Setup sensors for drag detection (only when drag is enabled)
167
+ const dragSensors = useSensors(useSensor(PointerSensor, {
168
+ activationConstraint: {
169
+ distance: 5, // Keep drag responsive while avoiding accidental drags
170
+ },
171
+ }), useSensor(KeyboardSensor));
172
+ // Only use sensors if onReorder or onCardMove is provided
173
+ const sensors = onReorder || onCardMove ? dragSensors : [];
174
+ const hiddenColumnIdSet = useMemo(() => new Set(hiddenColumnIds.map((columnId) => String(columnId))), [hiddenColumnIds]);
175
+ const explicitlyVisibleColumns = useMemo(() => columns.filter((column) => !hiddenColumnIdSet.has(String(column.id))), [columns, hiddenColumnIdSet]);
176
+ // Loading skeleton
177
+ if (isLoading) {
178
+ const loadingColumns = explicitlyVisibleColumns.length > 0 ? explicitlyVisibleColumns : columns;
179
+ return (_jsx("div", { className: "flex gap-4 pb-4", children: loadingColumns.map((column) => (_jsxs("div", { className: "w-72 flex-shrink-0 animate-pulse rounded-lg bg-muted/50 p-4", children: [_jsx("div", { className: "mb-4 h-6 w-24 rounded bg-muted" }), _jsxs("div", { className: "space-y-3", children: [_jsx("div", { className: "h-24 rounded bg-muted" }), _jsx("div", { className: "h-24 rounded bg-muted" })] })] }, column.id))) }));
180
+ }
181
+ // In headless mode the consumer supplies the DndContext — KanbanBoardInner
182
+ // uses useDndMonitor to listen to it from inside the same tree.
183
+ if (headless) {
184
+ return _jsx(KanbanBoardInner, { ...props, headless: true });
185
+ }
186
+ return (_jsx(DndContext, { sensors: sensors, collisionDetection: kanbanCollisionDetection, measuring: {
187
+ droppable: {
188
+ strategy: MeasuringStrategy.Always,
189
+ },
190
+ }, children: _jsx(KanbanBoardInner, { ...props, headless: false }) }));
191
+ }
192
+ function HiddenColumnsRail({ hiddenColumns, itemsByStatus, onShowColumn, labels, }) {
193
+ return (_jsx("aside", { "data-slot": "kanban-hidden-columns-rail", className: "flex w-64 flex-shrink-0 flex-col gap-3 self-start rounded-lg border border-border/60 bg-background/95 p-3 shadow-sm backdrop-blur", children: _jsxs(Collapsible, { defaultOpen: true, children: [_jsxs(CollapsibleTrigger, { "data-slot": "kanban-hidden-columns-trigger", className: "flex w-full items-center gap-2 rounded-md p-1 text-left transition-colors hover:bg-accent/50", children: [_jsx(ChevronRight, { className: "h-4 w-4 text-muted-foreground transition-transform duration-200 [[data-open]>&]:rotate-90" }), _jsx(EyeOff, { className: "h-4 w-4 text-muted-foreground" }), _jsx("h3", { className: "font-medium text-sm", children: labels.hiddenColumns }), _jsx("span", { className: "ml-auto rounded-full bg-muted px-1.5 py-0.5 text-muted-foreground text-xs", children: hiddenColumns.length })] }), _jsx(CollapsibleContent, { "data-slot": "kanban-hidden-columns-content", className: "pt-3", children: _jsx("div", { className: "space-y-2", children: hiddenColumns.map((column) => {
194
+ const columnItems = itemsByStatus.get(column.id) ?? [];
195
+ return (_jsxs("div", { "data-slot": "kanban-hidden-column", "data-column-id": column.id, className: "flex items-center gap-3 rounded-md border border-border/60 bg-background p-3 shadow-sm", children: [column.icon ? (_jsx("span", { className: "flex-shrink-0", children: column.icon })) : (_jsx("div", { className: cn('h-2.5 w-2.5 rounded-full', column.accentClass ?? 'bg-muted-foreground') })), _jsx("div", { className: "min-w-0 flex-1", children: _jsx("div", { className: "truncate font-medium text-sm", children: column.label }) }), _jsx("span", { className: "text-muted-foreground text-sm tabular-nums", children: columnItems.length }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { render: _jsx("button", { type: "button", className: "rounded-md border border-border/70 p-2 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground", "aria-label": "Actions" }), children: _jsx(MoreHorizontal, { className: "h-4 w-4" }) }), _jsx(DropdownMenuContent, { align: "end", children: _jsx(DropdownMenuItem, { onSelect: () => onShowColumn(column.id), children: interpolate(labels.showColumn, { column: column.label }) }) })] })] }, column.id));
196
+ }) }) })] }) }));
197
+ }
@@ -0,0 +1,16 @@
1
+ import type { ReactNode } from 'react';
2
+ interface KanbanCardWrapperProps {
3
+ /** Unique ID for the draggable item */
4
+ id: string;
5
+ /** Whether the card is currently being dragged */
6
+ isDragging?: boolean;
7
+ /** The card content to render */
8
+ children: ReactNode;
9
+ }
10
+ /**
11
+ * KanbanCardWrapper Component
12
+ *
13
+ * Wraps card content to make it draggable within the kanban board.
14
+ */
15
+ export declare function KanbanCardWrapper({ id, isDragging, children }: KanbanCardWrapperProps): import("react/jsx-runtime").JSX.Element;
16
+ export {};
@@ -0,0 +1,20 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useSortable } from '@dnd-kit/sortable';
4
+ import { CSS } from '@dnd-kit/utilities';
5
+ import { cn } from '../lib/utils.js';
6
+ /**
7
+ * KanbanCardWrapper Component
8
+ *
9
+ * Wraps card content to make it draggable within the kanban board.
10
+ */
11
+ export function KanbanCardWrapper({ id, isDragging, children }) {
12
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging: isSortableDragging, } = useSortable({ id });
13
+ const style = {
14
+ transform: CSS.Transform.toString(transform),
15
+ transition,
16
+ };
17
+ const isCurrentlyDragging = isDragging || isSortableDragging;
18
+ return (_jsx("div", { ref: setNodeRef, style: style, ...attributes, ...listeners, "data-slot": "kanban-card", className: cn('cursor-grab active:cursor-grabbing', 'touch-manipulation', // Better touch handling
19
+ 'transition-shadow duration-200', isCurrentlyDragging && 'z-50 opacity-50 shadow-lg'), children: children }));
20
+ }
@@ -0,0 +1,16 @@
1
+ import { type CollisionDetection } from '@dnd-kit/core';
2
+ type CollisionArgs = Parameters<CollisionDetection>[0];
3
+ type CollisionResult = ReturnType<CollisionDetection>;
4
+ interface CollisionDetectionFns {
5
+ pointerWithinFn?: CollisionDetection;
6
+ closestCenterFn?: CollisionDetection;
7
+ }
8
+ export declare function runKanbanCollisionDetection(args: CollisionArgs, fns?: CollisionDetectionFns): CollisionResult;
9
+ export declare const kanbanCollisionDetection: CollisionDetection;
10
+ export declare function isKanbanCardDropAllowed<TStatus extends string>({ itemId, sourceStatus, targetStatus, canDropCard, }: {
11
+ itemId: string;
12
+ sourceStatus: TStatus;
13
+ targetStatus: TStatus;
14
+ canDropCard?: (itemId: string, targetStatus: TStatus) => boolean;
15
+ }): boolean;
16
+ export {};
@@ -0,0 +1,18 @@
1
+ import { closestCenter, pointerWithin } from '@dnd-kit/core';
2
+ export function runKanbanCollisionDetection(args, fns = {}) {
3
+ const pointerWithinFn = fns.pointerWithinFn ?? pointerWithin;
4
+ const closestCenterFn = fns.closestCenterFn ?? closestCenter;
5
+ const activeId = String(args.active.id);
6
+ const pointerCollisions = pointerWithinFn(args).filter((collision) => String(collision.id) !== activeId);
7
+ if (pointerCollisions.length > 0) {
8
+ return pointerCollisions;
9
+ }
10
+ return closestCenterFn(args).filter((collision) => String(collision.id) !== activeId);
11
+ }
12
+ export const kanbanCollisionDetection = (args) => runKanbanCollisionDetection(args);
13
+ export function isKanbanCardDropAllowed({ itemId, sourceStatus, targetStatus, canDropCard, }) {
14
+ if (sourceStatus === targetStatus) {
15
+ return true;
16
+ }
17
+ return canDropCard ? canDropCard(itemId, targetStatus) : true;
18
+ }
@@ -0,0 +1,19 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { KanbanColumn as KanbanColumnDef, KanbanItem, KanbanLabels } from './types.js';
3
+ interface KanbanColumnProps<TItem extends KanbanItem> {
4
+ column: KanbanColumnDef;
5
+ items: TItem[];
6
+ count: number;
7
+ isOver?: boolean;
8
+ onAddItem?: () => void;
9
+ onHideColumn?: () => void;
10
+ labels: KanbanLabels;
11
+ children: ReactNode;
12
+ }
13
+ /**
14
+ * KanbanColumn Component
15
+ *
16
+ * A single column in the kanban board that can receive dropped items.
17
+ */
18
+ export declare function KanbanColumn<TItem extends KanbanItem>({ column, count, isOver, onAddItem, onHideColumn, labels, children, }: KanbanColumnProps<TItem>): import("react/jsx-runtime").JSX.Element;
19
+ export {};
@@ -0,0 +1,19 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useDroppable } from '@dnd-kit/core';
4
+ import { MoreHorizontal, Plus } from 'lucide-react';
5
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '../components/ui/dropdown-menu.js';
6
+ import { cn } from '../lib/utils.js';
7
+ const interpolate = (template, vars) => Object.entries(vars).reduce((s, [k, v]) => s.replace(new RegExp(`\\{\\{${k}\\}\\}`, 'g'), v), template);
8
+ /**
9
+ * KanbanColumn Component
10
+ *
11
+ * A single column in the kanban board that can receive dropped items.
12
+ */
13
+ export function KanbanColumn({ column, count, isOver, onAddItem, onHideColumn, labels, children, }) {
14
+ const { setNodeRef, isOver: isDroppableOver } = useDroppable({
15
+ id: column.id,
16
+ });
17
+ const showDropIndicator = isOver || isDroppableOver;
18
+ return (_jsxs("div", { ref: setNodeRef, "data-slot": "kanban-column", "data-column-id": column.id, className: cn('group/column flex w-72 flex-shrink-0 flex-col rounded-lg bg-muted/30', 'transition-colors duration-200', showDropIndicator && 'bg-primary/5 ring-2 ring-primary/20'), children: [_jsxs("div", { "data-slot": "kanban-column-header", className: "flex items-center gap-2 p-3 pb-2", children: [column.icon ? (_jsx("span", { className: "flex-shrink-0", children: column.icon })) : (_jsx("div", { className: cn('h-2 w-2 rounded-full', column.accentClass ?? 'bg-muted-foreground') })), _jsx("h3", { "data-slot": "kanban-column-title", className: "font-medium text-sm", children: column.label }), _jsx("span", { className: "rounded-full bg-muted px-1.5 py-0.5 text-muted-foreground text-xs", children: count }), (onAddItem || onHideColumn) && (_jsxs("div", { "data-slot": "kanban-column-actions", className: "ml-auto flex items-center gap-1", children: [onAddItem && (_jsx("button", { type: "button", onClick: onAddItem, className: "rounded p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground", "aria-label": interpolate(labels.addItemTo, { column: column.label }), children: _jsx(Plus, { className: "h-4 w-4" }) })), onHideColumn && (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { render: _jsx("button", { type: "button", className: "rounded p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground", "aria-label": "Actions" }), children: _jsx(MoreHorizontal, { className: "h-4 w-4" }) }), _jsx(DropdownMenuContent, { align: "end", children: _jsx(DropdownMenuItem, { onSelect: onHideColumn, children: labels.hideColumn }) })] }))] }))] }), _jsxs("div", { "data-slot": "kanban-column-content", className: cn('min-h-[100px] flex-1 overflow-y-auto p-2 pt-0 [contain:content]', 'transition-colors duration-200'), children: [children, onAddItem && (_jsxs("button", { type: "button", onClick: onAddItem, className: cn('flex w-full items-center justify-center gap-1.5 rounded-md border border-transparent border-dashed p-2 text-muted-foreground text-sm opacity-0 transition-[color,opacity,background-color,border-color] hover:border-muted-foreground/30 hover:bg-accent/50 hover:text-foreground focus-visible:opacity-100 group-hover/column:opacity-100', count > 0 && 'mt-2'), "aria-label": interpolate(labels.addItemTo, { column: column.label }), children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx("span", { children: "Add" })] }))] }), showDropIndicator && count === 0 && (_jsx("div", { className: "mx-2 mb-2 h-1 rounded-full bg-primary/30" }))] }));
19
+ }
@@ -0,0 +1,16 @@
1
+ import type { DragEndEvent } from '@dnd-kit/core';
2
+ import type { KanbanItem } from './types.js';
3
+ export interface KanbanColumnDropHandlerArgs<TItem extends KanbanItem, TStatus extends string> {
4
+ event: DragEndEvent;
5
+ items: TItem[];
6
+ columns: {
7
+ id: string;
8
+ }[];
9
+ onCardMove?: (itemId: string, newStatus: TStatus) => void;
10
+ onReorder?: (itemId: string, newStatus: TStatus, newPosition: number) => void;
11
+ canDropCard?: (itemId: string, targetStatus: TStatus) => boolean;
12
+ }
13
+ /** Returns true if the drag was handled as a kanban column drop (column-to-column
14
+ * move or reorder); false if `over` was null or did not resolve to a kanban column/
15
+ * item, so the consumer can handle it (e.g. an external drop slot). */
16
+ export declare function handleKanbanColumnDrop<TItem extends KanbanItem, TStatus extends string>(args: KanbanColumnDropHandlerArgs<TItem, TStatus>): boolean;
@@ -0,0 +1,84 @@
1
+ import { isKanbanCardDropAllowed } from './kanban-collision.js';
2
+ /** Returns true if the drag was handled as a kanban column drop (column-to-column
3
+ * move or reorder); false if `over` was null or did not resolve to a kanban column/
4
+ * item, so the consumer can handle it (e.g. an external drop slot). */
5
+ export function handleKanbanColumnDrop(args) {
6
+ const { event, items, columns, onCardMove, onReorder, canDropCard } = args;
7
+ const { active, over } = event;
8
+ if (!over)
9
+ return false;
10
+ const activeItemId = active.id;
11
+ const overId = over.id;
12
+ const draggedItem = items.find((item) => item.id === activeItemId);
13
+ if (!draggedItem)
14
+ return false;
15
+ // Build itemsByStatus map for position resolution
16
+ const itemsByStatus = new Map();
17
+ for (const column of columns) {
18
+ itemsByStatus.set(column.id, []);
19
+ }
20
+ for (const item of items) {
21
+ const status = item.status;
22
+ const columnItems = itemsByStatus.get(status);
23
+ if (columnItems) {
24
+ columnItems.push(item);
25
+ }
26
+ }
27
+ for (const [status, columnItems] of itemsByStatus) {
28
+ columnItems.sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
29
+ itemsByStatus.set(status, columnItems);
30
+ }
31
+ // Resolve drop target: check if over.id is a column id
32
+ const targetColumn = columns.find((col) => col.id === overId);
33
+ if (targetColumn) {
34
+ const targetStatus = targetColumn.id;
35
+ const columnItems = itemsByStatus.get(targetStatus) ?? [];
36
+ if (!isKanbanCardDropAllowed({
37
+ itemId: activeItemId,
38
+ sourceStatus: draggedItem.status,
39
+ targetStatus,
40
+ canDropCard,
41
+ })) {
42
+ return true; // handled (rejected) — don't fall through to external handler
43
+ }
44
+ if (draggedItem.status !== targetStatus || draggedItem.position !== columnItems.length) {
45
+ if (onReorder) {
46
+ onReorder(activeItemId, targetStatus, columnItems.length);
47
+ }
48
+ if (onCardMove && draggedItem.status !== targetStatus) {
49
+ void Promise.resolve(onCardMove(activeItemId, targetStatus)).catch(() => {
50
+ // Mutation handlers handle toast + rollback on failure.
51
+ });
52
+ }
53
+ }
54
+ return true;
55
+ }
56
+ // Check if over.id is an item id in one of the kanban columns
57
+ const targetItem = items.find((item) => item.id === overId);
58
+ if (!targetItem)
59
+ return false;
60
+ const targetStatus = targetItem.status;
61
+ const columnItems = itemsByStatus.get(targetStatus) ?? [];
62
+ const targetIndex = columnItems.findIndex((item) => item.id === overId);
63
+ if (targetIndex === -1)
64
+ return false;
65
+ if (!isKanbanCardDropAllowed({
66
+ itemId: activeItemId,
67
+ sourceStatus: draggedItem.status,
68
+ targetStatus,
69
+ canDropCard,
70
+ })) {
71
+ return true; // handled (rejected)
72
+ }
73
+ if (draggedItem.status !== targetStatus || draggedItem.position !== targetIndex) {
74
+ if (onReorder) {
75
+ onReorder(activeItemId, targetStatus, targetIndex);
76
+ }
77
+ if (onCardMove && draggedItem.status !== targetStatus) {
78
+ void Promise.resolve(onCardMove(activeItemId, targetStatus)).catch(() => {
79
+ // Mutation handlers handle toast + rollback on failure.
80
+ });
81
+ }
82
+ }
83
+ return true;
84
+ }
@@ -0,0 +1,47 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface KanbanItem {
3
+ id: string;
4
+ status: string;
5
+ position: number;
6
+ }
7
+ export interface KanbanColumn {
8
+ id: string;
9
+ label: string;
10
+ /** Tailwind class string for the column accent — e.g. 'bg-success-500' or a token-driven class. */
11
+ accentClass?: string;
12
+ /** Optional icon node rendered next to the column label. */
13
+ icon?: ReactNode;
14
+ /** Whether the column accepts new items via the add-button. */
15
+ canAdd?: boolean;
16
+ }
17
+ export interface KanbanLabels {
18
+ /** "Add to {{column}}" — `{{column}}` replaced at render. */
19
+ addItemTo: string;
20
+ hideColumn: string;
21
+ noItems: string;
22
+ /** "Show {{column}}" */
23
+ showColumn: string;
24
+ hiddenColumns: string;
25
+ }
26
+ export declare const defaultKanbanLabels: KanbanLabels;
27
+ export interface KanbanBoardProps<TItem extends KanbanItem, TStatus extends string = string> {
28
+ columns: KanbanColumn[];
29
+ items: TItem[];
30
+ renderCard: (item: TItem) => ReactNode;
31
+ onReorder?: (itemId: string, newStatus: TStatus, newPosition: number) => void;
32
+ onCardMove?: (itemId: string, newStatus: TStatus) => void;
33
+ canDropCard?: (itemId: string, targetStatus: TStatus) => boolean;
34
+ onAddItem?: (columnId: TStatus) => void;
35
+ showEmptyColumns?: boolean;
36
+ hiddenColumnIds?: TStatus[];
37
+ onHiddenColumnIdsChange?: (ids: TStatus[]) => void;
38
+ isLoading?: boolean;
39
+ emptyColumnContent?: ReactNode;
40
+ labels?: Partial<KanbanLabels>;
41
+ className?: string;
42
+ /** When true, KanbanBoard skips its internal `<DndContext>`. The consumer must
43
+ * wrap KanbanBoard and any sibling drop targets (e.g. RailDropSlot) in a shared
44
+ * `<DndContext>` and supply a unified `onDragEnd` that routes column drops via
45
+ * `handleKanbanColumnDrop` and external drops manually. */
46
+ headless?: boolean;
47
+ }
@@ -0,0 +1,7 @@
1
+ export const defaultKanbanLabels = {
2
+ addItemTo: 'Add to {{column}}',
3
+ hideColumn: 'Hide column',
4
+ noItems: 'No items',
5
+ showColumn: 'Show {{column}}',
6
+ hiddenColumns: 'Hidden columns',
7
+ };
@@ -0,0 +1,2 @@
1
+ export type { ListCellProps, ListRowProps } from './list-row.js';
2
+ export { ListCell, ListRow } from './list-row.js';
@@ -0,0 +1 @@
1
+ export { ListCell, ListRow } from './list-row.js';
@@ -0,0 +1,52 @@
1
+ /**
2
+ * List Row Component
3
+ *
4
+ * Base row wrapper for Linear-style table rows.
5
+ * Provides consistent styling for hover, selection, and keyboard focus states.
6
+ */
7
+ import type { ReactNode } from 'react';
8
+ export interface ListRowProps {
9
+ /** Whether this row is selected */
10
+ selected?: boolean;
11
+ /** Whether any item in the list is selected (keeps checkbox always visible) */
12
+ selectionActive?: boolean;
13
+ /** Callback when selection changes (shiftKey for range selection) */
14
+ onSelect?: (selected: boolean, shiftKey?: boolean) => void;
15
+ /** Click handler for row navigation */
16
+ onClick?: () => void;
17
+ /** Mouse enter handler for prefetching */
18
+ onMouseEnter?: () => void;
19
+ /** Mouse leave handler */
20
+ onMouseLeave?: () => void;
21
+ /** Focus handler for keyboard intent prefetching */
22
+ onFocus?: () => void;
23
+ /** Blur handler */
24
+ onBlur?: () => void;
25
+ /** Whether this row has keyboard focus */
26
+ focused?: boolean;
27
+ /** Children to render inside the row */
28
+ children: ReactNode;
29
+ /** Additional class names */
30
+ className?: string;
31
+ }
32
+ /**
33
+ * A Linear-style list row with checkbox, hover states, and click handling.
34
+ * Compact ~40-48px height for dense, scannable lists.
35
+ */
36
+ export declare function ListRow({ selected, selectionActive, onSelect, onClick, onMouseEnter, onMouseLeave, onFocus, onBlur, focused, children, className, }: ListRowProps): import("react/jsx-runtime").JSX.Element;
37
+ /**
38
+ * Cell wrapper for consistent column sizing
39
+ */
40
+ export interface ListCellProps {
41
+ /** Width class (e.g., 'w-8', 'w-24', 'flex-1') */
42
+ width?: string;
43
+ /** Whether to shrink if needed */
44
+ shrink?: boolean;
45
+ /** Alignment */
46
+ align?: 'left' | 'center' | 'right';
47
+ /** Children */
48
+ children?: ReactNode;
49
+ /** Additional class names */
50
+ className?: string;
51
+ }
52
+ export declare function ListCell({ width, shrink, align, children, className, }: ListCellProps): import("react/jsx-runtime").JSX.Element;