@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.
- package/LICENSE +21 -0
- package/README.md +463 -3
- package/dist/Avatar/Avatar.d.ts +21 -0
- package/dist/Avatar/Avatar.d.ts.map +1 -0
- package/dist/Avatar/Avatar.stylesheet.d.ts +17 -0
- package/dist/Avatar/Avatar.stylesheet.d.ts.map +1 -0
- package/dist/Button/Button.d.ts +39 -0
- package/dist/Button/Button.d.ts.map +1 -0
- package/dist/Button/Button.stylesheet.d.ts +17 -0
- package/dist/Button/Button.stylesheet.d.ts.map +1 -0
- package/dist/ButtonGroup/ButtonGroup.d.ts +16 -0
- package/dist/ButtonGroup/ButtonGroup.d.ts.map +1 -0
- package/dist/ButtonGroup/ButtonGroup.stylesheet.d.ts +12 -0
- package/dist/ButtonGroup/ButtonGroup.stylesheet.d.ts.map +1 -0
- package/dist/Card/Card.d.ts +36 -0
- package/dist/Card/Card.d.ts.map +1 -0
- package/dist/Card/Card.stylesheet.d.ts +17 -0
- package/dist/Card/Card.stylesheet.d.ts.map +1 -0
- package/dist/Dropdown/Dropdown.d.ts +205 -0
- package/dist/Dropdown/Dropdown.d.ts.map +1 -0
- package/dist/Dropdown/Dropdown.stylesheet.d.ts +24 -0
- package/dist/Dropdown/Dropdown.stylesheet.d.ts.map +1 -0
- package/dist/FieldSet/FieldSet.d.ts +22 -0
- package/dist/FieldSet/FieldSet.d.ts.map +1 -0
- package/dist/FieldSet/FieldSet.stylesheet.d.ts +34 -0
- package/dist/FieldSet/FieldSet.stylesheet.d.ts.map +1 -0
- package/dist/FieldShell/FieldShell.d.ts +42 -0
- package/dist/FieldShell/FieldShell.d.ts.map +1 -0
- package/dist/FieldShell/FieldShell.stylesheet.d.ts +32 -0
- package/dist/FieldShell/FieldShell.stylesheet.d.ts.map +1 -0
- package/dist/FormField/FormField.d.ts +45 -0
- package/dist/FormField/FormField.d.ts.map +1 -0
- package/dist/FormField/FormField.stylesheet.d.ts +24 -0
- package/dist/FormField/FormField.stylesheet.d.ts.map +1 -0
- package/dist/PasswordInput/PasswordInput.d.ts +15 -0
- package/dist/PasswordInput/PasswordInput.d.ts.map +1 -0
- package/dist/PasswordInput/PasswordInput.stylesheet.d.ts +24 -0
- package/dist/PasswordInput/PasswordInput.stylesheet.d.ts.map +1 -0
- package/dist/Popover/Popover.d.ts +47 -0
- package/dist/Popover/Popover.d.ts.map +1 -0
- package/dist/Popover/Popover.stylesheet.d.ts +21 -0
- package/dist/Popover/Popover.stylesheet.d.ts.map +1 -0
- package/dist/Progress/Progress.d.ts +18 -0
- package/dist/Progress/Progress.d.ts.map +1 -0
- package/dist/Progress/Progress.stylesheet.d.ts +18 -0
- package/dist/Progress/Progress.stylesheet.d.ts.map +1 -0
- package/dist/RadioButton/RadioButton.d.ts +18 -0
- package/dist/RadioButton/RadioButton.d.ts.map +1 -0
- package/dist/RadioButton/RadioButton.stylesheet.d.ts +15 -0
- package/dist/RadioButton/RadioButton.stylesheet.d.ts.map +1 -0
- package/dist/RadioGroup/RadioGroup.d.ts +40 -0
- package/dist/RadioGroup/RadioGroup.d.ts.map +1 -0
- package/dist/RadioGroup/RadioGroup.stylesheet.d.ts +11 -0
- package/dist/RadioGroup/RadioGroup.stylesheet.d.ts.map +1 -0
- package/dist/Skeleton/Skeleton.d.ts +18 -0
- package/dist/Skeleton/Skeleton.d.ts.map +1 -0
- package/dist/Skeleton/Skeleton.stylesheet.d.ts +25 -0
- package/dist/Skeleton/Skeleton.stylesheet.d.ts.map +1 -0
- package/dist/Slider/Slider.d.ts +12 -0
- package/dist/Slider/Slider.d.ts.map +1 -0
- package/dist/Slider/Slider.stylesheet.d.ts +18 -0
- package/dist/Slider/Slider.stylesheet.d.ts.map +1 -0
- package/dist/Spinner/Spinner.d.ts +19 -0
- package/dist/Spinner/Spinner.d.ts.map +1 -0
- package/dist/Spinner/Spinner.stylesheet.d.ts +14 -0
- package/dist/Spinner/Spinner.stylesheet.d.ts.map +1 -0
- package/dist/Tabs/Tabs.d.ts +46 -0
- package/dist/Tabs/Tabs.d.ts.map +1 -0
- package/dist/Tabs/Tabs.stylesheet.d.ts +15 -0
- package/dist/Tabs/Tabs.stylesheet.d.ts.map +1 -0
- package/dist/TextField/TextField.d.ts +24 -0
- package/dist/TextField/TextField.d.ts.map +1 -0
- package/dist/TextField/TextField.stylesheet.d.ts +29 -0
- package/dist/TextField/TextField.stylesheet.d.ts.map +1 -0
- package/dist/Toggle/Toggle.d.ts +12 -0
- package/dist/Toggle/Toggle.d.ts.map +1 -0
- package/dist/Toggle/Toggle.stylesheet.d.ts +16 -0
- package/dist/Toggle/Toggle.stylesheet.d.ts.map +1 -0
- package/dist/Tooltip/Tooltip.d.ts +34 -0
- package/dist/Tooltip/Tooltip.d.ts.map +1 -0
- package/dist/Tooltip/Tooltip.stylesheet.d.ts +22 -0
- package/dist/Tooltip/Tooltip.stylesheet.d.ts.map +1 -0
- package/dist/Typography/Typography.d.ts +28 -0
- package/dist/Typography/Typography.d.ts.map +1 -0
- package/dist/Typography/Typography.stylesheet.d.ts +16 -0
- package/dist/Typography/Typography.stylesheet.d.ts.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2947 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/listbox.stylesheet.d.ts +24 -0
- package/dist/internal/listbox.stylesheet.d.ts.map +1 -0
- package/dist/internal/useListboxKeyboard.d.ts +90 -0
- package/dist/internal/useListboxKeyboard.d.ts.map +1 -0
- 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
|