beads-kanban-ui 0.1.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 (154) hide show
  1. package/.designs/beads-kanban-ui-bj0.md +73 -0
  2. package/.designs/beads-kanban-ui-qxq.md +144 -0
  3. package/.designs/epic-support.md +282 -0
  4. package/.env.local.example +2 -0
  5. package/.eslintrc.json +3 -0
  6. package/.gitattributes +3 -0
  7. package/.github/workflows/release.yml +123 -0
  8. package/.history/README_20260121193710.md +227 -0
  9. package/.history/README_20260121193918.md +227 -0
  10. package/.history/README_20260121193921.md +227 -0
  11. package/.history/README_20260121193933.md +227 -0
  12. package/.history/README_20260121193934.md +227 -0
  13. package/.history/README_20260121193944.md +227 -0
  14. package/.history/README_20260121193953.md +227 -0
  15. package/.history/src/app/page_20260121133429.tsx +134 -0
  16. package/.history/src/app/page_20260121133928.tsx +134 -0
  17. package/.history/src/app/page_20260121144850.tsx +138 -0
  18. package/.history/src/app/page_20260121144854.tsx +138 -0
  19. package/.history/src/app/page_20260121144858.tsx +138 -0
  20. package/.history/src/app/page_20260121144902.tsx +138 -0
  21. package/.history/src/app/page_20260121144906.tsx +138 -0
  22. package/.history/src/app/page_20260121144911.tsx +138 -0
  23. package/.history/src/app/page_20260121144928.tsx +138 -0
  24. package/.playwright-mcp/.playwright-mcp/morphing-dialog-wheel-scroll-fix.png +0 -0
  25. package/.playwright-mcp/beams-test.png +0 -0
  26. package/.playwright-mcp/card-verification.png +0 -0
  27. package/.playwright-mcp/design-doc-dialog-fix-verification.png +0 -0
  28. package/.playwright-mcp/dialog-width-test.png +0 -0
  29. package/.playwright-mcp/homepage.png +0 -0
  30. package/.playwright-mcp/morphing-dialog-expanded.png +0 -0
  31. package/.playwright-mcp/morphing-dialog-fixes-final.png +0 -0
  32. package/.playwright-mcp/morphing-dialog-open.png +0 -0
  33. package/.playwright-mcp/page-2026-01-21T14-08-31-529Z.png +0 -0
  34. package/.playwright-mcp/page-2026-01-21T14-09-23-431Z.png +0 -0
  35. package/.playwright-mcp/page-2026-01-21T14-10-28-773Z.png +0 -0
  36. package/.playwright-mcp/page-2026-01-21T14-10-47-432Z.png +0 -0
  37. package/.playwright-mcp/page-2026-01-21T14-11-12-350Z.png +0 -0
  38. package/.playwright-mcp/screenshot-after-click.png +0 -0
  39. package/.playwright-mcp/screenshot-after-dialog-click.png +0 -0
  40. package/.playwright-mcp/sheet-restored-after-dialog-close.png +0 -0
  41. package/.playwright-mcp/test-1-sheet-open-with-overlay.png +0 -0
  42. package/.playwright-mcp/test-2-morphing-dialog-with-overlay.png +0 -0
  43. package/.playwright-mcp/test-3-sheet-open-dark-overlay.png +0 -0
  44. package/.playwright-mcp/test-4-morphing-dialog-with-dark-overlay.png +0 -0
  45. package/.playwright-mcp/test-5-morphing-dialog-scrolled.png +0 -0
  46. package/.playwright-mcp/test-6-sheet-restored-after-dialog-close.png +0 -0
  47. package/.playwright-mcp/wheel-scroll-fixed.png +0 -0
  48. package/README.md +243 -0
  49. package/Screenshots/bead-detail.png +0 -0
  50. package/Screenshots/dashboard.png +0 -0
  51. package/Screenshots/kanban-board.png +0 -0
  52. package/components.json +27 -0
  53. package/logo/logo.svg +1 -0
  54. package/next.config.js +9 -0
  55. package/npm/README.md +37 -0
  56. package/npm/bin/cli.js +107 -0
  57. package/npm/package.json +20 -0
  58. package/npm/scripts/postinstall.js +132 -0
  59. package/package.json +62 -0
  60. package/postcss.config.js +6 -0
  61. package/public/logo.svg +1 -0
  62. package/restart.sh +5 -0
  63. package/server/Cargo.lock +1685 -0
  64. package/server/Cargo.toml +24 -0
  65. package/server/src/db.rs +570 -0
  66. package/server/src/main.rs +141 -0
  67. package/server/src/routes/beads.rs +413 -0
  68. package/server/src/routes/cli.rs +150 -0
  69. package/server/src/routes/fs.rs +360 -0
  70. package/server/src/routes/git.rs +169 -0
  71. package/server/src/routes/mod.rs +107 -0
  72. package/server/src/routes/projects.rs +177 -0
  73. package/server/src/routes/watch.rs +211 -0
  74. package/src/app/globals.css +101 -0
  75. package/src/app/layout.tsx +36 -0
  76. package/src/app/page.tsx +348 -0
  77. package/src/app/project/kanban-board.tsx +356 -0
  78. package/src/app/project/page.tsx +18 -0
  79. package/src/app/settings/page.tsx +224 -0
  80. package/src/components/Beams.css +5 -0
  81. package/src/components/Beams.jsx +307 -0
  82. package/src/components/Galaxy.css +5 -0
  83. package/src/components/Galaxy.jsx +333 -0
  84. package/src/components/activity-timeline.tsx +172 -0
  85. package/src/components/add-project-dialog.tsx +219 -0
  86. package/src/components/bead-card.tsx +196 -0
  87. package/src/components/bead-detail.tsx +306 -0
  88. package/src/components/color-picker.tsx +101 -0
  89. package/src/components/comment-input.tsx +155 -0
  90. package/src/components/comment-list.tsx +147 -0
  91. package/src/components/dependency-badge.tsx +106 -0
  92. package/src/components/design-doc-dialog.tsx +58 -0
  93. package/src/components/design-doc-preview.tsx +97 -0
  94. package/src/components/design-doc-viewer.tsx +199 -0
  95. package/src/components/editable-project-name.tsx +178 -0
  96. package/src/components/epic-card.tsx +263 -0
  97. package/src/components/folder-browser.tsx +273 -0
  98. package/src/components/footer.tsx +27 -0
  99. package/src/components/kanban/default.tsx +184 -0
  100. package/src/components/kanban-column.tsx +167 -0
  101. package/src/components/project-card.tsx +191 -0
  102. package/src/components/quick-filter-bar.tsx +279 -0
  103. package/src/components/scan-directory-dialog.tsx +368 -0
  104. package/src/components/status-donut.tsx +197 -0
  105. package/src/components/subtask-list.tsx +128 -0
  106. package/src/components/tag-picker.tsx +252 -0
  107. package/src/components/ui/.gitkeep +0 -0
  108. package/src/components/ui/alert-dialog.tsx +141 -0
  109. package/src/components/ui/avatar.tsx +67 -0
  110. package/src/components/ui/badge.tsx +230 -0
  111. package/src/components/ui/button.tsx +433 -0
  112. package/src/components/ui/card/index.tsx +24 -0
  113. package/src/components/ui/card/roiui-card.module.css +197 -0
  114. package/src/components/ui/card/roiui-card.tsx +154 -0
  115. package/src/components/ui/card/shadcn-card.tsx +76 -0
  116. package/src/components/ui/chart.tsx +369 -0
  117. package/src/components/ui/dialog.tsx +122 -0
  118. package/src/components/ui/dropdown-menu.tsx +201 -0
  119. package/src/components/ui/input.tsx +22 -0
  120. package/src/components/ui/kanban.tsx +522 -0
  121. package/src/components/ui/morphing-dialog.tsx +457 -0
  122. package/src/components/ui/popover.tsx +33 -0
  123. package/src/components/ui/progress.tsx +28 -0
  124. package/src/components/ui/scroll-area.tsx +48 -0
  125. package/src/components/ui/select.tsx +159 -0
  126. package/src/components/ui/separator.tsx +31 -0
  127. package/src/components/ui/sheet.tsx +142 -0
  128. package/src/components/ui/skeleton.tsx +15 -0
  129. package/src/components/ui/toast.tsx +129 -0
  130. package/src/components/ui/toaster.tsx +35 -0
  131. package/src/components/ui/tooltip.tsx +30 -0
  132. package/src/hooks/.gitkeep +0 -0
  133. package/src/hooks/use-bead-filters.ts +261 -0
  134. package/src/hooks/use-beads.ts +162 -0
  135. package/src/hooks/use-branch-statuses.ts +161 -0
  136. package/src/hooks/use-epics.ts +173 -0
  137. package/src/hooks/use-file-watcher.ts +111 -0
  138. package/src/hooks/use-keyboard-navigation.ts +282 -0
  139. package/src/hooks/use-project.ts +61 -0
  140. package/src/hooks/use-projects.ts +93 -0
  141. package/src/hooks/use-toast.ts +194 -0
  142. package/src/hooks/useClickOutside.tsx +26 -0
  143. package/src/lib/.gitkeep +0 -0
  144. package/src/lib/api.ts +186 -0
  145. package/src/lib/beads-parser.ts +252 -0
  146. package/src/lib/cli.ts +193 -0
  147. package/src/lib/db.ts +145 -0
  148. package/src/lib/design-doc.ts +74 -0
  149. package/src/lib/epic-parser.ts +242 -0
  150. package/src/lib/git.ts +102 -0
  151. package/src/lib/utils.ts +12 -0
  152. package/src/types/index.ts +107 -0
  153. package/tailwind.config.ts +85 -0
  154. package/tsconfig.json +26 -0
