courthive-components 6.1.0 → 6.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,6 @@
1
1
  import { ScoreEntryState, SideNumber } from './logic/scoreEntryState';
2
+ import { ScoreEntryModel } from './logic/scoreEntryModel';
3
+ import { ScoreEntryStore } from './logic/scoreEntryStore';
2
4
  import { StatusCodeGroups } from './logic/statusCodes';
3
5
  /**
4
6
  * One column the region contributes to the participant rows.
@@ -19,26 +21,99 @@ export type ScoreColumn = {
19
21
  /** A CSS grid track size. Defaults to the standard score-column width. */
20
22
  width?: string;
21
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
+ */
22
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;
23
46
  /** The columns this region contributes to each participant row. */
24
47
  columns?: () => ScoreColumn[];
25
48
  /** The cells to place in one side's row. Must return one per column. */
26
49
  rowCells?: (sideNumber: SideNumber) => HTMLElement[];
27
50
  /** A block beneath the rows — a text field, or a keypad. */
28
51
  block?: () => HTMLElement;
29
- /** The score as it currently stands, for the result band. */
30
- scoreString?: () => string | undefined;
31
- /** Whether that score is a finished result. The region's judgement, never the card's. */
32
- isComplete?: () => boolean;
33
- /** The winner the score implies, when no ending overrides it. */
34
- winningSide?: () => SideNumber | undefined;
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;
35
63
  /**
36
64
  * A matchUpStatus the region itself parsed out of what was typed — Free Score only.
37
65
  *
38
- * Used ONLY when the operator has selected no ending. See `resolveReportedEnding` for why that
39
- * precedence and not the other.
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.
40
69
  */
41
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;
42
117
  };
