@scalewing/react 1.61.0 → 1.62.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.62.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 1b9b367: `FixtureCard`'s enabled score boxes take the page's background, so an empty box stays visible on a tinted dark `Card` (FutMas F-010). `FixtureRow`'s boxes are unchanged.
8
+
9
+ ## 1.62.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 4585080: `FixtureCard`, one pairing as a small vertical scorecard for inside a `Card` (FutMas F-010, Scores redesign): an optional top line (`meta`, `menu`), the two sides with `note`s and a score box at each one's end, the score's polite error line, then `actions` and `detail`. It takes `FixtureRow`'s `FixtureScore` and the same boxes, needs no `FixtureGroup`, and draws no edge or fill. New exports: `FixtureCard`, `FixtureCardProps`, `FixtureCardSide`. New classes: `sw-fixture-card` and `sw-fixture-card-*`. Purely additive: `FixtureRow`'s output and CSS are unchanged.
14
+
3
15
  ## 1.61.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -47,6 +47,7 @@ import {
47
47
  DescriptionList,
48
48
  DescriptionItem,
49
49
  EmptyState,
50
+ FixtureCard,
50
51
  FixtureGroup,
51
52
  FixtureRow,
52
53
  ScheduleGrid,
@@ -1296,6 +1297,43 @@ When the real screenshot exists, a `PreviewImage` (`src`, `width`, `height`) hol
1296
1297
  </FixtureGroup>
