@deftui/react 0.2.5 → 0.3.1

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 (49) hide show
  1. package/README.md +2 -2
  2. package/dist/index.d.mts +7 -5
  3. package/dist/index.mjs +4 -4
  4. package/dist/styles.css +3 -0
  5. package/dist/vendor/README.md +10 -9
  6. package/dist/vendor/coss/calendar.d.mts +4 -0
  7. package/dist/vendor/coss/calendar.mjs +70 -0
  8. package/dist/vendor/coss/card.d.mts +23 -0
  9. package/dist/vendor/coss/card.mjs +160 -0
  10. package/dist/vendor/coss/dialog.d.mts +1 -1
  11. package/dist/vendor/coss/dialog.mjs +1 -1
  12. package/dist/vendor/coss/table.d.mts +15 -0
  13. package/dist/vendor/coss/table.mjs +73 -0
  14. package/dist/vendor/reui.io/cascader/cascader-async.d.mts +20 -0
  15. package/dist/vendor/reui.io/cascader/cascader-async.mjs +578 -0
  16. package/dist/vendor/reui.io/cascader/cascader-context.d.mts +12 -0
  17. package/dist/vendor/reui.io/cascader/cascader-context.mjs +56 -0
  18. package/dist/vendor/reui.io/cascader/cascader-footer.d.mts +60 -0
  19. package/dist/vendor/reui.io/cascader/cascader-footer.mjs +445 -0
  20. package/dist/vendor/reui.io/cascader/cascader-i18n.mjs +64 -0
  21. package/dist/vendor/reui.io/cascader/cascader-item.d.mts +92 -0
  22. package/dist/vendor/reui.io/cascader/cascader-item.mjs +533 -0
  23. package/dist/vendor/reui.io/cascader/cascader-lib.d.mts +3 -0
  24. package/dist/vendor/reui.io/cascader/cascader-lib.mjs +688 -0
  25. package/dist/vendor/reui.io/cascader/cascader-nav.d.mts +56 -0
  26. package/dist/vendor/reui.io/cascader/cascader-nav.mjs +310 -0
  27. package/dist/vendor/reui.io/cascader/cascader-types.d.mts +140 -0
  28. package/dist/vendor/reui.io/cascader/cascader-virtual.d.mts +18 -0
  29. package/dist/vendor/reui.io/cascader/cascader-virtual.mjs +159 -0
  30. package/dist/vendor/reui.io/cascader/cascader.d.mts +299 -0
  31. package/dist/vendor/reui.io/cascader/cascader.mjs +1470 -0
  32. package/dist/vendor/reui.io/timeline.d.mts +21 -0
  33. package/dist/vendor/reui.io/timeline.mjs +127 -0
  34. package/dist/vendor/tailored/lightbox.d.mts +1 -1
  35. package/dist/vendor/tailored/lightbox.mjs +1 -1
  36. package/dist/widgets/datetime.d.mts +1 -1
  37. package/dist/widgets/datetime.mjs +6 -1
  38. package/dist/widgets/export.d.mts +28 -0
  39. package/dist/widgets/index.d.mts +69 -20
  40. package/dist/widgets/index.mjs +61 -11
  41. package/package.json +2 -2
  42. package/dist/vendor/shadcn/calendar.d.mts +0 -10
  43. package/dist/vendor/shadcn/calendar.mjs +0 -107
  44. package/dist/vendor/shadcn/card.d.mts +0 -11
  45. package/dist/vendor/shadcn/card.mjs +0 -55
  46. package/dist/vendor/shadcn/table.d.mts +0 -9
  47. package/dist/vendor/shadcn/table.mjs +0 -59
  48. package/dist/vendor/tailored/file-tree.d.mts +0 -21
  49. package/dist/vendor/tailored/file-tree.mjs +0 -146
