@vipengele/react-ui 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +463 -3
  3. package/dist/Avatar/Avatar.d.ts +21 -0
  4. package/dist/Avatar/Avatar.d.ts.map +1 -0
  5. package/dist/Avatar/Avatar.stylesheet.d.ts +17 -0
  6. package/dist/Avatar/Avatar.stylesheet.d.ts.map +1 -0
  7. package/dist/Button/Button.d.ts +39 -0
  8. package/dist/Button/Button.d.ts.map +1 -0
  9. package/dist/Button/Button.stylesheet.d.ts +17 -0
  10. package/dist/Button/Button.stylesheet.d.ts.map +1 -0
  11. package/dist/ButtonGroup/ButtonGroup.d.ts +16 -0
  12. package/dist/ButtonGroup/ButtonGroup.d.ts.map +1 -0
  13. package/dist/ButtonGroup/ButtonGroup.stylesheet.d.ts +12 -0
  14. package/dist/ButtonGroup/ButtonGroup.stylesheet.d.ts.map +1 -0
  15. package/dist/Card/Card.d.ts +36 -0
  16. package/dist/Card/Card.d.ts.map +1 -0
  17. package/dist/Card/Card.stylesheet.d.ts +17 -0
  18. package/dist/Card/Card.stylesheet.d.ts.map +1 -0
  19. package/dist/Dropdown/Dropdown.d.ts +205 -0
  20. package/dist/Dropdown/Dropdown.d.ts.map +1 -0
  21. package/dist/Dropdown/Dropdown.stylesheet.d.ts +24 -0
  22. package/dist/Dropdown/Dropdown.stylesheet.d.ts.map +1 -0
  23. package/dist/FieldSet/FieldSet.d.ts +22 -0
  24. package/dist/FieldSet/FieldSet.d.ts.map +1 -0
  25. package/dist/FieldSet/FieldSet.stylesheet.d.ts +34 -0
  26. package/dist/FieldSet/FieldSet.stylesheet.d.ts.map +1 -0
  27. package/dist/FieldShell/FieldShell.d.ts +42 -0
  28. package/dist/FieldShell/FieldShell.d.ts.map +1 -0
  29. package/dist/FieldShell/FieldShell.stylesheet.d.ts +32 -0
  30. package/dist/FieldShell/FieldShell.stylesheet.d.ts.map +1 -0
  31. package/dist/FormField/FormField.d.ts +45 -0
  32. package/dist/FormField/FormField.d.ts.map +1 -0
  33. package/dist/FormField/FormField.stylesheet.d.ts +24 -0
  34. package/dist/FormField/FormField.stylesheet.d.ts.map +1 -0
  35. package/dist/PasswordInput/PasswordInput.d.ts +15 -0
  36. package/dist/PasswordInput/PasswordInput.d.ts.map +1 -0
  37. package/dist/PasswordInput/PasswordInput.stylesheet.d.ts +24 -0
  38. package/dist/PasswordInput/PasswordInput.stylesheet.d.ts.map +1 -0
  39. package/dist/Popover/Popover.d.ts +47 -0
  40. package/dist/Popover/Popover.d.ts.map +1 -0
  41. package/dist/Popover/Popover.stylesheet.d.ts +21 -0
  42. package/dist/Popover/Popover.stylesheet.d.ts.map +1 -0
  43. package/dist/Progress/Progress.d.ts +18 -0
  44. package/dist/Progress/Progress.d.ts.map +1 -0
  45. package/dist/Progress/Progress.stylesheet.d.ts +18 -0
  46. package/dist/Progress/Progress.stylesheet.d.ts.map +1 -0
  47. package/dist/RadioButton/RadioButton.d.ts +18 -0
  48. package/dist/RadioButton/RadioButton.d.ts.map +1 -0
  49. package/dist/RadioButton/RadioButton.stylesheet.d.ts +15 -0
  50. package/dist/RadioButton/RadioButton.stylesheet.d.ts.map +1 -0
  51. package/dist/RadioGroup/RadioGroup.d.ts +40 -0
  52. package/dist/RadioGroup/RadioGroup.d.ts.map +1 -0
  53. package/dist/RadioGroup/RadioGroup.stylesheet.d.ts +11 -0
  54. package/dist/RadioGroup/RadioGroup.stylesheet.d.ts.map +1 -0
  55. package/dist/Skeleton/Skeleton.d.ts +18 -0
  56. package/dist/Skeleton/Skeleton.d.ts.map +1 -0
  57. package/dist/Skeleton/Skeleton.stylesheet.d.ts +25 -0
  58. package/dist/Skeleton/Skeleton.stylesheet.d.ts.map +1 -0
  59. package/dist/Slider/Slider.d.ts +12 -0
  60. package/dist/Slider/Slider.d.ts.map +1 -0
  61. package/dist/Slider/Slider.stylesheet.d.ts +18 -0
  62. package/dist/Slider/Slider.stylesheet.d.ts.map +1 -0
  63. package/dist/Spinner/Spinner.d.ts +19 -0
  64. package/dist/Spinner/Spinner.d.ts.map +1 -0
  65. package/dist/Spinner/Spinner.stylesheet.d.ts +14 -0
  66. package/dist/Spinner/Spinner.stylesheet.d.ts.map +1 -0
  67. package/dist/Tabs/Tabs.d.ts +46 -0
  68. package/dist/Tabs/Tabs.d.ts.map +1 -0
  69. package/dist/Tabs/Tabs.stylesheet.d.ts +15 -0
  70. package/dist/Tabs/Tabs.stylesheet.d.ts.map +1 -0
  71. package/dist/TextField/TextField.d.ts +24 -0
  72. package/dist/TextField/TextField.d.ts.map +1 -0
  73. package/dist/TextField/TextField.stylesheet.d.ts +29 -0
  74. package/dist/TextField/TextField.stylesheet.d.ts.map +1 -0
  75. package/dist/Toggle/Toggle.d.ts +12 -0
  76. package/dist/Toggle/Toggle.d.ts.map +1 -0
  77. package/dist/Toggle/Toggle.stylesheet.d.ts +16 -0
  78. package/dist/Toggle/Toggle.stylesheet.d.ts.map +1 -0
  79. package/dist/Tooltip/Tooltip.d.ts +34 -0
  80. package/dist/Tooltip/Tooltip.d.ts.map +1 -0
  81. package/dist/Tooltip/Tooltip.stylesheet.d.ts +22 -0
  82. package/dist/Tooltip/Tooltip.stylesheet.d.ts.map +1 -0
  83. package/dist/Typography/Typography.d.ts +28 -0
  84. package/dist/Typography/Typography.d.ts.map +1 -0
  85. package/dist/Typography/Typography.stylesheet.d.ts +16 -0
  86. package/dist/Typography/Typography.stylesheet.d.ts.map +1 -0
  87. package/dist/index.d.ts +22 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +2947 -0
  90. package/dist/index.js.map +1 -0
  91. package/dist/internal/listbox.stylesheet.d.ts +24 -0
  92. package/dist/internal/listbox.stylesheet.d.ts.map +1 -0
  93. package/dist/internal/useListboxKeyboard.d.ts +90 -0
  94. package/dist/internal/useListboxKeyboard.d.ts.map +1 -0
  95. package/package.json +50 -5
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pedro Gomes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,466 @@
1
1
  # @vipengele/react-ui
