@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.
- package/README.md +78 -0
- package/bin/arkcn.mjs +78 -0
- package/cli/commands.mjs +251 -0
- package/cli/project.mjs +168 -0
- package/cli/registry.mjs +50 -0
- package/cli/ui.mjs +46 -0
- package/mcp/server.mjs +149 -0
- package/package.json +201 -0
- package/registry/base.css +680 -0
- package/registry/index.json +1833 -0
- package/registry/items/accordion.json +18 -0
- package/registry/items/activity-feed.json +18 -0
- package/registry/items/alert-dialog.json +18 -0
- package/registry/items/alert.json +17 -0
- package/registry/items/angle-slider.json +17 -0
- package/registry/items/app-shell.json +28 -0
- package/registry/items/aspect-ratio.json +13 -0
- package/registry/items/attachment.json +19 -0
- package/registry/items/avatar.json +17 -0
- package/registry/items/badge.json +18 -0
- package/registry/items/breadcrumb.json +18 -0
- package/registry/items/bubble.json +18 -0
- package/registry/items/button-group.json +19 -0
- package/registry/items/button.json +18 -0
- package/registry/items/calendar.json +19 -0
- package/registry/items/canvas.json +24 -0
- package/registry/items/card.json +15 -0
- package/registry/items/carousel.json +19 -0
- package/registry/items/cascader.json +23 -0
- package/registry/items/chart.json +17 -0
- package/registry/items/chat.json +23 -0
- package/registry/items/checkbox.json +18 -0
- package/registry/items/clipboard.json +19 -0
- package/registry/items/collapsible.json +15 -0
- package/registry/items/color-picker.json +19 -0
- package/registry/items/combobox.json +20 -0
- package/registry/items/command.json +21 -0
- package/registry/items/comment-thread.json +25 -0
- package/registry/items/context-menu.json +18 -0
- package/registry/items/controllable.json +14 -0
- package/registry/items/data-grid.json +24 -0
- package/registry/items/data-table-adapter.json +14 -0
- package/registry/items/data-table.json +32 -0
- package/registry/items/date-input.json +17 -0
- package/registry/items/dialog.json +19 -0
- package/registry/items/direction.json +13 -0
- package/registry/items/drag-preview.json +16 -0
- package/registry/items/drawer.json +18 -0
- package/registry/items/dropdown-menu.json +18 -0
- package/registry/items/editable.json +17 -0
- package/registry/items/empty.json +17 -0
- package/registry/items/field.json +19 -0
- package/registry/items/file-upload.json +19 -0
- package/registry/items/floating-panel.json +19 -0
- package/registry/items/floating-toolbar.json +16 -0
- package/registry/items/form.json +14 -0
- package/registry/items/format.json +15 -0
- package/registry/items/gantt.json +22 -0
- package/registry/items/history.json +14 -0
- package/registry/items/hotkeys.json +18 -0
- package/registry/items/hover-card.json +17 -0
- package/registry/items/image-cropper.json +17 -0
- package/registry/items/input-group.json +20 -0
- package/registry/items/input-otp.json +18 -0
- package/registry/items/input.json +15 -0
- package/registry/items/item.json +19 -0
- package/registry/items/json-tree-view.json +17 -0
- package/registry/items/kanban.json +26 -0
- package/registry/items/kbd.json +15 -0
- package/registry/items/label.json +15 -0
- package/registry/items/live-region.json +16 -0
- package/registry/items/marker.json +18 -0
- package/registry/items/marquee.json +17 -0
- package/registry/items/menubar.json +19 -0
- package/registry/items/message.json +15 -0
- package/registry/items/navigation-menu.json +19 -0
- package/registry/items/node-graph.json +22 -0
- package/registry/items/number-input.json +18 -0
- package/registry/items/pagination.json +18 -0
- package/registry/items/password-input.json +18 -0
- package/registry/items/popconfirm.json +20 -0
- package/registry/items/popover.json +18 -0
- package/registry/items/progress.json +17 -0
- package/registry/items/qr-code.json +17 -0
- package/registry/items/query-builder.json +23 -0
- package/registry/items/radio-group.json +17 -0
- package/registry/items/rating-group.json +18 -0
- package/registry/items/resizable.json +17 -0
- package/registry/items/rich-text-editor.json +37 -0
- package/registry/items/row-layout.json +15 -0
- package/registry/items/scheduler.json +23 -0
- package/registry/items/scroll-area.json +17 -0
- package/registry/items/segment-group.json +17 -0
- package/registry/items/select.json +19 -0
- package/registry/items/separator.json +15 -0
- package/registry/items/sheet.json +19 -0
- package/registry/items/sidebar.json +27 -0
- package/registry/items/signature-pad.json +19 -0
- package/registry/items/skeleton.json +15 -0
- package/registry/items/slider.json +17 -0
- package/registry/items/sonner.json +17 -0
- package/registry/items/spinner.json +17 -0
- package/registry/items/steps.json +19 -0
- package/registry/items/swap.json +17 -0
- package/registry/items/switch.json +17 -0
- package/registry/items/table.json +16 -0
- package/registry/items/tabs.json +18 -0
- package/registry/items/tags-input.json +18 -0
- package/registry/items/textarea.json +15 -0
- package/registry/items/time.json +14 -0
- package/registry/items/timer.json +18 -0
- package/registry/items/toc.json +17 -0
- package/registry/items/toggle-group.json +19 -0
- package/registry/items/toggle.json +18 -0
- package/registry/items/tooltip.json +17 -0
- package/registry/items/tour.json +19 -0
- package/registry/items/transfer-list.json +24 -0
- package/registry/items/tree-select.json +24 -0
- package/registry/items/tree-view.json +18 -0
- package/registry/items/use-mobile.json +13 -0
- package/registry/items/utils.json +17 -0
- package/registry/items/virtual-list.json +16 -0
- package/src/components/ui/accordion.tsx +111 -0
- package/src/components/ui/activity-feed.tsx +228 -0
- package/src/components/ui/alert-dialog.tsx +204 -0
- package/src/components/ui/alert.tsx +55 -0
- package/src/components/ui/angle-slider.tsx +117 -0
- package/src/components/ui/app-shell.tsx +985 -0
- package/src/components/ui/aspect-ratio.tsx +35 -0
- package/src/components/ui/attachment.tsx +193 -0
- package/src/components/ui/avatar.tsx +94 -0
- package/src/components/ui/badge.tsx +45 -0
- package/src/components/ui/breadcrumb.tsx +93 -0
- package/src/components/ui/bubble.tsx +119 -0
- package/src/components/ui/button-group.tsx +79 -0
- package/src/components/ui/button.tsx +65 -0
- package/src/components/ui/calendar.tsx +172 -0
- package/src/components/ui/canvas.tsx +723 -0
- package/src/components/ui/card.tsx +69 -0
- package/src/components/ui/carousel.tsx +228 -0
- package/src/components/ui/cascader.tsx +754 -0
- package/src/components/ui/chart.tsx +303 -0
- package/src/components/ui/chat.tsx +544 -0
- package/src/components/ui/checkbox.tsx +94 -0
- package/src/components/ui/clipboard.tsx +94 -0
- package/src/components/ui/collapsible.tsx +32 -0
- package/src/components/ui/color-picker.tsx +441 -0
- package/src/components/ui/combobox.tsx +400 -0
- package/src/components/ui/command.tsx +292 -0
- package/src/components/ui/comment-thread.tsx +923 -0
- package/src/components/ui/context-menu.tsx +331 -0
- package/src/components/ui/data-grid.tsx +1227 -0
- package/src/components/ui/data-table.tsx +993 -0
- package/src/components/ui/date-input.tsx +103 -0
- package/src/components/ui/dialog.tsx +159 -0
- package/src/components/ui/direction.tsx +32 -0
- package/src/components/ui/drawer.tsx +200 -0
- package/src/components/ui/dropdown-menu.tsx +329 -0
- package/src/components/ui/editable.tsx +90 -0
- package/src/components/ui/empty.tsx +86 -0
- package/src/components/ui/field.tsx +218 -0
- package/src/components/ui/file-upload.tsx +204 -0
- package/src/components/ui/floating-panel.tsx +199 -0
- package/src/components/ui/floating-toolbar.tsx +79 -0
- package/src/components/ui/format.tsx +22 -0
- package/src/components/ui/gantt.tsx +950 -0
- package/src/components/ui/hotkeys.tsx +340 -0
- package/src/components/ui/hover-card.tsx +83 -0
- package/src/components/ui/image-cropper.tsx +100 -0
- package/src/components/ui/input-group.tsx +133 -0
- package/src/components/ui/input-otp.tsx +82 -0
- package/src/components/ui/input.tsx +18 -0
- package/src/components/ui/item.tsx +174 -0
- package/src/components/ui/json-tree-view.tsx +37 -0
- package/src/components/ui/kanban.tsx +884 -0
- package/src/components/ui/kbd.tsx +20 -0
- package/src/components/ui/label.tsx +30 -0
- package/src/components/ui/live-region.tsx +54 -0
- package/src/components/ui/marker.tsx +66 -0
- package/src/components/ui/marquee.tsx +61 -0
- package/src/components/ui/menubar.tsx +456 -0
- package/src/components/ui/message.tsx +74 -0
- package/src/components/ui/navigation-menu.tsx +202 -0
- package/src/components/ui/node-graph.tsx +1723 -0
- package/src/components/ui/number-input.tsx +148 -0
- package/src/components/ui/pagination.tsx +88 -0
- package/src/components/ui/password-input.tsx +104 -0
- package/src/components/ui/popconfirm.tsx +149 -0
- package/src/components/ui/popover.tsx +126 -0
- package/src/components/ui/progress.tsx +114 -0
- package/src/components/ui/qr-code.tsx +51 -0
- package/src/components/ui/query-builder.tsx +938 -0
- package/src/components/ui/radio-group.tsx +108 -0
- package/src/components/ui/rating-group.tsx +91 -0
- package/src/components/ui/resizable.tsx +41 -0
- package/src/components/ui/rich-text-editor.tsx +845 -0
- package/src/components/ui/scheduler.tsx +1164 -0
- package/src/components/ui/scroll-area.tsx +82 -0
- package/src/components/ui/segment-group.tsx +95 -0
- package/src/components/ui/select.tsx +354 -0
- package/src/components/ui/separator.tsx +32 -0
- package/src/components/ui/sheet.tsx +130 -0
- package/src/components/ui/sidebar.tsx +665 -0
- package/src/components/ui/signature-pad.tsx +112 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/slider.tsx +183 -0
- package/src/components/ui/sonner.tsx +37 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/steps.tsx +152 -0
- package/src/components/ui/swap.tsx +33 -0
- package/src/components/ui/switch.tsx +83 -0
- package/src/components/ui/table.tsx +72 -0
- package/src/components/ui/tabs.tsx +100 -0
- package/src/components/ui/tags-input.tsx +153 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/timer.tsx +67 -0
- package/src/components/ui/toc.tsx +64 -0
- package/src/components/ui/toggle-group.tsx +89 -0
- package/src/components/ui/toggle.tsx +46 -0
- package/src/components/ui/tooltip.tsx +113 -0
- package/src/components/ui/tour.tsx +168 -0
- package/src/components/ui/transfer-list.tsx +700 -0
- package/src/components/ui/tree-select.tsx +638 -0
- package/src/components/ui/tree-view.tsx +217 -0
- package/src/components/ui/virtual-list.tsx +343 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/lib/controllable.ts +31 -0
- package/src/lib/data-table-adapter.ts +67 -0
- package/src/lib/drag-preview.ts +39 -0
- package/src/lib/form.ts +39 -0
- package/src/lib/history.ts +55 -0
- package/src/lib/row-layout.ts +81 -0
- package/src/lib/table.ts +483 -0
- package/src/lib/time.ts +37 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/arkcn.css +764 -0
- 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
|
+
}
|