@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,873 @@
1
+ 'use strict';
2
+
3
+ var React2 = require('react');
4
+ var clsx = require('clsx');
5
+ var tailwindMerge = require('tailwind-merge');
6
+ var reactDom = require('react-dom');
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/filter/Filter.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
+ function selectTriggerLabel(options, selected, placeholder) {
445
+ if (selected.length === 0) {
446
+ return { text: placeholder, isPlaceholder: true };
447
+ }
448
+ if (selected.length === 1) {
449
+ const match = options.find((o) => o.value === selected[0]);
450
+ return { text: match?.label ?? placeholder, isPlaceholder: match == null };
451
+ }
452
+ return { text: `${selected.length} selected`, isPlaceholder: false };
453
+ }
454
+ var TYPEAHEAD_RESET_MS = 500;
455
+ function nextEnabledIndex(options, from, dir) {
456
+ for (let i = from + dir; i >= 0 && i < options.length; i += dir) {
457
+ if (!options[i].disabled) return i;
458
+ }
459
+ return from;
460
+ }
461
+ function firstEnabledIndex(options) {
462
+ const i = options.findIndex((o) => !o.disabled);
463
+ return i;
464
+ }
465
+ function toArray(value) {
466
+ if (value == null) return [];
467
+ return Array.isArray(value) ? value : [value];
468
+ }
469
+ function useSelectListbox(options) {
470
+ const {
471
+ options: opts,
472
+ value: valueProp,
473
+ defaultValue,
474
+ onValueChange,
475
+ multiple = false,
476
+ disabled = false,
477
+ closeOnSelect = true,
478
+ id,
479
+ ref,
480
+ placeholder = "Select"
481
+ } = options;
482
+ const listboxId = `${id}-listbox`;
483
+ const isControlled = valueProp !== void 0;
484
+ const [uncontrolled, setUncontrolled] = React2__namespace.useState(
485
+ () => toArray(defaultValue)
486
+ );
487
+ const selected = isControlled ? toArray(valueProp) : uncontrolled;
488
+ const [open, setOpen] = React2__namespace.useState(false);
489
+ const [activeIndex, setActiveIndex] = React2__namespace.useState(-1);
490
+ const [anchorRect, setAnchorRect] = React2__namespace.useState(null);
491
+ const triggerRef = React2__namespace.useRef(null);
492
+ const setTriggerRef = React2__namespace.useCallback(
493
+ (node) => {
494
+ triggerRef.current = node;
495
+ if (typeof ref === "function") ref(node);
496
+ else if (ref != null) ref.current = node;
497
+ },
498
+ [ref]
499
+ );
500
+ const typeahead = React2__namespace.useRef({ buffer: "", at: 0 });
501
+ const commit = (next) => {
502
+ if (!isControlled) setUncontrolled(next);
503
+ onValueChange?.(multiple ? next : next[0] ?? "");
504
+ };
505
+ const measure = React2__namespace.useCallback(() => {
506
+ const node = triggerRef.current;
507
+ if (node) setAnchorRect(node.getBoundingClientRect());
508
+ }, []);
509
+ const openListbox = React2__namespace.useCallback(() => {
510
+ measure();
511
+ const selectedIndex = opts.findIndex((o) => selected.includes(o.value));
512
+ setActiveIndex(
513
+ selectedIndex >= 0 ? selectedIndex : firstEnabledIndex(opts)
514
+ );
515
+ setOpen(true);
516
+ }, [measure, opts, selected]);
517
+ const closeListbox = React2__namespace.useCallback((restoreFocus) => {
518
+ setOpen(false);
519
+ setActiveIndex(-1);
520
+ setAnchorRect(null);
521
+ if (restoreFocus) triggerRef.current?.focus();
522
+ }, []);
523
+ const handleSelect = (optionValue) => {
524
+ if (multiple) {
525
+ const next = selected.includes(optionValue) ? selected.filter((v) => v !== optionValue) : [...selected, optionValue];
526
+ commit(next);
527
+ const idx = opts.findIndex((o) => o.value === optionValue);
528
+ if (idx >= 0) setActiveIndex(idx);
529
+ } else {
530
+ commit([optionValue]);
531
+ if (closeOnSelect) {
532
+ closeListbox(true);
533
+ } else {
534
+ const idx = opts.findIndex((o) => o.value === optionValue);
535
+ if (idx >= 0) setActiveIndex(idx);
536
+ }
537
+ }
538
+ };
539
+ const reset = () => {
540
+ commit([]);
541
+ };
542
+ React2__namespace.useEffect(() => {
543
+ if (!open) return;
544
+ const onReflow = () => measure();
545
+ window.addEventListener("scroll", onReflow, true);
546
+ window.addEventListener("resize", onReflow);
547
+ return () => {
548
+ window.removeEventListener("scroll", onReflow, true);
549
+ window.removeEventListener("resize", onReflow);
550
+ };
551
+ }, [open, measure]);
552
+ React2__namespace.useEffect(() => {
553
+ if (!open) return;
554
+ const onPointerDown = (event) => {
555
+ const target = event.target;
556
+ const trigger = triggerRef.current;
557
+ const listbox = document.getElementById(listboxId);
558
+ const panel = listbox?.closest("[data-slot=select-listbox-panel]") ?? listbox;
559
+ if (trigger?.contains(target) || panel?.contains(target)) return;
560
+ closeListbox(false);
561
+ };
562
+ document.addEventListener("pointerdown", onPointerDown);
563
+ return () => document.removeEventListener("pointerdown", onPointerDown);
564
+ }, [open, listboxId, closeListbox]);
565
+ const handleKeyDown = (event) => {
566
+ if (disabled) return;
567
+ switch (event.key) {
568
+ case "ArrowDown": {
569
+ event.preventDefault();
570
+ if (!open) {
571
+ openListbox();
572
+ } else {
573
+ setActiveIndex((i) => nextEnabledIndex(opts, i < 0 ? -1 : i, 1));
574
+ }
575
+ return;
576
+ }
577
+ case "ArrowUp": {
578
+ event.preventDefault();
579
+ if (!open) {
580
+ openListbox();
581
+ } else {
582
+ setActiveIndex(
583
+ (i) => i < 0 ? firstEnabledIndex(opts) : nextEnabledIndex(opts, i, -1)
584
+ );
585
+ }
586
+ return;
587
+ }
588
+ case "Home": {
589
+ if (!open) return;
590
+ event.preventDefault();
591
+ setActiveIndex(firstEnabledIndex(opts));
592
+ return;
593
+ }
594
+ case "End": {
595
+ if (!open) return;
596
+ event.preventDefault();
597
+ setActiveIndex(nextEnabledIndex(opts, opts.length, -1));
598
+ return;
599
+ }
600
+ case "Enter":
601
+ case " ": {
602
+ event.preventDefault();
603
+ if (!open) {
604
+ openListbox();
605
+ } else if (activeIndex >= 0 && !opts[activeIndex]?.disabled) {
606
+ handleSelect(opts[activeIndex].value);
607
+ }
608
+ return;
609
+ }
610
+ case "Escape": {
611
+ if (open) {
612
+ event.preventDefault();
613
+ closeListbox(true);
614
+ }
615
+ return;
616
+ }
617
+ case "Tab": {
618
+ if (open) closeListbox(false);
619
+ return;
620
+ }
621
+ default: {
622
+ if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {
623
+ if (!open) openListbox();
624
+ const now = typeof performance !== "undefined" ? performance.now() : Date.now();
625
+ const t = typeahead.current;
626
+ t.buffer = now - t.at > TYPEAHEAD_RESET_MS ? event.key : t.buffer + event.key;
627
+ t.at = now;
628
+ const query = t.buffer.toLowerCase();
629
+ const match = opts.findIndex(
630
+ (o) => !o.disabled && o.label.toLowerCase().startsWith(query)
631
+ );
632
+ if (match >= 0) setActiveIndex(match);
633
+ }
634
+ }
635
+ }
636
+ };
637
+ const handleTriggerClick = () => {
638
+ if (disabled) return;
639
+ if (open) closeListbox(true);
640
+ else openListbox();
641
+ };
642
+ const { text, isPlaceholder } = selectTriggerLabel(
643
+ opts,
644
+ selected,
645
+ placeholder
646
+ );
647
+ const activeId = open && activeIndex >= 0 ? `${listboxId}-opt-${activeIndex}` : void 0;
648
+ return {
649
+ selected,
650
+ open,
651
+ setTriggerRef,
652
+ listboxId,
653
+ activeId,
654
+ triggerProps: {
655
+ onClick: handleTriggerClick,
656
+ onKeyDown: handleKeyDown,
657
+ "aria-haspopup": "listbox",
658
+ "aria-expanded": open,
659
+ "aria-controls": open ? listboxId : void 0,
660
+ "aria-activedescendant": activeId
661
+ },
662
+ listboxProps: {
663
+ id: listboxId,
664
+ options: opts,
665
+ selected,
666
+ multiple,
667
+ activeIndex,
668
+ anchorRect,
669
+ onSelect: handleSelect,
670
+ onActiveChange: setActiveIndex
671
+ },
672
+ closeListbox,
673
+ text,
674
+ isPlaceholder,
675
+ reset
676
+ };
677
+ }
678
+ var filterTriggerVariants = classVarianceAuthority.cva(
679
+ [
680
+ // Layout — chip row: label + optional badge + caret, 6px gap (spacing/1-5).
681
+ "inline-flex h-10 w-fit shrink-0 items-center gap-1.5",
682
+ "rounded-sm px-3",
683
+ "cursor-pointer text-left align-middle transition-colors",
684
+ // Surface — card/neutral/primary (the `neutral` segment is stripped in the
685
+ // token name transform → `card-primary-*`).
686
+ "bg-card-primary-background border border-card-primary-border",
687
+ // System focus ring (ADR-0010/0014). `outline-solid` restores the style the
688
+ // base `outline-none` zeroed (TW4 gotcha) so the 2px ring actually paints.
689
+ "outline-none",
690
+ "focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus",
691
+ // Decorative svgs (caret) never intercept the click and keep explicit sizes.
692
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0"
693
+ ],
694
+ {
695
+ variants: {
696
+ disabled: {
697
+ // Muted + not-allowed; the ring is suppressed (a disabled control is not
698
+ // focusable, but belt-and-braces if a consumer forces focus).
699
+ true: "cursor-not-allowed focus-visible:outline-none",
700
+ false: ""
701
+ }
702
+ },
703
+ defaultVariants: {
704
+ disabled: false
705
+ }
706
+ }
707
+ );
708
+ var filterCountBadgeVariants = classVarianceAuthority.cva([
709
+ "inline-flex h-5 min-w-5 items-center justify-center px-0.5",
710
+ "rounded-xs bg-brand-primary-background",
711
+ // `body-04` = the Figma badge text style (Archivo Regular 12/16) — NOT
712
+ // a `caption-*` utility (those render Chakra Petch, `font-caption`).
713
+ "body-04 text-brand-primary-foreground"
714
+ ]);
715
+ function FilterTrigger({
716
+ displayText,
717
+ count = 0,
718
+ open = false,
719
+ disabled = false,
720
+ id,
721
+ className,
722
+ ref,
723
+ ...props
724
+ }) {
725
+ const Caret = open ? react.CaretUpIcon : react.CaretDownIcon;
726
+ const caretColor = disabled ? "text-card-primary-foreground-muted" : open ? "text-card-primary-foreground" : "text-card-primary-foreground-muted group-hover:text-card-primary-foreground";
727
+ return /* @__PURE__ */ jsxRuntime.jsxs(
728
+ "button",
729
+ {
730
+ type: "button",
731
+ id,
732
+ ref,
733
+ role: "combobox",
734
+ "data-slot": "filter-trigger",
735
+ "data-open": open || void 0,
736
+ disabled,
737
+ className: cn("group", filterTriggerVariants({ disabled }), className),
738
+ ...props,
739
+ children: [
740
+ /* @__PURE__ */ jsxRuntime.jsx(
741
+ "span",
742
+ {
743
+ "data-slot": "filter-label",
744
+ className: cn(
745
+ "body-03 truncate",
746
+ disabled ? "text-card-primary-foreground-muted" : "text-card-primary-foreground"
747
+ ),
748
+ children: displayText
749
+ }
750
+ ),
751
+ count > 0 && /* @__PURE__ */ jsxRuntime.jsx(
752
+ "span",
753
+ {
754
+ "data-slot": "filter-count",
755
+ "aria-hidden": "true",
756
+ className: filterCountBadgeVariants(),
757
+ children: count
758
+ }
759
+ ),
760
+ /* @__PURE__ */ jsxRuntime.jsx(
761
+ "span",
762
+ {
763
+ "data-slot": "filter-caret",
764
+ "aria-hidden": "true",
765
+ className: "flex items-center",
766
+ children: /* @__PURE__ */ jsxRuntime.jsx(
767
+ Caret,
768
+ {
769
+ weight: "fill",
770
+ className: cn("size-3 transition-transform", caretColor)
771
+ }
772
+ )
773
+ }
774
+ )
775
+ ]
776
+ }
777
+ );
778
+ }
779
+ function Filter({
780
+ options,
781
+ value: valueProp,
782
+ defaultValue,
783
+ onValueChange,
784
+ multiple = false,
785
+ label,
786
+ disabled,
787
+ id: idProp,
788
+ className,
789
+ ref,
790
+ ...props
791
+ }) {
792
+ const generatedId = React2__namespace.useId();
793
+ const id = idProp ?? generatedId;
794
+ const {
795
+ selected,
796
+ open,
797
+ setTriggerRef,
798
+ triggerProps,
799
+ listboxProps,
800
+ reset,
801
+ text
802
+ } = useSelectListbox({
803
+ options,
804
+ value: valueProp,
805
+ defaultValue,
806
+ onValueChange,
807
+ multiple,
808
+ disabled,
809
+ // Filter's dropdown persists after a pick (single stays open like multi).
810
+ closeOnSelect: false,
811
+ id,
812
+ ref,
813
+ // The single-select empty state shows the filter `label`, not a generic
814
+ // "Select" placeholder — so `selectTriggerLabel`'s placeholder IS `label`.
815
+ // (In multi the trigger text is always `label` regardless — see below.)
816
+ placeholder: label
817
+ });
818
+ const displayText = multiple ? label : text;
819
+ const count = multiple ? selected.length : 0;
820
+ const canReset = selected.length > 0;
821
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
822
+ /* @__PURE__ */ jsxRuntime.jsx(
823
+ FilterTrigger,
824
+ {
825
+ ref: setTriggerRef,
826
+ id,
827
+ displayText,
828
+ count,
829
+ open,
830
+ disabled,
831
+ "aria-label": label,
832
+ className,
833
+ ...triggerProps,
834
+ ...props
835
+ }
836
+ ),
837
+ open && /* @__PURE__ */ jsxRuntime.jsx(
838
+ SelectListbox,
839
+ {
840
+ ...listboxProps,
841
+ widthMode: "auto",
842
+ "aria-label": label,
843
+ footer: /* @__PURE__ */ jsxRuntime.jsx(
844
+ "button",
845
+ {
846
+ type: "button",
847
+ "data-slot": "filter-reset",
848
+ disabled: !canReset,
849
+ onClick: () => {
850
+ if (canReset) reset();
851
+ },
852
+ className: cn(
853
+ // A ghost button styled as `button/neutral/secondary`
854
+ // (`button-secondary-*`): foreground when enabled, muted when
855
+ // disabled. System focus ring (ADR-0010/0014) — `outline-solid`
856
+ // beside `outline-2` so it actually paints (TW4 gotcha).
857
+ "button-01 inline-flex h-8 items-center rounded-xs bg-transparent",
858
+ "outline-none transition-colors",
859
+ "focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus",
860
+ canReset ? "cursor-pointer text-button-secondary-foreground hover:text-button-secondary-foreground-accent" : "cursor-not-allowed text-button-secondary-foreground-muted"
861
+ ),
862
+ children: "Reset"
863
+ }
864
+ )
865
+ }
866
+ )
867
+ ] });
868
+ }
869
+
870
+ exports.Filter = Filter;
871
+ exports.FilterTrigger = FilterTrigger;
872
+ exports.filterCountBadgeVariants = filterCountBadgeVariants;
873
+ exports.filterTriggerVariants = filterTriggerVariants;