@scalewing/react 1.26.0 → 1.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +87 -1
  3. package/dist/components/ActionBar.d.ts +14 -0
  4. package/dist/components/ActionBar.js +5 -2
  5. package/dist/components/DenominationGrid.d.ts +1 -1
  6. package/dist/components/DenominationGrid.js +3 -0
  7. package/dist/components/Stepper.d.ts +44 -0
  8. package/dist/components/Stepper.js +77 -0
  9. package/dist/components/Steps.d.ts +18 -0
  10. package/dist/components/Steps.js +34 -0
  11. package/dist/components/action-bar/ActionBarSummary.d.ts +22 -0
  12. package/dist/components/action-bar/ActionBarSummary.js +23 -0
  13. package/dist/components/denomination-grid/ColumnHeadText.d.ts +7 -0
  14. package/dist/components/denomination-grid/ColumnHeadText.js +13 -0
  15. package/dist/components/denomination-grid/DenominationStrip.js +3 -7
  16. package/dist/components/denomination-grid/DenominationTiles.js +2 -1
  17. package/dist/components/denomination-grid/EntryCell.d.ts +15 -0
  18. package/dist/components/denomination-grid/EntryCell.js +16 -0
  19. package/dist/components/denomination-grid/RowLabelLine.d.ts +3 -2
  20. package/dist/components/denomination-grid/RowLabelLine.js +5 -3
  21. package/dist/components/denomination-grid/StripRow.d.ts +13 -0
  22. package/dist/components/denomination-grid/StripRow.js +19 -0
  23. package/dist/components/denomination-grid/types.d.ts +44 -1
  24. package/dist/components/denomination-grid/validate-entry.d.ts +15 -0
  25. package/dist/components/denomination-grid/validate-entry.js +78 -0
  26. package/dist/components/steps/Step.d.ts +24 -0
  27. package/dist/components/steps/Step.js +62 -0
  28. package/dist/components/steps/step-status.d.ts +26 -0
  29. package/dist/components/steps/step-status.js +37 -0
  30. package/dist/components/steps/steps-context.d.ts +16 -0
  31. package/dist/components/steps/steps-context.js +3 -0
  32. package/dist/css/css-action-bar-summary.d.ts +2 -0
  33. package/dist/css/css-action-bar-summary.js +48 -0
  34. package/dist/css/css-action-bar.js +4 -0
  35. package/dist/css/css-components.js +7 -0
  36. package/dist/css/css-denomination-entry.d.ts +2 -0
  37. package/dist/css/css-denomination-entry.js +78 -0
  38. package/dist/css/css-denomination-grid.js +4 -0
  39. package/dist/css/css-stepper.d.ts +10 -0
  40. package/dist/css/css-stepper.js +126 -0
  41. package/dist/css/css-steps.d.ts +9 -0
  42. package/dist/css/css-steps.js +203 -0
  43. package/dist/css/stylesheet.js +4 -0
  44. package/dist/index.d.ts +4 -2
  45. package/dist/index.js +3 -1
  46. package/dist/stepper-count.d.ts +37 -0
  47. package/dist/stepper-count.js +50 -0
  48. package/dist/styles.css +324 -0
  49. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.27.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 495d542: `ActionBar` `summary` (with `ActionBarSummary` and `ActionBarSummaryTone`): a labelled figure first in the bar's polite status region (`docs/requests/teisoro-action-bar-summary.md`, Teisoro F-011 release R1).
