@orz/ui 0.0.0-stage → 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +541 -2
  3. package/dist/components/avatar.d.ts +14 -0
  4. package/dist/components/avatar.d.ts.map +1 -0
  5. package/dist/components/avatar.js +23 -0
  6. package/dist/components/avatar.js.map +1 -0
  7. package/dist/components/badge.d.ts +10 -0
  8. package/dist/components/badge.d.ts.map +1 -0
  9. package/dist/components/badge.js +26 -0
  10. package/dist/components/badge.js.map +1 -0
  11. package/dist/components/button.d.ts +11 -0
  12. package/dist/components/button.d.ts.map +1 -0
  13. package/dist/components/button.js +40 -0
  14. package/dist/components/button.js.map +1 -0
  15. package/dist/components/card.d.ts +12 -0
  16. package/dist/components/card.d.ts.map +1 -0
  17. package/dist/components/card.js +25 -0
  18. package/dist/components/card.js.map +1 -0
  19. package/dist/components/color-picker.d.ts +58 -0
  20. package/dist/components/color-picker.d.ts.map +1 -0
  21. package/dist/components/color-picker.js +950 -0
  22. package/dist/components/color-picker.js.map +1 -0
  23. package/dist/components/confirm-dialog.d.ts +26 -0
  24. package/dist/components/confirm-dialog.d.ts.map +1 -0
  25. package/dist/components/confirm-dialog.js +18 -0
  26. package/dist/components/confirm-dialog.js.map +1 -0
  27. package/dist/components/dialog.d.ts +26 -0
  28. package/dist/components/dialog.d.ts.map +1 -0
  29. package/dist/components/dialog.js +149 -0
  30. package/dist/components/dialog.js.map +1 -0
  31. package/dist/components/dropdown-menu.d.ts +30 -0
  32. package/dist/components/dropdown-menu.d.ts.map +1 -0
  33. package/dist/components/dropdown-menu.js +100 -0
  34. package/dist/components/dropdown-menu.js.map +1 -0
  35. package/dist/components/filter-menu.d.ts +98 -0
  36. package/dist/components/filter-menu.d.ts.map +1 -0
  37. package/dist/components/filter-menu.js +745 -0
  38. package/dist/components/filter-menu.js.map +1 -0
  39. package/dist/components/highlighted-input.d.ts +40 -0
  40. package/dist/components/highlighted-input.d.ts.map +1 -0
  41. package/dist/components/highlighted-input.js +95 -0
  42. package/dist/components/highlighted-input.js.map +1 -0
  43. package/dist/components/hover-card.d.ts +7 -0
  44. package/dist/components/hover-card.d.ts.map +1 -0
  45. package/dist/components/hover-card.js +22 -0
  46. package/dist/components/hover-card.js.map +1 -0
  47. package/dist/components/input.d.ts +4 -0
  48. package/dist/components/input.d.ts.map +1 -0
  49. package/dist/components/input.js +7 -0
  50. package/dist/components/input.js.map +1 -0
  51. package/dist/components/label.d.ts +5 -0
  52. package/dist/components/label.d.ts.map +1 -0
  53. package/dist/components/label.js +8 -0
  54. package/dist/components/label.js.map +1 -0
  55. package/dist/components/link-chip.d.ts +67 -0
  56. package/dist/components/link-chip.d.ts.map +1 -0
  57. package/dist/components/link-chip.js +71 -0
  58. package/dist/components/link-chip.js.map +1 -0
  59. package/dist/components/load-failure.d.ts +24 -0
  60. package/dist/components/load-failure.d.ts.map +1 -0
  61. package/dist/components/load-failure.js +27 -0
  62. package/dist/components/load-failure.js.map +1 -0
  63. package/dist/components/load-more.d.ts +15 -0
  64. package/dist/components/load-more.d.ts.map +1 -0
  65. package/dist/components/load-more.js +25 -0
  66. package/dist/components/load-more.js.map +1 -0
  67. package/dist/components/media-controls.d.ts +50 -0
  68. package/dist/components/media-controls.d.ts.map +1 -0
  69. package/dist/components/media-controls.js +71 -0
  70. package/dist/components/media-controls.js.map +1 -0
  71. package/dist/components/popover.d.ts +9 -0
  72. package/dist/components/popover.d.ts.map +1 -0
  73. package/dist/components/popover.js +82 -0
  74. package/dist/components/popover.js.map +1 -0
  75. package/dist/components/query-box.d.ts +78 -0
  76. package/dist/components/query-box.d.ts.map +1 -0
  77. package/dist/components/query-box.js +332 -0
  78. package/dist/components/query-box.js.map +1 -0
  79. package/dist/components/query-highlight.d.ts +14 -0
  80. package/dist/components/query-highlight.d.ts.map +1 -0
  81. package/dist/components/query-highlight.js +132 -0
  82. package/dist/components/query-highlight.js.map +1 -0
  83. package/dist/components/select.d.ts +19 -0
  84. package/dist/components/select.d.ts.map +1 -0
  85. package/dist/components/select.js +38 -0
  86. package/dist/components/select.js.map +1 -0
  87. package/dist/components/separator.d.ts +5 -0
  88. package/dist/components/separator.d.ts.map +1 -0
  89. package/dist/components/separator.js +9 -0
  90. package/dist/components/separator.js.map +1 -0
  91. package/dist/components/sheet.d.ts +16 -0
  92. package/dist/components/sheet.d.ts.map +1 -0
  93. package/dist/components/sheet.js +41 -0
  94. package/dist/components/sheet.js.map +1 -0
  95. package/dist/components/skeleton.d.ts +3 -0
  96. package/dist/components/skeleton.d.ts.map +1 -0
  97. package/dist/components/skeleton.js +7 -0
  98. package/dist/components/skeleton.js.map +1 -0
  99. package/dist/components/slider.d.ts +8 -0
  100. package/dist/components/slider.d.ts.map +1 -0
  101. package/dist/components/slider.js +14 -0
  102. package/dist/components/slider.js.map +1 -0
  103. package/dist/components/sonner.d.ts +4 -0
  104. package/dist/components/sonner.d.ts.map +1 -0
  105. package/dist/components/sonner.js +26 -0
  106. package/dist/components/sonner.js.map +1 -0
  107. package/dist/components/switch.d.ts +5 -0
  108. package/dist/components/switch.d.ts.map +1 -0
  109. package/dist/components/switch.js +8 -0
  110. package/dist/components/switch.js.map +1 -0
  111. package/dist/components/table.d.ts +11 -0
  112. package/dist/components/table.d.ts.map +1 -0
  113. package/dist/components/table.js +28 -0
  114. package/dist/components/table.js.map +1 -0
  115. package/dist/components/tabs.d.ts +8 -0
  116. package/dist/components/tabs.d.ts.map +1 -0
  117. package/dist/components/tabs.js +18 -0
  118. package/dist/components/tabs.js.map +1 -0
  119. package/dist/components/textarea.d.ts +4 -0
  120. package/dist/components/textarea.d.ts.map +1 -0
  121. package/dist/components/textarea.js +7 -0
  122. package/dist/components/textarea.js.map +1 -0
  123. package/dist/components/theme-editor/draft.d.ts +235 -0
  124. package/dist/components/theme-editor/draft.d.ts.map +1 -0
  125. package/dist/components/theme-editor/draft.js +638 -0
  126. package/dist/components/theme-editor/draft.js.map +1 -0
  127. package/dist/components/theme-editor/export-panel.d.ts +14 -0
  128. package/dist/components/theme-editor/export-panel.d.ts.map +1 -0
  129. package/dist/components/theme-editor/export-panel.js +39 -0
  130. package/dist/components/theme-editor/export-panel.js.map +1 -0
  131. package/dist/components/theme-editor/form.d.ts +69 -0
  132. package/dist/components/theme-editor/form.d.ts.map +1 -0
  133. package/dist/components/theme-editor/form.js +350 -0
  134. package/dist/components/theme-editor/form.js.map +1 -0
  135. package/dist/components/theme-editor/import-panel.d.ts +21 -0
  136. package/dist/components/theme-editor/import-panel.d.ts.map +1 -0
  137. package/dist/components/theme-editor/import-panel.js +117 -0
  138. package/dist/components/theme-editor/import-panel.js.map +1 -0
  139. package/dist/components/theme-editor/theme-list.d.ts +23 -0
  140. package/dist/components/theme-editor/theme-list.d.ts.map +1 -0
  141. package/dist/components/theme-editor/theme-list.js +42 -0
  142. package/dist/components/theme-editor/theme-list.js.map +1 -0
  143. package/dist/components/theme-editor/token-list.d.ts +30 -0
  144. package/dist/components/theme-editor/token-list.d.ts.map +1 -0
  145. package/dist/components/theme-editor/token-list.js +124 -0
  146. package/dist/components/theme-editor/token-list.js.map +1 -0
  147. package/dist/components/theme-editor/tokens.d.ts +37 -0
  148. package/dist/components/theme-editor/tokens.d.ts.map +1 -0
  149. package/dist/components/theme-editor/tokens.js +155 -0
  150. package/dist/components/theme-editor/tokens.js.map +1 -0
  151. package/dist/components/theme-editor.d.ts +26 -0
  152. package/dist/components/theme-editor.d.ts.map +1 -0
  153. package/dist/components/theme-editor.js +467 -0
  154. package/dist/components/theme-editor.js.map +1 -0
  155. package/dist/components/theme-menu.d.ts +28 -0
  156. package/dist/components/theme-menu.d.ts.map +1 -0
  157. package/dist/components/theme-menu.js +308 -0
  158. package/dist/components/theme-menu.js.map +1 -0
  159. package/dist/components/tooltip.d.ts +11 -0
  160. package/dist/components/tooltip.d.ts.map +1 -0
  161. package/dist/components/tooltip.js +18 -0
  162. package/dist/components/tooltip.js.map +1 -0
  163. package/dist/components/unsaved-changes-dialog.d.ts +39 -0
  164. package/dist/components/unsaved-changes-dialog.d.ts.map +1 -0
  165. package/dist/components/unsaved-changes-dialog.js +46 -0
  166. package/dist/components/unsaved-changes-dialog.js.map +1 -0
  167. package/dist/hooks/scroll-insets.d.ts +89 -0
  168. package/dist/hooks/scroll-insets.d.ts.map +1 -0
  169. package/dist/hooks/scroll-insets.js +265 -0
  170. package/dist/hooks/scroll-insets.js.map +1 -0
  171. package/dist/hooks/use-element-height.d.ts +11 -0
  172. package/dist/hooks/use-element-height.d.ts.map +1 -0
  173. package/dist/hooks/use-element-height.js +33 -0
  174. package/dist/hooks/use-element-height.js.map +1 -0
  175. package/dist/hooks/use-media-query.d.ts +22 -0
  176. package/dist/hooks/use-media-query.d.ts.map +1 -0
  177. package/dist/hooks/use-media-query.js +38 -0
  178. package/dist/hooks/use-media-query.js.map +1 -0
  179. package/dist/hooks/use-overlay-scrollbars.d.ts +31 -0
  180. package/dist/hooks/use-overlay-scrollbars.d.ts.map +1 -0
  181. package/dist/hooks/use-overlay-scrollbars.js +80 -0
  182. package/dist/hooks/use-overlay-scrollbars.js.map +1 -0
  183. package/dist/hooks/use-paged-append.d.ts +7 -0
  184. package/dist/hooks/use-paged-append.d.ts.map +1 -0
  185. package/dist/hooks/use-paged-append.js +53 -0
  186. package/dist/hooks/use-paged-append.js.map +1 -0
  187. package/dist/hooks/use-query-completion.d.ts +27 -0
  188. package/dist/hooks/use-query-completion.d.ts.map +1 -0
  189. package/dist/hooks/use-query-completion.js +100 -0
  190. package/dist/hooks/use-query-completion.js.map +1 -0
  191. package/dist/hooks/use-slash-shortcut.d.ts +9 -0
  192. package/dist/hooks/use-slash-shortcut.d.ts.map +1 -0
  193. package/dist/hooks/use-slash-shortcut.js +38 -0
  194. package/dist/hooks/use-slash-shortcut.js.map +1 -0
  195. package/dist/hooks/viewport-chrome.d.ts +50 -0
  196. package/dist/hooks/viewport-chrome.d.ts.map +1 -0
  197. package/dist/hooks/viewport-chrome.js +170 -0
  198. package/dist/hooks/viewport-chrome.js.map +1 -0
  199. package/dist/index.d.ts +2 -0
  200. package/dist/index.d.ts.map +1 -0
  201. package/dist/index.js +2 -0
  202. package/dist/index.js.map +1 -0
  203. package/dist/lib/cell-width-table.d.ts +3 -0
  204. package/dist/lib/cell-width-table.d.ts.map +1 -0
  205. package/dist/lib/cell-width-table.js +6 -0
  206. package/dist/lib/cell-width-table.js.map +1 -0
  207. package/dist/lib/cell-width.d.ts +7 -0
  208. package/dist/lib/cell-width.d.ts.map +1 -0
  209. package/dist/lib/cell-width.js +50 -0
  210. package/dist/lib/cell-width.js.map +1 -0
  211. package/dist/lib/clipboard.d.ts +8 -0
  212. package/dist/lib/clipboard.d.ts.map +1 -0
  213. package/dist/lib/clipboard.js +19 -0
  214. package/dist/lib/clipboard.js.map +1 -0
  215. package/dist/lib/color.d.ts +40 -0
  216. package/dist/lib/color.d.ts.map +1 -0
  217. package/dist/lib/color.js +177 -0
  218. package/dist/lib/color.js.map +1 -0
  219. package/dist/lib/scroll-map.d.ts +20 -0
  220. package/dist/lib/scroll-map.d.ts.map +1 -0
  221. package/dist/lib/scroll-map.js +46 -0
  222. package/dist/lib/scroll-map.js.map +1 -0
  223. package/dist/lib/text-metrics.d.ts +33 -0
  224. package/dist/lib/text-metrics.d.ts.map +1 -0
  225. package/dist/lib/text-metrics.js +89 -0
  226. package/dist/lib/text-metrics.js.map +1 -0
  227. package/dist/lib/theme-file.d.ts +51 -0
  228. package/dist/lib/theme-file.d.ts.map +1 -0
  229. package/dist/lib/theme-file.js +511 -0
  230. package/dist/lib/theme-file.js.map +1 -0
  231. package/dist/lib/unsaved-guard.d.ts +33 -0
  232. package/dist/lib/unsaved-guard.d.ts.map +1 -0
  233. package/dist/lib/unsaved-guard.js +37 -0
  234. package/dist/lib/unsaved-guard.js.map +1 -0
  235. package/dist/lib/utils.d.ts +3 -0
  236. package/dist/lib/utils.d.ts.map +1 -0
  237. package/dist/lib/utils.js +6 -0
  238. package/dist/lib/utils.js.map +1 -0
  239. package/dist/theme/custom.d.ts +8 -0
  240. package/dist/theme/custom.d.ts.map +1 -0
  241. package/dist/theme/custom.js +56 -0
  242. package/dist/theme/custom.js.map +1 -0
  243. package/dist/theme/index.d.ts +6 -0
  244. package/dist/theme/index.d.ts.map +1 -0
  245. package/dist/theme/index.js +5 -0
  246. package/dist/theme/index.js.map +1 -0
  247. package/dist/theme/paint-guard.d.ts +20 -0
  248. package/dist/theme/paint-guard.d.ts.map +1 -0
  249. package/dist/theme/paint-guard.js +120 -0
  250. package/dist/theme/paint-guard.js.map +1 -0
  251. package/dist/theme/picks.d.ts +22 -0
  252. package/dist/theme/picks.d.ts.map +1 -0
  253. package/dist/theme/picks.js +43 -0
  254. package/dist/theme/picks.js.map +1 -0
  255. package/dist/theme/pref.d.ts +39 -0
  256. package/dist/theme/pref.d.ts.map +1 -0
  257. package/dist/theme/pref.js +129 -0
  258. package/dist/theme/pref.js.map +1 -0
  259. package/dist/theme/provider.d.ts +41 -0
  260. package/dist/theme/provider.d.ts.map +1 -0
  261. package/dist/theme/provider.js +103 -0
  262. package/dist/theme/provider.js.map +1 -0
  263. package/dist/theme/store.d.ts +104 -0
  264. package/dist/theme/store.d.ts.map +1 -0
  265. package/dist/theme/store.js +509 -0
  266. package/dist/theme/store.js.map +1 -0
  267. package/package.json +94 -3
  268. package/styles.css +79 -0
  269. package/theme.css +186 -0
  270. package/vendor/shadcn-tailwind.css +664 -0
