@multicomma/arkcn 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +78 -0
  2. package/bin/arkcn.mjs +78 -0
  3. package/cli/commands.mjs +251 -0
  4. package/cli/project.mjs +168 -0
  5. package/cli/registry.mjs +50 -0
  6. package/cli/ui.mjs +46 -0
  7. package/mcp/server.mjs +149 -0
  8. package/package.json +201 -0
  9. package/registry/base.css +680 -0
  10. package/registry/index.json +1833 -0
  11. package/registry/items/accordion.json +18 -0
  12. package/registry/items/activity-feed.json +18 -0
  13. package/registry/items/alert-dialog.json +18 -0
  14. package/registry/items/alert.json +17 -0
  15. package/registry/items/angle-slider.json +17 -0
  16. package/registry/items/app-shell.json +28 -0
  17. package/registry/items/aspect-ratio.json +13 -0
  18. package/registry/items/attachment.json +19 -0
  19. package/registry/items/avatar.json +17 -0
  20. package/registry/items/badge.json +18 -0
  21. package/registry/items/breadcrumb.json +18 -0
  22. package/registry/items/bubble.json +18 -0
  23. package/registry/items/button-group.json +19 -0
  24. package/registry/items/button.json +18 -0
  25. package/registry/items/calendar.json +19 -0
  26. package/registry/items/canvas.json +24 -0
  27. package/registry/items/card.json +15 -0
  28. package/registry/items/carousel.json +19 -0
  29. package/registry/items/cascader.json +23 -0
  30. package/registry/items/chart.json +17 -0
  31. package/registry/items/chat.json +23 -0
  32. package/registry/items/checkbox.json +18 -0
  33. package/registry/items/clipboard.json +19 -0
  34. package/registry/items/collapsible.json +15 -0
  35. package/registry/items/color-picker.json +19 -0
  36. package/registry/items/combobox.json +20 -0
  37. package/registry/items/command.json +21 -0
  38. package/registry/items/comment-thread.json +25 -0
  39. package/registry/items/context-menu.json +18 -0
  40. package/registry/items/controllable.json +14 -0
  41. package/registry/items/data-grid.json +24 -0
  42. package/registry/items/data-table-adapter.json +14 -0
  43. package/registry/items/data-table.json +32 -0
  44. package/registry/items/date-input.json +17 -0
  45. package/registry/items/dialog.json +19 -0
  46. package/registry/items/direction.json +13 -0
  47. package/registry/items/drag-preview.json +16 -0
  48. package/registry/items/drawer.json +18 -0
  49. package/registry/items/dropdown-menu.json +18 -0
  50. package/registry/items/editable.json +17 -0
  51. package/registry/items/empty.json +17 -0
  52. package/registry/items/field.json +19 -0
  53. package/registry/items/file-upload.json +19 -0
  54. package/registry/items/floating-panel.json +19 -0
  55. package/registry/items/floating-toolbar.json +16 -0
  56. package/registry/items/form.json +14 -0
  57. package/registry/items/format.json +15 -0
  58. package/registry/items/gantt.json +22 -0
  59. package/registry/items/history.json +14 -0
  60. package/registry/items/hotkeys.json +18 -0
  61. package/registry/items/hover-card.json +17 -0
  62. package/registry/items/image-cropper.json +17 -0
  63. package/registry/items/input-group.json +20 -0
  64. package/registry/items/input-otp.json +18 -0
  65. package/registry/items/input.json +15 -0
  66. package/registry/items/item.json +19 -0
  67. package/registry/items/json-tree-view.json +17 -0
  68. package/registry/items/kanban.json +26 -0
  69. package/registry/items/kbd.json +15 -0
  70. package/registry/items/label.json +15 -0
  71. package/registry/items/live-region.json +16 -0
  72. package/registry/items/marker.json +18 -0
  73. package/registry/items/marquee.json +17 -0
  74. package/registry/items/menubar.json +19 -0
  75. package/registry/items/message.json +15 -0
  76. package/registry/items/navigation-menu.json +19 -0
  77. package/registry/items/node-graph.json +22 -0
  78. package/registry/items/number-input.json +18 -0
  79. package/registry/items/pagination.json +18 -0
  80. package/registry/items/password-input.json +18 -0
  81. package/registry/items/popconfirm.json +20 -0
  82. package/registry/items/popover.json +18 -0
  83. package/registry/items/progress.json +17 -0
  84. package/registry/items/qr-code.json +17 -0
  85. package/registry/items/query-builder.json +23 -0
  86. package/registry/items/radio-group.json +17 -0
  87. package/registry/items/rating-group.json +18 -0
  88. package/registry/items/resizable.json +17 -0
  89. package/registry/items/rich-text-editor.json +37 -0
  90. package/registry/items/row-layout.json +15 -0
  91. package/registry/items/scheduler.json +23 -0
  92. package/registry/items/scroll-area.json +17 -0
  93. package/registry/items/segment-group.json +17 -0
  94. package/registry/items/select.json +19 -0
  95. package/registry/items/separator.json +15 -0
  96. package/registry/items/sheet.json +19 -0
  97. package/registry/items/sidebar.json +27 -0
  98. package/registry/items/signature-pad.json +19 -0
  99. package/registry/items/skeleton.json +15 -0
  100. package/registry/items/slider.json +17 -0
  101. package/registry/items/sonner.json +17 -0
  102. package/registry/items/spinner.json +17 -0
  103. package/registry/items/steps.json +19 -0
  104. package/registry/items/swap.json +17 -0
  105. package/registry/items/switch.json +17 -0
  106. package/registry/items/table.json +16 -0
  107. package/registry/items/tabs.json +18 -0
  108. package/registry/items/tags-input.json +18 -0
  109. package/registry/items/textarea.json +15 -0
  110. package/registry/items/time.json +14 -0
  111. package/registry/items/timer.json +18 -0
  112. package/registry/items/toc.json +17 -0
  113. package/registry/items/toggle-group.json +19 -0
  114. package/registry/items/toggle.json +18 -0
  115. package/registry/items/tooltip.json +17 -0
  116. package/registry/items/tour.json +19 -0
  117. package/registry/items/transfer-list.json +24 -0
  118. package/registry/items/tree-select.json +24 -0
  119. package/registry/items/tree-view.json +18 -0
  120. package/registry/items/use-mobile.json +13 -0
  121. package/registry/items/utils.json +17 -0
  122. package/registry/items/virtual-list.json +16 -0
  123. package/src/components/ui/accordion.tsx +111 -0
  124. package/src/components/ui/activity-feed.tsx +228 -0
  125. package/src/components/ui/alert-dialog.tsx +204 -0
  126. package/src/components/ui/alert.tsx +55 -0
  127. package/src/components/ui/angle-slider.tsx +117 -0
  128. package/src/components/ui/app-shell.tsx +985 -0
  129. package/src/components/ui/aspect-ratio.tsx +35 -0
  130. package/src/components/ui/attachment.tsx +193 -0
  131. package/src/components/ui/avatar.tsx +94 -0
  132. package/src/components/ui/badge.tsx +45 -0
  133. package/src/components/ui/breadcrumb.tsx +93 -0
  134. package/src/components/ui/bubble.tsx +119 -0
  135. package/src/components/ui/button-group.tsx +79 -0
  136. package/src/components/ui/button.tsx +65 -0
  137. package/src/components/ui/calendar.tsx +172 -0
  138. package/src/components/ui/canvas.tsx +723 -0
  139. package/src/components/ui/card.tsx +69 -0
  140. package/src/components/ui/carousel.tsx +228 -0
  141. package/src/components/ui/cascader.tsx +754 -0
  142. package/src/components/ui/chart.tsx +303 -0
  143. package/src/components/ui/chat.tsx +544 -0
  144. package/src/components/ui/checkbox.tsx +94 -0
  145. package/src/components/ui/clipboard.tsx +94 -0
  146. package/src/components/ui/collapsible.tsx +32 -0
  147. package/src/components/ui/color-picker.tsx +441 -0
  148. package/src/components/ui/combobox.tsx +400 -0
  149. package/src/components/ui/command.tsx +292 -0
  150. package/src/components/ui/comment-thread.tsx +923 -0
  151. package/src/components/ui/context-menu.tsx +331 -0
  152. package/src/components/ui/data-grid.tsx +1227 -0
  153. package/src/components/ui/data-table.tsx +993 -0
  154. package/src/components/ui/date-input.tsx +103 -0
  155. package/src/components/ui/dialog.tsx +159 -0
  156. package/src/components/ui/direction.tsx +32 -0
  157. package/src/components/ui/drawer.tsx +200 -0
  158. package/src/components/ui/dropdown-menu.tsx +329 -0
  159. package/src/components/ui/editable.tsx +90 -0
  160. package/src/components/ui/empty.tsx +86 -0
  161. package/src/components/ui/field.tsx +218 -0
  162. package/src/components/ui/file-upload.tsx +204 -0
  163. package/src/components/ui/floating-panel.tsx +199 -0
  164. package/src/components/ui/floating-toolbar.tsx +79 -0
  165. package/src/components/ui/format.tsx +22 -0
  166. package/src/components/ui/gantt.tsx +950 -0
  167. package/src/components/ui/hotkeys.tsx +340 -0
  168. package/src/components/ui/hover-card.tsx +83 -0
  169. package/src/components/ui/image-cropper.tsx +100 -0
  170. package/src/components/ui/input-group.tsx +133 -0
  171. package/src/components/ui/input-otp.tsx +82 -0
  172. package/src/components/ui/input.tsx +18 -0
  173. package/src/components/ui/item.tsx +174 -0
  174. package/src/components/ui/json-tree-view.tsx +37 -0
  175. package/src/components/ui/kanban.tsx +884 -0
  176. package/src/components/ui/kbd.tsx +20 -0
  177. package/src/components/ui/label.tsx +30 -0
  178. package/src/components/ui/live-region.tsx +54 -0
  179. package/src/components/ui/marker.tsx +66 -0
  180. package/src/components/ui/marquee.tsx +61 -0
  181. package/src/components/ui/menubar.tsx +456 -0
  182. package/src/components/ui/message.tsx +74 -0
  183. package/src/components/ui/navigation-menu.tsx +202 -0
  184. package/src/components/ui/node-graph.tsx +1723 -0
  185. package/src/components/ui/number-input.tsx +148 -0
  186. package/src/components/ui/pagination.tsx +88 -0
  187. package/src/components/ui/password-input.tsx +104 -0
  188. package/src/components/ui/popconfirm.tsx +149 -0
  189. package/src/components/ui/popover.tsx +126 -0
  190. package/src/components/ui/progress.tsx +114 -0
  191. package/src/components/ui/qr-code.tsx +51 -0
  192. package/src/components/ui/query-builder.tsx +938 -0
  193. package/src/components/ui/radio-group.tsx +108 -0
  194. package/src/components/ui/rating-group.tsx +91 -0
  195. package/src/components/ui/resizable.tsx +41 -0
  196. package/src/components/ui/rich-text-editor.tsx +845 -0
  197. package/src/components/ui/scheduler.tsx +1164 -0
  198. package/src/components/ui/scroll-area.tsx +82 -0
  199. package/src/components/ui/segment-group.tsx +95 -0
  200. package/src/components/ui/select.tsx +354 -0
  201. package/src/components/ui/separator.tsx +32 -0
  202. package/src/components/ui/sheet.tsx +130 -0
  203. package/src/components/ui/sidebar.tsx +665 -0
  204. package/src/components/ui/signature-pad.tsx +112 -0
  205. package/src/components/ui/skeleton.tsx +7 -0
  206. package/src/components/ui/slider.tsx +183 -0
  207. package/src/components/ui/sonner.tsx +37 -0
  208. package/src/components/ui/spinner.tsx +16 -0
  209. package/src/components/ui/steps.tsx +152 -0
  210. package/src/components/ui/swap.tsx +33 -0
  211. package/src/components/ui/switch.tsx +83 -0
  212. package/src/components/ui/table.tsx +72 -0
  213. package/src/components/ui/tabs.tsx +100 -0
  214. package/src/components/ui/tags-input.tsx +153 -0
  215. package/src/components/ui/textarea.tsx +17 -0
  216. package/src/components/ui/timer.tsx +67 -0
  217. package/src/components/ui/toc.tsx +64 -0
  218. package/src/components/ui/toggle-group.tsx +89 -0
  219. package/src/components/ui/toggle.tsx +46 -0
  220. package/src/components/ui/tooltip.tsx +113 -0
  221. package/src/components/ui/tour.tsx +168 -0
  222. package/src/components/ui/transfer-list.tsx +700 -0
  223. package/src/components/ui/tree-select.tsx +638 -0
  224. package/src/components/ui/tree-view.tsx +217 -0
  225. package/src/components/ui/virtual-list.tsx +343 -0
  226. package/src/hooks/use-mobile.ts +19 -0
  227. package/src/lib/controllable.ts +31 -0
  228. package/src/lib/data-table-adapter.ts +67 -0
  229. package/src/lib/drag-preview.ts +39 -0
  230. package/src/lib/form.ts +39 -0
  231. package/src/lib/history.ts +55 -0
  232. package/src/lib/row-layout.ts +81 -0
  233. package/src/lib/table.ts +483 -0
  234. package/src/lib/time.ts +37 -0
  235. package/src/lib/utils.ts +6 -0
  236. package/src/styles/arkcn.css +764 -0
  237. package/src/styles/index.css +3 -0