@@ -0,0 +1,522 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { cn } from '@/lib/utils';
5
+ import {
6
+ defaultDropAnimation,
7
+ defaultDropAnimationSideEffects,
8
+ DndContext,
9
+ DragEndEvent,
10
+ DragOverEvent,
11
+ DragOverlay,
12
+ DragStartEvent,
13
+ DropAnimation,
14
+ KeyboardSensor,
15
+ PointerSensor,
16
+ UniqueIdentifier,
17
+ useSensor,
18
+ useSensors,
19
+ type DraggableAttributes,
20
+ type DraggableSyntheticListeners,
21
+ } from '@dnd-kit/core';
22
+ import {
23
+ arrayMove,
24
+ rectSortingStrategy,
25
+ SortableContext,
26
+ sortableKeyboardCoordinates,
27
+ useSortable,
28
+ verticalListSortingStrategy,
29
+ } from '@dnd-kit/sortable';
30
+ import { CSS } from '@dnd-kit/utilities';
31
+ import { Slot } from '@radix-ui/react-slot';
32
+
33
+ interface KanbanContextProps<T> {
34
+ columns: Record<string, T[]>;
35
+ setColumns: (columns: Record<string, T[]>) => void;
36
+ getItemId: (item: T) => string;
37
+ columnIds: string[];
38
+ activeId: UniqueIdentifier | null;
39
+ setActiveId: (id: UniqueIdentifier | null) => void;
40
+ findContainer: (id: UniqueIdentifier) => string | undefined;
41
+ isColumn: (id: UniqueIdentifier) => boolean;
42
+ }
43
+
44
+ const KanbanContext = React.createContext<KanbanContextProps<any>>({
45
+ columns: {},
46
+ setColumns: () => {},
47
+ getItemId: () => '',
48
+ columnIds: [],
49
+ activeId: null,
50
+ setActiveId: () => {},
51
+ findContainer: () => undefined,
52
+ isColumn: () => false,
53
+ });
54
+
55
+ const ColumnContext = React.createContext<{
56
+ attributes: DraggableAttributes;
57
+ listeners: DraggableSyntheticListeners | undefined;
58
+ isDragging?: boolean;
59
+ disabled?: boolean;
60
+ }>({
61
+ attributes: {} as DraggableAttributes,
62
+ listeners: undefined,
63
+ isDragging: false,
64
+ disabled: false,
65
+ });
66
+
67
+ const ItemContext = React.createContext<{
68
+ listeners: DraggableSyntheticListeners | undefined;
69
+ isDragging?: boolean;
70
+ disabled?: boolean;
71
+ }>({
72
+ listeners: undefined,
73
+ isDragging: false,
74
+ disabled: false,
75
+ });
76
+
77
+ const dropAnimationConfig: DropAnimation = {
78
+ ...defaultDropAnimation,
79
+ sideEffects: defaultDropAnimationSideEffects({
80
+ styles: {
81
+ active: {
82
+ opacity: '0.4',
83
+ },
84
+ },
85
+ }),
86
+ };
87
+
88
+ export interface KanbanMoveEvent {
89
+ event: DragEndEvent;
90
+ activeContainer: string;
91
+ activeIndex: number;
92
+ overContainer: string;
93
+ overIndex: number;
94
+ }
95
+
96
+ export interface KanbanRootProps<T> {
97
+ value: Record<string, T[]>;
98
+ onValueChange: (value: Record<string, T[]>) => void;
99
+ getItemValue: (item: T) => string;
100
+ children: React.ReactNode;
101
+ className?: string;
102
+ onMove?: (event: KanbanMoveEvent) => void;
103
+ }
104
+
105
+ function Kanban<T>({ value, onValueChange, getItemValue, children, className, onMove }: KanbanRootProps<T>) {
106
+ const columns = value;
107
+ const setColumns = onValueChange;
108
+ const [activeId, setActiveId] = React.useState<UniqueIdentifier | null>(null);
109
+
110
+ const sensors = useSensors(
111
+ useSensor(PointerSensor, {
112
+ activationConstraint: {
113
+ distance: 10,
114
+ },
115
+ }),
116
+ useSensor(KeyboardSensor, {
117
+ coordinateGetter: sortableKeyboardCoordinates,
118
+ }),
119
+ );
120
+
121
+ const columnIds = React.useMemo(() => Object.keys(columns), [columns]);
122
+
123
+ const isColumn = React.useCallback((id: UniqueIdentifier) => columnIds.includes(id as string), [columnIds]);
124
+
125
+ const findContainer = React.useCallback(
126
+ (id: UniqueIdentifier) => {
127
+ if (isColumn(id)) return id as string;
128
+ return columnIds.find((key) => columns[key].some((item) => getItemValue(item) === id));
129
+ },
130
+ [columns, columnIds, getItemValue, isColumn],
131
+ );
132
+
133
+ const handleDragStart = React.useCallback((event: DragStartEvent) => {
134
+ setActiveId(event.active.id);
135
+ }, []);
136
+
137
+ const handleDragOver = React.useCallback(
138
+ (event: DragOverEvent) => {
139
+ if (onMove) {
140
+ return;
141
+ }
142
+
143
+ const { active, over } = event;
144
+ if (!over) return;
145
+
146
+ if (isColumn(active.id)) return;
147
+
148
+ const activeContainer = findContainer(active.id);
149
+ const overContainer = findContainer(over.id);
150
+
151
+ // Only handle moving items between different columns
152
+ if (!activeContainer || !overContainer || activeContainer === overContainer) {
153
+ return;
154
+ }
155
+
156
+ const activeItems = columns[activeContainer];
157
+ const overItems = columns[overContainer];
158
+
159
+ const activeIndex = activeItems.findIndex((item: T) => getItemValue(item) === active.id);
160
+ let overIndex = overItems.findIndex((item: T) => getItemValue(item) === over.id);
161
+
162
+ // If dropping on the column itself, not an item
163
+ if (isColumn(over.id)) {
164
+ overIndex = overItems.length;
165
+ }
166
+
167
+ const newOverItems = [...overItems];
168
+ const [movedItem] = activeItems.splice(activeIndex, 1);
169
+ newOverItems.splice(overIndex, 0, movedItem);
170
+
171
+ setColumns({
172
+ ...columns,
173
+ [activeContainer]: [...activeItems],
174
+ [overContainer]: newOverItems,
175
+ });
176
+ },
177
+ [findContainer, getItemValue, isColumn, setColumns, columns, onMove],
178
+ );
179
+
180
+ const handleDragEnd = React.useCallback(
181
+ (event: DragEndEvent) => {
182
+ const { active, over } = event;
183
+ setActiveId(null);
184
+
185
+ if (!over) return;
186
+
187
+ // Handle item move callback
188
+ if (onMove && !isColumn(active.id)) {
189
+ const activeContainer = findContainer(active.id);
190
+ const overContainer = findContainer(over.id);
191
+
192
+ if (activeContainer && overContainer) {
193
+ const activeIndex = columns[activeContainer].findIndex((item: T) => getItemValue(item) === active.id);
194
+ const overIndex = isColumn(over.id)
195
+ ? columns[overContainer].length
196
+ : columns[overContainer].findIndex((item: T) => getItemValue(item) === over.id);
197
+
198
+ onMove({
199
+ event,
200
+ activeContainer,
201
+ activeIndex,
202
+ overContainer,
203
+ overIndex,
204
+ });
205
+ }
206
+ return;
207
+ }
208
+
209
+ // Handle column reordering
210
+ if (isColumn(active.id) && isColumn(over.id)) {
211
+ const activeIndex = columnIds.indexOf(active.id as string);
212
+ const overIndex = columnIds.indexOf(over.id as string);
213
+ if (activeIndex !== overIndex) {
214
+ const newOrder = arrayMove(Object.keys(columns), activeIndex, overIndex);
215
+ const newColumns: Record<string, T[]> = {};
216
+ newOrder.forEach((key) => {
217
+ newColumns[key] = columns[key];
218
+ });
219
+ setColumns(newColumns);
220
+ }
221
+ return;
222
+ }
223
+
224
+ const activeContainer = findContainer(active.id);
225
+ const overContainer = findContainer(over.id);
226
+
227
+ // Handle item reordering within the same column
228
+ if (activeContainer && overContainer && activeContainer === overContainer) {
229
+ const container = activeContainer;
230
+ const activeIndex = columns[container].findIndex((item: T) => getItemValue(item) === active.id);
231
+ const overIndex = columns[container].findIndex((item: T) => getItemValue(item) === over.id);
232
+
233
+ if (activeIndex !== overIndex) {
234
+ setColumns({
235
+ ...columns,
236
+ [container]: arrayMove(columns[container], activeIndex, overIndex),
237
+ });
238
+ }
239
+ }
240
+ },
241
+ [columnIds, columns, findContainer, getItemValue, isColumn, setColumns, onMove],
242
+ );
243
+
244
+ const contextValue = React.useMemo(
245
+ () => ({
246
+ columns,
247
+ setColumns,
248
+ getItemId: getItemValue,
249
+ columnIds,
250
+ activeId,
251
+ setActiveId,
252
+ findContainer,
253
+ isColumn,
254
+ }),
255
+ [columns, setColumns, getItemValue, columnIds, activeId, findContainer, isColumn],
256
+ );
257
+
258
+ return (
259
+ <KanbanContext.Provider value={contextValue}>
260
+ <DndContext sensors={sensors} onDragStart={handleDragStart} onDragOver={handleDragOver} onDragEnd={handleDragEnd}>
261
+ <div data-slot="kanban" data-dragging={activeId !== null} className={cn(className)}>
262
+ {children}
263
+ </div>
264
+ </DndContext>
265
+ </KanbanContext.Provider>
266
+ );
267
+ }
268
+
269
+ export interface KanbanBoardProps {
270
+ className?: string;
271
+ children: React.ReactNode;
272
+ }
273
+
274
+ function KanbanBoard({ children, className }: KanbanBoardProps) {
275
+ const { columnIds } = React.useContext(KanbanContext);
276
+
277
+ return (
278
+ <SortableContext items={columnIds} strategy={rectSortingStrategy}>
279
+ <div data-slot="kanban-board" className={cn('grid auto-rows-fr sm:grid-cols-3 gap-4', className)}>
280
+ {children}
281
+ </div>
282
+ </SortableContext>
283
+ );
284
+ }
285
+
286
+ export interface KanbanColumnProps {
287
+ value: string;
288
+ className?: string;
289
+ children: React.ReactNode;
290
+ disabled?: boolean;
291
+ }
292
+
293
+ function KanbanColumn({ value, className, children, disabled }: KanbanColumnProps) {
294
+ const {
295
+ setNodeRef,
296
+ transform,
297
+ transition,
298
+ attributes,
299
+ listeners,
300
+ isDragging: isSortableDragging,
301
+ } = useSortable({
302
+ id: value,
303
+ disabled,
304
+ });
305
+
306
+ const { activeId, isColumn } = React.useContext(KanbanContext);
307
+ const isColumnDragging = activeId ? isColumn(activeId) : false;
308
+
309
+ const style = {
310
+ transition,
311
+ transform: CSS.Translate.toString(transform),
312
+ } as React.CSSProperties;
313
+
314
+ return (
315
+ <ColumnContext.Provider value={{ attributes, listeners, isDragging: isColumnDragging, disabled }}>
316
+ <div
317
+ data-slot="kanban-column"
318
+ data-value={value}
319
+ data-dragging={isSortableDragging}
320
+ data-disabled={disabled}
321
+ ref={setNodeRef}
322
+ style={style}
323
+ className={cn(
324
+ 'group/kanban-column flex flex-col',
325
+ isSortableDragging && 'opacity-50',
326
+ disabled && 'opacity-50',
327
+ className,
328
+ )}
329
+ >
330
+ {children}
331
+ </div>
332
+ </ColumnContext.Provider>
333
+ );
334
+ }
335
+
336
+ export interface KanbanColumnHandleProps {
337
+ asChild?: boolean;
338
+ className?: string;
339
+ children?: React.ReactNode;
340
+ cursor?: boolean;
341
+ }
342
+
343
+ function KanbanColumnHandle({ asChild, className, children, cursor = true }: KanbanColumnHandleProps) {
344
+ const { attributes, listeners, isDragging, disabled } = React.useContext(ColumnContext);
345
+
346
+ const Comp = asChild ? Slot : 'div';
347
+
348
+ return (
349
+ <Comp
350
+ data-slot="kanban-column-handle"
351
+ data-dragging={isDragging}
352
+ data-disabled={disabled}
353
+ {...attributes}
354
+ {...listeners}
355
+ className={cn(
356
+ 'opacity-0 transition-opacity group-hover/kanban-column:opacity-100',
357
+ cursor && (isDragging ? '!cursor-grabbing' : '!cursor-grab'),
358
+ className,
359
+ )}
360
+ >
361
+ {children}
362
+ </Comp>
363
+ );
364
+ }
365
+
366
+ export interface KanbanItemProps {
367
+ value: string;
368
+ asChild?: boolean;
369
+ className?: string;
370
+ children: React.ReactNode;
371
+ disabled?: boolean;
372
+ }
373
+
374
+ function KanbanItem({ value, asChild = false, className, children, disabled }: KanbanItemProps) {
375
+ const {
376
+ setNodeRef,
377
+ transform,
378
+ transition,
379
+ attributes,
380
+ listeners,
381
+ isDragging: isSortableDragging,
382
+ } = useSortable({
383
+ id: value,
384
+ disabled,
385
+ });
386
+
387
+ const { activeId, isColumn } = React.useContext(KanbanContext);
388
+ const isItemDragging = activeId ? !isColumn(activeId) : false;
389
+
390
+ const style = {
391
+ transition,
392
+ transform: CSS.Translate.toString(transform),
393
+ } as React.CSSProperties;
394
+
395
+ const Comp = asChild ? Slot : 'div';
396
+
397
+ return (
398
+ <ItemContext.Provider value={{ listeners, isDragging: isItemDragging, disabled }}>
399
+ <Comp
400
+ data-slot="kanban-item"
401
+ data-value={value}
402
+ data-dragging={isSortableDragging}
403
+ data-disabled={disabled}
404
+ ref={setNodeRef}
405
+ style={style}
406
+ {...attributes}
407
+ className={cn(isSortableDragging && 'opacity-50', disabled && 'opacity-50', className)}
408
+ >
409
+ {children}
410
+ </Comp>
411
+ </ItemContext.Provider>
412
+ );
413
+ }
414
+
415
+ export interface KanbanItemHandleProps {
416
+ asChild?: boolean;
417
+ className?: string;
418
+ children?: React.ReactNode;
419
+ cursor?: boolean;
420
+ }
421
+
422
+ function KanbanItemHandle({ asChild, className, children, cursor = true }: KanbanItemHandleProps) {
423
+ const { listeners, isDragging, disabled } = React.useContext(ItemContext);
424
+
425
+ const Comp = asChild ? Slot : 'div';
426
+
427
+ return (
428
+ <Comp
429
+ data-slot="kanban-item-handle"
430
+ data-dragging={isDragging}
431
+ data-disabled={disabled}
432
+ {...listeners}
433
+ className={cn(cursor && (isDragging ? '!cursor-grabbing' : '!cursor-grab'), className)}
434
+ >
435
+ {children}
436
+ </Comp>
437
+ );
438
+ }
439
+
440
+ export interface KanbanColumnContentProps {
441
+ value: string;
442
+ className?: string;
443
+ children: React.ReactNode;
444
+ }
445
+
446
+ function KanbanColumnContent({ value, className, children }: KanbanColumnContentProps) {
447
+ const { columns, getItemId } = React.useContext(KanbanContext);
448
+
449
+ const itemIds = React.useMemo(() => columns[value].map(getItemId), [columns, getItemId, value]);
450
+
451
+ return (
452
+ <SortableContext items={itemIds} strategy={verticalListSortingStrategy}>
453
+ <div data-slot="kanban-column-content" className={cn('flex flex-col gap-2', className)}>
454
+ {children}
455
+ </div>
456
+ </SortableContext>
457
+ );
458
+ }
459
+
460
+ export interface KanbanOverlayProps {
461
+ className?: string;
462
+ children?: React.ReactNode | ((params: { value: UniqueIdentifier; variant: 'column' | 'item' }) => React.ReactNode);
463
+ }
464
+
465
+ function KanbanOverlay({ children, className }: KanbanOverlayProps) {
466
+ const { activeId, isColumn } = React.useContext(KanbanContext);
467
+ const [dimensions, setDimensions] = React.useState<{ width: number; height: number } | null>(null);
468
+
469
+ React.useEffect(() => {
470
+ if (activeId) {
471
+ const element = document.querySelector(
472
+ `[data-slot="kanban-${isColumn(activeId) ? 'column' : 'item'}"][data-value="${activeId}"]`,
473
+ );
474
+ if (element) {
475
+ const rect = element.getBoundingClientRect();
476
+ setDimensions({ width: rect.width, height: rect.height });
477
+ }
478
+ } else {
479
+ setDimensions(null);
480
+ }
481
+ }, [activeId]);
482
+
483
+ const style = {
484
+ width: dimensions?.width,
485
+ height: dimensions?.height,
486
+ } as React.CSSProperties;
487
+
488
+ const content = React.useMemo(() => {
489
+ if (!activeId) return null;
490
+ if (typeof children === 'function') {
491
+ return children({
492
+ value: activeId,
493
+ variant: isColumn(activeId) ? 'column' : 'item',
494
+ });
495
+ }
496
+ return children;
497
+ }, [activeId, children, isColumn]);
498
+
499
+ return (
500
+ <DragOverlay dropAnimation={dropAnimationConfig}>
501
+ <div
502
+ data-slot="kanban-overlay"
503
+ data-dragging={true}
504
+ style={style}
505
+ className={cn('pointer-events-none', className, activeId ? '!cursor-grabbing' : '')}
506
+ >
507
+ {content}
508
+ </div>
509
+ </DragOverlay>
510
+ );
511
+ }
512
+
513
+ export {
514
+ Kanban,
515
+ KanbanBoard,
516
+ KanbanColumn,
517
+ KanbanColumnHandle,
518
+ KanbanItem,
519
+ KanbanItemHandle,
520
+ KanbanColumnContent,
521
+ KanbanOverlay,
522
+ };