8
+
9
+ - `summary: { label, value, detail?, tone?: 'default' | 'danger' | 'success' }`: the label and detail in muted label-size words, the value in the `heading` variant at tabular figures, on one baseline; `tone` colors the value only. Any `status` follows it in the same live region.
10
+ - A blank `label`, `value` or `detail`, or an unknown `tone`, throws a `RangeError`.
11
+ - New generated classes `sw-action-bar-summary`, `sw-action-bar-summary-label`, `sw-action-bar-summary-value`, `sw-action-bar-summary-detail`, `sw-action-bar-summary-danger` and `sw-action-bar-summary-success`. No new tokens or dependencies.
12
+
13
+ - db1d77b: `DenominationGrid` count entry (`docs/requests/teisoro-denomination-grid-entry.md`, Teisoro F-011 release R1), with the new types `DenominationGridEntry` and `DenominationGridRowEmphasis`:
14
+
15
+ - `DenominationGridRow.entry` (`onChange(columnKey, value)`, `decrementLabel`, `incrementLabel`, `max?`, `errors?`, `placeholder?`) makes a strip row's `cells` editable, one full-width `Stepper` per column named by the row label and the column's label and caption. Entry counts are zero or more and unsigned, and every count's name is its own. A strip with an entry row centres its heads and read-only counts; a `cellTones` tone draws an entry count's border (`accent` is the `Stepper`'s changed look).
16
+ - `DenominationGridRow.caption` (a muted line under the label), `emphasis` (`'default' | 'quiet'`, the subtle fill) and `disabled` (faded, entry disabled, `aria-disabled`), both strip only.
17
+ - `DenominationGridColumn.caption`: a muted note after the column label, such as "× 100".
18
+ - Entry rows need the strip layout; bad caps, messages, button names, captions or emphasis throw a `RangeError`.
19
+ - `Stepper` is now `position: relative`, so its hidden label and empty error region never widen a scroll region's page.
20
+ - New generated classes `sw-denomination-entry`, `sw-denomination-strip-entry`, `sw-denomination-label-words`, `sw-denomination-label-caption`, `sw-denomination-head-caption`, `sw-denomination-row-quiet` and `sw-denomination-row-disabled`. No new tokens or dependencies.
21
+
22
+ - 92357d4: `Steps` and `Step` (with `StepsProps`, `StepProps`, `StepStatus`, `StepHeadingLevel` and `StepsLabels`): a one-page vertical stepper (`docs/requests/teisoro-steps.md`, Teisoro F-011 release R1).
23
+
24
+ - `Steps`: an `ol` of steps numbered by their place, with `headingLevel` (2 by default) and `labels` (the status words assistive technology hears; English by default).
25
+ - `Step`: `status` (`'upcoming' | 'current' | 'done' | 'error' | 'skipped'`), `title`, `summary`, `onEdit` and `editLabel`, `skippedReason`, `headingLevel`, `children`. The current and error steps are open with their fields; a done step folds to a check, its title, a truncated summary and a ghost Edit named by its title; steps ahead and skipped ones are greyed. A line joins each marker to the next.
26
+ - The current step has `aria-current="step"`; focus moves to a step's heading when it becomes current from a folded state (the first such step in an update).
27
+ - New generated classes `sw-steps`, `sw-steps-item`, `sw-steps-item-<status>`, `sw-steps-header`, `sw-steps-marker`, `sw-steps-title`, `sw-steps-reason`, `sw-steps-summary`, `sw-steps-edit` and `sw-steps-body`, from tokens (the `xs` control height marker, the `title` and body type). No new tokens or dependencies.
28
+
29
+ - 35934e1: `Stepper` (with `StepperProps` and `StepperWidth`): a whole-number count changed with − and + or typed, the web counterpart of the react-native `Stepper` (`docs/requests/teisoro-stepper.md`, Teisoro F-011 release R1).
30
+
31
+ - `label`, `labelVisuallyHidden`, `value` (`number | null`), `onChange`, `min` (default 0), `max` (open when left out), `step` (default 1), `decrementLabel`, `incrementLabel`, `error`, `changed`, `disabled`, `readOnly`, `valueText`, `placeholder`, `name`, `width` (`'fixed' | 'full'`), and a ref to the input.
32
+ - The count is a native text input with `role="spinbutton"` and the one tab stop (ArrowUp and ArrowDown step it, Home and End go to `min` and a finite `max`); the − and + buttons are named pointer targets outside the tab order that keep focus where it was, all three in a `group` named by the label. − stops at `min`, + at `max`; an empty box counts as `min`.
33
+ - Typed text other than digits (and a leading minus when `min` is below zero) is refused; a typed count is reported as typed, even outside the bounds, for the consumer's `error`.
34
+ - New generated classes `sw-stepper`, `sw-stepper-full`, `sw-stepper-track`, `sw-stepper-button`, `sw-stepper-input`, `sw-stepper-invalid`, `sw-stepper-changed` and `sw-stepper-readonly`, sized from the `md` control height and spacing step 6. No new tokens or dependencies.
35
+
3
36
  ## 1.26.0
4
37
 
5
38
  ### Minor Changes
package/README.md CHANGED
@@ -29,6 +29,9 @@ import {
29
29
  Grid,
30
30
  DateField,
31
31
  Field,
32
+ Stepper,
33
+ Steps,
34
+ Step,
32
35
  Select,
33
36
  Switch,
34
37
  Split,
@@ -52,7 +55,25 @@ Import the CSS once at the application entry. Do not copy it into your source tr
52
55
 
53
56
  `Box` `border` draws a hairline in the `border` color token: `border` (or `border={true}`) is solid, and `border="dashed"` is dashed (the generated `sw-border` and `sw-border-dashed` classes; a `style` you pass still wins), for a space to fill in by hand such as a blank on a printed form. It does not make the box look or act like an input. Every `Box`-based component takes it.
54
57
 
55
- `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. They also take `lg` (64rem), for content that fits only from a laptop up, such as a row of labelled destinations that a tablet shows as glyphs (name each glyph-only control, for example with `Tooltip relationship="label"`); the layout props keep `md`. `breakpointQuery('below', 'lg')` returns the query a hide class uses (`not all and (min-width: 64rem)`), for an app whose script must follow the same width. An unknown breakpoint throws a `RangeError`. 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.
58
+ `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. They also take `lg` (64rem), for content that fits only from a laptop up, such as a row of labelled destinations that a tablet shows as glyphs (name each glyph-only control, for example with `Tooltip relationship="label"`); the layout props keep `md`. `breakpointQuery('below', 'lg')` returns the query a hide class uses (`not all and (min-width: 64rem)`), for an app whose script must follow the same width. An unknown breakpoint throws a `RangeError`. 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. `Steps` is a one-page vertical stepper: an `ol` of `Step` panels in order, numbered by their place, a line joining each marker to the next (in the success color below a done step). A `Step` takes `status` (`'upcoming' | 'current' | 'done' | 'error' | 'skipped'`) and `title` (a heading at the list's `headingLevel`, 2 by default, or the step's own). The current step is open with its `children` and has `aria-current="step"`; an error step stays open with a "!" marker. A done step folds to one line: a check, the title, its `summary` (truncated) and, with `onEdit` and `editLabel`, a ghost small `Button` at the end named by its label and the title ("Edit The site"). A step ahead is greyed with its number in a ring; a skipped step is greyed with its `skippedReason` after the title (pass your own separator, such as "· not needed"). A folded step's heading ends in its status word for assistive technology (visually hidden; English by default, `labels` on `Steps` takes your words). When a step becomes current from a folded state (upcoming, done or skipped), focus moves to its heading, a programmatic focus target; if several do in one update, the first in the list takes it. A first render, a move between current and error, and a step that turns to error leave focus alone: after a refused submit, move focus to the first invalid field yourself, as with `Field`. An error step has no `aria-current`; its heading's status word says it needs attention. Scalewing holds no order rules: the consumer sets each step's status. Pass the `Step`s as direct children (a Fragment around several would share one number). A `Step` outside `Steps`, a Fragment child, a blank title or reason, an unknown status or heading level, `onEdit` without `editLabel`, or a blank status word throws a `RangeError`.
59
+
60
+ ```tsx
61
+ <Steps aria-label="Heron survey">
62
+ <Step
63
+ status="done"
64
+ title="The site"
65
+ summary="North marsh"
66
+ editLabel="Edit"
67
+ onEdit={() => setCurrent(0)}
68
+ />
69
+ <Step status="current" title="The count">
70
+ {fields}
71
+ </Step>
72
+ <Step status="upcoming" title="Review" />
73
+ </Steps>
74
+ ```
75
+
76
+ `Select` is a labeled listbox menu when the open list must match the canvas.
56
77
 
57
78
  `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.
58
79
 
@@ -84,6 +105,21 @@ A disabled native text control (`<input>`, `<select>` or `<textarea>` on the can
84
105
  </Field>
85
106
  ```
86
107
 
108
+ `Stepper` is a whole-number count changed with − and + or typed. The count is a native text input with `role="spinbutton"` and `inputmode="numeric"`, named by `label` (visible above it, or `labelVisuallyHidden` where table headers say it) and the one tab stop: ArrowUp and ArrowDown step it by `step` (default 1), Home goes to `min` and End to a finite `max` (a range below zero takes `inputmode="text"`, since a phone's numeric pad has no minus key). The − and + buttons are pointer targets outside the tab order, named by `decrementLabel` and `incrementLabel` (your copy), and a press on one leaves focus where it was; the count and both buttons sit in a `group` named by the label. − is disabled at `min` (default 0) and + at `max` (no upper bound when left out). `value` is a count or `null` for an empty box, which the buttons treat as `min` and `placeholder` can fill. Typed text is refused unless it is digits (with a leading minus when `min` is below zero), so `onChange` only receives a whole number or `null`; a typed count is reported as typed even outside the bounds, so say why in `error` (a polite message, `aria-invalid` and the danger border, as `Field`'s), and the buttons then move it back inside them. `changed` takes `Field`'s changed look; `readOnly` keeps the count focusable and hides the buttons in their own room; `disabled` stops all three. The count is a few digits wide and keeps that width (`width="fixed"`), or fills its container (`width="full"`, for a table cell). Bounds that are not whole numbers, `min` above `max`, or a `step` that is not above zero throw a `RangeError`.
109
+
110
+ ```tsx
111
+ <Stepper
112
+ label="Herons"
113
+ value={count}
114
+ onChange={setCount}
115
+ max={40}
116
+ decrementLabel="One less"
117
+ incrementLabel="One more"
118
+ placeholder="0"
119
+ error={count !== null && count > nests ? `Only ${nests} nests.` : undefined}
120
+ />
121
+ ```
122
+
87
123
  `Select` is a labeled listbox menu: supply `label`, `options` (`value` and `label`), a controlled `value` and `onChange`; `size="xs"` and `labelVisuallyHidden` are for toolbar chrome, and `action` is a last listbox command that never becomes the value. The closed trigger matches `Field`: its text is centred in the control, it ends in the `Accordion` chevron, and it is as wide as its longest option (as a native select is), so it keeps its width when the value changes; past the available width the label ellipsizes. The option labels that size it are drawn by CSS from `data-label`, so the trigger's text content and accessible value are only the current label. Its label is drawn as `Field`'s (the same label words and mark inside a `<label>` that keeps the canvas type), so a `Select` beside a `Field` lines up at the label and the control.
88
124
 
89
125
  `Select` `placeholder` shows in the closed trigger, in the muted color, while `value` matches no option (such as `''`); it is not an option, is never selected in the list and never becomes the value, and it counts toward the trigger's width. `required` marks the label with `Field`'s `aria-hidden` asterisk and sets `aria-required` on the trigger. `error` renders a message under the control (`sw-field-error`), links it by `aria-describedby`, sets `aria-invalid` and the danger border; like `Field`'s it is announced politely from a live region that is always there, never as an alert, and an empty string is no error. A blank `placeholder` throws a `RangeError`.
@@ -134,6 +170,26 @@ A disabled native text control (`<input>`, `<select>` or `<textarea>` on the can
134
170
 
135
171
  `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`.
136
172
 
173
+ `ActionBar` `summary` (`{ label, value, detail?, tone? }`, type `ActionBarSummary`) puts a labelled figure first in the same polite status region, before any `status`: the label and the detail in muted label-size words, the value between them in the `heading` variant (24 below `md`) at tabular figures, all on one baseline and wrapping as one phrase, such as "Counted **$33,790.00** short $4,000.00". `tone` (`'default' | 'danger' | 'success'`, such as danger while short or over and success once it matches) colors the value only, so say why in the detail. Format the value yourself; each new figure is announced politely. For a guided form, the actions are a secondary Cancel and one primary that stays `disabled` until the form is valid. A blank `label`, `value` or `detail`, or an unknown `tone`, throws a `RangeError`.
174
+
175
+ ```tsx
176
+ <ActionBar
177
+ summary={{
178
+ label: 'Counted',
179
+ value: '14 nests',
180
+ detail: 'short 2',
181
+ tone: 'danger',
182
+ }}
183
+ >
184
+ <Button variant="secondary" onPress={cancel}>
185
+ Cancel
186
+ </Button>
187
+ <Button disabled={!matches} onPress={save}>
188
+ Save count
189
+ </Button>
190
+ </ActionBar>
191
+ ```
192
+
137
193
  ```tsx
138
194
  <Stack gap={4}>
139
195
  {longForm}
@@ -369,6 +425,36 @@ Values outside the catalog throw a `RangeError`. There is no per-breakpoint span
369
425
 
370
426
  `DenominationGrid` shows integer counts per unit across a fixed set of columns. The `strip` layout is a captioned table: each row has a toned label with an icon slot, muted zero cells, optional signed deltas toned by sign, and an optional consumer-formatted `total` that shows under the label on a phone. Pass `totalLabel` (for example "Total") to name the total column: it renders a `th scope="col"` shown over the totals from `md` up and visually hidden below it, and without it the corner stays an empty cell as before (either way the column takes no width on a phone). `labelWidth` (`xs` to `xl`, the `Table` column sizes) lines a strip's columns up with every other strip of the same columns in a container of the same width, such as the cards of a feed: the row labels take that width and the count columns share the rest of the region equally, so no content moves a column. On a phone the total cell stays in the table, visually hidden, so a screen reader still reads each total in its row; the copy under the label is `aria-hidden`. A strip wider than its container scrolls sideways inside its own keyboard-focusable region, named after `label`, with the row labels pinned at the start and the same edge shade as `Table` while counts are scrolled out of view (the start shade is cast from the pinned labels' edge, since they cover the region's own); the page never scrolls sideways. The `tiles` layout stacks the column label, the count, and an optional `subtotal` string per column. Each tiles row shows its label line (icon, label and `total`) when the grid has several rows, or when a lone row has an icon or a `total`; a lone plain row is named by the grid's `label` alone. Every row is a region named by its `label`, except a lone plain row whose `label` is the grid's own: it has no name of its own, so assistive technology hears the grid's group once rather than a region repeating it. `rowRole="group"` (default `"region"`) keeps every named row's name but makes it a `group` instead of a landmark region, for a page with many grids (one per order card, say) whose rows would otherwise fill the landmark list; the strip ignores it, since its rows are table rows, and an unknown `rowRole` throws a `RangeError` in either layout. A row's icon and its label words stay on one line in both layouts (`sw-denomination-label-line`): a long label wraps its words beside the icon, never under it, and on a phone the strip's total goes on its own line under them. A row's `cellTones` (one Badge tone or `null` per column) tones a single count in place: the count is set in the tone over its signed or zero look, and in the tiles layout its tile's border takes the tone too, so a short bill turns red without leaving its row; `neutral` or `null` leaves a count as it is, a row's `tone` still colors only its label, and a `cellTones` that is not one known tone or `null` per column throws. The tone never says why on its own; put that in words beside the grid. Every negative count, signed or not and in either layout, is written with the typographic minus ("−1"), never a hyphen. The primitive does no arithmetic and no currency formatting; tones reuse the Badge vocabulary. Invalid columns, rows, or cells throw.
371
427
 
428
+ A strip row with `entry` makes its counts editable: each column holds a `Stepper` (`width="full"`, its label hidden and named by the row label and the column's label and caption as its header reads, such as "Received $20" or "Counted $1 × 100") whose count is the row's `cells` entry (`null` is an empty box). `entry.onChange(columnKey, value)` reports a change; `entry.decrementLabel` and `entry.incrementLabel` name the buttons; `entry.max` caps each column (`null` for none), `entry.errors` puts a message under a column's count (`null` for none), and `entry.placeholder` fills an empty box. The minimum is 0. The grid still does no arithmetic, so recompute the row's `total` in `onChange`. A strip with an entry row centres its heads and read-only counts over the steppers, and each count keeps spacing step 8 of room, so a narrow strip scrolls in its region instead of crushing it. A `cellTones` tone on an entry count draws its border in the tone (danger for a difference); an `accent` tone, for a suggestion or a change, is the `Stepper`'s `changed` look (border and tint); a message in `entry.errors` wins. A row `caption` is a muted line under its label ("from the customer"), and a column `caption` a muted note after its label ("× 100"). Captions show in both layouts. In the strip, `emphasis="quiet"` sets a row on the subtle fill, for a row to compare against, and `disabled` fades a row that does not apply, disables its entry and marks it `aria-disabled`. Entry rows, quiet rows and disabled rows need the `strip` layout. An entry row's counts are zero or more and it is not `signed`; its label is its own among the entry rows, as each column's label and caption are, so every count has its own name. Breaking any of these, a cap or message list that is not one per column, a cap that is not a whole count of zero or more, blank button names or captions, an unknown `emphasis`, or a `disabled` that is not a boolean throws a `RangeError`.
429
+
430
+ ```tsx
431
+ <DenominationGrid
432
+ label="Tag packs"
433
+ columns={[
434
+ { key: 's', label: 'S', caption: '× 10' },
435
+ { key: 'm', label: 'M' },
436
+ ]}
437
+ totalLabel="Total"
438
+ rows={[
439
+ {
440
+ id: 'fitted',
441
+ label: 'Fitted',
442
+ caption: 'this morning',
443
+ cells: fitted,
444
+ total: `${tags(fitted)} tags`,
445
+ entry: {
446
+ onChange: (key, value) => setCount(key, value),
447
+ decrementLabel: 'One less',
448
+ incrementLabel: 'One more',
449
+ max: [6, 4],
450
+ placeholder: '0',
451
+ },
452
+ },
453
+ { id: 'stock', label: 'In stock', emphasis: 'quiet', cells: [6, 4] },
454
+ ]}
455
+ />
456
+ ```
457
+
372
458
  `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.
373
459
 
374
460
  `DateField` is a controlled date-only field that draws its own calendar. Supply `value`, `min`, and `max` as valid `YYYY-MM-DD` dates; use an empty `value` for a blank field. Its callback returns a date-only string or an empty string, without timezone conversion. Invalid serialized dates and inverted bounds throw; a value outside the bounds remains visible with invalid styling. `required` marks the label with the same `aria-hidden` asterisk as `Field` and sets the entry's native `required`. Its label is drawn as `Field`'s (the same label words and mark inside a `<label>` that keeps the canvas type), so its label row and its gap to the entry match a `Field` beside it. For a blank required field, supply `error` when form validation runs to show the message and invalid styling.
@@ -1,5 +1,7 @@
1
1
  import { type HTMLAttributes, type ReactNode } from 'react';
2
2
  import { type Breakpoint } from '../css/breakpoints.js';
3
+ import { type ActionBarSummary } from './action-bar/ActionBarSummary.js';
4
+ export type { ActionBarSummary, ActionBarSummaryTone, } from './action-bar/ActionBarSummary.js';
3
5
  export type ActionBarProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
4
6
  /** The actions, usually two or three `Button`s. */
5
7
  children: ReactNode;
@@ -13,6 +15,12 @@ export type ActionBarProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> &
13
15
  * and elements in one `span` to keep it wrapping as a sentence.
14
16
  */
15
17
  status?: ReactNode;
18
+ /**
19
+ * A labelled figure before the status, in the same polite live region:
20
+ * the label, the value large at tabular figures, and a detail, such as
21
+ * "Counted $33,790.00 short $4,000.00". `tone` colors the value.
22
+ */
23
+ summary?: ActionBarSummary;
16
24
  /** Sticky only below this breakpoint; from it up the bar sits in page flow. */
17
25
  stickyBelow?: Breakpoint;
18
26
  };
@@ -35,6 +43,12 @@ export declare const ActionBar: import("react").ForwardRefExoticComponent<Omit<H
35
43
  * and elements in one `span` to keep it wrapping as a sentence.
36
44
  */
37
45
  status?: ReactNode;
46
+ /**
47
+ * A labelled figure before the status, in the same polite live region:
48
+ * the label, the value large at tabular figures, and a detail, such as
49
+ * "Counted $33,790.00 short $4,000.00". `tone` colors the value.
50
+ */
51
+ summary?: ActionBarSummary;
38
52
  /** Sticky only below this breakpoint; from it up the bar sits in page flow. */
39
53
  stickyBelow?: Breakpoint;
40
54
  } & import("react").RefAttributes<HTMLDivElement>>;
@@ -2,12 +2,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { forwardRef } from 'react';
3
3
  import { cx } from '../class-names.js';
4
4
  import { actionBarStickyClass } from '../css/css-action-bar.js';
5
+ import { ActionBarSummaryLine, assertActionBarSummary, } from './action-bar/ActionBarSummary.js';
5
6
  /**
6
7
  * The actions for a long page on a glass bar that sticks to the bottom of
7
8
  * the viewport while the content it follows scrolls by. Place it last in
8
9
  * that content: it stays stuck while its parent is on screen and then rests
9
10
  * in its own place at the end.
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("div", { className: "sw-action-bar-status", role: "status", children: status }), _jsx("div", { className: "sw-action-bar-actions", children: children })] }));
12
+ export const ActionBar = forwardRef(function ActionBar({ children, className, status, stickyBelow, summary, ...rest }, ref) {
13
+ if (summary)
14
+ assertActionBarSummary(summary);
15
+ return (_jsxs("div", { ref: ref, className: cx('sw-action-bar', actionBarStickyClass(stickyBelow), className), ...rest, children: [_jsxs("div", { className: "sw-action-bar-status", role: "status", children: [summary ? _jsx(ActionBarSummaryLine, { summary: summary }) : null, status] }), _jsx("div", { className: "sw-action-bar-actions", children: children })] }));
13
16
  });
@@ -1,3 +1,3 @@
1
1
  import { type DenominationGridProps } from './denomination-grid/types.js';
2
- export type { DenominationGridColumn, DenominationGridLayout, DenominationGridProps, DenominationGridRow, DenominationGridRowRole, DenominationGridTone, DenominationLabelWidth, } from './denomination-grid/types.js';
2
+ export type { DenominationGridColumn, DenominationGridEntry, DenominationGridLayout, DenominationGridProps, DenominationGridRow, DenominationGridRowEmphasis, DenominationGridRowRole, DenominationGridTone, DenominationLabelWidth, } from './denomination-grid/types.js';
3
3
  export declare function DenominationGrid({ label, columns, rows, layout, subtotal, zeroLabel, totalLabel, rowRole, labelWidth, }: DenominationGridProps): import("react").JSX.Element;
@@ -3,8 +3,11 @@ import { denominationLabelWidths } from '../css/css-denomination-aligned.js';
3
3
  import { DenominationStrip } from './denomination-grid/DenominationStrip.js';
4
4
  import { DenominationTiles } from './denomination-grid/DenominationTiles.js';
5
5
  import { assertDenominationGrid } from './denomination-grid/validate.js';
6
+ import { assertDenominationEntries, assertDenominationExtras, } from './denomination-grid/validate-entry.js';
6
7
  export function DenominationGrid({ label, columns, rows, layout = 'strip', subtotal, zeroLabel = '—', totalLabel, rowRole = 'region', labelWidth, }) {
7
8
  assertDenominationGrid(label, columns, rows);
9
+ assertDenominationExtras(layout, columns, rows);
10
+ assertDenominationEntries(layout, columns, rows);
8
11
  if (!zeroLabel)
9
12
  throw new RangeError('zeroLabel must not be empty');
10
13
  if (totalLabel !== undefined && !totalLabel.trim())
@@ -0,0 +1,44 @@
1
+ /** `fixed` sizes the count for a few digits; `full` fills its container, such as a table cell. */
2
+ export type StepperWidth = 'fixed' | 'full';
3
+ export type StepperProps = {
4
+ /** Names the count. Keep it visible unless headers around it say it (`labelVisuallyHidden`). */
5
+ label: string;
6
+ labelVisuallyHidden?: boolean;
7
+ /** The count, or `null` for an empty box (none). */
8
+ value: number | null;
9
+ /** A whole count, or `null` when the box is emptied. Typed counts are reported as typed, even outside the bounds. */
10
+ onChange: (value: number | null) => void;
11
+ /** The lowest count − reaches; defaults to 0. */
12
+ min?: number;
13
+ /** The highest count + reaches; no upper bound when left out. */
14
+ max?: number;
15
+ /** How much one press, or one arrow key, moves the count; defaults to 1. */
16
+ step?: number;
17
+ /** The − button's name, such as "One less". */
18
+ decrementLabel: string;
19
+ /** The + button's name, such as "One more". */
20
+ incrementLabel: string;
21
+ /**
22
+ * A message under the control, as `Field`'s: a polite live region linked by
23
+ * `aria-describedby`, with `aria-invalid` and the danger border. An empty
24
+ * string is no error.
25
+ */
26
+ error?: string;
27
+ /** `Field`'s changed look (the accent border and tint) for a count someone changed, or a suggestion. `error` wins. */
28
+ changed?: boolean;
29
+ disabled?: boolean;
30
+ /** Readable and focusable, never changed: the buttons are hidden and keep their room. */
31
+ readOnly?: boolean;
32
+ /** Says the count in words for assistive technology (`aria-valuetext`), such as "12 straps". */
33
+ valueText?: string;
34
+ /** Shown in an empty box, such as "0". */
35
+ placeholder?: string;
36
+ name?: string;
37
+ width?: StepperWidth;
38
+ };
39
+ /**
40
+ * A whole-number count between − and + buttons that can also be typed: one
41
+ * tab stop (the count, a spin button that ArrowUp and ArrowDown step), with
42
+ * the buttons as pointer targets beside it. − stops at `min`, + at `max`.
43
+ */
44
+ export declare const Stepper: import("react").ForwardRefExoticComponent<StepperProps & import("react").RefAttributes<HTMLInputElement>>;
@@ -0,0 +1,77 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef, useId, useState, } from 'react';
4
+ import { cx } from '../class-names.js';
5
+ import { assertStepperBounds, readStepperEntry, stepperMoves, stepperText, } from '../stepper-count.js';
6
+ import { FieldErrorRegion } from './FieldErrorRegion.js';
7
+ import { FieldLabelText } from './FieldLabelText.js';
8
+ /** Keeps focus where it was (the count, or nowhere) when a button is pressed. */
9
+ function keepFocus(event) {
10
+ event.preventDefault();
11
+ }
12
+ /**
13
+ * A whole-number count between − and + buttons that can also be typed: one
14
+ * tab stop (the count, a spin button that ArrowUp and ArrowDown step), with
15
+ * the buttons as pointer targets beside it. − stops at `min`, + at `max`.
16
+ */
17
+ export const Stepper = forwardRef(function Stepper({ label, labelVisuallyHidden = false, value, onChange, min = 0, max = Number.POSITIVE_INFINITY, step = 1, decrementLabel, incrementLabel, error, changed = false, disabled = false, readOnly = false, valueText, placeholder, name, width = 'fixed', }, ref) {
18
+ const bounds = { min, max, step };
19
+ assertStepperBounds(bounds);
20
+ if (value !== null && !Number.isSafeInteger(value))
21
+ throw new RangeError('Stepper value must be a whole number or null');
22
+ const inputId = useId();
23
+ const labelId = useId();
24
+ const errorId = useId();
25
+ const [draft, setDraft] = useState(null);
26
+ const allowsNegative = min < 0;
27
+ const moves = stepperMoves(value, bounds, disabled || readOnly);
28
+ // The typed text stays while it means the count the consumer holds, so
29
+ // "007" or a lone minus survives a render; otherwise the count's text.
30
+ const draftEntry = draft === null ? null : readStepperEntry(draft, allowsNegative);
31
+ const draftShowsValue = draftEntry !== null &&
32
+ (draftEntry.kind === 'empty'
33
+ ? value === null
34
+ : draftEntry.kind === 'count' && draftEntry.value === value);
35
+ const text = draft !== null && draftShowsValue ? draft : stepperText(value);
36
+ function move(next) {
37
+ if (next === null)
38
+ return;
39
+ setDraft(null);
40
+ onChange(next);
41
+ }
42
+ function onType(event) {
43
+ const next = event.target.value;
44
+ const entry = readStepperEntry(next, allowsNegative);
45
+ // A refused keystroke changes nothing: the controlled box keeps its text.
46
+ if (entry.kind === 'refused')
47
+ return;
48
+ setDraft(next);
49
+ const count = entry.kind === 'count' ? entry.value : null;
50
+ if (count !== value)
51
+ onChange(count);
52
+ }
53
+ /** ArrowUp and ArrowDown step; Home and End go to min and a finite max. */
54
+ function keyTarget(key) {
55
+ if (key === 'ArrowUp')
56
+ return moves.up;
57
+ if (key === 'ArrowDown')
58
+ return moves.down;
59
+ if (key === 'Home')
60
+ return moves.down === null ? null : min;
61
+ if (key === 'End' && Number.isFinite(max))
62
+ return moves.up === null ? null : max;
63
+ return undefined;
64
+ }
65
+ function onKeyDown(event) {
66
+ const target = keyTarget(event.key);
67
+ if (target === undefined)
68
+ return;
69
+ event.preventDefault();
70
+ move(target);
71
+ }
72
+ const invalid = Boolean(error);
73
+ return (_jsxs("div", { className: cx('sw-stepper', width === 'full' && 'sw-stepper-full', invalid && 'sw-stepper-invalid', changed && !invalid && 'sw-stepper-changed', readOnly && 'sw-stepper-readonly'), children: [_jsx("label", { className: labelVisuallyHidden ? 'sw-sr-only' : undefined, htmlFor: inputId, id: labelId, children: _jsx(FieldLabelText, { label: label }) }), _jsxs("div", { "aria-labelledby": labelId, className: "sw-stepper-track", role: "group", children: [_jsx("button", { "aria-controls": inputId, "aria-label": decrementLabel, className: "sw-stepper-button", disabled: moves.down === null, onClick: () => move(moves.down), onMouseDown: keepFocus, tabIndex: -1, type: "button", children: _jsx("span", { "aria-hidden": "true", children: '−' }) }), _jsx("input", { ref: ref, "aria-describedby": invalid ? errorId : undefined, "aria-invalid": invalid || undefined, "aria-valuemax": Number.isFinite(max) ? max : undefined, "aria-valuemin": min, "aria-valuenow": value ?? undefined, "aria-valuetext": valueText, autoComplete: "off", className: "sw-stepper-input", disabled: disabled, id: inputId,
74
+ // iOS's numeric pad has no minus key, so a range below zero takes
75
+ // the text keyboard.
76
+ inputMode: allowsNegative ? 'text' : 'numeric', name: name, onBlur: () => setDraft(null), onChange: onType, onKeyDown: onKeyDown, placeholder: placeholder, readOnly: readOnly, role: "spinbutton", type: "text", value: text }), _jsx("button", { "aria-controls": inputId, "aria-label": incrementLabel, className: "sw-stepper-button", disabled: moves.up === null, onClick: () => move(moves.up), onMouseDown: keepFocus, tabIndex: -1, type: "button", children: _jsx("span", { "aria-hidden": "true", children: "+" }) })] }), _jsx(FieldErrorRegion, { className: "sw-field-error", id: errorId, message: error })] }));
77
+ });
@@ -0,0 +1,18 @@
1
+ import { type HTMLAttributes, type ReactNode } from 'react';
2
+ import { type StepHeadingLevel, type StepsLabels } from './steps/step-status.js';
3
+ export { Step, type StepProps } from './steps/Step.js';
4
+ export type { StepHeadingLevel, StepsLabels, StepStatus, } from './steps/step-status.js';
5
+ export type StepsProps = Omit<HTMLAttributes<HTMLOListElement>, 'children'> & {
6
+ /** The `Step`s, in order; each is numbered by its place. */
7
+ children: ReactNode;
8
+ /** Each step title's heading level; defaults to 2. */
9
+ headingLevel?: StepHeadingLevel;
10
+ /** The status words assistive technology hears after a folded step's title. English by default. */
11
+ labels?: Partial<StepsLabels>;
12
+ };
13
+ /**
14
+ * A one-page vertical stepper: a form's steps in order, the current one
15
+ * open, the done ones folded to a line with their summary and Edit, the
16
+ * ones ahead greyed, a line joining each marker to the next.
17
+ */
18
+ export declare function Steps({ children, className, headingLevel, labels, ...rest }: StepsProps): import("react").JSX.Element;
@@ -0,0 +1,34 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Children, Fragment, isValidElement, useRef, } from 'react';
4
+ import { cx } from '../class-names.js';
5
+ import { defaultStepsLabels, stepHeadingLevels, } from './steps/step-status.js';
6
+ import { StepPlaceContext } from './steps/steps-context.js';
7
+ export { Step } from './steps/Step.js';
8
+ function assertSteps(labels, level) {
9
+ if (Object.values(labels).some((word) => typeof word !== 'string' || !word.trim()))
10
+ throw new RangeError('Steps labels must not be empty');
11
+ if (!stepHeadingLevels.includes(level))
12
+ throw new RangeError('Steps headingLevel must be 2, 3 or 4');
13
+ }
14
+ /**
15
+ * A one-page vertical stepper: a form's steps in order, the current one
16
+ * open, the done ones folded to a line with their summary and Edit, the
17
+ * ones ahead greyed, a line joining each marker to the next.
18
+ */
19
+ export function Steps({ children, className, headingLevel = 2, labels, ...rest }) {
20
+ const words = { ...defaultStepsLabels, ...labels };
21
+ assertSteps(words, headingLevel);
22
+ const steps = Children.toArray(children).filter(isValidElement);
23
+ // A Fragment would share one number among its steps: pass them directly.
24
+ if (steps.some((step) => step.type === Fragment))
25
+ throw new RangeError('Steps children must be Steps, not a Fragment');
26
+ // The first step to open in a commit claims focus (see Step).
27
+ const focusClaimRef = useRef(false);
28
+ return (_jsx("ol", { className: cx('sw-steps', className), ...rest, children: steps.map((step, index) => (_jsx(StepPlaceContext.Provider, { value: {
29
+ number: index + 1,
30
+ headingLevel,
31
+ labels: words,
32
+ focusClaimRef,
33
+ }, children: step }, step.key))) }));
34
+ }
@@ -0,0 +1,22 @@
1
+ export declare const actionBarSummaryTones: readonly ["default", "danger", "success"];
2
+ /** The value's color: the text color, or danger (short) or success (matched). */
3
+ export type ActionBarSummaryTone = (typeof actionBarSummaryTones)[number];
4
+ /** A labelled figure for the bar, such as "Counted $33,790.00 short $4,000.00". */
5
+ export type ActionBarSummary = {
6
+ /** What the figure is, such as "Counted". */
7
+ label: string;
8
+ /** The figure, formatted by the consumer, such as "$33,790.00". */
9
+ value: string;
10
+ /** A short line after the figure, such as "short $4,000.00". */
11
+ detail?: string;
12
+ /** Colors the value only; the words say why. Defaults to `default`. */
13
+ tone?: ActionBarSummaryTone;
14
+ };
15
+ export declare function assertActionBarSummary(summary: ActionBarSummary): void;
16
+ /**
17
+ * The bar's figure: its label, the value in the heading variant at tabular
18
+ * figures, and its detail, on one baseline, wrapping as one phrase.
19
+ */
20
+ export declare function ActionBarSummaryLine({ summary, }: {
21
+ summary: ActionBarSummary;
22
+ }): import("react").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from '../../class-names.js';
3
+ export const actionBarSummaryTones = ['default', 'danger', 'success'];
4
+ function isBlank(text) {
5
+ return typeof text !== 'string' || !text.trim();
6
+ }
7
+ export function assertActionBarSummary(summary) {
8
+ if (isBlank(summary.label) || isBlank(summary.value))
9
+ throw new RangeError('ActionBar summary needs a label and a value');
10
+ if (summary.detail !== undefined && isBlank(summary.detail))
11
+ throw new RangeError('ActionBar summary detail must not be empty');
12
+ if (summary.tone !== undefined &&
13
+ !actionBarSummaryTones.includes(summary.tone))
14
+ throw new RangeError(`Unknown ActionBar summary tone: ${String(summary.tone)}`);
15
+ }
16
+ /**
17
+ * The bar's figure: its label, the value in the heading variant at tabular
18
+ * figures, and its detail, on one baseline, wrapping as one phrase.
19
+ */
20
+ export function ActionBarSummaryLine({ summary, }) {
21
+ const tone = summary.tone ?? 'default';
22
+ return (_jsxs("span", { className: cx('sw-action-bar-summary', tone !== 'default' && `sw-action-bar-summary-${tone}`), children: [_jsx("span", { className: "sw-action-bar-summary-label", children: summary.label }), ' ', _jsx("span", { className: "sw-action-bar-summary-value sw-text-heading sw-tabular", children: summary.value }), summary.detail ? (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "sw-action-bar-summary-detail", children: summary.detail })] })) : null] }));
23
+ }
@@ -0,0 +1,7 @@
1
+ import { type DenominationGridColumn } from './types.js';
2
+ /** A column's name as its header reads: the label, then its caption ("$1 × 100"). */
3
+ export declare function columnName(column: DenominationGridColumn): string;
4
+ /** A column's label and, after it, its muted caption ("$1 × 100"). */
5
+ export declare function ColumnHeadText({ column }: {
6
+ column: DenominationGridColumn;
7
+ }): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** A column's name as its header reads: the label, then its caption ("$1 × 100"). */
3
+ export function columnName(column) {
4
+ return column.caption === undefined
5
+ ? column.label
6
+ : `${column.label} ${column.caption}`;
7
+ }
8
+ /** A column's label and, after it, its muted caption ("$1 × 100"). */
9
+ export function ColumnHeadText({ column }) {
10
+ if (column.caption === undefined)
11
+ return _jsx(_Fragment, { children: column.label });
12
+ return (_jsxs(_Fragment, { children: [column.label, ' ', _jsx("span", { className: "sw-denomination-head-caption", children: column.caption })] }));
13
+ }
@@ -1,10 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../../class-names.js';
3
3
  import { denominationLabelWidthClass } from '../../css/css-denomination-aligned.js';
