@scalewing/react 1.56.0 → 1.58.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 (51) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +68 -1
  3. package/dist/components/Field.d.ts +3 -72
  4. package/dist/components/Field.js +6 -4
  5. package/dist/components/FixtureGroup.d.ts +3 -2
  6. package/dist/components/FixtureGroup.js +4 -3
  7. package/dist/components/FixtureRow.d.ts +4 -2
  8. package/dist/components/FixtureRow.js +12 -22
  9. package/dist/components/Table.d.ts +2 -2
  10. package/dist/components/Table.js +1 -1
  11. package/dist/components/Text.d.ts +19 -0
  12. package/dist/components/Text.js +6 -2
  13. package/dist/components/field/FieldDescription.d.ts +15 -0
  14. package/dist/components/field/FieldDescription.js +20 -0
  15. package/dist/components/field/field-checks.d.ts +4 -4
  16. package/dist/components/field/field-checks.js +6 -4
  17. package/dist/components/field/field-props.d.ts +82 -0
  18. package/dist/components/field/field-props.js +1 -0
  19. package/dist/components/fixture/FixtureScoreBoxes.d.ts +19 -0
  20. package/dist/components/fixture/FixtureScoreBoxes.js +57 -0
  21. package/dist/components/fixture/row-parts.d.ts +17 -0
  22. package/dist/components/fixture/row-parts.js +19 -0
  23. package/dist/components/fixture/score-digits.d.ts +7 -0
  24. package/dist/components/fixture/score-digits.js +9 -0
  25. package/dist/components/fixture/types.d.ts +68 -4
  26. package/dist/components/fixture/validate-score.d.ts +10 -0
  27. package/dist/components/fixture/validate-score.js +39 -0
  28. package/dist/components/fixture/validate.d.ts +6 -6
  29. package/dist/components/fixture/validate.js +12 -8
  30. package/dist/components/table/TableRow.d.ts +18 -2
  31. package/dist/components/table/TableRow.js +4 -3
  32. package/dist/css/css-field-description.d.ts +2 -0
  33. package/dist/css/css-field-description.js +39 -0
  34. package/dist/css/css-field.js +4 -0
  35. package/dist/css/css-fixture-detail.d.ts +2 -0
  36. package/dist/css/css-fixture-detail.js +44 -0
  37. package/dist/css/css-fixture-list.d.ts +1 -0
  38. package/dist/css/css-fixture-list.js +50 -0
  39. package/dist/css/css-fixture-score.d.ts +2 -0
  40. package/dist/css/css-fixture-score.js +97 -0
  41. package/dist/css/css-fixture.js +11 -50
  42. package/dist/css/css-font-mono.js +2 -1
  43. package/dist/css/css-table-ledger.d.ts +4 -0
  44. package/dist/css/css-table-ledger.js +6 -0
  45. package/dist/css/css-table-row-rule.d.ts +2 -0
  46. package/dist/css/css-table-row-rule.js +22 -0
  47. package/dist/css/css-table.js +4 -0
  48. package/dist/index.d.ts +4 -4
  49. package/dist/index.js +3 -3
  50. package/dist/styles.css +112 -0
  51. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.58.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cf53284: `Field` `descriptionTone` (opt-in, `FieldDescriptionTone`, `fieldDescriptionTones`: `'muted'` default, `'warning'`): a warning description in the warning colour after a small `aria-hidden` alert glyph, for a hint that needs the person but is not an error; it stays in the control's `aria-describedby`, sets no `aria-invalid` and no danger border, gives way to `error`, and takes `CanvasText` in forced colors. `FixtureGroup` `titleHidden` (opt-in): the title still names the list (`aria-labelledby`) but is not drawn, for a group whose heading is already on screen. `Text` `font` (opt-in, `TextFont`, `textFonts`: `'sans'` default, `'mono'`): the platform's monospace face through the generated `sw-font-mono` class at the variant's size, weight and colour, for a code read character by character. Unknown tones and fonts throw a `RangeError`. New classes `sw-field-description-warning`, `sw-field-description-glyph`. `FieldProps` now lives in `components/field/field-props.ts` and is still exported from the package root.
8
+
9
+ ## 1.57.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 9a42d28: `FixtureRow` `score` (opt-in, `FixtureScore`: `{ label, home, away, onChange(side, value), onSubmit(), onCancel?(), disabled?, error? }`, boxes `FixtureScoreBox`: `{ value, label, ref?, invalid? }`): two score boxes between the sides in place of `center` (now optional when `score` is given), digits only (`inputmode="numeric"`, two digits, selected on focus) in a group named by `label`, each box named by its own `label`; Enter calls `onSubmit`, Escape `onCancel`; 32 px squares on a fine pointer and 44 px on a coarse one; `error` is said politely under the row and marks the boxes invalid. A score row is never a link or a button, and works with `flag`, `select` and `action`. `FixtureRow` `detail` (opt-in): a full-width part under the row, outside its press area. `TableRow` `rule="after"` (opt-in, `TableRowRule`, `tableRowRules`): a 2 px muted rule under the row, such as a cut under the last playoff place, `CanvasText` in forced colors. New types `FixtureScore`, `FixtureScoreBox`, `FixtureScoreSide`, `TableRowRule`; new classes `sw-fixture-score`, `sw-fixture-score-box`, `sw-fixture-score-dash`, `sw-fixture-score-error`, `sw-fixture-item-below`, `sw-fixture-detail`, `sw-table-row-rule-after`.
14
+
3
15
  ## 1.56.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -189,6 +189,21 @@ Import the CSS once at the application entry. Do not copy it into your source tr
189
189
  </Field>
