@stealthscale/component-feedback 0.1.0 → 0.2.1

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/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # @stealthscale/component-feedback
2
2
 
3
- Draws the system reporting on itself: what is loading, what went wrong, what state a thing is in.
4
- Every component binds a recipe and draws nothing of its own, so a theme restyles all of them by
5
- extending the recipe. The preset under `./theme` registers the recipes with an application's
6
- compiler.
3
+ React components that report state: `Alert`, `Skeleton`, `SkeletonText`, `Spinner`, `Loader`,
4
+ `Progress`, `ProgressCircle`, `Meter`, `EmptyState` and `Toast`. Each component renders through a
5
+ recipe, so a theme restyles it by extending the recipe. The preset under `./theme` registers the
6
+ recipes with an application's compiler.
7
7
 
8
- Every value a theme can change on a component is an axis of its recipe, so a caller sets it as a
9
- prop and writes no style. A caller changes the element a component draws with `as`.
8
+ Every value a theme can change is a recipe axis, and a caller sets it as a prop. A caller changes
9
+ the rendered element with `as`.
10
10
 
11
11
  ## Install
12
12
 
@@ -14,15 +14,83 @@ prop and writes no style. A caller changes the element a component draws with `a
14
14
  pnpm add @stealthscale/component-feedback
15
15
  ```
16
16
 
17
- The package peers on `react` and `@stealthscale/theme`. An application lists the preset under
18
- `./theme` among the presets its compiler installs.
17
+ The package peers on `react`, `@stealthscale/theme` and `@stealthscale/hooks`. It depends on
18
+ `@zag-js/progress`, which tracks the value of a progress bar and a meter, and `@zag-js/toast`, which
19
+ runs the toasts. Add the preset under `./theme` to the presets the application's compiler installs.
20
+
21
+ ## Alert
22
+
23
+ Renders a notice the reader needs to act on or know about. Compose it as `Alert.Root` around an
24
+ icon, the text, optional trailing controls and a close trigger.
25
+
26
+ ```tsx
27
+ import { Alert } from "@stealthscale/component-feedback";
28
+
29
+ <Alert.Root live="assertive" status="error">
30
+ <Alert.Indicator>
31
+ <TriangleAlertIcon />
32
+ </Alert.Indicator>
33
+ <Alert.Content>
34
+ <Alert.Title>Payment failed</Alert.Title>
35
+ <Alert.Description>The card was declined.</Alert.Description>
36
+ </Alert.Content>
37
+ <Alert.CloseTrigger label="Dismiss payment failed" onClick={dismiss}>
38
+ <XIcon />
39
+ </Alert.CloseTrigger>
40
+ </Alert.Root>;
41
+ ```
42
+
43
+ | Axis | Values | Default |
44
+ | --------- | ------------------------------------------------ | --------- |
45
+ | `status` | `info`, `success`, `warning`, `error`, `neutral` | `info` |
46
+ | `variant` | `solid`, `subtle`, `surface`, `outline`, `plain` | `subtle` |
47
+ | `size` | `sm`, `md`, `lg` | `md` |
48
+ | `layout` | `stacked`, `inline` | `stacked` |
49
+ | `radius` | `l1`, `l2`, `l3`, `full` | `l3` |
50
+ | `edge` | `top`, `bottom`, `end` | none |
51
+ | `motion` | `fade`, `rise`, `reveal` | none |
52
+
53
+ | Part | Element | What it renders |
54
+ | -------------- | -------- | ------------------------------------------------- |
55
+ | `Root` | `div` | The container, with the role of its `live` level |
56
+ | `Indicator` | `div` | The icon, sized by the recipe, `aria-hidden` |
57
+ | `Content` | `div` | The title and the description |
58
+ | `Title` | `span` | The headline, which states the severity |
59
+ | `Description` | `span` | The supporting text |
60
+ | `Aside` | `div` | Trailing controls such as a retry button |
61
+ | `CloseTrigger` | `button` | The dismiss control, named by `label` (`Dismiss`) |
62
+
63
+ `live` sets the role that announces the alert:
64
+
65
+ | `live` | Role | Use it for |
66
+ | ----------- | -------- | ---------------------------------------- |
67
+ | `assertive` | `alert` | An alert raised by a user action |
68
+ | `polite` | `status` | Progress that can wait for a pause |
69
+ | `off` | none | A notice present from the initial render |
70
+
71
+ The default is `polite`. Keep a live region mounted and empty and write into it, because screen
72
+ readers do not reliably announce a region mounted with its text.
73
+
74
+ State the severity in the title. The indicator is `aria-hidden`, and an alert that conveys its
75
+ status through color and icon alone fails WCAG 1.4.1.
76
+
77
+ `Alert.CloseTrigger` takes the alert's ink, so it matches every look and status. On the solid look
78
+ it hovers to a tint of the contrast ink and draws its focus ring in the contrast ink. The box is
79
+ `max(24px, 1.5em)` square, and its glyph is aligned with the padding edge. Pass a `label` that
80
+ includes the notice's title. Label each control in `Alert.Aside` with its target, such as
81
+ `Retry the payment`.
82
+
83
+ `edge` draws a border in the palette's `solid` along one edge, so the rule follows the rounded
84
+ corners. `end` is the right edge in a left-to-right document and the left edge in a right-to-left
85
+ one.
86
+
87
+ `Alert.Title` is a `span`. Pass `as="h2"` for a notice that stays on the page and belongs in the
88
+ outline. Under forced colors the root draws a hairline `CanvasText` outline.
19
89
 
