@multicomma/arkcn 0.1.2 → 0.1.3
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.
- package/package.json +1 -1
- package/registry/index.json +1 -1
- package/registry/items/accordion.json +302 -0
- package/registry/items/activity-feed.json +408 -0
- package/registry/items/alert-dialog.json +676 -0
- package/registry/items/alert.json +113 -0
- package/registry/items/angle-slider.json +378 -0
- package/registry/items/app-shell.json +2654 -1
- package/registry/items/aspect-ratio.json +36 -0
- package/registry/items/attachment.json +299 -1
- package/registry/items/avatar.json +213 -0
- package/registry/items/badge.json +44 -0
- package/registry/items/breadcrumb.json +191 -0
- package/registry/items/bubble.json +146 -0
- package/registry/items/button-group.json +110 -0
- package/registry/items/button.json +52 -0
- package/registry/items/calendar.json +499 -1
- package/registry/items/canvas.json +446 -1
- package/registry/items/card.json +192 -0
- package/registry/items/carousel.json +208 -1
- package/registry/items/cascader.json +797 -1
- package/registry/items/chart.json +1764 -1
- package/registry/items/chat.json +993 -0
- package/registry/items/checkbox.json +503 -0
- package/registry/items/clipboard.json +308 -0
- package/registry/items/collapsible.json +226 -0
- package/registry/items/color-picker.json +1261 -0
- package/registry/items/combobox.json +1123 -1
- package/registry/items/command.json +837 -1
- package/registry/items/comment-thread.json +952 -1
- package/registry/items/context-menu.json +1344 -1
- package/registry/items/data-grid.json +449 -1
- package/registry/items/data-table.json +1310 -1
- package/registry/items/date-input.json +476 -0
- package/registry/items/dialog.json +592 -0
- package/registry/items/direction.json +29 -1
- package/registry/items/drawer.json +797 -0
- package/registry/items/dropdown-menu.json +1311 -1
- package/registry/items/editable.json +503 -0
- package/registry/items/empty.json +166 -0
- package/registry/items/field.json +286 -1
- package/registry/items/file-upload.json +638 -0
- package/registry/items/floating-panel.json +720 -0
- package/registry/items/floating-toolbar.json +43 -0
- package/registry/items/format.json +109 -0
- package/registry/items/gantt.json +773 -1
- package/registry/items/hooks/use-mobile.json +1 -0
- package/registry/items/hotkeys.json +99 -1
- package/registry/items/hover-card.json +357 -0
- package/registry/items/image-cropper.json +431 -0
- package/registry/items/input-group.json +189 -0
- package/registry/items/input-otp.json +181 -0
- package/registry/items/input.json +28 -0
- package/registry/items/item.json +308 -0
- package/registry/items/json-tree-view.json +336 -0
- package/registry/items/kanban.json +624 -1
- package/registry/items/kbd.json +54 -0
- package/registry/items/label.json +28 -0
- package/registry/items/lib/controllable.json +1 -0
- package/registry/items/lib/data-table-adapter.json +1 -0
- package/registry/items/lib/drag-preview.json +1 -0
- package/registry/items/lib/form.json +1 -0
- package/registry/items/lib/history.json +1 -0
- package/registry/items/lib/row-layout.json +1 -0
- package/registry/items/lib/table.json +1 -0
- package/registry/items/lib/time.json +1 -0
- package/registry/items/lib/utils.json +1 -0
- package/registry/items/live-region.json +43 -0
- package/registry/items/marker.json +96 -0
- package/registry/items/marquee.json +300 -0
- package/registry/items/menubar.json +1282 -1
- package/registry/items/message.json +166 -0
- package/registry/items/navigation-menu.json +561 -1
- package/registry/items/node-graph.json +1037 -1
- package/registry/items/number-input.json +519 -0
- package/registry/items/pagination.json +244 -1
- package/registry/items/password-input.json +311 -0
- package/registry/items/popconfirm.json +566 -0
- package/registry/items/popover.json +615 -0
- package/registry/items/progress.json +516 -0
- package/registry/items/qr-code.json +249 -0
- package/registry/items/query-builder.json +1368 -1
- package/registry/items/radio-group.json +362 -0
- package/registry/items/rating-group.json +304 -0
- package/registry/items/resizable.json +242 -1
- package/registry/items/rich-text-editor.json +934 -1
- package/registry/items/scheduler.json +1013 -1
- package/registry/items/scroll-area.json +227 -0
- package/registry/items/segment-group.json +363 -0
- package/registry/items/select.json +956 -0
- package/registry/items/separator.json +45 -1
- package/registry/items/sheet.json +592 -0
- package/registry/items/sidebar.json +785 -1
- package/registry/items/signature-pad.json +330 -0
- package/registry/items/skeleton.json +28 -0
- package/registry/items/slider.json +799 -0
- package/registry/items/sonner.json +135 -0
- package/registry/items/spinner.json +28 -0
- package/registry/items/steps.json +483 -0
- package/registry/items/swap.json +107 -0
- package/registry/items/switch.json +362 -0
- package/registry/items/table.json +210 -0
- package/registry/items/tabs.json +326 -0
- package/registry/items/tags-input.json +640 -0
- package/registry/items/textarea.json +28 -0
- package/registry/items/timer.json +289 -0
- package/registry/items/toc.json +372 -0
- package/registry/items/toggle-group.json +214 -1
- package/registry/items/toggle.json +124 -0
- package/registry/items/tooltip.json +417 -0
- package/registry/items/tour.json +498 -0
- package/registry/items/transfer-list.json +609 -1
- package/registry/items/tree-select.json +742 -1
- package/registry/items/tree-view.json +794 -0
- package/registry/items/virtual-list.json +214 -1
- package/src/components/ui/app-shell.tsx +11 -0
- package/src/components/ui/attachment.tsx +1 -0
- package/src/components/ui/calendar.tsx +1 -0
- package/src/components/ui/canvas.tsx +8 -0
- package/src/components/ui/carousel.tsx +3 -0
- package/src/components/ui/cascader.tsx +1 -0
- package/src/components/ui/chart.tsx +10 -0
- package/src/components/ui/combobox.tsx +3 -0
- package/src/components/ui/command.tsx +1 -0
- package/src/components/ui/comment-thread.tsx +11 -0
- package/src/components/ui/context-menu.tsx +6 -0
- package/src/components/ui/data-grid.tsx +8 -0
- package/src/components/ui/data-table.tsx +6 -0
- package/src/components/ui/direction.tsx +2 -0
- package/src/components/ui/dropdown-menu.tsx +6 -0
- package/src/components/ui/field.tsx +1 -0
- package/src/components/ui/gantt.tsx +15 -0
- package/src/components/ui/hotkeys.tsx +3 -0
- package/src/components/ui/kanban.tsx +3 -0
- package/src/components/ui/menubar.tsx +6 -0
- package/src/components/ui/navigation-menu.tsx +1 -0
- package/src/components/ui/node-graph.tsx +23 -0
- package/src/components/ui/pagination.tsx +1 -0
- package/src/components/ui/query-builder.tsx +7 -0
- package/src/components/ui/resizable.tsx +1 -0
- package/src/components/ui/rich-text-editor.tsx +54 -0
- package/src/components/ui/scheduler.tsx +26 -0
- package/src/components/ui/separator.tsx +1 -0
- package/src/components/ui/sidebar.tsx +5 -0
- package/src/components/ui/toggle-group.tsx +3 -0
- package/src/components/ui/transfer-list.tsx +11 -0
- package/src/components/ui/tree-select.tsx +5 -0
- package/src/components/ui/virtual-list.tsx +7 -0
|
@@ -28,6 +28,629 @@
|
|
|
28
28
|
"useKanban",
|
|
29
29
|
"useKanbanColumn"
|
|
30
30
|
],
|
|
31
|
+
"parts": [
|
|
32
|
+
{
|
|
33
|
+
"name": "Kanban",
|
|
34
|
+
"props": [
|
|
35
|
+
{
|
|
36
|
+
"name": "onCardMove",
|
|
37
|
+
"type": "((details: KanbanCardMoveDetails) => void)",
|
|
38
|
+
"required": false,
|
|
39
|
+
"description": "Called with `{ cardId, fromColumnId, toColumnId, fromIndex, toIndex }`; apply it with `moveCard`.",
|
|
40
|
+
"from": "arkcn"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "onColumnMove",
|
|
44
|
+
"type": "((details: KanbanColumnMoveDetails) => void)",
|
|
45
|
+
"required": false,
|
|
46
|
+
"description": "Called with `{ columnId, fromIndex, toIndex }`; apply it with `moveColumn`.",
|
|
47
|
+
"from": "arkcn"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "children",
|
|
51
|
+
"type": "ReactNode",
|
|
52
|
+
"required": false,
|
|
53
|
+
"description": "Content rendered inside the part.",
|
|
54
|
+
"from": "@types/react"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "className",
|
|
58
|
+
"type": "string",
|
|
59
|
+
"required": false,
|
|
60
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
61
|
+
"from": "@types/react"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "style",
|
|
65
|
+
"type": "CSSProperties",
|
|
66
|
+
"required": false,
|
|
67
|
+
"description": "Inline styles applied to the rendered element.",
|
|
68
|
+
"from": "@types/react"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "KanbanAddTrigger",
|
|
74
|
+
"props": [
|
|
75
|
+
{
|
|
76
|
+
"name": "asChild",
|
|
77
|
+
"type": "boolean",
|
|
78
|
+
"required": false,
|
|
79
|
+
"description": "Render the child element instead of the default one, merging props and behaviour onto it.",
|
|
80
|
+
"from": "arkcn"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "size",
|
|
84
|
+
"type": "\"default\" | \"xs\" | \"sm\" | \"lg\" | \"icon\" | \"icon-xs\" | \"icon-sm\" | \"icon-lg\" | null",
|
|
85
|
+
"required": false,
|
|
86
|
+
"default": "\"sm\"",
|
|
87
|
+
"description": "Size variant; sets a `data-size` attribute.",
|
|
88
|
+
"from": "arkcn"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "variant",
|
|
92
|
+
"type": "\"link\" | \"default\" | \"outline\" | \"secondary\" | \"ghost\" | \"destructive\" | null",
|
|
93
|
+
"required": false,
|
|
94
|
+
"default": "\"ghost\"",
|
|
95
|
+
"description": "Visual variant, styled with `cva`; sets a `data-variant` attribute.",
|
|
96
|
+
"from": "arkcn"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": "children",
|
|
100
|
+
"type": "ReactNode",
|
|
101
|
+
"required": false,
|
|
102
|
+
"description": "Content rendered inside the part.",
|
|
103
|
+
"from": "@types/react"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"name": "className",
|
|
107
|
+
"type": "string",
|
|
108
|
+
"required": false,
|
|
109
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
110
|
+
"from": "@types/react"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "style",
|
|
114
|
+
"type": "CSSProperties",
|
|
115
|
+
"required": false,
|
|
116
|
+
"description": "Inline styles applied to the rendered element.",
|
|
117
|
+
"from": "@types/react"
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"name": "KanbanBoard",
|
|
123
|
+
"props": [
|
|
124
|
+
{
|
|
125
|
+
"name": "children",
|
|
126
|
+
"type": "ReactNode",
|
|
127
|
+
"required": false,
|
|
128
|
+
"description": "Content rendered inside the part.",
|
|
129
|
+
"from": "@types/react"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"name": "className",
|
|
133
|
+
"type": "string",
|
|
134
|
+
"required": false,
|
|
135
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
136
|
+
"from": "@types/react"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"name": "style",
|
|
140
|
+
"type": "CSSProperties",
|
|
141
|
+
"required": false,
|
|
142
|
+
"description": "Inline styles applied to the rendered element.",
|
|
143
|
+
"from": "@types/react"
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"name": "KanbanCard",
|
|
149
|
+
"props": [
|
|
150
|
+
{
|
|
151
|
+
"name": "value",
|
|
152
|
+
"type": "string",
|
|
153
|
+
"required": true,
|
|
154
|
+
"description": "The controlled value.",
|
|
155
|
+
"from": "arkcn"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "children",
|
|
159
|
+
"type": "ReactNode",
|
|
160
|
+
"required": false,
|
|
161
|
+
"description": "Content rendered inside the part.",
|
|
162
|
+
"from": "@types/react"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"name": "className",
|
|
166
|
+
"type": "string",
|
|
167
|
+
"required": false,
|
|
168
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
169
|
+
"from": "@types/react"
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"name": "style",
|
|
173
|
+
"type": "CSSProperties",
|
|
174
|
+
"required": false,
|
|
175
|
+
"description": "Inline styles applied to the rendered element.",
|
|
176
|
+
"from": "@types/react"
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"name": "KanbanCardDescription",
|
|
182
|
+
"props": [
|
|
183
|
+
{
|
|
184
|
+
"name": "children",
|
|
185
|
+
"type": "ReactNode",
|
|
186
|
+
"required": false,
|
|
187
|
+
"description": "Content rendered inside the part.",
|
|
188
|
+
"from": "@types/react"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"name": "className",
|
|
192
|
+
"type": "string",
|
|
193
|
+
"required": false,
|
|
194
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
195
|
+
"from": "@types/react"
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"name": "style",
|
|
199
|
+
"type": "CSSProperties",
|
|
200
|
+
"required": false,
|
|
201
|
+
"description": "Inline styles applied to the rendered element.",
|
|
202
|
+
"from": "@types/react"
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"name": "KanbanCardFooter",
|
|
208
|
+
"props": [
|
|
209
|
+
{
|
|
210
|
+
"name": "children",
|
|
211
|
+
"type": "ReactNode",
|
|
212
|
+
"required": false,
|
|
213
|
+
"description": "Content rendered inside the part.",
|
|
214
|
+
"from": "@types/react"
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"name": "className",
|
|
218
|
+
"type": "string",
|
|
219
|
+
"required": false,
|
|
220
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
221
|
+
"from": "@types/react"
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"name": "style",
|
|
225
|
+
"type": "CSSProperties",
|
|
226
|
+
"required": false,
|
|
227
|
+
"description": "Inline styles applied to the rendered element.",
|
|
228
|
+
"from": "@types/react"
|
|
229
|
+
}
|
|
230
|
+
]
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"name": "KanbanCardHandle",
|
|
234
|
+
"props": [
|
|
235
|
+
{
|
|
236
|
+
"name": "asChild",
|
|
237
|
+
"type": "boolean",
|
|
238
|
+
"required": false,
|
|
239
|
+
"description": "Use the provided child element as the default rendered element, combining their props and behavior.",
|
|
240
|
+
"from": "@ark-ui/react"
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"name": "children",
|
|
244
|
+
"type": "ReactNode",
|
|
245
|
+
"required": false,
|
|
246
|
+
"description": "Content rendered inside the part.",
|
|
247
|
+
"from": "@types/react"
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"name": "className",
|
|
251
|
+
"type": "string",
|
|
252
|
+
"required": false,
|
|
253
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
254
|
+
"from": "@types/react"
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
"name": "style",
|
|
258
|
+
"type": "CSSProperties",
|
|
259
|
+
"required": false,
|
|
260
|
+
"description": "Inline styles applied to the rendered element.",
|
|
261
|
+
"from": "@types/react"
|
|
262
|
+
}
|
|
263
|
+
]
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
"name": "KanbanCardHeader",
|
|
267
|
+
"props": [
|
|
268
|
+
{
|
|
269
|
+
"name": "children",
|
|
270
|
+
"type": "ReactNode",
|
|
271
|
+
"required": false,
|
|
272
|
+
"description": "Content rendered inside the part.",
|
|
273
|
+
"from": "@types/react"
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"name": "className",
|
|
277
|
+
"type": "string",
|
|
278
|
+
"required": false,
|
|
279
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
280
|
+
"from": "@types/react"
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"name": "style",
|
|
284
|
+
"type": "CSSProperties",
|
|
285
|
+
"required": false,
|
|
286
|
+
"description": "Inline styles applied to the rendered element.",
|
|
287
|
+
"from": "@types/react"
|
|
288
|
+
}
|
|
289
|
+
]
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"name": "KanbanCardTitle",
|
|
293
|
+
"props": [
|
|
294
|
+
{
|
|
295
|
+
"name": "children",
|
|
296
|
+
"type": "ReactNode",
|
|
297
|
+
"required": false,
|
|
298
|
+
"description": "Content rendered inside the part.",
|
|
299
|
+
"from": "@types/react"
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"name": "className",
|
|
303
|
+
"type": "string",
|
|
304
|
+
"required": false,
|
|
305
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
306
|
+
"from": "@types/react"
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"name": "style",
|
|
310
|
+
"type": "CSSProperties",
|
|
311
|
+
"required": false,
|
|
312
|
+
"description": "Inline styles applied to the rendered element.",
|
|
313
|
+
"from": "@types/react"
|
|
314
|
+
}
|
|
315
|
+
]
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
"name": "KanbanColumn",
|
|
319
|
+
"props": [
|
|
320
|
+
{
|
|
321
|
+
"name": "value",
|
|
322
|
+
"type": "string",
|
|
323
|
+
"required": true,
|
|
324
|
+
"description": "The controlled value.",
|
|
325
|
+
"from": "arkcn"
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
"name": "children",
|
|
329
|
+
"type": "ReactNode",
|
|
330
|
+
"required": false,
|
|
331
|
+
"description": "Content rendered inside the part.",
|
|
332
|
+
"from": "@types/react"
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
"name": "className",
|
|
336
|
+
"type": "string",
|
|
337
|
+
"required": false,
|
|
338
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
339
|
+
"from": "@types/react"
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"name": "style",
|
|
343
|
+
"type": "CSSProperties",
|
|
344
|
+
"required": false,
|
|
345
|
+
"description": "Inline styles applied to the rendered element.",
|
|
346
|
+
"from": "@types/react"
|
|
347
|
+
}
|
|
348
|
+
]
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
"name": "KanbanColumnActions",
|
|
352
|
+
"props": [
|
|
353
|
+
{
|
|
354
|
+
"name": "children",
|
|
355
|
+
"type": "ReactNode",
|
|
356
|
+
"required": false,
|
|
357
|
+
"description": "Content rendered inside the part.",
|
|
358
|
+
"from": "@types/react"
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
"name": "className",
|
|
362
|
+
"type": "string",
|
|
363
|
+
"required": false,
|
|
364
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
365
|
+
"from": "@types/react"
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
"name": "style",
|
|
369
|
+
"type": "CSSProperties",
|
|
370
|
+
"required": false,
|
|
371
|
+
"description": "Inline styles applied to the rendered element.",
|
|
372
|
+
"from": "@types/react"
|
|
373
|
+
}
|
|
374
|
+
]
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
"name": "KanbanColumnContent",
|
|
378
|
+
"props": [
|
|
379
|
+
{
|
|
380
|
+
"name": "children",
|
|
381
|
+
"type": "ReactNode",
|
|
382
|
+
"required": false,
|
|
383
|
+
"description": "Content rendered inside the part.",
|
|
384
|
+
"from": "@types/react"
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
"name": "className",
|
|
388
|
+
"type": "string",
|
|
389
|
+
"required": false,
|
|
390
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
391
|
+
"from": "@types/react"
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
"name": "style",
|
|
395
|
+
"type": "CSSProperties",
|
|
396
|
+
"required": false,
|
|
397
|
+
"description": "Inline styles applied to the rendered element.",
|
|
398
|
+
"from": "@types/react"
|
|
399
|
+
}
|
|
400
|
+
]
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
"name": "KanbanColumnCount",
|
|
404
|
+
"props": [
|
|
405
|
+
{
|
|
406
|
+
"name": "asChild",
|
|
407
|
+
"type": "boolean",
|
|
408
|
+
"required": false,
|
|
409
|
+
"description": "Render the child element instead of the default one, merging props and behaviour onto it.",
|
|
410
|
+
"from": "arkcn"
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
"name": "variant",
|
|
414
|
+
"type": "\"link\" | \"default\" | \"outline\" | \"secondary\" | \"ghost\" | \"destructive\" | null",
|
|
415
|
+
"required": false,
|
|
416
|
+
"description": "Visual variant, styled with `cva`; sets a `data-variant` attribute.",
|
|
417
|
+
"from": "arkcn"
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
"name": "children",
|
|
421
|
+
"type": "ReactNode",
|
|
422
|
+
"required": false,
|
|
423
|
+
"description": "Content rendered inside the part.",
|
|
424
|
+
"from": "@types/react"
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
"name": "className",
|
|
428
|
+
"type": "string",
|
|
429
|
+
"required": false,
|
|
430
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
431
|
+
"from": "@types/react"
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
"name": "style",
|
|
435
|
+
"type": "CSSProperties",
|
|
436
|
+
"required": false,
|
|
437
|
+
"description": "Inline styles applied to the rendered element.",
|
|
438
|
+
"from": "@types/react"
|
|
439
|
+
}
|
|
440
|
+
]
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
"name": "KanbanColumnFooter",
|
|
444
|
+
"props": [
|
|
445
|
+
{
|
|
446
|
+
"name": "children",
|
|
447
|
+
"type": "ReactNode",
|
|
448
|
+
"required": false,
|
|
449
|
+
"description": "Content rendered inside the part.",
|
|
450
|
+
"from": "@types/react"
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
"name": "className",
|
|
454
|
+
"type": "string",
|
|
455
|
+
"required": false,
|
|
456
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
457
|
+
"from": "@types/react"
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
"name": "style",
|
|
461
|
+
"type": "CSSProperties",
|
|
462
|
+
"required": false,
|
|
463
|
+
"description": "Inline styles applied to the rendered element.",
|
|
464
|
+
"from": "@types/react"
|
|
465
|
+
}
|
|
466
|
+
]
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
"name": "KanbanColumnHandle",
|
|
470
|
+
"props": [
|
|
471
|
+
{
|
|
472
|
+
"name": "asChild",
|
|
473
|
+
"type": "boolean",
|
|
474
|
+
"required": false,
|
|
475
|
+
"description": "Use the provided child element as the default rendered element, combining their props and behavior.",
|
|
476
|
+
"from": "@ark-ui/react"
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
"name": "children",
|
|
480
|
+
"type": "ReactNode",
|
|
481
|
+
"required": false,
|
|
482
|
+
"description": "Content rendered inside the part.",
|
|
483
|
+
"from": "@types/react"
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
"name": "className",
|
|
487
|
+
"type": "string",
|
|
488
|
+
"required": false,
|
|
489
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
490
|
+
"from": "@types/react"
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
"name": "style",
|
|
494
|
+
"type": "CSSProperties",
|
|
495
|
+
"required": false,
|
|
496
|
+
"description": "Inline styles applied to the rendered element.",
|
|
497
|
+
"from": "@types/react"
|
|
498
|
+
}
|
|
499
|
+
]
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
"name": "KanbanColumnHeader",
|
|
503
|
+
"props": [
|
|
504
|
+
{
|
|
505
|
+
"name": "children",
|
|
506
|
+
"type": "ReactNode",
|
|
507
|
+
"required": false,
|
|
508
|
+
"description": "Content rendered inside the part.",
|
|
509
|
+
"from": "@types/react"
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
"name": "className",
|
|
513
|
+
"type": "string",
|
|
514
|
+
"required": false,
|
|
515
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
516
|
+
"from": "@types/react"
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
"name": "style",
|
|
520
|
+
"type": "CSSProperties",
|
|
521
|
+
"required": false,
|
|
522
|
+
"description": "Inline styles applied to the rendered element.",
|
|
523
|
+
"from": "@types/react"
|
|
524
|
+
}
|
|
525
|
+
]
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
"name": "KanbanColumnTitle",
|
|
529
|
+
"props": [
|
|
530
|
+
{
|
|
531
|
+
"name": "children",
|
|
532
|
+
"type": "ReactNode",
|
|
533
|
+
"required": false,
|
|
534
|
+
"description": "Content rendered inside the part.",
|
|
535
|
+
"from": "@types/react"
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
"name": "className",
|
|
539
|
+
"type": "string",
|
|
540
|
+
"required": false,
|
|
541
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
542
|
+
"from": "@types/react"
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
"name": "style",
|
|
546
|
+
"type": "CSSProperties",
|
|
547
|
+
"required": false,
|
|
548
|
+
"description": "Inline styles applied to the rendered element.",
|
|
549
|
+
"from": "@types/react"
|
|
550
|
+
}
|
|
551
|
+
]
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
"name": "KanbanDropIndicator",
|
|
555
|
+
"props": [
|
|
556
|
+
{
|
|
557
|
+
"name": "edge",
|
|
558
|
+
"type": "Edge",
|
|
559
|
+
"required": true,
|
|
560
|
+
"description": "Which edge the indicator marks.",
|
|
561
|
+
"from": "arkcn"
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
"name": "gap",
|
|
565
|
+
"type": "string",
|
|
566
|
+
"required": false,
|
|
567
|
+
"default": "\"0.5rem\"",
|
|
568
|
+
"description": "Gap between the indicator and the edge.",
|
|
569
|
+
"from": "arkcn"
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
"name": "children",
|
|
573
|
+
"type": "ReactNode",
|
|
574
|
+
"required": false,
|
|
575
|
+
"description": "Content rendered inside the part.",
|
|
576
|
+
"from": "@types/react"
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
"name": "className",
|
|
580
|
+
"type": "string",
|
|
581
|
+
"required": false,
|
|
582
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
583
|
+
"from": "@types/react"
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
"name": "style",
|
|
587
|
+
"type": "CSSProperties",
|
|
588
|
+
"required": false,
|
|
589
|
+
"description": "Inline styles applied to the rendered element.",
|
|
590
|
+
"from": "@types/react"
|
|
591
|
+
}
|
|
592
|
+
]
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
"name": "KanbanDropSlot",
|
|
596
|
+
"props": [
|
|
597
|
+
{
|
|
598
|
+
"name": "height",
|
|
599
|
+
"type": "number",
|
|
600
|
+
"required": false,
|
|
601
|
+
"description": "Height of the drop slot, normally the dragged card's height.",
|
|
602
|
+
"from": "arkcn"
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
"name": "children",
|
|
606
|
+
"type": "ReactNode",
|
|
607
|
+
"required": false,
|
|
608
|
+
"description": "Content rendered inside the part.",
|
|
609
|
+
"from": "@types/react"
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
"name": "className",
|
|
613
|
+
"type": "string",
|
|
614
|
+
"required": false,
|
|
615
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
616
|
+
"from": "@types/react"
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
"name": "style",
|
|
620
|
+
"type": "CSSProperties",
|
|
621
|
+
"required": false,
|
|
622
|
+
"description": "Inline styles applied to the rendered element.",
|
|
623
|
+
"from": "@types/react"
|
|
624
|
+
}
|
|
625
|
+
]
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
"name": "KanbanEmpty",
|
|
629
|
+
"props": [
|
|
630
|
+
{
|
|
631
|
+
"name": "children",
|
|
632
|
+
"type": "ReactNode",
|
|
633
|
+
"required": false,
|
|
634
|
+
"description": "Content rendered inside the part.",
|
|
635
|
+
"from": "@types/react"
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
"name": "className",
|
|
639
|
+
"type": "string",
|
|
640
|
+
"required": false,
|
|
641
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
642
|
+
"from": "@types/react"
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
"name": "style",
|
|
646
|
+
"type": "CSSProperties",
|
|
647
|
+
"required": false,
|
|
648
|
+
"description": "Inline styles applied to the rendered element.",
|
|
649
|
+
"from": "@types/react"
|
|
650
|
+
}
|
|
651
|
+
]
|
|
652
|
+
}
|
|
653
|
+
],
|
|
31
654
|
"dependencies": {
|
|
32
655
|
"@ark-ui/react": "^5.39.1",
|
|
33
656
|
"@atlaskit/pragmatic-drag-and-drop": "^3.1.0",
|
|
@@ -45,7 +668,7 @@
|
|
|
45
668
|
"files": [
|
|
46
669
|
{
|
|
47
670
|
"path": "components/ui/kanban.tsx",
|
|
48
|
-
"content": "import * as React from \"react\"\nimport { combine } from \"@atlaskit/pragmatic-drag-and-drop/combine\"\nimport { draggable, dropTargetForElements, monitorForElements } from \"@atlaskit/pragmatic-drag-and-drop/element/adapter\"\nimport { reorder } from \"@atlaskit/pragmatic-drag-and-drop/reorder\"\nimport { autoScrollForElements } from \"@atlaskit/pragmatic-drag-and-drop-auto-scroll/element\"\nimport { attachClosestEdge, extractClosestEdge, type Edge } from \"@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge\"\nimport { getReorderDestinationIndex } from \"@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index\"\nimport { ark } from \"@ark-ui/react\"\nimport { GripVerticalIcon } from \"lucide-react\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport { cloneDragPreview } from \"@/lib/drag-preview\"\nimport { cn } from \"@/lib/utils\"\nimport { LiveRegion, useLiveRegion } from \"@/components/ui/live-region\"\n\n/**\n * Kanban — a compositional, data-agnostic board on Pragmatic drag and drop.\n * The consumer owns columns and cards and re-renders them from its own state;\n * the primitive wires dragging and reports intents through `onCardMove` and\n * `onColumnMove`. `moveCard`/`moveColumn` are optional helpers for the common\n * `{ id, cards: { id }[] }[]` shape.\n */\n\ntype KanbanCardMoveDetails = {\n cardId: string\n fromColumnId: string\n toColumnId: string\n fromIndex: number\n toIndex: number\n}\n\ntype KanbanColumnMoveDetails = {\n columnId: string\n fromIndex: number\n toIndex: number\n}\n\ntype DragData =\n | { instanceId: symbol; type: \"card\"; cardId: string; columnId: string }\n | { instanceId: symbol; type: \"column\"; columnId: string }\n\nconst CARD_TARGET = \"kanban-card-target\"\nconst COLUMN_TARGET = \"kanban-column-target\"\n\ntype KanbanDragging = { type: \"card\" | \"column\"; id: string; columnId?: string; height: number }\n/** Where a dragged card would land: before `beforeCardId`, or at the end when null. */\ntype KanbanPreview = { columnId: string; beforeCardId: string | null }\n\ntype KanbanGrabbed = { type: \"card\" | \"column\"; id: string }\n\ntype KanbanContextValue = {\n instanceId: symbol\n dragging: KanbanDragging | null\n preview: KanbanPreview | null\n /** Keyboard \"pick up\" state; arrows move the grabbed card/column. */\n grabbed: KanbanGrabbed | null\n grab: (item: KanbanGrabbed | null) => void\n moveByKey: (item: KanbanGrabbed, direction: \"up\" | \"down\" | \"left\" | \"right\") => void\n}\n\nconst focusLater = (selector: string) =>\n requestAnimationFrame(() => requestAnimationFrame(() => document.querySelector<HTMLElement>(selector)?.focus()))\n\nconst cssEscape = (value: string) => (typeof CSS !== \"undefined\" && CSS.escape ? CSS.escape(value) : value)\n\nconst KanbanContext = React.createContext<KanbanContextValue | null>(null)\n\nfunction useKanban() {\n const ctx = React.useContext(KanbanContext)\n if (!ctx) throw new Error(\"Kanban parts must be used within <Kanban>\")\n return ctx\n}\n\nconst isOwnData = (instanceId: symbol, data: Record<string | symbol, unknown>): data is DragData =>\n data.instanceId === instanceId && (data.type === \"card\" || data.type === \"column\")\n\nfunction cardsOf(columnEl: Element) {\n return Array.from(columnEl.querySelectorAll<HTMLElement>(\":scope [data-slot=kanban-card]\"))\n}\n\ntype DropTargetLike = { data: Record<string | symbol, unknown> }\n\n/** Resolve a card drag against the current drop targets into a concrete destination. */\nfunction resolveCardDrop(root: HTMLElement, data: Extract<DragData, { type: \"card\" }>, targets: DropTargetLike[]) {\n const columnTarget = targets.find((t) => t.data[COLUMN_TARGET] === true)\n if (!columnTarget) return null\n const toColumnId = String(columnTarget.data.columnId)\n const columnEls = Array.from(root.querySelectorAll<HTMLElement>(\"[data-slot=kanban-column]\"))\n const fromColumnEl = columnEls.find((el) => el.dataset.value === data.columnId)\n const toColumnEl = columnEls.find((el) => el.dataset.value === toColumnId)\n if (!fromColumnEl || !toColumnEl) return null\n const fromIndex = cardsOf(fromColumnEl).findIndex((el) => el.dataset.value === data.cardId)\n const toCards = cardsOf(toColumnEl)\n const cardTarget = targets.find((t) => t.data[CARD_TARGET] === true)\n const sameColumn = data.columnId === toColumnId\n\n let toIndex: number\n if (cardTarget && String(cardTarget.data.cardId) !== data.cardId) {\n const indexOfTarget = toCards.findIndex((el) => el.dataset.value === String(cardTarget.data.cardId))\n const edge = extractClosestEdge(cardTarget.data)\n toIndex = sameColumn\n ? getReorderDestinationIndex({\n startIndex: fromIndex,\n indexOfTarget,\n closestEdgeOfTarget: edge,\n axis: \"vertical\",\n })\n : edge === \"bottom\"\n ? indexOfTarget + 1\n : indexOfTarget\n } else if (cardTarget) {\n // Hovering the dragged card itself: no move.\n toIndex = fromIndex\n } else {\n toIndex = sameColumn ? toCards.length - 1 : toCards.length\n }\n const noop = sameColumn && toIndex === fromIndex\n const remaining = toCards.filter((el) => el.dataset.value !== data.cardId)\n const beforeCardId = remaining[toIndex]?.dataset.value ?? null\n return { toColumnId, toIndex, fromIndex, sameColumn, noop, beforeCardId }\n}\n\nfunction Kanban({\n onCardMove,\n onColumnMove,\n className,\n children,\n ...props\n}: React.ComponentProps<\"div\"> & {\n onCardMove?: (details: KanbanCardMoveDetails) => void\n onColumnMove?: (details: KanbanColumnMoveDetails) => void\n}) {\n const [instanceId] = React.useState(() => Symbol(\"kanban\"))\n const [dragging, setDragging] = React.useState<KanbanDragging | null>(null)\n const [preview, setPreviewState] = React.useState<KanbanPreview | null>(null)\n const [grabbed, setGrabbed] = React.useState<KanbanGrabbed | null>(null)\n const { message: announcement, announce: setAnnouncement } = useLiveRegion()\n const previewKey = React.useRef<string | null>(null)\n const ref = React.useRef<HTMLDivElement>(null)\n const callbacks = React.useRef({ onCardMove, onColumnMove })\n callbacks.current = { onCardMove, onColumnMove }\n\n const grab = React.useCallback(\n (item: KanbanGrabbed | null) => {\n setGrabbed(item)\n setAnnouncement(\n item\n ? `Picked up ${item.type} ${item.id}. Use the arrow keys to move, Space or Enter to drop, Escape to cancel.`\n : \"Dropped.\"\n )\n },\n [setAnnouncement]\n )\n\n const moveByKey = React.useCallback(\n (item: KanbanGrabbed, direction: \"up\" | \"down\" | \"left\" | \"right\") => {\n const root = ref.current\n if (!root) return\n const columnEls = Array.from(root.querySelectorAll<HTMLElement>(\"[data-slot=kanban-column]\"))\n const columnIds = columnEls.map((el) => el.dataset.value ?? \"\")\n const columnName = (el: HTMLElement) =>\n el.querySelector(\"[data-slot=kanban-column-title]\")?.textContent ?? el.dataset.value\n\n if (item.type === \"column\") {\n const fromIndex = columnIds.indexOf(item.id)\n const toIndex = direction === \"left\" ? fromIndex - 1 : direction === \"right\" ? fromIndex + 1 : fromIndex\n if (fromIndex < 0 || toIndex === fromIndex || toIndex < 0 || toIndex >= columnIds.length) return\n callbacks.current.onColumnMove?.({ columnId: item.id, fromIndex, toIndex })\n setAnnouncement(`Moved column to position ${toIndex + 1} of ${columnIds.length}.`)\n focusLater(`[data-slot=kanban-column][data-value=\"${cssEscape(item.id)}\"] [data-slot=kanban-column-handle]`)\n return\n }\n\n const fromColumnEl = columnEls.find((el) => cardsOf(el).some((c) => c.dataset.value === item.id))\n if (!fromColumnEl) return\n const fromColumnId = fromColumnEl.dataset.value ?? \"\"\n const fromCards = cardsOf(fromColumnEl)\n const fromIndex = fromCards.findIndex((c) => c.dataset.value === item.id)\n let toColumnId = fromColumnId\n let toIndex = fromIndex\n if (direction === \"up\" || direction === \"down\") {\n toIndex = direction === \"up\" ? fromIndex - 1 : fromIndex + 1\n if (toIndex < 0 || toIndex >= fromCards.length) return\n } else {\n const ci = columnIds.indexOf(fromColumnId)\n const targetEl = columnEls[direction === \"left\" ? ci - 1 : ci + 1]\n if (!targetEl) return\n toColumnId = targetEl.dataset.value ?? \"\"\n toIndex = Math.min(fromIndex, cardsOf(targetEl).length)\n }\n callbacks.current.onCardMove?.({ cardId: item.id, fromColumnId, toColumnId, fromIndex, toIndex })\n const targetEl = columnEls[columnIds.indexOf(toColumnId)]\n const count = toColumnId === fromColumnId ? fromCards.length : cardsOf(targetEl).length + 1\n setAnnouncement(`Moved to ${columnName(targetEl)}, position ${toIndex + 1} of ${count}.`)\n focusLater(`[data-slot=kanban-card][data-value=\"${cssEscape(item.id)}\"]`)\n },\n [setAnnouncement]\n )\n\n React.useEffect(() => {\n const root = ref.current\n if (!root) return\n const setPreview = (next: KanbanPreview | null) => {\n const key = next ? `${next.columnId}::${next.beforeCardId ?? \"\"}` : null\n if (key === previewKey.current) return\n previewKey.current = key\n setPreviewState(next)\n }\n const updatePreview = (data: DragData, targets: DropTargetLike[]) => {\n if (data.type !== \"card\") return\n const resolved = resolveCardDrop(root, data, targets)\n setPreview(\n resolved && !resolved.noop ? { columnId: resolved.toColumnId, beforeCardId: resolved.beforeCardId } : null\n )\n }\n return monitorForElements({\n canMonitor: ({ source }) => isOwnData(instanceId, source.data),\n onDragStart: ({ source }) => {\n const data = source.data as DragData\n setDragging({\n type: data.type,\n id: data.type === \"card\" ? data.cardId : data.columnId,\n columnId: data.type === \"card\" ? data.columnId : undefined,\n height: source.element.getBoundingClientRect().height,\n })\n },\n onDrag: ({ source, location }) => updatePreview(source.data as DragData, location.current.dropTargets),\n onDropTargetChange: ({ source, location }) =>\n updatePreview(source.data as DragData, location.current.dropTargets),\n onDrop: ({ source, location }) => {\n setDragging(null)\n setPreview(null)\n const data = source.data as DragData\n const targets = location.current.dropTargets\n const columnEls = Array.from(root.querySelectorAll<HTMLElement>(\"[data-slot=kanban-column]\"))\n const columnIds = columnEls.map((el) => el.dataset.value ?? \"\")\n\n if (data.type === \"column\") {\n const target = targets.find((t) => t.data[COLUMN_TARGET] === true)\n if (!target) return\n const targetId = String(target.data.columnId)\n const fromIndex = columnIds.indexOf(data.columnId)\n const indexOfTarget = columnIds.indexOf(targetId)\n if (fromIndex < 0 || indexOfTarget < 0) return\n const toIndex = getReorderDestinationIndex({\n startIndex: fromIndex,\n indexOfTarget,\n closestEdgeOfTarget: extractClosestEdge(target.data),\n axis: \"horizontal\",\n })\n if (toIndex === fromIndex) return\n callbacks.current.onColumnMove?.({ columnId: data.columnId, fromIndex, toIndex })\n return\n }\n\n const resolved = resolveCardDrop(root, data, targets)\n if (!resolved || resolved.noop) return\n callbacks.current.onCardMove?.({\n cardId: data.cardId,\n fromColumnId: data.columnId,\n toColumnId: resolved.toColumnId,\n fromIndex: resolved.fromIndex,\n toIndex: resolved.toIndex,\n })\n },\n })\n }, [instanceId])\n\n const ctx = React.useMemo(\n () => ({ instanceId, dragging, preview, grabbed, grab, moveByKey }),\n [instanceId, dragging, preview, grabbed, grab, moveByKey]\n )\n return (\n <KanbanContext.Provider value={ctx}>\n <div\n ref={ref}\n data-slot=\"kanban\"\n data-dragging={dragging ? dragging.type : undefined}\n data-grabbed={grabbed ? grabbed.type : undefined}\n className={cn(\"flex min-h-0 flex-1 flex-col\", className)}\n {...props}\n >\n {children}\n <LiveRegion data-slot=\"kanban-live-region\" message={announcement} />\n </div>\n </KanbanContext.Provider>\n )\n}\n\n/** Keyboard handling shared by cards and column handles. */\nfunction useKanbanKeyboard(item: KanbanGrabbed) {\n const { grabbed, grab, moveByKey } = useKanban()\n const isGrabbed = grabbed?.type === item.type && grabbed.id === item.id\n const onKeyDown = (event: React.KeyboardEvent) => {\n const key = event.key\n if (key === \" \" || key === \"Enter\") {\n event.preventDefault()\n grab(isGrabbed ? null : item)\n return\n }\n if (!isGrabbed) return\n if (key === \"Escape\") {\n event.preventDefault()\n grab(null)\n return\n }\n const direction = { ArrowUp: \"up\", ArrowDown: \"down\", ArrowLeft: \"left\", ArrowRight: \"right\" }[key] as\n \"up\" | \"down\" | \"left\" | \"right\" | undefined\n if (!direction) return\n event.preventDefault()\n moveByKey(item, direction)\n }\n return { isGrabbed, onKeyDown, onBlur: () => isGrabbed && grab(null) }\n}\n\n/* -------------------------------------------------------------------------- */\n/* Board */\n/* -------------------------------------------------------------------------- */\n\nfunction KanbanBoard({ className, ...props }: React.ComponentProps<\"div\">) {\n const { instanceId } = useKanban()\n const ref = React.useRef<HTMLDivElement>(null)\n React.useEffect(() => {\n const el = ref.current\n if (!el) return\n return autoScrollForElements({\n element: el,\n canScroll: ({ source }) => isOwnData(instanceId, source.data),\n })\n }, [instanceId])\n return (\n <div\n ref={ref}\n data-slot=\"kanban-board\"\n className={cn(\"flex min-h-0 flex-1 items-start gap-4 overflow-x-auto p-1 pb-4\", className)}\n {...props}\n />\n )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Column */\n/* -------------------------------------------------------------------------- */\n\ntype ColumnContextValue = {\n value: string\n handleRef: React.RefObject<HTMLElement | null>\n headerRef: React.RefObject<HTMLElement | null>\n isOver: boolean\n isDragging: boolean\n}\n\nconst ColumnContext = React.createContext<ColumnContextValue | null>(null)\n\nfunction useKanbanColumn() {\n const ctx = React.useContext(ColumnContext)\n if (!ctx) throw new Error(\"Kanban column parts must be used within <KanbanColumn>\")\n return ctx\n}\n\nfunction KanbanColumn({\n value,\n draggable: isDraggable = true,\n className,\n children,\n ...props\n}: React.ComponentProps<\"section\"> & {\n value: string\n /** Allow the column itself to be reordered (drag by its handle or header). */\n draggable?: boolean\n}) {\n const { instanceId, dragging, preview } = useKanban()\n const ref = React.useRef<HTMLElement>(null)\n const handleRef = React.useRef<HTMLElement | null>(null)\n const headerRef = React.useRef<HTMLElement | null>(null)\n const [isOver, setIsOver] = React.useState(false)\n const [isDragging, setIsDragging] = React.useState(false)\n const [closestEdge, setClosestEdge] = React.useState<Edge | null>(null)\n\n React.useEffect(() => {\n const el = ref.current\n if (!el) return\n const cleanups = [\n dropTargetForElements({\n element: el,\n canDrop: ({ source }) => isOwnData(instanceId, source.data),\n getIsSticky: () => true,\n getData: ({ source, input, element }) => {\n const base = { [COLUMN_TARGET]: true, columnId: value }\n if ((source.data as DragData).type === \"column\") {\n return attachClosestEdge(base, { element, input, allowedEdges: [\"left\", \"right\"] })\n }\n return base\n },\n onDragEnter: ({ source, self }) => {\n const data = source.data as DragData\n if (data.type === \"card\") setIsOver(true)\n else if (data.columnId !== value) setClosestEdge(extractClosestEdge(self.data))\n },\n onDrag: ({ source, self }) => {\n const data = source.data as DragData\n if (data.type === \"column\" && data.columnId !== value) {\n const edge = extractClosestEdge(self.data)\n setClosestEdge((prev) => (prev === edge ? prev : edge))\n }\n },\n onDragLeave: () => {\n setIsOver(false)\n setClosestEdge(null)\n },\n onDrop: () => {\n setIsOver(false)\n setClosestEdge(null)\n },\n }),\n ]\n if (isDraggable) {\n cleanups.push(\n draggable({\n element: el,\n dragHandle: handleRef.current ?? headerRef.current ?? undefined,\n getInitialData: (): DragData => ({ instanceId, type: \"column\", columnId: value }),\n onGenerateDragPreview: (args) => cloneDragPreview(args),\n onDragStart: () => setIsDragging(true),\n onDrop: () => setIsDragging(false),\n })\n )\n }\n return combine(...cleanups)\n }, [instanceId, value, isDraggable])\n\n const ctx = React.useMemo(() => ({ value, handleRef, headerRef, isOver, isDragging }), [value, isOver, isDragging])\n return (\n <ColumnContext.Provider value={ctx}>\n <section\n ref={ref}\n data-slot=\"kanban-column\"\n data-value={value}\n data-over={isOver && dragging?.type === \"card\" ? \"\" : undefined}\n data-target={preview?.columnId === value ? \"\" : undefined}\n data-dragging={isDragging ? \"\" : undefined}\n data-edge={closestEdge ?? undefined}\n className={cn(\n \"group/kanban-column relative flex max-h-full w-72 shrink-0 flex-col rounded-xl border bg-muted/40 transition-colors data-dragging:opacity-40 data-target:border-primary/30\",\n className\n )}\n {...props}\n >\n {children}\n {closestEdge && <KanbanDropIndicator edge={closestEdge} />}\n </section>\n </ColumnContext.Provider>\n )\n}\n\nfunction KanbanColumnHeader({ className, ...props }: React.ComponentProps<\"header\">) {\n const { headerRef } = useKanbanColumn()\n return (\n <header\n ref={headerRef as React.RefObject<HTMLElement>}\n data-slot=\"kanban-column-header\"\n className={cn(\"flex items-center gap-2 px-3 py-2.5\", className)}\n {...props}\n />\n )\n}\n\nfunction KanbanColumnHandle({\n className,\n children,\n onKeyDown,\n onBlur,\n asChild,\n ...props\n}: React.ComponentProps<typeof ark.button>) {\n const { handleRef, value } = useKanbanColumn()\n const keyboard = useKanbanKeyboard({ type: \"column\", id: value })\n return (\n <ark.button\n asChild={asChild}\n ref={handleRef as React.RefObject<HTMLButtonElement>}\n type=\"button\"\n data-slot=\"kanban-column-handle\"\n data-grabbed={keyboard.isGrabbed ? \"\" : undefined}\n aria-label=\"Drag column\"\n aria-pressed={keyboard.isGrabbed}\n aria-describedby={undefined}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (!event.defaultPrevented) keyboard.onKeyDown(event)\n }}\n onBlur={(event) => {\n onBlur?.(event)\n keyboard.onBlur()\n }}\n className={cn(\n \"-ms-1 flex size-6 cursor-grab items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50 active:cursor-grabbing data-grabbed:bg-primary data-grabbed:text-primary-foreground [&_svg]:size-4\",\n className\n )}\n {...props}\n >\n {asChild ? children : (children ?? <GripVerticalIcon />)}\n </ark.button>\n )\n}\n\nfunction KanbanColumnTitle({ className, ...props }: React.ComponentProps<\"h3\">) {\n return <h3 data-slot=\"kanban-column-title\" className={cn(\"truncate text-sm font-semibold\", className)} {...props} />\n}\n\nfunction KanbanColumnCount({ className, ...props }: React.ComponentProps<typeof Badge>) {\n return (\n <Badge\n data-slot=\"kanban-column-count\"\n variant=\"secondary\"\n className={cn(\"h-5 min-w-5 justify-center px-1.5 tabular-nums\", className)}\n {...props}\n />\n )\n}\n\nfunction KanbanColumnActions({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div data-slot=\"kanban-column-actions\" className={cn(\"ms-auto flex items-center gap-1\", className)} {...props} />\n )\n}\n\n/** The scrolling list of cards. Renders the landing slot at the end when a card would be appended. */\nfunction KanbanColumnContent({ className, children, ...props }: React.ComponentProps<\"div\">) {\n const { instanceId, dragging, preview } = useKanban()\n const column = useKanbanColumn()\n const ref = React.useRef<HTMLDivElement>(null)\n React.useEffect(() => {\n const el = ref.current\n if (!el) return\n return autoScrollForElements({\n element: el,\n canScroll: ({ source }) => isOwnData(instanceId, source.data),\n })\n }, [instanceId])\n return (\n <div\n ref={ref}\n data-slot=\"kanban-column-content\"\n className={cn(\n \"no-scrollbar flex min-h-16 flex-1 flex-col gap-(--kanban-gap) overflow-y-auto px-2 pb-2 [--kanban-gap:0.5rem]\",\n className\n )}\n {...props}\n >\n {children}\n {dragging?.type === \"card\" && preview?.columnId === column.value && preview.beforeCardId === null && (\n <KanbanDropSlot height={dragging.height} />\n )}\n </div>\n )\n}\n\nfunction KanbanColumnFooter({ className, ...props }: React.ComponentProps<\"footer\">) {\n return <footer data-slot=\"kanban-column-footer\" className={cn(\"flex items-center px-2 pb-2\", className)} {...props} />\n}\n\n/** A ghost-style full-width trigger for the footer, e.g. \"Add card\". Polymorphic via `asChild`. */\nfunction KanbanAddTrigger({\n className,\n variant = \"ghost\",\n size = \"sm\",\n ...props\n}: React.ComponentProps<typeof Button>) {\n return (\n <Button\n data-slot=\"kanban-add-trigger\"\n variant={variant}\n size={size}\n className={cn(\"w-full justify-start text-muted-foreground hover:text-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction KanbanEmpty({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"kanban-empty\"\n className={cn(\n \"flex flex-1 items-center justify-center rounded-lg border border-dashed px-3 py-6 text-center text-xs text-muted-foreground\",\n className\n )}\n {...props}\n />\n )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Card */\n/* -------------------------------------------------------------------------- */\n\ntype CardContextValue = { handleRef: React.RefObject<HTMLElement | null>; value: string }\nconst CardContext = React.createContext<CardContextValue | null>(null)\n\nfunction KanbanCard({\n value,\n draggable: isDraggable = true,\n className,\n children,\n onKeyDown,\n onBlur,\n ...props\n}: React.ComponentProps<\"article\"> & { value: string; draggable?: boolean }) {\n const { instanceId, dragging, preview } = useKanban()\n const column = useKanbanColumn()\n const keyboard = useKanbanKeyboard({ type: \"card\", id: value })\n const ref = React.useRef<HTMLElement>(null)\n const handleRef = React.useRef<HTMLElement | null>(null)\n const [isDragging, setIsDragging] = React.useState(false)\n const isSlotBefore = dragging?.type === \"card\" && preview?.columnId === column.value && preview.beforeCardId === value\n // The dragged card stays as an outlined placeholder until a landing spot exists, then collapses.\n const isCollapsed = isDragging && preview !== null\n\n React.useEffect(() => {\n const el = ref.current\n if (!el) return\n const cleanups = [\n dropTargetForElements({\n element: el,\n canDrop: ({ source }) => isOwnData(instanceId, source.data) && (source.data as DragData).type === \"card\",\n getIsSticky: () => true,\n getData: ({ input, element }) =>\n attachClosestEdge(\n { [CARD_TARGET]: true, cardId: value, columnId: column.value },\n {\n element,\n input,\n allowedEdges: [\"top\", \"bottom\"],\n }\n ),\n }),\n ]\n if (isDraggable) {\n cleanups.push(\n draggable({\n element: el,\n dragHandle: handleRef.current ?? undefined,\n getInitialData: (): DragData => ({ instanceId, type: \"card\", cardId: value, columnId: column.value }),\n onGenerateDragPreview: (args) => cloneDragPreview(args),\n onDragStart: () => setIsDragging(true),\n onDrop: () => setIsDragging(false),\n })\n )\n }\n return combine(...cleanups)\n }, [instanceId, value, column.value, isDraggable])\n\n const ctx = React.useMemo(() => ({ handleRef, value }), [value])\n return (\n <CardContext.Provider value={ctx}>\n {isSlotBefore && dragging && <KanbanDropSlot height={dragging.height} />}\n <article\n ref={ref}\n data-slot=\"kanban-card\"\n data-value={value}\n data-dragging={isDragging ? \"\" : undefined}\n data-collapsed={isCollapsed ? \"\" : undefined}\n data-grabbed={keyboard.isGrabbed ? \"\" : undefined}\n tabIndex={isDraggable ? 0 : undefined}\n aria-roledescription={isDraggable ? \"draggable card\" : undefined}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (!event.defaultPrevented && event.target === event.currentTarget) keyboard.onKeyDown(event)\n }}\n onBlur={(event) => {\n onBlur?.(event)\n if (event.target === event.currentTarget) keyboard.onBlur()\n }}\n className={cn(\n \"group/kanban-card relative flex cursor-grab flex-col gap-2 rounded-lg border bg-card p-3 text-sm text-card-foreground shadow-xs transition-shadow outline-none hover:shadow-sm focus-visible:ring-3 focus-visible:ring-ring/50 active:cursor-grabbing data-grabbed:border-primary data-grabbed:ring-3 data-grabbed:ring-primary/30\",\n \"data-dragging:border-dashed data-dragging:border-primary/40 data-dragging:bg-primary/5 data-dragging:shadow-none data-dragging:*:invisible\",\n // Collapse (not `display:none`): removing the dragged element from layout cancels a native drag in Chrome.\n \"data-collapsed:pointer-events-none data-collapsed:-mb-(--kanban-gap) data-collapsed:h-0 data-collapsed:min-h-0 data-collapsed:overflow-hidden data-collapsed:border-0 data-collapsed:p-0 data-collapsed:opacity-0\",\n className\n )}\n {...props}\n >\n {children}\n </article>\n </CardContext.Provider>\n )\n}\n\n/** Optional explicit drag handle; without it the whole card is the handle. */\nfunction KanbanCardHandle({\n className,\n children,\n onKeyDown,\n onBlur,\n asChild,\n ...props\n}: React.ComponentProps<typeof ark.button>) {\n const ctx = React.useContext(CardContext)\n const keyboard = useKanbanKeyboard({ type: \"card\", id: ctx?.value ?? \"\" })\n return (\n <ark.button\n asChild={asChild}\n ref={(el: HTMLElement | null) => {\n if (ctx) ctx.handleRef.current = el\n }}\n type=\"button\"\n data-slot=\"kanban-card-handle\"\n data-grabbed={keyboard.isGrabbed ? \"\" : undefined}\n aria-label=\"Drag card\"\n aria-pressed={keyboard.isGrabbed}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (!event.defaultPrevented) {\n keyboard.onKeyDown(event)\n event.stopPropagation()\n }\n }}\n onBlur={(event) => {\n onBlur?.(event)\n keyboard.onBlur()\n }}\n className={cn(\n \"flex size-6 cursor-grab items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50 active:cursor-grabbing data-grabbed:bg-primary data-grabbed:text-primary-foreground [&_svg]:size-4\",\n className\n )}\n {...props}\n >\n {asChild ? children : (children ?? <GripVerticalIcon />)}\n </ark.button>\n )\n}\n\nfunction KanbanCardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"kanban-card-header\"\n className={cn(\"flex items-start justify-between gap-2\", className)}\n {...props}\n />\n )\n}\n\nfunction KanbanCardTitle({ className, ...props }: React.ComponentProps<\"p\">) {\n return <p data-slot=\"kanban-card-title\" className={cn(\"leading-snug font-medium\", className)} {...props} />\n}\n\nfunction KanbanCardDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return (\n <p\n data-slot=\"kanban-card-description\"\n className={cn(\"line-clamp-3 text-xs text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction KanbanCardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"kanban-card-footer\"\n className={cn(\"flex items-center gap-2 text-xs text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Drop slot + indicator */\n/* -------------------------------------------------------------------------- */\n\n/** An outlined placeholder the size of the dragged card, marking where it will land. */\nfunction KanbanDropSlot({ height, className, style, ...props }: React.ComponentProps<\"div\"> & { height?: number }) {\n return (\n <div\n data-slot=\"kanban-drop-slot\"\n aria-hidden\n style={{ height, ...style }}\n className={cn(\n \"shrink-0 rounded-lg border-2 border-dashed border-primary/40 bg-primary/5\",\n height === undefined && \"min-h-16\",\n className\n )}\n {...props}\n />\n )\n}\n\n/** A line on the given edge of its (relative) parent, with a terminal dot. */\nfunction KanbanDropIndicator({\n edge,\n gap = \"0.5rem\",\n className,\n ...props\n}: React.ComponentProps<\"div\"> & { edge: Edge; gap?: string }) {\n const horizontal = edge === \"top\" || edge === \"bottom\"\n return (\n <div\n data-slot=\"kanban-drop-indicator\"\n data-edge={edge}\n aria-hidden\n style={{ [\"--kanban-gap\" as string]: gap }}\n className={cn(\n \"pointer-events-none absolute z-10 bg-primary\",\n \"before:absolute before:size-2 before:rounded-full before:border-2 before:border-primary before:bg-background\",\n horizontal\n ? \"inset-x-0 h-0.5 before:top-1/2 before:-left-1 before:-translate-y-1/2\"\n : \"inset-y-0 w-0.5 before:-top-1 before:left-1/2 before:-translate-x-1/2\",\n edge === \"top\" && \"top-[calc(-1*var(--kanban-gap)/2-1px)]\",\n edge === \"bottom\" && \"bottom-[calc(-1*var(--kanban-gap)/2-1px)]\",\n edge === \"left\" && \"left-[calc(-1*var(--kanban-gap)/2-1px)]\",\n edge === \"right\" && \"right-[calc(-1*var(--kanban-gap)/2-1px)]\",\n className\n )}\n {...props}\n />\n )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Helpers */\n/* -------------------------------------------------------------------------- */\n\ntype KanbanColumnLike<TCard extends { id: string }> = { id: string; cards: TCard[] }\n\n/** Apply an `onCardMove` intent to a `{ id, cards }[]` state shape. */\nfunction moveCard<TCard extends { id: string }, TColumn extends KanbanColumnLike<TCard>>(\n columns: TColumn[],\n { cardId, fromColumnId, toColumnId, toIndex }: KanbanCardMoveDetails\n): TColumn[] {\n const from = columns.find((c) => c.id === fromColumnId)\n const card = from?.cards.find((c) => c.id === cardId)\n if (!from || !card) return columns\n return columns.map((column) => {\n if (column.id === fromColumnId && column.id === toColumnId) {\n const startIndex = column.cards.indexOf(card)\n return { ...column, cards: reorder({ list: column.cards, startIndex, finishIndex: toIndex }) }\n }\n if (column.id === fromColumnId) return { ...column, cards: column.cards.filter((c) => c !== card) }\n if (column.id === toColumnId) {\n const cards = [...column.cards]\n cards.splice(toIndex, 0, card)\n return { ...column, cards }\n }\n return column\n })\n}\n\n/** Apply an `onColumnMove` intent to a column array. */\nfunction moveColumn<TColumn extends { id: string }>(\n columns: TColumn[],\n { fromIndex, toIndex }: KanbanColumnMoveDetails\n): TColumn[] {\n return reorder({ list: columns, startIndex: fromIndex, finishIndex: toIndex })\n}\n\nexport {\n Kanban,\n KanbanAddTrigger,\n KanbanBoard,\n KanbanCard,\n KanbanCardDescription,\n KanbanCardFooter,\n KanbanCardHandle,\n KanbanCardHeader,\n KanbanCardTitle,\n KanbanColumn,\n KanbanColumnActions,\n KanbanColumnContent,\n KanbanColumnCount,\n KanbanColumnFooter,\n KanbanColumnHandle,\n KanbanColumnHeader,\n KanbanColumnTitle,\n KanbanDropIndicator,\n KanbanDropSlot,\n KanbanEmpty,\n moveCard,\n moveColumn,\n useKanban,\n useKanbanColumn,\n type KanbanCardMoveDetails,\n type KanbanColumnMoveDetails,\n}\n"
|
|
671
|
+
"content": "import * as React from \"react\"\nimport { combine } from \"@atlaskit/pragmatic-drag-and-drop/combine\"\nimport { draggable, dropTargetForElements, monitorForElements } from \"@atlaskit/pragmatic-drag-and-drop/element/adapter\"\nimport { reorder } from \"@atlaskit/pragmatic-drag-and-drop/reorder\"\nimport { autoScrollForElements } from \"@atlaskit/pragmatic-drag-and-drop-auto-scroll/element\"\nimport { attachClosestEdge, extractClosestEdge, type Edge } from \"@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge\"\nimport { getReorderDestinationIndex } from \"@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index\"\nimport { ark } from \"@ark-ui/react\"\nimport { GripVerticalIcon } from \"lucide-react\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport { cloneDragPreview } from \"@/lib/drag-preview\"\nimport { cn } from \"@/lib/utils\"\nimport { LiveRegion, useLiveRegion } from \"@/components/ui/live-region\"\n\n/**\n * Kanban — a compositional, data-agnostic board on Pragmatic drag and drop.\n * The consumer owns columns and cards and re-renders them from its own state;\n * the primitive wires dragging and reports intents through `onCardMove` and\n * `onColumnMove`. `moveCard`/`moveColumn` are optional helpers for the common\n * `{ id, cards: { id }[] }[]` shape.\n */\n\ntype KanbanCardMoveDetails = {\n cardId: string\n fromColumnId: string\n toColumnId: string\n fromIndex: number\n toIndex: number\n}\n\ntype KanbanColumnMoveDetails = {\n columnId: string\n fromIndex: number\n toIndex: number\n}\n\ntype DragData =\n | { instanceId: symbol; type: \"card\"; cardId: string; columnId: string }\n | { instanceId: symbol; type: \"column\"; columnId: string }\n\nconst CARD_TARGET = \"kanban-card-target\"\nconst COLUMN_TARGET = \"kanban-column-target\"\n\ntype KanbanDragging = { type: \"card\" | \"column\"; id: string; columnId?: string; height: number }\n/** Where a dragged card would land: before `beforeCardId`, or at the end when null. */\ntype KanbanPreview = { columnId: string; beforeCardId: string | null }\n\ntype KanbanGrabbed = { type: \"card\" | \"column\"; id: string }\n\ntype KanbanContextValue = {\n instanceId: symbol\n dragging: KanbanDragging | null\n preview: KanbanPreview | null\n /** Keyboard \"pick up\" state; arrows move the grabbed card/column. */\n grabbed: KanbanGrabbed | null\n grab: (item: KanbanGrabbed | null) => void\n moveByKey: (item: KanbanGrabbed, direction: \"up\" | \"down\" | \"left\" | \"right\") => void\n}\n\nconst focusLater = (selector: string) =>\n requestAnimationFrame(() => requestAnimationFrame(() => document.querySelector<HTMLElement>(selector)?.focus()))\n\nconst cssEscape = (value: string) => (typeof CSS !== \"undefined\" && CSS.escape ? CSS.escape(value) : value)\n\nconst KanbanContext = React.createContext<KanbanContextValue | null>(null)\n\nfunction useKanban() {\n const ctx = React.useContext(KanbanContext)\n if (!ctx) throw new Error(\"Kanban parts must be used within <Kanban>\")\n return ctx\n}\n\nconst isOwnData = (instanceId: symbol, data: Record<string | symbol, unknown>): data is DragData =>\n data.instanceId === instanceId && (data.type === \"card\" || data.type === \"column\")\n\nfunction cardsOf(columnEl: Element) {\n return Array.from(columnEl.querySelectorAll<HTMLElement>(\":scope [data-slot=kanban-card]\"))\n}\n\ntype DropTargetLike = { data: Record<string | symbol, unknown> }\n\n/** Resolve a card drag against the current drop targets into a concrete destination. */\nfunction resolveCardDrop(root: HTMLElement, data: Extract<DragData, { type: \"card\" }>, targets: DropTargetLike[]) {\n const columnTarget = targets.find((t) => t.data[COLUMN_TARGET] === true)\n if (!columnTarget) return null\n const toColumnId = String(columnTarget.data.columnId)\n const columnEls = Array.from(root.querySelectorAll<HTMLElement>(\"[data-slot=kanban-column]\"))\n const fromColumnEl = columnEls.find((el) => el.dataset.value === data.columnId)\n const toColumnEl = columnEls.find((el) => el.dataset.value === toColumnId)\n if (!fromColumnEl || !toColumnEl) return null\n const fromIndex = cardsOf(fromColumnEl).findIndex((el) => el.dataset.value === data.cardId)\n const toCards = cardsOf(toColumnEl)\n const cardTarget = targets.find((t) => t.data[CARD_TARGET] === true)\n const sameColumn = data.columnId === toColumnId\n\n let toIndex: number\n if (cardTarget && String(cardTarget.data.cardId) !== data.cardId) {\n const indexOfTarget = toCards.findIndex((el) => el.dataset.value === String(cardTarget.data.cardId))\n const edge = extractClosestEdge(cardTarget.data)\n toIndex = sameColumn\n ? getReorderDestinationIndex({\n startIndex: fromIndex,\n indexOfTarget,\n closestEdgeOfTarget: edge,\n axis: \"vertical\",\n })\n : edge === \"bottom\"\n ? indexOfTarget + 1\n : indexOfTarget\n } else if (cardTarget) {\n // Hovering the dragged card itself: no move.\n toIndex = fromIndex\n } else {\n toIndex = sameColumn ? toCards.length - 1 : toCards.length\n }\n const noop = sameColumn && toIndex === fromIndex\n const remaining = toCards.filter((el) => el.dataset.value !== data.cardId)\n const beforeCardId = remaining[toIndex]?.dataset.value ?? null\n return { toColumnId, toIndex, fromIndex, sameColumn, noop, beforeCardId }\n}\n\nfunction Kanban({\n onCardMove,\n onColumnMove,\n className,\n children,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** Called with `{ cardId, fromColumnId, toColumnId, fromIndex, toIndex }`; apply it with `moveCard`. */\n onCardMove?: (details: KanbanCardMoveDetails) => void\n /** Called with `{ columnId, fromIndex, toIndex }`; apply it with `moveColumn`. */\n onColumnMove?: (details: KanbanColumnMoveDetails) => void\n}) {\n const [instanceId] = React.useState(() => Symbol(\"kanban\"))\n const [dragging, setDragging] = React.useState<KanbanDragging | null>(null)\n const [preview, setPreviewState] = React.useState<KanbanPreview | null>(null)\n const [grabbed, setGrabbed] = React.useState<KanbanGrabbed | null>(null)\n const { message: announcement, announce: setAnnouncement } = useLiveRegion()\n const previewKey = React.useRef<string | null>(null)\n const ref = React.useRef<HTMLDivElement>(null)\n const callbacks = React.useRef({ onCardMove, onColumnMove })\n callbacks.current = { onCardMove, onColumnMove }\n\n const grab = React.useCallback(\n (item: KanbanGrabbed | null) => {\n setGrabbed(item)\n setAnnouncement(\n item\n ? `Picked up ${item.type} ${item.id}. Use the arrow keys to move, Space or Enter to drop, Escape to cancel.`\n : \"Dropped.\"\n )\n },\n [setAnnouncement]\n )\n\n const moveByKey = React.useCallback(\n (item: KanbanGrabbed, direction: \"up\" | \"down\" | \"left\" | \"right\") => {\n const root = ref.current\n if (!root) return\n const columnEls = Array.from(root.querySelectorAll<HTMLElement>(\"[data-slot=kanban-column]\"))\n const columnIds = columnEls.map((el) => el.dataset.value ?? \"\")\n const columnName = (el: HTMLElement) =>\n el.querySelector(\"[data-slot=kanban-column-title]\")?.textContent ?? el.dataset.value\n\n if (item.type === \"column\") {\n const fromIndex = columnIds.indexOf(item.id)\n const toIndex = direction === \"left\" ? fromIndex - 1 : direction === \"right\" ? fromIndex + 1 : fromIndex\n if (fromIndex < 0 || toIndex === fromIndex || toIndex < 0 || toIndex >= columnIds.length) return\n callbacks.current.onColumnMove?.({ columnId: item.id, fromIndex, toIndex })\n setAnnouncement(`Moved column to position ${toIndex + 1} of ${columnIds.length}.`)\n focusLater(`[data-slot=kanban-column][data-value=\"${cssEscape(item.id)}\"] [data-slot=kanban-column-handle]`)\n return\n }\n\n const fromColumnEl = columnEls.find((el) => cardsOf(el).some((c) => c.dataset.value === item.id))\n if (!fromColumnEl) return\n const fromColumnId = fromColumnEl.dataset.value ?? \"\"\n const fromCards = cardsOf(fromColumnEl)\n const fromIndex = fromCards.findIndex((c) => c.dataset.value === item.id)\n let toColumnId = fromColumnId\n let toIndex = fromIndex\n if (direction === \"up\" || direction === \"down\") {\n toIndex = direction === \"up\" ? fromIndex - 1 : fromIndex + 1\n if (toIndex < 0 || toIndex >= fromCards.length) return\n } else {\n const ci = columnIds.indexOf(fromColumnId)\n const targetEl = columnEls[direction === \"left\" ? ci - 1 : ci + 1]\n if (!targetEl) return\n toColumnId = targetEl.dataset.value ?? \"\"\n toIndex = Math.min(fromIndex, cardsOf(targetEl).length)\n }\n callbacks.current.onCardMove?.({ cardId: item.id, fromColumnId, toColumnId, fromIndex, toIndex })\n const targetEl = columnEls[columnIds.indexOf(toColumnId)]\n const count = toColumnId === fromColumnId ? fromCards.length : cardsOf(targetEl).length + 1\n setAnnouncement(`Moved to ${columnName(targetEl)}, position ${toIndex + 1} of ${count}.`)\n focusLater(`[data-slot=kanban-card][data-value=\"${cssEscape(item.id)}\"]`)\n },\n [setAnnouncement]\n )\n\n React.useEffect(() => {\n const root = ref.current\n if (!root) return\n const setPreview = (next: KanbanPreview | null) => {\n const key = next ? `${next.columnId}::${next.beforeCardId ?? \"\"}` : null\n if (key === previewKey.current) return\n previewKey.current = key\n setPreviewState(next)\n }\n const updatePreview = (data: DragData, targets: DropTargetLike[]) => {\n if (data.type !== \"card\") return\n const resolved = resolveCardDrop(root, data, targets)\n setPreview(\n resolved && !resolved.noop ? { columnId: resolved.toColumnId, beforeCardId: resolved.beforeCardId } : null\n )\n }\n return monitorForElements({\n canMonitor: ({ source }) => isOwnData(instanceId, source.data),\n onDragStart: ({ source }) => {\n const data = source.data as DragData\n setDragging({\n type: data.type,\n id: data.type === \"card\" ? data.cardId : data.columnId,\n columnId: data.type === \"card\" ? data.columnId : undefined,\n /** Height of the drop slot, normally the dragged card's height. */\n height: source.element.getBoundingClientRect().height,\n })\n },\n onDrag: ({ source, location }) => updatePreview(source.data as DragData, location.current.dropTargets),\n onDropTargetChange: ({ source, location }) =>\n updatePreview(source.data as DragData, location.current.dropTargets),\n onDrop: ({ source, location }) => {\n setDragging(null)\n setPreview(null)\n const data = source.data as DragData\n const targets = location.current.dropTargets\n const columnEls = Array.from(root.querySelectorAll<HTMLElement>(\"[data-slot=kanban-column]\"))\n const columnIds = columnEls.map((el) => el.dataset.value ?? \"\")\n\n if (data.type === \"column\") {\n const target = targets.find((t) => t.data[COLUMN_TARGET] === true)\n if (!target) return\n const targetId = String(target.data.columnId)\n const fromIndex = columnIds.indexOf(data.columnId)\n const indexOfTarget = columnIds.indexOf(targetId)\n if (fromIndex < 0 || indexOfTarget < 0) return\n const toIndex = getReorderDestinationIndex({\n startIndex: fromIndex,\n indexOfTarget,\n closestEdgeOfTarget: extractClosestEdge(target.data),\n axis: \"horizontal\",\n })\n if (toIndex === fromIndex) return\n callbacks.current.onColumnMove?.({ columnId: data.columnId, fromIndex, toIndex })\n return\n }\n\n const resolved = resolveCardDrop(root, data, targets)\n if (!resolved || resolved.noop) return\n callbacks.current.onCardMove?.({\n cardId: data.cardId,\n fromColumnId: data.columnId,\n toColumnId: resolved.toColumnId,\n fromIndex: resolved.fromIndex,\n toIndex: resolved.toIndex,\n })\n },\n })\n }, [instanceId])\n\n const ctx = React.useMemo(\n () => ({ instanceId, dragging, preview, grabbed, grab, moveByKey }),\n [instanceId, dragging, preview, grabbed, grab, moveByKey]\n )\n return (\n <KanbanContext.Provider value={ctx}>\n <div\n ref={ref}\n data-slot=\"kanban\"\n data-dragging={dragging ? dragging.type : undefined}\n data-grabbed={grabbed ? grabbed.type : undefined}\n className={cn(\"flex min-h-0 flex-1 flex-col\", className)}\n {...props}\n >\n {children}\n <LiveRegion data-slot=\"kanban-live-region\" message={announcement} />\n </div>\n </KanbanContext.Provider>\n )\n}\n\n/** Keyboard handling shared by cards and column handles. */\nfunction useKanbanKeyboard(item: KanbanGrabbed) {\n const { grabbed, grab, moveByKey } = useKanban()\n const isGrabbed = grabbed?.type === item.type && grabbed.id === item.id\n const onKeyDown = (event: React.KeyboardEvent) => {\n const key = event.key\n if (key === \" \" || key === \"Enter\") {\n event.preventDefault()\n grab(isGrabbed ? null : item)\n return\n }\n if (!isGrabbed) return\n if (key === \"Escape\") {\n event.preventDefault()\n grab(null)\n return\n }\n const direction = { ArrowUp: \"up\", ArrowDown: \"down\", ArrowLeft: \"left\", ArrowRight: \"right\" }[key] as\n \"up\" | \"down\" | \"left\" | \"right\" | undefined\n if (!direction) return\n event.preventDefault()\n moveByKey(item, direction)\n }\n return { isGrabbed, onKeyDown, onBlur: () => isGrabbed && grab(null) }\n}\n\n/* -------------------------------------------------------------------------- */\n/* Board */\n/* -------------------------------------------------------------------------- */\n\nfunction KanbanBoard({ className, ...props }: React.ComponentProps<\"div\">) {\n const { instanceId } = useKanban()\n const ref = React.useRef<HTMLDivElement>(null)\n React.useEffect(() => {\n const el = ref.current\n if (!el) return\n return autoScrollForElements({\n element: el,\n canScroll: ({ source }) => isOwnData(instanceId, source.data),\n })\n }, [instanceId])\n return (\n <div\n ref={ref}\n data-slot=\"kanban-board\"\n className={cn(\"flex min-h-0 flex-1 items-start gap-4 overflow-x-auto p-1 pb-4\", className)}\n {...props}\n />\n )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Column */\n/* -------------------------------------------------------------------------- */\n\ntype ColumnContextValue = {\n value: string\n handleRef: React.RefObject<HTMLElement | null>\n headerRef: React.RefObject<HTMLElement | null>\n isOver: boolean\n isDragging: boolean\n}\n\nconst ColumnContext = React.createContext<ColumnContextValue | null>(null)\n\nfunction useKanbanColumn() {\n const ctx = React.useContext(ColumnContext)\n if (!ctx) throw new Error(\"Kanban column parts must be used within <KanbanColumn>\")\n return ctx\n}\n\nfunction KanbanColumn({\n value,\n draggable: isDraggable = true,\n className,\n children,\n ...props\n}: React.ComponentProps<\"section\"> & {\n value: string\n /** Allow the column itself to be reordered (drag by its handle or header). */\n draggable?: boolean\n}) {\n const { instanceId, dragging, preview } = useKanban()\n const ref = React.useRef<HTMLElement>(null)\n const handleRef = React.useRef<HTMLElement | null>(null)\n const headerRef = React.useRef<HTMLElement | null>(null)\n const [isOver, setIsOver] = React.useState(false)\n const [isDragging, setIsDragging] = React.useState(false)\n const [closestEdge, setClosestEdge] = React.useState<Edge | null>(null)\n\n React.useEffect(() => {\n const el = ref.current\n if (!el) return\n const cleanups = [\n dropTargetForElements({\n element: el,\n canDrop: ({ source }) => isOwnData(instanceId, source.data),\n getIsSticky: () => true,\n getData: ({ source, input, element }) => {\n const base = { [COLUMN_TARGET]: true, columnId: value }\n if ((source.data as DragData).type === \"column\") {\n return attachClosestEdge(base, { element, input, allowedEdges: [\"left\", \"right\"] })\n }\n return base\n },\n onDragEnter: ({ source, self }) => {\n const data = source.data as DragData\n if (data.type === \"card\") setIsOver(true)\n else if (data.columnId !== value) setClosestEdge(extractClosestEdge(self.data))\n },\n onDrag: ({ source, self }) => {\n const data = source.data as DragData\n if (data.type === \"column\" && data.columnId !== value) {\n const edge = extractClosestEdge(self.data)\n setClosestEdge((prev) => (prev === edge ? prev : edge))\n }\n },\n onDragLeave: () => {\n setIsOver(false)\n setClosestEdge(null)\n },\n onDrop: () => {\n setIsOver(false)\n setClosestEdge(null)\n },\n }),\n ]\n if (isDraggable) {\n cleanups.push(\n draggable({\n element: el,\n dragHandle: handleRef.current ?? headerRef.current ?? undefined,\n getInitialData: (): DragData => ({ instanceId, type: \"column\", columnId: value }),\n onGenerateDragPreview: (args) => cloneDragPreview(args),\n onDragStart: () => setIsDragging(true),\n onDrop: () => setIsDragging(false),\n })\n )\n }\n return combine(...cleanups)\n }, [instanceId, value, isDraggable])\n\n const ctx = React.useMemo(() => ({ value, handleRef, headerRef, isOver, isDragging }), [value, isOver, isDragging])\n return (\n <ColumnContext.Provider value={ctx}>\n <section\n ref={ref}\n data-slot=\"kanban-column\"\n data-value={value}\n data-over={isOver && dragging?.type === \"card\" ? \"\" : undefined}\n data-target={preview?.columnId === value ? \"\" : undefined}\n data-dragging={isDragging ? \"\" : undefined}\n data-edge={closestEdge ?? undefined}\n className={cn(\n \"group/kanban-column relative flex max-h-full w-72 shrink-0 flex-col rounded-xl border bg-muted/40 transition-colors data-dragging:opacity-40 data-target:border-primary/30\",\n className\n )}\n {...props}\n >\n {children}\n {closestEdge && <KanbanDropIndicator edge={closestEdge} />}\n </section>\n </ColumnContext.Provider>\n )\n}\n\nfunction KanbanColumnHeader({ className, ...props }: React.ComponentProps<\"header\">) {\n const { headerRef } = useKanbanColumn()\n return (\n <header\n ref={headerRef as React.RefObject<HTMLElement>}\n data-slot=\"kanban-column-header\"\n className={cn(\"flex items-center gap-2 px-3 py-2.5\", className)}\n {...props}\n />\n )\n}\n\nfunction KanbanColumnHandle({\n className,\n children,\n onKeyDown,\n onBlur,\n asChild,\n ...props\n}: React.ComponentProps<typeof ark.button>) {\n const { handleRef, value } = useKanbanColumn()\n const keyboard = useKanbanKeyboard({ type: \"column\", id: value })\n return (\n <ark.button\n asChild={asChild}\n ref={handleRef as React.RefObject<HTMLButtonElement>}\n type=\"button\"\n data-slot=\"kanban-column-handle\"\n data-grabbed={keyboard.isGrabbed ? \"\" : undefined}\n aria-label=\"Drag column\"\n aria-pressed={keyboard.isGrabbed}\n aria-describedby={undefined}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (!event.defaultPrevented) keyboard.onKeyDown(event)\n }}\n onBlur={(event) => {\n onBlur?.(event)\n keyboard.onBlur()\n }}\n className={cn(\n \"-ms-1 flex size-6 cursor-grab items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50 active:cursor-grabbing data-grabbed:bg-primary data-grabbed:text-primary-foreground [&_svg]:size-4\",\n className\n )}\n {...props}\n >\n {asChild ? children : (children ?? <GripVerticalIcon />)}\n </ark.button>\n )\n}\n\nfunction KanbanColumnTitle({ className, ...props }: React.ComponentProps<\"h3\">) {\n return <h3 data-slot=\"kanban-column-title\" className={cn(\"truncate text-sm font-semibold\", className)} {...props} />\n}\n\nfunction KanbanColumnCount({ className, ...props }: React.ComponentProps<typeof Badge>) {\n return (\n <Badge\n data-slot=\"kanban-column-count\"\n variant=\"secondary\"\n className={cn(\"h-5 min-w-5 justify-center px-1.5 tabular-nums\", className)}\n {...props}\n />\n )\n}\n\nfunction KanbanColumnActions({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div data-slot=\"kanban-column-actions\" className={cn(\"ms-auto flex items-center gap-1\", className)} {...props} />\n )\n}\n\n/** The scrolling list of cards. Renders the landing slot at the end when a card would be appended. */\nfunction KanbanColumnContent({ className, children, ...props }: React.ComponentProps<\"div\">) {\n const { instanceId, dragging, preview } = useKanban()\n const column = useKanbanColumn()\n const ref = React.useRef<HTMLDivElement>(null)\n React.useEffect(() => {\n const el = ref.current\n if (!el) return\n return autoScrollForElements({\n element: el,\n canScroll: ({ source }) => isOwnData(instanceId, source.data),\n })\n }, [instanceId])\n return (\n <div\n ref={ref}\n data-slot=\"kanban-column-content\"\n className={cn(\n \"no-scrollbar flex min-h-16 flex-1 flex-col gap-(--kanban-gap) overflow-y-auto px-2 pb-2 [--kanban-gap:0.5rem]\",\n className\n )}\n {...props}\n >\n {children}\n {dragging?.type === \"card\" && preview?.columnId === column.value && preview.beforeCardId === null && (\n <KanbanDropSlot height={dragging.height} />\n )}\n </div>\n )\n}\n\nfunction KanbanColumnFooter({ className, ...props }: React.ComponentProps<\"footer\">) {\n return <footer data-slot=\"kanban-column-footer\" className={cn(\"flex items-center px-2 pb-2\", className)} {...props} />\n}\n\n/** A ghost-style full-width trigger for the footer, e.g. \"Add card\". Polymorphic via `asChild`. */\nfunction KanbanAddTrigger({\n className,\n variant = \"ghost\",\n size = \"sm\",\n ...props\n}: React.ComponentProps<typeof Button>) {\n return (\n <Button\n data-slot=\"kanban-add-trigger\"\n variant={variant}\n size={size}\n className={cn(\"w-full justify-start text-muted-foreground hover:text-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction KanbanEmpty({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"kanban-empty\"\n className={cn(\n \"flex flex-1 items-center justify-center rounded-lg border border-dashed px-3 py-6 text-center text-xs text-muted-foreground\",\n className\n )}\n {...props}\n />\n )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Card */\n/* -------------------------------------------------------------------------- */\n\ntype CardContextValue = { handleRef: React.RefObject<HTMLElement | null>; value: string }\nconst CardContext = React.createContext<CardContextValue | null>(null)\n\nfunction KanbanCard({\n value,\n draggable: isDraggable = true,\n className,\n children,\n onKeyDown,\n onBlur,\n ...props\n}: React.ComponentProps<\"article\"> & { value: string; draggable?: boolean }) {\n const { instanceId, dragging, preview } = useKanban()\n const column = useKanbanColumn()\n const keyboard = useKanbanKeyboard({ type: \"card\", id: value })\n const ref = React.useRef<HTMLElement>(null)\n const handleRef = React.useRef<HTMLElement | null>(null)\n const [isDragging, setIsDragging] = React.useState(false)\n const isSlotBefore = dragging?.type === \"card\" && preview?.columnId === column.value && preview.beforeCardId === value\n // The dragged card stays as an outlined placeholder until a landing spot exists, then collapses.\n const isCollapsed = isDragging && preview !== null\n\n React.useEffect(() => {\n const el = ref.current\n if (!el) return\n const cleanups = [\n dropTargetForElements({\n element: el,\n canDrop: ({ source }) => isOwnData(instanceId, source.data) && (source.data as DragData).type === \"card\",\n getIsSticky: () => true,\n getData: ({ input, element }) =>\n attachClosestEdge(\n { [CARD_TARGET]: true, cardId: value, columnId: column.value },\n {\n element,\n input,\n allowedEdges: [\"top\", \"bottom\"],\n }\n ),\n }),\n ]\n if (isDraggable) {\n cleanups.push(\n draggable({\n element: el,\n dragHandle: handleRef.current ?? undefined,\n getInitialData: (): DragData => ({ instanceId, type: \"card\", cardId: value, columnId: column.value }),\n onGenerateDragPreview: (args) => cloneDragPreview(args),\n onDragStart: () => setIsDragging(true),\n onDrop: () => setIsDragging(false),\n })\n )\n }\n return combine(...cleanups)\n }, [instanceId, value, column.value, isDraggable])\n\n const ctx = React.useMemo(() => ({ handleRef, value }), [value])\n return (\n <CardContext.Provider value={ctx}>\n {isSlotBefore && dragging && <KanbanDropSlot height={dragging.height} />}\n <article\n ref={ref}\n data-slot=\"kanban-card\"\n data-value={value}\n data-dragging={isDragging ? \"\" : undefined}\n data-collapsed={isCollapsed ? \"\" : undefined}\n data-grabbed={keyboard.isGrabbed ? \"\" : undefined}\n tabIndex={isDraggable ? 0 : undefined}\n aria-roledescription={isDraggable ? \"draggable card\" : undefined}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (!event.defaultPrevented && event.target === event.currentTarget) keyboard.onKeyDown(event)\n }}\n onBlur={(event) => {\n onBlur?.(event)\n if (event.target === event.currentTarget) keyboard.onBlur()\n }}\n className={cn(\n \"group/kanban-card relative flex cursor-grab flex-col gap-2 rounded-lg border bg-card p-3 text-sm text-card-foreground shadow-xs transition-shadow outline-none hover:shadow-sm focus-visible:ring-3 focus-visible:ring-ring/50 active:cursor-grabbing data-grabbed:border-primary data-grabbed:ring-3 data-grabbed:ring-primary/30\",\n \"data-dragging:border-dashed data-dragging:border-primary/40 data-dragging:bg-primary/5 data-dragging:shadow-none data-dragging:*:invisible\",\n // Collapse (not `display:none`): removing the dragged element from layout cancels a native drag in Chrome.\n \"data-collapsed:pointer-events-none data-collapsed:-mb-(--kanban-gap) data-collapsed:h-0 data-collapsed:min-h-0 data-collapsed:overflow-hidden data-collapsed:border-0 data-collapsed:p-0 data-collapsed:opacity-0\",\n className\n )}\n {...props}\n >\n {children}\n </article>\n </CardContext.Provider>\n )\n}\n\n/** Optional explicit drag handle; without it the whole card is the handle. */\nfunction KanbanCardHandle({\n className,\n children,\n onKeyDown,\n onBlur,\n asChild,\n ...props\n}: React.ComponentProps<typeof ark.button>) {\n const ctx = React.useContext(CardContext)\n const keyboard = useKanbanKeyboard({ type: \"card\", id: ctx?.value ?? \"\" })\n return (\n <ark.button\n asChild={asChild}\n ref={(el: HTMLElement | null) => {\n if (ctx) ctx.handleRef.current = el\n }}\n type=\"button\"\n data-slot=\"kanban-card-handle\"\n data-grabbed={keyboard.isGrabbed ? \"\" : undefined}\n aria-label=\"Drag card\"\n aria-pressed={keyboard.isGrabbed}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (!event.defaultPrevented) {\n keyboard.onKeyDown(event)\n event.stopPropagation()\n }\n }}\n onBlur={(event) => {\n onBlur?.(event)\n keyboard.onBlur()\n }}\n className={cn(\n \"flex size-6 cursor-grab items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50 active:cursor-grabbing data-grabbed:bg-primary data-grabbed:text-primary-foreground [&_svg]:size-4\",\n className\n )}\n {...props}\n >\n {asChild ? children : (children ?? <GripVerticalIcon />)}\n </ark.button>\n )\n}\n\nfunction KanbanCardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"kanban-card-header\"\n className={cn(\"flex items-start justify-between gap-2\", className)}\n {...props}\n />\n )\n}\n\nfunction KanbanCardTitle({ className, ...props }: React.ComponentProps<\"p\">) {\n return <p data-slot=\"kanban-card-title\" className={cn(\"leading-snug font-medium\", className)} {...props} />\n}\n\nfunction KanbanCardDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return (\n <p\n data-slot=\"kanban-card-description\"\n className={cn(\"line-clamp-3 text-xs text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction KanbanCardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"kanban-card-footer\"\n className={cn(\"flex items-center gap-2 text-xs text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Drop slot + indicator */\n/* -------------------------------------------------------------------------- */\n\n/** An outlined placeholder the size of the dragged card, marking where it will land. */\nfunction KanbanDropSlot({ height, className, style, ...props }: React.ComponentProps<\"div\"> & { height?: number }) {\n return (\n <div\n data-slot=\"kanban-drop-slot\"\n aria-hidden\n style={{ height, ...style }}\n className={cn(\n \"shrink-0 rounded-lg border-2 border-dashed border-primary/40 bg-primary/5\",\n height === undefined && \"min-h-16\",\n className\n )}\n {...props}\n />\n )\n}\n\n/** A line on the given edge of its (relative) parent, with a terminal dot. */\nfunction KanbanDropIndicator({\n edge,\n gap = \"0.5rem\",\n className,\n ...props\n}: React.ComponentProps<\"div\"> & { edge: Edge; gap?: string }) {\n const horizontal = edge === \"top\" || edge === \"bottom\"\n return (\n <div\n data-slot=\"kanban-drop-indicator\"\n data-edge={edge}\n aria-hidden\n style={{ [\"--kanban-gap\" as string]: gap }}\n className={cn(\n \"pointer-events-none absolute z-10 bg-primary\",\n \"before:absolute before:size-2 before:rounded-full before:border-2 before:border-primary before:bg-background\",\n horizontal\n ? \"inset-x-0 h-0.5 before:top-1/2 before:-left-1 before:-translate-y-1/2\"\n : \"inset-y-0 w-0.5 before:-top-1 before:left-1/2 before:-translate-x-1/2\",\n edge === \"top\" && \"top-[calc(-1*var(--kanban-gap)/2-1px)]\",\n edge === \"bottom\" && \"bottom-[calc(-1*var(--kanban-gap)/2-1px)]\",\n edge === \"left\" && \"left-[calc(-1*var(--kanban-gap)/2-1px)]\",\n edge === \"right\" && \"right-[calc(-1*var(--kanban-gap)/2-1px)]\",\n className\n )}\n {...props}\n />\n )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Helpers */\n/* -------------------------------------------------------------------------- */\n\ntype KanbanColumnLike<TCard extends { id: string }> = { id: string; cards: TCard[] }\n\n/** Apply an `onCardMove` intent to a `{ id, cards }[]` state shape. */\nfunction moveCard<TCard extends { id: string }, TColumn extends KanbanColumnLike<TCard>>(\n columns: TColumn[],\n { cardId, fromColumnId, toColumnId, toIndex }: KanbanCardMoveDetails\n): TColumn[] {\n const from = columns.find((c) => c.id === fromColumnId)\n const card = from?.cards.find((c) => c.id === cardId)\n if (!from || !card) return columns\n return columns.map((column) => {\n if (column.id === fromColumnId && column.id === toColumnId) {\n const startIndex = column.cards.indexOf(card)\n return { ...column, cards: reorder({ list: column.cards, startIndex, finishIndex: toIndex }) }\n }\n if (column.id === fromColumnId) return { ...column, cards: column.cards.filter((c) => c !== card) }\n if (column.id === toColumnId) {\n const cards = [...column.cards]\n cards.splice(toIndex, 0, card)\n return { ...column, cards }\n }\n return column\n })\n}\n\n/** Apply an `onColumnMove` intent to a column array. */\nfunction moveColumn<TColumn extends { id: string }>(\n columns: TColumn[],\n { fromIndex, toIndex }: KanbanColumnMoveDetails\n): TColumn[] {\n return reorder({ list: columns, startIndex: fromIndex, finishIndex: toIndex })\n}\n\nexport {\n Kanban,\n KanbanAddTrigger,\n KanbanBoard,\n KanbanCard,\n KanbanCardDescription,\n KanbanCardFooter,\n KanbanCardHandle,\n KanbanCardHeader,\n KanbanCardTitle,\n KanbanColumn,\n KanbanColumnActions,\n KanbanColumnContent,\n KanbanColumnCount,\n KanbanColumnFooter,\n KanbanColumnHandle,\n KanbanColumnHeader,\n KanbanColumnTitle,\n KanbanDropIndicator,\n KanbanDropSlot,\n KanbanEmpty,\n moveCard,\n moveColumn,\n useKanban,\n useKanbanColumn,\n type KanbanCardMoveDetails,\n type KanbanColumnMoveDetails,\n}\n"
|
|
49
672
|
}
|
|
50
673
|
],
|
|
51
674
|
"docs": "## Kanban\n`src/components/ui/kanban.tsx` is a compositional, data-agnostic board on\n`@atlaskit/pragmatic-drag-and-drop` (+ `-hitbox` closest-edge/reorder index, `-auto-scroll`).\nThe consumer owns columns/cards state; `Kanban` only wires dragging and reports intents via\n`onCardMove({ cardId, fromColumnId, toColumnId, fromIndex, toIndex })` and\n`onColumnMove({ columnId, fromIndex, toIndex })`. `moveCard`/`moveColumn` apply those to a\n`{ id, cards: { id }[] }[]` shape. Parts: `KanbanBoard` (horizontal scroller, auto-scroll),\n`KanbanColumn` (`value`, drop target for cards and columns, `draggable` by its\n`KanbanColumnHandle` or header), `KanbanColumnHeader/Title/Count/Actions/Content/Footer`,\n`KanbanAddTrigger`, `KanbanEmpty`, `KanbanCard` (`value`, draggable + drop target with\ntop/bottom edges, optional `KanbanCardHandle`), `KanbanCardHeader/Title/Description/Footer`,\n`KanbanDropSlot` (outlined placeholder sized like the dragged card; the root computes the\nlanding spot live via `preview` in context, cards render the slot before themselves and\n`KanbanColumnContent` renders it at the end) and `KanbanDropIndicator` (edge line, used for\ncolumn reordering). While a landing spot exists the source card collapses (`data-collapsed`,\nheight 0 rather than `display:none`, which would cancel Chrome's native drag); with no\nlanding spot it stays in place as a dashed outline. State via `data-dragging`, `data-over`,\n`data-target`, `data-edge` on the root/column/card. Indices are derived from DOM order at drop time, so cards need no index\nprop. Keyboard reordering is not provided (Pragmatic leaves it to the consumer).\nKeyboard: cards (and `KanbanCardHandle`/`KanbanColumnHandle`) are focusable; Space/Enter\npicks up (`data-grabbed`, `aria-pressed`), arrows move within/between columns via the same\n`onCardMove`/`onColumnMove` intents, Space/Enter drops, Escape cancels, focus follows the\nmoved item, and a visually hidden live region announces each step. Native drag previews are\ncrisp clones via `cloneDragPreview` in `src/lib/drag-preview.ts`.\n`src/demo/kanban/` is the reference (tasks data, route `/projects/board`, `AppShellMain fixed`)."
|