1297
1298
  ```
1298
1299
 
1300
+ `FixtureCard` is one pairing as a small vertical scorecard, for a narrow column where `FixtureRow`'s boxes in the row's middle cannot fit. It draws no edge, fill or radius: it sits inside a `Card`, and you separate several with your own divider. It needs no `FixtureGroup`. `home` and `away` (`{ name, note? }`) are stacked, each name in body at weight 500 with its `note` under it in muted caption, and that side's score box at the end, the boxes in one column. Names wrap and never truncate. `score` is `FixtureRow`'s `FixtureScore`, with the same boxes and keys (digits only, selected on focus, Enter calls `onSubmit`, Escape calls `onCancel`, 32 px on a fine pointer, 44 px on a coarse one) and the same polite `error` line, here under the sides; the root is a group named by `score.label`. `meta` (caption size in the muted ink; a coloured `Text` inside it keeps its colour) and `menu` make a top line, drawn only with one of them; `actions` is a wrapping row under the sides and `detail` is under the actions with no type of its own, so give it a `Text`. A blank name or score label throws a `RangeError`.
1301
+
1302
+ ```tsx
1303
+ <Card padding={4}>
1304
+ <Stack gap={3}>
1305
+ <FixtureCard
1306
+ meta="Sun, Oct 18"
1307
+ menu={
1308
+ <ActionMenu
1309
+ label="More for Heron Marsh v Otter Pool"
1310
+ items={items}
1311
+ trigger={<Dots />}
1312
+ triggerAppearance="icon"
1313
+ />
1314
+ }
1315
+ home={{ name: 'Heron Marsh', note: 'North hide' }}
1316
+ away={{ name: 'Otter Pool' }}
1317
+ score={{
1318
+ label: 'Count, Heron Marsh v Otter Pool',
1319
+ home: { value: tally.home, label: 'Heron Marsh count' },
1320
+ away: { value: tally.away, label: 'Otter Pool count' },
1321
+ onChange: change,
1322
+ onSubmit: record,
1323
+ onCancel: reset,
1324
+ }}
1325
+ actions={
1326
+ <Button size="sm" variant="neutral" onPress={record}>
1327
+ Record
1328
+ </Button>
1329
+ }
1330
+ />
1331
+ <Separator decorative />
1332
+ {/* the next FixtureCard */}
1333
+ </Stack>
1334
+ </Card>
1335
+ ```
1336
+
1299
1337
  Muted words on a row's quiet fill: a pressable `FixtureRow` (and a detail under one with an `action` or `select`, which the fill spans), a pressable `ListRow` (hovered or pressed) and a `TableRow pressable` (hovered, pressed or with its action focused) show the quiet grey fill, on which the muted colour falls under 4.5:1 in most palettes (4.14:1 in `ink` light, 3.47:1 in `harvest` dark). While the fill shows, the parts on it read the muted token as the text colour, which keeps 4.5:1 there in every palette and scheme: the details, the meta, a muted `TableCell`, the chevron, and a consumer's `Text color="muted"` inside the row, with no prop and no change at rest. Toned words (a value's or a cell's success, danger or accent, a danger title, a Badge) keep their tone. Forced colours replace both colours and outline the hovered row in `Highlight`, as before.
1300
1338
 
1301
1339
  `FixtureRow` `flag` (opt-in, `FixtureFlag`: `{ label, tone? }`) marks an issue at the row's start, such as a clash: a round mark a space-4 across with a "!", in the tone's tint and ink (`warning`, amber, the default, for something that needs the person; `danger`, red), its edge keeping 3:1 on the surface in every palette and the text colour in forced colors. The mark is `aria-hidden` with `label` as its `title` tooltip, and `label` is read first in the row from a visually hidden span ("Can't-play clash Heron Marsh 8:00 Otter Pool"); with `aria-label` it is put before your words with a comma ("Can't-play clash, Heron Marsh vs Otter Pool, 8:00"), so do not repeat it there. From `md` the mark sits on the first line at the row's start, the home side kept clear of it; below `md` it leads the second line, before the details, so it never squeezes the home name. A row with nothing on that line below `md` (no `status`, and no `meta` or one hidden below `md` by `metaHideBelow`) keeps the mark inline on its first line instead, as from `md` (class `sw-fixture-row-flag-inline`), rather than gaining a line that holds only the flag. A blank `label` or an unknown `tone` throws a `RangeError`. `action` (opt-in) is a control after the row, such as a small neutral `Button` ("Move", named in your words: `aria-label="Move Heron Marsh vs Otter Pool"`): it is the row's sibling in its list item, outside a link or button row's press area and after it in the Tab order, a press on it never opens the row, and it stays centred on the row's height in the run's last column at every width (from `md` the row's one line, below `md` both lines), so every row's action lines up. Where `:has` is supported the hover fill spans the row and its action. An empty slot draws nothing. The run has a column for it at the end, and one for `select` at the start (`sw-fixture-list` is `auto minmax(0, 1fr) auto minmax(0, 1fr) auto auto auto`, six below `md`); a row spans every column but those two (`grid-column: 2 / -2`), and an `auto` column no row uses takes no space. New classes `sw-fixture-row-flagged`, `sw-fixture-flag`, `sw-fixture-flag-mark`, `sw-fixture-flag-warning`, `sw-fixture-flag-danger`, `sw-fixture-item-action` and `sw-fixture-action`.
@@ -0,0 +1,11 @@
1
+ import { type FixtureCardProps } from './fixture/types.js';
2
+ export type { FixtureCardProps, FixtureCardSide } from './fixture/types.js';
3
+ /**
4
+ * One fixture as a small vertical scorecard, for inside a `Card`: an
5
+ * optional top line, the two sides each with its score box at the end (the
6
+ * boxes in one column), the score's message, then `actions` and `detail`.
7
+ * It draws no edge or fill of its own, and the consumer separates several
8
+ * with its own divider. The boxes are FixtureRow's. The root is a group
9
+ * named by `score.label`. Invalid words or props throw a `RangeError`.
10
+ */
11
+ export declare function FixtureCard(props: FixtureCardProps): import("react").JSX.Element;
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useId } from 'react';
4
+ import { FixtureScoreError } from './fixture/FixtureScoreBoxes.js';
5
+ import { ScoreBox } from './fixture/ScoreBox.js';
6
+ import { assertFixtureCard } from './fixture/validate-card.js';
7
+ import { hasSlot } from './slot.js';
8
+ /** One side's row: its name and note, then its score box at the end. */
9
+ function CardSide({ errorId, place, score, side, }) {
10
+ return (_jsxs("div", { className: "sw-fixture-card-side", children: [_jsxs("div", { className: "sw-fixture-card-team", children: [_jsx("div", { className: "sw-fixture-card-name", children: side.name }), hasSlot(side.note) ? (_jsx("div", { className: "sw-fixture-card-note", children: side.note })) : null] }), _jsx(ScoreBox, { errorId: errorId, score: score, side: place })] }));
11
+ }
12
+ /**
13
+ * One fixture as a small vertical scorecard, for inside a `Card`: an
14
+ * optional top line, the two sides each with its score box at the end (the
15
+ * boxes in one column), the score's message, then `actions` and `detail`.
16
+ * It draws no edge or fill of its own, and the consumer separates several
17
+ * with its own divider. The boxes are FixtureRow's. The root is a group
18
+ * named by `score.label`. Invalid words or props throw a `RangeError`.
19
+ */
20
+ export function FixtureCard(props) {
21
+ assertFixtureCard(props);
22
+ const errorId = useId();
23
+ const { actions, away, detail, home, menu, meta, score } = props;
24
+ const hasMeta = hasSlot(meta);
25
+ const hasMenu = hasSlot(menu);
26
+ return (_jsxs("div", { "aria-label": score.label, className: "sw-fixture-card", role: "group", children: [hasMeta || hasMenu ? (_jsxs("div", { className: "sw-fixture-card-top", children: [hasMeta ? _jsx("div", { className: "sw-fixture-card-meta", children: meta }) : null, hasMenu ? _jsx("div", { className: "sw-fixture-card-menu", children: menu }) : null] })) : null, _jsx(CardSide, { errorId: errorId, place: "home", score: score, side: home }), _jsx(CardSide, { errorId: errorId, place: "away", score: score, side: away }), _jsx(FixtureScoreError, { className: "sw-fixture-card-error", error: score.error, id: errorId }), hasSlot(actions) ? (_jsx("div", { className: "sw-fixture-card-actions", children: actions })) : null, hasSlot(detail) ? (_jsx("div", { className: "sw-fixture-card-detail", children: detail })) : null] }));
27
+ }
@@ -1,19 +1,19 @@
1
1
  import { type FixtureScore } from './types.js';
2
2
  /**
3
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.
4
+ * the away box, in a group named by `label`.
7
5
  */
8
6
  export declare function FixtureScoreBoxes({ errorId, score, }: {
9
7
  errorId: string;
10
8
  score: FixtureScore;
11
9
  }): import("react").JSX.Element;
12
10
  /**
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.
11
+ * The score's message: a polite live region, present (and empty, taking no
12
+ * room) while there is no error, so a new one is said. `className` is where
13
+ * it is drawn: under a FixtureRow by default.
15
14
  */
16
- export declare function FixtureScoreError({ error, id, }: {
15
+ export declare function FixtureScoreError({ className, error, id, }: {
16
+ className?: string;
17
17
  error: string | undefined;
18
18
  id: string;
19
19
  }): import("react").JSX.Element;
@@ -1,57 +1,19 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
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
- }
4
+ import { ScoreBox } from './ScoreBox.js';
35
5
  /**
36
6
  * 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.
7
+ * the away box, in a group named by `label`.
40
8
  */
41
9
  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')] }));
10
+ return (_jsxs("span", { "aria-label": score.label, className: cx('sw-fixture-center', 'sw-fixture-score'), role: "group", children: [_jsx(ScoreBox, { errorId: errorId, score: score, side: "home" }), _jsx("span", { "aria-hidden": "true", className: "sw-fixture-score-dash", children: "\u2013" }), _jsx(ScoreBox, { errorId: errorId, score: score, side: "away" })] }));
50
11
  }
51
12
  /**
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.
13
+ * The score's message: a polite live region, present (and empty, taking no
14
+ * room) while there is no error, so a new one is said. `className` is where
15
+ * it is drawn: under a FixtureRow by default.
54
16
  */
55
- export function FixtureScoreError({ error, id, }) {
56
- return (_jsx("span", { "aria-live": "polite", className: "sw-fixture-score-error", id: id, children: error || null }));
17
+ export function FixtureScoreError({ className = 'sw-fixture-score-error', error, id, }) {
18
+ return (_jsx("span", { "aria-live": "polite", className: className, id: id, children: error || null }));
57
19
  }
@@ -0,0 +1,19 @@
1
+ import { type KeyboardEvent } from 'react';
2
+ import { type FixtureScore, type FixtureScoreSide } from './types.js';
3
+ /**
4
+ * Enter records and Escape puts the boxes back. A key that ends an IME
5
+ * composition is the composition's, not the row's.
6
+ */
7
+ export declare function scoreKeyDown(score: FixtureScore): (event: KeyboardEvent<HTMLInputElement>) => void;
8
+ /**
9
+ * One side's score box, wherever a fixture draws it: a text input named by
10
+ * the box's `label`, digits only (non-digits are dropped before `onChange`),
11
+ * its text selected on focus so a typed score replaces it. It is described
12
+ * by the score's error while there is one, and marked by its own `invalid`,
13
+ * else by any error.
14
+ */
15
+ export declare function ScoreBox({ errorId, score, side, }: {
16
+ errorId: string;
17
+ score: FixtureScore;
18
+ side: FixtureScoreSide;
19
+ }): import("react").JSX.Element;
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { scoreBoxMaxDigits, scoreDigits } from './score-digits.js';
4
+ /**
5
+ * Enter records and Escape puts the boxes back. A key that ends an IME
6
+ * composition is the composition's, not the row's.
7
+ */
8
+ export function scoreKeyDown(score) {
9
+ return (event) => {
10
+ if (event.nativeEvent.isComposing)
11
+ return;
12
+ if (event.key === 'Enter') {
13
+ event.preventDefault();
14
+ score.onSubmit();
15
+ }
16
+ else if (event.key === 'Escape' && score.onCancel) {
17
+ // Kept from an enclosing dialog, whose Escape would close it.
18
+ event.preventDefault();
19
+ score.onCancel();
20
+ }
21
+ };
22
+ }
23
+ /**
24
+ * One side's score box, wherever a fixture draws it: a text input named by
25
+ * the box's `label`, digits only (non-digits are dropped before `onChange`),
26
+ * its text selected on focus so a typed score replaces it. It is described
27
+ * by the score's error while there is one, and marked by its own `invalid`,
28
+ * else by any error.
29
+ */
30
+ export function ScoreBox({ errorId, score, side, }) {
31
+ const { invalid, label, ref, value } = score[side];
32
+ const hasError = Boolean(score.error);
33
+ const marked = invalid ?? hasError;
34
+ return (_jsx("input", { ref: ref, "aria-describedby": hasError ? errorId : undefined, "aria-invalid": marked || undefined, "aria-label": label, autoComplete: "off", className: "sw-fixture-score-box", disabled: score.disabled ?? false, inputMode: "numeric", maxLength: scoreBoxMaxDigits, onChange: (event) => {
35
+ const digits = scoreDigits(event.currentTarget.value);
36
+ if (digits !== value)
37
+ score.onChange(side, digits);
38
+ }, onFocus: (event) => event.currentTarget.select(), onKeyDown: scoreKeyDown(score), pattern: "[0-9]*", type: "text", value: value }));
39
+ }
@@ -138,6 +138,32 @@ export type FixtureRowProps = {
138
138
  */
139
139
  'aria-label'?: string;
140
140
  };
141
+ /** One side of a FixtureCard. */
142
+ export type FixtureCardSide = {
143
+ name: string;
144
+ /** Muted words under the name, such as a side's form or a walkover. */
145
+ note?: ReactNode;
146
+ };
147
+ export type FixtureCardProps = {
148
+ home: FixtureCardSide;
149
+ away: FixtureCardSide;
150
+ /**
151
+ * The score boxes, one at each side's end. `label` names the card's group;
152
+ * `error` is said under the sides.
153
+ */
154
+ score: FixtureScore;
155
+ /**
156
+ * The start of the top line, such as the day: caption size in the muted
157
+ * ink. A coloured `Text` inside it keeps its own colour.
158
+ */
159
+ meta?: ReactNode;
160
+ /** The end of the top line, such as an icon `ActionMenu`. */
161
+ menu?: ReactNode;
162
+ /** Under the sides in a wrapping row, such as small neutral `Button`s. */
163
+ actions?: ReactNode;
164
+ /** Under the actions, such as a save message: no type of its own, so give it a `Text`. */
165
+ detail?: ReactNode;
166
+ };
141
167
  export type FixtureGroupProps = {
142
168
  /** The run's caption, such as a date and a venue; it names the list. */
143
169
  title: string;
@@ -0,0 +1,3 @@
1
+ import { type FixtureCardProps } from './types.js';
2
+ /** Fails closed on a card it cannot name: a side without a name, or a score `assertFixtureScore` refuses. */
3
+ export declare function assertFixtureCard({ away, home, score, }: FixtureCardProps): void;
@@ -0,0 +1,8 @@
1
+ import { assertFixtureScore, isBlank } from './validate-score.js';
2
+ /** Fails closed on a card it cannot name: a side without a name, or a score `assertFixtureScore` refuses. */
3
+ export function assertFixtureCard({ away, home, score, }) {
4
+ const name = 'FixtureCard';
5
+ if (isBlank(home?.name) || isBlank(away?.name))
6
+ throw new RangeError(`${name} home and away need nonempty names`);
7
+ assertFixtureScore(name, score);
8
+ }
@@ -1,10 +1,15 @@
1
- import { type FixtureRowProps } from './types.js';
1
+ import { type FixtureRowProps, type FixtureScore } from './types.js';
2
+ export declare function isBlank(words: unknown): boolean;
3
+ /**
4
+ * Fails closed on a score without a group label, two boxes with string
5
+ * values and labels, an `onChange` and an `onSubmit`, or with an `onCancel`
6
+ * or `error` of the wrong type. `name` is the component that was given it.
7
+ */
8
+ export declare function assertFixtureScore(name: string, score: FixtureScore | undefined): void;
2
9
  /**
3
10
  * Fails closed on a middle the row cannot draw: neither a `center` nor a
4
11
  * `score`, or both; a `centerKind` with a `score` (it styles `center`); a
5
12
  * 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.
13
+ * target); or a score `assertFixtureScore` refuses.
9
14
  */
10
15
  export declare function assertFixtureMiddle({ center, centerKind, href, onPress, score, }: FixtureRowProps): void;
@@ -1,4 +1,4 @@
1
- function isBlank(words) {
1
+ export function isBlank(words) {
2
2
  return typeof words !== 'string' || !words.trim();
3
3
  }
4
4
  function isBox(box) {
@@ -7,13 +7,26 @@ function isBox(box) {
7
7
  !isBlank(box.label) &&
8
8
  (box.invalid === undefined || typeof box.invalid === 'boolean'));
9
9
  }
10
+ /**
11
+ * Fails closed on a score without a group label, two boxes with string
12
+ * values and labels, an `onChange` and an `onSubmit`, or with an `onCancel`
13
+ * or `error` of the wrong type. `name` is the component that was given it.
14
+ */
15
+ export function assertFixtureScore(name, score) {
16
+ if (isBlank(score?.label) ||
17
+ !isBox(score?.home) ||
18
+ !isBox(score?.away) ||
19
+ typeof score?.onChange !== 'function' ||
20
+ typeof score.onSubmit !== 'function' ||
21
+ (score.onCancel !== undefined && typeof score.onCancel !== 'function') ||
22
+ (score.error !== undefined && typeof score.error !== 'string'))
23
+ throw new RangeError(`${name} score needs a label, home and away boxes (value and label), onChange and onSubmit`);
24
+ }
10
25
  /**
11
26
  * Fails closed on a middle the row cannot draw: neither a `center` nor a
12
27
  * `score`, or both; a `centerKind` with a `score` (it styles `center`); a
13
28
  * 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.
29
+ * target); or a score `assertFixtureScore` refuses.
17
30
  */
18
31
  export function assertFixtureMiddle({ center, centerKind, href, onPress, score, }) {
19
32
  const name = 'FixtureRow';
@@ -28,12 +41,5 @@ export function assertFixtureMiddle({ center, centerKind, href, onPress, score,
28
41
  throw new RangeError(`${name} centerKind styles center: leave it out with score`);
29
42
  if (href !== undefined || onPress !== undefined)
30
43
  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`);