43
118
  export type ScoreEntryCardParams = {
44
119
  sides: [{
@@ -54,30 +129,63 @@ export type ScoreEntryCardParams = {
54
129
  title?: string;
55
130
  /** The score region for the active approach. */
56
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;
57
137
  /** The policy's reason-code groups, if the tournament has a scoring policy attached. */
58
138
  statusCodeGroups?: StatusCodeGroups;
59
139
  /** Ending labels, so a locale can supply them. */
60
140
  labels?: Record<string, string>;
61
141
  /** The approach switcher's current label, e.g. `'Dynamic Sets'`. Omit to hide the switcher. */
62
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. */
63
151
  onSwitchApproach?: () => void;
152
+ /** Offered when the host can edit the scoring format. Omit and the format chip stays inert text. */
153
+ onEditFormat?: () => void;
64
154
  onCancel?: () => void;
155
+ /** An ending already recorded, for reopening an outcome. Build it with `hydrateScoreEntryState`. */
156
+ initialState?: ScoreEntryState;
65
157
  onClear?: () => void;
66
- onSubmit?: (outcome: {
67
- matchUpStatus?: string;
68
- winningSide?: number;
69
- reasonCode?: string;
70
- }) => void;
158
+ onSubmit?: (outcome: ScoreEntryOutcome) => void;
71
159
  onClose?: () => void;
72
160
  };
73
- /** A card instance: its element, plus the handle the host needs to react to score-region changes. */
74
161
  export type ScoreEntryCard = {
75
162
  element: HTMLElement;
76
163
  /** Re-render the band and the submit gate. Call when the score region's value changes. */
77
164
  refresh: () => void;
78
165
  /** Rebuild everything, including the region's cells. Call when the region's COLUMNS change. */
79
166
  rerender: () => void;
80
- /** The current ending state, for a host that needs to inspect it. */
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. */
81
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;
82
190
  };
83
191
  export declare function renderScoreEntryCard(params: ScoreEntryCardParams): ScoreEntryCard;
@@ -0,0 +1,94 @@
1
+ import { SetScore } from './types';
2
+ import { ScoreEntryCard, ScoreEntryCardParams, ScoreEntryOutcome } from './scoreEntryCard';
3
+ /** The three entry approaches. The key is what `onSelectApproach` reports and what a host persists. */
4
+ export type ScoreEntryApproach = 'dynamicSets' | 'freeScore' | 'dialPad';
5
+ export type ScoreEntryDialogParams = Omit<ScoreEntryCardParams, 'region' | 'approachLabel' | 'approaches' | 'onSelectApproach' | 'onSwitchApproach' | 'onEditFormat' | 'onClose' | 'onSubmit'> & {
6
+ /**
7
+ * Called with the outcome, then the dialog closes.
8
+ *
9
+ * `sets` is added to what the card reports, because the dialog knows its regions carry them and a
10
+ * host saving to the factory needs the structured sets rather than only the formatted string.
11
+ */
12
+ onSubmit?: (outcome: ScoreEntryOutcome & {
13
+ sets: SetScore[];
14
+ }) => void;
15
+ /** Which approach opens. Defaults to Dynamic Sets. */
16
+ approach?: ScoreEntryApproach;
17
+ /** Which approaches the switcher offers. Defaults to all three; a single entry hides the menu. */
18
+ approaches?: ScoreEntryApproach[];
19
+ /** Sets already recorded, e.g. from a saved matchUp. Ignored when `matchUp` carries a score. */
20
+ sets?: SetScore[];
21
+ /**
22
+ * A matchUp whose outcome is being REOPENED.
23
+ *
24
+ * Everything a recorded outcome needs comes from here: the sets, the format, the ending, and the
25
+ * reason code — CA, 2026-09-28: *"we need to be able to open existing outcomes!"* Anything passed
26
+ * explicitly wins, so a host can override one part without unpacking the rest.
27
+ *
28
+ * `sides` stays separate rather than being read off `matchUp.sides`, because the card wants display
29
+ * names and a matchUp carries participants; that mapping belongs to the host, which already has it.
30
+ */
31
+ matchUp?: {
32
+ matchUpFormat?: string;
33
+ matchUpStatus?: string;
34
+ winningSide?: number;
35
+ score?: {
36
+ sets?: SetScore[];
37
+ };
38
+ sideStatusCodes?: Record<number, string>;
39
+ matchUpStatusCode?: string;
40
+ matchUpStatusCodes?: unknown[];
41
+ };
42
+ /** Called with the chosen approach whenever it changes, so a host can remember the preference. */
43
+ onApproachChange?: (approach: ScoreEntryApproach) => void;
44
+ /** Called with a format chosen in the picker. Omit and the format chip stays inert text. */
45
+ onFormatChange?: (matchUpFormat: string) => void;
46
+ /**
47
+ * Called when a format change DISCARDS part of the score, with what was lost and why.
48
+ *
49
+ * Only when something was actually discarded — a change that costs the operator nothing says
50
+ * nothing. `discarded` carries the sets themselves rather than a count, because a host that means to
51
+ * tell the operator has to quote them in their own numbers, and `reason` is the factory validator's
52
+ * own words about the first casualty.
53
+ */
54
+ onScoreDiscarded?: (discarded: {
55
+ sets: SetScore[];
56
+ discarded: SetScore[];
57
+ reason?: string;
58
+ matchUpFormat: string;
59
+ }) => void;
60
+ /** Called when the dialog closes, by `[X]`, by a footer button, or by `close()`. */
61
+ onClose?: () => void;
62
+ /**
63
+ * Whether to focus the first entry field on open. Defaults to true.
64
+ *
65
+ * `false` for a host that opens the dialog as a side effect of something else, where stealing focus
66
+ * would interrupt what the operator was actually doing.
67
+ */
68
+ autoFocus?: boolean;
69
+ /** Overrides cModal's config. `maxWidth`, `clickAway` and `padding` have deliberate defaults. */
70
+ modalConfig?: Record<string, any>;
71
+ /**
72
+ * The format picker. Defaults to this package's `getMatchUpFormatModal`.
73
+ *
74
+ * Injectable because a unit test has no business opening the real picker's modal — it is a second
75
+ * dialog with its own state — and because a host may have its own.
76
+ */
77
+ openFormatPicker?: (params: {
78
+ existingMatchUpFormat: string;
79
+ callback: (matchUpFormat: string) => void;
80
+ }) => void;
81
+ };
82
+ export type ScoreEntryDialog = {
83
+ /** The card, for a host that needs to read the state or drive it. */
84
+ card: ScoreEntryCard;
85
+ /** The approach currently shown. */
86
+ approach: () => ScoreEntryApproach;
87
+ /** Switch approach, carrying the typed score across. */
88
+ setApproach: (approach: ScoreEntryApproach) => void;
89
+ /** Change the scoring format, rebuilding the region under it. */
90
+ setMatchUpFormat: (matchUpFormat: string) => void;
91
+ /** Close the dialog. Runs `onClose` exactly once, however the close was triggered. */
92
+ close: () => void;
93
+ };
94
+ export declare function openScoreEntryDialog(params: ScoreEntryDialogParams): ScoreEntryDialog;