@@ -0,0 +1,638 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Popover as PopoverPrimitive, createTreeCollection, type TreeCollection, type TreeNode } from "@ark-ui/react"
5
+ import { ChevronDownIcon, SearchIcon, XIcon } from "lucide-react"
6
+ import { useControllable } from "@/lib/controllable"
7
+ import { cn } from "@/lib/utils"
8
+ import { Button } from "@/components/ui/button"
9
+ import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"
10
+ import { PopoverContent, PopoverTrigger } from "@/components/ui/popover"
11
+ import {
12
+ TreeView,
13
+ TreeViewBranch,
14
+ TreeViewBranchContent,
15
+ TreeViewBranchControl,
16
+ TreeViewBranchIndentGuide,
17
+ TreeViewBranchIndicator,
18
+ TreeViewBranchText,
19
+ TreeViewBranchTrigger,
20
+ TreeViewItem,
21
+ TreeViewItemIndicator,
22
+ TreeViewItemText,
23
+ TreeViewNodeCheckbox,
24
+ TreeViewNodeProvider,
25
+ TreeViewTree,
26
+ } from "@/components/ui/tree-view"
27
+
28
+ /* -------------------------------- helpers -------------------------------- */
29
+
30
+ const uniq = <T,>(list: T[]) => Array.from(new Set(list))
31
+
32
+ /**
33
+ * Collapses a list of selected leaf values into the top-most nodes that are fully
34
+ * selected, so "Engineering" stands in for every engineer under it.
35
+ */
36
+ function collapseValues<T extends TreeNode>(collection: TreeCollection<T>, value: string[]): T[] {
37
+ const selected = new Set(value)
38
+ const out: T[] = []
39
+ const walk = (node: T) => {
40
+ for (const child of collection.getNodeChildren(node)) {
41
+ const v = collection.getNodeValue(child)
42
+ if (collection.isBranchNode(child)) {
43
+ const leaves = collection.getDescendantValues(v)
44
+ if (leaves.length > 0 && leaves.every((l) => selected.has(l))) out.push(child)
45
+ else walk(child)
46
+ } else if (selected.has(v)) out.push(child)
47
+ }
48
+ }
49
+ walk(collection.rootNode)
50
+ return out
51
+ }
52
+
53
+ /* -------------------------------- context -------------------------------- */
54
+
55
+ type TreeSelectValueChangeDetails<T extends TreeNode = TreeNode> = { value: string[]; nodes: T[] }
56
+
57
+ type TreeSelectContextValue<T extends TreeNode = TreeNode> = {
58
+ collection: TreeCollection<T>
59
+ /** The collection narrowed by the search query (ancestors of matches kept). */
60
+ filtered: TreeCollection<T>
61
+ value: string[]
62
+ setValue: (value: string[]) => void
63
+ multiple: boolean
64
+ selectableBranches: boolean
65
+ closeOnSelect: boolean
66
+ expandedValue: string[]
67
+ setExpandedValue: (value: string[]) => void
68
+ open: boolean
69
+ setOpen: (open: boolean) => void
70
+ query: string
71
+ setQuery: (query: string) => void
72
+ disabled: boolean
73
+ readOnly: boolean
74
+ invalid: boolean
75
+ /** Toggle (multiple) or pick (single) a node, with branch semantics. */
76
+ toggle: (node: T) => void
77
+ /** Remove one value, or every leaf under a branch value. */
78
+ remove: (value: string) => void
79
+ clear: () => void
80
+ selectedNodes: T[]
81
+ /** Selected nodes with fully selected branches collapsed to the branch. */
82
+ displayNodes: T[]
83
+ contentRef: React.RefObject<HTMLDivElement | null>
84
+ }
85
+
86
+ const TreeSelectContext = React.createContext<TreeSelectContextValue | null>(null)
87
+
88
+ function useTreeSelect<T extends TreeNode = TreeNode>() {
89
+ const ctx = React.useContext(TreeSelectContext)
90
+ if (!ctx) throw new Error("TreeSelect parts must be used inside <TreeSelect>")
91
+ return ctx as unknown as TreeSelectContextValue<T>
92
+ }
93
+
94
+ /* ---------------------------------- root --------------------------------- */
95
+
96
+ type TreeSelectProps<T extends TreeNode> = Omit<
97
+ React.ComponentProps<typeof PopoverPrimitive.Root>,
98
+ "open" | "defaultOpen" | "onOpenChange" | "children"
99
+ > & {
100
+ collection: TreeCollection<T>
101
+ value?: string[]
102
+ defaultValue?: string[]
103
+ onValueChange?: (details: TreeSelectValueChangeDetails<T>) => void
104
+ /** Checkbox selection of leaves; a branch stands for all of its leaves. */
105
+ multiple?: boolean
106
+ /** Single mode only: allow picking a branch itself instead of expanding it. */
107
+ selectableBranches?: boolean
108
+ /** Close after a pick. Defaults to `!multiple`. */
109
+ closeOnSelect?: boolean
110
+ expandedValue?: string[]
111
+ defaultExpandedValue?: string[]
112
+ onExpandedChange?: (details: { expandedValue: string[] }) => void
113
+ open?: boolean
114
+ defaultOpen?: boolean
115
+ onOpenChange?: (details: { open: boolean }) => void
116
+ disabled?: boolean
117
+ readOnly?: boolean
118
+ invalid?: boolean
119
+ children: React.ReactNode
120
+ }
121
+
122
+ function TreeSelect<T extends TreeNode>({
123
+ collection,
124
+ value: valueProp,
125
+ defaultValue = [],
126
+ onValueChange,
127
+ multiple = false,
128
+ selectableBranches = false,
129
+ closeOnSelect = !multiple,
130
+ expandedValue: expandedProp,
131
+ defaultExpandedValue = [],
132
+ onExpandedChange,
133
+ open: openProp,
134
+ defaultOpen = false,
135
+ onOpenChange,
136
+ disabled = false,
137
+ readOnly = false,
138
+ invalid = false,
139
+ positioning,
140
+ lazyMount = true,
141
+ unmountOnExit = true,
142
+ children,
143
+ ...props
144
+ }: TreeSelectProps<T>) {
145
+ const [value, setValueState] = useControllable<string[]>(valueProp, defaultValue, (next) =>
146
+ onValueChange?.({ value: next, nodes: collection.findNodes(next) })
147
+ )
148
+ const [expandedValue, setExpandedValue] = useControllable<string[]>(expandedProp, defaultExpandedValue, (next) =>
149
+ onExpandedChange?.({ expandedValue: next })
150
+ )
151
+ const [open, setOpenState] = useControllable<boolean>(openProp, defaultOpen, (next) => onOpenChange?.({ open: next }))
152
+ const [query, setQuery] = React.useState("")
153
+ const contentRef = React.useRef<HTMLDivElement | null>(null)
154
+
155
+ const setOpen = React.useCallback(
156
+ (next: boolean) => {
157
+ if (disabled) return
158
+ setOpenState(next)
159
+ if (!next) setQuery("")
160
+ },
161
+ [disabled, setOpenState]
162
+ )
163
+ const setValue = React.useCallback(
164
+ (next: string[]) => {
165
+ if (readOnly) return
166
+ setValueState(collection.sort(uniq(next)))
167
+ },
168
+ [collection, readOnly, setValueState]
169
+ )
170
+
171
+ const filtered = React.useMemo(() => {
172
+ const q = query.trim().toLowerCase()
173
+ if (!q) return collection
174
+ return collection.filter((node) => collection.stringifyNode(node).toLowerCase().includes(q))
175
+ }, [collection, query])
176
+
177
+ const toggle = React.useCallback(
178
+ (node: T) => {
179
+ const v = collection.getNodeValue(node)
180
+ const isBranch = collection.isBranchNode(node)
181
+ if (multiple) {
182
+ if (isBranch) {
183
+ const leaves = collection.getDescendantValues(v)
184
+ const all = leaves.length > 0 && leaves.every((l) => value.includes(l))
185
+ setValue(all ? value.filter((x) => !leaves.includes(x)) : [...value, ...leaves])
186
+ } else {
187
+ setValue(value.includes(v) ? value.filter((x) => x !== v) : [...value, v])
188
+ }
189
+ if (closeOnSelect) setOpen(false)
190
+ return
191
+ }
192
+ if (isBranch && !selectableBranches) return
193
+ setValue([v])
194
+ if (closeOnSelect) setOpen(false)
195
+ },
196
+ [collection, multiple, selectableBranches, closeOnSelect, value, setValue, setOpen]
197
+ )
198
+ const remove = React.useCallback(
199
+ (v: string) => {
200
+ const node = collection.findNode(v)
201
+ const drop = node && collection.isBranchNode(node) ? collection.getDescendantValues(v) : [v]
202
+ setValue(value.filter((x) => !drop.includes(x)))
203
+ },
204
+ [collection, value, setValue]
205
+ )
206
+ const clear = React.useCallback(() => setValue([]), [setValue])
207
+
208
+ const selectedNodes = React.useMemo(() => collection.findNodes(value), [collection, value])
209
+ const displayNodes = React.useMemo(
210
+ () => (multiple ? collapseValues(collection, value) : selectedNodes),
211
+ [collection, multiple, value, selectedNodes]
212
+ )
213
+
214
+ const ctx: TreeSelectContextValue<T> = {
215
+ collection,
216
+ filtered,
217
+ value,
218
+ setValue,
219
+ multiple,
220
+ selectableBranches,
221
+ closeOnSelect,
222
+ expandedValue,
223
+ setExpandedValue,
224
+ open,
225
+ setOpen,
226
+ query,
227
+ setQuery,
228
+ disabled,
229
+ readOnly,
230
+ invalid,
231
+ toggle,
232
+ remove,
233
+ clear,
234
+ selectedNodes,
235
+ displayNodes,
236
+ contentRef,
237
+ }
238
+
239
+ return (
240
+ <TreeSelectContext.Provider value={ctx as unknown as TreeSelectContextValue}>
241
+ <PopoverPrimitive.Root
242
+ open={open}
243
+ onOpenChange={({ open: next }) => setOpen(next)}
244
+ positioning={{ placement: "bottom-start", gutter: 4, sameWidth: true, ...positioning }}
245
+ lazyMount={lazyMount}
246
+ unmountOnExit={unmountOnExit}
247
+ {...props}
248
+ >
249
+ {children}
250
+ </PopoverPrimitive.Root>
251
+ </TreeSelectContext.Provider>
252
+ )
253
+ }
254
+
255
+ /* -------------------------------- trigger -------------------------------- */
256
+
257
+ function TreeSelectTrigger({
258
+ className,
259
+ size = "default",
260
+ variant = "default",
261
+ children,
262
+ onKeyDown,
263
+ ...props
264
+ }: React.ComponentProps<"div"> & { size?: "sm" | "default"; variant?: "default" | "unstyled" }) {
265
+ const ctx = useTreeSelect()
266
+ return (
267
+ <PopoverTrigger asChild>
268
+ <div
269
+ data-slot="tree-select-trigger"
270
+ role="combobox"
271
+ aria-haspopup="tree"
272
+ aria-expanded={ctx.open}
273
+ aria-disabled={ctx.disabled || undefined}
274
+ aria-invalid={ctx.invalid || undefined}
275
+ aria-readonly={ctx.readOnly || undefined}
276
+ tabIndex={ctx.disabled ? -1 : 0}
277
+ data-size={size}
278
+ data-variant={variant}
279
+ data-state={ctx.open ? "open" : "closed"}
280
+ data-disabled={ctx.disabled ? "" : undefined}
281
+ data-invalid={ctx.invalid ? "" : undefined}
282
+ data-placeholder-shown={ctx.value.length === 0 ? "" : undefined}
283
+ className={cn(
284
+ "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",
285
+ variant === "default" &&
286
+ "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",
287
+ className
288
+ )}
289
+ onKeyDown={(event) => {
290
+ onKeyDown?.(event)
291
+ if (event.defaultPrevented || ctx.disabled) return
292
+ if (event.key === "Enter" || event.key === " " || event.key === "ArrowDown") {
293
+ event.preventDefault()
294
+ ctx.setOpen(true)
295
+ } else if (event.key === "Backspace" && ctx.multiple && ctx.value.length > 0) {
296
+ const last = ctx.displayNodes[ctx.displayNodes.length - 1]
297
+ if (last) ctx.remove(ctx.collection.getNodeValue(last))
298
+ }
299
+ }}
300
+ {...props}
301
+ >
302
+ {children}
303
+ </div>
304
+ </PopoverTrigger>
305
+ )
306
+ }
307
+
308
+ function TreeSelectIndicator({ className, children, ...props }: React.ComponentProps<"span">) {
309
+ const ctx = useTreeSelect()
310
+ return (
311
+ <span
312
+ data-slot="tree-select-indicator"
313
+ data-state={ctx.open ? "open" : "closed"}
314
+ className={cn("ml-auto flex shrink-0 items-center text-muted-foreground", className)}
315
+ {...props}
316
+ >
317
+ {children ?? <ChevronDownIcon className="size-4" />}
318
+ </span>
319
+ )
320
+ }
321
+
322
+ /** Selected labels as text (fully selected branches collapse to the branch), or the placeholder. */
323
+ function TreeSelectValue<T extends TreeNode>({
324
+ placeholder,
325
+ separator = ", ",
326
+ children,
327
+ className,
328
+ ...props
329
+ }: Omit<React.ComponentProps<"span">, "children"> & {
330
+ placeholder?: React.ReactNode
331
+ separator?: string
332
+ children?: (nodes: T[]) => React.ReactNode
333
+ }) {
334
+ const ctx = useTreeSelect<T>()
335
+ const empty = ctx.displayNodes.length === 0
336
+ return (
337
+ <span
338
+ data-slot="tree-select-value"
339
+ data-placeholder-shown={empty ? "" : undefined}
340
+ className={cn("line-clamp-1 flex-1 text-left", className)}
341
+ {...props}
342
+ >
343
+ {empty
344
+ ? placeholder
345
+ : children
346
+ ? children(ctx.displayNodes)
347
+ : ctx.displayNodes.map((n) => ctx.collection.stringifyNode(n)).join(separator)}
348
+ </span>
349
+ )
350
+ }
351
+
352
+ /** Selected nodes as removable chips (multiple mode). */
353
+ function TreeSelectChips<T extends TreeNode>({
354
+ placeholder,
355
+ children,
356
+ className,
357
+ ...props
358
+ }: Omit<React.ComponentProps<"div">, "children"> & {
359
+ placeholder?: React.ReactNode
360
+ children?: (node: T) => React.ReactNode
361
+ }) {
362
+ const ctx = useTreeSelect<T>()
363
+ const empty = ctx.displayNodes.length === 0
364
+ return (
365
+ <div
366
+ data-slot="tree-select-chips"
367
+ data-placeholder-shown={empty ? "" : undefined}
368
+ className={cn("flex min-w-0 flex-1 flex-wrap items-center gap-1 text-left", className)}
369
+ {...props}
370
+ >
371
+ {empty
372
+ ? placeholder
373
+ : ctx.displayNodes.map((node) =>
374
+ children ? (
375
+ <React.Fragment key={ctx.collection.getNodeValue(node)}>{children(node)}</React.Fragment>
376
+ ) : (
377
+ <TreeSelectChip key={ctx.collection.getNodeValue(node)} node={node} />
378
+ )
379
+ )}
380
+ </div>
381
+ )
382
+ }
383
+
384
+ function TreeSelectChip<T extends TreeNode>({
385
+ node,
386
+ showRemove = true,
387
+ className,
388
+ children,
389
+ ...props
390
+ }: React.ComponentProps<"span"> & { node: T; showRemove?: boolean }) {
391
+ const ctx = useTreeSelect<T>()
392
+ const value = ctx.collection.getNodeValue(node)
393
+ const label = ctx.collection.stringifyNode(node)
394
+ return (
395
+ <span
396
+ data-slot="tree-select-chip"
397
+ data-value={value}
398
+ data-branch={ctx.collection.isBranchNode(node) ? "" : undefined}
399
+ className={cn(
400
+ "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",
401
+ className
402
+ )}
403
+ {...props}
404
+ >
405
+ {children ?? label}
406
+ {showRemove && !ctx.readOnly && (
407
+ <Button
408
+ type="button"
409
+ variant="ghost"
410
+ size="icon-xs"
411
+ data-slot="tree-select-chip-remove"
412
+ aria-label={`Remove ${label}`}
413
+ disabled={ctx.disabled}
414
+ className="-ml-1 opacity-50 hover:opacity-100"
415
+ onClick={(event) => {
416
+ event.stopPropagation()
417
+ ctx.remove(value)
418
+ }}
419
+ onKeyDown={(event) => event.stopPropagation()}
420
+ >
421
+ <XIcon className="pointer-events-none" />
422
+ </Button>
423
+ )}
424
+ </span>
425
+ )
426
+ }
427
+
428
+ function TreeSelectClearTrigger({ className, children, ...props }: React.ComponentProps<typeof Button>) {
429
+ const ctx = useTreeSelect()
430
+ if (ctx.value.length === 0 || ctx.readOnly) return null
431
+ return (
432
+ <Button
433
+ type="button"
434
+ variant="ghost"
435
+ size="icon-xs"
436
+ data-slot="tree-select-clear-trigger"
437
+ aria-label="Clear selection"
438
+ disabled={ctx.disabled}
439
+ className={cn("-mr-1 text-muted-foreground hover:text-foreground", className)}
440
+ onClick={(event) => {
441
+ event.stopPropagation()
442
+ ctx.clear()
443
+ }}
444
+ onKeyDown={(event) => event.stopPropagation()}
445
+ {...props}
446
+ >
447
+ {children ?? <XIcon />}
448
+ </Button>
449
+ )
450
+ }
451
+
452
+ /** Hidden inputs for native form submission. */
453
+ function TreeSelectHiddenInput({
454
+ name,
455
+ ...props
456
+ }: Omit<React.ComponentProps<"input">, "type" | "value"> & { name: string }) {
457
+ const ctx = useTreeSelect()
458
+ return (
459
+ <>
460
+ {ctx.value.map((v) => (
461
+ <input key={v} type="hidden" name={name} value={v} data-slot="tree-select-hidden-input" {...props} />
462
+ ))}
463
+ </>
464
+ )
465
+ }
466
+
467
+ /* -------------------------------- content -------------------------------- */
468
+
469
+ function TreeSelectContent({ className, ...props }: React.ComponentProps<typeof PopoverContent>) {
470
+ const ctx = useTreeSelect()
471
+ return (
472
+ <PopoverContent
473
+ ref={ctx.contentRef}
474
+ data-slot="tree-select-content"
475
+ className={cn("w-(--reference-width) min-w-48 gap-1 p-1", className)}
476
+ {...props}
477
+ />
478
+ )
479
+ }
480
+
481
+ function TreeSelectSearch({
482
+ className,
483
+ placeholder = "Search…",
484
+ ...props
485
+ }: Omit<React.ComponentProps<typeof InputGroupInput>, "value" | "onChange">) {
486
+ const ctx = useTreeSelect()
487
+ return (
488
+ <InputGroup data-slot="tree-select-search" className={cn("h-8 shrink-0", className)}>
489
+ <InputGroupAddon>
490
+ <SearchIcon />
491
+ </InputGroupAddon>
492
+ <InputGroupInput
493
+ role="searchbox"
494
+ placeholder={placeholder}
495
+ value={ctx.query}
496
+ onChange={(event) => ctx.setQuery(event.target.value)}
497
+ onKeyDown={(event) => {
498
+ if (event.key === "ArrowDown") {
499
+ const first = ctx.contentRef.current?.querySelector<HTMLElement>(
500
+ "[data-slot=tree-view-item], [data-slot=tree-view-branch-control]"
501
+ )
502
+ if (first) {
503
+ event.preventDefault()
504
+ first.focus()
505
+ }
506
+ }
507
+ }}
508
+ {...props}
509
+ />
510
+ </InputGroup>
511
+ )
512
+ }
513
+
514
+ function TreeSelectEmpty({ className, children, ...props }: React.ComponentProps<"div">) {
515
+ const ctx = useTreeSelect()
516
+ if (ctx.filtered.getNodeChildren(ctx.filtered.rootNode).length > 0) return null
517
+ return (
518
+ <div
519
+ data-slot="tree-select-empty"
520
+ className={cn("px-2 py-4 text-center text-sm text-muted-foreground", className)}
521
+ {...props}
522
+ >
523
+ {children ?? "No results"}
524
+ </div>
525
+ )
526
+ }
527
+
528
+ /* ---------------------------------- tree --------------------------------- */
529
+
530
+ type TreeSelectRenderNode<T extends TreeNode> = (node: T) => React.ReactNode
531
+
532
+ const TreeSelectRenderContext = React.createContext<TreeSelectRenderNode<TreeNode> | undefined>(undefined)
533
+
534
+ /**
535
+ * The tree inside the popup. Renders every node of the (filtered) collection;
536
+ * pass a render-prop child to customise the row label.
537
+ */
538
+ function TreeSelectTree<T extends TreeNode>({
539
+ className,
540
+ children,
541
+ ...props
542
+ }: Omit<React.ComponentProps<typeof TreeViewTree>, "children"> & { children?: TreeSelectRenderNode<T> }) {
543
+ const ctx = useTreeSelect<T>()
544
+ const searching = ctx.query.trim().length > 0
545
+ const searchExpanded = React.useMemo(
546
+ () => (searching ? ctx.filtered.getBranchValues() : null),
547
+ [searching, ctx.filtered]
548
+ )
549
+ const roots = ctx.filtered.getNodeChildren(ctx.filtered.rootNode)
550
+ if (roots.length === 0) return null
551
+ return (
552
+ <TreeSelectRenderContext.Provider value={children as TreeSelectRenderNode<TreeNode> | undefined}>
553
+ <TreeView
554
+ collection={ctx.filtered}
555
+ selectionMode={ctx.multiple ? "multiple" : "single"}
556
+ selectedValue={ctx.multiple ? [] : ctx.value}
557
+ onSelectionChange={({ selectedValue }) => {
558
+ const picked = selectedValue.find((v) => ctx.multiple || !ctx.value.includes(v)) ?? selectedValue[0]
559
+ const node = picked != null ? ctx.filtered.findNode(picked) : undefined
560
+ if (node) ctx.toggle(node)
561
+ }}
562
+ checkedValue={ctx.multiple ? ctx.value : undefined}
563
+ onCheckedChange={({ checkedValue }) => ctx.setValue(checkedValue)}
564
+ expandedValue={searchExpanded ?? ctx.expandedValue}
565
+ onExpandedChange={({ expandedValue }) => {
566
+ if (!searching) ctx.setExpandedValue(expandedValue)
567
+ }}
568
+ data-slot="tree-select-tree"
569
+ className="w-full"
570
+ >
571
+ <TreeViewTree className={cn("max-h-72 overflow-y-auto", className)} {...props}>
572
+ {roots.map((node, index) => (
573
+ <TreeSelectNode key={ctx.filtered.getNodeValue(node)} node={node} indexPath={[index]} />
574
+ ))}
575
+ </TreeViewTree>
576
+ </TreeView>
577
+ </TreeSelectRenderContext.Provider>
578
+ )
579
+ }
580
+
581
+ function TreeSelectNode<T extends TreeNode>({ node, indexPath }: { node: T; indexPath: number[] }) {
582
+ const ctx = useTreeSelect<T>()
583
+ const render = React.useContext(TreeSelectRenderContext)
584
+ const value = ctx.filtered.getNodeValue(node)
585
+ const label = render ? render(node) : ctx.filtered.stringifyNode(node)
586
+ const children = ctx.filtered.getNodeChildren(node)
587
+ const isBranch = ctx.filtered.isBranchNode(node)
588
+ return (
589
+ <TreeViewNodeProvider node={node} indexPath={indexPath}>
590
+ {isBranch ? (
591
+ <TreeViewBranch data-slot="tree-select-branch">
592
+ <TreeViewBranchControl>
593
+ <TreeViewBranchTrigger>
594
+ <TreeViewBranchIndicator />
595
+ </TreeViewBranchTrigger>
596
+ {ctx.multiple && <TreeViewNodeCheckbox />}
597
+ <TreeViewBranchText>{label}</TreeViewBranchText>
598
+ </TreeViewBranchControl>
599
+ <TreeViewBranchContent>
600
+ <TreeViewBranchIndentGuide />
601
+ {children.map((child, index) => (
602
+ <TreeSelectNode key={ctx.filtered.getNodeValue(child)} node={child} indexPath={[...indexPath, index]} />
603
+ ))}
604
+ </TreeViewBranchContent>
605
+ </TreeViewBranch>
606
+ ) : (
607
+ <TreeViewItem data-slot="tree-select-item" data-value={value}>
608
+ {ctx.multiple && <TreeViewNodeCheckbox />}
609
+ <TreeViewItemText>{label}</TreeViewItemText>
610
+ {!ctx.multiple && <TreeViewItemIndicator />}
611
+ </TreeViewItem>
612
+ )}
613
+ </TreeViewNodeProvider>
614
+ )
615
+ }
616
+
617
+ export {
618
+ TreeSelect,
619
+ TreeSelectChip,
620
+ TreeSelectChips,
621
+ TreeSelectClearTrigger,
622
+ TreeSelectContent,
623
+ TreeSelectEmpty,
624
+ TreeSelectHiddenInput,
625
+ TreeSelectIndicator,
626
+ TreeSelectNode,
627
+ TreeSelectSearch,
628
+ TreeSelectTree,
629
+ TreeSelectTrigger,
630
+ TreeSelectValue,
631
+ collapseValues,
632
+ createTreeCollection,
633
+ useTreeSelect,
634
+ type TreeCollection,
635
+ type TreeNode,
636
+ type TreeSelectProps,
637
+ type TreeSelectValueChangeDetails,
638
+ }