@scalewing/react 1.27.0 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +60 -2
- package/dist/components/DenominationGrid.d.ts +4 -1
- package/dist/components/DenominationGrid.js +8 -3
- package/dist/components/Dialog.d.ts +28 -4
- package/dist/components/Dialog.js +20 -2
- package/dist/components/Notice.d.ts +69 -0
- package/dist/components/Notice.js +48 -0
- package/dist/components/Progress.d.ts +3 -1
- package/dist/components/Progress.js +4 -0
- package/dist/components/Toast.d.ts +20 -4
- package/dist/components/Toast.js +44 -21
- package/dist/components/denomination-grid/DenominationStrip.d.ts +5 -2
- package/dist/components/denomination-grid/DenominationStrip.js +2 -2
- package/dist/components/denomination-grid/DenominationTiles.d.ts +5 -2
- package/dist/components/denomination-grid/DenominationTiles.js +2 -2
- package/dist/components/denomination-grid/types.d.ts +9 -0
- package/dist/components/dialog/DialogFooter.d.ts +14 -0
- package/dist/components/dialog/DialogFooter.js +11 -0
- package/dist/components/dialog/DialogTitle.d.ts +11 -4
- package/dist/components/dialog/DialogTitle.js +8 -6
- package/dist/components/toast/ToastContent.d.ts +22 -0
- package/dist/components/toast/ToastContent.js +21 -0
- package/dist/components/toast/use-toast-hold.d.ts +28 -0
- package/dist/components/toast/use-toast-hold.js +53 -0
- package/dist/components/toast/use-toast-timer.d.ts +15 -0
- package/dist/components/toast/use-toast-timer.js +34 -0
- package/dist/css/css-components.js +9 -0
- package/dist/css/css-denomination-grid.js +4 -0
- package/dist/css/css-denomination-label.d.ts +10 -0
- package/dist/css/css-denomination-label.js +36 -0
- package/dist/css/css-dialog-frame.d.ts +2 -0
- package/dist/css/css-dialog-frame.js +83 -0
- package/dist/css/css-dialog.js +20 -4
- package/dist/css/css-notice.d.ts +22 -0
- package/dist/css/css-notice.js +168 -0
- package/dist/css/css-progress.d.ts +3 -0
- package/dist/css/css-progress.js +13 -6
- package/dist/css/css-toast-card.d.ts +5 -0
- package/dist/css/css-toast-card.js +122 -0
- package/dist/css/css-variables.js +1 -0
- package/dist/css/stylesheet.js +6 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.js +5 -4
- package/dist/styles.css +299 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.28.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
- 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).
|
|
9
|
+
|
|
10
|
+
- `description`: a muted line under the title and the dialog's accessible description. `icon`: a consumer glyph or tile before the title, `aria-hidden`.
|
|
11
|
+
- `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.
|
|
12
|
+
- `size="xl"`: 65rem (`--sw-dialog-max-xl`). `role="alertdialog"` for a warning that interrupts.
|
|
13
|
+
- 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).
|
|
14
|
+
- 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.
|
|
15
|
+
|
|
16
|
+
- 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).
|
|
17
|
+
|
|
18
|
+
- 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.
|
|
19
|
+
- Its role defaults to `none` (no live region) unless `role` is given. Forced colors draw a rule under it.
|
|
20
|
+
- An unknown `variant` throws a `RangeError`.
|
|
21
|
+
- New generated class `sw-notice-band`. No new tokens or dependencies.
|
|
22
|
+
|
|
23
|
+
- 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).
|
|
24
|
+
|
|
25
|
+
- `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.
|
|
26
|
+
- 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.
|
|
27
|
+
- 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.
|
|
28
|
+
- An unknown `tone` or `role`, a blank `title`, or nothing to say throws a `RangeError`.
|
|
29
|
+
- 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.
|
|
30
|
+
|
|
31
|
+
- 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.
|
|
32
|
+
- 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).
|
|
33
|
+
|
|
34
|
+
- `title`: a bold first line; the children become a muted detail.
|
|
35
|
+
- `action: { label, onPress }`: a ghost small Button at the end; it runs `onPress`, then closes the toast.
|
|
36
|
+
- `showRemaining`: a thin `aria-hidden` line that runs down over the toast's duration.
|
|
37
|
+
- `placement: 'bottom' | 'top-end'` (default `bottom`, unchanged).
|
|
38
|
+
- 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.
|
|
39
|
+
- A travelling toast (`anchor` and `target`) ignores `placement` and `showRemaining`. When the toast closes with focus inside, focus returns to where it came from.
|
|
40
|
+
- `title` is now the toast's bold first line and is no longer forwarded to the element as the HTML `title` attribute.
|
|
41
|
+
- A blank `title` or `action.label`, a missing `onPress`, or an unknown `placement` throws a `RangeError`.
|
|
42
|
+
- 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.
|
|
43
|
+
|
|
3
44
|
## 1.27.0
|
|
4
45
|
|
|
5
46
|
### 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.
|
|
@@ -348,7 +391,22 @@ 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
|
-
`
|
|
394
|
+
`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`.
|
|
395
|
+
|
|
396
|
+
```tsx
|
|
397
|
+
<Notice tone="danger" title="$30.00 short.">
|
|
398
|
+
Add the bills or change the amount.
|
|
399
|
+
</Notice>
|
|
400
|
+
|
|
401
|
+
<Notice
|
|
402
|
+
tone="info"
|
|
403
|
+
role="none"
|
|
404
|
+
title="Three surveys are due this week."
|
|
405
|
+
action={<Button size="sm" variant="ghost" onPress={openTasks}>Open tasks</Button>}
|
|
406
|
+
/>
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
`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
410
|
|
|
353
411
|
`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
412
|
|
|
@@ -423,7 +481,7 @@ Values outside the catalog throw a `RangeError`. There is no per-breakpoint span
|
|
|
423
481
|
</Grid>
|
|
424
482
|
```
|
|
425
483
|
|
|
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.
|
|
484
|
+
`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
485
|
|
|
428
486
|
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
487
|
|
|
@@ -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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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,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>>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Children, forwardRef, } from 'react';
|
|
3
|
+
import { cx } from '../class-names.js';
|
|
4
|
+
import { noticeTones, noticeVariants, } from '../css/css-notice.js';
|
|
5
|
+
export { noticeTones, noticeVariants };
|
|
6
|
+
const noticeRoles = ['status', 'alert', 'none'];
|
|
7
|
+
function defaultRole(tone, variant) {
|
|
8
|
+
if (variant === 'band')
|
|
9
|
+
return 'none';
|
|
10
|
+
return tone === 'danger' ? 'alert' : 'status';
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The message's parts, without the empty ones (`false`, `null`,
|
|
14
|
+
* `undefined`, `''`) a condition leaves; fragments flatten.
|
|
15
|
+
*/
|
|
16
|
+
function messageParts(children) {
|
|
17
|
+
return Children.toArray(children).filter((part) => part !== '');
|
|
18
|
+
}
|
|
19
|
+
/** A sentence, even one with values interpolated, may share the title's row. */
|
|
20
|
+
function isText(parts) {
|
|
21
|
+
return parts.every((part) => typeof part === 'string' || typeof part === 'number');
|
|
22
|
+
}
|
|
23
|
+
function assertNoticeProps(tone, title, role, variant, hasMessage) {
|
|
24
|
+
if (!noticeVariants.includes(variant))
|
|
25
|
+
throw new RangeError(`Unknown Notice variant: ${String(variant)}`);
|
|
26
|
+
if (!noticeTones.includes(tone))
|
|
27
|
+
throw new RangeError(`Unknown Notice tone: ${String(tone)}`);
|
|
28
|
+
if (title !== undefined && (typeof title !== 'string' || !title.trim()))
|
|
29
|
+
throw new RangeError('Notice title must not be empty');
|
|
30
|
+
if (role !== undefined && !noticeRoles.includes(role))
|
|
31
|
+
throw new RangeError(`Unknown Notice role: ${String(role)}`);
|
|
32
|
+
if (title === undefined && !hasMessage)
|
|
33
|
+
throw new RangeError('Notice needs a title or a message');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A tinted note in the page's flow: information, a good outcome, a caution
|
|
37
|
+
* or a refusal, with an optional bold lead, glyph and one action. It stays
|
|
38
|
+
* until the consumer removes it. `variant="band"` is the same note as a
|
|
39
|
+
* full-width strip of page chrome. Pass `tabIndex={-1}` and a ref to move
|
|
40
|
+
* focus to it.
|
|
41
|
+
*/
|
|
42
|
+
export const Notice = forwardRef(function Notice({ action, children, className, icon, role, title, tone, variant = 'default', ...rest }, ref) {
|
|
43
|
+
const parts = messageParts(children);
|
|
44
|
+
const hasMessage = parts.length > 0;
|
|
45
|
+
assertNoticeProps(tone, title, role, variant, hasMessage);
|
|
46
|
+
const announced = role ?? defaultRole(tone, variant);
|
|
47
|
+
return (_jsxs("div", { ...rest, ref: ref, className: cx('sw-notice', `sw-notice-${tone}`, variant === 'band' && 'sw-notice-band', className), role: announced === 'none' ? undefined : announced, children: [icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-notice-icon", children: icon })) : null, _jsxs("div", { className: cx('sw-notice-content', hasMessage && !isText(parts) && 'sw-notice-stacked'), children: [title ? _jsx("strong", { className: "sw-notice-title", children: title }) : null, hasMessage ? _jsx("div", { className: "sw-notice-body", children: children }) : null] }), action ? _jsx("div", { className: "sw-notice-action", children: action }) : null] }));
|
|
48
|
+
});
|
|
@@ -1,6 +1,8 @@
|
|
|
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 { progressTones } from '../css/css-progress.js';
|
|
5
|
+
export { progressTones };
|
|
4
6
|
export function Progress({ label, value, max, tone = 'accent', showCount = true, }) {
|
|
5
7
|
if (!label.trim())
|
|
6
8
|
throw new RangeError('label must not be empty');
|
|
@@ -8,6 +10,8 @@ export function Progress({ label, value, max, tone = 'accent', showCount = true,
|
|
|
8
10
|
throw new RangeError('max must be a positive finite number');
|
|
9
11
|
if (!Number.isFinite(value) || value < 0 || value > max)
|
|
10
12
|
throw new RangeError('value must be finite and within 0..max');
|
|
13
|
+
if (!progressTones.includes(tone))
|
|
14
|
+
throw new RangeError(`Unknown Progress tone: ${String(tone)}`);
|
|
11
15
|
const labelId = useId();
|
|
12
16
|
return (_jsxs("div", { className: `sw-progress sw-progress-${tone}`, children: [_jsxs("div", { className: "sw-progress-heading", children: [_jsx("span", { className: "sw-progress-label", id: labelId, children: label }), showCount ? (_jsxs("span", { "aria-hidden": "true", className: "sw-progress-count", children: [value, " / ", max] })) : null] }), _jsx("progress", { "aria-labelledby": labelId, className: "sw-progress-bar", max: max, value: value })] }));
|
|
13
17
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { type HTMLAttributes, type ReactNode } from 'react';
|
|
2
2
|
import { toastTones, type ToastTone } from '../css/css-toast.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
3
|
+
import { toastPlacements, type ToastPlacement } from '../css/css-toast-card.js';
|
|
4
|
+
import { type ToastAction } from './toast/ToastContent.js';
|
|
5
|
+
export { toastPlacements, toastTones };
|
|
6
|
+
export type { ToastAction, ToastPlacement, ToastTone };
|
|
7
|
+
export type ToastProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'popover' | 'title'> & {
|
|
6
8
|
open: boolean;
|
|
7
9
|
onOpenChange: (open: boolean) => void;
|
|
8
10
|
/**
|
|
@@ -19,6 +21,20 @@ export type ToastProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'popo
|
|
|
19
21
|
tone?: ToastTone;
|
|
20
22
|
/** A consumer glyph before the message, hidden from assistive technology. */
|
|
21
23
|
icon?: ReactNode;
|
|
24
|
+
/** A bold first line; the children become the muted detail under it. */
|
|
25
|
+
title?: string;
|
|
26
|
+
/**
|
|
27
|
+
* One press at the end of the toast, such as "Open". It runs `onPress`,
|
|
28
|
+
* then closes the toast. The toast takes the pointer while it has one.
|
|
29
|
+
*/
|
|
30
|
+
action?: ToastAction;
|
|
31
|
+
/** A thin line under the message that runs down with the time left. */
|
|
32
|
+
showRemaining?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* `bottom` (default) centers it near the bottom edge; `top-end` puts it
|
|
35
|
+
* in the top corner at the end of the line, as a card.
|
|
36
|
+
*/
|
|
37
|
+
placement?: ToastPlacement;
|
|
22
38
|
children: ReactNode;
|
|
23
39
|
};
|
|
24
|
-
export declare function Toast({ anchor, children, className, onOpenChange, open, target, timeoutMs,
|
|
40
|
+
export declare function Toast({ action, anchor, children, className, icon, onBlur, onFocus, onOpenChange, onPointerEnter, onPointerLeave, open, placement, showRemaining, style, target, timeoutMs, title, tone, ...rest }: ToastProps): import("react").JSX.Element;
|
package/dist/components/Toast.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { useCallback, useLayoutEffect, useRef, } from 'react';
|
|
4
4
|
import { cx } from '../class-names.js';
|
|
5
5
|
import { toastTones } from '../css/css-toast.js';
|
|
6
|
+
import { toastPlacements } from '../css/css-toast-card.js';
|
|
6
7
|
import { spacingClassNames } from '../spacing-classes.js';
|
|
7
8
|
import { applyTravelVars, clearTravelVars, travelTranslate, } from '../toast-travel.js';
|
|
8
|
-
|
|
9
|
+
import { assertToastContent, ToastContent, } from './toast/ToastContent.js';
|
|
10
|
+
import { useToastHold } from './toast/use-toast-hold.js';
|
|
11
|
+
import { useToastTimer } from './toast/use-toast-timer.js';
|
|
12
|
+
export { toastPlacements, toastTones };
|
|
9
13
|
const DEFAULT_TIMEOUT_MS = 800;
|
|
10
14
|
/** A warning or an error must be read, not glimpsed. */
|
|
11
15
|
const URGENT_TIMEOUT_MS = 6000;
|
|
@@ -17,10 +21,28 @@ function defaultTimeout(tone) {
|
|
|
17
21
|
function travels(anchor, target) {
|
|
18
22
|
return Boolean(anchor && target);
|
|
19
23
|
}
|
|
20
|
-
|
|
24
|
+
function reducedMotion() {
|
|
25
|
+
return (typeof window !== 'undefined' &&
|
|
26
|
+
typeof window.matchMedia === 'function' &&
|
|
27
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches);
|
|
28
|
+
}
|
|
29
|
+
export function Toast({ action, anchor = null, children, className, icon, onBlur, onFocus, onOpenChange, onPointerEnter, onPointerLeave, open, placement = 'bottom', showRemaining = false, style, target = null, timeoutMs, title, tone = 'neutral', ...rest }) {
|
|
30
|
+
assertToastContent(title, action);
|
|
31
|
+
if (!toastPlacements.includes(placement))
|
|
32
|
+
throw new RangeError(`Unknown Toast placement: ${String(placement)}`);
|
|
21
33
|
const nodeRef = useRef(null);
|
|
22
34
|
const dismissAfter = timeoutMs ?? defaultTimeout(tone);
|
|
23
35
|
const moving = travels(anchor, target);
|
|
36
|
+
const interactive = !moving && (Boolean(action) || showRemaining);
|
|
37
|
+
const { handlers, paused, returnFocus } = useToastHold({
|
|
38
|
+
enabled: interactive,
|
|
39
|
+
onBlur,
|
|
40
|
+
onFocus,
|
|
41
|
+
onPointerEnter,
|
|
42
|
+
onPointerLeave,
|
|
43
|
+
open,
|
|
44
|
+
});
|
|
45
|
+
const close = useCallback(() => onOpenChange(false), [onOpenChange]);
|
|
24
46
|
useLayoutEffect(() => {
|
|
25
47
|
const node = nodeRef.current;
|
|
26
48
|
if (!node || typeof node.showPopover !== 'function') {
|
|
@@ -28,6 +50,7 @@ export function Toast({ anchor = null, children, className, onOpenChange, open,
|
|
|
28
50
|
}
|
|
29
51
|
if (!open) {
|
|
30
52
|
if (node.matches(':popover-open')) {
|
|
53
|
+
returnFocus(node);
|
|
31
54
|
node.hidePopover();
|
|
32
55
|
}
|
|
33
56
|
clearTravelVars(node);
|
|
@@ -44,28 +67,28 @@ export function Toast({ anchor = null, children, className, onOpenChange, open,
|
|
|
44
67
|
if (!node.matches(':popover-open')) {
|
|
45
68
|
node.showPopover();
|
|
46
69
|
}
|
|
47
|
-
}, [anchor, moving, open, target]);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
const timer = window.setTimeout(() => {
|
|
58
|
-
onOpenChange(false);
|
|
59
|
-
}, dismissAfter);
|
|
60
|
-
return () => {
|
|
61
|
-
window.clearTimeout(timer);
|
|
62
|
-
};
|
|
63
|
-
}, [dismissAfter, moving, open, onOpenChange]);
|
|
70
|
+
}, [anchor, moving, open, returnFocus, target]);
|
|
71
|
+
// A travelling toast closes when its animation ends, unless motion is
|
|
72
|
+
// reduced; every other toast closes on the timer, held while paused.
|
|
73
|
+
useToastTimer({
|
|
74
|
+
duration: dismissAfter,
|
|
75
|
+
onExpire: close,
|
|
76
|
+
paused,
|
|
77
|
+
running: open && (!moving || reducedMotion()),
|
|
78
|
+
});
|
|
64
79
|
function onAnimationEnd(event) {
|
|
65
80
|
if (event.target !== event.currentTarget || !moving) {
|
|
66
81
|
return;
|
|
67
82
|
}
|
|
68
83
|
onOpenChange(false);
|
|
69
84
|
}
|
|
70
|
-
return (
|
|
85
|
+
return (_jsxs("div", { ...rest, ref: nodeRef, className: cx('sw-toast', tone !== 'neutral' && `sw-toast-${tone}`, !moving && placement !== 'bottom' && `sw-toast-${placement}`, interactive && 'sw-toast-interactive', paused && 'sw-toast-paused', moving && 'sw-toast-travel', ...spacingClassNames({ padding: 3 }), className), onAnimationEnd: onAnimationEnd, ...handlers, popover: "manual", role: tone === 'danger' ? 'alert' : 'status', style: showRemaining
|
|
86
|
+
? {
|
|
87
|
+
...style,
|
|
88
|
+
'--sw-toast-duration': `${dismissAfter}ms`,
|
|
89
|
+
}
|
|
90
|
+
: style, children: [_jsx(ToastContent, { action: action, icon: icon, onAction: () => {
|
|
91
|
+
action?.onPress();
|
|
92
|
+
close();
|
|
93
|
+
}, title: title, children: children }), showRemaining && open && !moving ? (_jsx("span", { "aria-hidden": "true", className: "sw-toast-remaining" })) : null] }));
|
|
71
94
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { type DenominationGridProps } from './types.js';
|
|
2
|
-
type StripProps = Required<Pick<DenominationGridProps, 'label' | 'columns' | 'rows' | 'zeroLabel'>> & Pick<DenominationGridProps, 'totalLabel' | 'labelWidth'
|
|
3
|
-
|
|
2
|
+
type StripProps = Required<Pick<DenominationGridProps, 'label' | 'columns' | 'rows' | 'zeroLabel'>> & Pick<DenominationGridProps, 'totalLabel' | 'labelWidth'> & {
|
|
3
|
+
/** The row labels' appearance class, if any. */
|
|
4
|
+
className?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function DenominationStrip({ className, label, columns, rows, zeroLabel, totalLabel, labelWidth, }: StripProps): import("react").JSX.Element;
|
|
4
7
|
export {};
|
|
@@ -16,7 +16,7 @@ function TotalHead({ totalLabel }) {
|
|
|
16
16
|
return _jsx("td", { className: "sw-denomination-corner sw-denomination-total-head" });
|
|
17
17
|
return (_jsx("th", { className: "sw-denomination-head sw-denomination-total-head", scope: "col", children: _jsx("span", { className: "sw-denomination-total-label", children: totalLabel }) }));
|
|
18
18
|
}
|
|
19
|
-
export function DenominationStrip({ label, columns, rows, zeroLabel, totalLabel, labelWidth, }) {
|
|
19
|
+
export function DenominationStrip({ className, label, columns, rows, zeroLabel, totalLabel, labelWidth, }) {
|
|
20
20
|
const hasTotals = rows.some((row) => row.total !== undefined);
|
|
21
21
|
// An aligned strip sizes its columns from the region's width; it needs
|
|
22
22
|
// the column count and whether a total column takes a share.
|
|
@@ -26,5 +26,5 @@ export function DenominationStrip({ label, columns, rows, zeroLabel, totalLabel,
|
|
|
26
26
|
'--sw-denomination-columns': columns.length,
|
|
27
27
|
'--sw-denomination-total': hasTotals ? 1 : 0,
|
|
28
28
|
};
|
|
29
|
-
return (_jsx(ScrollRegion, { "aria-label": label, className: cx('sw-denomination-scroll', aligned && 'sw-denomination-scroll-aligned'), children: _jsxs("table", { className: cx('sw-denomination-grid sw-denomination-strip', labelWidth && 'sw-denomination-strip-aligned', rows.some((row) => row.entry) && 'sw-denomination-strip-entry', labelWidth && denominationLabelWidthClass(labelWidth)), style: aligned, children: [_jsx("caption", { className: "sw-sr-only", children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("td", { className: "sw-denomination-corner" }), columns.map((column) => (_jsx("th", { className: "sw-denomination-head", scope: "col", children: _jsx(ColumnHeadText, { column: column }) }, column.key))), hasTotals ? _jsx(TotalHead, { totalLabel: totalLabel }) : null] }) }), _jsx("tbody", { children: rows.map((row) => (_jsx(StripRow, { columns: columns, hasTotals: hasTotals, row: row, zeroLabel: zeroLabel }, row.id))) })] }) }));
|
|
29
|
+
return (_jsx(ScrollRegion, { "aria-label": label, className: cx('sw-denomination-scroll', aligned && 'sw-denomination-scroll-aligned'), children: _jsxs("table", { className: cx('sw-denomination-grid sw-denomination-strip', labelWidth && 'sw-denomination-strip-aligned', rows.some((row) => row.entry) && 'sw-denomination-strip-entry', labelWidth && denominationLabelWidthClass(labelWidth), className), style: aligned, children: [_jsx("caption", { className: "sw-sr-only", children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("td", { className: "sw-denomination-corner" }), columns.map((column) => (_jsx("th", { className: "sw-denomination-head", scope: "col", children: _jsx(ColumnHeadText, { column: column }) }, column.key))), hasTotals ? _jsx(TotalHead, { totalLabel: totalLabel }) : null] }) }), _jsx("tbody", { children: rows.map((row) => (_jsx(StripRow, { columns: columns, hasTotals: hasTotals, row: row, zeroLabel: zeroLabel }, row.id))) })] }) }));
|
|
30
30
|
}
|