@scalewing/react 1.27.0 → 1.29.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 (67) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +99 -3
  3. package/dist/components/Checkbox.js +5 -4
  4. package/dist/components/DenominationGrid.d.ts +4 -1
  5. package/dist/components/DenominationGrid.js +8 -3
  6. package/dist/components/Dialog.d.ts +28 -4
  7. package/dist/components/Dialog.js +20 -2
  8. package/dist/components/GlyphTile.d.ts +21 -0
  9. package/dist/components/GlyphTile.js +17 -0
  10. package/dist/components/MultiSelect.d.ts +10 -0
  11. package/dist/components/MultiSelect.js +62 -0
  12. package/dist/components/Notice.d.ts +69 -0
  13. package/dist/components/Notice.js +48 -0
  14. package/dist/components/Progress.d.ts +3 -1
  15. package/dist/components/Progress.js +4 -0
  16. package/dist/components/SegmentedControl.d.ts +8 -0
  17. package/dist/components/SegmentedControl.js +4 -3
  18. package/dist/components/Toast.d.ts +20 -4
  19. package/dist/components/Toast.js +44 -21
  20. package/dist/components/checkbox/CheckboxControl.d.ts +9 -0
  21. package/dist/components/checkbox/CheckboxControl.js +9 -0
  22. package/dist/components/denomination-grid/DenominationStrip.d.ts +5 -2
  23. package/dist/components/denomination-grid/DenominationStrip.js +2 -2
  24. package/dist/components/denomination-grid/DenominationTiles.d.ts +5 -2
  25. package/dist/components/denomination-grid/DenominationTiles.js +2 -2
  26. package/dist/components/denomination-grid/types.d.ts +9 -0
  27. package/dist/components/dialog/DialogFooter.d.ts +14 -0
  28. package/dist/components/dialog/DialogFooter.js +11 -0
  29. package/dist/components/dialog/DialogTitle.d.ts +11 -4
  30. package/dist/components/dialog/DialogTitle.js +8 -6
  31. package/dist/components/multi-select/MultiSelectPanel.d.ts +25 -0
  32. package/dist/components/multi-select/MultiSelectPanel.js +41 -0
  33. package/dist/components/multi-select/types.d.ts +35 -0
  34. package/dist/components/multi-select/types.js +1 -0
  35. package/dist/components/multi-select/validate.d.ts +3 -0
  36. package/dist/components/multi-select/validate.js +25 -0
  37. package/dist/components/toast/ToastContent.d.ts +22 -0
  38. package/dist/components/toast/ToastContent.js +21 -0
  39. package/dist/components/toast/use-toast-hold.d.ts +28 -0
  40. package/dist/components/toast/use-toast-hold.js +53 -0
  41. package/dist/components/toast/use-toast-timer.d.ts +15 -0
  42. package/dist/components/toast/use-toast-timer.js +34 -0
  43. package/dist/css/css-components.js +16 -0
  44. package/dist/css/css-denomination-grid.js +4 -0
  45. package/dist/css/css-denomination-label.d.ts +10 -0
  46. package/dist/css/css-denomination-label.js +36 -0
  47. package/dist/css/css-dialog-frame.d.ts +2 -0
  48. package/dist/css/css-dialog-frame.js +83 -0
  49. package/dist/css/css-dialog.js +20 -4
  50. package/dist/css/css-glyph-tile.d.ts +5 -0
  51. package/dist/css/css-glyph-tile.js +57 -0
  52. package/dist/css/css-multi-select.d.ts +2 -0
  53. package/dist/css/css-multi-select.js +156 -0
  54. package/dist/css/css-notice.d.ts +22 -0
  55. package/dist/css/css-notice.js +168 -0
  56. package/dist/css/css-progress.d.ts +3 -0
  57. package/dist/css/css-progress.js +13 -6
  58. package/dist/css/css-segmented.js +58 -0
  59. package/dist/css/css-toast-card.d.ts +5 -0
  60. package/dist/css/css-toast-card.js +122 -0
  61. package/dist/css/css-variables.js +12 -1
  62. package/dist/css/stacking.js +5 -2
  63. package/dist/css/stylesheet.js +10 -0
  64. package/dist/index.d.ts +8 -5
  65. package/dist/index.js +7 -4
  66. package/dist/styles.css +522 -3
  67. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,65 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.29.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 98f6f89: `GlyphTile` (with `GlyphTileProps`, `GlyphTileHue`, `GlyphTileSize` and `glyphTileSizes`): a small rounded square in a categorical colour holding the consumer's glyph in white, the mark of a kind beside its name (`docs/requests/teisoro-glyph-tile.md`, Teisoro F-011 release R3).
