@medalsocial/meda 1.1.2 → 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 (42) 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/timeline/index.d.ts +3 -0
  32. package/dist/timeline/index.js +2 -0
  33. package/dist/timeline/lane-timeline-types.d.ts +61 -0
  34. package/dist/timeline/lane-timeline-types.js +6 -0
  35. package/dist/timeline/lane-timeline.d.ts +2 -0
  36. package/dist/timeline/lane-timeline.js +85 -0
  37. package/dist/timeline/lane.d.ts +10 -0
  38. package/dist/timeline/lane.js +39 -0
  39. package/dist/timeline/public.d.ts +1 -0
  40. package/dist/timeline/time-axis.d.ts +18 -0
  41. package/dist/timeline/time-axis.js +22 -0
  42. 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;
@@ -0,0 +1,37 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Checkbox } from '../components/ui/checkbox.js';
4
+ import { cn } from '../lib/utils.js';
5
+ /**
6
+ * A Linear-style list row with checkbox, hover states, and click handling.
7
+ * Compact ~40-48px height for dense, scannable lists.
8
+ */
9
+ export function ListRow({ selected, selectionActive, onSelect, onClick, onMouseEnter, onMouseLeave, onFocus, onBlur, focused, children, className, }) {
10
+ const isInteractive = !!onClick;
11
+ const interactiveProps = isInteractive
12
+ ? {
13
+ role: 'button',
14
+ tabIndex: 0,
15
+ onClick,
16
+ onKeyDown: (e) => {
17
+ if (e.key === 'Enter' || e.key === ' ') {
18
+ e.preventDefault();
19
+ onClick?.();
20
+ }
21
+ },
22
+ }
23
+ : {};
24
+ return (_jsxs("div", { ...interactiveProps, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, onFocus: onFocus, onBlur: onBlur, className: cn('group relative flex items-center gap-3 border-border border-b px-3 py-2.5', 'transition-all duration-150', isInteractive && 'cursor-pointer hover:bg-accent/50', isInteractive &&
25
+ "before:absolute before:inset-y-0 before:left-0 before:w-[2px] before:scale-y-0 before:bg-primary before:transition-transform before:duration-150 before:content-['']", isInteractive && 'hover:before:scale-y-100', isInteractive && 'focus-within:bg-accent/30 focus-within:before:scale-y-100', selected && isInteractive && 'before:!scale-y-100 bg-accent', selected && !isInteractive && 'bg-accent', focused &&
26
+ isInteractive &&
27
+ 'before:!scale-y-100 bg-accent/30 ring-1 ring-primary/50 ring-inset', focused && !isInteractive && 'bg-accent/30 ring-1 ring-primary/50 ring-inset', className), children: [onSelect && (_jsx("div", { className: "w-6 flex-shrink-0", onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), children: _jsx(Checkbox, { checked: selected, onClick: (e) => {
28
+ e.preventDefault();
29
+ onSelect(!selected, e.shiftKey);
30
+ }, className: cn('transition-opacity', selectionActive || selected
31
+ ? 'opacity-100'
32
+ : 'opacity-0 group-hover:opacity-100 data-[state=checked]:opacity-100') }) })), children] }));
33
+ }
34
+ export function ListCell({ width = 'flex-1', shrink = true, align = 'left', children, className, }) {
35
+ return (_jsx("div", { className: cn(width, shrink === false && 'flex-shrink-0', align === 'center' && 'text-center', align === 'right' && 'text-right', 'min-w-0', // Allow truncation
36
+ className), children: children }));
37
+ }
@@ -0,0 +1,31 @@
1
+ import { type ReactNode } from 'react';
2
+ /**
3
+ * Props for DragModeBanner.
4
+ *
5
+ * **Important:** This component uses `useDndMonitor` internally and must be
6
+ * rendered inside the same `<DndContext>` tree it is monitoring.
7
+ *
8
+ * **Known limitation:** The ESC chip is decorative only. Keyboard-initiated
9
+ * drags cancel naturally via dnd-kit's KeyboardSensor; pointer-initiated drags
10
+ * require the user to release outside any drop zone — a manual ESC handler for
11
+ * pointer drags is not yet implemented.
12
+ */
13
+ export interface DragModeBannerProps {
14
+ /** Banner message shown while a drag is active. */
15
+ message: ReactNode;
16
+ /**
17
+ * When provided, render an ESC chip indicating the user can press Escape to
18
+ * cancel. See known limitation note above.
19
+ */
20
+ cancelKey?: 'ESC';
21
+ /**
22
+ * Optional predicate; when supplied only activates the banner for matching
23
+ * drag ids. Receives the `active.id` of the current drag.
24
+ */
25
+ isActive?: (activeId: string | number) => boolean;
26
+ className?: string;
27
+ }
28
+ export declare function DragModeBanner({ message, cancelKey, isActive, className }: DragModeBannerProps): import("react/jsx-runtime").JSX.Element | null;
29
+ export declare namespace DragModeBanner {
30
+ var displayName: string;
31
+ }
@@ -0,0 +1,19 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useDndMonitor } from '@dnd-kit/core';
4
+ import { useState } from 'react';
5
+ import { cn } from '../lib/utils.js';
6
+ export function DragModeBanner({ message, cancelKey, isActive, className }) {
7
+ const [activeId, setActiveId] = useState(null);
8
+ useDndMonitor({
9
+ onDragStart: (e) => setActiveId(e.active.id),
10
+ onDragEnd: () => setActiveId(null),
11
+ onDragCancel: () => setActiveId(null),
12
+ });
13
+ if (activeId == null)
14
+ return null;
15
+ if (isActive && !isActive(activeId))
16
+ return null;
17
+ return (_jsxs("div", { role: "status", "aria-live": "polite", className: cn('pointer-events-none flex items-center justify-center gap-3 px-4 py-2 text-sm text-muted-foreground', className), children: [_jsx("span", { children: message }), cancelKey === 'ESC' && (_jsx("kbd", { className: "rounded border border-border bg-muted px-1.5 py-0.5 font-medium font-mono text-xs", children: "ESC" }))] }));
18
+ }
19
+ DragModeBanner.displayName = 'DragModeBanner';
@@ -2,11 +2,17 @@ export { AppShell, AppShellBody } from './app-shell.js';
2
2
  export type { CommandGroupDefinition } from './command-palette.js';
