@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
|
@@ -21,6 +21,747 @@
|
|
|
21
21
|
"createTreeCollection",
|
|
22
22
|
"useTreeSelect"
|
|
23
23
|
],
|
|
24
|
+
"parts": [
|
|
25
|
+
{
|
|
26
|
+
"name": "TreeSelect",
|
|
27
|
+
"props": [
|
|
28
|
+
{
|
|
29
|
+
"name": "children",
|
|
30
|
+
"type": "ReactNode",
|
|
31
|
+
"required": true,
|
|
32
|
+
"description": "Content rendered inside the part.",
|
|
33
|
+
"from": "arkcn"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "collection",
|
|
37
|
+
"type": "TreeCollection<T>",
|
|
38
|
+
"required": true,
|
|
39
|
+
"description": "The collection of items to render.",
|
|
40
|
+
"from": "arkcn"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "closeOnSelect",
|
|
44
|
+
"type": "boolean",
|
|
45
|
+
"required": false,
|
|
46
|
+
"default": "!multiple",
|
|
47
|
+
"description": "Close after a pick. Defaults to `!multiple`.",
|
|
48
|
+
"from": "arkcn"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "defaultExpandedValue",
|
|
52
|
+
"type": "string[]",
|
|
53
|
+
"required": false,
|
|
54
|
+
"default": "[]",
|
|
55
|
+
"description": "Branches open initially when uncontrolled.",
|
|
56
|
+
"from": "arkcn"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "defaultOpen",
|
|
60
|
+
"type": "boolean",
|
|
61
|
+
"required": false,
|
|
62
|
+
"default": "false",
|
|
63
|
+
"description": "The initial open state when uncontrolled.",
|
|
64
|
+
"from": "arkcn"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"name": "defaultValue",
|
|
68
|
+
"type": "string[]",
|
|
69
|
+
"required": false,
|
|
70
|
+
"default": "[]",
|
|
71
|
+
"description": "The initial value when uncontrolled.",
|
|
72
|
+
"from": "arkcn"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "disabled",
|
|
76
|
+
"type": "boolean",
|
|
77
|
+
"required": false,
|
|
78
|
+
"default": "false",
|
|
79
|
+
"description": "Disables the part and everything inside it.",
|
|
80
|
+
"from": "arkcn"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "expandedValue",
|
|
84
|
+
"type": "string[]",
|
|
85
|
+
"required": false,
|
|
86
|
+
"description": "Controlled open branches.",
|
|
87
|
+
"from": "arkcn"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"name": "invalid",
|
|
91
|
+
"type": "boolean",
|
|
92
|
+
"required": false,
|
|
93
|
+
"default": "false",
|
|
94
|
+
"description": "Marks the part invalid for styling and assistive technology.",
|
|
95
|
+
"from": "arkcn"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "multiple",
|
|
99
|
+
"type": "boolean",
|
|
100
|
+
"required": false,
|
|
101
|
+
"default": "false",
|
|
102
|
+
"description": "Checkbox selection of leaves; a branch stands for all of its leaves.",
|
|
103
|
+
"from": "arkcn"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"name": "onExpandedChange",
|
|
107
|
+
"type": "((details: { expandedValue: string[]; }) => void)",
|
|
108
|
+
"required": false,
|
|
109
|
+
"description": "Called when branches open or close.",
|
|
110
|
+
"from": "arkcn"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "onOpenChange",
|
|
114
|
+
"type": "((details: { open: boolean; }) => void)",
|
|
115
|
+
"required": false,
|
|
116
|
+
"description": "Called when the open state changes.",
|
|
117
|
+
"from": "arkcn"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"name": "onValueChange",
|
|
121
|
+
"type": "((details: TreeSelectValueChangeDetails<T>) => void)",
|
|
122
|
+
"required": false,
|
|
123
|
+
"description": "Called with the new value whenever it changes.",
|
|
124
|
+
"from": "arkcn"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"name": "open",
|
|
128
|
+
"type": "boolean",
|
|
129
|
+
"required": false,
|
|
130
|
+
"description": "The controlled open state.",
|
|
131
|
+
"from": "arkcn"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"name": "readOnly",
|
|
135
|
+
"type": "boolean",
|
|
136
|
+
"required": false,
|
|
137
|
+
"default": "false",
|
|
138
|
+
"description": "Shows the value but prevents changes.",
|
|
139
|
+
"from": "arkcn"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"name": "selectableBranches",
|
|
143
|
+
"type": "boolean",
|
|
144
|
+
"required": false,
|
|
145
|
+
"default": "false",
|
|
146
|
+
"description": "Single mode only: allow picking a branch itself instead of expanding it.",
|
|
147
|
+
"from": "arkcn"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"name": "value",
|
|
151
|
+
"type": "string[]",
|
|
152
|
+
"required": false,
|
|
153
|
+
"description": "The controlled value.",
|
|
154
|
+
"from": "arkcn"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"name": "autoFocus",
|
|
158
|
+
"type": "boolean",
|
|
159
|
+
"required": false,
|
|
160
|
+
"default": "true",
|
|
161
|
+
"description": "Whether to automatically set focus on the first focusable\ncontent within the popover when opened.",
|
|
162
|
+
"from": "@zag-js/popover"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"name": "closeOnEscape",
|
|
166
|
+
"type": "boolean",
|
|
167
|
+
"required": false,
|
|
168
|
+
"default": "true",
|
|
169
|
+
"description": "Whether to close the popover when the escape key is pressed.",
|
|
170
|
+
"from": "@zag-js/popover"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"name": "closeOnInteractOutside",
|
|
174
|
+
"type": "boolean",
|
|
175
|
+
"required": false,
|
|
176
|
+
"default": "true",
|
|
177
|
+
"description": "Whether to close the popover when the user clicks outside of the popover.",
|
|
178
|
+
"from": "@zag-js/popover"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"name": "defaultTriggerValue",
|
|
182
|
+
"type": "string | null",
|
|
183
|
+
"required": false,
|
|
184
|
+
"description": "The initial trigger value when rendered.\nUse when you don't need to control the trigger value.",
|
|
185
|
+
"from": "@zag-js/popover"
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"name": "finalFocusEl",
|
|
189
|
+
"type": "(() => MaybeElement)",
|
|
190
|
+
"required": false,
|
|
191
|
+
"description": "Element to receive focus when the popover is closed.",
|
|
192
|
+
"from": "@zag-js/popover"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"name": "hideMode",
|
|
196
|
+
"type": "HideMode",
|
|
197
|
+
"required": false,
|
|
198
|
+
"default": "'display-none'",
|
|
199
|
+
"description": "How to hide content when mounted but not present.\n- `'display-none'`: HTML `hidden` attribute. Effects stay alive.\n- `'activity'`: React 19 `<Activity mode=\"hidden\">`. Effects pause. Requires React 19+.",
|
|
200
|
+
"from": "@ark-ui/react"
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"name": "id",
|
|
204
|
+
"type": "string",
|
|
205
|
+
"required": false,
|
|
206
|
+
"description": "The unique identifier of the machine.",
|
|
207
|
+
"from": "@zag-js/types"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"name": "ids",
|
|
211
|
+
"type": "ElementIds",
|
|
212
|
+
"required": false,
|
|
213
|
+
"description": "The ids of the elements in the popover. Useful for composition.",
|
|
214
|
+
"from": "@zag-js/popover"
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"name": "immediate",
|
|
218
|
+
"type": "boolean",
|
|
219
|
+
"required": false,
|
|
220
|
+
"description": "Whether to synchronize the present change immediately or defer it to the next frame",
|
|
221
|
+
"from": "@zag-js/presence"
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"name": "initialFocusEl",
|
|
225
|
+
"type": "(() => HTMLElement | null)",
|
|
226
|
+
"required": false,
|
|
227
|
+
"description": "The element to focus on when the popover is opened.",
|
|
228
|
+
"from": "@zag-js/popover"
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"name": "lazyMount",
|
|
232
|
+
"type": "boolean",
|
|
233
|
+
"required": false,
|
|
234
|
+
"default": "true",
|
|
235
|
+
"description": "Whether to enable lazy mounting",
|
|
236
|
+
"from": "@ark-ui/react"
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"name": "modal",
|
|
240
|
+
"type": "boolean",
|
|
241
|
+
"required": false,
|
|
242
|
+
"default": "false",
|
|
243
|
+
"description": "Whether the popover should be modal. When set to `true`:\n- interaction with outside elements will be disabled\n- only popover content will be visible to screen readers\n- scrolling is blocked\n- focus is trapped within the popover",
|
|
244
|
+
"from": "@zag-js/popover"
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"name": "onEnterComplete",
|
|
248
|
+
"type": "VoidFunction",
|
|
249
|
+
"required": false,
|
|
250
|
+
"description": "Function called when the animation ends in the open state",
|
|
251
|
+
"from": "@zag-js/presence"
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"name": "onEscapeKeyDown",
|
|
255
|
+
"type": "((event: KeyboardEvent) => void)",
|
|
256
|
+
"required": false,
|
|
257
|
+
"description": "Function called when the escape key is pressed",
|
|
258
|
+
"from": "@zag-js/dismissable"
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"name": "onExitComplete",
|
|
262
|
+
"type": "VoidFunction",
|
|
263
|
+
"required": false,
|
|
264
|
+
"description": "Function called when the animation ends in the closed state",
|
|
265
|
+
"from": "@zag-js/presence"
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"name": "onFocusOutside",
|
|
269
|
+
"type": "((event: FocusOutsideEvent) => void)",
|
|
270
|
+
"required": false,
|
|
271
|
+
"description": "Function called when the focus is moved outside the component",
|
|
272
|
+
"from": "@zag-js/interact-outside"
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"name": "onInteractOutside",
|
|
276
|
+
"type": "((event: InteractOutsideEvent) => void)",
|
|
277
|
+
"required": false,
|
|
278
|
+
"description": "Function called when an interaction happens outside the component",
|
|
279
|
+
"from": "@zag-js/interact-outside"
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
"name": "onPointerDownOutside",
|
|
283
|
+
"type": "((event: PointerDownOutsideEvent) => void)",
|
|
284
|
+
"required": false,
|
|
285
|
+
"description": "Function called when the pointer is pressed down outside the component",
|
|
286
|
+
"from": "@zag-js/interact-outside"
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
"name": "onRequestDismiss",
|
|
290
|
+
"type": "((event: LayerDismissEvent) => void)",
|
|
291
|
+
"required": false,
|
|
292
|
+
"description": "Function called when this layer is closed due to a parent layer being closed",
|
|
293
|
+
"from": "@zag-js/dismissable"
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"name": "onTriggerValueChange",
|
|
297
|
+
"type": "((details: TriggerValueChangeDetails) => void)",
|
|
298
|
+
"required": false,
|
|
299
|
+
"description": "Function called when the trigger value changes.",
|
|
300
|
+
"from": "@zag-js/popover"
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"name": "persistentElements",
|
|
304
|
+
"type": "(() => Element | null)[]",
|
|
305
|
+
"required": false,
|
|
306
|
+
"description": "Returns the persistent elements that:\n- should not have pointer-events disabled\n- should not trigger the dismiss event",
|
|
307
|
+
"from": "@zag-js/dismissable"
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"name": "portalled",
|
|
311
|
+
"type": "boolean",
|
|
312
|
+
"required": false,
|
|
313
|
+
"default": "true",
|
|
314
|
+
"description": "Whether the popover is portalled. This will proxy the tabbing behavior regardless of the DOM position\nof the popover content.",
|
|
315
|
+
"from": "@zag-js/popover"
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
"name": "positioning",
|
|
319
|
+
"type": "PositioningOptions",
|
|
320
|
+
"required": false,
|
|
321
|
+
"description": "The user provided options used to position the popover content",
|
|
322
|
+
"from": "@zag-js/popover"
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
"name": "present",
|
|
326
|
+
"type": "boolean",
|
|
327
|
+
"required": false,
|
|
328
|
+
"description": "Whether the node is present (controlled by the user)",
|
|
329
|
+
"from": "@zag-js/presence"
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
"name": "restoreFocus",
|
|
333
|
+
"type": "boolean",
|
|
334
|
+
"required": false,
|
|
335
|
+
"default": "true",
|
|
336
|
+
"description": "Whether to restore focus to the element that had focus before the popover was opened.",
|
|
337
|
+
"from": "@zag-js/popover"
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
"name": "skipAnimationOnMount",
|
|
341
|
+
"type": "boolean",
|
|
342
|
+
"required": false,
|
|
343
|
+
"default": "false",
|
|
344
|
+
"description": "Whether to allow the initial presence animation.",
|
|
345
|
+
"from": "@ark-ui/react"
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
"name": "translations",
|
|
349
|
+
"type": "IntlTranslations",
|
|
350
|
+
"required": false,
|
|
351
|
+
"description": "Specifies the localized strings that identifies the accessibility elements and their states",
|
|
352
|
+
"from": "@zag-js/popover"
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
"name": "triggerValue",
|
|
356
|
+
"type": "string | null",
|
|
357
|
+
"required": false,
|
|
358
|
+
"description": "The controlled trigger value",
|
|
359
|
+
"from": "@zag-js/popover"
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
"name": "unmountOnExit",
|
|
363
|
+
"type": "boolean",
|
|
364
|
+
"required": false,
|
|
365
|
+
"default": "true",
|
|
366
|
+
"description": "Whether to unmount on exit.",
|
|
367
|
+
"from": "@ark-ui/react"
|
|
368
|
+
}
|
|
369
|
+
]
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
"name": "TreeSelectChip",
|
|
373
|
+
"props": [
|
|
374
|
+
{
|
|
375
|
+
"name": "node",
|
|
376
|
+
"type": "T",
|
|
377
|
+
"required": true,
|
|
378
|
+
"description": "The tree node this part renders.",
|
|
379
|
+
"from": "arkcn"
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
"name": "showRemove",
|
|
383
|
+
"type": "boolean",
|
|
384
|
+
"required": false,
|
|
385
|
+
"default": "true",
|
|
386
|
+
"description": "Render the remove button on the chip.",
|
|
387
|
+
"from": "arkcn"
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
"name": "children",
|
|
391
|
+
"type": "ReactNode",
|
|
392
|
+
"required": false,
|
|
393
|
+
"description": "Content rendered inside the part.",
|
|
394
|
+
"from": "@types/react"
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
"name": "className",
|
|
398
|
+
"type": "string",
|
|
399
|
+
"required": false,
|
|
400
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
401
|
+
"from": "@types/react"
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
"name": "style",
|
|
405
|
+
"type": "CSSProperties",
|
|
406
|
+
"required": false,
|
|
407
|
+
"description": "Inline styles applied to the rendered element.",
|
|
408
|
+
"from": "@types/react"
|
|
409
|
+
}
|
|
410
|
+
]
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
"name": "TreeSelectChips",
|
|
414
|
+
"props": [
|
|
415
|
+
{
|
|
416
|
+
"name": "children",
|
|
417
|
+
"type": "((node: T) => ReactNode)",
|
|
418
|
+
"required": false,
|
|
419
|
+
"description": "Content rendered inside the part.",
|
|
420
|
+
"from": "arkcn"
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
"name": "placeholder",
|
|
424
|
+
"type": "ReactNode",
|
|
425
|
+
"required": false,
|
|
426
|
+
"description": "Text shown while there is no value.",
|
|
427
|
+
"from": "arkcn"
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
"name": "className",
|
|
431
|
+
"type": "string",
|
|
432
|
+
"required": false,
|
|
433
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
434
|
+
"from": "@types/react"
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
"name": "style",
|
|
438
|
+
"type": "CSSProperties",
|
|
439
|
+
"required": false,
|
|
440
|
+
"description": "Inline styles applied to the rendered element.",
|
|
441
|
+
"from": "@types/react"
|
|
442
|
+
}
|
|
443
|
+
]
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
"name": "TreeSelectClearTrigger",
|
|
447
|
+
"props": [
|
|
448
|
+
{
|
|
449
|
+
"name": "asChild",
|
|
450
|
+
"type": "boolean",
|
|
451
|
+
"required": false,
|
|
452
|
+
"description": "Render the child element instead of the default one, merging props and behaviour onto it.",
|
|
453
|
+
"from": "arkcn"
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
"name": "size",
|
|
457
|
+
"type": "\"default\" | \"xs\" | \"sm\" | \"lg\" | \"icon\" | \"icon-xs\" | \"icon-sm\" | \"icon-lg\" | null",
|
|
458
|
+
"required": false,
|
|
459
|
+
"description": "Size variant; sets a `data-size` attribute.",
|
|
460
|
+
"from": "arkcn"
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
"name": "variant",
|
|
464
|
+
"type": "\"link\" | \"default\" | \"outline\" | \"secondary\" | \"ghost\" | \"destructive\" | null",
|
|
465
|
+
"required": false,
|
|
466
|
+
"description": "Visual variant, styled with `cva`; sets a `data-variant` attribute.",
|
|
467
|
+
"from": "arkcn"
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
"name": "children",
|
|
471
|
+
"type": "ReactNode",
|
|
472
|
+
"required": false,
|
|
473
|
+
"description": "Content rendered inside the part.",
|
|
474
|
+
"from": "@types/react"
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
"name": "className",
|
|
478
|
+
"type": "string",
|
|
479
|
+
"required": false,
|
|
480
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
481
|
+
"from": "@types/react"
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
"name": "style",
|
|
485
|
+
"type": "CSSProperties",
|
|
486
|
+
"required": false,
|
|
487
|
+
"description": "Inline styles applied to the rendered element.",
|
|
488
|
+
"from": "@types/react"
|
|
489
|
+
}
|
|
490
|
+
]
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
"name": "TreeSelectContent",
|
|
494
|
+
"props": [
|
|
495
|
+
{
|
|
496
|
+
"name": "asChild",
|
|
497
|
+
"type": "boolean",
|
|
498
|
+
"required": false,
|
|
499
|
+
"description": "Use the provided child element as the default rendered element, combining their props and behavior.",
|
|
500
|
+
"from": "@ark-ui/react"
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
"name": "children",
|
|
504
|
+
"type": "ReactNode",
|
|
505
|
+
"required": false,
|
|
506
|
+
"description": "Content rendered inside the part.",
|
|
507
|
+
"from": "@types/react"
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
"name": "className",
|
|
511
|
+
"type": "string",
|
|
512
|
+
"required": false,
|
|
513
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
514
|
+
"from": "@types/react"
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
"name": "style",
|
|
518
|
+
"type": "CSSProperties",
|
|
519
|
+
"required": false,
|
|
520
|
+
"description": "Inline styles applied to the rendered element.",
|
|
521
|
+
"from": "@types/react"
|
|
522
|
+
}
|
|
523
|
+
]
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
"name": "TreeSelectEmpty",
|
|
527
|
+
"props": [
|
|
528
|
+
{
|
|
529
|
+
"name": "children",
|
|
530
|
+
"type": "ReactNode",
|
|
531
|
+
"required": false,
|
|
532
|
+
"description": "Content rendered inside the part.",
|
|
533
|
+
"from": "@types/react"
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
"name": "className",
|
|
537
|
+
"type": "string",
|
|
538
|
+
"required": false,
|
|
539
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
540
|
+
"from": "@types/react"
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
"name": "style",
|
|
544
|
+
"type": "CSSProperties",
|
|
545
|
+
"required": false,
|
|
546
|
+
"description": "Inline styles applied to the rendered element.",
|
|
547
|
+
"from": "@types/react"
|
|
548
|
+
}
|
|
549
|
+
]
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
"name": "TreeSelectHiddenInput",
|
|
553
|
+
"props": [
|
|
554
|
+
{
|
|
555
|
+
"name": "children",
|
|
556
|
+
"type": "ReactNode",
|
|
557
|
+
"required": false,
|
|
558
|
+
"description": "Content rendered inside the part.",
|
|
559
|
+
"from": "@types/react"
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
"name": "className",
|
|
563
|
+
"type": "string",
|
|
564
|
+
"required": false,
|
|
565
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
566
|
+
"from": "@types/react"
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
"name": "style",
|
|
570
|
+
"type": "CSSProperties",
|
|
571
|
+
"required": false,
|
|
572
|
+
"description": "Inline styles applied to the rendered element.",
|
|
573
|
+
"from": "@types/react"
|
|
574
|
+
}
|
|
575
|
+
]
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
"name": "TreeSelectIndicator",
|
|
579
|
+
"props": [
|
|
580
|
+
{
|
|
581
|
+
"name": "children",
|
|
582
|
+
"type": "ReactNode",
|
|
583
|
+
"required": false,
|
|
584
|
+
"description": "Content rendered inside the part.",
|
|
585
|
+
"from": "@types/react"
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
"name": "className",
|
|
589
|
+
"type": "string",
|
|
590
|
+
"required": false,
|
|
591
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
592
|
+
"from": "@types/react"
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
"name": "style",
|
|
596
|
+
"type": "CSSProperties",
|
|
597
|
+
"required": false,
|
|
598
|
+
"description": "Inline styles applied to the rendered element.",
|
|
599
|
+
"from": "@types/react"
|
|
600
|
+
}
|
|
601
|
+
]
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
"name": "TreeSelectNode",
|
|
605
|
+
"props": [
|
|
606
|
+
{
|
|
607
|
+
"name": "indexPath",
|
|
608
|
+
"type": "number[]",
|
|
609
|
+
"required": true,
|
|
610
|
+
"description": "Position of the node in the tree.",
|
|
611
|
+
"from": "arkcn"
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
"name": "node",
|
|
615
|
+
"type": "T",
|
|
616
|
+
"required": true,
|
|
617
|
+
"description": "The tree node this part renders.",
|
|
618
|
+
"from": "arkcn"
|
|
619
|
+
}
|
|
620
|
+
]
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
"name": "TreeSelectSearch",
|
|
624
|
+
"props": [
|
|
625
|
+
{
|
|
626
|
+
"name": "children",
|
|
627
|
+
"type": "ReactNode",
|
|
628
|
+
"required": false,
|
|
629
|
+
"description": "Content rendered inside the part.",
|
|
630
|
+
"from": "@types/react"
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
"name": "className",
|
|
634
|
+
"type": "string",
|
|
635
|
+
"required": false,
|
|
636
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
637
|
+
"from": "@types/react"
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
"name": "style",
|
|
641
|
+
"type": "CSSProperties",
|
|
642
|
+
"required": false,
|
|
643
|
+
"description": "Inline styles applied to the rendered element.",
|
|
644
|
+
"from": "@types/react"
|
|
645
|
+
}
|
|
646
|
+
]
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
"name": "TreeSelectTree",
|
|
650
|
+
"props": [
|
|
651
|
+
{
|
|
652
|
+
"name": "children",
|
|
653
|
+
"type": "TreeSelectRenderNode<T>",
|
|
654
|
+
"required": false,
|
|
655
|
+
"description": "Content rendered inside the part.",
|
|
656
|
+
"from": "arkcn"
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
"name": "asChild",
|
|
660
|
+
"type": "boolean",
|
|
661
|
+
"required": false,
|
|
662
|
+
"description": "Use the provided child element as the default rendered element, combining their props and behavior.",
|
|
663
|
+
"from": "@ark-ui/react"
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
"name": "className",
|
|
667
|
+
"type": "string",
|
|
668
|
+
"required": false,
|
|
669
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
670
|
+
"from": "@types/react"
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
"name": "style",
|
|
674
|
+
"type": "CSSProperties",
|
|
675
|
+
"required": false,
|
|
676
|
+
"description": "Inline styles applied to the rendered element.",
|
|
677
|
+
"from": "@types/react"
|
|
678
|
+
}
|
|
679
|
+
]
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
"name": "TreeSelectTrigger",
|
|
683
|
+
"props": [
|
|
684
|
+
{
|
|
685
|
+
"name": "size",
|
|
686
|
+
"type": "\"default\" | \"sm\"",
|
|
687
|
+
"required": false,
|
|
688
|
+
"default": "\"default\"",
|
|
689
|
+
"description": "Size variant; sets a `data-size` attribute.",
|
|
690
|
+
"from": "arkcn"
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
"name": "variant",
|
|
694
|
+
"type": "\"default\" | \"unstyled\"",
|
|
695
|
+
"required": false,
|
|
696
|
+
"default": "\"default\"",
|
|
697
|
+
"description": "Visual variant, styled with `cva`; sets a `data-variant` attribute.",
|
|
698
|
+
"from": "arkcn"
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
"name": "children",
|
|
702
|
+
"type": "ReactNode",
|
|
703
|
+
"required": false,
|
|
704
|
+
"description": "Content rendered inside the part.",
|
|
705
|
+
"from": "@types/react"
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
"name": "className",
|
|
709
|
+
"type": "string",
|
|
710
|
+
"required": false,
|
|
711
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
712
|
+
"from": "@types/react"
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
"name": "style",
|
|
716
|
+
"type": "CSSProperties",
|
|
717
|
+
"required": false,
|
|
718
|
+
"description": "Inline styles applied to the rendered element.",
|
|
719
|
+
"from": "@types/react"
|
|
720
|
+
}
|
|
721
|
+
]
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
"name": "TreeSelectValue",
|
|
725
|
+
"props": [
|
|
726
|
+
{
|
|
727
|
+
"name": "children",
|
|
728
|
+
"type": "((nodes: T[]) => ReactNode)",
|
|
729
|
+
"required": false,
|
|
730
|
+
"description": "Content rendered inside the part.",
|
|
731
|
+
"from": "arkcn"
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
"name": "placeholder",
|
|
735
|
+
"type": "ReactNode",
|
|
736
|
+
"required": false,
|
|
737
|
+
"description": "Text shown while there is no value.",
|
|
738
|
+
"from": "arkcn"
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
"name": "separator",
|
|
742
|
+
"type": "string",
|
|
743
|
+
"required": false,
|
|
744
|
+
"default": "\", \"",
|
|
745
|
+
"description": "Rendered between the selected labels.",
|
|
746
|
+
"from": "arkcn"
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
"name": "className",
|
|
750
|
+
"type": "string",
|
|
751
|
+
"required": false,
|
|
752
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
753
|
+
"from": "@types/react"
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
"name": "style",
|
|
757
|
+
"type": "CSSProperties",
|
|
758
|
+
"required": false,
|
|
759
|
+
"description": "Inline styles applied to the rendered element.",
|
|
760
|
+
"from": "@types/react"
|
|
761
|
+
}
|
|
762
|
+
]
|
|
763
|
+
}
|
|
764
|
+
],
|
|
24
765
|
"dependencies": {
|
|
25
766
|
"@ark-ui/react": "^5.39.1",
|
|
26
767
|
"lucide-react": "^1.41.0"
|
|
@@ -36,7 +777,7 @@
|
|
|
36
777
|
"files": [
|
|
37
778
|
{
|
|
38
779
|
"path": "components/ui/tree-select.tsx",
|
|
39
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Popover as PopoverPrimitive, createTreeCollection, type TreeCollection, type TreeNode } from \"@ark-ui/react\"\nimport { ChevronDownIcon, SearchIcon, XIcon } from \"lucide-react\"\nimport { useControllable } from \"@/lib/controllable\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { InputGroup, InputGroupAddon, InputGroupInput } from \"@/components/ui/input-group\"\nimport { PopoverContent, PopoverTrigger } from \"@/components/ui/popover\"\nimport {\n TreeView,\n TreeViewBranch,\n TreeViewBranchContent,\n TreeViewBranchControl,\n TreeViewBranchIndentGuide,\n TreeViewBranchIndicator,\n TreeViewBranchText,\n TreeViewBranchTrigger,\n TreeViewItem,\n TreeViewItemIndicator,\n TreeViewItemText,\n TreeViewNodeCheckbox,\n TreeViewNodeProvider,\n TreeViewTree,\n} from \"@/components/ui/tree-view\"\n\n/* -------------------------------- helpers -------------------------------- */\n\nconst uniq = <T,>(list: T[]) => Array.from(new Set(list))\n\n/**\n * Collapses a list of selected leaf values into the top-most nodes that are fully\n * selected, so \"Engineering\" stands in for every engineer under it.\n */\nfunction collapseValues<T extends TreeNode>(collection: TreeCollection<T>, value: string[]): T[] {\n const selected = new Set(value)\n const out: T[] = []\n const walk = (node: T) => {\n for (const child of collection.getNodeChildren(node)) {\n const v = collection.getNodeValue(child)\n if (collection.isBranchNode(child)) {\n const leaves = collection.getDescendantValues(v)\n if (leaves.length > 0 && leaves.every((l) => selected.has(l))) out.push(child)\n else walk(child)\n } else if (selected.has(v)) out.push(child)\n }\n }\n walk(collection.rootNode)\n return out\n}\n\n/* -------------------------------- context -------------------------------- */\n\ntype TreeSelectValueChangeDetails<T extends TreeNode = TreeNode> = { value: string[]; nodes: T[] }\n\ntype TreeSelectContextValue<T extends TreeNode = TreeNode> = {\n collection: TreeCollection<T>\n /** The collection narrowed by the search query (ancestors of matches kept). */\n filtered: TreeCollection<T>\n value: string[]\n setValue: (value: string[]) => void\n multiple: boolean\n selectableBranches: boolean\n closeOnSelect: boolean\n expandedValue: string[]\n setExpandedValue: (value: string[]) => void\n open: boolean\n setOpen: (open: boolean) => void\n query: string\n setQuery: (query: string) => void\n disabled: boolean\n readOnly: boolean\n invalid: boolean\n /** Toggle (multiple) or pick (single) a node, with branch semantics. */\n toggle: (node: T) => void\n /** Remove one value, or every leaf under a branch value. */\n remove: (value: string) => void\n clear: () => void\n selectedNodes: T[]\n /** Selected nodes with fully selected branches collapsed to the branch. */\n displayNodes: T[]\n contentRef: React.RefObject<HTMLDivElement | null>\n}\n\nconst TreeSelectContext = React.createContext<TreeSelectContextValue | null>(null)\n\nfunction useTreeSelect<T extends TreeNode = TreeNode>() {\n const ctx = React.useContext(TreeSelectContext)\n if (!ctx) throw new Error(\"TreeSelect parts must be used inside <TreeSelect>\")\n return ctx as unknown as TreeSelectContextValue<T>\n}\n\n/* ---------------------------------- root --------------------------------- */\n\ntype TreeSelectProps<T extends TreeNode> = Omit<\n React.ComponentProps<typeof PopoverPrimitive.Root>,\n \"open\" | \"defaultOpen\" | \"onOpenChange\" | \"children\"\n> & {\n collection: TreeCollection<T>\n value?: string[]\n defaultValue?: string[]\n onValueChange?: (details: TreeSelectValueChangeDetails<T>) => void\n /** Checkbox selection of leaves; a branch stands for all of its leaves. */\n multiple?: boolean\n /** Single mode only: allow picking a branch itself instead of expanding it. */\n selectableBranches?: boolean\n /** Close after a pick. Defaults to `!multiple`. */\n closeOnSelect?: boolean\n expandedValue?: string[]\n defaultExpandedValue?: string[]\n onExpandedChange?: (details: { expandedValue: string[] }) => void\n open?: boolean\n defaultOpen?: boolean\n onOpenChange?: (details: { open: boolean }) => void\n disabled?: boolean\n readOnly?: boolean\n invalid?: boolean\n children: React.ReactNode\n}\n\nfunction TreeSelect<T extends TreeNode>({\n collection,\n value: valueProp,\n defaultValue = [],\n onValueChange,\n multiple = false,\n selectableBranches = false,\n closeOnSelect = !multiple,\n expandedValue: expandedProp,\n defaultExpandedValue = [],\n onExpandedChange,\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n disabled = false,\n readOnly = false,\n invalid = false,\n positioning,\n lazyMount = true,\n unmountOnExit = true,\n children,\n ...props\n}: TreeSelectProps<T>) {\n const [value, setValueState] = useControllable<string[]>(valueProp, defaultValue, (next) =>\n onValueChange?.({ value: next, nodes: collection.findNodes(next) })\n )\n const [expandedValue, setExpandedValue] = useControllable<string[]>(expandedProp, defaultExpandedValue, (next) =>\n onExpandedChange?.({ expandedValue: next })\n )\n const [open, setOpenState] = useControllable<boolean>(openProp, defaultOpen, (next) => onOpenChange?.({ open: next }))\n const [query, setQuery] = React.useState(\"\")\n const contentRef = React.useRef<HTMLDivElement | null>(null)\n\n const setOpen = React.useCallback(\n (next: boolean) => {\n if (disabled) return\n setOpenState(next)\n if (!next) setQuery(\"\")\n },\n [disabled, setOpenState]\n )\n const setValue = React.useCallback(\n (next: string[]) => {\n if (readOnly) return\n setValueState(collection.sort(uniq(next)))\n },\n [collection, readOnly, setValueState]\n )\n\n const filtered = React.useMemo(() => {\n const q = query.trim().toLowerCase()\n if (!q) return collection\n return collection.filter((node) => collection.stringifyNode(node).toLowerCase().includes(q))\n }, [collection, query])\n\n const toggle = React.useCallback(\n (node: T) => {\n const v = collection.getNodeValue(node)\n const isBranch = collection.isBranchNode(node)\n if (multiple) {\n if (isBranch) {\n const leaves = collection.getDescendantValues(v)\n const all = leaves.length > 0 && leaves.every((l) => value.includes(l))\n setValue(all ? value.filter((x) => !leaves.includes(x)) : [...value, ...leaves])\n } else {\n setValue(value.includes(v) ? value.filter((x) => x !== v) : [...value, v])\n }\n if (closeOnSelect) setOpen(false)\n return\n }\n if (isBranch && !selectableBranches) return\n setValue([v])\n if (closeOnSelect) setOpen(false)\n },\n [collection, multiple, selectableBranches, closeOnSelect, value, setValue, setOpen]\n )\n const remove = React.useCallback(\n (v: string) => {\n const node = collection.findNode(v)\n const drop = node && collection.isBranchNode(node) ? collection.getDescendantValues(v) : [v]\n setValue(value.filter((x) => !drop.includes(x)))\n },\n [collection, value, setValue]\n )\n const clear = React.useCallback(() => setValue([]), [setValue])\n\n const selectedNodes = React.useMemo(() => collection.findNodes(value), [collection, value])\n const displayNodes = React.useMemo(\n () => (multiple ? collapseValues(collection, value) : selectedNodes),\n [collection, multiple, value, selectedNodes]\n )\n\n const ctx: TreeSelectContextValue<T> = {\n collection,\n filtered,\n value,\n setValue,\n multiple,\n selectableBranches,\n closeOnSelect,\n expandedValue,\n setExpandedValue,\n open,\n setOpen,\n query,\n setQuery,\n disabled,\n readOnly,\n invalid,\n toggle,\n remove,\n clear,\n selectedNodes,\n displayNodes,\n contentRef,\n }\n\n return (\n <TreeSelectContext.Provider value={ctx as unknown as TreeSelectContextValue}>\n <PopoverPrimitive.Root\n open={open}\n onOpenChange={({ open: next }) => setOpen(next)}\n positioning={{ placement: \"bottom-start\", gutter: 4, sameWidth: true, ...positioning }}\n lazyMount={lazyMount}\n unmountOnExit={unmountOnExit}\n {...props}\n >\n {children}\n </PopoverPrimitive.Root>\n </TreeSelectContext.Provider>\n )\n}\n\n/* -------------------------------- trigger -------------------------------- */\n\nfunction TreeSelectTrigger({\n className,\n size = \"default\",\n variant = \"default\",\n children,\n onKeyDown,\n ...props\n}: React.ComponentProps<\"div\"> & { size?: \"sm\" | \"default\"; variant?: \"default\" | \"unstyled\" }) {\n const ctx = useTreeSelect()\n return (\n <PopoverTrigger asChild>\n <div\n data-slot=\"tree-select-trigger\"\n role=\"combobox\"\n aria-haspopup=\"tree\"\n aria-expanded={ctx.open}\n aria-disabled={ctx.disabled || undefined}\n aria-invalid={ctx.invalid || undefined}\n aria-readonly={ctx.readOnly || undefined}\n tabIndex={ctx.disabled ? -1 : 0}\n data-size={size}\n data-variant={variant}\n data-state={ctx.open ? \"open\" : \"closed\"}\n data-disabled={ctx.disabled ? \"\" : undefined}\n data-invalid={ctx.invalid ? \"\" : undefined}\n data-placeholder-shown={ctx.value.length === 0 ? \"\" : undefined}\n className={cn(\n \"flex cursor-default items-center justify-between gap-1.5 text-sm outline-none select-none data-placeholder-shown:text-muted-foreground data-disabled:cursor-not-allowed data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n variant === \"default\" &&\n \"w-full rounded-lg border border-input bg-transparent py-1 pr-2 pl-2.5 transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 data-invalid:border-destructive data-invalid:ring-3 data-invalid:ring-destructive/20 data-[size=default]:min-h-8 data-[size=sm]:min-h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] dark:bg-input/30 dark:hover:bg-input/50 dark:data-invalid:border-destructive/50 dark:data-invalid:ring-destructive/40\",\n className\n )}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (event.defaultPrevented || ctx.disabled) return\n if (event.key === \"Enter\" || event.key === \" \" || event.key === \"ArrowDown\") {\n event.preventDefault()\n ctx.setOpen(true)\n } else if (event.key === \"Backspace\" && ctx.multiple && ctx.value.length > 0) {\n const last = ctx.displayNodes[ctx.displayNodes.length - 1]\n if (last) ctx.remove(ctx.collection.getNodeValue(last))\n }\n }}\n {...props}\n >\n {children}\n </div>\n </PopoverTrigger>\n )\n}\n\nfunction TreeSelectIndicator({ className, children, ...props }: React.ComponentProps<\"span\">) {\n const ctx = useTreeSelect()\n return (\n <span\n data-slot=\"tree-select-indicator\"\n data-state={ctx.open ? \"open\" : \"closed\"}\n className={cn(\"ml-auto flex shrink-0 items-center text-muted-foreground\", className)}\n {...props}\n >\n {children ?? <ChevronDownIcon className=\"size-4\" />}\n </span>\n )\n}\n\n/** Selected labels as text (fully selected branches collapse to the branch), or the placeholder. */\nfunction TreeSelectValue<T extends TreeNode>({\n placeholder,\n separator = \", \",\n children,\n className,\n ...props\n}: Omit<React.ComponentProps<\"span\">, \"children\"> & {\n placeholder?: React.ReactNode\n separator?: string\n children?: (nodes: T[]) => React.ReactNode\n}) {\n const ctx = useTreeSelect<T>()\n const empty = ctx.displayNodes.length === 0\n return (\n <span\n data-slot=\"tree-select-value\"\n data-placeholder-shown={empty ? \"\" : undefined}\n className={cn(\"line-clamp-1 flex-1 text-left\", className)}\n {...props}\n >\n {empty\n ? placeholder\n : children\n ? children(ctx.displayNodes)\n : ctx.displayNodes.map((n) => ctx.collection.stringifyNode(n)).join(separator)}\n </span>\n )\n}\n\n/** Selected nodes as removable chips (multiple mode). */\nfunction TreeSelectChips<T extends TreeNode>({\n placeholder,\n children,\n className,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & {\n placeholder?: React.ReactNode\n children?: (node: T) => React.ReactNode\n}) {\n const ctx = useTreeSelect<T>()\n const empty = ctx.displayNodes.length === 0\n return (\n <div\n data-slot=\"tree-select-chips\"\n data-placeholder-shown={empty ? \"\" : undefined}\n className={cn(\"flex min-w-0 flex-1 flex-wrap items-center gap-1 text-left\", className)}\n {...props}\n >\n {empty\n ? placeholder\n : ctx.displayNodes.map((node) =>\n children ? (\n <React.Fragment key={ctx.collection.getNodeValue(node)}>{children(node)}</React.Fragment>\n ) : (\n <TreeSelectChip key={ctx.collection.getNodeValue(node)} node={node} />\n )\n )}\n </div>\n )\n}\n\nfunction TreeSelectChip<T extends TreeNode>({\n node,\n showRemove = true,\n className,\n children,\n ...props\n}: React.ComponentProps<\"span\"> & { node: T; showRemove?: boolean }) {\n const ctx = useTreeSelect<T>()\n const value = ctx.collection.getNodeValue(node)\n const label = ctx.collection.stringifyNode(node)\n return (\n <span\n data-slot=\"tree-select-chip\"\n data-value={value}\n data-branch={ctx.collection.isBranchNode(node) ? \"\" : undefined}\n className={cn(\n \"flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-data-[slot=tree-select-chip-remove]:pr-0\",\n className\n )}\n {...props}\n >\n {children ?? label}\n {showRemove && !ctx.readOnly && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n data-slot=\"tree-select-chip-remove\"\n aria-label={`Remove ${label}`}\n disabled={ctx.disabled}\n className=\"-ml-1 opacity-50 hover:opacity-100\"\n onClick={(event) => {\n event.stopPropagation()\n ctx.remove(value)\n }}\n onKeyDown={(event) => event.stopPropagation()}\n >\n <XIcon className=\"pointer-events-none\" />\n </Button>\n )}\n </span>\n )\n}\n\nfunction TreeSelectClearTrigger({ className, children, ...props }: React.ComponentProps<typeof Button>) {\n const ctx = useTreeSelect()\n if (ctx.value.length === 0 || ctx.readOnly) return null\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n data-slot=\"tree-select-clear-trigger\"\n aria-label=\"Clear selection\"\n disabled={ctx.disabled}\n className={cn(\"-mr-1 text-muted-foreground hover:text-foreground\", className)}\n onClick={(event) => {\n event.stopPropagation()\n ctx.clear()\n }}\n onKeyDown={(event) => event.stopPropagation()}\n {...props}\n >\n {children ?? <XIcon />}\n </Button>\n )\n}\n\n/** Hidden inputs for native form submission. */\nfunction TreeSelectHiddenInput({\n name,\n ...props\n}: Omit<React.ComponentProps<\"input\">, \"type\" | \"value\"> & { name: string }) {\n const ctx = useTreeSelect()\n return (\n <>\n {ctx.value.map((v) => (\n <input key={v} type=\"hidden\" name={name} value={v} data-slot=\"tree-select-hidden-input\" {...props} />\n ))}\n </>\n )\n}\n\n/* -------------------------------- content -------------------------------- */\n\nfunction TreeSelectContent({ className, ...props }: React.ComponentProps<typeof PopoverContent>) {\n const ctx = useTreeSelect()\n return (\n <PopoverContent\n ref={ctx.contentRef}\n data-slot=\"tree-select-content\"\n className={cn(\"w-(--reference-width) min-w-48 gap-1 p-1\", className)}\n {...props}\n />\n )\n}\n\nfunction TreeSelectSearch({\n className,\n placeholder = \"Search…\",\n ...props\n}: Omit<React.ComponentProps<typeof InputGroupInput>, \"value\" | \"onChange\">) {\n const ctx = useTreeSelect()\n return (\n <InputGroup data-slot=\"tree-select-search\" className={cn(\"h-8 shrink-0\", className)}>\n <InputGroupAddon>\n <SearchIcon />\n </InputGroupAddon>\n <InputGroupInput\n role=\"searchbox\"\n placeholder={placeholder}\n value={ctx.query}\n onChange={(event) => ctx.setQuery(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === \"ArrowDown\") {\n const first = ctx.contentRef.current?.querySelector<HTMLElement>(\n \"[data-slot=tree-view-item], [data-slot=tree-view-branch-control]\"\n )\n if (first) {\n event.preventDefault()\n first.focus()\n }\n }\n }}\n {...props}\n />\n </InputGroup>\n )\n}\n\nfunction TreeSelectEmpty({ className, children, ...props }: React.ComponentProps<\"div\">) {\n const ctx = useTreeSelect()\n if (ctx.filtered.getNodeChildren(ctx.filtered.rootNode).length > 0) return null\n return (\n <div\n data-slot=\"tree-select-empty\"\n className={cn(\"px-2 py-4 text-center text-sm text-muted-foreground\", className)}\n {...props}\n >\n {children ?? \"No results\"}\n </div>\n )\n}\n\n/* ---------------------------------- tree --------------------------------- */\n\ntype TreeSelectRenderNode<T extends TreeNode> = (node: T) => React.ReactNode\n\nconst TreeSelectRenderContext = React.createContext<TreeSelectRenderNode<TreeNode> | undefined>(undefined)\n\n/**\n * The tree inside the popup. Renders every node of the (filtered) collection;\n * pass a render-prop child to customise the row label.\n */\nfunction TreeSelectTree<T extends TreeNode>({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof TreeViewTree>, \"children\"> & { children?: TreeSelectRenderNode<T> }) {\n const ctx = useTreeSelect<T>()\n const searching = ctx.query.trim().length > 0\n const searchExpanded = React.useMemo(\n () => (searching ? ctx.filtered.getBranchValues() : null),\n [searching, ctx.filtered]\n )\n const roots = ctx.filtered.getNodeChildren(ctx.filtered.rootNode)\n if (roots.length === 0) return null\n return (\n <TreeSelectRenderContext.Provider value={children as TreeSelectRenderNode<TreeNode> | undefined}>\n <TreeView\n collection={ctx.filtered}\n selectionMode={ctx.multiple ? \"multiple\" : \"single\"}\n selectedValue={ctx.multiple ? [] : ctx.value}\n onSelectionChange={({ selectedValue }) => {\n const picked = selectedValue.find((v) => ctx.multiple || !ctx.value.includes(v)) ?? selectedValue[0]\n const node = picked != null ? ctx.filtered.findNode(picked) : undefined\n if (node) ctx.toggle(node)\n }}\n checkedValue={ctx.multiple ? ctx.value : undefined}\n onCheckedChange={({ checkedValue }) => ctx.setValue(checkedValue)}\n expandedValue={searchExpanded ?? ctx.expandedValue}\n onExpandedChange={({ expandedValue }) => {\n if (!searching) ctx.setExpandedValue(expandedValue)\n }}\n data-slot=\"tree-select-tree\"\n className=\"w-full\"\n >\n <TreeViewTree className={cn(\"max-h-72 overflow-y-auto\", className)} {...props}>\n {roots.map((node, index) => (\n <TreeSelectNode key={ctx.filtered.getNodeValue(node)} node={node} indexPath={[index]} />\n ))}\n </TreeViewTree>\n </TreeView>\n </TreeSelectRenderContext.Provider>\n )\n}\n\nfunction TreeSelectNode<T extends TreeNode>({ node, indexPath }: { node: T; indexPath: number[] }) {\n const ctx = useTreeSelect<T>()\n const render = React.useContext(TreeSelectRenderContext)\n const value = ctx.filtered.getNodeValue(node)\n const label = render ? render(node) : ctx.filtered.stringifyNode(node)\n const children = ctx.filtered.getNodeChildren(node)\n const isBranch = ctx.filtered.isBranchNode(node)\n return (\n <TreeViewNodeProvider node={node} indexPath={indexPath}>\n {isBranch ? (\n <TreeViewBranch data-slot=\"tree-select-branch\">\n <TreeViewBranchControl>\n <TreeViewBranchTrigger>\n <TreeViewBranchIndicator />\n </TreeViewBranchTrigger>\n {ctx.multiple && <TreeViewNodeCheckbox />}\n <TreeViewBranchText>{label}</TreeViewBranchText>\n </TreeViewBranchControl>\n <TreeViewBranchContent>\n <TreeViewBranchIndentGuide />\n {children.map((child, index) => (\n <TreeSelectNode key={ctx.filtered.getNodeValue(child)} node={child} indexPath={[...indexPath, index]} />\n ))}\n </TreeViewBranchContent>\n </TreeViewBranch>\n ) : (\n <TreeViewItem data-slot=\"tree-select-item\" data-value={value}>\n {ctx.multiple && <TreeViewNodeCheckbox />}\n <TreeViewItemText>{label}</TreeViewItemText>\n {!ctx.multiple && <TreeViewItemIndicator />}\n </TreeViewItem>\n )}\n </TreeViewNodeProvider>\n )\n}\n\nexport {\n TreeSelect,\n TreeSelectChip,\n TreeSelectChips,\n TreeSelectClearTrigger,\n TreeSelectContent,\n TreeSelectEmpty,\n TreeSelectHiddenInput,\n TreeSelectIndicator,\n TreeSelectNode,\n TreeSelectSearch,\n TreeSelectTree,\n TreeSelectTrigger,\n TreeSelectValue,\n collapseValues,\n createTreeCollection,\n useTreeSelect,\n type TreeCollection,\n type TreeNode,\n type TreeSelectProps,\n type TreeSelectValueChangeDetails,\n}\n"
|
|
780
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Popover as PopoverPrimitive, createTreeCollection, type TreeCollection, type TreeNode } from \"@ark-ui/react\"\nimport { ChevronDownIcon, SearchIcon, XIcon } from \"lucide-react\"\nimport { useControllable } from \"@/lib/controllable\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { InputGroup, InputGroupAddon, InputGroupInput } from \"@/components/ui/input-group\"\nimport { PopoverContent, PopoverTrigger } from \"@/components/ui/popover\"\nimport {\n TreeView,\n TreeViewBranch,\n TreeViewBranchContent,\n TreeViewBranchControl,\n TreeViewBranchIndentGuide,\n TreeViewBranchIndicator,\n TreeViewBranchText,\n TreeViewBranchTrigger,\n TreeViewItem,\n TreeViewItemIndicator,\n TreeViewItemText,\n TreeViewNodeCheckbox,\n TreeViewNodeProvider,\n TreeViewTree,\n} from \"@/components/ui/tree-view\"\n\n/* -------------------------------- helpers -------------------------------- */\n\nconst uniq = <T,>(list: T[]) => Array.from(new Set(list))\n\n/**\n * Collapses a list of selected leaf values into the top-most nodes that are fully\n * selected, so \"Engineering\" stands in for every engineer under it.\n */\nfunction collapseValues<T extends TreeNode>(collection: TreeCollection<T>, value: string[]): T[] {\n const selected = new Set(value)\n const out: T[] = []\n const walk = (node: T) => {\n for (const child of collection.getNodeChildren(node)) {\n const v = collection.getNodeValue(child)\n if (collection.isBranchNode(child)) {\n const leaves = collection.getDescendantValues(v)\n if (leaves.length > 0 && leaves.every((l) => selected.has(l))) out.push(child)\n else walk(child)\n } else if (selected.has(v)) out.push(child)\n }\n }\n walk(collection.rootNode)\n return out\n}\n\n/* -------------------------------- context -------------------------------- */\n\ntype TreeSelectValueChangeDetails<T extends TreeNode = TreeNode> = { value: string[]; nodes: T[] }\n\ntype TreeSelectContextValue<T extends TreeNode = TreeNode> = {\n collection: TreeCollection<T>\n /** The collection narrowed by the search query (ancestors of matches kept). */\n filtered: TreeCollection<T>\n value: string[]\n setValue: (value: string[]) => void\n multiple: boolean\n selectableBranches: boolean\n closeOnSelect: boolean\n /** Controlled open branches. */\n expandedValue: string[]\n setExpandedValue: (value: string[]) => void\n open: boolean\n setOpen: (open: boolean) => void\n query: string\n setQuery: (query: string) => void\n disabled: boolean\n readOnly: boolean\n invalid: boolean\n /** Toggle (multiple) or pick (single) a node, with branch semantics. */\n toggle: (node: T) => void\n /** Remove one value, or every leaf under a branch value. */\n remove: (value: string) => void\n clear: () => void\n selectedNodes: T[]\n /** Selected nodes with fully selected branches collapsed to the branch. */\n displayNodes: T[]\n contentRef: React.RefObject<HTMLDivElement | null>\n}\n\nconst TreeSelectContext = React.createContext<TreeSelectContextValue | null>(null)\n\nfunction useTreeSelect<T extends TreeNode = TreeNode>() {\n const ctx = React.useContext(TreeSelectContext)\n if (!ctx) throw new Error(\"TreeSelect parts must be used inside <TreeSelect>\")\n return ctx as unknown as TreeSelectContextValue<T>\n}\n\n/* ---------------------------------- root --------------------------------- */\n\ntype TreeSelectProps<T extends TreeNode> = Omit<\n React.ComponentProps<typeof PopoverPrimitive.Root>,\n \"open\" | \"defaultOpen\" | \"onOpenChange\" | \"children\"\n> & {\n collection: TreeCollection<T>\n value?: string[]\n defaultValue?: string[]\n onValueChange?: (details: TreeSelectValueChangeDetails<T>) => void\n /** Checkbox selection of leaves; a branch stands for all of its leaves. */\n multiple?: boolean\n /** Single mode only: allow picking a branch itself instead of expanding it. */\n selectableBranches?: boolean\n /** Close after a pick. Defaults to `!multiple`. */\n closeOnSelect?: boolean\n /** Controlled open branches. */\n expandedValue?: string[]\n /** Branches open initially when uncontrolled. */\n defaultExpandedValue?: string[]\n /** Called when branches open or close. */\n onExpandedChange?: (details: { expandedValue: string[] }) => void\n open?: boolean\n defaultOpen?: boolean\n onOpenChange?: (details: { open: boolean }) => void\n disabled?: boolean\n readOnly?: boolean\n invalid?: boolean\n children: React.ReactNode\n}\n\nfunction TreeSelect<T extends TreeNode>({\n collection,\n value: valueProp,\n defaultValue = [],\n onValueChange,\n multiple = false,\n selectableBranches = false,\n closeOnSelect = !multiple,\n expandedValue: expandedProp,\n defaultExpandedValue = [],\n onExpandedChange,\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n disabled = false,\n readOnly = false,\n invalid = false,\n positioning,\n lazyMount = true,\n unmountOnExit = true,\n children,\n ...props\n}: TreeSelectProps<T>) {\n const [value, setValueState] = useControllable<string[]>(valueProp, defaultValue, (next) =>\n onValueChange?.({ value: next, nodes: collection.findNodes(next) })\n )\n const [expandedValue, setExpandedValue] = useControllable<string[]>(expandedProp, defaultExpandedValue, (next) =>\n onExpandedChange?.({ expandedValue: next })\n )\n const [open, setOpenState] = useControllable<boolean>(openProp, defaultOpen, (next) => onOpenChange?.({ open: next }))\n const [query, setQuery] = React.useState(\"\")\n const contentRef = React.useRef<HTMLDivElement | null>(null)\n\n const setOpen = React.useCallback(\n (next: boolean) => {\n if (disabled) return\n setOpenState(next)\n if (!next) setQuery(\"\")\n },\n [disabled, setOpenState]\n )\n const setValue = React.useCallback(\n (next: string[]) => {\n if (readOnly) return\n setValueState(collection.sort(uniq(next)))\n },\n [collection, readOnly, setValueState]\n )\n\n const filtered = React.useMemo(() => {\n const q = query.trim().toLowerCase()\n if (!q) return collection\n return collection.filter((node) => collection.stringifyNode(node).toLowerCase().includes(q))\n }, [collection, query])\n\n const toggle = React.useCallback(\n (node: T) => {\n const v = collection.getNodeValue(node)\n const isBranch = collection.isBranchNode(node)\n if (multiple) {\n if (isBranch) {\n const leaves = collection.getDescendantValues(v)\n const all = leaves.length > 0 && leaves.every((l) => value.includes(l))\n setValue(all ? value.filter((x) => !leaves.includes(x)) : [...value, ...leaves])\n } else {\n setValue(value.includes(v) ? value.filter((x) => x !== v) : [...value, v])\n }\n if (closeOnSelect) setOpen(false)\n return\n }\n if (isBranch && !selectableBranches) return\n setValue([v])\n if (closeOnSelect) setOpen(false)\n },\n [collection, multiple, selectableBranches, closeOnSelect, value, setValue, setOpen]\n )\n const remove = React.useCallback(\n (v: string) => {\n const node = collection.findNode(v)\n const drop = node && collection.isBranchNode(node) ? collection.getDescendantValues(v) : [v]\n setValue(value.filter((x) => !drop.includes(x)))\n },\n [collection, value, setValue]\n )\n const clear = React.useCallback(() => setValue([]), [setValue])\n\n const selectedNodes = React.useMemo(() => collection.findNodes(value), [collection, value])\n const displayNodes = React.useMemo(\n () => (multiple ? collapseValues(collection, value) : selectedNodes),\n [collection, multiple, value, selectedNodes]\n )\n\n const ctx: TreeSelectContextValue<T> = {\n collection,\n filtered,\n value,\n setValue,\n multiple,\n selectableBranches,\n closeOnSelect,\n expandedValue,\n setExpandedValue,\n open,\n setOpen,\n query,\n setQuery,\n disabled,\n readOnly,\n invalid,\n toggle,\n remove,\n clear,\n selectedNodes,\n displayNodes,\n contentRef,\n }\n\n return (\n <TreeSelectContext.Provider value={ctx as unknown as TreeSelectContextValue}>\n <PopoverPrimitive.Root\n open={open}\n onOpenChange={({ open: next }) => setOpen(next)}\n positioning={{ placement: \"bottom-start\", gutter: 4, sameWidth: true, ...positioning }}\n lazyMount={lazyMount}\n unmountOnExit={unmountOnExit}\n {...props}\n >\n {children}\n </PopoverPrimitive.Root>\n </TreeSelectContext.Provider>\n )\n}\n\n/* -------------------------------- trigger -------------------------------- */\n\nfunction TreeSelectTrigger({\n className,\n size = \"default\",\n variant = \"default\",\n children,\n onKeyDown,\n ...props\n}: React.ComponentProps<\"div\"> & { size?: \"sm\" | \"default\"; variant?: \"default\" | \"unstyled\" }) {\n const ctx = useTreeSelect()\n return (\n <PopoverTrigger asChild>\n <div\n data-slot=\"tree-select-trigger\"\n role=\"combobox\"\n aria-haspopup=\"tree\"\n aria-expanded={ctx.open}\n aria-disabled={ctx.disabled || undefined}\n aria-invalid={ctx.invalid || undefined}\n aria-readonly={ctx.readOnly || undefined}\n tabIndex={ctx.disabled ? -1 : 0}\n data-size={size}\n data-variant={variant}\n data-state={ctx.open ? \"open\" : \"closed\"}\n data-disabled={ctx.disabled ? \"\" : undefined}\n data-invalid={ctx.invalid ? \"\" : undefined}\n data-placeholder-shown={ctx.value.length === 0 ? \"\" : undefined}\n className={cn(\n \"flex cursor-default items-center justify-between gap-1.5 text-sm outline-none select-none data-placeholder-shown:text-muted-foreground data-disabled:cursor-not-allowed data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n variant === \"default\" &&\n \"w-full rounded-lg border border-input bg-transparent py-1 pr-2 pl-2.5 transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 data-invalid:border-destructive data-invalid:ring-3 data-invalid:ring-destructive/20 data-[size=default]:min-h-8 data-[size=sm]:min-h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] dark:bg-input/30 dark:hover:bg-input/50 dark:data-invalid:border-destructive/50 dark:data-invalid:ring-destructive/40\",\n className\n )}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (event.defaultPrevented || ctx.disabled) return\n if (event.key === \"Enter\" || event.key === \" \" || event.key === \"ArrowDown\") {\n event.preventDefault()\n ctx.setOpen(true)\n } else if (event.key === \"Backspace\" && ctx.multiple && ctx.value.length > 0) {\n const last = ctx.displayNodes[ctx.displayNodes.length - 1]\n if (last) ctx.remove(ctx.collection.getNodeValue(last))\n }\n }}\n {...props}\n >\n {children}\n </div>\n </PopoverTrigger>\n )\n}\n\nfunction TreeSelectIndicator({ className, children, ...props }: React.ComponentProps<\"span\">) {\n const ctx = useTreeSelect()\n return (\n <span\n data-slot=\"tree-select-indicator\"\n data-state={ctx.open ? \"open\" : \"closed\"}\n className={cn(\"ml-auto flex shrink-0 items-center text-muted-foreground\", className)}\n {...props}\n >\n {children ?? <ChevronDownIcon className=\"size-4\" />}\n </span>\n )\n}\n\n/** Selected labels as text (fully selected branches collapse to the branch), or the placeholder. */\nfunction TreeSelectValue<T extends TreeNode>({\n placeholder,\n separator = \", \",\n children,\n className,\n ...props\n}: Omit<React.ComponentProps<\"span\">, \"children\"> & {\n placeholder?: React.ReactNode\n /** Rendered between the selected labels. */\n separator?: string\n children?: (nodes: T[]) => React.ReactNode\n}) {\n const ctx = useTreeSelect<T>()\n const empty = ctx.displayNodes.length === 0\n return (\n <span\n data-slot=\"tree-select-value\"\n data-placeholder-shown={empty ? \"\" : undefined}\n className={cn(\"line-clamp-1 flex-1 text-left\", className)}\n {...props}\n >\n {empty\n ? placeholder\n : children\n ? children(ctx.displayNodes)\n : ctx.displayNodes.map((n) => ctx.collection.stringifyNode(n)).join(separator)}\n </span>\n )\n}\n\n/** Selected nodes as removable chips (multiple mode). */\nfunction TreeSelectChips<T extends TreeNode>({\n placeholder,\n children,\n className,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & {\n placeholder?: React.ReactNode\n children?: (node: T) => React.ReactNode\n}) {\n const ctx = useTreeSelect<T>()\n const empty = ctx.displayNodes.length === 0\n return (\n <div\n data-slot=\"tree-select-chips\"\n data-placeholder-shown={empty ? \"\" : undefined}\n className={cn(\"flex min-w-0 flex-1 flex-wrap items-center gap-1 text-left\", className)}\n {...props}\n >\n {empty\n ? placeholder\n : ctx.displayNodes.map((node) =>\n children ? (\n <React.Fragment key={ctx.collection.getNodeValue(node)}>{children(node)}</React.Fragment>\n ) : (\n <TreeSelectChip key={ctx.collection.getNodeValue(node)} node={node} />\n )\n )}\n </div>\n )\n}\n\nfunction TreeSelectChip<T extends TreeNode>({\n node,\n showRemove = true,\n className,\n children,\n ...props\n}: React.ComponentProps<\"span\"> & { node: T; showRemove?: boolean }) {\n const ctx = useTreeSelect<T>()\n const value = ctx.collection.getNodeValue(node)\n const label = ctx.collection.stringifyNode(node)\n return (\n <span\n data-slot=\"tree-select-chip\"\n data-value={value}\n data-branch={ctx.collection.isBranchNode(node) ? \"\" : undefined}\n className={cn(\n \"flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-data-[slot=tree-select-chip-remove]:pr-0\",\n className\n )}\n {...props}\n >\n {children ?? label}\n {showRemove && !ctx.readOnly && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n data-slot=\"tree-select-chip-remove\"\n aria-label={`Remove ${label}`}\n disabled={ctx.disabled}\n className=\"-ml-1 opacity-50 hover:opacity-100\"\n onClick={(event) => {\n event.stopPropagation()\n ctx.remove(value)\n }}\n onKeyDown={(event) => event.stopPropagation()}\n >\n <XIcon className=\"pointer-events-none\" />\n </Button>\n )}\n </span>\n )\n}\n\nfunction TreeSelectClearTrigger({ className, children, ...props }: React.ComponentProps<typeof Button>) {\n const ctx = useTreeSelect()\n if (ctx.value.length === 0 || ctx.readOnly) return null\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n data-slot=\"tree-select-clear-trigger\"\n aria-label=\"Clear selection\"\n disabled={ctx.disabled}\n className={cn(\"-mr-1 text-muted-foreground hover:text-foreground\", className)}\n onClick={(event) => {\n event.stopPropagation()\n ctx.clear()\n }}\n onKeyDown={(event) => event.stopPropagation()}\n {...props}\n >\n {children ?? <XIcon />}\n </Button>\n )\n}\n\n/** Hidden inputs for native form submission. */\nfunction TreeSelectHiddenInput({\n name,\n ...props\n}: Omit<React.ComponentProps<\"input\">, \"type\" | \"value\"> & { name: string }) {\n const ctx = useTreeSelect()\n return (\n <>\n {ctx.value.map((v) => (\n <input key={v} type=\"hidden\" name={name} value={v} data-slot=\"tree-select-hidden-input\" {...props} />\n ))}\n </>\n )\n}\n\n/* -------------------------------- content -------------------------------- */\n\nfunction TreeSelectContent({ className, ...props }: React.ComponentProps<typeof PopoverContent>) {\n const ctx = useTreeSelect()\n return (\n <PopoverContent\n ref={ctx.contentRef}\n data-slot=\"tree-select-content\"\n className={cn(\"w-(--reference-width) min-w-48 gap-1 p-1\", className)}\n {...props}\n />\n )\n}\n\nfunction TreeSelectSearch({\n className,\n placeholder = \"Search…\",\n ...props\n}: Omit<React.ComponentProps<typeof InputGroupInput>, \"value\" | \"onChange\">) {\n const ctx = useTreeSelect()\n return (\n <InputGroup data-slot=\"tree-select-search\" className={cn(\"h-8 shrink-0\", className)}>\n <InputGroupAddon>\n <SearchIcon />\n </InputGroupAddon>\n <InputGroupInput\n role=\"searchbox\"\n placeholder={placeholder}\n value={ctx.query}\n onChange={(event) => ctx.setQuery(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === \"ArrowDown\") {\n const first = ctx.contentRef.current?.querySelector<HTMLElement>(\n \"[data-slot=tree-view-item], [data-slot=tree-view-branch-control]\"\n )\n if (first) {\n event.preventDefault()\n first.focus()\n }\n }\n }}\n {...props}\n />\n </InputGroup>\n )\n}\n\nfunction TreeSelectEmpty({ className, children, ...props }: React.ComponentProps<\"div\">) {\n const ctx = useTreeSelect()\n if (ctx.filtered.getNodeChildren(ctx.filtered.rootNode).length > 0) return null\n return (\n <div\n data-slot=\"tree-select-empty\"\n className={cn(\"px-2 py-4 text-center text-sm text-muted-foreground\", className)}\n {...props}\n >\n {children ?? \"No results\"}\n </div>\n )\n}\n\n/* ---------------------------------- tree --------------------------------- */\n\ntype TreeSelectRenderNode<T extends TreeNode> = (node: T) => React.ReactNode\n\nconst TreeSelectRenderContext = React.createContext<TreeSelectRenderNode<TreeNode> | undefined>(undefined)\n\n/**\n * The tree inside the popup. Renders every node of the (filtered) collection;\n * pass a render-prop child to customise the row label.\n */\nfunction TreeSelectTree<T extends TreeNode>({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof TreeViewTree>, \"children\"> & { children?: TreeSelectRenderNode<T> }) {\n const ctx = useTreeSelect<T>()\n const searching = ctx.query.trim().length > 0\n const searchExpanded = React.useMemo(\n () => (searching ? ctx.filtered.getBranchValues() : null),\n [searching, ctx.filtered]\n )\n const roots = ctx.filtered.getNodeChildren(ctx.filtered.rootNode)\n if (roots.length === 0) return null\n return (\n <TreeSelectRenderContext.Provider value={children as TreeSelectRenderNode<TreeNode> | undefined}>\n <TreeView\n collection={ctx.filtered}\n selectionMode={ctx.multiple ? \"multiple\" : \"single\"}\n selectedValue={ctx.multiple ? [] : ctx.value}\n onSelectionChange={({ selectedValue }) => {\n const picked = selectedValue.find((v) => ctx.multiple || !ctx.value.includes(v)) ?? selectedValue[0]\n const node = picked != null ? ctx.filtered.findNode(picked) : undefined\n if (node) ctx.toggle(node)\n }}\n checkedValue={ctx.multiple ? ctx.value : undefined}\n onCheckedChange={({ checkedValue }) => ctx.setValue(checkedValue)}\n expandedValue={searchExpanded ?? ctx.expandedValue}\n onExpandedChange={({ expandedValue }) => {\n if (!searching) ctx.setExpandedValue(expandedValue)\n }}\n data-slot=\"tree-select-tree\"\n className=\"w-full\"\n >\n <TreeViewTree className={cn(\"max-h-72 overflow-y-auto\", className)} {...props}>\n {roots.map((node, index) => (\n <TreeSelectNode key={ctx.filtered.getNodeValue(node)} node={node} indexPath={[index]} />\n ))}\n </TreeViewTree>\n </TreeView>\n </TreeSelectRenderContext.Provider>\n )\n}\n\nfunction TreeSelectNode<T extends TreeNode>({ node, indexPath }: { node: T; indexPath: number[] }) {\n const ctx = useTreeSelect<T>()\n const render = React.useContext(TreeSelectRenderContext)\n const value = ctx.filtered.getNodeValue(node)\n const label = render ? render(node) : ctx.filtered.stringifyNode(node)\n const children = ctx.filtered.getNodeChildren(node)\n const isBranch = ctx.filtered.isBranchNode(node)\n return (\n <TreeViewNodeProvider node={node} indexPath={indexPath}>\n {isBranch ? (\n <TreeViewBranch data-slot=\"tree-select-branch\">\n <TreeViewBranchControl>\n <TreeViewBranchTrigger>\n <TreeViewBranchIndicator />\n </TreeViewBranchTrigger>\n {ctx.multiple && <TreeViewNodeCheckbox />}\n <TreeViewBranchText>{label}</TreeViewBranchText>\n </TreeViewBranchControl>\n <TreeViewBranchContent>\n <TreeViewBranchIndentGuide />\n {children.map((child, index) => (\n <TreeSelectNode key={ctx.filtered.getNodeValue(child)} node={child} indexPath={[...indexPath, index]} />\n ))}\n </TreeViewBranchContent>\n </TreeViewBranch>\n ) : (\n <TreeViewItem data-slot=\"tree-select-item\" data-value={value}>\n {ctx.multiple && <TreeViewNodeCheckbox />}\n <TreeViewItemText>{label}</TreeViewItemText>\n {!ctx.multiple && <TreeViewItemIndicator />}\n </TreeViewItem>\n )}\n </TreeViewNodeProvider>\n )\n}\n\nexport {\n TreeSelect,\n TreeSelectChip,\n TreeSelectChips,\n TreeSelectClearTrigger,\n TreeSelectContent,\n TreeSelectEmpty,\n TreeSelectHiddenInput,\n TreeSelectIndicator,\n TreeSelectNode,\n TreeSelectSearch,\n TreeSelectTree,\n TreeSelectTrigger,\n TreeSelectValue,\n collapseValues,\n createTreeCollection,\n useTreeSelect,\n type TreeCollection,\n type TreeNode,\n type TreeSelectProps,\n type TreeSelectValueChangeDetails,\n}\n"
|
|
40
781
|
}
|
|
41
782
|
],
|
|
42
783
|
"docs": "## Tree select and cascader\nBoth live on Ark's `TreeCollection` (`createTreeCollection`, re-exported) and a Popover, and own\nonly the open state, the search query, and the value (`value`/`defaultValue`/`onValueChange`,\nalways a `string[]`); the trigger is a focusable `div[role=combobox]` via `PopoverTrigger asChild`\nso chips and clear buttons inside it stay real buttons. Both use `src/lib/controllable.ts`\n`useControllable(controlled, default, onChange)` (the shared controllable-state hook; prefer it\nover local copies).\n`src/components/ui/tree-select.tsx`: `TreeSelect { collection, multiple, selectableBranches,\ncloseOnSelect, expandedValue…, open…, disabled, readOnly, invalid }` > `TreeSelectTrigger`\n(`size`, `variant=\"unstyled\"`; Enter/Space/ArrowDown open, Backspace removes the last chip) >\n`TreeSelectValue` (labels joined, render-prop child gets the nodes) or `TreeSelectChips` >\n`TreeSelectChip node=` (remove button), `TreeSelectClearTrigger`, `TreeSelectIndicator`;\n`TreeSelectContent` (same width as the trigger) > `TreeSelectSearch` (filters the collection with\nancestors kept and auto-expands matches; ArrowDown moves into the tree), `TreeSelectTree`\n(renders the toolkit `TreeView` parts recursively through `TreeSelectNode`; render-prop child\ncustomises the row label), `TreeSelectEmpty`; `TreeSelectHiddenInput name=`. Multiple mode is\nArk checkbox semantics: `value` holds leaf values only, a branch toggles all its leaves, and\n`collapseValues(collection, value)` (used by the value/chips) shows a fully selected branch as one\nchip. Single mode ignores branch picks unless `selectableBranches`. `useTreeSelect()` exposes it all.\n`src/components/ui/cascader.tsx`: `Cascader { collection, changeOnSelect, expandTrigger:\nclick|hover, open…, disabled, readOnly, invalid }` > `CascaderTrigger` > `CascaderValue`\n(the path as \"A / B / C\", `separator`, render-prop child gets the path nodes), `CascaderClearTrigger`,\n`CascaderIndicator`; `CascaderContent` (owns the keyboard: ↑/↓ within a column, → opens a branch,\n← back to the parent, Home/End, Enter/Space activates) > `CascaderSearch` (matches every level,\nflat results replace the columns), `CascaderColumns` (`role=\"tree\"`, `aria-activedescendant`;\ncolumn `i + 1` lists the children of `expandedPath[i]`; render-prop child customises items) >\n`CascaderColumn depth=` > `CascaderItem node=` (`data-branch/expanded/selected/highlighted/disabled`)\n> `CascaderItemText` + `CascaderItemIndicator` (chevron on branches, check on the selected leaf),\n`CascaderSearchResults` > `CascaderSearchResult path=`, `CascaderEmpty`, `CascaderHiddenInput`.\nPicking a leaf closes; a branch opens the next column and, with `changeOnSelect`, is itself the\nvalue. Opening starts from the current value's path. `useCascader()` exposes it all.\n`src/demo/pickers/` is the reference (`/access`, \"Roles & access\"): folder picker with branches selectable and\nicons, people/teams multi-select with chips, hover-expanding location cascader with search, and a\nchange-on-select category cascader."
|