@financedistrict/apps-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +19 -0
- package/README.md +119 -0
- package/dist/Radio-BYWz1n7p.d.cts +32 -0
- package/dist/Radio-BYWz1n7p.d.ts +32 -0
- package/dist/SelectTrigger-By0iPBI3.d.cts +78 -0
- package/dist/SelectTrigger-By0iPBI3.d.ts +78 -0
- package/dist/accordion.cjs +322 -0
- package/dist/accordion.d.cts +156 -0
- package/dist/accordion.d.ts +156 -0
- package/dist/accordion.js +2 -0
- package/dist/apps-ui.umd.js +1 -0
- package/dist/button.cjs +366 -0
- package/dist/button.d.cts +52 -0
- package/dist/button.d.ts +52 -0
- package/dist/button.js +3 -0
- package/dist/checkbox.cjs +74 -0
- package/dist/checkbox.d.cts +63 -0
- package/dist/checkbox.d.ts +63 -0
- package/dist/checkbox.js +2 -0
- package/dist/chunk-3RU7JMFE.js +27 -0
- package/dist/chunk-5TWRK4NW.js +783 -0
- package/dist/chunk-7EOSDFS3.js +9 -0
- package/dist/chunk-AIQ4UOGK.js +142 -0
- package/dist/chunk-B3MMGTPV.js +200 -0
- package/dist/chunk-DAHQYLUI.js +146 -0
- package/dist/chunk-DGBXADSS.js +1 -0
- package/dist/chunk-E6KPNTWW.js +295 -0
- package/dist/chunk-EOAB6G2D.js +149 -0
- package/dist/chunk-FAOVKDVM.js +32 -0
- package/dist/chunk-FV76Z4Z2.js +78 -0
- package/dist/chunk-GLTLBUZ7.js +285 -0
- package/dist/chunk-GTZFV43T.js +154 -0
- package/dist/chunk-HRGZNL4A.js +88 -0
- package/dist/chunk-IEVN3J2F.js +98 -0
- package/dist/chunk-IRS3T6LF.js +352 -0
- package/dist/chunk-IYYHCQXG.js +146 -0
- package/dist/chunk-JIXW3455.js +89 -0
- package/dist/chunk-N4VAP2GB.js +77 -0
- package/dist/chunk-SJ2ZRYVV.js +167 -0
- package/dist/chunk-SNU4YXOR.js +66 -0
- package/dist/chunk-TT6JQATV.js +99 -0
- package/dist/chunk-UPQ3ZPCO.js +66 -0
- package/dist/chunk-UVUSVFOH.js +264 -0
- package/dist/chunk-WJFPGGJB.js +255 -0
- package/dist/chunk-WXEBQA7A.js +371 -0
- package/dist/chunk-Y2O4YYZQ.js +184 -0
- package/dist/cut-corner.cjs +111 -0
- package/dist/cut-corner.d.cts +49 -0
- package/dist/cut-corner.d.ts +49 -0
- package/dist/cut-corner.js +1 -0
- package/dist/dialog.cjs +441 -0
- package/dist/dialog.d.cts +240 -0
- package/dist/dialog.d.ts +240 -0
- package/dist/dialog.js +3 -0
- package/dist/feature-card.cjs +182 -0
- package/dist/feature-card.d.cts +54 -0
- package/dist/feature-card.d.ts +54 -0
- package/dist/feature-card.js +2 -0
- package/dist/filter.cjs +873 -0
- package/dist/filter.d.cts +129 -0
- package/dist/filter.d.ts +129 -0
- package/dist/filter.js +6 -0
- package/dist/index.cjs +4583 -0
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +27 -0
- package/dist/inline-select.cjs +856 -0
- package/dist/inline-select.d.cts +92 -0
- package/dist/inline-select.d.ts +92 -0
- package/dist/inline-select.js +6 -0
- package/dist/input.cjs +263 -0
- package/dist/input.d.cts +157 -0
- package/dist/input.d.ts +157 -0
- package/dist/input.js +4 -0
- package/dist/panel.cjs +185 -0
- package/dist/panel.d.cts +103 -0
- package/dist/panel.d.ts +103 -0
- package/dist/panel.js +2 -0
- package/dist/property-grid.cjs +110 -0
- package/dist/property-grid.d.cts +56 -0
- package/dist/property-grid.d.ts +56 -0
- package/dist/property-grid.js +2 -0
- package/dist/radio.cjs +74 -0
- package/dist/radio.d.cts +39 -0
- package/dist/radio.d.ts +39 -0
- package/dist/radio.js +2 -0
- package/dist/scrollbar.cjs +34 -0
- package/dist/scrollbar.d.cts +35 -0
- package/dist/scrollbar.d.ts +35 -0
- package/dist/scrollbar.js +1 -0
- package/dist/search-bar.cjs +408 -0
- package/dist/search-bar.d.cts +42 -0
- package/dist/search-bar.d.ts +42 -0
- package/dist/search-bar.js +4 -0
- package/dist/select.cjs +998 -0
- package/dist/select.d.cts +357 -0
- package/dist/select.d.ts +357 -0
- package/dist/select.js +5 -0
- package/dist/selectable-feature-card.cjs +159 -0
- package/dist/selectable-feature-card.d.cts +74 -0
- package/dist/selectable-feature-card.d.ts +74 -0
- package/dist/selectable-feature-card.js +3 -0
- package/dist/sidebar-item.cjs +175 -0
- package/dist/sidebar-item.d.cts +75 -0
- package/dist/sidebar-item.d.ts +75 -0
- package/dist/sidebar-item.js +2 -0
- package/dist/switch.cjs +86 -0
- package/dist/switch.d.cts +72 -0
- package/dist/switch.d.ts +72 -0
- package/dist/switch.js +2 -0
- package/dist/table-cell.cjs +196 -0
- package/dist/table-cell.d.cts +76 -0
- package/dist/table-cell.d.ts +76 -0
- package/dist/table-cell.js +2 -0
- package/dist/table-title.cjs +34 -0
- package/dist/table-title.d.cts +16 -0
- package/dist/table-title.d.ts +16 -0
- package/dist/table-title.js +2 -0
- package/dist/tabs.cjs +299 -0
- package/dist/tabs.d.cts +116 -0
- package/dist/tabs.d.ts +116 -0
- package/dist/tabs.js +2 -0
- package/dist/toast.cjs +87 -0
- package/dist/toast.d.cts +48 -0
- package/dist/toast.d.ts +48 -0
- package/dist/toast.js +2 -0
- package/dist/token-input.cjs +415 -0
- package/dist/token-input.d.cts +90 -0
- package/dist/token-input.d.ts +90 -0
- package/dist/token-input.js +3 -0
- package/dist/tooltip.cjs +384 -0
- package/dist/tooltip.d.cts +142 -0
- package/dist/tooltip.d.ts +142 -0
- package/dist/tooltip.js +2 -0
- package/package.json +200 -0
- package/src/fonts.css +33 -0
package/dist/select.cjs
ADDED
|
@@ -0,0 +1,998 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React2 = require('react');
|
|
4
|
+
var reactDom = require('react-dom');
|
|
5
|
+
var clsx = require('clsx');
|
|
6
|
+
var tailwindMerge = require('tailwind-merge');
|
|
7
|
+
var classVarianceAuthority = require('class-variance-authority');
|
|
8
|
+
var react = require('@phosphor-icons/react');
|
|
9
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
10
|
+
|
|
11
|
+
function _interopNamespace(e) {
|
|
12
|
+
if (e && e.__esModule) return e;
|
|
13
|
+
var n = Object.create(null);
|
|
14
|
+
if (e) {
|
|
15
|
+
Object.keys(e).forEach(function (k) {
|
|
16
|
+
if (k !== 'default') {
|
|
17
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
18
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () { return e[k]; }
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
n.default = e;
|
|
26
|
+
return Object.freeze(n);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var React2__namespace = /*#__PURE__*/_interopNamespace(React2);
|
|
30
|
+
|
|
31
|
+
// src/select/Select.tsx
|
|
32
|
+
function cn(...inputs) {
|
|
33
|
+
return tailwindMerge.twMerge(clsx.clsx(inputs));
|
|
34
|
+
}
|
|
35
|
+
var scrollbarVariants = classVarianceAuthority.cva(
|
|
36
|
+
[
|
|
37
|
+
// Firefox (approximate): thin themed scrollbar over a transparent track.
|
|
38
|
+
"[scrollbar-width:thin]",
|
|
39
|
+
"[scrollbar-color:var(--color-card-foreground-muted)_transparent]",
|
|
40
|
+
// WebKit gutter — 12px, both axes so the utility is axis-agnostic.
|
|
41
|
+
"[&::-webkit-scrollbar]:w-3 [&::-webkit-scrollbar]:h-3",
|
|
42
|
+
// WebKit thumb — 4px pill: a 4px transparent border + content-box clip
|
|
43
|
+
// leaves a 4px-wide fill centred in the 12px gutter.
|
|
44
|
+
"[&::-webkit-scrollbar-thumb]:rounded-full",
|
|
45
|
+
"[&::-webkit-scrollbar-thumb]:border-4 [&::-webkit-scrollbar-thumb]:border-solid [&::-webkit-scrollbar-thumb]:border-transparent",
|
|
46
|
+
"[&::-webkit-scrollbar-thumb]:bg-clip-content",
|
|
47
|
+
"[&::-webkit-scrollbar-thumb]:bg-card-foreground-muted",
|
|
48
|
+
// WebKit track — transparent by default (stroke is the `stroke` variant).
|
|
49
|
+
"[&::-webkit-scrollbar-track]:bg-transparent"
|
|
50
|
+
],
|
|
51
|
+
{
|
|
52
|
+
variants: {
|
|
53
|
+
stroke: {
|
|
54
|
+
// 1px leading-edge track stroke (Figma `stroke`, on by default).
|
|
55
|
+
true: "[&::-webkit-scrollbar-track]:border-l [&::-webkit-scrollbar-track]:border-card-border",
|
|
56
|
+
false: ""
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
defaultVariants: { stroke: true }
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
var checkboxVariants = classVarianceAuthority.cva([
|
|
63
|
+
// Box — 20×20 (`size-5`), 2px radius (Figma `radius/xs`), 1px border.
|
|
64
|
+
// `peer` lets the sibling indicator react to :checked / :disabled.
|
|
65
|
+
"peer size-5 shrink-0 appearance-none rounded-xs border",
|
|
66
|
+
"cursor-pointer transition-colors",
|
|
67
|
+
// Default
|
|
68
|
+
"bg-background border-border",
|
|
69
|
+
// Hover (unselected) — focus keeps the DEFAULT border; only the ring signals
|
|
70
|
+
// focus (so focus doesn't visually read as hover).
|
|
71
|
+
"hover:border-border-accent",
|
|
72
|
+
// Focus ring — suppress the native ring, draw the shared `--focus` ring the
|
|
73
|
+
// same way ghost Button does: 2px stroke, 2px offset (`outline-solid`
|
|
74
|
+
// restores what `outline-none` zeroed).
|
|
75
|
+
"outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-focus",
|
|
76
|
+
// Selected
|
|
77
|
+
"checked:bg-brand-primary-background checked:border-brand-primary-border",
|
|
78
|
+
"checked:hover:bg-brand-primary-accent checked:hover:border-brand-primary-border",
|
|
79
|
+
"checked:focus-visible:bg-brand-primary-accent checked:focus-visible:border-brand-primary-border",
|
|
80
|
+
// Disabled — shared neutral-muted look whether selected or not (per Figma)
|
|
81
|
+
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:border-border-muted",
|
|
82
|
+
"checked:disabled:bg-background checked:disabled:border-border-muted"
|
|
83
|
+
]);
|
|
84
|
+
function Checkbox({
|
|
85
|
+
className,
|
|
86
|
+
wrapperClassName,
|
|
87
|
+
onChange,
|
|
88
|
+
onCheckedChange,
|
|
89
|
+
...props
|
|
90
|
+
}) {
|
|
91
|
+
const handleChange = (event) => {
|
|
92
|
+
onChange?.(event);
|
|
93
|
+
onCheckedChange?.(event.currentTarget.checked);
|
|
94
|
+
};
|
|
95
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: cn("relative inline-flex shrink-0", wrapperClassName), children: [
|
|
96
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
97
|
+
"input",
|
|
98
|
+
{
|
|
99
|
+
type: "checkbox",
|
|
100
|
+
"data-slot": "checkbox",
|
|
101
|
+
className: cn(checkboxVariants(), className),
|
|
102
|
+
...props,
|
|
103
|
+
onChange: handleChange
|
|
104
|
+
}
|
|
105
|
+
),
|
|
106
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
107
|
+
react.CheckIcon,
|
|
108
|
+
{
|
|
109
|
+
"aria-hidden": "true",
|
|
110
|
+
weight: "regular",
|
|
111
|
+
"data-slot": "checkbox-indicator",
|
|
112
|
+
className: cn(
|
|
113
|
+
"pointer-events-none invisible absolute inset-0 m-auto size-4",
|
|
114
|
+
"text-brand-primary-foreground",
|
|
115
|
+
"peer-checked:visible peer-disabled:text-foreground-muted"
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
] });
|
|
120
|
+
}
|
|
121
|
+
var selectListboxVariants = classVarianceAuthority.cva([
|
|
122
|
+
"flex flex-col",
|
|
123
|
+
"bg-card-background border border-card-border rounded-sm shadow-s",
|
|
124
|
+
// Small vertical inset so the first/last option isn't flush to the border.
|
|
125
|
+
"py-1",
|
|
126
|
+
// Scroll when the list outgrows its max-height (set inline from the rect).
|
|
127
|
+
"overflow-y-auto",
|
|
128
|
+
// The panel is not itself focusable; focus stays on the trigger.
|
|
129
|
+
"outline-none"
|
|
130
|
+
]);
|
|
131
|
+
var selectListboxPanelVariants = classVarianceAuthority.cva([
|
|
132
|
+
"flex flex-col min-h-0",
|
|
133
|
+
"bg-card-background border border-card-border rounded-sm shadow-s",
|
|
134
|
+
"overflow-hidden",
|
|
135
|
+
"outline-none"
|
|
136
|
+
]);
|
|
137
|
+
var selectListboxScrollVariants = classVarianceAuthority.cva([
|
|
138
|
+
"flex flex-col min-h-0 flex-1",
|
|
139
|
+
"py-1",
|
|
140
|
+
"overflow-y-auto",
|
|
141
|
+
"outline-none"
|
|
142
|
+
]);
|
|
143
|
+
var selectListboxFooterVariants = classVarianceAuthority.cva([
|
|
144
|
+
"shrink-0",
|
|
145
|
+
"border-t border-t-card-border",
|
|
146
|
+
"px-3 py-1"
|
|
147
|
+
]);
|
|
148
|
+
var selectOptionVariants = classVarianceAuthority.cva(
|
|
149
|
+
[
|
|
150
|
+
"body-03",
|
|
151
|
+
"flex w-full items-center gap-2",
|
|
152
|
+
"px-3 py-2",
|
|
153
|
+
"cursor-pointer select-none",
|
|
154
|
+
"transition-colors",
|
|
155
|
+
// Reserve the 3px left accent bar on EVERY row (transparent by default,
|
|
156
|
+
// recoloured only when single-selected). Keeping the border-box constant
|
|
157
|
+
// across rows means selecting one never shifts its text right — same
|
|
158
|
+
// no-layout-shift trick Input uses for its `showBottomLine` bottom line.
|
|
159
|
+
"border-l-3",
|
|
160
|
+
// Mirror the 3px on the right (always transparent) so content is inset
|
|
161
|
+
// equally on both sides — the left accent bar would otherwise make the left
|
|
162
|
+
// padding read 3px larger than the right.
|
|
163
|
+
"border-r-3 border-r-transparent"
|
|
164
|
+
// NOTE: no text-/border-colour in the base. cva concatenates class strings
|
|
165
|
+
// without resolving conflicting Tailwind utilities, so a base colour + a
|
|
166
|
+
// compound colour would both survive and CSS source-order (not our intent)
|
|
167
|
+
// would pick the winner. Every state below sets exactly ONE text colour and
|
|
168
|
+
// ONE left-border colour via compound variants.
|
|
169
|
+
],
|
|
170
|
+
{
|
|
171
|
+
variants: {
|
|
172
|
+
active: { true: "", false: "" },
|
|
173
|
+
selected: { true: "", false: "" },
|
|
174
|
+
multiple: { true: "", false: "" },
|
|
175
|
+
disabled: { true: "cursor-not-allowed", false: "" }
|
|
176
|
+
},
|
|
177
|
+
compoundVariants: [
|
|
178
|
+
// ── Text colour (exactly one matches per state) ──
|
|
179
|
+
{ disabled: true, className: "text-foreground-muted" },
|
|
180
|
+
{ disabled: false, selected: false, className: "text-foreground" },
|
|
181
|
+
// Multi keeps the normal foreground when selected (the checkbox marks it).
|
|
182
|
+
{
|
|
183
|
+
disabled: false,
|
|
184
|
+
selected: true,
|
|
185
|
+
multiple: true,
|
|
186
|
+
className: "text-foreground"
|
|
187
|
+
},
|
|
188
|
+
// Single-select selected → brand foreground (Figma node 4921:8203).
|
|
189
|
+
{
|
|
190
|
+
disabled: false,
|
|
191
|
+
selected: true,
|
|
192
|
+
multiple: false,
|
|
193
|
+
className: "text-card-brand-foreground"
|
|
194
|
+
},
|
|
195
|
+
// ── Left accent bar colour (exactly one matches per state) ──
|
|
196
|
+
// Invisible on every row except the single-select selected one, where it
|
|
197
|
+
// becomes the brand accent. Width is reserved in the base, so this is a
|
|
198
|
+
// pure colour change — no content shift.
|
|
199
|
+
{ selected: false, className: "border-l-transparent" },
|
|
200
|
+
{ selected: true, multiple: true, className: "border-l-transparent" },
|
|
201
|
+
{
|
|
202
|
+
selected: true,
|
|
203
|
+
multiple: false,
|
|
204
|
+
className: "border-l-card-brand-border-accent"
|
|
205
|
+
},
|
|
206
|
+
// ── Background ──
|
|
207
|
+
// Neutral hover/keyboard highlight (Figma card/neutral/normal/accent) —
|
|
208
|
+
// every row EXCEPT a single-select selected one (which stays branded).
|
|
209
|
+
{ active: true, selected: false, className: "bg-card-accent" },
|
|
210
|
+
{
|
|
211
|
+
active: true,
|
|
212
|
+
selected: true,
|
|
213
|
+
multiple: true,
|
|
214
|
+
className: "bg-card-accent"
|
|
215
|
+
},
|
|
216
|
+
// Single-select selected: brand-tinted row. No neutral `active` bg matches
|
|
217
|
+
// this combo, so it stays branded on hover.
|
|
218
|
+
{ selected: true, multiple: false, className: "bg-card-brand-accent" }
|
|
219
|
+
],
|
|
220
|
+
defaultVariants: {
|
|
221
|
+
active: false,
|
|
222
|
+
selected: false,
|
|
223
|
+
multiple: false,
|
|
224
|
+
disabled: false
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
var LISTBOX_OFFSET = 4;
|
|
229
|
+
var LISTBOX_MARGIN = 8;
|
|
230
|
+
var LISTBOX_MAX_HEIGHT = 208;
|
|
231
|
+
function computeListboxPosition(anchor, listboxHeight, viewportHeight) {
|
|
232
|
+
const flipsAbove = anchor.bottom + listboxHeight > viewportHeight && anchor.top > listboxHeight;
|
|
233
|
+
return {
|
|
234
|
+
left: anchor.left,
|
|
235
|
+
width: anchor.width,
|
|
236
|
+
top: flipsAbove ? anchor.top - listboxHeight : anchor.bottom,
|
|
237
|
+
placement: flipsAbove ? "top" : "bottom"
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function computeListboxHorizontal(anchor, widthMode, measuredWidth, viewportWidth, margin) {
|
|
241
|
+
if (widthMode === "trigger") {
|
|
242
|
+
return { left: anchor.left, width: anchor.width };
|
|
243
|
+
}
|
|
244
|
+
const maxWidth = viewportWidth - 2 * margin;
|
|
245
|
+
const upperBound = viewportWidth - margin - measuredWidth;
|
|
246
|
+
const left = Math.max(margin, Math.min(anchor.left, upperBound));
|
|
247
|
+
return { left, minWidth: anchor.width, maxWidth };
|
|
248
|
+
}
|
|
249
|
+
function SelectListbox({
|
|
250
|
+
id,
|
|
251
|
+
options,
|
|
252
|
+
selected,
|
|
253
|
+
multiple,
|
|
254
|
+
activeIndex,
|
|
255
|
+
anchorRect,
|
|
256
|
+
widthMode = "trigger",
|
|
257
|
+
onSelect,
|
|
258
|
+
onActiveChange,
|
|
259
|
+
footer,
|
|
260
|
+
"aria-label": ariaLabel,
|
|
261
|
+
"aria-labelledby": ariaLabelledby
|
|
262
|
+
}) {
|
|
263
|
+
const listRef = React2__namespace.useRef(null);
|
|
264
|
+
const panelRef = React2__namespace.useRef(null);
|
|
265
|
+
const hasFooter = footer != null;
|
|
266
|
+
const [style, setStyle] = React2__namespace.useState(null);
|
|
267
|
+
React2__namespace.useLayoutEffect(() => {
|
|
268
|
+
if (anchorRect == null) {
|
|
269
|
+
setStyle(null);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const measureEl = hasFooter ? panelRef.current : listRef.current;
|
|
273
|
+
const measured = measureEl ? measureEl.offsetHeight : 0;
|
|
274
|
+
const height = Math.min(measured || LISTBOX_MAX_HEIGHT, LISTBOX_MAX_HEIGHT);
|
|
275
|
+
const pos = computeListboxPosition(anchorRect, height, window.innerHeight);
|
|
276
|
+
const measuredWidth = measureEl ? measureEl.offsetWidth : 0;
|
|
277
|
+
const horizontal = computeListboxHorizontal(
|
|
278
|
+
anchorRect,
|
|
279
|
+
widthMode,
|
|
280
|
+
measuredWidth,
|
|
281
|
+
window.innerWidth,
|
|
282
|
+
LISTBOX_MARGIN
|
|
283
|
+
);
|
|
284
|
+
setStyle({
|
|
285
|
+
position: "absolute",
|
|
286
|
+
top: pos.top + window.scrollY + (pos.placement === "bottom" ? LISTBOX_OFFSET : -LISTBOX_OFFSET),
|
|
287
|
+
left: horizontal.left + window.scrollX,
|
|
288
|
+
// `"trigger"`: fixed pixel width = trigger width (Select, unchanged).
|
|
289
|
+
// `"auto"`: content-sized via `max-content`, bounded by min/max width.
|
|
290
|
+
...widthMode === "trigger" ? { width: horizontal.width } : {
|
|
291
|
+
width: "max-content",
|
|
292
|
+
minWidth: horizontal.minWidth,
|
|
293
|
+
maxWidth: horizontal.maxWidth
|
|
294
|
+
},
|
|
295
|
+
// Bound to the design token (208px). `overflow-y-auto` on the panel
|
|
296
|
+
// scrolls the list past this cap.
|
|
297
|
+
maxHeight: "var(--container-4xs)",
|
|
298
|
+
zIndex: 50
|
|
299
|
+
});
|
|
300
|
+
}, [anchorRect, widthMode, hasFooter]);
|
|
301
|
+
React2__namespace.useLayoutEffect(() => {
|
|
302
|
+
if (activeIndex < 0) return;
|
|
303
|
+
const el = listRef.current?.querySelector(
|
|
304
|
+
`#${CSS.escape(`${id}-opt-${activeIndex}`)}`
|
|
305
|
+
);
|
|
306
|
+
el?.scrollIntoView?.({ block: "nearest" });
|
|
307
|
+
}, [activeIndex, id]);
|
|
308
|
+
const positionStyle = style ?? (widthMode === "auto" ? { position: "absolute", visibility: "hidden", width: "max-content" } : { position: "absolute", visibility: "hidden" });
|
|
309
|
+
const optionItems = /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: options.map((option, index) => {
|
|
310
|
+
const isSelected = selected.includes(option.value);
|
|
311
|
+
const isActive = index === activeIndex;
|
|
312
|
+
const isDisabled = option.disabled === true;
|
|
313
|
+
const iconColorClass = isSelected && !multiple ? "text-card-brand-foreground" : "text-foreground-muted";
|
|
314
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
315
|
+
"li",
|
|
316
|
+
{
|
|
317
|
+
id: `${id}-opt-${index}`,
|
|
318
|
+
role: "option",
|
|
319
|
+
"aria-selected": isSelected,
|
|
320
|
+
"aria-disabled": isDisabled || void 0,
|
|
321
|
+
"data-slot": "select-option",
|
|
322
|
+
"data-active": isActive || void 0,
|
|
323
|
+
className: selectOptionVariants({
|
|
324
|
+
active: isActive,
|
|
325
|
+
selected: isSelected,
|
|
326
|
+
multiple,
|
|
327
|
+
disabled: isDisabled
|
|
328
|
+
}),
|
|
329
|
+
onMouseEnter: () => {
|
|
330
|
+
if (!isDisabled) onActiveChange(index);
|
|
331
|
+
},
|
|
332
|
+
onMouseDown: (event) => {
|
|
333
|
+
event.preventDefault();
|
|
334
|
+
if (!isDisabled) onSelect(option.value);
|
|
335
|
+
},
|
|
336
|
+
children: [
|
|
337
|
+
multiple && // Visual mirror of the row's selection — the `option` carries the
|
|
338
|
+
// semantics, so the checkbox is hidden from AT and out of the tab
|
|
339
|
+
// order. `readOnly` because the <li> owns the toggle.
|
|
340
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
341
|
+
Checkbox,
|
|
342
|
+
{
|
|
343
|
+
checked: isSelected,
|
|
344
|
+
disabled: true,
|
|
345
|
+
tabIndex: -1,
|
|
346
|
+
"aria-hidden": "true",
|
|
347
|
+
"data-slot": "select-option-checkbox",
|
|
348
|
+
className: cn(
|
|
349
|
+
"pointer-events-none disabled:border-border",
|
|
350
|
+
"checked:disabled:bg-brand-primary-background checked:disabled:border-brand-primary-border"
|
|
351
|
+
),
|
|
352
|
+
wrapperClassName: "[&_[data-slot=checkbox-indicator]]:text-brand-primary-foreground"
|
|
353
|
+
}
|
|
354
|
+
),
|
|
355
|
+
option.leadingSlot != null && // After the checkbox in multi mode. Decorative; muted foreground
|
|
356
|
+
// except on the single-select selected row (brand), per iconColorClass.
|
|
357
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
358
|
+
"span",
|
|
359
|
+
{
|
|
360
|
+
"data-slot": "select-option-leading",
|
|
361
|
+
"aria-hidden": "true",
|
|
362
|
+
className: cn(
|
|
363
|
+
"flex shrink-0 items-center [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
364
|
+
iconColorClass
|
|
365
|
+
),
|
|
366
|
+
children: option.leadingSlot
|
|
367
|
+
}
|
|
368
|
+
),
|
|
369
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate", children: option.label }),
|
|
370
|
+
option.trailingSlot != null && /* @__PURE__ */ jsxRuntime.jsx(
|
|
371
|
+
"span",
|
|
372
|
+
{
|
|
373
|
+
"data-slot": "select-option-trailing",
|
|
374
|
+
"aria-hidden": "true",
|
|
375
|
+
className: cn(
|
|
376
|
+
"flex shrink-0 items-center [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
377
|
+
iconColorClass
|
|
378
|
+
),
|
|
379
|
+
children: option.trailingSlot
|
|
380
|
+
}
|
|
381
|
+
)
|
|
382
|
+
]
|
|
383
|
+
},
|
|
384
|
+
option.value
|
|
385
|
+
);
|
|
386
|
+
}) });
|
|
387
|
+
if (!hasFooter) {
|
|
388
|
+
return reactDom.createPortal(
|
|
389
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
390
|
+
"ul",
|
|
391
|
+
{
|
|
392
|
+
ref: listRef,
|
|
393
|
+
id,
|
|
394
|
+
role: "listbox",
|
|
395
|
+
"aria-multiselectable": multiple || void 0,
|
|
396
|
+
"aria-label": ariaLabel,
|
|
397
|
+
"aria-labelledby": ariaLabelledby,
|
|
398
|
+
"data-slot": "select-listbox",
|
|
399
|
+
className: cn(selectListboxVariants(), scrollbarVariants()),
|
|
400
|
+
style: positionStyle,
|
|
401
|
+
children: optionItems
|
|
402
|
+
}
|
|
403
|
+
),
|
|
404
|
+
document.body
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
return reactDom.createPortal(
|
|
408
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
409
|
+
"div",
|
|
410
|
+
{
|
|
411
|
+
ref: panelRef,
|
|
412
|
+
"data-slot": "select-listbox-panel",
|
|
413
|
+
className: selectListboxPanelVariants(),
|
|
414
|
+
style: positionStyle,
|
|
415
|
+
children: [
|
|
416
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
417
|
+
"ul",
|
|
418
|
+
{
|
|
419
|
+
ref: listRef,
|
|
420
|
+
id,
|
|
421
|
+
role: "listbox",
|
|
422
|
+
"aria-multiselectable": multiple || void 0,
|
|
423
|
+
"aria-label": ariaLabel,
|
|
424
|
+
"aria-labelledby": ariaLabelledby,
|
|
425
|
+
"data-slot": "select-listbox",
|
|
426
|
+
className: cn(selectListboxScrollVariants(), scrollbarVariants()),
|
|
427
|
+
children: optionItems
|
|
428
|
+
}
|
|
429
|
+
),
|
|
430
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
431
|
+
"div",
|
|
432
|
+
{
|
|
433
|
+
"data-slot": "select-listbox-footer",
|
|
434
|
+
className: selectListboxFooterVariants(),
|
|
435
|
+
children: footer
|
|
436
|
+
}
|
|
437
|
+
)
|
|
438
|
+
]
|
|
439
|
+
}
|
|
440
|
+
),
|
|
441
|
+
document.body
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
var inputVariants = classVarianceAuthority.cva(
|
|
445
|
+
[
|
|
446
|
+
// Field row layout — icons + input on one line, vertically centred.
|
|
447
|
+
"flex w-full items-center",
|
|
448
|
+
// Colour transitions for the focus border change.
|
|
449
|
+
"transition-colors",
|
|
450
|
+
// I-beam text cursor over the WHOLE field row (not just the inner input),
|
|
451
|
+
// so clicking anywhere in the padding/icon area reads as "type here". This
|
|
452
|
+
// is the only hover affordance — hover intentionally does NOT recolour the
|
|
453
|
+
// border (user-authorized deviation from the Figma). `cursor-text` sits in
|
|
454
|
+
// the base; `has-[:disabled]:cursor-not-allowed` below is declared AFTER it
|
|
455
|
+
// so it wins on a disabled field (same-specificity utilities, later class
|
|
456
|
+
// takes precedence — confirmed by tailwind-merge ordering).
|
|
457
|
+
"cursor-text",
|
|
458
|
+
// Disabled: not-allowed cursor + block pointer interaction on the whole
|
|
459
|
+
// field (the native `disabled` on the input already stops typing; icons are
|
|
460
|
+
// dimmed separately by the `disabled`-derived `iconStateClass` in <Input>).
|
|
461
|
+
// Declared after `cursor-text` so it overrides the I-beam when disabled.
|
|
462
|
+
"has-[:disabled]:cursor-not-allowed"
|
|
463
|
+
],
|
|
464
|
+
{
|
|
465
|
+
variants: {
|
|
466
|
+
variation: {
|
|
467
|
+
field: [
|
|
468
|
+
"bg-input-background border border-input-border rounded-sm",
|
|
469
|
+
// Focus (mouse or keyboard) strengthens the whole border to the
|
|
470
|
+
// accent. Hover does NOT change colour (cursor-only affordance).
|
|
471
|
+
"focus-within:border-input-border-accent"
|
|
472
|
+
],
|
|
473
|
+
line: [
|
|
474
|
+
"border-b border-input-border",
|
|
475
|
+
// Focus (mouse or keyboard) strengthens the bottom line to the
|
|
476
|
+
// accent. Hover does NOT change colour (cursor-only affordance).
|
|
477
|
+
"focus-within:border-input-border-accent"
|
|
478
|
+
]
|
|
479
|
+
},
|
|
480
|
+
size: {
|
|
481
|
+
// sm/lg heights reuse the Button scale; gap follows Figma per size.
|
|
482
|
+
sm: "h-8 gap-1",
|
|
483
|
+
lg: "h-10 gap-2"
|
|
484
|
+
},
|
|
485
|
+
error: {
|
|
486
|
+
// Error out-ranks focus: declare the destructive border for the resting
|
|
487
|
+
// state AND its `focus-within:` form so an errored field never recolours
|
|
488
|
+
// to the accent when focused (a bare `border-…` alone would lose to the
|
|
489
|
+
// variation's `focus-within:` accent utility above). No `hover:` form is
|
|
490
|
+
// needed — hover no longer changes colour on any variation. The `error`
|
|
491
|
+
// variant's classes are emitted after the variation's, so at equal
|
|
492
|
+
// specificity the destructive `focus-within:` wins by order.
|
|
493
|
+
true: "border-input-destructive-foreground-accent focus-within:border-input-destructive-foreground-accent",
|
|
494
|
+
false: ""
|
|
495
|
+
},
|
|
496
|
+
// Bottom-line visibility toggle. LINE ONLY — the hide effect is applied
|
|
497
|
+
// via a compound variant below (scoped to `variation:line`), so `field`
|
|
498
|
+
// is unaffected. Boolean → `true`/`false`; `true` (default) keeps today's
|
|
499
|
+
// behaviour. No classes here: `true` is the resting `border-input-border`
|
|
500
|
+
// from the variation, and `false`'s hide is the compound variant.
|
|
501
|
+
showBottomLine: {
|
|
502
|
+
true: "",
|
|
503
|
+
false: ""
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
compoundVariants: [
|
|
507
|
+
// field padding — Figma field-lg = px-3 (12px) / py-2 (8px).
|
|
508
|
+
{ variation: "field", size: "lg", className: "px-3 py-2" },
|
|
509
|
+
// field-sm: DERIVED. px-2 (8px = spacing/2) steps down from field-lg's
|
|
510
|
+
// px-3 (12px) and equals py-2 for a uniform spacing/2 inset all around.
|
|
511
|
+
{ variation: "field", size: "sm", className: "px-2 py-2" },
|
|
512
|
+
// line — no horizontal padding (Figma px-0); vertical padding per size.
|
|
513
|
+
{ variation: "line", size: "lg", className: "py-2" },
|
|
514
|
+
{ variation: "line", size: "sm", className: "py-2.5" },
|
|
515
|
+
// Hide the resting bottom line (LINE ONLY): recolour to transparent while
|
|
516
|
+
// keeping `border-b` for a stable 1px width (no layout shift). Gated on
|
|
517
|
+
// `error:false` so an errored field keeps its destructive rest border
|
|
518
|
+
// (transparent would otherwise last-win over it). Focus still wins:
|
|
519
|
+
// `focus-within:border-input-border-accent` out-ranks `border-transparent`
|
|
520
|
+
// (later in class order), so the line reappears on focus. `field` is
|
|
521
|
+
// untouched — this compound only matches `variation:line`.
|
|
522
|
+
{
|
|
523
|
+
variation: "line",
|
|
524
|
+
showBottomLine: false,
|
|
525
|
+
error: false,
|
|
526
|
+
className: "border-transparent"
|
|
527
|
+
}
|
|
528
|
+
],
|
|
529
|
+
defaultVariants: {
|
|
530
|
+
variation: "field",
|
|
531
|
+
size: "lg",
|
|
532
|
+
error: false,
|
|
533
|
+
showBottomLine: true
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
);
|
|
537
|
+
function selectTriggerLabel(options, selected, placeholder) {
|
|
538
|
+
if (selected.length === 0) {
|
|
539
|
+
return { text: placeholder, isPlaceholder: true };
|
|
540
|
+
}
|
|
541
|
+
if (selected.length === 1) {
|
|
542
|
+
const match = options.find((o) => o.value === selected[0]);
|
|
543
|
+
return { text: match?.label ?? placeholder, isPlaceholder: match == null };
|
|
544
|
+
}
|
|
545
|
+
return { text: `${selected.length} selected`, isPlaceholder: false };
|
|
546
|
+
}
|
|
547
|
+
function SelectTrigger({
|
|
548
|
+
variation = "field",
|
|
549
|
+
error = false,
|
|
550
|
+
showBottomLine = true,
|
|
551
|
+
open = false,
|
|
552
|
+
displayText,
|
|
553
|
+
isPlaceholder,
|
|
554
|
+
label,
|
|
555
|
+
hint,
|
|
556
|
+
leadingSlot,
|
|
557
|
+
id,
|
|
558
|
+
hintId,
|
|
559
|
+
disabled,
|
|
560
|
+
className,
|
|
561
|
+
wrapperClassName,
|
|
562
|
+
fieldClassName,
|
|
563
|
+
ref,
|
|
564
|
+
...props
|
|
565
|
+
}) {
|
|
566
|
+
const iconStateClass = cn(
|
|
567
|
+
error ? "text-input-destructive-foreground-accent" : "text-input-foreground",
|
|
568
|
+
disabled && "text-input-foreground-muted"
|
|
569
|
+
);
|
|
570
|
+
const HintIcon = error ? react.WarningIcon : react.InfoIcon;
|
|
571
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex flex-col gap-1.5", wrapperClassName), children: [
|
|
572
|
+
label != null && /* @__PURE__ */ jsxRuntime.jsx(
|
|
573
|
+
"label",
|
|
574
|
+
{
|
|
575
|
+
htmlFor: id,
|
|
576
|
+
"data-slot": "select-label",
|
|
577
|
+
className: cn(
|
|
578
|
+
"caption-03-medium uppercase",
|
|
579
|
+
error ? "text-input-destructive-foreground-accent" : "text-input-foreground",
|
|
580
|
+
disabled && "text-input-foreground-muted"
|
|
581
|
+
),
|
|
582
|
+
children: label
|
|
583
|
+
}
|
|
584
|
+
),
|
|
585
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
586
|
+
"button",
|
|
587
|
+
{
|
|
588
|
+
type: "button",
|
|
589
|
+
id,
|
|
590
|
+
ref,
|
|
591
|
+
role: "combobox",
|
|
592
|
+
"data-slot": "select-trigger",
|
|
593
|
+
"data-variation": variation,
|
|
594
|
+
"data-error": error || void 0,
|
|
595
|
+
disabled,
|
|
596
|
+
"aria-invalid": error || void 0,
|
|
597
|
+
"aria-describedby": hintId,
|
|
598
|
+
className: cn(
|
|
599
|
+
// Reuse the Input field-row styling (field/line + focus/error border).
|
|
600
|
+
inputVariants({ variation, size: "lg", error, showBottomLine }),
|
|
601
|
+
// The field is a pointer target, not a text field: pointer cursor,
|
|
602
|
+
// and not-allowed when disabled (overrides `inputVariants`' I-beam).
|
|
603
|
+
"cursor-pointer disabled:cursor-not-allowed",
|
|
604
|
+
// No system focus ring: like Input (ADR-0011), this field-shaped
|
|
605
|
+
// control signals focus purely by recolouring its 1px border to the
|
|
606
|
+
// accent (via `inputVariants`' `focus-within:`). The focusable element
|
|
607
|
+
// here is the field itself, so kill the browser's default outline —
|
|
608
|
+
// otherwise it layers ~1.5px on top of the border and reads as 2px.
|
|
609
|
+
"outline-none",
|
|
610
|
+
// Left-align content; the value grows, the caret stays trailing.
|
|
611
|
+
"text-left",
|
|
612
|
+
// Icon slots inherit the shared state colour; 16px default size for
|
|
613
|
+
// any nested svg that hasn't set its own size (mirrors Input).
|
|
614
|
+
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
615
|
+
iconStateClass,
|
|
616
|
+
fieldClassName,
|
|
617
|
+
className
|
|
618
|
+
),
|
|
619
|
+
...props,
|
|
620
|
+
children: [
|
|
621
|
+
leadingSlot != null && /* @__PURE__ */ jsxRuntime.jsx(
|
|
622
|
+
"span",
|
|
623
|
+
{
|
|
624
|
+
"data-slot": "select-leading",
|
|
625
|
+
"aria-hidden": "true",
|
|
626
|
+
className: "flex items-center",
|
|
627
|
+
children: leadingSlot
|
|
628
|
+
}
|
|
629
|
+
),
|
|
630
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
631
|
+
"span",
|
|
632
|
+
{
|
|
633
|
+
"data-slot": "select-value",
|
|
634
|
+
className: cn(
|
|
635
|
+
"body-03 w-full min-w-0 truncate",
|
|
636
|
+
isPlaceholder ? "text-input-foreground-muted" : "text-input-foreground-accent",
|
|
637
|
+
disabled && "text-input-foreground-muted"
|
|
638
|
+
),
|
|
639
|
+
children: displayText
|
|
640
|
+
}
|
|
641
|
+
),
|
|
642
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
643
|
+
"span",
|
|
644
|
+
{
|
|
645
|
+
"data-slot": "select-trailing",
|
|
646
|
+
"aria-hidden": "true",
|
|
647
|
+
className: "flex items-center",
|
|
648
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
649
|
+
react.CaretDownIcon,
|
|
650
|
+
{
|
|
651
|
+
weight: "fill",
|
|
652
|
+
className: cn("size-3 transition-transform", open && "rotate-180")
|
|
653
|
+
}
|
|
654
|
+
)
|
|
655
|
+
}
|
|
656
|
+
)
|
|
657
|
+
]
|
|
658
|
+
}
|
|
659
|
+
),
|
|
660
|
+
hint != null && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
661
|
+
"p",
|
|
662
|
+
{
|
|
663
|
+
id: hintId,
|
|
664
|
+
"data-slot": "select-hint",
|
|
665
|
+
className: cn(
|
|
666
|
+
"body-04 flex items-center gap-1",
|
|
667
|
+
error ? "text-input-destructive-foreground-accent" : "text-input-foreground",
|
|
668
|
+
disabled && "text-input-foreground-muted"
|
|
669
|
+
),
|
|
670
|
+
children: [
|
|
671
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
672
|
+
HintIcon,
|
|
673
|
+
{
|
|
674
|
+
"aria-hidden": "true",
|
|
675
|
+
weight: "regular",
|
|
676
|
+
className: "size-4 shrink-0"
|
|
677
|
+
}
|
|
678
|
+
),
|
|
679
|
+
hint
|
|
680
|
+
]
|
|
681
|
+
}
|
|
682
|
+
)
|
|
683
|
+
] });
|
|
684
|
+
}
|
|
685
|
+
var TYPEAHEAD_RESET_MS = 500;
|
|
686
|
+
function nextEnabledIndex(options, from, dir) {
|
|
687
|
+
for (let i = from + dir; i >= 0 && i < options.length; i += dir) {
|
|
688
|
+
if (!options[i].disabled) return i;
|
|
689
|
+
}
|
|
690
|
+
return from;
|
|
691
|
+
}
|
|
692
|
+
function firstEnabledIndex(options) {
|
|
693
|
+
const i = options.findIndex((o) => !o.disabled);
|
|
694
|
+
return i;
|
|
695
|
+
}
|
|
696
|
+
function toArray(value) {
|
|
697
|
+
if (value == null) return [];
|
|
698
|
+
return Array.isArray(value) ? value : [value];
|
|
699
|
+
}
|
|
700
|
+
function useSelectListbox(options) {
|
|
701
|
+
const {
|
|
702
|
+
options: opts,
|
|
703
|
+
value: valueProp,
|
|
704
|
+
defaultValue,
|
|
705
|
+
onValueChange,
|
|
706
|
+
multiple = false,
|
|
707
|
+
disabled = false,
|
|
708
|
+
closeOnSelect = true,
|
|
709
|
+
id,
|
|
710
|
+
ref,
|
|
711
|
+
placeholder = "Select"
|
|
712
|
+
} = options;
|
|
713
|
+
const listboxId = `${id}-listbox`;
|
|
714
|
+
const isControlled = valueProp !== void 0;
|
|
715
|
+
const [uncontrolled, setUncontrolled] = React2__namespace.useState(
|
|
716
|
+
() => toArray(defaultValue)
|
|
717
|
+
);
|
|
718
|
+
const selected = isControlled ? toArray(valueProp) : uncontrolled;
|
|
719
|
+
const [open, setOpen] = React2__namespace.useState(false);
|
|
720
|
+
const [activeIndex, setActiveIndex] = React2__namespace.useState(-1);
|
|
721
|
+
const [anchorRect, setAnchorRect] = React2__namespace.useState(null);
|
|
722
|
+
const triggerRef = React2__namespace.useRef(null);
|
|
723
|
+
const setTriggerRef = React2__namespace.useCallback(
|
|
724
|
+
(node) => {
|
|
725
|
+
triggerRef.current = node;
|
|
726
|
+
if (typeof ref === "function") ref(node);
|
|
727
|
+
else if (ref != null) ref.current = node;
|
|
728
|
+
},
|
|
729
|
+
[ref]
|
|
730
|
+
);
|
|
731
|
+
const typeahead = React2__namespace.useRef({ buffer: "", at: 0 });
|
|
732
|
+
const commit = (next) => {
|
|
733
|
+
if (!isControlled) setUncontrolled(next);
|
|
734
|
+
onValueChange?.(multiple ? next : next[0] ?? "");
|
|
735
|
+
};
|
|
736
|
+
const measure = React2__namespace.useCallback(() => {
|
|
737
|
+
const node = triggerRef.current;
|
|
738
|
+
if (node) setAnchorRect(node.getBoundingClientRect());
|
|
739
|
+
}, []);
|
|
740
|
+
const openListbox = React2__namespace.useCallback(() => {
|
|
741
|
+
measure();
|
|
742
|
+
const selectedIndex = opts.findIndex((o) => selected.includes(o.value));
|
|
743
|
+
setActiveIndex(
|
|
744
|
+
selectedIndex >= 0 ? selectedIndex : firstEnabledIndex(opts)
|
|
745
|
+
);
|
|
746
|
+
setOpen(true);
|
|
747
|
+
}, [measure, opts, selected]);
|
|
748
|
+
const closeListbox = React2__namespace.useCallback((restoreFocus) => {
|
|
749
|
+
setOpen(false);
|
|
750
|
+
setActiveIndex(-1);
|
|
751
|
+
setAnchorRect(null);
|
|
752
|
+
if (restoreFocus) triggerRef.current?.focus();
|
|
753
|
+
}, []);
|
|
754
|
+
const handleSelect = (optionValue) => {
|
|
755
|
+
if (multiple) {
|
|
756
|
+
const next = selected.includes(optionValue) ? selected.filter((v) => v !== optionValue) : [...selected, optionValue];
|
|
757
|
+
commit(next);
|
|
758
|
+
const idx = opts.findIndex((o) => o.value === optionValue);
|
|
759
|
+
if (idx >= 0) setActiveIndex(idx);
|
|
760
|
+
} else {
|
|
761
|
+
commit([optionValue]);
|
|
762
|
+
if (closeOnSelect) {
|
|
763
|
+
closeListbox(true);
|
|
764
|
+
} else {
|
|
765
|
+
const idx = opts.findIndex((o) => o.value === optionValue);
|
|
766
|
+
if (idx >= 0) setActiveIndex(idx);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
const reset = () => {
|
|
771
|
+
commit([]);
|
|
772
|
+
};
|
|
773
|
+
React2__namespace.useEffect(() => {
|
|
774
|
+
if (!open) return;
|
|
775
|
+
const onReflow = () => measure();
|
|
776
|
+
window.addEventListener("scroll", onReflow, true);
|
|
777
|
+
window.addEventListener("resize", onReflow);
|
|
778
|
+
return () => {
|
|
779
|
+
window.removeEventListener("scroll", onReflow, true);
|
|
780
|
+
window.removeEventListener("resize", onReflow);
|
|
781
|
+
};
|
|
782
|
+
}, [open, measure]);
|
|
783
|
+
React2__namespace.useEffect(() => {
|
|
784
|
+
if (!open) return;
|
|
785
|
+
const onPointerDown = (event) => {
|
|
786
|
+
const target = event.target;
|
|
787
|
+
const trigger = triggerRef.current;
|
|
788
|
+
const listbox = document.getElementById(listboxId);
|
|
789
|
+
const panel = listbox?.closest("[data-slot=select-listbox-panel]") ?? listbox;
|
|
790
|
+
if (trigger?.contains(target) || panel?.contains(target)) return;
|
|
791
|
+
closeListbox(false);
|
|
792
|
+
};
|
|
793
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
794
|
+
return () => document.removeEventListener("pointerdown", onPointerDown);
|
|
795
|
+
}, [open, listboxId, closeListbox]);
|
|
796
|
+
const handleKeyDown = (event) => {
|
|
797
|
+
if (disabled) return;
|
|
798
|
+
switch (event.key) {
|
|
799
|
+
case "ArrowDown": {
|
|
800
|
+
event.preventDefault();
|
|
801
|
+
if (!open) {
|
|
802
|
+
openListbox();
|
|
803
|
+
} else {
|
|
804
|
+
setActiveIndex((i) => nextEnabledIndex(opts, i < 0 ? -1 : i, 1));
|
|
805
|
+
}
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
case "ArrowUp": {
|
|
809
|
+
event.preventDefault();
|
|
810
|
+
if (!open) {
|
|
811
|
+
openListbox();
|
|
812
|
+
} else {
|
|
813
|
+
setActiveIndex(
|
|
814
|
+
(i) => i < 0 ? firstEnabledIndex(opts) : nextEnabledIndex(opts, i, -1)
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
case "Home": {
|
|
820
|
+
if (!open) return;
|
|
821
|
+
event.preventDefault();
|
|
822
|
+
setActiveIndex(firstEnabledIndex(opts));
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
case "End": {
|
|
826
|
+
if (!open) return;
|
|
827
|
+
event.preventDefault();
|
|
828
|
+
setActiveIndex(nextEnabledIndex(opts, opts.length, -1));
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
case "Enter":
|
|
832
|
+
case " ": {
|
|
833
|
+
event.preventDefault();
|
|
834
|
+
if (!open) {
|
|
835
|
+
openListbox();
|
|
836
|
+
} else if (activeIndex >= 0 && !opts[activeIndex]?.disabled) {
|
|
837
|
+
handleSelect(opts[activeIndex].value);
|
|
838
|
+
}
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
case "Escape": {
|
|
842
|
+
if (open) {
|
|
843
|
+
event.preventDefault();
|
|
844
|
+
closeListbox(true);
|
|
845
|
+
}
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
case "Tab": {
|
|
849
|
+
if (open) closeListbox(false);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
default: {
|
|
853
|
+
if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
854
|
+
if (!open) openListbox();
|
|
855
|
+
const now = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
856
|
+
const t = typeahead.current;
|
|
857
|
+
t.buffer = now - t.at > TYPEAHEAD_RESET_MS ? event.key : t.buffer + event.key;
|
|
858
|
+
t.at = now;
|
|
859
|
+
const query = t.buffer.toLowerCase();
|
|
860
|
+
const match = opts.findIndex(
|
|
861
|
+
(o) => !o.disabled && o.label.toLowerCase().startsWith(query)
|
|
862
|
+
);
|
|
863
|
+
if (match >= 0) setActiveIndex(match);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
const handleTriggerClick = () => {
|
|
869
|
+
if (disabled) return;
|
|
870
|
+
if (open) closeListbox(true);
|
|
871
|
+
else openListbox();
|
|
872
|
+
};
|
|
873
|
+
const { text, isPlaceholder } = selectTriggerLabel(
|
|
874
|
+
opts,
|
|
875
|
+
selected,
|
|
876
|
+
placeholder
|
|
877
|
+
);
|
|
878
|
+
const activeId = open && activeIndex >= 0 ? `${listboxId}-opt-${activeIndex}` : void 0;
|
|
879
|
+
return {
|
|
880
|
+
selected,
|
|
881
|
+
open,
|
|
882
|
+
setTriggerRef,
|
|
883
|
+
listboxId,
|
|
884
|
+
activeId,
|
|
885
|
+
triggerProps: {
|
|
886
|
+
onClick: handleTriggerClick,
|
|
887
|
+
onKeyDown: handleKeyDown,
|
|
888
|
+
"aria-haspopup": "listbox",
|
|
889
|
+
"aria-expanded": open,
|
|
890
|
+
"aria-controls": open ? listboxId : void 0,
|
|
891
|
+
"aria-activedescendant": activeId
|
|
892
|
+
},
|
|
893
|
+
listboxProps: {
|
|
894
|
+
id: listboxId,
|
|
895
|
+
options: opts,
|
|
896
|
+
selected,
|
|
897
|
+
multiple,
|
|
898
|
+
activeIndex,
|
|
899
|
+
anchorRect,
|
|
900
|
+
onSelect: handleSelect,
|
|
901
|
+
onActiveChange: setActiveIndex
|
|
902
|
+
},
|
|
903
|
+
closeListbox,
|
|
904
|
+
text,
|
|
905
|
+
isPlaceholder,
|
|
906
|
+
reset
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
function Select({
|
|
910
|
+
options,
|
|
911
|
+
value: valueProp,
|
|
912
|
+
defaultValue,
|
|
913
|
+
onValueChange,
|
|
914
|
+
multiple = false,
|
|
915
|
+
variation = "field",
|
|
916
|
+
placeholder = "Select",
|
|
917
|
+
label,
|
|
918
|
+
error = false,
|
|
919
|
+
hint,
|
|
920
|
+
leadingSlot,
|
|
921
|
+
showBottomLine = true,
|
|
922
|
+
disabled,
|
|
923
|
+
id: idProp,
|
|
924
|
+
className,
|
|
925
|
+
wrapperClassName,
|
|
926
|
+
fieldClassName,
|
|
927
|
+
ref,
|
|
928
|
+
...props
|
|
929
|
+
}) {
|
|
930
|
+
const generatedId = React2__namespace.useId();
|
|
931
|
+
const id = idProp ?? generatedId;
|
|
932
|
+
const hintId = hint != null ? `${id}-hint` : void 0;
|
|
933
|
+
const {
|
|
934
|
+
open,
|
|
935
|
+
setTriggerRef,
|
|
936
|
+
triggerProps,
|
|
937
|
+
listboxProps,
|
|
938
|
+
text,
|
|
939
|
+
isPlaceholder
|
|
940
|
+
} = useSelectListbox({
|
|
941
|
+
options,
|
|
942
|
+
value: valueProp,
|
|
943
|
+
defaultValue,
|
|
944
|
+
onValueChange,
|
|
945
|
+
multiple,
|
|
946
|
+
disabled,
|
|
947
|
+
id,
|
|
948
|
+
ref,
|
|
949
|
+
placeholder
|
|
950
|
+
});
|
|
951
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
952
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
953
|
+
SelectTrigger,
|
|
954
|
+
{
|
|
955
|
+
ref: setTriggerRef,
|
|
956
|
+
id,
|
|
957
|
+
variation,
|
|
958
|
+
error,
|
|
959
|
+
showBottomLine,
|
|
960
|
+
open,
|
|
961
|
+
displayText: text,
|
|
962
|
+
isPlaceholder,
|
|
963
|
+
label,
|
|
964
|
+
hint,
|
|
965
|
+
hintId,
|
|
966
|
+
leadingSlot,
|
|
967
|
+
disabled,
|
|
968
|
+
className,
|
|
969
|
+
wrapperClassName,
|
|
970
|
+
fieldClassName,
|
|
971
|
+
...triggerProps,
|
|
972
|
+
...props
|
|
973
|
+
}
|
|
974
|
+
),
|
|
975
|
+
open && /* @__PURE__ */ jsxRuntime.jsx(
|
|
976
|
+
SelectListbox,
|
|
977
|
+
{
|
|
978
|
+
...listboxProps,
|
|
979
|
+
"aria-labelledby": label != null ? id : void 0,
|
|
980
|
+
"aria-label": label == null ? props["aria-label"] : void 0
|
|
981
|
+
}
|
|
982
|
+
)
|
|
983
|
+
] });
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
exports.Select = Select;
|
|
987
|
+
exports.SelectListbox = SelectListbox;
|
|
988
|
+
exports.SelectTrigger = SelectTrigger;
|
|
989
|
+
exports.computeListboxHorizontal = computeListboxHorizontal;
|
|
990
|
+
exports.computeListboxPosition = computeListboxPosition;
|
|
991
|
+
exports.nextEnabledIndex = nextEnabledIndex;
|
|
992
|
+
exports.selectListboxFooterVariants = selectListboxFooterVariants;
|
|
993
|
+
exports.selectListboxPanelVariants = selectListboxPanelVariants;
|
|
994
|
+
exports.selectListboxScrollVariants = selectListboxScrollVariants;
|
|
995
|
+
exports.selectListboxVariants = selectListboxVariants;
|
|
996
|
+
exports.selectOptionVariants = selectOptionVariants;
|
|
997
|
+
exports.selectTriggerLabel = selectTriggerLabel;
|
|
998
|
+
exports.useSelectListbox = useSelectListbox;
|