@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.
Files changed (136) hide show
  1. package/LICENSE +19 -0
  2. package/README.md +119 -0
  3. package/dist/Radio-BYWz1n7p.d.cts +32 -0
  4. package/dist/Radio-BYWz1n7p.d.ts +32 -0
  5. package/dist/SelectTrigger-By0iPBI3.d.cts +78 -0
  6. package/dist/SelectTrigger-By0iPBI3.d.ts +78 -0
  7. package/dist/accordion.cjs +322 -0
  8. package/dist/accordion.d.cts +156 -0
  9. package/dist/accordion.d.ts +156 -0
  10. package/dist/accordion.js +2 -0
  11. package/dist/apps-ui.umd.js +1 -0
  12. package/dist/button.cjs +366 -0
  13. package/dist/button.d.cts +52 -0
  14. package/dist/button.d.ts +52 -0
  15. package/dist/button.js +3 -0
  16. package/dist/checkbox.cjs +74 -0
  17. package/dist/checkbox.d.cts +63 -0
  18. package/dist/checkbox.d.ts +63 -0
  19. package/dist/checkbox.js +2 -0
  20. package/dist/chunk-3RU7JMFE.js +27 -0
  21. package/dist/chunk-5TWRK4NW.js +783 -0
  22. package/dist/chunk-7EOSDFS3.js +9 -0
  23. package/dist/chunk-AIQ4UOGK.js +142 -0
  24. package/dist/chunk-B3MMGTPV.js +200 -0
  25. package/dist/chunk-DAHQYLUI.js +146 -0
  26. package/dist/chunk-DGBXADSS.js +1 -0
  27. package/dist/chunk-E6KPNTWW.js +295 -0
  28. package/dist/chunk-EOAB6G2D.js +149 -0
  29. package/dist/chunk-FAOVKDVM.js +32 -0
  30. package/dist/chunk-FV76Z4Z2.js +78 -0
  31. package/dist/chunk-GLTLBUZ7.js +285 -0
  32. package/dist/chunk-GTZFV43T.js +154 -0
  33. package/dist/chunk-HRGZNL4A.js +88 -0
  34. package/dist/chunk-IEVN3J2F.js +98 -0
  35. package/dist/chunk-IRS3T6LF.js +352 -0
  36. package/dist/chunk-IYYHCQXG.js +146 -0
  37. package/dist/chunk-JIXW3455.js +89 -0
  38. package/dist/chunk-N4VAP2GB.js +77 -0
  39. package/dist/chunk-SJ2ZRYVV.js +167 -0
  40. package/dist/chunk-SNU4YXOR.js +66 -0
  41. package/dist/chunk-TT6JQATV.js +99 -0
  42. package/dist/chunk-UPQ3ZPCO.js +66 -0
  43. package/dist/chunk-UVUSVFOH.js +264 -0
  44. package/dist/chunk-WJFPGGJB.js +255 -0
  45. package/dist/chunk-WXEBQA7A.js +371 -0
  46. package/dist/chunk-Y2O4YYZQ.js +184 -0
  47. package/dist/cut-corner.cjs +111 -0
  48. package/dist/cut-corner.d.cts +49 -0
  49. package/dist/cut-corner.d.ts +49 -0
  50. package/dist/cut-corner.js +1 -0
  51. package/dist/dialog.cjs +441 -0
  52. package/dist/dialog.d.cts +240 -0
  53. package/dist/dialog.d.ts +240 -0
  54. package/dist/dialog.js +3 -0
  55. package/dist/feature-card.cjs +182 -0
  56. package/dist/feature-card.d.cts +54 -0
  57. package/dist/feature-card.d.ts +54 -0
  58. package/dist/feature-card.js +2 -0
  59. package/dist/filter.cjs +873 -0
  60. package/dist/filter.d.cts +129 -0
  61. package/dist/filter.d.ts +129 -0
  62. package/dist/filter.js +6 -0
  63. package/dist/index.cjs +4583 -0
  64. package/dist/index.d.cts +29 -0
  65. package/dist/index.d.ts +29 -0
  66. package/dist/index.js +27 -0
  67. package/dist/inline-select.cjs +856 -0
  68. package/dist/inline-select.d.cts +92 -0
  69. package/dist/inline-select.d.ts +92 -0
  70. package/dist/inline-select.js +6 -0
  71. package/dist/input.cjs +263 -0
  72. package/dist/input.d.cts +157 -0
  73. package/dist/input.d.ts +157 -0
  74. package/dist/input.js +4 -0
  75. package/dist/panel.cjs +185 -0
  76. package/dist/panel.d.cts +103 -0
  77. package/dist/panel.d.ts +103 -0
  78. package/dist/panel.js +2 -0
  79. package/dist/property-grid.cjs +110 -0
  80. package/dist/property-grid.d.cts +56 -0
  81. package/dist/property-grid.d.ts +56 -0
  82. package/dist/property-grid.js +2 -0
  83. package/dist/radio.cjs +74 -0
  84. package/dist/radio.d.cts +39 -0
  85. package/dist/radio.d.ts +39 -0
  86. package/dist/radio.js +2 -0
  87. package/dist/scrollbar.cjs +34 -0
  88. package/dist/scrollbar.d.cts +35 -0
  89. package/dist/scrollbar.d.ts +35 -0
  90. package/dist/scrollbar.js +1 -0
  91. package/dist/search-bar.cjs +408 -0
  92. package/dist/search-bar.d.cts +42 -0
  93. package/dist/search-bar.d.ts +42 -0
  94. package/dist/search-bar.js +4 -0
  95. package/dist/select.cjs +998 -0
  96. package/dist/select.d.cts +357 -0
  97. package/dist/select.d.ts +357 -0
  98. package/dist/select.js +5 -0
  99. package/dist/selectable-feature-card.cjs +159 -0
  100. package/dist/selectable-feature-card.d.cts +74 -0
  101. package/dist/selectable-feature-card.d.ts +74 -0
  102. package/dist/selectable-feature-card.js +3 -0
  103. package/dist/sidebar-item.cjs +175 -0
  104. package/dist/sidebar-item.d.cts +75 -0
  105. package/dist/sidebar-item.d.ts +75 -0
  106. package/dist/sidebar-item.js +2 -0
  107. package/dist/switch.cjs +86 -0
  108. package/dist/switch.d.cts +72 -0
  109. package/dist/switch.d.ts +72 -0
  110. package/dist/switch.js +2 -0
  111. package/dist/table-cell.cjs +196 -0
  112. package/dist/table-cell.d.cts +76 -0
  113. package/dist/table-cell.d.ts +76 -0
  114. package/dist/table-cell.js +2 -0
  115. package/dist/table-title.cjs +34 -0
  116. package/dist/table-title.d.cts +16 -0
  117. package/dist/table-title.d.ts +16 -0
  118. package/dist/table-title.js +2 -0
  119. package/dist/tabs.cjs +299 -0
  120. package/dist/tabs.d.cts +116 -0
  121. package/dist/tabs.d.ts +116 -0
  122. package/dist/tabs.js +2 -0
  123. package/dist/toast.cjs +87 -0
  124. package/dist/toast.d.cts +48 -0
  125. package/dist/toast.d.ts +48 -0
  126. package/dist/toast.js +2 -0
  127. package/dist/token-input.cjs +415 -0
  128. package/dist/token-input.d.cts +90 -0
  129. package/dist/token-input.d.ts +90 -0
  130. package/dist/token-input.js +3 -0
  131. package/dist/tooltip.cjs +384 -0
  132. package/dist/tooltip.d.cts +142 -0
  133. package/dist/tooltip.d.ts +142 -0
  134. package/dist/tooltip.js +2 -0
  135. package/package.json +200 -0
  136. package/src/fonts.css +33 -0
