@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
@@ -0,0 +1,82 @@
1
+ import { type SpacingStep } from '@scalewing/tokens';
2
+ import { type ReactNode } from 'react';
3
+ import { type FieldWidth } from '../../css/css-field-width.js';
4
+ import { type RequiredMark } from '../FieldLabelText.js';
5
+ import { type FieldDescriptionTone } from './FieldDescription.js';
6
+ import { type FieldVariant } from './field-checks.js';
7
+ export type FieldSize = 'xs' | 'md';
8
+ export type FieldProps = {
9
+ children: ReactNode;
10
+ gap?: SpacingStep;
11
+ label: string;
12
+ labelVisuallyHidden?: boolean;
13
+ size?: FieldSize;
14
+ description?: string;
15
+ /**
16
+ * `warning` draws `description` in the warning colour after a small alert
17
+ * glyph, for a hint that needs the person but is not an error, such as a
18
+ * value another row already has. It stays the control's description and
19
+ * sets no invalid state; `error` still replaces it. Default `muted`; an
20
+ * unknown tone throws a `RangeError`.
21
+ */
22
+ descriptionTone?: FieldDescriptionTone;
23
+ error?: string;
24
+ /**
25
+ * Marks the control invalid (`aria-invalid` and the danger border) without
26
+ * a message of its own, for a field whose error is shown elsewhere, such
27
+ * as one message under a group of fields. Point the control's
28
+ * `aria-describedby` at that message. `error` implies it. Passing
29
+ * `invalid`, even `false` but not `undefined`, requires one native control
30
+ * child from the first render.
31
+ */
32
+ invalid?: boolean;
33
+ /**
34
+ * Marks a value the person changed from a saved one, such as a field in a
35
+ * correction: the control's border (or an adorned frame's) in the accent
36
+ * color, thicker than the plain hairline, and its fill tinted with 12 % of
37
+ * the accent, so a changed field reads apart from the focused one (focus
38
+ * draws a ring and never tints). The value keeps 4.5:1 on the tint; while
39
+ * a placeholder shows, the fill stays plain. It is a cue, not the record:
40
+ * say what it was in `description` ("Was 25"). `invalid` and `error`
41
+ * win. Like `invalid`, passing it, even `false` but not `undefined`,
42
+ * requires one native control child from the first render.
43
+ */
44
+ changed?: boolean;
45
+ required?: boolean;
46
+ /**
47
+ * `hidden` leaves out the label's " *" while the control stays required
48
+ * (and announced so); `visible` (default) draws it.
49
+ * When hidden, say in visible text which fields must be filled; a
50
+ * placeholder alone is not enough.
51
+ */
52
+ requiredMark?: RequiredMark;
53
+ /** Short text inside the control frame before the value, such as a currency sign. Not part of the value. */
54
+ prefix?: string;
55
+ /** Short text inside the control frame after the value, such as a unit. Not part of the value. */
56
+ suffix?: string;
57
+ /**
58
+ * `outlined` (default) is the labelled form field. `search` is a search
59
+ * box: the label is not drawn (it stays the input's name), a `leading`
60
+ * glyph sits at the frame's start, the input is `type="search"`, and a
61
+ * clear button named `clearLabel` shows while the input holds text and
62
+ * is enabled; it calls `onClear` (set the value to `''`) and puts focus
63
+ * back in the input. Needs one native `input` child with a `value`.
64
+ */
65
+ variant?: FieldVariant;
66
+ /** A search field's glyph at its start, such as a magnifier; hidden from assistive technology. */
67
+ leading?: ReactNode;
68
+ /** A search field's clear button name, such as "Clear search". */
69
+ clearLabel?: string;
70
+ /** Called by a search field's clear button; the consumer empties the value. */
71
+ onClear?: () => void;
72
+ /**
73
+ * Caps the control's width at a Table column size (`xs` 4rem, `sm` 6rem,
74
+ * `md` 8rem, `lg` 12rem, `xl` 16rem), at the start of the column and
75
+ * never wider than it, so a short value such as a rate or a count does
76
+ * not stretch across a form. The label and messages keep the column's
77
+ * width. Left out, the control fills the column. It needs one native
78
+ * text input, select or textarea child (a `TypeError` otherwise); an
79
+ * unknown width throws a `RangeError`.
80
+ */
81
+ width?: FieldWidth;
82
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -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
  /**
@@ -82,6 +140,12 @@ export type FixtureRowProps = {
82
140
  export type FixtureGroupProps = {
83
141
  /** The run's caption, such as a date and a venue; it names the list. */
84
142
  title: string;
143
+ /**
144
+ * Keeps `title` as the list's name but does not draw it, for a run whose
145
+ * heading is already on screen, such as one row in a sheet titled with
146
+ * the game and its day. Default `false`.
147
+ */
148
+ titleHidden?: boolean;
85
149
  /** The run's FixtureRows. */
86
150
  children: ReactNode;
87
151
  };
