@flanksource/clicky-ui 0.2.21 → 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 (227) hide show
  1. package/dist/components/Combobox.cjs +5 -2
  2. package/dist/components/Combobox.cjs.map +1 -1
  3. package/dist/components/Combobox.d.ts.map +1 -1
  4. package/dist/components/Combobox.js +5 -2
  5. package/dist/components/Combobox.js.map +1 -1
  6. package/dist/components/FilterBar.cjs +280 -253
  7. package/dist/components/FilterBar.cjs.map +1 -1
  8. package/dist/components/FilterBar.d.ts.map +1 -1
  9. package/dist/components/FilterBar.js +280 -253
  10. package/dist/components/FilterBar.js.map +1 -1
  11. package/dist/components/JsonSchemaForm.cjs +3 -1
  12. package/dist/components/JsonSchemaForm.cjs.map +1 -1
  13. package/dist/components/JsonSchemaForm.d.ts.map +1 -1
  14. package/dist/components/JsonSchemaForm.js +4 -2
  15. package/dist/components/JsonSchemaForm.js.map +1 -1
  16. package/dist/components/MultiSelect.cjs +29 -32
  17. package/dist/components/MultiSelect.cjs.map +1 -1
  18. package/dist/components/MultiSelect.d.ts.map +1 -1
  19. package/dist/components/MultiSelect.js +30 -33
  20. package/dist/components/MultiSelect.js.map +1 -1
  21. package/dist/components/TimeRange.cjs +29 -37
  22. package/dist/components/TimeRange.cjs.map +1 -1
  23. package/dist/components/TimeRange.d.ts.map +1 -1
  24. package/dist/components/TimeRange.js +30 -38
  25. package/dist/components/TimeRange.js.map +1 -1
  26. package/dist/components/TreePickerField.cjs +6 -3
  27. package/dist/components/TreePickerField.cjs.map +1 -1
  28. package/dist/components/TreePickerField.d.ts.map +1 -1
  29. package/dist/components/TreePickerField.js +6 -3
  30. package/dist/components/TreePickerField.js.map +1 -1
  31. package/dist/components/icon-menu-picker.cjs +9 -3
  32. package/dist/components/icon-menu-picker.cjs.map +1 -1
  33. package/dist/components/icon-menu-picker.d.ts.map +1 -1
  34. package/dist/components/icon-menu-picker.js +9 -3
  35. package/dist/components/icon-menu-picker.js.map +1 -1
  36. package/dist/components/json-schema-form-fields.cjs +80 -2
  37. package/dist/components/json-schema-form-fields.cjs.map +1 -1
  38. package/dist/components/json-schema-form-fields.d.ts +15 -0
  39. package/dist/components/json-schema-form-fields.d.ts.map +1 -1
  40. package/dist/components/json-schema-form-fields.js +81 -3
  41. package/dist/components/json-schema-form-fields.js.map +1 -1
  42. package/dist/components/json-schema-form-refs.cjs +60 -0
  43. package/dist/components/json-schema-form-refs.cjs.map +1 -0
  44. package/dist/components/json-schema-form-refs.d.ts +3 -0
  45. package/dist/components/json-schema-form-refs.d.ts.map +1 -0
  46. package/dist/components/json-schema-form-refs.js +60 -0
  47. package/dist/components/json-schema-form-refs.js.map +1 -0
  48. package/dist/components/json-schema-form-render.cjs +14 -2
  49. package/dist/components/json-schema-form-render.cjs.map +1 -1
  50. package/dist/components/json-schema-form-render.d.ts.map +1 -1
  51. package/dist/components/json-schema-form-render.js +15 -3
  52. package/dist/components/json-schema-form-render.js.map +1 -1
  53. package/dist/components/json-schema-form-resolve.cjs +5 -0
  54. package/dist/components/json-schema-form-resolve.cjs.map +1 -1
  55. package/dist/components/json-schema-form-resolve.d.ts.map +1 -1
  56. package/dist/components/json-schema-form-resolve.js +5 -0
  57. package/dist/components/json-schema-form-resolve.js.map +1 -1
  58. package/dist/components/json-schema-form-template-menu.cjs +83 -0
  59. package/dist/components/json-schema-form-template-menu.cjs.map +1 -0
  60. package/dist/components/json-schema-form-template-menu.d.ts +15 -0
  61. package/dist/components/json-schema-form-template-menu.d.ts.map +1 -0
  62. package/dist/components/json-schema-form-template-menu.js +83 -0
  63. package/dist/components/json-schema-form-template-menu.js.map +1 -0
  64. package/dist/components/json-schema-form-template.cjs +18 -0
  65. package/dist/components/json-schema-form-template.cjs.map +1 -0
  66. package/dist/components/json-schema-form-template.d.ts +13 -0
  67. package/dist/components/json-schema-form-template.d.ts.map +1 -0
  68. package/dist/components/json-schema-form-template.js +18 -0
  69. package/dist/components/json-schema-form-template.js.map +1 -0
  70. package/dist/components/json-schema-form-types.d.ts +5 -1
  71. package/dist/components/json-schema-form-types.d.ts.map +1 -1
  72. package/dist/components.cjs +4 -0
  73. package/dist/components.cjs.map +1 -1
  74. package/dist/components.d.ts +3 -0
  75. package/dist/components.d.ts.map +1 -1
  76. package/dist/components.js +4 -0
  77. package/dist/components.js.map +1 -1
  78. package/dist/data/Clicky.cjs +6 -9
  79. package/dist/data/Clicky.cjs.map +1 -1
  80. package/dist/data/Clicky.d.ts.map +1 -1
  81. package/dist/data/Clicky.js +6 -9
  82. package/dist/data/Clicky.js.map +1 -1
  83. package/dist/data/DataTable.cjs +23 -17
  84. package/dist/data/DataTable.cjs.map +1 -1
  85. package/dist/data/DataTable.d.ts +7 -1
  86. package/dist/data/DataTable.d.ts.map +1 -1
  87. package/dist/data/DataTable.js +24 -18
  88. package/dist/data/DataTable.js.map +1 -1
  89. package/dist/data/Gauge.cjs +37 -4
  90. package/dist/data/Gauge.cjs.map +1 -1
  91. package/dist/data/Gauge.d.ts +6 -1
  92. package/dist/data/Gauge.d.ts.map +1 -1
  93. package/dist/data/Gauge.js +37 -4
  94. package/dist/data/Gauge.js.map +1 -1
  95. package/dist/data/TimeseriesCoreBars.cjs +127 -29
  96. package/dist/data/TimeseriesCoreBars.cjs.map +1 -1
  97. package/dist/data/TimeseriesCoreBars.d.ts +11 -1
  98. package/dist/data/TimeseriesCoreBars.d.ts.map +1 -1
  99. package/dist/data/TimeseriesCoreBars.js +128 -30
  100. package/dist/data/TimeseriesCoreBars.js.map +1 -1
  101. package/dist/data/TimeseriesGauge.cjs +105 -47
  102. package/dist/data/TimeseriesGauge.cjs.map +1 -1
  103. package/dist/data/TimeseriesGauge.d.ts +6 -1
  104. package/dist/data/TimeseriesGauge.d.ts.map +1 -1
  105. package/dist/data/TimeseriesGauge.js +105 -47
  106. package/dist/data/TimeseriesGauge.js.map +1 -1
  107. package/dist/data/WorkloadCard.cjs +371 -0
  108. package/dist/data/WorkloadCard.cjs.map +1 -0
  109. package/dist/data/WorkloadCard.d.ts +24 -0
  110. package/dist/data/WorkloadCard.d.ts.map +1 -0
  111. package/dist/data/WorkloadCard.js +371 -0
  112. package/dist/data/WorkloadCard.js.map +1 -0
  113. package/dist/data/WorkloadCard.model.cjs +51 -0
  114. package/dist/data/WorkloadCard.model.cjs.map +1 -0
  115. package/dist/data/WorkloadCard.model.d.ts +53 -0
  116. package/dist/data/WorkloadCard.model.d.ts.map +1 -0
  117. package/dist/data/WorkloadCard.model.js +51 -0
  118. package/dist/data/WorkloadCard.model.js.map +1 -0
  119. package/dist/data/ai/ChatFab.cjs +3 -1
  120. package/dist/data/ai/ChatFab.cjs.map +1 -1
  121. package/dist/data/ai/ChatFab.d.ts.map +1 -1
  122. package/dist/data/ai/ChatFab.js +3 -1
  123. package/dist/data/ai/ChatFab.js.map +1 -1
  124. package/dist/data/ai/ChatWindow.cjs +25 -7
  125. package/dist/data/ai/ChatWindow.cjs.map +1 -1
  126. package/dist/data/ai/ChatWindow.d.ts +11 -3
  127. package/dist/data/ai/ChatWindow.d.ts.map +1 -1
  128. package/dist/data/ai/ChatWindow.js +26 -8
  129. package/dist/data/ai/ChatWindow.js.map +1 -1
  130. package/dist/data/ai/chat-window-context.cjs +2 -1
  131. package/dist/data/ai/chat-window-context.cjs.map +1 -1
  132. package/dist/data/ai/chat-window-context.d.ts +1 -1
  133. package/dist/data/ai/chat-window-context.d.ts.map +1 -1
  134. package/dist/data/ai/chat-window-context.js +2 -1
  135. package/dist/data/ai/chat-window-context.js.map +1 -1
  136. package/dist/data/ai/context.cjs.map +1 -1
  137. package/dist/data/ai/context.d.ts +3 -0
  138. package/dist/data/ai/context.d.ts.map +1 -1
  139. package/dist/data/ai/context.js.map +1 -1
  140. package/dist/data/chat/Chat.cjs +9 -1
  141. package/dist/data/chat/Chat.cjs.map +1 -1
  142. package/dist/data/chat/Chat.d.ts +4 -2
  143. package/dist/data/chat/Chat.d.ts.map +1 -1
  144. package/dist/data/chat/Chat.js +10 -2
  145. package/dist/data/chat/Chat.js.map +1 -1
  146. package/dist/data/chat/Message.cjs +13 -4
  147. package/dist/data/chat/Message.cjs.map +1 -1
  148. package/dist/data/chat/Message.d.ts +4 -2
  149. package/dist/data/chat/Message.d.ts.map +1 -1
  150. package/dist/data/chat/Message.js +13 -4
  151. package/dist/data/chat/Message.js.map +1 -1
  152. package/dist/data/chat/ToolCall.cjs +9 -4
  153. package/dist/data/chat/ToolCall.cjs.map +1 -1
  154. package/dist/data/chat/ToolCall.d.ts +3 -2
  155. package/dist/data/chat/ToolCall.d.ts.map +1 -1
  156. package/dist/data/chat/ToolCall.js +9 -4
  157. package/dist/data/chat/ToolCall.js.map +1 -1
  158. package/dist/data/chat/index.d.ts +1 -1
  159. package/dist/data/chat/index.d.ts.map +1 -1
  160. package/dist/data/chat/types.cjs.map +1 -1
  161. package/dist/data/chat/types.d.ts +7 -0
  162. package/dist/data/chat/types.d.ts.map +1 -1
  163. package/dist/data/chat/types.js.map +1 -1
  164. package/dist/data/version-info.cjs +2 -2
  165. package/dist/data/version-info.js +2 -2
  166. package/dist/data.cjs +7 -0
  167. package/dist/data.cjs.map +1 -1
  168. package/dist/data.d.ts +5 -3
  169. package/dist/data.d.ts.map +1 -1
  170. package/dist/data.js +8 -1
  171. package/dist/data.js.map +1 -1
  172. package/dist/hooks/use-sort.cjs +19 -10
  173. package/dist/hooks/use-sort.cjs.map +1 -1
  174. package/dist/hooks/use-sort.d.ts +3 -0
  175. package/dist/hooks/use-sort.d.ts.map +1 -1
  176. package/dist/hooks/use-sort.js +19 -10
  177. package/dist/hooks/use-sort.js.map +1 -1
  178. package/dist/index.cjs +11 -0
  179. package/dist/index.cjs.map +1 -1
  180. package/dist/index.js +11 -0
  181. package/dist/index.js.map +1 -1
  182. package/dist/layout/AppShell.cjs +6 -10
  183. package/dist/layout/AppShell.cjs.map +1 -1
  184. package/dist/layout/AppShell.d.ts +2 -0
  185. package/dist/layout/AppShell.d.ts.map +1 -1
  186. package/dist/layout/AppShell.js +6 -10
  187. package/dist/layout/AppShell.js.map +1 -1
  188. package/dist/overlay/DropdownMenu.cjs +50 -38
  189. package/dist/overlay/DropdownMenu.cjs.map +1 -1
  190. package/dist/overlay/DropdownMenu.d.ts +5 -1
  191. package/dist/overlay/DropdownMenu.d.ts.map +1 -1
  192. package/dist/overlay/DropdownMenu.js +50 -38
  193. package/dist/overlay/DropdownMenu.js.map +1 -1
  194. package/dist/overlay/HoverCard.cjs +7 -2
  195. package/dist/overlay/HoverCard.cjs.map +1 -1
  196. package/dist/overlay/HoverCard.d.ts.map +1 -1
  197. package/dist/overlay/HoverCard.js +7 -2
  198. package/dist/overlay/HoverCard.js.map +1 -1
  199. package/dist/overlay/Modal.cjs +20 -17
  200. package/dist/overlay/Modal.cjs.map +1 -1
  201. package/dist/overlay/Modal.d.ts.map +1 -1
  202. package/dist/overlay/Modal.js +21 -18
  203. package/dist/overlay/Modal.js.map +1 -1
  204. package/dist/overlay/Toast.cjs +9 -1
  205. package/dist/overlay/Toast.cjs.map +1 -1
  206. package/dist/overlay/Toast.d.ts.map +1 -1
  207. package/dist/overlay/Toast.js +9 -1
  208. package/dist/overlay/Toast.js.map +1 -1
  209. package/dist/overlay/modalStack.cjs +53 -1
  210. package/dist/overlay/modalStack.cjs.map +1 -1
  211. package/dist/overlay/modalStack.d.ts +12 -0
  212. package/dist/overlay/modalStack.d.ts.map +1 -1
  213. package/dist/overlay/modalStack.js +54 -2
  214. package/dist/overlay/modalStack.js.map +1 -1
  215. package/dist/overlay/zIndex.cjs +20 -0
  216. package/dist/overlay/zIndex.cjs.map +1 -0
  217. package/dist/overlay/zIndex.d.ts +39 -0
  218. package/dist/overlay/zIndex.d.ts.map +1 -0
  219. package/dist/overlay/zIndex.js +20 -0
  220. package/dist/overlay/zIndex.js.map +1 -0
  221. package/dist/rpc/AcceptPicker.cjs +28 -26
  222. package/dist/rpc/AcceptPicker.cjs.map +1 -1
  223. package/dist/rpc/AcceptPicker.d.ts.map +1 -1
  224. package/dist/rpc/AcceptPicker.js +29 -27
  225. package/dist/rpc/AcceptPicker.js.map +1 -1
  226. package/dist/styles.css +1 -1
  227. package/package.json +1 -1