@@ -0,0 +1,783 @@
1
+ import { inputVariants } from './chunk-IEVN3J2F.js';
2
+ import { Checkbox } from './chunk-SNU4YXOR.js';
3
+ import { scrollbarVariants } from './chunk-FAOVKDVM.js';
4
+ import { cn } from './chunk-7EOSDFS3.js';
5
+ import * as React2 from 'react';
6
+ import { createPortal } from 'react-dom';
7
+ import { cva } from 'class-variance-authority';
8
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
9
+ import { CaretDownIcon, WarningIcon, InfoIcon } from '@phosphor-icons/react';
10
+
11
+ var selectListboxVariants = cva([
12
+ "flex flex-col",
13
+ "bg-card-background border border-card-border rounded-sm shadow-s",
14
+ // Small vertical inset so the first/last option isn't flush to the border.
15
+ "py-1",
16
+ // Scroll when the list outgrows its max-height (set inline from the rect).
17
+ "overflow-y-auto",
18
+ // The panel is not itself focusable; focus stays on the trigger.
19
+ "outline-none"
20
+ ]);
21
+ var selectListboxPanelVariants = cva([
22
+ "flex flex-col min-h-0",
23
+ "bg-card-background border border-card-border rounded-sm shadow-s",
24
+ "overflow-hidden",
25
+ "outline-none"
26
+ ]);
27
+ var selectListboxScrollVariants = cva([
28
+ "flex flex-col min-h-0 flex-1",
29
+ "py-1",
30
+ "overflow-y-auto",
31
+ "outline-none"
32
+ ]);
33
+ var selectListboxFooterVariants = cva([
34
+ "shrink-0",
35
+ "border-t border-t-card-border",
36
+ "px-3 py-1"
37
+ ]);
38
+ var selectOptionVariants = cva(
39
+ [
40
+ "body-03",
41
+ "flex w-full items-center gap-2",
42
+ "px-3 py-2",
43
+ "cursor-pointer select-none",
44
+ "transition-colors",
45
+ // Reserve the 3px left accent bar on EVERY row (transparent by default,
46
+ // recoloured only when single-selected). Keeping the border-box constant
47
+ // across rows means selecting one never shifts its text right — same
48
+ // no-layout-shift trick Input uses for its `showBottomLine` bottom line.
49
+ "border-l-3",
50
+ // Mirror the 3px on the right (always transparent) so content is inset
51
+ // equally on both sides — the left accent bar would otherwise make the left
52
+ // padding read 3px larger than the right.
53
+ "border-r-3 border-r-transparent"
54
+ // NOTE: no text-/border-colour in the base. cva concatenates class strings
55
+ // without resolving conflicting Tailwind utilities, so a base colour + a
56
+ // compound colour would both survive and CSS source-order (not our intent)
57
+ // would pick the winner. Every state below sets exactly ONE text colour and
58
+ // ONE left-border colour via compound variants.
59
+ ],
60
+ {
61
+ variants: {
62
+ active: { true: "", false: "" },
63
+ selected: { true: "", false: "" },
64
+ multiple: { true: "", false: "" },
65
+ disabled: { true: "cursor-not-allowed", false: "" }
66
+ },
67
+ compoundVariants: [
68
+ // ── Text colour (exactly one matches per state) ──
69
+ { disabled: true, className: "text-foreground-muted" },
70
+ { disabled: false, selected: false, className: "text-foreground" },
71
+ // Multi keeps the normal foreground when selected (the checkbox marks it).
72
+ {
73
+ disabled: false,
74
+ selected: true,
75
+ multiple: true,
76
+ className: "text-foreground"
77
+ },
78
+ // Single-select selected → brand foreground (Figma node 4921:8203).
79
+ {
80
+ disabled: false,
81
+ selected: true,
82
+ multiple: false,
83
+ className: "text-card-brand-foreground"
84
+ },
85
+ // ── Left accent bar colour (exactly one matches per state) ──
86
+ // Invisible on every row except the single-select selected one, where it
87
+ // becomes the brand accent. Width is reserved in the base, so this is a
88
+ // pure colour change — no content shift.
89
+ { selected: false, className: "border-l-transparent" },
90
+ { selected: true, multiple: true, className: "border-l-transparent" },
91
+ {
92
+ selected: true,
93
+ multiple: false,
94
+ className: "border-l-card-brand-border-accent"
95
+ },
96
+ // ── Background ──
97
+ // Neutral hover/keyboard highlight (Figma card/neutral/normal/accent) —
98
+ // every row EXCEPT a single-select selected one (which stays branded).
99
+ { active: true, selected: false, className: "bg-card-accent" },
100
+ {
101
+ active: true,
102
+ selected: true,
103
+ multiple: true,
104
+ className: "bg-card-accent"
105
+ },
106
+ // Single-select selected: brand-tinted row. No neutral `active` bg matches
107
+ // this combo, so it stays branded on hover.
108
+ { selected: true, multiple: false, className: "bg-card-brand-accent" }
109
+ ],
110
+ defaultVariants: {
111
+ active: false,
112
+ selected: false,
113
+ multiple: false,
114
+ disabled: false
115
+ }
116
+ }
117
+ );
118
+ var LISTBOX_OFFSET = 4;
119
+ var LISTBOX_MARGIN = 8;
120
+ var LISTBOX_MAX_HEIGHT = 208;
121
+ function computeListboxPosition(anchor, listboxHeight, viewportHeight) {
122
+ const flipsAbove = anchor.bottom + listboxHeight > viewportHeight && anchor.top > listboxHeight;
123
+ return {
124
+ left: anchor.left,
125
+ width: anchor.width,
126
+ top: flipsAbove ? anchor.top - listboxHeight : anchor.bottom,
127
+ placement: flipsAbove ? "top" : "bottom"
128
+ };
129
+ }
130
+ function computeListboxHorizontal(anchor, widthMode, measuredWidth, viewportWidth, margin) {
131
+ if (widthMode === "trigger") {
132
+ return { left: anchor.left, width: anchor.width };
133
+ }
134
+ const maxWidth = viewportWidth - 2 * margin;
135
+ const upperBound = viewportWidth - margin - measuredWidth;
136
+ const left = Math.max(margin, Math.min(anchor.left, upperBound));
137
+ return { left, minWidth: anchor.width, maxWidth };
138
+ }
139
+ function SelectListbox({
140
+ id,
141
+ options,
142
+ selected,
143
+ multiple,
144
+ activeIndex,
145
+ anchorRect,
146
+ widthMode = "trigger",
147
+ onSelect,
148
+ onActiveChange,
149
+ footer,
150
+ "aria-label": ariaLabel,
151
+ "aria-labelledby": ariaLabelledby
152
+ }) {
153
+ const listRef = React2.useRef(null);
154
+ const panelRef = React2.useRef(null);
155
+ const hasFooter = footer != null;
156
+ const [style, setStyle] = React2.useState(null);
157
+ React2.useLayoutEffect(() => {
158
+ if (anchorRect == null) {
159
+ setStyle(null);
160
+ return;
161
+ }
162
+ const measureEl = hasFooter ? panelRef.current : listRef.current;
163
+ const measured = measureEl ? measureEl.offsetHeight : 0;
164
+ const height = Math.min(measured || LISTBOX_MAX_HEIGHT, LISTBOX_MAX_HEIGHT);
165
+ const pos = computeListboxPosition(anchorRect, height, window.innerHeight);
166
+ const measuredWidth = measureEl ? measureEl.offsetWidth : 0;
167
+ const horizontal = computeListboxHorizontal(
168
+ anchorRect,
169
+ widthMode,
170
+ measuredWidth,
171
+ window.innerWidth,
172
+ LISTBOX_MARGIN
173
+ );
174
+ setStyle({
175
+ position: "absolute",
176
+ top: pos.top + window.scrollY + (pos.placement === "bottom" ? LISTBOX_OFFSET : -LISTBOX_OFFSET),
177
+ left: horizontal.left + window.scrollX,
178
+ // `"trigger"`: fixed pixel width = trigger width (Select, unchanged).
179
+ // `"auto"`: content-sized via `max-content`, bounded by min/max width.
180
+ ...widthMode === "trigger" ? { width: horizontal.width } : {
181
+ width: "max-content",
182
+ minWidth: horizontal.minWidth,
183
+ maxWidth: horizontal.maxWidth
184
+ },
185
+ // Bound to the design token (208px). `overflow-y-auto` on the panel
186
+ // scrolls the list past this cap.
187
+ maxHeight: "var(--container-4xs)",
188
+ zIndex: 50
189
+ });
190
+ }, [anchorRect, widthMode, hasFooter]);
191
+ React2.useLayoutEffect(() => {
192
+ if (activeIndex < 0) return;
193
+ const el = listRef.current?.querySelector(
194
+ `#${CSS.escape(`${id}-opt-${activeIndex}`)}`
195
+ );
196
+ el?.scrollIntoView?.({ block: "nearest" });
197
+ }, [activeIndex, id]);
198
+ const positionStyle = style ?? (widthMode === "auto" ? { position: "absolute", visibility: "hidden", width: "max-content" } : { position: "absolute", visibility: "hidden" });
199
+ const optionItems = /* @__PURE__ */ jsx(Fragment, { children: options.map((option, index) => {
200
+ const isSelected = selected.includes(option.value);
201
+ const isActive = index === activeIndex;
202
+ const isDisabled = option.disabled === true;
203
+ const iconColorClass = isSelected && !multiple ? "text-card-brand-foreground" : "text-foreground-muted";
204
+ return /* @__PURE__ */ jsxs(
205
+ "li",
206
+ {
207
+ id: `${id}-opt-${index}`,
208
+ role: "option",
209
+ "aria-selected": isSelected,
210
+ "aria-disabled": isDisabled || void 0,
211
+ "data-slot": "select-option",
212
+ "data-active": isActive || void 0,
213
+ className: selectOptionVariants({
214
+ active: isActive,
215
+ selected: isSelected,
216
+ multiple,
217
+ disabled: isDisabled
218
+ }),
219
+ onMouseEnter: () => {
220
+ if (!isDisabled) onActiveChange(index);
221
+ },
222
+ onMouseDown: (event) => {
223
+ event.preventDefault();
224
+ if (!isDisabled) onSelect(option.value);
225
+ },
226
+ children: [
227
+ multiple && // Visual mirror of the row's selection — the `option` carries the
228
+ // semantics, so the checkbox is hidden from AT and out of the tab
229
+ // order. `readOnly` because the <li> owns the toggle.
230
+ /* @__PURE__ */ jsx(
231
+ Checkbox,
232
+ {
233
+ checked: isSelected,
234
+ disabled: true,
235
+ tabIndex: -1,
236
+ "aria-hidden": "true",
237
+ "data-slot": "select-option-checkbox",
238
+ className: cn(
239
+ "pointer-events-none disabled:border-border",
240
+ "checked:disabled:bg-brand-primary-background checked:disabled:border-brand-primary-border"
241
+ ),
242
+ wrapperClassName: "[&_[data-slot=checkbox-indicator]]:text-brand-primary-foreground"
243
+ }
244
+ ),
245
+ option.leadingSlot != null && // After the checkbox in multi mode. Decorative; muted foreground
246
+ // except on the single-select selected row (brand), per iconColorClass.
247
+ /* @__PURE__ */ jsx(
248
+ "span",
249
+ {
250
+ "data-slot": "select-option-leading",
251
+ "aria-hidden": "true",
252
+ className: cn(
253
+ "flex shrink-0 items-center [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
254
+ iconColorClass
255
+ ),
256
+ children: option.leadingSlot
257
+ }
258
+ ),
259
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: option.label }),
260
+ option.trailingSlot != null && /* @__PURE__ */ jsx(
261
+ "span",
262
+ {
263
+ "data-slot": "select-option-trailing",
264
+ "aria-hidden": "true",
265
+ className: cn(
266
+ "flex shrink-0 items-center [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
267
+ iconColorClass
268
+ ),
269
+ children: option.trailingSlot
270
+ }
271
+ )
272
+ ]
273
+ },
274
+ option.value
275
+ );
276
+ }) });
277
+ if (!hasFooter) {
278
+ return createPortal(
279
+ /* @__PURE__ */ jsx(
280
+ "ul",
281
+ {
282
+ ref: listRef,
283
+ id,
284
+ role: "listbox",
285
+ "aria-multiselectable": multiple || void 0,
286
+ "aria-label": ariaLabel,
287
+ "aria-labelledby": ariaLabelledby,
288
+ "data-slot": "select-listbox",
289
+ className: cn(selectListboxVariants(), scrollbarVariants()),
290
+ style: positionStyle,
291
+ children: optionItems
292
+ }
293
+ ),
294
+ document.body
295
+ );
296
+ }
297
+ return createPortal(
298
+ /* @__PURE__ */ jsxs(
299
+ "div",
300
+ {
301
+ ref: panelRef,
302
+ "data-slot": "select-listbox-panel",
303
+ className: selectListboxPanelVariants(),
304
+ style: positionStyle,
305
+ children: [
306
+ /* @__PURE__ */ jsx(
307
+ "ul",
308
+ {
309
+ ref: listRef,
310
+ id,
311
+ role: "listbox",
312
+ "aria-multiselectable": multiple || void 0,
313
+ "aria-label": ariaLabel,
314
+ "aria-labelledby": ariaLabelledby,
315
+ "data-slot": "select-listbox",
316
+ className: cn(selectListboxScrollVariants(), scrollbarVariants()),
317
+ children: optionItems
318
+ }
319
+ ),
320
+ /* @__PURE__ */ jsx(
321
+ "div",
322
+ {
323
+ "data-slot": "select-listbox-footer",
324
+ className: selectListboxFooterVariants(),
325
+ children: footer
326
+ }
327
+ )
328
+ ]
329
+ }
330
+ ),
331
+ document.body
332
+ );
333
+ }
334
+ function selectTriggerLabel(options, selected, placeholder) {
335
+ if (selected.length === 0) {
336
+ return { text: placeholder, isPlaceholder: true };
337
+ }
338
+ if (selected.length === 1) {
339
+ const match = options.find((o) => o.value === selected[0]);
340
+ return { text: match?.label ?? placeholder, isPlaceholder: match == null };
341
+ }
342
+ return { text: `${selected.length} selected`, isPlaceholder: false };
343
+ }
344
+ function SelectTrigger({
345
+ variation = "field",
346
+ error = false,
347
+ showBottomLine = true,
348
+ open = false,
349
+ displayText,
350
+ isPlaceholder,
351
+ label,
352
+ hint,
353
+ leadingSlot,
354
+ id,
355
+ hintId,
356
+ disabled,
357
+ className,
358
+ wrapperClassName,
359
+ fieldClassName,
360
+ ref,
361
+ ...props
362
+ }) {
363
+ const iconStateClass = cn(
364
+ error ? "text-input-destructive-foreground-accent" : "text-input-foreground",
365
+ disabled && "text-input-foreground-muted"
366
+ );
367
+ const HintIcon = error ? WarningIcon : InfoIcon;
368
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-1.5", wrapperClassName), children: [
369
+ label != null && /* @__PURE__ */ jsx(
370
+ "label",
371
+ {
372
+ htmlFor: id,
373
+ "data-slot": "select-label",
374
+ className: cn(
375
+ "caption-03-medium uppercase",
376
+ error ? "text-input-destructive-foreground-accent" : "text-input-foreground",
377
+ disabled && "text-input-foreground-muted"
378
+ ),
379
+ children: label
380
+ }
381
+ ),
382
+ /* @__PURE__ */ jsxs(
383
+ "button",
384
+ {
385
+ type: "button",
386
+ id,
387
+ ref,
388
+ role: "combobox",
389
+ "data-slot": "select-trigger",
390
+ "data-variation": variation,
391
+ "data-error": error || void 0,
392
+ disabled,
393
+ "aria-invalid": error || void 0,
394
+ "aria-describedby": hintId,
395
+ className: cn(
396
+ // Reuse the Input field-row styling (field/line + focus/error border).
397
+ inputVariants({ variation, size: "lg", error, showBottomLine }),
398
+ // The field is a pointer target, not a text field: pointer cursor,
399
+ // and not-allowed when disabled (overrides `inputVariants`' I-beam).
400
+ "cursor-pointer disabled:cursor-not-allowed",
401
+ // No system focus ring: like Input (ADR-0011), this field-shaped
402
+ // control signals focus purely by recolouring its 1px border to the
403
+ // accent (via `inputVariants`' `focus-within:`). The focusable element
404
+ // here is the field itself, so kill the browser's default outline —
405
+ // otherwise it layers ~1.5px on top of the border and reads as 2px.
406
+ "outline-none",
407
+ // Left-align content; the value grows, the caret stays trailing.
408
+ "text-left",
409
+ // Icon slots inherit the shared state colour; 16px default size for
410
+ // any nested svg that hasn't set its own size (mirrors Input).
411
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
412
+ iconStateClass,
413
+ fieldClassName,
414
+ className
415
+ ),
416
+ ...props,
417
+ children: [
418
+ leadingSlot != null && /* @__PURE__ */ jsx(
419
+ "span",
420
+ {
421
+ "data-slot": "select-leading",
422
+ "aria-hidden": "true",
423
+ className: "flex items-center",
424
+ children: leadingSlot
425
+ }
426
+ ),
427
+ /* @__PURE__ */ jsx(
428
+ "span",
429
+ {
430
+ "data-slot": "select-value",
431
+ className: cn(
432
+ "body-03 w-full min-w-0 truncate",
433
+ isPlaceholder ? "text-input-foreground-muted" : "text-input-foreground-accent",
434
+ disabled && "text-input-foreground-muted"
435
+ ),
436
+ children: displayText
437
+ }
438
+ ),
439
+ /* @__PURE__ */ jsx(
440
+ "span",
441
+ {
442
+ "data-slot": "select-trailing",
443
+ "aria-hidden": "true",
444
+ className: "flex items-center",
445
+ children: /* @__PURE__ */ jsx(
446
+ CaretDownIcon,
447
+ {
448
+ weight: "fill",
449
+ className: cn("size-3 transition-transform", open && "rotate-180")
450
+ }
451
+ )
452
+ }
453
+ )
454
+ ]
455
+ }
456
+ ),
457
+ hint != null && /* @__PURE__ */ jsxs(
458
+ "p",
459
+ {
460
+ id: hintId,
461
+ "data-slot": "select-hint",
462
+ className: cn(
463
+ "body-04 flex items-center gap-1",
464
+ error ? "text-input-destructive-foreground-accent" : "text-input-foreground",
465
+ disabled && "text-input-foreground-muted"
466
+ ),
467
+ children: [
468
+ /* @__PURE__ */ jsx(
469
+ HintIcon,
470
+ {
471
+ "aria-hidden": "true",
472
+ weight: "regular",
473
+ className: "size-4 shrink-0"
474
+ }
475
+ ),
476
+ hint
477
+ ]
478
+ }
479
+ )
480
+ ] });
481
+ }
482
+ var TYPEAHEAD_RESET_MS = 500;
483
+ function nextEnabledIndex(options, from, dir) {
484
+ for (let i = from + dir; i >= 0 && i < options.length; i += dir) {
485
+ if (!options[i].disabled) return i;
486
+ }
487
+ return from;
488
+ }
489
+ function firstEnabledIndex(options) {
490
+ const i = options.findIndex((o) => !o.disabled);
491
+ return i;
492
+ }
493
+ function toArray(value) {
494
+ if (value == null) return [];
495
+ return Array.isArray(value) ? value : [value];
496
+ }
497
+ function useSelectListbox(options) {
498
+ const {
499
+ options: opts,
500
+ value: valueProp,
501
+ defaultValue,
502
+ onValueChange,
503
+ multiple = false,
504
+ disabled = false,
505
+ closeOnSelect = true,
506
+ id,
507
+ ref,
508
+ placeholder = "Select"
509
+ } = options;
510
+ const listboxId = `${id}-listbox`;
511
+ const isControlled = valueProp !== void 0;
512
+ const [uncontrolled, setUncontrolled] = React2.useState(
513
+ () => toArray(defaultValue)
514
+ );
515
+ const selected = isControlled ? toArray(valueProp) : uncontrolled;
516
+ const [open, setOpen] = React2.useState(false);
517
+ const [activeIndex, setActiveIndex] = React2.useState(-1);
518
+ const [anchorRect, setAnchorRect] = React2.useState(null);
519
+ const triggerRef = React2.useRef(null);
520
+ const setTriggerRef = React2.useCallback(
521
+ (node) => {
522
+ triggerRef.current = node;
523
+ if (typeof ref === "function") ref(node);
524
+ else if (ref != null) ref.current = node;
525
+ },
526
+ [ref]
527
+ );
528
+ const typeahead = React2.useRef({ buffer: "", at: 0 });
529
+ const commit = (next) => {
530
+ if (!isControlled) setUncontrolled(next);
531
+ onValueChange?.(multiple ? next : next[0] ?? "");
532
+ };
533
+ const measure = React2.useCallback(() => {
534
+ const node = triggerRef.current;
535
+ if (node) setAnchorRect(node.getBoundingClientRect());
536
+ }, []);
537
+ const openListbox = React2.useCallback(() => {
538
+ measure();
539
+ const selectedIndex = opts.findIndex((o) => selected.includes(o.value));
540
+ setActiveIndex(
541
+ selectedIndex >= 0 ? selectedIndex : firstEnabledIndex(opts)
542
+ );
543
+ setOpen(true);
544
+ }, [measure, opts, selected]);
545
+ const closeListbox = React2.useCallback((restoreFocus) => {
546
+ setOpen(false);
547
+ setActiveIndex(-1);
548
+ setAnchorRect(null);
549
+ if (restoreFocus) triggerRef.current?.focus();
550
+ }, []);
551
+ const handleSelect = (optionValue) => {
552
+ if (multiple) {
553
+ const next = selected.includes(optionValue) ? selected.filter((v) => v !== optionValue) : [...selected, optionValue];
554
+ commit(next);
555
+ const idx = opts.findIndex((o) => o.value === optionValue);
556
+ if (idx >= 0) setActiveIndex(idx);
557
+ } else {
558
+ commit([optionValue]);
559
+ if (closeOnSelect) {
560
+ closeListbox(true);
561
+ } else {
562
+ const idx = opts.findIndex((o) => o.value === optionValue);
563
+ if (idx >= 0) setActiveIndex(idx);
564
+ }
565
+ }
566
+ };
567
+ const reset = () => {
568
+ commit([]);
569
+ };
570
+ React2.useEffect(() => {
571
+ if (!open) return;
572
+ const onReflow = () => measure();
573
+ window.addEventListener("scroll", onReflow, true);
574
+ window.addEventListener("resize", onReflow);
575
+ return () => {
576
+ window.removeEventListener("scroll", onReflow, true);
577
+ window.removeEventListener("resize", onReflow);
578
+ };
579
+ }, [open, measure]);
580
+ React2.useEffect(() => {
581
+ if (!open) return;
582
+ const onPointerDown = (event) => {
583
+ const target = event.target;
584
+ const trigger = triggerRef.current;
585
+ const listbox = document.getElementById(listboxId);
586
+ const panel = listbox?.closest("[data-slot=select-listbox-panel]") ?? listbox;
587
+ if (trigger?.contains(target) || panel?.contains(target)) return;
588
+ closeListbox(false);
589
+ };
590
+ document.addEventListener("pointerdown", onPointerDown);
591
+ return () => document.removeEventListener("pointerdown", onPointerDown);
592
+ }, [open, listboxId, closeListbox]);
593
+ const handleKeyDown = (event) => {
594
+ if (disabled) return;
595
+ switch (event.key) {
596
+ case "ArrowDown": {
597
+ event.preventDefault();
598
+ if (!open) {
599
+ openListbox();
600
+ } else {
601
+ setActiveIndex((i) => nextEnabledIndex(opts, i < 0 ? -1 : i, 1));
602
+ }
603
+ return;
604
+ }
605
+ case "ArrowUp": {
606
+ event.preventDefault();
607
+ if (!open) {
608
+ openListbox();
609
+ } else {
610
+ setActiveIndex(
611
+ (i) => i < 0 ? firstEnabledIndex(opts) : nextEnabledIndex(opts, i, -1)
612
+ );
613
+ }
614
+ return;
615
+ }
616
+ case "Home": {
617
+ if (!open) return;
618
+ event.preventDefault();
619
+ setActiveIndex(firstEnabledIndex(opts));
620
+ return;
621
+ }
622
+ case "End": {
623
+ if (!open) return;
624
+ event.preventDefault();
625
+ setActiveIndex(nextEnabledIndex(opts, opts.length, -1));
626
+ return;
627
+ }
628
+ case "Enter":
629
+ case " ": {
630
+ event.preventDefault();
631
+ if (!open) {
632
+ openListbox();
633
+ } else if (activeIndex >= 0 && !opts[activeIndex]?.disabled) {
634
+ handleSelect(opts[activeIndex].value);
635
+ }
636
+ return;
637
+ }
638
+ case "Escape": {
639
+ if (open) {
640
+ event.preventDefault();
641
+ closeListbox(true);
642
+ }
643
+ return;
644
+ }
645
+ case "Tab": {
646
+ if (open) closeListbox(false);
647
+ return;
648
+ }
649
+ default: {
650
+ if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {
651
+ if (!open) openListbox();
652
+ const now = typeof performance !== "undefined" ? performance.now() : Date.now();
653
+ const t = typeahead.current;
654
+ t.buffer = now - t.at > TYPEAHEAD_RESET_MS ? event.key : t.buffer + event.key;
655
+ t.at = now;
656
+ const query = t.buffer.toLowerCase();
657
+ const match = opts.findIndex(
658
+ (o) => !o.disabled && o.label.toLowerCase().startsWith(query)
659
+ );
660
+ if (match >= 0) setActiveIndex(match);
661
+ }
662
+ }
663
+ }
664
+ };
665
+ const handleTriggerClick = () => {
666
+ if (disabled) return;
667
+ if (open) closeListbox(true);
668
+ else openListbox();
669
+ };
670
+ const { text, isPlaceholder } = selectTriggerLabel(
671
+ opts,
672
+ selected,
673
+ placeholder
674
+ );
675
+ const activeId = open && activeIndex >= 0 ? `${listboxId}-opt-${activeIndex}` : void 0;
676
+ return {
677
+ selected,
678
+ open,
679
+ setTriggerRef,
680
+ listboxId,
681
+ activeId,
682
+ triggerProps: {
683
+ onClick: handleTriggerClick,
684
+ onKeyDown: handleKeyDown,
685
+ "aria-haspopup": "listbox",
686
+ "aria-expanded": open,
687
+ "aria-controls": open ? listboxId : void 0,
688
+ "aria-activedescendant": activeId
689
+ },
690
+ listboxProps: {
691
+ id: listboxId,
692
+ options: opts,
693
+ selected,
694
+ multiple,
695
+ activeIndex,
696
+ anchorRect,
697
+ onSelect: handleSelect,
698
+ onActiveChange: setActiveIndex
699
+ },
700
+ closeListbox,
701
+ text,
702
+ isPlaceholder,
703
+ reset
704
+ };
705
+ }
706
+ function Select({
707
+ options,
708
+ value: valueProp,
709
+ defaultValue,
710
+ onValueChange,
711
+ multiple = false,
712
+ variation = "field",
713
+ placeholder = "Select",
714
+ label,
715
+ error = false,
716
+ hint,
717
+ leadingSlot,
718
+ showBottomLine = true,
719
+ disabled,
720
+ id: idProp,
721
+ className,
722
+ wrapperClassName,
723
+ fieldClassName,
724
+ ref,
725
+ ...props
726
+ }) {
727
+ const generatedId = React2.useId();
728
+ const id = idProp ?? generatedId;
729
+ const hintId = hint != null ? `${id}-hint` : void 0;
730
+ const {
731
+ open,
732
+ setTriggerRef,
733
+ triggerProps,
734
+ listboxProps,
735
+ text,
736
+ isPlaceholder
737
+ } = useSelectListbox({
738
+ options,
739
+ value: valueProp,
740
+ defaultValue,
741
+ onValueChange,
742
+ multiple,
743
+ disabled,
744
+ id,
745
+ ref,
746
+ placeholder
747
+ });
748
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
749
+ /* @__PURE__ */ jsx(
750
+ SelectTrigger,
751
+ {
752
+ ref: setTriggerRef,
753
+ id,
754
+ variation,
755
+ error,
756
+ showBottomLine,
757
+ open,
758
+ displayText: text,
759
+ isPlaceholder,
760
+ label,
761
+ hint,
762
+ hintId,
763
+ leadingSlot,
764
+ disabled,
765
+ className,
766
+ wrapperClassName,
767
+ fieldClassName,
768
+ ...triggerProps,
769
+ ...props
770
+ }
771
+ ),
772
+ open && /* @__PURE__ */ jsx(
773
+ SelectListbox,
774
+ {
775
+ ...listboxProps,
776
+ "aria-labelledby": label != null ? id : void 0,
777
+ "aria-label": label == null ? props["aria-label"] : void 0
778
+ }
779
+ )
780
+ ] });
781
+ }
782
+
783
+ export { Select, SelectListbox, SelectTrigger, computeListboxHorizontal, computeListboxPosition, nextEnabledIndex, selectListboxFooterVariants, selectListboxPanelVariants, selectListboxScrollVariants, selectListboxVariants, selectOptionVariants, selectTriggerLabel, useSelectListbox };