190
190
  ```
191
191
 
192
+ `Field` `descriptionTone="warning"` (default `"muted"`; `fieldDescriptionTones`, `FieldDescriptionTone`) draws the `description` in the warning colour (`--sw-color-warning`, 4.5:1 on the canvas in both schemes) after a small alert glyph a space-4 square (private chrome, `aria-hidden`, centred on its first line), for a hint that needs the person but is not an error, such as a number another row already has. It stays the control's description (`aria-describedby`, after any ids the control already lists) and sets no invalid state: no `aria-invalid`, no danger border, nothing announced. `error` still replaces it on screen, and is the only way to mark the field invalid. In forced colors the words and glyph take `CanvasText` (an error takes `Mark`), so the glyph is the cue. It works at every `size`, including `xs` in a table cell. An unknown tone throws a `RangeError`. New classes `sw-field-description-warning`, `sw-field-description-glyph`.
193
+
194
+ ```tsx
195
+ <Field
196
+ description={twin ? `${twin} also wears ${band}` : undefined}
197
+ descriptionTone="warning"
198
+ label={`Band number for ${bird}`}
199
+ labelVisuallyHidden
200
+ size="xs"
201
+ width="xs"
202
+ >
203
+ <input inputMode="numeric" value={band} onChange={setBand} />
204
+ </Field>
205
+ ```
206
+
192
207
  Every press control and link takes an explicit `tabindex="0"`: `Button` (and the controls built on it, `CalendarButton` and `InfoTip`), `CommandPaletteTrigger`, `Box as="a"` with an `href`, `SectionNav` links, a link or button `FixtureRow`, `AppShell` links and its menu and close buttons, the `Select`, `MultiSelect`, `ActionMenu` and `ContextSwitcher` triggers, `CommandPaletteTrigger` (and a `ContextSwitcher`'s listbox while it holds focus), `SegmentedControl` items (every item, as outside Safari: its arrow keys change the value, not the focus, so it is not a roving group), `DateField`'s calendar button and month steps, a search `Field`'s clear button, a `Table`'s sort and `TableCell onPress` buttons, `Checkbox`, `Switch`, and the checked radio of a `RadioGroup` or `FilterChips` (the first enabled one while none is; the others keep no `tabindex`, so each browser's arrow-key group rule stands). Safari's default (its "Press Tab to highlight each item" setting off) leaves a button, a link, a checkbox and a radio out of the Tab order unless it carries one, so a Safari user would Tab past them; the explicit `0` gives them the order every other browser already does. A `tabIndex` you pass wins, and controls that are deliberately no tab stop keep their `-1`: a `Stepper`'s − and +, and a roving item (a `Tabs` tab, a menu item, a calendar day, a `DateToggleCalendar` day, a `ScheduleGrid` cell) other than the current one. A `<summary>` is already in Safari's order and takes none.
193
208
 
194
209
  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.
@@ -776,7 +791,7 @@ The layout follows the window, with generated media queries only. From 75rem (12
776
791
 
777
792
  A header `TableCell` (`as="th"`) with `sort` (`'ascending'`, `'descending'` or `'none'`) and `onSort` is its column's sort control: its children become a button in the header's own style with a chevron (before the name in a `numeric` or end-aligned column), and the cell carries `aria-sort` only while it is the sorted column. `sort` on a data cell, or one of the two without the other, throws a `TypeError`. `Table` `layout="fixed"` (default `auto`) sizes the columns from the header row alone, so a filter that hides a row moves no column, and `verticalAlign="top"` (default `middle`) starts every cell of a tall row on its first line. `TableCell` `width` is `min` (an auto table's column takes the least room its content allows, on one line; set it on each cell of the column) or a size, `xs` to `xl` (4, 6, 8, 12 and 16 rem); in a fixed table a column without a width shares what is left, so leave one column, such as a description, unsized. The HTML `width` attribute is not accepted. Unknown values throw a `RangeError`.
778
793
 
779
- A ledger read across as well as down takes `Table` `columnRules` (a hairline at the inline start of every cell after a row's first, mirrored right to left) and `TableCell` `tone` (`success`, `danger`, `muted` or `accent`: the cell's text in the theme colour, as `Text color` uses it; never the only signal, so keep the sign or the words). `TableRow` `variant="sub"` (default `"default"`) sets a row under the one above, as a batch's items: its row header, a `TableCell as="th" scope="row"`, is indented a `space-6` step with a guide bar at the indent (a sub row without a row header is not indented). `TableRow` `variant="total"` is a total mid-table, such as a subtotal the rows above add up to before the table goes on: its cells at weight 600 under a 2 px rule across its top (the cells' own collapsed top border, so forced colors keep it), as a `TableFooter` total is drawn, with no fill, so accent text in it keeps the contrast it has on the table's own surface. Name it with a row header and keep it in the body, after the rows it adds up (not the body's first row under a pinned header, whose rule would sit on its own); a cell's `tone` keeps its colour, and `struck` works on it. Its cells are all at weight 600, so a quiet link or a `TableCell onPress` action in it is no longer set apart by weight: keep a total row's name plain text (a `TableFooter` row is the same). `TableRow` `struck` keeps a voided row on the record: the subtle fill and the text muted and struck through, toned cells included; it works with any variant. A line-through is not read aloud, so say why in the row, such as a `Badge` "Voided": a Badge in a line of text is not struck, but one that is itself a flex item (a direct child of `Inline`) is, so put it in the title's line. A row header in the body or footer (`th scope="row"`) reads as body text. An unknown `tone` or `variant` throws a `RangeError`.
794
+ A ledger read across as well as down takes `Table` `columnRules` (a hairline at the inline start of every cell after a row's first, mirrored right to left) and `TableCell` `tone` (`success`, `danger`, `muted` or `accent`: the cell's text in the theme colour, as `Text color` uses it; never the only signal, so keep the sign or the words). `TableRow` `variant="sub"` (default `"default"`) sets a row under the one above, as a batch's items: its row header, a `TableCell as="th" scope="row"`, is indented a `space-6` step with a guide bar at the indent (a sub row without a row header is not indented). `TableRow` `variant="total"` is a total mid-table, such as a subtotal the rows above add up to before the table goes on: its cells at weight 600 under a 2 px rule across its top (the cells' own collapsed top border, so forced colors keep it), as a `TableFooter` total is drawn, with no fill, so accent text in it keeps the contrast it has on the table's own surface. Name it with a row header and keep it in the body, after the rows it adds up (not the body's first row under a pinned header, whose rule would sit on its own); a cell's `tone` keeps its colour, and `struck` works on it. Its cells are all at weight 600, so a quiet link or a `TableCell onPress` action in it is no longer set apart by weight: keep a total row's name plain text (a `TableFooter` row is the same). `TableRow` `struck` keeps a voided row on the record: the subtle fill and the text muted and struck through, toned cells included; it works with any variant. A line-through is not read aloud, so say why in the row, such as a `Badge` "Voided": a Badge in a line of text is not struck, but one that is itself a flex item (a direct child of `Inline`) is, so put it in the title's line. A row header in the body or footer (`th scope="row"`) reads as body text. `TableRow` `rule="after"` (opt-in, `TableRowRule`; `tableRowRules` lists the values) draws a rule under the row, such as the cut under the last playoff place: its cells' bottom border 2 px in the muted grey, stronger than the 1 px hairlines between rows (3:1 on the surface) without a colour's meaning, kept in `CanvasText` in forced colors and resolved over the next row's top by the collapsed-border model. The row's words, weight and fill are unchanged and the line is not read aloud, so say what it means in words beside the table ("The line under 4th place marks the playoff places"). It works with any variant, `struck` and `selected`; class `sw-table-row-rule-after`. An unknown `tone`, `variant` or `rule` throws a `RangeError`.
780
795
 
781
796
  `Table` `stickyHeader` pins the header: the whole `TableHeader` as one group, every row in it (the column headings and any row under them, such as a running total) with all its cells, `th` and `td`, each row under the one above, on the glass with its row and column rules. `true` (default) pins it to the top of the table's own scroll region, `false` lets it scroll away, and `'page'` pins it to the page's top as the page scrolls. With `'page'`, while the table fits its width its region stops being a scroll box (`sw-table-sticky-page`), so the header stays in view under any sticky `AppHeader` (which covers it); a table wider than its region scrolls sideways in it and pins inside it. Use it for a long table whose last row, such as a non-sticky `TableFooter` total, should scroll with the rows. An unknown value throws a `RangeError`.
782
797
 
@@ -1241,6 +1256,15 @@ const { headings, omissions } = inspectMarkdown(page.body, {
1241
1256
  </FixtureGroup>
1242
1257
  ```
1243
1258
 
