@scalewing/react 1.55.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 (48) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +71 -4
  3. package/dist/components/DateToggleCalendar.d.ts +3 -1
  4. package/dist/components/DateToggleCalendar.js +4 -2
  5. package/dist/components/FixtureRow.d.ts +6 -4
  6. package/dist/components/FixtureRow.js +18 -20
  7. package/dist/components/ScheduleGrid.d.ts +3 -2
  8. package/dist/components/ScheduleGrid.js +21 -2
  9. package/dist/components/Table.d.ts +2 -2
  10. package/dist/components/Table.js +1 -1
  11. package/dist/components/date-toggle/types.d.ts +9 -2
  12. package/dist/components/fixture/FixtureScoreBoxes.d.ts +19 -0
  13. package/dist/components/fixture/FixtureScoreBoxes.js +57 -0
  14. package/dist/components/fixture/row-parts.d.ts +17 -0
  15. package/dist/components/fixture/row-parts.js +19 -0
  16. package/dist/components/fixture/score-digits.d.ts +7 -0
  17. package/dist/components/fixture/score-digits.js +9 -0
  18. package/dist/components/fixture/types.d.ts +80 -4
  19. package/dist/components/fixture/validate-score.d.ts +10 -0
  20. package/dist/components/fixture/validate-score.js +39 -0
  21. package/dist/components/fixture/validate.d.ts +6 -5
  22. package/dist/components/fixture/validate.js +17 -7
  23. package/dist/components/schedule-grid/labels.js +6 -1
  24. package/dist/components/schedule-grid/types.d.ts +10 -2
  25. package/dist/components/schedule-grid/use-schedule-carry.d.ts +5 -3
  26. package/dist/components/schedule-grid/use-schedule-carry.js +38 -16
  27. package/dist/components/table/TableRow.d.ts +18 -2
  28. package/dist/components/table/TableRow.js +4 -3
  29. package/dist/css/css-fixture-action.js +3 -3
  30. package/dist/css/css-fixture-detail.d.ts +2 -0
  31. package/dist/css/css-fixture-detail.js +44 -0
  32. package/dist/css/css-fixture-flag.js +12 -1
  33. package/dist/css/css-fixture-list.d.ts +1 -0
  34. package/dist/css/css-fixture-list.js +50 -0
  35. package/dist/css/css-fixture-score.d.ts +2 -0
  36. package/dist/css/css-fixture-score.js +97 -0
  37. package/dist/css/css-fixture-select.d.ts +2 -0
  38. package/dist/css/css-fixture-select.js +47 -0
  39. package/dist/css/css-fixture.js +16 -50
  40. package/dist/css/css-table-ledger.d.ts +4 -0
  41. package/dist/css/css-table-ledger.js +6 -0
  42. package/dist/css/css-table-row-rule.d.ts +2 -0
  43. package/dist/css/css-table-row-rule.js +22 -0
  44. package/dist/css/css-table.js +4 -0
  45. package/dist/index.d.ts +3 -3
  46. package/dist/index.js +1 -1
  47. package/dist/styles.css +125 -5
  48. package/package.json +1 -1
@@ -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';
@@ -23,14 +23,77 @@ export type FixtureFlag = {
23
23
  /** `warning` (default) is amber, for something that needs the person; `danger` is red. */
24
24
  tone?: FixtureFlagTone;
25
25
  };
