@energy8platform/shell 0.4.0 → 0.4.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.
- package/dist/html.cjs.js +30 -5
- package/dist/html.cjs.js.map +1 -1
- package/dist/html.d.ts +2 -2
- package/dist/html.esm.js +30 -5
- package/dist/html.esm.js.map +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.esm.js +1 -1
- package/dist/pixi.cjs.js +113 -28
- package/dist/pixi.cjs.js.map +1 -1
- package/dist/pixi.d.ts +2 -2
- package/dist/pixi.esm.js +113 -28
- package/dist/pixi.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/core/version.ts +1 -1
- package/src/ui/html/components/pickers.ts +13 -1
- package/src/ui/html/shell.css.ts +19 -3
- package/src/ui/pixi/components/GameInfo.ts +52 -13
- package/src/ui/pixi/components/pickers.ts +66 -15
package/package.json
CHANGED
package/src/core/version.ts
CHANGED
|
@@ -12,6 +12,9 @@ interface SheetOpts {
|
|
|
12
12
|
* with the shell's mobile breakpoint (driven by CSS custom props, so an open modal
|
|
13
13
|
* re-columns live on resize/rotate without being rebuilt). */
|
|
14
14
|
columns: number | { wide: number; mobile: number };
|
|
15
|
+
/** Size the columns to the widest chip label (via `--chip-min`) instead of a fixed count, so
|
|
16
|
+
* variable-width labels (e.g. a wide currency's bet values) reflow rather than clip. */
|
|
17
|
+
autoFit?: boolean;
|
|
15
18
|
confirmLabel: string;
|
|
16
19
|
onConfirm: (id: string) => void;
|
|
17
20
|
/** Called to dismiss the picker (should invoke host.actions.closeOverlay()). */
|
|
@@ -33,6 +36,15 @@ function buildSheet(opts: SheetOpts): Sheet {
|
|
|
33
36
|
const cols = typeof opts.columns === 'number' ? { wide: opts.columns, mobile: opts.columns } : opts.columns;
|
|
34
37
|
grid.style.setProperty('--cols', String(cols.wide));
|
|
35
38
|
grid.style.setProperty('--cols-m', String(cols.mobile));
|
|
39
|
+
if (opts.autoFit) {
|
|
40
|
+
// Widest label in ch (tabular digits ≈ 1ch each; symbols/separators are narrower, so this
|
|
41
|
+
// slightly over-estimates → safe) + chip padding/border. Floored so normal-width currencies
|
|
42
|
+
// keep the compact ~6-per-row look and only wide currencies drop to fewer columns.
|
|
43
|
+
const maxLen = opts.choices.reduce((m, c) => Math.max(m, c.label.length), 0);
|
|
44
|
+
// Floor at 6em ≈ the natural width of a chip in the 6-wide layout (44em card): short labels
|
|
45
|
+
// clamp here so a normal currency keeps 6 columns; wider labels raise it and drop to fewer.
|
|
46
|
+
grid.style.setProperty('--chip-min', `max(6em, calc(${maxLen}ch + 1.4em))`);
|
|
47
|
+
}
|
|
36
48
|
let selected = opts.selected;
|
|
37
49
|
let focusIndex = opts.choices.findIndex((c) => c.id === selected);
|
|
38
50
|
if (focusIndex < 0) focusIndex = 0;
|
|
@@ -105,7 +117,7 @@ function buildSheet(opts: SheetOpts): Sheet {
|
|
|
105
117
|
/** Bet picker — all available bets as chips (6 per row, 3 on mobile), accent Confirm applies it. */
|
|
106
118
|
export function openBetModal(host: ShellHost): { root: HTMLElement; onKey: (e: KeyboardEvent) => boolean } {
|
|
107
119
|
return buildSheet({
|
|
108
|
-
ge: 'bet-modal', title: host.t('Bet'), columns: { wide: 6, mobile: 3 }, confirmLabel: host.t('Confirm'),
|
|
120
|
+
ge: 'bet-modal', title: host.t('Bet'), columns: { wide: 6, mobile: 3 }, autoFit: true, confirmLabel: host.t('Confirm'),
|
|
109
121
|
choices: host.state.availableBets.map((b) => ({ id: String(b), label: host.formatCurrency(b) })),
|
|
110
122
|
selected: String(host.state.bet),
|
|
111
123
|
onClose: () => host.actions.closeOverlay(),
|
package/src/ui/html/shell.css.ts
CHANGED
|
@@ -203,7 +203,12 @@ export const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
203
203
|
#${SHELL_ROOT_ID} .ge-gi-hk-block { display:flex; flex-direction:column; }
|
|
204
204
|
#${SHELL_ROOT_ID} .ge-gi-hk { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:9px 0; }
|
|
205
205
|
#${SHELL_ROOT_ID} .ge-gi-hk + .ge-gi-hk { border-top:1px solid var(--shell-plaque-line); }
|
|
206
|
-
|
|
206
|
+
/* Let the keycaps SHRINK instead of hogging the row: flex:0 1 auto lets the chips block give up
|
|
207
|
+
width down to its widest single combo, and flex-wrap reflows the extra combos onto another line.
|
|
208
|
+
On narrow/mobile widths the many-chip rows (Raise/Lower bet) otherwise squeeze the label off the
|
|
209
|
+
right edge and clip it. (No min-width:0 here — the min-content floor is what keeps a combo intact
|
|
210
|
+
and wrapping rather than overflowing.) */
|
|
211
|
+
#${SHELL_ROOT_ID} .ge-gi-hk-chips { display:flex; align-items:center; flex-wrap:wrap; gap:4px; flex:0 1 auto; }
|
|
207
212
|
#${SHELL_ROOT_ID} .ge-gi-hk-combo { display:inline-flex; align-items:center; gap:4px; }
|
|
208
213
|
#${SHELL_ROOT_ID} .ge-gi-hk-chip { display:inline-flex; align-items:center; justify-content:center;
|
|
209
214
|
padding:2px 7px; border-radius:6px; border:1px solid var(--shell-plaque-line);
|
|
@@ -212,7 +217,10 @@ export const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
212
217
|
font-weight:600; line-height:1.5; white-space:nowrap; min-width:1.6em; text-align:center; }
|
|
213
218
|
#${SHELL_ROOT_ID} .ge-gi-hk-sep { color:var(--shell-plaque-label); font-size:11px; padding:0 1px; }
|
|
214
219
|
#${SHELL_ROOT_ID} .ge-gi-hk-sep2 { color:var(--shell-plaque-label); font-size:11px; padding:0 4px; }
|
|
215
|
-
|
|
220
|
+
/* flex:1 1 auto (content-based basis, NOT flex:1's 0 basis) so the label keeps demanding its word
|
|
221
|
+
width and shrinks in PROPORTION to the chips — both give ground and wrap, instead of the label
|
|
222
|
+
collapsing to nothing. No min-width:0: the min-content floor stops a word being clipped. */
|
|
223
|
+
#${SHELL_ROOT_ID} .ge-gi-hk-tx { color:rgba(255,255,255,.88); font-size:14px; font-weight:600; text-align:right; flex:1 1 auto; }
|
|
216
224
|
|
|
217
225
|
/* controls — two blocks (gameplay / menu & info), icon/name/description per control */
|
|
218
226
|
#${SHELL_ROOT_ID} .ge-gi-ctl-block + .ge-gi-ctl-block { margin-top:16px; padding-top:4px; border-top:1px solid var(--shell-plaque-line); }
|
|
@@ -226,7 +234,7 @@ export const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
226
234
|
#${SHELL_ROOT_ID} .ge-gi-ctl-ic--bet { flex-direction:column; font-size:18px; gap:0; }
|
|
227
235
|
/* buy bonus draws the real control-bar badge, scaled down & non-interactive */
|
|
228
236
|
#${SHELL_ROOT_ID} .ge-gi-ctl-ic .ge-shell-buybonus { width:46px; height:46px; font-size:8px; border-width:2px; pointer-events:none; }
|
|
229
|
-
#${SHELL_ROOT_ID} .ge-gi-ctl-tx { display:flex; flex-direction:column; gap:2px; }
|
|
237
|
+
#${SHELL_ROOT_ID} .ge-gi-ctl-tx { display:flex; flex-direction:column; gap:2px; min-width:0; }
|
|
230
238
|
#${SHELL_ROOT_ID} .ge-gi-ctl-tx b { color:#fff; font-size:15px; font-weight:700; }
|
|
231
239
|
#${SHELL_ROOT_ID} .ge-gi-ctl-tx span { color:rgba(255,255,255,.7); font-size:13px; line-height:1.4; }
|
|
232
240
|
|
|
@@ -513,6 +521,14 @@ export const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
513
521
|
#${SHELL_ROOT_ID}.ge-mobile .ge-sheet-grid { grid-template-columns:repeat(var(--cols-m,var(--cols,3)),1fr); }
|
|
514
522
|
/* the bet picker packs 6 chips/row → give its card room (autoplay & co. stay at the 28em default) */
|
|
515
523
|
#${SHELL_ROOT_ID} .ge-sheet[data-ge="bet-modal"] .ge-modal-card { max-width:44em; }
|
|
524
|
+
/* Bet chips carry variable-width currency (a wide currency makes labels grow). Reflow the columns
|
|
525
|
+
to the widest label (--chip-min, computed in JS) so no chip is ever clipped, and cap the grid
|
|
526
|
+
height so a long bet ladder scrolls instead of overflowing the card's clipped edge. */
|
|
527
|
+
#${SHELL_ROOT_ID} .ge-sheet[data-ge="bet-modal"] .ge-sheet-grid,
|
|
528
|
+
#${SHELL_ROOT_ID}.ge-mobile .ge-sheet[data-ge="bet-modal"] .ge-sheet-grid {
|
|
529
|
+
grid-template-columns:repeat(auto-fill, minmax(min(100%, var(--chip-min, 6em)), 1fr));
|
|
530
|
+
max-height:min(50vh, 28em); overflow-y:auto; overflow-x:hidden; }
|
|
531
|
+
#${SHELL_ROOT_ID} .ge-sheet[data-ge="bet-modal"] .ge-chip { min-width:0; }
|
|
516
532
|
#${SHELL_ROOT_ID} .ge-chip { pointer-events:auto; cursor:pointer; border:1px solid var(--shell-plaque-line);
|
|
517
533
|
border-radius:.8em; background:rgba(255,255,255,.04); color:#fff; font-size:1em; font-weight:700;
|
|
518
534
|
font-variant-numeric:tabular-nums; padding:.8em .55em; transition:background .12s ease, border-color .12s ease; }
|
|
@@ -282,27 +282,66 @@ function sectionHotkeys(host: PixiComponentContext, title: string, width: number
|
|
|
282
282
|
return sec;
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
/** Render a single hotkey row: keycap chip(s) on the left, action name on the right.
|
|
285
|
+
/** Render a single hotkey row: keycap chip(s) on the left, action name on the right. The row is
|
|
286
|
+
* constrained to `inner` (chips left, label right). On narrow widths the chip combos stack onto
|
|
287
|
+
* their own lines and the label wraps within the leftover width, so nothing clips past the plaque
|
|
288
|
+
* — the Pixi analogue of the HTML `.ge-gi-hk-chips { flex:0 1 auto; flex-wrap }` + label wrap. */
|
|
286
289
|
function hkRow(host: PixiComponentContext, r: HkRow, inner: number): FlexBox {
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
// Build chips container
|
|
290
|
-
const chipsCol = new FlexBox({ direction: 'row', align: 'center', gap: 4 });
|
|
290
|
+
const GAP = 14;
|
|
291
|
+
const LABEL_MIN = 92; // reserve at least this much for the action label before wrapping the chips
|
|
291
292
|
const chipKeys = buildChipKeys(r);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
293
|
+
|
|
294
|
+
// Lay the chips on one line and measure. If that leaves too little room for the label, stack the
|
|
295
|
+
// combos (split at the '/') onto their own lines so the chips give up horizontal width.
|
|
296
|
+
let chips: FlexBox = chipLine(host, chipKeys);
|
|
297
|
+
let chipsW = chips.measureSize().w;
|
|
298
|
+
if (chipsW > inner - GAP - LABEL_MIN) {
|
|
299
|
+
chips = chipCombos(host, chipKeys);
|
|
300
|
+
chipsW = chips.measureSize().w;
|
|
298
301
|
}
|
|
299
302
|
|
|
300
|
-
|
|
301
|
-
|
|
303
|
+
// The label takes the leftover width and WRAPS within it (never clips); right-aligned to sit
|
|
304
|
+
// against the plaque edge like the DOM `.ge-gi-hk-tx`.
|
|
305
|
+
const labelW = Math.max(LABEL_MIN, inner - chipsW - GAP);
|
|
306
|
+
const nameText = makeText(host.t(r.name), { size: 15, weight: '700', color: '#ffffff', wrapWidth: labelW, align: 'right' });
|
|
307
|
+
|
|
308
|
+
const row = new FlexBox({ direction: 'row', align: 'center', justify: 'space-between', gap: GAP, padding: { top: 8, bottom: 8 }, width: inner });
|
|
309
|
+
row.add(chips);
|
|
302
310
|
row.add(nameText);
|
|
303
311
|
return row;
|
|
304
312
|
}
|
|
305
313
|
|
|
314
|
+
/** Chips on a single row, with '/' separators between the combos (as flattened by buildChipKeys). */
|
|
315
|
+
function chipLine(host: PixiComponentContext, chipKeys: string[]): FlexBox {
|
|
316
|
+
const line = new FlexBox({ direction: 'row', align: 'center', gap: 4 });
|
|
317
|
+
for (const key of chipKeys) {
|
|
318
|
+
if (key === '/') line.add(makeText('/', { size: 12, weight: '500', color: host.tokens.plaqueLabel }));
|
|
319
|
+
else line.add(keycap(host, key));
|
|
320
|
+
}
|
|
321
|
+
return line;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Chips stacked as combos — split at the '/' separators, one combo per line (drops the inline
|
|
325
|
+
* '/', which reads as a line break once the combos are stacked). */
|
|
326
|
+
function chipCombos(host: PixiComponentContext, chipKeys: string[]): FlexBox {
|
|
327
|
+
const col = new FlexBox({ direction: 'column', align: 'start', gap: 4 });
|
|
328
|
+
let line = new FlexBox({ direction: 'row', align: 'center', gap: 4 });
|
|
329
|
+
let filled = false;
|
|
330
|
+
const flush = (): void => {
|
|
331
|
+
if (!filled) return;
|
|
332
|
+
col.add(line);
|
|
333
|
+
line = new FlexBox({ direction: 'row', align: 'center', gap: 4 });
|
|
334
|
+
filled = false;
|
|
335
|
+
};
|
|
336
|
+
for (const key of chipKeys) {
|
|
337
|
+
if (key === '/') { flush(); continue; }
|
|
338
|
+
line.add(keycap(host, key));
|
|
339
|
+
filled = true;
|
|
340
|
+
}
|
|
341
|
+
flush();
|
|
342
|
+
return col;
|
|
343
|
+
}
|
|
344
|
+
|
|
306
345
|
/** Flatten a row's chip list into display tokens — for bet rows, produce: Shift ↑ / Shift = style. */
|
|
307
346
|
function buildChipKeys(r: HkRow): string[] {
|
|
308
347
|
if ((r.name === 'Raise bet' || r.name === 'Lower bet') && r.chips.length === 4) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { PixiComponentContext, ShellLayer } from '../context';
|
|
2
2
|
import { CardModal } from '../primitives/card';
|
|
3
3
|
import { Chip } from '../primitives/controls';
|
|
4
|
-
import { FlexBox } from '../primitives/flex';
|
|
4
|
+
import { FlexBox, type Sizable } from '../primitives/flex';
|
|
5
|
+
import { ScrollBox } from '../primitives/scroll';
|
|
5
6
|
|
|
6
7
|
interface Choice {
|
|
7
8
|
id: string;
|
|
@@ -13,8 +14,15 @@ interface SheetOpts {
|
|
|
13
14
|
title: string;
|
|
14
15
|
choices: Choice[];
|
|
15
16
|
selected: string;
|
|
16
|
-
/** Chips per row — a fixed number, or `{ wide, mobile }` that reflows with the layout.
|
|
17
|
+
/** Chips per row — a fixed number, or `{ wide, mobile }` that reflows with the layout.
|
|
18
|
+
* With `autoFit`, this is the *maximum* column count; the grid drops to fewer when the labels
|
|
19
|
+
* are too wide to fit that many. */
|
|
17
20
|
columns: number | { wide: number; mobile: number };
|
|
21
|
+
/** Size the columns to the widest chip label (instead of always packing `columns`), and cap the
|
|
22
|
+
* grid height with a scroll region. For variable-width labels (a wide currency's bet values)
|
|
23
|
+
* this reflows + scrolls rather than clipping — the Pixi analogue of the HTML shell's
|
|
24
|
+
* `auto-fill minmax` + `overflow-y:auto`. */
|
|
25
|
+
autoFit?: boolean;
|
|
18
26
|
/** Max card width in em (the 6-wide bet picker needs 44em vs the 28em default). */
|
|
19
27
|
maxEm?: number;
|
|
20
28
|
confirmLabel: string;
|
|
@@ -36,8 +44,24 @@ class PickerModal extends CardModal {
|
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
/** A centred picker (chips grid + accent Confirm) on the shared card modal. */
|
|
47
|
+
/** A ScrollBox that reports a fixed box to the FlexBox layout — so a capped, scrolling grid slots
|
|
48
|
+
* into the card body like any other sized child (measured/positioned by its viewport, not its
|
|
49
|
+
* full content bounds). */
|
|
50
|
+
class SizedScrollBox extends ScrollBox implements Sizable {
|
|
51
|
+
constructor(private readonly boxW: number, private readonly boxH: number, canvas?: HTMLCanvasElement) {
|
|
52
|
+
super(canvas);
|
|
53
|
+
this.setViewport(boxW, boxH);
|
|
54
|
+
}
|
|
55
|
+
measureSize(): { w: number; h: number } {
|
|
56
|
+
return { w: this.boxW, h: this.boxH };
|
|
57
|
+
}
|
|
58
|
+
setLayoutSize(): void {
|
|
59
|
+
/* fixed viewport — ignore layout-imposed sizing */
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
39
63
|
function buildSheet(host: PixiComponentContext, opts: SheetOpts): ShellLayer {
|
|
40
|
-
const
|
|
64
|
+
const maxColumns = typeof opts.columns === 'number'
|
|
41
65
|
? opts.columns
|
|
42
66
|
: host.layout === 'mobile' ? opts.columns.mobile : opts.columns.wide;
|
|
43
67
|
|
|
@@ -91,27 +115,53 @@ function buildSheet(host: PixiComponentContext, opts: SheetOpts): ShellLayer {
|
|
|
91
115
|
const em = modal.emSize;
|
|
92
116
|
const gap = 0.65 * em;
|
|
93
117
|
const innerW = modal.cardWidth - 2.4 * em;
|
|
118
|
+
|
|
119
|
+
// Build the chips first (at natural width) so autoFit can measure the widest label.
|
|
120
|
+
for (let i = 0; i < opts.choices.length; i++) {
|
|
121
|
+
const c = opts.choices[i];
|
|
122
|
+
const chipIndex = i;
|
|
123
|
+
chips.push(new Chip(host, c.id, c.label, chipIndex === focusIndex, em, (id) => {
|
|
124
|
+
const idx = opts.choices.findIndex((ch) => ch.id === id);
|
|
125
|
+
if (idx >= 0) setHighlight(idx);
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Column count: with autoFit, drop below the max until the widest label fits. Short labels
|
|
130
|
+
// (a normal currency) exceed the max and clamp back to it — so the compact 6-wide layout is
|
|
131
|
+
// preserved and only wide currencies reflow to fewer columns.
|
|
132
|
+
let columns = maxColumns;
|
|
133
|
+
if (opts.autoFit) {
|
|
134
|
+
const widest = chips.reduce((m, ch) => Math.max(m, ch.measureSize().w), 0);
|
|
135
|
+
const fitCols = Math.floor((innerW + gap) / (widest + gap));
|
|
136
|
+
columns = Math.max(1, Math.min(maxColumns, fitCols));
|
|
137
|
+
}
|
|
94
138
|
const colW = (innerW - gap * (columns - 1)) / columns;
|
|
95
139
|
|
|
96
140
|
const grid = new FlexBox({ direction: 'column', align: 'start', gap });
|
|
97
|
-
for (let i = 0; i <
|
|
98
|
-
const rowChoices = opts.choices.slice(i, i + columns);
|
|
141
|
+
for (let i = 0; i < chips.length; i += columns) {
|
|
99
142
|
const row = new FlexBox({ direction: 'row', align: 'center', gap });
|
|
100
|
-
for (let j = 0; j <
|
|
101
|
-
const
|
|
102
|
-
const chipIndex = i + j;
|
|
103
|
-
const chip = new Chip(host, c.id, c.label, chipIndex === focusIndex, em, (id) => {
|
|
104
|
-
const idx = opts.choices.findIndex((ch) => ch.id === id);
|
|
105
|
-
if (idx >= 0) setHighlight(idx);
|
|
106
|
-
});
|
|
143
|
+
for (let j = 0; j < columns && i + j < chips.length; j++) {
|
|
144
|
+
const chip = chips[i + j];
|
|
107
145
|
chip.setLayoutSize(colW, undefined);
|
|
108
|
-
chips.push(chip);
|
|
109
146
|
row.add(chip);
|
|
110
147
|
}
|
|
111
|
-
row.layout();
|
|
112
148
|
grid.add(row);
|
|
113
149
|
}
|
|
114
|
-
|
|
150
|
+
|
|
151
|
+
// Cap the grid height and scroll when the ladder overflows — the Pixi analogue of the HTML
|
|
152
|
+
// shell's `max-height:min(50vh,28em); overflow-y:auto`. (Mask hit-testing clips to the viewport
|
|
153
|
+
// but still lets clicks through to chips inside it — verified against pixi.js 8.16's
|
|
154
|
+
// EventBoundary.) When it fits, drop the scroll box entirely so nothing is masked needlessly.
|
|
155
|
+
const gridH = grid.measureSize().h;
|
|
156
|
+
const maxGridH = Math.min(host.screenH * 0.5, 28 * em);
|
|
157
|
+
if (opts.autoFit && gridH > maxGridH) {
|
|
158
|
+
const scroll = new SizedScrollBox(innerW, maxGridH, host.canvas);
|
|
159
|
+
scroll.content.addChild(grid);
|
|
160
|
+
scroll.refresh();
|
|
161
|
+
modal.body.add(scroll);
|
|
162
|
+
} else {
|
|
163
|
+
modal.body.add(grid);
|
|
164
|
+
}
|
|
115
165
|
|
|
116
166
|
modal.setActions([
|
|
117
167
|
{
|
|
@@ -130,6 +180,7 @@ export function openBetPicker(host: PixiComponentContext): ShellLayer {
|
|
|
130
180
|
tag: 'bet-modal',
|
|
131
181
|
title: host.t('Bet'),
|
|
132
182
|
columns: { wide: 6, mobile: 3 },
|
|
183
|
+
autoFit: true, // reflow columns + scroll when a wide currency makes the labels grow
|
|
133
184
|
maxEm: 44, // wider card to fit 6 chips/row
|
|
134
185
|
confirmLabel: host.t('Confirm'),
|
|
135
186
|
choices: host.state.availableBets.map((b) => ({ id: String(b), label: host.fmt(b) })),
|