@@ -0,0 +1,745 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CheckIcon, ChevronRightIcon, SearchIcon } from "lucide-react";
3
+ import { Fragment, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react";
4
+ import { cn } from "../lib/utils.js";
5
+ import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger, } from "./popover.js";
6
+ /** Case-folded, with diacritics dropped, so `frappe` finds `Frappé`. */
7
+ function fold(text) {
8
+ // A final sigma lowers to ς only at a word's end, which a single character
9
+ // folded on its own never is; σ for both keeps a text matching itself.
10
+ return text
11
+ .normalize("NFD")
12
+ .replace(/\p{M}/gu, "")
13
+ .toLowerCase()
14
+ .replace(/ς/g, "σ");
15
+ }
16
+ /**
17
+ * Where `query` occurs in `text`, folded as above, as a range of `text`'s own
18
+ * indices; `null` when it does not. An empty query matches at 0 with nothing
19
+ * to mark.
20
+ */
21
+ export function matchText(text, query) {
22
+ const needle = fold(query);
23
+ if (needle === "")
24
+ return { start: 0, end: 0 };
25
+ // Folding changes lengths (é is one unit, then two, then one), so each
26
+ // folded unit remembers the source character it came from.
27
+ let folded = "";
28
+ const starts = [];
29
+ const ends = [];
30
+ let at = 0;
31
+ for (const char of text) {
32
+ const part = fold(char);
33
+ if (part === "" && ends.length > 0) {
34
+ // A combining mark already in the source joins its base letter's range.
35
+ ends[ends.length - 1] = at + char.length;
36
+ }
37
+ for (let i = 0; i < part.length; i++) {
38
+ starts.push(at);
39
+ ends.push(at + char.length);
40
+ }
41
+ folded += part;
42
+ at += char.length;
43
+ }
44
+ const hit = folded.indexOf(needle);
45
+ if (hit < 0)
46
+ return null;
47
+ return {
48
+ start: starts[hit],
49
+ end: ends[hit + needle.length - 1],
50
+ };
51
+ }
52
+ /**
53
+ * `text`, with the part `query` matched in a `<mark>`. One element, so a
54
+ * flex row lays it out as one item rather than three.
55
+ */
56
+ export function MatchedText({ text, query }) {
57
+ const range = matchText(text, query);
58
+ if (!range || range.start === range.end)
59
+ return _jsx("span", { children: text });
60
+ return (_jsxs("span", { children: [text.slice(0, range.start), _jsx("mark", { className: "bg-transparent font-semibold text-inherit underline decoration-primary/60 underline-offset-2", children: text.slice(range.start, range.end) }), text.slice(range.end)] }));
61
+ }
62
+ const FOCUSABLE = 'input, textarea, select, button, a[href], [contenteditable="true"], [tabindex]:not([tabindex="-1"])';
63
+ const TABBABLE = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable="true"]';
64
+ /** Focuses the first element after (or before) `from` in tab order that takes it. */
65
+ function focusBeside(from, backwards, skip) {
66
+ const all = [...document.querySelectorAll(TABBABLE)].filter((el) => el === from || (!skip?.contains(el) && el.tabIndex >= 0));
67
+ const index = all.indexOf(from);
68
+ const candidates = backwards
69
+ ? all.slice(0, Math.max(index, 0)).reverse()
70
+ : all.slice(index + 1);
71
+ for (const el of candidates) {
72
+ el.focus({ preventScroll: false });
73
+ if (document.activeElement === el)
74
+ return;
75
+ }
76
+ // Nothing beyond the trigger takes focus; keep it somewhere useful.
77
+ from.focus({ preventScroll: true });
78
+ }
79
+ /** Ray casting, the test Radix Menu's submenu grace area uses. */
80
+ function insidePolygon(point, polygon) {
81
+ let inside = false;
82
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
83
+ const a = polygon[i];
84
+ const b = polygon[j];
85
+ if (a.y > point.y !== b.y > point.y &&
86
+ point.x < ((b.x - a.x) * (point.y - a.y)) / (b.y - a.y) + a.x) {
87
+ inside = !inside;
88
+ }
89
+ }
90
+ return inside;
91
+ }
92
+ /** How long the pointer may take crossing other rows toward an open submenu. */
93
+ const GRACE_MS = 300;
94
+ const ROW_CLASS = "group/option relative flex w-full cursor-default items-center rounded-md text-sm select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4";
95
+ const CELL_CLASS = "gap-1.5 py-1 pl-1.5";
96
+ const OPTION_CLASS = cn(ROW_CLASS, CELL_CLASS, "data-highlighted:bg-accent data-highlighted:text-accent-foreground");
97
+ /**
98
+ * A split row's left half, the part that picks the row. Only it lights up:
99
+ * the chevron's half opens the submenu, which hovering the row already did.
100
+ */
101
+ const ACTION_CLASS = cn("relative flex min-w-0 flex-1 items-center self-stretch rounded-md pr-6.5", CELL_CLASS, "group-data-highlighted/option:bg-accent group-data-highlighted/option:text-accent-foreground");
102
+ function step(from, at, by) {
103
+ for (let i = at + by; i >= 0 && i < from.length; i += by) {
104
+ const row = from[i];
105
+ if (!row.disabled)
106
+ return row;
107
+ }
108
+ return undefined;
109
+ }
110
+ function FilterBox({ expanded, ...props }) {
111
+ return (_jsxs("div", { className: "flex items-center gap-2 border-b px-2.5", children: [_jsx(SearchIcon, { "aria-hidden": true, className: "size-4 shrink-0 text-muted-foreground" }), _jsx("input", { type: "text", role: "combobox", "aria-expanded": expanded, "aria-autocomplete": "list", autoComplete: "off", autoCorrect: "off", spellCheck: false, className: "h-9 min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground", ...props })] }));
112
+ }
113
+ function Mark({ className, children, }) {
114
+ return (_jsx("span", { className: cn("pointer-events-none absolute flex size-4 items-center justify-center", className), children: children }));
115
+ }
116
+ const isEscapeOfComposition = (event) => event.isComposing || event.keyCode === 229;
117
+ /**
118
+ * A popover with a filter box over a list, the combobox pattern: focus stays
119
+ * in the box, so typing always filters, and the arrow keys walk a highlight
120
+ * that `aria-activedescendant` follows. The caller filters; this component
121
+ * owns the surface, the highlight and the keys.
122
+ */
123
+ export function FilterMenu({ ref, children, open: openProp, defaultOpen = false, onOpenChange, query, onQueryChange, sections, renderRow, selectedId, onHighlight, onSelect, getSubmenu, submenuFilter, selectedSubmenuId, submenuLabel, header, footer, placeholder = "Filter…", inputLabel = "Filter", listLabel, empty = (q) => (q === "" ? "Nothing to pick" : `No matches for "${q}"`), align = "start", side, sideOffset, className, }) {
124
+ const [openState, setOpenState] = useState(defaultOpen);
125
+ const open = openProp ?? openState;
126
+ const [highlight, setHighlightId] = useState(null);
127
+ // Non-null while the highlight is inside the highlighted row's submenu.
128
+ const [subHighlight, setSubHighlight] = useState(null);
129
+ // A submenu opens for a highlight the user moved, not for one resting on
130
+ // the committed row or the query's first hit.
131
+ const [moved, setMoved] = useState(false);
132
+ // Kept by parent, so passing back over the parent row on the way into its
133
+ // submenu does not wipe what was typed there.
134
+ const [subQuery, setSubQuery] = useState(null);
135
+ // The submenu's filter box has focus, and with it the keys.
136
+ const [subKeys, setSubKeys] = useState(false);
137
+ // Like `mode`: typing in the submenu's box puts its highlight on the first
138
+ // hit until the user moves it.
139
+ const subMode = useRef("user");
140
+ const reported = useRef(null);
141
+ const input = useRef(null);
142
+ const subInput = useRef(null);
143
+ const trigger = useRef(null);
144
+ const content = useRef(null);
145
+ const list = useRef(null);
146
+ const submenu = useRef(null);
147
+ const anchor = useRef(null);
148
+ const tabbing = useRef(null);
149
+ const grace = useRef(null);
150
+ const graceTimer = useRef(null);
151
+ const pointerIn = useRef(null);
152
+ // A row's leave fires at the same point as the move that crossed out of it,
153
+ // so the direction it left in is only known from the sample before.
154
+ const lastX = useRef(null);
155
+ /**
156
+ * Who placed the highlight: resting on the committed row, on the query's
157
+ * first hit, or by the user. The first two follow the list as it changes.
158
+ */
159
+ const mode = useRef("rest");
160
+ const base = useId();
161
+ const listId = `${base}-list`;
162
+ const submenuId = `${base}-submenu`;
163
+ const visible = useMemo(() => sections.filter((section) => section.rows.length > 0), [sections]);
164
+ const rows = useMemo(() => visible.flatMap((s) => s.rows), [visible]);
165
+ const indexOf = useMemo(() => new Map(rows.map((row, i) => [row.id, i])), [rows]);
166
+ const optionId = (index) => `${base}-option-${index}`;
167
+ const subOptionId = (index) => `${base}-suboption-${index}`;
168
+ const onHighlightRef = useRef(onHighlight);
169
+ onHighlightRef.current = onHighlight;
170
+ const report = useCallback((row) => {
171
+ const id = row?.id ?? null;
172
+ if (reported.current === id)
173
+ return;
174
+ reported.current = id;
175
+ onHighlightRef.current?.(row);
176
+ }, []);
177
+ // Moving the highlight in the list always leaves the submenu.
178
+ const setHighlightState = useCallback((id, byUser = false) => {
179
+ setHighlightId(id);
180
+ setSubHighlight(null);
181
+ setSubKeys(false);
182
+ subMode.current = "user";
183
+ setMoved(byUser);
184
+ }, []);
185
+ /** A highlight the user moved, reported as such. */
186
+ const move = (row) => {
187
+ mode.current = "user";
188
+ setHighlightState(row?.id ?? null, true);
189
+ report(row);
190
+ };
191
+ const committed = selectedId !== undefined && indexOf.has(selectedId) ? selectedId : null;
192
+ /** The highlight back on the committed row, which shows no preview. */
193
+ const rest = useCallback(() => {
194
+ mode.current = "rest";
195
+ setHighlightState(committed);
196
+ report(null);
197
+ }, [committed, report, setHighlightState]);
198
+ useImperativeHandle(ref, () => ({ resetHighlight: rest }), [rest]);
199
+ const setOpen = (next) => {
200
+ if (next === open)
201
+ return;
202
+ // Cleared here as well as below, so the opening render is already
203
+ // unfiltered rather than one pass later.
204
+ if (next)
205
+ onQueryChange("");
206
+ setOpenState(next);
207
+ onOpenChange?.(next);
208
+ };
209
+ // In an effect rather than setOpen, so a controlled `open` that the parent
210
+ // flips gets the same reset and the same closing null.
211
+ const wasOpen = useRef(false);
212
+ const onQueryChangeRef = useRef(onQueryChange);
213
+ onQueryChangeRef.current = onQueryChange;
214
+ useLayoutEffect(() => {
215
+ if (open === wasOpen.current)
216
+ return;
217
+ wasOpen.current = open;
218
+ if (!open)
219
+ report(null);
220
+ else {
221
+ setSubQuery(null);
222
+ if (query !== "")
223
+ onQueryChangeRef.current("");
224
+ }
225
+ }, [open, query, report]);
226
+ const highlightIndex = highlight === null ? -1 : (indexOf.get(highlight) ?? -1);
227
+ const parent = highlightIndex < 0 ? undefined : rows[highlightIndex];
228
+ const hasSubmenu = (row) => (getSubmenu?.(row, "")?.length ?? 0) > 0;
229
+ const subFilter = parent && hasSubmenu(parent) ? submenuFilter?.(parent) : undefined;
230
+ const ownQuery = subFilter && subQuery && subQuery.parent === parent?.id ? subQuery : null;
231
+ const subText = ownQuery?.text ?? "";
232
+ const subRows = useMemo(() => (parent && getSubmenu?.(parent, subText)) || [], [parent, getSubmenu, subText]);
233
+ const submenuOpen = open && moved && parent !== undefined && hasSubmenu(parent);
234
+ const subBox = submenuOpen && subFilter !== undefined;
235
+ const inSubBox = subBox && subKeys;
236
+ const subIndex = subHighlight === null
237
+ ? -1
238
+ : subRows.findIndex((row) => row.id === subHighlight);
239
+ // Radix only flips a side submenu to the other side and never shifts it
240
+ // sideways, so where neither side has room it opens below the row instead.
241
+ const [subBelow, setSubBelow] = useState(false);
242
+ const placeSubmenu = useCallback(() => {
243
+ const surface = content.current?.getBoundingClientRect();
244
+ const width = submenu.current?.offsetWidth;
245
+ if (!surface || !width)
246
+ return;
247
+ const room = document.documentElement.clientWidth;
248
+ setSubBelow(surface.right + width > room && surface.left - width < 0);
249
+ }, []);
250
+ // Not an effect on opening: the portal mounts the submenu a commit later.
251
+ const submenuRef = useCallback((el) => {
252
+ submenu.current = el;
253
+ if (el)
254
+ placeSubmenu();
255
+ }, [placeSubmenu]);
256
+ useEffect(() => {
257
+ if (!submenuOpen)
258
+ return;
259
+ window.addEventListener("resize", placeSubmenu);
260
+ return () => window.removeEventListener("resize", placeSubmenu);
261
+ }, [submenuOpen, placeSubmenu]);
262
+ // Typing puts the highlight on the best hit, so Enter takes it; clearing the
263
+ // query puts it back on the committed row. Until the user moves it, it keeps
264
+ // following those as the list changes (a late load, a filter that lags the
265
+ // query); a row of theirs that leaves the list takes the highlight with it.
266
+ const lastQuery = useRef(null);
267
+ useLayoutEffect(() => {
268
+ if (!open) {
269
+ lastQuery.current = null;
270
+ return;
271
+ }
272
+ if (lastQuery.current !== query) {
273
+ const opening = lastQuery.current === null;
274
+ lastQuery.current = query;
275
+ mode.current = query === "" || opening ? "rest" : "hit";
276
+ }
277
+ if (mode.current === "rest") {
278
+ setHighlightState(committed);
279
+ report(null);
280
+ }
281
+ else if (mode.current === "hit") {
282
+ const first = rows.find((row) => !row.disabled) ?? null;
283
+ setHighlightState(first?.id ?? null);
284
+ report(first);
285
+ }
286
+ else if (highlight !== null && !indexOf.has(highlight)) {
287
+ setHighlightState(null);
288
+ report(null);
289
+ }
290
+ else if (inSubBox && subMode.current === "hit") {
291
+ const first = step(subRows, -1, 1) ?? null;
292
+ if ((first?.id ?? null) !== subHighlight) {
293
+ setSubHighlight(first?.id ?? null);
294
+ report(first ?? parent ?? null);
295
+ }
296
+ }
297
+ else if (subHighlight !== null && subIndex < 0) {
298
+ // The submenu row is gone; the parent row still stands.
299
+ setSubHighlight(null);
300
+ report(parent ?? null);
301
+ }
302
+ }, [
303
+ open,
304
+ query,
305
+ rows,
306
+ indexOf,
307
+ highlight,
308
+ committed,
309
+ report,
310
+ setHighlightState,
311
+ subHighlight,
312
+ subIndex,
313
+ subRows,
314
+ inSubBox,
315
+ parent,
316
+ ]);
317
+ // Focus follows whichever box has the keys. Only between the two boxes, or
318
+ // back from nowhere when the submenu took the focused box away with it.
319
+ useLayoutEffect(() => {
320
+ if (!open)
321
+ return;
322
+ const target = inSubBox ? subInput.current : input.current;
323
+ const active = document.activeElement;
324
+ if (!target || active === target)
325
+ return;
326
+ if (active === null ||
327
+ active === document.body ||
328
+ active === input.current ||
329
+ active === subInput.current) {
330
+ target.focus({ preventScroll: true });
331
+ }
332
+ }, [open, inSubBox]);
333
+ const inSubBoxRef = useRef(inSubBox);
334
+ inSubBoxRef.current = inSubBox;
335
+ // The submenu mounts a render after it opens, so a box entered by → is not
336
+ // there yet when the effect above runs.
337
+ const subInputRef = useCallback((el) => {
338
+ subInput.current = el;
339
+ if (el && inSubBoxRef.current)
340
+ el.focus({ preventScroll: true });
341
+ }, []);
342
+ const revealHighlight = useRef(() => { });
343
+ revealHighlight.current = () => {
344
+ if (!open)
345
+ return;
346
+ const id = subIndex >= 0
347
+ ? subOptionId(subIndex)
348
+ : highlightIndex >= 0
349
+ ? optionId(highlightIndex)
350
+ : null;
351
+ if (id)
352
+ document.getElementById(id)?.scrollIntoView?.({ block: "nearest" });
353
+ };
354
+ useLayoutEffect(() => revealHighlight.current());
355
+ // The effect above runs before the portal has mounted the list on open, and
356
+ // the list only gets its height once the surface is placed, so the
357
+ // committed row is revealed again as the list takes a size. Only until the
358
+ // user scrolls it: a page scroll or a window resize resizes it as well.
359
+ const revealOnResize = useRef(false);
360
+ const stopRevealOnResize = () => {
361
+ revealOnResize.current = false;
362
+ };
363
+ const listRef = useCallback((el) => {
364
+ list.current = el;
365
+ if (el === null || typeof ResizeObserver === "undefined")
366
+ return;
367
+ revealOnResize.current = true;
368
+ const observer = new ResizeObserver(() => {
369
+ if (revealOnResize.current)
370
+ revealHighlight.current();
371
+ });
372
+ observer.observe(el);
373
+ // With a cleanup returned, React no longer calls the ref with null.
374
+ return () => {
375
+ observer.disconnect();
376
+ list.current = null;
377
+ };
378
+ }, []);
379
+ const clearGrace = useCallback(() => {
380
+ grace.current = null;
381
+ if (graceTimer.current !== null)
382
+ clearTimeout(graceTimer.current);
383
+ graceTimer.current = null;
384
+ }, []);
385
+ useEffect(() => clearGrace, [clearGrace]);
386
+ useEffect(() => {
387
+ if (!open)
388
+ clearGrace();
389
+ }, [open, clearGrace]);
390
+ /** `heading: false` for the list's own leave, which fires where the row's did. */
391
+ const inGrace = (event, heading = true) => {
392
+ const area = grace.current;
393
+ if (area === null || Date.now() >= area.until)
394
+ return false;
395
+ // As Radix does, only while heading for the submenu's side: a pointer
396
+ // moving straight down the list grazes the triangle's thin edge. A sample
397
+ // with no sideways movement keeps the last direction.
398
+ if (event.clientX !== area.x) {
399
+ area.towards = area.right === event.clientX > area.x;
400
+ area.x = event.clientX;
401
+ }
402
+ return ((area.towards || !heading) &&
403
+ insidePolygon({ x: event.clientX, y: event.clientY }, area.polygon));
404
+ };
405
+ /**
406
+ * On the menu's own border where a submenu sits flush against it: the one
407
+ * pixel between the list and the submenu, crossed either way.
408
+ */
409
+ const onSeam = (event) => {
410
+ const rect = submenu.current?.getBoundingClientRect();
411
+ if (!rect || event.relatedTarget !== content.current)
412
+ return false;
413
+ const { clientX: x, clientY: y } = event;
414
+ return (y >= rect.top &&
415
+ y <= rect.bottom &&
416
+ Math.min(Math.abs(x - rect.left), Math.abs(x - rect.right)) <= 2);
417
+ };
418
+ /** The highlight goes after `ms` unless the pointer is back in the list or the submenu. */
419
+ const closeUnlessBack = (ms) => {
420
+ if (graceTimer.current !== null)
421
+ clearTimeout(graceTimer.current);
422
+ graceTimer.current = setTimeout(() => {
423
+ graceTimer.current = null;
424
+ if (pointerIn.current === null)
425
+ move(null);
426
+ }, ms);
427
+ };
428
+ const choose = (row) => {
429
+ if (row.disabled)
430
+ return;
431
+ onSelect(row);
432
+ setOpen(false);
433
+ };
434
+ const moveIntoSubmenu = (row) => {
435
+ mode.current = "user";
436
+ subMode.current = "user";
437
+ clearGrace();
438
+ setMoved(true);
439
+ setSubHighlight(row.id);
440
+ report(row);
441
+ };
442
+ /** The submenu's filter box takes the keys; its highlight stays put. */
443
+ const enterSubBox = () => {
444
+ mode.current = "user";
445
+ clearGrace();
446
+ setMoved(true);
447
+ setSubKeys(true);
448
+ };
449
+ const backToParent = () => {
450
+ setSubHighlight(null);
451
+ setSubKeys(false);
452
+ subMode.current = "user";
453
+ report(parent ?? null);
454
+ };
455
+ /** Both boxes' keys; `box` says which one they were pressed in. */
456
+ const onKeyDown = (event, box) => {
457
+ // Safari ends a composition with an Enter whose isComposing is already
458
+ // false; keyCode 229 still marks it as the IME's.
459
+ if (event.nativeEvent.isComposing || event.keyCode === 229)
460
+ return;
461
+ const inSubmenu = box === "submenu" || subIndex >= 0;
462
+ const from = inSubmenu ? subRows : rows;
463
+ const at = inSubmenu ? subIndex : highlightIndex;
464
+ const field = event.currentTarget;
465
+ let next;
466
+ switch (event.key) {
467
+ case "ArrowDown":
468
+ next = step(from, at < 0 ? -1 : at, 1);
469
+ break;
470
+ case "ArrowUp":
471
+ next = step(from, at < 0 ? from.length : at, -1);
472
+ break;
473
+ case "Home":
474
+ next = step(from, -1, 1);
475
+ break;
476
+ case "End":
477
+ next = step(from, from.length, -1);
478
+ break;
479
+ case "ArrowRight": {
480
+ if (inSubmenu || field.selectionStart !== field.value.length)
481
+ return;
482
+ if (!parent || !hasSubmenu(parent))
483
+ return;
484
+ const first = step(subRows, -1, 1);
485
+ // A filter box is somewhere to go even with every row filtered out.
486
+ if (!first && !subFilter)
487
+ return;
488
+ event.preventDefault();
489
+ if (subFilter)
490
+ enterSubBox();
491
+ if (first)
492
+ moveIntoSubmenu(first);
493
+ return;
494
+ }
495
+ case "ArrowLeft":
496
+ if (!inSubmenu)
497
+ return;
498
+ // The submenu's own box moves its caret first, as → does in the menu's.
499
+ if (box === "submenu" &&
500
+ (field.selectionStart !== 0 || field.selectionEnd !== 0)) {
501
+ return;
502
+ }
503
+ event.preventDefault();
504
+ backToParent();
505
+ return;
506
+ case "Enter": {
507
+ event.preventDefault();
508
+ const row = at < 0 ? undefined : from[at];
509
+ if (row)
510
+ choose(row);
511
+ return;
512
+ }
513
+ case "Tab":
514
+ event.preventDefault();
515
+ tabbing.current = event.shiftKey ? "backward" : "forward";
516
+ setOpen(false);
517
+ return;
518
+ default:
519
+ return;
520
+ }
521
+ event.preventDefault();
522
+ if (!next)
523
+ return;
524
+ if (inSubmenu)
525
+ moveIntoSubmenu(next);
526
+ else
527
+ move(next);
528
+ };
529
+ // The row's height and the surface's sides: the submenu sits against the
530
+ // surface's border whatever the list's padding and scrollbar take.
531
+ const anchorRow = useCallback((row) => {
532
+ anchor.current = row && {
533
+ // Through it, floating-ui follows the row's scrolling and resizing.
534
+ contextElement: row,
535
+ getBoundingClientRect: () => {
536
+ const own = row.getBoundingClientRect();
537
+ const surface = content.current?.getBoundingClientRect() ?? own;
538
+ return DOMRect.fromRect({
539
+ x: surface.x,
540
+ y: own.y,
541
+ width: surface.width,
542
+ height: own.height,
543
+ });
544
+ },
545
+ };
546
+ }, []);
547
+ /** Leaving the parent row: the triangle toward the submenu's near edge. */
548
+ const onParentLeave = (event) => {
549
+ const rect = submenu.current?.getBoundingClientRect();
550
+ if (!rect)
551
+ return;
552
+ const right = rect.left >= event.clientX;
553
+ const near = right ? rect.left : rect.right;
554
+ const from = lastX.current ?? event.clientX;
555
+ grace.current = {
556
+ polygon: [
557
+ { x: event.clientX + (right ? -5 : 5), y: event.clientY },
558
+ { x: near, y: rect.top },
559
+ { x: near, y: rect.bottom },
560
+ ],
561
+ until: Date.now() + GRACE_MS,
562
+ right,
563
+ x: event.clientX,
564
+ towards: event.clientX !== from && right === event.clientX > from,
565
+ };
566
+ };
567
+ let flatIndex = 0;
568
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, ref: trigger,
569
+ // What a select's trigger does; a popover's opens on click alone.
570
+ onKeyDown: (event) => {
571
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp")
572
+ return;
573
+ event.preventDefault();
574
+ setOpen(true);
575
+ }, children: children }), _jsxs(PopoverContent, { ref: content, align: align, side: side, sideOffset: sideOffset, className: cn("flex max-h-[min(24rem,var(--radix-popover-content-available-height))] flex-col overflow-hidden p-0", className),
576
+ // Pressing anything but a control (a row, the search icon, padding)
577
+ // would move focus onto the surface, out of the box and its keys.
578
+ onMouseDown: (event) => {
579
+ if (!event.target.closest(FOCUSABLE)) {
580
+ event.preventDefault();
581
+ }
582
+ }, onEscapeKeyDown: (event) => {
583
+ // That Escape cancels the composition, not the menu.
584
+ if (isEscapeOfComposition(event))
585
+ event.preventDefault();
586
+ }, onOpenAutoFocus: (event) => {
587
+ // The box, not the first tabbable, which may be in `header`.
588
+ event.preventDefault();
589
+ input.current?.focus({ preventScroll: true });
590
+ }, onCloseAutoFocus: (event) => {
591
+ const direction = tabbing.current;
592
+ tabbing.current = null;
593
+ if (direction === null || !trigger.current)
594
+ return;
595
+ event.preventDefault();
596
+ // The surface may already be gone by now, its ref with it.
597
+ focusBeside(trigger.current, direction === "backward", content.current);
598
+ }, children: [_jsx(FilterBox, { ref: input, "aria-label": inputLabel, expanded: open, "aria-controls": listId, "aria-activedescendant": subIndex >= 0
599
+ ? subOptionId(subIndex)
600
+ : highlightIndex < 0
601
+ ? undefined
602
+ : optionId(highlightIndex), placeholder: placeholder, value: query, onChange: (event) => onQueryChange(event.target.value), onKeyDown: (event) => onKeyDown(event, "menu"),
603
+ // Pressed back into while the submenu's box had the keys.
604
+ onFocus: () => {
605
+ if (subKeys)
606
+ backToParent();
607
+ } }), header, _jsxs("div", { ref: listRef, id: listId, role: "listbox", "aria-label": listLabel, className: "min-h-0 flex-1 overflow-y-auto p-1", onWheel: stopRevealOnResize, onTouchMove: stopRevealOnResize,
608
+ // Where a drag of the scrollbar starts, too.
609
+ onPointerDown: stopRevealOnResize, onPointerEnter: (event) => {
610
+ pointerIn.current = "list";
611
+ lastX.current = event.clientX;
612
+ }, onPointerMove: (event) => {
613
+ lastX.current = event.clientX;
614
+ }, onPointerLeave: (event) => {
615
+ pointerIn.current = null;
616
+ const to = event.relatedTarget;
617
+ if (to instanceof Node && submenu.current?.contains(to))
618
+ return;
619
+ if (onSeam(event)) {
620
+ // However long the pointer lingered on the scrollbar on its way.
621
+ closeUnlessBack(GRACE_MS);
622
+ return;
623
+ }
624
+ if (inGrace(event, false)) {
625
+ // Crossing the gap toward the submenu; if the pointer never
626
+ // arrives, the highlight still goes when the grace runs out.
627
+ closeUnlessBack((grace.current?.until ?? 0) - Date.now());
628
+ return;
629
+ }
630
+ if (highlight !== null)
631
+ move(null);
632
+ }, children: [rows.length === 0 && (_jsx("div", { role: "presentation", className: "px-2 py-4 text-center text-muted-foreground text-sm", children: empty(query) })), visible.map((section, sectionIndex) => {
633
+ // By position: a section id may hold spaces, which would split
634
+ // aria-labelledby's id list.
635
+ const labelId = `${base}-section-${sectionIndex}`;
636
+ const options = section.rows.map((row) => {
637
+ const index = flatIndex++;
638
+ const highlighted = index === highlightIndex;
639
+ const selected = row.id === selectedId;
640
+ const split = hasSubmenu(row);
641
+ // One gutter for every row of a menu with submenus, so what
642
+ // renderRow puts at the right edge lines up.
643
+ const gutter = getSubmenu !== undefined;
644
+ const cell = (_jsxs(_Fragment, { children: [renderRow(row, { highlighted, selected, query }), selected && (_jsx(Mark, { className: split ? "right-0.5" : gutter ? "right-7" : "right-2", children: _jsx(CheckIcon, { "aria-hidden": true }) }))] }));
645
+ return (_jsxs("div", { ref: highlighted ? anchorRow : undefined, id: optionId(index), role: "option", tabIndex: -1, "aria-selected": highlighted && subIndex < 0, "aria-disabled": row.disabled || undefined, "aria-haspopup": split ? "listbox" : undefined, "aria-expanded": split ? highlighted && submenuOpen : undefined, "data-highlighted": highlighted ? "" : undefined, "data-selected": selected ? "" : undefined, "data-disabled": row.disabled ? "" : undefined, className: split
646
+ ? ROW_CLASS
647
+ : cn(OPTION_CLASS, gutter ? "pr-13" : "pr-8"), onPointerMove: (event) => {
648
+ if (row.disabled)
649
+ return;
650
+ // A highlight resting here (the committed row, the first
651
+ // hit) still takes the hover, so its submenu opens; and
652
+ // back on the parent from its submenu's box, the keys
653
+ // come back too.
654
+ if (highlighted && moved && subIndex < 0 && !subKeys) {
655
+ return;
656
+ }
657
+ if (inGrace(event))
658
+ return;
659
+ clearGrace();
660
+ move(row);
661
+ }, onPointerLeave: highlighted && split ? onParentLeave : undefined, onClick: () => choose(row), children: [split ? (_jsx("span", { "data-slot": "submenu-action", className: ACTION_CLASS, children: cell })) : (cell), split && (_jsxs("span", { "aria-hidden": true, "data-slot": "submenu-trigger", className: "relative flex w-6.5 shrink-0 items-center justify-end self-stretch pr-2 text-muted-foreground group-aria-expanded/option:text-foreground",
662
+ // A split button's other half: it opens the submenu,
663
+ // where the rest of the row picks the row. A touch
664
+ // has no hover to open it any other way.
665
+ onClick: (event) => {
666
+ event.stopPropagation();
667
+ if (row.disabled || (highlighted && submenuOpen))
668
+ return;
669
+ clearGrace();
670
+ move(row);
671
+ }, children: [_jsx("span", { "data-slot": "submenu-divider", className: "absolute inset-y-1.5 left-0 w-px bg-current opacity-20" }), _jsx(ChevronRightIcon, { "aria-hidden": true })] }))] }, row.id));
672
+ });
673
+ return section.label === undefined ? (_jsx(Fragment, { children: options }, section.id)) : (_jsxs("div", { role: "group", "aria-labelledby": labelId, children: [_jsx("div", { id: labelId, className: "px-1.5 py-1 text-muted-foreground text-xs", children: section.label }), options] }, section.id));
674
+ })] }), footer, _jsxs(Popover, { open: submenuOpen, children: [_jsx(PopoverAnchor, { virtualRef: anchor }), _jsxs(PopoverContent, { ref: submenuRef, side: subBelow ? "bottom" : "right", align: "start", sideOffset: 0, className: cn("flex max-h-[min(24rem,var(--radix-popover-content-available-height))] w-60 flex-col overflow-hidden p-0",
675
+ // placeSubmenu measures this width to decide whether it fits
676
+ // beside the menu. Capped to the room beside it, it would
677
+ // always fit, and stay there ever narrower instead of dropping.
678
+ !subBelow && "max-w-none"), style: ownQuery
679
+ ? {
680
+ minHeight: `min(${ownQuery.height}px, var(--radix-popover-content-available-height))`,
681
+ }
682
+ : undefined, onOpenAutoFocus: (event) => event.preventDefault(), onCloseAutoFocus: (event) => event.preventDefault(), onInteractOutside: (event) => {
683
+ const target = event.target;
684
+ if (target instanceof Node && content.current?.contains(target))
685
+ event.preventDefault();
686
+ },
687
+ // The topmost layer gets Escape, so the menu's own keys live here
688
+ // while the submenu is open.
689
+ onEscapeKeyDown: (event) => {
690
+ event.preventDefault();
691
+ if (isEscapeOfComposition(event))
692
+ return;
693
+ if (subIndex >= 0 || inSubBox)
694
+ backToParent();
695
+ else
696
+ setOpen(false);
697
+ }, onMouseDown: (event) => {
698
+ if (!event.target.closest(FOCUSABLE)) {
699
+ event.preventDefault();
700
+ }
701
+ },
702
+ // Hover only: a tap's leave comes with the pointer lifting, not
703
+ // moving off, and would close the submenu under the tap.
704
+ onPointerEnter: (event) => {
705
+ if (event.pointerType === "touch")
706
+ return;
707
+ pointerIn.current = "submenu";
708
+ clearGrace();
709
+ // Typing with the pointer over a submenu filters the submenu.
710
+ if (subBox && !subKeys)
711
+ enterSubBox();
712
+ }, onPointerLeave: (event) => {
713
+ if (event.pointerType === "touch")
714
+ return;
715
+ pointerIn.current = null;
716
+ const to = event.relatedTarget;
717
+ if (to instanceof Node && list.current?.contains(to))
718
+ return;
719
+ if (onSeam(event))
720
+ closeUnlessBack(GRACE_MS);
721
+ else
722
+ move(null);
723
+ }, children: [subBox && parent && (_jsx(FilterBox, { ref: subInputRef, "aria-label": subFilter.label ??
724
+ (submenuLabel ? `Filter ${submenuLabel(parent)}` : "Filter"), expanded: true, "aria-controls": submenuId, "aria-activedescendant": subIndex < 0 ? undefined : subOptionId(subIndex), placeholder: subFilter.placeholder ?? "Filter…", value: subText, onChange: (event) => {
725
+ subMode.current = "hit";
726
+ setSubQuery({
727
+ parent: parent.id,
728
+ text: event.target.value,
729
+ height: ownQuery?.height ??
730
+ submenu.current?.getBoundingClientRect().height ??
731
+ 0,
732
+ });
733
+ }, onKeyDown: (event) => onKeyDown(event, "submenu"), onFocus: () => {
734
+ if (!subKeys)
735
+ enterSubBox();
736
+ } })), _jsxs("div", { id: submenuId, role: "listbox", "aria-label": parent ? submenuLabel?.(parent) : undefined, className: "min-h-0 flex-1 overflow-y-auto p-1", children: [submenuOpen && subRows.length === 0 && (_jsx("div", { role: "presentation", className: "px-2 py-4 text-center text-muted-foreground text-sm", children: empty(subText) })), subRows.map((row, index) => {
737
+ const highlighted = index === subIndex;
738
+ const selected = row.id === selectedSubmenuId;
739
+ return (_jsxs("div", { id: subOptionId(index), role: "option", tabIndex: -1, "aria-selected": highlighted, "aria-disabled": row.disabled || undefined, "data-highlighted": highlighted ? "" : undefined, "data-selected": selected ? "" : undefined, "data-disabled": row.disabled ? "" : undefined, className: cn(OPTION_CLASS, "pr-8"), onPointerMove: () => {
740
+ if (!row.disabled && !highlighted)
741
+ moveIntoSubmenu(row);
742
+ }, onClick: () => choose(row), children: [renderRow(row, { highlighted, selected, query: subText }), selected && (_jsx(Mark, { className: "right-2", children: _jsx(CheckIcon, { "aria-hidden": true }) }))] }, row.id));
743
+ })] })] })] })] })] }));
744
+ }
745
+ //# sourceMappingURL=filter-menu.js.map