26
+ /**
27
+ * A leading checkbox that chooses the row, such as games picked to
28
+ * postpone: its own control before the home side, outside the row's link
29
+ * or button. `label` names it, not drawn ("Select Heron Marsh vs Otter
30
+ * Pool").
31
+ */
32
+ export type FixtureSelect = {
33
+ label: string;
34
+ checked: boolean;
35
+ onCheckedChange: (checked: boolean) => void;
36
+ disabled?: boolean;
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
+ };
26
79
  export type FixtureRowProps = {
27
80
  home: FixtureSide;
28
81
  away: FixtureSide;
29
- /** What sits between the sides: a kickoff time ("8:00") or a score ("2–1"). */
30
- 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;
31
93
  /**
32
94
  * `time` (default) draws the centre as a small grey chip; `score` as
33
- * 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`.
34
97
  */
35
98
  centerKind?: FixtureCenterKind;
36
99
  /** Details at the row's end, such as the field; under the sides below `md`. */
@@ -46,6 +109,19 @@ export type FixtureRowProps = {
46
109
  * stop. Name it in your words ("Move Heron Marsh vs Otter Pool").
47
110
  */
48
111
  action?: ReactNode;
112
+ /**
113
+ * A leading checkbox before the row, outside its press area: the
114
+ * standard 24 px box, a 44 px target on a coarse pointer. Works with `flag`,
115
+ * `action`, `href` and `onPress`.
116
+ */
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;
49
125
  /** Makes the whole row a link to this address. */
50
126
  href?: string;
51
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,9 +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`, or an
7
- * `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.
8
9
  */
9
- export declare function assertFixtureRow({ away, center, centerKind, flag, home, href, metaHideBelow, onPress, status, 'aria-label': ariaLabel, }: FixtureRowProps): void;
10
+ export declare function assertFixtureRow(props: FixtureRowProps): void;
@@ -2,22 +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`, or an
13
- * `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.
14
16
  */
15
- export function assertFixtureRow({ away, center, centerKind, flag, home, href, metaHideBelow, onPress, 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, }) {
16
23
  const name = 'FixtureRow';
17
24
  if (isBlank(home?.name) || isBlank(away?.name))
18
25
  throw new RangeError(`${name} home and away need nonempty names`);
19
- if (isBlank(center))
20
- throw new RangeError(`${name} center must be nonempty`);
21
26
  if (centerKind !== undefined && !isOneOf(fixtureCenterKinds, centerKind))
22
27
  throw new RangeError(`${name} centerKind must be one of ${fixtureCenterKinds.join(', ')}`);
23
28
  if (metaHideBelow !== undefined &&
@@ -30,6 +35,11 @@ export function assertFixtureRow({ away, center, centerKind, flag, home, href, m
30
35
  (isBlank(flag?.label) ||
31
36
  (flag.tone !== undefined && !isOneOf(fixtureFlagTones, flag.tone))))
32
37
  throw new RangeError(`${name} flag needs a label and one of ${fixtureFlagTones.join(', ')}`);
38
+ if (select !== undefined &&
39
+ (isBlank(select?.label) ||
40
+ typeof select.checked !== 'boolean' ||
41
+ typeof select.onCheckedChange !== 'function'))
42
+ throw new RangeError(`${name} select needs a label, checked and onCheckedChange`);
33
43
  if (href !== undefined && isBlank(href))
34
44
  throw new RangeError(`${name} href must be nonempty when given`);
35
45
  if (ariaLabel !== undefined) {
@@ -1,10 +1,15 @@
1
+ const pickedUpNext = {
2
+ keyboard: 'Choose a cell with the arrow keys, then press Enter to drop it or Escape to cancel.',
3
+ tap: 'Press a cell to drop it there, or press outside the grid to cancel.',
4
+ drag: 'Release it on a cell to drop it there.',
5
+ };
1
6
  export const defaultScheduleGridLabels = {
2
7
  open: 'Open',
3
8
  movingFrom: 'Moving from here',
4
9
  releaseToMove: 'Release to move here',
5
10
  releaseToSwap: 'Release to swap',
6
11
  instructions: 'To move an item, press M or Space on it, choose a cell with the arrow keys, then press Enter to drop it or Escape to cancel. Dropping it on another item swaps the two.',
7
- pickedUp: (item) => `Picked up ${item}. Choose a cell with the arrow keys, then press Enter to drop it or Escape to cancel.`,
12
+ pickedUp: (item, mode) => `Picked up ${item}. ${pickedUpNext[mode] ?? pickedUpNext.keyboard}`,
8
13
  moved: (item, row, column) => `Moved ${item} to ${row}, ${column}.`,
9
14
  swapped: (item, other) => `Swapped ${item} with ${other}.`,
10
15
  cancelled: (item) => `Cancelled. ${item} stays where it was.`,
@@ -32,6 +32,11 @@ export type ScheduleGridTarget = ({
32
32
  export type ScheduleGridDropCheck = true | {
33
33
  reason: string;
34
34
  };
35
+ /**
36
+ * How an item was picked up: by keys, by a press then a press (touch or a
37
+ * click), or by a mouse or pen drag.
38
+ */
39
+ export type ScheduleGridCarryMode = 'keyboard' | 'tap' | 'drag';
35
40
  /** The grid's own words, English by default. */
36
41
  export type ScheduleGridLabels = {
37
42
  /** An empty cell. */
@@ -44,8 +49,11 @@ export type ScheduleGridLabels = {
44
49
  releaseToSwap: string;
45
50
  /** How to move an item with keys; the grid's description. */
46
51
  instructions: string;
47
- /** Announced when an item is picked up. */
48
- pickedUp: (item: string) => string;
52
+ /**
53
+ * Announced when an item is picked up; `mode` says how, so the words can
54
+ * fit the input (a one-argument function still works).
55
+ */
56
+ pickedUp: (item: string, mode: ScheduleGridCarryMode) => string;
49
57
  /** Announced after a move to an empty cell. */
50
58
  moved: (item: string, row: string, column: string) => string;
51
59
  /** Announced after a swap. */
@@ -1,7 +1,7 @@
1
1
  import { type GridModel, type GridPosition } from './grid-model.js';
2
- import { type ScheduleGridLabels, type ScheduleGridProps } from './types.js';
2
+ import { type ScheduleGridCarryMode, type ScheduleGridLabels, type ScheduleGridProps } from './types.js';
3
3
  /** How an item is being carried: by keys, by a press then a press, or by a drag. */
4
- export type CarryMode = 'keyboard' | 'tap' | 'drag';
4
+ export type CarryMode = ScheduleGridCarryMode;
5
5
  export type Carry = {
6
6
  itemId: string;
7
7
  mode: CarryMode;
@@ -19,7 +19,9 @@ type CarryOptions = Pick<ScheduleGridProps, 'canDrop' | 'columns' | 'disabled' |
19
19
  * The carried item and what picking up, dropping and cancelling do: the
20
20
  * consumer's `canDrop` decides each drop, `onMove` or `onSwap` reports it,
21
21
  * and each step is announced. A carry ends when its item leaves the grid
22
- * or the grid is disabled.
22
+ * or the grid is disabled. Drops and cancels read the carry from a ref set
23
+ * as it changes, so a release handled before React commits the pick-up
24
+ * still ends it.
23
25
  */
24
26
  export declare function useScheduleCarry({ canDrop, columns, disabled, labels, model, onMove, onSwap, rows, }: CarryOptions): {
25
27
  announcement: Announcement;
@@ -1,13 +1,20 @@
1
- import { useCallback, useState } from 'react';
1
+ import { useCallback, useLayoutEffect, useRef, useState } from 'react';
2
2
  import { cellStatus } from './grid-model.js';
3
3
  /**
4
4
  * The carried item and what picking up, dropping and cancelling do: the
5
5
  * consumer's `canDrop` decides each drop, `onMove` or `onSwap` reports it,
6
6
  * and each step is announced. A carry ends when its item leaves the grid
7
- * or the grid is disabled.
7
+ * or the grid is disabled. Drops and cancels read the carry from a ref set
8
+ * as it changes, so a release handled before React commits the pick-up
9
+ * still ends it.
8
10
  */
9
11
  export function useScheduleCarry({ canDrop, columns, disabled, labels, model, onMove, onSwap, rows, }) {
10
- const [held, setHeld] = useState(null);
12
+ const [held, setHeldState] = useState(null);
13
+ const heldRef = useRef(null);
14
+ const setHeld = useCallback((next) => {
15
+ heldRef.current = next;
16
+ setHeldState(next);
17
+ }, []);
11
18
  const [announcement, setAnnouncement] = useState({
12
19
  text: '',
13
20
  id: 0,
@@ -17,7 +24,19 @@ export function useScheduleCarry({ canDrop, columns, disabled, labels, model, on
17
24
  // stays ended: re-enabling never resumes it (state adjusted while
18
25
  // rendering, as React recommends over an effect).
19
26
  if (held !== null && carry === null)
20
- setHeld(null);
27
+ setHeldState(null);
28
+ useLayoutEffect(() => {
29
+ const live = heldRef.current;
30
+ if (live !== null && (disabled || !model.itemById(live.itemId)))
31
+ heldRef.current = null;
32
+ });
33
+ /** The live carry, current even before a render commits it. */
34
+ const current = useCallback(() => {
35
+ const live = heldRef.current;
36
+ return live !== null && !disabled && model.itemById(live.itemId)
37
+ ? live
38
+ : null;
39
+ }, [disabled, model]);
21
40
  const announce = useCallback((text) => {
22
41
  setAnnouncement((last) => ({ text, id: last.id + 1 }));
23
42
  }, []);
@@ -26,22 +45,24 @@ export function useScheduleCarry({ canDrop, columns, disabled, labels, model, on
26
45
  if (disabled || !item)
27
46
  return;
28
47
  setHeld({ itemId, mode });
29
- announce(labels.pickedUp(item.label));
30
- }, [announce, disabled, labels, model]);
48
+ announce(labels.pickedUp(item.label, mode));
49
+ }, [announce, disabled, labels, model, setHeld]);
31
50
  /** Puts the item back; returns where it is, for focus. */
32
51
  const cancel = useCallback(() => {
33
- if (!carry)
34
- return undefined;
52
+ const live = current();
35
53
  setHeld(null);
36
- announce(labels.cancelled(model.itemById(carry.itemId).label));
37
- return model.positionOf(carry.itemId);
38
- }, [announce, carry, labels, model]);
54
+ if (!live)
55
+ return undefined;
56
+ announce(labels.cancelled(model.itemById(live.itemId).label));
57
+ return model.positionOf(live.itemId);
58
+ }, [announce, current, labels, model, setHeld]);
39
59
  /** Drops on a cell: moves, swaps, cancels on its own cell, or says why not. */
40
60
  const drop = useCallback((position) => {
41
- if (!carry)
61
+ const live = current();
62
+ if (!live)
42
63
  return;
43
- const item = model.itemById(carry.itemId);
44
- const status = cellStatus(model, carry.itemId, position, canDrop);
64
+ const item = model.itemById(live.itemId);
65
+ const status = cellStatus(model, live.itemId, position, canDrop);
45
66
  if (status.kind === 'source') {
46
67
  cancel();
47
68
  return;
@@ -49,7 +70,7 @@ export function useScheduleCarry({ canDrop, columns, disabled, labels, model, on
49
70
  if (status.kind === 'blocked') {
50
71
  announce(status.reason);
51
72
  // A refused drag ends; keys and presses keep the item to try again.
52
- if (carry.mode === 'drag')
73
+ if (live.mode === 'drag')
53
74
  setHeld(null);
54
75
  return;
55
76
  }
@@ -66,13 +87,14 @@ export function useScheduleCarry({ canDrop, columns, disabled, labels, model, on
66
87
  announce,
67
88
  canDrop,
68
89
  cancel,
69
- carry,
70
90
  columns,
91
+ current,
71
92
  labels,
72
93
  model,
73
94
  onMove,
74
95
  onSwap,
75
96
  rows,
97
+ setHeld,
76
98
  ]);
77
99
  return { announcement, cancel, carry, drop, pickUp };
78
100
  }
@@ -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
  });
@@ -3,7 +3,7 @@ import { quietFill } from './quiet-fill.js';
3
3
  * The action sits after the row in its own column, centred on the row's
4
4
  * height, outside the row's press area, keeping the row's end padding
5
5
  * after it. Where :has() is supported the hover fill spans the whole item,
6
- * so the row and its action read as one line.
6
+ * so the row, its select and its action read as one line.
7
7
  */
8
8
  function actionRules() {
9
9
  return `.sw-fixture-action {
@@ -17,11 +17,11 @@ function actionRules() {
17
17
 
18
18
  @supports selector(:has(*)) {
19
19
  @media (hover: hover) {
20
- .sw-fixture-item-action:has(> .sw-fixture-row-pressable:hover) {
20
+ :is(.sw-fixture-item-action, .sw-fixture-item-select):has(> .sw-fixture-row-pressable:hover) {
21
21
  background: ${quietFill};
22
22
  }
23
23
 
24
- .sw-fixture-item-action > .sw-fixture-row-pressable:hover {
24
+ :is(.sw-fixture-item-action, .sw-fixture-item-select) > .sw-fixture-row-pressable:hover {
25
25
  background: transparent;
26
26
  }
27
27
  }
@@ -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
+ }
@@ -53,7 +53,11 @@ ${fixtureFlagTones
53
53
  .sw-fixture-flag .sw-fixture-flag-mark { color: CanvasText; }
54
54
  }`;
55
55
  }
56
- /* Below `md` the flag leads the details' line; after the row's narrow rules. */
56
+ /*
57
+ * Below `md` the flag leads the details' line; after the row's narrow
58
+ * rules. A row with no details' line there (no status, and no meta or one
59
+ * hidden below md) keeps the flag inline on its first line instead.
60
+ */
57
61
  function flagNarrowRules() {
58
62
  return `@media ${belowMd} {
59
63
  .sw-fixture-flag { grid-row: 2; }
@@ -63,6 +67,12 @@ function flagNarrowRules() {
63
67
  .sw-fixture-row-flagged .sw-fixture-end {
64
68
  padding-inline-start: calc(var(--sw-space-4) + var(--sw-space-2));
65
69
  }
70
+
71
+ .sw-fixture-row-flag-inline .sw-fixture-flag { grid-row: 1; }
72
+
73
+ .sw-fixture-row-flag-inline .sw-fixture-team-home {
74
+ padding-inline-start: calc(var(--sw-space-4) + var(--sw-space-2));
75
+ }
66
76
  }`;
67
77
  }
68
78
  /* FixtureRow's flag (see components/FixtureRow.tsx); after the row rules. */
@@ -74,6 +84,7 @@ ${flagNarrowRules()}`;
74
84
  export function fixtureFlagClassCatalog() {
75
85
  return [
76
86
  'sw-fixture-row-flagged',
87
+ 'sw-fixture-row-flag-inline',
77
88
  'sw-fixture-flag',
78
89
  'sw-fixture-flag-mark',
79
90
  ...fixtureFlagTones.map((tone) => `sw-fixture-flag-${tone}`),
@@ -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[];