@scalewing/react 1.56.0 → 1.57.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 (35) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +42 -1
  3. package/dist/components/FixtureRow.d.ts +4 -2
  4. package/dist/components/FixtureRow.js +12 -22
  5. package/dist/components/Table.d.ts +2 -2
  6. package/dist/components/Table.js +1 -1
  7. package/dist/components/fixture/FixtureScoreBoxes.d.ts +19 -0
  8. package/dist/components/fixture/FixtureScoreBoxes.js +57 -0
  9. package/dist/components/fixture/row-parts.d.ts +17 -0
  10. package/dist/components/fixture/row-parts.js +19 -0
  11. package/dist/components/fixture/score-digits.d.ts +7 -0
  12. package/dist/components/fixture/score-digits.js +9 -0
  13. package/dist/components/fixture/types.d.ts +62 -4
  14. package/dist/components/fixture/validate-score.d.ts +10 -0
  15. package/dist/components/fixture/validate-score.js +39 -0
  16. package/dist/components/fixture/validate.d.ts +6 -6
  17. package/dist/components/fixture/validate.js +12 -8
  18. package/dist/components/table/TableRow.d.ts +18 -2
  19. package/dist/components/table/TableRow.js +4 -3
  20. package/dist/css/css-fixture-detail.d.ts +2 -0
  21. package/dist/css/css-fixture-detail.js +44 -0
  22. package/dist/css/css-fixture-list.d.ts +1 -0
  23. package/dist/css/css-fixture-list.js +50 -0
  24. package/dist/css/css-fixture-score.d.ts +2 -0
  25. package/dist/css/css-fixture-score.js +97 -0
  26. package/dist/css/css-fixture.js +11 -50
  27. package/dist/css/css-table-ledger.d.ts +4 -0
  28. package/dist/css/css-table-ledger.js +6 -0
  29. package/dist/css/css-table-row-rule.d.ts +2 -0
  30. package/dist/css/css-table-row-rule.js +22 -0
  31. package/dist/css/css-table.js +4 -0
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.js +1 -1
  34. package/dist/styles.css +89 -0
  35. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.57.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 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`.
8
+
3
9
  ## 1.56.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -776,7 +776,7 @@ The layout follows the window, with generated media queries only. From 75rem (12
776
776
 
777
777
  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
778
 
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`.
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. `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
780
 
781
781
  `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
782
 
