@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
|
@@ -20,6 +20,614 @@
|
|
|
20
20
|
"TransferListSelectAll",
|
|
21
21
|
"useTransferList"
|
|
22
22
|
],
|
|
23
|
+
"parts": [
|
|
24
|
+
{
|
|
25
|
+
"name": "TransferList",
|
|
26
|
+
"props": [
|
|
27
|
+
{
|
|
28
|
+
"name": "items",
|
|
29
|
+
"type": "T[]",
|
|
30
|
+
"required": true,
|
|
31
|
+
"description": "Every item, in catalog order.",
|
|
32
|
+
"from": "arkcn"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "defaultValue",
|
|
36
|
+
"type": "string[]",
|
|
37
|
+
"required": false,
|
|
38
|
+
"default": "[]",
|
|
39
|
+
"description": "The initial value when uncontrolled.",
|
|
40
|
+
"from": "arkcn"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "disabled",
|
|
44
|
+
"type": "boolean",
|
|
45
|
+
"required": false,
|
|
46
|
+
"default": "false",
|
|
47
|
+
"description": "Disables the part and everything inside it.",
|
|
48
|
+
"from": "arkcn"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "filter",
|
|
52
|
+
"type": "((item: T, query: string) => boolean)",
|
|
53
|
+
"required": false,
|
|
54
|
+
"description": "Case-insensitive label match by default.",
|
|
55
|
+
"from": "arkcn"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "itemDisabled",
|
|
59
|
+
"type": "((item: T) => boolean)",
|
|
60
|
+
"required": false,
|
|
61
|
+
"default": "(item) => !!(item as TransferListItemBase).disabled",
|
|
62
|
+
"description": "Returns true for items that can never move.",
|
|
63
|
+
"from": "arkcn"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "itemToString",
|
|
67
|
+
"type": "((item: T) => string)",
|
|
68
|
+
"required": false,
|
|
69
|
+
"default": "(item) => (item as TransferListItemBase).label",
|
|
70
|
+
"description": "Label for an item.",
|
|
71
|
+
"from": "arkcn"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "itemToValue",
|
|
75
|
+
"type": "((item: T) => string)",
|
|
76
|
+
"required": false,
|
|
77
|
+
"default": "(item) => (item as TransferListItemBase).value",
|
|
78
|
+
"description": "Unique value for an item.",
|
|
79
|
+
"from": "arkcn"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"name": "onValueChange",
|
|
83
|
+
"type": "((details: TransferListValueChangeDetails<T>) => void)",
|
|
84
|
+
"required": false,
|
|
85
|
+
"description": "Called with the new value whenever it changes.",
|
|
86
|
+
"from": "arkcn"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"name": "titles",
|
|
90
|
+
"type": "Partial<Record<TransferListSide, string>>",
|
|
91
|
+
"required": false,
|
|
92
|
+
"description": "Labels for the live region: \"Moved 3 items to {target}\".",
|
|
93
|
+
"from": "arkcn"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"name": "value",
|
|
97
|
+
"type": "string[]",
|
|
98
|
+
"required": false,
|
|
99
|
+
"description": "Values on the target side. Kept in catalog order.",
|
|
100
|
+
"from": "arkcn"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "children",
|
|
104
|
+
"type": "ReactNode",
|
|
105
|
+
"required": false,
|
|
106
|
+
"description": "Content rendered inside the part.",
|
|
107
|
+
"from": "@types/react"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"name": "className",
|
|
111
|
+
"type": "string",
|
|
112
|
+
"required": false,
|
|
113
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
114
|
+
"from": "@types/react"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"name": "style",
|
|
118
|
+
"type": "CSSProperties",
|
|
119
|
+
"required": false,
|
|
120
|
+
"description": "Inline styles applied to the rendered element.",
|
|
121
|
+
"from": "@types/react"
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"name": "TransferListControls",
|
|
127
|
+
"props": [
|
|
128
|
+
{
|
|
129
|
+
"name": "children",
|
|
130
|
+
"type": "ReactNode",
|
|
131
|
+
"required": false,
|
|
132
|
+
"description": "Content rendered inside the part.",
|
|
133
|
+
"from": "@types/react"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"name": "className",
|
|
137
|
+
"type": "string",
|
|
138
|
+
"required": false,
|
|
139
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
140
|
+
"from": "@types/react"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"name": "style",
|
|
144
|
+
"type": "CSSProperties",
|
|
145
|
+
"required": false,
|
|
146
|
+
"description": "Inline styles applied to the rendered element.",
|
|
147
|
+
"from": "@types/react"
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"name": "TransferListEmpty",
|
|
153
|
+
"props": [
|
|
154
|
+
{
|
|
155
|
+
"name": "children",
|
|
156
|
+
"type": "ReactNode",
|
|
157
|
+
"required": false,
|
|
158
|
+
"description": "Content rendered inside the part.",
|
|
159
|
+
"from": "@types/react"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"name": "className",
|
|
163
|
+
"type": "string",
|
|
164
|
+
"required": false,
|
|
165
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
166
|
+
"from": "@types/react"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"name": "style",
|
|
170
|
+
"type": "CSSProperties",
|
|
171
|
+
"required": false,
|
|
172
|
+
"description": "Inline styles applied to the rendered element.",
|
|
173
|
+
"from": "@types/react"
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"name": "TransferListItem",
|
|
179
|
+
"props": [
|
|
180
|
+
{
|
|
181
|
+
"name": "item",
|
|
182
|
+
"type": "T",
|
|
183
|
+
"required": true,
|
|
184
|
+
"description": "The item this part renders.",
|
|
185
|
+
"from": "arkcn"
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"name": "children",
|
|
189
|
+
"type": "ReactNode",
|
|
190
|
+
"required": false,
|
|
191
|
+
"description": "Content rendered inside the part.",
|
|
192
|
+
"from": "arkcn"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"name": "className",
|
|
196
|
+
"type": "string",
|
|
197
|
+
"required": false,
|
|
198
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
199
|
+
"from": "@types/react"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"name": "style",
|
|
203
|
+
"type": "CSSProperties",
|
|
204
|
+
"required": false,
|
|
205
|
+
"description": "Inline styles applied to the rendered element.",
|
|
206
|
+
"from": "@types/react"
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"name": "TransferListItemIndicator",
|
|
212
|
+
"props": [
|
|
213
|
+
{
|
|
214
|
+
"name": "children",
|
|
215
|
+
"type": "ReactNode",
|
|
216
|
+
"required": false,
|
|
217
|
+
"description": "Content rendered inside the part.",
|
|
218
|
+
"from": "@types/react"
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"name": "className",
|
|
222
|
+
"type": "string",
|
|
223
|
+
"required": false,
|
|
224
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
225
|
+
"from": "@types/react"
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"name": "style",
|
|
229
|
+
"type": "CSSProperties",
|
|
230
|
+
"required": false,
|
|
231
|
+
"description": "Inline styles applied to the rendered element.",
|
|
232
|
+
"from": "@types/react"
|
|
233
|
+
}
|
|
234
|
+
]
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"name": "TransferListItemText",
|
|
238
|
+
"props": [
|
|
239
|
+
{
|
|
240
|
+
"name": "children",
|
|
241
|
+
"type": "ReactNode",
|
|
242
|
+
"required": false,
|
|
243
|
+
"description": "Content rendered inside the part.",
|
|
244
|
+
"from": "@types/react"
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"name": "className",
|
|
248
|
+
"type": "string",
|
|
249
|
+
"required": false,
|
|
250
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
251
|
+
"from": "@types/react"
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"name": "style",
|
|
255
|
+
"type": "CSSProperties",
|
|
256
|
+
"required": false,
|
|
257
|
+
"description": "Inline styles applied to the rendered element.",
|
|
258
|
+
"from": "@types/react"
|
|
259
|
+
}
|
|
260
|
+
]
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"name": "TransferListItems",
|
|
264
|
+
"props": [
|
|
265
|
+
{
|
|
266
|
+
"name": "children",
|
|
267
|
+
"type": "((item: T) => ReactNode)",
|
|
268
|
+
"required": false,
|
|
269
|
+
"description": "Content rendered inside the part.",
|
|
270
|
+
"from": "arkcn"
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"name": "className",
|
|
274
|
+
"type": "string",
|
|
275
|
+
"required": false,
|
|
276
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
277
|
+
"from": "@types/react"
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"name": "style",
|
|
281
|
+
"type": "CSSProperties",
|
|
282
|
+
"required": false,
|
|
283
|
+
"description": "Inline styles applied to the rendered element.",
|
|
284
|
+
"from": "@types/react"
|
|
285
|
+
}
|
|
286
|
+
]
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
"name": "TransferListMoveAllTrigger",
|
|
290
|
+
"props": [
|
|
291
|
+
{
|
|
292
|
+
"name": "direction",
|
|
293
|
+
"type": "TransferListDirection",
|
|
294
|
+
"required": true,
|
|
295
|
+
"description": "`right` moves source to target; `left` moves target to source.",
|
|
296
|
+
"from": "arkcn"
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
"name": "asChild",
|
|
300
|
+
"type": "boolean",
|
|
301
|
+
"required": false,
|
|
302
|
+
"description": "Render the child element instead of the default one, merging props and behaviour onto it.",
|
|
303
|
+
"from": "arkcn"
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
"name": "size",
|
|
307
|
+
"type": "\"default\" | \"xs\" | \"sm\" | \"lg\" | \"icon\" | \"icon-xs\" | \"icon-sm\" | \"icon-lg\" | null",
|
|
308
|
+
"required": false,
|
|
309
|
+
"description": "Size variant; sets a `data-size` attribute.",
|
|
310
|
+
"from": "arkcn"
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
"name": "variant",
|
|
314
|
+
"type": "\"link\" | \"default\" | \"outline\" | \"secondary\" | \"ghost\" | \"destructive\" | null",
|
|
315
|
+
"required": false,
|
|
316
|
+
"description": "Visual variant, styled with `cva`; sets a `data-variant` attribute.",
|
|
317
|
+
"from": "arkcn"
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
"name": "children",
|
|
321
|
+
"type": "ReactNode",
|
|
322
|
+
"required": false,
|
|
323
|
+
"description": "Content rendered inside the part.",
|
|
324
|
+
"from": "@types/react"
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
"name": "className",
|
|
328
|
+
"type": "string",
|
|
329
|
+
"required": false,
|
|
330
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
331
|
+
"from": "@types/react"
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
"name": "style",
|
|
335
|
+
"type": "CSSProperties",
|
|
336
|
+
"required": false,
|
|
337
|
+
"description": "Inline styles applied to the rendered element.",
|
|
338
|
+
"from": "@types/react"
|
|
339
|
+
}
|
|
340
|
+
]
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
"name": "TransferListMoveTrigger",
|
|
344
|
+
"props": [
|
|
345
|
+
{
|
|
346
|
+
"name": "direction",
|
|
347
|
+
"type": "TransferListDirection",
|
|
348
|
+
"required": true,
|
|
349
|
+
"description": "`right` moves source to target; `left` moves target to source.",
|
|
350
|
+
"from": "arkcn"
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
"name": "asChild",
|
|
354
|
+
"type": "boolean",
|
|
355
|
+
"required": false,
|
|
356
|
+
"description": "Render the child element instead of the default one, merging props and behaviour onto it.",
|
|
357
|
+
"from": "arkcn"
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
"name": "size",
|
|
361
|
+
"type": "\"default\" | \"xs\" | \"sm\" | \"lg\" | \"icon\" | \"icon-xs\" | \"icon-sm\" | \"icon-lg\" | null",
|
|
362
|
+
"required": false,
|
|
363
|
+
"description": "Size variant; sets a `data-size` attribute.",
|
|
364
|
+
"from": "arkcn"
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
"name": "variant",
|
|
368
|
+
"type": "\"link\" | \"default\" | \"outline\" | \"secondary\" | \"ghost\" | \"destructive\" | null",
|
|
369
|
+
"required": false,
|
|
370
|
+
"description": "Visual variant, styled with `cva`; sets a `data-variant` attribute.",
|
|
371
|
+
"from": "arkcn"
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
"name": "children",
|
|
375
|
+
"type": "ReactNode",
|
|
376
|
+
"required": false,
|
|
377
|
+
"description": "Content rendered inside the part.",
|
|
378
|
+
"from": "@types/react"
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
"name": "className",
|
|
382
|
+
"type": "string",
|
|
383
|
+
"required": false,
|
|
384
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
385
|
+
"from": "@types/react"
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
"name": "style",
|
|
389
|
+
"type": "CSSProperties",
|
|
390
|
+
"required": false,
|
|
391
|
+
"description": "Inline styles applied to the rendered element.",
|
|
392
|
+
"from": "@types/react"
|
|
393
|
+
}
|
|
394
|
+
]
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
"name": "TransferListPanel",
|
|
398
|
+
"props": [
|
|
399
|
+
{
|
|
400
|
+
"name": "side",
|
|
401
|
+
"type": "TransferListSide",
|
|
402
|
+
"required": true,
|
|
403
|
+
"description": "Which side of the anchor or container the part sits on.",
|
|
404
|
+
"from": "arkcn"
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
"name": "children",
|
|
408
|
+
"type": "ReactNode",
|
|
409
|
+
"required": false,
|
|
410
|
+
"description": "Content rendered inside the part.",
|
|
411
|
+
"from": "@types/react"
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
"name": "className",
|
|
415
|
+
"type": "string",
|
|
416
|
+
"required": false,
|
|
417
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
418
|
+
"from": "@types/react"
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
"name": "style",
|
|
422
|
+
"type": "CSSProperties",
|
|
423
|
+
"required": false,
|
|
424
|
+
"description": "Inline styles applied to the rendered element.",
|
|
425
|
+
"from": "@types/react"
|
|
426
|
+
}
|
|
427
|
+
]
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
"name": "TransferListPanelCount",
|
|
431
|
+
"props": [
|
|
432
|
+
{
|
|
433
|
+
"name": "children",
|
|
434
|
+
"type": "((selected: number, total: number) => ReactNode)",
|
|
435
|
+
"required": false,
|
|
436
|
+
"description": "Content rendered inside the part.",
|
|
437
|
+
"from": "arkcn"
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
"name": "className",
|
|
441
|
+
"type": "string",
|
|
442
|
+
"required": false,
|
|
443
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
444
|
+
"from": "@types/react"
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
"name": "style",
|
|
448
|
+
"type": "CSSProperties",
|
|
449
|
+
"required": false,
|
|
450
|
+
"description": "Inline styles applied to the rendered element.",
|
|
451
|
+
"from": "@types/react"
|
|
452
|
+
}
|
|
453
|
+
]
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
"name": "TransferListPanelHeader",
|
|
457
|
+
"props": [
|
|
458
|
+
{
|
|
459
|
+
"name": "children",
|
|
460
|
+
"type": "ReactNode",
|
|
461
|
+
"required": false,
|
|
462
|
+
"description": "Content rendered inside the part.",
|
|
463
|
+
"from": "@types/react"
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
"name": "className",
|
|
467
|
+
"type": "string",
|
|
468
|
+
"required": false,
|
|
469
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
470
|
+
"from": "@types/react"
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
"name": "style",
|
|
474
|
+
"type": "CSSProperties",
|
|
475
|
+
"required": false,
|
|
476
|
+
"description": "Inline styles applied to the rendered element.",
|
|
477
|
+
"from": "@types/react"
|
|
478
|
+
}
|
|
479
|
+
]
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
"name": "TransferListPanelTitle",
|
|
483
|
+
"props": [
|
|
484
|
+
{
|
|
485
|
+
"name": "children",
|
|
486
|
+
"type": "ReactNode",
|
|
487
|
+
"required": false,
|
|
488
|
+
"description": "Content rendered inside the part.",
|
|
489
|
+
"from": "@types/react"
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
"name": "className",
|
|
493
|
+
"type": "string",
|
|
494
|
+
"required": false,
|
|
495
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
496
|
+
"from": "@types/react"
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
"name": "style",
|
|
500
|
+
"type": "CSSProperties",
|
|
501
|
+
"required": false,
|
|
502
|
+
"description": "Inline styles applied to the rendered element.",
|
|
503
|
+
"from": "@types/react"
|
|
504
|
+
}
|
|
505
|
+
]
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
"name": "TransferListSearch",
|
|
509
|
+
"props": [
|
|
510
|
+
{
|
|
511
|
+
"name": "children",
|
|
512
|
+
"type": "ReactNode",
|
|
513
|
+
"required": false,
|
|
514
|
+
"description": "Content rendered inside the part.",
|
|
515
|
+
"from": "@types/react"
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
"name": "className",
|
|
519
|
+
"type": "string",
|
|
520
|
+
"required": false,
|
|
521
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
522
|
+
"from": "@types/react"
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
"name": "style",
|
|
526
|
+
"type": "CSSProperties",
|
|
527
|
+
"required": false,
|
|
528
|
+
"description": "Inline styles applied to the rendered element.",
|
|
529
|
+
"from": "@types/react"
|
|
530
|
+
}
|
|
531
|
+
]
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
"name": "TransferListSelectAll",
|
|
535
|
+
"props": [
|
|
536
|
+
{
|
|
537
|
+
"name": "asChild",
|
|
538
|
+
"type": "boolean",
|
|
539
|
+
"required": false,
|
|
540
|
+
"description": "Use the provided child element as the default rendered element, combining their props and behavior.",
|
|
541
|
+
"from": "@ark-ui/react"
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
"name": "children",
|
|
545
|
+
"type": "ReactNode",
|
|
546
|
+
"required": false,
|
|
547
|
+
"description": "Content rendered inside the part.",
|
|
548
|
+
"from": "@types/react"
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
"name": "className",
|
|
552
|
+
"type": "string",
|
|
553
|
+
"required": false,
|
|
554
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
555
|
+
"from": "@types/react"
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
"name": "defaultChecked",
|
|
559
|
+
"type": "CheckedState",
|
|
560
|
+
"required": false,
|
|
561
|
+
"description": "The initial checked state of the checkbox when rendered.\nUse when you don't need to control the checked state of the checkbox.",
|
|
562
|
+
"from": "@zag-js/checkbox"
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
"name": "disabled",
|
|
566
|
+
"type": "boolean",
|
|
567
|
+
"required": false,
|
|
568
|
+
"description": "Whether the checkbox is disabled",
|
|
569
|
+
"from": "@zag-js/checkbox"
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
"name": "form",
|
|
573
|
+
"type": "string",
|
|
574
|
+
"required": false,
|
|
575
|
+
"description": "The id of the form that the checkbox belongs to.",
|
|
576
|
+
"from": "@zag-js/checkbox"
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
"name": "ids",
|
|
580
|
+
"type": "ElementIds",
|
|
581
|
+
"required": false,
|
|
582
|
+
"description": "The ids of the elements in the checkbox. Useful for composition.",
|
|
583
|
+
"from": "@zag-js/checkbox"
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
"name": "invalid",
|
|
587
|
+
"type": "boolean",
|
|
588
|
+
"required": false,
|
|
589
|
+
"description": "Whether the checkbox is invalid",
|
|
590
|
+
"from": "@zag-js/checkbox"
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
"name": "name",
|
|
594
|
+
"type": "string",
|
|
595
|
+
"required": false,
|
|
596
|
+
"description": "The name of the input field in a checkbox.\nUseful for form submission.",
|
|
597
|
+
"from": "@zag-js/checkbox"
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
"name": "readOnly",
|
|
601
|
+
"type": "boolean",
|
|
602
|
+
"required": false,
|
|
603
|
+
"description": "Whether the checkbox is read-only",
|
|
604
|
+
"from": "@zag-js/checkbox"
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
"name": "required",
|
|
608
|
+
"type": "boolean",
|
|
609
|
+
"required": false,
|
|
610
|
+
"description": "Whether the checkbox is required",
|
|
611
|
+
"from": "@zag-js/checkbox"
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
"name": "style",
|
|
615
|
+
"type": "CSSProperties",
|
|
616
|
+
"required": false,
|
|
617
|
+
"description": "Inline styles applied to the rendered element.",
|
|
618
|
+
"from": "@types/react"
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
"name": "value",
|
|
622
|
+
"type": "string",
|
|
623
|
+
"required": false,
|
|
624
|
+
"default": "\"on\"",
|
|
625
|
+
"description": "The value of checkbox input. Useful for form submission.",
|
|
626
|
+
"from": "@zag-js/checkbox"
|
|
627
|
+
}
|
|
628
|
+
]
|
|
629
|
+
}
|
|
630
|
+
],
|
|
23
631
|
"dependencies": {
|
|
24
632
|
"@ark-ui/react": "^5.39.1",
|
|
25
633
|
"lucide-react": "^1.41.0"
|
|
@@ -35,7 +643,7 @@
|
|
|
35
643
|
"files": [
|
|
36
644
|
{
|
|
37
645
|
"path": "components/ui/transfer-list.tsx",
|
|
38
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ark } from \"@ark-ui/react\"\nimport {\n CheckIcon,\n ChevronLeftIcon,\n ChevronRightIcon,\n ChevronsLeftIcon,\n ChevronsRightIcon,\n SearchIcon,\n} from \"lucide-react\"\nimport { useControllable } from \"@/lib/controllable\"\nimport { cn } from \"@/lib/utils\"\nimport { LiveRegion, useLiveRegion } from \"@/components/ui/live-region\"\nimport { Button } from \"@/components/ui/button\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\nimport { InputGroup, InputGroupAddon, InputGroupInput } from \"@/components/ui/input-group\"\n\n/* -------------------------------- context -------------------------------- */\n\ntype TransferListSide = \"source\" | \"target\"\ntype TransferListDirection = \"right\" | \"left\"\n\ntype TransferListItemBase = { value: string; label: string; disabled?: boolean }\n\ntype TransferListValueChangeDetails<T> = {\n /** Values now on the target side, in catalog order. */\n value: string[]\n items: T[]\n /** What just moved and where. */\n moved: T[]\n direction: TransferListDirection\n}\n\ntype TransferListContextValue<T = TransferListItemBase> = {\n items: T[]\n itemToValue: (item: T) => string\n itemToString: (item: T) => string\n itemDisabled: (item: T) => boolean\n value: string[]\n /** Items on each side, in catalog order. */\n sides: Record<TransferListSide, T[]>\n /** Visible (searched) items on each side. */\n visible: Record<TransferListSide, T[]>\n query: Record<TransferListSide, string>\n setQuery: (side: TransferListSide, query: string) => void\n selected: Record<TransferListSide, string[]>\n toggle: (side: TransferListSide, value: string, force?: boolean) => void\n selectRange: (side: TransferListSide, from: string, to: string) => void\n selectAll: (side: TransferListSide, checked: boolean) => void\n /** Move the selected (or given) values across. */\n move: (direction: TransferListDirection, values?: string[]) => void\n moveAll: (direction: TransferListDirection) => void\n disabled: boolean\n announcement: string\n focused: Record<TransferListSide, string | null>\n setFocused: (side: TransferListSide, value: string | null) => void\n}\n\nconst TransferListContext = React.createContext<TransferListContextValue | null>(null)\nconst TransferListSideContext = React.createContext<TransferListSide | null>(null)\n\nfunction useTransferList<T = TransferListItemBase>() {\n const ctx = React.useContext(TransferListContext)\n if (!ctx) throw new Error(\"TransferList parts must be used inside <TransferList>\")\n return ctx as unknown as TransferListContextValue<T>\n}\n\nfunction useTransferListSide() {\n const side = React.useContext(TransferListSideContext)\n if (!side) throw new Error(\"This part must be used inside <TransferListPanel>\")\n return side\n}\n\nconst sideOf = (direction: TransferListDirection): TransferListSide => (direction === \"right\" ? \"source\" : \"target\")\n\n/* ---------------------------------- root --------------------------------- */\n\ntype TransferListProps<T> = Omit<React.ComponentProps<\"div\">, \"defaultValue\" | \"onChange\"> & {\n items: T[]\n itemToValue?: (item: T) => string\n itemToString?: (item: T) => string\n itemDisabled?: (item: T) => boolean\n /** Values on the target side. Kept in catalog order. */\n value?: string[]\n defaultValue?: string[]\n onValueChange?: (details: TransferListValueChangeDetails<T>) => void\n /** Case-insensitive label match by default. */\n filter?: (item: T, query: string) => boolean\n disabled?: boolean\n /** Labels for the live region: \"Moved 3 items to {target}\". */\n titles?: Partial<Record<TransferListSide, string>>\n}\n\nfunction TransferList<T = TransferListItemBase>({\n items,\n itemToValue = (item) => (item as TransferListItemBase).value,\n itemToString = (item) => (item as TransferListItemBase).label,\n itemDisabled = (item) => !!(item as TransferListItemBase).disabled,\n value: valueProp,\n defaultValue = [],\n onValueChange,\n filter,\n disabled = false,\n titles,\n className,\n children,\n ...props\n}: TransferListProps<T>) {\n const order = React.useMemo(() => new Map(items.map((item, i) => [itemToValue(item), i])), [items, itemToValue])\n const sortValues = React.useCallback(\n (values: string[]) =>\n Array.from(new Set(values))\n .filter((v) => order.has(v))\n .sort((a, b) => order.get(a)! - order.get(b)!),\n [order]\n )\n const [value, setValueState] = useControllable<string[]>(valueProp, defaultValue)\n const [query, setQueryState] = React.useState<Record<TransferListSide, string>>({ source: \"\", target: \"\" })\n const [selected, setSelected] = React.useState<Record<TransferListSide, string[]>>({ source: [], target: [] })\n const [focused, setFocusedState] = React.useState<Record<TransferListSide, string | null>>({\n source: null,\n target: null,\n })\n const { message: announcement, announce: setAnnouncement } = useLiveRegion()\n\n const sides = React.useMemo(() => {\n const target = new Set(value)\n return {\n source: items.filter((item) => !target.has(itemToValue(item))),\n target: items.filter((item) => target.has(itemToValue(item))),\n }\n }, [items, value, itemToValue])\n\n const matches = React.useCallback(\n (item: T, q: string) =>\n filter ? filter(item, q) : itemToString(item).toLowerCase().includes(q.trim().toLowerCase()),\n [filter, itemToString]\n )\n const visible = React.useMemo(\n () => ({\n source: query.source.trim() ? sides.source.filter((i) => matches(i, query.source)) : sides.source,\n target: query.target.trim() ? sides.target.filter((i) => matches(i, query.target)) : sides.target,\n }),\n [sides, query, matches]\n )\n\n const setQuery = React.useCallback((side: TransferListSide, q: string) => {\n setQueryState((prev) => ({ ...prev, [side]: q }))\n }, [])\n const setFocused = React.useCallback((side: TransferListSide, v: string | null) => {\n setFocusedState((prev) => (prev[side] === v ? prev : { ...prev, [side]: v }))\n }, [])\n\n const toggle = React.useCallback((side: TransferListSide, v: string, force?: boolean) => {\n setSelected((prev) => {\n const has = prev[side].includes(v)\n const next = force ?? !has\n if (next === has) return prev\n return { ...prev, [side]: next ? [...prev[side], v] : prev[side].filter((x) => x !== v) }\n })\n }, [])\n\n const selectRange = React.useCallback(\n (side: TransferListSide, from: string, to: string) => {\n const list = visible[side].filter((i) => !itemDisabled(i)).map(itemToValue)\n const a = list.indexOf(from)\n const b = list.indexOf(to)\n if (a < 0 || b < 0) return\n const range = list.slice(Math.min(a, b), Math.max(a, b) + 1)\n setSelected((prev) => ({ ...prev, [side]: Array.from(new Set([...prev[side], ...range])) }))\n },\n [visible, itemDisabled, itemToValue]\n )\n\n const selectAll = React.useCallback(\n (side: TransferListSide, checked: boolean) => {\n const list = visible[side].filter((i) => !itemDisabled(i)).map(itemToValue)\n setSelected((prev) => ({\n ...prev,\n [side]: checked ? Array.from(new Set([...prev[side], ...list])) : prev[side].filter((v) => !list.includes(v)),\n }))\n },\n [visible, itemDisabled, itemToValue]\n )\n\n const move = React.useCallback(\n (direction: TransferListDirection, values?: string[]) => {\n if (disabled) return\n const from = sideOf(direction)\n const allowed = new Set(sides[from].filter((i) => !itemDisabled(i)).map(itemToValue))\n const moving = (values ?? selected[from]).filter((v) => allowed.has(v))\n if (moving.length === 0) return\n const next = sortValues(direction === \"right\" ? [...value, ...moving] : value.filter((v) => !moving.includes(v)))\n setValueState(next)\n setSelected((prev) => ({ ...prev, [from]: prev[from].filter((v) => !moving.includes(v)) }))\n const movedItems = items.filter((item) => moving.includes(itemToValue(item)))\n const to = direction === \"right\" ? \"target\" : \"source\"\n const title = titles?.[to] ?? (to === \"target\" ? \"the selected list\" : \"the available list\")\n setAnnouncement(\n movedItems.length === 1\n ? `Moved ${itemToString(movedItems[0])} to ${title}.`\n : `Moved ${movedItems.length} items to ${title}.`\n )\n onValueChange?.({\n value: next,\n items: items.filter((i) => next.includes(itemToValue(i))),\n moved: movedItems,\n direction,\n })\n },\n [\n disabled,\n sides,\n selected,\n value,\n items,\n itemToValue,\n itemToString,\n itemDisabled,\n sortValues,\n setValueState,\n onValueChange,\n titles,\n setAnnouncement,\n ]\n )\n\n const moveAll = React.useCallback(\n (direction: TransferListDirection) => {\n const from = sideOf(direction)\n move(direction, visible[from].map(itemToValue))\n },\n [move, visible, itemToValue]\n )\n\n const ctx: TransferListContextValue<T> = {\n items,\n itemToValue,\n itemToString,\n itemDisabled,\n value,\n sides,\n visible,\n query,\n setQuery,\n selected,\n toggle,\n selectRange,\n selectAll,\n move,\n moveAll,\n disabled,\n announcement,\n focused,\n setFocused,\n }\n\n return (\n <TransferListContext.Provider value={ctx as unknown as TransferListContextValue}>\n <div\n data-slot=\"transfer-list\"\n data-disabled={disabled ? \"\" : undefined}\n className={cn(\"grid grid-cols-1 gap-2 sm:grid-cols-[1fr_auto_1fr] sm:items-stretch\", className)}\n {...props}\n >\n {children}\n <LiveRegion data-slot=\"transfer-list-live-region\" message={announcement} />\n </div>\n </TransferListContext.Provider>\n )\n}\n\n/* ---------------------------------- panel -------------------------------- */\n\nfunction TransferListPanel({ side, className, ...props }: React.ComponentProps<\"div\"> & { side: TransferListSide }) {\n const ctx = useTransferList()\n return (\n <TransferListSideContext.Provider value={side}>\n <div\n data-slot=\"transfer-list-panel\"\n data-side={side}\n data-disabled={ctx.disabled ? \"\" : undefined}\n className={cn(\n \"flex min-h-64 min-w-0 flex-col overflow-hidden rounded-lg border border-input bg-background text-sm dark:bg-input/30 data-disabled:opacity-50\",\n className\n )}\n {...props}\n />\n </TransferListSideContext.Provider>\n )\n}\n\nfunction TransferListPanelHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"transfer-list-panel-header\"\n className={cn(\"flex h-9 shrink-0 items-center gap-2 border-b px-2.5\", className)}\n {...props}\n />\n )\n}\n\nfunction TransferListPanelTitle({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span data-slot=\"transfer-list-panel-title\" className={cn(\"flex-1 truncate font-medium\", className)} {...props} />\n )\n}\n\n/** \"selected / total\" for the side; `children` overrides with `(selected, total) => ReactNode`. */\nfunction TransferListPanelCount({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"span\">, \"children\"> & {\n children?: (selected: number, total: number) => React.ReactNode\n}) {\n const ctx = useTransferList()\n const side = useTransferListSide()\n const total = ctx.sides[side].length\n const count = ctx.selected[side].length\n return (\n <span\n data-slot=\"transfer-list-panel-count\"\n className={cn(\"text-xs text-muted-foreground tabular-nums\", className)}\n {...props}\n >\n {children ? children(count, total) : count > 0 ? `${count} / ${total}` : total}\n </span>\n )\n}\n\n/** Header checkbox that selects every visible enabled item on this side. */\nfunction TransferListSelectAll({\n className,\n ...props\n}: Omit<React.ComponentProps<typeof Checkbox>, \"checked\" | \"onCheckedChange\">) {\n const ctx = useTransferList()\n const side = useTransferListSide()\n const enabled = ctx.visible[side].filter((i) => !ctx.itemDisabled(i)).map(ctx.itemToValue)\n const count = enabled.filter((v) => ctx.selected[side].includes(v)).length\n const checked = enabled.length > 0 && count === enabled.length ? true : count > 0 ? \"indeterminate\" : false\n return (\n <Checkbox\n data-slot=\"transfer-list-select-all\"\n aria-label={`Select all ${side === \"source\" ? \"available\" : \"selected\"} items`}\n checked={checked}\n disabled={ctx.disabled || enabled.length === 0}\n onCheckedChange={({ checked: next }) => ctx.selectAll(side, next === true)}\n className={className}\n {...props}\n />\n )\n}\n\nfunction TransferListSearch({\n className,\n placeholder = \"Search…\",\n ...props\n}: Omit<React.ComponentProps<typeof InputGroupInput>, \"value\" | \"onChange\">) {\n const ctx = useTransferList()\n const side = useTransferListSide()\n return (\n <InputGroup\n data-slot=\"transfer-list-search\"\n className={cn(\"h-8 shrink-0 rounded-none border-0 border-b shadow-none has-focus-visible:ring-0\", className)}\n >\n <InputGroupAddon>\n <SearchIcon />\n </InputGroupAddon>\n <InputGroupInput\n role=\"searchbox\"\n placeholder={placeholder}\n value={ctx.query[side]}\n disabled={ctx.disabled}\n onChange={(event) => ctx.setQuery(side, event.target.value)}\n {...props}\n />\n </InputGroup>\n )\n}\n\n/* ---------------------------------- items -------------------------------- */\n\nconst TransferListRenderContext = React.createContext<((item: unknown) => React.ReactNode) | undefined>(undefined)\n\n/**\n * The listbox for one side. Roving focus; ↑/↓ move, Space toggles, Shift+↑/↓ extends,\n * ⌘/Ctrl+A selects all, Enter (or double-click) moves the focused item across.\n */\nfunction TransferListItems<T = TransferListItemBase>({\n className,\n children,\n onKeyDown,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & { children?: (item: T) => React.ReactNode }) {\n const ctx = useTransferList<T>()\n const side = useTransferListSide()\n const ref = React.useRef<HTMLDivElement>(null)\n const list = ctx.visible[side]\n const values = list.map(ctx.itemToValue)\n const focused = ctx.focused[side]\n const active = focused && values.includes(focused) ? focused : (values[0] ?? null)\n\n const focusValue = (v: string) => {\n ctx.setFocused(side, v)\n ref.current?.querySelector<HTMLElement>(`[data-slot=transfer-list-item][data-value=\"${CSS.escape(v)}\"]`)?.focus()\n }\n\n return (\n <TransferListRenderContext.Provider value={children as ((item: unknown) => React.ReactNode) | undefined}>\n <div\n ref={ref}\n data-slot=\"transfer-list-items\"\n role=\"listbox\"\n aria-multiselectable\n aria-label={side === \"source\" ? \"Available\" : \"Selected\"}\n data-active-value={active ?? undefined}\n className={cn(\"flex min-h-0 flex-1 flex-col gap-px overflow-y-auto p-1 outline-none\", className)}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (event.defaultPrevented || ctx.disabled) return\n const enabledValues = list.filter((i) => !ctx.itemDisabled(i)).map(ctx.itemToValue)\n const index = active ? values.indexOf(active) : -1\n const step = (delta: number) => {\n const next = values[Math.max(0, Math.min(values.length - 1, index + delta))]\n if (!next) return\n if (event.shiftKey && active && enabledValues.includes(active)) ctx.selectRange(side, active, next)\n focusValue(next)\n }\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault()\n step(1)\n break\n case \"ArrowUp\":\n event.preventDefault()\n step(-1)\n break\n case \"Home\":\n event.preventDefault()\n if (values[0]) focusValue(values[0])\n break\n case \"End\":\n event.preventDefault()\n if (values.length) focusValue(values[values.length - 1])\n break\n case \" \":\n event.preventDefault()\n if (active && enabledValues.includes(active)) ctx.toggle(side, active)\n break\n case \"Enter\":\n event.preventDefault()\n if (active && enabledValues.includes(active)) {\n const next = values[index + 1] ?? values[index - 1] ?? null\n ctx.move(side === \"source\" ? \"right\" : \"left\", [active])\n ctx.setFocused(side, next)\n if (next) queueMicrotask(() => focusValue(next))\n }\n break\n case \"a\":\n case \"A\":\n if (event.metaKey || event.ctrlKey) {\n event.preventDefault()\n ctx.selectAll(side, true)\n }\n break\n }\n }}\n {...props}\n >\n {list.map((item) => {\n const v = ctx.itemToValue(item)\n return <TransferListItem key={v} item={item} tabIndex={v === active ? 0 : -1} />\n })}\n </div>\n </TransferListRenderContext.Provider>\n )\n}\n\nfunction TransferListItem<T = TransferListItemBase>({\n item,\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & { item: T; children?: React.ReactNode }) {\n const ctx = useTransferList<T>()\n const side = useTransferListSide()\n const render = React.useContext(TransferListRenderContext)\n const value = ctx.itemToValue(item)\n const disabled = ctx.disabled || ctx.itemDisabled(item)\n const selected = ctx.selected[side].includes(value)\n return (\n <div\n data-slot=\"transfer-list-item\"\n role=\"option\"\n aria-selected={selected}\n aria-disabled={disabled || undefined}\n data-value={value}\n data-selected={selected ? \"\" : undefined}\n data-disabled={disabled ? \"\" : undefined}\n className={cn(\n \"flex h-7 cursor-default items-center gap-2 rounded-md px-2 outline-none select-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset data-selected:bg-muted data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n onFocus={() => ctx.setFocused(side, value)}\n onClick={(event) => {\n if (disabled) return\n const anchor = ctx.focused[side]\n if (event.shiftKey && anchor && anchor !== value) ctx.selectRange(side, anchor, value)\n else ctx.toggle(side, value)\n ctx.setFocused(side, value)\n }}\n onDoubleClick={() => {\n if (!disabled) ctx.move(side === \"source\" ? \"right\" : \"left\", [value])\n }}\n {...props}\n >\n {children ?? (\n <>\n <TransferListItemIndicator />\n <TransferListItemText>{render ? render(item) : ctx.itemToString(item)}</TransferListItemText>\n </>\n )}\n </div>\n )\n}\n\n/** Checkbox-styled indicator that follows the item's `data-selected`. */\nfunction TransferListItemIndicator({ className, children, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n aria-hidden\n data-slot=\"transfer-list-item-indicator\"\n className={cn(\n \"flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input text-primary-foreground in-data-selected:border-primary in-data-selected:bg-primary not-in-data-selected:**:data-check:hidden dark:bg-input/30 dark:in-data-selected:bg-primary [&_svg]:size-3\",\n className\n )}\n {...props}\n >\n {children ?? <CheckIcon data-check=\"\" />}\n </span>\n )\n}\n\nfunction TransferListItemText({ className, ...props }: React.ComponentProps<\"span\">) {\n return <span data-slot=\"transfer-list-item-text\" className={cn(\"flex-1 truncate\", className)} {...props} />\n}\n\nfunction TransferListEmpty({ className, children, ...props }: React.ComponentProps<\"div\">) {\n const ctx = useTransferList()\n const side = useTransferListSide()\n if (ctx.visible[side].length > 0) return null\n const searching = ctx.query[side].trim().length > 0\n return (\n <div\n data-slot=\"transfer-list-empty\"\n className={cn(\n \"flex flex-1 items-center justify-center px-4 py-6 text-center text-sm text-muted-foreground\",\n className\n )}\n {...props}\n >\n {children ?? (searching ? \"No matches\" : \"Nothing here\")}\n </div>\n )\n}\n\n/* -------------------------------- controls ------------------------------- */\n\nfunction TransferListControls({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"transfer-list-controls\"\n className={cn(\"flex items-center justify-center gap-1 sm:flex-col\", className)}\n {...props}\n />\n )\n}\n\nconst directionIcon = {\n right: <ChevronRightIcon />,\n left: <ChevronLeftIcon />,\n}\nconst directionAllIcon = {\n right: <ChevronsRightIcon />,\n left: <ChevronsLeftIcon />,\n}\n\n/** Moves the selected items in `direction` (\"right\" = source → target). Polymorphic via `asChild`. */\nfunction TransferListMoveTrigger({\n direction,\n asChild,\n className,\n children,\n ...props\n}: React.ComponentProps<typeof Button> & { direction: TransferListDirection; asChild?: boolean }) {\n const ctx = useTransferList()\n const from = sideOf(direction)\n const count = ctx.selected[from].filter((v) => ctx.sides[from].some((i) => ctx.itemToValue(i) === v)).length\n const disabled = ctx.disabled || count === 0\n if (asChild) {\n return (\n <ark.button\n asChild\n data-slot=\"transfer-list-move-trigger\"\n data-direction={direction}\n disabled={disabled}\n onClick={() => ctx.move(direction)}\n {...(props as React.ComponentProps<typeof ark.button>)}\n >\n {children}\n </ark.button>\n )\n }\n return (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n data-slot=\"transfer-list-move-trigger\"\n data-direction={direction}\n aria-label={direction === \"right\" ? `Move ${count} selected right` : `Move ${count} selected left`}\n disabled={disabled}\n onClick={() => ctx.move(direction)}\n className={cn(\"rotate-90 sm:rotate-0\", className)}\n {...props}\n >\n {children ?? directionIcon[direction]}\n </Button>\n )\n}\n\n/** Moves every visible item in `direction`. Polymorphic via `asChild`. */\nfunction TransferListMoveAllTrigger({\n direction,\n asChild,\n className,\n children,\n ...props\n}: React.ComponentProps<typeof Button> & { direction: TransferListDirection; asChild?: boolean }) {\n const ctx = useTransferList()\n const from = sideOf(direction)\n const disabled = ctx.disabled || ctx.visible[from].every((i) => ctx.itemDisabled(i))\n if (asChild) {\n return (\n <ark.button\n asChild\n data-slot=\"transfer-list-move-all-trigger\"\n data-direction={direction}\n disabled={disabled}\n onClick={() => ctx.moveAll(direction)}\n {...(props as React.ComponentProps<typeof ark.button>)}\n >\n {children}\n </ark.button>\n )\n }\n return (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n data-slot=\"transfer-list-move-all-trigger\"\n data-direction={direction}\n aria-label={direction === \"right\" ? \"Move all right\" : \"Move all left\"}\n disabled={disabled}\n onClick={() => ctx.moveAll(direction)}\n className={cn(\"rotate-90 sm:rotate-0\", className)}\n {...props}\n >\n {children ?? directionAllIcon[direction]}\n </Button>\n )\n}\n\nexport {\n TransferList,\n TransferListControls,\n TransferListEmpty,\n TransferListItem,\n TransferListItemIndicator,\n TransferListItemText,\n TransferListItems,\n TransferListMoveAllTrigger,\n TransferListMoveTrigger,\n TransferListPanel,\n TransferListPanelCount,\n TransferListPanelHeader,\n TransferListPanelTitle,\n TransferListSearch,\n TransferListSelectAll,\n useTransferList,\n type TransferListDirection,\n type TransferListItemBase,\n type TransferListProps,\n type TransferListSide,\n type TransferListValueChangeDetails,\n}\n"
|
|
646
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ark } from \"@ark-ui/react\"\nimport {\n CheckIcon,\n ChevronLeftIcon,\n ChevronRightIcon,\n ChevronsLeftIcon,\n ChevronsRightIcon,\n SearchIcon,\n} from \"lucide-react\"\nimport { useControllable } from \"@/lib/controllable\"\nimport { cn } from \"@/lib/utils\"\nimport { LiveRegion, useLiveRegion } from \"@/components/ui/live-region\"\nimport { Button } from \"@/components/ui/button\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\nimport { InputGroup, InputGroupAddon, InputGroupInput } from \"@/components/ui/input-group\"\n\n/* -------------------------------- context -------------------------------- */\n\ntype TransferListSide = \"source\" | \"target\"\ntype TransferListDirection = \"right\" | \"left\"\n\ntype TransferListItemBase = { value: string; label: string; disabled?: boolean }\n\ntype TransferListValueChangeDetails<T> = {\n /** Values now on the target side, in catalog order. */\n value: string[]\n /** Every item, in catalog order. */\n items: T[]\n /** What just moved and where. */\n moved: T[]\n /** `right` moves source to target; `left` moves target to source. */\n direction: TransferListDirection\n}\n\ntype TransferListContextValue<T = TransferListItemBase> = {\n /** Every item, in catalog order. */\n items: T[]\n /** Unique value for an item. */\n itemToValue: (item: T) => string\n /** Label for an item. */\n itemToString: (item: T) => string\n /** Returns true for items that can never move. */\n itemDisabled: (item: T) => boolean\n value: string[]\n /** Items on each side, in catalog order. */\n sides: Record<TransferListSide, T[]>\n /** Visible (searched) items on each side. */\n visible: Record<TransferListSide, T[]>\n query: Record<TransferListSide, string>\n setQuery: (side: TransferListSide, query: string) => void\n selected: Record<TransferListSide, string[]>\n toggle: (side: TransferListSide, value: string, force?: boolean) => void\n selectRange: (side: TransferListSide, from: string, to: string) => void\n selectAll: (side: TransferListSide, checked: boolean) => void\n /** Move the selected (or given) values across. */\n move: (direction: TransferListDirection, values?: string[]) => void\n moveAll: (direction: TransferListDirection) => void\n disabled: boolean\n announcement: string\n focused: Record<TransferListSide, string | null>\n setFocused: (side: TransferListSide, value: string | null) => void\n}\n\nconst TransferListContext = React.createContext<TransferListContextValue | null>(null)\nconst TransferListSideContext = React.createContext<TransferListSide | null>(null)\n\nfunction useTransferList<T = TransferListItemBase>() {\n const ctx = React.useContext(TransferListContext)\n if (!ctx) throw new Error(\"TransferList parts must be used inside <TransferList>\")\n return ctx as unknown as TransferListContextValue<T>\n}\n\nfunction useTransferListSide() {\n const side = React.useContext(TransferListSideContext)\n if (!side) throw new Error(\"This part must be used inside <TransferListPanel>\")\n return side\n}\n\nconst sideOf = (direction: TransferListDirection): TransferListSide => (direction === \"right\" ? \"source\" : \"target\")\n\n/* ---------------------------------- root --------------------------------- */\n\ntype TransferListProps<T> = Omit<React.ComponentProps<\"div\">, \"defaultValue\" | \"onChange\"> & {\n /** Every item, in catalog order. */\n items: T[]\n /** Unique value for an item. */\n itemToValue?: (item: T) => string\n /** Label for an item. */\n itemToString?: (item: T) => string\n /** Returns true for items that can never move. */\n itemDisabled?: (item: T) => boolean\n /** Values on the target side. Kept in catalog order. */\n value?: string[]\n defaultValue?: string[]\n onValueChange?: (details: TransferListValueChangeDetails<T>) => void\n /** Case-insensitive label match by default. */\n filter?: (item: T, query: string) => boolean\n disabled?: boolean\n /** Labels for the live region: \"Moved 3 items to {target}\". */\n titles?: Partial<Record<TransferListSide, string>>\n}\n\nfunction TransferList<T = TransferListItemBase>({\n items,\n itemToValue = (item) => (item as TransferListItemBase).value,\n itemToString = (item) => (item as TransferListItemBase).label,\n itemDisabled = (item) => !!(item as TransferListItemBase).disabled,\n value: valueProp,\n defaultValue = [],\n onValueChange,\n filter,\n disabled = false,\n titles,\n className,\n children,\n ...props\n}: TransferListProps<T>) {\n const order = React.useMemo(() => new Map(items.map((item, i) => [itemToValue(item), i])), [items, itemToValue])\n const sortValues = React.useCallback(\n (values: string[]) =>\n Array.from(new Set(values))\n .filter((v) => order.has(v))\n .sort((a, b) => order.get(a)! - order.get(b)!),\n [order]\n )\n const [value, setValueState] = useControllable<string[]>(valueProp, defaultValue)\n const [query, setQueryState] = React.useState<Record<TransferListSide, string>>({ source: \"\", target: \"\" })\n const [selected, setSelected] = React.useState<Record<TransferListSide, string[]>>({ source: [], target: [] })\n const [focused, setFocusedState] = React.useState<Record<TransferListSide, string | null>>({\n source: null,\n target: null,\n })\n const { message: announcement, announce: setAnnouncement } = useLiveRegion()\n\n const sides = React.useMemo(() => {\n const target = new Set(value)\n return {\n source: items.filter((item) => !target.has(itemToValue(item))),\n target: items.filter((item) => target.has(itemToValue(item))),\n }\n }, [items, value, itemToValue])\n\n const matches = React.useCallback(\n (item: T, q: string) =>\n filter ? filter(item, q) : itemToString(item).toLowerCase().includes(q.trim().toLowerCase()),\n [filter, itemToString]\n )\n const visible = React.useMemo(\n () => ({\n source: query.source.trim() ? sides.source.filter((i) => matches(i, query.source)) : sides.source,\n target: query.target.trim() ? sides.target.filter((i) => matches(i, query.target)) : sides.target,\n }),\n [sides, query, matches]\n )\n\n const setQuery = React.useCallback((side: TransferListSide, q: string) => {\n setQueryState((prev) => ({ ...prev, [side]: q }))\n }, [])\n const setFocused = React.useCallback((side: TransferListSide, v: string | null) => {\n setFocusedState((prev) => (prev[side] === v ? prev : { ...prev, [side]: v }))\n }, [])\n\n const toggle = React.useCallback((side: TransferListSide, v: string, force?: boolean) => {\n setSelected((prev) => {\n const has = prev[side].includes(v)\n const next = force ?? !has\n if (next === has) return prev\n return { ...prev, [side]: next ? [...prev[side], v] : prev[side].filter((x) => x !== v) }\n })\n }, [])\n\n const selectRange = React.useCallback(\n (side: TransferListSide, from: string, to: string) => {\n const list = visible[side].filter((i) => !itemDisabled(i)).map(itemToValue)\n const a = list.indexOf(from)\n const b = list.indexOf(to)\n if (a < 0 || b < 0) return\n const range = list.slice(Math.min(a, b), Math.max(a, b) + 1)\n setSelected((prev) => ({ ...prev, [side]: Array.from(new Set([...prev[side], ...range])) }))\n },\n [visible, itemDisabled, itemToValue]\n )\n\n const selectAll = React.useCallback(\n (side: TransferListSide, checked: boolean) => {\n const list = visible[side].filter((i) => !itemDisabled(i)).map(itemToValue)\n setSelected((prev) => ({\n ...prev,\n [side]: checked ? Array.from(new Set([...prev[side], ...list])) : prev[side].filter((v) => !list.includes(v)),\n }))\n },\n [visible, itemDisabled, itemToValue]\n )\n\n const move = React.useCallback(\n (direction: TransferListDirection, values?: string[]) => {\n if (disabled) return\n const from = sideOf(direction)\n const allowed = new Set(sides[from].filter((i) => !itemDisabled(i)).map(itemToValue))\n const moving = (values ?? selected[from]).filter((v) => allowed.has(v))\n if (moving.length === 0) return\n const next = sortValues(direction === \"right\" ? [...value, ...moving] : value.filter((v) => !moving.includes(v)))\n setValueState(next)\n setSelected((prev) => ({ ...prev, [from]: prev[from].filter((v) => !moving.includes(v)) }))\n const movedItems = items.filter((item) => moving.includes(itemToValue(item)))\n const to = direction === \"right\" ? \"target\" : \"source\"\n const title = titles?.[to] ?? (to === \"target\" ? \"the selected list\" : \"the available list\")\n setAnnouncement(\n movedItems.length === 1\n ? `Moved ${itemToString(movedItems[0])} to ${title}.`\n : `Moved ${movedItems.length} items to ${title}.`\n )\n onValueChange?.({\n value: next,\n /** Every item, in catalog order. */\n items: items.filter((i) => next.includes(itemToValue(i))),\n moved: movedItems,\n direction,\n })\n },\n [\n disabled,\n sides,\n selected,\n value,\n items,\n itemToValue,\n itemToString,\n itemDisabled,\n sortValues,\n setValueState,\n onValueChange,\n titles,\n setAnnouncement,\n ]\n )\n\n const moveAll = React.useCallback(\n (direction: TransferListDirection) => {\n const from = sideOf(direction)\n move(direction, visible[from].map(itemToValue))\n },\n [move, visible, itemToValue]\n )\n\n const ctx: TransferListContextValue<T> = {\n items,\n itemToValue,\n itemToString,\n itemDisabled,\n value,\n sides,\n visible,\n query,\n setQuery,\n selected,\n toggle,\n selectRange,\n selectAll,\n move,\n moveAll,\n disabled,\n announcement,\n focused,\n setFocused,\n }\n\n return (\n <TransferListContext.Provider value={ctx as unknown as TransferListContextValue}>\n <div\n data-slot=\"transfer-list\"\n data-disabled={disabled ? \"\" : undefined}\n className={cn(\"grid grid-cols-1 gap-2 sm:grid-cols-[1fr_auto_1fr] sm:items-stretch\", className)}\n {...props}\n >\n {children}\n <LiveRegion data-slot=\"transfer-list-live-region\" message={announcement} />\n </div>\n </TransferListContext.Provider>\n )\n}\n\n/* ---------------------------------- panel -------------------------------- */\n\nfunction TransferListPanel({ side, className, ...props }: React.ComponentProps<\"div\"> & { side: TransferListSide }) {\n const ctx = useTransferList()\n return (\n <TransferListSideContext.Provider value={side}>\n <div\n data-slot=\"transfer-list-panel\"\n data-side={side}\n data-disabled={ctx.disabled ? \"\" : undefined}\n className={cn(\n \"flex min-h-64 min-w-0 flex-col overflow-hidden rounded-lg border border-input bg-background text-sm dark:bg-input/30 data-disabled:opacity-50\",\n className\n )}\n {...props}\n />\n </TransferListSideContext.Provider>\n )\n}\n\nfunction TransferListPanelHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"transfer-list-panel-header\"\n className={cn(\"flex h-9 shrink-0 items-center gap-2 border-b px-2.5\", className)}\n {...props}\n />\n )\n}\n\nfunction TransferListPanelTitle({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span data-slot=\"transfer-list-panel-title\" className={cn(\"flex-1 truncate font-medium\", className)} {...props} />\n )\n}\n\n/** \"selected / total\" for the side; `children` overrides with `(selected, total) => ReactNode`. */\nfunction TransferListPanelCount({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"span\">, \"children\"> & {\n children?: (selected: number, total: number) => React.ReactNode\n}) {\n const ctx = useTransferList()\n const side = useTransferListSide()\n const total = ctx.sides[side].length\n const count = ctx.selected[side].length\n return (\n <span\n data-slot=\"transfer-list-panel-count\"\n className={cn(\"text-xs text-muted-foreground tabular-nums\", className)}\n {...props}\n >\n {children ? children(count, total) : count > 0 ? `${count} / ${total}` : total}\n </span>\n )\n}\n\n/** Header checkbox that selects every visible enabled item on this side. */\nfunction TransferListSelectAll({\n className,\n ...props\n}: Omit<React.ComponentProps<typeof Checkbox>, \"checked\" | \"onCheckedChange\">) {\n const ctx = useTransferList()\n const side = useTransferListSide()\n const enabled = ctx.visible[side].filter((i) => !ctx.itemDisabled(i)).map(ctx.itemToValue)\n const count = enabled.filter((v) => ctx.selected[side].includes(v)).length\n const checked = enabled.length > 0 && count === enabled.length ? true : count > 0 ? \"indeterminate\" : false\n return (\n <Checkbox\n data-slot=\"transfer-list-select-all\"\n aria-label={`Select all ${side === \"source\" ? \"available\" : \"selected\"} items`}\n checked={checked}\n disabled={ctx.disabled || enabled.length === 0}\n onCheckedChange={({ checked: next }) => ctx.selectAll(side, next === true)}\n className={className}\n {...props}\n />\n )\n}\n\nfunction TransferListSearch({\n className,\n placeholder = \"Search…\",\n ...props\n}: Omit<React.ComponentProps<typeof InputGroupInput>, \"value\" | \"onChange\">) {\n const ctx = useTransferList()\n const side = useTransferListSide()\n return (\n <InputGroup\n data-slot=\"transfer-list-search\"\n className={cn(\"h-8 shrink-0 rounded-none border-0 border-b shadow-none has-focus-visible:ring-0\", className)}\n >\n <InputGroupAddon>\n <SearchIcon />\n </InputGroupAddon>\n <InputGroupInput\n role=\"searchbox\"\n placeholder={placeholder}\n value={ctx.query[side]}\n disabled={ctx.disabled}\n onChange={(event) => ctx.setQuery(side, event.target.value)}\n {...props}\n />\n </InputGroup>\n )\n}\n\n/* ---------------------------------- items -------------------------------- */\n\nconst TransferListRenderContext = React.createContext<((item: unknown) => React.ReactNode) | undefined>(undefined)\n\n/**\n * The listbox for one side. Roving focus; ↑/↓ move, Space toggles, Shift+↑/↓ extends,\n * ⌘/Ctrl+A selects all, Enter (or double-click) moves the focused item across.\n */\nfunction TransferListItems<T = TransferListItemBase>({\n className,\n children,\n onKeyDown,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & { children?: (item: T) => React.ReactNode }) {\n const ctx = useTransferList<T>()\n const side = useTransferListSide()\n const ref = React.useRef<HTMLDivElement>(null)\n const list = ctx.visible[side]\n const values = list.map(ctx.itemToValue)\n const focused = ctx.focused[side]\n const active = focused && values.includes(focused) ? focused : (values[0] ?? null)\n\n const focusValue = (v: string) => {\n ctx.setFocused(side, v)\n ref.current?.querySelector<HTMLElement>(`[data-slot=transfer-list-item][data-value=\"${CSS.escape(v)}\"]`)?.focus()\n }\n\n return (\n <TransferListRenderContext.Provider value={children as ((item: unknown) => React.ReactNode) | undefined}>\n <div\n ref={ref}\n data-slot=\"transfer-list-items\"\n role=\"listbox\"\n aria-multiselectable\n aria-label={side === \"source\" ? \"Available\" : \"Selected\"}\n data-active-value={active ?? undefined}\n className={cn(\"flex min-h-0 flex-1 flex-col gap-px overflow-y-auto p-1 outline-none\", className)}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (event.defaultPrevented || ctx.disabled) return\n const enabledValues = list.filter((i) => !ctx.itemDisabled(i)).map(ctx.itemToValue)\n const index = active ? values.indexOf(active) : -1\n const step = (delta: number) => {\n const next = values[Math.max(0, Math.min(values.length - 1, index + delta))]\n if (!next) return\n if (event.shiftKey && active && enabledValues.includes(active)) ctx.selectRange(side, active, next)\n focusValue(next)\n }\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault()\n step(1)\n break\n case \"ArrowUp\":\n event.preventDefault()\n step(-1)\n break\n case \"Home\":\n event.preventDefault()\n if (values[0]) focusValue(values[0])\n break\n case \"End\":\n event.preventDefault()\n if (values.length) focusValue(values[values.length - 1])\n break\n case \" \":\n event.preventDefault()\n if (active && enabledValues.includes(active)) ctx.toggle(side, active)\n break\n case \"Enter\":\n event.preventDefault()\n if (active && enabledValues.includes(active)) {\n const next = values[index + 1] ?? values[index - 1] ?? null\n ctx.move(side === \"source\" ? \"right\" : \"left\", [active])\n ctx.setFocused(side, next)\n if (next) queueMicrotask(() => focusValue(next))\n }\n break\n case \"a\":\n case \"A\":\n if (event.metaKey || event.ctrlKey) {\n event.preventDefault()\n ctx.selectAll(side, true)\n }\n break\n }\n }}\n {...props}\n >\n {list.map((item) => {\n const v = ctx.itemToValue(item)\n return <TransferListItem key={v} item={item} tabIndex={v === active ? 0 : -1} />\n })}\n </div>\n </TransferListRenderContext.Provider>\n )\n}\n\nfunction TransferListItem<T = TransferListItemBase>({\n item,\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & { item: T; children?: React.ReactNode }) {\n const ctx = useTransferList<T>()\n const side = useTransferListSide()\n const render = React.useContext(TransferListRenderContext)\n const value = ctx.itemToValue(item)\n const disabled = ctx.disabled || ctx.itemDisabled(item)\n const selected = ctx.selected[side].includes(value)\n return (\n <div\n data-slot=\"transfer-list-item\"\n role=\"option\"\n aria-selected={selected}\n aria-disabled={disabled || undefined}\n data-value={value}\n data-selected={selected ? \"\" : undefined}\n data-disabled={disabled ? \"\" : undefined}\n className={cn(\n \"flex h-7 cursor-default items-center gap-2 rounded-md px-2 outline-none select-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset data-selected:bg-muted data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n onFocus={() => ctx.setFocused(side, value)}\n onClick={(event) => {\n if (disabled) return\n const anchor = ctx.focused[side]\n if (event.shiftKey && anchor && anchor !== value) ctx.selectRange(side, anchor, value)\n else ctx.toggle(side, value)\n ctx.setFocused(side, value)\n }}\n onDoubleClick={() => {\n if (!disabled) ctx.move(side === \"source\" ? \"right\" : \"left\", [value])\n }}\n {...props}\n >\n {children ?? (\n <>\n <TransferListItemIndicator />\n <TransferListItemText>{render ? render(item) : ctx.itemToString(item)}</TransferListItemText>\n </>\n )}\n </div>\n )\n}\n\n/** Checkbox-styled indicator that follows the item's `data-selected`. */\nfunction TransferListItemIndicator({ className, children, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n aria-hidden\n data-slot=\"transfer-list-item-indicator\"\n className={cn(\n \"flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input text-primary-foreground in-data-selected:border-primary in-data-selected:bg-primary not-in-data-selected:**:data-check:hidden dark:bg-input/30 dark:in-data-selected:bg-primary [&_svg]:size-3\",\n className\n )}\n {...props}\n >\n {children ?? <CheckIcon data-check=\"\" />}\n </span>\n )\n}\n\nfunction TransferListItemText({ className, ...props }: React.ComponentProps<\"span\">) {\n return <span data-slot=\"transfer-list-item-text\" className={cn(\"flex-1 truncate\", className)} {...props} />\n}\n\nfunction TransferListEmpty({ className, children, ...props }: React.ComponentProps<\"div\">) {\n const ctx = useTransferList()\n const side = useTransferListSide()\n if (ctx.visible[side].length > 0) return null\n const searching = ctx.query[side].trim().length > 0\n return (\n <div\n data-slot=\"transfer-list-empty\"\n className={cn(\n \"flex flex-1 items-center justify-center px-4 py-6 text-center text-sm text-muted-foreground\",\n className\n )}\n {...props}\n >\n {children ?? (searching ? \"No matches\" : \"Nothing here\")}\n </div>\n )\n}\n\n/* -------------------------------- controls ------------------------------- */\n\nfunction TransferListControls({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"transfer-list-controls\"\n className={cn(\"flex items-center justify-center gap-1 sm:flex-col\", className)}\n {...props}\n />\n )\n}\n\nconst directionIcon = {\n right: <ChevronRightIcon />,\n left: <ChevronLeftIcon />,\n}\nconst directionAllIcon = {\n right: <ChevronsRightIcon />,\n left: <ChevronsLeftIcon />,\n}\n\n/** Moves the selected items in `direction` (\"right\" = source → target). Polymorphic via `asChild`. */\nfunction TransferListMoveTrigger({\n direction,\n asChild,\n className,\n children,\n ...props\n}: React.ComponentProps<typeof Button> & { direction: TransferListDirection; asChild?: boolean }) {\n const ctx = useTransferList()\n const from = sideOf(direction)\n const count = ctx.selected[from].filter((v) => ctx.sides[from].some((i) => ctx.itemToValue(i) === v)).length\n const disabled = ctx.disabled || count === 0\n if (asChild) {\n return (\n <ark.button\n asChild\n data-slot=\"transfer-list-move-trigger\"\n data-direction={direction}\n disabled={disabled}\n onClick={() => ctx.move(direction)}\n {...(props as React.ComponentProps<typeof ark.button>)}\n >\n {children}\n </ark.button>\n )\n }\n return (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n data-slot=\"transfer-list-move-trigger\"\n data-direction={direction}\n aria-label={direction === \"right\" ? `Move ${count} selected right` : `Move ${count} selected left`}\n disabled={disabled}\n onClick={() => ctx.move(direction)}\n className={cn(\"rotate-90 sm:rotate-0\", className)}\n {...props}\n >\n {children ?? directionIcon[direction]}\n </Button>\n )\n}\n\n/** Moves every visible item in `direction`. Polymorphic via `asChild`. */\nfunction TransferListMoveAllTrigger({\n direction,\n asChild,\n className,\n children,\n ...props\n}: React.ComponentProps<typeof Button> & { direction: TransferListDirection; asChild?: boolean }) {\n const ctx = useTransferList()\n const from = sideOf(direction)\n const disabled = ctx.disabled || ctx.visible[from].every((i) => ctx.itemDisabled(i))\n if (asChild) {\n return (\n <ark.button\n asChild\n data-slot=\"transfer-list-move-all-trigger\"\n data-direction={direction}\n disabled={disabled}\n onClick={() => ctx.moveAll(direction)}\n {...(props as React.ComponentProps<typeof ark.button>)}\n >\n {children}\n </ark.button>\n )\n }\n return (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n data-slot=\"transfer-list-move-all-trigger\"\n data-direction={direction}\n aria-label={direction === \"right\" ? \"Move all right\" : \"Move all left\"}\n disabled={disabled}\n onClick={() => ctx.moveAll(direction)}\n className={cn(\"rotate-90 sm:rotate-0\", className)}\n {...props}\n >\n {children ?? directionAllIcon[direction]}\n </Button>\n )\n}\n\nexport {\n TransferList,\n TransferListControls,\n TransferListEmpty,\n TransferListItem,\n TransferListItemIndicator,\n TransferListItemText,\n TransferListItems,\n TransferListMoveAllTrigger,\n TransferListMoveTrigger,\n TransferListPanel,\n TransferListPanelCount,\n TransferListPanelHeader,\n TransferListPanelTitle,\n TransferListSearch,\n TransferListSelectAll,\n useTransferList,\n type TransferListDirection,\n type TransferListItemBase,\n type TransferListProps,\n type TransferListSide,\n type TransferListValueChangeDetails,\n}\n"
|
|
39
647
|
}
|
|
40
648
|
],
|
|
41
649
|
"docs": "## Transfer list\n`src/components/ui/transfer-list.tsx` moves items between two lists. `TransferList { items,\nitemToValue, itemToString, itemDisabled, value/defaultValue/onValueChange({ value, items, moved,\ndirection }), filter, disabled, titles }` owns the per-side search, selection (ticks), and focus;\n`value` is the target side and is always kept in catalog order (moving never reorders). Parts:\n`TransferListPanel side=\"source\"|\"target\"` > `TransferListPanelHeader` > `TransferListSelectAll`\n(toolkit Checkbox over the visible enabled rows, indeterminate) + `TransferListPanelTitle` +\n`TransferListPanelCount` (\"selected / total\"), `TransferListSearch` (per side; select-all and\nmove-all respect it), `TransferListItems` (`role=\"listbox\"`, roving focus; ↑/↓, Shift+↑/↓ and\nShift+click extend, Space ticks, Enter or double-click moves the focused item across, ⌘/Ctrl+A\nticks all visible; render-prop child customises the label) > `TransferListItem item=`\n(`data-selected/disabled`) > `TransferListItemIndicator` (checkbox-styled span) +\n`TransferListItemText`, `TransferListEmpty`; `TransferListControls` > `TransferListMoveTrigger\ndirection=\"right\"|\"left\"` (selected items; disabled when nothing is ticked) and\n`TransferListMoveAllTrigger` (all visible), both polymorphic via `asChild`; a live region announces\nmoves. Disabled items never move. `/access` has the reference (permissions with group badges)."
|