@@ -6,6 +6,7 @@ const reactDom = require("react-dom");
6
6
  const utils = require("../lib/utils.cjs");
7
7
  const Icon = require("../data/Icon.cjs");
8
8
  const jsonSchemaFormSize = require("./json-schema-form-size.cjs");
9
+ const modalStack = require("../overlay/modalStack.cjs");
9
10
  const UiClose = require("../icons/components/UiClose.cjs");
10
11
  const UiChevronDown = require("../icons/components/UiChevronDown.cjs");
11
12
  const UiCheck = require("../icons/components/UiCheck.cjs");
@@ -40,6 +41,7 @@ function Combobox(props) {
40
41
  const inputRef = react.useRef(null);
41
42
  const listRef = react.useRef(null);
42
43
  const [open, setOpen] = react.useState(false);
44
+ const floatingZ = modalStack.useFloatingZIndex();
43
45
  const [menuPos, setMenuPos] = react.useState(null);
44
46
  const [query, setQuery] = react.useState("");
45
47
  const [highlighted, setHighlighted] = react.useState(-1);
@@ -301,9 +303,10 @@ function Combobox(props) {
301
303
  // cap is also bounded by the viewport so the menu can't overflow.
302
304
  minWidth: menuPos.width,
303
305
  maxWidth: menuPos.maxWidth,
304
- maxHeight: menuPos.maxHeight
306
+ maxHeight: menuPos.maxHeight,
307
+ zIndex: floatingZ
305
308
  },
306
- className: "z-[9999] w-max overflow-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg shadow-black/5",
309
+ className: "w-max overflow-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg shadow-black/5",
307
310
  children: [
308
311
  loading && filtered.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-2 py-4 text-center text-sm text-muted-foreground", children: "Loading…" }),
309
312
  !loading && filtered.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-2 py-4 text-center text-sm text-muted-foreground", children: "No results" }),
@@ -1 +1 @@
1
- {"version":3,"file":"Combobox.cjs","sources":["../../src/components/Combobox.tsx"],"sourcesContent":["import {\n Fragment,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type KeyboardEvent,\n type ReactNode,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/utils\";\nimport { Icon, LabelIcon, type LabelIconSpec } from \"../data/Icon\";\nimport { UiChevronDown, UiClose, UiCheck } from \"../icons\";\nimport { inputSizeClass, type FormSize } from \"./json-schema-form-size\";\n\n// Upper bound the open dropdown grows to in order to show full option labels\n// before they truncate. The menu's minimum is always the input's width.\nconst MENU_MAX_WIDTH_PX = 400;\n\n// Matches the menu's `max-h-64` (16rem) cap; also the threshold below which the\n// menu flips above the input instead of opening downward off-screen.\nconst MENU_MAX_HEIGHT_PX = 256;\n\nexport type ComboboxOption = {\n value: string;\n label: string;\n disabled?: boolean;\n /** Leading glyph for the option: a runtime icon name or a rendered node. */\n icon?: LabelIconSpec;\n /**\n * Optional section label. A non-interactive header renders above the first\n * option of each group (options are grouped by contiguous `group` value in\n * the order provided). Options without a `group` render no header.\n */\n group?: string;\n};\n\ntype ComboboxBaseProps = {\n /** Available options, rendered in the order provided. */\n options: ComboboxOption[];\n /** Ghost text when no value is set. */\n placeholder?: string;\n /** Optional label rendered inline inside the control, before the input. */\n label?: ReactNode;\n /**\n * Accessible name for the input. Use when `label` is a non-string node (e.g.\n * an icon) so the control still has a text name for screen readers; defaults\n * to `label` when it is a string.\n */\n ariaLabel?: string;\n /** Disables the input. */\n disabled?: boolean;\n /** When true, the value cannot be cleared (the clear button is hidden). */\n required?: boolean;\n /**\n * Marks the control as invalid: renders a destructive border and sets\n * aria-invalid. Purely presentational — the consumer decides what invalid\n * means (e.g. a value absent from the options in a strict picker).\n */\n invalid?: boolean;\n /**\n * When false, the value is restricted to the provided options — typed text\n * that does not match an option is discarded instead of committed. Defaults\n * to true (freeform entry allowed).\n */\n allowCustomValue?: boolean;\n /** HTML id for the input element. */\n id?: string;\n /**\n * Overrides the input's height/padding/text size with an explicit size token\n * (xs–xl). When unset, the input uses the global density tokens.\n */\n size?: FormSize;\n /** Classes applied to the root wrapper. */\n className?: string;\n /** Shows a loading indicator when options are being fetched. */\n loading?: boolean;\n /**\n * Optional async search invoked (debounced ~250ms) as the user types. The\n * consumer fetches and feeds matching options back via the `options` prop\n * (setting `loading` while in flight). When set, the component renders the\n * provided `options` as-is instead of filtering them client-side — the\n * server has already filtered. When absent, typing filters `options`\n * client-side as before.\n */\n onSearch?: (query: string) => void;\n /** Called when a key is pressed in the input. */\n onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void;\n /** Trailing in-field adornment, rendered left of the clear/chevron controls. */\n suffix?: ReactNode;\n /** Leading in-field adornment, rendered at the left edge of the input. */\n prefix?: ReactNode;\n};\n\nexport type ComboboxSingleProps = ComboboxBaseProps & {\n multiple?: false;\n /** Controlled selected value. */\n value: string;\n /** Called when the selected value changes (from list or freeform input). */\n onChange: (value: string) => void;\n};\n\nexport type ComboboxMultiProps = ComboboxBaseProps & {\n multiple: true;\n /** Controlled selected values. */\n value: string[];\n /** Called with the complete next value array after each toggle. */\n onChange: (value: string[]) => void;\n};\n\nexport type ComboboxProps = ComboboxSingleProps | ComboboxMultiProps;\n\nexport function Combobox(props: ComboboxProps) {\n const {\n options,\n placeholder,\n label,\n ariaLabel: ariaLabelProp,\n disabled,\n required,\n invalid,\n allowCustomValue = true,\n id,\n size,\n className,\n loading,\n onSearch,\n onKeyDown: onKeyDownProp,\n suffix,\n prefix,\n } = props;\n const multiple = props.multiple === true;\n const selectedValues = useMemo<string[]>(\n () => (multiple ? props.value : props.value ? [props.value] : []),\n [multiple, props.value],\n );\n\n const rootRef = useRef<HTMLDivElement>(null);\n const anchorRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n const [open, setOpen] = useState(false);\n // Fixed-position coordinates for the portaled listbox, measured from the\n // input row. `width` is the input width (the menu's minimum); `maxWidth` caps\n // content growth at 400px, further bounded by the viewport. Null until first\n // measured so we never render at (0,0).\n // Either `top` (opening downward) or `bottom` (flipped upward when space below\n // is tight, e.g. a control docked near the viewport bottom) is set, never both.\n const [menuPos, setMenuPos] = useState<{\n top?: number;\n bottom?: number;\n left: number;\n width: number;\n maxWidth: number;\n maxHeight: number;\n } | null>(null);\n // `query` is the type-ahead filter text, kept separate from the committed\n // value. It is empty unless the user is actively typing, so opening the\n // dropdown shows every option rather than only the selected one.\n const [query, setQuery] = useState(\"\");\n const [highlighted, setHighlighted] = useState(-1);\n\n const isSelected = (optValue: string) => selectedValues.includes(optValue);\n\n // Closed-state input text. Single: the selected option's label (or its raw\n // value as a fallback). Multi: a summary mirroring MultiSelect.\n const closedLabel = useMemo(() => {\n if (multiple) {\n const labels = options\n .filter((o) => selectedValues.includes(o.value))\n .map((o) => o.label);\n if (labels.length === 0) return \"\";\n if (labels.length <= 2) return labels.join(\", \");\n return `${labels.length} selected`;\n }\n const single = selectedValues[0] ?? \"\";\n return options.find((o) => o.value === single)?.label ?? single;\n }, [multiple, options, selectedValues]);\n\n // While open the input mirrors the query; while closed it shows the\n // committed selection's label/summary.\n const displayValue = open ? query : closedLabel;\n\n const filtered = useMemo(() => {\n // With onSearch the server already filtered. The matches replace the list,\n // but any already-selected value not in the matches is pinned so the user's\n // selection stays visible and toggleable. Selected values carry no label\n // object here, so synthesize {value, label: value} (the typeahead's value is\n // its own human label).\n if (onSearch) return withSelectedOptions(options, selectedValues);\n const q = query.toLowerCase().trim();\n if (!q) return options;\n return options.filter(\n (o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q),\n );\n }, [onSearch, options, query, selectedValues]);\n\n // Debounced server-side search: fires onSearch ~250ms after the query settles\n // while the menu is open. The empty-query call lets the consumer reset its\n // result set. Only active when onSearch is provided.\n useEffect(() => {\n if (!onSearch || !open) return;\n const handle = setTimeout(() => onSearch(query.trim()), 250);\n return () => clearTimeout(handle);\n }, [onSearch, open, query]);\n\n useEffect(() => {\n setHighlighted(-1);\n }, [filtered.length]);\n\n useEffect(() => {\n if (!open) return;\n // Dismiss when the pointer goes down, OR focus moves, anywhere outside the\n // control and its portaled listbox — so clicking elsewhere in a dialog (the\n // backdrop never intercepts a document-level listener) or tabbing to another\n // field both close the menu. The listbox is portaled out of rootRef, so a\n // click inside it would otherwise read as \"outside\" and close before select.\n const onAway = (e: Event) => {\n const target = e.target as Node;\n if (!rootRef.current?.contains(target) && !listRef.current?.contains(target)) {\n commitAndClose();\n }\n };\n document.addEventListener(\"mousedown\", onAway);\n document.addEventListener(\"focusin\", onAway);\n return () => {\n document.removeEventListener(\"mousedown\", onAway);\n document.removeEventListener(\"focusin\", onAway);\n };\n });\n\n // Position the portaled listbox with fixed coordinates measured from the\n // input row, so it escapes any overflow-hidden / scroll ancestor (e.g. a\n // Modal body) that would otherwise clip it. Mirrors HoverCard's approach.\n useLayoutEffect(() => {\n if (!open) {\n setMenuPos(null);\n return;\n }\n const update = () => {\n const anchor = anchorRef.current;\n if (!anchor) return;\n const rect = anchor.getBoundingClientRect();\n // The menu may grow rightward up to 400px, but not past the viewport edge\n // (leaving an 8px gutter). Never let the cap fall below the input width.\n const viewportCap = window.innerWidth - rect.left - 8;\n const maxWidth = Math.max(rect.width, Math.min(MENU_MAX_WIDTH_PX, viewportCap));\n // Flip upward when there isn't room below for the menu but there is above\n // (the input is docked near the viewport bottom). The available side bounds\n // the menu height so it never runs off-screen.\n const spaceBelow = window.innerHeight - rect.bottom - 8;\n const spaceAbove = rect.top - 8;\n const openUp = spaceBelow < MENU_MAX_HEIGHT_PX && spaceAbove > spaceBelow;\n const maxHeight = Math.min(MENU_MAX_HEIGHT_PX, openUp ? spaceAbove : spaceBelow);\n setMenuPos({\n ...(openUp\n ? { bottom: window.innerHeight - rect.top + 4 }\n : { top: rect.bottom + 4 }),\n left: rect.left,\n width: rect.width,\n maxWidth,\n maxHeight,\n });\n };\n update();\n window.addEventListener(\"scroll\", update, true);\n window.addEventListener(\"resize\", update);\n return () => {\n window.removeEventListener(\"scroll\", update, true);\n window.removeEventListener(\"resize\", update);\n };\n }, [open]);\n\n function emit(next: string[]) {\n if (multiple) {\n props.onChange(next);\n } else {\n props.onChange(next[0] ?? \"\");\n }\n }\n\n function openMenu() {\n setQuery(\"\");\n setHighlighted(-1);\n setOpen(true);\n }\n\n function commitAndClose() {\n // Freeform commit (single, allowCustomValue only): a non-empty query that\n // doesn't match an option becomes the value. An empty query keeps the\n // existing selection. Multi and strict modes never commit freeform text.\n const trimmed = query.trim();\n if (!multiple && allowCustomValue && trimmed && trimmed !== selectedValues[0]) {\n emit([trimmed]);\n }\n setQuery(\"\");\n setOpen(false);\n setHighlighted(-1);\n }\n\n function selectOption(opt: ComboboxOption) {\n if (multiple) {\n const next = isSelected(opt.value)\n ? selectedValues.filter((v) => v !== opt.value)\n : [...selectedValues, opt.value];\n emit(next);\n setQuery(\"\");\n setHighlighted(-1);\n inputRef.current?.focus();\n return;\n }\n emit([opt.value]);\n setQuery(\"\");\n setOpen(false);\n setHighlighted(-1);\n inputRef.current?.focus();\n }\n\n function clear() {\n if (selectedValues.length > 0) emit([]);\n setQuery(\"\");\n setHighlighted(-1);\n inputRef.current?.focus();\n }\n\n function scrollToHighlighted(index: number) {\n const list = listRef.current;\n if (!list) return;\n // Query option rows by role rather than child index — interleaved group\n // header rows are direct children too, so children[index] would be offset.\n const item = list.querySelectorAll<HTMLElement>('[role=\"option\"]')[index];\n item?.scrollIntoView({ block: \"nearest\" });\n }\n\n function onKeyDown(e: KeyboardEvent<HTMLInputElement>) {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n if (!open) {\n openMenu();\n return;\n }\n const next = highlighted < filtered.length - 1 ? highlighted + 1 : 0;\n setHighlighted(next);\n scrollToHighlighted(next);\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n if (!open) return;\n const next = highlighted > 0 ? highlighted - 1 : filtered.length - 1;\n setHighlighted(next);\n scrollToHighlighted(next);\n } else if (e.key === \"Enter\") {\n if (open && highlighted >= 0 && filtered[highlighted] && !filtered[highlighted].disabled) {\n e.preventDefault();\n selectOption(filtered[highlighted]);\n return;\n }\n commitAndClose();\n onKeyDownProp?.(e);\n } else if (e.key === \"Escape\") {\n if (open) {\n e.preventDefault();\n setQuery(\"\");\n setOpen(false);\n setHighlighted(-1);\n }\n } else {\n onKeyDownProp?.(e);\n }\n }\n\n const listId = id ? `${id}-listbox` : undefined;\n const ariaLabel = ariaLabelProp ?? (typeof label === \"string\" ? label : undefined);\n const showClear = !required && !loading && !disabled && selectedValues.length > 0;\n\n return (\n <div ref={rootRef} className={cn(\"relative\", className)}>\n <div ref={anchorRef} data-jsf-control className=\"relative flex items-center\">\n {label != null && (\n <span className=\"pointer-events-none absolute left-2 z-10 whitespace-nowrap font-medium uppercase tracking-wide text-muted-foreground text-[10px]\">\n {label}\n </span>\n )}\n <input\n ref={inputRef}\n id={id}\n type=\"text\"\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={listId}\n aria-activedescendant={highlighted >= 0 ? `${listId}-${highlighted}` : undefined}\n aria-label={ariaLabel}\n aria-invalid={invalid || undefined}\n autoComplete=\"off\"\n disabled={disabled}\n placeholder={placeholder}\n value={displayValue}\n onChange={(e) => {\n setQuery(e.target.value);\n if (!open) setOpen(true);\n }}\n onFocus={openMenu}\n // Reopen on click too: focus only fires on the first focus, so a click\n // on an already-focused-but-closed input (e.g. after selecting, or\n // after Escape) would otherwise do nothing.\n onClick={() => {\n if (!open) openMenu();\n }}\n onKeyDown={onKeyDown}\n className={cn(\n \"w-full rounded-md border border-input bg-background text-foreground\",\n size ? inputSizeClass[size] : \"h-control-h px-control-px text-sm\",\n prefix && \"pl-8\",\n suffix ? (showClear ? \"pr-[5.5rem]\" : \"pr-14\") : showClear ? \"pr-14\" : \"pr-8\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n invalid && \"border-destructive focus-visible:ring-destructive\",\n )}\n style={label != null ? labelPadding(label) : undefined}\n />\n {prefix && (\n <div className=\"absolute inset-y-0 left-1.5 flex items-center\">{prefix}</div>\n )}\n {suffix && (\n <div className={cn(\"absolute flex h-full items-center\", showClear ? \"right-[3.75rem]\" : \"right-7\")}>\n {suffix}\n </div>\n )}\n {showClear && (\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label=\"Clear\"\n onClick={clear}\n className=\"absolute right-7 flex h-full items-center px-1 text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={UiClose} className=\"text-xs\" />\n </button>\n )}\n {loading ? (\n <span className=\"pointer-events-none absolute right-2 top-1/2 -translate-y-1/2\">\n <svg className=\"h-3.5 w-3.5 animate-spin text-muted-foreground\" viewBox=\"0 0 16 16\" fill=\"none\">\n <circle cx=\"8\" cy=\"8\" r=\"6.5\" stroke=\"currentColor\" strokeWidth=\"2\" opacity=\"0.25\" />\n <path d=\"M14.5 8a6.5 6.5 0 0 0-6.5-6.5\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n </svg>\n </span>\n ) : (\n <button\n type=\"button\"\n tabIndex={-1}\n disabled={disabled}\n aria-label=\"Toggle options\"\n onClick={() => {\n if (open) {\n commitAndClose();\n } else {\n openMenu();\n inputRef.current?.focus();\n }\n }}\n className=\"pointer-events-auto absolute right-0 flex h-full items-center px-2 text-muted-foreground\"\n >\n <Icon icon={UiChevronDown} className=\"text-xs\" />\n </button>\n )}\n </div>\n {open && menuPos && typeof document !== \"undefined\" &&\n createPortal(\n <div\n id={listId}\n ref={listRef}\n role=\"listbox\"\n aria-multiselectable={multiple || undefined}\n style={{\n position: \"fixed\",\n ...(menuPos.top != null ? { top: menuPos.top } : { bottom: menuPos.bottom }),\n left: menuPos.left,\n // Grow to fit the widest option, but never narrower than the input\n // nor wider than 400px (beyond which option labels truncate). The\n // cap is also bounded by the viewport so the menu can't overflow.\n minWidth: menuPos.width,\n maxWidth: menuPos.maxWidth,\n maxHeight: menuPos.maxHeight,\n }}\n className=\"z-[9999] w-max overflow-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg shadow-black/5\"\n >\n {loading && filtered.length === 0 && (\n <div className=\"px-2 py-4 text-center text-sm text-muted-foreground\">Loading…</div>\n )}\n {!loading && filtered.length === 0 && (\n <div className=\"px-2 py-4 text-center text-sm text-muted-foreground\">No results</div>\n )}\n {filtered.map((opt, i) => {\n const selected = isSelected(opt.value);\n // A header renders above the first option of each group. Derived\n // from the surviving (filtered) options, so an empty group emits no\n // header. The option keeps `i` (its index in `filtered`) for\n // id/highlight/select — headers are role=\"presentation\", never\n // counted in the highlight/scroll index.\n const showHeader = opt.group != null && opt.group !== filtered[i - 1]?.group;\n return (\n <Fragment key={opt.value}>\n {showHeader && (\n <div\n role=\"presentation\"\n className=\"select-none px-2 pb-1 pt-2 text-[10px] font-medium uppercase tracking-wide text-muted-foreground\"\n >\n {opt.group}\n </div>\n )}\n <div\n id={listId ? `${listId}-${i}` : undefined}\n role=\"option\"\n aria-selected={selected}\n aria-disabled={opt.disabled}\n onMouseDown={(e) => {\n e.preventDefault();\n if (!opt.disabled) selectOption(opt);\n }}\n onMouseEnter={() => setHighlighted(i)}\n className={cn(\n \"flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm\",\n i === highlighted && \"bg-accent\",\n selected && \"font-medium\",\n opt.disabled && \"cursor-not-allowed opacity-50\",\n )}\n >\n <Icon\n icon={UiCheck}\n className={cn(\"text-xs shrink-0\", selected ? \"opacity-100\" : \"opacity-0\")}\n />\n <LabelIcon icon={opt.icon} className=\"text-sm text-muted-foreground\" />\n <span className=\"min-w-0 truncate\">{opt.label}</span>\n </div>\n </Fragment>\n );\n })}\n </div>,\n document.body,\n )}\n </div>\n );\n}\n\n// Reserve room on the left of the input for the inline label so typed text\n// does not overlap it. Node labels (an icon from comboboxLabelProps) are\n// compact, so reserve a fixed icon-sized inset; text labels scale with their\n// character count at roughly 0.6rem per character plus padding.\nfunction labelPadding(label: ReactNode) {\n if (typeof label !== \"string\") return { paddingLeft: \"1.75rem\" };\n return { paddingLeft: `${Math.min(label.length * 0.62 + 0.75, 9)}rem` };\n}\n\n// withSelectedOptions returns the option list plus any selected value that isn't\n// already present, synthesized as {value, label: value}. Used by the server\n// (onSearch) typeahead so an already-selected value missing from the current\n// matches still renders as a checked row. Selected-but-absent values come first\n// so they don't get scrolled past by a long match list.\nfunction withSelectedOptions(\n options: ComboboxOption[],\n selectedValues: string[],\n): ComboboxOption[] {\n const present = new Set(options.map((o) => o.value));\n const pinned = selectedValues\n .filter((v) => !present.has(v))\n .map((v) => ({ value: v, label: v }));\n return pinned.length === 0 ? options : [...pinned, ...options];\n}\n"],"names":["useMemo","useRef","useState","useEffect","useLayoutEffect","jsxs","cn","jsx","inputSizeClass","Icon","UiClose","UiChevronDown","createPortal","Fragment","UiCheck","LabelIcon"],"mappings":"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"}
1
+ {"version":3,"file":"Combobox.cjs","sources":["../../src/components/Combobox.tsx"],"sourcesContent":["import {\n Fragment,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type KeyboardEvent,\n type ReactNode,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/utils\";\nimport { Icon, LabelIcon, type LabelIconSpec } from \"../data/Icon\";\nimport { UiChevronDown, UiClose, UiCheck } from \"../icons\";\nimport { inputSizeClass, type FormSize } from \"./json-schema-form-size\";\nimport { useFloatingZIndex } from \"../overlay/modalStack\";\n\n// Upper bound the open dropdown grows to in order to show full option labels\n// before they truncate. The menu's minimum is always the input's width.\nconst MENU_MAX_WIDTH_PX = 400;\n\n// Matches the menu's `max-h-64` (16rem) cap; also the threshold below which the\n// menu flips above the input instead of opening downward off-screen.\nconst MENU_MAX_HEIGHT_PX = 256;\n\nexport type ComboboxOption = {\n value: string;\n label: string;\n disabled?: boolean;\n /** Leading glyph for the option: a runtime icon name or a rendered node. */\n icon?: LabelIconSpec;\n /**\n * Optional section label. A non-interactive header renders above the first\n * option of each group (options are grouped by contiguous `group` value in\n * the order provided). Options without a `group` render no header.\n */\n group?: string;\n};\n\ntype ComboboxBaseProps = {\n /** Available options, rendered in the order provided. */\n options: ComboboxOption[];\n /** Ghost text when no value is set. */\n placeholder?: string;\n /** Optional label rendered inline inside the control, before the input. */\n label?: ReactNode;\n /**\n * Accessible name for the input. Use when `label` is a non-string node (e.g.\n * an icon) so the control still has a text name for screen readers; defaults\n * to `label` when it is a string.\n */\n ariaLabel?: string;\n /** Disables the input. */\n disabled?: boolean;\n /** When true, the value cannot be cleared (the clear button is hidden). */\n required?: boolean;\n /**\n * Marks the control as invalid: renders a destructive border and sets\n * aria-invalid. Purely presentational — the consumer decides what invalid\n * means (e.g. a value absent from the options in a strict picker).\n */\n invalid?: boolean;\n /**\n * When false, the value is restricted to the provided options — typed text\n * that does not match an option is discarded instead of committed. Defaults\n * to true (freeform entry allowed).\n */\n allowCustomValue?: boolean;\n /** HTML id for the input element. */\n id?: string;\n /**\n * Overrides the input's height/padding/text size with an explicit size token\n * (xs–xl). When unset, the input uses the global density tokens.\n */\n size?: FormSize;\n /** Classes applied to the root wrapper. */\n className?: string;\n /** Shows a loading indicator when options are being fetched. */\n loading?: boolean;\n /**\n * Optional async search invoked (debounced ~250ms) as the user types. The\n * consumer fetches and feeds matching options back via the `options` prop\n * (setting `loading` while in flight). When set, the component renders the\n * provided `options` as-is instead of filtering them client-side — the\n * server has already filtered. When absent, typing filters `options`\n * client-side as before.\n */\n onSearch?: (query: string) => void;\n /** Called when a key is pressed in the input. */\n onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void;\n /** Trailing in-field adornment, rendered left of the clear/chevron controls. */\n suffix?: ReactNode;\n /** Leading in-field adornment, rendered at the left edge of the input. */\n prefix?: ReactNode;\n};\n\nexport type ComboboxSingleProps = ComboboxBaseProps & {\n multiple?: false;\n /** Controlled selected value. */\n value: string;\n /** Called when the selected value changes (from list or freeform input). */\n onChange: (value: string) => void;\n};\n\nexport type ComboboxMultiProps = ComboboxBaseProps & {\n multiple: true;\n /** Controlled selected values. */\n value: string[];\n /** Called with the complete next value array after each toggle. */\n onChange: (value: string[]) => void;\n};\n\nexport type ComboboxProps = ComboboxSingleProps | ComboboxMultiProps;\n\nexport function Combobox(props: ComboboxProps) {\n const {\n options,\n placeholder,\n label,\n ariaLabel: ariaLabelProp,\n disabled,\n required,\n invalid,\n allowCustomValue = true,\n id,\n size,\n className,\n loading,\n onSearch,\n onKeyDown: onKeyDownProp,\n suffix,\n prefix,\n } = props;\n const multiple = props.multiple === true;\n const selectedValues = useMemo<string[]>(\n () => (multiple ? props.value : props.value ? [props.value] : []),\n [multiple, props.value],\n );\n\n const rootRef = useRef<HTMLDivElement>(null);\n const anchorRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n const [open, setOpen] = useState(false);\n const floatingZ = useFloatingZIndex();\n // Fixed-position coordinates for the portaled listbox, measured from the\n // input row. `width` is the input width (the menu's minimum); `maxWidth` caps\n // content growth at 400px, further bounded by the viewport. Null until first\n // measured so we never render at (0,0).\n // Either `top` (opening downward) or `bottom` (flipped upward when space below\n // is tight, e.g. a control docked near the viewport bottom) is set, never both.\n const [menuPos, setMenuPos] = useState<{\n top?: number;\n bottom?: number;\n left: number;\n width: number;\n maxWidth: number;\n maxHeight: number;\n } | null>(null);\n // `query` is the type-ahead filter text, kept separate from the committed\n // value. It is empty unless the user is actively typing, so opening the\n // dropdown shows every option rather than only the selected one.\n const [query, setQuery] = useState(\"\");\n const [highlighted, setHighlighted] = useState(-1);\n\n const isSelected = (optValue: string) => selectedValues.includes(optValue);\n\n // Closed-state input text. Single: the selected option's label (or its raw\n // value as a fallback). Multi: a summary mirroring MultiSelect.\n const closedLabel = useMemo(() => {\n if (multiple) {\n const labels = options\n .filter((o) => selectedValues.includes(o.value))\n .map((o) => o.label);\n if (labels.length === 0) return \"\";\n if (labels.length <= 2) return labels.join(\", \");\n return `${labels.length} selected`;\n }\n const single = selectedValues[0] ?? \"\";\n return options.find((o) => o.value === single)?.label ?? single;\n }, [multiple, options, selectedValues]);\n\n // While open the input mirrors the query; while closed it shows the\n // committed selection's label/summary.\n const displayValue = open ? query : closedLabel;\n\n const filtered = useMemo(() => {\n // With onSearch the server already filtered. The matches replace the list,\n // but any already-selected value not in the matches is pinned so the user's\n // selection stays visible and toggleable. Selected values carry no label\n // object here, so synthesize {value, label: value} (the typeahead's value is\n // its own human label).\n if (onSearch) return withSelectedOptions(options, selectedValues);\n const q = query.toLowerCase().trim();\n if (!q) return options;\n return options.filter(\n (o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q),\n );\n }, [onSearch, options, query, selectedValues]);\n\n // Debounced server-side search: fires onSearch ~250ms after the query settles\n // while the menu is open. The empty-query call lets the consumer reset its\n // result set. Only active when onSearch is provided.\n useEffect(() => {\n if (!onSearch || !open) return;\n const handle = setTimeout(() => onSearch(query.trim()), 250);\n return () => clearTimeout(handle);\n }, [onSearch, open, query]);\n\n useEffect(() => {\n setHighlighted(-1);\n }, [filtered.length]);\n\n useEffect(() => {\n if (!open) return;\n // Dismiss when the pointer goes down, OR focus moves, anywhere outside the\n // control and its portaled listbox — so clicking elsewhere in a dialog (the\n // backdrop never intercepts a document-level listener) or tabbing to another\n // field both close the menu. The listbox is portaled out of rootRef, so a\n // click inside it would otherwise read as \"outside\" and close before select.\n const onAway = (e: Event) => {\n const target = e.target as Node;\n if (!rootRef.current?.contains(target) && !listRef.current?.contains(target)) {\n commitAndClose();\n }\n };\n document.addEventListener(\"mousedown\", onAway);\n document.addEventListener(\"focusin\", onAway);\n return () => {\n document.removeEventListener(\"mousedown\", onAway);\n document.removeEventListener(\"focusin\", onAway);\n };\n });\n\n // Position the portaled listbox with fixed coordinates measured from the\n // input row, so it escapes any overflow-hidden / scroll ancestor (e.g. a\n // Modal body) that would otherwise clip it. Mirrors HoverCard's approach.\n useLayoutEffect(() => {\n if (!open) {\n setMenuPos(null);\n return;\n }\n const update = () => {\n const anchor = anchorRef.current;\n if (!anchor) return;\n const rect = anchor.getBoundingClientRect();\n // The menu may grow rightward up to 400px, but not past the viewport edge\n // (leaving an 8px gutter). Never let the cap fall below the input width.\n const viewportCap = window.innerWidth - rect.left - 8;\n const maxWidth = Math.max(rect.width, Math.min(MENU_MAX_WIDTH_PX, viewportCap));\n // Flip upward when there isn't room below for the menu but there is above\n // (the input is docked near the viewport bottom). The available side bounds\n // the menu height so it never runs off-screen.\n const spaceBelow = window.innerHeight - rect.bottom - 8;\n const spaceAbove = rect.top - 8;\n const openUp = spaceBelow < MENU_MAX_HEIGHT_PX && spaceAbove > spaceBelow;\n const maxHeight = Math.min(MENU_MAX_HEIGHT_PX, openUp ? spaceAbove : spaceBelow);\n setMenuPos({\n ...(openUp\n ? { bottom: window.innerHeight - rect.top + 4 }\n : { top: rect.bottom + 4 }),\n left: rect.left,\n width: rect.width,\n maxWidth,\n maxHeight,\n });\n };\n update();\n window.addEventListener(\"scroll\", update, true);\n window.addEventListener(\"resize\", update);\n return () => {\n window.removeEventListener(\"scroll\", update, true);\n window.removeEventListener(\"resize\", update);\n };\n }, [open]);\n\n function emit(next: string[]) {\n if (multiple) {\n props.onChange(next);\n } else {\n props.onChange(next[0] ?? \"\");\n }\n }\n\n function openMenu() {\n setQuery(\"\");\n setHighlighted(-1);\n setOpen(true);\n }\n\n function commitAndClose() {\n // Freeform commit (single, allowCustomValue only): a non-empty query that\n // doesn't match an option becomes the value. An empty query keeps the\n // existing selection. Multi and strict modes never commit freeform text.\n const trimmed = query.trim();\n if (!multiple && allowCustomValue && trimmed && trimmed !== selectedValues[0]) {\n emit([trimmed]);\n }\n setQuery(\"\");\n setOpen(false);\n setHighlighted(-1);\n }\n\n function selectOption(opt: ComboboxOption) {\n if (multiple) {\n const next = isSelected(opt.value)\n ? selectedValues.filter((v) => v !== opt.value)\n : [...selectedValues, opt.value];\n emit(next);\n setQuery(\"\");\n setHighlighted(-1);\n inputRef.current?.focus();\n return;\n }\n emit([opt.value]);\n setQuery(\"\");\n setOpen(false);\n setHighlighted(-1);\n inputRef.current?.focus();\n }\n\n function clear() {\n if (selectedValues.length > 0) emit([]);\n setQuery(\"\");\n setHighlighted(-1);\n inputRef.current?.focus();\n }\n\n function scrollToHighlighted(index: number) {\n const list = listRef.current;\n if (!list) return;\n // Query option rows by role rather than child index — interleaved group\n // header rows are direct children too, so children[index] would be offset.\n const item = list.querySelectorAll<HTMLElement>('[role=\"option\"]')[index];\n item?.scrollIntoView({ block: \"nearest\" });\n }\n\n function onKeyDown(e: KeyboardEvent<HTMLInputElement>) {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n if (!open) {\n openMenu();\n return;\n }\n const next = highlighted < filtered.length - 1 ? highlighted + 1 : 0;\n setHighlighted(next);\n scrollToHighlighted(next);\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n if (!open) return;\n const next = highlighted > 0 ? highlighted - 1 : filtered.length - 1;\n setHighlighted(next);\n scrollToHighlighted(next);\n } else if (e.key === \"Enter\") {\n if (open && highlighted >= 0 && filtered[highlighted] && !filtered[highlighted].disabled) {\n e.preventDefault();\n selectOption(filtered[highlighted]);\n return;\n }\n commitAndClose();\n onKeyDownProp?.(e);\n } else if (e.key === \"Escape\") {\n if (open) {\n e.preventDefault();\n setQuery(\"\");\n setOpen(false);\n setHighlighted(-1);\n }\n } else {\n onKeyDownProp?.(e);\n }\n }\n\n const listId = id ? `${id}-listbox` : undefined;\n const ariaLabel = ariaLabelProp ?? (typeof label === \"string\" ? label : undefined);\n const showClear = !required && !loading && !disabled && selectedValues.length > 0;\n\n return (\n <div ref={rootRef} className={cn(\"relative\", className)}>\n <div ref={anchorRef} data-jsf-control className=\"relative flex items-center\">\n {label != null && (\n <span className=\"pointer-events-none absolute left-2 z-10 whitespace-nowrap font-medium uppercase tracking-wide text-muted-foreground text-[10px]\">\n {label}\n </span>\n )}\n <input\n ref={inputRef}\n id={id}\n type=\"text\"\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={listId}\n aria-activedescendant={highlighted >= 0 ? `${listId}-${highlighted}` : undefined}\n aria-label={ariaLabel}\n aria-invalid={invalid || undefined}\n autoComplete=\"off\"\n disabled={disabled}\n placeholder={placeholder}\n value={displayValue}\n onChange={(e) => {\n setQuery(e.target.value);\n if (!open) setOpen(true);\n }}\n onFocus={openMenu}\n // Reopen on click too: focus only fires on the first focus, so a click\n // on an already-focused-but-closed input (e.g. after selecting, or\n // after Escape) would otherwise do nothing.\n onClick={() => {\n if (!open) openMenu();\n }}\n onKeyDown={onKeyDown}\n className={cn(\n \"w-full rounded-md border border-input bg-background text-foreground\",\n size ? inputSizeClass[size] : \"h-control-h px-control-px text-sm\",\n prefix && \"pl-8\",\n suffix ? (showClear ? \"pr-[5.5rem]\" : \"pr-14\") : showClear ? \"pr-14\" : \"pr-8\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n invalid && \"border-destructive focus-visible:ring-destructive\",\n )}\n style={label != null ? labelPadding(label) : undefined}\n />\n {prefix && (\n <div className=\"absolute inset-y-0 left-1.5 flex items-center\">{prefix}</div>\n )}\n {suffix && (\n <div className={cn(\"absolute flex h-full items-center\", showClear ? \"right-[3.75rem]\" : \"right-7\")}>\n {suffix}\n </div>\n )}\n {showClear && (\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label=\"Clear\"\n onClick={clear}\n className=\"absolute right-7 flex h-full items-center px-1 text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={UiClose} className=\"text-xs\" />\n </button>\n )}\n {loading ? (\n <span className=\"pointer-events-none absolute right-2 top-1/2 -translate-y-1/2\">\n <svg className=\"h-3.5 w-3.5 animate-spin text-muted-foreground\" viewBox=\"0 0 16 16\" fill=\"none\">\n <circle cx=\"8\" cy=\"8\" r=\"6.5\" stroke=\"currentColor\" strokeWidth=\"2\" opacity=\"0.25\" />\n <path d=\"M14.5 8a6.5 6.5 0 0 0-6.5-6.5\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n </svg>\n </span>\n ) : (\n <button\n type=\"button\"\n tabIndex={-1}\n disabled={disabled}\n aria-label=\"Toggle options\"\n onClick={() => {\n if (open) {\n commitAndClose();\n } else {\n openMenu();\n inputRef.current?.focus();\n }\n }}\n className=\"pointer-events-auto absolute right-0 flex h-full items-center px-2 text-muted-foreground\"\n >\n <Icon icon={UiChevronDown} className=\"text-xs\" />\n </button>\n )}\n </div>\n {open && menuPos && typeof document !== \"undefined\" &&\n createPortal(\n <div\n id={listId}\n ref={listRef}\n role=\"listbox\"\n aria-multiselectable={multiple || undefined}\n style={{\n position: \"fixed\",\n ...(menuPos.top != null ? { top: menuPos.top } : { bottom: menuPos.bottom }),\n left: menuPos.left,\n // Grow to fit the widest option, but never narrower than the input\n // nor wider than 400px (beyond which option labels truncate). The\n // cap is also bounded by the viewport so the menu can't overflow.\n minWidth: menuPos.width,\n maxWidth: menuPos.maxWidth,\n maxHeight: menuPos.maxHeight,\n zIndex: floatingZ,\n }}\n className=\"w-max overflow-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg shadow-black/5\"\n >\n {loading && filtered.length === 0 && (\n <div className=\"px-2 py-4 text-center text-sm text-muted-foreground\">Loading…</div>\n )}\n {!loading && filtered.length === 0 && (\n <div className=\"px-2 py-4 text-center text-sm text-muted-foreground\">No results</div>\n )}\n {filtered.map((opt, i) => {\n const selected = isSelected(opt.value);\n // A header renders above the first option of each group. Derived\n // from the surviving (filtered) options, so an empty group emits no\n // header. The option keeps `i` (its index in `filtered`) for\n // id/highlight/select — headers are role=\"presentation\", never\n // counted in the highlight/scroll index.\n const showHeader = opt.group != null && opt.group !== filtered[i - 1]?.group;\n return (\n <Fragment key={opt.value}>\n {showHeader && (\n <div\n role=\"presentation\"\n className=\"select-none px-2 pb-1 pt-2 text-[10px] font-medium uppercase tracking-wide text-muted-foreground\"\n >\n {opt.group}\n </div>\n )}\n <div\n id={listId ? `${listId}-${i}` : undefined}\n role=\"option\"\n aria-selected={selected}\n aria-disabled={opt.disabled}\n onMouseDown={(e) => {\n e.preventDefault();\n if (!opt.disabled) selectOption(opt);\n }}\n onMouseEnter={() => setHighlighted(i)}\n className={cn(\n \"flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm\",\n i === highlighted && \"bg-accent\",\n selected && \"font-medium\",\n opt.disabled && \"cursor-not-allowed opacity-50\",\n )}\n >\n <Icon\n icon={UiCheck}\n className={cn(\"text-xs shrink-0\", selected ? \"opacity-100\" : \"opacity-0\")}\n />\n <LabelIcon icon={opt.icon} className=\"text-sm text-muted-foreground\" />\n <span className=\"min-w-0 truncate\">{opt.label}</span>\n </div>\n </Fragment>\n );\n })}\n </div>,\n document.body,\n )}\n </div>\n );\n}\n\n// Reserve room on the left of the input for the inline label so typed text\n// does not overlap it. Node labels (an icon from comboboxLabelProps) are\n// compact, so reserve a fixed icon-sized inset; text labels scale with their\n// character count at roughly 0.6rem per character plus padding.\nfunction labelPadding(label: ReactNode) {\n if (typeof label !== \"string\") return { paddingLeft: \"1.75rem\" };\n return { paddingLeft: `${Math.min(label.length * 0.62 + 0.75, 9)}rem` };\n}\n\n// withSelectedOptions returns the option list plus any selected value that isn't\n// already present, synthesized as {value, label: value}. Used by the server\n// (onSearch) typeahead so an already-selected value missing from the current\n// matches still renders as a checked row. Selected-but-absent values come first\n// so they don't get scrolled past by a long match list.\nfunction withSelectedOptions(\n options: ComboboxOption[],\n selectedValues: string[],\n): ComboboxOption[] {\n const present = new Set(options.map((o) => o.value));\n const pinned = selectedValues\n .filter((v) => !present.has(v))\n .map((v) => ({ value: v, label: v }));\n return pinned.length === 0 ? options : [...pinned, ...options];\n}\n"],"names":["useMemo","useRef","useState","useFloatingZIndex","useEffect","useLayoutEffect","jsxs","cn","jsx","inputSizeClass","Icon","UiClose","UiChevronDown","createPortal","Fragment","UiCheck","LabelIcon"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"Combobox.d.ts","sourceRoot":"","sources":["../../src/components/Combobox.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,aAAa,EAClB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAEnE,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAUxE,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,KAAK,iBAAiB,GAAG;IACvB,yDAAyD;IACzD,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,qCAAqC;IACrC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,iDAAiD;IACjD,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IACzD,gFAAgF;IAChF,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,iBAAiB,GAAG;IACpD,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,iBAAiB,GAAG;IACnD,QAAQ,EAAE,IAAI,CAAC;IACf,kCAAkC;IAClC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,mEAAmE;IACnE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,mBAAmB,GAAG,kBAAkB,CAAC;AAErE,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,2CA6a5C"}
1
+ {"version":3,"file":"Combobox.d.ts","sourceRoot":"","sources":["../../src/components/Combobox.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,aAAa,EAClB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAEnE,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAWxE,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,KAAK,iBAAiB,GAAG;IACvB,yDAAyD;IACzD,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,qCAAqC;IACrC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,iDAAiD;IACjD,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IACzD,gFAAgF;IAChF,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,iBAAiB,GAAG;IACpD,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,iBAAiB,GAAG;IACnD,QAAQ,EAAE,IAAI,CAAC;IACf,kCAAkC;IAClC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,mEAAmE;IACnE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,mBAAmB,GAAG,kBAAkB,CAAC;AAErE,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,2CA+a5C"}
@@ -4,6 +4,7 @@ import { createPortal } from "react-dom";
4
4
  import { cn } from "../lib/utils.js";
5
5
  import { Icon, LabelIcon } from "../data/Icon.js";
6
6
  import { inputSizeClass } from "./json-schema-form-size.js";
7
+ import { useFloatingZIndex } from "../overlay/modalStack.js";
7
8
  import { UiClose } from "../icons/components/UiClose.js";
8
9
  import { UiChevronDown } from "../icons/components/UiChevronDown.js";
9
10
  import { UiCheck } from "../icons/components/UiCheck.js";
@@ -38,6 +39,7 @@ function Combobox(props) {
38
39
  const inputRef = useRef(null);
39
40
  const listRef = useRef(null);
40
41
  const [open, setOpen] = useState(false);
42
+ const floatingZ = useFloatingZIndex();
41
43
  const [menuPos, setMenuPos] = useState(null);
42
44
  const [query, setQuery] = useState("");
43
45
  const [highlighted, setHighlighted] = useState(-1);
@@ -299,9 +301,10 @@ function Combobox(props) {
299
301
  // cap is also bounded by the viewport so the menu can't overflow.
300
302
  minWidth: menuPos.width,
301
303
  maxWidth: menuPos.maxWidth,
302
- maxHeight: menuPos.maxHeight
304
+ maxHeight: menuPos.maxHeight,
305
+ zIndex: floatingZ
303
306
  },
304
- className: "z-[9999] w-max overflow-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg shadow-black/5",
307
+ className: "w-max overflow-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg shadow-black/5",
305
308
  children: [
306
309
  loading && filtered.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-2 py-4 text-center text-sm text-muted-foreground", children: "Loading…" }),
307
310
  !loading && filtered.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-2 py-4 text-center text-sm text-muted-foreground", children: "No results" }),
@@ -1 +1 @@
1
- {"version":3,"file":"Combobox.js","sources":["../../src/components/Combobox.tsx"],"sourcesContent":["import {\n Fragment,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type KeyboardEvent,\n type ReactNode,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/utils\";\nimport { Icon, LabelIcon, type LabelIconSpec } from \"../data/Icon\";\nimport { UiChevronDown, UiClose, UiCheck } from \"../icons\";\nimport { inputSizeClass, type FormSize } from \"./json-schema-form-size\";\n\n// Upper bound the open dropdown grows to in order to show full option labels\n// before they truncate. The menu's minimum is always the input's width.\nconst MENU_MAX_WIDTH_PX = 400;\n\n// Matches the menu's `max-h-64` (16rem) cap; also the threshold below which the\n// menu flips above the input instead of opening downward off-screen.\nconst MENU_MAX_HEIGHT_PX = 256;\n\nexport type ComboboxOption = {\n value: string;\n label: string;\n disabled?: boolean;\n /** Leading glyph for the option: a runtime icon name or a rendered node. */\n icon?: LabelIconSpec;\n /**\n * Optional section label. A non-interactive header renders above the first\n * option of each group (options are grouped by contiguous `group` value in\n * the order provided). Options without a `group` render no header.\n */\n group?: string;\n};\n\ntype ComboboxBaseProps = {\n /** Available options, rendered in the order provided. */\n options: ComboboxOption[];\n /** Ghost text when no value is set. */\n placeholder?: string;\n /** Optional label rendered inline inside the control, before the input. */\n label?: ReactNode;\n /**\n * Accessible name for the input. Use when `label` is a non-string node (e.g.\n * an icon) so the control still has a text name for screen readers; defaults\n * to `label` when it is a string.\n */\n ariaLabel?: string;\n /** Disables the input. */\n disabled?: boolean;\n /** When true, the value cannot be cleared (the clear button is hidden). */\n required?: boolean;\n /**\n * Marks the control as invalid: renders a destructive border and sets\n * aria-invalid. Purely presentational — the consumer decides what invalid\n * means (e.g. a value absent from the options in a strict picker).\n */\n invalid?: boolean;\n /**\n * When false, the value is restricted to the provided options — typed text\n * that does not match an option is discarded instead of committed. Defaults\n * to true (freeform entry allowed).\n */\n allowCustomValue?: boolean;\n /** HTML id for the input element. */\n id?: string;\n /**\n * Overrides the input's height/padding/text size with an explicit size token\n * (xs–xl). When unset, the input uses the global density tokens.\n */\n size?: FormSize;\n /** Classes applied to the root wrapper. */\n className?: string;\n /** Shows a loading indicator when options are being fetched. */\n loading?: boolean;\n /**\n * Optional async search invoked (debounced ~250ms) as the user types. The\n * consumer fetches and feeds matching options back via the `options` prop\n * (setting `loading` while in flight). When set, the component renders the\n * provided `options` as-is instead of filtering them client-side — the\n * server has already filtered. When absent, typing filters `options`\n * client-side as before.\n */\n onSearch?: (query: string) => void;\n /** Called when a key is pressed in the input. */\n onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void;\n /** Trailing in-field adornment, rendered left of the clear/chevron controls. */\n suffix?: ReactNode;\n /** Leading in-field adornment, rendered at the left edge of the input. */\n prefix?: ReactNode;\n};\n\nexport type ComboboxSingleProps = ComboboxBaseProps & {\n multiple?: false;\n /** Controlled selected value. */\n value: string;\n /** Called when the selected value changes (from list or freeform input). */\n onChange: (value: string) => void;\n};\n\nexport type ComboboxMultiProps = ComboboxBaseProps & {\n multiple: true;\n /** Controlled selected values. */\n value: string[];\n /** Called with the complete next value array after each toggle. */\n onChange: (value: string[]) => void;\n};\n\nexport type ComboboxProps = ComboboxSingleProps | ComboboxMultiProps;\n\nexport function Combobox(props: ComboboxProps) {\n const {\n options,\n placeholder,\n label,\n ariaLabel: ariaLabelProp,\n disabled,\n required,\n invalid,\n allowCustomValue = true,\n id,\n size,\n className,\n loading,\n onSearch,\n onKeyDown: onKeyDownProp,\n suffix,\n prefix,\n } = props;\n const multiple = props.multiple === true;\n const selectedValues = useMemo<string[]>(\n () => (multiple ? props.value : props.value ? [props.value] : []),\n [multiple, props.value],\n );\n\n const rootRef = useRef<HTMLDivElement>(null);\n const anchorRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n const [open, setOpen] = useState(false);\n // Fixed-position coordinates for the portaled listbox, measured from the\n // input row. `width` is the input width (the menu's minimum); `maxWidth` caps\n // content growth at 400px, further bounded by the viewport. Null until first\n // measured so we never render at (0,0).\n // Either `top` (opening downward) or `bottom` (flipped upward when space below\n // is tight, e.g. a control docked near the viewport bottom) is set, never both.\n const [menuPos, setMenuPos] = useState<{\n top?: number;\n bottom?: number;\n left: number;\n width: number;\n maxWidth: number;\n maxHeight: number;\n } | null>(null);\n // `query` is the type-ahead filter text, kept separate from the committed\n // value. It is empty unless the user is actively typing, so opening the\n // dropdown shows every option rather than only the selected one.\n const [query, setQuery] = useState(\"\");\n const [highlighted, setHighlighted] = useState(-1);\n\n const isSelected = (optValue: string) => selectedValues.includes(optValue);\n\n // Closed-state input text. Single: the selected option's label (or its raw\n // value as a fallback). Multi: a summary mirroring MultiSelect.\n const closedLabel = useMemo(() => {\n if (multiple) {\n const labels = options\n .filter((o) => selectedValues.includes(o.value))\n .map((o) => o.label);\n if (labels.length === 0) return \"\";\n if (labels.length <= 2) return labels.join(\", \");\n return `${labels.length} selected`;\n }\n const single = selectedValues[0] ?? \"\";\n return options.find((o) => o.value === single)?.label ?? single;\n }, [multiple, options, selectedValues]);\n\n // While open the input mirrors the query; while closed it shows the\n // committed selection's label/summary.\n const displayValue = open ? query : closedLabel;\n\n const filtered = useMemo(() => {\n // With onSearch the server already filtered. The matches replace the list,\n // but any already-selected value not in the matches is pinned so the user's\n // selection stays visible and toggleable. Selected values carry no label\n // object here, so synthesize {value, label: value} (the typeahead's value is\n // its own human label).\n if (onSearch) return withSelectedOptions(options, selectedValues);\n const q = query.toLowerCase().trim();\n if (!q) return options;\n return options.filter(\n (o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q),\n );\n }, [onSearch, options, query, selectedValues]);\n\n // Debounced server-side search: fires onSearch ~250ms after the query settles\n // while the menu is open. The empty-query call lets the consumer reset its\n // result set. Only active when onSearch is provided.\n useEffect(() => {\n if (!onSearch || !open) return;\n const handle = setTimeout(() => onSearch(query.trim()), 250);\n return () => clearTimeout(handle);\n }, [onSearch, open, query]);\n\n useEffect(() => {\n setHighlighted(-1);\n }, [filtered.length]);\n\n useEffect(() => {\n if (!open) return;\n // Dismiss when the pointer goes down, OR focus moves, anywhere outside the\n // control and its portaled listbox — so clicking elsewhere in a dialog (the\n // backdrop never intercepts a document-level listener) or tabbing to another\n // field both close the menu. The listbox is portaled out of rootRef, so a\n // click inside it would otherwise read as \"outside\" and close before select.\n const onAway = (e: Event) => {\n const target = e.target as Node;\n if (!rootRef.current?.contains(target) && !listRef.current?.contains(target)) {\n commitAndClose();\n }\n };\n document.addEventListener(\"mousedown\", onAway);\n document.addEventListener(\"focusin\", onAway);\n return () => {\n document.removeEventListener(\"mousedown\", onAway);\n document.removeEventListener(\"focusin\", onAway);\n };\n });\n\n // Position the portaled listbox with fixed coordinates measured from the\n // input row, so it escapes any overflow-hidden / scroll ancestor (e.g. a\n // Modal body) that would otherwise clip it. Mirrors HoverCard's approach.\n useLayoutEffect(() => {\n if (!open) {\n setMenuPos(null);\n return;\n }\n const update = () => {\n const anchor = anchorRef.current;\n if (!anchor) return;\n const rect = anchor.getBoundingClientRect();\n // The menu may grow rightward up to 400px, but not past the viewport edge\n // (leaving an 8px gutter). Never let the cap fall below the input width.\n const viewportCap = window.innerWidth - rect.left - 8;\n const maxWidth = Math.max(rect.width, Math.min(MENU_MAX_WIDTH_PX, viewportCap));\n // Flip upward when there isn't room below for the menu but there is above\n // (the input is docked near the viewport bottom). The available side bounds\n // the menu height so it never runs off-screen.\n const spaceBelow = window.innerHeight - rect.bottom - 8;\n const spaceAbove = rect.top - 8;\n const openUp = spaceBelow < MENU_MAX_HEIGHT_PX && spaceAbove > spaceBelow;\n const maxHeight = Math.min(MENU_MAX_HEIGHT_PX, openUp ? spaceAbove : spaceBelow);\n setMenuPos({\n ...(openUp\n ? { bottom: window.innerHeight - rect.top + 4 }\n : { top: rect.bottom + 4 }),\n left: rect.left,\n width: rect.width,\n maxWidth,\n maxHeight,\n });\n };\n update();\n window.addEventListener(\"scroll\", update, true);\n window.addEventListener(\"resize\", update);\n return () => {\n window.removeEventListener(\"scroll\", update, true);\n window.removeEventListener(\"resize\", update);\n };\n }, [open]);\n\n function emit(next: string[]) {\n if (multiple) {\n props.onChange(next);\n } else {\n props.onChange(next[0] ?? \"\");\n }\n }\n\n function openMenu() {\n setQuery(\"\");\n setHighlighted(-1);\n setOpen(true);\n }\n\n function commitAndClose() {\n // Freeform commit (single, allowCustomValue only): a non-empty query that\n // doesn't match an option becomes the value. An empty query keeps the\n // existing selection. Multi and strict modes never commit freeform text.\n const trimmed = query.trim();\n if (!multiple && allowCustomValue && trimmed && trimmed !== selectedValues[0]) {\n emit([trimmed]);\n }\n setQuery(\"\");\n setOpen(false);\n setHighlighted(-1);\n }\n\n function selectOption(opt: ComboboxOption) {\n if (multiple) {\n const next = isSelected(opt.value)\n ? selectedValues.filter((v) => v !== opt.value)\n : [...selectedValues, opt.value];\n emit(next);\n setQuery(\"\");\n setHighlighted(-1);\n inputRef.current?.focus();\n return;\n }\n emit([opt.value]);\n setQuery(\"\");\n setOpen(false);\n setHighlighted(-1);\n inputRef.current?.focus();\n }\n\n function clear() {\n if (selectedValues.length > 0) emit([]);\n setQuery(\"\");\n setHighlighted(-1);\n inputRef.current?.focus();\n }\n\n function scrollToHighlighted(index: number) {\n const list = listRef.current;\n if (!list) return;\n // Query option rows by role rather than child index — interleaved group\n // header rows are direct children too, so children[index] would be offset.\n const item = list.querySelectorAll<HTMLElement>('[role=\"option\"]')[index];\n item?.scrollIntoView({ block: \"nearest\" });\n }\n\n function onKeyDown(e: KeyboardEvent<HTMLInputElement>) {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n if (!open) {\n openMenu();\n return;\n }\n const next = highlighted < filtered.length - 1 ? highlighted + 1 : 0;\n setHighlighted(next);\n scrollToHighlighted(next);\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n if (!open) return;\n const next = highlighted > 0 ? highlighted - 1 : filtered.length - 1;\n setHighlighted(next);\n scrollToHighlighted(next);\n } else if (e.key === \"Enter\") {\n if (open && highlighted >= 0 && filtered[highlighted] && !filtered[highlighted].disabled) {\n e.preventDefault();\n selectOption(filtered[highlighted]);\n return;\n }\n commitAndClose();\n onKeyDownProp?.(e);\n } else if (e.key === \"Escape\") {\n if (open) {\n e.preventDefault();\n setQuery(\"\");\n setOpen(false);\n setHighlighted(-1);\n }\n } else {\n onKeyDownProp?.(e);\n }\n }\n\n const listId = id ? `${id}-listbox` : undefined;\n const ariaLabel = ariaLabelProp ?? (typeof label === \"string\" ? label : undefined);\n const showClear = !required && !loading && !disabled && selectedValues.length > 0;\n\n return (\n <div ref={rootRef} className={cn(\"relative\", className)}>\n <div ref={anchorRef} data-jsf-control className=\"relative flex items-center\">\n {label != null && (\n <span className=\"pointer-events-none absolute left-2 z-10 whitespace-nowrap font-medium uppercase tracking-wide text-muted-foreground text-[10px]\">\n {label}\n </span>\n )}\n <input\n ref={inputRef}\n id={id}\n type=\"text\"\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={listId}\n aria-activedescendant={highlighted >= 0 ? `${listId}-${highlighted}` : undefined}\n aria-label={ariaLabel}\n aria-invalid={invalid || undefined}\n autoComplete=\"off\"\n disabled={disabled}\n placeholder={placeholder}\n value={displayValue}\n onChange={(e) => {\n setQuery(e.target.value);\n if (!open) setOpen(true);\n }}\n onFocus={openMenu}\n // Reopen on click too: focus only fires on the first focus, so a click\n // on an already-focused-but-closed input (e.g. after selecting, or\n // after Escape) would otherwise do nothing.\n onClick={() => {\n if (!open) openMenu();\n }}\n onKeyDown={onKeyDown}\n className={cn(\n \"w-full rounded-md border border-input bg-background text-foreground\",\n size ? inputSizeClass[size] : \"h-control-h px-control-px text-sm\",\n prefix && \"pl-8\",\n suffix ? (showClear ? \"pr-[5.5rem]\" : \"pr-14\") : showClear ? \"pr-14\" : \"pr-8\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n invalid && \"border-destructive focus-visible:ring-destructive\",\n )}\n style={label != null ? labelPadding(label) : undefined}\n />\n {prefix && (\n <div className=\"absolute inset-y-0 left-1.5 flex items-center\">{prefix}</div>\n )}\n {suffix && (\n <div className={cn(\"absolute flex h-full items-center\", showClear ? \"right-[3.75rem]\" : \"right-7\")}>\n {suffix}\n </div>\n )}\n {showClear && (\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label=\"Clear\"\n onClick={clear}\n className=\"absolute right-7 flex h-full items-center px-1 text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={UiClose} className=\"text-xs\" />\n </button>\n )}\n {loading ? (\n <span className=\"pointer-events-none absolute right-2 top-1/2 -translate-y-1/2\">\n <svg className=\"h-3.5 w-3.5 animate-spin text-muted-foreground\" viewBox=\"0 0 16 16\" fill=\"none\">\n <circle cx=\"8\" cy=\"8\" r=\"6.5\" stroke=\"currentColor\" strokeWidth=\"2\" opacity=\"0.25\" />\n <path d=\"M14.5 8a6.5 6.5 0 0 0-6.5-6.5\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n </svg>\n </span>\n ) : (\n <button\n type=\"button\"\n tabIndex={-1}\n disabled={disabled}\n aria-label=\"Toggle options\"\n onClick={() => {\n if (open) {\n commitAndClose();\n } else {\n openMenu();\n inputRef.current?.focus();\n }\n }}\n className=\"pointer-events-auto absolute right-0 flex h-full items-center px-2 text-muted-foreground\"\n >\n <Icon icon={UiChevronDown} className=\"text-xs\" />\n </button>\n )}\n </div>\n {open && menuPos && typeof document !== \"undefined\" &&\n createPortal(\n <div\n id={listId}\n ref={listRef}\n role=\"listbox\"\n aria-multiselectable={multiple || undefined}\n style={{\n position: \"fixed\",\n ...(menuPos.top != null ? { top: menuPos.top } : { bottom: menuPos.bottom }),\n left: menuPos.left,\n // Grow to fit the widest option, but never narrower than the input\n // nor wider than 400px (beyond which option labels truncate). The\n // cap is also bounded by the viewport so the menu can't overflow.\n minWidth: menuPos.width,\n maxWidth: menuPos.maxWidth,\n maxHeight: menuPos.maxHeight,\n }}\n className=\"z-[9999] w-max overflow-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg shadow-black/5\"\n >\n {loading && filtered.length === 0 && (\n <div className=\"px-2 py-4 text-center text-sm text-muted-foreground\">Loading…</div>\n )}\n {!loading && filtered.length === 0 && (\n <div className=\"px-2 py-4 text-center text-sm text-muted-foreground\">No results</div>\n )}\n {filtered.map((opt, i) => {\n const selected = isSelected(opt.value);\n // A header renders above the first option of each group. Derived\n // from the surviving (filtered) options, so an empty group emits no\n // header. The option keeps `i` (its index in `filtered`) for\n // id/highlight/select — headers are role=\"presentation\", never\n // counted in the highlight/scroll index.\n const showHeader = opt.group != null && opt.group !== filtered[i - 1]?.group;\n return (\n <Fragment key={opt.value}>\n {showHeader && (\n <div\n role=\"presentation\"\n className=\"select-none px-2 pb-1 pt-2 text-[10px] font-medium uppercase tracking-wide text-muted-foreground\"\n >\n {opt.group}\n </div>\n )}\n <div\n id={listId ? `${listId}-${i}` : undefined}\n role=\"option\"\n aria-selected={selected}\n aria-disabled={opt.disabled}\n onMouseDown={(e) => {\n e.preventDefault();\n if (!opt.disabled) selectOption(opt);\n }}\n onMouseEnter={() => setHighlighted(i)}\n className={cn(\n \"flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm\",\n i === highlighted && \"bg-accent\",\n selected && \"font-medium\",\n opt.disabled && \"cursor-not-allowed opacity-50\",\n )}\n >\n <Icon\n icon={UiCheck}\n className={cn(\"text-xs shrink-0\", selected ? \"opacity-100\" : \"opacity-0\")}\n />\n <LabelIcon icon={opt.icon} className=\"text-sm text-muted-foreground\" />\n <span className=\"min-w-0 truncate\">{opt.label}</span>\n </div>\n </Fragment>\n );\n })}\n </div>,\n document.body,\n )}\n </div>\n );\n}\n\n// Reserve room on the left of the input for the inline label so typed text\n// does not overlap it. Node labels (an icon from comboboxLabelProps) are\n// compact, so reserve a fixed icon-sized inset; text labels scale with their\n// character count at roughly 0.6rem per character plus padding.\nfunction labelPadding(label: ReactNode) {\n if (typeof label !== \"string\") return { paddingLeft: \"1.75rem\" };\n return { paddingLeft: `${Math.min(label.length * 0.62 + 0.75, 9)}rem` };\n}\n\n// withSelectedOptions returns the option list plus any selected value that isn't\n// already present, synthesized as {value, label: value}. Used by the server\n// (onSearch) typeahead so an already-selected value missing from the current\n// matches still renders as a checked row. Selected-but-absent values come first\n// so they don't get scrolled past by a long match list.\nfunction withSelectedOptions(\n options: ComboboxOption[],\n selectedValues: string[],\n): ComboboxOption[] {\n const present = new Set(options.map((o) => o.value));\n const pinned = selectedValues\n .filter((v) => !present.has(v))\n .map((v) => ({ value: v, label: v }));\n return pinned.length === 0 ? options : [...pinned, ...options];\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Combobox.js","sources":["../../src/components/Combobox.tsx"],"sourcesContent":["import {\n Fragment,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type KeyboardEvent,\n type ReactNode,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/utils\";\nimport { Icon, LabelIcon, type LabelIconSpec } from \"../data/Icon\";\nimport { UiChevronDown, UiClose, UiCheck } from \"../icons\";\nimport { inputSizeClass, type FormSize } from \"./json-schema-form-size\";\nimport { useFloatingZIndex } from \"../overlay/modalStack\";\n\n// Upper bound the open dropdown grows to in order to show full option labels\n// before they truncate. The menu's minimum is always the input's width.\nconst MENU_MAX_WIDTH_PX = 400;\n\n// Matches the menu's `max-h-64` (16rem) cap; also the threshold below which the\n// menu flips above the input instead of opening downward off-screen.\nconst MENU_MAX_HEIGHT_PX = 256;\n\nexport type ComboboxOption = {\n value: string;\n label: string;\n disabled?: boolean;\n /** Leading glyph for the option: a runtime icon name or a rendered node. */\n icon?: LabelIconSpec;\n /**\n * Optional section label. A non-interactive header renders above the first\n * option of each group (options are grouped by contiguous `group` value in\n * the order provided). Options without a `group` render no header.\n */\n group?: string;\n};\n\ntype ComboboxBaseProps = {\n /** Available options, rendered in the order provided. */\n options: ComboboxOption[];\n /** Ghost text when no value is set. */\n placeholder?: string;\n /** Optional label rendered inline inside the control, before the input. */\n label?: ReactNode;\n /**\n * Accessible name for the input. Use when `label` is a non-string node (e.g.\n * an icon) so the control still has a text name for screen readers; defaults\n * to `label` when it is a string.\n */\n ariaLabel?: string;\n /** Disables the input. */\n disabled?: boolean;\n /** When true, the value cannot be cleared (the clear button is hidden). */\n required?: boolean;\n /**\n * Marks the control as invalid: renders a destructive border and sets\n * aria-invalid. Purely presentational — the consumer decides what invalid\n * means (e.g. a value absent from the options in a strict picker).\n */\n invalid?: boolean;\n /**\n * When false, the value is restricted to the provided options — typed text\n * that does not match an option is discarded instead of committed. Defaults\n * to true (freeform entry allowed).\n */\n allowCustomValue?: boolean;\n /** HTML id for the input element. */\n id?: string;\n /**\n * Overrides the input's height/padding/text size with an explicit size token\n * (xs–xl). When unset, the input uses the global density tokens.\n */\n size?: FormSize;\n /** Classes applied to the root wrapper. */\n className?: string;\n /** Shows a loading indicator when options are being fetched. */\n loading?: boolean;\n /**\n * Optional async search invoked (debounced ~250ms) as the user types. The\n * consumer fetches and feeds matching options back via the `options` prop\n * (setting `loading` while in flight). When set, the component renders the\n * provided `options` as-is instead of filtering them client-side — the\n * server has already filtered. When absent, typing filters `options`\n * client-side as before.\n */\n onSearch?: (query: string) => void;\n /** Called when a key is pressed in the input. */\n onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void;\n /** Trailing in-field adornment, rendered left of the clear/chevron controls. */\n suffix?: ReactNode;\n /** Leading in-field adornment, rendered at the left edge of the input. */\n prefix?: ReactNode;\n};\n\nexport type ComboboxSingleProps = ComboboxBaseProps & {\n multiple?: false;\n /** Controlled selected value. */\n value: string;\n /** Called when the selected value changes (from list or freeform input). */\n onChange: (value: string) => void;\n};\n\nexport type ComboboxMultiProps = ComboboxBaseProps & {\n multiple: true;\n /** Controlled selected values. */\n value: string[];\n /** Called with the complete next value array after each toggle. */\n onChange: (value: string[]) => void;\n};\n\nexport type ComboboxProps = ComboboxSingleProps | ComboboxMultiProps;\n\nexport function Combobox(props: ComboboxProps) {\n const {\n options,\n placeholder,\n label,\n ariaLabel: ariaLabelProp,\n disabled,\n required,\n invalid,\n allowCustomValue = true,\n id,\n size,\n className,\n loading,\n onSearch,\n onKeyDown: onKeyDownProp,\n suffix,\n prefix,\n } = props;\n const multiple = props.multiple === true;\n const selectedValues = useMemo<string[]>(\n () => (multiple ? props.value : props.value ? [props.value] : []),\n [multiple, props.value],\n );\n\n const rootRef = useRef<HTMLDivElement>(null);\n const anchorRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n const [open, setOpen] = useState(false);\n const floatingZ = useFloatingZIndex();\n // Fixed-position coordinates for the portaled listbox, measured from the\n // input row. `width` is the input width (the menu's minimum); `maxWidth` caps\n // content growth at 400px, further bounded by the viewport. Null until first\n // measured so we never render at (0,0).\n // Either `top` (opening downward) or `bottom` (flipped upward when space below\n // is tight, e.g. a control docked near the viewport bottom) is set, never both.\n const [menuPos, setMenuPos] = useState<{\n top?: number;\n bottom?: number;\n left: number;\n width: number;\n maxWidth: number;\n maxHeight: number;\n } | null>(null);\n // `query` is the type-ahead filter text, kept separate from the committed\n // value. It is empty unless the user is actively typing, so opening the\n // dropdown shows every option rather than only the selected one.\n const [query, setQuery] = useState(\"\");\n const [highlighted, setHighlighted] = useState(-1);\n\n const isSelected = (optValue: string) => selectedValues.includes(optValue);\n\n // Closed-state input text. Single: the selected option's label (or its raw\n // value as a fallback). Multi: a summary mirroring MultiSelect.\n const closedLabel = useMemo(() => {\n if (multiple) {\n const labels = options\n .filter((o) => selectedValues.includes(o.value))\n .map((o) => o.label);\n if (labels.length === 0) return \"\";\n if (labels.length <= 2) return labels.join(\", \");\n return `${labels.length} selected`;\n }\n const single = selectedValues[0] ?? \"\";\n return options.find((o) => o.value === single)?.label ?? single;\n }, [multiple, options, selectedValues]);\n\n // While open the input mirrors the query; while closed it shows the\n // committed selection's label/summary.\n const displayValue = open ? query : closedLabel;\n\n const filtered = useMemo(() => {\n // With onSearch the server already filtered. The matches replace the list,\n // but any already-selected value not in the matches is pinned so the user's\n // selection stays visible and toggleable. Selected values carry no label\n // object here, so synthesize {value, label: value} (the typeahead's value is\n // its own human label).\n if (onSearch) return withSelectedOptions(options, selectedValues);\n const q = query.toLowerCase().trim();\n if (!q) return options;\n return options.filter(\n (o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q),\n );\n }, [onSearch, options, query, selectedValues]);\n\n // Debounced server-side search: fires onSearch ~250ms after the query settles\n // while the menu is open. The empty-query call lets the consumer reset its\n // result set. Only active when onSearch is provided.\n useEffect(() => {\n if (!onSearch || !open) return;\n const handle = setTimeout(() => onSearch(query.trim()), 250);\n return () => clearTimeout(handle);\n }, [onSearch, open, query]);\n\n useEffect(() => {\n setHighlighted(-1);\n }, [filtered.length]);\n\n useEffect(() => {\n if (!open) return;\n // Dismiss when the pointer goes down, OR focus moves, anywhere outside the\n // control and its portaled listbox — so clicking elsewhere in a dialog (the\n // backdrop never intercepts a document-level listener) or tabbing to another\n // field both close the menu. The listbox is portaled out of rootRef, so a\n // click inside it would otherwise read as \"outside\" and close before select.\n const onAway = (e: Event) => {\n const target = e.target as Node;\n if (!rootRef.current?.contains(target) && !listRef.current?.contains(target)) {\n commitAndClose();\n }\n };\n document.addEventListener(\"mousedown\", onAway);\n document.addEventListener(\"focusin\", onAway);\n return () => {\n document.removeEventListener(\"mousedown\", onAway);\n document.removeEventListener(\"focusin\", onAway);\n };\n });\n\n // Position the portaled listbox with fixed coordinates measured from the\n // input row, so it escapes any overflow-hidden / scroll ancestor (e.g. a\n // Modal body) that would otherwise clip it. Mirrors HoverCard's approach.\n useLayoutEffect(() => {\n if (!open) {\n setMenuPos(null);\n return;\n }\n const update = () => {\n const anchor = anchorRef.current;\n if (!anchor) return;\n const rect = anchor.getBoundingClientRect();\n // The menu may grow rightward up to 400px, but not past the viewport edge\n // (leaving an 8px gutter). Never let the cap fall below the input width.\n const viewportCap = window.innerWidth - rect.left - 8;\n const maxWidth = Math.max(rect.width, Math.min(MENU_MAX_WIDTH_PX, viewportCap));\n // Flip upward when there isn't room below for the menu but there is above\n // (the input is docked near the viewport bottom). The available side bounds\n // the menu height so it never runs off-screen.\n const spaceBelow = window.innerHeight - rect.bottom - 8;\n const spaceAbove = rect.top - 8;\n const openUp = spaceBelow < MENU_MAX_HEIGHT_PX && spaceAbove > spaceBelow;\n const maxHeight = Math.min(MENU_MAX_HEIGHT_PX, openUp ? spaceAbove : spaceBelow);\n setMenuPos({\n ...(openUp\n ? { bottom: window.innerHeight - rect.top + 4 }\n : { top: rect.bottom + 4 }),\n left: rect.left,\n width: rect.width,\n maxWidth,\n maxHeight,\n });\n };\n update();\n window.addEventListener(\"scroll\", update, true);\n window.addEventListener(\"resize\", update);\n return () => {\n window.removeEventListener(\"scroll\", update, true);\n window.removeEventListener(\"resize\", update);\n };\n }, [open]);\n\n function emit(next: string[]) {\n if (multiple) {\n props.onChange(next);\n } else {\n props.onChange(next[0] ?? \"\");\n }\n }\n\n function openMenu() {\n setQuery(\"\");\n setHighlighted(-1);\n setOpen(true);\n }\n\n function commitAndClose() {\n // Freeform commit (single, allowCustomValue only): a non-empty query that\n // doesn't match an option becomes the value. An empty query keeps the\n // existing selection. Multi and strict modes never commit freeform text.\n const trimmed = query.trim();\n if (!multiple && allowCustomValue && trimmed && trimmed !== selectedValues[0]) {\n emit([trimmed]);\n }\n setQuery(\"\");\n setOpen(false);\n setHighlighted(-1);\n }\n\n function selectOption(opt: ComboboxOption) {\n if (multiple) {\n const next = isSelected(opt.value)\n ? selectedValues.filter((v) => v !== opt.value)\n : [...selectedValues, opt.value];\n emit(next);\n setQuery(\"\");\n setHighlighted(-1);\n inputRef.current?.focus();\n return;\n }\n emit([opt.value]);\n setQuery(\"\");\n setOpen(false);\n setHighlighted(-1);\n inputRef.current?.focus();\n }\n\n function clear() {\n if (selectedValues.length > 0) emit([]);\n setQuery(\"\");\n setHighlighted(-1);\n inputRef.current?.focus();\n }\n\n function scrollToHighlighted(index: number) {\n const list = listRef.current;\n if (!list) return;\n // Query option rows by role rather than child index — interleaved group\n // header rows are direct children too, so children[index] would be offset.\n const item = list.querySelectorAll<HTMLElement>('[role=\"option\"]')[index];\n item?.scrollIntoView({ block: \"nearest\" });\n }\n\n function onKeyDown(e: KeyboardEvent<HTMLInputElement>) {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n if (!open) {\n openMenu();\n return;\n }\n const next = highlighted < filtered.length - 1 ? highlighted + 1 : 0;\n setHighlighted(next);\n scrollToHighlighted(next);\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n if (!open) return;\n const next = highlighted > 0 ? highlighted - 1 : filtered.length - 1;\n setHighlighted(next);\n scrollToHighlighted(next);\n } else if (e.key === \"Enter\") {\n if (open && highlighted >= 0 && filtered[highlighted] && !filtered[highlighted].disabled) {\n e.preventDefault();\n selectOption(filtered[highlighted]);\n return;\n }\n commitAndClose();\n onKeyDownProp?.(e);\n } else if (e.key === \"Escape\") {\n if (open) {\n e.preventDefault();\n setQuery(\"\");\n setOpen(false);\n setHighlighted(-1);\n }\n } else {\n onKeyDownProp?.(e);\n }\n }\n\n const listId = id ? `${id}-listbox` : undefined;\n const ariaLabel = ariaLabelProp ?? (typeof label === \"string\" ? label : undefined);\n const showClear = !required && !loading && !disabled && selectedValues.length > 0;\n\n return (\n <div ref={rootRef} className={cn(\"relative\", className)}>\n <div ref={anchorRef} data-jsf-control className=\"relative flex items-center\">\n {label != null && (\n <span className=\"pointer-events-none absolute left-2 z-10 whitespace-nowrap font-medium uppercase tracking-wide text-muted-foreground text-[10px]\">\n {label}\n </span>\n )}\n <input\n ref={inputRef}\n id={id}\n type=\"text\"\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={listId}\n aria-activedescendant={highlighted >= 0 ? `${listId}-${highlighted}` : undefined}\n aria-label={ariaLabel}\n aria-invalid={invalid || undefined}\n autoComplete=\"off\"\n disabled={disabled}\n placeholder={placeholder}\n value={displayValue}\n onChange={(e) => {\n setQuery(e.target.value);\n if (!open) setOpen(true);\n }}\n onFocus={openMenu}\n // Reopen on click too: focus only fires on the first focus, so a click\n // on an already-focused-but-closed input (e.g. after selecting, or\n // after Escape) would otherwise do nothing.\n onClick={() => {\n if (!open) openMenu();\n }}\n onKeyDown={onKeyDown}\n className={cn(\n \"w-full rounded-md border border-input bg-background text-foreground\",\n size ? inputSizeClass[size] : \"h-control-h px-control-px text-sm\",\n prefix && \"pl-8\",\n suffix ? (showClear ? \"pr-[5.5rem]\" : \"pr-14\") : showClear ? \"pr-14\" : \"pr-8\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n invalid && \"border-destructive focus-visible:ring-destructive\",\n )}\n style={label != null ? labelPadding(label) : undefined}\n />\n {prefix && (\n <div className=\"absolute inset-y-0 left-1.5 flex items-center\">{prefix}</div>\n )}\n {suffix && (\n <div className={cn(\"absolute flex h-full items-center\", showClear ? \"right-[3.75rem]\" : \"right-7\")}>\n {suffix}\n </div>\n )}\n {showClear && (\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label=\"Clear\"\n onClick={clear}\n className=\"absolute right-7 flex h-full items-center px-1 text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={UiClose} className=\"text-xs\" />\n </button>\n )}\n {loading ? (\n <span className=\"pointer-events-none absolute right-2 top-1/2 -translate-y-1/2\">\n <svg className=\"h-3.5 w-3.5 animate-spin text-muted-foreground\" viewBox=\"0 0 16 16\" fill=\"none\">\n <circle cx=\"8\" cy=\"8\" r=\"6.5\" stroke=\"currentColor\" strokeWidth=\"2\" opacity=\"0.25\" />\n <path d=\"M14.5 8a6.5 6.5 0 0 0-6.5-6.5\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n </svg>\n </span>\n ) : (\n <button\n type=\"button\"\n tabIndex={-1}\n disabled={disabled}\n aria-label=\"Toggle options\"\n onClick={() => {\n if (open) {\n commitAndClose();\n } else {\n openMenu();\n inputRef.current?.focus();\n }\n }}\n className=\"pointer-events-auto absolute right-0 flex h-full items-center px-2 text-muted-foreground\"\n >\n <Icon icon={UiChevronDown} className=\"text-xs\" />\n </button>\n )}\n </div>\n {open && menuPos && typeof document !== \"undefined\" &&\n createPortal(\n <div\n id={listId}\n ref={listRef}\n role=\"listbox\"\n aria-multiselectable={multiple || undefined}\n style={{\n position: \"fixed\",\n ...(menuPos.top != null ? { top: menuPos.top } : { bottom: menuPos.bottom }),\n left: menuPos.left,\n // Grow to fit the widest option, but never narrower than the input\n // nor wider than 400px (beyond which option labels truncate). The\n // cap is also bounded by the viewport so the menu can't overflow.\n minWidth: menuPos.width,\n maxWidth: menuPos.maxWidth,\n maxHeight: menuPos.maxHeight,\n zIndex: floatingZ,\n }}\n className=\"w-max overflow-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg shadow-black/5\"\n >\n {loading && filtered.length === 0 && (\n <div className=\"px-2 py-4 text-center text-sm text-muted-foreground\">Loading…</div>\n )}\n {!loading && filtered.length === 0 && (\n <div className=\"px-2 py-4 text-center text-sm text-muted-foreground\">No results</div>\n )}\n {filtered.map((opt, i) => {\n const selected = isSelected(opt.value);\n // A header renders above the first option of each group. Derived\n // from the surviving (filtered) options, so an empty group emits no\n // header. The option keeps `i` (its index in `filtered`) for\n // id/highlight/select — headers are role=\"presentation\", never\n // counted in the highlight/scroll index.\n const showHeader = opt.group != null && opt.group !== filtered[i - 1]?.group;\n return (\n <Fragment key={opt.value}>\n {showHeader && (\n <div\n role=\"presentation\"\n className=\"select-none px-2 pb-1 pt-2 text-[10px] font-medium uppercase tracking-wide text-muted-foreground\"\n >\n {opt.group}\n </div>\n )}\n <div\n id={listId ? `${listId}-${i}` : undefined}\n role=\"option\"\n aria-selected={selected}\n aria-disabled={opt.disabled}\n onMouseDown={(e) => {\n e.preventDefault();\n if (!opt.disabled) selectOption(opt);\n }}\n onMouseEnter={() => setHighlighted(i)}\n className={cn(\n \"flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm\",\n i === highlighted && \"bg-accent\",\n selected && \"font-medium\",\n opt.disabled && \"cursor-not-allowed opacity-50\",\n )}\n >\n <Icon\n icon={UiCheck}\n className={cn(\"text-xs shrink-0\", selected ? \"opacity-100\" : \"opacity-0\")}\n />\n <LabelIcon icon={opt.icon} className=\"text-sm text-muted-foreground\" />\n <span className=\"min-w-0 truncate\">{opt.label}</span>\n </div>\n </Fragment>\n );\n })}\n </div>,\n document.body,\n )}\n </div>\n );\n}\n\n// Reserve room on the left of the input for the inline label so typed text\n// does not overlap it. Node labels (an icon from comboboxLabelProps) are\n// compact, so reserve a fixed icon-sized inset; text labels scale with their\n// character count at roughly 0.6rem per character plus padding.\nfunction labelPadding(label: ReactNode) {\n if (typeof label !== \"string\") return { paddingLeft: \"1.75rem\" };\n return { paddingLeft: `${Math.min(label.length * 0.62 + 0.75, 9)}rem` };\n}\n\n// withSelectedOptions returns the option list plus any selected value that isn't\n// already present, synthesized as {value, label: value}. Used by the server\n// (onSearch) typeahead so an already-selected value missing from the current\n// matches still renders as a checked row. Selected-but-absent values come first\n// so they don't get scrolled past by a long match list.\nfunction withSelectedOptions(\n options: ComboboxOption[],\n selectedValues: string[],\n): ComboboxOption[] {\n const present = new Set(options.map((o) => o.value));\n const pinned = selectedValues\n .filter((v) => !present.has(v))\n .map((v) => ({ value: v, label: v }));\n return pinned.length === 0 ? options : [...pinned, ...options];\n}\n"],"names":[],"mappings":";;;;;;;;;;AAmBA,MAAM,oBAAoB;AAI1B,MAAM,qBAAqB;AA2FpB,SAAS,SAAS,OAAsB;AAC7C,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA,mBAAmB;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA;AAAA,EAAA,IACE;AACJ,QAAM,WAAW,MAAM,aAAa;AACpC,QAAM,iBAAiB;AAAA,IACrB,MAAO,WAAW,MAAM,QAAQ,MAAM,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAA;AAAA,IAC9D,CAAC,UAAU,MAAM,KAAK;AAAA,EAAA;AAGxB,QAAM,UAAU,OAAuB,IAAI;AAC3C,QAAM,YAAY,OAAuB,IAAI;AAC7C,QAAM,WAAW,OAAyB,IAAI;AAC9C,QAAM,UAAU,OAAuB,IAAI;AAC3C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,YAAY,kBAAA;AAOlB,QAAM,CAAC,SAAS,UAAU,IAAI,SAOpB,IAAI;AAId,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,EAAE;AAEjD,QAAM,aAAa,CAAC,aAAqB,eAAe,SAAS,QAAQ;AAIzE,QAAM,cAAc,QAAQ,MAAM;;AAChC,QAAI,UAAU;AACZ,YAAM,SAAS,QACZ,OAAO,CAAC,MAAM,eAAe,SAAS,EAAE,KAAK,CAAC,EAC9C,IAAI,CAAC,MAAM,EAAE,KAAK;AACrB,UAAI,OAAO,WAAW,EAAG,QAAO;AAChC,UAAI,OAAO,UAAU,EAAG,QAAO,OAAO,KAAK,IAAI;AAC/C,aAAO,GAAG,OAAO,MAAM;AAAA,IACzB;AACA,UAAM,SAAS,eAAe,CAAC,KAAK;AACpC,aAAO,aAAQ,KAAK,CAAC,MAAM,EAAE,UAAU,MAAM,MAAtC,mBAAyC,UAAS;AAAA,EAC3D,GAAG,CAAC,UAAU,SAAS,cAAc,CAAC;AAItC,QAAM,eAAe,OAAO,QAAQ;AAEpC,QAAM,WAAW,QAAQ,MAAM;AAM7B,QAAI,SAAU,QAAO,oBAAoB,SAAS,cAAc;AAChE,UAAM,IAAI,MAAM,YAAA,EAAc,KAAA;AAC9B,QAAI,CAAC,EAAG,QAAO;AACf,WAAO,QAAQ;AAAA,MACb,CAAC,MAAM,EAAE,MAAM,cAAc,SAAS,CAAC,KAAK,EAAE,MAAM,YAAA,EAAc,SAAS,CAAC;AAAA,IAAA;AAAA,EAEhF,GAAG,CAAC,UAAU,SAAS,OAAO,cAAc,CAAC;AAK7C,YAAU,MAAM;AACd,QAAI,CAAC,YAAY,CAAC,KAAM;AACxB,UAAM,SAAS,WAAW,MAAM,SAAS,MAAM,KAAA,CAAM,GAAG,GAAG;AAC3D,WAAO,MAAM,aAAa,MAAM;AAAA,EAClC,GAAG,CAAC,UAAU,MAAM,KAAK,CAAC;AAE1B,YAAU,MAAM;AACd,mBAAe,EAAE;AAAA,EACnB,GAAG,CAAC,SAAS,MAAM,CAAC;AAEpB,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AAMX,UAAM,SAAS,CAAC,MAAa;;AAC3B,YAAM,SAAS,EAAE;AACjB,UAAI,GAAC,aAAQ,YAAR,mBAAiB,SAAS,YAAW,GAAC,aAAQ,YAAR,mBAAiB,SAAS,UAAS;AAC5E,uBAAA;AAAA,MACF;AAAA,IACF;AACA,aAAS,iBAAiB,aAAa,MAAM;AAC7C,aAAS,iBAAiB,WAAW,MAAM;AAC3C,WAAO,MAAM;AACX,eAAS,oBAAoB,aAAa,MAAM;AAChD,eAAS,oBAAoB,WAAW,MAAM;AAAA,IAChD;AAAA,EACF,CAAC;AAKD,kBAAgB,MAAM;AACpB,QAAI,CAAC,MAAM;AACT,iBAAW,IAAI;AACf;AAAA,IACF;AACA,UAAM,SAAS,MAAM;AACnB,YAAM,SAAS,UAAU;AACzB,UAAI,CAAC,OAAQ;AACb,YAAM,OAAO,OAAO,sBAAA;AAGpB,YAAM,cAAc,OAAO,aAAa,KAAK,OAAO;AACpD,YAAM,WAAW,KAAK,IAAI,KAAK,OAAO,KAAK,IAAI,mBAAmB,WAAW,CAAC;AAI9E,YAAM,aAAa,OAAO,cAAc,KAAK,SAAS;AACtD,YAAM,aAAa,KAAK,MAAM;AAC9B,YAAM,SAAS,aAAa,sBAAsB,aAAa;AAC/D,YAAM,YAAY,KAAK,IAAI,oBAAoB,SAAS,aAAa,UAAU;AAC/E,iBAAW;AAAA,QACT,GAAI,SACA,EAAE,QAAQ,OAAO,cAAc,KAAK,MAAM,EAAA,IAC1C,EAAE,KAAK,KAAK,SAAS,EAAA;AAAA,QACzB,MAAM,KAAK;AAAA,QACX,OAAO,KAAK;AAAA,QACZ;AAAA,QACA;AAAA,MAAA,CACD;AAAA,IACH;AACA,WAAA;AACA,WAAO,iBAAiB,UAAU,QAAQ,IAAI;AAC9C,WAAO,iBAAiB,UAAU,MAAM;AACxC,WAAO,MAAM;AACX,aAAO,oBAAoB,UAAU,QAAQ,IAAI;AACjD,aAAO,oBAAoB,UAAU,MAAM;AAAA,IAC7C;AAAA,EACF,GAAG,CAAC,IAAI,CAAC;AAET,WAAS,KAAK,MAAgB;AAC5B,QAAI,UAAU;AACZ,YAAM,SAAS,IAAI;AAAA,IACrB,OAAO;AACL,YAAM,SAAS,KAAK,CAAC,KAAK,EAAE;AAAA,IAC9B;AAAA,EACF;AAEA,WAAS,WAAW;AAClB,aAAS,EAAE;AACX,mBAAe,EAAE;AACjB,YAAQ,IAAI;AAAA,EACd;AAEA,WAAS,iBAAiB;AAIxB,UAAM,UAAU,MAAM,KAAA;AACtB,QAAI,CAAC,YAAY,oBAAoB,WAAW,YAAY,eAAe,CAAC,GAAG;AAC7E,WAAK,CAAC,OAAO,CAAC;AAAA,IAChB;AACA,aAAS,EAAE;AACX,YAAQ,KAAK;AACb,mBAAe,EAAE;AAAA,EACnB;AAEA,WAAS,aAAa,KAAqB;;AACzC,QAAI,UAAU;AACZ,YAAM,OAAO,WAAW,IAAI,KAAK,IAC7B,eAAe,OAAO,CAAC,MAAM,MAAM,IAAI,KAAK,IAC5C,CAAC,GAAG,gBAAgB,IAAI,KAAK;AACjC,WAAK,IAAI;AACT,eAAS,EAAE;AACX,qBAAe,EAAE;AACjB,qBAAS,YAAT,mBAAkB;AAClB;AAAA,IACF;AACA,SAAK,CAAC,IAAI,KAAK,CAAC;AAChB,aAAS,EAAE;AACX,YAAQ,KAAK;AACb,mBAAe,EAAE;AACjB,mBAAS,YAAT,mBAAkB;AAAA,EACpB;AAEA,WAAS,QAAQ;;AACf,QAAI,eAAe,SAAS,EAAG,MAAK,CAAA,CAAE;AACtC,aAAS,EAAE;AACX,mBAAe,EAAE;AACjB,mBAAS,YAAT,mBAAkB;AAAA,EACpB;AAEA,WAAS,oBAAoB,OAAe;AAC1C,UAAM,OAAO,QAAQ;AACrB,QAAI,CAAC,KAAM;AAGX,UAAM,OAAO,KAAK,iBAA8B,iBAAiB,EAAE,KAAK;AACxE,iCAAM,eAAe,EAAE,OAAO,UAAA;AAAA,EAChC;AAEA,WAAS,UAAU,GAAoC;AACrD,QAAI,EAAE,QAAQ,aAAa;AACzB,QAAE,eAAA;AACF,UAAI,CAAC,MAAM;AACT,iBAAA;AACA;AAAA,MACF;AACA,YAAM,OAAO,cAAc,SAAS,SAAS,IAAI,cAAc,IAAI;AACnE,qBAAe,IAAI;AACnB,0BAAoB,IAAI;AAAA,IAC1B,WAAW,EAAE,QAAQ,WAAW;AAC9B,QAAE,eAAA;AACF,UAAI,CAAC,KAAM;AACX,YAAM,OAAO,cAAc,IAAI,cAAc,IAAI,SAAS,SAAS;AACnE,qBAAe,IAAI;AACnB,0BAAoB,IAAI;AAAA,IAC1B,WAAW,EAAE,QAAQ,SAAS;AAC5B,UAAI,QAAQ,eAAe,KAAK,SAAS,WAAW,KAAK,CAAC,SAAS,WAAW,EAAE,UAAU;AACxF,UAAE,eAAA;AACF,qBAAa,SAAS,WAAW,CAAC;AAClC;AAAA,MACF;AACA,qBAAA;AACA,qDAAgB;AAAA,IAClB,WAAW,EAAE,QAAQ,UAAU;AAC7B,UAAI,MAAM;AACR,UAAE,eAAA;AACF,iBAAS,EAAE;AACX,gBAAQ,KAAK;AACb,uBAAe,EAAE;AAAA,MACnB;AAAA,IACF,OAAO;AACL,qDAAgB;AAAA,IAClB;AAAA,EACF;AAEA,QAAM,SAAS,KAAK,GAAG,EAAE,aAAa;AACtC,QAAM,YAAY,kBAAkB,OAAO,UAAU,WAAW,QAAQ;AACxE,QAAM,YAAY,CAAC,YAAY,CAAC,WAAW,CAAC,YAAY,eAAe,SAAS;AAEhF,SACE,qBAAC,SAAI,KAAK,SAAS,WAAW,GAAG,YAAY,SAAS,GACpD,UAAA;AAAA,IAAA,qBAAC,SAAI,KAAK,WAAW,oBAAgB,MAAC,WAAU,8BAC7C,UAAA;AAAA,MAAA,SAAS,QACR,oBAAC,QAAA,EAAK,WAAU,oIACb,UAAA,OACH;AAAA,MAEF;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,KAAK;AAAA,UACL;AAAA,UACA,MAAK;AAAA,UACL,MAAK;AAAA,UACL,iBAAe;AAAA,UACf,iBAAe;AAAA,UACf,yBAAuB,eAAe,IAAI,GAAG,MAAM,IAAI,WAAW,KAAK;AAAA,UACvE,cAAY;AAAA,UACZ,gBAAc,WAAW;AAAA,UACzB,cAAa;AAAA,UACb;AAAA,UACA;AAAA,UACA,OAAO;AAAA,UACP,UAAU,CAAC,MAAM;AACf,qBAAS,EAAE,OAAO,KAAK;AACvB,gBAAI,CAAC,KAAM,SAAQ,IAAI;AAAA,UACzB;AAAA,UACA,SAAS;AAAA,UAIT,SAAS,MAAM;AACb,gBAAI,CAAC,KAAM,UAAA;AAAA,UACb;AAAA,UACA;AAAA,UACA,WAAW;AAAA,YACT;AAAA,YACA,OAAO,eAAe,IAAI,IAAI;AAAA,YAC9B,UAAU;AAAA,YACV,SAAU,YAAY,gBAAgB,UAAW,YAAY,UAAU;AAAA,YACvE;AAAA,YACA;AAAA,YACA,WAAW;AAAA,UAAA;AAAA,UAEb,OAAO,SAAS,OAAO,aAAa,KAAK,IAAI;AAAA,QAAA;AAAA,MAAA;AAAA,MAE9C,UACC,oBAAC,OAAA,EAAI,WAAU,iDAAiD,UAAA,QAAO;AAAA,MAExE,UACC,oBAAC,OAAA,EAAI,WAAW,GAAG,qCAAqC,YAAY,oBAAoB,SAAS,GAC9F,UAAA,OAAA,CACH;AAAA,MAED,aACC;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,UAAU;AAAA,UACV,cAAW;AAAA,UACX,SAAS;AAAA,UACT,WAAU;AAAA,UAEV,UAAA,oBAAC,MAAA,EAAK,MAAM,SAAS,WAAU,UAAA,CAAU;AAAA,QAAA;AAAA,MAAA;AAAA,MAG5C,UACC,oBAAC,QAAA,EAAK,WAAU,iEACd,UAAA,qBAAC,OAAA,EAAI,WAAU,kDAAiD,SAAQ,aAAY,MAAK,QACvF,UAAA;AAAA,QAAA,oBAAC,UAAA,EAAO,IAAG,KAAI,IAAG,KAAI,GAAE,OAAM,QAAO,gBAAe,aAAY,KAAI,SAAQ,QAAO;AAAA,QACnF,oBAAC,UAAK,GAAE,iCAAgC,QAAO,gBAAe,aAAY,KAAI,eAAc,QAAA,CAAQ;AAAA,MAAA,EAAA,CACtG,GACF,IAEA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,UAAU;AAAA,UACV;AAAA,UACA,cAAW;AAAA,UACX,SAAS,MAAM;;AACb,gBAAI,MAAM;AACR,6BAAA;AAAA,YACF,OAAO;AACL,uBAAA;AACA,6BAAS,YAAT,mBAAkB;AAAA,YACpB;AAAA,UACF;AAAA,UACA,WAAU;AAAA,UAEV,UAAA,oBAAC,MAAA,EAAK,MAAM,eAAe,WAAU,UAAA,CAAU;AAAA,QAAA;AAAA,MAAA;AAAA,IACjD,GAEJ;AAAA,IACC,QAAQ,WAAW,OAAO,aAAa,eACtC;AAAA,MACA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,IAAI;AAAA,UACJ,KAAK;AAAA,UACL,MAAK;AAAA,UACL,wBAAsB,YAAY;AAAA,UAClC,OAAO;AAAA,YACL,UAAU;AAAA,YACV,GAAI,QAAQ,OAAO,OAAO,EAAE,KAAK,QAAQ,QAAQ,EAAE,QAAQ,QAAQ,OAAA;AAAA,YACnE,MAAM,QAAQ;AAAA;AAAA;AAAA;AAAA,YAId,UAAU,QAAQ;AAAA,YAClB,UAAU,QAAQ;AAAA,YAClB,WAAW,QAAQ;AAAA,YACnB,QAAQ;AAAA,UAAA;AAAA,UAEV,WAAU;AAAA,UAET,UAAA;AAAA,YAAA,WAAW,SAAS,WAAW,yBAC7B,OAAA,EAAI,WAAU,uDAAsD,UAAA,WAAA,CAAQ;AAAA,YAE9E,CAAC,WAAW,SAAS,WAAW,KAC/B,oBAAC,OAAA,EAAI,WAAU,uDAAsD,UAAA,aAAA,CAAU;AAAA,YAEhF,SAAS,IAAI,CAAC,KAAK,MAAM;;AACxB,oBAAM,WAAW,WAAW,IAAI,KAAK;AAMrC,oBAAM,aAAa,IAAI,SAAS,QAAQ,IAAI,YAAU,cAAS,IAAI,CAAC,MAAd,mBAAiB;AACvE,0CACG,UAAA,EACE,UAAA;AAAA,gBAAA,cACC;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,MAAK;AAAA,oBACL,WAAU;AAAA,oBAET,UAAA,IAAI;AAAA,kBAAA;AAAA,gBAAA;AAAA,gBAGT;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,IAAI,SAAS,GAAG,MAAM,IAAI,CAAC,KAAK;AAAA,oBAChC,MAAK;AAAA,oBACL,iBAAe;AAAA,oBACf,iBAAe,IAAI;AAAA,oBACnB,aAAa,CAAC,MAAM;AAClB,wBAAE,eAAA;AACF,0BAAI,CAAC,IAAI,SAAU,cAAa,GAAG;AAAA,oBACrC;AAAA,oBACA,cAAc,MAAM,eAAe,CAAC;AAAA,oBACpC,WAAW;AAAA,sBACT;AAAA,sBACA,MAAM,eAAe;AAAA,sBACrB,YAAY;AAAA,sBACZ,IAAI,YAAY;AAAA,oBAAA;AAAA,oBAGlB,UAAA;AAAA,sBAAA;AAAA,wBAAC;AAAA,wBAAA;AAAA,0BACC,MAAM;AAAA,0BACN,WAAW,GAAG,oBAAoB,WAAW,gBAAgB,WAAW;AAAA,wBAAA;AAAA,sBAAA;AAAA,0CAEzE,WAAA,EAAU,MAAM,IAAI,MAAM,WAAU,iCAAgC;AAAA,sBACrE,oBAAC,QAAA,EAAK,WAAU,oBAAoB,cAAI,MAAA,CAAM;AAAA,oBAAA;AAAA,kBAAA;AAAA,gBAAA;AAAA,cAChD,EAAA,GAhCa,IAAI,KAiCnB;AAAA,YAEJ,CAAC;AAAA,UAAA;AAAA,QAAA;AAAA,MAAA;AAAA,MAED,SAAS;AAAA,IAAA;AAAA,EACX,GACJ;AAEJ;AAMA,SAAS,aAAa,OAAkB;AACtC,MAAI,OAAO,UAAU,SAAU,QAAO,EAAE,aAAa,UAAA;AACrD,SAAO,EAAE,aAAa,GAAG,KAAK,IAAI,MAAM,SAAS,OAAO,MAAM,CAAC,CAAC,MAAA;AAClE;AAOA,SAAS,oBACP,SACA,gBACkB;AAClB,QAAM,UAAU,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;AACnD,QAAM,SAAS,eACZ,OAAO,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC,EAC7B,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI;AACtC,SAAO,OAAO,WAAW,IAAI,UAAU,CAAC,GAAG,QAAQ,GAAG,OAAO;AAC/D;"}