courthive-components 6.0.1 → 6.2.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/matchUpFormat/matchUpFormatLogic.d.ts +1 -1
- 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/approaches/dialPadLogic.d.ts +5 -0
- package/dist/components/scoring/keyboard.d.ts +24 -0
- package/dist/components/scoring/logic/dynamicSetsLogic.d.ts +26 -8
- 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/scoreEntryModel.d.ts +175 -0
- package/dist/components/scoring/logic/scoreEntrySelectors.d.ts +88 -0
- package/dist/components/scoring/logic/scoreEntryState.d.ts +163 -0
- package/dist/components/scoring/logic/scoreEntryStore.d.ts +8 -0
- package/dist/components/scoring/logic/scoreEntrySummary.d.ts +28 -0
- package/dist/components/scoring/logic/statusCodes.d.ts +34 -0
- package/dist/components/scoring/logic/tiebreakEntry.d.ts +14 -0
- package/dist/components/scoring/regions/dialPadRegion.d.ts +17 -0
- package/dist/components/scoring/regions/dynamicSetsRegion.d.ts +28 -0
- package/dist/components/scoring/regions/freeScoreRegion.d.ts +24 -0
- package/dist/components/scoring/regions/scoreLine.d.ts +2 -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 +191 -0
- package/dist/components/scoring/scoreEntryDialog.d.ts +94 -0
- package/dist/components/scoring/types.d.ts +4 -0
- package/dist/components/scoring/utils/aggregateDecider.d.ts +18 -0
- package/dist/courthive-components.css +1 -1
- package/dist/courthive-components.es.js +8996 -7025
- package/dist/courthive-components.umd.js +44 -44
- package/dist/index.d.ts +4 -1
- package/dist/types.d.ts +10 -0
- package/package.json +7 -7
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ScoreEntryResolution, SideNumber } from './scoreEntryState';
|
|
2
|
+
import { ScoreEntryModel } from './scoreEntryModel';
|
|
3
|
+
import { SetScore } from '../types';
|
|
4
|
+
/** One visible column: a set's principal cells, or the tiebreak cells beside a set that went to one. */
|
|
5
|
+
export type ScoreSlot = {
|
|
6
|
+
kind: 'games' | 'tiebreak';
|
|
7
|
+
setIndex: number;
|
|
8
|
+
};
|
|
9
|
+
export type ColumnOptions = {
|
|
10
|
+
/**
|
|
11
|
+
* A set the operator has clicked back into, whose tiebreak column stays open though its points are
|
|
12
|
+
* known. Presentation, held by the region — passed in rather than kept in the model.
|
|
13
|
+
*/
|
|
14
|
+
editingSet?: number;
|
|
15
|
+
};
|
|
16
|
+
/** The ending, resolved. The one place `resolveScoreEntry` is asked about a model. */
|
|
17
|
+
export declare function resolveEnding(model: ScoreEntryModel): ScoreEntryResolution;
|
|
18
|
+
/** Sets with BOTH principal cells in, in factory shape, positioned by `setNumber`. A partial set is not here. */
|
|
19
|
+
export declare function enteredSets(model: ScoreEntryModel): SetScore[];
|
|
20
|
+
/** The entered sets that are SETTLED — see `isSettled`. A `4-2` is entered; it is not completed. */
|
|
21
|
+
export declare function completedSets(model: ScoreEntryModel): SetScore[];
|
|
22
|
+
/** Whether ANYTHING has been typed, complete or not — a single digit counts. */
|
|
23
|
+
export declare function hasEntry(model: ScoreEntryModel): boolean;
|
|
24
|
+
/** The score as it stands, in the factory's own line — or nothing while an ending has set it aside. */
|
|
25
|
+
export declare function scoreString(model: ScoreEntryModel): string | undefined;
|
|
26
|
+
/** What a score-clearing ending set aside, so the band can name it. Nothing unless one is chosen. */
|
|
27
|
+
export declare function discardedScore(model: ScoreEntryModel): string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Whether the score is a FINISHED result: the factory names a winner, the last set is settled, and
|
|
30
|
+
* nothing is wrong with what came before it. An ending that discards the score makes this false,
|
|
31
|
+
* because there is no result to be finished.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isComplete(model: ScoreEntryModel): boolean;
|
|
34
|
+
/** The winner: the ending's when it names one, otherwise the finished score's. */
|
|
35
|
+
export declare function winningSide(model: ScoreEntryModel): SideNumber | undefined;
|
|
36
|
+
/** The ending's status when one is chosen; `COMPLETED` for a finished score; nothing otherwise. */
|
|
37
|
+
export declare function matchUpStatus(model: ScoreEntryModel): string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* The first thing wrong with the score, in words, or `undefined`.
|
|
40
|
+
*
|
|
41
|
+
* Three checks, in order:
|
|
42
|
+
*
|
|
43
|
+
* 1. a tiebreak that contradicts who won the set, with the participant NAMED — the factory refuses it
|
|
44
|
+
* too, but "side 1" is not a message an operator can act on;
|
|
45
|
+
* 2. the FACTORY's judgement of each entered set (`validateSetScore` with `allowIncomplete`), which
|
|
46
|
+
* forgives unfinished games but knows `3-7` cannot be. A set whose tiebreak points are still owed
|
|
47
|
+
* is skipped, because the card is asking for them and an error at that moment would answer its
|
|
48
|
+
* own question;
|
|
49
|
+
* 3. invariant 10A: a set before the last entered one that is empty or unfinished.
|
|
50
|
+
*
|
|
51
|
+
* Silent while a score-clearing ending is chosen: nothing typed will be submitted.
|
|
52
|
+
*/
|
|
53
|
+
export type ErrorOptions = {
|
|
54
|
+
/** Names for the two sides, so a message can say who rather than "side 1". */
|
|
55
|
+
sideNames?: [string, string];
|
|
56
|
+
};
|
|
57
|
+
export declare function error(model: ScoreEntryModel, options?: ErrorOptions): string | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* The visible columns, left to right.
|
|
60
|
+
*
|
|
61
|
+
* Every set that holds anything is shown, whatever came before it — this is the rule that ends the
|
|
62
|
+
* ghost set: a cleared set 1 beside a typed set 2 shows BOTH, one empty and one full, so the operator
|
|
63
|
+
* sees the score the model has. Then one empty set opens after the last holding one, and only when
|
|
64
|
+
* everything before it is settled and the match is not decided; a timed format plays every bolt, so
|
|
65
|
+
* for it the only question is whether one is left. A tiebreak column sits beside a set whose games call
|
|
66
|
+
* for points it does not yet have, or that the operator has reopened.
|
|
67
|
+
*/
|
|
68
|
+
export declare function columns(model: ScoreEntryModel, options?: ColumnOptions): ScoreSlot[];
|
|
69
|
+
/**
|
|
70
|
+
* Whether a set is finished: both principal cells in, no tiebreak points owed, and the factory accepts
|
|
71
|
+
* it as a COMPLETE legal set. A `7-6` whose points are unknown is not settled, so the next set does not
|
|
72
|
+
* open under it.
|
|
73
|
+
*
|
|
74
|
+
* ── Strict `validateSetScore`, and not `analyzeSet`'s winning side ──
|
|
75
|
+
*
|
|
76
|
+
* Measured 2026-09-30, before `#5049(factory)`: `analyzeSet` named a winner for a `3-1` in a match
|
|
77
|
+
* tiebreak to ten, and for a `10-9` — any lead read as a win. Both answers are fixed in 7.4.0, and
|
|
78
|
+
* `dynamicSetsLogic` now asks the factory too. Strict validation stays the rule here all the same: it is
|
|
79
|
+
* the same answer `error` reports, so "settled" and "no error" cannot come apart.
|
|
80
|
+
*
|
|
81
|
+
* A timed set is the exception: any pair of scores ends it, a tie included, so both cells in is the
|
|
82
|
+
* whole test — strict validation refuses a `0-0` bolt, which is unusual but not impossible.
|
|
83
|
+
*/
|
|
84
|
+
export declare function isSettled(model: ScoreEntryModel, index: number): boolean;
|
|
85
|
+
/** Whether the set's games call for a tiebreak whose points the operator has not supplied yet. */
|
|
86
|
+
export declare function tiebreakOutstanding(model: ScoreEntryModel, index: number): boolean;
|
|
87
|
+
/** The value in one cell. Exported for regions, which render cells from the model and never read them back. */
|
|
88
|
+
export declare function cellValue(model: ScoreEntryModel, setIndex: number, side: SideNumber, kind: ScoreSlot['kind']): number | undefined;
|
|
@@ -0,0 +1,163 @@
|
|
|
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
|
+
/**
|
|
85
|
+
* The card's state for a matchUp that ALREADY has an outcome.
|
|
86
|
+
*
|
|
87
|
+
* Without this the dialog opened blank on a scored matchUp: no recorded ending, no reason code. The
|
|
88
|
+
* shipping modal hydrates, so swapping the card in without this would lose the display of every outcome
|
|
89
|
+
* already entered — CA, 2026-09-28: *"we need to be able to open existing outcomes!"*
|
|
90
|
+
*
|
|
91
|
+
* ── The one inversion, again ──
|
|
92
|
+
*
|
|
93
|
+
* A stored matchUp names the WINNER. This card records the side the ending HAPPENED TO, and derives the
|
|
94
|
+
* winner from it. So hydration runs that mapping backwards: the exiting side is the one that did not
|
|
95
|
+
* win. `hydrateIrregularEnding` is reused for the status itself rather than re-deriving it, so the
|
|
96
|
+
* reverse lookup from `DOUBLE_WALKOVER` to the `WALKOVER` an operator actually clicks lives in exactly
|
|
97
|
+
* one place.
|
|
98
|
+
*
|
|
99
|
+
* Three cases and a refusal:
|
|
100
|
+
*
|
|
101
|
+
* - a DOUBLE exit — both sides out, so no side is distinguished. `sideNumber: 1` is arbitrary and only
|
|
102
|
+
* decides nothing: `bothSidesOut` makes the resolution `NEITHER_SIDE`, and the card draws the pill on
|
|
103
|
+
* both rows regardless.
|
|
104
|
+
* - an ending that resolves NOBODY — recorded at match level, because there is no side to attach it to.
|
|
105
|
+
* - a single exit — recorded against `3 - winningSide`.
|
|
106
|
+
* - a winner-requiring status with NO winning side is a corrupt record, and is refused: it returns the
|
|
107
|
+
* empty state rather than inventing a placement. `hydrateIrregularEnding` makes the same choice for
|
|
108
|
+
* an unmapped double exit, and for the same reason — a guess here silently advances a participant.
|
|
109
|
+
*/
|
|
110
|
+
export declare function hydrateScoreEntryState(matchUp?: {
|
|
111
|
+
matchUpStatus?: string;
|
|
112
|
+
winningSide?: number;
|
|
113
|
+
sideStatusCodes?: Record<number, string>;
|
|
114
|
+
matchUpStatusCode?: string;
|
|
115
|
+
matchUpStatusCodes?: unknown[];
|
|
116
|
+
}): ScoreEntryState;
|
|
117
|
+
export declare function reasonCodeStatus(state: ScoreEntryState): string | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* Resolve the card into a submittable outcome.
|
|
120
|
+
*
|
|
121
|
+
* ── The one piece of derivation that lives here ──
|
|
122
|
+
*
|
|
123
|
+
* A per-side ending is recorded against the side it HAPPENED TO, and the winner is the other side.
|
|
124
|
+
* That inversion is the whole reason the separate winner question can be deleted, so it is stated
|
|
125
|
+
* once, here, and asserted on its own:
|
|
126
|
+
*
|
|
127
|
+
* sideEnding.sideNumber === 1 → winnerSelection 2
|
|
128
|
+
*
|
|
129
|
+
* Everything past that point is `resolveIrregularEnding`'s answer, unmodified — including the
|
|
130
|
+
* fail-closed states. Note in particular that a side ending with `bothSidesOut` on an ending with no
|
|
131
|
+
* double form cannot occur, because `toggleBothSidesOut` refuses to set it; if it somehow does, the
|
|
132
|
+
* resolver's own guard returns `isValid: false` rather than inventing a status.
|
|
133
|
+
*/
|
|
134
|
+
export declare function resolveScoreEntry(state: ScoreEntryState): ScoreEntryResolution;
|
|
135
|
+
/**
|
|
136
|
+
* A resolution for an ending the SCORE REGION reported rather than the operator selected.
|
|
137
|
+
*
|
|
138
|
+
* Free Score parses endings out of typed text — "6-4 ret" is a retirement — and that must keep
|
|
139
|
+
* working, because typing the whole result is the entire reason that approach exists. But the card now
|
|
140
|
+
* owns endings through its own controls, so two sources can speak at once and the precedence has to be
|
|
141
|
+
* stated rather than emergent:
|
|
142
|
+
*
|
|
143
|
+
* **An explicitly selected ending always wins.** A region-reported one is used only when the
|
|
144
|
+
* operator has selected nothing.
|
|
145
|
+
*
|
|
146
|
+
* That direction and not the other, because a click is an unambiguous instruction and parsed text is
|
|
147
|
+
* an inference. An operator who types "ret", then clicks Suspended, means Suspended; clearing the
|
|
148
|
+
* click falls back to what they typed, which is still on screen in front of them.
|
|
149
|
+
*
|
|
150
|
+
* Resolution goes through `resolveIrregularEnding` like everything else, so a parsed ending and a
|
|
151
|
+
* clicked one cannot disagree about what they mean — only about which one applies.
|
|
152
|
+
*/
|
|
153
|
+
export declare function resolveReportedEnding(matchUpStatus?: string, winningSide?: number): ScoreEntryResolution;
|
|
154
|
+
/**
|
|
155
|
+
* Which endings the card offers on a player row, and which in the match-level group.
|
|
156
|
+
*
|
|
157
|
+
* Derived from the same two sets the approaches use, in `SELECTABLE_ENDINGS` order, so the card
|
|
158
|
+
* cannot offer an ending the vocabulary does not hold or drop one it does. The split is not a layout
|
|
159
|
+
* choice: it is `WINNER_REQUIRING_STATUSES` and `NON_DIRECTING_ENDINGS`, which come from the
|
|
160
|
+
* factory's `directingMatchUpStatuses` / `nonDirectingMatchUpStatuses`.
|
|
161
|
+
*/
|
|
162
|
+
export declare function sideEndingOptions(): string[];
|
|
163
|
+
export declare function matchEndingOptions(): string[];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { CreateScoreEntryModelParams, ScoreEntryModel } from './scoreEntryModel';
|
|
2
|
+
export type ScoreEntryStore = {
|
|
3
|
+
/** The model as it stands. Never mutated; read it again after any transition. */
|
|
4
|
+
get: () => ScoreEntryModel;
|
|
5
|
+
/** Replace the model. The caller says what must be redrawn. */
|
|
6
|
+
set: (next: ScoreEntryModel) => void;
|
|
7
|
+
};
|
|
8
|
+
export declare function createScoreEntryStore(params?: CreateScoreEntryModelParams): ScoreEntryStore;
|
|
@@ -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;
|
|
@@ -42,6 +42,40 @@ export declare function groupKeyForStatus(matchUpStatus: string | undefined): st
|
|
|
42
42
|
*
|
|
43
43
|
* Returns `undefined` for a provenance element, which carries no code at all.
|
|
44
44
|
*/
|
|
45
|
+
/**
|
|
46
|
+
* The reason code a matchUp actually recorded, for the control that has to re-display it.
|
|
47
|
+
*
|
|
48
|
+
* ── Why not `matchUpStatusCodes[0]` ──
|
|
49
|
+
*
|
|
50
|
+
* `matchUpStatusCodes` is POSITIONAL BY SIDE, so a reason belonging to side 2 sits at index 1 and
|
|
51
|
+
* index 0 is an empty string that `normalizeStatusCode` turns into `undefined`. The picker then opens
|
|
52
|
+
* empty, the operator's reason is missing from the control meant to show it, and re-saving loses it.
|
|
53
|
+
*
|
|
54
|
+
* Measured against published **7.1.0**, because the exposure is narrower than it first looks: a code
|
|
55
|
+
* submitted by this dialog is stored EXACTLY as submitted, so `['W1']` round-trips through index 0 and
|
|
56
|
+
* reads correctly today. The index-1 case arrives by PROPAGATION — a reason carried into a connected
|
|
57
|
+
* structure is placed at the arriving side's index — and factory #5016 both fixes that carry and gives
|
|
58
|
+
* the fact a home that states what it attributes to.
|
|
59
|
+
*
|
|
60
|
+
* ── The order, and why each step is needed ──
|
|
61
|
+
*
|
|
62
|
+
* 1. `sideStatusCodes[sideNumber]` — the side's own, once the factory writes it.
|
|
63
|
+
* 2. `matchUpStatusCode` — the MATCH's, for an ending that resolves nobody (ABANDONED, CANCELLED,
|
|
64
|
+
* INCOMPLETE). There is no side to ask.
|
|
65
|
+
* 3. `matchUpStatusCodes[0]` — records written before either field existed. Not optional: the peer
|
|
66
|
+
* range still admits a factory major that has neither, so dropping it would read nothing at all
|
|
67
|
+
* from every record stored to date.
|
|
68
|
+
*
|
|
69
|
+
* The fields are mirrored onto this package's own `MatchUp` rather than imported from the factory, for
|
|
70
|
+
* the reason `types.ts` already records: they do not exist in the older major the peer range admits.
|
|
71
|
+
*/
|
|
72
|
+
export declare function recordedStatusCode(matchUp?: {
|
|
73
|
+
matchUpStatus?: string;
|
|
74
|
+
winningSide?: number;
|
|
75
|
+
sideStatusCodes?: Record<number, string>;
|
|
76
|
+
matchUpStatusCode?: string;
|
|
77
|
+
matchUpStatusCodes?: unknown[];
|
|
78
|
+
}): string | undefined;
|
|
45
79
|
export declare function normalizeStatusCode(entry: unknown): string | undefined;
|
|
46
80
|
/**
|
|
47
81
|
* The codes a policy offers for this status, or an empty list when it offers none.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SetFormat } from './dynamicSetsLogic';
|
|
2
|
+
import { SideNumber } from './scoreEntryState';
|
|
3
|
+
/** The points a tiebreak-only set is played to, or `undefined` if this set is not one. */
|
|
4
|
+
export declare function tiebreakOnlyTarget(setFormat?: SetFormat): number | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Both sides' points, from the LOWER score and the side it was entered on.
|
|
7
|
+
*
|
|
8
|
+
* `undefined` when the set carries no target to complete against, or when the factory declines — never
|
|
9
|
+
* a guess. The caller leaves what was typed alone in that case rather than inventing the other half.
|
|
10
|
+
*/
|
|
11
|
+
export declare function completeTiebreakOnly(lowValue: number, lowSide: SideNumber, setFormat?: SetFormat): {
|
|
12
|
+
side1: number;
|
|
13
|
+
side2: number;
|
|
14
|
+
} | undefined;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ScoreEntryStore } from '../logic/scoreEntryStore';
|
|
2
|
+
import { ScoreRegion } from '../scoreEntryCard';
|
|
3
|
+
import { SetScore } from '../types';
|
|
4
|
+
export type DialPadRegionParams = {
|
|
5
|
+
/** The model the card holds. Omit it and the region makes its own from the params below. */
|
|
6
|
+
store?: ScoreEntryStore;
|
|
7
|
+
matchUpFormat?: string;
|
|
8
|
+
/** Sets already recorded, e.g. from a saved matchUp. Read only when no `store` is given. */
|
|
9
|
+
sets?: SetScore[];
|
|
10
|
+
onChange?: () => void;
|
|
11
|
+
};
|
|
12
|
+
export type DialPadRegion = ScoreRegion & {
|
|
13
|
+
store: ScoreEntryStore;
|
|
14
|
+
/** The entered sets, for a host or a test reading the region directly. The card reads the model. */
|
|
15
|
+
getSets: () => SetScore[];
|
|
16
|
+
};
|
|
17
|
+
export declare function createDialPadRegion(params: DialPadRegionParams): DialPadRegion;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ScoreRegion } from '../scoreEntryCard';
|
|
2
|
+
import { ScoreEntryStore } from '../logic/scoreEntryStore';
|
|
3
|
+
import { SetScore } from '../types';
|
|
4
|
+
export type DynamicSetsRegionParams = {
|
|
5
|
+
/** The model the card holds. Omit it and the region makes its own from the params below. */
|
|
6
|
+
store?: ScoreEntryStore;
|
|
7
|
+
matchUpFormat?: string;
|
|
8
|
+
/** Participant names, so an integrity message can say who rather than "side 1". */
|
|
9
|
+
sideNames?: [string, string];
|
|
10
|
+
/** Sets already recorded, e.g. from a saved matchUp. */
|
|
11
|
+
sets?: SetScore[];
|
|
12
|
+
/** Whether smart complements starts enabled. Defaults to on. */
|
|
13
|
+
smartComplements?: boolean;
|
|
14
|
+
smartComplementsLabel?: string;
|
|
15
|
+
/** Called after a change affecting only the score. Wire to the card's `refresh`. */
|
|
16
|
+
onChange?: () => void;
|
|
17
|
+
/** Called when the COLUMNS change. Wire to the card's `rerender`. */
|
|
18
|
+
onStructureChange?: () => void;
|
|
19
|
+
};
|
|
20
|
+
export type DynamicSetsRegion = ScoreRegion & {
|
|
21
|
+
store: ScoreEntryStore;
|
|
22
|
+
/** The entered sets, for a host or a test reading the region directly. The card reads the model. */
|
|
23
|
+
getSets: () => SetScore[];
|
|
24
|
+
/** The model's integrity message, named with this region's `sideNames`. The card computes its own. */
|
|
25
|
+
error: () => string | undefined;
|
|
26
|
+
smartComplementsEnabled: () => boolean;
|
|
27
|
+
};
|
|
28
|
+
export declare function createDynamicSetsRegion(params: DynamicSetsRegionParams): DynamicSetsRegion;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ScoreEntryStore } from '../logic/scoreEntryStore';
|
|
2
|
+
import { ScoreRegion } from '../scoreEntryCard';
|
|
3
|
+
import { SetScore } from '../types';
|
|
4
|
+
export type FreeScoreRegionParams = {
|
|
5
|
+
/** The model the card holds. Omit it and the region makes its own from the params below. */
|
|
6
|
+
store?: ScoreEntryStore;
|
|
7
|
+
matchUpFormat?: string;
|
|
8
|
+
/** The score to open with, already formatted (e.g. `'6-4 6-3'`). With a `store`, the model's own line is used. */
|
|
9
|
+
initialText?: string;
|
|
10
|
+
/** Label above the field. */
|
|
11
|
+
label?: string;
|
|
12
|
+
/** Called after any change. Wire to the card's `refresh`. */
|
|
13
|
+
onChange?: () => void;
|
|
14
|
+
};
|
|
15
|
+
export type FreeScoreRegion = ScoreRegion & {
|
|
16
|
+
store: ScoreEntryStore;
|
|
17
|
+
/** What is currently in the field. */
|
|
18
|
+
getText: () => string;
|
|
19
|
+
/** The entered sets, for a host or a test reading the region directly. The card reads the model. */
|
|
20
|
+
getSets: () => SetScore[];
|
|
21
|
+
/** Text in the field counts as entry the model cannot see; required here. */
|
|
22
|
+
hasEntry: () => boolean;
|
|
23
|
+
};
|
|
24
|
+
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;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { ScoreEntryState, SideNumber } from './logic/scoreEntryState';
|
|
2
|
+
import { ScoreEntryModel } from './logic/scoreEntryModel';
|
|
3
|
+
import { ScoreEntryStore } from './logic/scoreEntryStore';
|
|
4
|
+
import { StatusCodeGroups } from './logic/statusCodes';
|
|
5
|
+
/**
|
|
6
|
+
* One column the region contributes to the participant rows.
|
|
7
|
+
*
|
|
8
|
+
* `heading` and `width` travel TOGETHER deliberately. The header row and the participant rows are
|
|
9
|
+
* built from this one array, so a heading can never end up over a column it does not describe — the
|
|
10
|
+
* correspondence is structural rather than two lists that have to be kept the same length by hand.
|
|
11
|
+
* CA asked for that alignment explicitly (2026-09-27) and this is what makes it hold rather than a
|
|
12
|
+
* rule someone has to remember.
|
|
13
|
+
*
|
|
14
|
+
* A column with no `heading` renders no header cell, and a region whose columns are all unheaded gets
|
|
15
|
+
* no header row at all — which is what Free Score and the Dial Pad want, since their single readout
|
|
16
|
+
* column needs no label.
|
|
17
|
+
*/
|
|
18
|
+
export type ScoreColumn = {
|
|
19
|
+
/** The heading above this column, e.g. `1st`. Omit for an unlabelled column. */
|
|
20
|
+
heading?: string;
|
|
21
|
+
/** A CSS grid track size. Defaults to the standard score-column width. */
|
|
22
|
+
width?: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* What a score region IS, now that the card holds the model: render, and intent.
|
|
26
|
+
*
|
|
27
|
+
* S5 of the state-engine extraction. This contract used to carry fourteen members, six of which were
|
|
28
|
+
* ANSWERS — `scoreString`, `isComplete`, `winningSide`, `error`, `hasEntry`, `getSets` — computed by each
|
|
29
|
+
* region on the spot from its private copy of the score. Those are selectors over the model now, asked
|
|
30
|
+
* by the card, and a region that still answered them would be a second opinion the card ignores.
|
|
31
|
+
*
|
|
32
|
+
* What is left is what a region genuinely knows and the model cannot: how to DRAW the score (cells,
|
|
33
|
+
* a block, a control for the band), where entry begins, what is its own to reset after the model was
|
|
34
|
+
* emptied, and — for Free Score only — what the TEXT says that the model cannot represent: an ending
|
|
35
|
+
* parsed out of it, and a half-typed field.
|
|
36
|
+
*/
|
|
37
|
+
export type ScoreRegion = {
|
|
38
|
+
/**
|
|
39
|
+
* The store the region renders, when it made its own.
|
|
40
|
+
*
|
|
41
|
+
* A region built standalone — the behavioural tests, or a host placing a card inline — creates a store
|
|
42
|
+
* from its params, and the card adopts it so the two share one model. A host that opens the dialog
|
|
43
|
+
* gives the same store to the card and every region, and this is never consulted.
|
|
44
|
+
*/
|
|
45
|
+
store?: ScoreEntryStore;
|
|
46
|
+
/** The columns this region contributes to each participant row. */
|
|
47
|
+
columns?: () => ScoreColumn[];
|
|
48
|
+
/** The cells to place in one side's row. Must return one per column. */
|
|
49
|
+
rowCells?: (sideNumber: SideNumber) => HTMLElement[];
|
|
50
|
+
/** A block beneath the rows — a text field, or a keypad. */
|
|
51
|
+
block?: () => HTMLElement;
|
|
52
|
+
/**
|
|
53
|
+
* A compact control the region wants in the result band's right edge rather than in a row of its own.
|
|
54
|
+
*
|
|
55
|
+
* CA, 2026-09-27, on the smart-complements checkbox: *"I don't think '[] Smart Complements' should take
|
|
56
|
+
* up a whole row of the modal. I think it can be a little icon to the far right side of the row where you
|
|
57
|
+
* have 'No result entered yet'. Just (Smart) maybe, something compact that toggles."*
|
|
58
|
+
*
|
|
59
|
+
* It stays a REGION concern — it is a Dynamic Sets behaviour and nothing else reads it — so the region
|
|
60
|
+
* supplies the element and the card only decides where it sits.
|
|
61
|
+
*/
|
|
62
|
+
bandControl?: () => HTMLElement | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* A matchUpStatus the region itself parsed out of what was typed — Free Score only.
|
|
65
|
+
*
|
|
66
|
+
* REPORTED, not selected: an inference about the text, which never enters the model's `ending`. Used
|
|
67
|
+
* ONLY when the operator has selected no ending. See `resolveReportedEnding` for why that precedence
|
|
68
|
+
* and not the other.
|
|
69
|
+
*/
|
|
70
|
+
matchUpStatus?: () => string | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* Reset what is the region's own after the card has emptied the model: a text field, a keypad mode,
|
|
73
|
+
* the set under edit. The score itself is already gone by the time this is called.
|
|
74
|
+
*/
|
|
75
|
+
clear?: () => void;
|
|
76
|
+
/**
|
|
77
|
+
* Put focus where entry begins, selecting what is there.
|
|
78
|
+
*
|
|
79
|
+
* Selecting matters as much as focusing: the old dialog's `focusAndSelect` is why typing into a cell
|
|
80
|
+
* that already holds a score REPLACES it. Used after `[Clear]`, and by a host opening the dialog.
|
|
81
|
+
*/
|
|
82
|
+
focusFirst?: () => void;
|
|
83
|
+
/**
|
|
84
|
+
* Entry the MODEL cannot see — Free Score's half-typed text. The card asks the model first and this
|
|
85
|
+
* second, so a `6-4 re` mid-word still counts as something to lose.
|
|
86
|
+
*/
|
|
87
|
+
hasEntry?: () => boolean;
|
|
88
|
+
};
|
|
89
|
+
/** One entry in the approach switcher's menu. */
|
|
90
|
+
export type ApproachOption = {
|
|
91
|
+
/** Reported to `onSelectApproach`. The host's own name for the approach. */
|
|
92
|
+
key: string;
|
|
93
|
+
label: string;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* What the card reports when Submit is pressed.
|
|
97
|
+
*
|
|
98
|
+
* `winningSide` is the ending's where one was recorded and the SCORE's otherwise. Both are real answers
|
|
99
|
+
* and a host needs whichever applies: measured 2026-09-27, this carried only the ending's, so an
|
|
100
|
+
* ordinary 6-4 6-3 submitted as `{ matchUpStatus: undefined, winningSide: undefined }` — the 95% case
|
|
101
|
+
* reporting nothing at all about who won.
|
|
102
|
+
*/
|
|
103
|
+
export type ScoreEntryOutcome = {
|
|
104
|
+
matchUpStatus?: string;
|
|
105
|
+
winningSide?: number;
|
|
106
|
+
reasonCode?: string;
|
|
107
|
+
/** The score as the region formats it, which is what a host stores alongside the outcome. */
|
|
108
|
+
score?: string;
|
|
109
|
+
/**
|
|
110
|
+
* The operator cleared a recorded outcome and submitted the empty result — REMOVE what is stored.
|
|
111
|
+
*
|
|
112
|
+
* Only ever `true`, never `false`, so a host that does not know about it is unaffected. It exists
|
|
113
|
+
* because an empty outcome is otherwise indistinguishable from a submission that says nothing, and
|
|
114
|
+
* the two must not be treated alike: one erases a stored score and the other must not.
|
|
115
|
+
*/
|
|
116
|
+
cleared?: boolean;
|
|
117
|
+
};
|
|
118
|
+
export type ScoreEntryCardParams = {
|
|
119
|
+
sides: [{
|
|
120
|
+
participantName: string;
|
|
121
|
+
seed?: string;
|
|
122
|
+
}, {
|
|
123
|
+
participantName: string;
|
|
124
|
+
seed?: string;
|
|
125
|
+
}];
|
|
126
|
+
matchUpFormat?: string;
|
|
127
|
+
/** e.g. `'R16 · Court 3'`. */
|
|
128
|
+
context?: string;
|
|
129
|
+
title?: string;
|
|
130
|
+
/** The score region for the active approach. */
|
|
131
|
+
region: ScoreRegion;
|
|
132
|
+
/**
|
|
133
|
+
* The model, when the host holds it — the dialog does, and shares it with every region it builds.
|
|
134
|
+
* Omitted, the card adopts the region's own store, or makes one.
|
|
135
|
+
*/
|
|
136
|
+
store?: ScoreEntryStore;
|
|
137
|
+
/** The policy's reason-code groups, if the tournament has a scoring policy attached. */
|
|
138
|
+
statusCodeGroups?: StatusCodeGroups;
|
|
139
|
+
/** Ending labels, so a locale can supply them. */
|
|
140
|
+
labels?: Record<string, string>;
|
|
141
|
+
/** The approach switcher's current label, e.g. `'Dynamic Sets'`. Omit to hide the switcher. */
|
|
142
|
+
approachLabel?: string;
|
|
143
|
+
/**
|
|
144
|
+
* Offered when the switcher should present a CHOICE. The label of the active approach stays
|
|
145
|
+
* `approachLabel`; these are what it can become.
|
|
146
|
+
*/
|
|
147
|
+
approaches?: ApproachOption[];
|
|
148
|
+
/** Called with the chosen approach's key. Wire to a host that swaps the region through `update`. */
|
|
149
|
+
onSelectApproach?: (key: string) => void;
|
|
150
|
+
/** Offered instead of `approaches` when the host drives switching from its own control. */
|
|
151
|
+
onSwitchApproach?: () => void;
|
|
152
|
+
/** Offered when the host can edit the scoring format. Omit and the format chip stays inert text. */
|
|
153
|
+
onEditFormat?: () => void;
|
|
154
|
+
onCancel?: () => void;
|
|
155
|
+
/** An ending already recorded, for reopening an outcome. Build it with `hydrateScoreEntryState`. */
|
|
156
|
+
initialState?: ScoreEntryState;
|
|
157
|
+
onClear?: () => void;
|
|
158
|
+
onSubmit?: (outcome: ScoreEntryOutcome) => void;
|
|
159
|
+
onClose?: () => void;
|
|
160
|
+
};
|
|
161
|
+
export type ScoreEntryCard = {
|
|
162
|
+
element: HTMLElement;
|
|
163
|
+
/** Re-render the band and the submit gate. Call when the score region's value changes. */
|
|
164
|
+
refresh: () => void;
|
|
165
|
+
/** Rebuild everything, including the region's cells. Call when the region's COLUMNS change. */
|
|
166
|
+
rerender: () => void;
|
|
167
|
+
/**
|
|
168
|
+
* Change the region (an approach switch) or the scoring format, keeping the ENDING state.
|
|
169
|
+
*
|
|
170
|
+
* Both inputs change while the dialog is open and both invalidate the region's cells, so they share one
|
|
171
|
+
* mutator. What they must NOT invalidate is the ending: a walkover recorded against a row is a fact
|
|
172
|
+
* about the match, not about the approach used to type it or the format the score is read under, and
|
|
173
|
+
* an operator who corrects `SET3-S:6/TB7` to `SET3-S:6/TB7@5` has not retracted it.
|
|
174
|
+
*/
|
|
175
|
+
update: (next: {
|
|
176
|
+
region?: ScoreRegion;
|
|
177
|
+
matchUpFormat?: string;
|
|
178
|
+
approachLabel?: string;
|
|
179
|
+
}) => void;
|
|
180
|
+
/** The store the card renders, for a host that builds regions on it. */
|
|
181
|
+
store: ScoreEntryStore;
|
|
182
|
+
/** The model as it stands. */
|
|
183
|
+
getModel: () => ScoreEntryModel;
|
|
184
|
+
/** The current ending state — `getModel().ending` — for a host that needs to inspect it. */
|
|
185
|
+
getState: () => ScoreEntryState;
|
|
186
|
+
/** Whether anything at all has been entered: a score, an ending, or text the model cannot see. */
|
|
187
|
+
holdsEntry: () => boolean;
|
|
188
|
+
/** The DOM id of the card's heading, for a host's `aria-labelledby`. */
|
|
189
|
+
titleId: string;
|
|
190
|
+
};
|
|
191
|
+
export declare function renderScoreEntryCard(params: ScoreEntryCardParams): ScoreEntryCard;
|