8
+
9
+ - `hue` (`green`, `blue`, `orange`, `purple`, `teal`, `rose` or `neutral`, from `@scalewing/tokens` categorical colours), `size` (`sm` 24, `md` 28 by default, `lg` 32), `children` (the glyph, drawn at spacing step 4).
10
+ - Always `aria-hidden`; forced colors draw its edge. An unknown `hue` or `size` throws a `RangeError`.
11
+ - New generated custom properties `--sw-categorical-<colour>` and `--sw-categorical-on` in both schemes, and classes `sw-glyph-tile`, `sw-glyph-tile-<hue>` and `sw-glyph-tile-<size>`. Needs `@scalewing/tokens` 1.5.0. No new dependencies.
12
+
13
+ - b8fe06f: `MultiSelect` (with `MultiSelectProps`, `MultiSelectOption` and `MultiSelectAlign`): several choices from a short list behind one trigger (`docs/requests/teisoro-multi-select.md`, Teisoro F-011 release R3).
14
+
15
+ - `label`, `options` (`{ value, label, icon?, separatorBefore? }`), `value`, `onChange` (checked values in the options' order), `allLabel`, `clearLabel`, `summary` (the trigger's words), `disabled`, `align`.
16
+ - A disclosure: a button with `aria-expanded` showing a group of native checkboxes, with All and Clear, on the popover layer beside the trigger; focus moves to the first checkbox, Escape returns it to the trigger, and focus or a press outside closes the panel.
17
+ - Blank words, no or duplicate options, an unknown value or `align` throw a `RangeError`.
18
+ - New generated `sw-multi-select-*` classes; the panel joins the popup layer in `css/stacking.ts`. No new tokens or dependencies.
19
+
20
+ - 4a648a9: `SegmentedControl` item `icon` (`docs/requests/teisoro-segmented-control-icon.md`, Teisoro F-011 release R3): a consumer mark (such as a `GlyphTile`) before an item's label, `aria-hidden`. A track with icons takes the `md` control height and label type, a subtle track fill and a raised selected item; a track without icons is unchanged. New generated classes `sw-segmented-with-icons` and `sw-segmented-item-icon`. No new tokens or dependencies.
21
+
22
+ ## 1.28.0
23
+
24
+ ### Minor Changes
25
+
26
+ - 73e7cf1: `DenominationGrid` `labelAppearance` (with `DenominationLabelAppearance` and `denominationLabelAppearances`): `'tone'` (default, unchanged) or `'plain'`, row labels in the text color at label weight with no tone stripe, in both layouts; a row's tone still colors its total (`docs/requests/teisoro-denomination-grid-label.md`, Teisoro F-011 release R2). An unknown value throws a `RangeError`. New generated class `sw-denomination-labels-plain`. No new tokens or dependencies.
27
+ - 6fc5c25: `Dialog` `description`, `icon`, `footer`, `summary`, `size="xl"` and `role` (with `DialogRole` and `dialogSizes`): a sheet's header and pinned footer (`docs/requests/teisoro-dialog-header-footer.md`, Teisoro F-011 release R2).
28
+
29
+ - `description`: a muted line under the title and the dialog's accessible description. `icon`: a consumer glyph or tile before the title, `aria-hidden`.
30
+ - `footer`: the actions, pinned to the dialog's bottom edge under a hairline while the content scrolls. `summary`: ActionBar's labelled figure at the footer's start, in a polite status region.
31
+ - `size="xl"`: 65rem (`--sw-dialog-max-xl`). `role="alertdialog"` for a warning that interrupts.
32
+ - An unknown `size` or `role`, a blank `description` or an invalid `summary` throws a `RangeError` (an unknown size used to render `md`). `role` is typed `'dialog' | 'alertdialog'` (any other role used to pass through to the element).
33
+ - New generated classes `sw-dialog-xl`, `sw-dialog-header-framed`, `sw-dialog-icon`, `sw-dialog-titles`, `sw-dialog-description`, `sw-dialog-footer`, `sw-dialog-footer-status` and `sw-dialog-footer-actions`. No new tokens or dependencies.
34
+
35
+ - 792f7f5: `Notice` `variant="band"` (with `NoticeVariant` and `noticeVariants`): a full-width square strip of page chrome, such as an environment strip above the app header (`docs/requests/teisoro-environment-strip.md`, Teisoro F-011 release R2).
36
+
37
+ - Square, the container's full width, at least the `xs` control height, caption type, its words centered; the tone's fill and ink as for every notice.
38
+ - Its role defaults to `none` (no live region) unless `role` is given. Forced colors draw a rule under it.
39
+ - An unknown `variant` throws a `RangeError`.
40
+ - New generated class `sw-notice-band`. No new tokens or dependencies.
41
+
42
+ - f543d5f: `Notice` (with `NoticeProps`, `NoticeTone`, `NoticeRole` and `noticeTones`): a tinted note in the page's flow (`docs/requests/teisoro-notice.md`, Teisoro F-011 release R2).
43
+
44
+ - `tone: 'info' | 'success' | 'warning' | 'danger'`, `title?` (a bold lead), `children` (the message), `action?` (one link or small `Button`), `icon?` (a consumer glyph, hidden from assistive technology), `role?: 'status' | 'alert' | 'none'` (danger is an alert, the others a status; `none` is no live region), a forwarded ref.
45
+ - A text message shares the title's row while it fits; a message of elements (a sentence and a `Checkbox` or `Field`) stacks under the title.
46
+ - The fill mixes the tone into the surface (the accent tint for info) and the words move the tone toward the text color, keeping 4.5:1 on the fill in every palette (see the request for the two palettes whose own danger is below that). Forced colors draw a border.
47
+ - An unknown `tone` or `role`, a blank `title`, or nothing to say throws a `RangeError`.
48
+ - New generated classes `sw-notice`, `sw-notice-info`, `sw-notice-success`, `sw-notice-warning`, `sw-notice-danger`, `sw-notice-icon`, `sw-notice-content`, `sw-notice-stacked`, `sw-notice-title`, `sw-notice-body` and `sw-notice-action`. No new tokens or dependencies.
49
+
50
+ - 9e372e1: `Progress` `tone="warning"` (with `progressTones`): the bar in the semantic warning color, such as the time left before an idle sign-out (`docs/requests/teisoro-progress-warning.md`, Teisoro F-011 release R2). New generated class `sw-progress-warning`. An unknown `tone` now throws a `RangeError` instead of rendering an unstyled bar. No new tokens or dependencies.
51
+ - 3d761ba: `Toast` `title`, `action`, `showRemaining` and `placement` (with `ToastAction`, `ToastPlacement` and `toastPlacements`): a corner card that confirms a save (`docs/requests/teisoro-toast-action.md`, Teisoro F-011 release R2).
52
+
53
+ - `title`: a bold first line; the children become a muted detail.
54
+ - `action: { label, onPress }`: a ghost small Button at the end; it runs `onPress`, then closes the toast.
55
+ - `showRemaining`: a thin `aria-hidden` line that runs down over the toast's duration.
56
+ - `placement: 'bottom' | 'top-end'` (default `bottom`, unchanged).
57
+ - A toast with an action or the line takes the pointer, and its time is held while the pointer or focus is on it. A plain toast is unchanged and stays click-through.
58
+ - A travelling toast (`anchor` and `target`) ignores `placement` and `showRemaining`. When the toast closes with focus inside, focus returns to where it came from.
59
+ - `title` is now the toast's bold first line and is no longer forwarded to the element as the HTML `title` attribute.
60
+ - A blank `title` or `action.label`, a missing `onPress`, or an unknown `placement` throws a `RangeError`.
61
+ - New generated classes `sw-toast-title`, `sw-toast-detail`, `sw-toast-action`, `sw-toast-interactive`, `sw-toast-top-end`, `sw-toast-remaining` and `sw-toast-paused`. No new tokens or dependencies.
62
+
3
63
  ## 1.27.0
4
64
 
5
65
  ### Minor Changes
package/README.md CHANGED
@@ -240,8 +240,51 @@ A `Button` that toggles something passes `aria-pressed`. The pressed button gets
240
240
 
241
241
  `Dialog` is a modal `<dialog>` controlled by `open`. Escape, a backdrop press, a platform close request, and a `<form method="dialog">` submit (or a submitter with `formmethod="dialog"`) are each prevented and call `onClose`; none of them closes the dialog itself. A consumer `onKeyDown`, `onCancel`, `onSubmit` or `onPointerDown` that prevents the event vetoes that request. Escape in a search field that holds text clears the field first, and a descendant's own `cancel` (a dismissed file picker) is not a close request. Set `open` to false to close it, or keep it true (for example while a form is saving) and it stays shown. `onClose` is not called when `open` turns false, and it is required: a dialog that must not be dismissed passes a callback that keeps `open` true. The `title` is an `h3` in the title style by default; `titleLevel={2}` renders it as an `h2` in the same style, for a dialog whose own section labels are `h3` headings, so they read as parts of the dialog and not as its peers. The default stays `3` so existing dialogs keep their outline; any value other than `2` renders an `h3`. `sheetBelow="md"` makes the dialog a bottom sheet below `md`: docked to the bottom edge at the full viewport width, top corners rounded and bottom corners square, a `space-8` strip of backdrop above it, and bottom padding that clears `env(safe-area-inset-bottom)` (with `viewport-fit=cover`); it slides up on open unless reduced motion is asked for, and has no grabber, since there is no drag. From `md` up it is the centered dialog at its `size`. `closeLabel` (such as "Close") adds an icon-only ghost close button at the end of the title row, named by the label, which calls `onClose` like Escape; the title still names the dialog. The button is the dialog's first control, so the browser focuses it on open; give a field `autoFocus` to start there instead. Without `closeLabel` the dialog has no close button and its markup is unchanged.
242
242
 
