@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,754 @@
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 { CheckIcon, ChevronDownIcon, ChevronRightIcon, 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
+
12
+ /* -------------------------------- helpers -------------------------------- */
13
+
14
+ /** Ancestors (root excluded) followed by the node itself. */
15
+ function pathNodes<T extends TreeNode>(collection: TreeCollection<T>, value: string): T[] {
16
+ const node = collection.findNode(value)
17
+ if (!node) return []
18
+ const parents = collection.getParentNodes(value).filter((n) => !collection.isRootNode(n))
19
+ return [...parents, node]
20
+ }
21
+
22
+ function pathValues<T extends TreeNode>(collection: TreeCollection<T>, value: string): string[] {
23
+ return pathNodes(collection, value).map((n) => collection.getNodeValue(n))
24
+ }
25
+
26
+ const itemId = (base: string, value: string) => `${base}-${value.replace(/[^a-zA-Z0-9_-]/g, "_")}`
27
+
28
+ /* -------------------------------- context -------------------------------- */
29
+
30
+ type CascaderValueChangeDetails<T extends TreeNode = TreeNode> = {
31
+ /** `[]` or the selected node's value. */
32
+ value: string[]
33
+ /** Nodes from the first level down to the selected node. */
34
+ path: T[]
35
+ }
36
+
37
+ type CascaderContextValue<T extends TreeNode = TreeNode> = {
38
+ collection: TreeCollection<T>
39
+ value: string[]
40
+ setValue: (value: string[]) => void
41
+ /** Branch values open from the first column down; column `i + 1` lists the children of `expandedPath[i]`. */
42
+ expandedPath: string[]
43
+ setExpandedPath: (path: string[]) => void
44
+ highlighted: string | null
45
+ setHighlighted: (value: string | null) => void
46
+ open: boolean
47
+ setOpen: (open: boolean) => void
48
+ query: string
49
+ setQuery: (query: string) => void
50
+ changeOnSelect: boolean
51
+ expandTrigger: "click" | "hover"
52
+ disabled: boolean
53
+ readOnly: boolean
54
+ invalid: boolean
55
+ /** Activate a node: pick a leaf (and close), or open a branch (and pick it when `changeOnSelect`). */
56
+ activate: (node: T) => void
57
+ /** Expand a branch and highlight it, without picking. */
58
+ expand: (node: T) => void
59
+ clear: () => void
60
+ /** Nodes on the path to the current value. */
61
+ valuePath: T[]
62
+ /** Flat search matches: each entry is a path of nodes. */
63
+ matches: T[][]
64
+ columns: T[][]
65
+ ids: { content: string; item: (value: string) => string }
66
+ contentRef: React.RefObject<HTMLDivElement | null>
67
+ moveHighlight: (key: string) => boolean
68
+ }
69
+
70
+ const CascaderContext = React.createContext<CascaderContextValue | null>(null)
71
+
72
+ function useCascader<T extends TreeNode = TreeNode>() {
73
+ const ctx = React.useContext(CascaderContext)
74
+ if (!ctx) throw new Error("Cascader parts must be used inside <Cascader>")
75
+ return ctx as unknown as CascaderContextValue<T>
76
+ }
77
+
78
+ /* ---------------------------------- root --------------------------------- */
79
+
80
+ type CascaderProps<T extends TreeNode> = Omit<
81
+ React.ComponentProps<typeof PopoverPrimitive.Root>,
82
+ "open" | "defaultOpen" | "onOpenChange" | "children"
83
+ > & {
84
+ collection: TreeCollection<T>
85
+ value?: string[]
86
+ defaultValue?: string[]
87
+ onValueChange?: (details: CascaderValueChangeDetails<T>) => void
88
+ /** Let a branch be the answer: clicking it picks it and keeps the popup open. */
89
+ changeOnSelect?: boolean
90
+ /** Open the next column on hover instead of click. */
91
+ expandTrigger?: "click" | "hover"
92
+ open?: boolean
93
+ defaultOpen?: boolean
94
+ onOpenChange?: (details: { open: boolean }) => void
95
+ disabled?: boolean
96
+ readOnly?: boolean
97
+ invalid?: boolean
98
+ children: React.ReactNode
99
+ }
100
+
101
+ function Cascader<T extends TreeNode>({
102
+ collection,
103
+ value: valueProp,
104
+ defaultValue = [],
105
+ onValueChange,
106
+ changeOnSelect = false,
107
+ expandTrigger = "click",
108
+ open: openProp,
109
+ defaultOpen = false,
110
+ onOpenChange,
111
+ disabled = false,
112
+ readOnly = false,
113
+ invalid = false,
114
+ positioning,
115
+ lazyMount = true,
116
+ unmountOnExit = true,
117
+ ids: idsProp,
118
+ children,
119
+ ...props
120
+ }: CascaderProps<T>) {
121
+ const [value, setValueState] = useControllable<string[]>(valueProp, defaultValue, (next) =>
122
+ onValueChange?.({ value: next, path: next[0] != null ? pathNodes(collection, next[0]) : [] })
123
+ )
124
+ const [open, setOpenState] = useControllable<boolean>(openProp, defaultOpen, (next) => onOpenChange?.({ open: next }))
125
+ const [expandedPath, setExpandedPath] = React.useState<string[]>([])
126
+ const [highlighted, setHighlighted] = React.useState<string | null>(null)
127
+ const [query, setQueryState] = React.useState("")
128
+ const contentRef = React.useRef<HTMLDivElement | null>(null)
129
+ const base = React.useId()
130
+ const ids = React.useMemo(
131
+ () => ({ content: idsProp?.content ?? `${base}-content`, item: (v: string) => itemId(`${base}-item`, v) }),
132
+ [base, idsProp?.content]
133
+ )
134
+
135
+ const setValue = React.useCallback(
136
+ (next: string[]) => {
137
+ if (!readOnly) setValueState(next)
138
+ },
139
+ [readOnly, setValueState]
140
+ )
141
+
142
+ const setOpen = React.useCallback(
143
+ (next: boolean) => {
144
+ if (disabled) return
145
+ if (next) {
146
+ // Start from the current value: its ancestors open, itself highlighted.
147
+ const current = value[0]
148
+ const path = current != null ? pathValues(collection, current) : []
149
+ setExpandedPath(path.slice(0, -1))
150
+ setHighlighted(current ?? null)
151
+ } else {
152
+ setQueryState("")
153
+ }
154
+ setOpenState(next)
155
+ },
156
+ [disabled, value, collection, setOpenState]
157
+ )
158
+
159
+ const columns = React.useMemo(() => {
160
+ const cols: T[][] = [collection.getNodeChildren(collection.rootNode)]
161
+ for (const v of expandedPath) {
162
+ const node = collection.findNode(v)
163
+ if (!node) break
164
+ cols.push(collection.getNodeChildren(node))
165
+ }
166
+ return cols
167
+ }, [collection, expandedPath])
168
+
169
+ const matches = React.useMemo(() => {
170
+ const tokens = query.trim().toLowerCase().split(/\s+/).filter(Boolean)
171
+ if (tokens.length === 0) return []
172
+ const out: T[][] = []
173
+ collection.visit({
174
+ onEnter: (node) => {
175
+ if (collection.isRootNode(node)) return
176
+ if (collection.isBranchNode(node) && !changeOnSelect) return
177
+ if (collection.getNodeDisabled(node)) return
178
+ const path = pathNodes(collection, collection.getNodeValue(node))
179
+ const text = path
180
+ .map((n) => collection.stringifyNode(n))
181
+ .join(" ")
182
+ .toLowerCase()
183
+ if (tokens.every((t) => text.includes(t))) out.push(path)
184
+ },
185
+ })
186
+ return out.slice(0, 50)
187
+ }, [collection, query, changeOnSelect])
188
+
189
+ const setQuery = React.useCallback((next: string) => {
190
+ setQueryState(next)
191
+ setHighlighted(null)
192
+ }, [])
193
+
194
+ const expand = React.useCallback(
195
+ (node: T) => {
196
+ const v = collection.getNodeValue(node)
197
+ setHighlighted(v)
198
+ if (collection.isBranchNode(node)) setExpandedPath(pathValues(collection, v))
199
+ else setExpandedPath(pathValues(collection, v).slice(0, -1))
200
+ },
201
+ [collection]
202
+ )
203
+
204
+ const activate = React.useCallback(
205
+ (node: T) => {
206
+ if (collection.getNodeDisabled(node)) return
207
+ const v = collection.getNodeValue(node)
208
+ if (collection.isBranchNode(node)) {
209
+ expand(node)
210
+ if (changeOnSelect) setValue([v])
211
+ return
212
+ }
213
+ setValue([v])
214
+ setOpen(false)
215
+ },
216
+ [collection, changeOnSelect, expand, setValue, setOpen]
217
+ )
218
+
219
+ const clear = React.useCallback(() => setValue([]), [setValue])
220
+
221
+ /** Keyboard navigation over the columns (or the search matches). Returns true when handled. */
222
+ const moveHighlight = React.useCallback(
223
+ (key: string) => {
224
+ if (matches.length > 0) {
225
+ const values = matches.map((path) => collection.getNodeValue(path[path.length - 1]))
226
+ const index = highlighted ? values.indexOf(highlighted) : -1
227
+ if (key === "ArrowDown") setHighlighted(values[Math.min(values.length - 1, index + 1)])
228
+ else if (key === "ArrowUp") setHighlighted(values[Math.max(0, index - 1)])
229
+ else if (key === "Home") setHighlighted(values[0])
230
+ else if (key === "End") setHighlighted(values[values.length - 1])
231
+ else if (key === "Enter" && highlighted) {
232
+ const node = collection.findNode(highlighted)
233
+ if (node) activate(node)
234
+ } else return false
235
+ return true
236
+ }
237
+ const enabled = (list: T[]) => list.filter((n) => !collection.getNodeDisabled(n))
238
+ const node = highlighted ? collection.findNode(highlighted) : undefined
239
+ if (!node) {
240
+ const col = enabled(columns[columns.length - 1] ?? [])
241
+ if (col.length && ["ArrowDown", "ArrowUp", "ArrowRight", "ArrowLeft", "Home", "End"].includes(key)) {
242
+ setHighlighted(collection.getNodeValue(key === "ArrowUp" || key === "End" ? col[col.length - 1] : col[0]))
243
+ return true
244
+ }
245
+ return false
246
+ }
247
+ const indexPath = collection.getIndexPath(highlighted!) ?? []
248
+ const siblings = enabled(collection.getSiblingNodes(indexPath))
249
+ const index = siblings.indexOf(node)
250
+ const isBranch = collection.isBranchNode(node)
251
+ switch (key) {
252
+ case "ArrowDown":
253
+ if (siblings[index + 1]) setHighlighted(collection.getNodeValue(siblings[index + 1]))
254
+ return true
255
+ case "ArrowUp":
256
+ if (siblings[index - 1]) setHighlighted(collection.getNodeValue(siblings[index - 1]))
257
+ return true
258
+ case "Home":
259
+ setHighlighted(collection.getNodeValue(siblings[0]))
260
+ return true
261
+ case "End":
262
+ setHighlighted(collection.getNodeValue(siblings[siblings.length - 1]))
263
+ return true
264
+ case "ArrowRight": {
265
+ if (!isBranch) return true
266
+ const first = enabled(collection.getNodeChildren(node))[0]
267
+ setExpandedPath(pathValues(collection, highlighted!))
268
+ if (first) setHighlighted(collection.getNodeValue(first))
269
+ return true
270
+ }
271
+ case "ArrowLeft": {
272
+ const parent = collection.getParentNode(highlighted!)
273
+ if (!parent || collection.isRootNode(parent)) return true
274
+ const pv = collection.getNodeValue(parent)
275
+ setHighlighted(pv)
276
+ setExpandedPath(pathValues(collection, pv).slice(0, -1))
277
+ return true
278
+ }
279
+ case "Enter":
280
+ case " ":
281
+ activate(node)
282
+ return true
283
+ default:
284
+ return false
285
+ }
286
+ },
287
+ [collection, columns, matches, highlighted, activate]
288
+ )
289
+
290
+ const valuePath = React.useMemo(() => (value[0] != null ? pathNodes(collection, value[0]) : []), [collection, value])
291
+
292
+ const ctx: CascaderContextValue<T> = {
293
+ collection,
294
+ value,
295
+ setValue,
296
+ expandedPath,
297
+ setExpandedPath,
298
+ highlighted,
299
+ setHighlighted,
300
+ open,
301
+ setOpen,
302
+ query,
303
+ setQuery,
304
+ changeOnSelect,
305
+ expandTrigger,
306
+ disabled,
307
+ readOnly,
308
+ invalid,
309
+ activate,
310
+ expand,
311
+ clear,
312
+ valuePath,
313
+ matches,
314
+ columns,
315
+ ids,
316
+ contentRef,
317
+ moveHighlight,
318
+ }
319
+
320
+ return (
321
+ <CascaderContext.Provider value={ctx as unknown as CascaderContextValue}>
322
+ <PopoverPrimitive.Root
323
+ open={open}
324
+ onOpenChange={({ open: next }) => setOpen(next)}
325
+ positioning={{ placement: "bottom-start", gutter: 4, ...positioning }}
326
+ lazyMount={lazyMount}
327
+ unmountOnExit={unmountOnExit}
328
+ ids={{ ...idsProp, content: ids.content }}
329
+ {...props}
330
+ >
331
+ {children}
332
+ </PopoverPrimitive.Root>
333
+ </CascaderContext.Provider>
334
+ )
335
+ }
336
+
337
+ /* -------------------------------- trigger -------------------------------- */
338
+
339
+ function CascaderTrigger({
340
+ className,
341
+ size = "default",
342
+ variant = "default",
343
+ children,
344
+ onKeyDown,
345
+ ...props
346
+ }: React.ComponentProps<"div"> & { size?: "sm" | "default"; variant?: "default" | "unstyled" }) {
347
+ const ctx = useCascader()
348
+ return (
349
+ <PopoverTrigger asChild>
350
+ <div
351
+ data-slot="cascader-trigger"
352
+ role="combobox"
353
+ aria-haspopup="tree"
354
+ aria-expanded={ctx.open}
355
+ aria-controls={ctx.open ? ctx.ids.content : undefined}
356
+ aria-disabled={ctx.disabled || undefined}
357
+ aria-invalid={ctx.invalid || undefined}
358
+ aria-readonly={ctx.readOnly || undefined}
359
+ tabIndex={ctx.disabled ? -1 : 0}
360
+ data-size={size}
361
+ data-variant={variant}
362
+ data-state={ctx.open ? "open" : "closed"}
363
+ data-disabled={ctx.disabled ? "" : undefined}
364
+ data-invalid={ctx.invalid ? "" : undefined}
365
+ data-placeholder-shown={ctx.value.length === 0 ? "" : undefined}
366
+ className={cn(
367
+ "flex cursor-default items-center justify-between gap-1.5 text-sm whitespace-nowrap 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",
368
+ variant === "default" &&
369
+ "w-full rounded-lg border border-input bg-transparent py-2 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]:h-8 data-[size=sm]: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",
370
+ className
371
+ )}
372
+ onKeyDown={(event) => {
373
+ onKeyDown?.(event)
374
+ if (event.defaultPrevented || ctx.disabled) return
375
+ if (event.key === "Enter" || event.key === " " || event.key === "ArrowDown") {
376
+ event.preventDefault()
377
+ ctx.setOpen(true)
378
+ } else if ((event.key === "Backspace" || event.key === "Delete") && ctx.value.length) {
379
+ ctx.clear()
380
+ }
381
+ }}
382
+ {...props}
383
+ >
384
+ {children}
385
+ </div>
386
+ </PopoverTrigger>
387
+ )
388
+ }
389
+
390
+ function CascaderIndicator({ className, children, ...props }: React.ComponentProps<"span">) {
391
+ const ctx = useCascader()
392
+ return (
393
+ <span
394
+ data-slot="cascader-indicator"
395
+ data-state={ctx.open ? "open" : "closed"}
396
+ className={cn("ml-auto flex shrink-0 items-center text-muted-foreground", className)}
397
+ {...props}
398
+ >
399
+ {children ?? <ChevronDownIcon className="size-4" />}
400
+ </span>
401
+ )
402
+ }
403
+
404
+ /** The selected path as text ("Country / State / City"), or the placeholder. */
405
+ function CascaderValue<T extends TreeNode>({
406
+ placeholder,
407
+ separator = " / ",
408
+ children,
409
+ className,
410
+ ...props
411
+ }: Omit<React.ComponentProps<"span">, "children"> & {
412
+ placeholder?: React.ReactNode
413
+ separator?: React.ReactNode
414
+ children?: (path: T[]) => React.ReactNode
415
+ }) {
416
+ const ctx = useCascader<T>()
417
+ const empty = ctx.valuePath.length === 0
418
+ return (
419
+ <span
420
+ data-slot="cascader-value"
421
+ data-placeholder-shown={empty ? "" : undefined}
422
+ className={cn("flex min-w-0 flex-1 items-center gap-1 truncate text-left", className)}
423
+ {...props}
424
+ >
425
+ {empty
426
+ ? placeholder
427
+ : children
428
+ ? children(ctx.valuePath)
429
+ : ctx.valuePath.map((node, index) => (
430
+ <React.Fragment key={ctx.collection.getNodeValue(node)}>
431
+ {index > 0 && (
432
+ <span data-slot="cascader-value-separator" className="text-muted-foreground">
433
+ {separator}
434
+ </span>
435
+ )}
436
+ <span className={cn(index < ctx.valuePath.length - 1 && "text-muted-foreground")}>
437
+ {ctx.collection.stringifyNode(node)}
438
+ </span>
439
+ </React.Fragment>
440
+ ))}
441
+ </span>
442
+ )
443
+ }
444
+
445
+ function CascaderClearTrigger({ className, children, ...props }: React.ComponentProps<typeof Button>) {
446
+ const ctx = useCascader()
447
+ if (ctx.value.length === 0 || ctx.readOnly) return null
448
+ return (
449
+ <Button
450
+ type="button"
451
+ variant="ghost"
452
+ size="icon-xs"
453
+ data-slot="cascader-clear-trigger"
454
+ aria-label="Clear selection"
455
+ disabled={ctx.disabled}
456
+ className={cn("-mr-1 text-muted-foreground hover:text-foreground", className)}
457
+ onClick={(event) => {
458
+ event.stopPropagation()
459
+ ctx.clear()
460
+ }}
461
+ onKeyDown={(event) => event.stopPropagation()}
462
+ {...props}
463
+ >
464
+ {children ?? <XIcon />}
465
+ </Button>
466
+ )
467
+ }
468
+
469
+ function CascaderHiddenInput({
470
+ name,
471
+ ...props
472
+ }: Omit<React.ComponentProps<"input">, "type" | "value"> & { name: string }) {
473
+ const ctx = useCascader()
474
+ return <input type="hidden" name={name} value={ctx.value[0] ?? ""} data-slot="cascader-hidden-input" {...props} />
475
+ }
476
+
477
+ /* -------------------------------- content -------------------------------- */
478
+
479
+ function CascaderContent({ className, onKeyDown, ...props }: React.ComponentProps<typeof PopoverContent>) {
480
+ const ctx = useCascader()
481
+ // Keep the highlighted item in view.
482
+ React.useEffect(() => {
483
+ if (!ctx.highlighted) return
484
+ const el = ctx.contentRef.current?.querySelector<HTMLElement>(`#${CSS.escape(ctx.ids.item(ctx.highlighted))}`)
485
+ el?.scrollIntoView({ block: "nearest", inline: "nearest" })
486
+ }, [ctx.highlighted, ctx.ids, ctx.contentRef, ctx.columns.length])
487
+ return (
488
+ <PopoverContent
489
+ ref={ctx.contentRef}
490
+ data-slot="cascader-content"
491
+ className={cn("w-auto min-w-(--reference-width) gap-1 p-1", className)}
492
+ onKeyDown={(event) => {
493
+ onKeyDown?.(event)
494
+ if (event.defaultPrevented) return
495
+ if (event.target instanceof HTMLInputElement) {
496
+ // The search keeps left/right for its caret only while it has text.
497
+ const vertical = ["ArrowDown", "ArrowUp", "Enter"].includes(event.key)
498
+ const horizontal = ["ArrowLeft", "ArrowRight"].includes(event.key) && event.target.value === ""
499
+ if (!vertical && !horizontal) return
500
+ }
501
+ if (ctx.moveHighlight(event.key)) event.preventDefault()
502
+ }}
503
+ {...props}
504
+ />
505
+ )
506
+ }
507
+
508
+ function CascaderSearch({
509
+ className,
510
+ placeholder = "Search…",
511
+ ...props
512
+ }: Omit<React.ComponentProps<typeof InputGroupInput>, "value" | "onChange">) {
513
+ const ctx = useCascader()
514
+ return (
515
+ <InputGroup data-slot="cascader-search" className={cn("h-8 shrink-0", className)}>
516
+ <InputGroupAddon>
517
+ <SearchIcon />
518
+ </InputGroupAddon>
519
+ <InputGroupInput
520
+ role="searchbox"
521
+ placeholder={placeholder}
522
+ value={ctx.query}
523
+ onChange={(event) => ctx.setQuery(event.target.value)}
524
+ aria-activedescendant={ctx.highlighted ? ctx.ids.item(ctx.highlighted) : undefined}
525
+ {...props}
526
+ />
527
+ </InputGroup>
528
+ )
529
+ }
530
+
531
+ /** The column view. Hidden while a search query has matches to show. */
532
+ function CascaderColumns<T extends TreeNode>({
533
+ className,
534
+ children,
535
+ ...props
536
+ }: Omit<React.ComponentProps<"div">, "children"> & { children?: (node: T) => React.ReactNode }) {
537
+ const ctx = useCascader<T>()
538
+ if (ctx.query.trim()) return null
539
+ return (
540
+ <div
541
+ data-slot="cascader-columns"
542
+ role="tree"
543
+ tabIndex={0}
544
+ aria-activedescendant={ctx.highlighted ? ctx.ids.item(ctx.highlighted) : undefined}
545
+ className={cn("flex max-h-72 overflow-x-auto outline-none", className)}
546
+ {...props}
547
+ >
548
+ {ctx.columns.map((nodes, depth) => (
549
+ <CascaderColumn key={depth} depth={depth}>
550
+ {nodes.map((node) =>
551
+ children ? (
552
+ <React.Fragment key={ctx.collection.getNodeValue(node)}>{children(node)}</React.Fragment>
553
+ ) : (
554
+ <CascaderItem key={ctx.collection.getNodeValue(node)} node={node} />
555
+ )
556
+ )}
557
+ </CascaderColumn>
558
+ ))}
559
+ </div>
560
+ )
561
+ }
562
+
563
+ function CascaderColumn({ className, depth, ...props }: React.ComponentProps<"div"> & { depth: number }) {
564
+ return (
565
+ <div
566
+ data-slot="cascader-column"
567
+ role="group"
568
+ data-depth={depth}
569
+ className={cn(
570
+ "flex min-w-40 shrink-0 flex-col gap-px overflow-y-auto not-first:border-l not-first:pl-1 not-last:pr-1",
571
+ className
572
+ )}
573
+ {...props}
574
+ />
575
+ )
576
+ }
577
+
578
+ function CascaderItem<T extends TreeNode>({
579
+ node,
580
+ className,
581
+ children,
582
+ ...props
583
+ }: Omit<React.ComponentProps<"div">, "children"> & { node: T; children?: React.ReactNode }) {
584
+ const ctx = useCascader<T>()
585
+ const value = ctx.collection.getNodeValue(node)
586
+ const isBranch = ctx.collection.isBranchNode(node)
587
+ const disabled = ctx.collection.getNodeDisabled(node)
588
+ const expanded = ctx.expandedPath.includes(value)
589
+ const selected = ctx.value[0] === value || ctx.valuePath.some((n) => ctx.collection.getNodeValue(n) === value)
590
+ return (
591
+ <div
592
+ id={ctx.ids.item(value)}
593
+ data-slot="cascader-item"
594
+ role="treeitem"
595
+ aria-level={ctx.collection.getDepth(value)}
596
+ aria-expanded={isBranch ? expanded : undefined}
597
+ aria-selected={selected}
598
+ aria-disabled={disabled || undefined}
599
+ data-value={value}
600
+ data-branch={isBranch ? "" : undefined}
601
+ data-expanded={expanded ? "" : undefined}
602
+ data-selected={selected ? "" : undefined}
603
+ data-highlighted={ctx.highlighted === value ? "" : undefined}
604
+ data-disabled={disabled ? "" : undefined}
605
+ className={cn(
606
+ "relative flex h-7 cursor-default items-center gap-1.5 rounded-md px-2 text-sm outline-none select-none data-expanded:bg-muted data-highlighted:bg-muted data-selected:font-medium data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
607
+ className
608
+ )}
609
+ onMouseEnter={() => {
610
+ if (disabled) return
611
+ if (ctx.expandTrigger === "hover") ctx.expand(node)
612
+ else ctx.setHighlighted(value)
613
+ }}
614
+ onClick={() => ctx.activate(node)}
615
+ {...props}
616
+ >
617
+ {children ?? (
618
+ <>
619
+ <CascaderItemText>{ctx.collection.stringifyNode(node)}</CascaderItemText>
620
+ <CascaderItemIndicator />
621
+ </>
622
+ )}
623
+ </div>
624
+ )
625
+ }
626
+
627
+ function CascaderItemText({ className, ...props }: React.ComponentProps<"span">) {
628
+ return <span data-slot="cascader-item-text" className={cn("flex-1 truncate", className)} {...props} />
629
+ }
630
+
631
+ /** Chevron on branches, check on the selected leaf. Reads the nearest item's state from the DOM attributes. */
632
+ function CascaderItemIndicator({ className, ...props }: React.ComponentProps<"span">) {
633
+ return (
634
+ <span
635
+ data-slot="cascader-item-indicator"
636
+ className={cn(
637
+ "ml-auto text-muted-foreground not-in-data-selected:**:data-check:hidden in-data-branch:**:data-check:hidden not-in-data-branch:**:data-chevron:hidden",
638
+ className
639
+ )}
640
+ {...props}
641
+ >
642
+ <ChevronRightIcon data-chevron="" />
643
+ <CheckIcon data-check="" />
644
+ </span>
645
+ )
646
+ }
647
+
648
+ /** Flat list of matching paths while searching. */
649
+ function CascaderSearchResults<T extends TreeNode>({
650
+ className,
651
+ children,
652
+ ...props
653
+ }: Omit<React.ComponentProps<"div">, "children"> & { children?: (path: T[]) => React.ReactNode }) {
654
+ const ctx = useCascader<T>()
655
+ if (!ctx.query.trim()) return null
656
+ return (
657
+ <div
658
+ data-slot="cascader-search-results"
659
+ role="listbox"
660
+ className={cn("flex max-h-72 flex-col gap-px overflow-y-auto", className)}
661
+ {...props}
662
+ >
663
+ {ctx.matches.map((path) => {
664
+ const last = path[path.length - 1]
665
+ const value = ctx.collection.getNodeValue(last)
666
+ return children ? (
667
+ <React.Fragment key={value}>{children(path)}</React.Fragment>
668
+ ) : (
669
+ <CascaderSearchResult key={value} path={path} />
670
+ )
671
+ })}
672
+ </div>
673
+ )
674
+ }
675
+
676
+ function CascaderSearchResult<T extends TreeNode>({
677
+ path,
678
+ separator = " / ",
679
+ className,
680
+ children,
681
+ ...props
682
+ }: React.ComponentProps<"div"> & { path: T[]; separator?: React.ReactNode }) {
683
+ const ctx = useCascader<T>()
684
+ const last = path[path.length - 1]
685
+ const value = ctx.collection.getNodeValue(last)
686
+ return (
687
+ <div
688
+ id={ctx.ids.item(value)}
689
+ data-slot="cascader-search-result"
690
+ role="option"
691
+ aria-selected={ctx.value[0] === value}
692
+ data-value={value}
693
+ data-selected={ctx.value[0] === value ? "" : undefined}
694
+ data-highlighted={ctx.highlighted === value ? "" : undefined}
695
+ className={cn(
696
+ "flex h-7 cursor-default items-center gap-1 rounded-md px-2 text-sm outline-none select-none data-highlighted:bg-muted data-selected:font-medium",
697
+ className
698
+ )}
699
+ onMouseEnter={() => ctx.setHighlighted(value)}
700
+ onClick={() => ctx.activate(last)}
701
+ {...props}
702
+ >
703
+ {children ??
704
+ path.map((node, index) => (
705
+ <React.Fragment key={ctx.collection.getNodeValue(node)}>
706
+ {index > 0 && <span className="text-muted-foreground">{separator}</span>}
707
+ <span className={cn("truncate", index < path.length - 1 && "text-muted-foreground")}>
708
+ {ctx.collection.stringifyNode(node)}
709
+ </span>
710
+ </React.Fragment>
711
+ ))}
712
+ </div>
713
+ )
714
+ }
715
+
716
+ function CascaderEmpty({ className, children, ...props }: React.ComponentProps<"div">) {
717
+ const ctx = useCascader()
718
+ const searching = ctx.query.trim().length > 0
719
+ if (searching ? ctx.matches.length > 0 : ctx.columns[0].length > 0) return null
720
+ return (
721
+ <div
722
+ data-slot="cascader-empty"
723
+ className={cn("px-2 py-4 text-center text-sm text-muted-foreground", className)}
724
+ {...props}
725
+ >
726
+ {children ?? "No results"}
727
+ </div>
728
+ )
729
+ }
730
+
731
+ export {
732
+ Cascader,
733
+ CascaderClearTrigger,
734
+ CascaderColumn,
735
+ CascaderColumns,
736
+ CascaderContent,
737
+ CascaderEmpty,
738
+ CascaderHiddenInput,
739
+ CascaderIndicator,
740
+ CascaderItem,
741
+ CascaderItemIndicator,
742
+ CascaderItemText,
743
+ CascaderSearch,
744
+ CascaderSearchResult,
745
+ CascaderSearchResults,
746
+ CascaderTrigger,
747
+ CascaderValue,
748
+ createTreeCollection,
749
+ useCascader,
750
+ type CascaderProps,
751
+ type CascaderValueChangeDetails,
752
+ type TreeCollection,
753
+ type TreeNode,
754
+ }