@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.
- package/README.md +179 -10
- package/ai-skills/dreamtree-ui.md +71 -12
- package/dist/ai-schema.json +452 -17
- package/dist/components/core/Accordion.d.ts.map +1 -1
- package/dist/components/core/Button.d.ts.map +1 -1
- package/dist/components/core/Checkbox.d.ts.map +1 -1
- package/dist/components/core/ColorPicker.d.ts.map +1 -1
- package/dist/components/core/DatePicker.d.ts.map +1 -1
- package/dist/components/core/DateRangePicker.d.ts.map +1 -1
- package/dist/components/core/Form.d.ts.map +1 -1
- package/dist/components/core/Input.d.ts.map +1 -1
- package/dist/components/core/LocationPicker.d.ts.map +1 -1
- package/dist/components/core/Popover.d.ts +4 -0
- package/dist/components/core/Popover.d.ts.map +1 -0
- package/dist/components/core/PriceRangePicker.d.ts.map +1 -1
- package/dist/components/core/ProgressBar.d.ts.map +1 -1
- package/dist/components/core/Radio.d.ts.map +1 -1
- package/dist/components/core/Rate.d.ts.map +1 -1
- package/dist/components/core/Select.d.ts.map +1 -1
- package/dist/components/core/Skeleton.d.ts.map +1 -1
- package/dist/components/core/Slider.d.ts +4 -0
- package/dist/components/core/Slider.d.ts.map +1 -0
- package/dist/components/core/SpeechToText.d.ts.map +1 -1
- package/dist/components/core/Switch.d.ts.map +1 -1
- package/dist/components/core/Table/ActionMenu.d.ts.map +1 -1
- package/dist/components/core/Table/ColumnMenu.d.ts +2 -1
- package/dist/components/core/Table/ColumnMenu.d.ts.map +1 -1
- package/dist/components/core/Table/MobileCard.d.ts +2 -2
- package/dist/components/core/Table/MobileCard.d.ts.map +1 -1
- package/dist/components/core/Table/Table.d.ts.map +1 -1
- package/dist/components/core/Table/TableRow.d.ts +4 -2
- package/dist/components/core/Table/TableRow.d.ts.map +1 -1
- package/dist/components/core/Tabs.d.ts.map +1 -1
- package/dist/components/core/Textarea.d.ts +4 -0
- package/dist/components/core/Textarea.d.ts.map +1 -0
- package/dist/components/core/ThreeDotPopover.d.ts.map +1 -1
- package/dist/components/core/Tooltip.d.ts.map +1 -1
- package/dist/components/feedback/Dialog.d.ts.map +1 -1
- package/dist/components/feedback/Drawer.d.ts +4 -0
- package/dist/components/feedback/Drawer.d.ts.map +1 -0
- package/dist/components/feedback/Toast.d.ts +14 -5
- package/dist/components/feedback/Toast.d.ts.map +1 -1
- package/dist/components/feedback/Tour.d.ts +4 -0
- package/dist/components/feedback/Tour.d.ts.map +1 -0
- package/dist/components/feedback/TourSpotlight.d.ts +9 -0
- package/dist/components/feedback/TourSpotlight.d.ts.map +1 -0
- package/dist/components/navigation/Breadcrumbs.d.ts.map +1 -1
- package/dist/components/navigation/FootNav.d.ts.map +1 -1
- package/dist/components/navigation/Navbar.d.ts.map +1 -1
- package/dist/components/navigation/Sidebar.d.ts +8 -2
- package/dist/components/navigation/Sidebar.d.ts.map +1 -1
- package/dist/components/utility/Badge.d.ts.map +1 -1
- package/dist/components/utility/FileUpload.d.ts.map +1 -1
- package/dist/components/utility/Sortable.d.ts +4 -0
- package/dist/components/utility/Sortable.d.ts.map +1 -0
- package/dist/esm/_vendor/object-assign/index.js +99 -0
- package/dist/esm/_vendor/prop-types/_vendor/react-is/cjs/react-is.development.js +190 -0
- package/dist/esm/_vendor/prop-types/_vendor/react-is/cjs/react-is.production.min.js +26 -0
- package/dist/esm/_vendor/prop-types/_vendor/react-is/index.js +19 -0
- package/dist/esm/_vendor/prop-types/checkPropTypes.js +115 -0
- package/dist/esm/_vendor/prop-types/factoryWithThrowingShims.js +74 -0
- package/dist/esm/_vendor/prop-types/factoryWithTypeCheckers.js +625 -0
- package/dist/esm/_vendor/prop-types/index.js +30 -0
- package/dist/esm/_vendor/prop-types/lib/ReactPropTypesSecret.js +21 -0
- package/dist/esm/_vendor/prop-types/lib/has.js +11 -0
- package/dist/esm/_virtual/_commonjsHelpers.js +5 -0
- package/dist/esm/_virtual/_rollupPluginBabelHelpers.js +329 -0
- package/dist/esm/_virtual/index.js +3 -0
- package/dist/esm/_virtual/index2.js +3 -0
- package/dist/esm/_virtual/react-is.development.js +3 -0
- package/dist/esm/_virtual/react-is.production.min.js +3 -0
- package/dist/esm/components/blocks/AppShell.js +205 -0
- package/dist/esm/components/blocks/ConfirmDialog.js +57 -0
- package/dist/esm/components/blocks/EmptyState.js +44 -0
- package/dist/esm/components/blocks/ErrorState.js +53 -0
- package/dist/esm/components/blocks/LoadingState.js +134 -0
- package/dist/esm/components/blocks/PageHeader.js +51 -0
- package/dist/esm/components/blocks/StatCard.js +79 -0
- package/dist/esm/components/blocks/StatusPill.js +55 -0
- package/dist/esm/components/core/Accordion.js +260 -0
- package/dist/esm/components/core/Button.js +59 -0
- package/dist/esm/components/core/Checkbox.js +188 -0
- package/dist/esm/components/core/ColorPicker.js +348 -0
- package/dist/esm/components/core/DatePicker.js +436 -0
- package/dist/esm/components/core/DateRangePicker.js +494 -0
- package/dist/esm/components/core/Form.js +522 -0
- package/dist/esm/components/core/Input.js +179 -0
- package/dist/esm/components/core/Loader.js +141 -0
- package/dist/esm/components/core/LocationPicker.js +161 -0
- package/dist/esm/components/core/Popover.js +171 -0
- package/dist/esm/components/core/PriceRangePicker.js +376 -0
- package/dist/esm/components/core/ProgressBar.js +152 -0
- package/dist/esm/components/core/Radio.js +156 -0
- package/dist/esm/components/core/Rate.js +197 -0
- package/dist/esm/components/core/RoundedTag.js +118 -0
- package/dist/esm/components/core/Select.js +857 -0
- package/dist/esm/components/core/Skeleton.js +100 -0
- package/dist/esm/components/core/Slider.js +134 -0
- package/dist/esm/components/core/SpeechToText.js +226 -0
- package/dist/esm/components/core/Switch.js +207 -0
- package/dist/esm/components/core/Table/ActionMenu.js +251 -0
- package/dist/esm/components/core/Table/ColumnMenu.js +55 -0
- package/dist/esm/components/core/Table/FieldInput.js +157 -0
- package/dist/esm/components/core/Table/MobileCard.js +187 -0
- package/dist/esm/components/core/Table/MobileFilters.js +72 -0
- package/dist/esm/components/core/Table/Pagination.js +157 -0
- package/dist/esm/components/core/Table/Table.js +1533 -0
- package/dist/esm/components/core/Table/TableRow.js +254 -0
- package/dist/esm/components/core/Table/constants.js +35 -0
- package/dist/esm/components/core/Table/helpers.js +124 -0
- package/dist/esm/components/core/Table/useClickOutside.js +26 -0
- package/dist/esm/components/core/Tabs.js +289 -0
- package/dist/esm/components/core/TextToSpeech.js +52 -0
- package/dist/esm/components/core/Textarea.js +125 -0
- package/dist/esm/components/core/ThreeDotPopover.js +160 -0
- package/dist/esm/components/core/Tooltip.js +372 -0
- package/dist/esm/components/feedback/Alert.js +96 -0
- package/dist/esm/components/feedback/Dialog.js +200 -0
- package/dist/esm/components/feedback/Drawer.js +171 -0
- package/dist/esm/components/feedback/Toast.js +362 -0
- package/dist/esm/components/feedback/Tour.js +214 -0
- package/dist/esm/components/feedback/TourSpotlight.js +71 -0
- package/dist/esm/components/navigation/Breadcrumbs.js +75 -0
- package/dist/esm/components/navigation/Dropdown.js +53 -0
- package/dist/esm/components/navigation/FootNav.js +51 -0
- package/dist/esm/components/navigation/Navbar.js +437 -0
- package/dist/esm/components/navigation/Sidebar.js +567 -0
- package/dist/esm/components/utility/Avatar.js +89 -0
- package/dist/esm/components/utility/Badge.js +47 -0
- package/dist/esm/components/utility/Card.js +85 -0
- package/dist/esm/components/utility/Carousel.js +97 -0
- package/dist/esm/components/utility/Condition.js +9 -0
- package/dist/esm/components/utility/FileUpload.js +177 -0
- package/dist/esm/components/utility/Pagination.js +111 -0
- package/dist/esm/components/utility/Sortable.js +190 -0
- package/dist/esm/components/utility/Stepper.js +101 -0
- package/dist/esm/hooks/useApi.js +169 -0
- package/dist/esm/hooks/useControllableState.js +32 -0
- package/dist/esm/hooks/useFocusTrap.js +85 -0
- package/dist/esm/hooks/useLatestRef.js +20 -0
- package/dist/esm/hooks/useMixins.js +100 -0
- package/dist/esm/hooks/useScrollLock.js +26 -0
- package/dist/esm/hooks/useSliderValues.js +135 -0
- package/dist/esm/hooks/useSortable.js +188 -0
- package/dist/esm/hooks/useTheme.js +53 -0
- package/dist/esm/hooks/useTourTarget.js +92 -0
- package/dist/esm/index.js +65 -0
- package/dist/esm/store/index.js +26 -0
- package/dist/esm/store/store.js +107 -0
- package/dist/esm/store/storeSlice.js +45 -0
- package/dist/esm/utils/cn.js +11 -0
- package/dist/esm/utils/emitter.js +169 -0
- package/dist/esm/utils/helpers.js +161 -0
- package/dist/esm/utils/position.js +134 -0
- package/dist/hooks/useApi.d.ts +2 -1
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/hooks/useControllableState.d.ts +11 -0
- package/dist/hooks/useControllableState.d.ts.map +1 -0
- package/dist/hooks/useFocusTrap.d.ts +15 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -0
- package/dist/hooks/useLatestRef.d.ts +8 -0
- package/dist/hooks/useLatestRef.d.ts.map +1 -0
- package/dist/hooks/useMixins.d.ts.map +1 -1
- package/dist/hooks/useScrollLock.d.ts +2 -0
- package/dist/hooks/useScrollLock.d.ts.map +1 -0
- package/dist/hooks/useSliderValues.d.ts +43 -0
- package/dist/hooks/useSliderValues.d.ts.map +1 -0
- package/dist/hooks/useSortable.d.ts +32 -0
- package/dist/hooks/useSortable.d.ts.map +1 -0
- package/dist/hooks/useTheme.d.ts.map +1 -1
- package/dist/hooks/useTourTarget.d.ts +15 -0
- package/dist/hooks/useTourTarget.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.esm.css +1 -1
- package/dist/index.esm.js +4800 -2212
- package/dist/index.js +4805 -2210
- package/dist/types/index.d.ts +193 -5
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/position.d.ts +30 -0
- package/dist/utils/position.d.ts.map +1 -0
- package/doc/README.md +7 -1
- package/mcp/catalog.snapshot.json +911 -212
- 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 };
|