@@ -0,0 +1,1470 @@
1
+ "use client";
2
+ import { useCascaderLoadState, useCascaderLoader } from "./cascader-async.mjs";
3
+ import { CascaderActionsContext, CascaderHighlightContext, CascaderRenderContext, CascaderStateContext, createCascaderHighlightStore, useCascaderActions, useCascaderState } from "./cascader-context.mjs";
4
+ import { resolveCascaderLabels } from "./cascader-i18n.mjs";
5
+ import { CASCADER_LIST_HEIGHT_CLASS, CASCADER_LIST_PAD_CLASS, CASCADER_ROOT_KEY, CASCADER_ROWS_CLASS, CASCADER_SCROLL_CLASS, applyCascadeSelection, buildCascaderIndex, collapseCascaderPath, createCascaderMoreNode, filterCascaderLevel, findAmbiguousCascaderLabels, findCascaderDataIssues, flattenCascaderTree, focusCascaderHome, getCascaderCheckedValues, getCascaderChildren, getCascaderCount, getCascaderIndeterminateFrom, getCascaderListedValues, getCascaderPath, getCascaderSelectedDescendants, getCascaderTabTarget, isCascaderBranch, isCascaderDisabled, isCascaderMoreNode, isCascaderSelectable, matchesCascaderQuery, mergeCascaderIndex, normalizeCascaderQuery, searchCascaderDeep, warnCascaderOnce, withCascaderMoreRow } from "./cascader-lib.mjs";
6
+ import { ScrollArea } from "../../coss/scroll-area.mjs";
7
+ import { cn } from "cn";
8
+ import { mergeProps } from "@base-ui/react/merge-props";
9
+ import * as React$1 from "react";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ import { useRender } from "@base-ui/react/use-render";
12
+ import { ChevronDownIcon, XIcon } from "lucide-react";
13
+ import { Combobox } from "@base-ui/react";
14
+ // src/vendor/reui.io/cascader/cascader.tsx
15
+ /** Stable `filteredItems` for a level swap; a fixed identity cannot loop. */
16
+ const EMPTY = [];
17
+ /** Stable empty array for the `actions` prop, so a cascader with no footer
18
+ * does not republish the actions context on every render. */
19
+ const EMPTY_ACTIONS = [];
20
+ /** The `onSearch` scope of a global search: no path, so the whole tree. */
21
+ const GLOBAL_SEARCH_PATH = [];
22
+ const ANNOUNCE_MARKER = "⁠";
23
+ const ANNOUNCE_DEBOUNCE = 150;
24
+ function useControllable(controlled, defaultValue, onChange, devName) {
25
+ const [uncontrolled, setUncontrolled] = React$1.useState(defaultValue);
26
+ const isControlled = controlled !== void 0;
27
+ const value = isControlled ? controlled : uncontrolled;
28
+ const wasControlled = React$1.useRef(isControlled);
29
+ React$1.useEffect(() => {
30
+ if (process.env.NODE_ENV === "production") return;
31
+ if (wasControlled.current === isControlled || !devName) return;
32
+ const from = wasControlled.current ? "controlled" : "uncontrolled";
33
+ const to = isControlled ? "controlled" : "uncontrolled";
34
+ wasControlled.current = isControlled;
35
+ warnCascaderOnce(`controlled-switch:${devName}`, `\`${devName}\` switched from ${from} to ${to}. Decide once: pass \`${devName}\` for the whole life of the component, or pass only the default and let the cascader own it.`);
36
+ }, [isControlled, devName]);
37
+ const latest = React$1.useRef({
38
+ value,
39
+ isControlled,
40
+ onChange
41
+ });
42
+ React$1.useEffect(() => {
43
+ latest.current = {
44
+ value,
45
+ isControlled,
46
+ onChange
47
+ };
48
+ });
49
+ const lastProp = React$1.useRef(controlled);
50
+ React$1.useEffect(() => {
51
+ lastProp.current = controlled;
52
+ });
53
+ return [value, React$1.useCallback((next) => {
54
+ const current = latest.current.value;
55
+ const resolved = typeof next === "function" ? next(current) : next;
56
+ if (Object.is(resolved, current)) {
57
+ if (!latest.current.isControlled || Object.is(resolved, lastProp.current)) return;
58
+ }
59
+ latest.current = {
60
+ ...latest.current,
61
+ value: resolved
62
+ };
63
+ if (!latest.current.isControlled) setUncontrolled(resolved);
64
+ latest.current.onChange?.(resolved);
65
+ }, [])];
66
+ }
67
+ function shallowEqualRecords(a, b) {
68
+ const left = a;
69
+ const right = b;
70
+ const keys = Object.keys(left);
71
+ if (keys.length !== Object.keys(right).length) return false;
72
+ for (const key of keys) if (!Object.is(left[key], right[key])) return false;
73
+ return true;
74
+ }
75
+ function shallowEqualItemLists(a, b) {
76
+ if (a.length !== b.length) return false;
77
+ for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i] && !shallowEqualRecords(a[i], b[i])) return false;
78
+ return true;
79
+ }
80
+ function useShallowStable(value, equal) {
81
+ const [stable, setStable] = React$1.useState(value);
82
+ if (Object.is(value, stable)) return stable;
83
+ if (typeof value === "object" && value !== null && typeof stable === "object" && stable !== null && equal(value, stable)) return stable;
84
+ setStable(value);
85
+ return value;
86
+ }
87
+ /** Normalizes to an array. `""` is "no selection", not a node: it is the
88
+ * uncontrolled default, and resolving it would render a blank chip. */
89
+ function toArray(value) {
90
+ if (value == null) return [];
91
+ if (Array.isArray(value)) return value.filter((entry) => entry !== "");
92
+ return value === "" ? [] : [value];
93
+ }
94
+ /** Misconfigurations that are SILENT by design. Once each, never in prod. */
95
+ function useCascaderDevWarnings(options) {
96
+ const { items, getParent, mode, multiple, cascade, indicator, selectable, searchScope, max, value, defaultValue, hasExpanded, hasPath, hasOnSearch } = options;
97
+ const issues = React$1.useMemo(() => process.env.NODE_ENV === "production" ? null : findCascaderDataIssues(items, getParent), [items, getParent]);
98
+ React$1.useEffect(() => {
99
+ if (process.env.NODE_ENV === "production") return;
100
+ for (const duplicate of issues?.duplicates ?? []) warnCascaderOnce(`duplicate-value:${duplicate}`, `Duplicate node value ${JSON.stringify(duplicate)}. \`value\` is the selection key, so only the first occurrence is indexed and the rest never render.`);
101
+ for (const cycle of issues?.cycles ?? []) warnCascaderOnce(`cycle:${cycle}`, `\`getParent\` puts ${JSON.stringify(cycle)} on a cycle. The depth walk is cycle-guarded so nothing hangs, but every depth on that chain is clamped rather than derived.`);
102
+ }, [issues]);
103
+ React$1.useEffect(() => {
104
+ if (process.env.NODE_ENV === "production") return;
105
+ for (const [prop, current] of [["value", value], ["defaultValue", defaultValue]]) {
106
+ if (current == null) continue;
107
+ if (multiple && !Array.isArray(current)) warnCascaderOnce(`value-shape:multiple:${prop}`, `\`multiple\` is set but \`${prop}\` is a string. Multi-select reads and writes a \`string[]\`; a string is treated as no selection at all.`);
108
+ else if (!multiple && Array.isArray(current)) warnCascaderOnce(`value-shape:single:${prop}`, `\`${prop}\` is an array but \`multiple\` is not set. Single-select reads and writes a \`string\`; only the first entry would survive a commit.`);
109
+ }
110
+ if (cascade && !multiple) warnCascaderOnce("cascade-without-multiple", "`cascade` does nothing without `multiple`: a single selection has no subtree to propagate over.");
111
+ if (cascade && selectable === "leaf") warnCascaderOnce("cascade-with-leaf-selectable", "`cascade` does nothing while `selectable=\"leaf\"`: no branch can be committed, so no commit ever has a subtree under it. Pass `selectable=\"any\"` or a predicate that accepts branches.");
112
+ if (!indicator && multiple) warnCascaderOnce("indicator-false-with-multiple", "`indicator={false}` does nothing with `multiple`: the checkbox is the selection control rather than a decoration, so it and its gutter stay. Mark selection your own way from `data-selected` on the row, and drop the prop.");
113
+ if (max != null && !multiple) warnCascaderOnce("max-without-multiple", "`max` does nothing without `multiple`: there is only ever one selection to cap.");
114
+ if (hasExpanded && mode !== "tree") warnCascaderOnce(`expanded-outside-tree:${mode}`, `\`expanded\` only does something in \`mode="tree"\`, and this cascader is in \`mode="${mode}"\`. Drill and columns navigate with \`path\`.`);
115
+ if (hasPath && mode === "tree") warnCascaderOnce("path-in-tree", "`path` does nothing in `mode=\"tree\"`: branches expand in place rather than replacing the level. Use `expanded`.");
116
+ if (searchScope === "deep" && mode === "tree") warnCascaderOnce("deep-search-in-tree", "`searchScope=\"deep\"` does nothing in `mode=\"tree\"`: a tree query already matches at any depth and auto-expands the ancestors of every hit.");
117
+ if (searchScope === "global" && mode === "tree") warnCascaderOnce("global-search-in-tree", "`searchScope=\"global\"` does nothing in `mode=\"tree\"`: a tree query already searches the whole tree and auto-expands the ancestors of every hit.");
118
+ if (hasOnSearch && mode === "tree") warnCascaderOnce("onsearch-in-tree", "`onSearch` does nothing in `mode=\"tree\"`: a tree query filters the loaded tree in place, and a server hit belongs to no visible branch, so its results would never render. The request is not fired. Filter the tree locally, or use drill or columns for server search.");
119
+ }, [
120
+ mode,
121
+ multiple,
122
+ cascade,
123
+ indicator,
124
+ selectable,
125
+ searchScope,
126
+ max,
127
+ value,
128
+ defaultValue,
129
+ hasExpanded,
130
+ hasPath,
131
+ hasOnSearch
132
+ ]);
133
+ }
134
+ /** No default on `T`: inferred from `items`, so `node.data` arrives typed. */
135
+ function Cascader({ items, getParent, getChildren, onSearch, searchDebounce, resolveValue, loadKey, prefetch, onLoadError, value: valueProp, defaultValue, onValueChange: onValueChangeProp, multiple = false, selectable = "leaf", indicator = true, actions, cascade = false, max, mode = "drill", expandTrigger, path: pathProp, defaultPath, onPathChange, expanded: expandedProp, defaultExpanded, onExpandedChange, open: openProp, defaultOpen = false, onOpenChange, closeOnSelect = true, inputValue: inputValueProp, defaultInputValue = "", onInputValueChange, searchScope = "level", filter, revealSelected = true, maxHeight, virtualize, virtualizeThreshold = 100, estimateRowSize = 32, overscan = 8, labels: labelsProp, renderItem, renderLabel, disabled, name, form, id, required, readOnly, invalid, inputRef, inline, children }) {
136
+ const baseIndex = React$1.useMemo(() => buildCascaderIndex(items, getParent), [items]);
137
+ const baseId = React$1.useId();
138
+ const stableLabelsProp = useShallowStable(labelsProp, shallowEqualRecords);
139
+ const labels = React$1.useMemo(() => resolveCascaderLabels(stableLabelsProp), [stableLabelsProp]);
140
+ useCascaderDevWarnings({
141
+ items,
142
+ getParent,
143
+ mode,
144
+ multiple,
145
+ cascade,
146
+ indicator,
147
+ selectable,
148
+ searchScope,
149
+ max,
150
+ value: valueProp,
151
+ defaultValue,
152
+ hasExpanded: expandedProp !== void 0 || defaultExpanded !== void 0 || onExpandedChange !== void 0,
153
+ hasPath: pathProp !== void 0 || defaultPath !== void 0 || onPathChange !== void 0,
154
+ hasOnSearch: onSearch !== void 0
155
+ });
156
+ const onValueChange = onValueChangeProp;
157
+ const [value, setValue] = useControllable(valueProp, defaultValue ?? (multiple ? [] : ""), void 0, "value");
158
+ const pathReasonRef = React$1.useRef("external");
159
+ const openReasonRef = React$1.useRef("none");
160
+ const [path, setPath] = useControllable(pathProp, defaultPath ?? [], (next) => onPathChange?.(next, { reason: pathReasonRef.current }), "path");
161
+ const [expandedList, setExpandedList] = useControllable(expandedProp, defaultExpanded ?? [], onExpandedChange, "expanded");
162
+ const [open, setOpen] = useControllable(openProp, defaultOpen, (next) => onOpenChange?.(next, { reason: openReasonRef.current }), "open");
163
+ /** The internal `setPath`: names its reason, then hands back to "external". */
164
+ const setPathWithReason = React$1.useCallback((next, reason) => {
165
+ pathReasonRef.current = reason;
166
+ try {
167
+ setPath(next);
168
+ } finally {
169
+ pathReasonRef.current = "external";
170
+ }
171
+ }, [setPath]);
172
+ const [query, setQuery] = useControllable(inputValueProp, defaultInputValue, onInputValueChange, "inputValue");
173
+ const expanded = React$1.useMemo(() => new Set(expandedList), [expandedList]);
174
+ const selectedValues = React$1.useMemo(() => toArray(value), [value]);
175
+ const levels = React$1.useMemo(() => {
176
+ if (mode === "tree") return [CASCADER_ROOT_KEY, ...expandedList];
177
+ if (mode === "columns") return [CASCADER_ROOT_KEY, ...path];
178
+ return [path.length ? path[path.length - 1] : CASCADER_ROOT_KEY];
179
+ }, [
180
+ mode,
181
+ path,
182
+ expandedList
183
+ ]);
184
+ const loader = useCascaderLoader({
185
+ base: baseIndex,
186
+ getChildren,
187
+ onSearch: mode === "tree" ? void 0 : onSearch,
188
+ resolveValue,
189
+ searchDebounce,
190
+ loadKey,
191
+ prefetch,
192
+ onLoadError,
193
+ enabled: open || !!inline,
194
+ query,
195
+ levels,
196
+ path: searchScope === "global" ? GLOBAL_SEARCH_PATH : path,
197
+ values: selectedValues
198
+ });
199
+ const loadStates = loader.states;
200
+ const { ensureLevel, retryLevel: retryLoaderLevel, invalidateLevel } = loader;
201
+ /**
202
+ * Whether pressing this branch has to FETCH first: in flight, failed, or
203
+ * never asked. `baseIndex`, as `ensureLevel` asks it: the two must agree or a
204
+ * press parks a navigation nothing will ever settle.
205
+ */
206
+ const needsChildren = React$1.useCallback((node) => {
207
+ if (!loader.active) return false;
208
+ const state = loadStates.get(node.value);
209
+ if (state) return state.loading || state.error;
210
+ return !baseIndex.childrenOf.has(node.value);
211
+ }, [
212
+ loader.active,
213
+ loadStates,
214
+ baseIndex
215
+ ]);
216
+ const pagingKey = JSON.stringify(Array.from(loadStates).filter(([, state]) => state.hasMore).map(([key]) => key));
217
+ const paging = React$1.useMemo(() => new Set(JSON.parse(pagingKey)), [pagingKey]);
218
+ const { pages, detached, links } = loader.store;
219
+ const index = React$1.useMemo(() => mergeCascaderIndex(baseIndex, pages, detached, links, paging), [
220
+ baseIndex,
221
+ pages,
222
+ detached,
223
+ links,
224
+ paging
225
+ ]);
226
+ /** Whether a level needs the paging pseudo-row. A never-fetched level gets
227
+ * nothing; that is the empty state's job. Tree has no per-level empty state,
228
+ * so there the row is also the branch's loading and error surface. */
229
+ const needsMoreRow = React$1.useCallback((key, childCount) => {
230
+ const state = loadStates.get(key);
231
+ if (!state) return false;
232
+ if (childCount > 0) return state.hasMore || state.loading || state.error;
233
+ return mode === "tree" && (state.loading || state.error);
234
+ }, [loadStates, mode]);
235
+ const labelCacheRef = React$1.useRef(/* @__PURE__ */ new Map());
236
+ for (const selected of selectedValues) {
237
+ const node = index.byValue.get(selected);
238
+ if (node) labelCacheRef.current.set(selected, node);
239
+ }
240
+ const resolveNode = React$1.useCallback((nodeValue) => index.byValue.get(nodeValue) ?? labelCacheRef.current.get(nodeValue) ?? {
241
+ value: nodeValue,
242
+ label: nodeValue
243
+ }, [index]);
244
+ const currentParentValue = path.length ? path[path.length - 1] : null;
245
+ const currentParent = currentParentValue ? index.byValue.get(currentParentValue) ?? null : null;
246
+ const currentLevelKey = currentParentValue ?? "\0root";
247
+ const isDeepSearching = (searchScope === "deep" || searchScope === "global") && query.trim().length > 0 && mode !== "tree";
248
+ const deepSearchWithin = searchScope === "global" ? null : currentParentValue;
249
+ const localDeepResults = React$1.useMemo(() => {
250
+ if (!isDeepSearching) return null;
251
+ return searchCascaderDeep(index, query, {
252
+ within: deepSearchWithin,
253
+ matches: filter
254
+ });
255
+ }, [
256
+ isDeepSearching,
257
+ index,
258
+ query,
259
+ deepSearchWithin,
260
+ filter
261
+ ]);
262
+ const deepResults = loader.searchResults ?? localDeepResults;
263
+ const levelItems = React$1.useMemo(() => {
264
+ const level = getCascaderChildren(index, currentParentValue);
265
+ return filterCascaderLevel(level, query, filter);
266
+ }, [
267
+ index,
268
+ currentParentValue,
269
+ query,
270
+ filter
271
+ ]);
272
+ /** The active level's rows plus the paging pseudo-row, appended HERE because
273
+ * every index Base UI hands out indexes this array: a DOM row missing from
274
+ * it shifts the highlight and every windowed row by one. */
275
+ const levelRendered = React$1.useMemo(() => {
276
+ if (deepResults) return deepResults;
277
+ if (!needsMoreRow(currentLevelKey, levelItems.length)) return levelItems;
278
+ return withCascaderMoreRow(levelItems, createCascaderMoreNode(currentLevelKey, levelItems.length), index.pinned);
279
+ }, [
280
+ deepResults,
281
+ levelItems,
282
+ currentLevelKey,
283
+ needsMoreRow,
284
+ index.pinned
285
+ ]);
286
+ const columns = React$1.useMemo(() => {
287
+ if (mode !== "columns") return [];
288
+ const trail = [];
289
+ for (let depth = 0; depth <= path.length; depth += 1) {
290
+ const parentValue = depth === 0 ? null : path[depth - 1];
291
+ const parent = parentValue ? index.byValue.get(parentValue) ?? null : null;
292
+ const isActive = depth === path.length;
293
+ let columnItems;
294
+ if (isActive) columnItems = levelRendered;
295
+ else {
296
+ const key = parentValue ?? "\0root";
297
+ const children = getCascaderChildren(index, parentValue);
298
+ columnItems = needsMoreRow(key, children.length) ? withCascaderMoreRow(children, createCascaderMoreNode(key, children.length), index.pinned) : children;
299
+ }
300
+ trail.push({
301
+ parent,
302
+ depth,
303
+ activeValue: path[depth] ?? null,
304
+ active: isActive,
305
+ items: columnItems
306
+ });
307
+ }
308
+ return trail;
309
+ }, [
310
+ mode,
311
+ path,
312
+ index,
313
+ levelRendered,
314
+ needsMoreRow
315
+ ]);
316
+ const treeSentinels = React$1.useMemo(() => {
317
+ if (mode !== "tree") return void 0;
318
+ const keys = /* @__PURE__ */ new Set();
319
+ const consider = (key) => {
320
+ if (needsMoreRow(key, index.childrenOf.get(key)?.length ?? 0)) keys.add(key);
321
+ };
322
+ consider(CASCADER_ROOT_KEY);
323
+ for (const value of expanded) consider(value);
324
+ return keys.size ? keys : void 0;
325
+ }, [
326
+ mode,
327
+ index,
328
+ expanded,
329
+ needsMoreRow
330
+ ]);
331
+ const treeRows = React$1.useMemo(() => {
332
+ if (mode !== "tree") return [];
333
+ const rows = flattenCascaderTree(index, expanded, treeSentinels);
334
+ const normalized = normalizeCascaderQuery(query);
335
+ if (!normalized) return rows;
336
+ const keep = /* @__PURE__ */ new Set();
337
+ const listed = getCascaderListedValues(index);
338
+ for (const node of index.all) {
339
+ if (!listed.has(node.value)) continue;
340
+ if (!(filter ? filter(node, normalized) : matchesCascaderQuery(node, normalized))) continue;
341
+ for (const ancestor of getCascaderPath(index, node.value)) keep.add(ancestor.value);
342
+ }
343
+ return flattenCascaderTree(index, /* @__PURE__ */ new Set([...expanded, ...keep])).filter((row) => keep.has(row.node.value));
344
+ }, [
345
+ mode,
346
+ index,
347
+ expanded,
348
+ treeSentinels,
349
+ query,
350
+ filter
351
+ ]);
352
+ const renderedItems = React$1.useMemo(() => mode === "tree" ? treeRows.map((row) => row.node) : levelRendered, [
353
+ mode,
354
+ treeRows,
355
+ levelRendered
356
+ ]);
357
+ /** Base UI never resets the highlighted index on an `items` change and has
358
+ * no imperative setter; its one automatic reset is an out-of-range clamp, so
359
+ * a single empty frame trips it. The frame rides `filteredItems`, so the
360
+ * `items` identity stays put. */
361
+ const levelKey = currentParentValue ?? "";
362
+ const [swap, setSwap] = React$1.useState({
363
+ key: levelKey,
364
+ active: false
365
+ });
366
+ if (swap.key !== levelKey) setSwap({
367
+ key: levelKey,
368
+ active: true
369
+ });
370
+ const swapping = swap.active && swap.key === levelKey;
371
+ React$1.useLayoutEffect(() => {
372
+ if (swap.active) setSwap((prev) => ({
373
+ ...prev,
374
+ active: false
375
+ }));
376
+ }, [swap.active]);
377
+ const resetHighlight = React$1.useCallback(() => {
378
+ setSwap((prev) => prev.active ? prev : {
379
+ ...prev,
380
+ active: true
381
+ });
382
+ }, []);
383
+ const [virtualRenderers, setVirtualRenderers] = React$1.useState(0);
384
+ const registerVirtualRenderer = React$1.useCallback(() => {
385
+ setVirtualRenderers((count) => count + 1);
386
+ return () => setVirtualRenderers((count) => Math.max(0, count - 1));
387
+ }, []);
388
+ const hasVirtualRenderer = virtualRenderers > 0;
389
+ const wantsVirtual = virtualize ?? renderedItems.length >= virtualizeThreshold;
390
+ const [virtualLatch, setVirtualLatch] = React$1.useState({
391
+ key: levelKey,
392
+ on: false
393
+ });
394
+ const virtualized = hasVirtualRenderer && (virtualLatch.key === levelKey ? virtualLatch.on || wantsVirtual : wantsVirtual);
395
+ if (!hasVirtualRenderer) {
396
+ if (virtualLatch.on || virtualLatch.key !== levelKey) setVirtualLatch({
397
+ key: levelKey,
398
+ on: false
399
+ });
400
+ } else if (virtualLatch.key !== levelKey) setVirtualLatch({
401
+ key: levelKey,
402
+ on: virtualized
403
+ });
404
+ else if (virtualized && !virtualLatch.on) setVirtualLatch({
405
+ key: levelKey,
406
+ on: true
407
+ });
408
+ /**
409
+ * One forced re-render, one commit AFTER windowing turns on. Base UI sizes
410
+ * `listRef` from `filteredItems` in a layout effect keyed on that array's
411
+ * identity, but `virtualized` reaches the list a commit later and the
412
+ * `CompositeList` teardown REPLACES `listRef` with an empty array nothing
413
+ * re-sizes, so ArrowDown stops at the last WINDOWED row. A passive effect is
414
+ * late enough: the teardown is scheduled from a layout effect.
415
+ */
416
+ const [virtualSyncs, setVirtualSyncs] = React$1.useState(0);
417
+ React$1.useEffect(() => {
418
+ if (!virtualized) return;
419
+ setVirtualSyncs((count) => count + 1);
420
+ }, [virtualized]);
421
+ React$1.useEffect(() => {
422
+ if (process.env.NODE_ENV === "production") return void 0;
423
+ if (virtualize !== true || hasVirtualRenderer) return void 0;
424
+ const timeout = setTimeout(() => {
425
+ console.error("[Cascader] `virtualize` is true but no windowing renderer is mounted. Render <CascaderVirtualItems /> inside <CascaderList>, or <CascaderVirtualColumn /> through the <CascaderColumns> slot.");
426
+ }, 0);
427
+ return () => clearTimeout(timeout);
428
+ }, [virtualize, hasVirtualRenderer]);
429
+ /** One signal per navigation, feeding the live region, bumped EXACTLY once
430
+ * per action. `seq` alternates the invisible marker so two identical
431
+ * announcements are still two mutations; `toggled` names the tree branch. */
432
+ const [announceSignal, setAnnounceSignal] = React$1.useState({
433
+ seq: 0,
434
+ toggled: null,
435
+ notice: null
436
+ });
437
+ const bumpAnnouncement = React$1.useCallback((toggled) => {
438
+ setAnnounceSignal((prev) => ({
439
+ seq: prev.seq + 1,
440
+ toggled,
441
+ notice: null
442
+ }));
443
+ }, []);
444
+ /** Speaks for interactions that change nothing on screen: a refusal returns
445
+ * before any state moves, a cascade sweep is invisible from the row pressed.
446
+ * The seq bump keeps two identical refusals two mutations. */
447
+ const announceNotice = React$1.useCallback((notice) => {
448
+ setAnnounceSignal((prev) => ({
449
+ seq: prev.seq + 1,
450
+ toggled: null,
451
+ notice
452
+ }));
453
+ }, []);
454
+ /**
455
+ * The navigation a press asked for and a fetch has not answered yet. Load
456
+ * BEFORE you move: drilling in first threw away the list the user was
457
+ * reading and, on failure, stranded them on an error screen for a level they
458
+ * had never seen. Holding here keeps the old rows and puts the progress on
459
+ * the pressed row, whose chevron `CascaderItem` swaps for a spinner.
460
+ */
461
+ const [pendingNavigation, setPendingNavigation] = React$1.useState(null);
462
+ /** Asks for a branch's children and parks the navigation until they land.
463
+ * BOTH entry points are called unconditionally: `retryLevel` no-ops unless
464
+ * the level errored, `ensureLevel` no-ops if it has state, so a repeat press
465
+ * is free and the readiness decision stays inside the loader. */
466
+ const requestChildren = React$1.useCallback((node, pending) => {
467
+ setPendingNavigation(pending);
468
+ retryLoaderLevel(node.value);
469
+ ensureLevel(node.value, "level");
470
+ bumpAnnouncement(null);
471
+ }, [
472
+ retryLoaderLevel,
473
+ ensureLevel,
474
+ bumpAnnouncement
475
+ ]);
476
+ const pushLevel = React$1.useCallback((nodeValue) => {
477
+ setPathWithReason((prev) => [...prev, nodeValue], "drill");
478
+ setQuery("");
479
+ bumpAnnouncement(null);
480
+ }, [
481
+ setPathWithReason,
482
+ setQuery,
483
+ bumpAnnouncement
484
+ ]);
485
+ const popLevel = React$1.useCallback(() => {
486
+ setPathWithReason((prev) => prev.length ? prev.slice(0, -1) : prev, "back");
487
+ setQuery("");
488
+ setPendingNavigation(null);
489
+ bumpAnnouncement(null);
490
+ }, [
491
+ setPathWithReason,
492
+ setQuery,
493
+ bumpAnnouncement
494
+ ]);
495
+ const goToDepth = React$1.useCallback((depth) => {
496
+ setPathWithReason((prev) => prev.slice(0, Math.max(0, depth)), "breadcrumb");
497
+ setQuery("");
498
+ setPendingNavigation(null);
499
+ bumpAnnouncement(null);
500
+ }, [
501
+ setPathWithReason,
502
+ setQuery,
503
+ bumpAnnouncement
504
+ ]);
505
+ const toggleExpanded = React$1.useCallback((nodeValue) => {
506
+ setExpandedList((prev) => prev.includes(nodeValue) ? prev.filter((v) => v !== nodeValue) : [...prev, nodeValue]);
507
+ bumpAnnouncement(nodeValue);
508
+ }, [setExpandedList, bumpAnnouncement]);
509
+ const isBranch = React$1.useCallback((node) => isCascaderBranch(index, node), [index]);
510
+ const isSelectable = React$1.useCallback((node) => isCascaderSelectable(index, node, selectable), [index, selectable]);
511
+ const isDisabled = React$1.useCallback((node) => isCascaderDisabled(index, node), [index]);
512
+ const branchesSelectable = selectable !== "leaf";
513
+ const openFlyoutsRef = React$1.useRef(/* @__PURE__ */ new Set());
514
+ const setFlyoutOpen = React$1.useCallback((key, open) => {
515
+ if (open) openFlyoutsRef.current.add(key);
516
+ else openFlyoutsRef.current.delete(key);
517
+ }, []);
518
+ const hasOpenFlyout = React$1.useCallback(() => openFlyoutsRef.current.size > 0, []);
519
+ const stableActions = useShallowStable(actions, shallowEqualItemLists);
520
+ const resolvedActions = React$1.useMemo(() => stableActions ?? EMPTY_ACTIONS, [stableActions]);
521
+ const selectedSet = React$1.useMemo(() => new Set(selectedValues), [selectedValues]);
522
+ const isSelected = React$1.useCallback((node) => selectedSet.has(node.value), [selectedSet]);
523
+ /** Selected-descendant counts, walked ONCE per selection change and never
524
+ * stored, so the flat value array stays the only source of truth. The walk
525
+ * is `selections x depth`, up from each selection rather than down from each
526
+ * row, which keeps the count and `isIndeterminate` O(1). */
527
+ const selectedDescendants = React$1.useMemo(() => getCascaderSelectedDescendants(index, selectedValues), [index, selectedValues]);
528
+ const selectedDescendantCount = React$1.useCallback((node) => selectedDescendants.get(node.value) ?? 0, [selectedDescendants]);
529
+ const indeterminateSet = React$1.useMemo(() => {
530
+ if (!cascade || !multiple) return null;
531
+ return getCascaderIndeterminateFrom(selectedDescendants, selectedValues);
532
+ }, [
533
+ cascade,
534
+ multiple,
535
+ selectedDescendants,
536
+ selectedValues
537
+ ]);
538
+ const isIndeterminate = React$1.useCallback((node) => indeterminateSet?.has(node.value) ?? false, [indeterminateSet]);
539
+ const highlightedRef = React$1.useRef(null);
540
+ const getHighlighted = React$1.useCallback(() => highlightedRef.current, []);
541
+ const [highlightStore] = React$1.useState(createCascaderHighlightStore);
542
+ const currentLoadState = React$1.useMemo(() => loadStates.get(currentLevelKey) ?? null, [loadStates, currentLevelKey]);
543
+ const searchLoadState = loader.searchState;
544
+ /** What the query announcement COUNTS: matches, not rows, since the list
545
+ * also carries a tree query's ancestor context and the Load-more row. Server
546
+ * hits count as-is; re-judging them locally would drop what it matched on
547
+ * data the client cannot see. */
548
+ const announcedMatches = React$1.useMemo(() => {
549
+ if (deepResults) return deepResults.length;
550
+ const normalized = normalizeCascaderQuery(query);
551
+ if (!normalized) return 0;
552
+ const matches = filter ?? matchesCascaderQuery;
553
+ return renderedItems.filter((node) => !isCascaderMoreNode(node) && matches(node, normalized)).length;
554
+ }, [
555
+ deepResults,
556
+ renderedItems,
557
+ query,
558
+ filter
559
+ ]);
560
+ const hasRows = renderedItems.some((node) => !isCascaderMoreNode(node));
561
+ const announcementTarget = React$1.useMemo(() => {
562
+ if (!open && !inline) return {
563
+ text: "",
564
+ defer: false
565
+ };
566
+ const total = renderedItems.length;
567
+ const marker = ANNOUNCE_MARKER.repeat(announceSignal.seq % 2);
568
+ if (announceSignal.notice) return {
569
+ text: `${announceSignal.notice}${marker}`,
570
+ defer: false
571
+ };
572
+ const toggled = mode === "tree" && announceSignal.toggled ? index.byValue.get(announceSignal.toggled) ?? null : null;
573
+ const pendingState = pendingNavigation ? loadStates.get(pendingNavigation.value) ?? null : null;
574
+ const levelState = query.trim() ? searchLoadState : currentLoadState;
575
+ let text;
576
+ let defer = false;
577
+ if (pendingState?.error) text = labels.error;
578
+ else if (pendingNavigation) text = labels.loading;
579
+ else if (levelState?.error) text = labels.error;
580
+ else if (levelState?.loading) text = query.trim() ? labels.searchingAnnouncement : hasRows ? labels.loadingMore : labels.loading;
581
+ else if (query.trim()) {
582
+ text = announcedMatches === 0 ? labels.empty : labels.resultsAnnouncement(announcedMatches);
583
+ defer = true;
584
+ } else if (toggled) text = expanded.has(toggled.value) ? labels.expandedAnnouncement(toggled.label, getCascaderCount(index, toggled)) : labels.collapsedAnnouncement(toggled.label);
585
+ else if (currentParent) text = labels.levelAnnouncement(currentParent.label, path.length + 1, total);
586
+ else text = labels.rootAnnouncement(total);
587
+ return {
588
+ text: `${text}${marker}`,
589
+ defer
590
+ };
591
+ }, [
592
+ open,
593
+ inline,
594
+ mode,
595
+ index,
596
+ expanded,
597
+ announceSignal,
598
+ query,
599
+ labels,
600
+ renderedItems.length,
601
+ hasRows,
602
+ announcedMatches,
603
+ currentParent,
604
+ path.length,
605
+ currentLoadState,
606
+ searchLoadState,
607
+ pendingNavigation,
608
+ loadStates
609
+ ]);
610
+ /** The COMMITTED announcement. Immediate texts commit during render (the
611
+ * pattern `swap` uses), so an async settle announces in the very commit that
612
+ * shows the rows. Only the deferred query count uses the timer below. */
613
+ const [announced, setAnnounced] = React$1.useState(announcementTarget);
614
+ if (!announcementTarget.defer && announced.text !== announcementTarget.text) setAnnounced(announcementTarget);
615
+ React$1.useEffect(() => {
616
+ if (!announcementTarget.defer) return void 0;
617
+ const timer = setTimeout(() => setAnnounced(announcementTarget), ANNOUNCE_DEBOUNCE);
618
+ return () => clearTimeout(timer);
619
+ }, [announcementTarget]);
620
+ const announcement = announced.text;
621
+ const stateValue = React$1.useMemo(() => ({
622
+ index,
623
+ path,
624
+ expanded,
625
+ query,
626
+ currentParent,
627
+ levelItems,
628
+ deepResults,
629
+ renderedItems,
630
+ columns,
631
+ treeRows,
632
+ selectedValues,
633
+ selectedDescendants,
634
+ loadStates,
635
+ searchState: searchLoadState,
636
+ announcement
637
+ }), [
638
+ index,
639
+ path,
640
+ expanded,
641
+ query,
642
+ currentParent,
643
+ levelItems,
644
+ deepResults,
645
+ renderedItems,
646
+ columns,
647
+ treeRows,
648
+ selectedValues,
649
+ selectedDescendants,
650
+ loadStates,
651
+ searchLoadState,
652
+ announcement
653
+ ]);
654
+ /**
655
+ * Latest committed props and derived state, WRITTEN IN AN EFFECT. That is
656
+ * what keeps the callbacks below `[]`-dep and stable for the life of the
657
+ * cascader; writing during render is unsafe under concurrent rendering. The
658
+ * cost: a read sees the PREVIOUS commit until the effect runs, so ONLY event
659
+ * handlers may read it. Anything used during render is memoised instead.
660
+ */
661
+ const latest = React$1.useRef({
662
+ index,
663
+ state: stateValue,
664
+ mode,
665
+ multiple,
666
+ cascade,
667
+ max,
668
+ labels,
669
+ closeOnSelect,
670
+ selectedValues,
671
+ deepResults,
672
+ onValueChange,
673
+ resolveNode,
674
+ isSelectable,
675
+ needsChildren,
676
+ expanded
677
+ });
678
+ React$1.useEffect(() => {
679
+ latest.current = {
680
+ index,
681
+ state: stateValue,
682
+ mode,
683
+ multiple,
684
+ cascade,
685
+ max,
686
+ labels,
687
+ closeOnSelect,
688
+ selectedValues,
689
+ deepResults,
690
+ onValueChange,
691
+ resolveNode,
692
+ isSelectable,
693
+ needsChildren,
694
+ expanded
695
+ };
696
+ });
697
+ const getIndex = React$1.useCallback(() => latest.current.index, []);
698
+ const getState = React$1.useCallback(() => latest.current.state, []);
699
+ const goToLevelAt = React$1.useCallback((nodeValue, depth) => {
700
+ setPathWithReason((prev) => [...prev.slice(0, Math.max(0, depth)), nodeValue], "drill");
701
+ setQuery("");
702
+ bumpAnnouncement(null);
703
+ }, [
704
+ setPathWithReason,
705
+ setQuery,
706
+ bumpAnnouncement
707
+ ]);
708
+ const goToTrail = React$1.useCallback((trail) => {
709
+ const { path: current } = latest.current.state;
710
+ if (trail.length === current.length && trail.every((entry, i) => entry === current[i])) resetHighlight();
711
+ else setPathWithReason(trail, "drill");
712
+ setQuery("");
713
+ bumpAnnouncement(null);
714
+ }, [
715
+ setPathWithReason,
716
+ resetHighlight,
717
+ setQuery,
718
+ bumpAnnouncement
719
+ ]);
720
+ const navigateAt = React$1.useCallback((node, depth) => {
721
+ if (isCascaderDisabled(latest.current.index, node)) return;
722
+ if (latest.current.mode === "tree") {
723
+ toggleExpanded(node.value);
724
+ return;
725
+ }
726
+ if (latest.current.needsChildren(node)) {
727
+ requestChildren(node, {
728
+ value: node.value,
729
+ kind: "at",
730
+ depth
731
+ });
732
+ return;
733
+ }
734
+ goToLevelAt(node.value, depth);
735
+ }, [
736
+ toggleExpanded,
737
+ goToLevelAt,
738
+ requestChildren
739
+ ]);
740
+ const navigate = React$1.useCallback((node) => {
741
+ const { mode: currentMode, deepResults: currentDeepResults, index: currentIndex, expanded: currentExpanded, needsChildren: pending } = latest.current;
742
+ if (isCascaderDisabled(currentIndex, node)) return;
743
+ if (currentMode === "tree") {
744
+ if (!currentExpanded.has(node.value) && pending(node)) {
745
+ requestChildren(node, {
746
+ value: node.value,
747
+ kind: "expand",
748
+ depth: 0
749
+ });
750
+ return;
751
+ }
752
+ toggleExpanded(node.value);
753
+ return;
754
+ }
755
+ if (currentDeepResults) {
756
+ const trail = getCascaderPath(currentIndex, node.value).map((entry) => entry.value);
757
+ if (pending(node)) {
758
+ requestChildren(node, {
759
+ value: node.value,
760
+ kind: "trail",
761
+ trail
762
+ });
763
+ return;
764
+ }
765
+ goToTrail(trail);
766
+ return;
767
+ }
768
+ if (pending(node)) {
769
+ requestChildren(node, {
770
+ value: node.value,
771
+ kind: "push",
772
+ depth: 0
773
+ });
774
+ return;
775
+ }
776
+ pushLevel(node.value);
777
+ }, [
778
+ toggleExpanded,
779
+ goToTrail,
780
+ pushLevel,
781
+ requestChildren
782
+ ]);
783
+ /**
784
+ * The other half of `requestChildren`: the fetch settled, so the held
785
+ * navigation happens or is dropped. Reads the load STATE, not a promise:
786
+ * only the loader knows a request was superseded, aborted or retried, and no
787
+ * entry at all means it has not reached the store yet. On failure nothing
788
+ * moves and the intent is KEPT, which is what makes the row's retry
789
+ * affordance mean something; only `popLevel`, `goToDepth` and a close drop it.
790
+ */
791
+ React$1.useEffect(() => {
792
+ if (!pendingNavigation) return;
793
+ const state = loadStates.get(pendingNavigation.value);
794
+ if (!state || state.loading || state.error) return;
795
+ setPendingNavigation(null);
796
+ const target = index.byValue.get(pendingNavigation.value);
797
+ if (target && isDisabled(target)) return;
798
+ if (pendingNavigation.kind === "expand") {
799
+ toggleExpanded(pendingNavigation.value);
800
+ return;
801
+ }
802
+ if (pendingNavigation.kind === "trail") {
803
+ goToTrail(pendingNavigation.trail);
804
+ return;
805
+ }
806
+ if (pendingNavigation.kind === "at") {
807
+ goToLevelAt(pendingNavigation.value, pendingNavigation.depth);
808
+ return;
809
+ }
810
+ pushLevel(pendingNavigation.value);
811
+ }, [
812
+ pendingNavigation,
813
+ loadStates,
814
+ index,
815
+ isDisabled,
816
+ toggleExpanded,
817
+ goToTrail,
818
+ goToLevelAt,
819
+ pushLevel
820
+ ]);
821
+ React$1.useEffect(() => {
822
+ if (!open && !inline) setPendingNavigation(null);
823
+ }, [open, inline]);
824
+ const emitSelection = React$1.useCallback((nextValues, node, reason) => {
825
+ const { selectedValues: current, multiple: currentMultiple, onValueChange: emit, index: currentIndex, resolveNode: resolve } = latest.current;
826
+ if (nextValues.length === current.length && nextValues.every((entry, i) => entry === current[i])) return;
827
+ const next = currentMultiple ? nextValues : nextValues[0] ?? "";
828
+ setValue(next);
829
+ if (!emit) return;
830
+ const chain = node ? getCascaderPath(currentIndex, node.value) : [];
831
+ emit(next, {
832
+ node,
833
+ path: node ? chain.length ? chain : [node] : [],
834
+ nodes: nextValues.map(resolve),
835
+ reason
836
+ });
837
+ }, [setValue]);
838
+ const setSelection = React$1.useCallback((values, reason) => {
839
+ const { selectedValues: current, resolveNode: resolve } = latest.current;
840
+ const before = new Set(current);
841
+ const after = new Set(values);
842
+ const added = values.find((entry) => !before.has(entry));
843
+ const removed = current.find((entry) => !after.has(entry));
844
+ if (reason === "clear") {
845
+ emitSelection(values, null, "clear");
846
+ return;
847
+ }
848
+ if (added) {
849
+ emitSelection(values, resolve(added), reason ?? "select");
850
+ return;
851
+ }
852
+ if (removed) {
853
+ emitSelection(values, resolve(removed), reason ?? "deselect");
854
+ return;
855
+ }
856
+ emitSelection(values, null, reason ?? "select");
857
+ }, [emitSelection]);
858
+ const commit = React$1.useCallback((node) => {
859
+ const { multiple: currentMultiple, cascade: currentCascade, selectedValues: current, max: currentMax, index: currentIndex, isSelectable: currentIsSelectable, labels: currentLabels, closeOnSelect: currentCloseOnSelect } = latest.current;
860
+ const selecting = !currentMultiple || !current.includes(node.value);
861
+ if (selecting && isCascaderDisabled(currentIndex, node)) return;
862
+ if (!currentMultiple) {
863
+ emitSelection([node.value], node, "select");
864
+ if (currentCloseOnSelect) {
865
+ openReasonRef.current = "item-press";
866
+ setOpen(false);
867
+ } else setQuery("");
868
+ return;
869
+ }
870
+ if (currentCascade) {
871
+ const next = applyCascadeSelection(currentIndex, current, node.value, selecting, currentIsSelectable);
872
+ if (selecting && currentMax != null && next.length > currentMax && next.length > current.length) {
873
+ announceNotice(currentLabels.maxReachedAnnouncement(currentMax));
874
+ return;
875
+ }
876
+ emitSelection(next, node, selecting ? "select" : "deselect");
877
+ const before = new Set(current);
878
+ const after = new Set(next);
879
+ let swept = 0;
880
+ for (const entry of selecting ? next : current) {
881
+ if (entry === node.value) continue;
882
+ if (selecting ? !before.has(entry) : !after.has(entry)) swept += 1;
883
+ }
884
+ if (swept > 0) announceNotice(currentLabels.cascadeAnnouncement(node.label ?? node.value, swept, selecting));
885
+ setQuery("");
886
+ return;
887
+ }
888
+ if (current.includes(node.value)) emitSelection(current.filter((v) => v !== node.value), node, "deselect");
889
+ else if (currentMax == null || current.length < currentMax) emitSelection([...current, node.value], node, "select");
890
+ else announceNotice(currentLabels.maxReachedAnnouncement(currentMax));
891
+ setQuery("");
892
+ }, [
893
+ emitSelection,
894
+ setOpen,
895
+ setQuery,
896
+ announceNotice
897
+ ]);
898
+ /** Safety net for paths that bypass the row-level veto (the drag-select
899
+ * mouseup). `details.cancel()` suppresses the commit and the close together:
900
+ * Base UI shares one event-details object between them. */
901
+ const handleComboboxValueChange = React$1.useCallback((next, details) => {
902
+ if (!multiple) {
903
+ const node = Array.isArray(next) ? next[0] ?? null : next;
904
+ if (node && isCascaderMoreNode(node)) {
905
+ details.cancel();
906
+ return;
907
+ }
908
+ if (node && isDisabled(node)) {
909
+ details.cancel();
910
+ return;
911
+ }
912
+ if (node && !isSelectable(node) && isBranch(node)) {
913
+ details.cancel();
914
+ navigate(node);
915
+ return;
916
+ }
917
+ if (node) {
918
+ if (!closeOnSelect) details.cancel();
919
+ commit(node);
920
+ }
921
+ return;
922
+ }
923
+ const nextNodes = Array.isArray(next) ? next : [];
924
+ const before = new Set(selectedValues);
925
+ const added = nextNodes.find((node) => !before.has(node.value));
926
+ if (added && (isCascaderMoreNode(added) || isDisabled(added))) {
927
+ details.cancel();
928
+ return;
929
+ }
930
+ if (added && !isSelectable(added) && isBranch(added)) {
931
+ details.cancel();
932
+ navigate(added);
933
+ return;
934
+ }
935
+ if (cascade) {
936
+ const nextValues = new Set(nextNodes.map((node) => node.value));
937
+ const removed = selectedValues.find((entry) => !nextValues.has(entry));
938
+ const toggled = added ?? (removed != null ? resolveNode(removed) : null);
939
+ if (toggled) commit(toggled);
940
+ return;
941
+ }
942
+ if (max != null && nextNodes.length > selectedValues.length && nextNodes.length > max) {
943
+ details.cancel();
944
+ announceNotice(labels.maxReachedAnnouncement(max));
945
+ return;
946
+ }
947
+ setSelection(nextNodes.map((node) => node.value));
948
+ setQuery("");
949
+ }, [
950
+ multiple,
951
+ selectedValues,
952
+ max,
953
+ cascade,
954
+ closeOnSelect,
955
+ labels,
956
+ announceNotice,
957
+ isSelectable,
958
+ isDisabled,
959
+ isBranch,
960
+ navigate,
961
+ commit,
962
+ resolveNode,
963
+ setSelection,
964
+ setQuery
965
+ ]);
966
+ const handleOpenChange = React$1.useCallback((nextOpen, details) => {
967
+ if (details.isCanceled) return;
968
+ if (!nextOpen && details.reason === "escape-key" && hasOpenFlyout()) {
969
+ details.cancel();
970
+ return;
971
+ }
972
+ openReasonRef.current = details.reason ?? "none";
973
+ setOpen(nextOpen);
974
+ if (!nextOpen) {
975
+ setQuery("");
976
+ return;
977
+ }
978
+ if (revealSelected) {
979
+ setQuery("");
980
+ const first = toArray(value)[0];
981
+ if (mode !== "tree") setPathWithReason(first ? getCascaderPath(index, first).slice(0, -1).map((node) => node.value) : [], "reveal");
982
+ }
983
+ }, [
984
+ setOpen,
985
+ setQuery,
986
+ revealSelected,
987
+ value,
988
+ index,
989
+ mode,
990
+ setPathWithReason,
991
+ hasOpenFlyout
992
+ ]);
993
+ const renderContext = React$1.useMemo(() => ({
994
+ renderItem,
995
+ renderLabel
996
+ }), [renderItem, renderLabel]);
997
+ /** The stable half: nothing in these deps moves on a keystroke, a level
998
+ * change or a highlight, which is what makes `React.memo` on `CascaderItem`
999
+ * hold while the user types. Destructured off `loader` so the memo depends
1000
+ * on its `[]`-dep callbacks, not the object it republishes per load. */
1001
+ const { active: hasLoader, loadMore, retryLevel, retrySearch, prefetchNode } = loader;
1002
+ const actionsValue = React$1.useMemo(() => ({
1003
+ index,
1004
+ mode,
1005
+ multiple,
1006
+ cascade,
1007
+ branchesSelectable,
1008
+ indicator,
1009
+ expandTrigger,
1010
+ actions: resolvedActions,
1011
+ searchScope,
1012
+ maxHeight,
1013
+ inline: !!inline,
1014
+ invalid: !!invalid,
1015
+ baseId,
1016
+ labels,
1017
+ virtualized,
1018
+ registerVirtualRenderer,
1019
+ virtualize,
1020
+ virtualizeThreshold,
1021
+ estimateRowSize,
1022
+ overscan,
1023
+ hasLoader,
1024
+ loadMore,
1025
+ retryLevel,
1026
+ retrySearch,
1027
+ invalidateLevel,
1028
+ getIndex,
1029
+ getState,
1030
+ getHighlighted,
1031
+ setPath,
1032
+ pushLevel,
1033
+ popLevel,
1034
+ goToDepth,
1035
+ toggleExpanded,
1036
+ setFlyoutOpen,
1037
+ hasOpenFlyout,
1038
+ setQuery,
1039
+ setSelection,
1040
+ commit,
1041
+ navigate,
1042
+ navigateAt,
1043
+ resolveNode,
1044
+ isBranch,
1045
+ isSelectable,
1046
+ isDisabled,
1047
+ isSelected,
1048
+ isIndeterminate,
1049
+ selectedDescendantCount
1050
+ }), [
1051
+ index,
1052
+ mode,
1053
+ multiple,
1054
+ cascade,
1055
+ branchesSelectable,
1056
+ indicator,
1057
+ expandTrigger,
1058
+ resolvedActions,
1059
+ searchScope,
1060
+ maxHeight,
1061
+ inline,
1062
+ invalid,
1063
+ baseId,
1064
+ labels,
1065
+ virtualized,
1066
+ registerVirtualRenderer,
1067
+ virtualize,
1068
+ virtualizeThreshold,
1069
+ estimateRowSize,
1070
+ overscan,
1071
+ hasLoader,
1072
+ loadMore,
1073
+ retryLevel,
1074
+ retrySearch,
1075
+ invalidateLevel,
1076
+ getIndex,
1077
+ getState,
1078
+ getHighlighted,
1079
+ setPath,
1080
+ pushLevel,
1081
+ popLevel,
1082
+ goToDepth,
1083
+ toggleExpanded,
1084
+ setFlyoutOpen,
1085
+ hasOpenFlyout,
1086
+ setQuery,
1087
+ setSelection,
1088
+ commit,
1089
+ navigate,
1090
+ navigateAt,
1091
+ resolveNode,
1092
+ isBranch,
1093
+ isSelectable,
1094
+ isDisabled,
1095
+ isSelected,
1096
+ isIndeterminate,
1097
+ selectedDescendantCount
1098
+ ]);
1099
+ const comboboxItems = React$1.useMemo(() => virtualized ? renderedItems.slice() : renderedItems, [
1100
+ renderedItems,
1101
+ virtualized,
1102
+ virtualSyncs
1103
+ ]);
1104
+ const comboboxValue = React$1.useMemo(() => {
1105
+ if (multiple) return selectedValues.map(resolveNode);
1106
+ return selectedValues.length ? resolveNode(selectedValues[0]) : null;
1107
+ }, [
1108
+ multiple,
1109
+ selectedValues,
1110
+ resolveNode
1111
+ ]);
1112
+ return /* @__PURE__ */ jsx(CascaderActionsContext.Provider, {
1113
+ value: actionsValue,
1114
+ children: /* @__PURE__ */ jsx(CascaderStateContext.Provider, {
1115
+ value: stateValue,
1116
+ children: /* @__PURE__ */ jsx(CascaderHighlightContext.Provider, {
1117
+ value: highlightStore,
1118
+ children: /* @__PURE__ */ jsx(CascaderRenderContext.Provider, {
1119
+ value: renderContext,
1120
+ children: /* @__PURE__ */ jsx(Combobox.Root, {
1121
+ items: renderedItems,
1122
+ filteredItems: swapping ? EMPTY : comboboxItems,
1123
+ filter: null,
1124
+ value: comboboxValue,
1125
+ onValueChange: handleComboboxValueChange,
1126
+ open,
1127
+ onOpenChange: handleOpenChange,
1128
+ inputValue: query,
1129
+ onInputValueChange: (next) => setQuery(next),
1130
+ multiple,
1131
+ disabled,
1132
+ name,
1133
+ ...form != null ? { form } : null,
1134
+ ...id != null ? { id } : null,
1135
+ ...required != null ? { required } : null,
1136
+ ...readOnly != null ? { readOnly } : null,
1137
+ ...inputRef ? { inputRef } : null,
1138
+ inline,
1139
+ virtualized,
1140
+ itemToStringValue: (item) => item?.value ?? "",
1141
+ itemToStringLabel: (item) => item?.label ?? "",
1142
+ isItemEqualToValue: (a, b) => a?.value === b?.value,
1143
+ onItemHighlighted: (item, details) => {
1144
+ highlightedRef.current = item ?? null;
1145
+ highlightStore.set({
1146
+ index: details.index,
1147
+ value: item?.value ?? null
1148
+ });
1149
+ prefetchNode(item);
1150
+ },
1151
+ children
1152
+ })
1153
+ })
1154
+ })
1155
+ })
1156
+ });
1157
+ }
1158
+ function CascaderTrigger({ className, children, showIcon = true, ref, ...props }) {
1159
+ const { invalid } = useCascaderActions();
1160
+ const triggerRef = React$1.useRef(null);
1161
+ const setTrigger = React$1.useCallback((node) => {
1162
+ triggerRef.current = node;
1163
+ if (typeof ref === "function") ref(node);
1164
+ else if (ref) ref.current = node;
1165
+ }, [ref]);
1166
+ /** 18 of 19 audited examples shipped an unnamed combobox: the trigger's
1167
+ * contents are the field's VALUE, so a screen reader never hears its NAME.
1168
+ * Checked in the DOM, since the name may arrive as a `<label for>`. */
1169
+ React$1.useEffect(() => {
1170
+ if (process.env.NODE_ENV === "production") return;
1171
+ const element = triggerRef.current;
1172
+ if (!element) return;
1173
+ if (element.hasAttribute("aria-label") || element.hasAttribute("aria-labelledby") || element.closest("label") !== null || element.id !== "" && element.ownerDocument.querySelector(`label[for="${CSS.escape(element.id)}"]`) !== null) return;
1174
+ warnCascaderOnce("trigger-unnamed", "`CascaderTrigger` has no accessible name. Its contents are the field's VALUE - they change with the selection - so a screen reader hears what is picked but never what the field is for. Pass `aria-label` or `aria-labelledby`, or reference the trigger's `id` from a `<label>`.");
1175
+ });
1176
+ return /* @__PURE__ */ jsxs(Combobox.Trigger, {
1177
+ ref: setTrigger,
1178
+ "data-slot": "cascader-trigger",
1179
+ ...invalid ? {
1180
+ "aria-invalid": true,
1181
+ "data-invalid": ""
1182
+ } : null,
1183
+ className: cn(TRIGGER_ICON_FALLBACK_CLASS, className),
1184
+ ...props,
1185
+ children: [children, showIcon ? /* @__PURE__ */ jsx(ChevronDownIcon, { className: `${TRIGGER_ICON_CLASS} pointer-events-none shrink-0` }) : null]
1186
+ });
1187
+ }
1188
+ /** Icon size for a combobox surface: 14px in mira and sera, 16px elsewhere. */
1189
+ const TRIGGER_ICON_CLASS = "text-muted-foreground size-4";
1190
+ /** The same ladder, applied only to trigger children that carry no size. */
1191
+ const TRIGGER_ICON_FALLBACK_CLASS = "[&_svg:not([class*='size-'])]:size-4";
1192
+ /** The chips field: a form-control surface, so it carries the invalid states. */
1193
+ const CHIPS_CLASS = "flex flex-wrap items-center border bg-clip-padding dark:bg-input/30 border-input focus-within:border-ring focus-within:ring-ring/50 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive dark:has-aria-invalid:border-destructive/50 min-h-9 gap-1.5 rounded-md bg-transparent px-2.5 py-1.5 text-sm shadow-xs transition-[color,box-shadow] focus-within:ring-3 has-aria-invalid:ring-3 has-data-[slot=combobox-chip]:px-1.5";
1194
+ const CHIP_CLASS = "text-foreground flex w-fit items-center justify-center gap-1 font-medium whitespace-nowrap bg-muted h-[calc(--spacing(5.5))] rounded-sm px-1.5 text-xs has-data-[slot=combobox-chip-remove]:pr-0";
1195
+ const CHIP_REMOVE_CLASS = "opacity-50 hover:opacity-100 -ml-1";
1196
+ /** The floating panel's surface. The shared popup's `InputGroup` sizing is
1197
+ * left out (mirrored in `cascader-nav.tsx`), as are its `max-h-72` and
1198
+ * `min-w-*`: `CascaderContent` sets both from the positioner's variables. */
1199
+ const CONTENT_SURFACE_CLASS = "bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 overflow-hidden ring-1 duration-100 ring-foreground/10 rounded-md shadow-md data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2";
1200
+ const EMPTY_CLASS = "text-muted-foreground hidden w-full justify-center py-2 text-center group-data-empty/combobox-content:flex text-sm";
1201
+ /**
1202
+ * The multi-select trigger surface: one removable chip per selection, where
1203
+ * `CascaderValue` would collapse to "3 selected". A component rather than a
1204
+ * recipe because of three easy misses: the popup anchored to the chips, an
1205
+ * accessible name on every remove button, and labels that disambiguate when
1206
+ * two branches use the same one.
1207
+ *
1208
+ * ```tsx
1209
+ * const anchor = useCascaderAnchor()
1210
+ * <Cascader multiple items={items}>
1211
+ * <CascaderChips ref={anchor} placeholder="Select attributes" />
1212
+ * <CascaderContent anchor={anchor}>...</CascaderContent>
1213
+ * </Cascader>
1214
+ * ```
1215
+ */
1216
+ function CascaderChips({ className, children, placeholder, strategy = "all", ...props }) {
1217
+ const { labels, multiple, invalid, resolveNode, index, isSelectable, setSelection } = useCascaderActions();
1218
+ const { selectedValues } = useCascaderState();
1219
+ React$1.useEffect(() => {
1220
+ if (process.env.NODE_ENV === "production") return;
1221
+ if (multiple) return;
1222
+ warnCascaderOnce("chips-without-multiple", "`CascaderChips` renders one chip per selection and there is only ever one without `multiple`. Use `CascaderValue` for a single-select trigger.");
1223
+ }, [multiple]);
1224
+ const nodes = (strategy === "all" ? selectedValues : getCascaderCheckedValues(index, selectedValues, strategy)).map(resolveNode);
1225
+ const ambiguous = findAmbiguousCascaderLabels(nodes);
1226
+ const removeClosure = (node) => {
1227
+ setSelection(applyCascadeSelection(index, selectedValues, node.value, false, isSelectable), "deselect");
1228
+ };
1229
+ const content = typeof children === "function" ? children(nodes) : children;
1230
+ return /* @__PURE__ */ jsx(Combobox.Chips, {
1231
+ "data-slot": "cascader-chips",
1232
+ "aria-label": labels.chipsLabel,
1233
+ ...invalid ? {
1234
+ "aria-invalid": true,
1235
+ "data-invalid": ""
1236
+ } : null,
1237
+ className: cn(CHIPS_CLASS, className),
1238
+ ...props,
1239
+ children: content ?? (nodes.length === 0 ? /* @__PURE__ */ jsx("span", {
1240
+ "data-slot": "cascader-chips-placeholder",
1241
+ className: "text-muted-foreground truncate",
1242
+ children: placeholder
1243
+ }) : nodes.map((node) => /* @__PURE__ */ jsx(CascaderChip, {
1244
+ node,
1245
+ showPath: ambiguous.has(node.value),
1246
+ ...strategy !== "all" ? { onRemove: () => removeClosure(node) } : null
1247
+ }, node.value)))
1248
+ });
1249
+ }
1250
+ /** One chip. Removal goes through Base UI's `ChipRemove`, which reports the
1251
+ * shortened selection to the root, so it lands in the same `setSelection` as
1252
+ * every other deselection and `cascade` applies to it. */
1253
+ function CascaderChip({ className, node, showPath = false, maxSegments = 2, showRemove = true, onRemove, children, ...props }) {
1254
+ const { labels, index } = useCascaderActions();
1255
+ const chain = showPath ? getCascaderPath(index, node.value) : [];
1256
+ const segments = collapseCascaderPath(chain, {
1257
+ maxSegments,
1258
+ collapse: "start"
1259
+ });
1260
+ const label = segments.length ? segments.map((segment) => segment.type === "node" ? segment.node.label : "…").join(` ${labels.pathSeparator} `) : node.label;
1261
+ return /* @__PURE__ */ jsxs(Combobox.Chip, {
1262
+ "data-slot": "cascader-chip",
1263
+ ...onRemove ? { onKeyDown: (event) => {
1264
+ if (event.key !== "Backspace" && event.key !== "Delete") return;
1265
+ event.preventBaseUIHandler();
1266
+ onRemove();
1267
+ } } : null,
1268
+ className: cn(CHIP_CLASS, "has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50", className),
1269
+ ...props,
1270
+ children: [children ?? /* @__PURE__ */ jsx("span", {
1271
+ className: "truncate",
1272
+ children: label
1273
+ }), showRemove ? /* @__PURE__ */ jsx(Combobox.ChipRemove, {
1274
+ "data-slot": "cascader-chip-remove",
1275
+ "aria-label": labels.removeChip(label),
1276
+ ...onRemove ? { onClick: (event) => {
1277
+ event.preventBaseUIHandler();
1278
+ onRemove();
1279
+ } } : null,
1280
+ className: CHIP_REMOVE_CLASS,
1281
+ children: /* @__PURE__ */ jsx(XIcon, { className: "pointer-events-none" })
1282
+ }) : null]
1283
+ });
1284
+ }
1285
+ /** Portal + Positioner + floating panel. Unlike shadcn's `ComboboxContent` it
1286
+ * does NOT clamp the popup to the anchor width (a cascader panel is routinely
1287
+ * wider), but it keeps `group/combobox-content`, which `CascaderEmpty` needs. */
1288
+ function CascaderContent({ className, side = "bottom", sideOffset = 6, align = "start", alignOffset = 0, anchor, collisionBoundary, collisionPadding, sticky, positionMethod, container, ref, ...props }) {
1289
+ const { labels } = useCascaderActions();
1290
+ const popupRef = React$1.useRef(null);
1291
+ const setPopup = React$1.useCallback((node) => {
1292
+ popupRef.current = node;
1293
+ if (typeof ref === "function") ref(node);
1294
+ else if (ref) ref.current = node;
1295
+ }, [ref]);
1296
+ /**
1297
+ * Focus goes to the search field, where the level keys and
1298
+ * `aria-activedescendant` live. Base UI's own default agrees but is COMPUTED
1299
+ * FROM THE FIRST RENDER, when the field has not mounted, and collapses to
1300
+ * "do not move focus": measured, a KEYBOARD open left focus on the trigger
1301
+ * and cost two Tabs to reach the footer where a pointer open cost one.
1302
+ *
1303
+ * `initialFocus` rather than an effect, because anything scheduled alongside
1304
+ * Base UI's focus manager is a race; the field is resolved INSIDE the
1305
+ * callback, which runs after the layout effects. Touch is handed back to
1306
+ * Base UI on purpose - focusing the popup keeps the Android keyboard shut.
1307
+ */
1308
+ const initialFocus = React$1.useCallback((openType) => {
1309
+ const popup = popupRef.current;
1310
+ if (!popup) return true;
1311
+ if (openType === "touch") return popup;
1312
+ return popup.querySelector("[data-slot=\"cascader-input\"]") ?? true;
1313
+ }, []);
1314
+ return /* @__PURE__ */ jsx(Combobox.Portal, {
1315
+ ...container !== void 0 ? { container } : null,
1316
+ children: /* @__PURE__ */ jsx(Combobox.Positioner, {
1317
+ side,
1318
+ sideOffset,
1319
+ align,
1320
+ alignOffset,
1321
+ anchor,
1322
+ ...collisionBoundary !== void 0 ? { collisionBoundary } : null,
1323
+ ...collisionPadding !== void 0 ? { collisionPadding } : null,
1324
+ ...sticky !== void 0 ? { sticky } : null,
1325
+ ...positionMethod !== void 0 ? { positionMethod } : null,
1326
+ className: "isolate z-50",
1327
+ children: /* @__PURE__ */ jsx(Combobox.Popup, {
1328
+ ref: setPopup,
1329
+ "data-slot": "cascader-content",
1330
+ "data-menu-target": "",
1331
+ "aria-label": labels.panelLabel,
1332
+ initialFocus,
1333
+ className: cn(CONTENT_SURFACE_CLASS, "group/combobox-content relative flex max-h-(--available-height) max-w-(--available-width) min-w-(--anchor-width) origin-(--transform-origin) flex-col", className),
1334
+ ...props
1335
+ })
1336
+ })
1337
+ });
1338
+ }
1339
+ /**
1340
+ * The nav + list(s) + footer container, with no positioning of its own: inside
1341
+ * `CascaderContent` for the popup case, alone for the embedded one.
1342
+ *
1343
+ * `min-h-0` and `max-h-full` are load-bearing. A flex child without `min-h-0`
1344
+ * refuses to shrink below its content, which is the usual reason a scroll area
1345
+ * never scrolls - worst embedded, where no positioner publishes
1346
+ * `--available-height`. It also owns the panel's TAB ORDER: the scroll area
1347
+ * makes itself a tab stop whenever the level overflows, stranding the footer,
1348
+ * and the panel is the only ancestor of both ends of that move in BOTH shapes.
1349
+ */
1350
+ function CascaderPanel({ className, ...props }) {
1351
+ const { mode } = useCascaderActions();
1352
+ const defaultProps = {
1353
+ "data-slot": "cascader-panel",
1354
+ "data-mode": mode,
1355
+ onKeyDown: React$1.useCallback((event) => {
1356
+ if (event.key !== "Tab" || event.defaultPrevented) return;
1357
+ if (event.altKey || event.ctrlKey || event.metaKey) return;
1358
+ const panel = event.currentTarget;
1359
+ const target = event.target;
1360
+ if (!target || !panel.contains(target)) return;
1361
+ const next = getCascaderTabTarget(panel, target, event.shiftKey);
1362
+ if (!next) return;
1363
+ event.preventDefault();
1364
+ next.focus();
1365
+ }, []),
1366
+ className: cn("flex max-h-full min-h-0 w-full flex-col", className)
1367
+ };
1368
+ return useRender({
1369
+ defaultTagName: "div",
1370
+ render: props.render,
1371
+ props: mergeProps(defaultProps, props)
1372
+ });
1373
+ }
1374
+ /**
1375
+ * Scroll container for one level: pattern 3 of the house scroll reference,
1376
+ * viewport-bounded popup scroll. The height is `min()` of `--available-height`
1377
+ * (the positioner's space between anchor and viewport edge) and `maxHeight`,
1378
+ * so neither can be ignored; a fixed pixel cap alone still reserved 288px for
1379
+ * a panel opened 200px above the fold and spilled off screen, and wasted room
1380
+ * in a tall window. The four classes are shared with `CascaderColumns` and
1381
+ * live in `cascader-lib.tsx`.
1382
+ */
1383
+ function CascaderList({ className, style, maxHeight: maxHeightProp, ...props }) {
1384
+ const { maxHeight, mode, labels, baseId, virtualized, searchScope } = useCascaderActions();
1385
+ const { currentParent, deepResults } = useCascaderState();
1386
+ const resolved = maxHeightProp ?? maxHeight;
1387
+ const listName = searchScope === "global" && deepResults ? labels.rootLevel : currentParent?.label ?? labels.rootLevel;
1388
+ return /* @__PURE__ */ jsx("div", {
1389
+ "data-slot": "cascader-list-shell",
1390
+ style: resolved != null ? {
1391
+ ...style,
1392
+ "--cascader-max-height": typeof resolved === "number" ? `${resolved}px` : resolved
1393
+ } : style,
1394
+ className: cn("relative flex max-h-full min-h-0", CASCADER_LIST_PAD_CLASS),
1395
+ children: /* @__PURE__ */ jsx("div", {
1396
+ "data-slot": "cascader-list-bounds",
1397
+ className: cn("flex w-full min-w-0 flex-col overscroll-contain", CASCADER_LIST_HEIGHT_CLASS),
1398
+ children: /* @__PURE__ */ jsx(ScrollArea, {
1399
+ className: CASCADER_SCROLL_CLASS,
1400
+ children: /* @__PURE__ */ jsx(Combobox.List, {
1401
+ "data-slot": "cascader-list",
1402
+ "aria-label": listName,
1403
+ id: `${baseId}-column-0`,
1404
+ ...mode === "tree" ? { role: "tree" } : null,
1405
+ ...virtualized ? { "data-virtualized": true } : null,
1406
+ className: cn(CASCADER_ROWS_CLASS, virtualized && "relative", className),
1407
+ ...props
1408
+ })
1409
+ })
1410
+ })
1411
+ });
1412
+ }
1413
+ /** The empty, loading and error surface, in ONE element that never unmounts.
1414
+ * `Combobox.Empty` renders whenever `filteredItems.length === 0`, as true of a
1415
+ * fetching level as of an empty one, so swapping siblings would announce "No
1416
+ * results found." over every async level. Swap the CHILDREN. */
1417
+ function CascaderEmpty({ className, children, skeletonRows, ...props }) {
1418
+ const { labels, retryLevel, retrySearch } = useCascaderActions();
1419
+ const { query, path, searchState } = useCascaderState();
1420
+ const levelKey = path.length ? path[path.length - 1] : CASCADER_ROOT_KEY;
1421
+ const loadState = useCascaderLoadState(levelKey);
1422
+ const state = query.trim() ? searchState ?? loadState : loadState;
1423
+ let body = children ?? labels.empty;
1424
+ if (state?.error) body = /* @__PURE__ */ jsxs("span", {
1425
+ "data-slot": "cascader-error",
1426
+ className: "flex flex-col items-center gap-1.5",
1427
+ children: [/* @__PURE__ */ jsx("span", { children: labels.error }), /* @__PURE__ */ jsx("button", {
1428
+ type: "button",
1429
+ "data-slot": "cascader-retry",
1430
+ onClick: (event) => {
1431
+ focusCascaderHome(event.currentTarget);
1432
+ if (query.trim() && searchState?.error) retrySearch();
1433
+ else retryLevel(levelKey);
1434
+ },
1435
+ className: "text-foreground hover:bg-accent focus-visible:ring-ring/50 rounded-md px-2 py-0.5 font-medium outline-hidden transition-colors focus-visible:ring-2",
1436
+ children: labels.retry
1437
+ })]
1438
+ });
1439
+ else if (state?.loading) body = /* @__PURE__ */ jsx("span", {
1440
+ "data-slot": "cascader-loading",
1441
+ className: "flex w-full items-center justify-center",
1442
+ children: labels.loading
1443
+ });
1444
+ return /* @__PURE__ */ jsx(Combobox.Empty, {
1445
+ "data-slot": "cascader-empty",
1446
+ "data-state": state?.error ? "error" : state?.loading ? "loading" : "empty",
1447
+ className: cn(EMPTY_CLASS, className),
1448
+ role: void 0,
1449
+ "aria-live": void 0,
1450
+ "aria-atomic": void 0,
1451
+ ...props,
1452
+ children: body
1453
+ });
1454
+ }
1455
+ /**
1456
+ * Polite live region for the current level and result count, which drill-down
1457
+ * otherwise hides in a visual breadcrumb. Built on `Combobox.Status` for its
1458
+ * initial-mutation marker: Safari and VoiceOver need a text change ~200ms
1459
+ * after mount before reading a polite region at all.
1460
+ */
1461
+ function CascaderStatus({ className, children, ...props }) {
1462
+ const { announcement } = useCascaderState();
1463
+ return /* @__PURE__ */ jsx(Combobox.Status, {
1464
+ "data-slot": "cascader-status",
1465
+ className: cn("sr-only", className),
1466
+ ...props,
1467
+ children: children ?? announcement
1468
+ });
1469
+ }
1470
+ export { Cascader, CascaderChip, CascaderChips, CascaderContent, CascaderEmpty, CascaderList, CascaderPanel, CascaderStatus, CascaderTrigger, createCascaderHighlightStore, useCascaderActions, useCascaderState };