3
3
  export { CommandPalette, useCommandGroup, useCommands } from './command-palette.js';
4
4
  export { ContextRail } from './context-rail.js';
5
+ export type { DragModeBannerProps } from './drag-mode-banner.js';
6
+ export { DragModeBanner } from './drag-mode-banner.js';
5
7
  export * as Extras from './extras/index.js';
6
8
  export { IconRail, RailDivider } from './icon-rail.js';
7
9
  export type { ShellStorageAdapter } from './layout-state.js';
8
10
  export { createLocalStorageAdapter } from './layout-state.js';
9
11
  export { motion } from './motion.js';
12
+ export type { RailDropSlotProps, RailDropSlotState } from './rail-drop-slot.js';
13
+ export { RailDropSlot } from './rail-drop-slot.js';
14
+ export type { RailDropZonesProps } from './rail-drop-zones.js';
15
+ export { RailDropZones } from './rail-drop-zones.js';
10
16
  export { ResizableHandle, ResizableShell, ResizableShellPanel } from './resizable-shell.js';
11
17
  export { RightPanel } from './right-panel.js';
12
18
  export { AppTabs, PanelToggle, ShellHeader, WorkspaceSwitcher } from './shell-header.js';
@@ -8,6 +8,8 @@ export { AppShell, AppShellBody } from './app-shell.js';
8
8
  // Command palette
9
9
  export { CommandPalette, useCommandGroup, useCommands } from './command-palette.js';
10
10
  export { ContextRail } from './context-rail.js';
11
+ // Drag-and-drop utilities
12
+ export { DragModeBanner } from './drag-mode-banner.js';
11
13
  // Extras (legacy components ported during Phase 15 — opt-in for apps that need them)
12
14
  export * as Extras from './extras/index.js';
13
15
  // Rails + main + panel
@@ -17,6 +19,8 @@ export { createLocalStorageAdapter } from './layout-state.js';
17
19
  // Hooks + tokens
18
20
  export { motion } from './motion.js';
19
21
  // Resize primitives
22
+ export { RailDropSlot } from './rail-drop-slot.js';
23
+ export { RailDropZones } from './rail-drop-zones.js';
20
24
  export { ResizableHandle, ResizableShell, ResizableShellPanel } from './resizable-shell.js';
21
25
  export { RightPanel } from './right-panel.js';
22
26
  // Header (and its individual children for advanced composition)
