@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/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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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"}
|