1259
+ `FixtureGroup` `titleHidden` (opt-in, default `false`) keeps `title` as the list's name (`aria-labelledby`) but does not draw the caption: it moves to a visually hidden element (`sw-sr-only`) out of the layout, so the first row starts where the caption would. Use it when the heading above already says what the caption would, such as one result in a sheet titled with the pairing and the day; the title is still required and still must be nonblank.
1260
+
1261
+ ```tsx
1262
+ <Text as="h3" variant="title">Heron Marsh vs Otter Pool</Text>
1263
+ <FixtureGroup title="Heron Marsh vs Otter Pool, Sunday 25 October" titleHidden>
1264
+ <FixtureRow home={{ name: 'Heron Marsh' }} away={{ name: 'Otter Pool' }} center="14–9" centerKind="score" />
1265
+ </FixtureGroup>
1266
+ ```
1267
+
1244
1268
  `FixtureRow` `flag` (opt-in, `FixtureFlag`: `{ label, tone? }`) marks an issue at the row's start, such as a clash: a round mark a space-4 across with a "!", in the tone's tint and ink (`warning`, amber, the default, for something that needs the person; `danger`, red), its edge keeping 3:1 on the surface in every palette and the text colour in forced colors. The mark is `aria-hidden` with `label` as its `title` tooltip, and `label` is read first in the row from a visually hidden span ("Can't-play clash Heron Marsh 8:00 Otter Pool"); with `aria-label` it is put before your words with a comma ("Can't-play clash, Heron Marsh vs Otter Pool, 8:00"), so do not repeat it there. From `md` the mark sits on the first line at the row's start, the home side kept clear of it; below `md` it leads the second line, before the details, so it never squeezes the home name. A row with nothing on that line below `md` (no `status`, and no `meta` or one hidden below `md` by `metaHideBelow`) keeps the mark inline on its first line instead, as from `md` (class `sw-fixture-row-flag-inline`), rather than gaining a line that holds only the flag. A blank `label` or an unknown `tone` throws a `RangeError`. `action` (opt-in) is a control after the row, such as a small neutral `Button` ("Move", named in your words: `aria-label="Move Heron Marsh vs Otter Pool"`): it is the row's sibling in its list item, outside a link or button row's press area and after it in the Tab order, a press on it never opens the row, and it stays centred on the row's height in the run's last column at every width (from `md` the row's one line, below `md` both lines), so every row's action lines up. Where `:has` is supported the hover fill spans the row and its action. An empty slot draws nothing. The run has a column for it at the end, and one for `select` at the start (`sw-fixture-list` is `auto minmax(0, 1fr) auto minmax(0, 1fr) auto auto auto`, six below `md`); a row spans every column but those two (`grid-column: 2 / -2`), and an `auto` column no row uses takes no space. New classes `sw-fixture-row-flagged`, `sw-fixture-flag`, `sw-fixture-flag-mark`, `sw-fixture-flag-warning`, `sw-fixture-flag-danger`, `sw-fixture-item-action` and `sw-fixture-action`.
1245
1269
 
1246
1270
  ```tsx
@@ -1290,6 +1314,47 @@ const { headings, omissions } = inspectMarkdown(page.body, {
1290
1314
  />
1291
1315
  ```
1292
1316
 
