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.
- package/dist/components/schedule-page/domain/utils.d.ts +21 -1
- package/dist/components/schedule-page/index.d.ts +1 -1
- package/dist/components/schedule-page/types.d.ts +26 -0
- package/dist/components/scoring/logic/dynamicSetsLogic.d.ts +15 -0
- package/dist/components/scoring/logic/irregularEnding.d.ts +156 -16
- package/dist/components/scoring/logic/outcomeProjection.d.ts +25 -0
- package/dist/components/scoring/logic/scoreEntryState.d.ts +130 -0
- package/dist/components/scoring/logic/scoreEntrySummary.d.ts +28 -0
- package/dist/components/scoring/regions/dialPadRegion.d.ts +12 -0
- package/dist/components/scoring/regions/dynamicSetsRegion.d.ts +19 -0
- package/dist/components/scoring/regions/freeScoreRegion.d.ts +18 -0
- package/dist/components/scoring/regions/scoreReadout.d.ts +11 -0
- package/dist/components/scoring/regions/setColumns.d.ts +17 -0
- package/dist/components/scoring/scoreEntryCard.d.ts +83 -0
- package/dist/components/scoring/types.d.ts +4 -0
- package/dist/courthive-components.css +1 -1
- package/dist/courthive-components.es.js +7014 -6945
- package/dist/courthive-components.umd.js +45 -45
- package/dist/index.d.ts +1 -1
- package/package.json +2 -2
|
@@ -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
|
-
/**
|
|
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.
|
|
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
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
|
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
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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;
|