20
90
  ## Skeleton
21
91
 
22
- Stands in for content that has not arrived. It wraps that content rather than replacing it, so a
23
- caller writes one tree and sets one prop, and the stand-in comes out the size of the thing it stands
24
- in for without anybody stating a width. While it waits it hides everything inside it. Once the
25
- content arrives the stand-in fades out and the content is shown.
92
+ Renders a placeholder over content that is still loading. Wrap the content in the skeleton, so the
93
+ placeholder takes the content's size and the layout does not shift when `loading` turns off.
26
94
 
27
95
  ```tsx
28
96
  import { Skeleton } from "@stealthscale/component-feedback";
@@ -39,15 +107,14 @@ import { Skeleton } from "@stealthscale/component-feedback";
39
107
  | `motion` | `none`, `pulse`, `shimmer` | `pulse` |
40
108
  | `radius` | `l1`, `l2`, `l3`, `full` | `l2` |
41
109
 
42
- The element carries no role. A reader learns more from being told the region is busy than from being
43
- told each box is, so a page states `aria-busy` on whatever is waiting and each bar announces nothing
44
- of its own.
110
+ The element has no role. Set `aria-busy` on the region that is loading. The animations stop under
111
+ reduced motion. Under forced colors a loading skeleton draws a hairline `GrayText` outline, because
112
+ the browser replaces its fill.
45
113
 
46
114
  ## SkeletonText
47
115
 
48
- Stands in for a paragraph of text. Every length is read off the line it stands in for, so a bar is
49
- one line tall and the space between two is half a line. Place it where text is read and it comes out
50
- the size of that text, so the page does not jump when the real words replace it.
116
+ Renders a placeholder paragraph with one bar per line. Each bar is one line height tall and the bars
117
+ are half a line height apart, both in `lh` units of the surrounding text.
51
118
 
52
119
  ```tsx
53
120
  import { SkeletonText } from "@stealthscale/component-feedback";
@@ -56,22 +123,364 @@ import { SkeletonText } from "@stealthscale/component-feedback";
56
123
  <SkeletonText lines={5} motion="shimmer" />;
