@vipengele/react-ui 0.0.0 → 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 (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +463 -3
  3. package/dist/Avatar/Avatar.d.ts +21 -0
  4. package/dist/Avatar/Avatar.d.ts.map +1 -0
  5. package/dist/Avatar/Avatar.stylesheet.d.ts +17 -0
  6. package/dist/Avatar/Avatar.stylesheet.d.ts.map +1 -0
  7. package/dist/Button/Button.d.ts +39 -0
  8. package/dist/Button/Button.d.ts.map +1 -0
  9. package/dist/Button/Button.stylesheet.d.ts +17 -0
  10. package/dist/Button/Button.stylesheet.d.ts.map +1 -0
  11. package/dist/ButtonGroup/ButtonGroup.d.ts +16 -0
  12. package/dist/ButtonGroup/ButtonGroup.d.ts.map +1 -0
  13. package/dist/ButtonGroup/ButtonGroup.stylesheet.d.ts +12 -0
  14. package/dist/ButtonGroup/ButtonGroup.stylesheet.d.ts.map +1 -0
  15. package/dist/Card/Card.d.ts +36 -0
  16. package/dist/Card/Card.d.ts.map +1 -0
  17. package/dist/Card/Card.stylesheet.d.ts +17 -0
  18. package/dist/Card/Card.stylesheet.d.ts.map +1 -0
  19. package/dist/Dropdown/Dropdown.d.ts +205 -0
  20. package/dist/Dropdown/Dropdown.d.ts.map +1 -0
  21. package/dist/Dropdown/Dropdown.stylesheet.d.ts +24 -0
  22. package/dist/Dropdown/Dropdown.stylesheet.d.ts.map +1 -0
  23. package/dist/FieldSet/FieldSet.d.ts +22 -0
  24. package/dist/FieldSet/FieldSet.d.ts.map +1 -0
  25. package/dist/FieldSet/FieldSet.stylesheet.d.ts +34 -0
  26. package/dist/FieldSet/FieldSet.stylesheet.d.ts.map +1 -0
  27. package/dist/FieldShell/FieldShell.d.ts +42 -0
  28. package/dist/FieldShell/FieldShell.d.ts.map +1 -0
  29. package/dist/FieldShell/FieldShell.stylesheet.d.ts +32 -0
  30. package/dist/FieldShell/FieldShell.stylesheet.d.ts.map +1 -0
  31. package/dist/FormField/FormField.d.ts +45 -0
  32. package/dist/FormField/FormField.d.ts.map +1 -0
  33. package/dist/FormField/FormField.stylesheet.d.ts +24 -0
  34. package/dist/FormField/FormField.stylesheet.d.ts.map +1 -0
  35. package/dist/PasswordInput/PasswordInput.d.ts +15 -0
  36. package/dist/PasswordInput/PasswordInput.d.ts.map +1 -0
  37. package/dist/PasswordInput/PasswordInput.stylesheet.d.ts +24 -0
  38. package/dist/PasswordInput/PasswordInput.stylesheet.d.ts.map +1 -0
  39. package/dist/Popover/Popover.d.ts +47 -0
  40. package/dist/Popover/Popover.d.ts.map +1 -0
  41. package/dist/Popover/Popover.stylesheet.d.ts +21 -0
  42. package/dist/Popover/Popover.stylesheet.d.ts.map +1 -0
  43. package/dist/Progress/Progress.d.ts +18 -0
  44. package/dist/Progress/Progress.d.ts.map +1 -0
  45. package/dist/Progress/Progress.stylesheet.d.ts +18 -0
  46. package/dist/Progress/Progress.stylesheet.d.ts.map +1 -0
  47. package/dist/RadioButton/RadioButton.d.ts +18 -0
  48. package/dist/RadioButton/RadioButton.d.ts.map +1 -0
  49. package/dist/RadioButton/RadioButton.stylesheet.d.ts +15 -0
  50. package/dist/RadioButton/RadioButton.stylesheet.d.ts.map +1 -0
  51. package/dist/RadioGroup/RadioGroup.d.ts +40 -0
  52. package/dist/RadioGroup/RadioGroup.d.ts.map +1 -0
  53. package/dist/RadioGroup/RadioGroup.stylesheet.d.ts +11 -0
  54. package/dist/RadioGroup/RadioGroup.stylesheet.d.ts.map +1 -0
  55. package/dist/Skeleton/Skeleton.d.ts +18 -0
  56. package/dist/Skeleton/Skeleton.d.ts.map +1 -0
  57. package/dist/Skeleton/Skeleton.stylesheet.d.ts +25 -0
  58. package/dist/Skeleton/Skeleton.stylesheet.d.ts.map +1 -0
  59. package/dist/Slider/Slider.d.ts +12 -0
  60. package/dist/Slider/Slider.d.ts.map +1 -0
  61. package/dist/Slider/Slider.stylesheet.d.ts +18 -0
  62. package/dist/Slider/Slider.stylesheet.d.ts.map +1 -0
  63. package/dist/Spinner/Spinner.d.ts +19 -0
  64. package/dist/Spinner/Spinner.d.ts.map +1 -0
  65. package/dist/Spinner/Spinner.stylesheet.d.ts +14 -0
  66. package/dist/Spinner/Spinner.stylesheet.d.ts.map +1 -0
  67. package/dist/Tabs/Tabs.d.ts +46 -0
  68. package/dist/Tabs/Tabs.d.ts.map +1 -0
  69. package/dist/Tabs/Tabs.stylesheet.d.ts +15 -0
  70. package/dist/Tabs/Tabs.stylesheet.d.ts.map +1 -0
  71. package/dist/TextField/TextField.d.ts +24 -0
  72. package/dist/TextField/TextField.d.ts.map +1 -0
  73. package/dist/TextField/TextField.stylesheet.d.ts +29 -0
  74. package/dist/TextField/TextField.stylesheet.d.ts.map +1 -0
  75. package/dist/Toggle/Toggle.d.ts +12 -0
  76. package/dist/Toggle/Toggle.d.ts.map +1 -0
  77. package/dist/Toggle/Toggle.stylesheet.d.ts +16 -0
  78. package/dist/Toggle/Toggle.stylesheet.d.ts.map +1 -0
  79. package/dist/Tooltip/Tooltip.d.ts +34 -0
  80. package/dist/Tooltip/Tooltip.d.ts.map +1 -0
  81. package/dist/Tooltip/Tooltip.stylesheet.d.ts +22 -0
  82. package/dist/Tooltip/Tooltip.stylesheet.d.ts.map +1 -0
  83. package/dist/Typography/Typography.d.ts +28 -0
  84. package/dist/Typography/Typography.d.ts.map +1 -0
  85. package/dist/Typography/Typography.stylesheet.d.ts +16 -0
  86. package/dist/Typography/Typography.stylesheet.d.ts.map +1 -0
  87. package/dist/index.d.ts +22 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +2947 -0
  90. package/dist/index.js.map +1 -0
  91. package/dist/internal/listbox.stylesheet.d.ts +24 -0
  92. package/dist/internal/listbox.stylesheet.d.ts.map +1 -0
  93. package/dist/internal/useListboxKeyboard.d.ts +90 -0
  94. package/dist/internal/useListboxKeyboard.d.ts.map +1 -0
  95. package/package.json +50 -5
package/dist/index.js ADDED
@@ -0,0 +1,2947 @@
1
+ import { User, X, ChevronDown, Check, Search } from '@vipengele/react-icons';
2
+ import { createContext, useState, isValidElement, Fragment as Fragment$1, cloneElement, useId, useMemo, useRef, useEffect, useLayoutEffect, useContext, Children, useCallback } from 'react';
3
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
4
+ import { useFloating, autoUpdate, offset, flip, shift, useHover, useFocus, useDismiss, useRole, useInteractions, useClick, FloatingFocusManager, size, useListNavigation, useTypeahead } from '@floating-ui/react';
5
+ import { createPortal, flushSync } from 'react-dom';
6
+
7
+ // src/Avatar/Avatar.tsx
8
+
9
+ // src/Avatar/Avatar.stylesheet.ts
10
+ var avatarStylesheet = `
11
+ .vpg-avatar {
12
+ display: inline-flex;
13
+ align-items: center;
14
+ justify-content: center;
15
+ flex: none;
16
+ box-sizing: border-box;
17
+ overflow: hidden;
18
+ background-color: var(--vpg-surface-raised);
19
+ color: var(--vpg-ink-muted);
20
+ font-family: inherit;
21
+ font-weight: 500;
22
+ line-height: 1;
23
+ /* The fallback content is centred text or a glyph; without this, a long initials string
24
+ would be selectable and could wrap out of the frame. */
25
+ user-select: none;
26
+ }
27
+
28
+ .vpg-avatar-circle {
29
+ border-radius: 50%;
30
+ }
31
+
32
+ .vpg-avatar-square {
33
+ border-radius: var(--vpg-radius);
34
+ }
35
+
36
+ .vpg-avatar-sm {
37
+ width: var(--vpg-size-xs);
38
+ height: var(--vpg-size-xs);
39
+ font-size: var(--vpg-font-size-xs);
40
+ }
41
+
42
+ .vpg-avatar-md {
43
+ width: var(--vpg-size-md);
44
+ height: var(--vpg-size-md);
45
+ font-size: var(--vpg-font-size-xs);
46
+ }
47
+
48
+ .vpg-avatar-lg {
49
+ width: var(--vpg-size-xl);
50
+ height: var(--vpg-size-xl);
51
+ font-size: var(--vpg-font-size-sm);
52
+ }
53
+
54
+ .vpg-avatar-xl {
55
+ width: var(--vpg-size-2xl);
56
+ height: var(--vpg-size-2xl);
57
+ font-size: var(--vpg-font-size-lg);
58
+ }
59
+
60
+ .vpg-avatar-image {
61
+ width: 100%;
62
+ height: 100%;
63
+ /* \`cover\` rather than \`contain\`: a non-square photo is cropped to the frame instead of
64
+ letterboxed against the surface colour. */
65
+ object-fit: cover;
66
+ display: block;
67
+ }
68
+
69
+ .vpg-avatar-icon {
70
+ /* Proportional to the frame, so one rule sizes the glyph across the whole size scale. */
71
+ width: 60%;
72
+ height: 60%;
73
+ }
74
+ `;
75
+ function initialsFrom(name) {
76
+ const words = name.split(/\s+/).filter((word) => word.length > 0);
77
+ const picked = words.length > 1 ? [...words.slice(0, 1), ...words.slice(-1)] : words.slice(0, 1);
78
+ return picked.map((word) => word.slice(0, 1)).join("").toUpperCase();
79
+ }
80
+ function Avatar({ src, name, alt, size: size2 = "md", shape = "circle", className, ...rest }) {
81
+ const [failedSrc, setFailedSrc] = useState(void 0);
82
+ const classes = ["vpg-avatar", `vpg-avatar-${size2}`, `vpg-avatar-${shape}`, className].filter(Boolean).join(" ");
83
+ const label = alt ?? name;
84
+ const showImage = src !== void 0 && src !== failedSrc;
85
+ const initials = name === void 0 ? "" : initialsFrom(name);
86
+ const labelling = showImage || label === void 0 ? void 0 : { role: "img", "aria-label": label };
87
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
88
+ /* @__PURE__ */ jsx("style", { href: "vpg-avatar", precedence: "vpg-avatar", children: avatarStylesheet }),
89
+ /* @__PURE__ */ jsx("span", { ...rest, ...labelling, className: classes, children: showImage ? /* @__PURE__ */ jsx("img", { src, alt: label ?? "", className: "vpg-avatar-image", onError: () => setFailedSrc(src) }) : initials ? (
90
+ // The root already announces `label`; repeating the initials would read them out as a
91
+ // second, meaningless word.
92
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: initials })
93
+ ) : /* @__PURE__ */ jsx(User, { className: "vpg-avatar-icon", "aria-hidden": "true" }) })
94
+ ] });
95
+ }
96
+
97
+ // src/Spinner/Spinner.stylesheet.ts
98
+ var spinnerStylesheet = `
99
+ @keyframes vpg-spinner-rotate {
100
+ to { transform: rotate(360deg); }
101
+ }
102
+
103
+ .vpg-spinner {
104
+ display: inline-block;
105
+ flex: none;
106
+ vertical-align: middle;
107
+ /* The track's stroke is \`currentColor\`, so this one declaration colours the whole spinner
108
+ and a caller's \`color\` prop overrides it inline without touching any theme property. */
109
+ color: var(--vpg-accent);
110
+ animation: vpg-spinner-rotate 0.7s linear infinite;
111
+ }
112
+
113
+ .vpg-spinner circle {
114
+ fill: none;
115
+ stroke: currentColor;
116
+ stroke-width: 3;
117
+ stroke-linecap: round;
118
+ /* Circumference of r=10 is ~62.8; a 44/62.8 dash leaves the gap that makes the rotation
119
+ readable. Without the gap the ring is closed and spinning it looks static. */
120
+ stroke-dasharray: 44 62.8;
121
+ }
122
+
123
+ .vpg-spinner-sm {
124
+ width: var(--vpg-icon-md);
125
+ height: var(--vpg-icon-md);
126
+ }
127
+
128
+ .vpg-spinner-md {
129
+ width: var(--vpg-icon-lg);
130
+ height: var(--vpg-icon-lg);
131
+ }
132
+
133
+ .vpg-spinner-lg {
134
+ width: var(--vpg-icon-xl);
135
+ height: var(--vpg-icon-xl);
136
+ }
137
+
138
+ @media (prefers-reduced-motion: reduce) {
139
+ /* Slowed, not stopped: a spinner that doesn't move reports nothing about the wait it exists
140
+ to report. */
141
+ .vpg-spinner {
142
+ animation-duration: 2.4s;
143
+ }
144
+ }
145
+ `;
146
+ var SIZE_CLASS = {
147
+ sm: "vpg-spinner-sm",
148
+ md: "vpg-spinner-md",
149
+ lg: "vpg-spinner-lg"
150
+ };
151
+ function Spinner({ size: size2 = "md", color, label = "Loading", className }) {
152
+ const classes = ["vpg-spinner", SIZE_CLASS[size2], className].filter(Boolean).join(" ");
153
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
154
+ /* @__PURE__ */ jsx("style", { href: "vpg-spinner", precedence: "vpg-spinner", children: spinnerStylesheet }),
155
+ /* @__PURE__ */ jsx("svg", { className: classes, viewBox: "0 0 24 24", role: "status", "aria-label": label, style: color === void 0 ? void 0 : { color }, children: /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }) })
156
+ ] });
157
+ }
158
+
159
+ // src/Button/Button.stylesheet.ts
160
+ var buttonStylesheet = `
161
+ .vpg-button {
162
+ display: inline-flex;
163
+ align-items: center;
164
+ justify-content: center;
165
+ gap: var(--vpg-space-2);
166
+ box-sizing: border-box;
167
+ border: 1px solid transparent;
168
+ border-radius: var(--vpg-radius);
169
+ font-family: inherit;
170
+ font-weight: 500;
171
+ line-height: 1;
172
+ white-space: nowrap;
173
+ text-decoration: none;
174
+ cursor: pointer;
175
+ /* Colour and background come from the variant rules below; every variant animates the same
176
+ two properties, so the transition belongs here rather than four times over. */
177
+ transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
178
+ }
179
+
180
+ .vpg-button:focus-visible {
181
+ /* Offset rather than inset so the ring stays legible against a same-coloured surface. */
182
+ outline: var(--vpg-focus-ring-width) solid var(--vpg-accent-ring);
183
+ outline-offset: var(--vpg-focus-ring-offset);
184
+ }
185
+
186
+ .vpg-button:disabled {
187
+ /* Pointer events stay on: a disabled button that swallows them gives no cursor feedback and
188
+ no tooltip target. \`cursor\` is what communicates the state. */
189
+ cursor: not-allowed;
190
+ opacity: 0.55;
191
+ }
192
+
193
+ .vpg-button-primary {
194
+ background-color: var(--vpg-accent);
195
+ color: var(--vpg-accent-contrast);
196
+ }
197
+
198
+ .vpg-button-primary:hover:not(:disabled) {
199
+ background-color: var(--vpg-accent-hover);
200
+ }
201
+
202
+ .vpg-button-primary:active:not(:disabled) {
203
+ background-color: var(--vpg-accent-press);
204
+ }
205
+
206
+ .vpg-button-secondary {
207
+ background-color: var(--vpg-surface-raised);
208
+ border-color: var(--vpg-border);
209
+ color: var(--vpg-ink);
210
+ }
211
+
212
+ .vpg-button-secondary:hover:not(:disabled) {
213
+ background-color: var(--vpg-surface-hover);
214
+ border-color: var(--vpg-border-strong);
215
+ }
216
+
217
+ .vpg-button-secondary:active:not(:disabled) {
218
+ background-color: var(--vpg-surface-press);
219
+ }
220
+
221
+ .vpg-button-ghost {
222
+ background-color: transparent;
223
+ color: var(--vpg-ink);
224
+ }
225
+
226
+ .vpg-button-ghost:hover:not(:disabled) {
227
+ background-color: var(--vpg-accent-wash);
228
+ }
229
+
230
+ .vpg-button-ghost:active:not(:disabled) {
231
+ background-color: var(--vpg-surface-press);
232
+ }
233
+
234
+ .vpg-button-danger {
235
+ background-color: var(--vpg-danger);
236
+ color: var(--vpg-danger-contrast);
237
+ }
238
+
239
+ .vpg-button-danger:hover:not(:disabled) {
240
+ background-color: var(--vpg-danger-hover);
241
+ }
242
+
243
+ .vpg-button-danger:active:not(:disabled) {
244
+ background-color: var(--vpg-danger-press);
245
+ }
246
+
247
+ .vpg-button-sm {
248
+ min-height: var(--vpg-size-sm);
249
+ padding: 0 var(--vpg-space-3);
250
+ font-size: var(--vpg-font-size-xs);
251
+ }
252
+
253
+ .vpg-button-md {
254
+ min-height: var(--vpg-size-md);
255
+ padding: 0 var(--vpg-space-4);
256
+ font-size: var(--vpg-font-size-sm);
257
+ }
258
+
259
+ .vpg-button-lg {
260
+ min-height: var(--vpg-size-xl);
261
+ padding: 0 var(--vpg-space-5);
262
+ font-size: var(--vpg-font-size-md);
263
+ }
264
+
265
+ /* Square: the horizontal padding is dropped and the min-height doubles as a width floor, so the
266
+ button stays square at every size without a second per-size scale. */
267
+ .vpg-button-icon-only {
268
+ padding: 0;
269
+ aspect-ratio: 1;
270
+ }
271
+
272
+ .vpg-button-icon-only.vpg-button-sm {
273
+ min-width: var(--vpg-size-sm);
274
+ }
275
+
276
+ .vpg-button-icon-only.vpg-button-md {
277
+ min-width: var(--vpg-size-md);
278
+ }
279
+
280
+ .vpg-button-icon-only.vpg-button-lg {
281
+ min-width: var(--vpg-size-xl);
282
+ }
283
+
284
+ .vpg-button-icon {
285
+ flex: none;
286
+ width: 1em;
287
+ height: 1em;
288
+ }
289
+
290
+ /* Keeps the button's accessible name intact while \`loading\` swaps its visible content for a
291
+ spinner: the label stays in the accessibility tree, but takes up no visual space. */
292
+ .vpg-button-visually-hidden {
293
+ position: absolute;
294
+ width: 1px;
295
+ height: 1px;
296
+ padding: 0;
297
+ margin: -1px;
298
+ overflow: hidden;
299
+ clip: rect(0, 0, 0, 0);
300
+ white-space: nowrap;
301
+ border: 0;
302
+ }
303
+ `;
304
+ function Button({
305
+ variant = "primary",
306
+ size: size2 = "md",
307
+ loading = false,
308
+ iconOnly = false,
309
+ leadingIcon: LeadingIcon,
310
+ trailingIcon: TrailingIcon,
311
+ className,
312
+ disabled = false,
313
+ children,
314
+ ...rest
315
+ }) {
316
+ const classes = ["vpg-button", `vpg-button-${variant}`, `vpg-button-${size2}`, iconOnly ? "vpg-button-icon-only" : void 0, className].filter(Boolean).join(" ");
317
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
318
+ /* @__PURE__ */ jsx("style", { href: "vpg-button", precedence: "vpg-button", children: buttonStylesheet }),
319
+ /* @__PURE__ */ jsx("button", { type: "button", ...rest, className: classes, disabled: disabled || loading, "aria-busy": loading || void 0, children: loading ? /* @__PURE__ */ jsxs(Fragment, { children: [
320
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx(Spinner, { size: size2, color: "currentColor" }) }),
321
+ children ? /* @__PURE__ */ jsx("span", { className: "vpg-button-visually-hidden", children }) : null
322
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
323
+ LeadingIcon ? /* @__PURE__ */ jsx(LeadingIcon, { className: "vpg-button-icon" }) : null,
324
+ children,
325
+ TrailingIcon ? /* @__PURE__ */ jsx(TrailingIcon, { className: "vpg-button-icon" }) : null
326
+ ] }) })
327
+ ] });
328
+ }
329
+
330
+ // src/ButtonGroup/ButtonGroup.stylesheet.ts
331
+ var buttonGroupStylesheet = `
332
+ .vpg-button-group {
333
+ display: inline-flex;
334
+ }
335
+
336
+ .vpg-button-group-horizontal {
337
+ flex-direction: row;
338
+ }
339
+
340
+ .vpg-button-group-vertical {
341
+ flex-direction: column;
342
+ }
343
+
344
+ /* Adjacent buttons overlap their shared border by one pixel rather than doubling it, and only
345
+ the outer corners of the group stay rounded. */
346
+ .vpg-button-group-horizontal > .vpg-button {
347
+ border-radius: 0;
348
+ }
349
+
350
+ .vpg-button-group-horizontal > .vpg-button:first-child {
351
+ border-top-left-radius: var(--vpg-radius);
352
+ border-bottom-left-radius: var(--vpg-radius);
353
+ }
354
+
355
+ .vpg-button-group-horizontal > .vpg-button:last-child {
356
+ border-top-right-radius: var(--vpg-radius);
357
+ border-bottom-right-radius: var(--vpg-radius);
358
+ }
359
+
360
+ .vpg-button-group-horizontal > .vpg-button + .vpg-button {
361
+ margin-left: -1px;
362
+ }
363
+
364
+ .vpg-button-group-vertical > .vpg-button {
365
+ border-radius: 0;
366
+ }
367
+
368
+ .vpg-button-group-vertical > .vpg-button:first-child {
369
+ border-top-left-radius: var(--vpg-radius);
370
+ border-top-right-radius: var(--vpg-radius);
371
+ }
372
+
373
+ .vpg-button-group-vertical > .vpg-button:last-child {
374
+ border-bottom-left-radius: var(--vpg-radius);
375
+ border-bottom-right-radius: var(--vpg-radius);
376
+ }
377
+
378
+ .vpg-button-group-vertical > .vpg-button + .vpg-button {
379
+ margin-top: -1px;
380
+ }
381
+
382
+ /* A focused or hovered button's border should read as whole rather than being clipped under
383
+ its neighbour's overlap. */
384
+ .vpg-button-group > .vpg-button:focus-visible,
385
+ .vpg-button-group > .vpg-button:hover {
386
+ position: relative;
387
+ z-index: 1;
388
+ }
389
+ `;
390
+ function ButtonGroup({ orientation = "horizontal", className, children, ...rest }) {
391
+ const classes = ["vpg-button-group", `vpg-button-group-${orientation}`, className].filter(Boolean).join(" ");
392
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
393
+ /* @__PURE__ */ jsx("style", { href: "vpg-button-group", precedence: "vpg-button-group", children: buttonGroupStylesheet }),
394
+ /* @__PURE__ */ jsx("div", { ...rest, role: "group", className: classes, children })
395
+ ] });
396
+ }
397
+
398
+ // src/Card/Card.stylesheet.ts
399
+ var cardStylesheet = `
400
+ .vpg-card {
401
+ display: flex;
402
+ flex-direction: column;
403
+ box-sizing: border-box;
404
+ background-color: var(--vpg-surface-raised);
405
+ border: 1px solid var(--vpg-border);
406
+ border-radius: var(--vpg-radius);
407
+ color: var(--vpg-ink);
408
+ }
409
+
410
+ .vpg-card-interactive {
411
+ cursor: pointer;
412
+ text-align: left;
413
+ }
414
+
415
+ .vpg-card-interactive:hover {
416
+ background-color: var(--vpg-surface-hover);
417
+ }
418
+
419
+ .vpg-card-interactive:active {
420
+ background-color: var(--vpg-surface-press);
421
+ }
422
+
423
+ .vpg-card-interactive:focus-visible {
424
+ /* Offset rather than inset so the ring stays legible against a same-coloured surface. */
425
+ outline: var(--vpg-focus-ring-width) solid var(--vpg-accent-ring);
426
+ outline-offset: var(--vpg-focus-ring-offset);
427
+ }
428
+
429
+ .vpg-card-header {
430
+ order: 1;
431
+ padding: 1rem 1rem 0;
432
+ }
433
+
434
+ .vpg-card-content {
435
+ order: 2;
436
+ padding: 1rem;
437
+ }
438
+
439
+ .vpg-card-footer {
440
+ order: 3;
441
+ padding: 0 1rem 1rem;
442
+ display: flex;
443
+ align-items: center;
444
+ gap: 0.5rem;
445
+ }
446
+ `;
447
+ function CardHeader({ className, children, ...rest }) {
448
+ const classes = ["vpg-card-header", className].filter(Boolean).join(" ");
449
+ return /* @__PURE__ */ jsx("div", { ...rest, className: classes, children });
450
+ }
451
+ function CardContent({ className, children, ...rest }) {
452
+ const classes = ["vpg-card-content", className].filter(Boolean).join(" ");
453
+ return /* @__PURE__ */ jsx("div", { ...rest, className: classes, children });
454
+ }
455
+ function CardFooter({ className, children, ...rest }) {
456
+ const classes = ["vpg-card-footer", className].filter(Boolean).join(" ");
457
+ return /* @__PURE__ */ jsx("div", { ...rest, className: classes, children });
458
+ }
459
+ function validateChildren(children) {
460
+ let headerCount = 0;
461
+ let contentCount = 0;
462
+ let footerCount = 0;
463
+ Children.forEach(children, (child) => {
464
+ if (child === null || child === void 0 || typeof child === "boolean") {
465
+ return;
466
+ }
467
+ if (!isValidElement(child)) {
468
+ throw new Error("Card only accepts Card.Header, Card.Content, and Card.Footer as children.");
469
+ }
470
+ if (child.type === CardHeader) {
471
+ headerCount += 1;
472
+ if (headerCount > 1) {
473
+ throw new Error("Card accepts at most one Card.Header child.");
474
+ }
475
+ } else if (child.type === CardContent) {
476
+ contentCount += 1;
477
+ if (contentCount > 1) {
478
+ throw new Error("Card accepts at most one Card.Content child.");
479
+ }
480
+ } else if (child.type === CardFooter) {
481
+ footerCount += 1;
482
+ if (footerCount > 1) {
483
+ throw new Error("Card accepts at most one Card.Footer child.");
484
+ }
485
+ } else {
486
+ throw new Error("Card only accepts Card.Header, Card.Content, and Card.Footer as children.");
487
+ }
488
+ });
489
+ if (contentCount === 0) {
490
+ throw new Error("Card requires a Card.Content child.");
491
+ }
492
+ }
493
+ var INTERACTIVE_DESCENDANT_SELECTOR = "button, a[href], input, select, textarea, [role='button']";
494
+ function isInteractiveDescendant(target, root) {
495
+ const closest = target.closest(INTERACTIVE_DESCENDANT_SELECTOR);
496
+ return closest !== null && closest !== root;
497
+ }
498
+ function CardImpl({ className, children, onClick, onKeyDown, ...rest }) {
499
+ validateChildren(children);
500
+ const interactive = onClick !== void 0;
501
+ const classes = ["vpg-card", interactive ? "vpg-card-interactive" : "", className].filter(Boolean).join(" ");
502
+ function handleClick(event) {
503
+ if (isInteractiveDescendant(event.target, event.currentTarget)) {
504
+ return;
505
+ }
506
+ onClick?.(event);
507
+ }
508
+ function handleKeyDown(event) {
509
+ onKeyDown?.(event);
510
+ if (event.key !== "Enter" && event.key !== " ") {
511
+ return;
512
+ }
513
+ if (isInteractiveDescendant(event.target, event.currentTarget)) {
514
+ return;
515
+ }
516
+ event.preventDefault();
517
+ event.currentTarget.click();
518
+ }
519
+ const interactiveProps = interactive ? { role: "button", tabIndex: 0, onClick: handleClick, onKeyDown: handleKeyDown } : { onKeyDown };
520
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
521
+ /* @__PURE__ */ jsx("style", { href: "vpg-card", precedence: "vpg-card", children: cardStylesheet }),
522
+ /* @__PURE__ */ jsx("div", { ...rest, ...interactiveProps, className: classes, children })
523
+ ] });
524
+ }
525
+ var Card = /* @__PURE__ */ Object.assign(CardImpl, {
526
+ Header: CardHeader,
527
+ Content: CardContent,
528
+ Footer: CardFooter
529
+ });
530
+
531
+ // src/FieldShell/FieldShell.stylesheet.ts
532
+ var fieldShellStylesheet = `
533
+ .vpg-field-shell {
534
+ display: flex;
535
+ align-items: center;
536
+ gap: var(--vpg-space-2);
537
+ box-sizing: border-box;
538
+ width: 100%;
539
+ min-height: var(--vpg-size-md);
540
+ padding: 0 var(--vpg-space-3);
541
+ background-color: var(--vpg-surface);
542
+ border: 1px solid var(--vpg-border-strong);
543
+ border-radius: var(--vpg-radius-sm);
544
+ color: var(--vpg-ink);
545
+ transition: border-color var(--vpg-duration-fast) var(--vpg-ease-standard),
546
+ box-shadow var(--vpg-duration-fast) var(--vpg-ease-standard),
547
+ opacity var(--vpg-duration-fast) var(--vpg-ease-standard),
548
+ background-color var(--vpg-duration-fast) var(--vpg-ease-standard);
549
+ }
550
+
551
+ /* The centre is whatever the caller passes as children \u2014 one control, or a pair of siblings such
552
+ as a chip row beside a trigger. \`min-width: 0\` is what stops a flex item propagating its
553
+ content's intrinsic width, so a wide centre shrinks inside the field rather than growing the
554
+ field past what contains it. */
555
+ .vpg-field-shell > *:not(.vpg-field-shell-leading, .vpg-field-shell-trailing) {
556
+ flex: 0 1 auto;
557
+ min-width: 0;
558
+ }
559
+
560
+ /* The centre's free space goes to its last element, and only to that one. A centre of one control
561
+ is that element, so a lone \`<input>\` fills the field. A centre of several \u2014 a chip row, then a
562
+ trigger \u2014 sizes every earlier element to its content and leaves the remainder to the trigger,
563
+ where the caret goes; splitting the space evenly instead would stretch the chip row to half the
564
+ field whatever it holds.
565
+
566
+ \`of\` counts among the centre elements only, so the rule finds the last one whether or not a
567
+ trailing slot follows it. */
568
+ .vpg-field-shell > *:nth-last-child(1 of :not(.vpg-field-shell-leading, .vpg-field-shell-trailing)) {
569
+ flex: 1;
570
+ }
571
+
572
+ .vpg-field-shell-leading,
573
+ .vpg-field-shell-trailing {
574
+ display: flex;
575
+ align-items: center;
576
+ flex: none;
577
+ color: var(--vpg-ink-muted);
578
+ }
579
+
580
+ .vpg-field-shell:has(> :focus-visible) {
581
+ border-color: var(--vpg-accent);
582
+ box-shadow: 0 0 0 var(--vpg-focus-ring-width) var(--vpg-accent-ring);
583
+ }
584
+
585
+ /* The field whose control has its listbox open takes the accent border, so a pointer-opened
586
+ combobox \u2014 which \`:focus-visible\` does not match \u2014 still reads as the field the listbox belongs
587
+ to. Border only: the ring is keyboard focus's, and an open listbox does not claim it. The
588
+ \`:not()\` hands an invalid field to the danger border outright, so which of the two wins never
589
+ depends on the order these rules sit in or on the order stylesheets are injected. */
590
+ .vpg-field-shell:has(> [aria-expanded="true"]):not(:has(> [aria-invalid="true"])) {
591
+ border-color: var(--vpg-accent);
592
+ }
593
+
594
+ .vpg-field-shell:has(> [aria-invalid="true"]) {
595
+ border-color: var(--vpg-danger);
596
+ }
597
+
598
+ .vpg-field-shell:has(> [aria-invalid="true"]):has(> :focus-visible) {
599
+ box-shadow: 0 0 0 var(--vpg-focus-ring-width) var(--vpg-danger-ring);
600
+ }
601
+
602
+ .vpg-field-shell:has(> :disabled) {
603
+ cursor: not-allowed;
604
+ opacity: 0.55;
605
+ }
606
+
607
+ .vpg-field-shell:hover:not(:has(> :disabled)) {
608
+ background-color: var(--vpg-surface-hover);
609
+ }
610
+ `;
611
+ function FieldShell({ leading, trailing, className, children, ref, ...rest }) {
612
+ const classes = ["vpg-field-shell", className].filter(Boolean).join(" ");
613
+ const hasLeading = leading !== void 0 && leading !== null && leading !== "" && leading !== false;
614
+ const hasTrailing = trailing !== void 0 && trailing !== null && trailing !== "" && trailing !== false;
615
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
616
+ /* @__PURE__ */ jsx("style", { href: "vpg-field-shell", precedence: "vpg-field-shell", children: fieldShellStylesheet }),
617
+ /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, children: [
618
+ hasLeading ? /* @__PURE__ */ jsx("span", { className: "vpg-field-shell-leading", children: leading }) : null,
619
+ children,
620
+ hasTrailing ? /* @__PURE__ */ jsx("span", { className: "vpg-field-shell-trailing", children: trailing }) : null
621
+ ] })
622
+ ] });
623
+ }
624
+
625
+ // src/internal/listbox.stylesheet.ts
626
+ var listboxStylesheet = `
627
+ .vpg-listbox {
628
+ position: absolute;
629
+ z-index: var(--vpg-layer-listbox);
630
+ box-sizing: border-box;
631
+ margin: 0;
632
+ padding: var(--vpg-space-1);
633
+ /* The size of a container, not steps of anything: no scale carries a measurement this large,
634
+ and a \`--vpg-*\` name the theme never assigns advertises a theming hook that doesn't
635
+ exist. Width is floating-ui's inline style, matching the field exactly \u2014 this rule must
636
+ never impose a floor wider than a narrow field, or the panel outgrows what it's anchored to. */
637
+ max-height: 16rem;
638
+ overflow-y: auto;
639
+ background-color: var(--vpg-surface-raised);
640
+ border: 1px solid var(--vpg-border);
641
+ border-radius: var(--vpg-radius);
642
+ box-shadow: var(--vpg-shadow-med);
643
+ color: var(--vpg-ink);
644
+ font-family: var(--vpg-font-sans);
645
+ font-size: var(--vpg-font-size-sm);
646
+ line-height: 1.5;
647
+ }
648
+
649
+ .vpg-listbox:focus-visible {
650
+ /* The listbox takes DOM focus from nothing \u2014 the highlight is virtual \u2014 but it is still a
651
+ focus target for a stray programmatic focus() call, and an invisible one reads as broken. */
652
+ outline: var(--vpg-focus-ring-width) solid var(--vpg-accent-ring);
653
+ outline-offset: var(--vpg-focus-ring-offset);
654
+ }
655
+
656
+ /* A search row turns the listbox into a panel holding two parts, and the floating element is then
657
+ the panel rather than the listbox: it draws the surface \`.vpg-listbox\` draws for a listbox
658
+ that is the floating element itself, and holds the search row still while the options scroll
659
+ under it. \`overflow: hidden\` keeps the scrolling options inside the panel's rounded corners. */
660
+ .vpg-listbox-panel {
661
+ position: absolute;
662
+ z-index: var(--vpg-layer-listbox);
663
+ box-sizing: border-box;
664
+ overflow: hidden;
665
+ background-color: var(--vpg-surface-raised);
666
+ border: 1px solid var(--vpg-border);
667
+ border-radius: var(--vpg-radius);
668
+ box-shadow: var(--vpg-shadow-med);
669
+ color: var(--vpg-ink);
670
+ font-family: var(--vpg-font-sans);
671
+ font-size: var(--vpg-font-size-sm);
672
+ line-height: 1.5;
673
+ }
674
+
675
+ /* The scrolling half of the panel, at the same height \`.vpg-listbox\` stands at \u2014 the size of
676
+ a container, not a step of anything, so no \`--vpg-*\` name stands for it. The search row
677
+ above it is outside this box and so never scrolls out of reach. */
678
+ .vpg-listbox-options {
679
+ padding: var(--vpg-space-1);
680
+ max-height: 16rem;
681
+ overflow-y: auto;
682
+ }
683
+
684
+ /* The panel's first row: the magnifier, then the input. Its lower border is the divider between
685
+ the search and the options under it. The row stands at the same height as an option, so the
686
+ panel's first two rows read as one rhythm. */
687
+ .vpg-listbox-search {
688
+ display: flex;
689
+ align-items: center;
690
+ gap: var(--vpg-space-2);
691
+ box-sizing: border-box;
692
+ min-height: var(--vpg-size-md);
693
+ padding-inline: var(--vpg-space-3);
694
+ border-bottom: 1px solid var(--vpg-border);
695
+ }
696
+
697
+ .vpg-listbox-search-icon {
698
+ flex: none;
699
+ width: var(--vpg-icon-md);
700
+ height: var(--vpg-icon-md);
701
+ color: var(--vpg-ink-muted);
702
+ }
703
+
704
+ /* The input carries none of a field's chrome: the panel's own border is the box around it, and a
705
+ border or focus ring here would draw a second box inside that one. The panel opens with the
706
+ caret already in this input, which is what marks it as the focused element. */
707
+ .vpg-listbox-search-input {
708
+ flex: 1;
709
+ min-width: 0;
710
+ appearance: none;
711
+ padding: 0;
712
+ background: none;
713
+ border: none;
714
+ outline: none;
715
+ color: inherit;
716
+ font-family: inherit;
717
+ font-size: inherit;
718
+ line-height: inherit;
719
+ }
720
+
721
+ .vpg-listbox-search-input::placeholder {
722
+ color: var(--vpg-ink-subtle);
723
+ }
724
+
725
+ /* Not an option: it carries no role, is never highlighted and cannot be selected \u2014 it exists so
726
+ a query matching nothing says so instead of leaving the panel blank. Its padding is an option's
727
+ block padding, so the message stands as tall as the option it stands in for. */
728
+ .vpg-listbox-empty {
729
+ padding: var(--vpg-space-2);
730
+ color: var(--vpg-ink-subtle);
731
+ }
732
+
733
+ /* A heading, never a row the keyboard can reach: it is the group's name, and the options under it
734
+ keep the indices they hold with no group around them. Its inline padding is an option's, so the
735
+ heading and the labels beneath it share one left edge, and it takes the type scale's smallest
736
+ step to read as a label over the rows rather than as one of them. */
737
+ .vpg-listbox-group-label {
738
+ padding-block: var(--vpg-space-1);
739
+ padding-inline: var(--vpg-space-3);
740
+ color: var(--vpg-ink-subtle);
741
+ font-size: var(--vpg-font-size-xs);
742
+ font-weight: var(--vpg-font-weight-medium);
743
+ }
744
+
745
+ /* The line between one group and the next, drawn by the group that follows another. A rule rather
746
+ than a border on the group itself: the line spans the padding the options sit inside, so it
747
+ reaches the full width of the list rather than stopping at an option's edge. */
748
+ .vpg-listbox-separator {
749
+ height: 1px;
750
+ margin-block: var(--vpg-space-1);
751
+ margin-inline: calc(var(--vpg-space-1) * -1);
752
+ background-color: var(--vpg-border);
753
+ }
754
+
755
+ .vpg-listbox-option {
756
+ display: flex;
757
+ align-items: center;
758
+ gap: var(--vpg-space-2);
759
+ box-sizing: border-box;
760
+ /* A row's height is a token read, not the sum of a padding and a line-height: min-height
761
+ plus centred content holds it at the control scale's md step regardless of what the label's
762
+ font metrics or an icon's box happen to add up to. */
763
+ min-height: var(--vpg-size-md);
764
+ padding-inline: var(--vpg-space-3);
765
+ border-radius: var(--vpg-radius-sm);
766
+ cursor: pointer;
767
+ transition: background-color var(--vpg-duration-fast) var(--vpg-ease-standard);
768
+ /* The highlight moves with the keyboard, not with the pointer, so an option must never look
769
+ selectable-by-drag. */
770
+ user-select: none;
771
+ }
772
+
773
+ /* The highlight is virtual \u2014 the option never takes DOM focus, so :focus/:hover cannot express
774
+ it and the component sets this attribute from its own highlighted index instead. Highlight
775
+ (background) and selection (a checkbox fill or a trailing check, never colour) read on
776
+ different visual channels, so a row can carry both at once without either one washing out the
777
+ other. */
778
+ .vpg-listbox-option[data-highlighted] {
779
+ background-color: var(--vpg-surface-hover);
780
+ }
781
+
782
+ .vpg-listbox-option[aria-disabled="true"] {
783
+ color: var(--vpg-ink-subtle);
784
+ cursor: default;
785
+ }
786
+
787
+ .vpg-listbox-option-icon {
788
+ flex: none;
789
+ width: var(--vpg-icon-md);
790
+ height: var(--vpg-icon-md);
791
+ color: var(--vpg-ink-muted);
792
+ }
793
+
794
+ /* A single-select row's only selection signal: colour alone would fail WCAG 1.4.1, and
795
+ \`aria-selected\` reaches assistive technology but not a sighted reader. Trailing edge matches
796
+ the shadcn/Radix select convention. A multi-select row already carries the checkbox as its
797
+ one encoding, so this mark never renders alongside it \u2014 \`Dropdown\` renders it only for a
798
+ selected option outside \`multiple\`. */
799
+ .vpg-listbox-option-check {
800
+ flex: none;
801
+ width: var(--vpg-icon-md);
802
+ height: var(--vpg-icon-md);
803
+ color: var(--vpg-accent);
804
+ }
805
+
806
+ .vpg-listbox-option-label {
807
+ flex: 1;
808
+ overflow: hidden;
809
+ text-overflow: ellipsis;
810
+ white-space: nowrap;
811
+ }
812
+
813
+ /* A drawn box rather than an <input type="checkbox">: the option already carries
814
+ role="option" + aria-selected, and a real checkbox inside it would be an interactive
815
+ element nested in one. */
816
+ .vpg-listbox-checkbox {
817
+ display: flex;
818
+ align-items: center;
819
+ justify-content: center;
820
+ flex: none;
821
+ box-sizing: border-box;
822
+ width: 1.125rem;
823
+ height: 1.125rem;
824
+ border: 1px solid var(--vpg-border-strong);
825
+ border-radius: var(--vpg-radius-sm);
826
+ color: var(--vpg-accent-contrast);
827
+ transition: background-color var(--vpg-duration-fast) var(--vpg-ease-standard),
828
+ border-color var(--vpg-duration-fast) var(--vpg-ease-standard);
829
+ }
830
+
831
+ .vpg-listbox-checkbox[data-checked] {
832
+ background-color: var(--vpg-accent);
833
+ border-color: var(--vpg-accent);
834
+ }
835
+
836
+ /* The row of chips a multi-select field shows beside its control. It is a direct child of the
837
+ field's shell, sitting before the control, within the width the shell leaves it. Wrapping is
838
+ the shape it takes when the field asks for more than one row: each further line grows the field
839
+ downwards rather than overflowing it, because the shell's height is a floor.
840
+
841
+ One row of chips stands the field at the control step, the same height it has with no chips,
842
+ so selecting the first option never makes the field jump. The field's content box is the
843
+ control step less its border \u2014 30px at the default scale \u2014 and a chip is 24px, leaving 3px on
844
+ each side. The spacing scale's smallest step is 4px, which would grow the field to 34px, so the
845
+ row pads each edge by half of it: 2px, which a single row fits inside and the shell centres, and
846
+ which keeps the top and bottom rows of a wrapped field clear of its border. The rows themselves
847
+ are a whole step apart. */
848
+ .vpg-listbox-chips {
849
+ display: flex;
850
+ flex-wrap: wrap;
851
+ gap: var(--vpg-space-1);
852
+ padding-block: calc(var(--vpg-space-1) / 2);
853
+ }
854
+
855
+ /* One row, with the chips that do not fit measured out of it and an indicator standing for them.
856
+ Which chips those are is the component's read, not a rule here: the attribute says the row
857
+ collapses, and each chip the read excluded carries \`data-hidden\`.
858
+
859
+ \`nowrap\` is what holds the field at the control step in the one case the read cannot resolve:
860
+ a chip too wide to share the row with the indicator still shows, and wrapping would put the two
861
+ on separate rows rather than shrinking the chip to the room the indicator leaves. */
862
+ .vpg-listbox-chips[data-collapsing] {
863
+ flex-wrap: nowrap;
864
+ }
865
+
866
+ .vpg-listbox-chips[data-collapsing] > [data-hidden] {
867
+ display: none;
868
+ }
869
+
870
+ /* The read that decides which chips fit needs every one of them on the row at its own width: a
871
+ hidden chip has no width to weigh, a shrunk one reports the row's constraint rather than its
872
+ label, and the row itself shrinks to whatever is left in it once some are hidden. This
873
+ attribute is set and removed inside one synchronous measurement, so the state it describes
874
+ never paints. */
875
+ .vpg-listbox-chips[data-collapsing][data-measuring] > * {
876
+ display: inline-flex;
877
+ flex: none;
878
+ }
879
+
880
+ /* A chip's height is a token read, not the sum of a padding and a line-height: a fixed height
881
+ plus centred content holds it at the control scale's xs step regardless of the label's font
882
+ metrics. Its end carries no padding, so the remove button's round hover fill sits concentric
883
+ with the chip's own rounded end. */
884
+ .vpg-listbox-chip {
885
+ display: inline-flex;
886
+ align-items: center;
887
+ gap: var(--vpg-space-1);
888
+ box-sizing: border-box;
889
+ height: var(--vpg-size-xs);
890
+ padding-inline: var(--vpg-space-2) 0;
891
+ background-color: var(--vpg-accent-wash);
892
+ border: 1px solid transparent;
893
+ border-radius: var(--vpg-radius-full);
894
+ color: var(--vpg-ink);
895
+ font-family: var(--vpg-font-sans);
896
+ font-size: var(--vpg-font-size-sm);
897
+ line-height: 1.5;
898
+ }
899
+
900
+ /* A chip never outgrows the row that holds it: a label too long for the field is cut short
901
+ rather than pushing the chip, and the field with it, past the field's border.
902
+
903
+ \`min-width\` is what lets it be cut short at all. A flex item's automatic minimum size is its
904
+ content's, which for a chip is its label's longest word \u2014 so a chip holding one long word takes
905
+ the whole row and pushes the overflow indicator out past the field's border. The label carries
906
+ the ellipsis that keeps the shorter box readable. */
907
+ .vpg-listbox-chips > .vpg-listbox-chip {
908
+ min-width: 0;
909
+ max-width: 100%;
910
+ }
911
+
912
+ /* The indicator standing for the chips the row has no width for. A chip's box without a chip's
913
+ remove button: the selection it covers is unpicked in the listbox, since the chip carrying it
914
+ is not on screen to remove it from \u2014 so it pads both ends alike and keeps its own width, which
915
+ is the width the read reserves before any chip is counted onto the row. */
916
+ .vpg-listbox-overflow-chip {
917
+ display: inline-flex;
918
+ align-items: center;
919
+ flex: none;
920
+ box-sizing: border-box;
921
+ height: var(--vpg-size-xs);
922
+ padding-inline: var(--vpg-space-2);
923
+ background-color: var(--vpg-accent-wash);
924
+ border: 1px solid transparent;
925
+ border-radius: var(--vpg-radius-full);
926
+ color: var(--vpg-ink-muted);
927
+ font-family: var(--vpg-font-sans);
928
+ font-size: var(--vpg-font-size-sm);
929
+ line-height: 1.5;
930
+ white-space: nowrap;
931
+ }
932
+
933
+ .vpg-listbox-chip-label {
934
+ overflow: hidden;
935
+ text-overflow: ellipsis;
936
+ white-space: nowrap;
937
+ }
938
+
939
+ /* A square as tall as the chip's inside: the glyph plus a spacing step on every side, which at the
940
+ default scale is 22px \u2014 the whole of the chip's height inside its border, so the target is as
941
+ large as a 24px chip allows. */
942
+ .vpg-listbox-chip-remove {
943
+ display: flex;
944
+ align-items: center;
945
+ justify-content: center;
946
+ flex: none;
947
+ appearance: none;
948
+ padding: var(--vpg-space-1);
949
+ background: none;
950
+ border: none;
951
+ border-radius: var(--vpg-radius-full);
952
+ color: var(--vpg-ink-muted);
953
+ cursor: pointer;
954
+ transition: background-color var(--vpg-duration-fast) var(--vpg-ease-standard),
955
+ color var(--vpg-duration-fast) var(--vpg-ease-standard);
956
+ }
957
+
958
+ .vpg-listbox-chip-remove-icon {
959
+ flex: none;
960
+ width: var(--vpg-icon-sm);
961
+ height: var(--vpg-icon-sm);
962
+ }
963
+
964
+ .vpg-listbox-chip-remove:hover {
965
+ background-color: var(--vpg-accent-wash);
966
+ color: var(--vpg-ink);
967
+ }
968
+
969
+ .vpg-listbox-chip-remove:focus-visible {
970
+ outline: var(--vpg-focus-ring-width) solid var(--vpg-accent-ring);
971
+ outline-offset: var(--vpg-focus-ring-offset);
972
+ }
973
+ `;
974
+ var LISTBOX_OFFSET = 4;
975
+ var VIEWPORT_PADDING = 12;
976
+ function withoutActiveDescendant(props) {
977
+ const rest = { ...props };
978
+ delete rest["aria-activedescendant"];
979
+ return rest;
980
+ }
981
+ function useListboxKeyboard({
982
+ listRef,
983
+ activeIndex,
984
+ onNavigate,
985
+ disabledIndices,
986
+ typeahead,
987
+ role,
988
+ search = false,
989
+ open,
990
+ onOpenChange
991
+ }) {
992
+ const { refs, floatingStyles, context, elements } = useFloating({
993
+ open,
994
+ onOpenChange: (next) => {
995
+ onOpenChange(next);
996
+ },
997
+ placement: "bottom-start",
998
+ // The listbox's left edge and width are the field's wherever the field is, flush against the
999
+ // viewport's edge or partly scrolled past it. At `bottom-start` with equal widths the listbox
1000
+ // overflows horizontally exactly where the field does, so nothing slides it sideways: a
1001
+ // `shift` would only pull it off the field's edge. `flip` alone moves it, from below the field
1002
+ // to above it. `size` runs last so it reads the placement `flip` settled on. Its width is the
1003
+ // field's border box, written straight onto the element because floating-ui computes it
1004
+ // outside render; `autoUpdate` observes the field's size, so a field that grows as its chips
1005
+ // wrap re-runs the whole chain.
1006
+ middleware: [
1007
+ offset(LISTBOX_OFFSET),
1008
+ flip({ padding: VIEWPORT_PADDING }),
1009
+ size({
1010
+ apply({ rects, elements: { floating } }) {
1011
+ floating.style.width = `${rects.reference.width}px`;
1012
+ }
1013
+ })
1014
+ ],
1015
+ whileElementsMounted: autoUpdate
1016
+ });
1017
+ const fieldElementRef = useRef(null);
1018
+ const fieldRef = useCallback(
1019
+ (node) => {
1020
+ fieldElementRef.current = node;
1021
+ refs.setPositionReference(node);
1022
+ },
1023
+ [refs]
1024
+ );
1025
+ function onFieldMouseDown(event) {
1026
+ const reference = refs.domReference.current;
1027
+ const target = event.target;
1028
+ if (reference.contains(target)) {
1029
+ return;
1030
+ }
1031
+ event.preventDefault();
1032
+ if (event.button !== 0 || target.closest("button") !== null) {
1033
+ return;
1034
+ }
1035
+ reference.focus();
1036
+ onOpenChange(true);
1037
+ }
1038
+ const disabledIndicesRef = useRef(disabledIndices);
1039
+ disabledIndicesRef.current = disabledIndices;
1040
+ const labelsRef = useMemo(
1041
+ () => ({
1042
+ get current() {
1043
+ return listRef.current.map(
1044
+ (item, index) => item !== null && !disabledIndicesRef.current.includes(index) ? item.textContent : null
1045
+ );
1046
+ }
1047
+ }),
1048
+ [listRef]
1049
+ );
1050
+ const click = useClick(context, { keyboardHandlers: false });
1051
+ const dismiss = useDismiss(context, {
1052
+ outsidePress: (event) => {
1053
+ const field = fieldElementRef.current;
1054
+ return field === null || !field.contains(event.target);
1055
+ },
1056
+ // In `search` mode the press that closes the listbox is the `click`, not the `pointerdown`
1057
+ // that starts it. Real focus is inside the floating element, and `FloatingFocusManager`
1058
+ // returns it to the reference element as the floating element unmounts; closing on
1059
+ // `pointerdown` puts that return before `mousedown`'s own default action, which then blanks
1060
+ // focus to `<body>` when the press landed on something unfocusable. Closing on `click`
1061
+ // leaves the browser's focus move first and the return last. Where the press landed on
1062
+ // something focusable, the manager sees focus already outside and leaves it there.
1063
+ outsidePressEvent: search ? "click" : "pointerdown"
1064
+ });
1065
+ const listboxRole = useRole(context, { role });
1066
+ const listNavigation = useListNavigation(context, {
1067
+ listRef,
1068
+ activeIndex,
1069
+ onNavigate,
1070
+ disabledIndices,
1071
+ virtual: true,
1072
+ loop: true
1073
+ });
1074
+ const typeaheadInteraction = useTypeahead(context, {
1075
+ listRef: labelsRef,
1076
+ activeIndex,
1077
+ onMatch: onNavigate,
1078
+ enabled: typeahead,
1079
+ ignoreKeys: [" "]
1080
+ });
1081
+ const {
1082
+ getReferenceProps: getMergedReferenceProps,
1083
+ getFloatingProps: getMergedFloatingProps,
1084
+ getItemProps
1085
+ } = useInteractions([click, dismiss, listboxRole, listNavigation, typeaheadInteraction]);
1086
+ const searchInteractions = useInteractions([listboxRole, listNavigation]);
1087
+ const getSearchProps = useCallback(
1088
+ (userProps) => searchInteractions.getReferenceProps({ "aria-autocomplete": "list", ...userProps }),
1089
+ [searchInteractions]
1090
+ );
1091
+ const getReferenceProps = useCallback(
1092
+ (userProps) => search ? withoutActiveDescendant(getMergedReferenceProps(userProps)) : getMergedReferenceProps(userProps),
1093
+ [getMergedReferenceProps, search]
1094
+ );
1095
+ const getFloatingProps = useCallback(
1096
+ (userProps) => search ? withoutActiveDescendant(getMergedFloatingProps(userProps)) : getMergedFloatingProps(userProps),
1097
+ [getMergedFloatingProps, search]
1098
+ );
1099
+ const themeRoot = elements.domReference?.closest(".vpg-root") ?? null;
1100
+ return {
1101
+ refs,
1102
+ floatingStyles,
1103
+ context,
1104
+ elements,
1105
+ themeRoot,
1106
+ fieldRef,
1107
+ onFieldMouseDown,
1108
+ getReferenceProps,
1109
+ getFloatingProps,
1110
+ getItemProps,
1111
+ getSearchProps
1112
+ };
1113
+ }
1114
+
1115
+ // src/Tooltip/Tooltip.stylesheet.ts
1116
+ var tooltipStylesheet = `
1117
+ .vpg-tooltip-trigger {
1118
+ /* Inline-flex rather than inline: the wrapper exists only to carry the ref and the hover/focus
1119
+ handlers, and must not add a baseline gap under the element it wraps. */
1120
+ display: inline-flex;
1121
+ }
1122
+
1123
+ .vpg-tooltip {
1124
+ position: absolute;
1125
+ z-index: var(--vpg-layer-tooltip);
1126
+ box-sizing: border-box;
1127
+ /* The size of a container, not a step of anything: no scale carries a measurement this large,
1128
+ and a \`--vpg-*\` name the theme never assigns advertises a theming hook that doesn't
1129
+ exist. */
1130
+ max-width: 16rem;
1131
+ padding: 0.25rem 0.5rem;
1132
+ border-radius: var(--vpg-radius-sm);
1133
+ background-color: var(--vpg-ink);
1134
+ color: var(--vpg-surface);
1135
+ font-family: var(--vpg-font-sans);
1136
+ font-size: var(--vpg-font-size-xs);
1137
+ line-height: 1.4;
1138
+ /* A tooltip that could be hovered would sit between the pointer and the trigger and flicker
1139
+ the trigger's own hover state off. */
1140
+ pointer-events: none;
1141
+ /* Long content wraps against max-width instead of stretching off-screen; an explicit newline
1142
+ in the content is still honoured. */
1143
+ white-space: pre-line;
1144
+ overflow-wrap: break-word;
1145
+ }
1146
+ `;
1147
+ var TOOLTIP_OFFSET = 8;
1148
+ var VIEWPORT_PADDING2 = 8;
1149
+ function Tooltip({ content, children, placement = "top", disabled = false, className }) {
1150
+ const [requestedOpen, setRequestedOpen] = useState(false);
1151
+ const enabled = !disabled;
1152
+ const open = requestedOpen && enabled;
1153
+ const { refs, floatingStyles, context, elements } = useFloating({
1154
+ open,
1155
+ onOpenChange: setRequestedOpen,
1156
+ placement,
1157
+ middleware: [offset(TOOLTIP_OFFSET), flip(), shift({ padding: VIEWPORT_PADDING2 })],
1158
+ whileElementsMounted: autoUpdate
1159
+ });
1160
+ const hover = useHover(context, { enabled });
1161
+ const focus = useFocus(context, { enabled });
1162
+ const dismiss = useDismiss(context, { enabled, referencePress: false });
1163
+ const tooltipRole = useRole(context, { enabled, role: "tooltip" });
1164
+ const { getReferenceProps, getFloatingProps } = useInteractions([hover, focus, dismiss, tooltipRole]);
1165
+ const { "aria-describedby": describedBy, ...referenceProps } = getReferenceProps();
1166
+ const hasSingleElementChild = isValidElement(children) && children.type !== Fragment$1;
1167
+ const trigger = hasSingleElementChild ? cloneElement(children, { "aria-describedby": describedBy }) : children;
1168
+ const bubble = open ? /* @__PURE__ */ jsx(
1169
+ "div",
1170
+ {
1171
+ ref: refs.setFloating,
1172
+ className: ["vpg-tooltip", className].filter(Boolean).join(" "),
1173
+ style: floatingStyles,
1174
+ ...getFloatingProps(),
1175
+ children: content
1176
+ }
1177
+ ) : null;
1178
+ const themeRoot = elements.domReference?.closest(".vpg-root") ?? null;
1179
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1180
+ /* @__PURE__ */ jsx("style", { href: "vpg-tooltip", precedence: "vpg-tooltip", children: tooltipStylesheet }),
1181
+ /* @__PURE__ */ jsx(
1182
+ "span",
1183
+ {
1184
+ ref: refs.setReference,
1185
+ className: "vpg-tooltip-trigger",
1186
+ "aria-describedby": hasSingleElementChild ? void 0 : describedBy,
1187
+ ...referenceProps,
1188
+ children: trigger
1189
+ }
1190
+ ),
1191
+ bubble !== null && themeRoot !== null ? createPortal(bubble, themeRoot) : bubble
1192
+ ] });
1193
+ }
1194
+
1195
+ // src/Dropdown/Dropdown.stylesheet.ts
1196
+ var dropdownStylesheet = `
1197
+ /* A block box the width of its container, like \`TextField\`: the field never widens as options
1198
+ are selected and never narrows below its container either. \`box-sizing: border-box\` keeps
1199
+ that width inclusive of the field's own border and padding.
1200
+
1201
+ \`min-width: 0\` is what makes that hold in a flex or grid container. The initial \`auto\`
1202
+ resolves there to the automatic minimum size \u2014 the root's min-content, which a row of chips
1203
+ and a long trigger label make wide \u2014 and that floor outranks \`width: 100%\`, so the field
1204
+ pushes its track open and overflows whatever width the container was given. */
1205
+ .vpg-dropdown {
1206
+ display: block;
1207
+ box-sizing: border-box;
1208
+ width: 100%;
1209
+ min-width: 0;
1210
+ color: var(--vpg-ink);
1211
+ font-family: var(--vpg-font-sans);
1212
+ }
1213
+
1214
+ /* \`align-self: stretch\` gives the trigger the field's full height, so a click anywhere across it
1215
+ opens the listbox; a percentage height would collapse, because the shell's height is a floor
1216
+ rather than a definite length. */
1217
+ .vpg-dropdown-trigger {
1218
+ display: flex;
1219
+ align-items: center;
1220
+ align-self: stretch;
1221
+ gap: var(--vpg-space-2);
1222
+ outline: none;
1223
+ color: inherit;
1224
+ font-size: var(--vpg-font-size-sm);
1225
+ line-height: 1.5;
1226
+ cursor: pointer;
1227
+ user-select: none;
1228
+ }
1229
+
1230
+ /* The shell lets every centre element shrink to nothing, which beside a row of chips would leave
1231
+ the trigger \u2014 the only thing that opens the listbox \u2014 with no width at all. A square of the
1232
+ field's own height keeps it a target and keeps room for the chevron. Qualified by both classes
1233
+ the shell element carries, so it outranks the shell's own \`min-width: 0\` whichever stylesheet
1234
+ the page injects first. */
1235
+ .vpg-field-shell.vpg-dropdown-control > .vpg-dropdown-trigger {
1236
+ min-width: var(--vpg-size-md);
1237
+ }
1238
+
1239
+ .vpg-dropdown-trigger-icon,
1240
+ .vpg-dropdown-chevron {
1241
+ flex: none;
1242
+ }
1243
+
1244
+ /* Matches \`.vpg-listbox-option-icon\`: the same icon at the same size in the trigger as in
1245
+ the list it came from, rather than lucide's own 24px default. */
1246
+ .vpg-dropdown-trigger-icon {
1247
+ width: var(--vpg-icon-md);
1248
+ height: var(--vpg-icon-md);
1249
+ }
1250
+
1251
+ /* \`margin-left: auto\` is what sets the chevron against the field's trailing edge. A \`multiple\`
1252
+ trigger holding a selection contains the chevron and nothing else, so no sibling claims the
1253
+ trigger's free space and the chevron would otherwise sit at its leading edge, floating
1254
+ mid-field. The margin is inert wherever a \`flex: 1\` sibling has already absorbed that space \u2014
1255
+ single-select's value and either mode's placeholder \u2014 since there is none left for it to take. */
1256
+ .vpg-dropdown-chevron {
1257
+ margin-left: auto;
1258
+ color: var(--vpg-ink-muted);
1259
+ }
1260
+
1261
+ /* Takes the trigger's free space, so the chevron's auto margin resolves to nothing and the label
1262
+ keeps the field's leading edge. */
1263
+ .vpg-dropdown-value {
1264
+ flex: 1;
1265
+ overflow: hidden;
1266
+ text-overflow: ellipsis;
1267
+ white-space: nowrap;
1268
+ }
1269
+
1270
+ .vpg-dropdown-placeholder {
1271
+ flex: 1;
1272
+ color: var(--vpg-ink-subtle);
1273
+ }
1274
+
1275
+ /* The clear button stands in the shell's trailing slot, a subtree the shell's \`> \` state rules do
1276
+ not reach into \u2014 so its focus ring is its own, and the field takes none from it. It carries no
1277
+ \`opacity\` either: the shell already dims for a disabled control, and a second fade here reads
1278
+ as more faded than the field around it. */
1279
+ .vpg-dropdown-clear {
1280
+ display: flex;
1281
+ align-items: center;
1282
+ justify-content: center;
1283
+ flex: none;
1284
+ appearance: none;
1285
+ padding: var(--vpg-space-1);
1286
+ background: none;
1287
+ border: none;
1288
+ border-radius: var(--vpg-radius-full);
1289
+ color: var(--vpg-ink-muted);
1290
+ cursor: pointer;
1291
+ transition: background-color var(--vpg-duration-fast) var(--vpg-ease-standard),
1292
+ color var(--vpg-duration-fast) var(--vpg-ease-standard);
1293
+ }
1294
+
1295
+ .vpg-dropdown-clear-icon {
1296
+ flex: none;
1297
+ width: var(--vpg-icon-sm);
1298
+ height: var(--vpg-icon-sm);
1299
+ }
1300
+
1301
+ .vpg-dropdown-clear:hover {
1302
+ background-color: var(--vpg-accent-wash);
1303
+ color: var(--vpg-ink);
1304
+ }
1305
+
1306
+ .vpg-dropdown-clear:focus-visible {
1307
+ outline: var(--vpg-focus-ring-width) solid var(--vpg-accent-ring);
1308
+ outline-offset: var(--vpg-focus-ring-offset);
1309
+ }
1310
+
1311
+ /* Names every selection to a screen reader while taking no space in the field, which is the only
1312
+ route to the chips the row has no width for: those are out of the flow altogether. */
1313
+ .vpg-dropdown-selection-description {
1314
+ position: absolute;
1315
+ width: 1px;
1316
+ height: 1px;
1317
+ padding: 0;
1318
+ margin: -1px;
1319
+ overflow: hidden;
1320
+ clip: rect(0, 0, 0, 0);
1321
+ white-space: nowrap;
1322
+ border: 0;
1323
+ }
1324
+ `;
1325
+ var DropdownContext = createContext(null);
1326
+ function useDropdownContext(component) {
1327
+ const context = useContext(DropdownContext);
1328
+ if (context === null) {
1329
+ throw new Error(`${component} must be rendered inside <Dropdown>.`);
1330
+ }
1331
+ return context;
1332
+ }
1333
+ function DropdownOption({ value, label, icon: OptionIcon, disabled = false }) {
1334
+ const { multiple, optionIndices, selectedValues, highlightedValue, select, registerOption, getItemProps } = useDropdownContext("Dropdown.Option");
1335
+ const index = optionIndices.get(value);
1336
+ if (index === void 0) {
1337
+ return null;
1338
+ }
1339
+ const selected = selectedValues.includes(value);
1340
+ const highlighted = highlightedValue === value;
1341
+ return /* @__PURE__ */ jsxs(
1342
+ "div",
1343
+ {
1344
+ ...getItemProps({
1345
+ active: highlighted,
1346
+ selected,
1347
+ onClick: () => {
1348
+ if (!disabled) {
1349
+ select(toValue({ value, label, icon: OptionIcon}));
1350
+ }
1351
+ }
1352
+ }),
1353
+ ref: (node) => {
1354
+ registerOption(index, node);
1355
+ },
1356
+ className: "vpg-listbox-option",
1357
+ "aria-disabled": disabled ? true : void 0,
1358
+ "data-highlighted": highlighted ? "" : void 0,
1359
+ children: [
1360
+ multiple ? /* @__PURE__ */ jsx("span", { className: "vpg-listbox-checkbox", "data-checked": selected ? "" : void 0, children: selected ? /* @__PURE__ */ jsx(Check, { size: 12, "aria-hidden": "true" }) : null }) : null,
1361
+ OptionIcon ? /* @__PURE__ */ jsx(OptionIcon, { className: "vpg-listbox-option-icon", "aria-hidden": "true" }) : null,
1362
+ /* @__PURE__ */ jsx("span", { className: "vpg-listbox-option-label", children: label }),
1363
+ !multiple && selected ? /* @__PURE__ */ jsx(Check, { className: "vpg-listbox-option-check", "aria-hidden": "true" }) : null
1364
+ ]
1365
+ }
1366
+ );
1367
+ }
1368
+ function DropdownGroup({ label, children }) {
1369
+ const { visibleGroups } = useDropdownContext("Dropdown.Group");
1370
+ const headingId = useId();
1371
+ const position = visibleGroups.indexOf(label);
1372
+ if (position === -1) {
1373
+ return null;
1374
+ }
1375
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1376
+ position > 0 ? /* @__PURE__ */ jsx("div", { className: "vpg-listbox-separator", "aria-hidden": "true" }) : null,
1377
+ /* @__PURE__ */ jsxs("div", { role: "group", className: "vpg-listbox-group", "aria-labelledby": headingId, children: [
1378
+ /* @__PURE__ */ jsx("div", { id: headingId, className: "vpg-listbox-group-label", children: label }),
1379
+ children
1380
+ ] })
1381
+ ] });
1382
+ }
1383
+ function isFalsyChild(child) {
1384
+ return child === null || child === void 0 || typeof child === "boolean";
1385
+ }
1386
+ function toDescriptor(props, group) {
1387
+ const { value, label, icon, disabled = false } = props;
1388
+ return { value, label, icon, disabled, group };
1389
+ }
1390
+ function readGroupOptions(children, group) {
1391
+ const options = [];
1392
+ Children.forEach(children, (child) => {
1393
+ if (isFalsyChild(child)) {
1394
+ return;
1395
+ }
1396
+ if (!isValidElement(child) || child.type !== DropdownOption) {
1397
+ throw new Error("Dropdown.Group only accepts Dropdown.Option as children.");
1398
+ }
1399
+ options.push(toDescriptor(child.props, group));
1400
+ });
1401
+ return options;
1402
+ }
1403
+ function readOptions(children) {
1404
+ const options = [];
1405
+ Children.forEach(children, (child) => {
1406
+ if (isFalsyChild(child)) {
1407
+ return;
1408
+ }
1409
+ if (isValidElement(child) && child.type === DropdownGroup) {
1410
+ const { label, children: groupChildren } = child.props;
1411
+ options.push(...readGroupOptions(groupChildren, label));
1412
+ return;
1413
+ }
1414
+ if (!isValidElement(child) || child.type !== DropdownOption) {
1415
+ throw new Error("Dropdown only accepts Dropdown.Option and Dropdown.Group as children.");
1416
+ }
1417
+ options.push(toDescriptor(child.props));
1418
+ });
1419
+ return options;
1420
+ }
1421
+ function groupsOf(options) {
1422
+ const groups = [];
1423
+ for (const option of options) {
1424
+ if (option.group !== void 0 && !groups.includes(option.group)) {
1425
+ groups.push(option.group);
1426
+ }
1427
+ }
1428
+ return groups;
1429
+ }
1430
+ function inGroupOrder(options) {
1431
+ return [
1432
+ ...options.filter((option) => option.group === void 0),
1433
+ ...groupsOf(options).flatMap((group) => options.filter((option) => option.group === group))
1434
+ ];
1435
+ }
1436
+ function toSelection(value) {
1437
+ if (value === null || value === void 0) {
1438
+ return [];
1439
+ }
1440
+ return Array.isArray(value) ? value : [value];
1441
+ }
1442
+ function matchesQuery(label, query) {
1443
+ return label.toLowerCase().includes(query.toLowerCase());
1444
+ }
1445
+ function firstEnabledIndex(options) {
1446
+ const index = options.findIndex((option) => !option.disabled);
1447
+ return index === -1 ? null : index;
1448
+ }
1449
+ function isPrintable(event) {
1450
+ return event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
1451
+ }
1452
+ function toValue({ value, label, icon }) {
1453
+ return icon === void 0 ? { value, label } : { value, label, icon };
1454
+ }
1455
+ function resolveSelection(selection, options) {
1456
+ return selection.map((selected) => {
1457
+ const option = options.find((candidate) => candidate.value === selected.value);
1458
+ return option === void 0 ? selected : toValue(option);
1459
+ });
1460
+ }
1461
+ function widthsOf(row, selector) {
1462
+ return Array.from(row.querySelectorAll(selector), (box) => box.getBoundingClientRect().width);
1463
+ }
1464
+ function countFitting(widths, gap, available) {
1465
+ let used = 0;
1466
+ let fitting = 0;
1467
+ for (const width of widths) {
1468
+ used += fitting === 0 ? width : gap + width;
1469
+ if (used > available) {
1470
+ break;
1471
+ }
1472
+ fitting += 1;
1473
+ }
1474
+ return fitting;
1475
+ }
1476
+ function measureHiddenChips(row) {
1477
+ row.setAttribute("data-measuring", "");
1478
+ const chips = widthsOf(row, ".vpg-listbox-chip");
1479
+ const indicator = widthsOf(row, ".vpg-listbox-overflow-chip");
1480
+ const available = row.clientWidth;
1481
+ row.removeAttribute("data-measuring");
1482
+ if (available === 0) {
1483
+ return 0;
1484
+ }
1485
+ const gap = Number.parseFloat(getComputedStyle(row).columnGap);
1486
+ if (countFitting(chips, gap, available) === chips.length) {
1487
+ return 0;
1488
+ }
1489
+ const showing = Math.max(countFitting([...indicator, ...chips], gap, available) - 1, 1);
1490
+ return chips.length - showing;
1491
+ }
1492
+ function DropdownImpl(props) {
1493
+ const {
1494
+ children,
1495
+ loadOptions,
1496
+ debounceMs = 300,
1497
+ loadingMessage = "Loading\u2026",
1498
+ errorMessage = "Something went wrong.",
1499
+ placeholder = "Select\u2026",
1500
+ searchable = true,
1501
+ searchPlaceholder = "Search",
1502
+ wrapChips = false,
1503
+ clearable = false,
1504
+ className,
1505
+ id,
1506
+ "aria-label": ariaLabel,
1507
+ "aria-labelledby": ariaLabelledBy,
1508
+ "aria-describedby": ariaDescribedBy,
1509
+ "aria-invalid": ariaInvalid
1510
+ } = props;
1511
+ const isAsync = loadOptions !== void 0;
1512
+ const multiple = props.multiple === true;
1513
+ const controlled = props.value !== void 0;
1514
+ const emitChange = props.onChange;
1515
+ const [uncontrolledSelection, setUncontrolledSelection] = useState(() => toSelection(props.defaultValue));
1516
+ const selection = controlled ? toSelection(props.value) : uncontrolledSelection;
1517
+ const selectedValues = selection.map((selected) => selected.value);
1518
+ const selectionDescriptionId = useId();
1519
+ const [open, setOpen] = useState(false);
1520
+ const [highlightedIndex, setHighlightedIndex] = useState(null);
1521
+ const [query, setQuery] = useState("");
1522
+ const listRef = useRef([]);
1523
+ const searchRef = useRef(null);
1524
+ const chipsRef = useRef(null);
1525
+ const [hiddenChipCount, setHiddenChipCount] = useState(0);
1526
+ const [asyncOptions, setAsyncOptions] = useState([]);
1527
+ const [asyncStatus, setAsyncStatus] = useState("idle");
1528
+ const searchTokenRef = useRef(0);
1529
+ const loadOptionsRef = useRef(loadOptions);
1530
+ useEffect(() => {
1531
+ loadOptionsRef.current = loadOptions;
1532
+ });
1533
+ useEffect(() => {
1534
+ if (!isAsync) {
1535
+ return;
1536
+ }
1537
+ const token = ++searchTokenRef.current;
1538
+ const timer = setTimeout(() => {
1539
+ setAsyncStatus("loading");
1540
+ const load = loadOptionsRef.current;
1541
+ load(query).then(
1542
+ (results) => {
1543
+ if (searchTokenRef.current !== token) {
1544
+ return;
1545
+ }
1546
+ const loaded = inGroupOrder(
1547
+ results.map((result) => ({
1548
+ value: result.value,
1549
+ label: result.label,
1550
+ icon: result.icon,
1551
+ disabled: result.disabled ?? false,
1552
+ group: result.group
1553
+ }))
1554
+ );
1555
+ setAsyncOptions(loaded);
1556
+ setAsyncStatus("idle");
1557
+ setHighlightedIndex(firstEnabledIndex(loaded));
1558
+ },
1559
+ () => {
1560
+ if (searchTokenRef.current !== token) {
1561
+ return;
1562
+ }
1563
+ setAsyncOptions([]);
1564
+ setAsyncStatus("error");
1565
+ setHighlightedIndex(null);
1566
+ }
1567
+ );
1568
+ }, debounceMs);
1569
+ return () => {
1570
+ clearTimeout(timer);
1571
+ };
1572
+ }, [isAsync, query, debounceMs]);
1573
+ const options = isAsync ? [] : readOptions(children);
1574
+ const matches = isAsync ? asyncOptions : searchable ? options.filter((option) => matchesQuery(option.label, query)) : options;
1575
+ const optionIndices = new Map(matches.map((option, index) => [option.value, index]));
1576
+ const visibleGroups = groupsOf(matches);
1577
+ const selectedEntries = resolveSelection(selection, options);
1578
+ const disabledIndices = matches.flatMap((option, index) => option.disabled ? [index] : []);
1579
+ const highlighted = highlightedIndex === null ? void 0 : matches[highlightedIndex];
1580
+ listRef.current.length = matches.length;
1581
+ const collapseChips = multiple && !wrapChips;
1582
+ const visibleChipCount = selectedEntries.length - hiddenChipCount;
1583
+ const chipLabels = JSON.stringify(selectedEntries.map((selected) => selected.label));
1584
+ const hiddenLabels = selectedEntries.slice(visibleChipCount).map((selected) => selected.label);
1585
+ const showClear = clearable && selectedEntries.length > 0;
1586
+ const describesSelection = multiple && selectedEntries.length > 0;
1587
+ const describedBy = [ariaDescribedBy, describesSelection ? selectionDescriptionId : null].filter(Boolean).join(" ");
1588
+ useLayoutEffect(() => {
1589
+ if (!collapseChips) {
1590
+ setHiddenChipCount(0);
1591
+ return;
1592
+ }
1593
+ const row = chipsRef.current;
1594
+ if (row === null) {
1595
+ return;
1596
+ }
1597
+ function measure() {
1598
+ setHiddenChipCount(measureHiddenChips(row));
1599
+ }
1600
+ measure();
1601
+ const observer = new ResizeObserver(() => {
1602
+ flushSync(measure);
1603
+ });
1604
+ observer.observe(row.parentElement);
1605
+ return () => {
1606
+ observer.disconnect();
1607
+ };
1608
+ }, [collapseChips, chipLabels]);
1609
+ function handleOpenChange(next) {
1610
+ setOpen(next);
1611
+ if (!next) {
1612
+ setHighlightedIndex(null);
1613
+ setQuery("");
1614
+ }
1615
+ }
1616
+ function applyQuery(next) {
1617
+ setQuery(next);
1618
+ if (isAsync) {
1619
+ setHighlightedIndex(null);
1620
+ return;
1621
+ }
1622
+ setHighlightedIndex(firstEnabledIndex(options.filter((option) => matchesQuery(option.label, next))));
1623
+ }
1624
+ function commit(next, reported) {
1625
+ if (!controlled) {
1626
+ setUncontrolledSelection(next);
1627
+ }
1628
+ emitChange?.(reported);
1629
+ }
1630
+ function select(option) {
1631
+ if (multiple) {
1632
+ const next = selectedValues.includes(option.value) ? selection.filter((selected) => selected.value !== option.value) : [...selection, option];
1633
+ commit(next, next);
1634
+ if (searchable) {
1635
+ setQuery("");
1636
+ setHighlightedIndex((isAsync ? asyncOptions : options).findIndex((candidate) => candidate.value === option.value));
1637
+ }
1638
+ return;
1639
+ }
1640
+ commit([option], option);
1641
+ handleOpenChange(false);
1642
+ }
1643
+ function remove(value) {
1644
+ const next = selection.filter((selected) => selected.value !== value);
1645
+ commit(next, next);
1646
+ }
1647
+ const {
1648
+ refs,
1649
+ floatingStyles,
1650
+ context: floatingContext,
1651
+ themeRoot,
1652
+ fieldRef,
1653
+ onFieldMouseDown,
1654
+ getReferenceProps,
1655
+ getFloatingProps,
1656
+ getItemProps,
1657
+ getSearchProps
1658
+ } = useListboxKeyboard({
1659
+ listRef,
1660
+ activeIndex: highlightedIndex,
1661
+ onNavigate: setHighlightedIndex,
1662
+ disabledIndices,
1663
+ // A search input owns every keystroke once one exists: type-ahead is the trigger's own way of
1664
+ // reaching an option by its label, for the mode with no input to type into.
1665
+ typeahead: !searchable,
1666
+ role: "select",
1667
+ search: searchable,
1668
+ open,
1669
+ onOpenChange: handleOpenChange
1670
+ });
1671
+ function clear() {
1672
+ commit([], multiple ? [] : null);
1673
+ refs.domReference.current.focus();
1674
+ }
1675
+ function handleTriggerKeyDown(event) {
1676
+ if (event.key !== "Enter" && event.key !== " ") {
1677
+ if (searchable && !open && isPrintable(event)) {
1678
+ event.preventDefault();
1679
+ handleOpenChange(true);
1680
+ applyQuery(event.key);
1681
+ }
1682
+ return;
1683
+ }
1684
+ event.preventDefault();
1685
+ if (!open) {
1686
+ handleOpenChange(true);
1687
+ return;
1688
+ }
1689
+ if (highlighted === void 0 || highlighted.disabled) {
1690
+ return;
1691
+ }
1692
+ select(toValue(highlighted));
1693
+ }
1694
+ function handleSearchChange(event) {
1695
+ applyQuery(event.target.value);
1696
+ }
1697
+ function handleSearchKeyDown(event) {
1698
+ const last = selection.at(-1);
1699
+ if (multiple && event.key === "Backspace" && query === "" && last !== void 0) {
1700
+ remove(last.value);
1701
+ return;
1702
+ }
1703
+ if (event.key !== "Enter") {
1704
+ return;
1705
+ }
1706
+ event.preventDefault();
1707
+ if (highlighted === void 0 || highlighted.disabled) {
1708
+ return;
1709
+ }
1710
+ select(toValue(highlighted));
1711
+ }
1712
+ const context = {
1713
+ multiple,
1714
+ optionIndices,
1715
+ visibleGroups,
1716
+ selectedValues,
1717
+ highlightedValue: highlighted === void 0 ? null : highlighted.value,
1718
+ select,
1719
+ registerOption(index, node) {
1720
+ if (node !== null) {
1721
+ listRef.current[index] = node;
1722
+ }
1723
+ },
1724
+ getItemProps
1725
+ };
1726
+ function renderTriggerContent() {
1727
+ const placeholderContent = /* @__PURE__ */ jsx("span", { className: "vpg-dropdown-placeholder", children: placeholder });
1728
+ if (multiple) {
1729
+ return selectedEntries.length === 0 ? placeholderContent : null;
1730
+ }
1731
+ const selected = selectedEntries[0];
1732
+ if (selected === void 0) {
1733
+ return placeholderContent;
1734
+ }
1735
+ const SelectedIcon = selected.icon;
1736
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1737
+ SelectedIcon ? /* @__PURE__ */ jsx(SelectedIcon, { className: "vpg-dropdown-trigger-icon", "aria-hidden": "true" }) : null,
1738
+ /* @__PURE__ */ jsx("span", { className: "vpg-dropdown-value", children: selected.label })
1739
+ ] });
1740
+ }
1741
+ function renderOverflowIndicator() {
1742
+ const indicator = /* @__PURE__ */ jsxs("span", { className: "vpg-listbox-overflow-chip", "data-hidden": hiddenChipCount === 0 ? "" : void 0, children: [
1743
+ "and ",
1744
+ hiddenChipCount,
1745
+ " more"
1746
+ ] });
1747
+ if (hiddenChipCount === 0) {
1748
+ return indicator;
1749
+ }
1750
+ return /* @__PURE__ */ jsx(Tooltip, { content: hiddenLabels.join(", "), children: indicator });
1751
+ }
1752
+ function renderClearButton() {
1753
+ return /* @__PURE__ */ jsx("button", { type: "button", className: "vpg-dropdown-clear", "aria-label": "Clear selection", onClick: clear, children: /* @__PURE__ */ jsx(X, { className: "vpg-dropdown-clear-icon", "aria-hidden": "true" }) });
1754
+ }
1755
+ function renderAsyncOption(option) {
1756
+ return /* @__PURE__ */ jsx(DropdownOption, { value: option.value, label: option.label, icon: option.icon, disabled: option.disabled }, option.value);
1757
+ }
1758
+ function renderAsyncRows() {
1759
+ if (asyncStatus === "loading") {
1760
+ return /* @__PURE__ */ jsx("div", { className: "vpg-listbox-empty", children: loadingMessage });
1761
+ }
1762
+ if (asyncStatus === "error") {
1763
+ return /* @__PURE__ */ jsx("div", { className: "vpg-listbox-empty", children: errorMessage });
1764
+ }
1765
+ if (matches.length === 0) {
1766
+ return /* @__PURE__ */ jsx("div", { className: "vpg-listbox-empty", children: "No results" });
1767
+ }
1768
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1769
+ matches.filter((option) => option.group === void 0).map(renderAsyncOption),
1770
+ visibleGroups.map((group) => /* @__PURE__ */ jsx(DropdownGroup, { label: group, children: matches.filter((option) => option.group === group).map(renderAsyncOption) }, group))
1771
+ ] });
1772
+ }
1773
+ const optionRows = isAsync ? renderAsyncRows() : searchable && matches.length === 0 ? /* @__PURE__ */ jsx("div", { className: "vpg-listbox-empty", children: "No results" }) : children;
1774
+ function renderSearchPanel() {
1775
+ return /* @__PURE__ */ jsx("div", { ref: refs.setFloating, className: "vpg-listbox-panel", style: floatingStyles, children: /* @__PURE__ */ jsx(FloatingFocusManager, { context: floatingContext, modal: false, initialFocus: searchRef, children: /* @__PURE__ */ jsxs("div", { children: [
1776
+ /* @__PURE__ */ jsxs("div", { className: "vpg-listbox-search", children: [
1777
+ /* @__PURE__ */ jsx(Search, { className: "vpg-listbox-search-icon", "aria-hidden": "true" }),
1778
+ /* @__PURE__ */ jsx(
1779
+ "input",
1780
+ {
1781
+ ref: searchRef,
1782
+ type: "text",
1783
+ role: "combobox",
1784
+ "aria-expanded": true,
1785
+ className: "vpg-listbox-search-input",
1786
+ autoComplete: "off",
1787
+ placeholder: searchPlaceholder,
1788
+ "aria-label": searchPlaceholder,
1789
+ value: query,
1790
+ ...getSearchProps({ onChange: handleSearchChange, onKeyDown: handleSearchKeyDown })
1791
+ }
1792
+ )
1793
+ ] }),
1794
+ /* @__PURE__ */ jsx(
1795
+ "div",
1796
+ {
1797
+ role: "listbox",
1798
+ className: "vpg-listbox-options",
1799
+ "aria-multiselectable": multiple ? true : void 0,
1800
+ ...getFloatingProps(),
1801
+ children: optionRows
1802
+ }
1803
+ )
1804
+ ] }) }) });
1805
+ }
1806
+ function renderFloating() {
1807
+ if (searchable) {
1808
+ return renderSearchPanel();
1809
+ }
1810
+ return /* @__PURE__ */ jsx(
1811
+ "div",
1812
+ {
1813
+ ref: refs.setFloating,
1814
+ role: "listbox",
1815
+ className: "vpg-listbox",
1816
+ style: floatingStyles,
1817
+ "aria-multiselectable": multiple ? true : void 0,
1818
+ ...getFloatingProps(),
1819
+ children: optionRows
1820
+ }
1821
+ );
1822
+ }
1823
+ const listbox = open ? renderFloating() : null;
1824
+ return /* @__PURE__ */ jsxs(DropdownContext.Provider, { value: context, children: [
1825
+ /* @__PURE__ */ jsx("style", { href: "vpg-dropdown", precedence: "vpg-dropdown", children: dropdownStylesheet }),
1826
+ /* @__PURE__ */ jsx("style", { href: "vpg-listbox", precedence: "vpg-listbox", children: listboxStylesheet }),
1827
+ /* @__PURE__ */ jsxs("div", { className: ["vpg-dropdown", className].filter(Boolean).join(" "), children: [
1828
+ /* @__PURE__ */ jsxs(
1829
+ FieldShell,
1830
+ {
1831
+ ref: fieldRef,
1832
+ className: "vpg-dropdown-control",
1833
+ onMouseDown: onFieldMouseDown,
1834
+ trailing: showClear ? renderClearButton() : void 0,
1835
+ children: [
1836
+ multiple && selectedEntries.length > 0 ? /* @__PURE__ */ jsxs("span", { ref: chipsRef, className: "vpg-listbox-chips", "data-collapsing": collapseChips ? "" : void 0, children: [
1837
+ selectedEntries.map((selected, index) => /* @__PURE__ */ jsxs("span", { className: "vpg-listbox-chip", "data-hidden": index >= visibleChipCount ? "" : void 0, children: [
1838
+ /* @__PURE__ */ jsx("span", { className: "vpg-listbox-chip-label", children: selected.label }),
1839
+ /* @__PURE__ */ jsx(
1840
+ "button",
1841
+ {
1842
+ type: "button",
1843
+ className: "vpg-listbox-chip-remove",
1844
+ "aria-label": `Remove ${selected.label}`,
1845
+ onClick: () => {
1846
+ remove(selected.value);
1847
+ },
1848
+ children: /* @__PURE__ */ jsx(X, { className: "vpg-listbox-chip-remove-icon", "aria-hidden": "true" })
1849
+ }
1850
+ )
1851
+ ] }, selected.value)),
1852
+ collapseChips ? renderOverflowIndicator() : null
1853
+ ] }) : null,
1854
+ /* @__PURE__ */ jsxs(
1855
+ "div",
1856
+ {
1857
+ ref: refs.setReference,
1858
+ role: "combobox",
1859
+ "aria-expanded": open,
1860
+ className: "vpg-dropdown-trigger",
1861
+ tabIndex: 0,
1862
+ id,
1863
+ "aria-label": ariaLabel,
1864
+ "aria-labelledby": ariaLabelledBy,
1865
+ "aria-describedby": describedBy === "" ? void 0 : describedBy,
1866
+ "aria-invalid": ariaInvalid,
1867
+ ...getReferenceProps({ onKeyDown: handleTriggerKeyDown }),
1868
+ children: [
1869
+ renderTriggerContent(),
1870
+ /* @__PURE__ */ jsx(ChevronDown, { size: 16, className: "vpg-dropdown-chevron", "aria-hidden": "true" })
1871
+ ]
1872
+ }
1873
+ )
1874
+ ]
1875
+ }
1876
+ ),
1877
+ describesSelection ? /* @__PURE__ */ jsxs("span", { id: selectionDescriptionId, className: "vpg-dropdown-selection-description", children: [
1878
+ "Selected: ",
1879
+ selectedEntries.map((selected) => selected.label).join(", ")
1880
+ ] }) : null
1881
+ ] }),
1882
+ listbox !== null && themeRoot !== null ? createPortal(listbox, themeRoot) : listbox
1883
+ ] });
1884
+ }
1885
+ var Dropdown = /* @__PURE__ */ Object.assign(DropdownImpl, {
1886
+ Option: DropdownOption,
1887
+ Group: DropdownGroup
1888
+ });
1889
+
1890
+ // src/FieldSet/FieldSet.stylesheet.ts
1891
+ var fieldSetStylesheet = `
1892
+ .vpg-fieldset {
1893
+ box-sizing: border-box;
1894
+ margin: 0;
1895
+ padding: var(--vpg-space-5);
1896
+ background-color: var(--vpg-surface-raised);
1897
+ border: 1px solid var(--vpg-border);
1898
+ border-radius: var(--vpg-radius-lg);
1899
+ }
1900
+
1901
+ .vpg-fieldset:disabled {
1902
+ opacity: 0.55;
1903
+ }
1904
+
1905
+ .vpg-fieldset > *:not(.vpg-fieldset-legend) ~ *:not(.vpg-fieldset-legend) {
1906
+ margin-top: var(--vpg-space-5);
1907
+ }
1908
+
1909
+ .vpg-fieldset-legend {
1910
+ padding: 0;
1911
+ margin: 0;
1912
+ font-size: var(--vpg-font-size-sm);
1913
+ font-weight: 600;
1914
+ color: var(--vpg-ink);
1915
+ }
1916
+ `;
1917
+ function FieldSet({ legend, children, className, ...rest }) {
1918
+ const classes = ["vpg-fieldset", className].filter(Boolean).join(" ");
1919
+ return /* @__PURE__ */ jsxs("fieldset", { ...rest, className: classes, children: [
1920
+ /* @__PURE__ */ jsx("style", { href: "vpg-fieldset", precedence: "vpg-fieldset", children: fieldSetStylesheet }),
1921
+ /* @__PURE__ */ jsx("legend", { className: "vpg-fieldset-legend", children: legend }),
1922
+ children
1923
+ ] });
1924
+ }
1925
+
1926
+ // src/FormField/FormField.stylesheet.ts
1927
+ var formFieldStylesheet = `
1928
+ .vpg-form-field {
1929
+ display: flex;
1930
+ flex-direction: column;
1931
+ }
1932
+
1933
+ .vpg-form-field-label {
1934
+ margin: 0 0 var(--vpg-space-2);
1935
+ font-size: var(--vpg-font-size-sm);
1936
+ font-weight: 500;
1937
+ color: var(--vpg-ink);
1938
+ }
1939
+
1940
+ .vpg-form-field-hint {
1941
+ margin-top: var(--vpg-space-1);
1942
+ font-size: var(--vpg-font-size-xs);
1943
+ color: var(--vpg-ink-muted);
1944
+ }
1945
+
1946
+ .vpg-form-field-error {
1947
+ margin-top: var(--vpg-space-1);
1948
+ font-size: var(--vpg-font-size-xs);
1949
+ color: var(--vpg-danger);
1950
+ }
1951
+ `;
1952
+ function FormField({ label, hint, error, children }) {
1953
+ const generatedId = useId();
1954
+ const labelId = `${generatedId}-label`;
1955
+ const hintId = `${generatedId}-hint`;
1956
+ const errorId = `${generatedId}-error`;
1957
+ if (!isValidElement(children) || children.type === Fragment$1) {
1958
+ throw new Error("FormField requires exactly one focusable element as its child.");
1959
+ }
1960
+ const hasHint = hint !== void 0 && hint !== null && hint !== "";
1961
+ const hasError = error !== void 0 && error !== null && error !== "";
1962
+ const describedByIds = [hasHint ? hintId : null, hasError ? errorId : null].filter((id) => id !== null);
1963
+ const mergedDescribedBy = [children.props["aria-describedby"], ...describedByIds].filter(Boolean).join(" ");
1964
+ const childId = children.props.id ?? generatedId;
1965
+ const cloneProps = {
1966
+ id: childId,
1967
+ "aria-labelledby": labelId
1968
+ };
1969
+ if (mergedDescribedBy) {
1970
+ cloneProps["aria-describedby"] = mergedDescribedBy;
1971
+ }
1972
+ if (hasError) {
1973
+ cloneProps["aria-invalid"] = true;
1974
+ }
1975
+ const control = cloneElement(children, cloneProps);
1976
+ return /* @__PURE__ */ jsxs("div", { className: "vpg-form-field", children: [
1977
+ /* @__PURE__ */ jsx("style", { href: "vpg-form-field", precedence: "vpg-form-field", children: formFieldStylesheet }),
1978
+ /* @__PURE__ */ jsx("label", { htmlFor: childId, id: labelId, className: "vpg-form-field-label", children: label }),
1979
+ control,
1980
+ hasHint ? /* @__PURE__ */ jsx("span", { id: hintId, className: "vpg-form-field-hint", children: hint }) : null,
1981
+ hasError ? /* @__PURE__ */ jsx("span", { id: errorId, className: "vpg-form-field-error", children: error }) : null
1982
+ ] });
1983
+ }
1984
+
1985
+ // src/TextField/TextField.stylesheet.ts
1986
+ var textFieldStylesheet = `
1987
+ .vpg-text-field {
1988
+ box-sizing: border-box;
1989
+ align-self: stretch;
1990
+ min-width: 0;
1991
+ padding: 0;
1992
+ border: none;
1993
+ outline: none;
1994
+ background: none;
1995
+ color: inherit;
1996
+ font-family: var(--vpg-font-sans);
1997
+ font-size: var(--vpg-font-size-sm);
1998
+ line-height: 1.5;
1999
+ }
2000
+
2001
+ .vpg-text-field::placeholder {
2002
+ color: var(--vpg-ink-subtle);
2003
+ }
2004
+ `;
2005
+ function TextField({ className, type = "text", leading, trailing, ...rest }) {
2006
+ const classes = ["vpg-text-field", className].filter(Boolean).join(" ");
2007
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2008
+ /* @__PURE__ */ jsx("style", { href: "vpg-text-field", precedence: "vpg-text-field", children: textFieldStylesheet }),
2009
+ /* @__PURE__ */ jsx(FieldShell, { leading, trailing, children: /* @__PURE__ */ jsx("input", { type, ...rest, className: classes }) })
2010
+ ] });
2011
+ }
2012
+
2013
+ // src/PasswordInput/PasswordInput.stylesheet.ts
2014
+ var passwordInputStylesheet = `
2015
+ .vpg-password-input-toggle {
2016
+ display: inline-flex;
2017
+ align-items: center;
2018
+ justify-content: center;
2019
+ box-sizing: border-box;
2020
+ border: none;
2021
+ border-radius: var(--vpg-radius-sm);
2022
+ padding: 0 var(--vpg-space-1);
2023
+ margin: 0;
2024
+ background: none;
2025
+ color: var(--vpg-ink-muted);
2026
+ font-family: inherit;
2027
+ font-size: var(--vpg-font-size-xs);
2028
+ line-height: 1;
2029
+ cursor: pointer;
2030
+ transition: color var(--vpg-duration-fast) var(--vpg-ease-standard);
2031
+ }
2032
+
2033
+ .vpg-password-input-toggle:hover:not(:disabled) {
2034
+ color: var(--vpg-ink);
2035
+ }
2036
+
2037
+ .vpg-password-input-toggle:focus-visible {
2038
+ outline: var(--vpg-focus-ring-width) solid var(--vpg-accent-ring);
2039
+ outline-offset: var(--vpg-focus-ring-offset);
2040
+ }
2041
+
2042
+ .vpg-password-input-toggle:disabled {
2043
+ cursor: not-allowed;
2044
+ }
2045
+ `;
2046
+ function PasswordInput({ className, disabled, ...rest }) {
2047
+ const [revealed, setRevealed] = useState(false);
2048
+ const label = revealed ? "Hide password" : "Show password";
2049
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2050
+ /* @__PURE__ */ jsx("style", { href: "vpg-password-input", precedence: "vpg-password-input", children: passwordInputStylesheet }),
2051
+ /* @__PURE__ */ jsx(
2052
+ TextField,
2053
+ {
2054
+ ...rest,
2055
+ className,
2056
+ disabled,
2057
+ type: revealed ? "text" : "password",
2058
+ trailing: /* @__PURE__ */ jsx(
2059
+ "button",
2060
+ {
2061
+ type: "button",
2062
+ className: "vpg-password-input-toggle",
2063
+ "aria-pressed": revealed,
2064
+ "aria-label": label,
2065
+ disabled,
2066
+ onClick: () => setRevealed((current) => !current),
2067
+ children: revealed ? "Hide" : "Show"
2068
+ }
2069
+ )
2070
+ }
2071
+ )
2072
+ ] });
2073
+ }
2074
+
2075
+ // src/Popover/Popover.stylesheet.ts
2076
+ var popoverStylesheet = `
2077
+ .vpg-popover-trigger {
2078
+ /* Inline-flex rather than inline: the wrapper exists only to carry the ref and the click
2079
+ handler, and must not add a baseline gap under the element it wraps. */
2080
+ display: inline-flex;
2081
+ }
2082
+
2083
+ .vpg-popover {
2084
+ position: absolute;
2085
+ z-index: var(--vpg-layer-popover);
2086
+ box-sizing: border-box;
2087
+ /* The size of a container, not a step of anything: no scale carries a measurement this large,
2088
+ and a \`--vpg-*\` name the theme never assigns advertises a theming hook that doesn't
2089
+ exist. */
2090
+ max-width: 20rem;
2091
+ padding: 1rem;
2092
+ background-color: var(--vpg-surface-raised);
2093
+ border: 1px solid var(--vpg-border);
2094
+ border-radius: var(--vpg-radius);
2095
+ box-shadow: var(--vpg-shadow-high);
2096
+ color: var(--vpg-ink);
2097
+ font-family: var(--vpg-font-sans);
2098
+ font-size: var(--vpg-font-size-sm);
2099
+ line-height: 1.5;
2100
+ /* The panel holds interactive content, so unlike a tooltip it must stay hit-testable. */
2101
+ overflow-wrap: break-word;
2102
+ }
2103
+
2104
+ .vpg-popover:focus-visible {
2105
+ /* The panel itself takes focus when it holds nothing tabbable; the ring is what makes that
2106
+ visible. Offset rather than inset so it stays legible against the panel's own surface. */
2107
+ outline: var(--vpg-focus-ring-width) solid var(--vpg-accent-ring);
2108
+ outline-offset: var(--vpg-focus-ring-offset);
2109
+ }
2110
+ `;
2111
+ var POPOVER_OFFSET = 10;
2112
+ var VIEWPORT_PADDING3 = 12;
2113
+ function Popover({ content, children, open, defaultOpen = false, onOpenChange, placement = "bottom", className }) {
2114
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
2115
+ const isOpen = open ?? uncontrolledOpen;
2116
+ const handleOpenChange = (next) => {
2117
+ if (open === void 0) {
2118
+ setUncontrolledOpen(next);
2119
+ }
2120
+ onOpenChange?.(next);
2121
+ };
2122
+ const { refs, floatingStyles, context, elements } = useFloating({
2123
+ open: isOpen,
2124
+ onOpenChange: handleOpenChange,
2125
+ placement,
2126
+ middleware: [offset(POPOVER_OFFSET), flip(), shift({ padding: VIEWPORT_PADDING3 })],
2127
+ whileElementsMounted: autoUpdate
2128
+ });
2129
+ const click = useClick(context);
2130
+ const dismiss = useDismiss(context);
2131
+ const dialogRole = useRole(context, { role: "dialog" });
2132
+ const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, dialogRole]);
2133
+ const {
2134
+ "aria-haspopup": ariaHaspopup,
2135
+ "aria-expanded": ariaExpanded,
2136
+ "aria-controls": ariaControls,
2137
+ ...referenceProps
2138
+ } = getReferenceProps();
2139
+ const hasSingleElementChild = isValidElement(children) && children.type !== Fragment$1;
2140
+ const trigger = hasSingleElementChild ? cloneElement(children, {
2141
+ "aria-haspopup": ariaHaspopup,
2142
+ "aria-expanded": ariaExpanded,
2143
+ "aria-controls": ariaControls
2144
+ }) : children;
2145
+ const panel = isOpen ? /* @__PURE__ */ jsx(FloatingFocusManager, { context, modal: true, children: /* @__PURE__ */ jsx(
2146
+ "div",
2147
+ {
2148
+ ref: refs.setFloating,
2149
+ className: ["vpg-popover", className].filter(Boolean).join(" "),
2150
+ style: floatingStyles,
2151
+ ...getFloatingProps(),
2152
+ children: content
2153
+ }
2154
+ ) }) : null;
2155
+ const themeRoot = elements.domReference?.closest(".vpg-root") ?? null;
2156
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2157
+ /* @__PURE__ */ jsx("style", { href: "vpg-popover", precedence: "vpg-popover", children: popoverStylesheet }),
2158
+ /* @__PURE__ */ jsx(
2159
+ "span",
2160
+ {
2161
+ ref: refs.setReference,
2162
+ className: "vpg-popover-trigger",
2163
+ "aria-haspopup": hasSingleElementChild ? void 0 : ariaHaspopup,
2164
+ "aria-expanded": hasSingleElementChild ? void 0 : ariaExpanded,
2165
+ "aria-controls": hasSingleElementChild ? void 0 : ariaControls,
2166
+ ...referenceProps,
2167
+ children: trigger
2168
+ }
2169
+ ),
2170
+ panel !== null && themeRoot !== null ? createPortal(panel, themeRoot) : panel
2171
+ ] });
2172
+ }
2173
+
2174
+ // src/Progress/Progress.stylesheet.ts
2175
+ var progressStylesheet = `
2176
+ @keyframes vpg-progress-sweep {
2177
+ 0% { left: -40%; width: 40%; }
2178
+ 50% { width: 60%; }
2179
+ 100% { left: 100%; width: 40%; }
2180
+ }
2181
+
2182
+ .vpg-progress {
2183
+ position: relative;
2184
+ display: block;
2185
+ overflow: hidden;
2186
+ box-sizing: border-box;
2187
+ border-radius: var(--vpg-radius-sm);
2188
+ background-color: var(--vpg-surface-raised);
2189
+ }
2190
+
2191
+ .vpg-progress-sm {
2192
+ height: 0.25rem;
2193
+ }
2194
+
2195
+ .vpg-progress-md {
2196
+ height: 0.5rem;
2197
+ }
2198
+
2199
+ .vpg-progress-lg {
2200
+ height: 0.75rem;
2201
+ }
2202
+
2203
+ .vpg-progress-fill {
2204
+ height: 100%;
2205
+ border-radius: inherit;
2206
+ background-color: var(--vpg-accent);
2207
+ }
2208
+
2209
+ .vpg-progress-determinate .vpg-progress-fill {
2210
+ transition: width 0.2s ease-out;
2211
+ }
2212
+
2213
+ .vpg-progress-indeterminate .vpg-progress-fill {
2214
+ position: absolute;
2215
+ top: 0;
2216
+ animation: vpg-progress-sweep 1.5s ease-in-out infinite;
2217
+ }
2218
+
2219
+ @media (prefers-reduced-motion: reduce) {
2220
+ /* Slowed, not stopped: a bar that never moves at all reports nothing about the wait it exists
2221
+ to report. */
2222
+ .vpg-progress-indeterminate .vpg-progress-fill {
2223
+ animation-duration: 3s;
2224
+ }
2225
+ }
2226
+ `;
2227
+ function clamp(value, max) {
2228
+ if (value < 0) return 0;
2229
+ if (value > max) return max;
2230
+ return value;
2231
+ }
2232
+ function Progress({ value, max = 100, size: size2 = "md", className, style, ...rest }) {
2233
+ const determinate = value !== void 0;
2234
+ const percentage = determinate ? max <= 0 ? 100 : clamp(value, max) / max * 100 : void 0;
2235
+ const classes = [
2236
+ "vpg-progress",
2237
+ `vpg-progress-${size2}`,
2238
+ determinate ? "vpg-progress-determinate" : "vpg-progress-indeterminate",
2239
+ className
2240
+ ].filter(Boolean).join(" ");
2241
+ const labelling = determinate ? { "aria-valuenow": clamp(value, max), "aria-valuemin": 0, "aria-valuemax": max } : void 0;
2242
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2243
+ /* @__PURE__ */ jsx("style", { href: "vpg-progress", precedence: "vpg-progress", children: progressStylesheet }),
2244
+ /* @__PURE__ */ jsx("div", { ...rest, role: "progressbar", ...labelling, className: classes, style, children: /* @__PURE__ */ jsx("div", { className: "vpg-progress-fill", style: determinate ? { width: `${percentage}%` } : void 0 }) })
2245
+ ] });
2246
+ }
2247
+
2248
+ // src/RadioGroup/RadioGroup.stylesheet.ts
2249
+ var radioGroupStylesheet = `
2250
+ .vpg-radio-group {
2251
+ display: flex;
2252
+ flex-direction: column;
2253
+ gap: 0.5rem;
2254
+ }
2255
+ `;
2256
+ var RadioGroupContext = createContext(null);
2257
+ function useRadioGroupContext() {
2258
+ return useContext(RadioGroupContext);
2259
+ }
2260
+ function RadioGroup({ value, defaultValue, onChange, name, className, children, ...rest }) {
2261
+ const generatedName = useId();
2262
+ const resolvedName = name ?? generatedName;
2263
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
2264
+ const controlled = value !== void 0;
2265
+ const selectedValue = controlled ? value : uncontrolledValue;
2266
+ const classes = ["vpg-radio-group", className].filter(Boolean).join(" ");
2267
+ const context = useMemo(
2268
+ () => ({
2269
+ name: resolvedName,
2270
+ value: selectedValue,
2271
+ onChange(next) {
2272
+ if (!controlled) {
2273
+ setUncontrolledValue(next);
2274
+ }
2275
+ onChange?.(next);
2276
+ }
2277
+ }),
2278
+ [resolvedName, selectedValue, controlled, onChange]
2279
+ );
2280
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2281
+ /* @__PURE__ */ jsx("style", { href: "vpg-radio-group", precedence: "vpg-radio-group", children: radioGroupStylesheet }),
2282
+ /* @__PURE__ */ jsx(RadioGroupContext.Provider, { value: context, children: /* @__PURE__ */ jsx("div", { ...rest, role: "radiogroup", className: classes, children }) })
2283
+ ] });
2284
+ }
2285
+
2286
+ // src/RadioButton/RadioButton.stylesheet.ts
2287
+ var radioButtonStylesheet = `
2288
+ .vpg-radio-button {
2289
+ appearance: none;
2290
+ -webkit-appearance: none;
2291
+ box-sizing: border-box;
2292
+ position: relative;
2293
+ display: inline-block;
2294
+ flex: none;
2295
+ width: var(--vpg-icon-md);
2296
+ height: var(--vpg-icon-md);
2297
+ margin: 0;
2298
+ padding: 0;
2299
+ border: 1px solid var(--vpg-border);
2300
+ border-radius: var(--vpg-radius-full);
2301
+ background-color: var(--vpg-surface);
2302
+ cursor: pointer;
2303
+ transition: border-color 120ms ease;
2304
+ }
2305
+
2306
+ .vpg-radio-button::before {
2307
+ content: "";
2308
+ position: absolute;
2309
+ inset: 0;
2310
+ margin: auto;
2311
+ width: calc(var(--vpg-icon-md) * 0.5);
2312
+ height: calc(var(--vpg-icon-md) * 0.5);
2313
+ border-radius: var(--vpg-radius-full);
2314
+ background-color: var(--vpg-accent);
2315
+ transform: scale(0);
2316
+ transition: transform 120ms ease;
2317
+ }
2318
+
2319
+ .vpg-radio-button:checked {
2320
+ border-color: var(--vpg-accent);
2321
+ }
2322
+
2323
+ .vpg-radio-button:checked::before {
2324
+ transform: scale(1);
2325
+ }
2326
+
2327
+ .vpg-radio-button:focus-visible {
2328
+ outline: var(--vpg-focus-ring-width) solid var(--vpg-accent-ring);
2329
+ outline-offset: var(--vpg-focus-ring-offset);
2330
+ }
2331
+
2332
+ .vpg-radio-button:disabled {
2333
+ cursor: not-allowed;
2334
+ opacity: 0.55;
2335
+ }
2336
+ `;
2337
+ function RadioButton({ className, name, checked, onChange, value, ...rest }) {
2338
+ const context = useRadioGroupContext();
2339
+ const manualChecked = checked !== void 0;
2340
+ const resolvedName = name ?? context?.name;
2341
+ const resolvedChecked = manualChecked ? checked : context ? value !== void 0 && context.value === value : void 0;
2342
+ function handleChange(event) {
2343
+ onChange?.(event);
2344
+ if (!manualChecked && context && value !== void 0) {
2345
+ context.onChange(value);
2346
+ }
2347
+ }
2348
+ const classes = ["vpg-radio-button", className].filter(Boolean).join(" ");
2349
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2350
+ /* @__PURE__ */ jsx("style", { href: "vpg-radio-button", precedence: "vpg-radio-button", children: radioButtonStylesheet }),
2351
+ /* @__PURE__ */ jsx(
2352
+ "input",
2353
+ {
2354
+ ...rest,
2355
+ type: "radio",
2356
+ name: resolvedName,
2357
+ value,
2358
+ checked: resolvedChecked,
2359
+ onChange: handleChange,
2360
+ className: classes
2361
+ }
2362
+ )
2363
+ ] });
2364
+ }
2365
+
2366
+ // src/Skeleton/Skeleton.stylesheet.ts
2367
+ var skeletonStylesheet = `
2368
+ @keyframes vpg-skeleton-shimmer {
2369
+ 0% { background-position: 200% 0; }
2370
+ 100% { background-position: -200% 0; }
2371
+ }
2372
+
2373
+ .vpg-skeleton {
2374
+ display: block;
2375
+ flex: none;
2376
+ box-sizing: border-box;
2377
+ background-image: linear-gradient(
2378
+ 90deg,
2379
+ oklch(from var(--vpg-surface) calc(l + var(--vpg-lift) * 2.5) c h) 25%,
2380
+ oklch(from var(--vpg-surface) calc(l - var(--vpg-sink) * 1.5) c h) 50%,
2381
+ oklch(from var(--vpg-surface) calc(l + var(--vpg-lift) * 2.5) c h) 75%
2382
+ );
2383
+ background-size: 200% 100%;
2384
+ animation: vpg-skeleton-shimmer 1.5s ease-in-out infinite;
2385
+ }
2386
+
2387
+ .vpg-skeleton-text {
2388
+ width: 100%;
2389
+ height: 1em;
2390
+ border-radius: var(--vpg-radius-sm);
2391
+ }
2392
+
2393
+ .vpg-skeleton-rect {
2394
+ width: 100%;
2395
+ height: 1.25rem;
2396
+ border-radius: var(--vpg-radius);
2397
+ }
2398
+
2399
+ .vpg-skeleton-circle {
2400
+ width: 2.5rem;
2401
+ /* Falls back to a square only when the caller sets a width but no height, so a single
2402
+ \`width\` prop is enough to draw a circle without a matching \`height\`. Explicit \`height\`
2403
+ (set inline, per-instance) still wins over this and sizes the other axis. */
2404
+ aspect-ratio: 1;
2405
+ border-radius: 50%;
2406
+ }
2407
+
2408
+ @media (prefers-reduced-motion: reduce) {
2409
+ /* Slowed, not stopped: a placeholder that never moves at all reports nothing about the wait
2410
+ it exists to report. */
2411
+ .vpg-skeleton {
2412
+ animation-duration: 3s;
2413
+ }
2414
+ }
2415
+ `;
2416
+ function toDimension(value) {
2417
+ if (value === void 0) return void 0;
2418
+ return typeof value === "number" ? `${value}px` : value;
2419
+ }
2420
+ function Skeleton({ variant = "text", width, height, className, style, ...rest }) {
2421
+ const classes = ["vpg-skeleton", `vpg-skeleton-${variant}`, className].filter(Boolean).join(" ");
2422
+ const dimensions = {
2423
+ width: toDimension(width),
2424
+ height: toDimension(height)
2425
+ };
2426
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2427
+ /* @__PURE__ */ jsx("style", { href: "vpg-skeleton", precedence: "vpg-skeleton", children: skeletonStylesheet }),
2428
+ /* @__PURE__ */ jsx("div", { ...rest, "aria-hidden": "true", className: classes, style: { ...dimensions, ...style } })
2429
+ ] });
2430
+ }
2431
+
2432
+ // src/Slider/Slider.stylesheet.ts
2433
+ var sliderStylesheet = `
2434
+ .vpg-slider {
2435
+ appearance: none;
2436
+ -webkit-appearance: none;
2437
+ box-sizing: border-box;
2438
+ display: block;
2439
+ width: 100%;
2440
+ margin: 0;
2441
+ padding: 0;
2442
+ background: transparent;
2443
+ cursor: pointer;
2444
+ }
2445
+
2446
+ .vpg-slider::-webkit-slider-runnable-track {
2447
+ box-sizing: border-box;
2448
+ width: 100%;
2449
+ height: var(--vpg-space-1);
2450
+ border-radius: var(--vpg-radius-full);
2451
+ background-color: var(--vpg-surface-sunken);
2452
+ border: 1px solid var(--vpg-border);
2453
+ }
2454
+
2455
+ .vpg-slider::-moz-range-track {
2456
+ box-sizing: border-box;
2457
+ width: 100%;
2458
+ height: var(--vpg-space-1);
2459
+ border-radius: var(--vpg-radius-full);
2460
+ background-color: var(--vpg-surface-sunken);
2461
+ border: 1px solid var(--vpg-border);
2462
+ }
2463
+
2464
+ .vpg-slider::-webkit-slider-thumb {
2465
+ -webkit-appearance: none;
2466
+ box-sizing: border-box;
2467
+ width: var(--vpg-icon-md);
2468
+ height: var(--vpg-icon-md);
2469
+ margin-top: calc((var(--vpg-space-1) - var(--vpg-icon-md)) / 2);
2470
+ border-radius: var(--vpg-radius-full);
2471
+ border: 1px solid var(--vpg-accent);
2472
+ background-color: var(--vpg-accent);
2473
+ cursor: pointer;
2474
+ transition: background-color 120ms ease, border-color 120ms ease;
2475
+ }
2476
+
2477
+ .vpg-slider::-moz-range-thumb {
2478
+ box-sizing: border-box;
2479
+ width: var(--vpg-icon-md);
2480
+ height: var(--vpg-icon-md);
2481
+ border-radius: var(--vpg-radius-full);
2482
+ border: 1px solid var(--vpg-accent);
2483
+ background-color: var(--vpg-accent);
2484
+ cursor: pointer;
2485
+ transition: background-color 120ms ease, border-color 120ms ease;
2486
+ }
2487
+
2488
+ .vpg-slider:focus-visible::-webkit-slider-thumb {
2489
+ outline: var(--vpg-focus-ring-width) solid var(--vpg-accent-ring);
2490
+ outline-offset: var(--vpg-focus-ring-offset);
2491
+ }
2492
+
2493
+ .vpg-slider:focus-visible::-moz-range-thumb {
2494
+ outline: var(--vpg-focus-ring-width) solid var(--vpg-accent-ring);
2495
+ outline-offset: var(--vpg-focus-ring-offset);
2496
+ }
2497
+
2498
+ .vpg-slider:disabled {
2499
+ cursor: not-allowed;
2500
+ opacity: 0.55;
2501
+ }
2502
+
2503
+ .vpg-slider:disabled::-webkit-slider-thumb {
2504
+ cursor: not-allowed;
2505
+ }
2506
+
2507
+ .vpg-slider:disabled::-moz-range-thumb {
2508
+ cursor: not-allowed;
2509
+ }
2510
+ `;
2511
+ function Slider({ className, ...rest }) {
2512
+ const classes = ["vpg-slider", className].filter(Boolean).join(" ");
2513
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2514
+ /* @__PURE__ */ jsx("style", { href: "vpg-slider", precedence: "vpg-slider", children: sliderStylesheet }),
2515
+ /* @__PURE__ */ jsx("input", { type: "range", ...rest, className: classes })
2516
+ ] });
2517
+ }
2518
+
2519
+ // src/Tabs/Tabs.stylesheet.ts
2520
+ var tabsStylesheet = `
2521
+ .vpg-tabs {
2522
+ display: flex;
2523
+ flex-direction: column;
2524
+ color: var(--vpg-ink);
2525
+ font-family: var(--vpg-font-sans);
2526
+ }
2527
+
2528
+ /* A vertical tablist sits beside its panel, not above it. */
2529
+ .vpg-tabs-vertical {
2530
+ flex-direction: row;
2531
+ }
2532
+
2533
+ .vpg-tabs-list {
2534
+ display: flex;
2535
+ }
2536
+
2537
+ .vpg-tabs-list-horizontal {
2538
+ flex-direction: row;
2539
+ border-bottom: 1px solid var(--vpg-border);
2540
+ }
2541
+
2542
+ .vpg-tabs-list-vertical {
2543
+ flex-direction: column;
2544
+ border-right: 1px solid var(--vpg-border);
2545
+ }
2546
+
2547
+ .vpg-tabs-tab {
2548
+ appearance: none;
2549
+ background: none;
2550
+ cursor: pointer;
2551
+ box-sizing: border-box;
2552
+ padding: 0.5rem 0.75rem;
2553
+ border: none;
2554
+ color: var(--vpg-ink-muted);
2555
+ font: inherit;
2556
+ font-size: var(--vpg-font-size-sm);
2557
+ line-height: 1.5;
2558
+ }
2559
+
2560
+ .vpg-tabs-list-horizontal .vpg-tabs-tab {
2561
+ border-bottom: 2px solid transparent;
2562
+ margin-bottom: -1px;
2563
+ }
2564
+
2565
+ .vpg-tabs-list-vertical .vpg-tabs-tab {
2566
+ text-align: left;
2567
+ border-right: 2px solid transparent;
2568
+ margin-right: -1px;
2569
+ }
2570
+
2571
+ .vpg-tabs-tab:hover:not(:disabled) {
2572
+ color: var(--vpg-ink);
2573
+ background-color: var(--vpg-surface-hover);
2574
+ }
2575
+
2576
+ .vpg-tabs-tab:focus-visible {
2577
+ /* Inset so the ring stays inside the list's own border rather than straddling it. */
2578
+ outline: var(--vpg-focus-ring-width) solid var(--vpg-accent-ring);
2579
+ outline-offset: calc(var(--vpg-focus-ring-offset) * -1);
2580
+ }
2581
+
2582
+ .vpg-tabs-tab:disabled {
2583
+ cursor: not-allowed;
2584
+ color: var(--vpg-ink-subtle);
2585
+ }
2586
+
2587
+ .vpg-tabs-tab-selected {
2588
+ color: var(--vpg-accent);
2589
+ }
2590
+
2591
+ /* Scoped by orientation to outweigh the transparent placeholder border above, which is itself
2592
+ orientation-scoped and would otherwise win on specificity. */
2593
+ .vpg-tabs-list-horizontal .vpg-tabs-tab-selected {
2594
+ border-bottom-color: var(--vpg-accent);
2595
+ }
2596
+
2597
+ .vpg-tabs-list-vertical .vpg-tabs-tab-selected {
2598
+ border-right-color: var(--vpg-accent);
2599
+ }
2600
+
2601
+ .vpg-tabs-panel {
2602
+ padding: 1rem 0;
2603
+ }
2604
+
2605
+ .vpg-tabs-vertical .vpg-tabs-panel {
2606
+ padding: 0 1rem;
2607
+ }
2608
+ `;
2609
+ var TabsContext = createContext(null);
2610
+ var ENABLED_TAB_SELECTOR = '[role="tab"]:not([disabled])';
2611
+ function useTabsContext(component) {
2612
+ const context = useContext(TabsContext);
2613
+ if (context === null) {
2614
+ throw new Error(`${component} must be rendered inside <Tabs>.`);
2615
+ }
2616
+ return context;
2617
+ }
2618
+ function tabId(baseId, value) {
2619
+ return `${baseId}-tab-${value}`;
2620
+ }
2621
+ function panelId(baseId, value) {
2622
+ return `${baseId}-panel-${value}`;
2623
+ }
2624
+ function TabsList({ className, children, onKeyDown, ...rest }) {
2625
+ const { orientation, activate } = useTabsContext("Tabs.List");
2626
+ const classes = ["vpg-tabs-list", `vpg-tabs-list-${orientation}`, className].filter(Boolean).join(" ");
2627
+ function handleKeyDown(event) {
2628
+ onKeyDown?.(event);
2629
+ const [previousKey, nextKey] = orientation === "vertical" ? ["ArrowUp", "ArrowDown"] : ["ArrowLeft", "ArrowRight"];
2630
+ const tabs = Array.from(event.currentTarget.querySelectorAll(ENABLED_TAB_SELECTOR));
2631
+ const current = tabs.indexOf(event.target);
2632
+ let next;
2633
+ if (event.key === nextKey) {
2634
+ next = (current + 1) % tabs.length;
2635
+ } else if (event.key === previousKey) {
2636
+ next = (current - 1 + tabs.length) % tabs.length;
2637
+ } else if (event.key === "Home") {
2638
+ next = 0;
2639
+ } else if (event.key === "End") {
2640
+ next = tabs.length - 1;
2641
+ } else {
2642
+ return;
2643
+ }
2644
+ event.preventDefault();
2645
+ const target = tabs[next];
2646
+ target.focus();
2647
+ activate(target.dataset.value);
2648
+ }
2649
+ return /* @__PURE__ */ jsx("div", { ...rest, role: "tablist", "aria-orientation": orientation, className: classes, onKeyDown: handleKeyDown, children });
2650
+ }
2651
+ function TabsTab({ value, disabled = false, className, children, onClick, ...rest }) {
2652
+ const { activeValue, tabStopValue, activate, baseId } = useTabsContext("Tabs.Tab");
2653
+ const selected = activeValue === value;
2654
+ const classes = ["vpg-tabs-tab", selected ? "vpg-tabs-tab-selected" : "", className].filter(Boolean).join(" ");
2655
+ function handleClick(event) {
2656
+ onClick?.(event);
2657
+ activate(value);
2658
+ }
2659
+ return /* @__PURE__ */ jsx(
2660
+ "button",
2661
+ {
2662
+ ...rest,
2663
+ type: "button",
2664
+ role: "tab",
2665
+ id: tabId(baseId, value),
2666
+ "aria-controls": selected ? panelId(baseId, value) : void 0,
2667
+ "aria-selected": selected,
2668
+ tabIndex: tabStopValue === value ? 0 : -1,
2669
+ disabled,
2670
+ "data-value": value,
2671
+ className: classes,
2672
+ onClick: handleClick,
2673
+ children
2674
+ }
2675
+ );
2676
+ }
2677
+ function TabsPanel({ value, className, children, ...rest }) {
2678
+ const { activeValue, baseId } = useTabsContext("Tabs.Panel");
2679
+ if (activeValue !== value) {
2680
+ return null;
2681
+ }
2682
+ const classes = ["vpg-tabs-panel", className].filter(Boolean).join(" ");
2683
+ return /* @__PURE__ */ jsx(
2684
+ "div",
2685
+ {
2686
+ ...rest,
2687
+ role: "tabpanel",
2688
+ id: panelId(baseId, value),
2689
+ "aria-labelledby": tabId(baseId, value),
2690
+ tabIndex: 0,
2691
+ className: classes,
2692
+ children
2693
+ }
2694
+ );
2695
+ }
2696
+ function firstTabValue(children, skipDisabled = false) {
2697
+ let found;
2698
+ Children.forEach(children, (child) => {
2699
+ if (found !== void 0 || !isValidElement(child)) {
2700
+ return;
2701
+ }
2702
+ if (child.type === TabsTab) {
2703
+ const props = child.props;
2704
+ if (skipDisabled && props.disabled) {
2705
+ return;
2706
+ }
2707
+ found = props.value;
2708
+ return;
2709
+ }
2710
+ found = firstTabValue(child.props.children, skipDisabled);
2711
+ });
2712
+ return found;
2713
+ }
2714
+ function isTabDisabled(children, value) {
2715
+ if (value === void 0) {
2716
+ return false;
2717
+ }
2718
+ let disabled = false;
2719
+ Children.forEach(children, (child) => {
2720
+ if (disabled || !isValidElement(child)) {
2721
+ return;
2722
+ }
2723
+ if (child.type === TabsTab) {
2724
+ const props = child.props;
2725
+ if (props.value === value) {
2726
+ disabled = props.disabled ?? false;
2727
+ }
2728
+ return;
2729
+ }
2730
+ disabled = isTabDisabled(child.props.children, value);
2731
+ });
2732
+ return disabled;
2733
+ }
2734
+ function TabsImpl({ value, defaultValue, onChange, orientation = "horizontal", className, children, ...rest }) {
2735
+ const baseId = useId();
2736
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
2737
+ const controlled = value !== void 0;
2738
+ const activeValue = controlled ? value : uncontrolledValue ?? firstTabValue(children, true) ?? firstTabValue(children);
2739
+ const tabStopValue = isTabDisabled(children, activeValue) ? firstTabValue(children, true) ?? activeValue : activeValue;
2740
+ const classes = ["vpg-tabs", `vpg-tabs-${orientation}`, className].filter(Boolean).join(" ");
2741
+ const context = useMemo(
2742
+ () => ({
2743
+ activeValue,
2744
+ tabStopValue,
2745
+ activate(next) {
2746
+ if (!controlled) {
2747
+ setUncontrolledValue(next);
2748
+ }
2749
+ onChange?.(next);
2750
+ },
2751
+ orientation,
2752
+ baseId
2753
+ }),
2754
+ [activeValue, tabStopValue, controlled, onChange, orientation, baseId]
2755
+ );
2756
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2757
+ /* @__PURE__ */ jsx("style", { href: "vpg-tabs", precedence: "vpg-tabs", children: tabsStylesheet }),
2758
+ /* @__PURE__ */ jsx(TabsContext.Provider, { value: context, children: /* @__PURE__ */ jsx("div", { ...rest, className: classes, children }) })
2759
+ ] });
2760
+ }
2761
+ var Tabs = /* @__PURE__ */ Object.assign(TabsImpl, {
2762
+ List: TabsList,
2763
+ Tab: TabsTab,
2764
+ Panel: TabsPanel
2765
+ });
2766
+
2767
+ // src/Toggle/Toggle.stylesheet.ts
2768
+ var toggleStylesheet = `
2769
+ .vpg-toggle {
2770
+ appearance: none;
2771
+ -webkit-appearance: none;
2772
+ box-sizing: border-box;
2773
+ display: inline-block;
2774
+ flex: none;
2775
+ /* A ratio of the track's own height: the width is a function of thumb travel, so binding it to
2776
+ a control-height step would restretch every switch when that step moves. */
2777
+ width: calc(var(--vpg-icon-lg) * 1.8);
2778
+ height: var(--vpg-icon-lg);
2779
+ margin: 0;
2780
+ padding: 0;
2781
+ border: 1px solid var(--vpg-border);
2782
+ border-radius: var(--vpg-radius-full);
2783
+ background-color: var(--vpg-surface-sunken);
2784
+ cursor: pointer;
2785
+ transition: background-color 120ms ease, border-color 120ms ease;
2786
+ }
2787
+
2788
+ .vpg-toggle::before {
2789
+ content: "";
2790
+ display: block;
2791
+ box-sizing: border-box;
2792
+ width: var(--vpg-icon-sm);
2793
+ height: var(--vpg-icon-sm);
2794
+ margin: 1px;
2795
+ border-radius: var(--vpg-radius-full);
2796
+ background-color: var(--vpg-surface-raised);
2797
+ transform: translateX(0);
2798
+ transition: transform 120ms ease, background-color 120ms ease;
2799
+ }
2800
+
2801
+ .vpg-toggle:checked {
2802
+ background-color: var(--vpg-accent);
2803
+ border-color: var(--vpg-accent);
2804
+ }
2805
+
2806
+ .vpg-toggle:checked::before {
2807
+ background-color: var(--vpg-accent-contrast);
2808
+ transform: translateX(calc(var(--vpg-icon-lg) * 1.8 - var(--vpg-icon-sm) - 4px));
2809
+ }
2810
+
2811
+ .vpg-toggle:focus-visible {
2812
+ outline: var(--vpg-focus-ring-width) solid var(--vpg-accent-ring);
2813
+ outline-offset: var(--vpg-focus-ring-offset);
2814
+ }
2815
+
2816
+ .vpg-toggle:disabled {
2817
+ cursor: not-allowed;
2818
+ opacity: 0.55;
2819
+ }
2820
+ `;
2821
+ function Toggle({ className, ...rest }) {
2822
+ const classes = ["vpg-toggle", className].filter(Boolean).join(" ");
2823
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2824
+ /* @__PURE__ */ jsx("style", { href: "vpg-toggle", precedence: "vpg-toggle", children: toggleStylesheet }),
2825
+ /* @__PURE__ */ jsx("input", { type: "checkbox", role: "switch", ...rest, className: classes })
2826
+ ] });
2827
+ }
2828
+
2829
+ // src/Typography/Typography.stylesheet.ts
2830
+ var typographyStylesheet = `
2831
+ .vpg-typography {
2832
+ margin: 0;
2833
+ font-family: inherit;
2834
+ }
2835
+
2836
+ .vpg-typography-display {
2837
+ font-size: var(--vpg-font-size-5xl);
2838
+ line-height: 1.1;
2839
+ letter-spacing: -0.02em;
2840
+ }
2841
+
2842
+ .vpg-typography-h1 {
2843
+ font-size: var(--vpg-font-size-4xl);
2844
+ line-height: 1.15;
2845
+ letter-spacing: -0.01em;
2846
+ }
2847
+
2848
+ .vpg-typography-h2 {
2849
+ font-size: var(--vpg-font-size-3xl);
2850
+ line-height: 1.2;
2851
+ }
2852
+
2853
+ .vpg-typography-h3 {
2854
+ font-size: var(--vpg-font-size-2xl);
2855
+ line-height: 1.25;
2856
+ }
2857
+
2858
+ .vpg-typography-h4 {
2859
+ font-size: var(--vpg-font-size-xl);
2860
+ line-height: 1.3;
2861
+ }
2862
+
2863
+ .vpg-typography-body-lg {
2864
+ font-size: var(--vpg-font-size-lg);
2865
+ line-height: 1.5;
2866
+ }
2867
+
2868
+ .vpg-typography-body-md {
2869
+ font-size: var(--vpg-font-size-md);
2870
+ line-height: 1.5;
2871
+ }
2872
+
2873
+ .vpg-typography-body-sm {
2874
+ font-size: var(--vpg-font-size-sm);
2875
+ line-height: 1.5;
2876
+ }
2877
+
2878
+ .vpg-typography-caption {
2879
+ font-size: var(--vpg-font-size-xs);
2880
+ line-height: 1.4;
2881
+ }
2882
+
2883
+ .vpg-typography-weight-regular {
2884
+ font-weight: 400;
2885
+ }
2886
+
2887
+ .vpg-typography-weight-medium {
2888
+ font-weight: 500;
2889
+ }
2890
+
2891
+ .vpg-typography-weight-bold {
2892
+ font-weight: 700;
2893
+ }
2894
+
2895
+ .vpg-typography-color-primary {
2896
+ color: var(--vpg-ink);
2897
+ }
2898
+
2899
+ .vpg-typography-color-secondary {
2900
+ color: var(--vpg-ink-muted);
2901
+ }
2902
+
2903
+ .vpg-typography-color-subtle {
2904
+ color: var(--vpg-ink-subtle);
2905
+ }
2906
+
2907
+ .vpg-typography-color-accent {
2908
+ color: var(--vpg-accent);
2909
+ }
2910
+ `;
2911
+ var VARIANT_ELEMENT = {
2912
+ display: "h1",
2913
+ h1: "h1",
2914
+ h2: "h2",
2915
+ h3: "h3",
2916
+ h4: "h4",
2917
+ "body-lg": "p",
2918
+ "body-md": "p",
2919
+ "body-sm": "p",
2920
+ caption: "span"
2921
+ };
2922
+ function Typography({
2923
+ variant = "body-md",
2924
+ weight = "regular",
2925
+ color = "primary",
2926
+ as,
2927
+ className,
2928
+ children,
2929
+ ...rest
2930
+ }) {
2931
+ const Component = as ?? VARIANT_ELEMENT[variant];
2932
+ const classes = [
2933
+ "vpg-typography",
2934
+ `vpg-typography-${variant}`,
2935
+ `vpg-typography-weight-${weight}`,
2936
+ `vpg-typography-color-${color}`,
2937
+ className
2938
+ ].filter(Boolean).join(" ");
2939
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2940
+ /* @__PURE__ */ jsx("style", { href: "vpg-typography", precedence: "vpg-typography", children: typographyStylesheet }),
2941
+ /* @__PURE__ */ jsx(Component, { ...rest, className: classes, children })
2942
+ ] });
2943
+ }
2944
+
2945
+ export { Avatar, Button, ButtonGroup, Card, Dropdown, FieldSet, FieldShell, FormField, PasswordInput, Popover, Progress, RadioButton, RadioGroup, Skeleton, Slider, Spinner, Tabs, TextField, Toggle, Tooltip, Typography };
2946
+ //# sourceMappingURL=index.js.map
2947
+ //# sourceMappingURL=index.js.map