@@ -0,0 +1,41 @@
1
+ import { type ReactNode, type Ref } from 'react';
2
+ /**
3
+ * Describes the slot's current relation to an active drag operation.
4
+ *
5
+ * - `'idle'` — no drag in progress.
6
+ * - `'active'` — a drag is in progress and this slot would accept it, but
7
+ * the cursor is not currently over this slot.
8
+ * - `'over'` — the dragged item is hovering over this slot and it would
9
+ * accept the drop.
10
+ * - `'rejected'` — the dragged item is hovering over this slot but the slot
11
+ * cannot accept it (disabled or `accepts` returns false), OR
12
+ * the slot is `disabled` while any drag is active.
13
+ */
14
+ export type RailDropSlotState = 'idle' | 'active' | 'over' | 'rejected';
15
+ export interface RailDropSlotProps {
16
+ /** Stable id used by dnd-kit; also rendered as `data-slot-id`. */
17
+ id: string;
18
+ /** Decide whether this slot accepts the dragged card. */
19
+ accepts?: (activeId: string) => boolean;
20
+ /** Whether this slot can currently receive a drop (e.g. capacity, online). */
21
+ disabled?: boolean;
22
+ /**
23
+ * Static content. Mutually exclusive with `render`.
24
+ * Use `render` when you need state-aware content.
25
+ */
26
+ children?: ReactNode;
27
+ /**
28
+ * State-aware content factory. Receives the slot's current `RailDropSlotState`
29
+ * so you can render different UI for idle / active / over / rejected states.
30
+ * Mutually exclusive with `children`.
31
+ */
32
+ render?: (state: RailDropSlotState) => ReactNode;
33
+ className?: string;
34
+ /** Visible label for assistive tech. */
35
+ ariaLabel?: string;
36
+ ref?: Ref<HTMLElement>;
37
+ }
38
+ export declare function RailDropSlot({ id, accepts, disabled, children, render, className, ariaLabel, ref, }: RailDropSlotProps): import("react/jsx-runtime").JSX.Element;
39
+ export declare namespace RailDropSlot {
40
+ var displayName: string;
41
+ }
@@ -0,0 +1,49 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useDndMonitor, useDroppable } from '@dnd-kit/core';
4
+ import { useState } from 'react';
5
+ import { cn } from '../lib/utils.js';
6
+ export function RailDropSlot({ id, accepts, disabled, children, render, className, ariaLabel, ref, }) {
7
+ const { isOver, active, setNodeRef } = useDroppable({
8
+ id,
9
+ disabled,
10
+ data: { type: 'rail-drop-slot' },
11
+ });
12
+ const [globalDragActive, setGlobalDragActive] = useState(false);
13
+ useDndMonitor({
14
+ onDragStart: () => setGlobalDragActive(true),
15
+ onDragEnd: () => setGlobalDragActive(false),
16
+ onDragCancel: () => setGlobalDragActive(false),
17
+ });
18
+ const wouldAccept = !disabled && (!accepts || (active?.id != null && accepts(String(active.id))));
19
+ let state;
20
+ if (isOver && wouldAccept) {
21
+ state = 'over';
22
+ }
23
+ else if (isOver && !wouldAccept) {
24
+ state = 'rejected';
25
+ }
26
+ else if (globalDragActive && disabled) {
27
+ state = 'rejected';
28
+ }
29
+ else if (globalDragActive && wouldAccept) {
30
+ state = 'active';
31
+ }
32
+ else {
33
+ state = 'idle';
34
+ }
35
+ // Legacy boolean aliases kept for backward-compat data attributes
36
+ const showHover = state === 'over';
37
+ const showRejected = state === 'rejected';
38
+ return (_jsx("section", { ref: (el) => {
39
+ setNodeRef(el);
40
+ if (typeof ref === 'function')
41
+ ref(el);
42
+ else if (ref)
43
+ ref.current = el;
44
+ }, "data-slot-id": id, "data-state": state, "aria-label": ariaLabel, "aria-disabled": disabled || undefined, className: cn('relative rounded-lg border border-border bg-card transition-all duration-150', state === 'idle' && '', state === 'active' && 'border-dashed border-primary/60 bg-primary/5', state === 'over' &&
45
+ 'border-primary bg-primary/5 ring-2 ring-primary/30 shadow-[0_0_0_4px_hsl(var(--primary)/0.1)]', state === 'rejected' && 'border-destructive/40 opacity-60',
46
+ // Legacy classes — keep working for consumers using data-state selectors
47
+ showHover && '', showRejected && '', disabled && state === 'idle' && 'opacity-60', className), children: render ? render(state) : children }));
48
+ }
49
+ RailDropSlot.displayName = 'RailDropSlot';
@@ -0,0 +1,28 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface RailDropZonesProps {
3
+ /** Section title — uppercased in the header. Defaults to "Drop zones". */
4
+ title?: string;
5
+ /** Subtitle shown under the title (e.g. "Eligible machines for ENG-405 · claude"). */
6
+ subtitle?: ReactNode;
7
+ /** Count badge rendered in the header corner, e.g. "3 / 4". */
8
+ count?: ReactNode;
9
+ /** Optional icon rendered at the start of the header. */
10
+ icon?: ReactNode;
11
+ /**
12
+ * When set, overrides the auto-detected drag-active state. Useful for
13
+ * Storybook stories that want to demonstrate the active visual without a
14
+ * real drag in progress.
15
+ */
16
+ forceActive?: boolean;
17
+ /**
18
+ * Optional predicate; when supplied only activates the rail outline for
19
+ * matching drag ids. Receives `active.id` of the current drag.
20
+ */
21
+ isActive?: (activeId: string | number) => boolean;
22
+ children: ReactNode;
23
+ className?: string;
24
+ }
25
+ export declare function RailDropZones({ title, subtitle, count, icon, forceActive, isActive, children, className, }: RailDropZonesProps): import("react/jsx-runtime").JSX.Element;
26
+ export declare namespace RailDropZones {
27
+ var displayName: string;
28
+ }
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useDndMonitor } from '@dnd-kit/core';
4
+ import { useState } from 'react';
5
+ import { cn } from '../lib/utils.js';
6
+ export function RailDropZones({ title = 'Drop zones', subtitle, count, icon, forceActive, isActive, children, className, }) {
7
+ const [activeId, setActiveId] = useState(null);
8
+ useDndMonitor({
9
+ onDragStart: (e) => setActiveId(e.active.id),
10
+ onDragEnd: () => setActiveId(null),
11
+ onDragCancel: () => setActiveId(null),
12
+ });
13
+ const auto = activeId != null && (!isActive || isActive(activeId));
14
+ const active = forceActive ?? auto;
15
+ return (_jsxs("section", { "data-active": active || undefined, className: cn('flex flex-col gap-3 rounded-lg border border-transparent p-3 transition-colors', active && 'border-2 border-dashed border-primary/60 bg-primary/5', className), children: [(title || subtitle || count) && (_jsxs("header", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "flex items-start gap-2", children: [icon && _jsx("span", { className: "text-primary", children: icon }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [title && (_jsx("span", { className: "font-medium text-muted-foreground text-xs uppercase tracking-wider", children: title })), subtitle && _jsx("span", { className: "text-muted-foreground text-xs", children: subtitle })] })] }), count && _jsx("span", { className: "text-muted-foreground text-xs", children: count })] })), _jsx("div", { className: "flex flex-col gap-2", children: children })] }));
16
+ }
17
+ RailDropZones.displayName = 'RailDropZones';
@@ -2,6 +2,9 @@ export type { DateSwitcherProps } from './date-switcher.js';
2
2
  export { DateSwitcher } from './date-switcher.js';
