@deftui/react 0.3.0 → 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 (29) hide show
  1. package/dist/index.d.mts +4 -3
  2. package/dist/index.mjs +2 -3
  3. package/dist/vendor/README.md +2 -1
  4. package/dist/vendor/reui.io/cascader/cascader-async.d.mts +20 -0
  5. package/dist/vendor/reui.io/cascader/cascader-async.mjs +578 -0
  6. package/dist/vendor/reui.io/cascader/cascader-context.d.mts +12 -0
  7. package/dist/vendor/reui.io/cascader/cascader-context.mjs +56 -0
  8. package/dist/vendor/reui.io/cascader/cascader-footer.d.mts +60 -0
  9. package/dist/vendor/reui.io/cascader/cascader-footer.mjs +445 -0
  10. package/dist/vendor/reui.io/cascader/cascader-i18n.mjs +64 -0
  11. package/dist/vendor/reui.io/cascader/cascader-item.d.mts +92 -0
  12. package/dist/vendor/reui.io/cascader/cascader-item.mjs +533 -0
  13. package/dist/vendor/reui.io/cascader/cascader-lib.d.mts +3 -0
  14. package/dist/vendor/reui.io/cascader/cascader-lib.mjs +688 -0
  15. package/dist/vendor/reui.io/cascader/cascader-nav.d.mts +56 -0
  16. package/dist/vendor/reui.io/cascader/cascader-nav.mjs +310 -0
  17. package/dist/vendor/reui.io/cascader/cascader-types.d.mts +140 -0
  18. package/dist/vendor/reui.io/cascader/cascader-virtual.d.mts +18 -0
  19. package/dist/vendor/reui.io/cascader/cascader-virtual.mjs +159 -0
  20. package/dist/vendor/reui.io/cascader/cascader.d.mts +299 -0
  21. package/dist/vendor/reui.io/cascader/cascader.mjs +1470 -0
  22. package/dist/vendor/reui.io/timeline.d.mts +21 -0
  23. package/dist/vendor/reui.io/timeline.mjs +127 -0
  24. package/dist/widgets/export.d.mts +28 -0
  25. package/dist/widgets/index.d.mts +55 -11
  26. package/dist/widgets/index.mjs +45 -2
  27. package/package.json +2 -2
  28. package/dist/vendor/tailored/file-tree.d.mts +0 -21
  29. package/dist/vendor/tailored/file-tree.mjs +0 -146