57
124
  ```
58
125
 
59
- The last bar of several is short, because a paragraph rarely fills its final line and a block of
60
- bars all one width reads as a table rather than as writing. `lines` defaults to three and never
61
- draws fewer than one. Draw this while the text is loading and draw the text itself once it arrives,
62
- so nothing here takes a loading state.
126
+ With two or more lines the last bar is 80% wide. `lines` defaults to 3 and renders at least one bar.
127
+ Render the placeholder while the text loads and the text once it arrives. The component has no
128
+ `loading` prop.
129
+
130
+ ## Spinner
131
+
132
+ Shows that work is running when there is no progress to report. Where the work reports how far along
133
+ it is, show `Progress` instead.
134
+
135
+ ```tsx
136
+ import { Button } from "@stealthscale/component-actions";
137
+ import { Spinner } from "@stealthscale/component-feedback";
138
+
139
+ <Spinner />;
140
+ <Spinner palette="primary" size="lg" stroke="heavy" track />;
141
+ <Spinner effect="glow" palette="accent" />;
142
+ <Button disabled>
143
+ <Spinner size="inherit" />
144
+ Saving
145
+ </Button>;
146
+ ```
147
+
148
+ | Axis | Values | Default |
149
+ | --------- | ----------------------------------------------------------------------------- | ----------- |
150
+ | `size` | `inherit`, `xs`, `sm`, `md`, `lg`, `xl`, `2xl`, `3xl`, `4xl` | `md` |
151
+ | `palette` | `current`, `primary`, `secondary`, `accent`, `neutral`, and the four statuses | `current` |
152
+ | `stroke` | `hairline`, `control`, `indicator`, `heavy` | `indicator` |
153
+ | `track` | `true` | off |
154
+ | `effect` | `glow`, `pulse` | none |
155
+
156
+ The element is an empty `span` with no role, so a screen reader skips it. Write the words the wait
157
+ needs beside it, and set `aria-busy` on the region that is waiting.
158
+
159
+ `palette` draws the arc in the palette's `solid` color. The theme engine keeps every `solid` at 3:1
160
+ or more against the page. `current` draws it in the surrounding ink, which is the label ink inside a
161
+ button. `inherit` sizes the spinner to the surrounding font size, so it keeps a line of text or a
162
+ button at its height.
163
+
164
+ `stroke` reads the theme's semantic widths, so a theme with heavier controls draws a heavier ring.
165
+ `heavy` is the 4px step, heavier than the `indicator` stroke of every published theme.
166
+
167
+ The arc turns on the theme's `spin` animation style, so it stops when the reader prefers reduced
168
+ motion. The words beside it still say what is happening. In forced colors mode the arc is drawn in
169
+ `CanvasText` and the track in `GrayText`.
170
+
171
+ ## Loader
172
+
173
+ Shows that something is loading, beside words or over the content it replaces. `LoaderOverlay`
174
+ covers a positioned container while its content loads.
175
+
176
+ ```tsx
177
+ import { Button } from "@stealthscale/component-actions";
178
+ import { Loader, LoaderOverlay } from "@stealthscale/component-feedback";
179
+ import { Card } from "@stealthscale/component-surfaces";
180
+
181
+ <Loader text="Matching" />;
182
+ <Loader placement="end" palette="primary" text="Matching" />;
183
+ <Button disabled={saving}>
184
+ <Loader label="Saving changes" loading={saving}>
185
+ Save changes
186
+ </Loader>
187
+ </Button>;
188
+ <Card.Root>
189
+ …
190
+ <LoaderOverlay scrim="glass">
191
+ <Loader text="Matching" />
192
+ </LoaderOverlay>
193
+ </Card.Root>;
194
+ ```
195
+
196
+ | Axis | Values | Default |
197
+ | --------- | ------------------------------------------------------------------ | --------- |
198
+ | `palette` | `primary`, `secondary`, `accent`, `neutral`, and the four statuses | inherited |
199
+ | `scrim` | `veil`, `glass`, `none`, on `LoaderOverlay` | `veil` |
200
+
201
+ | Prop | Type | Default | Effect |
202
+ | ----------- | -------------- | --------- | -------------------------------------------------- |
203
+ | `loading` | `boolean` | `true` | With `false`, the children render unchanged |
204
+ | `text` | `ReactNode` | none | Renders the words beside the spinner, not children |
205
+ | `placement` | `start`, `end` | `start` | The side of the words the spinner is rendered on |
206
+ | `label` | `string` | `Loading` | Read by screen readers in place of hidden children |
207
+ | `spinner` | `ReactNode` | a spinner | Replaces the default spinner |
208
+
209
+ With `text`, the loader renders the spinner and the words in one inline row, so they stay on one
210
+ line in any container. Without `text`, the loader hides its children with `visibility: hidden` and
211
+ centres the spinner over them in the same grid cell. The children keep their box, so a button keeps
212
+ its width and a row does not reflow when the content returns. The loader needs no positioned
213
+ ancestor.
214
+
215
+ Hidden children leave the accessibility tree, so the loader renders `label` in their place for
216
+ screen readers. Set `aria-busy` on the region that is loading.
217
+
218
+ The default spinner is at `inherit` and `current`, so it takes the size and the ink of the
219
+ surrounding text. `palette` draws it in the palette's `solid` color instead. Pass `spinner` to
220
+ change its stroke, track or effect.
221
+
222
+ `LoaderOverlay` is absolutely positioned at `inset: 0` and takes its container's corner radius, so
223
+ the container must be positioned, as `Card.Root` is. `veil` fills it with the panel at 80% opacity
224
+ and `glass` blurs the content behind it.
225
+
226
+ ## Progress
227
+
228
+ Reports how far a task has come. The root runs the Zag progress machine. The track is the progress
229
+ bar a screen reader reports, with `role="progressbar"`, the value range and the formatted value.
230
+
231
+ ```tsx
232
+ import { Progress } from "@stealthscale/component-feedback";
233
+
234
+ <Progress.Root value={62}>
235
+ <Progress.Label>Reconciling payouts</Progress.Label>
236
+ <Progress.ValueText />
237
+ <Progress.Track>
238
+ <Progress.Range />
239
+ </Progress.Track>
240
+ </Progress.Root>;
241
+ <Progress.Root value={null}>
242
+ <Progress.Label>Preparing the export</Progress.Label>
243
+ <Progress.Track>
244
+ <Progress.Range />
245
+ </Progress.Track>
246
+ </Progress.Root>;
247
+ ```
248
+
249
+ | Part | Element | What it renders |
250
+ | ----------- | ------- | --------------------------------------------------------------------------- |
251
+ | `Root` | `div` | A grid of the label, the value and the track, with the machine and variants |
252
+ | `Label` | `span` | The words that name the bar |
253
+ | `ValueText` | `span` | Its children, or the formatted value, `62%` by default |
254
+ | `Track` | `div` | The progress bar, with the machine's role and value range |
255
+ | `Range` | `div` | The fill, as wide as the value's share of the track |
256
+ | `Segment` | `span` | One part of the fill, as wide as its value's share of the range |
257
+ | `Marker` | `div` | A tick across the track at a value, such as a target |
258
+
259
+ | Axis | Values | Default |
260
+ | ---------- | ------------------------------------------------------------------ | --------- |
261
+ | `size` | `xs`, `sm`, `md`, `lg`, `xl` | `md` |
262
+ | `variant` | `subtle`, `outline` | `outline` |
263
+ | `shape` | `square`, `rounded`, `full` | `full` |
264
+ | `palette` | `primary`, `secondary`, `accent`, `neutral`, and the four statuses | `primary` |
265
+ | `layout` | `stacked`, `inline` | `stacked` |
266
+ | `striped` | `true` | off |
267
+ | `animated` | `true` | off |
268
+ | `effect` | `glow`, `pulse` | none |
269
+
270
+ `value` sets the value between `min` and `max`, 0 and 100 by default. Without `value` or
271
+ `defaultValue` the machine starts at the midpoint. `null` is a value the machine does not know. The
272
+ track then leaves out `aria-valuenow`, the value text is empty, and a segment of the range crosses
273
+ the track once every 1.2 seconds. `formatOptions` and `locale` format the value with
274
+ `Intl.NumberFormat`, as a percentage by default.
275
+
276
+ The size is the track's thickness: 4, 6, 8, 12 and 16px. The label and the value keep the label
277
+ style at every size. `inline` puts the label, the track and the value on one row, and the track
278
+ takes the room the words leave. `animated` moves the stripes at 40px a second, adding them where
279
+ `striped` is not set. Both motions stop for a reader who asked for less motion, and the segment then
280
+ spreads over the whole track.
281
+
282
+ ### Markers and segments
283
+
284
+ ```tsx
285
+ <Progress.Root max={1200} value={864}>
286
+ <Progress.Label>Test run</Progress.Label>
287
+ <Progress.ValueText>864 of 1,200</Progress.ValueText>
288
+ <Progress.Track aria-valuetext="864 of 1,200">
289
+ <Progress.Segment color="success" value={812} />
290
+ <Progress.Segment color="error" value={12} />
291
+ <Progress.Segment color="neutral" value={40} />
292
+ <Progress.Marker value={900} />
293
+ </Progress.Track>
294
+ </Progress.Root>
295
+ ```
296
+
297
+ - `Marker` places a tick at its `value`'s share of the range, from `min` to `max`. A value past
298
+ either end is placed at that end, inside the track.
299
+ - The tick is as wide as the theme's `indicator` border, 2px unless the theme states another, and
300
+ 150% of the track's thickness. It is painted in `fg` with a hairline edge in `bg.panel`, so the
301
+ tick or its edge contrasts with the range and with the track.
302
+ - `Segment` fills the track from its start in the order the segments render, in place of `Range`.
303
+ Each segment is as wide as its `value`'s share of `max - min`.
304
+ - The first eight segments take `series.1` to `series.8` in order, the colors a chart gives its
305
+ series, and the ninth starts again at `series.1`. `color` picks one series color, or a palette
306
+ whose `solid` fills the segment.
307
+ - The first segment takes the track's start corners and the last segment takes its end corners.
308
+ Segments that add up to more than the range shrink in proportion.
309
+ - The track reports the root's `value`, so pass the segments' sum.
310
+ - Label each color in a key beside the bar. Compose the key from the data package's `ColorSwatch`,
311
+ with `var(--colors-series-1)` for a series color, or from its `Status` for a palette.
312
+
313
+ ### Accessibility
314
+
315
+ - A rendered `Progress.Label` names the track through `aria-labelledby`. A bar without a label takes
316
+ `aria-label` on `Progress.Track`.
317
+ - The marker and the segments are hidden from assistive technology. Write the marked value in
318
+ `ValueText` and in the track's `aria-valuetext`, and list the parts in a key.
319
+ - The track states the formatted value as `aria-valuetext`. For a count in the caller's units, pass
320
+ `aria-valuetext` to the track and the same words to `Progress.ValueText`.
321
+ - `Progress.ValueText` is not a live region, so a screen reader does not announce every change. Pass
322
+ `aria-live="polite"` to announce them.
323
+ - Under forced colors the range fills with `Highlight`, and the track has a hairline `CanvasText`
324
+ outline. The marker is `CanvasText` with a `Canvas` edge. The segments keep their colors, and a
325
+ `Canvas` hairline separates each segment from the one before it.
326
+
327
+ ### Machine options
328
+
329
+ The root takes `value`, `defaultValue`, `onValueChange`, `min`, `max`, `formatOptions`, `locale`,
330
+ `id`, `ids` and `dir`. It does not take `orientation`, because the recipe styles a horizontal track
331
+ only, or `translations`, because the words are the caller's. The machine's `view` part is not
332
+ offered. Its circle parts are `ProgressCircle`.
333
+
334
+ ## ProgressCircle
335
+
336
+ Reports how far a task has come as a ring, with the value in its middle. It runs the same machine as
337
+ `Progress`. The ring is the progress bar a screen reader reports.
338
+
339
+ ```tsx
340
+ import { ProgressCircle } from "@stealthscale/component-feedback";
341
+
342
+ <ProgressCircle.Root size="lg" value={62}>
343
+ <ProgressCircle.Circle>
344
+ <ProgressCircle.Track />
345
+ <ProgressCircle.Range />
346
+ </ProgressCircle.Circle>
347
+ <ProgressCircle.ValueText />
348
+ <ProgressCircle.Label>Uploading the report</ProgressCircle.Label>
349
+ </ProgressCircle.Root>;
350
+ ```
351
+
352
+ | Part | Element | What it renders |
353
+ | ----------- | -------- | ---------------------------------------------------------- |
354
+ | `Root` | `div` | A grid of the ring, the value in its middle and the label |
355
+ | `Circle` | `svg` | The progress bar, with the machine's role and value range |
356
+ | `Track` | `circle` | The whole ring |
357
+ | `Range` | `circle` | The part of the ring from the top to the value |
358
+ | `ValueText` | `span` | Its children, or the formatted value, in the ring's middle |
359
+ | `Label` | `span` | The words that name the ring, under it or beside it |
360
+
361
+ | Axis | Values | Default |
362
+ | --------- | ------------------------------------------------------------------ | --------- |
363
+ | `size` | `xs`, `sm`, `md`, `lg`, `xl` | `md` |
364
+ | `variant` | `subtle`, `outline` | `outline` |
365
+ | `shape` | `square`, `full` | `full` |
366
+ | `palette` | `primary`, `secondary`, `accent`, `neutral`, and the four statuses | `primary` |
367
+ | `layout` | `stacked`, `inline` | `stacked` |
368
+
369
+ - The ring is 24, 32, 40, 48 and 64px wide, and 4, 4, 4, 6 and 8px thick, from `xs` to `xl`.
370
+ - The value text shows from `md` up, in the largest text style whose "100%" fits inside the ring.
371
+ `xs` and `sm` hide it. The ring's `aria-valuetext` states the value at every size.
372
+ - `stacked` puts the label under the ring. `inline` puts it beside the ring.
373
+ - The root is as wide as its content. A flex column or a grid does not stretch it.
374
+ - `shape` rounds or squares the ends of the range.
375
+ - `outline` strokes the track in the muted ground. `subtle` strokes it in the palette's muted role.
376
+ - `null` is a value the machine does not know. The ring then leaves out `aria-valuenow`. A quarter
377
+ arc turns once every 1.2 seconds.
378
+ - For a reader who asked for less motion, the ring stops, and the range is a dashed line around the
379
+ whole ring.
380
+ - The root takes the same machine options as `Progress.Root`.
381
+
382
+ ### Accessibility
383
+
384
+ - A rendered `ProgressCircle.Label` names the ring through `aria-labelledby`. A ring without a label
385
+ takes `aria-label` on `ProgressCircle.Circle`.
386
+ - The ring states the formatted value as `aria-valuetext`. For a count in the caller's units, pass
387
+ `aria-valuetext` to `ProgressCircle.Circle`. Pass the short form, such as `3/5`, as the children
388
+ of `ProgressCircle.ValueText`.
389
+ - `ProgressCircle.ValueText` is not a live region. Pass `aria-live="polite"` to announce each
390
+ change.
391
+ - Under forced colors the range strokes in `Highlight`. The track is a hairline `CanvasText` circle
392
+ at the ring's outer edge.
393
+
394
+ ## Meter
395
+
396
+ Shows a measurement within a known range, such as the storage an account uses. A meter is the
397
+ progress bar under a root of its own: `Meter.Root` renders `Progress.Root` with the track in the
398
+ `meter` role, and every other part is the progress bar's. The track is the meter a screen reader
399
+ reports, with the value range and the formatted value. A meter's value changes only when its caller
400
+ measures again. A progress bar reports a task that is still running.
401
+
402
+ ```tsx
403
+ import { Meter } from "@stealthscale/component-feedback";
404
+
405
+ <Meter.Root formatOptions={{ style: "unit", unit: "gigabyte" }} max={50} value={38.2}>
406
+ <Meter.Label>Storage used</Meter.Label>
407
+ <Meter.ValueText />
408
+ <Meter.Track>
409
+ <Meter.Range />
410
+ </Meter.Track>
411
+ </Meter.Root>;
412
+ <Meter.Root max={268} palette="success" value={268}>
413
+ <Meter.Label>Deals closed this quarter</Meter.Label>
414
+ <Meter.ValueText>268 / 250</Meter.ValueText>
415
+ <Meter.Track aria-valuetext="268, target 250">
416
+ <Meter.Range />
417
+ <Meter.Marker value={250} />
418
+ </Meter.Track>
419
+ </Meter.Root>;
420
+ ```
421
+
422
+ | Part | Element | What it renders |
423
+ | ----------- | ------- | --------------------------------------------------------------- |
424
+ | `Root` | `div` | The progress bar's root, with the track in the `meter` role |
425
+ | `Label` | `span` | The words that name the meter |
426
+ | `ValueText` | `span` | Its children, or the formatted value, `62%` by default |
427
+ | `Track` | `div` | The meter, with `role="meter"` and the value range |
428
+ | `Range` | `div` | The fill, as wide as the value's share of the track |
429
+ | `Segment` | `span` | One part of the fill, such as the photos in a disk's used space |
430
+ | `Marker` | `div` | A tick across the track at a value, such as a target |
431
+
432
+ | Axis | Values | Default |
433
+ | --------- | ------------------------------------------------------------------ | --------- |
434
+ | `size` | `xs`, `sm`, `md`, `lg`, `xl` | `md` |
435
+ | `variant` | `subtle`, `outline` | `outline` |
436
+ | `shape` | `square`, `rounded`, `full` | `full` |
437
+ | `palette` | `primary`, `secondary`, `accent`, `neutral`, and the four statuses | `primary` |
438
+ | `layout` | `stacked`, `inline` | `stacked` |
439
+
440
+ - `value` is a required number between `min` and `max`, 0 and 100 by default, and never `null`.
441
+ `formatOptions` and `locale` format it with `Intl.NumberFormat`, as a percentage unless
442
+ `formatOptions` states another style.
443
+ - The meter renders the progress recipe, so its elements take the progress bar's classes, such as
444
+ `progress__track`, and the tracks of a meter and a progress bar in one column start and end at the
445
+ same edges.
446
+ - The root does not take `defaultValue` or `onValueChange`, because only the caller changes a
447
+ meter's value.
448
+ - The root does not take `striped`, `animated` or `effect`. Those motions mark work under way, and a
449
+ meter reports a measurement.
450
+ - The caller picks `palette` from its own threshold, because a share is neither good nor bad by
451
+ itself: 90% is good news for a download and bad news for a disk.
452
+ - For a measure against a target, set `max` to the larger of the two and mark the target with
453
+ `Meter.Marker`. A measure past its target then fills the track.
454
+ - `Meter.Segment` and `Meter.Marker` work as they do on the progress bar.
455
+
456
+ ### Accessibility
457
+
458
+ - A rendered `Meter.Label` names the track through `aria-labelledby`. A meter without a label takes
459
+ `aria-label` on `Meter.Track`.
460
+ - The track states the formatted value as `aria-valuetext`. For a value in words, such as a
461
+ password's strength or a target, pass `aria-valuetext` to the track and the same words to
462
+ `Meter.ValueText`.
463
+ - `Meter.ValueText` is not a live region.
464
+ - A meter split into the parts of a whole sets `value` to `max` and reports 100%. Pass the parts as
465
+ its `aria-valuetext`, joined with `Intl.ListFormat`.
466
+
467
+ ### Not offered
468
+
469
+ - Thresholds such as the `low`, `high` and `optimum` of the HTML `meter` element. The caller picks
470
+ the palette.
471
+ - An orientation.
63
472
 
64
473
  ## EmptyState
65
474
 
66
- Draws the panel a page shows where there is nothing to show, composed as `EmptyState.Root` holding a
67
- mark, a heading and a line saying what would be here.
475
+ Renders the panel a surface shows when it has no content. Compose it as `EmptyState.Root` around an
476
+ `EmptyState.Content` column of an icon, a title and a description.
68
477
 
69
478
  ```tsx
