@dreamtree-org/twreact-ui 1.3.0 → 1.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 (184) hide show
  1. package/README.md +179 -10
  2. package/ai-skills/dreamtree-ui.md +71 -12
  3. package/dist/ai-schema.json +452 -17
  4. package/dist/components/core/Accordion.d.ts.map +1 -1
  5. package/dist/components/core/Button.d.ts.map +1 -1
  6. package/dist/components/core/Checkbox.d.ts.map +1 -1
  7. package/dist/components/core/ColorPicker.d.ts.map +1 -1
  8. package/dist/components/core/DatePicker.d.ts.map +1 -1
  9. package/dist/components/core/DateRangePicker.d.ts.map +1 -1
  10. package/dist/components/core/Form.d.ts.map +1 -1
  11. package/dist/components/core/Input.d.ts.map +1 -1
  12. package/dist/components/core/LocationPicker.d.ts.map +1 -1
  13. package/dist/components/core/Popover.d.ts +4 -0
  14. package/dist/components/core/Popover.d.ts.map +1 -0
  15. package/dist/components/core/PriceRangePicker.d.ts.map +1 -1
  16. package/dist/components/core/ProgressBar.d.ts.map +1 -1
  17. package/dist/components/core/Radio.d.ts.map +1 -1
  18. package/dist/components/core/Rate.d.ts.map +1 -1
  19. package/dist/components/core/Select.d.ts.map +1 -1
  20. package/dist/components/core/Skeleton.d.ts.map +1 -1
  21. package/dist/components/core/Slider.d.ts +4 -0
  22. package/dist/components/core/Slider.d.ts.map +1 -0
  23. package/dist/components/core/SpeechToText.d.ts.map +1 -1
  24. package/dist/components/core/Switch.d.ts.map +1 -1
  25. package/dist/components/core/Table/ActionMenu.d.ts.map +1 -1
  26. package/dist/components/core/Table/ColumnMenu.d.ts +2 -1
  27. package/dist/components/core/Table/ColumnMenu.d.ts.map +1 -1
  28. package/dist/components/core/Table/MobileCard.d.ts +2 -2
  29. package/dist/components/core/Table/MobileCard.d.ts.map +1 -1
  30. package/dist/components/core/Table/Table.d.ts.map +1 -1
  31. package/dist/components/core/Table/TableRow.d.ts +4 -2
  32. package/dist/components/core/Table/TableRow.d.ts.map +1 -1
  33. package/dist/components/core/Tabs.d.ts.map +1 -1
  34. package/dist/components/core/Textarea.d.ts +4 -0
  35. package/dist/components/core/Textarea.d.ts.map +1 -0
  36. package/dist/components/core/ThreeDotPopover.d.ts.map +1 -1
  37. package/dist/components/core/Tooltip.d.ts.map +1 -1
  38. package/dist/components/feedback/Dialog.d.ts.map +1 -1
  39. package/dist/components/feedback/Drawer.d.ts +4 -0
  40. package/dist/components/feedback/Drawer.d.ts.map +1 -0
  41. package/dist/components/feedback/Toast.d.ts +14 -5
  42. package/dist/components/feedback/Toast.d.ts.map +1 -1
  43. package/dist/components/feedback/Tour.d.ts +4 -0
  44. package/dist/components/feedback/Tour.d.ts.map +1 -0
  45. package/dist/components/feedback/TourSpotlight.d.ts +9 -0
  46. package/dist/components/feedback/TourSpotlight.d.ts.map +1 -0
  47. package/dist/components/navigation/Breadcrumbs.d.ts.map +1 -1
  48. package/dist/components/navigation/FootNav.d.ts.map +1 -1
  49. package/dist/components/navigation/Navbar.d.ts.map +1 -1
  50. package/dist/components/navigation/Sidebar.d.ts +8 -2
  51. package/dist/components/navigation/Sidebar.d.ts.map +1 -1
  52. package/dist/components/utility/Badge.d.ts.map +1 -1
  53. package/dist/components/utility/FileUpload.d.ts.map +1 -1
  54. package/dist/components/utility/Sortable.d.ts +4 -0
  55. package/dist/components/utility/Sortable.d.ts.map +1 -0
  56. package/dist/esm/_vendor/object-assign/index.js +99 -0
  57. package/dist/esm/_vendor/prop-types/_vendor/react-is/cjs/react-is.development.js +190 -0
  58. package/dist/esm/_vendor/prop-types/_vendor/react-is/cjs/react-is.production.min.js +26 -0
  59. package/dist/esm/_vendor/prop-types/_vendor/react-is/index.js +19 -0
  60. package/dist/esm/_vendor/prop-types/checkPropTypes.js +115 -0
  61. package/dist/esm/_vendor/prop-types/factoryWithThrowingShims.js +74 -0
  62. package/dist/esm/_vendor/prop-types/factoryWithTypeCheckers.js +625 -0
  63. package/dist/esm/_vendor/prop-types/index.js +30 -0
  64. package/dist/esm/_vendor/prop-types/lib/ReactPropTypesSecret.js +21 -0
  65. package/dist/esm/_vendor/prop-types/lib/has.js +11 -0
  66. package/dist/esm/_virtual/_commonjsHelpers.js +5 -0
  67. package/dist/esm/_virtual/_rollupPluginBabelHelpers.js +329 -0
  68. package/dist/esm/_virtual/index.js +3 -0
  69. package/dist/esm/_virtual/index2.js +3 -0
  70. package/dist/esm/_virtual/react-is.development.js +3 -0
  71. package/dist/esm/_virtual/react-is.production.min.js +3 -0
  72. package/dist/esm/components/blocks/AppShell.js +205 -0
  73. package/dist/esm/components/blocks/ConfirmDialog.js +57 -0
  74. package/dist/esm/components/blocks/EmptyState.js +44 -0
  75. package/dist/esm/components/blocks/ErrorState.js +53 -0
  76. package/dist/esm/components/blocks/LoadingState.js +134 -0
  77. package/dist/esm/components/blocks/PageHeader.js +51 -0
  78. package/dist/esm/components/blocks/StatCard.js +79 -0
  79. package/dist/esm/components/blocks/StatusPill.js +55 -0
  80. package/dist/esm/components/core/Accordion.js +260 -0
  81. package/dist/esm/components/core/Button.js +59 -0
  82. package/dist/esm/components/core/Checkbox.js +188 -0
  83. package/dist/esm/components/core/ColorPicker.js +348 -0
  84. package/dist/esm/components/core/DatePicker.js +436 -0
  85. package/dist/esm/components/core/DateRangePicker.js +494 -0
  86. package/dist/esm/components/core/Form.js +522 -0
  87. package/dist/esm/components/core/Input.js +179 -0
  88. package/dist/esm/components/core/Loader.js +141 -0
  89. package/dist/esm/components/core/LocationPicker.js +161 -0
  90. package/dist/esm/components/core/Popover.js +171 -0
  91. package/dist/esm/components/core/PriceRangePicker.js +376 -0
  92. package/dist/esm/components/core/ProgressBar.js +152 -0
  93. package/dist/esm/components/core/Radio.js +156 -0
  94. package/dist/esm/components/core/Rate.js +197 -0
  95. package/dist/esm/components/core/RoundedTag.js +118 -0
  96. package/dist/esm/components/core/Select.js +857 -0
  97. package/dist/esm/components/core/Skeleton.js +100 -0
  98. package/dist/esm/components/core/Slider.js +134 -0
  99. package/dist/esm/components/core/SpeechToText.js +226 -0
  100. package/dist/esm/components/core/Switch.js +207 -0
  101. package/dist/esm/components/core/Table/ActionMenu.js +251 -0
  102. package/dist/esm/components/core/Table/ColumnMenu.js +55 -0
  103. package/dist/esm/components/core/Table/FieldInput.js +157 -0
  104. package/dist/esm/components/core/Table/MobileCard.js +187 -0
  105. package/dist/esm/components/core/Table/MobileFilters.js +72 -0
  106. package/dist/esm/components/core/Table/Pagination.js +157 -0
  107. package/dist/esm/components/core/Table/Table.js +1533 -0
  108. package/dist/esm/components/core/Table/TableRow.js +254 -0
  109. package/dist/esm/components/core/Table/constants.js +35 -0
  110. package/dist/esm/components/core/Table/helpers.js +124 -0
  111. package/dist/esm/components/core/Table/useClickOutside.js +26 -0
  112. package/dist/esm/components/core/Tabs.js +289 -0
  113. package/dist/esm/components/core/TextToSpeech.js +52 -0
  114. package/dist/esm/components/core/Textarea.js +125 -0
  115. package/dist/esm/components/core/ThreeDotPopover.js +160 -0
  116. package/dist/esm/components/core/Tooltip.js +372 -0
  117. package/dist/esm/components/feedback/Alert.js +96 -0
  118. package/dist/esm/components/feedback/Dialog.js +200 -0
  119. package/dist/esm/components/feedback/Drawer.js +171 -0
  120. package/dist/esm/components/feedback/Toast.js +362 -0
  121. package/dist/esm/components/feedback/Tour.js +214 -0
  122. package/dist/esm/components/feedback/TourSpotlight.js +71 -0
  123. package/dist/esm/components/navigation/Breadcrumbs.js +75 -0
  124. package/dist/esm/components/navigation/Dropdown.js +53 -0
  125. package/dist/esm/components/navigation/FootNav.js +51 -0
  126. package/dist/esm/components/navigation/Navbar.js +437 -0
  127. package/dist/esm/components/navigation/Sidebar.js +567 -0
  128. package/dist/esm/components/utility/Avatar.js +89 -0
  129. package/dist/esm/components/utility/Badge.js +47 -0
  130. package/dist/esm/components/utility/Card.js +85 -0
  131. package/dist/esm/components/utility/Carousel.js +97 -0
  132. package/dist/esm/components/utility/Condition.js +9 -0
  133. package/dist/esm/components/utility/FileUpload.js +177 -0
  134. package/dist/esm/components/utility/Pagination.js +111 -0
  135. package/dist/esm/components/utility/Sortable.js +190 -0
  136. package/dist/esm/components/utility/Stepper.js +101 -0
  137. package/dist/esm/hooks/useApi.js +169 -0
  138. package/dist/esm/hooks/useControllableState.js +32 -0
  139. package/dist/esm/hooks/useFocusTrap.js +85 -0
  140. package/dist/esm/hooks/useLatestRef.js +20 -0
  141. package/dist/esm/hooks/useMixins.js +100 -0
  142. package/dist/esm/hooks/useScrollLock.js +26 -0
  143. package/dist/esm/hooks/useSliderValues.js +135 -0
  144. package/dist/esm/hooks/useSortable.js +188 -0
  145. package/dist/esm/hooks/useTheme.js +53 -0
  146. package/dist/esm/hooks/useTourTarget.js +92 -0
  147. package/dist/esm/index.js +65 -0
  148. package/dist/esm/store/index.js +26 -0
  149. package/dist/esm/store/store.js +107 -0
  150. package/dist/esm/store/storeSlice.js +45 -0
  151. package/dist/esm/utils/cn.js +11 -0
  152. package/dist/esm/utils/emitter.js +169 -0
  153. package/dist/esm/utils/helpers.js +161 -0
  154. package/dist/esm/utils/position.js +134 -0
  155. package/dist/hooks/useApi.d.ts +2 -1
  156. package/dist/hooks/useApi.d.ts.map +1 -1
  157. package/dist/hooks/useControllableState.d.ts +11 -0
  158. package/dist/hooks/useControllableState.d.ts.map +1 -0
  159. package/dist/hooks/useFocusTrap.d.ts +15 -0
  160. package/dist/hooks/useFocusTrap.d.ts.map +1 -0
  161. package/dist/hooks/useLatestRef.d.ts +8 -0
  162. package/dist/hooks/useLatestRef.d.ts.map +1 -0
  163. package/dist/hooks/useMixins.d.ts.map +1 -1
  164. package/dist/hooks/useScrollLock.d.ts +2 -0
  165. package/dist/hooks/useScrollLock.d.ts.map +1 -0
  166. package/dist/hooks/useSliderValues.d.ts +43 -0
  167. package/dist/hooks/useSliderValues.d.ts.map +1 -0
  168. package/dist/hooks/useSortable.d.ts +32 -0
  169. package/dist/hooks/useSortable.d.ts.map +1 -0
  170. package/dist/hooks/useTheme.d.ts.map +1 -1
  171. package/dist/hooks/useTourTarget.d.ts +15 -0
  172. package/dist/hooks/useTourTarget.d.ts.map +1 -0
  173. package/dist/index.css +1 -1
  174. package/dist/index.d.ts +8 -2
  175. package/dist/index.esm.css +1 -1
  176. package/dist/index.esm.js +4800 -2212
  177. package/dist/index.js +4805 -2210
  178. package/dist/types/index.d.ts +193 -5
  179. package/dist/types/index.d.ts.map +1 -1
  180. package/dist/utils/position.d.ts +30 -0
  181. package/dist/utils/position.d.ts.map +1 -0
  182. package/doc/README.md +7 -1
  183. package/mcp/catalog.snapshot.json +911 -212
  184. package/package.json +14 -2
