courthive-components 6.0.1 → 6.1.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.
@@ -1,5 +1,25 @@
1
- import { CatalogMatchUpItem, MatchUpSide } from '../types';
1
+ import { CatalogMatchUpItem, MatchUpSide, ScheduleCellSide } from '../types';
2
2
  export declare function deepClone<T>(obj: T): T;
3
3
  export declare function matchUpLabel(item: CatalogMatchUpItem): string;
4
4
  export declare function participantLabel(side?: MatchUpSide): string;
5
+ /**
6
+ * Everything a catalog item can be found by, as one lower-cased line.
7
+ *
8
+ * `sides[].participantName` alone left doubles players unfindable by their own
9
+ * name: the factory composes a PAIR's `participantName` from family names only
10
+ * (`Phoebus/Smith`), so a search for "Aiden Phoebus" matched his singles matchUps
11
+ * and silently skipped every doubles matchUp he was in. `individualParticipants`
12
+ * carries the given+family names that the pair name drops, so both are pushed.
13
+ */
14
+ /**
15
+ * "Aiden Phoebus / Ravi Smith" — a doubles side's members, spelled out in full.
16
+ *
17
+ * Empty for a singles side and for an un-hydrated pair, which is the caller's cue to
18
+ * set no tooltip at all rather than an empty one.
19
+ *
20
+ * Pure and here rather than inline in the renderer so it can be tested without a DOM:
21
+ * the ecosystem's DOM layer is Playwright/Storybook, and `test-storybook` cannot
22
+ * currently run in this repo (612/612 suites fail in the runner, measured 2026-09-27).
23
+ */
24
+ export declare function memberNamesLabel(side?: MatchUpSide | ScheduleCellSide): string;
5
25
  export declare function matchUpSearchKey(item: CatalogMatchUpItem): string;
@@ -9,7 +9,7 @@ export { SchedulePageControl } from './controller/schedulePageControl';
9
9
  export { SchedulePageStore } from './engine/schedulePageStore';
10
10
  export { filterMatchUpCatalog, groupMatchUpCatalog, computeBaseRoundByEvent, isCompletedStatus } from './domain/matchUpCatalogProjections';
11
11
  export { buildScheduleIssueIndex } from './domain/scheduleIssues';
12
- export { matchUpLabel, participantLabel, matchUpSearchKey, deepClone } from './domain/utils';
12
+ export { matchUpLabel, participantLabel, matchUpSearchKey, memberNamesLabel, deepClone } from './domain/utils';
13
13
  export { computeActiveStrip, computeActiveStripCell, computeActiveStripDropTarget, computeReschedulePlacements } from './domain/activeStrip';
14
14
  export type { ActiveStripCell, ActiveStripCellState, ActiveStripCourtColumn, ActiveStripDropCandidate, ActiveStripDropTarget, ActiveStripGrid, ActiveStripGridMatchUp, ActiveStripStatusOptions, RescheduleCandidate, ReschedulePlacement } from './domain/activeStrip';
15
15
  export { buildScheduleDateStrip } from './ui/dateStrip';
@@ -9,6 +9,19 @@ export interface MatchUpSide {
9
9
  participantId?: string;
10
10
  seedNumber?: number;
11
11
  ranking?: number;
12
+ /**
13
+ * Hydrated members of a pair or team, present on `inContext` matchUps.
14
+ *
15
+ * Load-bearing for search, not for layout. A PAIR participant's own
16
+ * `participantName` is composed by the factory from FAMILY NAMES ALONE
17
+ * (`Phoebus/Smith`), so a doubles side carries no given name anywhere — and a
18
+ * consumer searching for "Aiden Phoebus" could not match the one cell he is
19
+ * actually in. These are the names that make him findable.
20
+ */
21
+ individualParticipants?: {
22
+ participantId?: string;
23
+ participantName?: string;
24
+ }[];
12
25
  }