@@ -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 cssFieldDescriptionClasses(): string;
2
+ export declare function fieldDescriptionClassCatalog(): string[];
@@ -0,0 +1,39 @@
1
+ import { spacingScale, typographyVariants } from '@scalewing/tokens';
2
+ const caption = typographyVariants.caption;
3
+ const glyphStep = 4;
4
+ /*
5
+ * A warning description (`descriptionTone="warning"`): the description's
6
+ * caption type in the warning colour (4.5:1 on the canvas in both schemes),
7
+ * led by a space-4 alert glyph centred on its first line, so the hint reads
8
+ * apart from a muted one without colour too. It never touches the control:
9
+ * no invalid border, no ARIA state. In forced colours the words and the
10
+ * glyph take CanvasText, apart from an error's Mark.
11
+ */
12
+ export function cssFieldDescriptionClasses() {
13
+ const glyphInset = (caption.lineHeight - spacingScale[glyphStep]) / 2;
14
+ return `.sw-field-description-warning {
15
+ align-items: flex-start;
16
+ color: var(--sw-color-warning);
17
+ display: flex;
18
+ gap: var(--sw-space-1);
19
+ }
20
+
21
+ .sw-field-description-glyph {
22
+ fill: none;
23
+ flex: none;
24
+ height: var(--sw-space-${glyphStep});
25
+ margin-block: ${glyphInset}px;
26
+ stroke: currentColor;
27
+ stroke-linecap: round;
28
+ stroke-linejoin: round;
29
+ stroke-width: 2;
30
+ width: var(--sw-space-${glyphStep});
31
+ }
32
+
33
+ @media (forced-colors: active) {
34
+ .sw-field-description-warning { color: CanvasText; }
35
+ }`;
36
+ }
37
+ export function fieldDescriptionClassCatalog() {
38
+ return ['sw-field-description-warning', 'sw-field-description-glyph'];
39
+ }
@@ -1,6 +1,7 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  import { controlSurface, disabledControlForcedColors, disabledControlSurface, } from './css-document.js';
3
3
  import { cssFieldChangedClasses } from './css-field-changed.js';
4
+ import { cssFieldDescriptionClasses, fieldDescriptionClassCatalog, } from './css-field-description.js';
4
5
  import { cssFieldWidthClasses, fieldWidthClassCatalog, } from './css-field-width.js';
5
6
  import { cssFieldSearchClasses, fieldSearchClassCatalog, } from './css-field-search.js';
6
7
  const caption = typographyVariants.caption;
@@ -76,6 +77,8 @@ export function cssFieldClasses() {
76
77
  .sw-field-description { color: var(--sw-color-muted); }
77
78
  .sw-field-error { color: var(--sw-color-danger); }
78
79
 
80
+ ${cssFieldDescriptionClasses()}
81
+
79
82
  /* An error region is always rendered as a live region; empty, it leaves the layout (and its gap) but stays in the accessibility tree. */
80
83
  .sw-field-error:empty { position: absolute; }
81
84
 
@@ -106,6 +109,7 @@ export function fieldClassCatalog() {
106
109
  'sw-field',
107
110
  'sw-field-required',
108
111
  'sw-field-description',
112
+ ...fieldDescriptionClassCatalog(),
109
113
  'sw-field-error',
110
114
  'sw-field-invalid',
111
115
  'sw-field-changed',
@@ -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;