@@ -0,0 +1,857 @@
1
+ 'use client';
2
+ import { objectWithoutProperties as _objectWithoutProperties, slicedToArray as _slicedToArray, objectSpread2 as _objectSpread2, toConsumableArray as _toConsumableArray, typeof as _typeof, defineProperty as _defineProperty } from '../../_virtual/_rollupPluginBabelHelpers.js';
3
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
+ import React, { useState, useRef, useImperativeHandle, useId, useMemo, useEffect, useLayoutEffect } from 'react';
5
+ import { createPortal } from 'react-dom';
6
+ import { cn } from '../../utils/cn.js';
7
+ import { useLatestRef } from '../../hooks/useLatestRef.js';
8
+ import { ChevronDown, X, Check, Search, Plus } from 'lucide-react';
9
+
10
+ var _excluded = ["options", "value", "onChange", "placeholder", "label", "error", "helperText", "disabled", "required", "multiSelect", "searchable", "grouped", "onMenuItemRender", "className", "allowClear", "creatable", "onCreateOption", "onSearch", "loading", "selectAllOption", "closeOnSelect", "maxTagCount", "renderGroupLabel", "name"];
11
+ /**
12
+ * Enhanced Select component (debounce removed)
13
+ * - Supports single and multi select
14
+ * - Searchable with immediate onSearch calls (no debounce)
15
+ * - Creatable (add option) when no match
16
+ * - Select All / Deselect All for multiSelect
17
+ * - Clear button
18
+ * - Grouped options with optional collapsing
19
+ * - ARIA & keyboard accessibility
20
+ * - Custom render per option via onMenuItemRender
21
+ * - Max tag count with overflow indicator
22
+ * - Auto-flip dropdown if not enough space below; use above instead
23
+ * - Adjusts dropdown max-height to available space
24
+ */
25
+ var isPlainObject = function isPlainObject(v) {
26
+ return v !== null && _typeof(v) === "object";
27
+ };
28
+ /**
29
+ * Normalize the `options` prop for `grouped` mode into
30
+ * `[{ label, options: [...] }]`. Accepts, in any mix:
31
+ * - group objects: `{ label, options: [{ value, label }] }`
32
+ * - flat options carrying a `group` key: `{ value, label, group }` — bucketed
33
+ * by `group` in first-seen order (no `group` → an unlabelled bucket)
34
+ * Malformed entries (null, primitives, `{ label, options: <not an array> }`
35
+ * without a `value`) are skipped instead of throwing; `malformed` reports
36
+ * whether anything was dropped so the caller can warn once.
37
+ */
38
+ var normalizeGroups = function normalizeGroups(options) {
39
+ var groups = [];
40
+ var malformed = !Array.isArray(options);
41
+ if (malformed) return {
42
+ groups: groups,
43
+ malformed: malformed
44
+ };
45
+ var buckets = new Map();
46
+ var bucketFor = function bucketFor(key) {
47
+ if (!buckets.has(key)) {
48
+ var g = {
49
+ label: key !== null && key !== void 0 ? key : undefined,
50
+ options: [],
51
+ _ungrouped: key == null
52
+ };
53
+ buckets.set(key, g);
54
+ groups.push(g);
55
+ }
56
+ return buckets.get(key);
57
+ };
58
+ options.forEach(function (item) {
59
+ if (!isPlainObject(item)) {
60
+ malformed = true;
61
+ return;
62
+ }
63
+ if (Array.isArray(item.options)) {
64
+ var valid = item.options.filter(isPlainObject);
65
+ if (valid.length !== item.options.length) malformed = true;
66
+ groups.push(_objectSpread2(_objectSpread2({}, item), {}, {
67
+ options: valid
68
+ }));
69
+ return;
70
+ }
71
+ if ("value" in item) {
72
+ var _item$group;
73
+ bucketFor((_item$group = item.group) !== null && _item$group !== void 0 ? _item$group : null).options.push(item);
74
+ return;
75
+ }
76
+ malformed = true;
77
+ });
78
+ return {
79
+ groups: groups,
80
+ malformed: malformed
81
+ };
82
+ };
83
+ var flattenGroups = function flattenGroups() {
84
+ var groups = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
85
+ // groups: [{ label, options: [{value,label,disabled}] }] (already normalized)
86
+ return groups.reduce(function (acc, g) {
87
+ var groupLabel = g.label;
88
+ g.options.forEach(function (opt) {
89
+ return acc.push(_objectSpread2(_objectSpread2({}, opt), {}, {
90
+ _group: groupLabel
91
+ }));
92
+ });
93
+ return acc;
94
+ }, []);
95
+ };
96
+ var highlight = function highlight() {
97
+ var label = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "";
98
+ var term = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "";
99
+ if (!term) return label;
100
+ var idx = label.toLowerCase().indexOf(term.toLowerCase());
101
+ if (idx === -1) return label;
102
+ return jsxs(Fragment, {
103
+ children: [label.slice(0, idx), jsx("mark", {
104
+ className: "bg-yellow-100 font-medium",
105
+ children: label.slice(idx, idx + term.length)
106
+ }), label.slice(idx + term.length)]
107
+ });
108
+ };
109
+ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
110
+ var _ref$options = _ref.options,
111
+ options = _ref$options === void 0 ? [] : _ref$options,
112
+ value = _ref.value,
113
+ onChange = _ref.onChange,
114
+ _ref$placeholder = _ref.placeholder,
115
+ placeholder = _ref$placeholder === void 0 ? "Select an option..." : _ref$placeholder,
116
+ label = _ref.label,
117
+ error = _ref.error,
118
+ helperText = _ref.helperText,
119
+ disabled = _ref.disabled,
120
+ required = _ref.required,
121
+ _ref$multiSelect = _ref.multiSelect,
122
+ multiSelect = _ref$multiSelect === void 0 ? false : _ref$multiSelect,
123
+ _ref$searchable = _ref.searchable,
124
+ searchable = _ref$searchable === void 0 ? false : _ref$searchable,
125
+ _ref$grouped = _ref.grouped,
126
+ grouped = _ref$grouped === void 0 ? false : _ref$grouped,
127
+ onMenuItemRender = _ref.onMenuItemRender,
128
+ className = _ref.className,
129
+ _ref$allowClear = _ref.allowClear,
130
+ allowClear = _ref$allowClear === void 0 ? true : _ref$allowClear,
131
+ _ref$creatable = _ref.creatable,
132
+ creatable = _ref$creatable === void 0 ? false : _ref$creatable,
133
+ onCreateOption = _ref.onCreateOption,
134
+ onSearch = _ref.onSearch,
135
+ _ref$loading = _ref.loading,
136
+ loading = _ref$loading === void 0 ? false : _ref$loading,
137
+ _ref$selectAllOption = _ref.selectAllOption,
138
+ selectAllOption = _ref$selectAllOption === void 0 ? true : _ref$selectAllOption,
139
+ _ref$closeOnSelect = _ref.closeOnSelect,
140
+ closeOnSelect = _ref$closeOnSelect === void 0 ? false : _ref$closeOnSelect,
141
+ _ref$maxTagCount = _ref.maxTagCount,
142
+ maxTagCount = _ref$maxTagCount === void 0 ? 3 : _ref$maxTagCount,
143
+ renderGroupLabel = _ref.renderGroupLabel,
144
+ name = _ref.name,
145
+ props = _objectWithoutProperties(_ref, _excluded);
146
+ var _useState = useState(false),
147
+ _useState2 = _slicedToArray(_useState, 2),
148
+ isOpen = _useState2[0],
149
+ setIsOpen = _useState2[1];
150
+ var _useState3 = useState(""),
151
+ _useState4 = _slicedToArray(_useState3, 2),
152
+ searchTerm = _useState4[0],
153
+ setSearchTerm = _useState4[1];
154
+ var _useState5 = useState(-1),
155
+ _useState6 = _slicedToArray(_useState5, 2),
156
+ focusedIndex = _useState6[0],
157
+ setFocusedIndex = _useState6[1];
158
+ var _useState7 = useState({}),
159
+ _useState8 = _slicedToArray(_useState7, 2),
160
+ collapsedGroups = _useState8[0],
161
+ setCollapsedGroups = _useState8[1];
162
+ var selectRef = useRef(null);
163
+ var triggerRef = useRef(null);
164
+ var searchRef = useRef(null);
165
+ var menuRef = useRef(null);
166
+ var listboxRef = useRef(null);
167
+ // New: dropdown placement & max height
168
+ var _useState9 = useState("bottom"),
169
+ _useState0 = _slicedToArray(_useState9, 2),
170
+ placement = _useState0[0],
171
+ setPlacement = _useState0[1]; // "bottom" | "top"
172
+ var _useState1 = useState(null),
173
+ _useState10 = _slicedToArray(_useState1, 2),
174
+ menuMaxHeight = _useState10[0],
175
+ setMenuMaxHeight = _useState10[1]; // px or null
176
+ // Fixed-position coordinates for the portaled menu (anchored to the trigger).
177
+ var _useState11 = useState(null),
178
+ _useState12 = _slicedToArray(_useState11, 2),
179
+ menuCoords = _useState12[0],
180
+ setMenuCoords = _useState12[1]; // { left, top, width } in viewport px
181
+ useImperativeHandle(forwardedRef, function () {
182
+ return selectRef.current;
183
+ }, []);
184
+ // Stable ids wiring label / helper / error / listbox to the trigger.
185
+ // A consumer `id` (spread onto the trigger via ...props) wins.
186
+ var reactId = useId();
187
+ var baseId = props.id || "select-".concat(reactId);
188
+ var labelId = "".concat(baseId, "-label");
189
+ var valueId = "".concat(baseId, "-value");
190
+ var listboxId = "".concat(baseId, "-listbox");
191
+ var errorId = "".concat(baseId, "-error");
192
+ var helperId = "".concat(baseId, "-helper");
193
+ var describedBy = error ? errorId : helperText ? helperId : undefined;
194
+ // grouped mode: tolerate flat options with a `group` key and malformed
195
+ // input rather than crashing the page.
196
+ var _useMemo = useMemo(function () {
197
+ return grouped ? normalizeGroups(options) : {
198
+ groups: [],
199
+ malformed: false
200
+ };
201
+ }, [options, grouped]),
202
+ groupedOptions = _useMemo.groups,
203
+ groupedMalformed = _useMemo.malformed;
204
+ useEffect(function () {
205
+ if (groupedMalformed) {
206
+ console.warn("[Select] `grouped` expects `[{ label, options: [...] }]` or flat options with a `group` key; malformed entries were ignored.");
207
+ }
208
+ }, [groupedMalformed]);
209
+ // normalize options to flat list for keyboard nav, but keep groups for UI
210
+ var flatOptions = useMemo(function () {
211
+ if (grouped) return flattenGroups(groupedOptions);
212
+ return (Array.isArray(options) ? options : []).filter(isPlainObject).map(function (opt) {
213
+ return _objectSpread2({}, opt);
214
+ });
215
+ }, [options, grouped, groupedOptions]);
216
+ var selectedValues = useMemo(function () {
217
+ return multiSelect ? Array.isArray(value) ? value : [] : value;
218
+ }, [multiSelect, value]);
219
+ var selectedOptions = useMemo(function () {
220
+ if (multiSelect) return flatOptions.filter(function (o) {
221
+ return selectedValues.includes(o.value);
222
+ });
223
+ return flatOptions.find(function (o) {
224
+ return o.value === selectedValues;
225
+ }) ? [flatOptions.find(function (o) {
226
+ return o.value === selectedValues;
227
+ })] : [];
228
+ }, [flatOptions, selectedValues, multiSelect]);
229
+ // Call onSearch immediately on every change (debounce removed)
230
+ var onSearchRef = useLatestRef(onSearch);
231
+ useEffect(function () {
232
+ var _onSearchRef$current;
233
+ (_onSearchRef$current = onSearchRef.current) === null || _onSearchRef$current === void 0 || _onSearchRef$current.call(onSearchRef, searchTerm);
234
+ }, [searchTerm, onSearchRef]);
235
+ // compute filtered structure for grouped or flat
236
+ var filtered = useMemo(function () {
237
+ if (!searchable || !searchTerm) {
238
+ return grouped ? groupedOptions : flatOptions;
239
+ }
240
+ var term = searchTerm.toLowerCase();
241
+ var matches = function matches(o) {
242
+ var _o$label;
243
+ return String((_o$label = o.label) !== null && _o$label !== void 0 ? _o$label : "").toLowerCase().includes(term);
244
+ };
245
+ if (grouped) {
246
+ return groupedOptions.map(function (g) {
247
+ return _objectSpread2(_objectSpread2({}, g), {}, {
248
+ options: g.options.filter(matches)
249
+ });
250
+ }).filter(function (g) {
251
+ return g.options.length > 0;
252
+ });
253
+ }
254
+ return flatOptions.filter(matches);
255
+ }, [searchable, searchTerm, grouped, groupedOptions, flatOptions]);
256
+ // The options the keyboard walks: exactly the rows on screen (search
257
+ // filter applied, collapsed groups skipped), in render order. Index `i`
258
+ // here is option row `optionId(i)` in the DOM.
259
+ var navOptions = useMemo(function () {
260
+ if (!grouped) return filtered;
261
+ return filtered.reduce(function (acc, g) {
262
+ if (!collapsedGroups[g.label]) acc.push.apply(acc, _toConsumableArray(g.options));
263
+ return acc;
264
+ }, []);
265
+ }, [grouped, filtered, collapsedGroups]);
266
+ var optionId = function optionId(index) {
267
+ return "".concat(baseId, "-option-").concat(index);
268
+ };
269
+ var activeDescendant = isOpen && focusedIndex >= 0 && focusedIndex < navOptions.length ? optionId(focusedIndex) : undefined;
270
+ // A new search term or a collapsed group changes what the rows are, so
271
+ // the active row no longer means the same option: start over.
272
+ useEffect(function () {
273
+ setFocusedIndex(-1);
274
+ }, [searchTerm, collapsedGroups]);
275
+ // Keep the active option visible while arrowing through a long list.
276
+ useEffect(function () {
277
+ var _el$scrollIntoView;
278
+ if (!activeDescendant || typeof document === "undefined") return;
279
+ var el = document.getElementById(activeDescendant);
280
+ el === null || el === void 0 || (_el$scrollIntoView = el.scrollIntoView) === null || _el$scrollIntoView === void 0 || _el$scrollIntoView.call(el, {
281
+ block: "nearest"
282
+ });
283
+ }, [activeDescendant]);
284
+ // ===== Placement/space calculation =====
285
+ var calculatePlacement = function calculatePlacement() {
286
+ var MARGIN = 8; // px margin from viewport edges
287
+ var DEFAULT_MENU_MAX = 240; // fallback max (approx 15rem)
288
+ if (!selectRef.current) return;
289
+ var rect = selectRef.current.getBoundingClientRect();
290
+ var viewportH = window.innerHeight;
291
+ var spaceBelow = viewportH - rect.bottom - MARGIN;
292
+ var spaceAbove = rect.top - MARGIN;
293
+ // desired approximate menu height (we can try to measure menu if already rendered)
294
+ var desiredMenuHeight = DEFAULT_MENU_MAX;
295
+ if (menuRef.current) {
296
+ // when menu exists, use its scrollHeight as desired
297
+ desiredMenuHeight = Math.min(menuRef.current.scrollHeight, DEFAULT_MENU_MAX);
298
+ }
299
+ // decide placement: prefer bottom unless not enough space and top has more space
300
+ var nextPlacement;
301
+ var nextMaxHeight;
302
+ if (spaceBelow >= desiredMenuHeight || spaceBelow >= spaceAbove) {
303
+ nextPlacement = "bottom";
304
+ nextMaxHeight = Math.min(desiredMenuHeight, Math.max(80, spaceBelow));
305
+ } else {
306
+ nextPlacement = "top";
307
+ nextMaxHeight = Math.min(desiredMenuHeight, Math.max(80, spaceAbove));
308
+ }
309
+ setPlacement(nextPlacement);
310
+ setMenuMaxHeight(nextMaxHeight);
311
+ // The menu is portaled to document.body with position:fixed, so it floats
312
+ // above any ancestor with overflow:hidden|auto|scroll. Anchor it to the
313
+ // trigger via the viewport-relative rect (no scroll offset for fixed).
314
+ // maxHeight here mirrors the inline style applied to the menu (+10 fudge).
315
+ var renderedMaxHeight = nextMaxHeight + 10;
316
+ var next = {
317
+ left: rect.left,
318
+ width: rect.width
319
+ };
320
+ if (nextPlacement === "bottom") {
321
+ next.top = rect.bottom + 4; // ~mt-1
322
+ } else {
323
+ // place above the trigger; bottom of menu sits just above the trigger top
324
+ next.top = Math.max(MARGIN, rect.top - 4 - renderedMaxHeight);
325
+ }
326
+ setMenuCoords(next);
327
+ };
328
+ // Anchor the portaled menu BEFORE the browser paints, so it never flashes
329
+ // at (0,0). The menu is already in the DOM (createPortal runs during render),
330
+ // we just need its fixed coords set synchronously.
331
+ useLayoutEffect(function () {
332
+ if (isOpen) calculatePlacement();
333
+ // eslint-disable-next-line react-hooks/exhaustive-deps
334
+ }, [isOpen, filtered.length]);
335
+ useEffect(function () {
336
+ if (!isOpen) {
337
+ // drop stale coords so the next open re-anchors from scratch
338
+ setMenuCoords(null);
339
+ return;
340
+ }
341
+ // initial calc on open; wait a tick to allow DOM to render
342
+ var raf = requestAnimationFrame(function () {
343
+ var _searchRef$current, _listboxRef$current;
344
+ calculatePlacement();
345
+ // Move focus into the popup (WAI-ARIA collapsible listbox): the search
346
+ // box when searchable, otherwise the listbox itself. Both carry
347
+ // aria-activedescendant and the same key handling as the trigger.
348
+ var inMenu = menuRef.current && menuRef.current.contains(document.activeElement);
349
+ if (searchable) (_searchRef$current = searchRef.current) === null || _searchRef$current === void 0 || _searchRef$current.focus();else if (!inMenu) (_listboxRef$current = listboxRef.current) === null || _listboxRef$current === void 0 || _listboxRef$current.focus();
350
+ });
351
+ // Reposition on resize/scroll while open (throttled by rAF). Use
352
+ // capture-phase scroll so we also catch any scrolling ancestor — the
353
+ // menu is portaled with position:fixed, so its coords must track the
354
+ // trigger as the page (or a scroll container) moves.
355
+ var rafId = null;
356
+ var onWindowChange = function onWindowChange() {
357
+ if (rafId) cancelAnimationFrame(rafId);
358
+ rafId = requestAnimationFrame(function () {
359
+ return calculatePlacement();
360
+ });
361
+ };
362
+ window.addEventListener("resize", onWindowChange, {
363
+ passive: true
364
+ });
365
+ window.addEventListener("scroll", onWindowChange, {
366
+ passive: true,
367
+ capture: true
368
+ });
369
+ return function () {
370
+ cancelAnimationFrame(raf);
371
+ if (rafId) cancelAnimationFrame(rafId);
372
+ window.removeEventListener("resize", onWindowChange);
373
+ window.removeEventListener("scroll", onWindowChange, {
374
+ capture: true
375
+ });
376
+ };
377
+ // eslint-disable-next-line react-hooks/exhaustive-deps
378
+ }, [isOpen, searchable, filtered.length]);
379
+ // ===== handlers =====
380
+ // Close the popup. When focus is inside it (the portaled menu unmounts),
381
+ // hand focus back to the trigger so keyboard users are not dropped on
382
+ // <body>.
383
+ var closeMenu = function closeMenu() {
384
+ var _triggerRef$current;
385
+ var focusInMenu = typeof document !== "undefined" && menuRef.current && menuRef.current.contains(document.activeElement);
386
+ setIsOpen(false);
387
+ setFocusedIndex(-1);
388
+ if (focusInMenu) (_triggerRef$current = triggerRef.current) === null || _triggerRef$current === void 0 || _triggerRef$current.focus();
389
+ };
390
+ var handleSelect = function handleSelect(option) {
391
+ if (option.disabled) return;
392
+ if (multiSelect) {
393
+ var exists = selectedValues.includes(option.value);
394
+ var newValues = exists ? selectedValues.filter(function (v) {
395
+ return v !== option.value;
396
+ }) : [].concat(_toConsumableArray(selectedValues), [option.value]);
397
+ onChange === null || onChange === void 0 || onChange(newValues);
398
+ if (closeOnSelect) closeMenu();
399
+ } else {
400
+ onChange === null || onChange === void 0 || onChange(option.value);
401
+ closeMenu();
402
+ }
403
+ };
404
+ var handleClear = function handleClear(e) {
405
+ var _triggerRef$current2;
406
+ e.stopPropagation();
407
+ if (multiSelect) onChange === null || onChange === void 0 || onChange([]);else onChange === null || onChange === void 0 || onChange(null);
408
+ (_triggerRef$current2 = triggerRef.current) === null || _triggerRef$current2 === void 0 || _triggerRef$current2.focus();
409
+ };
410
+ var handleCreate = function handleCreate() {
411
+ if (!onCreateOption) return;
412
+ onCreateOption({
413
+ value: searchTerm,
414
+ setIsOpen: setIsOpen
415
+ });
416
+ setSearchTerm("");
417
+ };
418
+ var handleSelectAll = function handleSelectAll() {
419
+ if (!multiSelect) return;
420
+ var allVals = flatOptions.filter(function (o) {
421
+ return !o.disabled;
422
+ }).map(function (o) {
423
+ return o.value;
424
+ });
425
+ var allSelected = allVals.every(function (v) {
426
+ return selectedValues.includes(v);
427
+ });
428
+ onChange === null || onChange === void 0 || onChange(allSelected ? [] : allVals);
429
+ };
430
+ var handleKeyDown = function handleKeyDown(e) {
431
+ var _e$target;
432
+ if (!isOpen) {
433
+ if (e.key === "Enter" || e.key === " ") {
434
+ e.preventDefault();
435
+ setIsOpen(true);
436
+ setTimeout(function () {
437
+ var _searchRef$current2;
438
+ return searchable ? (_searchRef$current2 = searchRef.current) === null || _searchRef$current2 === void 0 ? void 0 : _searchRef$current2.focus() : null;
439
+ }, 0);
440
+ }
441
+ return;
442
+ }
443
+ // Keyboard navigation walks the rows actually shown (search-filtered,
444
+ // collapsed groups skipped), so the active row is always visible.
445
+ var currentList = navOptions;
446
+ var last = currentList.length - 1;
447
+ var inTextBox = e.target === searchRef.current;
448
+ // A real button inside the listbox (group toggle) keeps its own
449
+ // Enter / Space activation.
450
+ if ((e.key === "Enter" || e.key === " ") && e.target !== e.currentTarget && ((_e$target = e.target) === null || _e$target === void 0 ? void 0 : _e$target.tagName) === "BUTTON") {
451
+ return;
452
+ }
453
+ switch (e.key) {
454
+ case "ArrowDown":
455
+ e.preventDefault();
456
+ if (last < 0) break;
457
+ setFocusedIndex(function (prev) {
458
+ return prev < last ? prev + 1 : 0;
459
+ });
460
+ break;
461
+ case "ArrowUp":
462
+ e.preventDefault();
463
+ if (last < 0) break;
464
+ setFocusedIndex(function (prev) {
465
+ return prev > 0 ? prev - 1 : last;
466
+ });
467
+ break;
468
+ case "Home":
469
+ // In the search box Home/End keep moving the caret.
470
+ if (inTextBox || last < 0) break;
471
+ e.preventDefault();
472
+ setFocusedIndex(0);
473
+ break;
474
+ case "End":
475
+ if (inTextBox || last < 0) break;
476
+ e.preventDefault();
477
+ setFocusedIndex(last);
478
+ break;
479
+ case "Enter":
480
+ e.preventDefault();
481
+ if (focusedIndex >= 0 && currentList[focusedIndex]) {
482
+ handleSelect(currentList[focusedIndex]);
483
+ } else if (inTextBox && creatable && searchTerm && onCreateOption) {
484
+ handleCreate();
485
+ }
486
+ break;
487
+ case " ":
488
+ // Space toggles the active option from the listbox (multi-select
489
+ // pattern); in the search box it types a space; on the trigger it
490
+ // keeps its native button behaviour.
491
+ if (e.target !== listboxRef.current) break;
492
+ e.preventDefault();
493
+ if (focusedIndex >= 0 && currentList[focusedIndex]) {
494
+ handleSelect(currentList[focusedIndex]);
495
+ }
496
+ break;
497
+ case "Escape":
498
+ closeMenu();
499
+ break;
500
+ case "Tab":
501
+ // Leaving the (portaled) popup: close it and continue the tab order
502
+ // from the trigger rather than from the end of <body>.
503
+ closeMenu();
504
+ break;
505
+ }
506
+ };
507
+ var handleClickOutside = function handleClickOutside(e) {
508
+ // The menu is portaled to document.body, so it lives OUTSIDE selectRef's
509
+ // DOM subtree. Treat clicks inside the portaled menu as "inside" too —
510
+ // otherwise selecting an option would close the menu before the option's
511
+ // own click handler runs.
512
+ var inTrigger = selectRef.current && selectRef.current.contains(e.target);
513
+ var inMenu = menuRef.current && menuRef.current.contains(e.target);
514
+ if (!inTrigger && !inMenu) {
515
+ setIsOpen(false);
516
+ setFocusedIndex(-1);
517
+ }
518
+ };
519
+ useEffect(function () {
520
+ document.addEventListener("mousedown", handleClickOutside);
521
+ return function () {
522
+ return document.removeEventListener("mousedown", handleClickOutside);
523
+ };
524
+ }, []);
525
+ var showClearButton = allowClear && !disabled && selectedOptions.length > 0;
526
+ var displayValue = function displayValue() {
527
+ var _selectedOptions$;
528
+ if (multiSelect) {
529
+ if (selectedOptions.length === 0) return placeholder;
530
+ if (selectedOptions.length <= maxTagCount) return jsx("div", {
531
+ className: "flex flex-wrap gap-1",
532
+ children: selectedOptions.map(function (opt) {
533
+ return jsxs("div", {
534
+ className: "flex items-center gap-1 rounded-full bg-gray-100 px-2 py-0.5 text-xs",
535
+ children: [jsx("span", {
536
+ className: "truncate max-w-[8rem]",
537
+ children: opt.label
538
+ }), jsx("div", {
539
+ "aria-label": "Remove ".concat(opt.label),
540
+ onClick: function onClick(e) {
541
+ e.stopPropagation();
542
+ handleSelect(opt);
543
+ },
544
+ children: jsx(X, {
545
+ className: "h-3 w-3"
546
+ })
547
+ })]
548
+ }, opt.value);
549
+ })
550
+ });
551
+ // overflow indicator
552
+ var rest = selectedOptions.length - maxTagCount;
553
+ return jsxs("div", {
554
+ className: "flex items-center gap-2",
555
+ children: [jsx("div", {
556
+ className: "flex flex-wrap gap-1",
557
+ children: selectedOptions.slice(0, maxTagCount).map(function (opt) {
558
+ return jsx("div", {
559
+ className: "flex items-center gap-1 rounded-full bg-gray-100 px-2 py-0.5 text-xs",
560
+ children: jsx("span", {
561
+ className: "truncate max-w-[6rem]",
562
+ children: opt.label
563
+ })
564
+ }, opt.value);
565
+ })
566
+ }), jsxs("div", {
567
+ className: "text-xs text-primary-500",
568
+ children: ["+", rest]
569
+ })]
570
+ });
571
+ }
572
+ return ((_selectedOptions$ = selectedOptions[0]) === null || _selectedOptions$ === void 0 ? void 0 : _selectedOptions$.label) || placeholder;
573
+ };
574
+ var renderGroupedOptions = function renderGroupedOptions() {
575
+ // Running index into `navOptions` (render order, collapsed skipped).
576
+ var navIndex = -1;
577
+ return jsx("div", {
578
+ className: "divide-y",
579
+ children: filtered.map(function (group, gi) {
580
+ var collapsed = !!collapsedGroups[group.label];
581
+ var groupOptionsId = "".concat(baseId, "-group-").concat(gi);
582
+ var groupName = typeof group.label === "string" ? group.label : "group ".concat(gi + 1);
583
+ return jsxs("div", {
584
+ role: group._ungrouped ? undefined : "group",
585
+ "aria-label": group._ungrouped ? undefined : groupName,
586
+ children: [!group._ungrouped && jsxs("div", {
587
+ className: "flex items-center justify-between px-3 py-2 text-sm font-medium",
588
+ children: [jsx("div", {
589
+ "aria-hidden": "true",
590
+ children: renderGroupLabel ? renderGroupLabel(group.label) : group.label
591
+ }), jsx("button", {
592
+ type: "button",
593
+ "aria-expanded": !collapsed,
594
+ "aria-controls": collapsed ? undefined : groupOptionsId,
595
+ "aria-label": "".concat(collapsed ? "Expand" : "Collapse", " ").concat(groupName),
596
+ className: "text-xs border border-slate-200 p-0.5 rounded-md cursor-pointer hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500",
597
+ onClick: function onClick() {
598
+ return setCollapsedGroups(function (prev) {
599
+ return _objectSpread2(_objectSpread2({}, prev), {}, _defineProperty({}, group.label, !prev[group.label]));
600
+ });
601
+ },
602
+ children: jsx(ChevronDown, {
603
+ "aria-hidden": "true",
604
+ className: cn("h-4 w-4 text-primary-400 transition-transform", {
605
+ "rotate-180": collapsed
606
+ })
607
+ })
608
+ })]
609
+ }), !collapsed && jsx("div", {
610
+ className: "pl-2",
611
+ id: groupOptionsId,
612
+ children: group.options.map(function (option) {
613
+ navIndex += 1;
614
+ var index = navIndex;
615
+ return jsxs("div", {
616
+ id: optionId(index),
617
+ role: "option",
618
+ "aria-selected": multiSelect ? selectedValues.includes(option.value) : selectedValues === option.value,
619
+ "aria-disabled": option.disabled ? true : undefined,
620
+ onClick: function onClick() {
621
+ return handleSelect(option);
622
+ },
623
+ className: cn("flex items-center px-3 py-2 text-sm hover:bg-gray-100 cursor-pointer", {
624
+ "opacity-50 cursor-not-allowed": option.disabled,
625
+ "bg-primary-50 text-primary-600": focusedIndex === index
626
+ }),
627
+ children: [multiSelect && jsx("div", {
628
+ className: "mr-2",
629
+ children: selectedValues.includes(option.value) ? jsx(Check, {
630
+ className: "h-4 w-4 text-primary-600"
631
+ }) : jsx("div", {
632
+ className: "h-4 w-4 rounded border border-gray-300"
633
+ })
634
+ }), onMenuItemRender ? onMenuItemRender(option) : highlight(option.label, searchTerm), !multiSelect && selectedValues === option.value && jsx(Check, {
635
+ className: "ml-auto h-4 w-4 text-primary-600"
636
+ })]
637
+ }, option.value);
638
+ })
639
+ })]
640
+ }, group.label || gi);
641
+ })
642
+ });
643
+ };
644
+ var renderFlatOptions = function renderFlatOptions() {
645
+ return jsxs("div", {
646
+ children: [loading ? jsx("div", {
647
+ className: "px-3 py-2 text-sm",
648
+ children: "Loading..."
649
+ }) : Array.isArray(filtered) && filtered.length === 0 ? jsx("div", {
650
+ className: "px-3 py-2 text-sm text-primary-500",
651
+ children: "No options found"
652
+ }) : (grouped ? flattenGroups(filtered) : filtered).map(function (option, index) {
653
+ return jsxs("div", {
654
+ id: optionId(index),
655
+ role: "option",
656
+ "aria-selected": multiSelect ? selectedValues.includes(option.value) : selectedValues === option.value,
657
+ "aria-disabled": option.disabled ? true : undefined,
658
+ onClick: function onClick() {
659
+ return handleSelect(option);
660
+ },
661
+ className: cn("flex items-center px-3 py-2 text-sm hover:bg-gray-100 cursor-pointer", {
662
+ "opacity-50 cursor-not-allowed": option.disabled,
663
+ "bg-primary-50 text-primary-600": focusedIndex === index,
664
+ "bg-primary-100": multiSelect && selectedValues.includes(option.value)
665
+ }),
666
+ children: [multiSelect && jsx("div", {
667
+ className: "mr-2",
668
+ children: selectedValues.includes(option.value) ? jsx(Check, {
669
+ className: "h-4 w-4 text-primary-600"
670
+ }) : jsx("div", {
671
+ className: "h-4 w-4 rounded border border-gray-300"
672
+ })
673
+ }), onMenuItemRender ? onMenuItemRender(option) : highlight(option.label, searchTerm), !multiSelect && selectedValues === option.value && jsx(Check, {
674
+ className: "ml-auto h-4 w-4 text-primary-600"
675
+ })]
676
+ }, option.value);
677
+ }), creatable && searchTerm && !loading && jsx("div", {
678
+ className: "border-t text-sm",
679
+ role: "presentation",
680
+ children: jsxs("button", {
681
+ type: "button",
682
+ onClick: handleCreate,
683
+ className: "flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-gray-100 focus:outline-none focus:bg-gray-100",
684
+ children: [jsx(Plus, {
685
+ className: "h-4 w-4",
686
+ "aria-hidden": "true"
687
+ }), "Add \"", searchTerm, "\""]
688
+ })
689
+ })]
690
+ });
691
+ };
692
+ return jsxs("div", {
693
+ className: "w-full",
694
+ children: [label && jsxs("label", {
695
+ id: labelId,
696
+ htmlFor: baseId,
697
+ className: "block text-sm font-medium mb-1",
698
+ children: [label, required && jsx("span", {
699
+ className: "text-error-500 ml-1",
700
+ children: "*"
701
+ })]
702
+ }), jsxs("div", {
703
+ ref: selectRef,
704
+ className: "relative",
705
+ children: [jsxs("button", _objectSpread2(_objectSpread2({
706
+ ref: triggerRef,
707
+ type: "button",
708
+ id: baseId,
709
+ "aria-haspopup": "listbox",
710
+ "aria-expanded": isOpen,
711
+ "aria-controls": isOpen ? listboxId : undefined,
712
+ "aria-labelledby": label ? "".concat(labelId, " ").concat(valueId) : valueId,
713
+ "aria-describedby": describedBy,
714
+ "aria-invalid": error ? true : undefined,
715
+ className: cn("flex w-full items-center justify-between rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:cursor-not-allowed disabled:opacity-50", {
716
+ "border-error-500 focus:ring-error-500": error,
717
+ "text-primary-500": !selectedOptions.length,
718
+ "pr-14": showClearButton
719
+ }, className),
720
+ onClick: function onClick() {
721
+ if (!disabled) {
722
+ setIsOpen(function (prev) {
723
+ return !prev;
724
+ });
725
+ setTimeout(function () {
726
+ var _searchRef$current3;
727
+ return searchable ? (_searchRef$current3 = searchRef.current) === null || _searchRef$current3 === void 0 ? void 0 : _searchRef$current3.focus() : null;
728
+ }, 0);
729
+ }
730
+ },
731
+ onKeyDown: handleKeyDown,
732
+ disabled: disabled
733
+ }, props), {}, {
734
+ children: [jsx("span", {
735
+ className: "truncate flex-1 text-left",
736
+ id: valueId,
737
+ children: displayValue()
738
+ }), jsx(ChevronDown, {
739
+ "aria-hidden": "true",
740
+ className: cn("ml-2 h-4 w-4 shrink-0 text-primary-400 transition-transform", {
741
+ "rotate-180": isOpen
742
+ })
743
+ })]
744
+ })), showClearButton && jsx("button", {
745
+ type: "button",
746
+ onClick: handleClear,
747
+ "aria-label": "Clear selection",
748
+ className: "absolute right-8 top-1/2 -translate-y-1/2 rounded p-1 text-primary-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500",
749
+ children: jsx(X, {
750
+ className: "h-4 w-4",
751
+ "aria-hidden": "true"
752
+ })
753
+ }), name && (multiSelect ?
754
+ // For multi-select, create a hidden input for each selected value
755
+ selectedValues.length > 0 ? selectedValues.map(function (val, idx) {
756
+ return jsx("input", {
757
+ type: "hidden",
758
+ name: name,
759
+ value: val !== null && val !== void 0 ? val : ""
760
+ }, "".concat(name, "-").concat(idx));
761
+ }) : jsx("input", {
762
+ type: "hidden",
763
+ name: name,
764
+ value: ""
765
+ }) : jsx("input", {
766
+ type: "hidden",
767
+ name: name,
768
+ value: selectedValues !== null && selectedValues !== void 0 ? selectedValues : ""
769
+ })), isOpen && typeof document !== "undefined" && /*#__PURE__*/createPortal(jsxs("div", {
770
+ ref: menuRef,
771
+ className: cn("fixed z-50 rounded-md border border-gray-300 bg-white shadow-lg"),
772
+ role: "dialog",
773
+ "data-placement": placement,
774
+ // Portaled to document.body with position:fixed so the menu
775
+ // escapes any ancestor overflow:hidden|auto|scroll. Coordinates
776
+ // are anchored to the trigger's viewport rect; width matches the
777
+ // trigger; maxHeight is driven by available space.
778
+ style: _objectSpread2({
779
+ left: menuCoords ? "".concat(menuCoords.left, "px") : undefined,
780
+ top: menuCoords ? "".concat(menuCoords.top, "px") : undefined,
781
+ width: menuCoords ? "".concat(menuCoords.width, "px") : undefined
782
+ }, menuMaxHeight ? {
783
+ maxHeight: "".concat(menuMaxHeight + 10, "px"),
784
+ overflow: "auto"
785
+ } : {}),
786
+ children: [searchable && jsx("div", {
787
+ className: "border-b border-gray-200 p-2",
788
+ children: jsxs("div", {
789
+ className: "relative",
790
+ children: [selectAllOption && multiSelect && jsx("div", {
791
+ className: " text-sm flex items-center gap-2 border-b absolute left-1 top-1/2 h-4 w-4 -translate-y-1/2",
792
+ children: function () {
793
+ var allSelected = selectedValues.length === flatOptions.filter(function (o) {
794
+ return !o.disabled;
795
+ }).length;
796
+ return jsx("button", {
797
+ type: "button",
798
+ onClick: handleSelectAll,
799
+ "aria-label": allSelected ? "Deselect all" : "Select all",
800
+ "aria-pressed": allSelected,
801
+ className: "text-sm rounded focus:outline-none focus:ring-2 focus:ring-primary-500",
802
+ children: allSelected ? jsx(Check, {
803
+ className: "h-4 w-4 text-primary-600",
804
+ "aria-hidden": "true"
805
+ }) : jsx("div", {
806
+ className: "h-4 w-4 rounded border border-gray-300"
807
+ })
808
+ });
809
+ }()
810
+ }), jsx("input", {
811
+ ref: searchRef,
812
+ type: "text",
813
+ className: cn("w-full rounded-md border-0 bg-transparent pl-8 pr-3 py-1 text-sm focus:outline-none focus:ring-0", {
814
+ "pl-2": !(selectAllOption && multiSelect)
815
+ }),
816
+ placeholder: "Search...",
817
+ value: searchTerm,
818
+ onChange: function onChange(e) {
819
+ return setSearchTerm(e.target.value);
820
+ },
821
+ onKeyDown: handleKeyDown,
822
+ "aria-label": "Search options",
823
+ "aria-controls": listboxId,
824
+ "aria-activedescendant": activeDescendant,
825
+ "aria-autocomplete": "list"
826
+ }), jsx(Search, {
827
+ className: "absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 text-primary-400"
828
+ })]
829
+ })
830
+ }), jsx("div", {
831
+ ref: listboxRef,
832
+ className: "overflow-auto focus:outline-none",
833
+ role: "listbox",
834
+ id: listboxId,
835
+ tabIndex: -1,
836
+ "aria-labelledby": label ? labelId : undefined,
837
+ "aria-label": label ? undefined : "Options",
838
+ "aria-multiselectable": multiSelect,
839
+ "aria-activedescendant": activeDescendant,
840
+ onKeyDown: handleKeyDown,
841
+ children: grouped ? renderGroupedOptions() : renderFlatOptions()
842
+ })]
843
+ }), document.body)]
844
+ }), error ? jsx("p", {
845
+ id: errorId,
846
+ className: "mt-1 text-sm text-error-500",
847
+ children: error
848
+ }) : helperText ? jsx("p", {
849
+ id: helperId,
850
+ className: "mt-1 text-sm text-muted",
851
+ children: helperText
852
+ }) : null]
853
+ });
854
+ });
855
+ Select.displayName = "Select";
856
+
857
+ export { Select as default };