4
- import { denominationCellToneClassNames } from '../../css/css-denomination-cell-tones.js';
5
- import { denominationCellView } from '../../denomination-cells.js';
6
4
  import { ScrollRegion } from '../ScrollRegion.js';
7
- import { RowLabelLine } from './RowLabelLine.js';
5
+ import { ColumnHeadText } from './ColumnHeadText.js';
6
+ import { StripRow } from './StripRow.js';
8
7
  /**
9
8
  * The total column's header: the consumer's name for the column, shown over
10
9
  * the totals from md up in the column heads' style, and visually hidden
@@ -27,8 +26,5 @@ export function DenominationStrip({ label, columns, rows, zeroLabel, totalLabel,
27
26
  '--sw-denomination-columns': columns.length,
28
27
  '--sw-denomination-total': hasTotals ? 1 : 0,
29
28
  };
30
- return (_jsx(ScrollRegion, { "aria-label": label, className: cx('sw-denomination-scroll', aligned && 'sw-denomination-scroll-aligned'), children: _jsxs("table", { className: cx('sw-denomination-grid sw-denomination-strip', labelWidth && 'sw-denomination-strip-aligned', labelWidth && denominationLabelWidthClass(labelWidth)), style: aligned, children: [_jsx("caption", { className: "sw-sr-only", children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("td", { className: "sw-denomination-corner" }), columns.map((column) => (_jsx("th", { className: "sw-denomination-head", scope: "col", children: 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) => {
31
- const view = denominationCellView(row.cells[index] ?? null, row.signed ?? false, zeroLabel);
32
- return (_jsx("td", { className: cx('sw-denomination-cell', `sw-denomination-cell-${view.state}`, ...denominationCellToneClassNames(row.cellTones?.[index])), children: view.text }, column.key));
33
- }), hasTotals ? (_jsx("td", { className: "sw-denomination-total", children: _jsx("span", { className: "sw-denomination-total-value", children: row.total ?? '' }) })) : null] }, row.id))) })] }) }));
29
+ return (_jsx(ScrollRegion, { "aria-label": label, className: cx('sw-denomination-scroll', aligned && 'sw-denomination-scroll-aligned'), children: _jsxs("table", { className: cx('sw-denomination-grid sw-denomination-strip', labelWidth && 'sw-denomination-strip-aligned', rows.some((row) => row.entry) && 'sw-denomination-strip-entry', labelWidth && denominationLabelWidthClass(labelWidth)), style: aligned, children: [_jsx("caption", { className: "sw-sr-only", children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("td", { className: "sw-denomination-corner" }), columns.map((column) => (_jsx("th", { className: "sw-denomination-head", scope: "col", children: _jsx(ColumnHeadText, { column: column }) }, column.key))), hasTotals ? _jsx(TotalHead, { totalLabel: totalLabel }) : null] }) }), _jsx("tbody", { children: rows.map((row) => (_jsx(StripRow, { columns: columns, hasTotals: hasTotals, row: row, zeroLabel: zeroLabel }, row.id))) })] }) }));
34
30
  }
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../../class-names.js';
3
3
  import { denominationCellToneClassNames } from '../../css/css-denomination-cell-tones.js';
4
4
  import { denominationCellView } from '../../denomination-cells.js';
5
+ import { ColumnHeadText } from './ColumnHeadText.js';
5
6
  import { RowLabelLine } from './RowLabelLine.js';
6
7
  /**
7
8
  * A lone row is named by the grid's own label, so its label line shows only
@@ -32,7 +33,7 @@ export function DenominationTiles({ label, columns, rows, rowRole, subtotal, zer
32
33
  const amount = subtotal && cell !== null && cell !== 0
33
34
  ? subtotal(cell, column)
34
35
  : null;
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));
36
+ return (_jsxs("li", { className: cx('sw-denomination-tile', ...denominationCellToneClassNames(row.cellTones?.[index])), children: [_jsx("span", { className: "sw-denomination-head", children: _jsx(ColumnHeadText, { column: column }) }), _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));
36
37
  }) })] }, row.id));
37
38
  }) }));
38
39
  }
@@ -0,0 +1,15 @@
1
+ import { type DenominationGridColumn, type DenominationGridEntry, type DenominationGridRow } from './types.js';
2
+ type EntryCellProps = {
3
+ row: DenominationGridRow;
4
+ entry: DenominationGridEntry;
5
+ column: DenominationGridColumn;
6
+ index: number;
7
+ };
8
+ /**
9
+ * One editable count of a strip row: a full-width `Stepper` named by the
10
+ * row and the column as its header reads ("Counted $1 × 100"), its cap and
11
+ * message from the entry, and the cell's tone on its border (an accent one
12
+ * is the Stepper's changed look).
13
+ */
14
+ export declare function EntryCell({ row, entry, column, index }: EntryCellProps): import("react").JSX.Element;
15
+ export {};
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx } from '../../class-names.js';
3
+ import { denominationCellToneClassNames } from '../../css/css-denomination-cell-tones.js';
4
+ import { Stepper } from '../Stepper.js';
5
+ import { columnName } from './ColumnHeadText.js';
6
+ /**
7
+ * One editable count of a strip row: a full-width `Stepper` named by the
8
+ * row and the column as its header reads ("Counted $1 × 100"), its cap and
9
+ * message from the entry, and the cell's tone on its border (an accent one
10
+ * is the Stepper's changed look).
11
+ */
12
+ export function EntryCell({ row, entry, column, index }) {
13
+ const max = entry.max?.[index] ?? undefined;
14
+ const tone = row.cellTones?.[index];
15
+ return (_jsx("td", { className: cx('sw-denomination-entry', ...denominationCellToneClassNames(tone)), children: _jsx(Stepper, { changed: tone === 'accent', decrementLabel: entry.decrementLabel, disabled: row.disabled, error: entry.errors?.[index] ?? undefined, incrementLabel: entry.incrementLabel, label: `${row.label} ${columnName(column)}`, labelVisuallyHidden: true, max: max, onChange: (value) => entry.onChange(column.key, value), placeholder: entry.placeholder, value: row.cells[index] ?? null, width: "full" }) }));
16
+ }
@@ -1,8 +1,9 @@
1
1
  import { type DenominationGridRow } from './types.js';
2
2
  /**
3
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.
4
+ * they wrap beside the glyph, never under it (Teisoro DRW-28). A caption is
5
+ * a muted line under the words, beside the same glyph. Both the strip and
6
+ * the tiles layouts draw a row label with it.
6
7
  */
7
8
  export declare function RowLabelLine({ row }: {
8
9
  row: DenominationGridRow;