@tulipfarm/sprout 0.3.0 → 0.4.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 (349) hide show
  1. package/README.md +45 -40
  2. package/dist/components/accordion.d.ts +2 -2
  3. package/dist/components/accordion.js +9 -9
  4. package/dist/components/agent-fanout.d.ts +2 -2
  5. package/dist/components/agent-fanout.js +65 -87
  6. package/dist/components/alert-dialog.d.ts +6 -6
  7. package/dist/components/alert-dialog.js +20 -20
  8. package/dist/components/alert.d.ts +6 -6
  9. package/dist/components/alert.js +13 -13
  10. package/dist/components/aspect-ratio.d.ts +2 -2
  11. package/dist/components/aspect-ratio.js +2 -2
  12. package/dist/components/attachment.d.ts +17 -17
  13. package/dist/components/attachment.js +82 -115
  14. package/dist/components/avatar.d.ts +10 -10
  15. package/dist/components/avatar.js +14 -14
  16. package/dist/components/badge.d.ts +4 -4
  17. package/dist/components/badge.js +18 -18
  18. package/dist/components/board.d.ts +107 -0
  19. package/dist/components/board.js +640 -0
  20. package/dist/components/brand-icon.d.ts +6 -6
  21. package/dist/components/brand-icon.js +55 -63
  22. package/dist/components/breadcrumb.d.ts +11 -11
  23. package/dist/components/breadcrumb.js +16 -18
  24. package/dist/components/button-group.d.ts +6 -6
  25. package/dist/components/button-group.js +14 -14
  26. package/dist/components/button.d.ts +11 -11
  27. package/dist/components/button.js +46 -56
  28. package/dist/components/calendar.d.ts +5 -5
  29. package/dist/components/calendar.js +44 -53
  30. package/dist/components/canvas.d.ts +4 -4
  31. package/dist/components/canvas.js +39 -46
  32. package/dist/components/card.d.ts +6 -6
  33. package/dist/components/card.js +17 -17
  34. package/dist/components/carousel.d.ts +7 -7
  35. package/dist/components/carousel.js +57 -75
  36. package/dist/components/chart.d.ts +5 -112
  37. package/dist/components/chart.js +5 -172
  38. package/dist/components/chat-confirmation.d.ts +13 -11
  39. package/dist/components/chat-confirmation.js +32 -38
  40. package/dist/components/chat-input.d.ts +4 -4
  41. package/dist/components/chat-input.js +74 -81
  42. package/dist/components/chat-plan.d.ts +2 -2
  43. package/dist/components/chat-plan.js +15 -20
  44. package/dist/components/chat-sources.d.ts +8 -8
  45. package/dist/components/chat-sources.js +18 -27
  46. package/dist/components/checkbox.d.ts +1 -1
  47. package/dist/components/checkbox.js +9 -9
  48. package/dist/components/choice-list.d.ts +2 -2
  49. package/dist/components/choice-list.js +16 -18
  50. package/dist/components/code-block.d.ts +5 -5
  51. package/dist/components/code-block.js +26 -26
  52. package/dist/components/collapsible.d.ts +1 -1
  53. package/dist/components/collapsible.js +7 -11
  54. package/dist/components/combobox.d.ts +3 -3
  55. package/dist/components/combobox.js +24 -24
  56. package/dist/components/command.d.ts +13 -13
  57. package/dist/components/command.js +61 -73
  58. package/dist/components/context-menu.d.ts +5 -5
  59. package/dist/components/context-menu.js +22 -22
  60. package/dist/components/dialog.d.ts +6 -6
  61. package/dist/components/dialog.js +23 -28
  62. package/dist/components/direction.d.ts +1 -1
  63. package/dist/components/direction.js +1 -1
  64. package/dist/components/dropdown-menu.d.ts +5 -5
  65. package/dist/components/dropdown-menu.js +20 -20
  66. package/dist/components/field.d.ts +11 -11
  67. package/dist/components/field.js +41 -61
  68. package/dist/components/file-explorer.d.ts +2 -2
  69. package/dist/components/file-explorer.js +89 -105
  70. package/dist/components/file-viewer.d.ts +10 -10
  71. package/dist/components/file-viewer.js +112 -140
  72. package/dist/components/form.d.ts +7 -7
  73. package/dist/components/form.js +24 -28
  74. package/dist/components/glass-banner.d.ts +3 -3
  75. package/dist/components/glass-banner.js +21 -24
  76. package/dist/components/gradient-banner.d.ts +5 -5
  77. package/dist/components/gradient-banner.js +13 -22
  78. package/dist/components/hover-card.d.ts +4 -4
  79. package/dist/components/hover-card.js +6 -6
  80. package/dist/components/input-group.d.ts +11 -11
  81. package/dist/components/input-group.js +22 -36
  82. package/dist/components/input-otp.d.ts +10 -10
  83. package/dist/components/input-otp.js +13 -15
  84. package/dist/components/input.d.ts +1 -1
  85. package/dist/components/input.js +7 -7
  86. package/dist/components/kbd.d.ts +3 -3
  87. package/dist/components/kbd.js +5 -5
  88. package/dist/components/label.d.ts +2 -2
  89. package/dist/components/label.js +2 -2
  90. package/dist/components/mention-list.d.ts +3 -3
  91. package/dist/components/mention-list.js +10 -10
  92. package/dist/components/menubar.d.ts +6 -6
  93. package/dist/components/menubar.js +22 -22
  94. package/dist/components/message-actions.d.ts +2 -2
  95. package/dist/components/message-actions.js +29 -31
  96. package/dist/components/message-response.d.ts +3 -3
  97. package/dist/components/message-response.js +18 -18
  98. package/dist/components/message-scroller.d.ts +12 -12
  99. package/dist/components/message-scroller.js +44 -56
  100. package/dist/components/message.d.ts +11 -11
  101. package/dist/components/message.js +25 -25
  102. package/dist/components/navigation-menu.d.ts +2 -2
  103. package/dist/components/navigation-menu.js +15 -15
  104. package/dist/components/notice-banner.d.ts +2 -2
  105. package/dist/components/notice-banner.js +2 -2
  106. package/dist/components/page-content.d.ts +2 -2
  107. package/dist/components/page-content.js +2 -2
  108. package/dist/components/page-header.d.ts +1 -1
  109. package/dist/components/page-header.js +16 -16
  110. package/dist/components/pagination.d.ts +9 -9
  111. package/dist/components/pagination.js +17 -17
  112. package/dist/components/plan-outline.d.ts +2 -2
  113. package/dist/components/plan-outline.js +43 -60
  114. package/dist/components/popover.d.ts +6 -6
  115. package/dist/components/popover.js +15 -15
  116. package/dist/components/presence-cursor.d.ts +2 -2
  117. package/dist/components/presence-cursor.js +3 -3
  118. package/dist/components/progress.d.ts +1 -1
  119. package/dist/components/progress.js +8 -8
  120. package/dist/components/prompt-list.d.ts +3 -3
  121. package/dist/components/prompt-list.js +6 -6
  122. package/dist/components/questionnaire.d.ts +19 -19
  123. package/dist/components/questionnaire.js +183 -242
  124. package/dist/components/radio-group.d.ts +2 -2
  125. package/dist/components/radio-group.js +6 -6
  126. package/dist/components/reasoning-effort.d.ts +3 -3
  127. package/dist/components/reasoning-effort.js +9 -12
  128. package/dist/components/record-list.d.ts +66 -0
  129. package/dist/components/record-list.js +151 -0
  130. package/dist/components/resizable.d.ts +5 -5
  131. package/dist/components/resizable.js +76 -102
  132. package/dist/components/result-card.d.ts +10 -5
  133. package/dist/components/result-card.js +42 -53
  134. package/dist/components/result-notice.d.ts +18 -0
  135. package/dist/components/result-notice.js +29 -0
  136. package/dist/components/review-card.d.ts +6 -6
  137. package/dist/components/review-card.js +18 -18
  138. package/dist/components/rich-text-editor.d.ts +2 -2
  139. package/dist/components/rich-text-editor.js +56 -72
  140. package/dist/components/schema-diagram.d.ts +3 -3
  141. package/dist/components/schema-diagram.js +39 -53
  142. package/dist/components/scroll-area.d.ts +1 -1
  143. package/dist/components/scroll-area.js +10 -14
  144. package/dist/components/search-dialog.d.ts +6 -6
  145. package/dist/components/search-dialog.js +16 -16
  146. package/dist/components/search-field.d.ts +1 -1
  147. package/dist/components/search-field.js +11 -11
  148. package/dist/components/select.d.ts +3 -3
  149. package/dist/components/select.js +20 -20
  150. package/dist/components/selection-slot.d.ts +2 -2
  151. package/dist/components/selection-slot.js +6 -9
  152. package/dist/components/separator.d.ts +15 -15
  153. package/dist/components/separator.js +22 -27
  154. package/dist/components/settings-list.d.ts +3 -3
  155. package/dist/components/settings-list.js +5 -5
  156. package/dist/components/sheet.d.ts +6 -6
  157. package/dist/components/sheet.js +27 -27
  158. package/dist/components/sidebar.d.ts +22 -22
  159. package/dist/components/sidebar.js +57 -58
  160. package/dist/components/skeleton.d.ts +5 -5
  161. package/dist/components/skeleton.js +14 -21
  162. package/dist/components/slider.d.ts +2 -2
  163. package/dist/components/slider.js +6 -6
  164. package/dist/components/spinner.d.ts +5 -5
  165. package/dist/components/spinner.js +22 -26
  166. package/dist/components/sql-viewer.d.ts +14 -14
  167. package/dist/components/sql-viewer.js +99 -128
  168. package/dist/components/state-panel-samples.d.ts +2 -2
  169. package/dist/components/state-panel-samples.js +209 -245
  170. package/dist/components/state-panel.d.ts +2 -35
  171. package/dist/components/state-panel.js +2 -29
  172. package/dist/components/state-sample.d.ts +7 -7
  173. package/dist/components/state-sample.js +18 -31
  174. package/dist/components/switch.d.ts +1 -1
  175. package/dist/components/switch.js +7 -7
  176. package/dist/components/table-nuqs.d.ts +2 -2
  177. package/dist/components/table-nuqs.js +11 -12
  178. package/dist/components/table.d.ts +4 -4
  179. package/dist/components/table.js +166 -235
  180. package/dist/components/tabs.d.ts +2 -2
  181. package/dist/components/tabs.js +12 -12
  182. package/dist/components/textarea.d.ts +2 -2
  183. package/dist/components/textarea.js +6 -8
  184. package/dist/components/timeline.d.ts +3 -0
  185. package/dist/components/timeline.js +3 -0
  186. package/dist/components/toast.d.ts +2 -2
  187. package/dist/components/toast.js +19 -25
  188. package/dist/components/toggle-group.d.ts +2 -2
  189. package/dist/components/toggle-group.js +18 -20
  190. package/dist/components/toggle.d.ts +2 -2
  191. package/dist/components/toggle.js +8 -8
  192. package/dist/components/tool-activity.d.ts +1 -1
  193. package/dist/components/tool-activity.js +83 -110
  194. package/dist/components/toolbar-status.d.ts +2 -2
  195. package/dist/components/toolbar-status.js +7 -7
  196. package/dist/components/tooltip.d.ts +5 -5
  197. package/dist/components/tooltip.js +12 -11
  198. package/dist/components/typed-text.d.ts +2 -2
  199. package/dist/components/typed-text.js +10 -12
  200. package/dist/components/view-switcher.d.ts +51 -0
  201. package/dist/components/view-switcher.js +114 -0
  202. package/dist/index.d.ts +98 -93
  203. package/dist/index.js +98 -93
  204. package/dist/internal/chart-kit.d.ts +142 -0
  205. package/dist/internal/chart-kit.js +200 -0
  206. package/dist/internal/charts/bars.d.ts +61 -0
  207. package/dist/internal/charts/bars.js +86 -0
  208. package/dist/internal/charts/lines.d.ts +67 -0
  209. package/dist/internal/charts/lines.js +70 -0
  210. package/dist/internal/charts/more.d.ts +77 -0
  211. package/dist/internal/charts/more.js +68 -0
  212. package/dist/internal/charts/parts.d.ts +54 -0
  213. package/dist/internal/charts/parts.js +82 -0
  214. package/dist/internal/charts/timeline-dates.d.ts +38 -0
  215. package/dist/internal/charts/timeline-dates.js +113 -0
  216. package/dist/internal/charts/timeline-window.d.ts +31 -0
  217. package/dist/internal/charts/timeline-window.js +26 -0
  218. package/dist/internal/charts/timeline.d.ts +72 -0
  219. package/dist/internal/charts/timeline.js +287 -0
  220. package/dist/internal/classes.d.ts +1 -1
  221. package/dist/internal/classes.js +7 -9
  222. package/dist/internal/code-tokens.d.ts +2 -2
  223. package/dist/internal/code-tokens.js +12 -12
  224. package/dist/internal/fieldset.d.ts +1 -1
  225. package/dist/internal/fieldset.js +6 -6
  226. package/dist/internal/file-kind.d.ts +1 -1
  227. package/dist/internal/file-kind.js +43 -51
  228. package/dist/internal/flow.d.ts +4 -4
  229. package/dist/internal/flow.js +20 -23
  230. package/dist/internal/languages.js +999 -999
  231. package/dist/internal/message-scroller.d.ts +3 -3
  232. package/dist/internal/message-scroller.js +64 -112
  233. package/dist/internal/pressable.d.ts +1 -1
  234. package/dist/internal/pressable.js +7 -7
  235. package/dist/internal/state-panel.d.ts +35 -0
  236. package/dist/internal/state-panel.js +22 -0
  237. package/dist/internal/syntax.d.ts +2 -2
  238. package/dist/internal/syntax.js +36 -47
  239. package/dist/styles/accordion.css +3 -5
  240. package/dist/styles/agent-fanout.css +64 -60
  241. package/dist/styles/alert-dialog.css +12 -26
  242. package/dist/styles/alert.css +10 -15
  243. package/dist/styles/attachment.css +51 -93
  244. package/dist/styles/avatar.css +17 -24
  245. package/dist/styles/badge.css +11 -36
  246. package/dist/styles/board.css +571 -0
  247. package/dist/styles/brand-icon.css +7 -8
  248. package/dist/styles/breadcrumb.css +4 -7
  249. package/dist/styles/button-group.css +8 -14
  250. package/dist/styles/button.css +22 -27
  251. package/dist/styles/calendar.css +24 -36
  252. package/dist/styles/canvas.css +2 -3
  253. package/dist/styles/card.css +7 -23
  254. package/dist/styles/carousel.css +11 -12
  255. package/dist/styles/chart.css +215 -31
  256. package/dist/styles/chat-input.css +15 -39
  257. package/dist/styles/chat-plan.css +3 -4
  258. package/dist/styles/chat-sources.css +3 -6
  259. package/dist/styles/checkbox.css +11 -25
  260. package/dist/styles/choice-list.css +17 -31
  261. package/dist/styles/code-block.css +11 -25
  262. package/dist/styles/collapsible.css +10 -15
  263. package/dist/styles/combobox.css +11 -15
  264. package/dist/styles/command.css +5 -8
  265. package/dist/styles/context-menu.css +2 -2
  266. package/dist/styles/dialog.css +5 -16
  267. package/dist/styles/dropdown-menu.css +2 -2
  268. package/dist/styles/field.css +15 -21
  269. package/dist/styles/file-explorer.css +19 -33
  270. package/dist/styles/file-viewer.css +13 -17
  271. package/dist/styles/form.css +3 -3
  272. package/dist/styles/glass-banner.css +12 -47
  273. package/dist/styles/gradient-banner.css +17 -35
  274. package/dist/styles/hover-card.css +9 -15
  275. package/dist/styles/index.css +116 -108
  276. package/dist/styles/input-group.css +25 -41
  277. package/dist/styles/input-otp.css +5 -6
  278. package/dist/styles/input.css +5 -5
  279. package/dist/styles/kbd.css +1 -1
  280. package/dist/styles/label.css +9 -30
  281. package/dist/styles/mention-list.css +6 -8
  282. package/dist/styles/menubar.css +5 -6
  283. package/dist/styles/message-actions.css +1 -1
  284. package/dist/styles/message-response.css +3 -5
  285. package/dist/styles/message-scroller.css +14 -21
  286. package/dist/styles/message.css +23 -60
  287. package/dist/styles/navigation-menu.css +14 -29
  288. package/dist/styles/notice-banner.css +1 -2
  289. package/dist/styles/page-header.css +7 -10
  290. package/dist/styles/pagination.css +1 -4
  291. package/dist/styles/plan-outline.css +20 -37
  292. package/dist/styles/popover.css +10 -19
  293. package/dist/styles/presence-cursor.css +1 -2
  294. package/dist/styles/progress.css +2 -4
  295. package/dist/styles/prompt-list.css +6 -17
  296. package/dist/styles/questionnaire.css +16 -40
  297. package/dist/styles/radio-group.css +12 -23
  298. package/dist/styles/reasoning-effort.css +8 -15
  299. package/dist/styles/record-list.css +243 -0
  300. package/dist/styles/resizable.css +10 -11
  301. package/dist/styles/result-card.css +17 -3
  302. package/dist/styles/result-notice.css +80 -0
  303. package/dist/styles/review-card.css +2 -3
  304. package/dist/styles/rich-text-editor.css +1 -2
  305. package/dist/styles/schema-diagram.css +12 -17
  306. package/dist/styles/scroll-area.css +8 -10
  307. package/dist/styles/search-dialog.css +1 -5
  308. package/dist/styles/search-field.css +1 -2
  309. package/dist/styles/select.css +7 -7
  310. package/dist/styles/selection-slot.css +1 -2
  311. package/dist/styles/separator.css +7 -8
  312. package/dist/styles/settings-list.css +1 -2
  313. package/dist/styles/sheet.css +40 -73
  314. package/dist/styles/sidebar.css +17 -21
  315. package/dist/styles/skeleton.css +9 -11
  316. package/dist/styles/slider.css +11 -21
  317. package/dist/styles/spinner.css +2 -2
  318. package/dist/styles/sql-viewer.css +29 -29
  319. package/dist/styles/state-panel.css +8 -9
  320. package/dist/styles/state-sample.css +20 -33
  321. package/dist/styles/switch.css +11 -18
  322. package/dist/styles/table.css +51 -77
  323. package/dist/styles/tabs.css +12 -16
  324. package/dist/styles/textarea.css +3 -3
  325. package/dist/styles/themes/chatgpt.css +11 -23
  326. package/dist/styles/themes/claude.css +12 -24
  327. package/dist/styles/themes/cloudflare.css +11 -23
  328. package/dist/styles/themes/discord.css +11 -23
  329. package/dist/styles/themes/github.css +11 -23
  330. package/dist/styles/themes/grok.css +11 -23
  331. package/dist/styles/themes/linear.css +12 -25
  332. package/dist/styles/themes/neutral.css +11 -23
  333. package/dist/styles/themes/notion.css +11 -23
  334. package/dist/styles/themes/perplexity.css +11 -23
  335. package/dist/styles/themes/raycast.css +11 -23
  336. package/dist/styles/themes/spotify.css +11 -23
  337. package/dist/styles/themes/stripe.css +11 -23
  338. package/dist/styles/themes/supabase.css +11 -23
  339. package/dist/styles/themes/tulipfarm.css +9 -10
  340. package/dist/styles/themes/vercel.css +12 -25
  341. package/dist/styles/timeline.css +276 -0
  342. package/dist/styles/toast.css +13 -20
  343. package/dist/styles/toggle-group.css +17 -23
  344. package/dist/styles/toggle.css +8 -8
  345. package/dist/styles/tool-activity.css +7 -13
  346. package/dist/styles/toolbar-status.css +2 -7
  347. package/dist/styles/tooltip.css +15 -21
  348. package/dist/styles/view-switcher.css +279 -0
  349. package/package.json +21 -1