2
2
 
3
- `0.0.0` is a placeholder. It carries no code, and exists only so that a trusted publisher can
4
- be enrolled on this name npm can enrol one only on a name the registry already holds.
3
+ Vipengele's themeable React component library. Components read theme exclusively through
4
+ `--vpg-*` CSS custom properties set by `@vipengele/react-tokens`' `ThemeProvider` there is no
5
+ `useTheme()` hook (see `docs/adr/0001-theming-via-css-custom-properties-no-context-hook.md`).
5
6
 
6
- Install a real release instead: <https://github.com/vipengele/react>
7
+ ```tsx
8
+ import { Spinner } from "@vipengele/react-ui";
9
+
10
+ <Spinner size="md" />;
11
+ ```
12
+
13
+ ## Tree-shaking
14
+
15
+ Components are plain named exports — never a namespace barrel — so a consumer importing one
16
+ component pulls in only that component and its stylesheet.
17
+
18
+ ## Styling
19
+
20
+ Each component ships its styles as a string injected through React 19's
21
+ `<style href precedence>`, not as a `.css` import: a stylesheet import is the module side effect
22
+ that `"sideEffects": false` would have to carve an exception for. React hoists and de-duplicates
23
+ by `href`, so N instances inject one stylesheet.
24
+
25
+ Theme properties are only ever *read* through `var()` in those stylesheets, never assigned as an
26
+ inline style. An inline declaration beats any stylesheet rule for the same property on the same
27
+ element, so an inline `--vpg-*` value would permanently shadow `ThemeProvider`'s dark-mode
28
+ reassignment and that instance would stop adapting to colour mode.
29
+
30
+ ## Components
31
+
32
+ ### `Spinner`
33
+
34
+ An indeterminate loading indicator. Sizes `sm | md | lg` (steps of the icon scale — a spinner is
35
+ glyph-sized), stroked in `var(--vpg-accent)`, rotated by a CSS
36
+ `@keyframes` rule that slows under `prefers-reduced-motion: reduce`. Exposes `role="status"` with
37
+ a `label` (default `"Loading"`) as its accessible name.
38
+
39
+ `color` sets an inline stroke override. It is an opt-in escape hatch for a spinner sitting on a
40
+ ground the theme doesn't know about: that instance no longer adapts to light/dark.
41
+
42
+ ### `Button`
43
+
44
+ The library's action atom. `variant` is `primary | secondary | ghost | danger`, `size` is
45
+ `sm | md | lg` — the same scale as `Spinner`, so a `loading` button holds its height when its
46
+ content is swapped for an inline `<Spinner size={size} color="currentColor" />`. `disabled` and
47
+ `loading` both disable interaction; `loading` additionally sets `aria-busy`.
48
+
49
+ `leadingIcon` and `trailingIcon` take the icon component itself — `<Button leadingIcon={Plus} />`
50
+ — never a name, so a bundler only ever sees icons actually referenced. Passing `iconOnly` renders
51
+ the button with no visible label, in which case `aria-label` is required rather than optional: it
52
+ is the button's only accessible name.
53
+
54
+ ### `Typography`
55
+
56
+ The library's text atom. `variant` is `display | h1 | h2 | h3 | h4 | body-lg | body-md |
57
+ body-sm | caption`, `weight` is `regular | medium | bold`, and `color` is a curated set of
58
+ `--vpg-*` ink tokens — `primary | secondary | subtle | accent` — not an arbitrary CSS colour,
59
+ so text always tracks light/dark mode.
60
+
61
+ `variant` also chooses the rendered HTML element (`display`/`h1`–`h4` render their matching
62
+ heading tag, `body-*` renders `<p>`, `caption` renders `<span>`). `as` overrides only the tag,
63
+ never the visual style, so a heading-styled label can render as a `<div>` where an `<h1>` would
64
+ break the document outline.
65
+
66
+ ### `ButtonGroup`
67
+
68
+ Groups plain `<Button>` children into a single attached control. `orientation` is
69
+ `horizontal | vertical`. Children render unmodified — no `cloneElement`, no context — the
70
+ segmented look comes entirely from `ButtonGroup`'s own stylesheet targeting `.vpg-button` as
71
+ a descendant.
72
+
73
+ ### `Avatar`
74
+
75
+ The library's person atom: an image, the person's initials, or a generic person glyph, in that
76
+ order of preference, framed as a `circle` or `square` (`shape`) at size `sm | md | lg | xl`.
77
+
78
+ `src` is the image to render; a failed load falls through to initials, then to the icon, so a
79
+ dead URL degrades instead of leaving a blank frame. `name` is the source of the initials — the
80
+ first character of the first word plus the first character of the last word, upper-cased — and
81
+ the accessible name unless `alt` overrides it.
82
+
83
+ ### `Skeleton`
84
+
85
+ A shimmering placeholder shaped to match the content it stands in for: `variant` is
86
+ `rect | circle | text`. `width`/`height` accept a number (treated as pixels) or a string
87
+ carrying its own unit; left unset, the variant's own stylesheet rule sizes it. Decorative by
88
+ construction — it renders with `aria-hidden="true"` and never reaches the accessibility tree.
89
+
90
+ ### `Card`
91
+
92
+ A structured content surface: `Card`, `Card.Header`, `Card.Content` (required), and
93
+ `Card.Footer`. At most one of each subcomponent is allowed among `Card`'s children — anything
94
+ else, including an arbitrary child or a second `Card.Header`, throws at render. Layout is
95
+ CSS-driven (`order` in a flex column), so the three subcomponents render header-above-content-
96
+ above-footer regardless of the order they're written in JSX.
97
+
98
+ Passing `onClick` makes the whole card interactive: it renders as `<div role="button"
99
+ tabIndex={0}>` with `Enter`/`Space` activating it, not as a native `<button>` — a `<button>`'s
100
+ content model forbids interactive content, and `Card.Footer`'s canonical content is a `<Button>`.
101
+
102
+ ### `FormField`
103
+
104
+ Labels exactly one focusable control — `label`/`hint`/`error`/`children`, flat props rather than
105
+ a compound component. `children` is a native input, `Toggle`, `RadioButton`, or `Dropdown`'s
106
+ trigger — a single element whose component forwards unknown props to its focusable root. Not a
107
+ group-shaped component like `RadioGroup`, which gets its accessible name from its own
108
+ `aria-label` instead.
109
+
110
+ `FormField` generates ids via `useId` and clones onto the child: `id` (the child's own `id` wins
111
+ if it already has one), `aria-describedby` (built from whichever of `hint`/`error` render, merged
112
+ with any `aria-describedby` the child already carries rather than overwritten), `aria-invalid`
113
+ (set when `error` is non-empty), and `aria-labelledby` — applied unconditionally, regardless of
114
+ what element the child renders as. `<label htmlFor>` only associates with labelable elements
115
+ (`input`/`select`/`textarea`/`button`/`meter`/`output`/`progress`), so a non-labelable trigger
116
+ (`Dropdown`'s `<div role="combobox">`) would otherwise get no accessible name at all;
117
+ `aria-labelledby` works on both, so every control gets it uniformly.
118
+
119
+ `children` that isn't a single valid element — text, an array, a `Fragment`, `null` — throws:
120
+ there's no single node to attach the label and description to.
121
+
122
+ ```tsx
123
+ <FormField label="Email" hint="We never share this" error={errors.email}>
124
+ <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
125
+ </FormField>
126
+ ```
127
+
128
+ ### `FieldSet`
129
+
130
+ A native `<fieldset>` + `<legend>` pair with spacing between `children`, for grouping related
131
+ controls — typically one or more `FormField`s, though it isn't restricted to them. It carries no
132
+ form-state logic of its own, purely layout: `legend` renders in the native `<legend>`, which
133
+ names the `<fieldset>` automatically with no id/aria wiring needed. `disabled` forwards straight
134
+ to the native `<fieldset>`, which disables every descendant form control for free.
135
+
136
+ A `<legend>` naming its `<fieldset>` doesn't extend to a `role="radiogroup"` element nested
137
+ inside it, which is why `RadioGroup` carries its own `aria-label` rather than relying on an
138
+ ancestor `FieldSet`'s legend.
139
+
140
+ ```tsx
141
+ <FieldSet legend="Shipping address">
142
+ <FormField label="Street">
143
+ <input />
144
+ </FormField>
145
+ <FormField label="City">
146
+ <input />
147
+ </FormField>
148
+ </FieldSet>
149
+ ```
150
+
151
+ ### `Progress`
152
+
153
+ A linear progress bar. `size` is `sm | md | lg`. Given a `value` (against `max`, default `100`),
154
+ it renders determinate — the fill's width tracks the percentage, and `role="progressbar"` carries
155
+ `aria-valuenow`/`-valuemin`/`-valuemax`; a `value` outside `[0, max]` is clamped rather than
156
+ over- or under-filling the track. Omitting `value` renders indeterminate instead: a looping
157
+ sweep with no `aria-value*` attributes, since a progress bar with no known value has nothing to
158
+ report as a percentage. The sweep slows rather than stops under `prefers-reduced-motion: reduce`.
159
+
160
+ ### `Tabs`
161
+
162
+ A tabbed interface: `Tabs`, `Tabs.List`, `Tabs.Tab`, and `Tabs.Panel`. Each `Tabs.Tab` is paired
163
+ with the `Tabs.Panel` carrying the same `value`; the `id`/`aria-controls`/`aria-labelledby` wiring
164
+ between them is generated with `useId`, so no DOM ids need supplying. Selection is controlled
165
+ through `value`/`onChange`, or left to `Tabs` itself — seeded by `defaultValue`, falling back to
166
+ the first `Tabs.Tab` in `children`.
167
+
168
+ The list follows the WAI-ARIA tabs pattern: a roving tabindex (the selected tab is the list's only
169
+ tab stop), automatic activation — an arrow key moves focus and selects in one step — wrap-around
170
+ at both ends, and `Home`/`End` jumping to the first/last tab. `orientation` is `horizontal` (the
171
+ default, Left/Right) or `vertical` (Up/Down); the off-axis arrow pair is left unhandled. A tab
172
+ with `disabled` is skipped by keyboard traversal entirely and cannot be clicked.
173
+
174
+ Only the selected panel is mounted — the others render nothing rather than staying in the DOM
175
+ hidden, so a panel's internal state does not survive a switch away from it.
176
+
177
+ ### `TextField`
178
+
179
+ A styled native `<input>` for free-text entry (`type` defaults to `"text"`; pass `"email"`,
180
+ `"password"`, etc. for any other native input type). Forwards every other `<input>` prop as-is.
181
+ Reads the same border/radius/surface tokens `Dropdown`'s trigger reads, and the same
182
+ `aria-invalid` styling hook, so a text field and a dropdown trigger read as the same kind of
183
+ control side by side in a form.
184
+
185
+ ### `Toggle`
186
+
187
+ A native `<input type="checkbox" role="switch">` styled as a switch. It forwards every
188
+ `<input>` prop except `type`/`role`, so `checked`/`onChange` (controlled) or `defaultChecked`
189
+ (uncontrolled), `disabled`, and `aria-label`/`aria-labelledby` all work exactly as they do on a
190
+ plain checkbox. No custom keyboard handling and no hand-set `aria-checked`: the native element
191
+ already exposes its checked state through the DOM, handles focus and keyboard interaction, and
192
+ participates in forms for free.
193
+
194
+ ### `RadioButton` / `RadioGroup`
195
+
196
+ `RadioButton` is a single styled native `<input type="radio">`. It forwards every `<input>` prop
197
+ except `type`, and is usable entirely on its own outside any `RadioGroup` — pass `name`,
198
+ `checked`/`onChange` (controlled) or `defaultChecked` (uncontrolled), and `value` manually, the
199
+ same as a plain radio input. No custom keyboard or roving-tabindex code: native radios sharing a
200
+ `name` get browser-native grouping and arrow-key behavior for free.
201
+
202
+ ### `Slider`
203
+
204
+ A native `<input type="range">` styled as a single-thumb slider. It forwards every `<input>`
205
+ prop except `type`, so `min`/`max`/`step`, `value`/`onChange` (controlled) or `defaultValue`
206
+ (uncontrolled), and `disabled` all work exactly as they do on a plain range input. No custom
207
+ keyboard or pointer handling: the native element already handles arrow-key stepping, dragging,
208
+ touch, and form participation for free. Two-thumb range selection is out of scope.
209
+
210
+ `RadioGroup` is a context provider grouping `RadioButton`s: `role="radiogroup"` on its own
211
+ wrapper, with an `aria-label` for its accessible name — independent of any ancestor `FieldSet`,
212
+ since a `<legend>` doesn't automatically name a nested `role="radiogroup"` element the way it
213
+ names the `<fieldset>` itself. Selection is controlled through `value`/`onChange`, or left to
214
+ `RadioGroup` itself, seeded by `defaultValue` — the same duality as `Tabs`. A shared `name` is
215
+ auto-generated with `useId` when not given explicitly, and every child `RadioButton` reads its
216
+ `name`, checked state, and change handler from context; an explicit `checked`/`onChange` on a
217
+ `RadioButton` still overrides what the group would otherwise provide.
218
+
219
+ ```tsx
220
+ <RadioGroup aria-label="Size" defaultValue="medium">
221
+ <RadioButton aria-label="Small" value="small" />
222
+ <RadioButton aria-label="Medium" value="medium" />
223
+ <RadioButton aria-label="Large" value="large" />
224
+ </RadioGroup>
225
+ ```
226
+
227
+ ### `Tooltip`
228
+
229
+ A small floating label describing its trigger. `content` is what the bubble shows, `children` is
230
+ the trigger, and `placement` (`top | bottom | left | right`, default `top`) is the preferred side —
231
+ the bubble flips to the opposite side when it wouldn't fit there and shifts to stay on-screen.
232
+
233
+ The trigger is wrapped in an inline `<span>` carrying the ref and the hover/focus handlers, never
234
+ cloned, so it can be any node — including a component that forwards neither a ref nor unknown
235
+ props. The bubble appears on hover and on keyboard focus, and is dismissed by moving away, by
236
+ blurring the trigger, or by `Escape`. It carries `role="tooltip"` and is wired to the trigger with
237
+ `aria-describedby` while open.
238
+
239
+ `disabled` suppresses the tooltip outright: no handler is registered and the bubble never renders.
240
+ It lives on `Tooltip` rather than being read off the trigger, because the trigger's props are
241
+ never inspected.
242
+
243
+ The bubble portals into the nearest ancestor `.vpg-root` — the subtree `ThemeProvider`
244
+ establishes — rather than `document.body`, so it keeps every `--vpg-*` value. On a page with no
245
+ `.vpg-root` ancestor it renders inline beside the trigger instead, positioned identically but
246
+ inheriting whatever theme surrounds it.
247
+
248
+ ### `Popover`
249
+
250
+ A floating panel of interactive content. `content` is what the panel holds, `children` is the
251
+ trigger, and `placement` (`top | bottom | left | right`, default `bottom`) is the preferred side —
252
+ the panel flips to the opposite side when it wouldn't fit there and shifts to stay on-screen.
253
+
254
+ The trigger is wrapped in an inline `<span>` carrying the ref and the click handler, never cloned,
255
+ so it can be any node. Clicking it opens the panel; clicking it again, pressing outside, or
256
+ pressing `Escape` closes it. The wrapper carries `aria-haspopup="dialog"` and an `aria-expanded`
257
+ that tracks the panel, which itself is a `role="dialog"`.
258
+
259
+ Open state is either controlled through `open`/`onOpenChange` or left to `Popover` itself, seeded
260
+ by `defaultOpen`. `onOpenChange` fires for every open/close request in both forms. `content` is a
261
+ plain node rather than a render prop taking a `close` callback: content that has to close the
262
+ popover itself belongs in the controlled form, where the consumer already owns the state.
263
+
264
+ While the panel is open, focus is trapped inside it and the rest of the page is hidden from
265
+ assistive technology; closing it returns focus to the trigger. The panel holds real interactive
266
+ content, so keyboard users must be able to reach it and must not fall out the back of it.
267
+
268
+ The panel portals into the nearest ancestor `.vpg-root` — the subtree `ThemeProvider`
269
+ establishes — rather than `document.body`, so it keeps every `--vpg-*` value. On a page with no
270
+ `.vpg-root` ancestor it renders inline beside the trigger instead, positioned identically but
271
+ inheriting whatever theme surrounds it.
272
+
273
+ ### `Dropdown`
274
+
275
+ A select-only combobox: `Dropdown` and `Dropdown.Option` children directly beneath it, with no
276
+ list layer — the floating listbox's positioning is `Dropdown`'s own business. The field fills its
277
+ container's width, the way `TextField` does, rather than shrinking to fit its selection — it never
278
+ widens as options are selected. Each
279
+ `Dropdown.Option` takes a `value`, a `label` (the string shown in the trigger, in its chip, matched
280
+ by the search query, and — with `searchable={false}` — by type-ahead on the trigger), an optional
281
+ leading `icon`, and `disabled`. A child that is neither a
282
+ `Dropdown.Option`, a `Dropdown.Group` nor falsy throws at render; falsy children — what `condition
283
+ && <Dropdown.Option />` produces — are skipped.
284
+
285
+ A selection is a `DropdownValue` — `{ value, label, icon? }`, or `null` for none — controlled
286
+ through `value`/`onChange` or left to `Dropdown` itself, seeded by `defaultValue`. `multiple`
287
+ switches all three to arrays: each option gains a checkbox, each selected value a removable chip
288
+ beside the trigger, and selecting toggles the option without closing the listbox. `onChange` hands
289
+ back the whole object of the option that was picked — `(value: DropdownValue | null) => void` in
290
+ single-select, where `null` is the emptied selection, and `(value: DropdownValue[]) => void` in
291
+ `multiple`, where the empty array is.
292
+
293
+ The `value` string is the identity, so a consumer that re-creates its value object on every render
294
+ keeps its selection. A `Dropdown.Option` carrying that `value` supplies the label and icon that
295
+ render; the value object's own are the fallback for a selection no option matches.
296
+
297
+ ```tsx
298
+ <Dropdown defaultValue={{ value: "medium", label: "Medium" }} onChange={(size) => setSize(size)}>
299
+ <Dropdown.Option value="small" label="Small" icon={Minus} />
300
+ <Dropdown.Option value="medium" label="Medium" />
301
+ <Dropdown.Option value="large" label="Large" disabled />
302
+ </Dropdown>
303
+ ```
304
+
305
+ `searchable` (default `true`) opens the listbox under a search row: a magnifier, an input hinted
306
+ by `searchPlaceholder` (default `"Search"`), then a divider. Typing filters the options to a
307
+ case-insensitive substring of their labels, wherever it falls in them, and a query matching none
308
+ of them says "No results" rather than leaving the panel blank. A selection the query filters out
309
+ of the list keeps its place in the trigger and its chip. `searchable={false}` renders the listbox
310
+ alone, with type-ahead on the trigger.
311
+
312
+ ```tsx
313
+ <Dropdown aria-label="Assignee" searchPlaceholder="Search people">
314
+ <Dropdown.Option value="ada" label="Ada Lovelace" icon={User} />
315
+ <Dropdown.Option value="grace" label="Grace Hopper" icon={User} />
316
+ </Dropdown>
317
+ ```
318
+
319
+ Every keystroke belongs to that search once the row exists. A printable character typed on the
320
+ closed trigger opens the popover and seeds the query with it. A pick in `multiple` mode keeps the
321
+ popover open and clears the query, so the next character searches every option again rather than
322
+ narrowing what is left of the picked option's own match. `Backspace` with no character left to
323
+ delete removes the last selection — `multiple`'s alone, since a single selection has no last
324
+ selection distinct from its only one, and `clearable` below is what empties that. The query clears as the
325
+ popover closes, so the next open starts on the full list.
326
+
327
+ #### Groups
328
+
329
+ `Dropdown.Group label` heads a run of `Dropdown.Option` children. A heading is not an option: it
330
+ takes no place in the flat list the arrow keys, `Home`/`End` and the wrap at either end travel, so
331
+ every option is reached exactly as it is with no group declared. Each group renders as a
332
+ `role="group"` named by its heading, with a separator drawn between one group and the next —
333
+ never before the first or after the last, and never declared by the consumer. A group the search
334
+ query leaves no option in renders nothing at all. A `Dropdown.Group` inside a `Dropdown.Group`
335
+ throws, as does any group child that is neither a `Dropdown.Option` nor falsy.
336
+
337
+ ```tsx
338
+ <Dropdown aria-label="Fruit">
339
+ <Dropdown.Option value="all" label="All fruit" />
340
+ <Dropdown.Group label="Citrus">
341
+ <Dropdown.Option value="lemon" label="Lemon" />
342
+ <Dropdown.Option value="lime" label="Lime" />
343
+ </Dropdown.Group>
344
+ <Dropdown.Group label="Stone">
345
+ <Dropdown.Option value="peach" label="Peach" />
346
+ </Dropdown.Group>
347
+ </Dropdown>
348
+ ```
349
+
350
+ An async result groups itself with a `group` string instead — see below.
351
+
352
+ The search input is a second `role="combobox"`, with `aria-autocomplete="list"`, its own
353
+ `aria-controls` on the listbox and the `aria-activedescendant` tracking the highlight; it is named
354
+ by `searchPlaceholder`, while the trigger keeps the accessible name and description. A non-modal
355
+ `FloatingFocusManager` puts real DOM focus in that input as the panel opens, and Escape, a
356
+ selection and a press outside each hand focus back to the trigger. `Enter` selects the highlighted
357
+ option; `Space` is a character in the query, not a selection key.
358
+
359
+ The trigger is a `<div role="combobox" tabIndex={0}>`, not a `<button>`: only `combobox` and a
360
+ handful of other roles may legally carry `aria-activedescendant`, and the highlighted option is
361
+ tracked virtually through exactly that attribute rather than by moving focus into the listbox.
362
+ The trigger also carries `aria-haspopup="listbox"`, `aria-expanded` and `aria-controls`, and
363
+ forwards `id`/`aria-label`/`aria-labelledby`/`aria-describedby`/`aria-invalid` — so a `Dropdown`
364
+ wrapped in a `FormField` gets its accessible name and description on the element that actually
365
+ takes focus.
366
+
367
+ Keyboard: `Enter`/`Space` opens the listbox and then selects the highlighted option (toggling it,
368
+ in `multiple`), the arrow keys move the highlight and wrap at both ends, `Home`/`End` jump to the
369
+ first/last option, `Escape` closes, and — with no search row to type into — typing a character
370
+ jumps the highlight to the next option whose label starts with it. Disabled options are skipped by
371
+ every one of those and cannot be clicked.
372
+
373
+ In `multiple` mode the chips render as siblings *before* the trigger inside a plain wrapper, never
374
+ inside it: floating-ui merges its own click and keyboard handlers into the trigger's, so a remove
375
+ button nested in there could not be reliably intercepted before those ran. The chips carry the
376
+ whole selection: the trigger beside them renders nothing once anything is selected, and is the
377
+ click target holding the chevron. The placeholder shows there while the selection is empty.
378
+
379
+ Those chips keep to one row, so a field with a selection stands at the same height as an empty
380
+ one. Which of them fit is measured against the width the field has — not capped at a number, which
381
+ would already overflow a narrow field and leave room unused in a wide one — and re-measured before
382
+ the next paint whenever that width changes. The rest give way to an indicator counting them, and a
383
+ chip too wide for the field shows with its label cut short rather than pushing the field past its
384
+ container.
385
+
386
+ The indicator standing for the rest reads "and N more" and takes no tab stop — a hidden selection
387
+ is removed by unchecking it in the listbox, since the chip carrying it is not on screen to remove
388
+ it from, so a stop there would be a stop with nothing to do. Hovering it shows the labels it
389
+ stands for in a `Tooltip`. That tooltip is the pointer's route to them; a screen reader's is the
390
+ trigger, which is described by every selection, hidden or not — and that description is *merged*
391
+ with whatever `aria-describedby` the trigger is given, so a `Dropdown` inside a `FormField` keeps
392
+ its hint and its error message alongside it rather than losing them to the selection.
393
+
394
+ `wrapChips` switches the measurement off and wraps the chips onto further rows instead, growing
395
+ the field downwards:
396
+
397
+ ```tsx
398
+ <Dropdown multiple wrapChips aria-label="Fruit" defaultValue={picked} onChange={setPicked}>
399
+ {fruit.map((name) => (
400
+ <Dropdown.Option key={name} value={name} label={name} />
401
+ ))}
402
+ </Dropdown>
403
+ ```
404
+
405
+ `clearable` (default `false`) puts a "Clear selection" `<button>` in the field's trailing slot
406
+ while anything is selected, and takes it away again once nothing is. Pressing it empties the whole
407
+ selection at once — `onChange` reports `null` in single-select and `[]` in `multiple` — without
408
+ opening the listbox, and leaves focus on the trigger. It is an adornment rather than a control the
409
+ field reads: the focus ring it takes is its own, and the field around it stays at rest.
410
+
411
+ ```tsx
412
+ <Dropdown clearable aria-label="Size" value={size} onChange={setSize}>
413
+ <Dropdown.Option value="small" label="Small" />
414
+ <Dropdown.Option value="large" label="Large" />
415
+ </Dropdown>
416
+ ```
417
+
418
+ The listbox — the whole panel, search row included — portals into the nearest ancestor
419
+ `.vpg-root` — the subtree `ThemeProvider` establishes — rather than `document.body`, so it
420
+ keeps every `--vpg-*` value. On a page with no `.vpg-root` ancestor it renders inline
421
+ beside the trigger instead.
422
+
423
+ #### Async data source
424
+
425
+ Pass `loadOptions` instead of `children` to back `Dropdown` with an API rather than a declared
426
+ list:
427
+
428
+ ```tsx
429
+ <Dropdown
430
+ aria-label="Country"
431
+ placeholder="Pick a country"
432
+ defaultValue={{ value: "jp", label: "Japan" }}
433
+ loadOptions={(query) => fetchCountries(query)}
434
+ />
435
+ ```
436
+
437
+ `loadOptions: (query: string) => Promise<DropdownAsyncOption[]>` — each result a `{value, label,
438
+ icon?, disabled?, group?}` — is called with the search query after it settles for `debounceMs` (default
439
+ `300`), and `Dropdown` renders whatever it resolves to. Filtering the query is the API's job in
440
+ this mode: results are shown as returned, never matched again client-side. `loadingMessage`
441
+ (default `"Loading…"`) shows while a search is pending, `errorMessage` (default `"Something went
442
+ wrong."`) shows if the promise rejects, and a search that returns nothing says "No results". A
443
+ response the query has moved past is discarded rather than applied — a slow earlier search
444
+ resolving after a faster later one, and equally one resolving while the next query is still
445
+ settling. The search is keyed off the query alone, so an inline arrow like the one above, fresh on
446
+ every render, is as correct as a memoised `loadOptions`. `children` goes unread when `loadOptions`
447
+ is set.
448
+
449
+ A result's `group` is the heading it stands under. Results carrying the same string are one group
450
+ however far apart they arrive in the array, the groups stand in the order their first result
451
+ arrives, and every result carrying no group at all comes before them.
452
+
453
+ A selection carries its own `label`, so the trigger and a `multiple` chip render it with nothing
454
+ fetched, no search run and no option child to match against — including for a `value` or
455
+ `defaultValue` handed straight to `Dropdown`.
456
+
457
+ ## Runtime dependencies
458
+
459
+ `@floating-ui/react` positions `Tooltip`'s bubble, `Popover`'s panel and `Dropdown`'s listbox —
460
+ and drives its virtual-focus list navigation and type-ahead. It travels only with the components
461
+ that need it — a bundle importing anything else does not pull it in, which `bundle-check/`
462
+ asserts.
463
+
464
+ ## Peer dependencies
465
+
466
+ React 19 and React DOM 19 — components render React and rely on `<style href precedence>`.
@@ -0,0 +1,21 @@
1
+ import { type HTMLAttributes } from "react";
2
+ export type AvatarSize = "sm" | "md" | "lg" | "xl";
3
+ export type AvatarShape = "circle" | "square";
4
+ export interface AvatarProps extends Omit<HTMLAttributes<HTMLSpanElement>, "children"> {
5
+ /** The image to render. An image that fails to load falls through to the initials, and then
6
+ * to the generic person icon, so a dead URL degrades instead of leaving a blank frame. */
7
+ src?: string;
8
+ /** The person the avatar stands for: the source of the initials, and the accessible name
9
+ * when no `alt` is given. */
10
+ name?: string;
11
+ /** Overrides `name` as the accessible name. */
12
+ alt?: string;
13
+ size?: AvatarSize;
14
+ shape?: AvatarShape;
15
+ }
16
+ /**
17
+ * The library's person atom: an image, the person's initials, or a generic person glyph, in
18
+ * that order of preference, framed as a circle or a rounded square.
19
+ */
20
+ export declare function Avatar({ src, name, alt, size, shape, className, ...rest }: AvatarProps): import("react").JSX.Element;
21
+ //# sourceMappingURL=Avatar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Avatar.d.ts","sourceRoot":"","sources":["../../src/Avatar/Avatar.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,cAAc,EAAY,MAAM,OAAO,CAAC;AAGtD,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAEnD,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE9C,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,EAAE,UAAU,CAAC;IACpF;8FAC0F;IAC1F,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;iCAC6B;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AAiBD;;;GAGG;AACH,wBAAgB,MAAM,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAW,EAAE,KAAgB,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,WAAW,+BAuCxG"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `<Avatar>`'s own styles, injected as an inline `<style>` rather than a `.css` import so the
3
+ * package can stay `"sideEffects": false` (same approach as `@vipengele/react-tokens`'s base stylesheet
4
+ * and `Button`'s stylesheet).
5
+ *
6
+ * Every `--vpg-*` property is *read* here through `var()` and never assigned inline by the
7
+ * component: an inline style declaration always wins over a stylesheet rule for the same
8
+ * property on the same element, so an inline `--vpg-surface-raised` would permanently
9
+ * shadow the dark-mode reassignment in `@vipengele/react-tokens`'s base stylesheet and this avatar
10
+ * would stop adapting to colour mode.
11
+ *
12
+ * The circle takes steps of the size scale and the initials steps of the type scale. The `xl`
13
+ * circle takes the size scale's display step, past the range a pointer targets, and the `sm`
14
+ * initials sit on the type scale's floor rather than under it.
15
+ */
16
+ export declare const avatarStylesheet = "\n.vpg-avatar {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n box-sizing: border-box;\n overflow: hidden;\n background-color: var(--vpg-surface-raised);\n color: var(--vpg-ink-muted);\n font-family: inherit;\n font-weight: 500;\n line-height: 1;\n /* The fallback content is centred text or a glyph; without this, a long initials string\n would be selectable and could wrap out of the frame. */\n user-select: none;\n}\n\n.vpg-avatar-circle {\n border-radius: 50%;\n}\n\n.vpg-avatar-square {\n border-radius: var(--vpg-radius);\n}\n\n.vpg-avatar-sm {\n width: var(--vpg-size-xs);\n height: var(--vpg-size-xs);\n font-size: var(--vpg-font-size-xs);\n}\n\n.vpg-avatar-md {\n width: var(--vpg-size-md);\n height: var(--vpg-size-md);\n font-size: var(--vpg-font-size-xs);\n}\n\n.vpg-avatar-lg {\n width: var(--vpg-size-xl);\n height: var(--vpg-size-xl);\n font-size: var(--vpg-font-size-sm);\n}\n\n.vpg-avatar-xl {\n width: var(--vpg-size-2xl);\n height: var(--vpg-size-2xl);\n font-size: var(--vpg-font-size-lg);\n}\n\n.vpg-avatar-image {\n width: 100%;\n height: 100%;\n /* `cover` rather than `contain`: a non-square photo is cropped to the frame instead of\n letterboxed against the surface colour. */\n object-fit: cover;\n display: block;\n}\n\n.vpg-avatar-icon {\n /* Proportional to the frame, so one rule sizes the glyph across the whole size scale. */\n width: 60%;\n height: 60%;\n}\n";
17
+ //# sourceMappingURL=Avatar.stylesheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Avatar.stylesheet.d.ts","sourceRoot":"","sources":["../../src/Avatar/Avatar.stylesheet.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,28CAgE5B,CAAC"}
@@ -0,0 +1,39 @@
1
+ import type { IconComponent } from "@vipengele/react-icons";
2
+ import type { ButtonHTMLAttributes, ReactNode } from "react";
3
+ export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
4
+ /** Shared with `Spinner`'s size scale, so a loading button holds its height. */
5
+ export type ButtonSize = "sm" | "md" | "lg";
6
+ interface ButtonOwnProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
7
+ variant?: ButtonVariant;
8
+ size?: ButtonSize;
9
+ /** Disables interaction and replaces the button's content with an inline `<Spinner>`. The
10
+ * button keeps its own text colour, so the spinner reads on every variant. */
11
+ loading?: boolean;
12
+ /** Rendered before the label. Pass the component itself — `<Button leadingIcon={Plus} />` —
13
+ * so a bundler only ever sees the icons actually referenced. */
14
+ leadingIcon?: IconComponent;
15
+ /** Rendered after the label, under the same rules as `leadingIcon`. */
16
+ trailingIcon?: IconComponent;
17
+ }
18
+ /**
19
+ * A square button carrying an icon and no text. `aria-label` is required rather than optional:
20
+ * without visible text, it is the button's only accessible name, and a runtime warning would
21
+ * land too late to stop the button shipping nameless.
22
+ */
23
+ interface IconOnlyButtonProps extends ButtonOwnProps {
24
+ iconOnly: true;
25
+ "aria-label": string;
26
+ children?: never;
27
+ }
28
+ interface LabelledButtonProps extends ButtonOwnProps {
29
+ iconOnly?: false;
30
+ children?: ReactNode;
31
+ }
32
+ export type ButtonProps = IconOnlyButtonProps | LabelledButtonProps;
33
+ /**
34
+ * The library's action atom, styled entirely from `--vpg-*` custom properties read through
35
+ * `var()` in its own stylesheet, so a themed instance follows colour mode without re-rendering.
36
+ */
37
+ export declare function Button({ variant, size, loading, iconOnly, leadingIcon: LeadingIcon, trailingIcon: TrailingIcon, className, disabled, children, ...rest }: ButtonProps): import("react").JSX.Element;
38
+ export {};
39
+ //# sourceMappingURL=Button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../src/Button/Button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,KAAK,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAI7D,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEzE,gFAAgF;AAChF,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE5C,UAAU,cAAe,SAAQ,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,UAAU,CAAC;IACxF,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB;kFAC8E;IAC9E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;oEACgE;IAChE,WAAW,CAAC,EAAE,aAAa,CAAC;IAC5B,uEAAuE;IACvE,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B;AAED;;;;GAIG;AACH,UAAU,mBAAoB,SAAQ,cAAc;IAClD,QAAQ,EAAE,IAAI,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB;AAED,UAAU,mBAAoB,SAAQ,cAAc;IAClD,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,MAAM,MAAM,WAAW,GAAG,mBAAmB,GAAG,mBAAmB,CAAC;AAEpE;;;GAGG;AACH,wBAAgB,MAAM,CAAC,EACrB,OAAmB,EACnB,IAAW,EACX,OAAe,EACf,QAAgB,EAChB,WAAW,EAAE,WAAW,EACxB,YAAY,EAAE,YAAY,EAC1B,SAAS,EACT,QAAgB,EAChB,QAAQ,EACR,GAAG,IAAI,EACR,EAAE,WAAW,+BAwCb"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `<Button>`'s own styles, injected as an inline `<style>` rather than a `.css` import so the
3
+ * package can stay `"sideEffects": false` (same approach as `@vipengele/react-tokens`'s base stylesheet
4
+ * and `@vipengele/react-icons`'s icon stylesheet).
5
+ *
6
+ * Every `--vpg-*` property is *read* here through `var()` and never assigned inline by the
7
+ * component: an inline style declaration always wins over a stylesheet rule for the same
8
+ * property on the same element, so an inline `--vpg-accent` would permanently shadow the
9
+ * dark-mode reassignment in `@vipengele/react-tokens`'s base stylesheet and this button would stop
10
+ * adapting to colour mode.
11
+ *
12
+ * Heights come from the size scale, paddings from the spacing scale and label sizes from the
13
+ * type scale. The danger variant reads the `--vpg-danger` ramp, which is derived exactly as the
14
+ * accent ramp is, so both variants shift by the same amount on hover and press.
15
+ */
16
+ export declare const buttonStylesheet = "\n.vpg-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--vpg-space-2);\n box-sizing: border-box;\n border: 1px solid transparent;\n border-radius: var(--vpg-radius);\n font-family: inherit;\n font-weight: 500;\n line-height: 1;\n white-space: nowrap;\n text-decoration: none;\n cursor: pointer;\n /* Colour and background come from the variant rules below; every variant animates the same\n two properties, so the transition belongs here rather than four times over. */\n transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;\n}\n\n.vpg-button:focus-visible {\n /* Offset rather than inset so the ring stays legible against a same-coloured surface. */\n outline: var(--vpg-focus-ring-width) solid var(--vpg-accent-ring);\n outline-offset: var(--vpg-focus-ring-offset);\n}\n\n.vpg-button:disabled {\n /* Pointer events stay on: a disabled button that swallows them gives no cursor feedback and\n no tooltip target. `cursor` is what communicates the state. */\n cursor: not-allowed;\n opacity: 0.55;\n}\n\n.vpg-button-primary {\n background-color: var(--vpg-accent);\n color: var(--vpg-accent-contrast);\n}\n\n.vpg-button-primary:hover:not(:disabled) {\n background-color: var(--vpg-accent-hover);\n}\n\n.vpg-button-primary:active:not(:disabled) {\n background-color: var(--vpg-accent-press);\n}\n\n.vpg-button-secondary {\n background-color: var(--vpg-surface-raised);\n border-color: var(--vpg-border);\n color: var(--vpg-ink);\n}\n\n.vpg-button-secondary:hover:not(:disabled) {\n background-color: var(--vpg-surface-hover);\n border-color: var(--vpg-border-strong);\n}\n\n.vpg-button-secondary:active:not(:disabled) {\n background-color: var(--vpg-surface-press);\n}\n\n.vpg-button-ghost {\n background-color: transparent;\n color: var(--vpg-ink);\n}\n\n.vpg-button-ghost:hover:not(:disabled) {\n background-color: var(--vpg-accent-wash);\n}\n\n.vpg-button-ghost:active:not(:disabled) {\n background-color: var(--vpg-surface-press);\n}\n\n.vpg-button-danger {\n background-color: var(--vpg-danger);\n color: var(--vpg-danger-contrast);\n}\n\n.vpg-button-danger:hover:not(:disabled) {\n background-color: var(--vpg-danger-hover);\n}\n\n.vpg-button-danger:active:not(:disabled) {\n background-color: var(--vpg-danger-press);\n}\n\n.vpg-button-sm {\n min-height: var(--vpg-size-sm);\n padding: 0 var(--vpg-space-3);\n font-size: var(--vpg-font-size-xs);\n}\n\n.vpg-button-md {\n min-height: var(--vpg-size-md);\n padding: 0 var(--vpg-space-4);\n font-size: var(--vpg-font-size-sm);\n}\n\n.vpg-button-lg {\n min-height: var(--vpg-size-xl);\n padding: 0 var(--vpg-space-5);\n font-size: var(--vpg-font-size-md);\n}\n\n/* Square: the horizontal padding is dropped and the min-height doubles as a width floor, so the\n button stays square at every size without a second per-size scale. */\n.vpg-button-icon-only {\n padding: 0;\n aspect-ratio: 1;\n}\n\n.vpg-button-icon-only.vpg-button-sm {\n min-width: var(--vpg-size-sm);\n}\n\n.vpg-button-icon-only.vpg-button-md {\n min-width: var(--vpg-size-md);\n}\n\n.vpg-button-icon-only.vpg-button-lg {\n min-width: var(--vpg-size-xl);\n}\n\n.vpg-button-icon {\n flex: none;\n width: 1em;\n height: 1em;\n}\n\n/* Keeps the button's accessible name intact while `loading` swaps its visible content for a\n spinner: the label stays in the accessibility tree, but takes up no visual space. */\n.vpg-button-visually-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n";
17
+ //# sourceMappingURL=Button.stylesheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Button.stylesheet.d.ts","sourceRoot":"","sources":["../../src/Button/Button.stylesheet.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,kmHA+I5B,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ export type ButtonGroupOrientation = "horizontal" | "vertical";
3
+ interface ButtonGroupOwnProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
4
+ orientation?: ButtonGroupOrientation;
5
+ children?: ReactNode;
6
+ }
7
+ export type ButtonGroupProps = ButtonGroupOwnProps;
8
+ /**
9
+ * Groups plain `<Button>` children into a single attached control. Children are rendered
10
+ * unmodified — no `cloneElement`, no context — the segmented look comes entirely from
11
+ * `ButtonGroup`'s own stylesheet targeting `.vpg-button` as a descendant, which makes
12
+ * `Button`'s rendered class name a contract this component depends on.
13
+ */
14
+ export declare function ButtonGroup({ orientation, className, children, ...rest }: ButtonGroupProps): import("react").JSX.Element;
15
+ export {};
16
+ //# sourceMappingURL=ButtonGroup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ButtonGroup.d.ts","sourceRoot":"","sources":["../../src/ButtonGroup/ButtonGroup.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvD,MAAM,MAAM,sBAAsB,GAAG,YAAY,GAAG,UAAU,CAAC;AAE/D,UAAU,mBAAoB,SAAQ,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC;IACpF,WAAW,CAAC,EAAE,sBAAsB,CAAC;IACrC,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,MAAM,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAEnD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAAE,WAA0B,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,EAAE,gBAAgB,+BAmBzG"}