1317
+ `FixtureRow` `score` (opt-in, `FixtureScore`) puts two score boxes between the sides in place of `center`, for typing a result: `{ label, home, away, onChange(side, value), onSubmit(), onCancel?(), disabled?, error? }`, each box a `FixtureScoreBox` `{ value, label, ref?, invalid? }`. The boxes sit in a `role="group"` named by `label` ("Score, Heron Marsh v Otter Pool"), each a text input named by its own `label` (the side's name), home first, with a hidden en dash between them; none of those words is drawn, so name them in the consumer's language. Each box is `inputmode="numeric"`, `pattern="[0-9]*"`, `maxLength` 2 and `autocomplete="off"`; typed or pasted non-digits are dropped before `onChange` is called with the side (`'home'` or `'away'`) and the box's digits, and a change that keeps the same digits is not reported. Focus selects a box's text, so a typed score replaces it. Enter in either box calls `onSubmit` (not while an IME composition is open) and Escape calls `onCancel`, which should put the boxes back, such as to the saved score (Escape is prevented then, so an enclosing `Dialog` stays open; without `onCancel` it is left alone). `disabled` shows both boxes locked, with the canvas's disabled look. `error` ("Type both scores.") is said under the row in a polite live region in caption type and the danger ink, from the home side's start; it describes both boxes (`aria-describedby`) and marks them `aria-invalid` with a danger edge (`Mark` in forced colors), and a box's `invalid` (`true` or `false`) overrides that for the box, so a filled box can stay unmarked; an empty `error` shows nothing. `ref` reaches a box's input, such as to focus the home box of the row a link opened or the next game's after a save. The boxes are squares the sm control height (32 px) on a fine pointer, so the row keeps its 44 px height, and the 44 px touch target on a coarse one; below `md` their margin is a space-2 step. A row with `score` is never a link or a button (its boxes are their own controls), and it works with `flag`, `select`, `action`, `meta`, `status` and `detail`; the Tab order is the select, the home box, the away box, anything in `detail`, then the action. On a touch screen the numeric keypad may have no Return key (iOS), so give touch users a visible action, such as a small neutral "Record score" `Button` in `action`. With neither `center` nor `score`, with both, with `centerKind` beside `score`, with `href` or `onPress` beside `score`, or with a score lacking a nonempty `label`, boxes with a string `value` and a nonempty `label`, an `onChange` or an `onSubmit` (or with an `onCancel` that is not a function or an `error` that is not a string), it throws a `RangeError`. `center` is optional when `score` is given. New types `FixtureScore`, `FixtureScoreBox`, `FixtureScoreSide`; new classes `sw-fixture-score`, `sw-fixture-score-box`, `sw-fixture-score-dash`, `sw-fixture-score-error`.
1318
+
1319
+ `FixtureRow` `detail` (opt-in, `ReactNode`) is a full-width part under the row, such as a reason field while an official score is changed: in the row's list item after the row (and after a score's message), outside a link or button row's press area, and before the action in the Tab order (so a reason field comes before a Record button), spanning the run's columns from the home side's start (after a `select`'s box) to the action's end, with the row's inline padding and a space-2 step below. An empty slot draws nothing. New classes `sw-fixture-item-below` (an item with a score or a detail) and `sw-fixture-detail`.
1320
+
1321
+ ```tsx
1322
+ // tally, missing, change, record, reset, reason, setReason and homeBox are your own.
1323
+ <FixtureRow
1324
+ home={{ name: 'Heron Marsh' }}
1325
+ away={{ name: 'Otter Pool' }}
1326
+ score={{
1327
+ label: 'Score, Heron Marsh v Otter Pool',
1328
+ home: { value: tally.home, label: 'Heron Marsh', ref: homeBox },
1329
+ away: { value: tally.away, label: 'Otter Pool' },
1330
+ onChange: change,
1331
+ onSubmit: record,
1332
+ onCancel: reset,
1333
+ error: missing ? 'Type both scores.' : undefined,
1334
+ }}
1335
+ meta="Changing the official 0–0"
1336
+ action={
1337
+ <Button
1338
+ aria-label="Record Heron Marsh vs Otter Pool"
1339
+ onPress={record}
1340
+ size="sm"
1341
+ variant="neutral"
1342
+ >
1343
+ Record score
1344
+ </Button>
1345
+ }
1346
+ detail={
1347
+ <Field label="Reason" labelVisuallyHidden>
1348
+ <input
1349
+ onChange={(event) => setReason(event.currentTarget.value)}
1350
+ placeholder="Why?"
1351
+ value={reason}
1352
+ />
1353
+ </Field>
1354
+ }
1355
+ />
1356
+ ```
1357
+
1293
1358
  `ScheduleGrid` is items in a grid of rows (such as times) by columns (such as places), moved between cells. Props: `label` (the grid's name), `columns` and `rows` (`{ id, label }[]`, in order), `rowsLabel` (opt-in, the header over the rows' labels, "Time"; without it that corner is blank and `aria-hidden`), `items` (`{ id, columnId, rowId, label, content }[]`: `label` is the item's name in words, `content` what its card draws; hold no controls in it), `disabledCells` (opt-in, `{ columnId, rowId, reason }[]`: never a target, drawn on the quiet fill with the reason), `canDrop` (opt-in, `(itemId, target) => true | { reason }`, where `target` is `{ kind: 'move', columnId, rowId }` for an empty cell or `{ kind: 'swap', itemId, columnId, rowId }` for another item; omitted, every target is allowed), `onMove(itemId, { columnId, rowId })`, `onSwap(itemId, otherItemId)`, `disabled` (opt-in: nothing can be picked up; the cells still take focus) and `labels` (`Partial<ScheduleGridLabels>`, an `undefined` entry keeping the default; `defaultScheduleGridLabels` holds the English words: `open`, `movingFrom`, `releaseToMove`, `releaseToSwap`, `instructions`, and the announcements `pickedUp(item, mode)`, `moved(item, row, column)`, `swapped(item, other)`, `cancelled(item)`). The grid holds no rules: `canDrop` decides every target and is asked for every cell while an item is carried (keep it cheap), and the grid moves nothing itself: `onMove` and `onSwap` report, and you update `items`. It is a `table` with `role="grid"` named by `label` and described by `labels.instructions` (visually hidden), its column and row headers `th`, and each cell a `gridcell` named by what it holds (the item's `label`, `labels.open` or a disabled cell's reason) and, while an item is carried, what it would do (", Moving from here", ", Release to move here", ", Release to swap", or ", " and the blocked reason); the drawing inside a cell (the card, its words and captions) is `aria-hidden`, so the name is read once. Cells with the same contents share a name, as the headers tell them apart for assistive technology: in a test, find one by its row (`within(getByRole('row', { name: /^10:30/ })).getAllByRole('gridcell')[1]`). The grid is one Tab stop with roving focus: the arrows move by a cell (the horizontal ones swapped in right-to-left), Home and End go to the row's ends and with Control (or ⌘) to the grid's first and last cells. On an item, M or Space picks it up; while carried, the arrows choose the target (the focused cell shows the accent ring and its caption), Enter or Space drops it there and Escape cancels and puts focus back on the item. A mouse drags (a press that moves 4 px; a pen too, where the browser does not pan the page under it): the cell under the pointer is the one hover target, and the release drops it; outside the grid it cancels. Touch never drags, so a finger still scrolls the page: a tap on an item picks it up and a tap on a cell drops it; a tap on its own cell, or a press anywhere outside the grid, cancels. A mouse may use the same press-then-press path (WCAG 2.5.7). Dropping on an empty allowed cell calls `onMove`, on another allowed item `onSwap`, on its own cell cancels; a blocked or disabled target is refused with its reason announced (a key or tap carry keeps the item to try elsewhere, a drag ends). Each step (picked up, moved, swapped, cancelled, refused) is said in a polite `role="status"` live region inside the grid, a repeat said again. `pickedUp` is given how the item was picked up as its second argument (`ScheduleGridCarryMode`: `'keyboard'`, `'tap'` for a press then a press, or `'drag'`), and the default words fit it ("Choose a cell with the arrow keys…" only for keys; "Press a cell to drop it there, or press outside the grid to cancel." for a tap; "Release it on a cell to drop it there." for a drag); a one-argument function still works. A carry by keys or taps is also cancelled (and announced) when focus leaves the grid, such as a Tab past it, but not when the window itself loses focus; a press on a row or column header keeps focus where it was, so it never cancels a carry. A drag's release always ends it, even one that arrives before the pick-up has rendered: it drops on the cell under it, or cancels outside the grid. While an item is carried its own cell takes a dashed edge and "Moving from here", every cell `canDrop` blocks shows its reason in the warning ink with a warning edge (an open one on the warning tint), the target has the accent ring (an open one on the accent tint), and disabled cells keep their grey look; only open cells are filled, so an item's card stays on the surface its words were made for. Columns share the width equally, each at least the `md` table column (8rem), the rows' labels the `sm` one; when they do not fit, the grid scrolls sideways inside its own box, never the page. Cells are at least 44 px plus a space-3 high. Forced colors keep every edge (a blocked one dotted) and draw rings in Highlight. Classes `sw-schedule-grid*` (see the catalog). A blank `label`, `rowsLabel` or word in `labels`, a message in `labels` that is not a function, no columns or rows, a blank or repeated id or label, an item or disabled cell outside the grid, two items in one cell, a cell disabled twice, a disabled cell that holds an item or has no reason, or a `canDrop` reason that is blank throws a `RangeError`.
1294
1359
 
1295
1360
  ```tsx
@@ -1420,6 +1485,8 @@ A row can also carry three trailing parts. `unread` marks it new for the person,
1420
1485
 
1421
1486
  `Text` takes an optional `align` (`start`, `center`, `end`) mapped to generated `sw-text-align-*` classes, for a heading that must stay centered when it wraps.
1422
1487
 
1488
+ `Text` `font="mono"` (default `"sans"`; `textFonts`, `TextFont`) sets the words in the platform's monospace face through the generated `sw-font-mono` class (`ui-monospace, monospace`, the generic names only, so each platform draws its own), keeping the variant's size, weight, letter spacing and `color`, for a code read or typed character by character, such as a join code: `<Text variant="headline" font="mono">R8M 3TW</Text>`. The face is fixed-width, so figures and letters line up; the text stays selectable. An unknown font throws a `RangeError`.
1489
+
1423
1490
  `Grid` places children in one to four (or six) equal-width columns with a token `gap` step. `columnsBelow={{ md: 2 }}` drops to fewer columns below the `md` breakpoint so tiles and stat cards stay readable on a phone. It accepts every `Box` prop.
1424
1491
 
1425
1492
  `fit` (`narrow` 15rem, `medium` 16.25rem, `wide` 18.75rem) fits the columns to the grid's width instead of a fixed count: as many columns as fit at that minimum, sharing the row, and one column when the grid is narrower than it, so a row of cards reflows on a tablet and stacks on a phone with no breakpoint. A fitted grid takes no `columns` or `columnsBelow` (either throws a `RangeError`, as an unknown `fit` does); keep `columnSpan` for fixed-column grids. `gridFits` lists the names.
@@ -1,75 +1,6 @@
1
- import { type SpacingStep } from '@scalewing/tokens';
2
- import { type ReactNode } from 'react';
3
- import { type RequiredMark } from './FieldLabelText.js';
4
- import { type FieldWidth } from '../css/css-field-width.js';
1
+ import { type FieldProps } from './field/field-props.js';
5
2
  import { fieldVariants, type FieldVariant } from './field/field-checks.js';
6
- export type FieldSize = 'xs' | 'md';
7
3
  export { fieldVariants, type FieldVariant };
8
- export type FieldProps = {
9
- children: ReactNode;
10
- gap?: SpacingStep;
11
- label: string;
12
- labelVisuallyHidden?: boolean;
13
- size?: FieldSize;
14
- description?: string;
15
- error?: string;
16
- /**
17
- * Marks the control invalid (`aria-invalid` and the danger border) without
18
- * a message of its own, for a field whose error is shown elsewhere, such
19
- * as one message under a group of fields. Point the control's
20
- * `aria-describedby` at that message. `error` implies it. Passing
21
- * `invalid`, even `false` but not `undefined`, requires one native control
22
- * child from the first render.
23
- */
24
- invalid?: boolean;
25
- /**
26
- * Marks a value the person changed from a saved one, such as a field in a
27
- * correction: the control's border (or an adorned frame's) in the accent
28
- * color, thicker than the plain hairline, and its fill tinted with 12 % of
29
- * the accent, so a changed field reads apart from the focused one (focus
30
- * draws a ring and never tints). The value keeps 4.5:1 on the tint; while
31
- * a placeholder shows, the fill stays plain. It is a cue, not the record:
32
- * say what it was in `description` ("Was 25"). `invalid` and `error`
33
- * win. Like `invalid`, passing it, even `false` but not `undefined`,
34
- * requires one native control child from the first render.
35
- */
36
- changed?: boolean;
37
- required?: boolean;
38
- /**
39
- * `hidden` leaves out the label's " *" while the control stays required
40
- * (and announced so); `visible` (default) draws it.
41
- * When hidden, say in visible text which fields must be filled; a
42
- * placeholder alone is not enough.
43
- */
44
- requiredMark?: RequiredMark;
45
- /** Short text inside the control frame before the value, such as a currency sign. Not part of the value. */
46
- prefix?: string;
47
- /** Short text inside the control frame after the value, such as a unit. Not part of the value. */
48
- suffix?: string;
49
- /**
50
- * `outlined` (default) is the labelled form field. `search` is a search
51
- * box: the label is not drawn (it stays the input's name), a `leading`
52
- * glyph sits at the frame's start, the input is `type="search"`, and a
53
- * clear button named `clearLabel` shows while the input holds text and
54
- * is enabled; it calls `onClear` (set the value to `''`) and puts focus
55
- * back in the input. Needs one native `input` child with a `value`.
56
- */
57
- variant?: FieldVariant;
58
- /** A search field's glyph at its start, such as a magnifier; hidden from assistive technology. */
59
- leading?: ReactNode;
60
- /** A search field's clear button name, such as "Clear search". */
61
- clearLabel?: string;
62
- /** Called by a search field's clear button; the consumer empties the value. */
63
- onClear?: () => void;
64
- /**
65
- * Caps the control's width at a Table column size (`xs` 4rem, `sm` 6rem,
66
- * `md` 8rem, `lg` 12rem, `xl` 16rem), at the start of the column and
67
- * never wider than it, so a short value such as a rate or a count does
68
- * not stretch across a form. The label and messages keep the column's
69
- * width. Left out, the control fills the column. It needs one native
70
- * text input, select or textarea child (a `TypeError` otherwise); an
71
- * unknown width throws a `RangeError`.
72
- */
73
- width?: FieldWidth;
74
- };
4
+ export type { FieldProps, FieldSize } from './field/field-props.js';
5
+ export { fieldDescriptionTones, type FieldDescriptionTone, } from './field/FieldDescription.js';
75
6
  export declare function Field(props: FieldProps): import("react").JSX.Element;
@@ -1,16 +1,18 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { cloneElement, isValidElement, useId, } from 'react';
3
+ import { cloneElement, isValidElement, useId } from 'react';
4
4
  import { adornedControlAria, FieldAdornment, } from './FieldAdornment.js';
5
5
  import { FieldErrorRegion } from './FieldErrorRegion.js';
6
- import { assertRequiredMark, FieldLabelText, } from './FieldLabelText.js';
6
+ import { assertRequiredMark, FieldLabelText } from './FieldLabelText.js';
7
7
  import { fieldWidthClass } from '../css/css-field-width.js';
8
+ import { FieldDescription } from './field/FieldDescription.js';
8
9
  import { SearchFrame } from './field/SearchFrame.js';
9
10
  import { assertFieldProps, fieldVariants, passesInvalid, } from './field/field-checks.js';
10
11
  import { Stack } from './Stack.js';
11
12
  export { fieldVariants };
13
+ export { fieldDescriptionTones, } from './field/FieldDescription.js';
12
14
  export function Field(props) {
13
- const { children, gap = 1, label, labelVisuallyHidden = false, size = 'md', description, error, invalid = false, changed = false, required = false, requiredMark = 'visible', prefix, suffix, variant = 'outlined', leading, clearLabel = '', onClear, width, } = props;
15
+ const { children, gap = 1, label, labelVisuallyHidden = false, size = 'md', description, descriptionTone = 'muted', error, invalid = false, changed = false, required = false, requiredMark = 'visible', prefix, suffix, variant = 'outlined', leading, clearLabel = '', onClear, width, } = props;
14
16
  assertFieldProps(props, children);
15
17
  const search = variant === 'search';
16
18
  const controlId = useId();
@@ -90,5 +92,5 @@ export function Field(props) {
90
92
  });
91
93
  const value = children.props.value;
92
94
  const hasText = value !== undefined && value !== null && String(value) !== '';
93
- return (_jsxs(Stack, { as: "div", className: className, gap: labelHidden ? 0 : gap, children: [_jsx("label", { className: search ? 'sw-sr-only' : undefined, htmlFor: children.props.id || controlId, id: labelId, children: labelText }), search ? (_jsx(SearchFrame, { clearLabel: clearLabel, leading: leading, onClear: onClear, showClear: hasText && !children.props.disabled && !children.props.readOnly, children: control })) : adorned ? (_jsx(FieldAdornment, { prefix: prefix, prefixId: prefixId, suffix: suffix, suffixId: suffixId, children: control })) : (control), description && !error ? (_jsx("span", { className: "sw-field-description", id: descriptionId, children: description })) : null, _jsx(FieldErrorRegion, { className: "sw-field-error", id: errorId, message: error })] }));
95
+ return (_jsxs(Stack, { as: "div", className: className, gap: labelHidden ? 0 : gap, children: [_jsx("label", { className: search ? 'sw-sr-only' : undefined, htmlFor: children.props.id || controlId, id: labelId, children: labelText }), search ? (_jsx(SearchFrame, { clearLabel: clearLabel, leading: leading, onClear: onClear, showClear: hasText && !children.props.disabled && !children.props.readOnly, children: control })) : adorned ? (_jsx(FieldAdornment, { prefix: prefix, prefixId: prefixId, suffix: suffix, suffixId: suffixId, children: control })) : (control), description && !error ? (_jsx(FieldDescription, { id: descriptionId, tone: descriptionTone, children: description })) : null, _jsx(FieldErrorRegion, { className: "sw-field-error", id: errorId, message: error })] }));
94
96
  }
@@ -3,7 +3,8 @@ export type { FixtureGroupProps } from './fixture/types.js';
3
3
  /**
4
4
  * A run of fixtures under a caption, such as a match day at a venue: the
5
5
  * caption names a list of FixtureRows, whose columns line up across the
6
- * run (one grid, each row a subgrid of it). A blank title throws a
6
+ * run (one grid, each row a subgrid of it). `titleHidden` keeps the
7
+ * caption as the list's name without drawing it. A blank title throws a
7
8
  * `RangeError`.
8
9
  */
9
- export declare function FixtureGroup({ children, title }: FixtureGroupProps): import("react").JSX.Element;
10
+ export declare function FixtureGroup({ children, title, titleHidden, }: FixtureGroupProps): import("react").JSX.Element;
@@ -5,12 +5,13 @@ import { FixtureListContext } from './fixture/fixture-list-context.js';
5
5
  /**
6
6
  * A run of fixtures under a caption, such as a match day at a venue: the
7
7
  * caption names a list of FixtureRows, whose columns line up across the
8
- * run (one grid, each row a subgrid of it). A blank title throws a
8
+ * run (one grid, each row a subgrid of it). `titleHidden` keeps the
9
+ * caption as the list's name without drawing it. A blank title throws a
9
10
  * `RangeError`.
10
11
  */
11
- export function FixtureGroup({ children, title }) {
12
+ export function FixtureGroup({ children, title, titleHidden = false, }) {
12
13
  if (typeof title !== 'string' || !title.trim())
13
14
  throw new RangeError('FixtureGroup title must be nonempty');
14
15
  const titleId = useId();
15
- return (_jsxs("div", { className: "sw-fixture-group", children: [_jsx("div", { className: "sw-fixture-group-title", id: titleId, children: title }), _jsx("ul", { "aria-labelledby": titleId, className: "sw-fixture-list", role: "list", children: _jsx(FixtureListContext.Provider, { value: true, children: children }) })] }));
16
+ return (_jsxs("div", { className: "sw-fixture-group", children: [_jsx("div", { className: titleHidden ? 'sw-sr-only' : 'sw-fixture-group-title', id: titleId, children: title }), _jsx("ul", { "aria-labelledby": titleId, className: "sw-fixture-list", role: "list", children: _jsx(FixtureListContext.Provider, { value: true, children: children }) })] }));
16
17
  }
@@ -1,5 +1,5 @@
1
1
  import { type FixtureRowProps } from './fixture/types.js';
2
- export type { FixtureFlag, FixtureRowProps, FixtureSelect, FixtureSide, FixtureStatus, } from './fixture/types.js';
2
+ export type { FixtureFlag, FixtureRowProps, FixtureScore, FixtureScoreBox, FixtureScoreSide, FixtureSelect, FixtureSide, FixtureStatus, } from './fixture/types.js';
3
3
  /**
4
4
  * One fixture in a FixtureGroup: the home side ending at the centre, the
5
5
  * kickoff time or score in the middle, the away side starting from it, then
@@ -7,6 +7,8 @@ export type { FixtureFlag, FixtureRowProps, FixtureSelect, FixtureSide, FixtureS
7
7
  * `href` or `onPress` the whole row is a link or a button with a chevron.
8
8
  * `flag` marks an issue at the row's start; `select` is a checkbox before
9
9
  * the row and `action` a control after it, both outside its press area.
10
- * Invalid words or props throw a `RangeError`.
10
+ * `score` puts two score boxes in the middle in place of `center`, and
11
+ * `detail` is a part under the row. Invalid words or props throw a
12
+ * `RangeError`.
11
13
  */
12
14
  export declare function FixtureRow(props: FixtureRowProps): import("react").JSX.Element;
@@ -1,30 +1,16 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useId } from 'react';
3
4
  import { cx } from '../class-names.js';
4
5
  import { hideClass } from '../css/breakpoints.js';
5
6
  import { tabStop } from '../tab-stop.js';
6
7
  import { Badge } from './Badge.js';
7
- import { Checkbox } from './Checkbox.js';
8
8
  import { useFixtureList } from './fixture/fixture-list-context.js';
9
+ import { FixtureScoreBoxes, FixtureScoreError, } from './fixture/FixtureScoreBoxes.js';
10
+ import { Flag, Select, Side } from './fixture/row-parts.js';
9
11
  import { assertFixtureRow } from './fixture/validate.js';
10
12
  import { plainPress } from './link-press.js';
11
13
  import { hasSlot } from './slot.js';
12
- /**
13
- * The issue marker at the row's start: a drawn mark (its tooltip the
14
- * label) and the label in hidden words, read first in the row's name.
15
- */
16
- function Flag({ flag }) {
17
- return (_jsxs("span", { className: `sw-fixture-flag sw-fixture-flag-${flag.tone ?? 'warning'}`, children: [_jsx("span", { "aria-hidden": "true", className: "sw-fixture-flag-mark", title: flag.label }), _jsx("span", { className: "sw-sr-only", children: flag.label })] }));
18
- }
19
- /** The leading checkbox: a Checkbox with its label visually hidden. */
20
- function Select({ select }) {
21
- return (_jsx("div", { className: "sw-fixture-select", children: _jsx(Checkbox, { checked: select.checked, disabled: select.disabled ?? false, label: select.label, labelVisuallyHidden: true, onCheckedChange: select.onCheckedChange }) }));
22
- }
23
- /** One side: its name, and its mark on the centre's side (hidden). */
24
- function Side({ side, place }) {
25
- const mark = hasSlot(side.mark) ? (_jsx("span", { "aria-hidden": "true", className: "sw-fixture-mark", children: side.mark })) : null;
26
- return (_jsxs("span", { className: `sw-fixture-team sw-fixture-team-${place}`, children: [place === 'away' ? mark : null, _jsx("span", { className: "sw-fixture-name", children: side.name }), place === 'home' ? mark : null] }));
27
- }
28
14
  /**
29
15
  * One fixture in a FixtureGroup: the home side ending at the centre, the
30
16
  * kickoff time or score in the middle, the away side starting from it, then
@@ -32,22 +18,26 @@ function Side({ side, place }) {
32
18
  * `href` or `onPress` the whole row is a link or a button with a chevron.
33
19
  * `flag` marks an issue at the row's start; `select` is a checkbox before
34
20
  * the row and `action` a control after it, both outside its press area.
35
- * Invalid words or props throw a `RangeError`.
21
+ * `score` puts two score boxes in the middle in place of `center`, and
22
+ * `detail` is a part under the row. Invalid words or props throw a
23
+ * `RangeError`.
36
24
  */
37
25
  export function FixtureRow(props) {
38
26
  useFixtureList();
39
27
  assertFixtureRow(props);
40
- const { action, away, center, centerKind = 'time', flag, home, href, meta, metaHideBelow, onPress, select, status, 'aria-label': ariaLabel, } = props;
28
+ const errorId = useId();
29
+ const { action, away, center, centerKind = 'time', detail, flag, home, href, meta, metaHideBelow, onPress, score, select, status, 'aria-label': ariaLabel, } = props;
41
30
  const hasMeta = hasSlot(meta);
42
31
  const pressable = href !== undefined || onPress !== undefined;
43
32
  const hasAction = hasSlot(action);
33
+ const hasDetail = hasSlot(detail);
44
34
  // Below md the end is a second line; with nothing on it (no status, and
45
35
  // meta absent or hidden below md) the flag stays on the first line.
46
36
  const endShownNarrow = status !== undefined || (hasMeta && metaHideBelow === undefined);
47
37
  const rowClass = cx('sw-fixture-row', pressable && 'sw-fixture-row-pressable', flag && 'sw-fixture-row-flagged', flag && !endShownNarrow && 'sw-fixture-row-flag-inline');
48
38
  const name = ariaLabel !== undefined && flag ? `${flag.label}, ${ariaLabel}` : ariaLabel;
49
39
  // Spaces as their own text nodes keep the parts apart in the name.
50
- const parts = (_jsxs(_Fragment, { children: [flag ? (_jsxs(_Fragment, { children: [_jsx(Flag, { flag: flag }), ' '] })) : null, _jsx(Side, { place: "home", side: home }), ' ', _jsx("span", { className: `sw-fixture-center sw-fixture-center-${centerKind}`, children: center }), ' ', _jsx(Side, { place: "away", side: away }), hasMeta || status ? (_jsxs(_Fragment, { children: [' ', _jsxs("span", { className: "sw-fixture-end", children: [hasMeta ? (_jsx("span", { className: cx('sw-fixture-meta', metaHideBelow && hideClass('below', metaHideBelow)), children: meta })) : null, hasMeta && status ? ' ' : null, status ? (_jsx(Badge, { size: "sm", tone: status.tone, children: status.label })) : null] })] })) : null, pressable ? (_jsx("span", { "aria-hidden": "true", className: "sw-fixture-chevron" })) : null] }));
40
+ const parts = (_jsxs(_Fragment, { children: [flag ? (_jsxs(_Fragment, { children: [_jsx(Flag, { flag: flag }), ' '] })) : null, _jsx(Side, { place: "home", side: home }), ' ', score ? (_jsx(FixtureScoreBoxes, { errorId: errorId, score: score })) : (_jsx("span", { className: `sw-fixture-center sw-fixture-center-${centerKind}`, children: center })), ' ', _jsx(Side, { place: "away", side: away }), hasMeta || status ? (_jsxs(_Fragment, { children: [' ', _jsxs("span", { className: "sw-fixture-end", children: [hasMeta ? (_jsx("span", { className: cx('sw-fixture-meta', metaHideBelow && hideClass('below', metaHideBelow)), children: meta })) : null, hasMeta && status ? ' ' : null, status ? (_jsx(Badge, { size: "sm", tone: status.tone, children: status.label })) : null] })] })) : null, pressable ? (_jsx("span", { "aria-hidden": "true", className: "sw-fixture-chevron" })) : null] }));
51
41
  let row;
52
42
  if (href !== undefined)
53
43
  row = (_jsx("a", { "aria-label": name, className: rowClass, href: href, onClick: onPress
@@ -62,5 +52,5 @@ export function FixtureRow(props) {
62
52
  row = (_jsx("button", { "aria-label": name, className: rowClass, onClick: onPress, tabIndex: tabStop, type: "button", children: parts }));
63
53
  else
64
54
  row = _jsx("div", { className: rowClass, children: parts });
65
- return (_jsxs("li", { className: cx('sw-fixture-item', select && 'sw-fixture-item-select', hasAction && 'sw-fixture-item-action'), children: [select ? _jsx(Select, { select: select }) : null, row, hasAction ? _jsx("span", { className: "sw-fixture-action", children: action }) : null] }));
55
+ return (_jsxs("li", { className: cx('sw-fixture-item', select && 'sw-fixture-item-select', hasAction && 'sw-fixture-item-action', (score || hasDetail) && 'sw-fixture-item-below'), children: [select ? _jsx(Select, { select: select }) : null, row, score ? _jsx(FixtureScoreError, { error: score.error, id: errorId }) : null, hasDetail ? _jsx("div", { className: "sw-fixture-detail", children: detail }) : null, hasAction ? _jsx("span", { className: "sw-fixture-action", children: action }) : null] }));
66
56
  }
@@ -2,10 +2,10 @@ import { type HTMLAttributes } from 'react';
2
2
  import { type TableLayout, type TableStickyHeader, type TableVerticalAlign } from './table/table-layout.js';
3
3
  import { type TableMaxHeight } from './table/table-max-height.js';
4
4
  export { TableCell, type TableCellProps, type TableColumnWidth, type TableSort, } from './table/TableCell.js';
5
- export { TableRow, type TableRowProps, type TableRowVariant, } from './table/TableRow.js';
5
+ export { TableRow, type TableRowProps, type TableRowRule, type TableRowVariant, } from './table/TableRow.js';
6
6
  export { TableSelectCell, type TableSelectCellProps, } from './table/TableSelectCell.js';
7
7
  export type { TableCellTone } from '../css/css-table-ledger.js';
8
- export { tableCellTones, tableRowVariants } from '../css/css-table-ledger.js';
8
+ export { tableCellTones, tableRowRules, tableRowVariants, } from '../css/css-table-ledger.js';
9
9
  export type { TableLayout, TableMaxHeight, TableStickyHeader, TableVerticalAlign, };
10
10
  export type TableDensity = 'comfortable' | 'compact';
11
11
  export type TableProps = HTMLAttributes<HTMLTableElement> & {
@@ -7,7 +7,7 @@ import { assertTableMaxHeight, tableMaxHeightStyle, } from './table/table-max-he
7
7
  export { TableCell, } from './table/TableCell.js';
8
8
  export { TableRow, } from './table/TableRow.js';
9
9
  export { TableSelectCell, } from './table/TableSelectCell.js';
10
- export { tableCellTones, tableRowVariants } from '../css/css-table-ledger.js';
10
+ export { tableCellTones, tableRowRules, tableRowVariants, } from '../css/css-table-ledger.js';
11
11
  /**
12
12
  * A wide table scrolls inside its own `ScrollRegion`, named after the
13
13
  * table; a bounded one (`maxHeight`) scrolls its rows in it too.
@@ -2,10 +2,21 @@ import { type SemanticColorKey, type TypographyVariant } from '@scalewing/tokens
2
2
  import { type HTMLAttributes, type LabelHTMLAttributes } from 'react';
3
3
  export type TextElement = 'p' | 'span' | 'h1' | 'h2' | 'h3' | 'h4' | 'label' | 'strong';
4
4
  export type TextAlign = 'start' | 'center' | 'end';
5
+ /** The faces Text sets its words in: the theme's sans or the platform's monospace. */
6
+ export declare const textFonts: readonly ["sans", "mono"];
7
+ export type TextFont = (typeof textFonts)[number];
5
8
  export type TextProps = HTMLAttributes<HTMLElement> & Pick<LabelHTMLAttributes<HTMLLabelElement>, 'htmlFor'> & {
6
9
  align?: TextAlign;
7
10
  as?: TextElement;
8
11
  color?: SemanticColorKey;
12
+ /**
13
+ * `mono` sets the words in the platform's monospace face (the generated
14
+ * `sw-font-mono` class) at the variant's size, weight and colour, for a
15
+ * code read or typed character by character, such as a join code. The
16
+ * face is fixed-width, so figures line up. Default `sans`; an unknown
17
+ * font throws a `RangeError`.
18
+ */
19
+ font?: TextFont;
9
20
  truncate?: boolean;
10
21
  variant?: TypographyVariant;
11
22
  };
@@ -13,6 +24,14 @@ export declare const Text: import("react").ForwardRefExoticComponent<HTMLAttribu
13
24
  align?: TextAlign;
14
25
  as?: TextElement;
15
26
  color?: SemanticColorKey;
27
+ /**
28
+ * `mono` sets the words in the platform's monospace face (the generated
29
+ * `sw-font-mono` class) at the variant's size, weight and colour, for a
30
+ * code read or typed character by character, such as a join code. The
31
+ * face is fixed-width, so figures line up. Default `sans`; an unknown
32
+ * font throws a `RangeError`.
33
+ */
34
+ font?: TextFont;
16
35
  truncate?: boolean;
17
36
  variant?: TypographyVariant;
18
37
  } & import("react").RefAttributes<HTMLElement>>;
@@ -1,6 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef, } from 'react';
3
3
  import { cx } from '../class-names.js';
4
+ /** The faces Text sets its words in: the theme's sans or the platform's monospace. */
5
+ export const textFonts = ['sans', 'mono'];
4
6
  const defaultElement = {
5
7
  body: 'p',
6
8
  caption: 'span',
@@ -13,7 +15,9 @@ const defaultElement = {
13
15
  lead: 'p',
14
16
  title: 'h3',
15
17
  };
16
- export const Text = forwardRef(function Text({ align, as, className, color = 'text', style, truncate = false, variant = 'body', ...rest }, ref) {
18
+ export const Text = forwardRef(function Text({ align, as, className, color = 'text', font = 'sans', style, truncate = false, variant = 'body', ...rest }, ref) {
19
+ if (!textFonts.includes(font))
20
+ throw new RangeError(`Unknown Text font: ${String(font)}`);
17
21
  const Component = as ?? defaultElement[variant];
18
- return (_jsx(Component, { ref: ref, className: cx(`sw-text-${variant}`, align && `sw-text-align-${align}`, truncate && 'sw-truncate', className), style: { color: `var(--sw-color-${color})`, ...style }, ...rest }));
22
+ return (_jsx(Component, { ref: ref, className: cx(`sw-text-${variant}`, align && `sw-text-align-${align}`, font === 'mono' && 'sw-font-mono', truncate && 'sw-truncate', className), style: { color: `var(--sw-color-${color})`, ...style }, ...rest }));
19
23
  });
@@ -0,0 +1,15 @@
1
+ /** How a Field's description reads: the quiet hint or a warning that is not an error. */
2
+ export declare const fieldDescriptionTones: readonly ["muted", "warning"];
3
+ export type FieldDescriptionTone = (typeof fieldDescriptionTones)[number];
4
+ export declare function assertFieldDescriptionTone(tone: unknown): void;
5
+ /**
6
+ * A Field's description under its control, the control's
7
+ * `aria-describedby` target. A warning one leads with a private alert
8
+ * glyph, hidden from assistive technology, so it reads apart from a muted
9
+ * hint without colour (and in forced colours, where both take CanvasText).
10
+ */
11
+ export declare function FieldDescription({ children, id, tone, }: {
12
+ children: string;
13
+ id: string;
14
+ tone: FieldDescriptionTone;
15
+ }): import("react").JSX.Element;
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** How a Field's description reads: the quiet hint or a warning that is not an error. */
3
+ export const fieldDescriptionTones = ['muted', 'warning'];
4
+ export function assertFieldDescriptionTone(tone) {
5
+ if (!fieldDescriptionTones.includes(tone))
6
+ throw new RangeError(`Unknown Field descriptionTone: ${String(tone)}`);
7
+ }
8
+ /** A triangle with an exclamation mark, drawn in the description's colour. */
9
+ const alertPath = 'M12 3.5 21.5 20h-19zM12 9.75v4.5M12 17.25v.01';
10
+ /**
11
+ * A Field's description under its control, the control's
12
+ * `aria-describedby` target. A warning one leads with a private alert
13
+ * glyph, hidden from assistive technology, so it reads apart from a muted
14
+ * hint without colour (and in forced colours, where both take CanvasText).
15
+ */
16
+ export function FieldDescription({ children, id, tone, }) {
17
+ if (tone === 'muted')
18
+ return (_jsx("span", { className: "sw-field-description", id: id, children: children }));
19
+ return (_jsxs("span", { className: "sw-field-description sw-field-description-warning", id: id, children: [_jsx("svg", { "aria-hidden": "true", className: "sw-field-description-glyph", focusable: "false", viewBox: "0 0 24 24", children: _jsx("path", { d: alertPath }) }), _jsx("span", { children: children })] }));
20
+ }
@@ -1,5 +1,5 @@
1
1
  import { type KeyboardEvent } from 'react';
2
- import type { FieldProps } from '../Field.js';
2
+ import type { FieldProps } from './field-props.js';
3
3
  export declare const fieldVariants: readonly ["outlined", "search"];
4
4
  export type FieldVariant = (typeof fieldVariants)[number];
5
5
  /** The props Field reads from, and sets on, its native control child. */
@@ -18,9 +18,9 @@ export type FieldControlProps = {
18
18
  onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
19
19
  };
20
20
  /**
21
- * Fails closed on a Field it cannot draw: an unknown variant or width,
22
- * search props on an outlined field, and a search field without its clear
23
- * button, its handler or one controlled native input.
21
+ * Fails closed on a Field it cannot draw: an unknown variant, width or
22
+ * description tone, search props on an outlined field, and a search field
23
+ * without its clear button, its handler or one controlled native input.
24
24
  */
25
25
  export declare function assertFieldProps(props: FieldProps, child: unknown): void;
26
26
  /**
@@ -1,14 +1,16 @@
1
1
  import { isValidElement } from 'react';
2
2
  import { assertFieldWidth } from '../../css/css-field-width.js';
3
+ import { assertFieldDescriptionTone } from './FieldDescription.js';
3
4
  export const fieldVariants = ['outlined', 'search'];
4
5
  /**
5
- * Fails closed on a Field it cannot draw: an unknown variant or width,
6
- * search props on an outlined field, and a search field without its clear
7
- * button, its handler or one controlled native input.
6
+ * Fails closed on a Field it cannot draw: an unknown variant, width or
7
+ * description tone, search props on an outlined field, and a search field
8
+ * without its clear button, its handler or one controlled native input.
8
9
  */
9
10
  export function assertFieldProps(props, child) {
10
- const { variant = 'outlined' } = props;
11
+ const { variant = 'outlined', descriptionTone = 'muted' } = props;
11
12
  assertFieldWidth(props.width);
13
+ assertFieldDescriptionTone(descriptionTone);
12
14
  if (!fieldVariants.includes(variant))
13
15
  throw new RangeError(`Unknown Field variant: ${String(variant)}`);
14
16
  if (variant !== 'search') {