243
+ A sheet over a page takes a fuller frame. `icon` is a consumer glyph or tile before the title (hidden from assistive technology) and `description` a muted line under the title that also describes the dialog (unless you pass `aria-describedby`). `footer` holds the actions, pinned to the dialog's bottom edge under a hairline while the content scrolls, and `summary` (ActionBar's `{ label, value, detail?, tone? }`) puts a labelled figure at the footer's start in a polite live region. `size="xl"` (65rem) holds a six-column count grid with its labels and total. `role="alertdialog"` is for a warning that interrupts, such as an idle sign-out: give its safe action `autoFocus` (the browser otherwise focuses the first footer button), and point `aria-describedby` at the warning in its body when that is not the `description`. An unknown `size` or `role`, a blank `description` or an invalid `summary` throws a `RangeError`.
244
+
245
+ ```tsx
246
+ <Dialog
247
+ open={open}
248
+ onClose={close}
249
+ size="xl"
250
+ closeLabel="Close"
251
+ icon={<ArrowRight />}
252
+ title="Move herons to the lake"
253
+ description="The same herons leave the river count."
254
+ summary={{ label: 'Lake', value: '+14', detail: 'river −14' }}
255
+ footer={
256
+ <>
257
+ <Button variant="secondary" onPress={close}>
258
+ Cancel
259
+ </Button>
260
+ <Button onPress={move}>Move 14 herons</Button>
261
+ </>
262
+ }
263
+ >
264
+ {countGrid}
265
+ </Dialog>
266
+ ```
267
+
243
268
  `Toast` is an auto-dismiss confirmation on the popover layer, controlled by `open` and `onOpenChange`; it does not trap focus and closes after `timeoutMs` (default 800, or 6000 for a `warning` or `danger` toast, which must be read). `anchor` and `target` send it from a press to a destination. `tone` (`neutral` by default, `success`, `warning`, `danger`) tints its border and an optional consumer `icon` (a Lucide glyph, hidden from assistive technology); the message keeps the text color, so its words carry the meaning. A `danger` toast is announced as `role="alert"`, the others as `role="status"`, so do not wrap the message in another live region. A toast still dismisses itself: an error the person must act on belongs in the page. A toast is as wide as its message, up to the page's gutter (spacing step 4) on each side, so on a phone a long message wraps inside the gutter instead of running to the screen's edges.
244
269
 
270
+ A toast can also be a corner card that confirms a save: `placement="top-end"` puts it `space-4` from the viewport's top and end corner, `title` is a bold first line with the children as a muted detail under it (pass plain text, not a `Text`), `action` (`{ label, onPress }`) is one ghost small Button at the end that runs `onPress` and then closes the toast, and `showRemaining` adds a thin line that runs down with the time left. A toast with an action or the line takes the pointer, and its time (and the line) is held while the pointer is over it or focus is inside it; each opening starts with the whole time. Pass the time you want, such as `timeoutMs={10000}`, and close it yourself when it no longer applies (for example when the next sale starts). It is never modal and never takes focus; when it closes with focus inside (after its action), focus goes back to where it came from. Keyboard users reach the action in the Toast's place in the DOM, so render it right after the control that saves. A travelling toast ignores `placement` and `showRemaining`.
271
+
272
+ ```tsx
273
+ <Toast
274
+ open={saved}
275
+ onOpenChange={setSaved}
276
+ placement="top-end"
277
+ tone="success"
278
+ icon={<Check />}
279
+ title="Heron count saved"
280
+ action={{ label: 'Open', onPress: openCount }}
281
+ showRemaining
282
+ timeoutMs={10000}
283
+ >
284
+ 12 herons · 2 nests
285
+ </Toast>
286
+ ```
287
+
245
288
  `Switch` is a controlled on/off input. It keeps native checkbox keyboard behavior and exposes switch semantics. Provide the current `checked` value and update it in `onCheckedChange`.
246
289
 
