@scalewing/react 1.15.0 → 1.17.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 +26 -0
- package/README.md +30 -5
- package/dist/components/DenominationGrid.d.ts +2 -2
- package/dist/components/DenominationGrid.js +4 -2
- package/dist/components/Dialog.d.ts +13 -1
- package/dist/components/Dialog.js +2 -2
- package/dist/components/Field.d.ts +10 -1
- package/dist/components/Field.js +17 -4
- package/dist/components/ScrollRegion.d.ts +4 -1
- package/dist/components/ScrollRegion.js +11 -2
- package/dist/components/denomination-grid/DenominationStrip.js +4 -2
- package/dist/components/denomination-grid/DenominationTiles.d.ts +2 -2
- package/dist/components/denomination-grid/DenominationTiles.js +13 -7
- package/dist/components/denomination-grid/RowLabelLine.d.ts +9 -0
- package/dist/components/denomination-grid/RowLabelLine.js +9 -0
- package/dist/components/denomination-grid/types.d.ts +19 -0
- package/dist/components/denomination-grid/validate.js +17 -0
- package/dist/components/use-scroll-overflow.d.ts +9 -0
- package/dist/components/use-scroll-overflow.js +44 -0
- package/dist/css/css-button.d.ts +1 -0
- package/dist/css/css-button.js +9 -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 +23 -1
- package/dist/css/css-document.d.ts +2 -0
- package/dist/css/css-document.js +16 -0
- package/dist/css/css-scroll-region.d.ts +16 -2
- package/dist/css/css-scroll-region.js +68 -2
- package/dist/css/css-toast.d.ts +1 -0
- package/dist/css/css-toast.js +9 -0
- 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 +83 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.17.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
- 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.
|
|
9
|
+
- 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.
|
|
10
|
+
- 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.
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 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.
|
|
15
|
+
|
|
16
|
+
## 1.16.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- b64a805: `Field` takes `invalid` (`docs/requests/teisoro-field-validation.md`, 2026-09-30 follow-up, Teisoro F-007-S05 task 1375, DRW-18). `invalid?: boolean` marks the native control invalid, `aria-invalid="true"` and the same danger border as `error` (on a `prefix`/`suffix` frame too, `Mark` in forced colors), without a message of its own, for fields whose one error is shown elsewhere, such as under a group of count fields; the consumer points the control's `aria-describedby` at that message, and it is kept. The field's polite region stays empty and its `description` stays. `error` implies it. Passing `invalid`, even `invalid={false}` but not `invalid={undefined}` (so a wrapper forwarding an optional prop is safe), requires one native control child from the first render (a `TypeError` otherwise), so a form that marks a composed child learns it while its fields are valid, not on its first failed submit. `error` is unchanged: it requires the native control only while it holds a message, so `error={undefined}` beside a composed child still renders. Without it nothing changes. No new dependencies.
|
|
21
|
+
- 68beae9: A programmatic focus target takes the accent focus ring (`docs/requests/teisoro-focus-target.md`, Teisoro F-007-S05 task 1375, NSF-34, ENT-29, DRW-30). The generated document canvas gains `:where([data-theme] [tabindex='-1']:focus-visible)` with the same ring as links and native controls (`--sw-focus-ring-width` solid `--sw-color-accent`, `--sw-focus-ring-offset` out), so a notice or card a script focuses after a save, such as a `Box tabIndex={-1} radius="lg"` round a `Card`, no longer shows the browser's outline. The ring shows only when focus is visible, as the browser's does, and the rule has zero specificity, so a component's own ring always wins. No new prop or class and no new dependencies.
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- 849ce60: `Button` sets a glyph one token gap from its label (`docs/requests/teisoro-button.md`, 2026-09-30 follow-up, Teisoro F-007-S05 task 1375). `.sw-button` gains `gap: var(--sw-space-2)`, so a glyph and a label passed as the button's own children sit 8 px apart at every size instead of touching. An icon-only button, a visually hidden name (`CalendarButton`) and a label already wrapped in one element with its own gap (`<Inline as="span" gap={2}>`) are unchanged: each is a single flex item, so there is never a second gap, and the wrapper can be dropped. A phrase split across elements (`Save <strong>draft</strong> now`) already drew its parts touching, because each is a flex item whose edge spaces collapse; they are now 8 px apart. Put such a phrase in one element, where its word spaces are kept. No API change and no new dependencies.
|
|
26
|
+
- 9a84726: `DenominationGrid` keeps a row's icon beside its label (`docs/requests/teisoro-denomination-grid.md`, 2026-09-30 follow-up, Teisoro F-007-S05 task 1375, DRW-28). A row's icon and words now sit in `.sw-denomination-label-line`, a flex row that does not wrap, in both layouts, so a long label wraps its words beside the icon instead of dropping them under it. Below `md` the strip no longer puts the icon on its own line over the label: the line stays a row (its gap narrows to spacing step 1 in the strip only; a tiles row keeps step 2), and the phone total keeps its own line under it. New generated class `sw-denomination-label-line`; no API change and no new dependencies.
|
|
27
|
+
- b7beef1: `Toast` keeps the page gutter on a phone (`docs/requests/teisoro-toast.md`, 2026-09-30 follow-up, Teisoro F-007-S05 task 1375, DRW-29). `.sw-toast` is capped at `calc(100% - var(--sw-space-4) - var(--sw-space-4))` of the top layer and wraps a long word (`overflow-wrap: break-word`), so a message wider than the screen wraps inside a 16 px gutter on each side instead of running to the edges. A toast is still as wide as its message up to that cap and centred, so a short toast is unchanged. No API change and no new dependencies.
|
|
28
|
+
|
|
3
29
|
## 1.15.0
|
|
4
30
|
|
|
5
31
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -50,7 +50,23 @@ Import the CSS once at the application entry. Do not copy it into your source tr
|
|
|
50
50
|
|
|
51
51
|
`Box hideBelow="md"` hides a region under 48rem and `hideFrom="md"` hides it at 48rem and wider, so a product can swap a wide layout for a narrow one without its own stylesheet. Hidden regions leave the accessibility tree; do not use this to protect data. `Box as="a"` is a layout link. Use `Button` for press actions. `Field` associates a native `<input>` or `<select>` with a label and token gap; native text controls inherit the generated document canvas. `Select` is a labeled listbox menu when the open list must match the canvas.
|
|
52
52
|
|
|
53
|
-
`Field` supports optional `description`, `error`, and `required` on one native input, select, or textarea child. It preserves an existing `aria-describedby` and appends a stable ID for the hint, or for the error, which replaces the hint on screen while it is supplied. `error` sets `aria-invalid` and token-owned invalid styling. The error sits in a polite live region (`aria-live="polite"`) that is always rendered, empty while there is no error, so an error that appears while the person types, or after a submit, is announced once, politely, when its text is swapped in; it is never `role="alert"`, so a submit that finds several errors does not fire several assertive alerts at once. Empty, the region takes no room in the layout. The consumer owns validation and localized messages, and after a refused submit it moves focus to the first invalid control (or to one form-level summary). `Checkbox`, `RadioGroup` and `DateField` expose their `error` the same way.
|
|
53
|
+
`Field` supports optional `description`, `error`, and `required` on one native input, select, or textarea child. It preserves an existing `aria-describedby` and appends a stable ID for the hint, or for the error, which replaces the hint on screen while it is supplied. `error` sets `aria-invalid` and token-owned invalid styling. The error sits in a polite live region (`aria-live="polite"`) that is always rendered, empty while there is no error, so an error that appears while the person types, or after a submit, is announced once, politely, when its text is swapped in; it is never `role="alert"`, so a submit that finds several errors does not fire several assertive alerts at once. Empty, the region takes no room in the layout. The consumer owns validation and localized messages, and after a refused submit it moves focus to the first invalid control (or to one form-level summary). `Checkbox`, `RadioGroup` and `DateField` expose their `error` the same way. `invalid` marks the control invalid the same way (`aria-invalid` and the danger border, `Mark` in forced colors) without a message of its own, for fields whose one error is shown elsewhere, such as a count that adds up to nothing under a group of count fields: point each control's `aria-describedby` at that message so it is read with the field. The field's own polite region stays empty and its `description` stays on screen; `error` implies `invalid`. Passing `invalid`, even `invalid={false}` but not `invalid={undefined}`, needs one native control child and throws a `TypeError` on the first render otherwise, so a form learns it while the fields are valid, not on its first failed submit. `error` keeps its rule: it needs the native control only while it holds a message. `invalid` is `Field`'s only: `DateField`, `Select` and the other controls with `error` do not have it yet.
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
<Field invalid={countIsZero} label="$1 bills">
|
|
57
|
+
<input aria-describedby={countIsZero ? countErrorId : undefined} />
|
|
58
|
+
</Field>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
An element a script focuses, such as a notice or a card announced after a save, takes the same accent focus ring as the canvas's links and native controls, 2 px past its edge (`--sw-focus-ring-width` and `--sw-focus-ring-offset`), instead of the browser's outline. Give it `tabIndex={-1}`, so it takes focus from a script but is no tab stop, and put the focus on the whole card rather than its text, so the ring goes round it clear of the words; a `Box` round the card with the card's `radius` makes the ring follow its corners. As with the browser's own outline, the ring shows only when focus is visible (`:focus-visible`): after a keyboard press, not after a mouse press. The rule has zero specificity, so a component's own ring always wins.
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
<Box ref={notice} tabIndex={-1} radius="lg" role="status">
|
|
65
|
+
<Card padding={4} variant="outlined">
|
|
66
|
+
<Text as="p">Survey saved.</Text>
|
|
67
|
+
</Card>
|
|
68
|
+
</Box>
|
|
69
|
+
```
|
|
54
70
|
|
|
55
71
|
A disabled native text control (`<input>`, `<select>` or `<textarea>` on the canvas, a `Field` frame with a `prefix` or `suffix`, or a `DateField` entry) takes the quiet subtle fill, a dashed border and a not-allowed cursor. Its value stays in the text color at full strength, so a locked value is still readable; forced colors keep the dashed border in the system's disabled color. A typed input's placeholder, enabled or disabled, is drawn in the muted color, which keeps 4.5:1 on the field in every palette.
|
|
56
72
|
|
|
@@ -124,15 +140,24 @@ A disabled native text control (`<input>`, `<select>` or `<textarea>` on the can
|
|
|
124
140
|
</Stack>
|
|
125
141
|
```
|
|
126
142
|
|
|
143
|
+
A `Button`'s own children are laid out in a row one token gap apart (spacing step 2, 8 px), so a glyph passed beside the label never touches it. Pass the glyph and the label as direct children. An icon-only button has one child and is unchanged, and a visually hidden name (`sw-sr-only`) is out of the flow, so it adds no gap. A label already wrapped in one element with its own gap, such as `<Inline as="span" gap={2}>`, is a single child and gets no second gap; it can drop the wrapper. A phrase split across elements, such as `Save <strong>draft</strong> now`, is several flex items: the spaces at their edges collapse, so its parts touched before and are a gap apart now. Put such a phrase in one element (`<span>Save <strong>draft</strong> now</span>`), where its word spaces are kept.
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
<Button onPress={logSighting}>
|
|
147
|
+
<Plus aria-hidden />
|
|
148
|
+
Log sighting
|
|
149
|
+
</Button>
|
|
150
|
+
```
|
|
151
|
+
|
|
127
152
|
A `Button` that toggles something passes `aria-pressed`. The pressed button gets a 2 px accent ring outside its fill, past a 2 px gap in `--sw-color-background`, so the ring meets only the page and keeps the accent's contrast on the canvas (4.5:1 or more in every palette, at least the 3:1 a state indicator needs) whatever its `variant` or fill. A focused pressed button moves its focus outline out past the ring. In forced colors, which drop shadows, the pressed button keeps the forced button colors, so its label and any glyph or badge inside it stay readable, and draws the same ring and its own border in the system highlight (`Highlight`); nothing opts out of forced colors. An unpressed button is drawn at full strength, so its label keeps its contrast. To make the choice stand out further, give the pressed button `primary` and the others `secondary`.
|
|
128
153
|
|
|
129
154
|
`Badge` is a non-interactive chicklet with a `tone` (`neutral`, `accent`, `success`, `danger`, `warning`) and a `size` (`md`, or `sm` for a table cell). A badge inside a filled `Button` (every variant but `ghost`) sits on `--sw-color-surface` with its words in the text color and its tone on its border, so it reads at 4.5:1 in every palette whatever the button's fill.
|
|
130
155
|
|
|
131
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.
|
|
132
157
|
|
|
133
|
-
`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`.
|
|
134
159
|
|
|
135
|
-
`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.
|
|
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.
|
|
136
161
|
|
|
137
162
|
`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`.
|
|
138
163
|
|
|
@@ -193,7 +218,7 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
|
|
|
193
218
|
/>
|
|
194
219
|
```
|
|
195
220
|
|
|
196
|
-
`Table` aligns data in rows inside its own keyboard-reachable scroll region, named after the table. `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.
|
|
221
|
+
`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.
|
|
197
222
|
|
|
198
223
|
`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.
|
|
199
224
|
|
|
@@ -237,7 +262,7 @@ Values outside the catalog throw a `RangeError`. There is no per-breakpoint span
|
|
|
237
262
|
</Grid>
|
|
238
263
|
```
|
|
239
264
|
|
|
240
|
-
`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. The primitive does no arithmetic and no currency formatting; tones reuse the Badge vocabulary. Invalid columns, rows, or cells throw.
|
|
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 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.
|
|
241
266
|
|
|
242
267
|
`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.
|
|
243
268
|
|
|
@@ -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
|
}
|
|
@@ -4,6 +4,11 @@ import { type DialogHTMLAttributes, type ReactNode } from 'react';
|
|
|
4
4
|
* (56rem) holds a row of six fields or a data grid without folding it.
|
|
5
5
|
*/
|
|
6
6
|
export type DialogSize = 'md' | 'lg';
|
|
7
|
+
/**
|
|
8
|
+
* The heading level of the dialog's title. It changes only the element (and
|
|
9
|
+
* so the outline a screen reader reads), never the title's look.
|
|
10
|
+
*/
|
|
11
|
+
export type DialogTitleLevel = 2 | 3;
|
|
7
12
|
export type DialogProps = Omit<DialogHTMLAttributes<HTMLDialogElement>, 'onClose' | 'open' | 'title' | 'children'> & {
|
|
8
13
|
/** Whether the dialog is shown. The element follows it and nothing else. */
|
|
9
14
|
open: boolean;
|
|
@@ -19,6 +24,13 @@ export type DialogProps = Omit<DialogHTMLAttributes<HTMLDialogElement>, 'onClose
|
|
|
19
24
|
onClose: () => void;
|
|
20
25
|
size?: DialogSize;
|
|
21
26
|
title: string;
|
|
27
|
+
/**
|
|
28
|
+
* The title's heading level, 3 (an `h3`) by default. Pass 2 when the
|
|
29
|
+
* dialog's own sections are `h3` headings, so they read as parts of the
|
|
30
|
+
* dialog rather than as its peers. The title's style does not change.
|
|
31
|
+
* Any value other than 2 (from an untyped caller) renders an `h3`.
|
|
32
|
+
*/
|
|
33
|
+
titleLevel?: DialogTitleLevel;
|
|
22
34
|
children: ReactNode;
|
|
23
35
|
};
|
|
24
|
-
export declare function Dialog({ children, className, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, size, title, ...rest }: DialogProps): import("react").JSX.Element;
|
|
36
|
+
export declare function Dialog({ children, className, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, size, title, titleLevel, ...rest }: DialogProps): import("react").JSX.Element;
|
|
@@ -6,7 +6,7 @@ import { spacingClassNames } from '../spacing-classes.js';
|
|
|
6
6
|
import { useDialogCloseRequests } from './dialog-close-requests.js';
|
|
7
7
|
import { Stack } from './Stack.js';
|
|
8
8
|
import { Text } from './Text.js';
|
|
9
|
-
export function Dialog({ children, className, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, size = 'md', title, ...rest }) {
|
|
9
|
+
export function Dialog({ children, className, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, size = 'md', title, titleLevel = 3, ...rest }) {
|
|
10
10
|
const titleId = useId();
|
|
11
11
|
const { nodeRef, handlers } = useDialogCloseRequests({
|
|
12
12
|
open,
|
|
@@ -16,5 +16,5 @@ export function Dialog({ children, className, onCancel, onClose, onKeyDown, onPo
|
|
|
16
16
|
onPointerDown,
|
|
17
17
|
onSubmit,
|
|
18
18
|
});
|
|
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(Text, { id: titleId, variant: "title", children: title }), children] }) }));
|
|
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(Text, { as: titleLevel === 2 ? 'h2' : 'h3', id: titleId, variant: "title", children: title }), children] }) }));
|
|
20
20
|
}
|
|
@@ -9,10 +9,19 @@ export type FieldProps = {
|
|
|
9
9
|
size?: FieldSize;
|
|
10
10
|
description?: string;
|
|
11
11
|
error?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Marks the control invalid (`aria-invalid` and the danger border) without
|
|
14
|
+
* a message of its own, for a field whose error is shown elsewhere, such
|
|
15
|
+
* as one message under a group of fields. Point the control's
|
|
16
|
+
* `aria-describedby` at that message. `error` implies it. Passing
|
|
17
|
+
* `invalid`, even `false` but not `undefined`, requires one native control
|
|
18
|
+
* child from the first render.
|
|
19
|
+
*/
|
|
20
|
+
invalid?: boolean;
|
|
12
21
|
required?: boolean;
|
|
13
22
|
/** Short text inside the control frame before the value, such as a currency sign. Not part of the value. */
|
|
14
23
|
prefix?: string;
|
|
15
24
|
/** Short text inside the control frame after the value, such as a unit. Not part of the value. */
|
|
16
25
|
suffix?: string;
|
|
17
26
|
};
|
|
18
|
-
export declare function Field(
|
|
27
|
+
export declare function Field(props: FieldProps): import("react").JSX.Element;
|
package/dist/components/Field.js
CHANGED
|
@@ -5,7 +5,19 @@ import { adornedControlAria, FieldAdornment, } from './FieldAdornment.js';
|
|
|
5
5
|
import { FieldErrorRegion } from './FieldErrorRegion.js';
|
|
6
6
|
import { FieldLabelText } from './FieldLabelText.js';
|
|
7
7
|
import { Stack } from './Stack.js';
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* `invalid` was passed, even as `false`, but not as `undefined` (a wrapper
|
|
10
|
+
* forwarding an optional prop passes nothing): such a field needs one native
|
|
11
|
+
* control from its first render, so a form learns it on the happy path
|
|
12
|
+
* instead of on its first failed submit. `error` keeps its older rule (it
|
|
13
|
+
* requires the control only while it holds a message), so a composed
|
|
14
|
+
* child beside `error={undefined}` still renders.
|
|
15
|
+
*/
|
|
16
|
+
function passesInvalid(props) {
|
|
17
|
+
return props.invalid !== undefined;
|
|
18
|
+
}
|
|
19
|
+
export function Field(props) {
|
|
20
|
+
const { children, gap = 1, label, labelVisuallyHidden = false, size = 'md', description, error, invalid = false, required = false, prefix, suffix, } = props;
|
|
9
21
|
const controlId = useId();
|
|
10
22
|
const descriptionId = useId();
|
|
11
23
|
const errorId = useId();
|
|
@@ -13,12 +25,13 @@ export function Field({ children, gap = 1, label, labelVisuallyHidden = false, s
|
|
|
13
25
|
const prefixId = useId();
|
|
14
26
|
const suffixId = useId();
|
|
15
27
|
const message = error || description;
|
|
28
|
+
const marksInvalid = Boolean(error) || invalid;
|
|
16
29
|
const adorned = Boolean(prefix || suffix);
|
|
17
|
-
const needsControl = Boolean(message || required || adorned);
|
|
30
|
+
const needsControl = Boolean(message || marksInvalid || passesInvalid(props) || required || adorned);
|
|
18
31
|
const className = [
|
|
19
32
|
'sw-field',
|
|
20
33
|
size === 'xs' && 'sw-field-xs',
|
|
21
|
-
|
|
34
|
+
marksInvalid && 'sw-field-invalid',
|
|
22
35
|
]
|
|
23
36
|
.filter(Boolean)
|
|
24
37
|
.join(' ');
|
|
@@ -46,7 +59,7 @@ export function Field({ children, gap = 1, label, labelVisuallyHidden = false, s
|
|
|
46
59
|
const control = cloneElement(children, {
|
|
47
60
|
id: children.props.id || controlId,
|
|
48
61
|
'aria-describedby': describedBy || undefined,
|
|
49
|
-
'aria-invalid':
|
|
62
|
+
'aria-invalid': marksInvalid ? true : children.props['aria-invalid'],
|
|
50
63
|
'aria-labelledby': adornment.labelledBy,
|
|
51
64
|
required: required || children.props.required,
|
|
52
65
|
});
|
|
@@ -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
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
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';
|
|
6
|
+
import { RowLabelLine } from './RowLabelLine.js';
|
|
5
7
|
/**
|
|
6
8
|
* The total column's header: a visually hidden `th` when the consumer names
|
|
7
9
|
* the column, otherwise the empty corner it has always been. Both carry
|
|
@@ -14,8 +16,8 @@ function TotalHead({ totalLabel }) {
|
|
|
14
16
|
}
|
|
15
17
|
export function DenominationStrip({ label, columns, rows, zeroLabel, totalLabel, }) {
|
|
16
18
|
const hasTotals = rows.some((row) => row.total !== undefined);
|
|
17
|
-
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: [
|
|
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) => {
|
|
18
20
|
const view = denominationCellView(row.cells[index] ?? null, row.signed ?? false, zeroLabel);
|
|
19
|
-
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));
|
|
20
22
|
}), hasTotals ? (_jsx("td", { className: "sw-denomination-total", children: _jsx("span", { className: "sw-denomination-total-value", children: row.total ?? '' }) })) : null] }, row.id))) })] }) }));
|
|
21
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,6 +1,8 @@
|
|
|
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';
|
|
5
|
+
import { RowLabelLine } from './RowLabelLine.js';
|
|
4
6
|
/**
|
|
5
7
|
* A lone row is named by the grid's own label, so its label line shows only
|
|
6
8
|
* when it adds something: an icon or a total. Several rows always show it.
|
|
@@ -9,24 +11,28 @@ function showsRowLabel(rowCount, row) {
|
|
|
9
11
|
return rowCount > 1 || Boolean(row.icon) || row.total !== undefined;
|
|
10
12
|
}
|
|
11
13
|
/**
|
|
12
|
-
* A row is
|
|
13
|
-
* line whose label repeats the grid's:
|
|
14
|
-
* group's name a second time (Teisoro
|
|
15
|
-
* 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.
|
|
16
19
|
*/
|
|
17
20
|
function namesRow(rowCount, row, gridLabel) {
|
|
18
21
|
return showsRowLabel(rowCount, row) || row.label !== gridLabel;
|
|
19
22
|
}
|
|
20
|
-
export function DenominationTiles({ label, columns, rows, subtotal, zeroLabel, }) {
|
|
23
|
+
export function DenominationTiles({ label, columns, rows, rowRole, subtotal, zeroLabel, }) {
|
|
21
24
|
return (_jsx("div", { "aria-label": label, className: "sw-denomination-grid sw-denomination-tiles", role: "group", children: rows.map((row) => {
|
|
22
25
|
const labelled = showsRowLabel(rows.length, row);
|
|
23
|
-
|
|
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) => {
|
|
24
30
|
const cell = row.cells[index] ?? null;
|
|
25
31
|
const view = denominationCellView(cell, row.signed ?? false, zeroLabel);
|
|
26
32
|
const amount = subtotal && cell !== null && cell !== 0
|
|
27
33
|
? subtotal(cell, column)
|
|
28
34
|
: null;
|
|
29
|
-
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));
|
|
30
36
|
}) })] }, row.id));
|
|
31
37
|
}) }));
|
|
32
38
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type DenominationGridRow } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A row label's glyph and words, kept on one line: when the words wrap,
|
|
4
|
+
* they wrap beside the glyph, never under it (Teisoro DRW-28). Both the
|
|
5
|
+
* strip and the tiles layouts draw a row label with it.
|
|
6
|
+
*/
|
|
7
|
+
export declare function RowLabelLine({ row }: {
|
|
8
|
+
row: DenominationGridRow;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A row label's glyph and words, kept on one line: when the words wrap,
|
|
4
|
+
* they wrap beside the glyph, never under it (Teisoro DRW-28). Both the
|
|
5
|
+
* strip and the tiles layouts draw a row label with it.
|
|
6
|
+
*/
|
|
7
|
+
export function RowLabelLine({ row }) {
|
|
8
|
+
return (_jsxs("span", { className: "sw-denomination-label-line", children: [row.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-icon", children: row.icon })) : null, _jsx("span", { className: "sw-denomination-label-text", children: row.label })] }));
|
|
9
|
+
}
|
|
@@ -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,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
|
+
}
|
package/dist/css/css-button.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export declare function buttonClassNames(options: {
|
|
|
5
5
|
}): string[];
|
|
6
6
|
export declare const pressedRingGap = 2;
|
|
7
7
|
export declare const pressedRingWidth = 4;
|
|
8
|
+
export declare const buttonContentGap = "var(--sw-space-2)";
|
|
8
9
|
export declare function cssButtonClasses(): string;
|
|
9
10
|
export declare const buttonGroupJustifies: readonly ["start", "end", "between"];
|
|
10
11
|
export declare function buttonClassCatalog(): string[];
|
package/dist/css/css-button.js
CHANGED
|
@@ -52,6 +52,14 @@ function sizeRules() {
|
|
|
52
52
|
})
|
|
53
53
|
.join('\n');
|
|
54
54
|
}
|
|
55
|
+
/*
|
|
56
|
+
* The button's own children are its flex items, so a glyph and its label
|
|
57
|
+
* sit one token gap apart. An icon-only button has one item, and a consumer
|
|
58
|
+
* that wraps its glyph and label in one element (an Inline span) has one
|
|
59
|
+
* item too, so neither gets a second gap. A visually hidden name is
|
|
60
|
+
* absolutely positioned, out of the flex flow, so it adds none either.
|
|
61
|
+
*/
|
|
62
|
+
export const buttonContentGap = 'var(--sw-space-2)';
|
|
55
63
|
export function cssButtonClasses() {
|
|
56
64
|
return `.sw-button {
|
|
57
65
|
appearance: none;
|
|
@@ -65,6 +73,7 @@ export function cssButtonClasses() {
|
|
|
65
73
|
font-family: var(--sw-font-sans);
|
|
66
74
|
font-size: ${label.fontSize}px;
|
|
67
75
|
font-weight: ${label.fontWeight};
|
|
76
|
+
gap: ${buttonContentGap};
|
|
68
77
|
justify-content: center;
|
|
69
78
|
letter-spacing: ${label.letterSpacing}px;
|
|
70
79
|
line-height: ${label.lineHeight}px;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type BadgeTone } from './css-data.js';
|
|
2
|
+
/**
|
|
3
|
+
* The classes that tone one count: a marker that applies the shared rules and
|
|
4
|
+
* the tone that sets their color. A strip cell (`td`) and a tile (`li`) take
|
|
5
|
+
* the same pair; null or `neutral` leaves the cell untoned.
|
|
6
|
+
*/
|
|
7
|
+
export declare function denominationCellToneClassNames(tone: BadgeTone | null | undefined): string[];
|
|
8
|
+
export declare function cssDenominationCellToneClasses(): string;
|
|
9
|
+
export declare function denominationCellToneClassCatalog(): string[];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { badgeTones } from './css-data.js';
|
|
2
|
+
/**
|
|
3
|
+
* The tones a single count can take. `neutral` is the cell's own look, so it
|
|
4
|
+
* adds no class: a cell is toned only by a tone that says something.
|
|
5
|
+
*/
|
|
6
|
+
const cellTones = badgeTones.filter((tone) => tone !== 'neutral');
|
|
7
|
+
/**
|
|
8
|
+
* The classes that tone one count: a marker that applies the shared rules and
|
|
9
|
+
* the tone that sets their color. A strip cell (`td`) and a tile (`li`) take
|
|
10
|
+
* the same pair; null or `neutral` leaves the cell untoned.
|
|
11
|
+
*/
|
|
12
|
+
export function denominationCellToneClassNames(tone) {
|
|
13
|
+
if (!tone || tone === 'neutral')
|
|
14
|
+
return [];
|
|
15
|
+
return ['sw-denomination-cell-toned', `sw-denomination-cell-tone-${tone}`];
|
|
16
|
+
}
|
|
17
|
+
/*
|
|
18
|
+
* A toned count is set in its tone, over a row's signed coloring and a
|
|
19
|
+
* zero's quiet opacity, and a toned tile draws its border in the tone, one
|
|
20
|
+
* hairline thicker with an inset shadow so no tile moves. The tone never
|
|
21
|
+
* carries meaning alone: the consumer's words say why a count is toned.
|
|
22
|
+
* Forced colors replace the tone with the system text color, and a toned
|
|
23
|
+
* zero keeps the zero's GrayText there, so it does not read as a count.
|
|
24
|
+
*/
|
|
25
|
+
export function cssDenominationCellToneClasses() {
|
|
26
|
+
const toneRules = cellTones
|
|
27
|
+
.map((tone) => `.sw-denomination-cell-tone-${tone} { --sw-denomination-cell-tone: var(--sw-color-${tone}); }`)
|
|
28
|
+
.join('\n');
|
|
29
|
+
return `${toneRules}
|
|
30
|
+
|
|
31
|
+
.sw-denomination-grid .sw-denomination-cell.sw-denomination-cell-toned,
|
|
32
|
+
.sw-denomination-grid .sw-denomination-cell-toned .sw-denomination-cell {
|
|
33
|
+
color: var(--sw-denomination-cell-tone);
|
|
34
|
+
opacity: 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.sw-denomination-tile.sw-denomination-cell-toned {
|
|
38
|
+
border-color: var(--sw-denomination-cell-tone);
|
|
39
|
+
box-shadow: inset 0 0 0 1px var(--sw-denomination-cell-tone);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@media (forced-colors: active) {
|
|
43
|
+
.sw-denomination-grid .sw-denomination-cell-zero.sw-denomination-cell-toned,
|
|
44
|
+
.sw-denomination-grid .sw-denomination-cell-toned .sw-denomination-cell-zero { color: GrayText; }
|
|
45
|
+
}`;
|
|
46
|
+
}
|
|
47
|
+
export function denominationCellToneClassCatalog() {
|
|
48
|
+
return [
|
|
49
|
+
'sw-denomination-cell-toned',
|
|
50
|
+
...cellTones.map((tone) => `sw-denomination-cell-tone-${tone}`),
|
|
51
|
+
];
|
|
52
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { typographyVariants } from '@scalewing/tokens';
|
|
2
2
|
import { breakpointQuery } from './breakpoints.js';
|
|
3
3
|
import { badgeTones } from './css-data.js';
|
|
4
|
-
import {
|
|
4
|
+
import { cssDenominationCellToneClasses, denominationCellToneClassCatalog, } from './css-denomination-cell-tones.js';
|
|
5
|
+
import { pinnedStartShadeRules, scrollRegionRules, } from './css-scroll-region.js';
|
|
5
6
|
import { visuallyHiddenDeclarations } from './css-utilities.js';
|
|
6
7
|
import { zIndex } from './stacking.js';
|
|
7
8
|
const caption = typographyVariants.caption;
|
|
@@ -25,6 +26,11 @@ ${toneRules()}
|
|
|
25
26
|
|
|
26
27
|
${scrollRegionRules('.sw-denomination-scroll')}
|
|
27
28
|
|
|
29
|
+
${pinnedStartShadeRules('.sw-denomination-scroll', [
|
|
30
|
+
'.sw-denomination-strip .sw-denomination-label',
|
|
31
|
+
'.sw-denomination-strip thead .sw-denomination-corner:first-child',
|
|
32
|
+
])}
|
|
33
|
+
|
|
28
34
|
.sw-denomination-strip {
|
|
29
35
|
border-collapse: collapse;
|
|
30
36
|
width: 100%;
|
|
@@ -72,6 +78,17 @@ ${scrollRegionRules('.sw-denomination-scroll')}
|
|
|
72
78
|
gap: var(--sw-space-2);
|
|
73
79
|
}
|
|
74
80
|
|
|
81
|
+
/*
|
|
82
|
+
* A row label's glyph and words are one line: the line does not wrap, so its
|
|
83
|
+
* narrowest width is the glyph, the gap and the longest word, and a long
|
|
84
|
+
* label wraps its words beside the glyph instead of dropping them under it.
|
|
85
|
+
*/
|
|
86
|
+
.sw-denomination-label-line {
|
|
87
|
+
align-items: center;
|
|
88
|
+
display: flex;
|
|
89
|
+
gap: var(--sw-space-2);
|
|
90
|
+
}
|
|
91
|
+
|
|
75
92
|
/*
|
|
76
93
|
* The row labels and the header corner above them stay in view while a wide
|
|
77
94
|
* strip scrolls under them, so each column head stays over its counts.
|
|
@@ -221,12 +238,15 @@ ${scrollRegionRules('.sw-denomination-scroll')}
|
|
|
221
238
|
.sw-denomination-total-value { ${visuallyHiddenDeclarations} }
|
|
222
239
|
.sw-denomination-total-inline { display: block; }
|
|
223
240
|
.sw-denomination-label-body { flex-direction: column; align-items: flex-start; gap: var(--sw-space-1); }
|
|
241
|
+
.sw-denomination-strip .sw-denomination-label-line { gap: var(--sw-space-1); }
|
|
224
242
|
.sw-denomination-strip th,
|
|
225
243
|
.sw-denomination-strip td { padding: var(--sw-space-1); }
|
|
226
244
|
.sw-denomination-strip .sw-denomination-label { padding-inline-start: calc(var(--sw-space-1) * 2); }
|
|
227
245
|
.sw-denomination-tile-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
228
246
|
}
|
|
229
247
|
|
|
248
|
+
${cssDenominationCellToneClasses()}
|
|
249
|
+
|
|
230
250
|
@media (forced-colors: active) {
|
|
231
251
|
.sw-denomination-label { border-inline-start-color: CanvasText; color: CanvasText; }
|
|
232
252
|
.sw-denomination-strip .sw-denomination-label::before { background: CanvasText; forced-color-adjust: none; }
|
|
@@ -249,6 +269,7 @@ export function denominationGridClassCatalog() {
|
|
|
249
269
|
'sw-denomination-row-signed',
|
|
250
270
|
'sw-denomination-label',
|
|
251
271
|
'sw-denomination-label-body',
|
|
272
|
+
'sw-denomination-label-line',
|
|
252
273
|
'sw-denomination-label-text',
|
|
253
274
|
'sw-denomination-icon',
|
|
254
275
|
'sw-denomination-cell',
|
|
@@ -263,5 +284,6 @@ export function denominationGridClassCatalog() {
|
|
|
263
284
|
'sw-denomination-tile-list',
|
|
264
285
|
'sw-denomination-tile',
|
|
265
286
|
'sw-denomination-subtotal',
|
|
287
|
+
...denominationCellToneClassCatalog(),
|
|
266
288
|
];
|
|
267
289
|
}
|
|
@@ -2,4 +2,6 @@
|
|
|
2
2
|
export declare const controlSurface = "background-color: var(--sw-glass-fill);\n border: 1px solid var(--sw-color-border);\n border-radius: var(--sw-radius-sm);\n color: inherit;\n font-family: inherit;\n font-size: inherit;\n padding-inline: var(--sw-control-md-padding-inline);";
|
|
3
3
|
export declare const disabledControlSurface = "background-color: var(--sw-color-subtle);\n border-style: dashed;\n cursor: not-allowed;\n opacity: 1;\n -webkit-text-fill-color: currentColor;";
|
|
4
4
|
export declare const disabledControlForcedColors = "border-color: GrayText;\n color: GrayText;\n -webkit-text-fill-color: GrayText;";
|
|
5
|
+
/** A script-only focus target in the canvas, at zero specificity. */
|
|
6
|
+
export declare const focusTargetSelector = ":where([data-theme] [tabindex='-1']:focus-visible)";
|
|
5
7
|
export declare function cssDocumentCanvas(): string;
|
package/dist/css/css-document.js
CHANGED
|
@@ -34,6 +34,8 @@ export const disabledControlSurface = `background-color: var(--sw-color-subtle);
|
|
|
34
34
|
export const disabledControlForcedColors = `border-color: GrayText;
|
|
35
35
|
color: GrayText;
|
|
36
36
|
-webkit-text-fill-color: GrayText;`;
|
|
37
|
+
/** A script-only focus target in the canvas, at zero specificity. */
|
|
38
|
+
export const focusTargetSelector = ":where([data-theme] [tabindex='-1']:focus-visible)";
|
|
37
39
|
export function cssDocumentCanvas() {
|
|
38
40
|
return `html,
|
|
39
41
|
body {
|
|
@@ -79,6 +81,20 @@ body {
|
|
|
79
81
|
outline-offset: var(--sw-focus-ring-offset);
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
/*
|
|
85
|
+
* A programmatic focus target (tabindex="-1": a notice, a card or a heading
|
|
86
|
+
* that a script focuses after a save) takes the accent ring past the ring
|
|
87
|
+
* offset, as links and native controls do, instead of the browser's outline.
|
|
88
|
+
* It shows when the browser would show its own (:focus-visible), so a target
|
|
89
|
+
* focused after a mouse press stays quiet. :where() keeps the rule at zero
|
|
90
|
+
* specificity: it still beats the user-agent outline, and every component's
|
|
91
|
+
* own ring (a roving tabindex="-1" item's included) wins over it.
|
|
92
|
+
*/
|
|
93
|
+
${focusTargetSelector} {
|
|
94
|
+
outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
|
|
95
|
+
outline-offset: var(--sw-focus-ring-offset);
|
|
96
|
+
}
|
|
97
|
+
|
|
82
98
|
[data-theme] :is(${typedInputs}) {
|
|
83
99
|
${controlSurface}
|
|
84
100
|
box-sizing: border-box;
|
|
@@ -2,7 +2,21 @@
|
|
|
2
2
|
* A scroll region is a full-width box that scrolls its own content when the
|
|
3
3
|
* content is wider (or taller) than the box, so a wide table never widens the
|
|
4
4
|
* page. It is a keyboard stop, so its focus ring is drawn inside its edge
|
|
5
|
-
* where an ancestor's clipping cannot hide it.
|
|
6
|
-
*
|
|
5
|
+
* where an ancestor's clipping cannot hide it. While its content is scrolled
|
|
6
|
+
* out past an inline edge, `ScrollRegion` adds `sw-scroll-more-start` or
|
|
7
|
+
* `sw-scroll-more-end`, and that edge draws a shade: the cue that the content
|
|
8
|
+
* goes on, which overlay scrollbars (phones, macOS) do not give. In forced
|
|
9
|
+
* colors the shade is not drawn and the system's scrollbar is the cue. Table
|
|
10
|
+
* and the DenominationGrid strip share it.
|
|
7
11
|
*/
|
|
8
12
|
export declare function scrollRegionRules(selector: string): string;
|
|
13
|
+
export declare function scrollRegionClassCatalog(): string[];
|
|
14
|
+
/**
|
|
15
|
+
* A region whose start column is pinned (sticky, on an opaque fill) hides
|
|
16
|
+
* its own start shade under that column, so the column casts the shade
|
|
17
|
+
* instead: a gradient a spacing step wide just past its inline end, drawn by
|
|
18
|
+
* its `::after` (the pinned cell is positioned, so it holds the gradient).
|
|
19
|
+
* `pinned` lists the pinned cells' selectors inside the region. Forced colors
|
|
20
|
+
* draw no shade here either.
|
|
21
|
+
*/
|
|
22
|
+
export declare function pinnedStartShadeRules(region: string, pinned: readonly string[]): string;
|
|
@@ -1,9 +1,40 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The shade an edge draws while content is scrolled out past it: an inset
|
|
3
|
+
* shadow a spacing step wide in the text color at low strength, so it reads
|
|
4
|
+
* on every palette and in dark mode. It is the box's own shadow, painted
|
|
5
|
+
* under the content, so no cell moves. A box scrolled to its middle shades
|
|
6
|
+
* both edges. The rules name the shadows outright rather than through
|
|
7
|
+
* inherited variables, so a scroll region inside another never takes its
|
|
8
|
+
* ancestor's shade.
|
|
9
|
+
*/
|
|
10
|
+
const shadeColor = 'color-mix(in srgb, var(--sw-color-text) 28%, transparent)';
|
|
11
|
+
function edgeShade(side) {
|
|
12
|
+
const offset = side === 'left' ? 'var(--sw-space-6)' : 'calc(-1 * var(--sw-space-6))';
|
|
13
|
+
return `inset ${offset} 0 var(--sw-space-5) calc(-1 * var(--sw-space-5)) ${shadeColor}`;
|
|
14
|
+
}
|
|
15
|
+
/** The start edge is the left in a left-to-right box and the right in RTL. */
|
|
16
|
+
function edgeShadeRules(selector) {
|
|
17
|
+
const more = (edge) => `${selector}.sw-scroll-more-${edge}`;
|
|
18
|
+
const both = `${more('start')}.sw-scroll-more-end`;
|
|
19
|
+
return [
|
|
20
|
+
`${more('start')} { box-shadow: ${edgeShade('left')}; }`,
|
|
21
|
+
`${more('end')} { box-shadow: ${edgeShade('right')}; }`,
|
|
22
|
+
`${more('start')}:dir(rtl) { box-shadow: ${edgeShade('right')}; }`,
|
|
23
|
+
`${more('end')}:dir(rtl) { box-shadow: ${edgeShade('left')}; }`,
|
|
24
|
+
// Last: it ties the :dir(rtl) rules on specificity and must win them.
|
|
25
|
+
`${both} { box-shadow: ${edgeShade('left')}, ${edgeShade('right')}; }`,
|
|
26
|
+
].join('\n');
|
|
27
|
+
}
|
|
1
28
|
/**
|
|
2
29
|
* A scroll region is a full-width box that scrolls its own content when the
|
|
3
30
|
* content is wider (or taller) than the box, so a wide table never widens the
|
|
4
31
|
* page. It is a keyboard stop, so its focus ring is drawn inside its edge
|
|
5
|
-
* where an ancestor's clipping cannot hide it.
|
|
6
|
-
*
|
|
32
|
+
* where an ancestor's clipping cannot hide it. While its content is scrolled
|
|
33
|
+
* out past an inline edge, `ScrollRegion` adds `sw-scroll-more-start` or
|
|
34
|
+
* `sw-scroll-more-end`, and that edge draws a shade: the cue that the content
|
|
35
|
+
* goes on, which overlay scrollbars (phones, macOS) do not give. In forced
|
|
36
|
+
* colors the shade is not drawn and the system's scrollbar is the cue. Table
|
|
37
|
+
* and the DenominationGrid strip share it.
|
|
7
38
|
*/
|
|
8
39
|
export function scrollRegionRules(selector) {
|
|
9
40
|
return `${selector} {
|
|
@@ -14,5 +45,40 @@ export function scrollRegionRules(selector) {
|
|
|
14
45
|
${selector}:focus-visible {
|
|
15
46
|
outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
|
|
16
47
|
outline-offset: calc(-1 * var(--sw-focus-ring-width));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
${edgeShadeRules(selector)}`;
|
|
51
|
+
}
|
|
52
|
+
export function scrollRegionClassCatalog() {
|
|
53
|
+
return ['sw-scroll-more-start', 'sw-scroll-more-end'];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A region whose start column is pinned (sticky, on an opaque fill) hides
|
|
57
|
+
* its own start shade under that column, so the column casts the shade
|
|
58
|
+
* instead: a gradient a spacing step wide just past its inline end, drawn by
|
|
59
|
+
* its `::after` (the pinned cell is positioned, so it holds the gradient).
|
|
60
|
+
* `pinned` lists the pinned cells' selectors inside the region. Forced colors
|
|
61
|
+
* draw no shade here either.
|
|
62
|
+
*/
|
|
63
|
+
export function pinnedStartShadeRules(region, pinned) {
|
|
64
|
+
const after = (direction) => pinned
|
|
65
|
+
.map((cell) => `${region}.sw-scroll-more-start${direction} ${cell}::after`)
|
|
66
|
+
.join(',\n');
|
|
67
|
+
return `${after('')} {
|
|
68
|
+
background: linear-gradient(to right, ${shadeColor}, transparent);
|
|
69
|
+
content: '';
|
|
70
|
+
inset-block: 0;
|
|
71
|
+
inset-inline-start: 100%;
|
|
72
|
+
pointer-events: none;
|
|
73
|
+
position: absolute;
|
|
74
|
+
width: var(--sw-space-5);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
${after(':dir(rtl)')} {
|
|
78
|
+
background: linear-gradient(to left, ${shadeColor}, transparent);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@media (forced-colors: active) {
|
|
82
|
+
${after('').replaceAll('\n', '\n ')} { display: none; }
|
|
17
83
|
}`;
|
|
18
84
|
}
|
package/dist/css/css-toast.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare const toastTones: readonly ["neutral", "success", "warning", "danger"];
|
|
2
2
|
export type ToastTone = (typeof toastTones)[number];
|
|
3
|
+
export declare const toastMaxWidth = "calc(100% - var(--sw-space-4) - var(--sw-space-4))";
|
|
3
4
|
export declare function cssToastClasses(): string;
|
|
4
5
|
export declare function toastClassCatalog(): string[];
|
package/dist/css/css-toast.js
CHANGED
|
@@ -9,6 +9,13 @@ function toneRules() {
|
|
|
9
9
|
.map((tone) => `.sw-toast-${tone} { --sw-toast-tone: var(--sw-color-${tone}); border-color: var(--sw-toast-tone); }`)
|
|
10
10
|
.join('\n');
|
|
11
11
|
}
|
|
12
|
+
/*
|
|
13
|
+
* The toast keeps the page's side gutter (spacing step 4 on each side): a
|
|
14
|
+
* long message wraps inside it instead of running to the screen's edges.
|
|
15
|
+
* 100% of the top layer's containing block leaves out a classic scrollbar;
|
|
16
|
+
* 100vw would not.
|
|
17
|
+
*/
|
|
18
|
+
export const toastMaxWidth = 'calc(100% - var(--sw-space-4) - var(--sw-space-4))';
|
|
12
19
|
export function cssToastClasses() {
|
|
13
20
|
return `.sw-toast {
|
|
14
21
|
background: var(--sw-glass-fill);
|
|
@@ -23,7 +30,9 @@ export function cssToastClasses() {
|
|
|
23
30
|
bottom: var(--sw-space-6);
|
|
24
31
|
left: 50%;
|
|
25
32
|
margin: 0;
|
|
33
|
+
max-width: ${toastMaxWidth};
|
|
26
34
|
overflow: visible;
|
|
35
|
+
overflow-wrap: break-word;
|
|
27
36
|
pointer-events: none;
|
|
28
37
|
position: fixed;
|
|
29
38
|
translate: -50% 0;
|
package/dist/css/stylesheet.js
CHANGED
|
@@ -18,6 +18,7 @@ import { filterChipsClassCatalog } from './css-filter-chips.js';
|
|
|
18
18
|
import { gridClassCatalog } from './css-grid.js';
|
|
19
19
|
import { gridSpanClassCatalog } from './css-grid-span.js';
|
|
20
20
|
import { denominationGridClassCatalog } from './css-denomination-grid.js';
|
|
21
|
+
import { scrollRegionClassCatalog } from './css-scroll-region.js';
|
|
21
22
|
import { fieldClassCatalog } from './css-field.js';
|
|
22
23
|
import { boxBorderClassCatalog } from './css-box-border.js';
|
|
23
24
|
import { cssComponentClasses } from './css-components.js';
|
|
@@ -53,6 +54,7 @@ export function utilityClassCatalog() {
|
|
|
53
54
|
...dataClassCatalog(),
|
|
54
55
|
...segmentedClassCatalog(),
|
|
55
56
|
...tableClassCatalog(),
|
|
57
|
+
...scrollRegionClassCatalog(),
|
|
56
58
|
...chartClassCatalog(),
|
|
57
59
|
...chromeClassCatalog(),
|
|
58
60
|
...dialogClassCatalog(),
|
|
@@ -6,7 +6,9 @@ export type DenominationCellView = {
|
|
|
6
6
|
/**
|
|
7
7
|
* Classifies one count for display. A null or zero count renders the zero
|
|
8
8
|
* label; a signed row prefixes positive counts with "+" so movement reads as
|
|
9
|
-
* a delta
|
|
9
|
+
* a delta, and every negative count takes the typographic minus "−", the
|
|
10
|
+
* sign a consumer's formatted money uses, never a hyphen. Formatting stays
|
|
11
|
+
* plain digits: the consumer owns locale grouping.
|
|
10
12
|
*/
|
|
11
13
|
export declare function denominationCellView(count: number | null, signed: boolean, zeroLabel: string): DenominationCellView;
|
|
12
14
|
export declare function isDenominationCount(count: number | null): boolean;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
/** The typographic minus (U+2212), not the hyphen-minus a number prints. */
|
|
2
|
+
const minusSign = '\u2212';
|
|
1
3
|
/**
|
|
2
4
|
* Classifies one count for display. A null or zero count renders the zero
|
|
3
5
|
* label; a signed row prefixes positive counts with "+" so movement reads as
|
|
4
|
-
* a delta
|
|
6
|
+
* a delta, and every negative count takes the typographic minus "−", the
|
|
7
|
+
* sign a consumer's formatted money uses, never a hyphen. Formatting stays
|
|
8
|
+
* plain digits: the consumer owns locale grouping.
|
|
5
9
|
*/
|
|
6
10
|
export function denominationCellView(count, signed, zeroLabel) {
|
|
7
11
|
if (count === null || count === 0)
|
|
8
12
|
return { state: 'zero', text: zeroLabel };
|
|
9
13
|
if (count < 0)
|
|
10
|
-
return { state: 'negative', text:
|
|
14
|
+
return { state: 'negative', text: `${minusSign}${Math.abs(count)}` };
|
|
11
15
|
return { state: 'positive', text: signed ? `+${count}` : String(count) };
|
|
12
16
|
}
|
|
13
17
|
export function isDenominationCount(count) {
|
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export { Button, type ButtonProps, type ButtonSize, type ButtonVariant, } from '
|
|
|
17
17
|
export { Box, type BoxBorder, type BoxElement, type BoxProps, } from './components/Box.js';
|
|
18
18
|
export { ButtonGroup, type ButtonGroupJustify, type ButtonGroupProps, } from './components/ButtonGroup.js';
|
|
19
19
|
export { Card, type CardProps, type CardVariant } from './components/Card.js';
|
|
20
|
-
export { Dialog, type DialogProps, type DialogSize, } from './components/Dialog.js';
|
|
20
|
+
export { Dialog, type DialogProps, type DialogSize, type DialogTitleLevel, } from './components/Dialog.js';
|
|
21
21
|
export { StatTile, type StatTileEmphasis, type StatTileProps, type StatTileTone, statTileTones, } from './components/StatTile.js';
|
|
22
22
|
export { TabPanel, type TabItem, type TabPanelProps, Tabs, type TabsProps, tabId, tabPanelId, } from './components/Tabs.js';
|
|
23
23
|
export { Toast, toastTones } from './components/Toast.js';
|
|
@@ -44,6 +44,6 @@ export type { GridColumns, GridProps } from './components/Grid.js';
|
|
|
44
44
|
export type { GridColumnSpan } from './css/css-grid-span.js';
|
|
45
45
|
export type { ColumnSpanProps } from './column-span-classes.js';
|
|
46
46
|
export { DenominationGrid } from './components/DenominationGrid.js';
|
|
47
|
-
export type { DenominationGridColumn, DenominationGridLayout, DenominationGridProps, DenominationGridRow, DenominationGridTone, } from './components/DenominationGrid.js';
|
|
47
|
+
export type { DenominationGridColumn, DenominationGridLayout, DenominationGridProps, DenominationGridRow, DenominationGridRowRole, DenominationGridTone, } from './components/DenominationGrid.js';
|
|
48
48
|
export { FilterChips } from './components/FilterChips.js';
|
|
49
49
|
export type { FilterChipOption, FilterChipsProps, } from './components/FilterChips.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Which inline edges of a scroll box have content scrolled out past them. */
|
|
2
|
+
export type ScrollOverflow = {
|
|
3
|
+
start: boolean;
|
|
4
|
+
end: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare const noScrollOverflow: ScrollOverflow;
|
|
7
|
+
/**
|
|
8
|
+
* Reads a box's horizontal overflow from its scroll metrics. `scrollLeft` is
|
|
9
|
+
* 0 at the inline start and grows toward the end in a left-to-right box; in
|
|
10
|
+
* a right-to-left box it is 0 at the start and negative toward the end, so
|
|
11
|
+
* the distance travelled is its magnitude in both.
|
|
12
|
+
*/
|
|
13
|
+
export declare function scrollOverflow(scrollLeft: number, scrollWidth: number, clientWidth: number): ScrollOverflow;
|
|
14
|
+
export declare function sameScrollOverflow(a: ScrollOverflow, b: ScrollOverflow): boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const noScrollOverflow = { start: false, end: false };
|
|
2
|
+
/**
|
|
3
|
+
* Sub-pixel layout can leave a box a fraction of a pixel wider than its
|
|
4
|
+
* content, or a scroll position a fraction short of its end; within this
|
|
5
|
+
* many pixels an edge counts as reached.
|
|
6
|
+
*/
|
|
7
|
+
const edgeTolerance = 1;
|
|
8
|
+
/**
|
|
9
|
+
* Reads a box's horizontal overflow from its scroll metrics. `scrollLeft` is
|
|
10
|
+
* 0 at the inline start and grows toward the end in a left-to-right box; in
|
|
11
|
+
* a right-to-left box it is 0 at the start and negative toward the end, so
|
|
12
|
+
* the distance travelled is its magnitude in both.
|
|
13
|
+
*/
|
|
14
|
+
export function scrollOverflow(scrollLeft, scrollWidth, clientWidth) {
|
|
15
|
+
const hidden = scrollWidth - clientWidth;
|
|
16
|
+
if (hidden <= edgeTolerance)
|
|
17
|
+
return noScrollOverflow;
|
|
18
|
+
const travelled = Math.abs(scrollLeft);
|
|
19
|
+
return {
|
|
20
|
+
start: travelled > edgeTolerance,
|
|
21
|
+
end: travelled < hidden - edgeTolerance,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function sameScrollOverflow(a, b) {
|
|
25
|
+
return a.start === b.start && a.end === b.end;
|
|
26
|
+
}
|
package/dist/styles.css
CHANGED
|
@@ -610,6 +610,20 @@ body {
|
|
|
610
610
|
outline-offset: var(--sw-focus-ring-offset);
|
|
611
611
|
}
|
|
612
612
|
|
|
613
|
+
/*
|
|
614
|
+
* A programmatic focus target (tabindex="-1": a notice, a card or a heading
|
|
615
|
+
* that a script focuses after a save) takes the accent ring past the ring
|
|
616
|
+
* offset, as links and native controls do, instead of the browser's outline.
|
|
617
|
+
* It shows when the browser would show its own (:focus-visible), so a target
|
|
618
|
+
* focused after a mouse press stays quiet. :where() keeps the rule at zero
|
|
619
|
+
* specificity: it still beats the user-agent outline, and every component's
|
|
620
|
+
* own ring (a roving tabindex="-1" item's included) wins over it.
|
|
621
|
+
*/
|
|
622
|
+
:where([data-theme] [tabindex='-1']:focus-visible) {
|
|
623
|
+
outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
|
|
624
|
+
outline-offset: var(--sw-focus-ring-offset);
|
|
625
|
+
}
|
|
626
|
+
|
|
613
627
|
[data-theme] :is(input[type='text'], input[type='email'], input[type='number'], input[type='search'], input[type='url'], input[type='password'], input:not([type]), textarea) {
|
|
614
628
|
background-color: var(--sw-glass-fill);
|
|
615
629
|
border: 1px solid var(--sw-color-border);
|
|
@@ -858,6 +872,7 @@ body {
|
|
|
858
872
|
font-family: var(--sw-font-sans);
|
|
859
873
|
font-size: 15px;
|
|
860
874
|
font-weight: 600;
|
|
875
|
+
gap: var(--sw-space-2);
|
|
861
876
|
justify-content: center;
|
|
862
877
|
letter-spacing: -0.2px;
|
|
863
878
|
line-height: 20px;
|
|
@@ -1348,6 +1363,12 @@ body {
|
|
|
1348
1363
|
outline-offset: calc(-1 * var(--sw-focus-ring-width));
|
|
1349
1364
|
}
|
|
1350
1365
|
|
|
1366
|
+
.sw-table-wrap.sw-scroll-more-start { box-shadow: inset var(--sw-space-6) 0 var(--sw-space-5) calc(-1 * var(--sw-space-5)) color-mix(in srgb, var(--sw-color-text) 28%, transparent); }
|
|
1367
|
+
.sw-table-wrap.sw-scroll-more-end { box-shadow: inset calc(-1 * var(--sw-space-6)) 0 var(--sw-space-5) calc(-1 * var(--sw-space-5)) color-mix(in srgb, var(--sw-color-text) 28%, transparent); }
|
|
1368
|
+
.sw-table-wrap.sw-scroll-more-start:dir(rtl) { box-shadow: inset calc(-1 * var(--sw-space-6)) 0 var(--sw-space-5) calc(-1 * var(--sw-space-5)) color-mix(in srgb, var(--sw-color-text) 28%, transparent); }
|
|
1369
|
+
.sw-table-wrap.sw-scroll-more-end:dir(rtl) { box-shadow: inset var(--sw-space-6) 0 var(--sw-space-5) calc(-1 * var(--sw-space-5)) color-mix(in srgb, var(--sw-color-text) 28%, transparent); }
|
|
1370
|
+
.sw-table-wrap.sw-scroll-more-start.sw-scroll-more-end { box-shadow: inset var(--sw-space-6) 0 var(--sw-space-5) calc(-1 * var(--sw-space-5)) color-mix(in srgb, var(--sw-color-text) 28%, transparent), inset calc(-1 * var(--sw-space-6)) 0 var(--sw-space-5) calc(-1 * var(--sw-space-5)) color-mix(in srgb, var(--sw-color-text) 28%, transparent); }
|
|
1371
|
+
|
|
1351
1372
|
.sw-table {
|
|
1352
1373
|
border-collapse: collapse;
|
|
1353
1374
|
width: 100%;
|
|
@@ -2839,6 +2860,33 @@ body {
|
|
|
2839
2860
|
outline-offset: calc(-1 * var(--sw-focus-ring-width));
|
|
2840
2861
|
}
|
|
2841
2862
|
|
|
2863
|
+
.sw-denomination-scroll.sw-scroll-more-start { box-shadow: inset var(--sw-space-6) 0 var(--sw-space-5) calc(-1 * var(--sw-space-5)) color-mix(in srgb, var(--sw-color-text) 28%, transparent); }
|
|
2864
|
+
.sw-denomination-scroll.sw-scroll-more-end { box-shadow: inset calc(-1 * var(--sw-space-6)) 0 var(--sw-space-5) calc(-1 * var(--sw-space-5)) color-mix(in srgb, var(--sw-color-text) 28%, transparent); }
|
|
2865
|
+
.sw-denomination-scroll.sw-scroll-more-start:dir(rtl) { box-shadow: inset calc(-1 * var(--sw-space-6)) 0 var(--sw-space-5) calc(-1 * var(--sw-space-5)) color-mix(in srgb, var(--sw-color-text) 28%, transparent); }
|
|
2866
|
+
.sw-denomination-scroll.sw-scroll-more-end:dir(rtl) { box-shadow: inset var(--sw-space-6) 0 var(--sw-space-5) calc(-1 * var(--sw-space-5)) color-mix(in srgb, var(--sw-color-text) 28%, transparent); }
|
|
2867
|
+
.sw-denomination-scroll.sw-scroll-more-start.sw-scroll-more-end { box-shadow: inset var(--sw-space-6) 0 var(--sw-space-5) calc(-1 * var(--sw-space-5)) color-mix(in srgb, var(--sw-color-text) 28%, transparent), inset calc(-1 * var(--sw-space-6)) 0 var(--sw-space-5) calc(-1 * var(--sw-space-5)) color-mix(in srgb, var(--sw-color-text) 28%, transparent); }
|
|
2868
|
+
|
|
2869
|
+
.sw-denomination-scroll.sw-scroll-more-start .sw-denomination-strip .sw-denomination-label::after,
|
|
2870
|
+
.sw-denomination-scroll.sw-scroll-more-start .sw-denomination-strip thead .sw-denomination-corner:first-child::after {
|
|
2871
|
+
background: linear-gradient(to right, color-mix(in srgb, var(--sw-color-text) 28%, transparent), transparent);
|
|
2872
|
+
content: '';
|
|
2873
|
+
inset-block: 0;
|
|
2874
|
+
inset-inline-start: 100%;
|
|
2875
|
+
pointer-events: none;
|
|
2876
|
+
position: absolute;
|
|
2877
|
+
width: var(--sw-space-5);
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
.sw-denomination-scroll.sw-scroll-more-start:dir(rtl) .sw-denomination-strip .sw-denomination-label::after,
|
|
2881
|
+
.sw-denomination-scroll.sw-scroll-more-start:dir(rtl) .sw-denomination-strip thead .sw-denomination-corner:first-child::after {
|
|
2882
|
+
background: linear-gradient(to left, color-mix(in srgb, var(--sw-color-text) 28%, transparent), transparent);
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
@media (forced-colors: active) {
|
|
2886
|
+
.sw-denomination-scroll.sw-scroll-more-start .sw-denomination-strip .sw-denomination-label::after,
|
|
2887
|
+
.sw-denomination-scroll.sw-scroll-more-start .sw-denomination-strip thead .sw-denomination-corner:first-child::after { display: none; }
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2842
2890
|
.sw-denomination-strip {
|
|
2843
2891
|
border-collapse: collapse;
|
|
2844
2892
|
width: 100%;
|
|
@@ -2886,6 +2934,17 @@ body {
|
|
|
2886
2934
|
gap: var(--sw-space-2);
|
|
2887
2935
|
}
|
|
2888
2936
|
|
|
2937
|
+
/*
|
|
2938
|
+
* A row label's glyph and words are one line: the line does not wrap, so its
|
|
2939
|
+
* narrowest width is the glyph, the gap and the longest word, and a long
|
|
2940
|
+
* label wraps its words beside the glyph instead of dropping them under it.
|
|
2941
|
+
*/
|
|
2942
|
+
.sw-denomination-label-line {
|
|
2943
|
+
align-items: center;
|
|
2944
|
+
display: flex;
|
|
2945
|
+
gap: var(--sw-space-2);
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2889
2948
|
/*
|
|
2890
2949
|
* The row labels and the header corner above them stay in view while a wide
|
|
2891
2950
|
* strip scrolls under them, so each column head stays over its counts.
|
|
@@ -3035,12 +3094,34 @@ body {
|
|
|
3035
3094
|
.sw-denomination-total-value { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
3036
3095
|
.sw-denomination-total-inline { display: block; }
|
|
3037
3096
|
.sw-denomination-label-body { flex-direction: column; align-items: flex-start; gap: var(--sw-space-1); }
|
|
3097
|
+
.sw-denomination-strip .sw-denomination-label-line { gap: var(--sw-space-1); }
|
|
3038
3098
|
.sw-denomination-strip th,
|
|
3039
3099
|
.sw-denomination-strip td { padding: var(--sw-space-1); }
|
|
3040
3100
|
.sw-denomination-strip .sw-denomination-label { padding-inline-start: calc(var(--sw-space-1) * 2); }
|
|
3041
3101
|
.sw-denomination-tile-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
3042
3102
|
}
|
|
3043
3103
|
|
|
3104
|
+
.sw-denomination-cell-tone-accent { --sw-denomination-cell-tone: var(--sw-color-accent); }
|
|
3105
|
+
.sw-denomination-cell-tone-success { --sw-denomination-cell-tone: var(--sw-color-success); }
|
|
3106
|
+
.sw-denomination-cell-tone-danger { --sw-denomination-cell-tone: var(--sw-color-danger); }
|
|
3107
|
+
.sw-denomination-cell-tone-warning { --sw-denomination-cell-tone: var(--sw-color-warning); }
|
|
3108
|
+
|
|
3109
|
+
.sw-denomination-grid .sw-denomination-cell.sw-denomination-cell-toned,
|
|
3110
|
+
.sw-denomination-grid .sw-denomination-cell-toned .sw-denomination-cell {
|
|
3111
|
+
color: var(--sw-denomination-cell-tone);
|
|
3112
|
+
opacity: 1;
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
.sw-denomination-tile.sw-denomination-cell-toned {
|
|
3116
|
+
border-color: var(--sw-denomination-cell-tone);
|
|
3117
|
+
box-shadow: inset 0 0 0 1px var(--sw-denomination-cell-tone);
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
@media (forced-colors: active) {
|
|
3121
|
+
.sw-denomination-grid .sw-denomination-cell-zero.sw-denomination-cell-toned,
|
|
3122
|
+
.sw-denomination-grid .sw-denomination-cell-toned .sw-denomination-cell-zero { color: GrayText; }
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3044
3125
|
@media (forced-colors: active) {
|
|
3045
3126
|
.sw-denomination-label { border-inline-start-color: CanvasText; color: CanvasText; }
|
|
3046
3127
|
.sw-denomination-strip .sw-denomination-label::before { background: CanvasText; forced-color-adjust: none; }
|
|
@@ -3221,7 +3302,9 @@ body {
|
|
|
3221
3302
|
bottom: var(--sw-space-6);
|
|
3222
3303
|
left: 50%;
|
|
3223
3304
|
margin: 0;
|
|
3305
|
+
max-width: calc(100% - var(--sw-space-4) - var(--sw-space-4));
|
|
3224
3306
|
overflow: visible;
|
|
3307
|
+
overflow-wrap: break-word;
|
|
3225
3308
|
pointer-events: none;
|
|
3226
3309
|
position: fixed;
|
|
3227
3310
|
translate: -50% 0;
|