70
479
  import { EmptyState } from "@stealthscale/component-feedback";
71
480
 
72
481
  <EmptyState.Root size="lg">
73
482
  <EmptyState.Content>
74
- <EmptyState.Indicator aria-hidden>
483
+ <EmptyState.Indicator>
75
484
  <InboxIcon />
76
485
  </EmptyState.Indicator>
77
486
  <EmptyState.Title>No invoices yet</EmptyState.Title>
@@ -80,21 +489,89 @@ import { EmptyState } from "@stealthscale/component-feedback";
80
489
  </EmptyState.Root>;
81
490
  ```
82
491
 
83
- | Axis | Values | Default |
84
- | ------ | ------------------------------------------------- | ------- |
85
- | `size` | `xs`, `sm`, `md`, `lg`, `xl`, `2xl`, `3xl`, `4xl` | `md` |
492
+ | Axis | Values | Default |
493
+ | ------ | ---------------- | ------- |
494
+ | `size` | `sm`, `md`, `lg` | `md` |
495
+
496
+ `size` sets four values together:
86
497
 
87
- One axis sets four parts together. The room inside the panel, the gap in the content, the box of the
88
- mark and the size of the title each read the scale of that name, so a small panel in a sidebar and a
89
- large one filling a page are one name apart. The description holds its size, because a line of
90
- explanation is read at the size the rest of the page is read at however big the panel is.
498
+ | Size | Icon | Title | Gap | Inset |
499
+ | ---- | ---- | ------------ | ---- | ----- |
500
+ | `sm` | 32px | `heading.xs` | 8px | 24px |
501
+ | `md` | 40px | `heading.sm` | 12px | 32px |
502
+ | `lg` | 50px | `heading.md` | 16px | 40px |
503
+
504
+ The icon has twice the gap below it, so the title and the description read as one group. The
505
+ description stays at `body.sm`. Pass the icon without a size. The indicator is `aria-hidden` by
506
+ default, because the title states the same thing in words.
507
+
508
+ The title is an `h2`. Pass a deeper level with `as` where the page outline requires it. The root has
509
+ no role.
510
+
511
+ ## Toast
512
+
513
+ Raises a short message into a region fixed to an edge of the window. Create one toaster in a module
514
+ the application shares, render its `Toast.Region` once at the application's root, and raise toasts
515
+ into the toaster from anywhere.
516
+
517
+ ```tsx
518
+ import { XIcon } from "lucide-react";
519
+
520
+ import { Toast } from "@stealthscale/component-feedback";
521
+
522
+ export const toaster = Toast.createToaster({ placement: "bottom-end" });
523
+
524
+ <Toast.Region toaster={toaster}>
525
+ {(toast) => (
526
+ <Toast.Root>
527
+ <Toast.Content>
528
+ <Toast.Title>{toast.title}</Toast.Title>
529
+ <Toast.Description>{toast.description}</Toast.Description>
530
+ </Toast.Content>
531
+ <Toast.CloseTrigger>
532
+ <XIcon />
533
+ </Toast.CloseTrigger>
534
+ </Toast.Root>
535
+ )}
536
+ </Toast.Region>;
537
+
538
+ toaster.success({ description: "€1,240.00 is on its way.", title: "Payout sent" });
539
+ ```
91
540
 
92
- The mark fills whatever box its size states, so a caller passes a glyph without sizing it. Hide it
93
- with `aria-hidden`, since the title beneath says the same thing in words. The title is an `h2`,
94
- which suits a panel standing in for a section's content. A page whose outline puts it deeper states
95
- its own level with `as`.
541
+ | Part | Element | What it renders |
542
+ | --------------- | -------- | -------------------------------------------------------------- |
543
+ | `Region` | `div` | The region of one toaster, and each toast through its function |
544
+ | `Root` | `div` | One toast, with `role="status"` |
545
+ | `Indicator` | `span` | The caller's glyph in the type's palette, `aria-hidden` |
546
+ | `Content` | `div` | The column of the title and the description |
547
+ | `Title` | `div` | The title the toast is named by |
548
+ | `Description` | `div` | The text the toast is described by |
549
+ | `ActionTrigger` | `button` | The button that runs the toast's action and dismisses it |
550
+ | `CloseTrigger` | `button` | The button that dismisses the toast |
96
551
 
97
- The root carries no role. A page of empty panels would otherwise be a page of landmarks.
552
+ - `Toast.createToaster` takes the store's options: `placement` (`top-start`, `top`, `top-end`,
553
+ `bottom-start`, `bottom`, `bottom-end`, default `bottom`), `max` (24), `overlap`, `gap` (16px),
554
+ `offsets` (`1rem`), `duration`, `removeDelay` (200 ms), `hotkey` (Alt+T) and `pauseOnPageIdle`
555
+ (true). Its toaster raises toasts with `create`, `success`, `error`, `warning`, `info`, `loading`
556
+ and `promise`, and changes them with `update`, `dismiss`, `pause` and `resume`.
557
+ - The region is a `region` landmark named `<label>, <placement> (alt+T)`, with `label`
558
+ `Notifications` by default, and announces each new toast through `aria-live="polite"`. Alt+T moves
559
+ focus to it. When the toast that has focus leaves, focus returns to the element it entered the
560
+ region from. The region's id comes from the placement, so render one region per placement.
561
+ - A toast closes after 5 seconds, a `success` toast after 2 and a `loading` toast never. `duration`
562
+ sets another. A pointer resting on the region or focus inside it pauses every toast, and the
563
+ region pauses every toast while the page is hidden.
564
+ - The type sets the toast's palette: `success`, `error`, `warning`, `info`, and `neutral` for
565
+ `loading`. The indicator inks the caller's glyph in the palette's solid. Pass a `Spinner` for a
566
+ loading toast.
567
+ - A toast is in the tab order, and Escape dismisses the focused toast. The close trigger is named
568
+ "Dismiss notification" unless you pass `aria-label`.
569
+ - With `overlap`, the toasts overlap until a pointer rests on the region or focus enters it. Past
570
+ `max`, the toaster queues toasts, errors first.
571
+ - A toast is at most `sizes.sm` wide, 384px at the default scale, and never wider than the window
572
+ less the offsets. The recipe has no axes.
573
+ - A missed toast is gone. Put nothing a reader needs only in a toast, and give a toast with an
574
+ action a longer `duration`.
98
575
 
99
576
  ## Licence
100
577