@@ -0,0 +1,56 @@
1
+ "use client";
2
+ import * as React$1 from "react";
3
+ // src/vendor/reui.io/cascader/cascader-context.tsx
4
+ const CascaderStateContext = React$1.createContext(void 0);
5
+ /**
6
+ * One provider serves every `T`, so the context holds an erased `unknown` value
7
+ * and this cast restores it. The primitive never inspects the payload.
8
+ */
9
+ function useCascaderState() {
10
+ const context = React$1.useContext(CascaderStateContext);
11
+ if (!context) throw new Error("useCascaderState must be used within a Cascader");
12
+ return context;
13
+ }
14
+ const CascaderActionsContext = React$1.createContext(void 0);
15
+ function useCascaderActions() {
16
+ const context = React$1.useContext(CascaderActionsContext);
17
+ if (!context) throw new Error("useCascaderActions must be used within a Cascader");
18
+ return context;
19
+ }
20
+ const CascaderRenderContext = React$1.createContext({});
21
+ function useCascaderRender() {
22
+ return React$1.useContext(CascaderRenderContext);
23
+ }
24
+ const NO_HIGHLIGHT = {
25
+ index: -1,
26
+ value: null
27
+ };
28
+ function createCascaderHighlightStore() {
29
+ let snapshot = NO_HIGHLIGHT;
30
+ const listeners = /* @__PURE__ */ new Set();
31
+ return {
32
+ subscribe(onStoreChange) {
33
+ listeners.add(onStoreChange);
34
+ return () => {
35
+ listeners.delete(onStoreChange);
36
+ };
37
+ },
38
+ getSnapshot() {
39
+ return snapshot;
40
+ },
41
+ set(next) {
42
+ if (next.index === snapshot.index && next.value === snapshot.value) return;
43
+ snapshot = next;
44
+ for (const listener of listeners) listener();
45
+ }
46
+ };
47
+ }
48
+ /** Shared and permanently empty, so the hook degrades outside a `Cascader`. */
49
+ const FALLBACK_HIGHLIGHT_STORE = createCascaderHighlightStore();
50
+ const CascaderHighlightContext = React$1.createContext(FALLBACK_HIGHLIGHT_STORE);
51
+ /** Subscribes to the highlight. Re-renders ONLY the calling component. */
52
+ function useCascaderHighlight() {
53
+ const store = React$1.useContext(CascaderHighlightContext);
54
+ return React$1.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
55
+ }
56
+ export { CascaderActionsContext, CascaderHighlightContext, CascaderRenderContext, CascaderStateContext, createCascaderHighlightStore, useCascaderActions, useCascaderHighlight, useCascaderRender, useCascaderState };
@@ -0,0 +1,60 @@
1
+ import * as React$1 from "react";
2
+ import { Popover } from "@base-ui/react";
3
+ export type CascaderFooterProps = React$1.ComponentProps<"div">;
4
+ /**
5
+ * Actions pinned below the list, a SIBLING of `CascaderList`. Children win
6
+ * over the root's `actions` prop; with neither it renders nothing.
7
+ */
8
+ declare function CascaderFooter({ className, children, onKeyDown, ...props }: CascaderFooterProps): React$1.JSX.Element | null;
9
+ export interface CascaderActionProps extends Omit<React$1.ComponentProps<"button">, "onSelect"> {
10
+ icon?: React$1.ReactNode;
11
+ /** Fires on press, after `onClick`, and not at all when disabled. */
12
+ onSelect?: () => void;
13
+ }
14
+ /**
15
+ * One footer command, shaped like a row and deliberately NOT one. A real
16
+ * `<button>`, never a `Combobox.Item`: an item would join the arrow-key ring,
17
+ * appear in `filteredItems`, and vanish the moment a query matched nothing -
18
+ * exactly when "Create new attribute" is most useful. Disabled is
19
+ * `aria-disabled`, never the native attribute: that one is not a tab stop and
20
+ * the panel's Tab order reads `button:not([disabled])`, so a footer whose ONLY
21
+ * row is a disabled command had no stop after the search field (measured on
22
+ * `c-cascader-8`). Staying focusable costs the guards below, plus the
23
+ * greyed-out look: `CASCADER_ACTION_CLASS` keys that off `aria-disabled`
24
+ * instead of `:disabled`.
25
+ */
26
+ declare function CascaderAction({ className, icon, children, onSelect, onClick, onKeyDown, disabled, ...props }: CascaderActionProps): React$1.JSX.Element;
27
+ export interface CascaderSubmenuProps {
28
+ open?: boolean;
29
+ defaultOpen?: boolean;
30
+ onOpenChange?: (open: boolean) => void;
31
+ children?: React$1.ReactNode;
32
+ }
33
+ /**
34
+ * A footer row plus the flyout it opens. Registers with the cascader root
35
+ * while open, which turns one Escape into two. Cleared in an EFFECT, so the
36
+ * flyout still reads as open during the event that closed it.
37
+ */
38
+ declare function CascaderSubmenu({ open: openProp, defaultOpen, onOpenChange, children }: CascaderSubmenuProps): React$1.JSX.Element;
39
+ export interface CascaderSubmenuTriggerProps extends Omit<React$1.ComponentProps<"button">, "onSelect"> {
40
+ icon?: React$1.ReactNode;
41
+ }
42
+ /**
43
+ * The footer row that opens the flyout, and its anchor. `aria-haspopup="menu"`
44
+ * rather than the `dialog` Base UI would announce: what opens is a list of
45
+ * commands with roving focus. `disabled` is intercepted, not forwarded:
46
+ * `Popover.Trigger` runs it through `useButton`, which writes the NATIVE
47
+ * attribute for a native `<button>` and takes the row out of the panel's Tab
48
+ * ring, the defect `CascaderAction` documents, and this component exposes no
49
+ * `focusableWhenDisabled` to opt out. Published as `aria-disabled`, with the
50
+ * arrow keys, Enter and Space, and the click closed by hand (`useClick`
51
+ * ignores `defaultPrevented`).
52
+ */
53
+ declare function CascaderSubmenuTrigger({ className, icon, children, onKeyDown, onClick, disabled, ...props }: CascaderSubmenuTriggerProps): React$1.JSX.Element;
54
+ export interface CascaderSubmenuContentProps extends Popover.Popup.Props, Pick<Popover.Positioner.Props, "side" | "align" | "sideOffset" | "alignOffset"> {}
55
+ /**
56
+ * The flyout itself. `Portal` with NO `container`: a nested portal resolves to
57
+ * the parent portal node, the mechanism the file header describes.
58
+ */
59
+ declare function CascaderSubmenuContent({ className, children, onKeyDown, side, align, sideOffset, alignOffset, ...props }: CascaderSubmenuContentProps): React$1.JSX.Element;
60
+ export { CascaderAction, CascaderFooter, CascaderSubmenu, CascaderSubmenuContent, CascaderSubmenuTrigger };
@@ -0,0 +1,445 @@
1
+ "use client";
2
+ import { useCascaderActions } from "./cascader-context.mjs";
3
+ import { CASCADER_ACTION_CLASS, CascaderGroup, CascaderLabel } from "./cascader-item.mjs";
4
+ import { CASCADER_LIST_PAD_CLASS, getCascaderFooterStops, isCascaderRtl } from "./cascader-lib.mjs";
5
+ import { cn } from "cn";
6
+ import * as React$1 from "react";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ import { ChevronRightIcon } from "lucide-react";
9
+ import { Popover } from "@base-ui/react";
10
+ import { useDirection } from "@base-ui/react/direction-provider";
11
+ // src/vendor/reui.io/cascader/cascader-footer.tsx
12
+ /**
13
+ * The pinned footer, and the side-anchored flyout a footer row can open. These
14
+ * are COMMANDS: nothing here joins the selection, the filter set or the
15
+ * highlight. The flyout is a Base UI `Popover` rendered as a REACT CHILD of
16
+ * `Combobox.Popup` with its OWN `Portal` and NO `container`: a nested portal
17
+ * resolves to the parent portal node, so it is a DOM sibling of the combobox
18
+ * popup (not clipped, not `aria-hidden`) but a React descendant, which is what
19
+ * the outside-press and focus-out whitelists read. `Combobox` builds no
20
+ * `FloatingTree`, so the flyout is not consulted first and one Escape would
21
+ * dismiss both; hence `CascaderSubmenu` registering with the root to turn one
22
+ * Escape into two. `Combobox.List` clicks its highlighted row on Enter, hence
23
+ * the footer sitting outside `CascaderList`. And a `Positioner` throws without
24
+ * its `Portal`, while `modal` stays `false` on the `Root` so the combobox
25
+ * keeps its own dismissal behaviour.
26
+ */
27
+ /**
28
+ * Keys the option list acts on, swallowed at the footer boundary. Escape and
29
+ * Tab are absent on purpose: Escape must reach the root, Tab must keep moving.
30
+ */
31
+ const FOOTER_SWALLOWED_KEYS = /* @__PURE__ */ new Set([
32
+ "Enter",
33
+ " ",
34
+ "ArrowUp",
35
+ "ArrowDown",
36
+ "Home",
37
+ "End",
38
+ "PageUp",
39
+ "PageDown"
40
+ ]);
41
+ /**
42
+ * Actions pinned below the list, a SIBLING of `CascaderList`. Children win
43
+ * over the root's `actions` prop; with neither it renders nothing.
44
+ */
45
+ function CascaderFooter({ className, children, onKeyDown, ...props }) {
46
+ const { actions, labels } = useCascaderActions();
47
+ const hasChildren = React$1.Children.count(children) > 0;
48
+ const handleKeyDown = React$1.useCallback((event) => {
49
+ onKeyDown?.(event);
50
+ if (event.defaultPrevented) return;
51
+ if (!FOOTER_SWALLOWED_KEYS.has(event.key)) return;
52
+ event.stopPropagation();
53
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
54
+ const footer = event.currentTarget;
55
+ const stops = getCascaderFooterStops(footer);
56
+ const active = document.activeElement;
57
+ const index = active ? stops.indexOf(active) : -1;
58
+ if (index === -1) return;
59
+ event.preventDefault();
60
+ const next = event.key === "ArrowDown" ? stops[index + 1] : stops[index - 1];
61
+ if (next) {
62
+ next.focus();
63
+ return;
64
+ }
65
+ if (event.key === "ArrowUp" && index > 0) return;
66
+ footer.closest("[data-slot=\"cascader-panel\"]")?.querySelector("[data-slot=\"cascader-input\"]")?.focus();
67
+ }, [onKeyDown]);
68
+ if (!hasChildren && actions.length === 0) return null;
69
+ return /* @__PURE__ */ jsx("div", {
70
+ "data-slot": "cascader-footer",
71
+ role: "group",
72
+ "aria-label": labels.actionsLabel,
73
+ onKeyDown: handleKeyDown,
74
+ className: cn("border-border/60 flex shrink-0 flex-col gap-0.5 border-t", CASCADER_LIST_PAD_CLASS, "p-(--cascader-list-pad,4px)", className),
75
+ ...props,
76
+ children: hasChildren ? children : /* @__PURE__ */ jsx(CascaderFooterActions, { actions })
77
+ });
78
+ }
79
+ function CascaderFooterActions({ actions }) {
80
+ return /* @__PURE__ */ jsx(Fragment, { children: actions.map((action, i) => action.items?.length ? /* @__PURE__ */ jsxs(CascaderSubmenu, { children: [/* @__PURE__ */ jsx(CascaderSubmenuTrigger, {
81
+ icon: action.icon,
82
+ disabled: action.disabled,
83
+ children: action.label
84
+ }), /* @__PURE__ */ jsx(CascaderSubmenuContent, { children: /* @__PURE__ */ jsx(CascaderActionList, { items: action.items }) })] }, actionKey(action, i)) : /* @__PURE__ */ jsx(CascaderAction, {
85
+ icon: action.icon,
86
+ disabled: action.disabled,
87
+ onSelect: action.onSelect,
88
+ children: action.label
89
+ }, actionKey(action, i))) });
90
+ }
91
+ function actionKey(action, index) {
92
+ if (action.value != null) return action.value;
93
+ if (typeof action.label === "string") return action.label;
94
+ return String(index);
95
+ }
96
+ /**
97
+ * Consecutive entries sharing a `group`, as runs not buckets: two separated
98
+ * runs with the same name stay two, so the author's order survives.
99
+ */
100
+ function groupActionRuns(items) {
101
+ const runs = [];
102
+ for (const item of items) {
103
+ const last = runs[runs.length - 1];
104
+ if (last && last.group === item.group) last.items.push(item);
105
+ else runs.push({
106
+ group: item.group,
107
+ items: [item]
108
+ });
109
+ }
110
+ return runs;
111
+ }
112
+ /**
113
+ * Flyout body for a data-driven submenu. A named run becomes a real
114
+ * `CascaderGroup`; unnamed runs stay unwrapped, as an unnamed group is noise.
115
+ */
116
+ function CascaderActionList({ items }) {
117
+ const { close } = useCascaderSubmenu();
118
+ const runs = React$1.useMemo(() => groupActionRuns(items), [items]);
119
+ const renderAction = (item, i) => /* @__PURE__ */ jsx(CascaderAction, {
120
+ icon: item.icon,
121
+ disabled: item.disabled,
122
+ onSelect: () => {
123
+ item.onSelect?.();
124
+ close();
125
+ },
126
+ children: item.label
127
+ }, actionKey(item, i));
128
+ return /* @__PURE__ */ jsx(Fragment, { children: runs.map((run, runIndex) => run.group ? /* @__PURE__ */ jsxs(CascaderGroup, {
129
+ className: "gap-0.5",
130
+ children: [/* @__PURE__ */ jsx(CascaderLabel, { children: run.group }), run.items.map(renderAction)]
131
+ }, `${run.group}-${runIndex}`) : /* @__PURE__ */ jsx(React$1.Fragment, { children: run.items.map(renderAction) }, `run-${runIndex}`)) });
132
+ }
133
+ /**
134
+ * The cascader popup's panel per style, spelled with `style-<name>:` variants
135
+ * rather than ReUI theme CSS so an installed footer needs only Tailwind.
136
+ */
137
+ const FLYOUT_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 max-h-72 overflow-hidden ring-1 duration-100 ring-foreground/10 rounded-md shadow-md";
138
+ /**
139
+ * One footer command, shaped like a row and deliberately NOT one. A real
140
+ * `<button>`, never a `Combobox.Item`: an item would join the arrow-key ring,
141
+ * appear in `filteredItems`, and vanish the moment a query matched nothing -
142
+ * exactly when "Create new attribute" is most useful. Disabled is
143
+ * `aria-disabled`, never the native attribute: that one is not a tab stop and
144
+ * the panel's Tab order reads `button:not([disabled])`, so a footer whose ONLY
145
+ * row is a disabled command had no stop after the search field (measured on
146
+ * `c-cascader-8`). Staying focusable costs the guards below, plus the
147
+ * greyed-out look: `CASCADER_ACTION_CLASS` keys that off `aria-disabled`
148
+ * instead of `:disabled`.
149
+ */
150
+ function CascaderAction({ className, icon, children, onSelect, onClick, onKeyDown, disabled, ...props }) {
151
+ const inMenu = React$1.useContext(CascaderMenuContext);
152
+ const handleClick = React$1.useCallback((event) => {
153
+ if (disabled) {
154
+ event.preventDefault();
155
+ return;
156
+ }
157
+ onClick?.(event);
158
+ if (event.defaultPrevented) return;
159
+ onSelect?.();
160
+ }, [
161
+ disabled,
162
+ onClick,
163
+ onSelect
164
+ ]);
165
+ const handleKeyDown = React$1.useCallback((event) => {
166
+ if (disabled) {
167
+ if (event.key === "Enter" || event.key === " ") event.preventDefault();
168
+ return;
169
+ }
170
+ onKeyDown?.(event);
171
+ }, [disabled, onKeyDown]);
172
+ return /* @__PURE__ */ jsxs("button", {
173
+ type: "button",
174
+ "data-slot": "cascader-action",
175
+ ...disabled ? {
176
+ "aria-disabled": true,
177
+ "data-disabled": ""
178
+ } : null,
179
+ onClick: handleClick,
180
+ onKeyDown: handleKeyDown,
181
+ ...inMenu ? {
182
+ role: "menuitem",
183
+ tabIndex: -1
184
+ } : null,
185
+ className: cn(CASCADER_ACTION_CLASS, className),
186
+ ...props,
187
+ children: [icon ? /* @__PURE__ */ jsx("span", {
188
+ "data-slot": "cascader-action-icon",
189
+ className: "text-muted-foreground flex shrink-0 items-center justify-center",
190
+ children: icon
191
+ }) : null, /* @__PURE__ */ jsx("span", {
192
+ className: "min-w-0 flex-1 truncate text-start",
193
+ children
194
+ })]
195
+ });
196
+ }
197
+ /** Marks the subtree INSIDE a flyout. See `inMenu` in `CascaderAction`. */
198
+ const CascaderMenuContext = React$1.createContext(false);
199
+ /**
200
+ * Every entry a menu's roving focus may land on, in DOM order, read from the
201
+ * DOM because the entries are whatever the consumer composed. A disabled
202
+ * `CascaderAction` is INCLUDED: it carries `aria-disabled`, not the native
203
+ * attribute, so `:not([disabled])` excludes only a consumer's own natively
204
+ * disabled `menuitem`, which cannot take focus.
205
+ */
206
+ function menuItems(popup) {
207
+ if (!popup) return [];
208
+ return Array.from(popup.querySelectorAll("[role=\"menuitem\"]:not([disabled])"));
209
+ }
210
+ const CascaderSubmenuContext = React$1.createContext(void 0);
211
+ /** The flyout's own state. `close()` is the one a custom entry usually wants. */
212
+ function useCascaderSubmenu() {
213
+ const context = React$1.useContext(CascaderSubmenuContext);
214
+ if (!context) throw new Error("useCascaderSubmenu must be used within a CascaderSubmenu");
215
+ return context;
216
+ }
217
+ /**
218
+ * A footer row plus the flyout it opens. Registers with the cascader root
219
+ * while open, which turns one Escape into two. Cleared in an EFFECT, so the
220
+ * flyout still reads as open during the event that closed it.
221
+ */
222
+ function CascaderSubmenu({ open: openProp, defaultOpen = false, onOpenChange, children }) {
223
+ const { setFlyoutOpen } = useCascaderActions();
224
+ const key = React$1.useId();
225
+ const triggerId = React$1.useId();
226
+ const rowRef = React$1.useRef(null);
227
+ const keyboardRef = React$1.useRef(false);
228
+ const [uncontrolled, setUncontrolled] = React$1.useState(defaultOpen);
229
+ const open = openProp ?? uncontrolled;
230
+ const setOpen = React$1.useCallback((next) => {
231
+ if (openProp == null) setUncontrolled(next);
232
+ onOpenChange?.(next);
233
+ }, [openProp, onOpenChange]);
234
+ const close = React$1.useCallback(() => setOpen(false), [setOpen]);
235
+ React$1.useEffect(() => {
236
+ setFlyoutOpen(key, open);
237
+ return () => setFlyoutOpen(key, false);
238
+ }, [
239
+ setFlyoutOpen,
240
+ key,
241
+ open
242
+ ]);
243
+ const context = React$1.useMemo(() => ({
244
+ rowRef,
245
+ open,
246
+ setOpen,
247
+ close,
248
+ triggerId,
249
+ keyboardRef
250
+ }), [
251
+ open,
252
+ setOpen,
253
+ close,
254
+ triggerId
255
+ ]);
256
+ return /* @__PURE__ */ jsx(CascaderSubmenuContext.Provider, {
257
+ value: context,
258
+ children: /* @__PURE__ */ jsx(Popover.Root, {
259
+ open,
260
+ onOpenChange: setOpen,
261
+ modal: false,
262
+ children
263
+ })
264
+ });
265
+ }
266
+ /**
267
+ * The footer row that opens the flyout, and its anchor. `aria-haspopup="menu"`
268
+ * rather than the `dialog` Base UI would announce: what opens is a list of
269
+ * commands with roving focus. `disabled` is intercepted, not forwarded:
270
+ * `Popover.Trigger` runs it through `useButton`, which writes the NATIVE
271
+ * attribute for a native `<button>` and takes the row out of the panel's Tab
272
+ * ring, the defect `CascaderAction` documents, and this component exposes no
273
+ * `focusableWhenDisabled` to opt out. Published as `aria-disabled`, with the
274
+ * arrow keys, Enter and Space, and the click closed by hand (`useClick`
275
+ * ignores `defaultPrevented`).
276
+ */
277
+ function CascaderSubmenuTrigger({ className, icon, children, onKeyDown, onClick, disabled, ...props }) {
278
+ const { labels } = useCascaderActions();
279
+ const { rowRef, setOpen, triggerId, keyboardRef } = useCascaderSubmenu();
280
+ const direction = useDirection();
281
+ const handleClick = React$1.useCallback((event) => {
282
+ if (disabled) {
283
+ event.preventDefault();
284
+ event.preventBaseUIHandler();
285
+ return;
286
+ }
287
+ onClick?.(event);
288
+ }, [disabled, onClick]);
289
+ const handleKeyDown = React$1.useCallback((event) => {
290
+ if (disabled) {
291
+ if (event.key === "Enter" || event.key === " ") event.preventDefault();
292
+ return;
293
+ }
294
+ onKeyDown?.(event);
295
+ if (event.defaultPrevented) return;
296
+ const openKey = isCascaderRtl(event.currentTarget, direction) ? "ArrowLeft" : "ArrowRight";
297
+ if (event.key === "Enter" || event.key === " ") {
298
+ keyboardRef.current = true;
299
+ return;
300
+ }
301
+ if (event.key !== openKey) return;
302
+ event.preventDefault();
303
+ keyboardRef.current = true;
304
+ setOpen(true);
305
+ }, [
306
+ disabled,
307
+ onKeyDown,
308
+ direction,
309
+ setOpen,
310
+ keyboardRef
311
+ ]);
312
+ return /* @__PURE__ */ jsxs(Popover.Trigger, {
313
+ ref: rowRef,
314
+ id: triggerId,
315
+ "data-slot": "cascader-submenu-trigger",
316
+ "aria-haspopup": "menu",
317
+ ...disabled ? {
318
+ "aria-disabled": true,
319
+ "data-disabled": ""
320
+ } : null,
321
+ onClick: handleClick,
322
+ onKeyDown: handleKeyDown,
323
+ className: cn(CASCADER_ACTION_CLASS, "aria-expanded:bg-accent aria-expanded:text-accent-foreground", className),
324
+ ...props,
325
+ children: [
326
+ icon ? /* @__PURE__ */ jsx("span", {
327
+ "data-slot": "cascader-action-icon",
328
+ className: "text-muted-foreground flex shrink-0 items-center justify-center",
329
+ children: icon
330
+ }) : null,
331
+ /* @__PURE__ */ jsx("span", {
332
+ className: "min-w-0 flex-1 truncate text-start",
333
+ children
334
+ }),
335
+ /* @__PURE__ */ jsxs("span", {
336
+ className: "sr-only",
337
+ children: [", ", labels.submenuAffordance]
338
+ }),
339
+ /* @__PURE__ */ jsx(ChevronRightIcon, {
340
+ "aria-hidden": "true",
341
+ className: "text-muted-foreground -me-0.5 size-4 shrink-0 rtl:-scale-x-100"
342
+ })
343
+ ]
344
+ });
345
+ }
346
+ /**
347
+ * The flyout itself. `Portal` with NO `container`: a nested portal resolves to
348
+ * the parent portal node, the mechanism the file header describes.
349
+ */
350
+ function CascaderSubmenuContent({ className, children, onKeyDown, side = "inline-end", align = "end", sideOffset = 8, alignOffset = 0, ...props }) {
351
+ const { rowRef, close, triggerId, keyboardRef } = useCascaderSubmenu();
352
+ const direction = useDirection();
353
+ const popupRef = React$1.useRef(null);
354
+ const typeaheadRef = React$1.useRef({
355
+ buffer: "",
356
+ at: 0
357
+ });
358
+ /**
359
+ * Keyboard opens land on the first entry; a pointer open falls through to
360
+ * Base UI's default, the popup, so a click paints no focus ring. Not an
361
+ * effect of our own: Base UI's focus manager runs on open and wins the race.
362
+ */
363
+ const initialFocus = React$1.useCallback(() => {
364
+ const byKeyboard = keyboardRef.current;
365
+ keyboardRef.current = false;
366
+ if (!byKeyboard) return true;
367
+ return menuItems(popupRef.current)[0] ?? true;
368
+ }, [keyboardRef]);
369
+ const closeAndReturn = React$1.useCallback(() => {
370
+ close();
371
+ rowRef.current?.focus();
372
+ }, [close, rowRef]);
373
+ const handleKeyDown = React$1.useCallback((event) => {
374
+ onKeyDown?.(event);
375
+ event.stopPropagation();
376
+ if (event.defaultPrevented) return;
377
+ const popup = popupRef.current;
378
+ const items = menuItems(popup);
379
+ if (items.length === 0) return;
380
+ const active = document.activeElement;
381
+ const index = active ? items.indexOf(active) : -1;
382
+ const move = (next) => {
383
+ event.preventDefault();
384
+ items[(next + items.length) % items.length]?.focus();
385
+ };
386
+ const closeKey = isCascaderRtl(event.currentTarget, direction) ? "ArrowRight" : "ArrowLeft";
387
+ switch (event.key) {
388
+ case "ArrowDown": return move(index + 1);
389
+ case "ArrowUp": return move(index === -1 ? items.length - 1 : index - 1);
390
+ case "Home": return move(0);
391
+ case "End": return move(items.length - 1);
392
+ case closeKey:
393
+ event.preventDefault();
394
+ return closeAndReturn();
395
+ case "Tab":
396
+ close();
397
+ rowRef.current?.focus();
398
+ return;
399
+ }
400
+ if (event.key.length !== 1 || event.metaKey || event.ctrlKey || event.altKey) return;
401
+ const now = event.timeStamp;
402
+ const state = typeaheadRef.current;
403
+ state.buffer = now - state.at > 500 ? event.key : state.buffer + event.key;
404
+ state.at = now;
405
+ const prefix = state.buffer.toLowerCase();
406
+ const from = index === -1 ? 0 : index;
407
+ const hit = [...items.slice(state.buffer.length > 1 ? from : from + 1), ...items.slice(0, state.buffer.length > 1 ? from : from + 1)].find((item) => (item.textContent ?? "").trim().toLowerCase().startsWith(prefix));
408
+ if (hit) {
409
+ event.preventDefault();
410
+ hit.focus();
411
+ }
412
+ }, [
413
+ onKeyDown,
414
+ direction,
415
+ close,
416
+ closeAndReturn,
417
+ rowRef
418
+ ]);
419
+ return /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Positioner, {
420
+ anchor: rowRef,
421
+ side,
422
+ align,
423
+ sideOffset,
424
+ alignOffset,
425
+ className: "isolate z-50",
426
+ children: /* @__PURE__ */ jsx(Popover.Popup, {
427
+ ref: popupRef,
428
+ initialFocus,
429
+ "data-slot": "cascader-submenu-content",
430
+ role: "menu",
431
+ "aria-orientation": "vertical",
432
+ "aria-labelledby": triggerId,
433
+ tabIndex: -1,
434
+ onKeyDown: handleKeyDown,
435
+ "data-menu-target": "",
436
+ className: cn(FLYOUT_SURFACE_CLASS, "flex max-w-(--available-width) min-w-48 flex-col gap-0.5 outline-hidden", CASCADER_LIST_PAD_CLASS, "p-(--cascader-list-pad,4px)", className),
437
+ ...props,
438
+ children: /* @__PURE__ */ jsx(CascaderMenuContext.Provider, {
439
+ value: true,
440
+ children
441
+ })
442
+ })
443
+ }) });
444
+ }
445
+ export { CascaderAction, CascaderFooter, CascaderSubmenu, CascaderSubmenuContent, CascaderSubmenuTrigger, useCascaderSubmenu };
@@ -0,0 +1,64 @@
1
+ // src/vendor/reui.io/cascader/cascader-i18n.tsx
2
+ /** Name of the root level. Hoisted so the root announcement can reuse it. */
3
+ const ROOT_LEVEL = "Top level";
4
+ /** Hoisted for the same reason: several defaults end in an item count. */
5
+ const itemCount = (count) => `${count} ${count === 1 ? "item" : "items"}`;
6
+ /**
7
+ * English defaults. Every string the primitive can render lives here, so a
8
+ * consumer can translate the whole surface by passing `labels`.
9
+ */
10
+ const CASCADER_LABELS = {
11
+ search: (parentLabel) => parentLabel ? `Search ${parentLabel}...` : "Search...",
12
+ back: "Back",
13
+ loading: "Loading...",
14
+ loadingMore: "Loading more...",
15
+ loadMore: "Load more",
16
+ error: "Could not load items.",
17
+ retry: "Retry",
18
+ empty: "No results found.",
19
+ selectedCount: (count) => `${count} selected`,
20
+ breadcrumbLabel: "Breadcrumb",
21
+ chipsLabel: "Selected items",
22
+ removeChip: (label) => `Remove ${label}`,
23
+ pathSeparator: "/",
24
+ rootLevel: ROOT_LEVEL,
25
+ itemCount,
26
+ branchAffordance: "submenu",
27
+ selectedState: "selected",
28
+ partiallySelectedState: "partially selected",
29
+ columnsLabel: "Levels",
30
+ actionsLabel: "Actions",
31
+ submenuAffordance: "opens a menu",
32
+ panelLabel: "Options",
33
+ keyboardHint: (mode, dir) => {
34
+ const open = dir === "rtl" ? "Left" : "Right";
35
+ const back = dir === "rtl" ? "Right" : "Left";
36
+ if (mode === "tree") return `Use the ${open} Arrow key to expand and the ${back} Arrow key to collapse.`;
37
+ if (mode === "columns") return `Use the ${open} Arrow key to open the next column and the ${back} Arrow key to go back.`;
38
+ return `Use the ${open} Arrow key to open a branch and the ${back} Arrow key to go back.`;
39
+ },
40
+ rootAnnouncement: (count) => `${ROOT_LEVEL}, ${itemCount(count)}`,
41
+ expandedAnnouncement: (label, count) => `${label} expanded, ${itemCount(count)}`,
42
+ collapsedAnnouncement: (label) => `${label} collapsed`,
43
+ levelAnnouncement: (parentLabel, depth, count) => `${parentLabel}, level ${depth}, ${itemCount(count)}`,
44
+ resultsAnnouncement: (count) => count === 1 ? "1 result" : `${count} results`,
45
+ maxReachedAnnouncement: (max) => `Selection limit of ${max} reached`,
46
+ cascadeAnnouncement: (label, count, selecting) => `${label} ${selecting ? "selected" : "deselected"}, ${itemCount(count)} followed`,
47
+ searchingAnnouncement: "Searching..."
48
+ };
49
+ /**
50
+ * Shallow-merges consumer overrides over the defaults, so `labels` can carry a
51
+ * single key without restating the rest.
52
+ */
53
+ function resolveCascaderLabels(labels) {
54
+ if (!labels) return CASCADER_LABELS;
55
+ return {
56
+ ...CASCADER_LABELS,
57
+ ...labels
58
+ };
59
+ }
60
+ /** Resolves the search placeholder, which may be a string or a function. */
61
+ function resolveCascaderSearchLabel(labels, parentLabel) {
62
+ return typeof labels.search === "function" ? labels.search(parentLabel) : labels.search;
63
+ }
64
+ export { CASCADER_LABELS, resolveCascaderLabels, resolveCascaderSearchLabel };