@@ -1290,6 +1290,47 @@ const { headings, omissions } = inspectMarkdown(page.body, {
1290
1290
  />
1291
1291
  ```
1292
1292
 
1293
+ `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`.
1294
+
1295
+ `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`.
1296
+
1297
+ ```tsx
1298
+ // tally, missing, change, record, reset, reason, setReason and homeBox are your own.
1299
+ <FixtureRow
1300
+ home={{ name: 'Heron Marsh' }}
1301
+ away={{ name: 'Otter Pool' }}
1302
+ score={{
1303
+ label: 'Score, Heron Marsh v Otter Pool',
1304
+ home: { value: tally.home, label: 'Heron Marsh', ref: homeBox },
1305
+ away: { value: tally.away, label: 'Otter Pool' },
1306
+ onChange: change,
1307
+ onSubmit: record,
1308
+ onCancel: reset,
1309
+ error: missing ? 'Type both scores.' : undefined,
1310
+ }}
1311
+ meta="Changing the official 0–0"
1312
+ action={
1313
+ <Button
1314
+ aria-label="Record Heron Marsh vs Otter Pool"
1315
+ onPress={record}
1316
+ size="sm"
1317
+ variant="neutral"
1318
+ >
1319
+ Record score
1320
+ </Button>
1321
+ }
1322
+ detail={
1323
+ <Field label="Reason" labelVisuallyHidden>
1324
+ <input
1325
+ onChange={(event) => setReason(event.currentTarget.value)}
1326
+ placeholder="Why?"
1327
+ value={reason}
1328
+ />
1329
+ </Field>
1330
+ }
1331
+ />
1332
+ ```
1333
+
1293
1334
  `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
1335
 
1295
1336
  ```tsx
@@ -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.
@@ -0,0 +1,19 @@
1
+ import { type FixtureScore } from './types.js';
2
+ /**
3
+ * The row's middle when it takes a score: the home box, an en dash and
4
+ * the away box, in a group named by `label`. Each box is a text input
5
+ * named by its own `label`, digits only (non-digits are dropped before
6
+ * `onChange`), its text selected on focus so a typed score replaces it.
7
+ */
8
+ export declare function FixtureScoreBoxes({ errorId, score, }: {
9
+ errorId: string;
10
+ score: FixtureScore;
11
+ }): import("react").JSX.Element;
12
+ /**
13
+ * The score's message under the row: a polite live region, present (and
14
+ * empty, taking no room) while there is no error, so a new one is said.
15
+ */
16
+ export declare function FixtureScoreError({ error, id, }: {
17
+ error: string | undefined;
18
+ id: string;
19
+ }): import("react").JSX.Element;
@@ -0,0 +1,57 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cx } from '../../class-names.js';
4
+ import { scoreBoxMaxDigits, scoreDigits } from './score-digits.js';
5
+ /** Whether a box is marked: its own `invalid`, else any error. */
6
+ function boxInvalid(invalid, hasError) {
7
+ return invalid ?? hasError;
8
+ }
9
+ /**
10
+ * Enter records and Escape puts the boxes back. A key that ends an IME
11
+ * composition is the composition's, not the row's.
12
+ */
13
+ function scoreKeyDown(score) {
14
+ return (event) => {
15
+ if (event.nativeEvent.isComposing)
16
+ return;
17
+ if (event.key === 'Enter') {
18
+ event.preventDefault();
19
+ score.onSubmit();
20
+ }
21
+ else if (event.key === 'Escape' && score.onCancel) {
22
+ // Kept from an enclosing dialog, whose Escape would close it.
23
+ event.preventDefault();
24
+ score.onCancel();
25
+ }
26
+ };
27
+ }
28
+ function ScoreBox({ describedBy, disabled, inputRef, invalid, label, onDigits, onKeyDown, value, }) {
29
+ return (_jsx("input", { ref: inputRef, "aria-describedby": describedBy, "aria-invalid": invalid || undefined, "aria-label": label, autoComplete: "off", className: "sw-fixture-score-box", disabled: disabled, inputMode: "numeric", maxLength: scoreBoxMaxDigits, onChange: (event) => {
30
+ const digits = scoreDigits(event.currentTarget.value);
31
+ if (digits !== value)
32
+ onDigits(digits);
33
+ }, onFocus: (event) => event.currentTarget.select(), onKeyDown: onKeyDown, pattern: "[0-9]*", type: "text", value: value }));
34
+ }
35
+ /**
36
+ * The row's middle when it takes a score: the home box, an en dash and
37
+ * the away box, in a group named by `label`. Each box is a text input
38
+ * named by its own `label`, digits only (non-digits are dropped before
39
+ * `onChange`), its text selected on focus so a typed score replaces it.
40
+ */
41
+ export function FixtureScoreBoxes({ errorId, score, }) {
42
+ const hasError = Boolean(score.error);
43
+ const disabled = score.disabled ?? false;
44
+ const onKeyDown = scoreKeyDown(score);
45
+ const box = (side) => {
46
+ const { invalid, label, ref, value } = score[side];
47
+ return (_jsx(ScoreBox, { describedBy: hasError ? errorId : undefined, disabled: disabled, inputRef: ref, invalid: boxInvalid(invalid, hasError), label: label, onDigits: (digits) => score.onChange(side, digits), onKeyDown: onKeyDown, value: value }));
48
+ };
49
+ return (_jsxs("span", { "aria-label": score.label, className: cx('sw-fixture-center', 'sw-fixture-score'), role: "group", children: [box('home'), _jsx("span", { "aria-hidden": "true", className: "sw-fixture-score-dash", children: "\u2013" }), box('away')] }));
50
+ }
51
+ /**
52
+ * The score's message under the row: a polite live region, present (and
53
+ * empty, taking no room) while there is no error, so a new one is said.
54
+ */
55
+ export function FixtureScoreError({ error, id, }) {
56
+ return (_jsx("span", { "aria-live": "polite", className: "sw-fixture-score-error", id: id, children: error || null }));
57
+ }
@@ -0,0 +1,17 @@
1
+ import { type FixtureFlag, type FixtureSelect, type FixtureSide } from './types.js';
2
+ /**
3
+ * The issue marker at the row's start: a drawn mark (its tooltip the
4
+ * label) and the label in hidden words, read first in the row's name.
5
+ */
6
+ export declare function Flag({ flag }: {
7
+ flag: FixtureFlag;
8
+ }): import("react").JSX.Element;
9
+ /** The leading checkbox: a Checkbox with its label visually hidden. */
10
+ export declare function Select({ select }: {
11
+ select: FixtureSelect;
12
+ }): import("react").JSX.Element;
13
+ /** One side: its name, and its mark on the centre's side (hidden). */
14
+ export declare function Side({ side, place, }: {
15
+ side: FixtureSide;
16
+ place: 'home' | 'away';
17
+ }): import("react").JSX.Element;
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { hasSlot } from '../slot.js';
3
+ import { Checkbox } from '../Checkbox.js';
4
+ /**
5
+ * The issue marker at the row's start: a drawn mark (its tooltip the
6
+ * label) and the label in hidden words, read first in the row's name.
7
+ */
8
+ export function Flag({ flag }) {
9
+ 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 })] }));
10
+ }
11
+ /** The leading checkbox: a Checkbox with its label visually hidden. */
12
+ export function Select({ select }) {
13
+ 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 }) }));
14
+ }
15
+ /** One side: its name, and its mark on the centre's side (hidden). */
16
+ export function Side({ side, place, }) {
17
+ const mark = hasSlot(side.mark) ? (_jsx("span", { "aria-hidden": "true", className: "sw-fixture-mark", children: side.mark })) : null;
18
+ 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] }));
19
+ }
@@ -0,0 +1,7 @@
1
+ /** The most digits a score box holds. */
2
+ export declare const scoreBoxMaxDigits = 2;
3
+ /**
4
+ * What a score box keeps of typed or pasted text: its digits only, the
5
+ * first two ("1a2" → "12", "3" stays out as it is not an ASCII digit).
6
+ */
7
+ export declare function scoreDigits(typed: string): string;
@@ -0,0 +1,9 @@
1
+ /** The most digits a score box holds. */
2
+ export const scoreBoxMaxDigits = 2;
3
+ /**
4
+ * What a score box keeps of typed or pasted text: its digits only, the
5
+ * first two ("1a2" → "12", "3" stays out as it is not an ASCII digit).
6
+ */
7
+ export function scoreDigits(typed) {
8
+ return typed.replace(/[^0-9]/g, '').slice(0, scoreBoxMaxDigits);
9
+ }
@@ -1,4 +1,4 @@
1
- import { type MouseEvent, type ReactNode } from 'react';
1
+ import { type MouseEvent, type ReactNode, type Ref } from 'react';
2
2
  import { type VisibilityBreakpoint } from '../../css/breakpoints.js';
3
3
  import { type BadgeTone } from '../../css/css-data.js';
4
4
  import { type FixtureCenterKind, type FixtureFlagTone } from '../../css/css-fixture.js';
@@ -35,14 +35,65 @@ export type FixtureSelect = {
35
35
  onCheckedChange: (checked: boolean) => void;
36
36
  disabled?: boolean;
37
37
  };
38
+ /** Which side's score box: the home side's (first) or the away side's. */
39
+ export type FixtureScoreSide = 'home' | 'away';
40
+ /** One side's score box. */
41
+ export type FixtureScoreBox = {
42
+ /** The box's digits, as typed ("" while empty); at most two. */
43
+ value: string;
44
+ /** Names the box, such as the side's name ("Heron Marsh"); not drawn. */
45
+ label: string;
46
+ /** The box's input, such as to focus it after a save. */
47
+ ref?: Ref<HTMLInputElement>;
48
+ /**
49
+ * Marks this box invalid. Without it, an `error` marks both boxes;
50
+ * `false` keeps this one unmarked, such as the box that is filled when
51
+ * the other is empty.
52
+ */
53
+ invalid?: boolean;
54
+ };
55
+ /**
56
+ * Two score boxes in the row's middle, typed by keyboard: the home side's
57
+ * and the away side's, digits only, in a group named by `label`.
58
+ */
59
+ export type FixtureScore = {
60
+ /** Names the boxes' group ("Score, Heron Marsh v Otter Pool"); not drawn. */
61
+ label: string;
62
+ home: FixtureScoreBox;
63
+ away: FixtureScoreBox;
64
+ /** A box's digits changed: non-digits are dropped before this is called. */
65
+ onChange: (side: FixtureScoreSide, value: string) => void;
66
+ /** Enter in either box: record the score. */
67
+ onSubmit: () => void;
68
+ /** Escape in either box: put the boxes back, such as to the saved score. */
69
+ onCancel?: () => void;
70
+ /** Both boxes shown and locked, such as a game not played yet. */
71
+ disabled?: boolean;
72
+ /**
73
+ * What is wrong ("Type both scores."): said politely under the row in the
74
+ * danger ink, describing both boxes, which are marked invalid (see
75
+ * `FixtureScoreBox.invalid`). Empty or absent, nothing is shown.
76
+ */
77
+ error?: string;
78
+ };
38
79
  export type FixtureRowProps = {
39
80
  home: FixtureSide;
40
81
  away: FixtureSide;
41
- /** What sits between the sides: a kickoff time ("8:00") or a score ("2–1"). */
42
- center: string;
82
+ /**
83
+ * What sits between the sides: a kickoff time ("8:00") or a score
84
+ * ("2–1"). Required unless `score` takes its place.
85
+ */
86
+ center?: string;
87
+ /**
88
+ * Score boxes between the sides in place of `center`, for typing a
89
+ * score: on a row that is not a link or a button (the boxes are their
90
+ * own controls). Works with `flag`, `select`, `action` and `detail`.
91
+ */
92
+ score?: FixtureScore;
43
93
  /**
44
94
  * `time` (default) draws the centre as a small grey chip; `score` as
45
- * bold figures. Both are tabular, so the rows' centres line up.
95
+ * bold figures. Both are tabular, so the rows' centres line up. It
96
+ * styles `center`, so leave it out with `score`.
46
97
  */
47
98
  centerKind?: FixtureCenterKind;
48
99
  /** Details at the row's end, such as the field; under the sides below `md`. */
@@ -64,6 +115,13 @@ export type FixtureRowProps = {
64
115
  * `action`, `href` and `onPress`.
65
116
  */
66
117
  select?: FixtureSelect;
118
+ /**
119
+ * A full-width part under the row, such as a reason field while a score
120
+ * is changed: in the row's list item, outside its link or button, lined
121
+ * up with the home side's start (after `select`'s box) and reaching the
122
+ * action's end.
123
+ */
124
+ detail?: ReactNode;
67
125
  /** Makes the whole row a link to this address. */
68
126
  href?: string;
69
127
  /**
@@ -0,0 +1,10 @@
1
+ import { type FixtureRowProps } from './types.js';
2
+ /**
3
+ * Fails closed on a middle the row cannot draw: neither a `center` nor a
4
+ * `score`, or both; a `centerKind` with a `score` (it styles `center`); a
5
+ * score on a link or button row (its boxes would sit inside the press
6
+ * target); a score without a group label, two boxes with string values
7
+ * and labels, an `onChange` and an `onSubmit`, or with an `onCancel` or
8
+ * `error` of the wrong type.
9
+ */
10
+ export declare function assertFixtureMiddle({ center, centerKind, href, onPress, score, }: FixtureRowProps): void;
@@ -0,0 +1,39 @@
1
+ function isBlank(words) {
2
+ return typeof words !== 'string' || !words.trim();
3
+ }
4
+ function isBox(box) {
5
+ return (box !== undefined &&
6
+ typeof box.value === 'string' &&
7
+ !isBlank(box.label) &&
8
+ (box.invalid === undefined || typeof box.invalid === 'boolean'));
9
+ }
10
+ /**
11
+ * Fails closed on a middle the row cannot draw: neither a `center` nor a
12
+ * `score`, or both; a `centerKind` with a `score` (it styles `center`); a
13
+ * score on a link or button row (its boxes would sit inside the press
14
+ * target); a score without a group label, two boxes with string values
15
+ * and labels, an `onChange` and an `onSubmit`, or with an `onCancel` or
16
+ * `error` of the wrong type.
17
+ */
18
+ export function assertFixtureMiddle({ center, centerKind, href, onPress, score, }) {
19
+ const name = 'FixtureRow';
20
+ if (score === undefined) {
21
+ if (isBlank(center))
22
+ throw new RangeError(`${name} center must be nonempty`);
23
+ return;
24
+ }
25
+ if (center !== undefined)
26
+ throw new RangeError(`${name} takes a center or a score, not both`);
27
+ if (centerKind !== undefined)
28
+ throw new RangeError(`${name} centerKind styles center: leave it out with score`);
29
+ if (href !== undefined || onPress !== undefined)
30
+ throw new RangeError(`${name} score boxes are their own controls: leave out href and onPress`);
31
+ if (isBlank(score.label) ||
32
+ !isBox(score.home) ||
33
+ !isBox(score.away) ||
34
+ typeof score.onChange !== 'function' ||
35
+ typeof score.onSubmit !== 'function' ||
36
+ (score.onCancel !== undefined && typeof score.onCancel !== 'function') ||
37
+ (score.error !== undefined && typeof score.error !== 'string'))
38
+ throw new RangeError(`${name} score needs a label, home and away boxes (value and label), onChange and onSubmit`);
39
+ }
@@ -1,10 +1,10 @@
1
1
  import { type FixtureRowProps } from './types.js';
2
2
  /**
3
3
  * Fails closed on a row it cannot name or draw: a side without a name, a
4
- * blank centre, an unknown centre kind or breakpoint, a status with blank
5
- * words or an unknown tone, a flag with blank words or an unknown tone, a
6
- * blank address or `aria-label`, a `select` without a label, a boolean
7
- * `checked` or an `onCheckedChange`, or an
8
- * `aria-label` on a row that is neither a link nor a button.
4
+ * middle `assertFixtureMiddle` refuses, an unknown centre kind or
5
+ * breakpoint, a status with blank words or an unknown tone, a flag with
6
+ * blank words or an unknown tone, a blank address or `aria-label`, a
7
+ * `select` without a label, a boolean `checked` or an `onCheckedChange`,
8
+ * or an `aria-label` on a row that is neither a link nor a button.
9
9
  */
10
- export declare function assertFixtureRow({ away, center, centerKind, flag, home, href, metaHideBelow, onPress, select, status, 'aria-label': ariaLabel, }: FixtureRowProps): void;
10
+ export declare function assertFixtureRow(props: FixtureRowProps): void;
@@ -2,23 +2,27 @@ import { visibilityBreakpoints } from '../../css/breakpoints.js';
2
2
  import { badgeTones } from '../../css/css-data.js';
3
3
  import { fixtureCenterKinds, fixtureFlagTones } from '../../css/css-fixture.js';
4
4
  import { isOneOf } from '../../is-one-of.js';
5
+ import { assertFixtureMiddle } from './validate-score.js';
5
6
  function isBlank(words) {
6
7
  return typeof words !== 'string' || !words.trim();
7
8
  }
8
9
  /**
9
10
  * Fails closed on a row it cannot name or draw: a side without a name, a
10
- * blank centre, an unknown centre kind or breakpoint, a status with blank
11
- * words or an unknown tone, a flag with blank words or an unknown tone, a
12
- * blank address or `aria-label`, a `select` without a label, a boolean
13
- * `checked` or an `onCheckedChange`, or an
14
- * `aria-label` on a row that is neither a link nor a button.
11
+ * middle `assertFixtureMiddle` refuses, an unknown centre kind or
12
+ * breakpoint, a status with blank words or an unknown tone, a flag with
13
+ * blank words or an unknown tone, a blank address or `aria-label`, a
14
+ * `select` without a label, a boolean `checked` or an `onCheckedChange`,
15
+ * or an `aria-label` on a row that is neither a link nor a button.
15
16
  */
16
- export function assertFixtureRow({ away, center, centerKind, flag, home, href, metaHideBelow, onPress, select, status, 'aria-label': ariaLabel, }) {
17
+ export function assertFixtureRow(props) {
18
+ assertFixtureParts(props);
19
+ assertFixtureMiddle(props);
20
+ }
21
+ /** Everything but the middle: the sides, status, flag, select and names. */
22
+ function assertFixtureParts({ away, centerKind, flag, home, href, metaHideBelow, onPress, select, status, 'aria-label': ariaLabel, }) {
17
23
  const name = 'FixtureRow';
18
24
  if (isBlank(home?.name) || isBlank(away?.name))
19
25
  throw new RangeError(`${name} home and away need nonempty names`);
20
- if (isBlank(center))
21
- throw new RangeError(`${name} center must be nonempty`);
22
26
  if (centerKind !== undefined && !isOneOf(fixtureCenterKinds, centerKind))
23
27
  throw new RangeError(`${name} centerKind must be one of ${fixtureCenterKinds.join(', ')}`);
24
28
  if (metaHideBelow !== undefined &&
@@ -1,6 +1,6 @@
1
1
  import { type HTMLAttributes } from 'react';
2
- import { type TableRowVariant } from '../../css/css-table-ledger.js';
3
- export type { TableRowVariant };
2
+ import { type TableRowRule, type TableRowVariant } from '../../css/css-table-ledger.js';
3
+ export type { TableRowRule, TableRowVariant };
4
4
  export type TableRowProps = HTMLAttributes<HTMLTableRowElement> & {
5
5
  selected?: boolean;
6
6
  /**
@@ -21,6 +21,14 @@ export type TableRowProps = HTMLAttributes<HTMLTableRowElement> & {
21
21
  * flex item, a direct child of `Inline`, is). Works with either variant.
22
22
  */
23
23
  struck?: boolean;
24
+ /**
25
+ * `after` draws a rule under the row, 2 px in the muted grey across
26
+ * every cell, such as the cut under the last playoff place. It is not
27
+ * colour-coded and is not read aloud: say what it means in words beside
28
+ * the table ("The line under 4th place marks the playoff places").
29
+ * Works with any variant.
30
+ */
31
+ rule?: TableRowRule;
24
32
  /**
25
33
  * Makes the whole row a press target for its one action, such as a
26
34
  * search result that is picked: the row's first link or button (a
@@ -57,6 +65,14 @@ export declare const TableRow: import("react").ForwardRefExoticComponent<HTMLAtt
57
65
  * flex item, a direct child of `Inline`, is). Works with either variant.
58
66
  */
59
67
  struck?: boolean;
68
+ /**
69
+ * `after` draws a rule under the row, 2 px in the muted grey across
70
+ * every cell, such as the cut under the last playoff place. It is not
71
+ * colour-coded and is not read aloud: say what it means in words beside
72
+ * the table ("The line under 4th place marks the playoff places").
73
+ * Works with any variant.
74
+ */
75
+ rule?: TableRowRule;
60
76
  /**
61
77
  * Makes the whole row a press target for its one action, such as a
62
78
  * search result that is picked: the row's first link or button (a
@@ -1,10 +1,11 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef, useRef, } from 'react';
3
3
  import { cx } from '../../class-names.js';
4
- import { assertTableRowVariant, } from '../../css/css-table-ledger.js';
4
+ import { assertTableRowRule, assertTableRowVariant, } from '../../css/css-table-ledger.js';
5
5
  import { rowPressAction } from './row-press.js';
6
- export const TableRow = forwardRef(function TableRow({ className, onClick, pressable = false, selected = false, struck = false, variant = 'default', ...rest }, ref) {
6
+ export const TableRow = forwardRef(function TableRow({ className, onClick, pressable = false, rule, selected = false, struck = false, variant = 'default', ...rest }, ref) {
7
7
  assertTableRowVariant(variant);
8
+ assertTableRowRule(rule);
8
9
  // The click the row hands its action bubbles back to the row; it is
9
10
  // the action's, so neither the row nor a consumer onClick takes it.
10
11
  const forwarding = useRef(false);
@@ -25,5 +26,5 @@ export const TableRow = forwardRef(function TableRow({ className, onClick, press
25
26
  forwarding.current = false;
26
27
  }
27
28
  };
28
- return (_jsx("tr", { ref: ref, "aria-selected": selected || undefined, className: cx(selected && 'sw-table-row-selected', variant === 'sub' && 'sw-table-row-sub', variant === 'total' && 'sw-table-row-total', struck && 'sw-table-row-struck', pressable && 'sw-table-row-press', className), onClick: pressable || onClick ? press : undefined, ...rest }));
29
+ return (_jsx("tr", { ref: ref, "aria-selected": selected || undefined, className: cx(selected && 'sw-table-row-selected', variant === 'sub' && 'sw-table-row-sub', variant === 'total' && 'sw-table-row-total', struck && 'sw-table-row-struck', rule === 'after' && 'sw-table-row-rule-after', pressable && 'sw-table-row-press', className), onClick: pressable || onClick ? press : undefined, ...rest }));
29
30
  });
@@ -0,0 +1,2 @@
1
+ export declare function cssFixtureDetailClasses(): string;
2
+ export declare function fixtureDetailClassCatalog(): string[];
@@ -0,0 +1,44 @@
1
+ /*
2
+ * Parts under the row (a score's message, then `detail`) sit in the
3
+ * item's next grid rows, spanning the run's columns from the home side's
4
+ * start (after a select's box) to the action's end, outside the row's
5
+ * press area. Their inline padding is the row's, so they line up with
6
+ * the home side's start; `detail` keeps the row's padding below it.
7
+ */
8
+ function detailRules() {
9
+ return `.sw-fixture-item-below > .sw-fixture-row {
10
+ grid-row: 1;
11
+ }
12
+
13
+ .sw-fixture-detail {
14
+ grid-column: 2 / -1;
15
+ grid-row: 3;
16
+ min-width: 0;
17
+ padding: 0 var(--sw-space-2) var(--sw-space-2);
18
+ }`;
19
+ }
20
+ /*
21
+ * Without subgrid each item is a block (or a flex row with a select or an
22
+ * action): the parts under the row wrap onto lines of their own.
23
+ */
24
+ function detailFallbackRules() {
25
+ return `@supports not (grid-template-columns: subgrid) {
26
+ .sw-fixture-item-below {
27
+ flex-wrap: wrap;
28
+ }
29
+
30
+ .sw-fixture-item-below > :is(.sw-fixture-score-error, .sw-fixture-detail) {
31
+ flex: 1 0 100%;
32
+ order: 1;
33
+ }
34
+ }`;
35
+ }
36
+ /* The parts under a FixtureRow (see components/FixtureRow.tsx); after the row rules. */
37
+ export function cssFixtureDetailClasses() {
38
+ return `${detailRules()}
39
+
40
+ ${detailFallbackRules()}`;
41
+ }
42
+ export function fixtureDetailClassCatalog() {
43
+ return ['sw-fixture-item-below', 'sw-fixture-detail'];
44
+ }
@@ -0,0 +1 @@
1
+ export declare function cssFixtureListRules(): string;
@@ -0,0 +1,50 @@
1
+ import { typographyVariants } from '@scalewing/tokens';
2
+ const caption = typographyVariants.caption;
3
+ /*
4
+ * The run: one grid whose columns are the select, the home side, the
5
+ * centre, the away side, the end (details and status), the chevron and the
6
+ * action. Each item is a subgrid of it, and its row a subgrid of every
7
+ * column but the select's and the action's, so every row's centre, end and
8
+ * action line up down the run. The space between the columns is the
9
+ * centre's, the end's, the chevron's, the select's and the action's own
10
+ * margin, so a run with none of them keeps no gap.
11
+ */
12
+ export function cssFixtureListRules() {
13
+ return `.sw-fixture-group {
14
+ display: flex;
15
+ flex-direction: column;
16
+ }
17
+
18
+ .sw-fixture-group-title {
19
+ color: var(--sw-color-muted);
20
+ font-family: var(--sw-font-sans);
21
+ font-size: ${caption.fontSize}px;
22
+ font-weight: 600;
23
+ letter-spacing: ${caption.letterSpacing}px;
24
+ line-height: ${caption.lineHeight}px;
25
+ padding-block: var(--sw-space-2) var(--sw-space-1);
26
+ }
27
+
28
+ .sw-fixture-list {
29
+ display: grid;
30
+ grid-template-columns: auto minmax(0, 1fr) auto minmax(0, 1fr) auto auto auto;
31
+ list-style: none;
32
+ margin: 0;
33
+ padding: 0;
34
+ }
35
+
36
+ .sw-fixture-item,
37
+ .sw-fixture-row {
38
+ display: grid;
39
+ grid-column: 1 / -1;
40
+ grid-template-columns: subgrid;
41
+ }
42
+
43
+ .sw-fixture-row {
44
+ grid-column: 2 / -2;
45
+ }
46
+
47
+ .sw-fixture-item {
48
+ border-top: 1px solid var(--sw-color-border);
49
+ }`;
50
+ }
@@ -0,0 +1,2 @@
1
+ export declare function cssFixtureScoreClasses(): string;
2
+ export declare function fixtureScoreClassCatalog(): string[];
@@ -0,0 +1,97 @@
1
+ import { typographyVariants } from '@scalewing/tokens';
2
+ import { breakpointQuery } from './breakpoints.js';
3
+ import { coarsePointerQuery, touchTarget } from './touch-target.js';
4
+ const body = typographyVariants.body;
5
+ const caption = typographyVariants.caption;
6
+ const belowMd = breakpointQuery('below', 'md');
7
+ /*
8
+ * A box is a text input, so the canvas already draws it as a control
9
+ * (`[data-theme] :is(input[type='text'], …)`: the surface, the hairline,
10
+ * the focus ring and the disabled look). `input.sw-fixture-score-box`
11
+ * matches that rule's specificity and comes later in the sheet, so the
12
+ * box's own size and figures win. It is a square the sm control height
13
+ * (32 px) on a fine pointer, so the row keeps its 44 px height, and the
14
+ * 44 px touch target on a coarse one; two tabular digits at weight 700
15
+ * fit it centred.
16
+ */
17
+ const box = '[data-theme] input.sw-fixture-score-box';
18
+ function boxRules() {
19
+ return `.sw-fixture-score {
20
+ align-items: center;
21
+ display: inline-flex;
22
+ gap: var(--sw-space-1);
23
+ }
24
+
25
+ ${box} {
26
+ block-size: var(--sw-control-sm-min-height);
27
+ font-size: ${body.fontSize}px;
28
+ font-variant-numeric: tabular-nums;
29
+ font-weight: 700;
30
+ inline-size: var(--sw-control-sm-min-height);
31
+ line-height: ${body.lineHeight}px;
32
+ min-height: 0;
33
+ padding: 0;
34
+ text-align: center;
35
+ }
36
+
37
+ @media ${coarsePointerQuery} {
38
+ ${box} {
39
+ block-size: ${touchTarget};
40
+ inline-size: ${touchTarget};
41
+ }
42
+ }
43
+
44
+ /* A phone's run is narrow: the boxes keep a space-2 margin, not the centre's space-3, so the names keep room. */
45
+ @media ${belowMd} {
46
+ .sw-fixture-score { margin-inline: var(--sw-space-2); }
47
+ }
48
+
49
+ [data-theme] input.sw-fixture-score-box[aria-invalid='true'] {
50
+ border-color: var(--sw-color-danger);
51
+ }
52
+
53
+ .sw-fixture-score-dash {
54
+ color: var(--sw-color-muted);
55
+ }`;
56
+ }
57
+ /*
58
+ * The score's message, under the row in the run's columns from the home
59
+ * side's start to the action's end, in caption type and the danger ink.
60
+ * Empty, it takes no room, but stays in place as the live region.
61
+ */
62
+ function errorRules() {
63
+ return `.sw-fixture-score-error {
64
+ color: var(--sw-color-danger);
65
+ display: block;
66
+ font-family: var(--sw-font-sans);
67
+ font-size: ${caption.fontSize}px;
68
+ grid-column: 2 / -1;
69
+ grid-row: 2;
70
+ letter-spacing: ${caption.letterSpacing}px;
71
+ line-height: ${caption.lineHeight}px;
72
+ padding-inline: var(--sw-space-2);
73
+ }
74
+
75
+ .sw-fixture-score-error:not(:empty) {
76
+ padding-block-end: var(--sw-space-2);
77
+ }
78
+
79
+ @media (forced-colors: active) {
80
+ [data-theme] input.sw-fixture-score-box[aria-invalid='true'] { border-color: Mark; }
81
+ .sw-fixture-score-dash { color: CanvasText; }
82
+ }`;
83
+ }
84
+ /* FixtureRow's score (see components/fixture/FixtureScoreBoxes.tsx); after the row rules. */
85
+ export function cssFixtureScoreClasses() {
86
+ return `${boxRules()}
87
+
88
+ ${errorRules()}`;
89
+ }
90
+ export function fixtureScoreClassCatalog() {
91
+ return [
92
+ 'sw-fixture-score',
93
+ 'sw-fixture-score-box',
94
+ 'sw-fixture-score-dash',
95
+ 'sw-fixture-score-error',
96
+ ];
97
+ }
@@ -3,7 +3,10 @@ import { breakpointQuery } from './breakpoints.js';
3
3
  import { chevronStrokeIn } from './chevron.js';
4
4
  import { quietFill } from './quiet-fill.js';
5
5
  import { cssFixtureActionClasses, fixtureActionClassCatalog, } from './css-fixture-action.js';
6
+ import { cssFixtureListRules } from './css-fixture-list.js';
7
+ import { cssFixtureDetailClasses, fixtureDetailClassCatalog, } from './css-fixture-detail.js';
6
8
  import { cssFixtureFlagClasses, fixtureFlagClassCatalog, } from './css-fixture-flag.js';
9
+ import { cssFixtureScoreClasses, fixtureScoreClassCatalog, } from './css-fixture-score.js';
7
10
  import { cssFixtureSelectClasses, fixtureSelectClassCatalog, } from './css-fixture-select.js';
8
11
  import { touchTarget } from './touch-target.js';
9
12
  const caption = typographyVariants.caption;
@@ -11,54 +14,6 @@ const label = typographyVariants.label;
11
14
  export const fixtureCenterKinds = ['time', 'score'];
12
15
  export { fixtureFlagTones } from './css-fixture-flag.js';
13
16
  const belowMd = breakpointQuery('below', 'md');
14
- /*
15
- * The run: one grid whose columns are the select, the home side, the
16
- * centre, the away side, the end (details and status), the chevron and the
17
- * action. Each item is a subgrid of it, and its row a subgrid of every
18
- * column but the select's and the action's, so every row's centre, end and
19
- * action line up down the run. The space between the columns is the
20
- * centre's, the end's, the chevron's, the select's and the action's own
21
- * margin, so a run with none of them keeps no gap.
22
- */
23
- function listRules() {
24
- return `.sw-fixture-group {
25
- display: flex;
26
- flex-direction: column;
27
- }
28
-
29
- .sw-fixture-group-title {
30
- color: var(--sw-color-muted);
31
- font-family: var(--sw-font-sans);
32
- font-size: ${caption.fontSize}px;
33
- font-weight: 600;
34
- letter-spacing: ${caption.letterSpacing}px;
35
- line-height: ${caption.lineHeight}px;
36
- padding-block: var(--sw-space-2) var(--sw-space-1);
37
- }
38
-
39
- .sw-fixture-list {
40
- display: grid;
41
- grid-template-columns: auto minmax(0, 1fr) auto minmax(0, 1fr) auto auto auto;
42
- list-style: none;
43
- margin: 0;
44
- padding: 0;
45
- }
46
-
47
- .sw-fixture-item,
48
- .sw-fixture-row {
49
- display: grid;
50
- grid-column: 1 / -1;
51
- grid-template-columns: subgrid;
52
- }
53
-
54
- .sw-fixture-row {
55
- grid-column: 2 / -2;
56
- }
57
-
58
- .sw-fixture-item {
59
- border-top: 1px solid var(--sw-color-border);
60
- }`;
61
- }
62
17
  /*
63
18
  * A row: the md control height (44 px), its parts centred on one line a
64
19
  * space-3 apart; a link or button row is the whole target, the text
@@ -243,7 +198,7 @@ function fallbackRules() {
243
198
  }
244
199
  /* FixtureGroup and FixtureRow (see components/FixtureRow.tsx). */
245
200
  export function cssFixtureClasses() {
246
- return `${listRules()}
201
+ return `${cssFixtureListRules()}
247
202
 
248
203
  ${rowRules()}
249
204
 
@@ -255,7 +210,11 @@ ${cssFixtureFlagClasses()}
255
210
 
256
211
  ${cssFixtureSelectClasses()}
257
212
 
258
- ${cssFixtureActionClasses()}`;
213
+ ${cssFixtureActionClasses()}
214
+
215
+ ${cssFixtureScoreClasses()}
216
+
217
+ ${cssFixtureDetailClasses()}`;
259
218
  }
260
219
  export function fixtureClassCatalog() {
261
220
  return [
@@ -277,5 +236,7 @@ export function fixtureClassCatalog() {
277
236
  ...fixtureFlagClassCatalog(),
278
237
  ...fixtureSelectClassCatalog(),
279
238
  ...fixtureActionClassCatalog(),
239
+ ...fixtureScoreClassCatalog(),
240
+ ...fixtureDetailClassCatalog(),
280
241
  ];
281
242
  }
@@ -7,7 +7,11 @@ export type TableCellTone = (typeof tableCellTones)[number];
7
7
  */
8
8
  export declare const tableRowVariants: readonly ["default", "sub", "total"];
9
9
  export type TableRowVariant = (typeof tableRowVariants)[number];
10
+ /** A rule a row draws: `after`, a line under it, such as a cut. */
11
+ export declare const tableRowRules: readonly ["after"];
12
+ export type TableRowRule = (typeof tableRowRules)[number];
10
13
  export declare function assertTableCellTone(tone: unknown): void;
11
14
  export declare function assertTableRowVariant(variant: unknown): void;
15
+ export declare function assertTableRowRule(rule: unknown): void;
12
16
  export declare function cssTableLedgerClasses(): string;
13
17
  export declare function tableLedgerClassCatalog(): string[];
@@ -5,6 +5,8 @@ export const tableCellTones = ['success', 'danger', 'muted', 'accent'];
5
5
  * or a total of the rows above it, mid-table.
6
6
  */
7
7
  export const tableRowVariants = ['default', 'sub', 'total'];
8
+ /** A rule a row draws: `after`, a line under it, such as a cut. */
9
+ export const tableRowRules = ['after'];
8
10
  export function assertTableCellTone(tone) {
9
11
  if (tone !== undefined && !tableCellTones.includes(tone))
10
12
  throw new RangeError(`Unknown TableCell tone: ${String(tone)}`);
@@ -13,6 +15,10 @@ export function assertTableRowVariant(variant) {
13
15
  if (!tableRowVariants.includes(variant))
14
16
  throw new RangeError(`Unknown TableRow variant: ${String(variant)}`);
15
17
  }
18
+ export function assertTableRowRule(rule) {
19
+ if (rule !== undefined && !tableRowRules.includes(rule))
20
+ throw new RangeError(`Unknown TableRow rule: ${String(rule)}`);
21
+ }
16
22
  function toneRules() {
17
23
  return tableCellTones
18
24
  .map((tone) => `.sw-table .sw-table-tone-${tone} {
@@ -0,0 +1,2 @@
1
+ export declare function cssTableRowRuleClasses(): string;
2
+ export declare function tableRowRuleClassCatalog(): string[];
@@ -0,0 +1,22 @@
1
+ /*
2
+ * A rule under a row (`TableRow rule="after"`), such as the cut under the
3
+ * last playoff place: its cells' bottom border 2 px in the muted grey, a
4
+ * line that reads stronger than the 1 px hairlines between rows (3:1 on
5
+ * the surface, as a line that carries meaning needs) without taking a
6
+ * colour's meaning. The collapsed-border model resolves it over the next
7
+ * row's top, and a border, so forced colours keep it (in `CanvasText`) and
8
+ * right-to-left needs nothing. The row's words and fill are unchanged; say
9
+ * what the line means in words beside the table.
10
+ */
11
+ export function cssTableRowRuleClasses() {
12
+ return `.sw-table .sw-table-row-rule-after > :is(th, td) {
13
+ border-bottom: 2px solid var(--sw-color-muted);
14
+ }
15
+
16
+ @media (forced-colors: active) {
17
+ .sw-table .sw-table-row-rule-after > :is(th, td) { border-bottom-color: CanvasText; }
18
+ }`;
19
+ }
20
+ export function tableRowRuleClassCatalog() {
21
+ return ['sw-table-row-rule-after'];
22
+ }
@@ -6,6 +6,7 @@ import { cssTableLayoutClasses, tableLayoutClassCatalog, } from './css-table-lay
6
6
  import { cssTableLedgerClasses, tableLedgerClassCatalog, } from './css-table-ledger.js';
7
7
  import { cssTableFooterClasses, tableFooterClassCatalog, } from './css-table-footer.js';
8
8
  import { cssTablePagePinClasses, tablePagePinClassCatalog, } from './css-table-page-pin.js';
9
+ import { cssTableRowRuleClasses, tableRowRuleClassCatalog, } from './css-table-row-rule.js';
9
10
  import { cssTableTotalRowClasses, tableTotalRowClassCatalog, } from './css-table-total-row.js';
10
11
  import { cssTableSelectClasses, tableSelectClassCatalog, } from './css-table-select.js';
11
12
  import { cssTablePressClasses, tablePressClassCatalog, } from './css-table-press.js';
@@ -96,6 +97,8 @@ ${cssTableLedgerClasses()}
96
97
 
97
98
  ${cssTableTotalRowClasses()}
98
99
 
100
+ ${cssTableRowRuleClasses()}
101
+
99
102
  ${cssTableFooterClasses()}
100
103
 
101
104
  ${cssTablePagePinClasses()}
@@ -122,6 +125,7 @@ export function tableClassCatalog() {
122
125
  ...tableSortClassCatalog(),
123
126
  ...tableLedgerClassCatalog(),
124
127
  ...tableTotalRowClassCatalog(),
128
+ ...tableRowRuleClassCatalog(),
125
129
  ...tableFooterClassCatalog(),
126
130
  ...tablePagePinClassCatalog(),
127
131
  ...tableBoundedClassCatalog(),
package/dist/index.d.ts CHANGED
@@ -59,7 +59,7 @@ export type { SplitProps } from './components/Split.js';
59
59
  export { Nav, navAppearances, type NavAppearance, type NavProps, } from './components/Nav.js';
60
60
  export { NavCount, navCountTones, type NavCountProps, type NavCountTone, } from './components/NavCount.js';
61
61
  export { FixtureGroup, type FixtureGroupProps, } from './components/FixtureGroup.js';
62
- export { FixtureRow, type FixtureRowProps, type FixtureFlag, type FixtureSelect, type FixtureSide, type FixtureStatus, } from './components/FixtureRow.js';
62
+ export { FixtureRow, type FixtureRowProps, type FixtureFlag, type FixtureScore, type FixtureScoreBox, type FixtureScoreSide, type FixtureSelect, type FixtureSide, type FixtureStatus, } from './components/FixtureRow.js';
63
63
  export { defaultScheduleGridLabels, ScheduleGrid, type ScheduleGridAxisEntry, type ScheduleGridCarryMode, type ScheduleGridDisabledCell, type ScheduleGridDropCheck, type ScheduleGridItem, type ScheduleGridLabels, type ScheduleGridProps, type ScheduleGridSlot, type ScheduleGridTarget, } from './components/ScheduleGrid.js';
64
64
  export { DateToggleCalendar, type DateToggleCalendarProps, } from './components/DateToggleCalendar.js';
65
65
  export { CommandPalette, commandPaletteShortcuts, type CommandPaletteGroup, type CommandPaletteItem, type CommandPaletteProps, type CommandPaletteShortcut, } from './components/CommandPalette.js';
@@ -73,7 +73,7 @@ export { Select } from './components/Select.js';
73
73
  export type { SelectAction, SelectOption, SelectProps, SelectWidth, } from './components/Select.js';
74
74
  export { MultiSelect, type MultiSelectAlign, type MultiSelectOption, type MultiSelectProps, } from './components/MultiSelect.js';
75
75
  export { Stack, type Align, type Justify, type StackProps, } from './components/Stack.js';
76
- export { Table, TableBody, TableCell, TableFooter, TableHeader, TableRow, TableSelectCell, tableCellTones, tableRowVariants, type TableBodyProps, type TableCellProps, type TableCellTone, type TableColumnWidth, type TableDensity, type TableFooterProps, type TableHeaderProps, type TableLayout, type TableMaxHeight, type TableProps, type TableRowProps, type TableRowVariant, type TableSelectCellProps, type TableSort, type TableStickyHeader, type TableVerticalAlign, } from './components/Table.js';
76
+ export { Table, TableBody, TableCell, TableFooter, TableHeader, TableRow, TableSelectCell, tableCellTones, tableRowRules, tableRowVariants, type TableBodyProps, type TableCellProps, type TableCellTone, type TableColumnWidth, type TableDensity, type TableFooterProps, type TableHeaderProps, type TableLayout, type TableMaxHeight, type TableProps, type TableRowProps, type TableRowRule, type TableRowVariant, type TableSelectCellProps, type TableSort, type TableStickyHeader, type TableVerticalAlign, } from './components/Table.js';
77
77
  export { Text, type TextAlign, type TextElement, type TextProps, } from './components/Text.js';
78
78
  export { ThemeProvider, useTheme, type ThemeProviderProps, } from './theme/ThemeProvider.js';
79
79
  export { themeCanvases, type ThemeCanvas } from './css/css-canvas-grouped.js';
package/dist/index.js CHANGED
@@ -69,7 +69,7 @@ export { Combobox, comboboxSizes, } from './components/Combobox.js';
69
69
  export { Select } from './components/Select.js';
70
70
  export { MultiSelect, } from './components/MultiSelect.js';
71
71
  export { Stack, } from './components/Stack.js';
72
- export { Table, TableBody, TableCell, TableFooter, TableHeader, TableRow, TableSelectCell, tableCellTones, tableRowVariants, } from './components/Table.js';
72
+ export { Table, TableBody, TableCell, TableFooter, TableHeader, TableRow, TableSelectCell, tableCellTones, tableRowRules, tableRowVariants, } from './components/Table.js';
73
73
  export { Text, } from './components/Text.js';
74
74
  export { ThemeProvider, useTheme, } from './theme/ThemeProvider.js';
75
75
  export { themeCanvases } from './css/css-canvas-grouped.js';
package/dist/styles.css CHANGED
@@ -2021,6 +2021,14 @@ body {
2021
2021
  .sw-table .sw-table-row-total > :is(th, td) { border-top-color: CanvasText; }
2022
2022
  }
2023
2023
 
2024
+ .sw-table .sw-table-row-rule-after > :is(th, td) {
2025
+ border-bottom: 2px solid var(--sw-color-muted);
2026
+ }
2027
+
2028
+ @media (forced-colors: active) {
2029
+ .sw-table .sw-table-row-rule-after > :is(th, td) { border-bottom-color: CanvasText; }
2030
+ }
2031
+
2024
2032
  .sw-table tfoot :is(th, td) {
2025
2033
  font-weight: 600;
2026
2034
  position: relative;
@@ -3605,6 +3613,87 @@ button.sw-date-toggle-day[aria-disabled='true'] {
3605
3613
  }
3606
3614
  }
3607
3615
 
3616
+ .sw-fixture-score {
3617
+ align-items: center;
3618
+ display: inline-flex;
3619
+ gap: var(--sw-space-1);
3620
+ }
3621
+
3622
+ [data-theme] input.sw-fixture-score-box {
3623
+ block-size: var(--sw-control-sm-min-height);
3624
+ font-size: 17px;
3625
+ font-variant-numeric: tabular-nums;
3626
+ font-weight: 700;
3627
+ inline-size: var(--sw-control-sm-min-height);
3628
+ line-height: 25px;
3629
+ min-height: 0;
3630
+ padding: 0;
3631
+ text-align: center;
3632
+ }
3633
+
3634
+ @media (pointer: coarse) {
3635
+ [data-theme] input.sw-fixture-score-box {
3636
+ block-size: var(--sw-control-md-min-height);
3637
+ inline-size: var(--sw-control-md-min-height);
3638
+ }
3639
+ }
3640
+
3641
+ /* A phone's run is narrow: the boxes keep a space-2 margin, not the centre's space-3, so the names keep room. */
3642
+ @media not all and (min-width: 48rem) {
3643
+ .sw-fixture-score { margin-inline: var(--sw-space-2); }
3644
+ }
3645
+
3646
+ [data-theme] input.sw-fixture-score-box[aria-invalid='true'] {
3647
+ border-color: var(--sw-color-danger);
3648
+ }
3649
+
3650
+ .sw-fixture-score-dash {
3651
+ color: var(--sw-color-muted);
3652
+ }
3653
+
3654
+ .sw-fixture-score-error {
3655
+ color: var(--sw-color-danger);
3656
+ display: block;
3657
+ font-family: var(--sw-font-sans);
3658
+ font-size: 13px;
3659
+ grid-column: 2 / -1;
3660
+ grid-row: 2;
3661
+ letter-spacing: -0.08px;
3662
+ line-height: 18px;
3663
+ padding-inline: var(--sw-space-2);
3664
+ }
3665
+
3666
+ .sw-fixture-score-error:not(:empty) {
3667
+ padding-block-end: var(--sw-space-2);
3668
+ }
3669
+
3670
+ @media (forced-colors: active) {
3671
+ [data-theme] input.sw-fixture-score-box[aria-invalid='true'] { border-color: Mark; }
3672
+ .sw-fixture-score-dash { color: CanvasText; }
3673
+ }
3674
+
3675
+ .sw-fixture-item-below > .sw-fixture-row {
3676
+ grid-row: 1;
3677
+ }
3678
+
3679
+ .sw-fixture-detail {
3680
+ grid-column: 2 / -1;
3681
+ grid-row: 3;
3682
+ min-width: 0;
3683
+ padding: 0 var(--sw-space-2) var(--sw-space-2);
3684
+ }
3685
+
3686
+ @supports not (grid-template-columns: subgrid) {
3687
+ .sw-fixture-item-below {
3688
+ flex-wrap: wrap;
3689
+ }
3690
+
3691
+ .sw-fixture-item-below > :is(.sw-fixture-score-error, .sw-fixture-detail) {
3692
+ flex: 1 0 100%;
3693
+ order: 1;
3694
+ }
3695
+ }
3696
+
3608
3697
  .sw-schedule-grid {
3609
3698
  max-width: 100%;
3610
3699
  overflow-x: auto;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalewing/react",
3
- "version": "1.56.0",
3
+ "version": "1.57.0",
4
4
  "description": "Scalewing DOM components and package-owned CSS utilities.",
5
5
  "type": "module",
6
6
  "license": "MIT",