3
3
  export type { EventCardProps, EventCardSize } from './event-card.js';
4
4
  export { EventCard } from './event-card.js';
5
+ export { LaneTimeline } from './lane-timeline.js';
6
+ export type { Lane, LaneBar, LaneLegendItem, LaneTimelineLabels, LaneTimelineProps, LaneTimelineRange, } from './lane-timeline-types.js';
7
+ export { defaultLaneTimelineLabels } from './lane-timeline-types.js';
5
8
  export type { LiveIndicatorProps } from './live-indicator.js';
6
9
  export { LiveIndicator } from './live-indicator.js';
7
10
  export type { ScrubBarProps } from './scrub-bar.js';
@@ -1,5 +1,7 @@
1
1
  export { DateSwitcher } from './date-switcher.js';
2
2
  export { EventCard } from './event-card.js';
3
+ export { LaneTimeline } from './lane-timeline.js';
4
+ export { defaultLaneTimelineLabels } from './lane-timeline-types.js';
3
5
  export { LiveIndicator } from './live-indicator.js';
4
6
  export { ScrubBar } from './scrub-bar.js';
5
7
  export { TimelineRail } from './timeline-rail.js';
@@ -0,0 +1,61 @@
1
+ import type { ReactNode } from 'react';
2
+ export type LaneTimelineRange = '1h' | '6h' | '24h' | '7d';
3
+ export interface LaneBar {
4
+ id: string;
5
+ start: Date;
6
+ end: Date;
7
+ label: string;
8
+ /** Tailwind class for the bar fill, e.g. 'bg-success-500/80'. */
9
+ fillClass: string;
10
+ /** Optional left-edge accent class, e.g. 'border-l-4 border-l-success-700'. */
11
+ accentClass?: string;
12
+ }
13
+ export interface Lane {
14
+ id: string;
15
+ label: string;
16
+ /** Optional secondary line under label (e.g. "3 sessions" or "offline"). */
17
+ sublabel?: string;
18
+ /** Tailwind class for the leading status dot (e.g. 'bg-success-500'). */
19
+ statusDotClass?: string;
20
+ bars: LaneBar[];
21
+ /** Render lane label muted; bars still render. */
22
+ muted?: boolean;
23
+ }
24
+ export interface LaneLegendItem {
25
+ label: string;
26
+ swatchClass: string;
27
+ }
28
+ export interface LaneTimelineLabels {
29
+ previousDate: string;
30
+ nextDate: string;
31
+ now: string;
32
+ /** "{n} active" — `{n}` replaced at render. */
33
+ activeCount: string;
34
+ }
35
+ export declare const defaultLaneTimelineLabels: LaneTimelineLabels;
36
+ export interface TimeTick {
37
+ /** Display label, e.g. "09:00". */
38
+ label: string;
39
+ /** Position as percentage 0–100 along the time axis. */
40
+ position: number;
41
+ }
42
+ export interface LaneTimelineProps {
43
+ lanes: Lane[];
44
+ /** Reference time for now-line + window end. Defaults to `new Date()`. */
45
+ now?: Date;
46
+ defaultRange?: LaneTimelineRange;
47
+ range?: LaneTimelineRange;
48
+ onRangeChange?: (range: LaneTimelineRange) => void;
49
+ selectedDate?: Date;
50
+ onDateChange?: (date: Date) => void;
51
+ title?: ReactNode;
52
+ /** Render-prop slot for a "Group: …" chip in the header. */
53
+ groupChip?: ReactNode;
54
+ /** Number of currently-active bars; powers the "● {n} active" indicator. */
55
+ activeCount?: number;
56
+ legend?: LaneLegendItem[];
57
+ selectedBarId?: string;
58
+ onSelectBar?: (bar: LaneBar, lane: Lane) => void;
59
+ labels?: Partial<LaneTimelineLabels>;
60
+ className?: string;
61
+ }
@@ -0,0 +1,6 @@
1
+ export const defaultLaneTimelineLabels = {
2
+ previousDate: 'Previous day',
3
+ nextDate: 'Next day',
4
+ now: 'now',
5
+ activeCount: '{n} active',
6
+ };
@@ -0,0 +1,2 @@
1
+ import { type LaneTimelineProps } from './lane-timeline-types.js';
2
+ export declare function LaneTimeline({ lanes, now, defaultRange, range: controlledRange, onRangeChange, selectedDate: controlledDate, onDateChange, title, groupChip, activeCount, legend, selectedBarId, onSelectBar, labels, className, }: LaneTimelineProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,85 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useMemo, useState } from 'react';
4
+ import { cn } from '../lib/utils.js';
5
+ import { LaneRow } from './lane.js';
6
+ import { defaultLaneTimelineLabels, } from './lane-timeline-types.js';
7
+ import { TimeAxis } from './time-axis.js';
8
+ const LABEL_GUTTER_PX = 140;
9
+ const RANGE_MS = {
10
+ '1h': 60 * 60_000,
11
+ '6h': 6 * 60 * 60_000,
12
+ '24h': 24 * 60 * 60_000,
13
+ '7d': 7 * 24 * 60 * 60_000,
14
+ };
15
+ export function LaneTimeline({ lanes, now, defaultRange = '6h', range: controlledRange, onRangeChange, selectedDate: controlledDate, onDateChange, title, groupChip, activeCount, legend, selectedBarId, onSelectBar, labels, className, }) {
16
+ const resolvedLabels = { ...defaultLaneTimelineLabels, ...(labels ?? {}) };
17
+ const [internalRange, setInternalRange] = useState(defaultRange);
18
+ const [internalDate, setInternalDate] = useState(() => now ?? new Date());
19
+ const range = controlledRange ?? internalRange;
20
+ const selectedDate = controlledDate ?? internalDate;
21
+ const referenceNow = now ?? new Date();
22
+ const { windowStart, windowEnd } = useMemo(() => {
23
+ const rangeMs = RANGE_MS[range];
24
+ const futurePadMs = Math.max(rangeMs / 6, 5 * 60_000);
25
+ const isToday = selectedDate.toDateString() === referenceNow.toDateString();
26
+ const end = isToday ? new Date(referenceNow.getTime() + futurePadMs) : endOfDay(selectedDate);
27
+ // futurePadMs is only relevant for the "today" path (gives the now-line breathing room).
28
+ // Historical days should render exactly rangeMs of window — no asymmetric pad.
29
+ const start = new Date(end.getTime() - rangeMs - (isToday ? futurePadMs : 0));
30
+ return { windowStart: start, windowEnd: end };
31
+ }, [range, selectedDate, referenceNow]);
32
+ const ticks = useMemo(() => buildTicks(windowStart, windowEnd, range), [windowStart, windowEnd, range]);
33
+ const handleRange = (r) => {
34
+ if (controlledRange === undefined)
35
+ setInternalRange(r);
36
+ onRangeChange?.(r);
37
+ };
38
+ const handleDate = (d) => {
39
+ if (controlledDate === undefined)
40
+ setInternalDate(d);
41
+ onDateChange?.(d);
42
+ };
43
+ const showNowLine = referenceNow.getTime() >= windowStart.getTime() &&
44
+ referenceNow.getTime() <= windowEnd.getTime();
45
+ const nowPositionPct = showNowLine
46
+ ? ((referenceNow.getTime() - windowStart.getTime()) /
47
+ (windowEnd.getTime() - windowStart.getTime())) *
48
+ 100
49
+ : 0;
50
+ return (_jsxs("section", { className: cn('flex flex-col rounded-lg border border-border bg-card p-4', className), children: [_jsx(TimeAxis, { range: range, onRangeChange: handleRange, selectedDate: selectedDate, onDateChange: handleDate, now: referenceNow, title: title, groupChip: groupChip, activeCount: activeCount, labels: resolvedLabels, ticks: ticks, labelGutterPx: LABEL_GUTTER_PX }), _jsxs("div", { className: "relative", children: [lanes.map((lane) => (_jsx(LaneRow, { lane: lane, windowStart: windowStart, windowEnd: windowEnd, labelGutterPx: LABEL_GUTTER_PX, selectedBarId: selectedBarId, onSelectBar: onSelectBar }, lane.id))), showNowLine && (_jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute top-0 bottom-0 w-px bg-primary", style: {
51
+ left: `calc(${LABEL_GUTTER_PX}px + (100% - ${LABEL_GUTTER_PX}px) * ${nowPositionPct / 100})`,
52
+ }, children: _jsx("div", { className: "-translate-x-1/2 -top-1 absolute size-2 rounded-full bg-primary" }) }))] }), legend && legend.length > 0 && (_jsxs("div", { className: "mt-3 flex items-center gap-4 border-border/40 border-t pt-3 text-muted-foreground text-xs", children: [_jsx("span", { className: "uppercase tracking-wider", children: "Legend" }), legend.map((item) => (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: cn('inline-block size-2 rounded-sm', item.swatchClass) }), item.label] }, item.label))), showNowLine && (_jsxs("span", { className: "ml-auto flex items-center gap-1.5", children: [_jsx("span", { className: "inline-block size-2 rounded-full bg-primary" }), resolvedLabels.now, " \u00B7 ", formatTime(referenceNow)] }))] }))] }));
53
+ }
54
+ function endOfDay(d) {
55
+ const e = new Date(d);
56
+ e.setHours(23, 59, 59, 999);
57
+ return e;
58
+ }
59
+ function formatTime(d) {
60
+ return new Intl.DateTimeFormat(undefined, {
61
+ hour: '2-digit',
62
+ minute: '2-digit',
63
+ hour12: false,
64
+ }).format(d);
65
+ }
66
+ function buildTicks(start, end, range) {
67
+ const span = end.getTime() - start.getTime();
68
+ const tickCount = range === '7d' ? 7 : range === '24h' ? 8 : 7;
69
+ const out = [];
70
+ for (let i = 0; i <= tickCount; i++) {
71
+ const t = new Date(start.getTime() + (span * i) / tickCount);
72
+ out.push({ position: (i / tickCount) * 100, label: formatTickLabel(t, range) });
73
+ }
74
+ return out;
75
+ }
76
+ function formatTickLabel(d, range) {
77
+ if (range === '7d') {
78
+ return new Intl.DateTimeFormat(undefined, { weekday: 'short' }).format(d);
79
+ }
80
+ return new Intl.DateTimeFormat(undefined, {
81
+ hour: '2-digit',
82
+ minute: '2-digit',
83
+ hour12: false,
84
+ }).format(d);
85
+ }
@@ -0,0 +1,10 @@
1
+ import type { Lane, LaneBar } from './lane-timeline-types.js';
2
+ export interface LaneRowProps {
3
+ lane: Lane;
4
+ windowStart: Date;
5
+ windowEnd: Date;
6
+ labelGutterPx: number;
7
+ selectedBarId?: string;
8
+ onSelectBar?: (bar: LaneBar, lane: Lane) => void;
9
+ }
10
+ export declare function LaneRow({ lane, windowStart, windowEnd, labelGutterPx, selectedBarId, onSelectBar, }: LaneRowProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cn } from '../lib/utils.js';
4
+ export function LaneRow({ lane, windowStart, windowEnd, labelGutterPx, selectedBarId, onSelectBar, }) {
5
+ return (_jsxs("div", { className: "grid items-center gap-0 border-border/40 border-b py-3", style: { gridTemplateColumns: `${labelGutterPx}px 1fr` }, children: [_jsxs("div", { className: "flex flex-col gap-0.5 pr-3", children: [_jsxs("div", { className: cn('flex items-center gap-2 text-sm', lane.muted && 'text-muted-foreground'), children: [lane.statusDotClass && (_jsx("span", { className: cn('inline-block size-2 rounded-full', lane.statusDotClass, lane.muted && 'opacity-40') })), _jsx("span", { className: "font-medium", children: lane.label })] }), lane.sublabel && _jsx("div", { className: "text-muted-foreground text-xs", children: lane.sublabel })] }), _jsx("div", { className: "relative h-7", children: lane.bars.map((bar) => {
6
+ const placement = placeBar(bar, windowStart, windowEnd);
7
+ if (!placement)
8
+ return null;
9
+ const selected = selectedBarId === bar.id;
10
+ const isSelectable = !!onSelectBar;
11
+ const barClassName = cn('absolute top-0 flex h-full items-center overflow-hidden rounded-md px-2 text-left text-xs text-white', bar.fillClass, bar.accentClass, isSelectable && [
12
+ 'data-[selected]:ring-2 data-[selected]:ring-primary data-[selected]:ring-offset-1 data-[selected]:ring-offset-background',
13
+ 'hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
14
+ ]);
15
+ const barStyle = { left: `${placement.left}%`, width: `${placement.width}%` };
16
+ const ariaLabel = `${lane.label} — ${bar.label}`;
17
+ if (!isSelectable) {
18
+ return (_jsx("div", { role: "img", "aria-label": ariaLabel, className: barClassName, style: barStyle, children: _jsx("span", { "aria-hidden": "true", className: "truncate", children: bar.label }) }, bar.id));
19
+ }
20
+ return (_jsx("button", { type: "button", onClick: () => onSelectBar(bar, lane), "data-selected": selected || undefined, className: barClassName, style: barStyle, "aria-label": ariaLabel, children: _jsx("span", { "aria-hidden": "true", className: "truncate", children: bar.label }) }, bar.id));
21
+ }) })] }));
22
+ }
23
+ function placeBar(bar, windowStart, windowEnd) {
24
+ const ws = windowStart.getTime();
25
+ const we = windowEnd.getTime();
26
+ const span = we - ws;
27
+ if (span <= 0)
28
+ return null;
29
+ const bs = bar.start.getTime();
30
+ const be = bar.end.getTime();
31
+ // Bar entirely outside the window (or exactly touching boundary) — skip.
32
+ if (be <= ws || bs >= we)
33
+ return null;
34
+ const clampedStart = Math.max(bs, ws);
35
+ const clampedEnd = Math.min(be, we);
36
+ const left = ((clampedStart - ws) / span) * 100;
37
+ const width = Math.max(((clampedEnd - clampedStart) / span) * 100, 0.5);
38
+ return { left, width };
39
+ }
@@ -1 +1,2 @@
1
+ export type { Lane, LaneBar, LaneLegendItem, LaneTimelineRange, } from './lane-timeline-types.js';
1
2
  export type { ScrubMark, TimelineEvent, TimelineEventKind } from './types.js';