13
26
  export interface CatalogMatchUpItem {
14
27
  matchUpId: string;
@@ -238,6 +251,19 @@ export interface ScheduleCellSide {
238
251
  teamName?: string;
239
252
  /** True if this side is a BYE (no participant) */
240
253
  bye?: boolean;
254
+ /**
255
+ * Hydrated members of a pair or team, present on `inContext` matchUps.
256
+ *
257
+ * Load-bearing for search, not for layout. A PAIR participant's own
258
+ * `participantName` is composed by the factory from FAMILY NAMES ALONE
259
+ * (`Phoebus/Smith`), so a doubles side carries no given name anywhere — and a
260
+ * consumer searching for "Aiden Phoebus" could not match the one cell he is
261
+ * actually in. These are the names that make him findable.
262
+ */
263
+ individualParticipants?: {
264
+ participantId?: string;
265
+ participantName?: string;
266
+ }[];
241
267
  }
242
268
  /**
243
269
  * Flat data shape the cell renderer consumes — mapped from factory matchUp objects.
@@ -41,6 +41,21 @@ export type SmartComplementResult = {
41
41
  * Get the format for a specific set index
42
42
  * Uses finalSetFormat for deciding set if available
43
43
  */
44
+ /**
45
+ * The `MatchUpConfig` for a TODS matchUpFormat string.
46
+ *
47
+ * Hoisted because it takes TWO sources to assemble and `dynamicSetsApproach` already had its own copy
48
+ * (`getMatchUpConfig`, line ~134): `bestOf` comes from `parseMatchUpFormat`, which resolves an
49
+ * `exactly:N` format down to a set count, while `exactly`, `setFormat` and `finalSetFormat` come
50
+ * straight off `matchUpFormatCode.parse`. A region building its own would be a second copy of a
51
+ * derivation whose two halves must agree, which is the shape every other divergence in this module
52
+ * family started as.
53
+ *
54
+ * Never parse a matchUpFormat with a regex — the factory owns that grammar, and an unparseable format
55
+ * falls back to SET3 rather than throwing, because a dialog that will not open is worse than one that
56
+ * opens on the wrong best-of.
57
+ */
58
+ export declare function matchUpConfigFor(matchUpFormat?: string): MatchUpConfig;
44
59
  export declare function getSetFormatForIndex(setIndex: number, config: MatchUpConfig): SetFormat | undefined;
45
60
  /**
46
61
  * Check if a set is tiebreak-only (e.g., TB10)
@@ -24,27 +24,64 @@
24
24
  export declare const NEITHER_SIDE = "NEITHER";
25
25
  /** Shown when an irregular ending has been chosen but the winner question is unanswered. */
26
26
  export declare const WINNER_REQUIRED_ERROR = "Select a winner";
27
- /** The statuses whose selection opens the winner question at all. */
27
+ /**
28
+ * Every irregular ending a set-entry approach offers, in display order.
29
+ *
30
+ * ── Why ten, when this list held six ──
31
+ *
32
+ * It held six because those are exactly the keys the factory's scoring policy can refine with
33
+ * `matchUpStatusCodes`, the reasoning being that a status with no code group cannot carry a reason.
34
+ * That is true but it was the wrong list to derive a VOCABULARY from, and the cost was measurable:
35
+ * `freeScoreApproach` has always parsed ten, so four statuses were typeable in one approach and
36
+ * unreachable in the other three. Rotating between approaches could therefore show an ending that
37
+ * could not be re-selected. The four additions (SUSPENDED, IN_PROGRESS, AWAITING_RESULT,
38
+ * DEAD_RUBBER) simply carry no reason, which is the designed quiet state, not a defect.
39
+ *
40
+ * Two things checked while widening, recorded because neither is visible from here:
41
+ *
42
+ * - `WITHDRAWN` is NOT a factory matchUpStatus (`matchUpStatusConstants.WITHDRAWN` will not even
43
+ * compile), yet the USTA policy keys a seventh `matchUpStatusCodes` group with it, from
44
+ * `entryStatusConstants`. This comment previously called that "a code group no status can reach"
45
+ * and a factory problem. Both halves were wrong, and this is settled, twice over:
46
+ *
47
+ * CA, 2026-09-27: "WITHDRAWN is a statusCode on a WALKOVER... WALKOVER (withdrawn injured or
48
+ * withdrawn ill)" — a withdrawal is expressed AS a walkover, and the WALKOVER group already ships
49
+ * that expression as `W5` / "Wo/Withdrawn". CA, 2026-09-19: the WALKOVER group records "a match
50
+ * that did not happen because someone withdrew", a RESULT; the WITHDRAWN group records "the
51
+ * withdrawal itself", an ENTRY action belonging to the entries UI. Same reasons, two events, two
52
+ * surfaces — which is why the groups are label-for-label parallel and must NOT be merged.
53
+ * `statusCodes.test.ts` holds that, including the measurement that refutes merging them.
54
+ * - The ten partition PERFECTLY onto the factory's own classification: three directing, seven
55
+ * non-directing, with nothing in both and nothing in neither. The `endingPartition` test asserts
56
+ * this rather than trusting it.
57
+ *
58
+ * Ordered so the three that name a winner come first, then the three an operator reaches for most
59
+ * often mid-tournament, then the remainder.
60
+ */
61
+ export declare const SELECTABLE_ENDINGS: string[];
62
+ /**
63
+ * The statuses whose selection opens the winner question at all.
64
+ *
65
+ * DERIVED from the factory's `directingMatchUpStatuses` rather than hand-listed, because "does a
66
+ * winner advance out of this status" is the factory's question to answer and it already answers it.
67
+ * Hand-adding a status to two sets is precisely where a mistake hides, and the eleventh ending
68
+ * should classify itself rather than wait for someone to remember both places.
69
+ */
28
70
  export declare const WINNER_REQUIRING_STATUSES: Set<string>;
29
71
  /**
30
72
  * Endings that resolve nobody: the match did not produce a result, so there is no winner to name
31
- * and no winner question to answer. The factory classifies these as non-directing
32
- * (`nonDirectingMatchUpStatuses`) — nothing advances out of them.
73
+ * and no winner question to answer. Nothing advances out of them.
33
74
  *
34
75
  * These are NOT a smaller version of the winner-requiring endings. Asking "who won an abandoned
35
76
  * match" is not a question with a missing answer; it is not a question. So unlike a walkover with
36
77
  * no winner selection, one of these is valid the moment it is chosen.
37
- */
38
- export declare const NON_DIRECTING_ENDINGS: Set<string>;
39
- /**
40
- * Every irregular ending a set-entry approach offers, in display order.
41
78
  *
42
- * These six are exactly the keys the factory's scoring policy can refine with `matchUpStatusCodes`
43
- * (ABANDONED, CANCELLED, DEFAULTED, INCOMPLETE, RETIRED, WALKOVER), which is why the list is this
44
- * list: a status with no code group cannot carry a reason, and a code group with no status cannot
45
- * be reached. Ordered so the three an operator reaches for most sit first.
79
+ * Also derived, and the complement of `WINNER_REQUIRING_STATUSES` over `SELECTABLE_ENDINGS` — but
80
+ * computed independently from `nonDirectingMatchUpStatuses` rather than as `!winnerRequiring`, so
81
+ * that a status the factory somehow classifies as NEITHER shows up as a test failure instead of
82
+ * being silently swept in here.
46
83
  */
47
- export declare const SELECTABLE_ENDINGS: string[];
84
+ export declare const NON_DIRECTING_ENDINGS: Set<string>;
48
85
  /** Whether choosing this ending obliges the operator to answer the winner question. */
49
86
  export declare function requiresWinner(selectedOutcome: string | undefined): boolean;
50
87
  /**
@@ -57,12 +94,52 @@ export declare function requiresWinner(selectedOutcome: string | undefined): boo
57
94
  * score is real. The same goes for `RETIRED` — a retirement keeps whatever was played, and needs no
58
95
  * completed set to be a valid result.
59
96
  *
60
- * `CANCELLED` and `DEAD_RUBBER` are deliberately NOT here even though this repo's `validateScore`
61
- * strips scores for them. They are unreachable from the set-entry approaches today (only freeScore
62
- * parses them) and the factory does not blank scores for them, so adding them here would be a
63
- * guess. Revisit when the status list widens — see Mentat SCORING_MODAL_STATUS_CODES.md M2.
97
+ * ── CANCELLED and DEAD_RUBBER are here now, and that is a CLIENT policy ──
98
+ *
99
+ * They were held out on the stated grounds that they were "unreachable from the set-entry approaches
100
+ * (only freeScore parses them)", with a note to revisit when the vocabulary widened. Two corrections
101
+ * to that, found on widening it:
102
+ *
103
+ * 1. CANCELLED was never unreachable — it has been in `SELECTABLE_ENDINGS` all along and renders as
104
+ * a radio in both Dynamic Sets and Dial Pad. Only DEAD_RUBBER was genuinely out of reach.
105
+ * 2. The rule was not absent, it was TRIPLICATED, and the three copies disagreed:
106
+ *
107
+ * NO_SCORE_STATUSES WALKOVER DOUBLE_WALKOVER
108
+ * validateScore WALKOVER CANCELLED DEAD_RUBBER
109
+ * freeScore display WALKOVER CANCELLED DEAD_RUBBER
110
+ *
111
+ * No copy held all four, and the two that agreed with each other both omitted DOUBLE_WALKOVER.
112
+ * This set is now the single one, and CA settled its contents (2026-09-27): selecting Cancelled
113
+ * or Dead Rubber clears any partial score present.
114
+ *
115
+ * Note what that means and does not mean. The FACTORY blanks scores for `{WALKOVER,
116
+ * DOUBLE_WALKOVER}` only (`modifyMatchUpScore.ts:236`, verified) — `removeScore` is its separate
117
+ * lever for anything else. So nothing server-side enforces the clearing of CANCELLED or DEAD_RUBBER;
118
+ * the client simply never submits a score for them, which reaches the same place. Do not "correct"
119
+ * this set toward the factory's: it is deliberately wider, and the widening is the policy.
120
+ *
121
+ * Still NOT here, and each for a reason worth keeping: `DEFAULTED` / `DOUBLE_DEFAULT`, because a
122
+ * player can default part-way through a match that was genuinely played; `RETIRED`, which keeps
123
+ * whatever was played; and ABANDONED / INCOMPLETE / SUSPENDED, where the partial score is the entire
124
+ * content of the result — "6-4 3-2, suspended" says when it was suspended.
64
125
  */
65
126
  export declare const NO_SCORE_STATUSES: Set<string>;
127
+ /**
128
+ * Endings a typed score legitimately survives alongside.
129
+ *
130
+ * The Dial Pad clears the selected ending when a digit is typed — the operator is starting over —
131
+ * and must not do that for an ending whose whole meaning includes a partial score. It decided this
132
+ * by asking `NON_DIRECTING_ENDINGS`, which is a DIFFERENT question: "does anyone advance" is not
133
+ * "is there a score". The two happened to coincide across the old six. They do not across the ten:
134
+ * DEAD_RUBBER and CANCELLED are non-directing yet carry no score, so the old gate would have
135
+ * preserved an ending that had just discarded the digits being typed.
136
+ *
137
+ * So it is stated as its own predicate over both axes, and the `endingPartition` test pins the case
138
+ * that made the difference.
139
+ */
140
+ export declare const SCORE_PRESERVING_ENDINGS: Set<string>;
141
+ /** Whether a typed score should be kept when this ending is selected. */
142
+ export declare function coexistsWithScore(selectedOutcome: string | undefined): boolean;
66
143
  /**
67
144
  * Whether this ending means "no score exists", so score validation must be skipped rather than run
68
145
  * and then overridden.
@@ -139,3 +216,66 @@ export declare function resolveIrregularEnding(params: {
139
216
  * `error: 'At least one set is required'` was the symptom of exactly that missing check.
140
217
  */
141
218
  export declare function applyIrregularEndingToValidation(validation: any, selectedOutcome: string | undefined, winnerSelection: WinnerSelection): IrregularEndingResolution;
219
+ /**
220
+ * What the controls should show for a matchUp that already carries a result.
221
+ *
222
+ * The documented INVERSE of `resolveIrregularEnding`. That function was hoisted here because three
223
+ * approaches each carried their own copy of the forward rule and disagreed; the inverse was left
224
+ * behind in each of them and diverged the same way. Measured before this existed:
225
+ *
226
+ * - `dynamicSetsApproach` restored all six endings and both double exits;
227
+ * - `dialPadApproach` restored only RETIRED / WALKOVER / DEFAULTED, so a saved ABANDONED,
228
+ * CANCELLED or INCOMPLETE re-opened reading as COMPLETED — and re-submitting silently replaced
229
+ * a non-directing status;
230
+ * - `freeScoreApproach` used a third mechanism entirely;
231
+ * - `inlineScoringApproach` discarded the saved result by construction.
232
+ *
233
+ * Round-tripping is the property that matters, and it is the one a test can hold: for every status
234
+ * in `SELECTABLE_ENDINGS`, `resolveIrregularEnding(hydrateIrregularEnding(m))` must return the
235
+ * status `m` was saved with.
236
+ *
237
+ * ── What each branch means ──
238
+ *
239
+ * A **double exit** is stored as its own status and carries no `winningSide`, so it inverts to the
240
+ * base ending plus an explicit `NEITHER_SIDE` — the tri-state's third value, recovered rather than
241
+ * guessed. A **non-directing** ending (ABANDONED, CANCELLED, INCOMPLETE) resolves nobody, so its
242
+ * winner is `undefined` and must stay that way; reading a stray `winningSide` back onto one would
243
+ * re-introduce the fail-open shape this module exists to prevent. Anything else — COMPLETED,
244
+ * TO_BE_PLAYED, IN_PROGRESS, a status this vocabulary does not offer, or no matchUp at all —
245
+ * inverts to COMPLETED with no winner, which is the inert state the controls open in.
246
+ *
247
+ * Deliberately NOT inferred: a `winningSide` present on a status that requires one is trusted, but a
248
+ * MISSING one is returned as `undefined` rather than defaulted. An unanswered winner is the
249
+ * fail-closed state (`WINNER_REQUIRED_ERROR`), and it must survive a save/reopen cycle as unanswered.
250
+ */
251
+ export declare function hydrateIrregularEnding(matchUp?: {
252
+ matchUpStatus?: string;
253
+ winningSide?: number;
254
+ }): {
255
+ selectedOutcome: string;
256
+ winnerSelection: WinnerSelection;
257
+ };
258
+ /**
259
+ * Display labels for every selectable ending, with locale overrides applied.
260
+ *
261
+ * Shared because it was not: Dynamic Sets and Dial Pad each kept their own map covering only the
262
+ * statuses they offered, and Dynamic Sets read it as `ENDING_LABELS[value]` with NO fallback — so
263
+ * widening `SELECTABLE_ENDINGS` without this would have rendered four radios labelled `undefined`.
264
+ * One map means an eleventh ending is either labelled or caught by `everySelectableEndingIsLabelled`.
265
+ *
266
+ * The fallback is the raw status rather than a blank, so an unlabelled ending is visibly wrong in the
267
+ * UI rather than invisibly missing.
268
+ */
269
+ export type EndingLabelOverrides = {
270
+ retired?: string;
271
+ walkover?: string;
272
+ defaulted?: string;
273
+ inProgress?: string;
274
+ awaitingResult?: string;
275
+ suspended?: string;
276
+ abandoned?: string;
277
+ cancelled?: string;
278
+ incomplete?: string;
279
+ deadRubber?: string;
280
+ };
281
+ export declare function endingLabels(labels?: EndingLabelOverrides): Record<string, string>;
@@ -0,0 +1,25 @@
1
+ import { ScoreOutcome } from '../types';
2
+ /** The fields a projection may replace. Everything else on the matchUp is carried through as-is. */
3
+ type ProjectableMatchUp = {
4
+ score?: any;
5
+ matchUpStatus?: string;
6
+ winningSide?: number;
7
+ [key: string]: any;
8
+ };
9
+ /**
10
+ * The matchUp the next approach should hydrate from.
11
+ *
12
+ * Returns the original when there is nothing in progress to carry — the ordinary case on first open,
13
+ * where the saved matchUp IS the truth.
14
+ *
15
+ * ── `winningSide` takes no fallback, and that is the whole care of this function ──
16
+ *
17
+ * It is read from the outcome and never from the saved matchUp. If the operator has cleared the
18
+ * score, the outcome carries no winner, and falling back to the stored one would put back a winner
19
+ * they had just removed — an unanswered question read as a particular answer, which is the fail-open
20
+ * shape (Mentat architectural standard A3) that `irregularEnding.ts` exists to prevent, one field
21
+ * over. `score` and `matchUpStatus` DO fall back, because an outcome that asserts neither is not
22
+ * claiming the match has no score or no status; it is simply not speaking about them.
23
+ */
24
+ export declare function projectOutcomeOntoMatchUp<T extends ProjectableMatchUp>(matchUp: T, outcome?: ScoreOutcome | null): T;
25
+ export {};
@@ -0,0 +1,130 @@
1
+ import { IrregularEndingResolution } from './irregularEnding';
2
+ /** Which side a per-side ending was recorded against. */
3
+ export type SideNumber = 1 | 2;
4
+ /**
5
+ * Everything the card holds that is not the score itself.
6
+ *
7
+ * The score lives in the score region (per-set inputs, a text field, or a keypad) and is passed in
8
+ * when resolving, because all three regions already own it and the card must not become a second
9
+ * place it can be edited.
10
+ */
11
+ export type ScoreEntryState = {
12
+ /**
13
+ * The side that ended early, and how. `sideNumber` is the side the ending HAPPENED TO — the one
14
+ * that retired or failed to appear — not the winner. The winner is derived from it.
15
+ */
16
+ sideEnding?: {
17
+ sideNumber: SideNumber;
18
+ status: string;
19
+ };
20
+ /**
21
+ * "…did not appear either — no one advances." The checkbox on the affected row that turns a side
22
+ * ending into a double exit.
23
+ *
24
+ * A separate flag rather than a third value on `sideEnding`, because it is a different assertion:
25
+ * the operator has said something about the OTHER side. Collapsing them is how the old tri-state
26
+ * came to treat "unanswered" and "neither" as the same thing.
27
+ */
28
+ bothSidesOut?: boolean;
29
+ /** The match-level ending. One of the seven; mutually exclusive with `sideEnding`. */
30
+ matchEnding?: string;
31
+ /** The policy reason code chosen for whichever ending is selected. */
32
+ reasonCode?: string;
33
+ };
34
+ /** What the card resolves to, plus the two things the UI needs that a resolution alone does not say. */
35
+ export type ScoreEntryResolution = IrregularEndingResolution & {
36
+ /** True when the selected ending means a typed score must be discarded rather than submitted. */
37
+ clearsScore: boolean;
38
+ /** True when an ending is selected at all, so the score region can render itself inert. */
39
+ hasEnding: boolean;
40
+ };
41
+ /** The empty card: no ending chosen, which is a reachable state and not a null one. */
42
+ export declare const emptyScoreEntryState: ScoreEntryState;
43
+ /**
44
+ * Record (or un-record) a per-side ending.
45
+ *
46
+ * Every control in the card is a TOGGLE, so choosing the ending already selected on that same side
47
+ * clears it. This is what gives the card a reachable empty state; a radio group has none, which is
48
+ * GOV.UK's standing objection to pre-selected radios — a user cannot get back to "nothing chosen"
49
+ * without reloading the page.
50
+ *
51
+ * Choosing a side ending clears any match-level ending, and vice versa. They answer the same
52
+ * question — how did this match end — so allowing both would let the card hold two answers and
53
+ * submit whichever the resolver happened to read first.
54
+ *
55
+ * `bothSidesOut` and `reasonCode` are dropped on any change of ending, because both are qualifiers
56
+ * OF an ending: a reason code chosen for a retirement is not a reason for a walkover, and the codes
57
+ * come from different policy groups. Carrying either across would submit a qualifier the operator
58
+ * never chose for the ending it ends up attached to.
59
+ */
60
+ export declare function chooseSideEnding(state: ScoreEntryState, sideNumber: SideNumber, status: string): ScoreEntryState;
61
+ /**
62
+ * Record (or un-record) a match-level ending. A toggle, on the same reasoning as above.
63
+ */
64
+ export declare function chooseMatchEnding(state: ScoreEntryState, status: string): ScoreEntryState;
65
+ /**
66
+ * Toggle "the other side did not appear either".
67
+ *
68
+ * Refused when there is no side ending to qualify, and refused when the ending has no double form:
69
+ * RETIRED is the case — the factory has no double-retirement status, and two players who both fail
70
+ * to finish are a double default or a double walkover depending on why, which is the operator's
71
+ * distinction to make and not one this module can infer. Returning the state unchanged rather than
72
+ * setting a flag that cannot resolve keeps the card from offering a checkbox whose answer is dropped.
73
+ */
74
+ export declare function toggleBothSidesOut(state: ScoreEntryState): ScoreEntryState;
75
+ /** Whether the card should offer the "did not appear either" checkbox at all. */
76
+ export declare function offersBothSidesOut(state: ScoreEntryState): boolean;
77
+ /** Choose (or clear) a reason code for the selected ending. A toggle, like everything else. */
78
+ export declare function chooseReasonCode(state: ScoreEntryState, code: string | undefined): ScoreEntryState;
79
+ /**
80
+ * The status whose policy code group the reason chips should come from.
81
+ *
82
+ * `undefined` when no ending is selected, because there is nothing to give a reason for.
83
+ */
84
+ export declare function reasonCodeStatus(state: ScoreEntryState): string | undefined;
85
+ /**
86
+ * Resolve the card into a submittable outcome.
87
+ *
88
+ * ── The one piece of derivation that lives here ──
89
+ *
90
+ * A per-side ending is recorded against the side it HAPPENED TO, and the winner is the other side.
91
+ * That inversion is the whole reason the separate winner question can be deleted, so it is stated
92
+ * once, here, and asserted on its own:
93
+ *
94
+ * sideEnding.sideNumber === 1 → winnerSelection 2
95
+ *
96
+ * Everything past that point is `resolveIrregularEnding`'s answer, unmodified — including the
97
+ * fail-closed states. Note in particular that a side ending with `bothSidesOut` on an ending with no
98
+ * double form cannot occur, because `toggleBothSidesOut` refuses to set it; if it somehow does, the
99
+ * resolver's own guard returns `isValid: false` rather than inventing a status.
100
+ */
101
+ export declare function resolveScoreEntry(state: ScoreEntryState): ScoreEntryResolution;
102
+ /**
103
+ * A resolution for an ending the SCORE REGION reported rather than the operator selected.
104
+ *
105
+ * Free Score parses endings out of typed text — "6-4 ret" is a retirement — and that must keep
106
+ * working, because typing the whole result is the entire reason that approach exists. But the card now
107
+ * owns endings through its own controls, so two sources can speak at once and the precedence has to be
108
+ * stated rather than emergent:
109
+ *
110
+ * **An explicitly selected ending always wins.** A region-reported one is used only when the
111
+ * operator has selected nothing.
112
+ *
113
+ * That direction and not the other, because a click is an unambiguous instruction and parsed text is
114
+ * an inference. An operator who types "ret", then clicks Suspended, means Suspended; clearing the
115
+ * click falls back to what they typed, which is still on screen in front of them.
116
+ *
117
+ * Resolution goes through `resolveIrregularEnding` like everything else, so a parsed ending and a
118
+ * clicked one cannot disagree about what they mean — only about which one applies.
119
+ */
120
+ export declare function resolveReportedEnding(matchUpStatus?: string, winningSide?: number): ScoreEntryResolution;
121
+ /**
122
+ * Which endings the card offers on a player row, and which in the match-level group.
123
+ *
124
+ * Derived from the same two sets the approaches use, in `SELECTABLE_ENDINGS` order, so the card
125
+ * cannot offer an ending the vocabulary does not hold or drop one it does. The split is not a layout
126
+ * choice: it is `WINNER_REQUIRING_STATUSES` and `NON_DIRECTING_ENDINGS`, which come from the
127
+ * factory's `directingMatchUpStatuses` / `nonDirectingMatchUpStatuses`.
128
+ */
129
+ export declare function sideEndingOptions(): string[];
130
+ export declare function matchEndingOptions(): string[];
@@ -0,0 +1,28 @@
1
+ import { ScoreEntryResolution } from './scoreEntryState';
2
+ export type ResultTone = 'good' | 'warn' | 'neutral';
3
+ export type ResultBand = {
4
+ tone: ResultTone;
5
+ /** The main sentence. Always present — a band with no headline would be a band with no purpose. */
6
+ headline: string;
7
+ /** The quieter right-hand note: what happens next, or what was recorded. */
8
+ detail?: string;
9
+ };
10
+ export type SummaryParams = {
11
+ resolution: ScoreEntryResolution;
12
+ /** Side 1's name first. */
13
+ sideNames: [string, string];
14
+ /** The score as it currently stands, already formatted (e.g. `'6-4 2-1'`). */
15
+ scoreString?: string;
16
+ /** Whether that score is a finished result — the score region's judgement, not this module's. */
17
+ scoreComplete?: boolean;
18
+ /** The winner a complete score implies, when no ending overrides it. */
19
+ scoreWinningSide?: 1 | 2;
20
+ /** Display labels for endings, so a locale can supply them. Defaults to English. */
21
+ labels?: Record<string, string>;
22
+ /** The chosen reason code's display form, e.g. `'Wo [inj]'`. */
23
+ reasonDisplay?: string;
24
+ /** What the winner advances to, e.g. `'the quarter-final'`. Omitted when unknown. */
25
+ advancesTo?: string;
26
+ };
27
+ /** The band for the current state of the card. */
28
+ export declare function scoreEntrySummary(params: SummaryParams): ResultBand;
@@ -0,0 +1,12 @@
1
+ import { ScoreRegion } from '../scoreEntryCard';
2
+ import { SetScore } from '../types';
3
+ export type DialPadRegionParams = {
4
+ matchUpFormat?: string;
5
+ /** Sets already recorded, e.g. from a saved matchUp. */
6
+ sets?: SetScore[];
7
+ onChange?: () => void;
8
+ };
9
+ export type DialPadRegion = ScoreRegion & {
10
+ getSets: () => SetScore[];
11
+ };
12
+ export declare function createDialPadRegion(params: DialPadRegionParams): DialPadRegion;
@@ -0,0 +1,19 @@
1
+ import { ScoreRegion } from '../scoreEntryCard';
2
+ import { SetScore } from '../types';
3
+ export type DynamicSetsRegionParams = {
4
+ matchUpFormat?: string;
5
+ /** Sets already recorded, e.g. from a saved matchUp. */
6
+ sets?: SetScore[];
7
+ /** Whether smart complements starts enabled. Defaults to on. */
8
+ smartComplements?: boolean;
9
+ smartComplementsLabel?: string;
10
+ /** Called after a change affecting only the score. Wire to the card's `refresh`. */
11
+ onChange?: () => void;
12
+ /** Called when the COLUMNS change. Wire to the card's `rerender`. */
13
+ onStructureChange?: () => void;
14
+ };
15
+ export type DynamicSetsRegion = ScoreRegion & {
16
+ getSets: () => SetScore[];
17
+ smartComplementsEnabled: () => boolean;
18
+ };
19
+ export declare function createDynamicSetsRegion(params: DynamicSetsRegionParams): DynamicSetsRegion;
@@ -0,0 +1,18 @@
1
+ import { ScoreRegion } from '../scoreEntryCard';
2
+ import { SetScore } from '../types';
3
+ export type FreeScoreRegionParams = {
4
+ matchUpFormat?: string;
5
+ /** The score to open with, already formatted (e.g. `'6-4 6-3'`). */
6
+ initialText?: string;
7
+ /** Label above the field. */
8
+ label?: string;
9
+ /** Called after any change. Wire to the card's `refresh`. */
10
+ onChange?: () => void;
11
+ };
12
+ export type FreeScoreRegion = ScoreRegion & {
13
+ /** What is currently in the field. */
14
+ getText: () => string;
15
+ /** The sets the parse produced, for submission. */
16
+ getSets: () => SetScore[];
17
+ };
18
+ export declare function createFreeScoreRegion(params: FreeScoreRegionParams): FreeScoreRegion;
@@ -0,0 +1,11 @@
1
+ import { SideNumber } from '../logic/scoreEntryState';
2
+ import { SetScore } from '../types';
3
+ /** The track width for the readout column, matching the artboards. */
4
+ export declare const READOUT_COLUMN_WIDTH = "200px";
5
+ export type ScoreReadouts = {
6
+ /** The cell for a side's row. Registers it, so later `update` calls reach it. */
7
+ cell: (sideNumber: SideNumber, sets: SetScore[]) => HTMLElement;
8
+ /** Rewrite every registered readout from the current sets. */
9
+ update: (sets: SetScore[]) => void;
10
+ };
11
+ export declare function createScoreReadouts(): ScoreReadouts;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Labels for the score columns.
3
+ *
4
+ * Shared by every region that renders per-set columns, so the Dial Pad and Dynamic Sets cannot end up
5
+ * heading the same column differently.
6
+ */
7
+ /**
8
+ * The column heading for a set: `1st`, `2nd`, `3rd`, `4th`, `5th`.
9
+ *
10
+ * CA, 2026-09-27, replacing `SET 1` / `SET 2` / `SET 3`. Shorter matters here for a reason beyond
11
+ * taste: the heading sits above a 62px column, and `SET 1` is wide enough that shrinking the column
12
+ * on a narrow viewport would either clip it or force the 44px input below its minimum target.
13
+ *
14
+ * The teens are handled even though a match will never have eleven sets, because an ordinal function
15
+ * that is wrong for 11 is simply a wrong ordinal function, and the next caller will not check.
16
+ */
17
+ export declare function ordinalSetLabel(setNumber: number): string;