@@ -0,0 +1,640 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { memo, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, } from 'react';
4
+ import { DndContext, DragOverlay, KeyboardSensor, PointerSensor, TouchSensor, closestCenter, pointerWithin, rectIntersection, useDroppable, useSensor, useSensors, } from '@dnd-kit/core';
5
+ import { SortableContext, arrayMove, defaultAnimateLayoutChanges, horizontalListSortingStrategy, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable';
6
+ import { CSS } from '@dnd-kit/utilities';
7
+ import { defaultRangeExtractor, useVirtualizer } from '@tanstack/react-virtual';
8
+ import { ChevronDown } from 'reicon-react/icons/ChevronDown';
9
+ import { ChevronRight } from 'reicon-react/icons/ChevronRight';
10
+ import { MoreH } from 'reicon-react/icons/MoreH';
11
+ import { Plus } from 'reicon-react/icons/Plus';
12
+ import { Reorder } from 'reicon-react/icons/Reorder';
13
+ import { cn } from '../internal/classes.js';
14
+ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from './alert-dialog.js';
15
+ import { Badge } from './badge.js';
16
+ import { Button } from './button.js';
17
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from './dropdown-menu.js';
18
+ import { Input } from './input.js';
19
+ import { SearchField } from './search-field.js';
20
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select.js';
21
+ import { Skeleton } from './skeleton.js';
22
+ const CARD = 'card:';
23
+ const COLUMN = 'column:';
24
+ const cellKey = (laneId, columnId) => `${laneId}\u0000${columnId}`;
25
+ const dataOf = (value) => value;
26
+ const dndAccessibility = {
27
+ container: typeof document === 'undefined' ? undefined : document.body,
28
+ };
29
+ const keyboardCodes = {
30
+ start: ['Space'],
31
+ cancel: ['Escape'],
32
+ end: ['Space'],
33
+ };
34
+ // dnd-kit only animates neighbors while the moved card is still in their list,
35
+ // so cards in a cell the dragged card just left would jump without this.
36
+ const animateLayoutChanges = args => defaultAnimateLayoutChanges({ ...args, wasDragging: true });
37
+ const dropAnimation = {
38
+ duration: 160,
39
+ easing: 'cubic-bezier(0.2, 0, 0, 1)',
40
+ };
41
+ const reducedMotion = () => typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
42
+ // Touch goes to TouchSensor, which waits for a press so a swipe still scrolls.
43
+ class NoTouchPointerSensor extends PointerSensor {
44
+ static activators = [
45
+ {
46
+ eventName: 'onPointerDown',
47
+ handler: ({ nativeEvent }) => nativeEvent.pointerType !== 'touch' && nativeEvent.isPrimary && nativeEvent.button === 0,
48
+ },
49
+ ];
50
+ }
51
+ const sameList = (a, b) => a !== undefined && a.length === b.length && a.every((x, i) => x === b[i]);
52
+ const collide = args => {
53
+ const type = dataOf(args.active.data.current)?.type;
54
+ const droppableContainers = args.droppableContainers.filter(container => type === 'column'
55
+ ? dataOf(container.data.current)?.type === 'column'
56
+ : dataOf(container.data.current)?.type !== 'column');
57
+ const scoped = { ...args, droppableContainers };
58
+ if (type === 'column')
59
+ return closestCenter(scoped);
60
+ const pointer = pointerWithin(scoped);
61
+ const hits = pointer.length ? pointer : rectIntersection(scoped);
62
+ const card = hits.find(hit => dataOf(droppableContainers.find(c => c.id === hit.id)?.data.current)?.type === 'card');
63
+ return card ? [card] : hits.slice(0, 1);
64
+ };
65
+ /** Moves `ids` into `to`, before `beforeId`, or to the end of that cell. */
66
+ function place(list, ids, to, beforeId, laneOf, withLanes) {
67
+ const moving = new Set(ids);
68
+ const moved = ids.flatMap(id => {
69
+ const item = list.find(i => i.id === id);
70
+ if (!item)
71
+ return [];
72
+ return [
73
+ {
74
+ ...item,
75
+ columnId: to.columnId,
76
+ ...(withLanes ? { laneId: to.laneId } : {}),
77
+ },
78
+ ];
79
+ });
80
+ const rest = list.filter(item => !moving.has(item.id));
81
+ let at = beforeId === null ? -1 : rest.findIndex(i => i.id === beforeId);
82
+ if (at === -1) {
83
+ at = rest.length;
84
+ for (let i = rest.length - 1; i >= 0; i--) {
85
+ const item = rest[i];
86
+ if (item.columnId === to.columnId && laneOf(item) === to.laneId) {
87
+ at = i + 1;
88
+ break;
89
+ }
90
+ }
91
+ }
92
+ rest.splice(at, 0, ...moved);
93
+ return rest;
94
+ }
95
+ /**
96
+ * A Kanban board. Columns run across, optional lanes run down, and cards sit
97
+ * in the cell where they meet. Cards drag to any cell, columns drag by their
98
+ * header, and titles rename in place. Lanes and the cards in each cell are
99
+ * virtualized, so thousands of cards stay fast.
100
+ */
101
+ export function Board({ columns, items, lanes, onItemsChange, onColumnsChange, onItemAdd, onColumnAdd, onColumnRetry, onColumnShowAll, columnText, onItemOpen, renderBody, renderTitle, renderCard, itemActions, searchable, searchText, laneBy, label = 'Board', className, }) {
102
+ const dndId = useId();
103
+ const withLanes = lanes !== undefined;
104
+ const laneList = useMemo(() => lanes ?? [{ id: '', title: label }], [lanes, label]);
105
+ const laneOf = (item) => (withLanes ? (item.laneId ?? '') : '');
106
+ const [draft, setDraft] = useState(null);
107
+ const draftRef = useRef(null);
108
+ const setDraftBoth = (next) => {
109
+ draftRef.current = next;
110
+ setDraft(next);
111
+ };
112
+ const shown = draft ?? items;
113
+ const [query, setQuery] = useState('');
114
+ const [selected, setSelected] = useState(() => new Set());
115
+ const [anchor, setAnchor] = useState(null);
116
+ const [collapsedColumns, setCollapsedColumns] = useState(() => new Set());
117
+ const [collapsedLanes, setCollapsedLanes] = useState(() => new Set());
118
+ const [active, setActive] = useState(null);
119
+ const [extras, setExtras] = useState([]);
120
+ const [laneLock, setLaneLock] = useState(null);
121
+ const movedRef = useRef(false);
122
+ const needle = query.trim().toLowerCase();
123
+ const matches = (item) => !needle ||
124
+ item.title.toLowerCase().includes(needle) ||
125
+ (searchText?.(item).toLowerCase().includes(needle) ?? false);
126
+ // Unchanged cells keep their array, so memoized cells skip re-rendering mid-drag.
127
+ const cellsRef = useRef(new Map());
128
+ const cells = useMemo(() => {
129
+ const map = new Map();
130
+ for (const item of shown) {
131
+ if (!matches(item))
132
+ continue;
133
+ const key = cellKey(laneOf(item), item.columnId);
134
+ const list = map.get(key);
135
+ if (list)
136
+ list.push(item);
137
+ else
138
+ map.set(key, [item]);
139
+ }
140
+ for (const [key, list] of map) {
141
+ const before = cellsRef.current.get(key);
142
+ if (sameList(before, list))
143
+ map.set(key, before);
144
+ }
145
+ cellsRef.current = map;
146
+ return map;
147
+ // eslint-disable-next-line react-hooks/exhaustive-deps
148
+ }, [shown, needle, searchText, withLanes]);
149
+ const columnTotals = useMemo(() => {
150
+ const totals = new Map();
151
+ for (const item of shown)
152
+ totals.set(item.columnId, (totals.get(item.columnId) ?? 0) + 1);
153
+ return totals;
154
+ }, [shown]);
155
+ const laneTotals = (laneId) => columns.reduce((sum, column) => sum + (cells.get(cellKey(laneId, column.id))?.length ?? 0), 0);
156
+ const visibleCount = [...cells.values()].reduce((s, l) => s + l.length, 0);
157
+ const findItem = (id) => (draftRef.current ?? items).find(item => item.id === id);
158
+ const cellOf = (item) => ({
159
+ laneId: laneOf(item),
160
+ columnId: item.columnId,
161
+ });
162
+ const cellName = (cell) => {
163
+ const column = columns.find(c => c.id === cell.columnId)?.title ?? '';
164
+ const lane = laneList.find(l => l.id === cell.laneId)?.title;
165
+ return withLanes && lane ? `${column}, ${lane}` : column;
166
+ };
167
+ const sensors = useSensors(useSensor(NoTouchPointerSensor, { activationConstraint: { distance: 4 } }), useSensor(TouchSensor, {
168
+ activationConstraint: { delay: 200, tolerance: 6 },
169
+ }), useSensor(KeyboardSensor, {
170
+ coordinateGetter: sortableKeyboardCoordinates,
171
+ keyboardCodes,
172
+ }));
173
+ const onDragStart = ({ active: a }) => {
174
+ const data = dataOf(a.data.current);
175
+ if (!data)
176
+ return;
177
+ setActive(data);
178
+ if (data.type !== 'card')
179
+ return;
180
+ // Lanes may grow but not shrink mid-drag, so the lane under the pointer
181
+ // does not move away when a card leaves it.
182
+ setLaneLock(new Map(laneVirtualizer.getVirtualItems().map(row => [laneList[row.index]?.id ?? '', row.size])));
183
+ const others = selected.has(data.id)
184
+ ? shown.filter(item => selected.has(item.id) && item.id !== data.id).map(item => item.id)
185
+ : [];
186
+ if (!selected.has(data.id))
187
+ setSelected(new Set());
188
+ setExtras(others);
189
+ const hidden = new Set(others);
190
+ setDraftBoth(items.filter(item => !hidden.has(item.id)));
191
+ };
192
+ const onDragOver = ({ active: a, over }) => {
193
+ const data = dataOf(a.data.current);
194
+ const target = dataOf(over?.data.current);
195
+ if (data?.type !== 'card' || !over || !target || movedRef.current)
196
+ return;
197
+ const list = draftRef.current;
198
+ const item = findItem(data.id);
199
+ if (!list || !item)
200
+ return;
201
+ let to;
202
+ let beforeId = null;
203
+ if (target.type === 'card') {
204
+ if (target.id === data.id)
205
+ return;
206
+ const overItem = findItem(target.id);
207
+ if (!overItem)
208
+ return;
209
+ to = cellOf(overItem);
210
+ const siblings = cells.get(cellKey(to.laneId, to.columnId)) ?? [];
211
+ const translated = a.rect.current.translated;
212
+ const below = translated !== null && translated.top > over.rect.top + over.rect.height / 2;
213
+ const index = siblings.findIndex(s => s.id === target.id);
214
+ beforeId = below ? (siblings[index + 1]?.id ?? null) : target.id;
215
+ }
216
+ else if (target.type === 'cell') {
217
+ to = target.cell;
218
+ }
219
+ else
220
+ return;
221
+ const from = cellOf(item);
222
+ if (from.laneId === to.laneId && from.columnId === to.columnId)
223
+ return;
224
+ movedRef.current = true;
225
+ requestAnimationFrame(() => {
226
+ movedRef.current = false;
227
+ });
228
+ setDraftBoth(place(list, [data.id], to, beforeId, laneOf, withLanes));
229
+ };
230
+ const finish = () => {
231
+ setDraftBoth(null);
232
+ setLaneLock(null);
233
+ setActive(null);
234
+ setExtras([]);
235
+ };
236
+ const onDragEnd = ({ active: a, over }) => {
237
+ const data = dataOf(a.data.current);
238
+ const target = dataOf(over?.data.current);
239
+ if (data?.type === 'column') {
240
+ if (target?.type === 'column' && target.id !== data.id && onColumnsChange) {
241
+ const ids = columns.map(c => c.id);
242
+ onColumnsChange(arrayMove(columns, ids.indexOf(data.id), ids.indexOf(target.id)));
243
+ }
244
+ return finish();
245
+ }
246
+ let list = draftRef.current;
247
+ const item = data?.type === 'card' ? findItem(data.id) : undefined;
248
+ if (!list || !item || data?.type !== 'card')
249
+ return finish();
250
+ const cell = cellOf(item);
251
+ const siblings = cells.get(cellKey(cell.laneId, cell.columnId)) ?? [];
252
+ if (target?.type === 'card' && target.id !== data.id) {
253
+ const ids = siblings.map(s => s.id);
254
+ const from = ids.indexOf(data.id);
255
+ const to = ids.indexOf(target.id);
256
+ if (from !== -1 && to !== -1) {
257
+ const order = arrayMove(ids, from, to);
258
+ list = place(list, [data.id], cell, order[to + 1] ?? null, laneOf, withLanes);
259
+ }
260
+ }
261
+ if (extras.length) {
262
+ list = [...list, ...items.filter(i => extras.includes(i.id))];
263
+ const after = list.indexOf(list.find(i => i.id === data.id)) + 1;
264
+ list = place(list, extras, cell, list[after]?.id ?? null, laneOf, withLanes);
265
+ const next = list[list.findIndex(i => i.id === extras.at(-1)) + 1];
266
+ if (next && cellKey(laneOf(next), next.columnId) !== cellKey(cell.laneId, cell.columnId))
267
+ list = place(list, extras, cell, null, laneOf, withLanes);
268
+ }
269
+ const changed = list.some((i, index) => i !== items[index]) || list.length !== items.length;
270
+ finish();
271
+ if (changed)
272
+ onItemsChange?.(list);
273
+ };
274
+ const announcements = {
275
+ onDragStart: ({ active: a }) => {
276
+ const data = dataOf(a.data.current);
277
+ if (data?.type === 'column')
278
+ return `Picked up column ${columns.find(c => c.id === data.id)?.title}.`;
279
+ const item = data?.type === 'card' ? findItem(data.id) : undefined;
280
+ return item ? `Picked up card ${item.title}.` : undefined;
281
+ },
282
+ onDragOver: ({ active: a, over }) => {
283
+ const data = dataOf(a.data.current);
284
+ if (!over)
285
+ return undefined;
286
+ if (data?.type === 'column') {
287
+ const target = dataOf(over.data.current);
288
+ return target?.type === 'column' ? `Over column ${columns.find(c => c.id === target.id)?.title}.` : undefined;
289
+ }
290
+ const item = data?.type === 'card' ? findItem(data.id) : undefined;
291
+ return item ? `Card ${item.title} is in ${cellName(cellOf(item))}.` : undefined;
292
+ },
293
+ onDragEnd: ({ active: a }) => {
294
+ const data = dataOf(a.data.current);
295
+ if (data?.type === 'column')
296
+ return 'Column dropped.';
297
+ const item = data?.type === 'card' ? findItem(data.id) : undefined;
298
+ return item ? `Card ${item.title} dropped in ${cellName(cellOf(item))}.` : undefined;
299
+ },
300
+ onDragCancel: () => 'Move cancelled. The card went back.',
301
+ };
302
+ const select = (item, event) => {
303
+ const next = new Set(selected);
304
+ if (event.shiftKey && anchor) {
305
+ const flat = laneList.flatMap(lane => columns.flatMap(c => cells.get(cellKey(lane.id, c.id)) ?? []));
306
+ const from = flat.findIndex(i => i.id === anchor);
307
+ const to = flat.findIndex(i => i.id === item.id);
308
+ if (from !== -1 && to !== -1)
309
+ for (const i of flat.slice(Math.min(from, to), Math.max(from, to) + 1))
310
+ next.add(i.id);
311
+ }
312
+ else if (next.has(item.id))
313
+ next.delete(item.id);
314
+ else
315
+ next.add(item.id);
316
+ setSelected(next);
317
+ setAnchor(item.id);
318
+ };
319
+ const rename = (item, title) => onItemsChange?.(items.map(i => (i.id === item.id ? { ...i, title } : i)));
320
+ const renameColumn = (column, title) => onColumnsChange?.(columns.map(c => (c.id === column.id ? { ...c, title } : c)));
321
+ const deleteColumn = (column) => {
322
+ onColumnsChange?.(columns.filter(c => c.id !== column.id));
323
+ if (items.some(i => i.columnId === column.id))
324
+ onItemsChange?.(items.filter(i => i.columnId !== column.id));
325
+ };
326
+ const latest = useRef({ select, rename });
327
+ useLayoutEffect(() => {
328
+ latest.current = { select, rename };
329
+ });
330
+ const onSelect = useCallback((item, event) => latest.current.select(item, event), []);
331
+ const onRename = useCallback((item, title) => latest.current.rename(item, title), []);
332
+ const toggle = (set, id) => {
333
+ const next = new Set(set);
334
+ if (next.has(id))
335
+ next.delete(id);
336
+ else
337
+ next.add(id);
338
+ return next;
339
+ };
340
+ const viewportRef = useRef(null);
341
+ const bodyRef = useRef(null);
342
+ const [bodyTop, setBodyTop] = useState(0);
343
+ useLayoutEffect(() => {
344
+ setBodyTop(bodyRef.current?.offsetTop ?? 0);
345
+ });
346
+ // Writes data attributes directly so scrolling never re-renders the board.
347
+ useEffect(() => {
348
+ const viewport = viewportRef.current;
349
+ const frame = viewport?.parentElement;
350
+ if (!viewport || !frame)
351
+ return;
352
+ const update = () => {
353
+ const { scrollLeft, scrollWidth, clientWidth } = viewport;
354
+ frame.toggleAttribute('data-more-start', scrollLeft > 1);
355
+ frame.toggleAttribute('data-more-end', scrollLeft + clientWidth < scrollWidth - 1);
356
+ };
357
+ update();
358
+ viewport.addEventListener('scroll', update, { passive: true });
359
+ const observer = new ResizeObserver(update);
360
+ observer.observe(viewport);
361
+ if (viewport.firstElementChild)
362
+ observer.observe(viewport.firstElementChild);
363
+ return () => {
364
+ viewport.removeEventListener('scroll', update);
365
+ observer.disconnect();
366
+ };
367
+ }, []);
368
+ const activeLane = active?.type === 'card'
369
+ ? laneList.findIndex(l => {
370
+ const item = findItem(active.id);
371
+ return item ? l.id === laneOf(item) : false;
372
+ })
373
+ : -1;
374
+ const laneVirtualizer = useVirtualizer({
375
+ count: withLanes ? laneList.length : 0,
376
+ getScrollElement: () => viewportRef.current,
377
+ estimateSize: () => 140,
378
+ overscan: 2,
379
+ scrollMargin: bodyTop,
380
+ initialRect: { width: 0, height: 720 },
381
+ getItemKey: index => laneList[index]?.id ?? index,
382
+ rangeExtractor: (range) => {
383
+ const indexes = defaultRangeExtractor(range);
384
+ return activeLane !== -1 && !indexes.includes(activeLane)
385
+ ? [...indexes, activeLane].sort((a, b) => a - b)
386
+ : indexes;
387
+ },
388
+ });
389
+ // Lanes sit in normal flow after one spacer, so a lane that grows pushes the
390
+ // ones below it in the same frame. The dragged card's lane, kept rendered
391
+ // outside the range, is placed on its own.
392
+ const laneRows = laneVirtualizer.getVirtualItems().map((row, i, rows) => ({
393
+ ...row,
394
+ pinned: rows.length > 1 &&
395
+ row.index === activeLane &&
396
+ rows[i - 1]?.index !== row.index - 1 &&
397
+ rows[i + 1]?.index !== row.index + 1,
398
+ }));
399
+ const firstFlow = laneRows.find(row => !row.pinned);
400
+ const onBoardKeyDown = (event) => {
401
+ if (event.key === 'Escape' && selected.size && !active)
402
+ setSelected(new Set());
403
+ };
404
+ const activeItem = active?.type === 'card' ? findItem(active.id) : undefined;
405
+ const activeColumn = active?.type === 'column' ? columns.find(c => c.id === active.id) : undefined;
406
+ const columnTemplate = columns
407
+ .map(c => (collapsedColumns.has(c.id) ? 'var(--sprout-board-collapsed-width)' : 'var(--sprout-board-column-width)'))
408
+ .join(' ');
409
+ const gridStyle = {
410
+ gridTemplateColumns: `${columnTemplate}${onColumnAdd ? ' var(--sprout-board-column-width)' : ''}`,
411
+ };
412
+ const loadedIn = (column) => columnTotals.get(column.id) ?? 0;
413
+ const showAllCount = (column) => onColumnShowAll &&
414
+ !column.status &&
415
+ !collapsedColumns.has(column.id) &&
416
+ column.total !== undefined &&
417
+ column.total > loadedIn(column)
418
+ ? column.total
419
+ : undefined;
420
+ // With lanes, a column's message and "Show all" show once, under its header
421
+ // and at the board foot, instead of in every lane.
422
+ const laneNotices = withLanes && columns.some(c => c.status === 'error' || c.status === 'unavailable');
423
+ const laneFoot = withLanes && columns.some(c => showAllCount(c) !== undefined);
424
+ const renderRow = (lane) => (_jsx("div", { className: "sprout-board-row", style: gridStyle, children: columns.map(column => (_jsx(BoardCell, { laneId: lane.id, column: column, name: cellName({ laneId: lane.id, columnId: column.id }), items: cells.get(cellKey(lane.id, column.id)) ?? [], collapsed: collapsedColumns.has(column.id), fill: !withLanes, empty: !column.status && !column.total && loadedIn(column) === 0 ? (columnText?.empty ?? 'No cards') : undefined, showAll: withLanes ? undefined : showAllCount(column), onShowAll: !withLanes && showAllCount(column) !== undefined ? onColumnShowAll : undefined, showAllText: columnText?.showAll, onRetry: !withLanes && column.status === 'error' ? onColumnRetry : undefined, activeId: active?.type === 'card' ? active.id : null, movable: !!onItemsChange && !needle, selected: selected, onSelect: onItemsChange ? onSelect : undefined, onRename: onItemsChange ? onRename : undefined, onItemAdd: onItemAdd, onItemOpen: onItemOpen, look: look }, column.id))) }));
425
+ const look = useMemo(() => ({ renderBody, renderTitle, renderCard, itemActions }), [renderBody, renderTitle, renderCard, itemActions]);
426
+ const toolbar = searchable || laneBy || selected.size > 0;
427
+ return (_jsxs("div", { "data-slot": "board", className: cn('sprout-board', className), onKeyDown: onBoardKeyDown, children: [toolbar && (_jsxs("div", { className: "sprout-board-toolbar", "data-slot": "board-toolbar", children: [searchable && (_jsx(SearchField, { label: "Search cards", value: query, onChange: setQuery, className: "sprout-board-search" })), laneBy && (_jsxs("span", { className: "sprout-board-lane-by", children: [_jsx("span", { children: laneBy.label ?? 'Lanes by' }), _jsxs(Select, { value: laneBy.value, onValueChange: value => {
428
+ if (typeof value === 'string')
429
+ laneBy.onChange(value);
430
+ }, children: [_jsx(SelectTrigger, { size: "sm", "aria-label": laneBy.label ?? 'Lanes by', children: _jsx(SelectValue, { children: laneBy.options.find(o => o.value === laneBy.value)?.label }) }), _jsx(SelectContent, { children: laneBy.options.map(option => (_jsx(SelectItem, { value: option.value, children: option.label }, option.value))) })] })] })), _jsx("span", { className: "sprout-board-status", role: "status", children: selected.size > 0 ? (_jsxs(_Fragment, { children: [selected.size, " selected", _jsx(Button, { variant: "ghost", size: "xs", onClick: () => setSelected(new Set()), children: "Clear" })] })) : needle ? (visibleCount ? (`${visibleCount.toLocaleString('en-US')} of ${items.length.toLocaleString('en-US')} cards`) : (`No cards match “${query.trim()}”`)) : (`${items.length.toLocaleString('en-US')} cards`) })] })), _jsxs(DndContext, { id: dndId, sensors: sensors, accessibility: { ...dndAccessibility, announcements }, collisionDetection: collide, onDragStart: onDragStart, onDragOver: onDragOver, onDragEnd: onDragEnd, onDragCancel: finish, children: [_jsx("div", { className: "sprout-board-frame", children: _jsx("div", { ref: viewportRef, role: "region", "aria-label": label, className: "sprout-board-viewport", "data-lanes": withLanes || undefined, children: _jsxs("div", { className: "sprout-board-canvas", children: [_jsxs("div", { className: "sprout-board-header", style: gridStyle, children: [_jsx(SortableContext, { items: columns.map(c => COLUMN + c.id), strategy: horizontalListSortingStrategy, children: columns.map(column => (_jsx(BoardColumnHeader, { column: column, loaded: loadedIn(column), collapsed: collapsedColumns.has(column.id), onCollapse: () => setCollapsedColumns(s => toggle(s, column.id)), onRename: onColumnsChange ? renameColumn : undefined, onDelete: onColumnsChange ? deleteColumn : undefined }, column.id))) }), onColumnAdd && _jsx(AddColumn, { onAdd: onColumnAdd }), laneNotices &&
431
+ columns.map(column => (_jsx("div", { className: "sprout-board-column-notice", children: !collapsedColumns.has(column.id) && _jsx(ColumnNotice, { column: column, onRetry: onColumnRetry }) }, `notice:${column.id}`)))] }), withLanes ? (_jsx("div", { ref: bodyRef, className: "sprout-board-lanes", style: { height: laneVirtualizer.getTotalSize() }, children: laneRows.map(row => {
432
+ const lane = laneList[row.index];
433
+ if (!lane)
434
+ return null;
435
+ const collapsed = collapsedLanes.has(lane.id);
436
+ const count = laneTotals(lane.id);
437
+ return (_jsxs("section", { ref: laneVirtualizer.measureElement, "data-index": row.index, "aria-label": `${lane.title}, ${count} cards`, className: "sprout-board-lane", "data-collapsed": collapsed || undefined, "data-pinned": row.pinned || undefined, style: {
438
+ ...(row.pinned
439
+ ? { top: row.start - bodyTop }
440
+ : row === firstFlow
441
+ ? { marginTop: row.start - bodyTop }
442
+ : {}),
443
+ minHeight: laneLock?.get(lane.id),
444
+ }, children: [_jsx("div", { className: "sprout-board-lane-bar", children: _jsxs("button", { type: "button", className: "sprout-board-lane-toggle", "aria-expanded": !collapsed, onClick: () => setCollapsedLanes(s => toggle(s, lane.id)), children: [collapsed ? _jsx(ChevronRight, { "aria-hidden": "true" }) : _jsx(ChevronDown, { "aria-hidden": "true" }), _jsx("span", { className: "sprout-board-lane-title", children: lane.title }), _jsx("span", { className: "sprout-board-count", children: count })] }) }), !collapsed && renderRow(lane)] }, row.key));
445
+ }) })) : (_jsx("div", { ref: bodyRef, className: "sprout-board-single", children: laneList[0] && renderRow(laneList[0]) })), laneFoot && onColumnShowAll && (_jsx("div", { className: "sprout-board-foot", style: gridStyle, children: columns.map(column => {
446
+ const count = showAllCount(column);
447
+ return (_jsx("div", { children: count !== undefined && (_jsx(ShowAll, { column: column, count: count, onShowAll: onColumnShowAll, text: columnText?.showAll })) }, column.id));
448
+ }) }))] }) }) }), _jsx(DragOverlay, { dropAnimation: reducedMotion() ? null : dropAnimation, children: activeItem ? (_jsxs("div", { className: "sprout-board-overlay", children: [_jsx(CardFace, { item: activeItem, look: look }), extras.length > 0 && (_jsxs(Badge, { variant: "default", className: "sprout-board-overlay-count", children: [extras.length + 1, " cards"] }))] })) : activeColumn ? (_jsx("div", { className: "sprout-board-column-header sprout-board-overlay", children: _jsx("span", { className: "sprout-board-column-title", "data-tone": activeColumn.tone ? '' : undefined, children: _jsx(ColumnTitle, { column: activeColumn }) }) })) : null })] })] }));
449
+ }
450
+ const cardTitle = (item, look) => (_jsx("span", { className: "sprout-board-card-title", children: look.renderTitle ? look.renderTitle(item) : item.title }));
451
+ function cardInside(item, look, parts, bodyId) {
452
+ if (look.renderCard)
453
+ return look.renderCard(item, parts);
454
+ const body = look.renderBody?.(item);
455
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sprout-board-card-head", children: [parts.title, parts.menu] }), body != null && (_jsx("div", { id: bodyId, className: "sprout-board-card-body", children: body }))] }));
456
+ }
457
+ function CardFace({ item, look }) {
458
+ return (_jsx("div", { className: "sprout-board-card", "data-overlay": "", "data-custom": look.renderCard ? '' : undefined, children: cardInside(item, look, { title: cardTitle(item, look), menu: null }) }));
459
+ }
460
+ const BoardCell = memo(function BoardCell({ laneId, column, name, items, collapsed, fill, empty, showAll, onShowAll, showAllText, onRetry, activeId, movable, selected, onSelect, onRename, onItemAdd, onItemOpen, look, }) {
461
+ const columnId = column.id;
462
+ const status = column.status;
463
+ const cell = useMemo(() => ({ laneId, columnId }), [laneId, columnId]);
464
+ const drop = useDroppable({
465
+ id: `cell:${cellKey(cell.laneId, cell.columnId)}`,
466
+ data: { type: 'cell', cell },
467
+ disabled: status !== undefined,
468
+ });
469
+ const scrollRef = useRef(null);
470
+ const activeIndex = activeId ? items.findIndex(i => i.id === activeId) : -1;
471
+ const virtualizer = useVirtualizer({
472
+ count: collapsed ? 0 : items.length,
473
+ getScrollElement: () => scrollRef.current,
474
+ estimateSize: () => 60,
475
+ gap: 4,
476
+ overscan: 6,
477
+ initialRect: { width: 0, height: 480 },
478
+ getItemKey: index => items[index]?.id ?? index,
479
+ rangeExtractor: (range) => {
480
+ const indexes = defaultRangeExtractor(range);
481
+ return activeIndex !== -1 && !indexes.includes(activeIndex)
482
+ ? [...indexes, activeIndex].sort((a, b) => a - b)
483
+ : indexes;
484
+ },
485
+ });
486
+ const [adding, setAdding] = useState(false);
487
+ if (collapsed)
488
+ return (_jsx("div", { ref: drop.setNodeRef, className: "sprout-board-cell", "data-collapsed": "", "data-over": drop.isOver || undefined, "aria-label": `${name}, ${items.length} cards, collapsed`, role: "group", children: _jsx("span", { className: "sprout-board-count", children: items.length || '' }) }));
489
+ return (_jsxs("div", { ref: drop.setNodeRef, className: "sprout-board-cell", "data-fill": fill || undefined, "data-over": drop.isOver || undefined, "data-status": status, children: [fill && _jsx(ColumnNotice, { column: column, onRetry: onRetry }), _jsxs("div", { ref: scrollRef, className: "sprout-board-cell-scroll", children: [_jsx(SortableContext, { items: items.map(i => CARD + i.id), strategy: verticalListSortingStrategy, children: _jsx("div", { role: "list", "aria-label": `${name}, ${items.length} cards`, "aria-busy": status === 'loading' || undefined, className: "sprout-board-cards", style: { height: virtualizer.getTotalSize() }, children: virtualizer.getVirtualItems().map(row => {
490
+ const item = items[row.index];
491
+ if (!item)
492
+ return null;
493
+ return (_jsx("div", { ref: virtualizer.measureElement, "data-index": row.index, role: "listitem", className: "sprout-board-card-slot", style: { top: row.start }, children: _jsx(BoardCard, { item: item, movable: movable, selected: selected.has(item.id), onSelect: onSelect, onRename: onRename, onOpen: onItemOpen, look: look }) }, row.key));
494
+ }) }) }), status === 'loading' && items.length === 0 && (_jsx("div", { className: "sprout-board-skeletons", "aria-hidden": "true", children: Array.from({ length: fill ? 3 : 2 }, (_, index) => (_jsxs("div", { className: "sprout-board-skeleton-card", children: [_jsx(Skeleton, {}), _jsx(Skeleton, {})] }, index))) })), empty !== undefined && items.length === 0 && _jsx("p", { className: "sprout-board-empty", children: empty })] }), showAll !== undefined && onShowAll && (_jsx(ShowAll, { column: column, count: showAll, onShowAll: onShowAll, text: showAllText })), onItemAdd &&
495
+ !status &&
496
+ (adding ? (_jsx(InlineText, { label: `New card in ${name}`, placeholder: "Card title", initial: "", keepOpen: true, onCommit: title => onItemAdd({
497
+ title,
498
+ columnId: cell.columnId,
499
+ laneId: fill ? undefined : cell.laneId,
500
+ }), onClose: () => setAdding(false) })) : (_jsxs(Button, { variant: "ghost", size: "xs", className: "sprout-board-add", onClick: () => setAdding(true), "aria-label": `Add card to ${name}`, children: [_jsx(Plus, { "aria-hidden": "true", "data-icon": "inline-start" }), "Add card"] })))] }));
501
+ });
502
+ const BoardCard = memo(function BoardCard({ item, movable, selected, onSelect, onRename, onOpen, look, }) {
503
+ const [editing, setEditing] = useState(false);
504
+ const sortable = useSortable({
505
+ id: CARD + item.id,
506
+ data: { type: 'card', id: item.id },
507
+ disabled: !movable || editing,
508
+ animateLayoutChanges,
509
+ });
510
+ const bodyId = useId();
511
+ const actions = look.itemActions?.(item);
512
+ const describe = !look.renderCard && !!look.renderBody;
513
+ const onClick = (event) => {
514
+ if (onSelect && (event.metaKey || event.ctrlKey || event.shiftKey)) {
515
+ event.preventDefault();
516
+ onSelect(item, event);
517
+ return;
518
+ }
519
+ onOpen?.(item);
520
+ };
521
+ const onKeyDown = (event) => {
522
+ if (event.target !== event.currentTarget)
523
+ return;
524
+ if (event.key === 'Enter' && onOpen) {
525
+ event.preventDefault();
526
+ onOpen(item);
527
+ }
528
+ else if (event.key === 'F2' && onRename) {
529
+ event.preventDefault();
530
+ setEditing(true);
531
+ }
532
+ };
533
+ return (_jsx("div", { ref: sortable.setNodeRef, ...sortable.attributes, ...sortable.listeners, "aria-disabled": undefined, role: "group", "aria-roledescription": movable ? 'draggable card' : 'card', "aria-label": item.title, "aria-describedby": describe ? bodyId : undefined, tabIndex: 0, className: "sprout-board-card", "data-selected": selected || undefined, "data-dragging": sortable.isDragging || undefined, "data-openable": onOpen ? '' : undefined, "data-custom": look.renderCard ? '' : undefined, style: {
534
+ transform: CSS.Translate.toString(sortable.transform),
535
+ transition: sortable.transition,
536
+ }, onClick: editing ? undefined : onClick, onDoubleClick: onRename && !onOpen && !editing ? () => setEditing(true) : undefined, onKeyDown: event => {
537
+ onKeyDown(event);
538
+ // Space in the menu button or title must not pick the card up.
539
+ if (event.target === event.currentTarget && !event.defaultPrevented)
540
+ sortable.listeners?.onKeyDown?.(event);
541
+ }, children: cardInside(item, look, {
542
+ title: editing && onRename ? (_jsx(InlineText, { label: "Card title", initial: item.title, onCommit: title => onRename(item, title), onClose: () => setEditing(false) })) : (cardTitle(item, look)),
543
+ menu: (actions != null || onRename) && !editing ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { render: _jsx(Button, { variant: "ghost", size: "icon-xs", className: "sprout-board-card-menu", "aria-label": `Actions for ${item.title}`, onClick: event => event.stopPropagation(), onPointerDown: event => event.stopPropagation() }), children: _jsx(MoreH, { "aria-hidden": "true" }) }), _jsxs(DropdownMenuContent, { align: "end", children: [onRename && _jsx(DropdownMenuItem, { onClick: () => setEditing(true), children: "Rename" }), onRename && actions != null && _jsx(DropdownMenuSeparator, {}), actions] })] })) : null,
544
+ }, bodyId) }));
545
+ });
546
+ const statusWords = {
547
+ loading: 'loading',
548
+ error: 'couldn’t load',
549
+ unavailable: 'unavailable',
550
+ };
551
+ function BoardColumnHeader({ column, loaded, collapsed, onCollapse, onRename, onDelete }) {
552
+ const [editing, setEditing] = useState(false);
553
+ const [confirming, setConfirming] = useState(false);
554
+ const sortable = useSortable({
555
+ id: COLUMN + column.id,
556
+ data: { type: 'column', id: column.id },
557
+ disabled: !onRename || editing,
558
+ });
559
+ const total = column.total ?? loaded;
560
+ // Without a `total`, the loaded count means nothing until the column is ready.
561
+ const counted = column.total !== undefined || !column.status;
562
+ const over = counted && column.limit !== undefined && total > column.limit;
563
+ const count = column.limit !== undefined ? `${formatCount(total)} / ${formatCount(column.limit)}` : formatCount(total);
564
+ const label = [
565
+ column.title,
566
+ counted && `${formatCount(total)} cards`,
567
+ counted && total > loaded && !column.status && `${loaded} shown`,
568
+ column.status && statusWords[column.status],
569
+ over && 'over limit',
570
+ ]
571
+ .filter(Boolean)
572
+ .join(', ');
573
+ const toggleButton = (_jsx("button", { type: "button", className: "sprout-board-column-toggle", "aria-expanded": !collapsed, "aria-label": `${collapsed ? 'Expand' : 'Collapse'} ${column.title}`, onClick: onCollapse, children: collapsed ? _jsx(ChevronRight, { "aria-hidden": "true" }) : _jsx(ChevronDown, { "aria-hidden": "true" }) }));
574
+ return (_jsxs("div", { ref: sortable.setNodeRef, ...sortable.listeners, role: "group", "aria-label": label, "aria-busy": column.status === 'loading' || undefined, className: "sprout-board-column-header", "data-collapsed": collapsed || undefined, "data-over-limit": over || undefined, "data-dragging": sortable.isDragging || undefined, style: {
575
+ transform: CSS.Translate.toString(sortable.transform),
576
+ transition: sortable.transition,
577
+ }, children: [collapsed && toggleButton, editing && onRename ? (_jsx(InlineText, { label: "Column title", initial: column.title, onCommit: title => onRename(column, title), onClose: () => setEditing(false) })) : onRename && !collapsed ? (_jsx("button", { type: "button", className: "sprout-board-column-title", "data-editable": "", onClick: () => setEditing(true), "aria-label": `Rename ${column.title}`, "data-tone": column.tone ? '' : undefined, children: _jsx(ColumnTitle, { column: column }) })) : (_jsx("span", { className: "sprout-board-column-title", "data-tone": column.tone && !collapsed ? '' : undefined, children: collapsed ? column.title : _jsx(ColumnTitle, { column: column }) })), !editing && counted && (_jsx("span", { className: "sprout-board-count", "aria-hidden": "true", children: count })), _jsx("span", { role: "status", className: "sprout-board-sr-only", children: column.status === 'loading' ? `Loading cards in ${column.title}…` : '' }), over && !collapsed && !editing && (_jsx(Badge, { variant: "warning", className: "sprout-board-limit", children: "Over limit" })), !collapsed && !editing && (_jsxs("span", { className: "sprout-board-column-tools", children: [toggleButton, onRename && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", ref: sortable.setActivatorNodeRef, ...sortable.attributes, className: "sprout-board-grip", "aria-label": `Move ${column.title}`, children: _jsx(Reorder, { "aria-hidden": "true" }) }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { render: _jsx(Button, { variant: "ghost", size: "icon-xs", "aria-label": `${column.title} column actions`, onPointerDown: event => event.stopPropagation() }), children: _jsx(MoreH, { "aria-hidden": "true" }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsx(DropdownMenuItem, { onClick: () => setEditing(true), children: "Rename" }), _jsx(DropdownMenuItem, { onClick: onCollapse, children: "Collapse" }), onDelete && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { variant: "destructive", onClick: () => setConfirming(true), children: "Delete column\u2026" })] }))] })] })] }))] })), onDelete && (_jsx(AlertDialog, { open: confirming, onOpenChange: setConfirming, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsxs(AlertDialogTitle, { children: ["Delete \u201C", column.title, "\u201D?"] }), _jsx(AlertDialogDescription, { children: total
578
+ ? `Its ${total} ${total === 1 ? 'card is' : 'cards are'} deleted too. This can’t be undone.`
579
+ : 'The column is empty. This can’t be undone.' })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Keep column" }), _jsx(AlertDialogAction, { variant: "destructive", onClick: () => {
580
+ setConfirming(false);
581
+ onDelete(column);
582
+ }, children: "Delete column" })] })] }) }))] }));
583
+ }
584
+ const formatCount = (count) => count.toLocaleString('en-US');
585
+ function ColumnTitle({ column }) {
586
+ if (!column.tone)
587
+ return column.title;
588
+ return (_jsx(Badge, { variant: column.tone, className: "sprout-board-column-badge", children: _jsx("span", { children: column.title }) }));
589
+ }
590
+ /** The error or unavailable message of a column. Nothing for other states. */
591
+ function ColumnNotice({ column, onRetry, }) {
592
+ if (column.status === 'error')
593
+ return (_jsxs("div", { className: "sprout-board-notice", "data-kind": "error", children: [_jsx("p", { role: "alert", children: column.error ?? 'Couldn’t load this column.' }), onRetry && (_jsx(Button, { variant: "outline", size: "xs", "aria-label": `Try again to load ${column.title}`, onClick: () => onRetry(column), children: "Try again" }))] }));
594
+ if (column.status === 'unavailable')
595
+ return (_jsx("p", { className: "sprout-board-notice", "data-kind": "unavailable", children: column.error ?? 'Cards are unavailable.' }));
596
+ return null;
597
+ }
598
+ function ShowAll({ column, count, onShowAll, text, }) {
599
+ return (_jsx(Button, { variant: "ghost", size: "xs", className: "sprout-board-show-all", "aria-label": text ? undefined : `Show all ${formatCount(count)} in ${column.title}`, onClick: () => onShowAll(column), children: text ? (_jsxs(_Fragment, { children: [text(count, column), _jsxs("span", { className: "sprout-board-sr-only", children: [", ", column.title] })] })) : (`Show all ${formatCount(count)}`) }));
600
+ }
601
+ function AddColumn({ onAdd }) {
602
+ const [adding, setAdding] = useState(false);
603
+ return (_jsx("div", { className: "sprout-board-add-column", children: adding ? (_jsx(InlineText, { label: "New column", placeholder: "Column title", initial: "", onCommit: onAdd, onClose: () => setAdding(false) })) : (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setAdding(true), children: [_jsx(Plus, { "aria-hidden": "true", "data-icon": "inline-start" }), "Add column"] })) }));
604
+ }
605
+ /** Enter saves, Escape cancels, blur saves. With `keepOpen` it clears after each save. */
606
+ function InlineText({ label, placeholder, initial, keepOpen, onCommit, onClose, }) {
607
+ const [value, setValue] = useState(initial);
608
+ const ref = useRef(null);
609
+ const done = useRef(false);
610
+ useEffect(() => {
611
+ ref.current?.focus();
612
+ ref.current?.select();
613
+ }, []);
614
+ const commit = (close) => {
615
+ const text = value.trim();
616
+ if (text && text !== initial)
617
+ onCommit(text);
618
+ if (close || !keepOpen) {
619
+ done.current = true;
620
+ onClose();
621
+ }
622
+ else
623
+ setValue('');
624
+ };
625
+ return (_jsx(Input, { ref: ref, "aria-label": label, placeholder: placeholder, value: value, className: "sprout-board-inline-input", onChange: event => setValue(event.currentTarget.value), onPointerDown: event => event.stopPropagation(), onClick: event => event.stopPropagation(), onKeyDown: event => {
626
+ event.stopPropagation();
627
+ if (event.key === 'Enter') {
628
+ event.preventDefault();
629
+ commit(false);
630
+ }
631
+ else if (event.key === 'Escape') {
632
+ event.preventDefault();
633
+ done.current = true;
634
+ onClose();
635
+ }
636
+ }, onBlur: () => {
637
+ if (!done.current)
638
+ commit(true);
639
+ } }));
640
+ }