@@ -0,0 +1,18 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { LaneTimelineLabels, LaneTimelineRange, TimeTick } from './lane-timeline-types.js';
3
+ export interface TimeAxisProps {
4
+ range: LaneTimelineRange;
5
+ onRangeChange: (range: LaneTimelineRange) => void;
6
+ selectedDate: Date;
7
+ onDateChange: (date: Date) => void;
8
+ /** Reference time used to determine whether `selectedDate` is "today" in the header label. */
9
+ now: Date;
10
+ title?: ReactNode;
11
+ groupChip?: ReactNode;
12
+ activeCount?: number;
13
+ labels: LaneTimelineLabels;
14
+ ticks: TimeTick[];
15
+ /** Width of the lane-label gutter on the left, in px (matches Lane component). */
16
+ labelGutterPx: number;
17
+ }
18
+ export declare function TimeAxis({ range, onRangeChange, selectedDate, onDateChange, now, title, groupChip, activeCount, labels, ticks, labelGutterPx, }: TimeAxisProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,22 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
4
+ import { cn } from '../lib/utils.js';
5
+ const RANGES = ['1h', '6h', '24h', '7d'];
6
+ export function TimeAxis({ range, onRangeChange, selectedDate, onDateChange, now, title, groupChip, activeCount, labels, ticks, labelGutterPx, }) {
7
+ const stepDate = (delta) => {
8
+ const next = new Date(selectedDate);
9
+ next.setDate(next.getDate() + delta);
10
+ onDateChange(next);
11
+ };
12
+ return (_jsxs("div", { className: "flex flex-col gap-3 border-border border-b pb-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [title && _jsx("div", { className: "font-medium text-sm", children: title }), _jsx("div", { className: "flex items-center gap-1 rounded-md bg-muted p-0.5", children: RANGES.map((r) => (_jsx("button", { type: "button", "aria-pressed": r === range, onClick: () => onRangeChange(r), "data-active": r === range || undefined, className: cn('rounded px-2 py-0.5 text-xs', 'data-[active]:bg-background data-[active]:text-foreground', r !== range && 'text-muted-foreground hover:text-foreground'), children: r }, r))) }), groupChip && _jsx("div", { className: "text-muted-foreground text-xs", children: groupChip }), typeof activeCount === 'number' && (_jsxs("div", { className: "text-muted-foreground text-xs", children: [_jsx("span", { className: "mr-1 inline-block size-1.5 rounded-full bg-success-500" }), labels.activeCount.replace('{n}', String(activeCount))] })), _jsxs("div", { className: "ml-auto flex items-center gap-1 rounded-md border border-border bg-card px-2 py-1 text-xs", children: [_jsx("button", { type: "button", "aria-label": labels.previousDate, onClick: () => stepDate(-1), className: "text-muted-foreground hover:text-foreground", children: _jsx(ChevronLeft, { className: "size-3.5" }) }), _jsx("div", { className: "px-1", children: formatDateHeader(selectedDate, now) }), _jsx("button", { type: "button", "aria-label": labels.nextDate, onClick: () => stepDate(1), className: "text-muted-foreground hover:text-foreground", children: _jsx(ChevronRight, { className: "size-3.5" }) })] })] }), _jsxs("div", { className: "grid items-end gap-0", style: { gridTemplateColumns: `${labelGutterPx}px 1fr` }, children: [_jsx("div", {}), _jsx("div", { className: "relative h-5", children: ticks.map((t) => (_jsx("div", { className: "-translate-x-1/2 absolute top-0 text-muted-foreground text-xs", style: { left: `${t.position}%` }, children: t.label }, `${t.position}:${t.label}`))) })] })] }));
13
+ }
14
+ function formatDateHeader(d, referenceNow) {
15
+ const isToday = d.toDateString() === referenceNow.toDateString();
16
+ const fmt = new Intl.DateTimeFormat(undefined, {
17
+ weekday: 'short',
18
+ month: 'short',
19
+ day: 'numeric',
20
+ }).format(d);
21
+ return isToday ? `Today · ${fmt}` : fmt;
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medalsocial/meda",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Shared Meda UI shell and runtime package.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -43,6 +43,14 @@
43
43
  "types": "./dist/timeline/index.d.ts",
44
44
  "default": "./dist/timeline/index.js"
45
45
  },
46
+ "./kanban": {
47
+ "types": "./dist/kanban/index.d.ts",
48
+ "import": "./dist/kanban/index.js"
49
+ },
50
+ "./list": {
51
+ "types": "./dist/list/index.d.ts",
52
+ "import": "./dist/list/index.js"
53
+ },
46
54
  "./marketing": {
47
55
  "types": "./dist/marketing/index.d.ts",
48
56
  "default": "./dist/marketing/index.js"
@@ -73,6 +81,9 @@
73
81
  "access": "public"
74
82
  },
75
83
  "peerDependencies": {
84
+ "@dnd-kit/core": "^6.3.1",
85
+ "@dnd-kit/sortable": "^10.0.0",
86
+ "@dnd-kit/utilities": "^3.2.2",
76
87
  "@react-three/fiber": "^9.0.0",
77
88
  "lucide-react": "^1.8.0",
78
89
  "next-themes": "^0.4.0",
@@ -80,6 +91,15 @@
80
91
  "react-dom": ">=19"
81
92
  },
82
93
  "peerDependenciesMeta": {
94
+ "@dnd-kit/core": {
95
+ "optional": false
96
+ },
97
+ "@dnd-kit/sortable": {
98
+ "optional": false
99
+ },
100
+ "@dnd-kit/utilities": {
101
+ "optional": false
102
+ },
83
103
  "@react-three/fiber": {
84
104
  "optional": true
85
105
  },
@@ -103,6 +123,9 @@
103
123
  "@biomejs/biome": "^2.4.12",
104
124
  "@changesets/changelog-github": "^0.5.1",
105
125
  "@changesets/cli": "^2.29.0",
126
+ "@dnd-kit/core": "^6.3.1",
127
+ "@dnd-kit/sortable": "^10.0.0",
128
+ "@dnd-kit/utilities": "^3.2.2",
106
129
  "@react-three/drei": "^10.7.7",
107
130
  "@react-three/fiber": "^9.6.0",
108
131
  "@size-limit/preset-small-lib": "^12.1.0",