@scalewing/react 1.16.0 → 1.18.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 +21 -0
- package/README.md +27 -4
- package/dist/components/ActionBar.d.ts +16 -6
- package/dist/components/ActionBar.js +1 -1
- package/dist/components/DenominationGrid.d.ts +2 -2
- package/dist/components/DenominationGrid.js +4 -2
- package/dist/components/Dialog.d.ts +32 -1
- package/dist/components/Dialog.js +4 -3
- package/dist/components/ScrollRegion.d.ts +4 -1
- package/dist/components/ScrollRegion.js +11 -2
- package/dist/components/Tabs.d.ts +22 -2
- package/dist/components/Tabs.js +4 -3
- package/dist/components/denomination-grid/DenominationStrip.js +2 -1
- package/dist/components/denomination-grid/DenominationTiles.d.ts +2 -2
- package/dist/components/denomination-grid/DenominationTiles.js +12 -7
- package/dist/components/denomination-grid/types.d.ts +19 -0
- package/dist/components/denomination-grid/validate.js +17 -0
- package/dist/components/dialog/DialogTitle.d.ts +15 -0
- package/dist/components/dialog/DialogTitle.js +14 -0
- package/dist/components/use-scroll-overflow.d.ts +9 -0
- package/dist/components/use-scroll-overflow.js +44 -0
- package/dist/css/css-action-bar.js +7 -0
- package/dist/css/css-denomination-cell-tones.d.ts +9 -0
- package/dist/css/css-denomination-cell-tones.js +52 -0
- package/dist/css/css-denomination-grid.js +10 -1
- package/dist/css/css-dialog.d.ts +2 -0
- package/dist/css/css-dialog.js +125 -3
- package/dist/css/css-scroll-region.d.ts +16 -2
- package/dist/css/css-scroll-region.js +68 -2
- package/dist/css/css-tabs.d.ts +1 -0
- package/dist/css/css-tabs.js +42 -1
- package/dist/css/stacking.js +3 -2
- package/dist/css/stylesheet.js +2 -0
- package/dist/denomination-cells.d.ts +3 -1
- package/dist/denomination-cells.js +6 -2
- package/dist/index.d.ts +2 -2
- package/dist/scroll-overflow.d.ts +14 -0
- package/dist/scroll-overflow.js +26 -0
- package/dist/styles.css +158 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.18.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ea2fdf0: `ActionBar` `status` takes a node (`docs/requests/fantasy-football-action-bar-status.md`, the fantasy-football companion's trade verdict): `status?: ReactNode`, widened from `string`, so a `Badge` and a short line can share the status. The status element is now a `div` (was a `p`) that keeps `className="sw-action-bar-status"` and `role="status"`, is still rendered while empty, and still takes no room when empty. It lays its children on one wrapping row a `space-2` gap apart (`space-1` between wrapped rows), so a line that does not fit beside its badge wraps under it at phone width. A string status renders as before; a phrase that mixes text and elements belongs in one `span`. Consumers whose tests looked for a `P` status element need to update that query. No new classes, tokens or dependencies.
|
|
8
|
+
- d02521e: `Dialog` `sheetBelow` and `closeLabel` (`docs/requests/fantasy-football-dialog-sheet.md`, the fantasy-football companion's phone review dialogs). `sheetBelow?: Breakpoint` (`'md'`): below that breakpoint the dialog is a bottom sheet, docked to the bottom edge at the full viewport width, top corners at `--sw-radius-lg` and bottom corners square, a `space-8` strip of backdrop left above it (the same dvh-aware cap), and bottom padding that clears `env(safe-area-inset-bottom)`; it slides up on open with `--sw-motion-default`, not under `prefers-reduced-motion`, and draws no grabber. From the breakpoint up it is the centered dialog at its `size`. `closeLabel?: string`: an icon-only ghost close `Button` ends the title row, named by the label, and calls `onClose`; the title still names the dialog and `titleLevel` still applies. The button is the first control, so it takes the focus on open unless content has `autoFocus`. Its X is private control chrome drawn in CSS (no icon dependency). Without `closeLabel` the markup is unchanged. New generated classes `sw-dialog-sheet-below-md`, `sw-dialog-header`, `sw-dialog-close`, `sw-dialog-close-glyph`. No new tokens and no new dependencies.
|
|
9
|
+
- c5fd0a9: `Tabs` `sticky` (`docs/requests/fantasy-football-tabs-sticky.md`, the fantasy-football companion's section tabs): `sticky?: boolean`, default `false`, keeps the strip at the top of the viewport (`top: env(safe-area-inset-top, 0px)`) while a long panel scrolls under it, on the sticky `AppHeader`'s `topChrome` layer. The stuck strip is a full-bleed band of the page canvas, not a glass card: `--sw-color-background` at 90 % over the glass blur and saturate tokens, with the strip's existing hairline, solid canvas under `prefers-reduced-transparency` and `Canvas` in forced colors. A sticky element only sticks within its parent, so make the strip a direct child of the page container that holds the panels; each tab's md inline padding (16 px, spacing step 4) lines the first label up with a `space-4` page gutter. New generated class `sw-tabs-sticky`. No new tokens and no new dependencies.
|
|
10
|
+
|
|
11
|
+
## 1.17.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- 7bb0397: `DenominationGrid` row `cellTones` (`docs/requests/teisoro-denomination-grid.md`, 2026-10-02 follow-up, Teisoro F-007 task 1550, MOV-10): `cellTones?: readonly (DenominationGridTone | null)[]`, one entry per column, tones a single count in place. The count is set in the tone (over a signed row's color and a zero's quiet opacity), and in the tiles layout the tile's border takes the tone too, without moving or resizing the tile. `neutral` or `null` leaves a count as it is; a row's `tone` is unchanged. The wrong length or an unknown tone throws a `RangeError`. New generated classes `sw-denomination-cell-toned` and `sw-denomination-cell-tone-{accent,success,danger,warning}`. No new dependencies.
|
|
16
|
+
- 051e0ec: `DenominationGrid` `rowRole` (`docs/requests/teisoro-denomination-grid.md`, 2026-10-02 follow-up, Teisoro F-007 task 1550, CHG-4): `rowRole?: 'region' | 'group'`, default `'region'`. At `'group'` each named tiles row keeps its `aria-label` but is a `group`, not a landmark region, so a page with many grids does not list a region per row. The default keeps today's regions; the strip ignores the prop; an unknown value throws a `RangeError`. New exported type `DenominationGridRowRole`. No new dependencies.
|
|
17
|
+
- b810ce3: `Dialog` `titleLevel` (`docs/requests/teisoro-dialog-title-level.md`, Teisoro F-007 task 1550, CHG-13): `titleLevel?: 2 | 3`, default `3`, sets the heading level of the dialog's title. `titleLevel={2}` renders it as an `h2` in the same title style, so a dialog's own section labels can be `h3` headings that read as its parts rather than its peers. The default keeps the `h3` every existing dialog has. New exported type `DialogTitleLevel`. No new dependencies.
|
|
18
|
+
- 0a49a53: A wide `Table` (and `DenominationGrid` strip) shows that it scrolls (`docs/requests/teisoro-table.md`, 2026-10-02 follow-up, Teisoro F-007 task 1550, VLT-1 and HIS-3). While its content is scrolled out past an inline edge, the scroll region adds `sw-scroll-more-start` and/or `sw-scroll-more-end` and that edge draws a soft inset shade (the text color at 28%, spacing steps 6 and 5), mirrored right to left, under the content so nothing moves. A table that fits is unchanged: no class, no shadow. The region measures on mount, on scroll and on resize; before mount and on the server it reports no overflow. In forced colors the system scrollbar is the cue. New generated classes `sw-scroll-more-start` and `sw-scroll-more-end`; no prop changes and no new dependencies.
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- 1648c50: `DenominationGrid` writes a negative count with the typographic minus "−" (U+2212), not a hyphen-minus (`docs/requests/teisoro-denomination-grid.md`, 2026-10-02 follow-up, Teisoro F-007 task 1550, AUD-18). It applies to every negative count, in signed and unsigned rows and in both layouts, so a count matches the sign of the consumer's formatted money. The cell's text is the only change: a test that matched `-1` in a cell now matches `−1`. No API change and no new dependencies.
|
|
23
|
+
|
|
3
24
|
## 1.16.0
|
|
4
25
|
|
|
5
26
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ A disabled native text control (`<input>`, `<select>` or `<textarea>` on the can
|
|
|
126
126
|
</Accordion>
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
`ActionBar` keeps the actions of a long page on a glass bar stuck to the bottom of the viewport, with an optional
|
|
129
|
+
`ActionBar` keeps the actions of a long page on a glass bar stuck to the bottom of the viewport, with an optional short `status` (such as when the work was last saved): a string, or a node such as a `Badge` and then a short line. Put it last in the content it acts on: it stays stuck while that content scrolls by and then rests in its own place at the end. `stickyBelow="md"` sticks only on a phone and leaves the bar in page flow from `md` up. The bar clears `env(safe-area-inset-bottom)` (set `viewport-fit=cover` in the page's viewport meta for the inset to apply). Its children are the actions: on a phone they share one row under the status. The status is a polite live region (`role="status"`, kept in the page even while empty), so a new status such as "Draft saved at 5:00 PM" is announced; do not announce the same save a second time with your own notice. The status is a `div` whose children sit on one row a `space-2` gap apart and wrap under one another when the row is full, so a line that does not fit beside its badge drops under it on a phone; put a phrase that mixes text and elements in one `span` so it wraps as a sentence. `undefined`, `null` or `false` leaves the status empty. An open `Select`, `ActionMenu` or `Tooltip` paints over the bar, including inside a glass `Card` or `Accordion`.
|
|
130
130
|
|
|
131
131
|
```tsx
|
|
132
132
|
<Stack gap={4}>
|
|
@@ -155,7 +155,7 @@ A `Button` that toggles something passes `aria-pressed`. The pressed button gets
|
|
|
155
155
|
|
|
156
156
|
`ActionMenu` opens independent commands from a labelled button. Provide localized command labels and callbacks; use `disabled` for unavailable commands and `destructive` for a dangerous command's presentation. Escape and choosing a command both return focus to the trigger (before the command runs, so a `Dialog` it opens hands focus back to the trigger on close), and outside interaction dismisses the menu. The open menu sits on the popover layer a `space-1` gap below its trigger (above it when it only fits there), at least `space-2` from every viewport edge (a classic scrollbar is not part of the viewport), and aligned to the trigger's inline start (its right edge in right-to-left), or to its other edge when the trigger ends a row and the menu would not fit from its start. `align="end"` (default `"start"`) lines the menu up with the trigger's inline end even where the start would fit, for a trigger that ends a card or a list row, so the menu stays over that card instead of hanging past it; it takes the start when the end would leave the screen. The menu is as wide as its longest command up to the viewport less both insets, and a longer command wraps. The menu follows its trigger through scroll, window resize, and a change in the menu's or the trigger's size. The menu is in the DOM only while it is open. The trigger and its commands use the compact xs control height for a fine pointer; on a coarse pointer the trigger is at least 44 × 44 px (the md control height, as on `CalendarButton`) and every command is at least 44 px tall.
|
|
157
157
|
|
|
158
|
-
`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.
|
|
158
|
+
`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.
|
|
159
159
|
|
|
160
160
|
`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.
|
|
161
161
|
|
|
@@ -218,7 +218,30 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
|
|
|
218
218
|
/>
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
-
`
|
|
221
|
+
`Tabs` is the strip for the sections of one page (`role="tablist"`, one `role="tab"` button per item; arrow keys, Home and End move and select), and `TabPanel` is each section's content, hidden while another tab is current. `sticky` (default `false`) keeps the strip at the top of the viewport, under `env(safe-area-inset-top)`, while a long panel scrolls under it, on the same layer as a sticky `AppHeader` (so a page uses one or the other at the top edge). The stuck strip is a full-bleed band of the page canvas, not a glass card: the canvas color at nine parts in ten over the glass blur, with the strip's hairline under it, solid canvas under Reduce Transparency and in forced colors. A sticky element only sticks within its parent: make the strip a direct child of the long page container that also holds the panels, not of a padded `Box` round the strip alone, and keep `overflow` visible on its ancestors. Put the page's side gutter on the title and the panels instead: each tab's inline padding is the md control's 16 px (spacing step 4), so the first label lines up with a `space-4` gutter when the strip runs edge to edge.
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
<Stack gap={3}>
|
|
225
|
+
<Box paddingX={4}>
|
|
226
|
+
<Text variant="title">Wetland reserve</Text>
|
|
227
|
+
</Box>
|
|
228
|
+
<Tabs
|
|
229
|
+
aria-label="Reserve log"
|
|
230
|
+
id="reserve"
|
|
231
|
+
items={groups}
|
|
232
|
+
onChange={setGroup}
|
|
233
|
+
sticky
|
|
234
|
+
value={group}
|
|
235
|
+
/>
|
|
236
|
+
<Box paddingX={4}>
|
|
237
|
+
<TabPanel id="birds" tabsId="reserve" value={group}>
|
|
238
|
+
{birdLog}
|
|
239
|
+
</TabPanel>
|
|
240
|
+
</Box>
|
|
241
|
+
</Stack>
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`Table` aligns data in rows inside its own keyboard-reachable scroll region, named after the table. While the table is wider than the region, each inline edge with columns scrolled out past it draws a soft inset shade (`sw-scroll-more-start`, `sw-scroll-more-end`, mirrored right to left), so a phone, whose scrollbars stay hidden, still shows that the table goes on; a table that fits draws none, and in forced colors the system scrollbar is the cue. `TableCell` takes `numeric` (tabular numerals, end aligned), `align` and `truncate`; `density="compact"` densifies cells. `TableRow selected` sets `aria-selected` and marks the row with a 4 px accent bar at its inline start (3:1 or more against the surface and the page), the right edge in a right-to-left table. The bar is drawn inside the first cell's padding and takes no layout space, so no column moves when a row is picked, and the row takes no fill, so text, muted and accent text keep their contrast; forced colors keep the bar in the system highlight.
|
|
222
245
|
|
|
223
246
|
`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.
|
|
224
247
|
|
|
@@ -262,7 +285,7 @@ Values outside the catalog throw a `RangeError`. There is no per-breakpoint span
|
|
|
262
285
|
</Grid>
|
|
263
286
|
```
|
|
264
287
|
|
|
265
|
-
`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 for a screen reader: it renders a visually hidden `th scope="col"`, and without it the corner stays an empty cell as before (either way the column takes no width on a phone). 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; 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. 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. The primitive does no arithmetic and no currency formatting; tones reuse the Badge vocabulary. Invalid columns, rows, or cells throw.
|
|
288
|
+
`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 for a screen reader: it renders a visually hidden `th scope="col"`, and without it the corner stays an empty cell as before (either way the column takes no width on a phone). 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.
|
|
266
289
|
|
|
267
290
|
`FilterChips` presents a long, wrapping single-choice filter set. Provide a localized group `label`, controlled `value`, `onChange`, and options with stable values and localized labels. An optional non-negative integer `count` renders as a tabular chicklet after the label and is part of the option's accessible name; a zero count quiets the chip until it is selected: it drops its glass fill and sets its label in the muted color (4.5:1 in every palette), without fading, since it can still be chosen. Native radios provide arrow and Space navigation; disabled options are skipped.
|
|
268
291
|
|
|
@@ -4,10 +4,15 @@ export type ActionBarProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> &
|
|
|
4
4
|
/** The actions, usually two or three `Button`s. */
|
|
5
5
|
children: ReactNode;
|
|
6
6
|
/**
|
|
7
|
-
* One short line about the work, such as when it was last saved
|
|
8
|
-
*
|
|
7
|
+
* One short line about the work, such as when it was last saved, or a
|
|
8
|
+
* `Badge` and then a short line ("Ready" and what is ready). It is a
|
|
9
|
+
* polite live region, so a new status is announced without moving focus;
|
|
10
|
+
* nothing (`undefined`, `null`, `false`) leaves it empty and taking no
|
|
11
|
+
* room. Its direct children sit on one row a small gap apart and wrap
|
|
12
|
+
* under one another when the row is full, so put a phrase that mixes text
|
|
13
|
+
* and elements in one `span` to keep it wrapping as a sentence.
|
|
9
14
|
*/
|
|
10
|
-
status?:
|
|
15
|
+
status?: ReactNode;
|
|
11
16
|
/** Sticky only below this breakpoint; from it up the bar sits in page flow. */
|
|
12
17
|
stickyBelow?: Breakpoint;
|
|
13
18
|
};
|
|
@@ -21,10 +26,15 @@ export declare const ActionBar: import("react").ForwardRefExoticComponent<Omit<H
|
|
|
21
26
|
/** The actions, usually two or three `Button`s. */
|
|
22
27
|
children: ReactNode;
|
|
23
28
|
/**
|
|
24
|
-
* One short line about the work, such as when it was last saved
|
|
25
|
-
*
|
|
29
|
+
* One short line about the work, such as when it was last saved, or a
|
|
30
|
+
* `Badge` and then a short line ("Ready" and what is ready). It is a
|
|
31
|
+
* polite live region, so a new status is announced without moving focus;
|
|
32
|
+
* nothing (`undefined`, `null`, `false`) leaves it empty and taking no
|
|
33
|
+
* room. Its direct children sit on one row a small gap apart and wrap
|
|
34
|
+
* under one another when the row is full, so put a phrase that mixes text
|
|
35
|
+
* and elements in one `span` to keep it wrapping as a sentence.
|
|
26
36
|
*/
|
|
27
|
-
status?:
|
|
37
|
+
status?: ReactNode;
|
|
28
38
|
/** Sticky only below this breakpoint; from it up the bar sits in page flow. */
|
|
29
39
|
stickyBelow?: Breakpoint;
|
|
30
40
|
} & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -9,5 +9,5 @@ import { actionBarStickyClass } from '../css/css-action-bar.js';
|
|
|
9
9
|
* in its own place at the end.
|
|
10
10
|
*/
|
|
11
11
|
export const ActionBar = forwardRef(function ActionBar({ children, className, status, stickyBelow, ...rest }, ref) {
|
|
12
|
-
return (_jsxs("div", { ref: ref, className: cx('sw-action-bar', actionBarStickyClass(stickyBelow), className), ...rest, children: [_jsx("
|
|
12
|
+
return (_jsxs("div", { ref: ref, className: cx('sw-action-bar', actionBarStickyClass(stickyBelow), className), ...rest, children: [_jsx("div", { className: "sw-action-bar-status", role: "status", children: status }), _jsx("div", { className: "sw-action-bar-actions", children: children })] }));
|
|
13
13
|
});
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { type DenominationGridProps } from './denomination-grid/types.js';
|
|
2
|
-
export type { DenominationGridColumn, DenominationGridLayout, DenominationGridProps, DenominationGridRow, DenominationGridTone, } from './denomination-grid/types.js';
|
|
3
|
-
export declare function DenominationGrid({ label, columns, rows, layout, subtotal, zeroLabel, totalLabel, }: DenominationGridProps): import("react").JSX.Element;
|
|
2
|
+
export type { DenominationGridColumn, DenominationGridLayout, DenominationGridProps, DenominationGridRow, DenominationGridRowRole, DenominationGridTone, } from './denomination-grid/types.js';
|
|
3
|
+
export declare function DenominationGrid({ label, columns, rows, layout, subtotal, zeroLabel, totalLabel, rowRole, }: DenominationGridProps): import("react").JSX.Element;
|
|
@@ -2,13 +2,15 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { DenominationStrip } from './denomination-grid/DenominationStrip.js';
|
|
3
3
|
import { DenominationTiles } from './denomination-grid/DenominationTiles.js';
|
|
4
4
|
import { assertDenominationGrid } from './denomination-grid/validate.js';
|
|
5
|
-
export function DenominationGrid({ label, columns, rows, layout = 'strip', subtotal, zeroLabel = '—', totalLabel, }) {
|
|
5
|
+
export function DenominationGrid({ label, columns, rows, layout = 'strip', subtotal, zeroLabel = '—', totalLabel, rowRole = 'region', }) {
|
|
6
6
|
assertDenominationGrid(label, columns, rows);
|
|
7
7
|
if (!zeroLabel)
|
|
8
8
|
throw new RangeError('zeroLabel must not be empty');
|
|
9
9
|
if (totalLabel !== undefined && !totalLabel.trim())
|
|
10
10
|
throw new RangeError('totalLabel must not be empty');
|
|
11
|
+
if (rowRole !== 'region' && rowRole !== 'group')
|
|
12
|
+
throw new RangeError("rowRole must be 'region' or 'group'");
|
|
11
13
|
if (layout === 'tiles')
|
|
12
|
-
return (_jsx(DenominationTiles, { columns: columns, label: label, rows: rows, subtotal: subtotal, zeroLabel: zeroLabel }));
|
|
14
|
+
return (_jsx(DenominationTiles, { columns: columns, label: label, rowRole: rowRole, rows: rows, subtotal: subtotal, zeroLabel: zeroLabel }));
|
|
13
15
|
return (_jsx(DenominationStrip, { columns: columns, label: label, rows: rows, totalLabel: totalLabel, zeroLabel: zeroLabel }));
|
|
14
16
|
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { type DialogHTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
import { type Breakpoint } from '../css/breakpoints.js';
|
|
2
3
|
/**
|
|
3
4
|
* `md` is the reading width (32rem) for a message or a short form; `lg`
|
|
4
5
|
* (56rem) holds a row of six fields or a data grid without folding it.
|
|
5
6
|
*/
|
|
6
7
|
export type DialogSize = 'md' | 'lg';
|
|
8
|
+
/**
|
|
9
|
+
* The heading level of the dialog's title. It changes only the element (and
|
|
10
|
+
* so the outline a screen reader reads), never the title's look.
|
|
11
|
+
*/
|
|
12
|
+
export type DialogTitleLevel = 2 | 3;
|
|
7
13
|
export type DialogProps = Omit<DialogHTMLAttributes<HTMLDialogElement>, 'onClose' | 'open' | 'title' | 'children'> & {
|
|
8
14
|
/** Whether the dialog is shown. The element follows it and nothing else. */
|
|
9
15
|
open: boolean;
|
|
@@ -19,6 +25,31 @@ export type DialogProps = Omit<DialogHTMLAttributes<HTMLDialogElement>, 'onClose
|
|
|
19
25
|
onClose: () => void;
|
|
20
26
|
size?: DialogSize;
|
|
21
27
|
title: string;
|
|
28
|
+
/**
|
|
29
|
+
* The title's heading level, 3 (an `h3`) by default. Pass 2 when the
|
|
30
|
+
* dialog's own sections are `h3` headings, so they read as parts of the
|
|
31
|
+
* dialog rather than as its peers. The title's style does not change.
|
|
32
|
+
* Any value other than 2 (from an untyped caller) renders an `h3`.
|
|
33
|
+
*/
|
|
34
|
+
titleLevel?: DialogTitleLevel;
|
|
35
|
+
/**
|
|
36
|
+
* Below this breakpoint the dialog is a bottom sheet: docked to the
|
|
37
|
+
* bottom edge at the full viewport width, its top corners rounded and its
|
|
38
|
+
* bottom corners square, a strip of backdrop left in view above it, and
|
|
39
|
+
* its bottom padding clearing `env(safe-area-inset-bottom)`. It slides up
|
|
40
|
+
* on open unless reduced motion is asked for. There is no drag grabber
|
|
41
|
+
* (and no drag): it closes as any dialog does. From the breakpoint up it
|
|
42
|
+
* is the centered dialog, at its `size`.
|
|
43
|
+
*/
|
|
44
|
+
sheetBelow?: Breakpoint;
|
|
45
|
+
/**
|
|
46
|
+
* The accessible name of a close button, such as "Close". When given, an
|
|
47
|
+
* icon-only ghost button ends the title row and asks `onClose`, like
|
|
48
|
+
* Escape. It is the first control in the dialog, so it takes the focus on
|
|
49
|
+
* open unless a control in the content has `autoFocus`. Left out, the
|
|
50
|
+
* dialog has no close button and its markup is unchanged.
|
|
51
|
+
*/
|
|
52
|
+
closeLabel?: string;
|
|
22
53
|
children: ReactNode;
|
|
23
54
|
};
|
|
24
|
-
export declare function Dialog({ children, className, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, size, title, ...rest }: DialogProps): import("react").JSX.Element;
|
|
55
|
+
export declare function Dialog({ children, className, closeLabel, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, sheetBelow, size, title, titleLevel, ...rest }: DialogProps): import("react").JSX.Element;
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useId } from 'react';
|
|
4
4
|
import { cx } from '../class-names.js';
|
|
5
|
+
import { dialogSheetClass } from '../css/css-dialog.js';
|
|
5
6
|
import { spacingClassNames } from '../spacing-classes.js';
|
|
6
7
|
import { useDialogCloseRequests } from './dialog-close-requests.js';
|
|
8
|
+
import { DialogTitle } from './dialog/DialogTitle.js';
|
|
7
9
|
import { Stack } from './Stack.js';
|
|
8
|
-
|
|
9
|
-
export function Dialog({ children, className, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, size = 'md', title, ...rest }) {
|
|
10
|
+
export function Dialog({ children, className, closeLabel, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, sheetBelow, size = 'md', title, titleLevel = 3, ...rest }) {
|
|
10
11
|
const titleId = useId();
|
|
11
12
|
const { nodeRef, handlers } = useDialogCloseRequests({
|
|
12
13
|
open,
|
|
@@ -16,5 +17,5 @@ export function Dialog({ children, className, onCancel, onClose, onKeyDown, onPo
|
|
|
16
17
|
onPointerDown,
|
|
17
18
|
onSubmit,
|
|
18
19
|
});
|
|
19
|
-
return (_jsx("dialog", { ...rest, ref: nodeRef, "aria-labelledby": titleId, className: cx('sw-dialog', size === 'lg' && 'sw-dialog-lg', ...spacingClassNames({ padding: 5 }), className), ...handlers, children: _jsxs(Stack, { gap: 4, children: [_jsx(
|
|
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] }) }));
|
|
20
21
|
}
|
|
@@ -9,7 +9,10 @@ type ScrollRegionProps = {
|
|
|
9
9
|
* Private wrapper that scrolls wide content inside its own box (see
|
|
10
10
|
* `scrollRegionRules`). It is a keyboard stop named after its content, so a
|
|
11
11
|
* wide table with no focusable cell can still be scrolled sideways from the
|
|
12
|
-
* keyboard and is announced as one group.
|
|
12
|
+
* keyboard and is announced as one group. While content is scrolled out past
|
|
13
|
+
* an inline edge, that edge draws a shade (`sw-scroll-more-start`,
|
|
14
|
+
* `sw-scroll-more-end`), so a phone, whose scrollbars hide, still shows that
|
|
15
|
+
* the table goes on.
|
|
13
16
|
*/
|
|
14
17
|
export declare function ScrollRegion({ children, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, }: ScrollRegionProps): import("react").JSX.Element;
|
|
15
18
|
export {};
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useRef } from 'react';
|
|
4
|
+
import { cx } from '../class-names.js';
|
|
5
|
+
import { useScrollOverflow } from './use-scroll-overflow.js';
|
|
2
6
|
/**
|
|
3
7
|
* Private wrapper that scrolls wide content inside its own box (see
|
|
4
8
|
* `scrollRegionRules`). It is a keyboard stop named after its content, so a
|
|
5
9
|
* wide table with no focusable cell can still be scrolled sideways from the
|
|
6
|
-
* keyboard and is announced as one group.
|
|
10
|
+
* keyboard and is announced as one group. While content is scrolled out past
|
|
11
|
+
* an inline edge, that edge draws a shade (`sw-scroll-more-start`,
|
|
12
|
+
* `sw-scroll-more-end`), so a phone, whose scrollbars hide, still shows that
|
|
13
|
+
* the table goes on.
|
|
7
14
|
*/
|
|
8
15
|
export function ScrollRegion({ children, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, }) {
|
|
9
|
-
|
|
16
|
+
const ref = useRef(null);
|
|
17
|
+
const overflow = useScrollOverflow(ref);
|
|
18
|
+
return (_jsx("div", { ref: ref, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, className: cx(className, overflow.start && 'sw-scroll-more-start', overflow.end && 'sw-scroll-more-end'), role: "group", tabIndex: 0, children: children }));
|
|
10
19
|
}
|
|
@@ -16,15 +16,35 @@ export type TabsProps = TabsLabel & {
|
|
|
16
16
|
items: readonly TabItem[];
|
|
17
17
|
value: string;
|
|
18
18
|
onChange: (id: string) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Keep the strip at the top of the viewport, under the top safe area,
|
|
21
|
+
* while a long panel scrolls under it. Off by default. The stuck strip is
|
|
22
|
+
* a full-bleed band of the page canvas, not a glass card: the canvas
|
|
23
|
+
* color, a little see-through over the glass blur, with its hairline
|
|
24
|
+
* underneath; solid under Reduce Transparency and in forced colors. It
|
|
25
|
+
* shares the sticky `AppHeader`'s layer, so a page uses one or the other
|
|
26
|
+
* at the top edge.
|
|
27
|
+
*
|
|
28
|
+
* A sticky element only sticks within its parent. Make the strip a direct
|
|
29
|
+
* child of the long page container that also holds the panels, not of a
|
|
30
|
+
* padded `Box` round the strip alone; an ancestor whose `overflow` is not
|
|
31
|
+
* `visible` becomes the box it sticks to. Put the page's side gutter on
|
|
32
|
+
* the title and the panels rather than on that container: each tab
|
|
33
|
+
* already has the md control's inline padding (16 px, spacing step 4), so
|
|
34
|
+
* the first label lines up with a `space-4` page gutter when the strip
|
|
35
|
+
* runs edge to edge.
|
|
36
|
+
*/
|
|
37
|
+
sticky?: boolean;
|
|
19
38
|
};
|
|
20
39
|
export declare function tabId(tabsId: string, id: string): string;
|
|
21
40
|
export declare function tabPanelId(tabsId: string, id: string): string;
|
|
22
41
|
/**
|
|
23
42
|
* A tab strip with `tablist` semantics for the sections of one page. Arrow
|
|
24
43
|
* keys, Home and End move the focus and select at once; the strip scrolls
|
|
25
|
-
* sideways when it overflows. Pair each item with a `TabPanel`.
|
|
44
|
+
* sideways when it overflows. Pair each item with a `TabPanel`. `sticky`
|
|
45
|
+
* keeps it at the top of the viewport over a long panel.
|
|
26
46
|
*/
|
|
27
|
-
export declare function Tabs({ id, items, onChange, value, ...labelProps }: TabsProps): import("react").JSX.Element;
|
|
47
|
+
export declare function Tabs({ id, items, onChange, sticky, value, ...labelProps }: TabsProps): import("react").JSX.Element;
|
|
28
48
|
export type TabPanelProps = Omit<HTMLAttributes<HTMLDivElement>, 'id'> & {
|
|
29
49
|
/** The `id` of the `Tabs` this panel belongs to. */
|
|
30
50
|
tabsId: string;
|
package/dist/components/Tabs.js
CHANGED
|
@@ -10,9 +10,10 @@ export function tabPanelId(tabsId, id) {
|
|
|
10
10
|
/**
|
|
11
11
|
* A tab strip with `tablist` semantics for the sections of one page. Arrow
|
|
12
12
|
* keys, Home and End move the focus and select at once; the strip scrolls
|
|
13
|
-
* sideways when it overflows. Pair each item with a `TabPanel`.
|
|
13
|
+
* sideways when it overflows. Pair each item with a `TabPanel`. `sticky`
|
|
14
|
+
* keeps it at the top of the viewport over a long panel.
|
|
14
15
|
*/
|
|
15
|
-
export function Tabs({ id, items, onChange, value, ...labelProps }) {
|
|
16
|
+
export function Tabs({ id, items, onChange, sticky = false, value, ...labelProps }) {
|
|
16
17
|
const tabs = useRef(new Map());
|
|
17
18
|
function select(next) {
|
|
18
19
|
if (!next || next.id === value) {
|
|
@@ -42,7 +43,7 @@ export function Tabs({ id, items, onChange, value, ...labelProps }) {
|
|
|
42
43
|
event.preventDefault();
|
|
43
44
|
select(target);
|
|
44
45
|
}
|
|
45
|
-
return (_jsx("div", { className:
|
|
46
|
+
return (_jsx("div", { className: cx('sw-tabs', sticky && 'sw-tabs-sticky'), onKeyDown: onKeyDown, role: "tablist", ...labelProps, children: items.map((item) => {
|
|
46
47
|
const selected = item.id === value;
|
|
47
48
|
return (_jsx("button", { "aria-controls": tabPanelId(id, item.id), "aria-selected": selected, className: cx('sw-tab', selected && 'sw-tab-selected'), id: tabId(id, item.id), onClick: () => select(item), ref: (node) => {
|
|
48
49
|
if (node) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../../class-names.js';
|
|
3
|
+
import { denominationCellToneClassNames } from '../../css/css-denomination-cell-tones.js';
|
|
3
4
|
import { denominationCellView } from '../../denomination-cells.js';
|
|
4
5
|
import { ScrollRegion } from '../ScrollRegion.js';
|
|
5
6
|
import { RowLabelLine } from './RowLabelLine.js';
|
|
@@ -17,6 +18,6 @@ export function DenominationStrip({ label, columns, rows, zeroLabel, totalLabel,
|
|
|
17
18
|
const hasTotals = rows.some((row) => row.total !== undefined);
|
|
18
19
|
return (_jsx(ScrollRegion, { "aria-label": label, className: "sw-denomination-scroll", children: _jsxs("table", { className: "sw-denomination-grid sw-denomination-strip", 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: column.label }, column.key))), hasTotals ? _jsx(TotalHead, { totalLabel: totalLabel }) : null] }) }), _jsx("tbody", { children: rows.map((row) => (_jsxs("tr", { className: cx('sw-denomination-row', `sw-denomination-row-${row.tone ?? 'neutral'}`, row.signed && 'sw-denomination-row-signed'), children: [_jsx("th", { className: "sw-denomination-label", scope: "row", children: _jsxs("span", { className: "sw-denomination-label-body", children: [_jsx(RowLabelLine, { row: row }), row.total !== undefined ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-total-inline", children: row.total })) : null] }) }), columns.map((column, index) => {
|
|
19
20
|
const view = denominationCellView(row.cells[index] ?? null, row.signed ?? false, zeroLabel);
|
|
20
|
-
return (_jsx("td", { className:
|
|
21
|
+
return (_jsx("td", { className: cx('sw-denomination-cell', `sw-denomination-cell-${view.state}`, ...denominationCellToneClassNames(row.cellTones?.[index])), children: view.text }, column.key));
|
|
21
22
|
}), hasTotals ? (_jsx("td", { className: "sw-denomination-total", children: _jsx("span", { className: "sw-denomination-total-value", children: row.total ?? '' }) })) : null] }, row.id))) })] }) }));
|
|
22
23
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { type DenominationGridProps } from './types.js';
|
|
2
|
-
type TilesProps = Required<Pick<DenominationGridProps, 'label' | 'columns' | 'rows' | 'zeroLabel'>> & Pick<DenominationGridProps, 'subtotal'>;
|
|
3
|
-
export declare function DenominationTiles({ label, columns, rows, subtotal, zeroLabel, }: TilesProps): import("react").JSX.Element;
|
|
2
|
+
type TilesProps = Required<Pick<DenominationGridProps, 'label' | 'columns' | 'rows' | 'zeroLabel' | 'rowRole'>> & Pick<DenominationGridProps, 'subtotal'>;
|
|
3
|
+
export declare function DenominationTiles({ label, columns, rows, rowRole, subtotal, zeroLabel, }: TilesProps): import("react").JSX.Element;
|
|
4
4
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../../class-names.js';
|
|
3
|
+
import { denominationCellToneClassNames } from '../../css/css-denomination-cell-tones.js';
|
|
3
4
|
import { denominationCellView } from '../../denomination-cells.js';
|
|
4
5
|
import { RowLabelLine } from './RowLabelLine.js';
|
|
5
6
|
/**
|
|
@@ -10,24 +11,28 @@ function showsRowLabel(rowCount, row) {
|
|
|
10
11
|
return rowCount > 1 || Boolean(row.icon) || row.total !== undefined;
|
|
11
12
|
}
|
|
12
13
|
/**
|
|
13
|
-
* A row is
|
|
14
|
-
* line whose label repeats the grid's:
|
|
15
|
-
* group's name a second time (Teisoro
|
|
16
|
-
* words keeps them, since nothing else on
|
|
14
|
+
* A row is named by its label (a region, or a group at `rowRole="group"`),
|
|
15
|
+
* except a lone row without its label line whose label repeats the grid's:
|
|
16
|
+
* that name would only say the group's name a second time (Teisoro
|
|
17
|
+
* SDAY-31). A lone row with its own words keeps them, since nothing else on
|
|
18
|
+
* screen says them.
|
|
17
19
|
*/
|
|
18
20
|
function namesRow(rowCount, row, gridLabel) {
|
|
19
21
|
return showsRowLabel(rowCount, row) || row.label !== gridLabel;
|
|
20
22
|
}
|
|
21
|
-
export function DenominationTiles({ label, columns, rows, subtotal, zeroLabel, }) {
|
|
23
|
+
export function DenominationTiles({ label, columns, rows, rowRole, subtotal, zeroLabel, }) {
|
|
22
24
|
return (_jsx("div", { "aria-label": label, className: "sw-denomination-grid sw-denomination-tiles", role: "group", children: rows.map((row) => {
|
|
23
25
|
const labelled = showsRowLabel(rows.length, row);
|
|
24
|
-
|
|
26
|
+
const named = namesRow(rows.length, row, label);
|
|
27
|
+
// A named section is a region; role="group" keeps the name without
|
|
28
|
+
// the landmark. An unnamed row stays a generic section either way.
|
|
29
|
+
return (_jsxs("section", { "aria-label": named ? row.label : undefined, className: cx('sw-denomination-row', `sw-denomination-row-${row.tone ?? 'neutral'}`), role: named && rowRole === 'group' ? 'group' : undefined, children: [labelled ? (_jsxs("span", { className: "sw-denomination-label", children: [_jsx(RowLabelLine, { row: row }), row.total !== undefined ? (_jsx("span", { className: "sw-denomination-total", children: row.total })) : null] })) : null, _jsx("ul", { className: "sw-denomination-tile-list", children: columns.map((column, index) => {
|
|
25
30
|
const cell = row.cells[index] ?? null;
|
|
26
31
|
const view = denominationCellView(cell, row.signed ?? false, zeroLabel);
|
|
27
32
|
const amount = subtotal && cell !== null && cell !== 0
|
|
28
33
|
? subtotal(cell, column)
|
|
29
34
|
: null;
|
|
30
|
-
return (_jsxs("li", { className:
|
|
35
|
+
return (_jsxs("li", { className: cx('sw-denomination-tile', ...denominationCellToneClassNames(row.cellTones?.[index])), children: [_jsx("span", { className: "sw-denomination-head", children: column.label }), _jsx("span", { className: `sw-denomination-cell sw-denomination-cell-${view.state}`, children: view.text }), subtotal ? (_jsx("span", { className: "sw-denomination-subtotal", children: amount ?? zeroLabel })) : null] }, column.key));
|
|
31
36
|
}) })] }, row.id));
|
|
32
37
|
}) }));
|
|
33
38
|
}
|
|
@@ -2,6 +2,11 @@ import { type ReactNode } from 'react';
|
|
|
2
2
|
import { type BadgeTone } from '../../css/css-data.js';
|
|
3
3
|
export type DenominationGridTone = BadgeTone;
|
|
4
4
|
export type DenominationGridLayout = 'strip' | 'tiles';
|
|
5
|
+
/**
|
|
6
|
+
* What a named tiles row is to assistive technology: a `region` (a landmark,
|
|
7
|
+
* the default) or a `group` (named, but not in the landmark list).
|
|
8
|
+
*/
|
|
9
|
+
export type DenominationGridRowRole = 'region' | 'group';
|
|
5
10
|
export type DenominationGridColumn = {
|
|
6
11
|
key: string;
|
|
7
12
|
label: string;
|
|
@@ -14,6 +19,13 @@ export type DenominationGridRow = {
|
|
|
14
19
|
tone?: DenominationGridTone;
|
|
15
20
|
signed?: boolean;
|
|
16
21
|
total?: string;
|
|
22
|
+
/**
|
|
23
|
+
* A tone per count, one entry per column (null for none), for a single
|
|
24
|
+
* count that needs attention, such as a bill the vault is short of. The
|
|
25
|
+
* count is set in the tone and, in the tiles layout, its tile's border
|
|
26
|
+
* too, in place. The tone never says why alone: put that in words.
|
|
27
|
+
*/
|
|
28
|
+
cellTones?: readonly (DenominationGridTone | null)[];
|
|
17
29
|
};
|
|
18
30
|
export type DenominationGridProps = {
|
|
19
31
|
label: string;
|
|
@@ -27,4 +39,11 @@ export type DenominationGridProps = {
|
|
|
27
39
|
* hidden column header), such as "Total". Only the strip has that column.
|
|
28
40
|
*/
|
|
29
41
|
totalLabel?: string;
|
|
42
|
+
/**
|
|
43
|
+
* The role of each named row in the tiles layout: `region` (the default,
|
|
44
|
+
* a landmark named by the row's label) or `group` (the same name, not a
|
|
45
|
+
* landmark), for a page with many grids whose rows should not fill the
|
|
46
|
+
* landmark list. Only the tiles layout has row containers.
|
|
47
|
+
*/
|
|
48
|
+
rowRole?: DenominationGridRowRole;
|
|
30
49
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { badgeTones } from '../../css/css-data.js';
|
|
1
2
|
import { isDenominationCount } from '../../denomination-cells.js';
|
|
2
3
|
export function assertDenominationGrid(label, columns, rows) {
|
|
3
4
|
if (!label.trim())
|
|
@@ -15,4 +16,20 @@ export function assertDenominationGrid(label, columns, rows) {
|
|
|
15
16
|
throw new RangeError('each row needs one cell per column');
|
|
16
17
|
if (rows.some((row) => row.cells.some((cell) => !isDenominationCount(cell))))
|
|
17
18
|
throw new RangeError('cells must be integers or null');
|
|
19
|
+
if (rows.some((row) => !hasValidCellTones(row, columns.length)))
|
|
20
|
+
throw new RangeError('cellTones needs one tone or null per column, from the Badge tones');
|
|
21
|
+
}
|
|
22
|
+
function hasValidCellTones(row, columnCount) {
|
|
23
|
+
const tones = row.cellTones;
|
|
24
|
+
if (tones === undefined)
|
|
25
|
+
return true;
|
|
26
|
+
if (!Array.isArray(tones) || tones.length !== columnCount)
|
|
27
|
+
return false;
|
|
28
|
+
// An index loop, not every(): every() skips the holes of a sparse array.
|
|
29
|
+
for (let index = 0; index < columnCount; index += 1) {
|
|
30
|
+
const tone = tones[index];
|
|
31
|
+
if (tone !== null && !badgeTones.includes(tone))
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
18
35
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type DialogTitleProps = {
|
|
2
|
+
id: string;
|
|
3
|
+
level: 2 | 3;
|
|
4
|
+
title: string;
|
|
5
|
+
/** The close button's accessible name; without it there is no button. */
|
|
6
|
+
closeLabel?: string;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* The dialog's title, which names it through `aria-labelledby`. With a
|
|
11
|
+
* `closeLabel` it shares a row with an icon-only ghost close button at its
|
|
12
|
+
* end; without one it is the title alone, as it always was.
|
|
13
|
+
*/
|
|
14
|
+
export declare function DialogTitle({ closeLabel, id, level, onClose, title, }: DialogTitleProps): import("react").JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from '../Button.js';
|
|
3
|
+
import { Text } from '../Text.js';
|
|
4
|
+
/**
|
|
5
|
+
* The dialog's title, which names it through `aria-labelledby`. With a
|
|
6
|
+
* `closeLabel` it shares a row with an icon-only ghost close button at its
|
|
7
|
+
* end; without one it is the title alone, as it always was.
|
|
8
|
+
*/
|
|
9
|
+
export function DialogTitle({ closeLabel, id, level, onClose, title, }) {
|
|
10
|
+
const heading = (_jsx(Text, { as: level === 2 ? 'h2' : 'h3', id: id, variant: "title", children: title }));
|
|
11
|
+
if (!closeLabel)
|
|
12
|
+
return heading;
|
|
13
|
+
return (_jsxs("div", { className: "sw-dialog-header", children: [heading, _jsxs(Button, { className: "sw-dialog-close", onPress: onClose, size: "sm", variant: "ghost", children: [_jsx("span", { className: "sw-sr-only", children: closeLabel }), _jsx("span", { "aria-hidden": "true", className: "sw-dialog-close-glyph" })] })] }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import { type ScrollOverflow } from '../scroll-overflow.js';
|
|
3
|
+
/**
|
|
4
|
+
* Follows which inline edges of a scroll box have more content past them:
|
|
5
|
+
* on mount, on every scroll, and whenever the box or its content changes
|
|
6
|
+
* size. Before mount (and on the server) it reports no overflow, so the
|
|
7
|
+
* box renders as it always has until it is measured.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useScrollOverflow(ref: RefObject<HTMLElement | null>): ScrollOverflow;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { noScrollOverflow, sameScrollOverflow, scrollOverflow, } from '../scroll-overflow.js';
|
|
4
|
+
/**
|
|
5
|
+
* Follows which inline edges of a scroll box have more content past them:
|
|
6
|
+
* on mount, on every scroll, and whenever the box or its content changes
|
|
7
|
+
* size. Before mount (and on the server) it reports no overflow, so the
|
|
8
|
+
* box renders as it always has until it is measured.
|
|
9
|
+
*/
|
|
10
|
+
export function useScrollOverflow(ref) {
|
|
11
|
+
const [overflow, setOverflow] = useState(noScrollOverflow);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const box = ref.current;
|
|
14
|
+
if (!box)
|
|
15
|
+
return;
|
|
16
|
+
function measure() {
|
|
17
|
+
if (!box)
|
|
18
|
+
return;
|
|
19
|
+
const next = scrollOverflow(box.scrollLeft, box.scrollWidth, box.clientWidth);
|
|
20
|
+
setOverflow((current) => sameScrollOverflow(current, next) ? current : next);
|
|
21
|
+
}
|
|
22
|
+
measure();
|
|
23
|
+
box.addEventListener('scroll', measure, { passive: true });
|
|
24
|
+
const resize = typeof ResizeObserver === 'undefined'
|
|
25
|
+
? null
|
|
26
|
+
: new ResizeObserver(measure);
|
|
27
|
+
if (resize) {
|
|
28
|
+
resize.observe(box);
|
|
29
|
+
for (const child of Array.from(box.children))
|
|
30
|
+
resize.observe(child);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
window.addEventListener('resize', measure);
|
|
34
|
+
}
|
|
35
|
+
return () => {
|
|
36
|
+
box.removeEventListener('scroll', measure);
|
|
37
|
+
if (resize)
|
|
38
|
+
resize.disconnect();
|
|
39
|
+
else
|
|
40
|
+
window.removeEventListener('resize', measure);
|
|
41
|
+
};
|
|
42
|
+
}, [ref]);
|
|
43
|
+
return overflow;
|
|
44
|
+
}
|
|
@@ -50,14 +50,21 @@ export function cssActionBarClasses() {
|
|
|
50
50
|
padding-right: max(var(--sw-space-4), env(safe-area-inset-right, 0px));
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/* A Badge and a short line sit on one row a small gap apart; a line that
|
|
54
|
+
does not fit beside the badge wraps under it. */
|
|
53
55
|
.sw-action-bar-status {
|
|
56
|
+
align-items: center;
|
|
54
57
|
color: var(--sw-color-muted);
|
|
58
|
+
column-gap: var(--sw-space-2);
|
|
59
|
+
display: flex;
|
|
55
60
|
flex: 1 1 auto;
|
|
61
|
+
flex-wrap: wrap;
|
|
56
62
|
font-family: var(--sw-font-sans);
|
|
57
63
|
font-size: ${caption.fontSize}px;
|
|
58
64
|
line-height: ${caption.lineHeight}px;
|
|
59
65
|
margin: 0;
|
|
60
66
|
min-width: 0;
|
|
67
|
+
row-gap: var(--sw-space-1);
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
/* With no status the live region stays in the page, so a first status is
|