44
+ assertFixtureScore(name, score);
39
45
  }
@@ -2,10 +2,7 @@ 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';
6
- function isBlank(words) {
7
- return typeof words !== 'string' || !words.trim();
8
- }
5
+ import { assertFixtureMiddle, isBlank } from './validate-score.js';
9
6
  /**
10
7
  * Fails closed on a row it cannot name or draw: a side without a name, a
11
8
  * middle `assertFixtureMiddle` refuses, an unknown centre kind or
@@ -23,6 +23,7 @@ const compactRoot = "[data-theme][data-density='compact']";
23
23
  */
24
24
  const statedBodyControls = [
25
25
  '.sw-date-field-input',
26
+ '.sw-fixture-card-name',
26
27
  '.sw-fixture-score-box',
27
28
  '.sw-segmented-filled .sw-segmented-item',
28
29
  '.sw-stepper-input',
@@ -0,0 +1,2 @@
1
+ export declare function cssFixtureCardClasses(): string;
2
+ export declare function fixtureCardClassCatalog(): string[];
@@ -0,0 +1,131 @@
1
+ import { typographyVariants } from '@scalewing/tokens';
2
+ const body = typographyVariants.body;
3
+ const caption = typographyVariants.caption;
4
+ /*
5
+ * FixtureCard draws no edge, fill or radius: it sits inside a Card. The two
6
+ * side rows share one shape, a name column and an `auto` box column, so
7
+ * their boxes (FixtureRow's `sw-fixture-score-box`, 32 px on a fine pointer
8
+ * and 44 px on a coarse one, and FixtureRow's forced-colours rules with it)
9
+ * line up at the end. Names wrap and never truncate.
10
+ */
11
+ function cardRules() {
12
+ return `.sw-fixture-card {
13
+ color: var(--sw-color-text);
14
+ font-family: var(--sw-font-sans);
15
+ min-width: 0;
16
+ }
17
+
18
+ .sw-fixture-card-top {
19
+ align-items: center;
20
+ display: flex;
21
+ gap: var(--sw-space-2);
22
+ margin-block-end: var(--sw-space-2);
23
+ }
24
+
25
+ .sw-fixture-card-meta {
26
+ color: var(--sw-color-muted);
27
+ font-size: ${caption.fontSize}px;
28
+ letter-spacing: ${caption.letterSpacing}px;
29
+ line-height: ${caption.lineHeight}px;
30
+ min-width: 0;
31
+ }
32
+
33
+ .sw-fixture-card-menu {
34
+ flex: none;
35
+ margin-inline-start: auto;
36
+ }
37
+
38
+ .sw-fixture-card-side {
39
+ align-items: center;
40
+ column-gap: var(--sw-space-3);
41
+ display: grid;
42
+ grid-template-columns: minmax(0, 1fr) auto;
43
+ }
44
+
45
+ .sw-fixture-card-side + .sw-fixture-card-side {
46
+ margin-block-start: var(--sw-space-2);
47
+ }
48
+
49
+ .sw-fixture-card-team {
50
+ min-width: 0;
51
+ }
52
+
53
+ .sw-fixture-card-name {
54
+ font-size: ${body.fontSize}px;
55
+ font-weight: 500;
56
+ letter-spacing: ${body.letterSpacing}px;
57
+ line-height: ${body.lineHeight}px;
58
+ overflow-wrap: anywhere;
59
+ }
60
+
61
+ .sw-fixture-card-note {
62
+ color: var(--sw-color-muted);
63
+ font-size: ${caption.fontSize}px;
64
+ letter-spacing: ${caption.letterSpacing}px;
65
+ line-height: ${caption.lineHeight}px;
66
+ overflow-wrap: anywhere;
67
+ }`;
68
+ }
69
+ /*
70
+ * A box on the Card's surface would share its fill in a tinted dark palette
71
+ * and nearly vanish while empty, so it takes the page's. A disabled box keeps
72
+ * the canvas's disabled look.
73
+ */
74
+ function boxRules() {
75
+ return `[data-theme] .sw-fixture-card input.sw-fixture-score-box:not(:disabled) {
76
+ background: var(--sw-color-background);
77
+ }`;
78
+ }
79
+ /*
80
+ * The score's message is a live region, so it stays in place while empty; a
81
+ * block with no words has no height, and its margin comes only with words.
82
+ */
83
+ function belowRules() {
84
+ return `.sw-fixture-card-error {
85
+ color: var(--sw-color-danger);
86
+ display: block;
87
+ font-size: ${caption.fontSize}px;
88
+ letter-spacing: ${caption.letterSpacing}px;
89
+ line-height: ${caption.lineHeight}px;
90
+ }
91
+
92
+ .sw-fixture-card-error:not(:empty) {
93
+ margin-block-start: var(--sw-space-2);
94
+ }
95
+
96
+ .sw-fixture-card-actions {
97
+ align-items: center;
98
+ display: flex;
99
+ flex-wrap: wrap;
100
+ gap: var(--sw-space-2);
101
+ margin-block-start: var(--sw-space-3);
102
+ }
103
+
104
+ .sw-fixture-card-detail {
105
+ margin-block-start: var(--sw-space-2);
106
+ min-width: 0;
107
+ }`;
108
+ }
109
+ /* FixtureCard (see components/FixtureCard.tsx); after the score's rules. */
110
+ export function cssFixtureCardClasses() {
111
+ return `${cardRules()}
112
+
113
+ ${boxRules()}
114
+
115
+ ${belowRules()}`;
116
+ }
117
+ export function fixtureCardClassCatalog() {
118
+ return [
119
+ 'sw-fixture-card',
120
+ 'sw-fixture-card-top',
121
+ 'sw-fixture-card-meta',
122
+ 'sw-fixture-card-menu',
123
+ 'sw-fixture-card-side',
124
+ 'sw-fixture-card-team',
125
+ 'sw-fixture-card-name',
126
+ 'sw-fixture-card-note',
127
+ 'sw-fixture-card-error',
128
+ 'sw-fixture-card-actions',
129
+ 'sw-fixture-card-detail',
130
+ ];
131
+ }
@@ -4,6 +4,7 @@ import { chevronStrokeIn } from './chevron.js';
4
4
  import { mutedReadsAsText, quietFill } from './quiet-fill.js';
5
5
  import { cssFixtureActionClasses, fixtureActionClassCatalog, } from './css-fixture-action.js';
6
6
  import { cssFixtureListRules, cssFixtureListSidesRules, } from './css-fixture-list.js';
7
+ import { cssFixtureCardClasses, fixtureCardClassCatalog, } from './css-fixture-card.js';
7
8
  import { cssFixtureDetailClasses, fixtureDetailClassCatalog, } from './css-fixture-detail.js';
8
9
  import { cssFixtureFlagClasses, fixtureFlagClassCatalog, } from './css-fixture-flag.js';
9
10
  import { cssFixtureScoreClasses, fixtureScoreClassCatalog, } from './css-fixture-score.js';
@@ -219,6 +220,8 @@ ${cssFixtureActionClasses()}
219
220
 
220
221
  ${cssFixtureScoreClasses()}
221
222
 
223
+ ${cssFixtureCardClasses()}
224
+
222
225
  ${cssFixtureDetailClasses()}`;
223
226
  }
224
227
  export function fixtureClassCatalog() {
@@ -243,6 +246,7 @@ export function fixtureClassCatalog() {
243
246
  ...fixtureSelectClassCatalog(),
244
247
  ...fixtureActionClassCatalog(),
245
248
  ...fixtureScoreClassCatalog(),
249
+ ...fixtureCardClassCatalog(),
246
250
  ...fixtureDetailClassCatalog(),
247
251
  ];
248
252
  }
package/dist/index.d.ts CHANGED
@@ -59,6 +59,7 @@ export { Split } from './components/Split.js';
59
59
  export type { SplitProps } from './components/Split.js';
60
60
  export { Nav, navAppearances, type NavAppearance, type NavProps, } from './components/Nav.js';
61
61
  export { NavCount, navCountTones, type NavCountProps, type NavCountTone, } from './components/NavCount.js';
62
+ export { FixtureCard, type FixtureCardProps, type FixtureCardSide, } from './components/FixtureCard.js';
62
63
  export { FixtureGroup, fixtureGroupSides, type FixtureGroupProps, type FixtureGroupSides, } from './components/FixtureGroup.js';
63
64
  export { FixtureRow, type FixtureRowProps, type FixtureFlag, type FixtureScore, type FixtureScoreBox, type FixtureScoreSide, type FixtureSelect, type FixtureSide, type FixtureStatus, } from './components/FixtureRow.js';
64
65
  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';
package/dist/index.js CHANGED
@@ -56,6 +56,7 @@ export { Inline } from './components/Inline.js';
56
56
  export { Split } from './components/Split.js';
57
57
  export { Nav, navAppearances, } from './components/Nav.js';
58
58
  export { NavCount, navCountTones, } from './components/NavCount.js';
59
+ export { FixtureCard, } from './components/FixtureCard.js';
59
60
  export { FixtureGroup, fixtureGroupSides, } from './components/FixtureGroup.js';
60
61
  export { FixtureRow, } from './components/FixtureRow.js';
61
62
  export { defaultScheduleGridLabels, ScheduleGrid, } from './components/ScheduleGrid.js';
package/dist/styles.css CHANGED
@@ -3738,6 +3738,92 @@ button.sw-date-toggle-day[aria-disabled='true'] {
3738
3738
  .sw-fixture-score-dash { color: CanvasText; }
3739
3739
  }
3740
3740
 
3741
+ .sw-fixture-card {
3742
+ color: var(--sw-color-text);
3743
+ font-family: var(--sw-font-sans);
3744
+ min-width: 0;
3745
+ }
3746
+
3747
+ .sw-fixture-card-top {
3748
+ align-items: center;
3749
+ display: flex;
3750
+ gap: var(--sw-space-2);
3751
+ margin-block-end: var(--sw-space-2);
3752
+ }
3753
+
3754
+ .sw-fixture-card-meta {
3755
+ color: var(--sw-color-muted);
3756
+ font-size: 13px;
3757
+ letter-spacing: -0.08px;
3758
+ line-height: 18px;
3759
+ min-width: 0;
3760
+ }
3761
+
3762
+ .sw-fixture-card-menu {
3763
+ flex: none;
3764
+ margin-inline-start: auto;
3765
+ }
3766
+
3767
+ .sw-fixture-card-side {
3768
+ align-items: center;
3769
+ column-gap: var(--sw-space-3);
3770
+ display: grid;
3771
+ grid-template-columns: minmax(0, 1fr) auto;
3772
+ }
3773
+
3774
+ .sw-fixture-card-side + .sw-fixture-card-side {
3775
+ margin-block-start: var(--sw-space-2);
3776
+ }
3777
+
3778
+ .sw-fixture-card-team {
3779
+ min-width: 0;
3780
+ }
3781
+
3782
+ .sw-fixture-card-name {
3783
+ font-size: 17px;
3784
+ font-weight: 500;
3785
+ letter-spacing: -0.2px;
3786
+ line-height: 25px;
3787
+ overflow-wrap: anywhere;
3788
+ }
3789
+
3790
+ .sw-fixture-card-note {
3791
+ color: var(--sw-color-muted);
3792
+ font-size: 13px;
3793
+ letter-spacing: -0.08px;
3794
+ line-height: 18px;
3795
+ overflow-wrap: anywhere;
3796
+ }
3797
+
3798
+ [data-theme] .sw-fixture-card input.sw-fixture-score-box:not(:disabled) {
3799
+ background: var(--sw-color-background);
3800
+ }
3801
+
3802
+ .sw-fixture-card-error {
3803
+ color: var(--sw-color-danger);
3804
+ display: block;
3805
+ font-size: 13px;
3806
+ letter-spacing: -0.08px;
3807
+ line-height: 18px;
3808
+ }
3809
+
3810
+ .sw-fixture-card-error:not(:empty) {
3811
+ margin-block-start: var(--sw-space-2);
3812
+ }
3813
+
3814
+ .sw-fixture-card-actions {
3815
+ align-items: center;
3816
+ display: flex;
3817
+ flex-wrap: wrap;
3818
+ gap: var(--sw-space-2);
3819
+ margin-block-start: var(--sw-space-3);
3820
+ }
3821
+
3822
+ .sw-fixture-card-detail {
3823
+ margin-block-start: var(--sw-space-2);
3824
+ min-width: 0;
3825
+ }
3826
+
3741
3827
  .sw-fixture-item-below > .sw-fixture-row {
3742
3828
  grid-row: 1;
3743
3829
  }
@@ -10946,7 +11032,7 @@ html:has(.sw-site-header-sticky) {
10946
11032
  line-height: 22px;
10947
11033
  }
10948
11034
 
10949
- [data-theme][data-density='compact'] :is(.sw-date-field-input, .sw-fixture-score-box, .sw-segmented-filled .sw-segmented-item, .sw-stepper-input, .sw-stepper-unit) {
11035
+ [data-theme][data-density='compact'] :is(.sw-date-field-input, .sw-fixture-card-name, .sw-fixture-score-box, .sw-segmented-filled .sw-segmented-item, .sw-stepper-input, .sw-stepper-unit) {
10950
11036
  font-size: 15px;
10951
11037
  letter-spacing: -0.2px;
10952
11038
  line-height: 22px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalewing/react",
3
- "version": "1.61.0",
3
+ "version": "1.62.1",
4
4
  "description": "Scalewing DOM components and package-owned CSS utilities.",
5
5
  "type": "module",
6
6
  "license": "MIT",