247
290
  `Checkbox` is a controlled native checkbox for one independent form or confirmation choice. `checked` and `onCheckedChange` own its state. The consumer supplies `error` after validation to describe and style an invalid choice (associated by `aria-describedby` with `aria-invalid` and announced from a polite live region, as `Field`'s error is, never an alert); `required` retains native form semantics.
@@ -284,7 +327,7 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
284
327
  />
285
328
  ```
286
329
 
287
- `SegmentedControl` is one exclusive choice shown as a track of segments (`role="radiogroup"`), named by `aria-label` or `aria-labelledby`, with a controlled `value` and `onChange`; `variant="filled"` gives every segment the same width and fills the selection with the accent, and `disabled` keeps the choice visible but inert. `error` puts a message under the track (`sw-field-error`), links it to the group by `aria-describedby` and sets `aria-invalid` and a danger outline on the group (`Mark` in forced colors); as `Field`'s it is announced politely from a live region that is always there, never as an alert, and an empty string is no error. `required` sets `aria-required` on the group; since the control has no label of its own, the element that labels it shows the mark. The track sits in a `.sw-segmented-field` wrapper that holds the message and takes the track's place in the layout, so a `ref` still reaches the `radiogroup`. `error` is for a control in a `Stack` or block layout, where the track keeps its width and the message wraps under it. While it shows a message the field is at least 24ch wide, capped at its container, so a short compact track does not squeeze the message into a column. In an `Inline` the field therefore grows and the rest of the row moves, while the track keeps its labels' width.
330
+ `SegmentedControl` is one exclusive choice shown as a track of segments (`role="radiogroup"`), named by `aria-label` or `aria-labelledby`, with a controlled `value` and `onChange`; `variant="filled"` gives every segment the same width and fills the selection with the accent, and `disabled` keeps the choice visible but inert. `error` puts a message under the track (`sw-field-error`), links it to the group by `aria-describedby` and sets `aria-invalid` and a danger outline on the group (`Mark` in forced colors); as `Field`'s it is announced politely from a live region that is always there, never as an alert, and an empty string is no error. `required` sets `aria-required` on the group; since the control has no label of its own, the element that labels it shows the mark. The track sits in a `.sw-segmented-field` wrapper that holds the message and takes the track's place in the layout, so a `ref` still reaches the `radiogroup`. `error` is for a control in a `Stack` or block layout, where the track keeps its width and the message wraps under it. While it shows a message the field is at least 24ch wide, capped at its container, so a short compact track does not squeeze the message into a column. In an `Inline` the field therefore grows and the rest of the row moves, while the track keeps its labels' width. An item's `icon` (such as a small `GlyphTile`) sits before its label, hidden from assistive technology, so the label names the segment; when any item has one, a compact track takes the `md` control height and label type on a subtle fill, with the selected item raised, and a filled track keeps its look and only gains the marks.
288
331
 
289
332
  ```tsx
290
333
  <SegmentedControl
@@ -348,7 +391,60 @@ A header `TableCell` (`as="th"`) with `sort` (`'ascending'`, `'descending'` or `
348
391
 
349
392
  `Spinner` shows indeterminate loading in small, medium, or large sizes. Supply localized `label` for the one announced status in a loading region. Use `decorative` on additional indicators beside that status so screen readers do not hear the same message repeatedly. Reduced motion leaves a static accented ring.
350
393
 
351
- `Progress` shows a known value between zero and a positive maximum. It uses native progressbar semantics and displays the value and maximum beside the localized label. Invalid bounds throw instead of silently clamping. Optional `tone` is `accent`, `success`, or `danger`. `showCount={false}` hides the visible `value / max` count when the page shows its own count caption, so the count appears once; the progress bar still exposes its value and maximum.
394
+ `GlyphTile` is a small rounded square in a categorical colour with the consumer's glyph in white: the mark of a kind beside its name, such as a service in a list. `hue` is one of the `@scalewing/tokens` categorical colours (`green`, `blue`, `orange`, `purple`, `teal`, `rose`, or `neutral`); which kind takes which hue is yours. `size` is `sm` (24), `md` (28, the default) or `lg` (32); the glyph is drawn at spacing step 4 in white, which keeps 3:1 on every fill; pass the SVG (a Lucide component) as the tile's direct child, since only a direct-child SVG is sized. It is always `aria-hidden`, so put the kind's name in visible text beside it. An unknown `hue` or `size` throws a `RangeError`.
395
+
396
+ ```tsx
397
+ <Inline gap={2} align="center">
398
+ <GlyphTile hue="teal">
399
+ <Waves />
400
+ </GlyphTile>
401
+ <Text>Reef</Text>
402
+ </Inline>
403
+ ```
404
+
405
+ `MultiSelect` is several choices from a short list behind one trigger, such as which kinds a list shows. The trigger reads the muted `label` and then `summary` (your words for what is chosen, such as "All entries" or "2 services") with a chevron; it opens a panel named by the label with `allLabel` and `clearLabel` commands over one checkbox per option, each with an optional `icon` (use a small `GlyphTile`, never a bare dot) and a hairline before an option with `separatorBefore`. `onChange` reports the checked values in the options' order. It is a disclosure of native checkboxes, not a listbox: the first checkbox takes focus as it opens, Escape closes it and returns focus to the trigger, focus or a press outside closes it, and disabling it while open closes it. Blank words, no or duplicate options, a value that is not an option, or an unknown `align` throw a `RangeError`.
406
+
407
+ ```tsx
408
+ <MultiSelect
409
+ label="Show"
410
+ summary={
411
+ shown.length === options.length ? 'All sightings' : `${shown.length} kinds`
412
+ }
413
+ options={[
414
+ {
415
+ value: 'forest',
416
+ label: 'Forest',
417
+ icon: (
418
+ <GlyphTile hue="green" size="sm">
419
+ <Trees />
420
+ </GlyphTile>
421
+ ),
422
+ },
423
+ { value: 'station', label: 'Station moves', separatorBefore: true },
424
+ ]}
425
+ value={shown}
426
+ onChange={setShown}
427
+ allLabel="All"
428
+ clearLabel="Clear"
429
+ />
430
+ ```
431
+
432
+ `Notice` is a tinted note in the page's flow (`tone`: `info`, `success`, `warning` or `danger`). The fill is the tone mixed into the surface (the accent tint for `info`) and the words are the tone moved toward the text color, so they keep 4.5:1 on the fill in every palette (only where a palette's own `danger` is under 4.5:1 on its surface does the note stay at least as readable as that). `title` is a bold lead; a text message (values interpolated in it included) shares its row while it fits and wraps under it otherwise, and a message with any element (a `span`, or a sentence and a `Checkbox` or `Field` in a `Stack`) is stacked under it. Write the message as plain text or a `span`, not a `Text`, which brings its own color. A control inside keeps its own colors; its muted description takes the notice's ink so it stays readable on the fill. `action` is one link or small `Button` at the end of the row; `icon` is a consumer glyph before the text, hidden from assistive technology. `danger` is `role="alert"` and the other tones `role="status"`; `role="none"` is for a note that is part of the page from the start (no live region). Screen readers announce an `alert` that appears, but not reliably a `status` mounted with its words: to have a status heard, keep the Notice mounted and change its words, or move focus to it. It never dismisses itself. Pass `tabIndex={-1}` and a `ref` to move focus to it. `variant="band"` makes it a full-width square strip of page chrome in caption type with its words centered, at least the `xs` control height, such as an environment strip first on the page (`<Notice tone="warning" variant="band" title="Field station">Records here stay out of the census.</Notice>`). It is as wide as its container, so put it outside any padded column, as the page's first child. A glyph or an action stays on the words' row and the words wrap. A band has no live region unless you pass `role`. An unknown `tone`, `role` or `variant`, a blank `title`, or a note with neither a title nor a message throws a `RangeError`.
433
+
434
+ ```tsx
435
+ <Notice tone="danger" title="$30.00 short.">
436
+ Add the bills or change the amount.
437
+ </Notice>
438
+
439
+ <Notice
440
+ tone="info"
441
+ role="none"
442
+ title="Three surveys are due this week."
443
+ action={<Button size="sm" variant="ghost" onPress={openTasks}>Open tasks</Button>}
444
+ />
445
+ ```
446
+
447
+ `Progress` shows a known value between zero and a positive maximum. It uses native progressbar semantics and displays the value and maximum beside the localized label. Invalid bounds throw instead of silently clamping. Optional `tone` is `accent`, `success`, `warning` (such as the time left before a sign-out) or `danger`; an unknown tone throws. `showCount={false}` hides the visible `value / max` count when the page shows its own count caption, so the count appears once; the progress bar still exposes its value and maximum.
352
448
 
353
449
  `Tooltip` adds supplementary plain-text help to one labelled, focusable trigger, or names an icon-only one (`relationship="label"`, below). Supply localized `content` and an existing trigger element; by default it has its own accessible name. It opens on hover or on a visible focus (`:focus-visible`: the keyboard, not the focus a click or a tap gives), closes on pointer leave, blur, Escape, or outside touch, and a tap on a control (a button, a link, a form control or its label, an editable region, an element with a widget role, or anything inside one) does what the control does and leaves the tooltip closed, so on a touch screen a `Tooltip` on a `Button` is never shown. For an icon-only hint whose button does nothing but show it, such as an ⓘ beside a figure, use `InfoTip` (below), which a tap, a click, Enter or Space opens; do not make a non-control focusable to get a tap. A tap still toggles a `Tooltip` whose trigger does nothing else on a tap, such as an existing focusable badge. The bubble is placed beside its trigger on the top layer while it shows and stays a `space-2` inset inside the screen. Keep required instructions visible outside the tooltip. `disabled` turns the tooltip off, not its trigger: the trigger stays mounted, enabled and focusable, and there is no tooltip, no `aria-describedby` from it, and Escape is left to the page. Use it when what the tooltip says is visible at some widths, such as text shown only from `md` up, instead of rendering the trigger with and without a `Tooltip`, which mounts a new trigger and drops its focus. Focus, hover and touch are still followed, so a trigger that still has focus, the pointer or an open touch toggle shows the tooltip as soon as it is enabled again (Escape pressed while disabled reaches the page and does not stop that). `disabled` must be the same in the server render and the first client render, or hydration mismatches; read a breakpoint through a hydration-safe store, such as `useSyncExternalStore` with a server snapshot. `relationship="label"` makes the tooltip its trigger's name instead of its description, for an icon-only control whose tooltip says what it is: the trigger gets `aria-labelledby` (after its own, if any) and no `aria-describedby` from the tooltip, so the text is read once. The tooltip stays in the page, hidden, so it names the trigger while it is not shown, and it wins over an `aria-label`. While `disabled` there is no tooltip to name the trigger, so give the trigger its own name for that state: visible text, such as a label shown only from `md` up, or an `aria-label`. Do not use `relationship="label"` while the trigger shows visible text that differs from `content`: the tooltip would replace that visible label as the name (WCAG 2.5.3, label in name). The default, `relationship="description"`, is the supplementary help above. `relationship` must also be the same in the server render and the first client render.
354
450
 
@@ -423,7 +519,7 @@ Values outside the catalog throw a `RangeError`. There is no per-breakpoint span
423
519
  </Grid>
424
520
  ```
425
521
 
426
- `DenominationGrid` shows integer counts per unit across a fixed set of columns. The `strip` layout is a captioned table: each row has a toned label with an icon slot, muted zero cells, optional signed deltas toned by sign, and an optional consumer-formatted `total` that shows under the label on a phone. Pass `totalLabel` (for example "Total") to name the total column: it renders a `th scope="col"` shown over the totals from `md` up and visually hidden below it, and without it the corner stays an empty cell as before (either way the column takes no width on a phone). `labelWidth` (`xs` to `xl`, the `Table` column sizes) lines a strip's columns up with every other strip of the same columns in a container of the same width, such as the cards of a feed: the row labels take that width and the count columns share the rest of the region equally, so no content moves a column. On a phone the total cell stays in the table, visually hidden, so a screen reader still reads each total in its row; the copy under the label is `aria-hidden`. A strip wider than its container scrolls sideways inside its own keyboard-focusable region, named after `label`, with the row labels pinned at the start and the same edge shade as `Table` while counts are scrolled out of view (the start shade is cast from the pinned labels' edge, since they cover the region's own); the page never scrolls sideways. The `tiles` layout stacks the column label, the count, and an optional `subtotal` string per column. Each tiles row shows its label line (icon, label and `total`) when the grid has several rows, or when a lone row has an icon or a `total`; a lone plain row is named by the grid's `label` alone. Every row is a region named by its `label`, except a lone plain row whose `label` is the grid's own: it has no name of its own, so assistive technology hears the grid's group once rather than a region repeating it. `rowRole="group"` (default `"region"`) keeps every named row's name but makes it a `group` instead of a landmark region, for a page with many grids (one per order card, say) whose rows would otherwise fill the landmark list; the strip ignores it, since its rows are table rows, and an unknown `rowRole` throws a `RangeError` in either layout. A row's icon and its label words stay on one line in both layouts (`sw-denomination-label-line`): a long label wraps its words beside the icon, never under it, and on a phone the strip's total goes on its own line under them. A row's `cellTones` (one Badge tone or `null` per column) tones a single count in place: the count is set in the tone over its signed or zero look, and in the tiles layout its tile's border takes the tone too, so a short bill turns red without leaving its row; `neutral` or `null` leaves a count as it is, a row's `tone` still colors only its label, and a `cellTones` that is not one known tone or `null` per column throws. The tone never says why on its own; put that in words beside the grid. Every negative count, signed or not and in either layout, is written with the typographic minus ("−1"), never a hyphen. The primitive does no arithmetic and no currency formatting; tones reuse the Badge vocabulary. Invalid columns, rows, or cells throw.
522
+ `DenominationGrid` shows integer counts per unit across a fixed set of columns. The `strip` layout is a captioned table: each row has a toned label with an icon slot, muted zero cells, optional signed deltas toned by sign, and an optional consumer-formatted `total` that shows under the label on a phone. Pass `totalLabel` (for example "Total") to name the total column: it renders a `th scope="col"` shown over the totals from `md` up and visually hidden below it, and without it the corner stays an empty cell as before (either way the column takes no width on a phone). `labelWidth` (`xs` to `xl`, the `Table` column sizes) lines a strip's columns up with every other strip of the same columns in a container of the same width, such as the cards of a feed: the row labels take that width and the count columns share the rest of the region equally, so no content moves a column. `labelAppearance="plain"` (default `"tone"`) sets every row label in the text color at label weight with no tone stripe, in both layouts, for a grid whose rows need no color to tell apart; a row's `tone` still colors its total, and an unknown value throws. On a phone the total cell stays in the table, visually hidden, so a screen reader still reads each total in its row; the copy under the label is `aria-hidden`. A strip wider than its container scrolls sideways inside its own keyboard-focusable region, named after `label`, with the row labels pinned at the start and the same edge shade as `Table` while counts are scrolled out of view (the start shade is cast from the pinned labels' edge, since they cover the region's own); the page never scrolls sideways. The `tiles` layout stacks the column label, the count, and an optional `subtotal` string per column. Each tiles row shows its label line (icon, label and `total`) when the grid has several rows, or when a lone row has an icon or a `total`; a lone plain row is named by the grid's `label` alone. Every row is a region named by its `label`, except a lone plain row whose `label` is the grid's own: it has no name of its own, so assistive technology hears the grid's group once rather than a region repeating it. `rowRole="group"` (default `"region"`) keeps every named row's name but makes it a `group` instead of a landmark region, for a page with many grids (one per order card, say) whose rows would otherwise fill the landmark list; the strip ignores it, since its rows are table rows, and an unknown `rowRole` throws a `RangeError` in either layout. A row's icon and its label words stay on one line in both layouts (`sw-denomination-label-line`): a long label wraps its words beside the icon, never under it, and on a phone the strip's total goes on its own line under them. A row's `cellTones` (one Badge tone or `null` per column) tones a single count in place: the count is set in the tone over its signed or zero look, and in the tiles layout its tile's border takes the tone too, so a short bill turns red without leaving its row; `neutral` or `null` leaves a count as it is, a row's `tone` still colors only its label, and a `cellTones` that is not one known tone or `null` per column throws. The tone never says why on its own; put that in words beside the grid. Every negative count, signed or not and in either layout, is written with the typographic minus ("−1"), never a hyphen. The primitive does no arithmetic and no currency formatting; tones reuse the Badge vocabulary. Invalid columns, rows, or cells throw.
427
523
 
428
524
  A strip row with `entry` makes its counts editable: each column holds a `Stepper` (`width="full"`, its label hidden and named by the row label and the column's label and caption as its header reads, such as "Received $20" or "Counted $1 × 100") whose count is the row's `cells` entry (`null` is an empty box). `entry.onChange(columnKey, value)` reports a change; `entry.decrementLabel` and `entry.incrementLabel` name the buttons; `entry.max` caps each column (`null` for none), `entry.errors` puts a message under a column's count (`null` for none), and `entry.placeholder` fills an empty box. The minimum is 0. The grid still does no arithmetic, so recompute the row's `total` in `onChange`. A strip with an entry row centres its heads and read-only counts over the steppers, and each count keeps spacing step 8 of room, so a narrow strip scrolls in its region instead of crushing it. A `cellTones` tone on an entry count draws its border in the tone (danger for a difference); an `accent` tone, for a suggestion or a change, is the `Stepper`'s `changed` look (border and tint); a message in `entry.errors` wins. A row `caption` is a muted line under its label ("from the customer"), and a column `caption` a muted note after its label ("× 100"). Captions show in both layouts. In the strip, `emphasis="quiet"` sets a row on the subtle fill, for a row to compare against, and `disabled` fades a row that does not apply, disables its entry and marks it `aria-disabled`. Entry rows, quiet rows and disabled rows need the `strip` layout. An entry row's counts are zero or more and it is not `signed`; its label is its own among the entry rows, as each column's label and caption are, so every count has its own name. Breaking any of these, a cap or message list that is not one per column, a cap that is not a whole count of zero or more, blank button names or captions, an unknown `emphasis`, or a `disabled` that is not a boolean throws a `RangeError`.
429
525
 
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useId } from 'react';
4
+ import { CheckboxControl } from './checkbox/CheckboxControl.js';
4
5
  import { FieldErrorRegion } from './FieldErrorRegion.js';
5
6
  export function Checkbox({ label, checked, onCheckedChange, disabled = false, required = false, description, error, }) {
6
7
  const descriptionId = useId();
@@ -11,8 +12,8 @@ export function Checkbox({ label, checked, onCheckedChange, disabled = false, re
11
12
  ]
12
13
  .filter((id) => id !== null)
13
14
  .join(' ');
14
- return (_jsxs("div", { className: "sw-checkbox", children: [_jsxs("label", { className: "sw-checkbox-label", children: [_jsxs("span", { className: "sw-checkbox-control", children: [_jsx("input", { "aria-describedby": describedBy || undefined, "aria-invalid": Boolean(error), checked: checked, className: "sw-checkbox-input", disabled: disabled, onChange: (event) => {
15
- if (!disabled)
16
- onCheckedChange(event.currentTarget.checked);
17
- }, required: required, type: "checkbox" }), _jsx("span", { "aria-hidden": "true", className: "sw-checkbox-mark" })] }), _jsx("span", { className: "sw-checkbox-text", children: label })] }), description ? (_jsx("span", { className: "sw-checkbox-description", id: descriptionId, children: description })) : null, _jsx(FieldErrorRegion, { className: "sw-checkbox-error", id: errorId, message: error })] }));
15
+ return (_jsxs("div", { className: "sw-checkbox", children: [_jsxs("label", { className: "sw-checkbox-label", children: [_jsx(CheckboxControl, { "aria-describedby": describedBy || undefined, "aria-invalid": Boolean(error), checked: checked, disabled: disabled, onChange: (event) => {
16
+ if (!disabled)
17
+ onCheckedChange(event.currentTarget.checked);
18
+ }, required: required }), _jsx("span", { className: "sw-checkbox-text", children: label })] }), description ? (_jsx("span", { className: "sw-checkbox-description", id: descriptionId, children: description })) : null, _jsx(FieldErrorRegion, { className: "sw-checkbox-error", id: errorId, message: error })] }));
18
19
  }
@@ -1,3 +1,6 @@
1
+ import { denominationLabelAppearances } from '../css/css-denomination-label.js';
1
2
  import { type DenominationGridProps } from './denomination-grid/types.js';
2
3
  export type { DenominationGridColumn, DenominationGridEntry, DenominationGridLayout, DenominationGridProps, DenominationGridRow, DenominationGridRowEmphasis, DenominationGridRowRole, DenominationGridTone, DenominationLabelWidth, } from './denomination-grid/types.js';
3
- export declare function DenominationGrid({ label, columns, rows, layout, subtotal, zeroLabel, totalLabel, rowRole, labelWidth, }: DenominationGridProps): import("react").JSX.Element;
4
+ export type { DenominationLabelAppearance } from '../css/css-denomination-label.js';
5
+ export { denominationLabelAppearances };
6
+ export declare function DenominationGrid({ label, columns, rows, layout, subtotal, zeroLabel, totalLabel, rowRole, labelWidth, labelAppearance, }: DenominationGridProps): import("react").JSX.Element;
@@ -1,10 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { denominationLabelWidths } from '../css/css-denomination-aligned.js';
3
+ import { denominationLabelAppearances, denominationLabelsPlainClass, } from '../css/css-denomination-label.js';
3
4
  import { DenominationStrip } from './denomination-grid/DenominationStrip.js';
4
5
  import { DenominationTiles } from './denomination-grid/DenominationTiles.js';
5
6
  import { assertDenominationGrid } from './denomination-grid/validate.js';
6
7
  import { assertDenominationEntries, assertDenominationExtras, } from './denomination-grid/validate-entry.js';
7
- export function DenominationGrid({ label, columns, rows, layout = 'strip', subtotal, zeroLabel = '—', totalLabel, rowRole = 'region', labelWidth, }) {
8
+ export { denominationLabelAppearances };
9
+ export function DenominationGrid({ label, columns, rows, layout = 'strip', subtotal, zeroLabel = '—', totalLabel, rowRole = 'region', labelWidth, labelAppearance = 'tone', }) {
8
10
  assertDenominationGrid(label, columns, rows);
9
11
  assertDenominationExtras(layout, columns, rows);
10
12
  assertDenominationEntries(layout, columns, rows);
@@ -16,7 +18,10 @@ export function DenominationGrid({ label, columns, rows, layout = 'strip', subto
16
18
  throw new RangeError("rowRole must be 'region' or 'group'");
17
19
  if (labelWidth !== undefined && !denominationLabelWidths.includes(labelWidth))
18
20
  throw new RangeError(`Unknown labelWidth: ${String(labelWidth)}`);
21
+ if (!denominationLabelAppearances.includes(labelAppearance))
22
+ throw new RangeError(`Unknown labelAppearance: ${String(labelAppearance)}`);
23
+ const labelClass = labelAppearance === 'plain' ? denominationLabelsPlainClass : undefined;
19
24
  if (layout === 'tiles')
20
- return (_jsx(DenominationTiles, { columns: columns, label: label, rowRole: rowRole, rows: rows, subtotal: subtotal, zeroLabel: zeroLabel }));
21
- return (_jsx(DenominationStrip, { columns: columns, label: label, labelWidth: labelWidth, rows: rows, totalLabel: totalLabel, zeroLabel: zeroLabel }));
25
+ return (_jsx(DenominationTiles, { className: labelClass, columns: columns, label: label, rowRole: rowRole, rows: rows, subtotal: subtotal, zeroLabel: zeroLabel }));
26
+ return (_jsx(DenominationStrip, { className: labelClass, columns: columns, label: label, labelWidth: labelWidth, rows: rows, totalLabel: totalLabel, zeroLabel: zeroLabel }));
22
27
  }
@@ -1,16 +1,21 @@
1
1
  import { type DialogHTMLAttributes, type ReactNode } from 'react';
2
2
  import { type Breakpoint } from '../css/breakpoints.js';
3
+ import { type ActionBarSummary } from './action-bar/ActionBarSummary.js';
4
+ export declare const dialogSizes: readonly ["md", "lg", "xl"];
3
5
  /**
4
6
  * `md` is the reading width (32rem) for a message or a short form; `lg`
5
- * (56rem) holds a row of six fields or a data grid without folding it.
7
+ * (56rem) holds a row of six fields or a data grid without folding it;
8
+ * `xl` (65rem) holds a grid of six count columns with its labels and total.
6
9
  */
7
- export type DialogSize = 'md' | 'lg';
10
+ export type DialogSize = (typeof dialogSizes)[number];
11
+ /** `alertdialog` for a dialog that interrupts to warn, such as an idle sign-out. */
12
+ export type DialogRole = 'dialog' | 'alertdialog';
8
13
  /**
9
14
  * The heading level of the dialog's title. It changes only the element (and
10
15
  * so the outline a screen reader reads), never the title's look.
11
16
  */
12
17
  export type DialogTitleLevel = 2 | 3;
13
- export type DialogProps = Omit<DialogHTMLAttributes<HTMLDialogElement>, 'onClose' | 'open' | 'title' | 'children'> & {
18
+ export type DialogProps = Omit<DialogHTMLAttributes<HTMLDialogElement>, 'onClose' | 'open' | 'title' | 'children' | 'role'> & {
14
19
  /** Whether the dialog is shown. The element follows it and nothing else. */
15
20
  open: boolean;
16
21
  /**
@@ -50,6 +55,25 @@ export type DialogProps = Omit<DialogHTMLAttributes<HTMLDialogElement>, 'onClose
50
55
  * dialog has no close button and its markup is unchanged.
51
56
  */
52
57
  closeLabel?: string;
58
+ /**
59
+ * A muted line under the title that describes the dialog; it becomes the
60
+ * dialog's accessible description unless `aria-describedby` is given.
61
+ */
62
+ description?: string;
63
+ /** A consumer glyph or tile before the title, hidden from assistive technology. */
64
+ icon?: ReactNode;
65
+ /**
66
+ * The dialog's actions, pinned to its bottom edge under a hairline while
67
+ * the content scrolls: a secondary Cancel and one named primary.
68
+ */
69
+ footer?: ReactNode;
70
+ /**
71
+ * A labelled figure at the start of the footer, as ActionBar's, in a
72
+ * polite live region: "Drawer +$1,000.00 vault −$1,000.00".
73
+ */
74
+ summary?: ActionBarSummary;
75
+ /** `alertdialog` for a warning that interrupts. Default `dialog`. */
76
+ role?: DialogRole;
53
77
  children: ReactNode;
54
78
  };
55
- export declare function Dialog({ children, className, closeLabel, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, sheetBelow, size, title, titleLevel, ...rest }: DialogProps): import("react").JSX.Element;
79
+ export declare function Dialog({ 'aria-describedby': describedBy, children, className, closeLabel, description, footer, icon, role, summary, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, sheetBelow, size, title, titleLevel, ...rest }: DialogProps): import("react").JSX.Element;
@@ -5,10 +5,28 @@ import { cx } from '../class-names.js';
5
5
  import { dialogSheetClass } from '../css/css-dialog.js';
6
6
  import { spacingClassNames } from '../spacing-classes.js';
7
7
  import { useDialogCloseRequests } from './dialog-close-requests.js';
8
+ import { assertActionBarSummary, } from './action-bar/ActionBarSummary.js';
9
+ import { DialogFooter } from './dialog/DialogFooter.js';
8
10
  import { DialogTitle } from './dialog/DialogTitle.js';
9
11
  import { Stack } from './Stack.js';
10
- export function Dialog({ children, className, closeLabel, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, sheetBelow, size = 'md', title, titleLevel = 3, ...rest }) {
12
+ export const dialogSizes = ['md', 'lg', 'xl'];
13
+ const dialogRoles = ['dialog', 'alertdialog'];
14
+ function assertDialogProps(size, role, description, summary) {
15
+ if (!dialogSizes.includes(size))
16
+ throw new RangeError(`Unknown Dialog size: ${String(size)}`);
17
+ if (role !== undefined && !dialogRoles.includes(role))
18
+ throw new RangeError(`Unknown Dialog role: ${String(role)}`);
19
+ if (description !== undefined &&
20
+ (typeof description !== 'string' || !description.trim()))
21
+ throw new RangeError('Dialog description must not be empty');
22
+ if (summary)
23
+ assertActionBarSummary(summary);
24
+ }
25
+ export function Dialog({ 'aria-describedby': describedBy, children, className, closeLabel, description, footer, icon, role, summary, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, sheetBelow, size = 'md', title, titleLevel = 3, ...rest }) {
26
+ assertDialogProps(size, role, description, summary);
11
27
  const titleId = useId();
28
+ const descriptionId = useId();
29
+ const hasFooter = Boolean(footer) || Boolean(summary);
12
30
  const { nodeRef, handlers } = useDialogCloseRequests({
13
31
  open,
14
32
  onClose,
@@ -17,5 +35,5 @@ export function Dialog({ children, className, closeLabel, onCancel, onClose, onK
17
35
  onPointerDown,
18
36
  onSubmit,
19
37
  });
20
- return (_jsx("dialog", { ...rest, ref: nodeRef, "aria-labelledby": titleId, className: cx('sw-dialog', size === 'lg' && 'sw-dialog-lg', sheetBelow && dialogSheetClass(sheetBelow), ...spacingClassNames({ padding: 5 }), className), ...handlers, children: _jsxs(Stack, { gap: 4, children: [_jsx(DialogTitle, { closeLabel: closeLabel, id: titleId, level: titleLevel === 2 ? 2 : 3, onClose: onClose, title: title }), children] }) }));
38
+ return (_jsx("dialog", { ...rest, ref: nodeRef, "aria-describedby": describedBy ?? (description ? descriptionId : undefined), "aria-labelledby": titleId, className: cx('sw-dialog', size !== 'md' && `sw-dialog-${size}`, sheetBelow && dialogSheetClass(sheetBelow), ...spacingClassNames({ padding: 5 }), className), role: role === 'alertdialog' ? 'alertdialog' : undefined, ...handlers, children: _jsxs(Stack, { gap: 4, children: [_jsx(DialogTitle, { closeLabel: closeLabel, description: description, descriptionId: descriptionId, icon: icon, id: titleId, level: titleLevel === 2 ? 2 : 3, onClose: onClose, title: title }), children, hasFooter ? (_jsx(DialogFooter, { summary: summary, children: footer })) : null] }) }));
21
39
  }
@@ -0,0 +1,21 @@
1
+ import { type CategoricalColor } from '@scalewing/tokens';
2
+ import { type HTMLAttributes, type ReactNode } from 'react';
3
+ import { type GlyphTileSize, glyphTileSizes } from '../css/css-glyph-tile.js';
4
+ export { glyphTileSizes };
5
+ export type { GlyphTileSize };
6
+ /** A categorical hue (`green`, `blue`, `orange`, `purple`, `teal`, `rose`) or `neutral`. */
7
+ export type GlyphTileHue = CategoricalColor;
8
+ export type GlyphTileProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children' | 'aria-hidden'> & {
9
+ /** The categorical fill: a hue that tells a kind apart, or `neutral`. */
10
+ hue: GlyphTileHue;
11
+ /** `sm` 24, `md` 28 (default), `lg` 32. */
12
+ size?: GlyphTileSize;
13
+ /** The consumer's glyph (Lucide), drawn in white at spacing step 4. */
14
+ children: ReactNode;
15
+ };
16
+ /**
17
+ * A small rounded square in a categorical colour holding a white glyph: a
18
+ * kind's mark beside its name, such as a service in a list. Decorative
19
+ * (`aria-hidden`): the visible label beside it carries the meaning.
20
+ */
21
+ export declare function GlyphTile({ children, className, hue, size, ...rest }: GlyphTileProps): import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { isCategoricalColor } from '@scalewing/tokens';
3
+ import { cx } from '../class-names.js';
4
+ import { glyphTileSizes } from '../css/css-glyph-tile.js';
5
+ export { glyphTileSizes };
6
+ /**
7
+ * A small rounded square in a categorical colour holding a white glyph: a
8
+ * kind's mark beside its name, such as a service in a list. Decorative
9
+ * (`aria-hidden`): the visible label beside it carries the meaning.
10
+ */
11
+ export function GlyphTile({ children, className, hue, size = 'md', ...rest }) {
12
+ if (!isCategoricalColor(hue))
13
+ throw new RangeError(`Unknown GlyphTile hue: ${String(hue)}`);
14
+ if (!glyphTileSizes.includes(size))
15
+ throw new RangeError(`Unknown GlyphTile size: ${String(size)}`);
16
+ return (_jsx("span", { ...rest, "aria-hidden": "true", className: cx('sw-glyph-tile', `sw-glyph-tile-${hue}`, `sw-glyph-tile-${size}`, className), children: children }));
17
+ }
@@ -0,0 +1,10 @@
1
+ import { type MultiSelectProps } from './multi-select/types.js';
2
+ export type { MultiSelectAlign, MultiSelectOption, MultiSelectProps, } from './multi-select/types.js';
3
+ /**
4
+ * Several choices from a short list behind one trigger: "Show All entries ▾"
5
+ * opens a panel with All, Clear and a checkbox per option. A disclosure (a
6
+ * button that shows a group of native checkboxes), not a listbox, so each
7
+ * option is an ordinary checkbox to every user. The consumer owns the
8
+ * checked values and the trigger's summary words.
9
+ */
10
+ export declare function MultiSelect({ align, allLabel, clearLabel, disabled, label, onChange, options, summary, value, }: MultiSelectProps): import("react").JSX.Element;
@@ -0,0 +1,62 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useId, useRef, useState } from 'react';
4
+ import { MultiSelectPanel } from './multi-select/MultiSelectPanel.js';
5
+ import { assertMultiSelect } from './multi-select/validate.js';
6
+ /**
7
+ * Several choices from a short list behind one trigger: "Show All entries ▾"
8
+ * opens a panel with All, Clear and a checkbox per option. A disclosure (a
9
+ * button that shows a group of native checkboxes), not a listbox, so each
10
+ * option is an ordinary checkbox to every user. The consumer owns the
11
+ * checked values and the trigger's summary words.
12
+ */
13
+ export function MultiSelect({ align = 'start', allLabel, clearLabel, disabled = false, label, onChange, options, summary, value, }) {
14
+ assertMultiSelect({
15
+ allLabel,
16
+ align,
17
+ clearLabel,
18
+ label,
19
+ options,
20
+ summary,
21
+ value,
22
+ });
23
+ const panelId = useId();
24
+ const triggerRef = useRef(null);
25
+ const [open, setOpen] = useState(false);
26
+ const checked = new Set(value);
27
+ const shown = open && !disabled;
28
+ // Disabling while open closes the panel for good (state adjusted during
29
+ // render), so enabling it again never reopens it, or pulls focus into it,
30
+ // on its own.
31
+ if (disabled && open)
32
+ setOpen(false);
33
+ function close(restoreFocus) {
34
+ setOpen(false);
35
+ if (restoreFocus)
36
+ triggerRef.current?.focus();
37
+ }
38
+ function report(next) {
39
+ onChange(options
40
+ .filter((option) => next.has(option.value))
41
+ .map((option) => option.value));
42
+ }
43
+ function onToggle(changed, isChecked) {
44
+ const next = new Set(checked);
45
+ if (isChecked)
46
+ next.add(changed);
47
+ else
48
+ next.delete(changed);
49
+ report(next);
50
+ }
51
+ function onTriggerKeyDown(event) {
52
+ if (event.key === 'ArrowDown' && !shown) {
53
+ event.preventDefault();
54
+ setOpen(true);
55
+ }
56
+ else if (event.key === 'Escape' && shown) {
57
+ event.preventDefault();
58
+ close(true);
59
+ }
60
+ }
61
+ return (_jsxs("div", { className: "sw-multi-select", children: [_jsxs("button", { "aria-controls": shown ? panelId : undefined, "aria-expanded": shown, className: "sw-multi-select-trigger", disabled: disabled, onClick: () => (shown ? close(true) : setOpen(true)), onKeyDown: onTriggerKeyDown, ref: triggerRef, type: "button", children: [_jsx("span", { className: "sw-multi-select-label", children: label }), ' ', _jsx("span", { className: "sw-multi-select-summary", children: summary })] }), shown ? (_jsx(MultiSelectPanel, { align: align, allLabel: allLabel, checked: checked, clearLabel: clearLabel, id: panelId, label: label, onAll: () => report(new Set(options.map((option) => option.value))), onClear: () => report(new Set()), onClose: close, onToggle: onToggle, options: options, triggerRef: triggerRef })) : null] }));
62
+ }
@@ -0,0 +1,69 @@
1
+ import { type HTMLAttributes, type ReactNode } from 'react';
2
+ import { type NoticeTone, type NoticeVariant, noticeTones, noticeVariants } from '../css/css-notice.js';
3
+ export { noticeTones, noticeVariants };
4
+ export type { NoticeTone, NoticeVariant };
5
+ /**
6
+ * How the notice is announced: `status` (polite), `alert` (assertive), or
7
+ * `none` (not a live region, for a notice that is part of the page from the
8
+ * start and needs no announcement).
9
+ */
10
+ export type NoticeRole = 'status' | 'alert' | 'none';
11
+ export type NoticeProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'role' | 'title'> & {
12
+ /** The meaning, which sets the fill, the words' color and the default role. */
13
+ tone: NoticeTone;
14
+ /** A bold lead before the message, such as "$30.00 short." */
15
+ title?: string;
16
+ /**
17
+ * The message. Text (values interpolated in it included) shares the
18
+ * title's row while it fits; any element (a `span`, a field, a checkbox)
19
+ * puts the message under the title.
20
+ */
21
+ children?: ReactNode;
22
+ /** One action after the text, such as a link or a small Button. */
23
+ action?: ReactNode;
24
+ /** A consumer glyph (Lucide) before the text, hidden from assistive technology. */
25
+ icon?: ReactNode;
26
+ /**
27
+ * Defaults to `alert` for danger and `status` for the other tones; a band
28
+ * is page chrome and defaults to `none`.
29
+ */
30
+ role?: NoticeRole;
31
+ /**
32
+ * `band` is a full-width square strip in caption type, its words
33
+ * centered, such as an environment strip above the app header.
34
+ */
35
+ variant?: NoticeVariant;
36
+ };
37
+ /**
38
+ * A tinted note in the page's flow: information, a good outcome, a caution
39
+ * or a refusal, with an optional bold lead, glyph and one action. It stays
40
+ * until the consumer removes it. `variant="band"` is the same note as a
41
+ * full-width strip of page chrome. Pass `tabIndex={-1}` and a ref to move
42
+ * focus to it.
43
+ */
44
+ export declare const Notice: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLDivElement>, "title" | "children" | "role"> & {
45
+ /** The meaning, which sets the fill, the words' color and the default role. */
46
+ tone: NoticeTone;
47
+ /** A bold lead before the message, such as "$30.00 short." */
48
+ title?: string;
49
+ /**
50
+ * The message. Text (values interpolated in it included) shares the
51
+ * title's row while it fits; any element (a `span`, a field, a checkbox)
52
+ * puts the message under the title.
53
+ */
54
+ children?: ReactNode;
55
+ /** One action after the text, such as a link or a small Button. */
56
+ action?: ReactNode;
57
+ /** A consumer glyph (Lucide) before the text, hidden from assistive technology. */
58
+ icon?: ReactNode;
59
+ /**
60
+ * Defaults to `alert` for danger and `status` for the other tones; a band
61
+ * is page chrome and defaults to `none`.
62
+ */
63
+ role?: NoticeRole;
64
+ /**
65
+ * `band` is a full-width square strip in caption type, its words
66
+ * centered, such as an environment strip above the app header.
67
+ */
68
+ variant?: NoticeVariant;
69
+ } & import("react").RefAttributes<HTMLDivElement>>;