@open-slot-ui/core 0.0.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.
@@ -0,0 +1,1813 @@
1
+ /**
2
+ * Zero-dependency reactive primitives — the "M" substrate.
3
+ * Deliberately tiny: a value box you can subscribe to, plus an effect helper.
4
+ * No framework, no global graph, no magic. This is the whole reactivity story.
5
+ */
6
+ type Dispose = () => void;
7
+ interface Subscribable {
8
+ subscribe(fn: (value: never) => void): Dispose;
9
+ }
10
+ declare class Signal<T> {
11
+ private _value;
12
+ private readonly subs;
13
+ constructor(initial: T);
14
+ get(): T;
15
+ set(next: T): void;
16
+ /** Mutate-in-place then notify (for objects whose identity is stable). */
17
+ update(mutate: (value: T) => void): void;
18
+ subscribe(fn: (value: T) => void): Dispose;
19
+ get size(): number;
20
+ }
21
+ /**
22
+ * Run `fn` once now and again whenever any dependency changes.
23
+ * Dependencies are explicit (no auto-tracking) — keeps the model trivial and predictable.
24
+ */
25
+ declare function effect(fn: () => void, deps: Subscribable[]): Dispose;
26
+
27
+ /** A typed event map: event name -> payload type. */
28
+ type EventMap = Record<string, unknown>;
29
+ /**
30
+ * One typed event bus. Period. (Charter B1.)
31
+ * No magic strings sprinkled around the codebase; payloads are checked.
32
+ */
33
+ declare class EventBus<E extends EventMap> {
34
+ private readonly map;
35
+ on<K extends keyof E>(type: K, fn: (payload: E[K]) => void): Dispose;
36
+ emit<K extends keyof E>(type: K, payload: E[K]): void;
37
+ }
38
+
39
+ /** Shared types with no dependencies — safe for everything to import. */
40
+ interface Rect {
41
+ x: number;
42
+ y: number;
43
+ width: number;
44
+ height: number;
45
+ }
46
+ /**
47
+ * The façade's outbound event vocabulary (grows as controls are added).
48
+ * A `type` (not `interface`) so it satisfies the EventBus `Record` constraint.
49
+ */
50
+ type OpenUIEvents = {
51
+ /** The player asked to spin (spin control activated while interactable). */
52
+ spinRequested: void;
53
+ /** The player asked to slam-stop / skip the reels (autoplay-engaged tap). */
54
+ skipRequested: void;
55
+ /** A control changed state — useful for analytics / e2e logging. */
56
+ stateChanged: {
57
+ id: string;
58
+ from: string;
59
+ to: string;
60
+ };
61
+ /** A generic button was activated. */
62
+ buttonActivated: {
63
+ id: string;
64
+ };
65
+ /** A slider/value control changed (e.g. sound volume 0..1). */
66
+ valueChanged: {
67
+ id: string;
68
+ value: number;
69
+ };
70
+ /** A panel opened or closed. */
71
+ panelToggled: {
72
+ id: string;
73
+ open: boolean;
74
+ };
75
+ /** A toggle (e.g. turbo) flipped. `on` = "any mode past the first". */
76
+ toggled: {
77
+ id: string;
78
+ on: boolean;
79
+ };
80
+ /** A turbo control changed mode (covers 2-mode AND 3-mode cyclers). */
81
+ turboChanged: {
82
+ id: string;
83
+ mode: string;
84
+ index: number;
85
+ };
86
+ /** A select control chose an option (its own typed event — never overloads valueChanged). */
87
+ optionSelected: {
88
+ id: string;
89
+ value: string;
90
+ index: number;
91
+ };
92
+ /** Autoplay started with a chosen count (Infinity = ∞) + optional RG limits. */
93
+ autoplayStarted: {
94
+ count: number;
95
+ lossLimit?: number;
96
+ singleWinLimit?: number;
97
+ };
98
+ /** Autoplay stopped. */
99
+ autoplayStopped: void;
100
+ /** The player began a press-and-hold turbo spin on the spin button. */
101
+ holdSpinStarted: void;
102
+ /** The player released the held spin button (stop the turbo loop). */
103
+ holdSpinStopped: void;
104
+ /** A control was hidden or shown at runtime (e.g. a responsive breakpoint change). */
105
+ visibilityChanged: {
106
+ id: string;
107
+ hidden: boolean;
108
+ };
109
+ /** A buy-feature card was activated. */
110
+ cardActivated: {
111
+ id: string;
112
+ };
113
+ /** Locale was changed via the façade. */
114
+ localeChanged: string;
115
+ /** A reality-check interval elapsed (RTS 13). `minutes` = the configured interval;
116
+ * `elapsedMs` = wall-clock since the last check; totals are session aggregates. */
117
+ realityCheck: {
118
+ minutes: number;
119
+ elapsedMs: number;
120
+ totalStaked: number;
121
+ totalWon: number;
122
+ };
123
+ /** The notice / error modal was shown. */
124
+ noticeShown: {
125
+ blocking: boolean;
126
+ };
127
+ /** The notice / error modal was dismissed. */
128
+ noticeDismissed: void;
129
+ };
130
+ /** A serializable snapshot of one control, for the e2e/introspection API. */
131
+ interface ControlSnapshot {
132
+ id: string;
133
+ role: string;
134
+ state: string;
135
+ interactable: boolean;
136
+ bounds: Rect | null;
137
+ animating: boolean;
138
+ }
139
+
140
+ /**
141
+ * Theming = data. Semantic tokens with safe fallbacks (Charter P8).
142
+ * A game theme is a small override map; nothing renders un-themed because
143
+ * the renderer always has these defaults to fall back on.
144
+ */
145
+ interface Theme {
146
+ color: {
147
+ accent: string;
148
+ accentText: string;
149
+ surface: string;
150
+ surfaceAlt: string;
151
+ text: string;
152
+ textDim: string;
153
+ disabled: string;
154
+ };
155
+ radius: {
156
+ pill: number;
157
+ card: number;
158
+ };
159
+ space: {
160
+ sm: number;
161
+ md: number;
162
+ lg: number;
163
+ };
164
+ type: {
165
+ family: string;
166
+ size: {
167
+ sm: number;
168
+ md: number;
169
+ lg: number;
170
+ };
171
+ };
172
+ /** Motion durations in ms. */
173
+ motion: {
174
+ fast: number;
175
+ base: number;
176
+ slow: number;
177
+ };
178
+ }
179
+ /** Neutral reference theme. The real game theme is built on top of this. */
180
+ declare const defaultTheme: Theme;
181
+ type DeepPartial<T> = {
182
+ [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
183
+ };
184
+ /** Produce a new theme from the default (or any base) plus an override patch. */
185
+ declare function extendTheme(base: Theme, patch: DeepPartial<Theme>): Theme;
186
+
187
+ /** Responsive screen model: orientation, a named breakpoint, and a fit scale. */
188
+ type Orientation = 'landscape' | 'portrait';
189
+ /** A named device bucket. Config can target these to restyle per device. */
190
+ type Breakpoint = 'mobile' | 'tablet' | 'desktop';
191
+ interface ScreenState {
192
+ width: number;
193
+ height: number;
194
+ orientation: Orientation;
195
+ /** Named device bucket, derived from the shorter screen edge (see LayoutConfig). */
196
+ breakpoint: Breakpoint;
197
+ /** Multiply reference-px sizes by this to fit the current screen. */
198
+ scale: number;
199
+ }
200
+ interface LayoutConfig {
201
+ /** Reference design resolution for landscape / portrait. */
202
+ refLandscape: [number, number];
203
+ refPortrait: [number, number];
204
+ /** Aspect ratio (w/h) below which we switch to the portrait layout. */
205
+ portraitBelowAspect: number;
206
+ /**
207
+ * Breakpoint thresholds on the SHORTER screen edge (px). A phone is "mobile" in
208
+ * both orientations because its short edge stays small — the right intuition for
209
+ * "what device is this". `<= mobile` → mobile, `<= tablet` → tablet, else desktop.
210
+ */
211
+ breakpoints: {
212
+ mobile: number;
213
+ tablet: number;
214
+ };
215
+ }
216
+ declare const defaultLayoutConfig: LayoutConfig;
217
+ /** Classify the shorter edge into a named device bucket. Pure, total. */
218
+ declare function breakpointFor(width: number, height: number, cfg: LayoutConfig): Breakpoint;
219
+ declare function computeScreen(width: number, height: number, cfg: LayoutConfig): ScreenState;
220
+
221
+ /** Explicit anchors — no magic numbers buried in components (Charter P9). */
222
+ type Anchor = 'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
223
+ interface LayoutSpec {
224
+ anchor: Anchor;
225
+ /** Offset from the anchor, in reference px (scaled with the screen). */
226
+ offset?: [number, number];
227
+ /** Extra scale multiplier on top of the screen's fit scale. */
228
+ scale?: number;
229
+ /** Rotation in degrees, clockwise. Default 0. */
230
+ rotation?: number;
231
+ }
232
+ interface Placement {
233
+ x: number;
234
+ y: number;
235
+ scale: number;
236
+ /** Rotation in radians (resolved from `LayoutSpec.rotation`, which is degrees). */
237
+ rotation: number;
238
+ }
239
+ /** Resolve a control's layout against the current screen. Pure math. */
240
+ declare function resolvePlacement(spec: LayoutSpec, screen: ScreenState): Placement;
241
+
242
+ /**
243
+ * Transitions are composable OOP descriptors (Charter B6) — pure data, renderer-agnostic.
244
+ * The renderer interprets the `kind`; the core never animates anything itself.
245
+ * Some controls turn, some squish, some tick up, some emit particles — all from these.
246
+ */
247
+ declare class Squish {
248
+ scale: number;
249
+ ms: number;
250
+ readonly kind: "squish";
251
+ constructor(scale?: number, ms?: number);
252
+ }
253
+ declare class Pulse {
254
+ scale: number;
255
+ ms: number;
256
+ readonly kind: "pulse";
257
+ constructor(scale?: number, ms?: number);
258
+ }
259
+ declare class Turn {
260
+ rps: number;
261
+ loop: boolean;
262
+ readonly kind: "turn";
263
+ /** Rotations per second; loops until the state changes. */
264
+ constructor(rps?: number, loop?: boolean);
265
+ }
266
+ declare class Fade {
267
+ alpha: number;
268
+ ms: number;
269
+ readonly kind: "fade";
270
+ constructor(alpha?: number, ms?: number);
271
+ }
272
+ declare class Sequence {
273
+ steps: Transition[];
274
+ readonly kind: "sequence";
275
+ constructor(steps: Transition[]);
276
+ }
277
+ declare class Parallel {
278
+ steps: Transition[];
279
+ readonly kind: "parallel";
280
+ constructor(steps: Transition[]);
281
+ }
282
+ type Transition = Squish | Pulse | Turn | Fade | Sequence | Parallel;
283
+
284
+ /** One state of a control: is it interactable, and what plays on entry. */
285
+ interface StateDef {
286
+ interactable: boolean;
287
+ transition?: Transition;
288
+ }
289
+ type StateMap = Record<string, StateDef>;
290
+ interface ControlOptions {
291
+ id: string;
292
+ role: string;
293
+ layout: LayoutSpec;
294
+ }
295
+ /** What a view reports back for introspection. Set by the renderer's view. */
296
+ type ViewInspect = () => {
297
+ bounds: Rect | null;
298
+ animating: boolean;
299
+ };
300
+ /**
301
+ * Base of every control: a finite state machine that is the single source of
302
+ * truth for look, interactivity, AND tests (Charter P5/P6/P12).
303
+ * Renderer-agnostic — it knows nothing about Pixi or the DOM.
304
+ */
305
+ declare abstract class Control {
306
+ readonly id: string;
307
+ readonly role: string;
308
+ layout: LayoutSpec;
309
+ /** Declared by the subclass: the legal states and their transitions. */
310
+ abstract readonly states: StateMap;
311
+ /** Current state name, observable. */
312
+ readonly state: Signal<string>;
313
+ /** The renderer sets this so `inspect()` can report bounds/animation. */
314
+ viewInspect?: ViewInspect;
315
+ /**
316
+ * Optional host gate (e.g. a ref-counted input lock). When set, the control is
317
+ * interactable only if its state allows it AND the gate returns true. Set per
318
+ * instance — by OpenUI on register — so multiple HUDs on one page never share
319
+ * lock state. Interactability stays *derived*, never stored (Charter P6).
320
+ */
321
+ gate?: () => boolean;
322
+ protected readonly disposers: Dispose[];
323
+ private readonly transitionSubs;
324
+ constructor(opts: ControlOptions, initial: string);
325
+ get current(): string;
326
+ /** Derived, never stored (Charter P6). Folds in the optional host gate. */
327
+ get interactable(): boolean;
328
+ /** Transition to a new state. Throws on unknown states (illegal states unrepresentable). */
329
+ setState(to: string): void;
330
+ /** The renderer subscribes to play the entry transition for each state change. */
331
+ onTransition(fn: (t: Transition | undefined, to: string, from: string) => void): Dispose;
332
+ inspect(): {
333
+ bounds: Rect | null;
334
+ animating: boolean;
335
+ };
336
+ /** Leak-free teardown (Charter P12). */
337
+ dispose(): void;
338
+ }
339
+
340
+ /**
341
+ * The spin control — the vertical-spine exemplar.
342
+ * A full state machine: each state declares interactability + an entry transition.
343
+ * The game drives it via the façade (`busy/idle/stop/disable`); the view drives
344
+ * the transient input states (`hover/pressed`) and calls `activate()` on release.
345
+ */
346
+ declare class SpinControl extends Control {
347
+ private readonly bus;
348
+ readonly states: StateMap;
349
+ /**
350
+ * When true, the view arms press-and-hold: holding the spin button fires a
351
+ * `holdSpinStarted` and the host turbo-spins until release (`holdSpinStopped`).
352
+ * A quick tap still spins once. Toggleable via the `spin.press` config.
353
+ */
354
+ holdToSpin: boolean;
355
+ /**
356
+ * Whether tapping mid-spin slam-stops/skips the reels. Stake Engine's
357
+ * `disabledSlamstop` jurisdiction flag sets this false → the button then LOCKS
358
+ * (dims, no tap) during a spin instead of skipping. Observable so the view can
359
+ * re-dim if it flips at runtime (e.g. on `applyJurisdiction`).
360
+ */
361
+ readonly allowSlamStop: Signal<boolean>;
362
+ /**
363
+ * Whether the keyboard (Space / Enter) can trigger a spin. Stake's
364
+ * `disabledSpacebar` sets this false (and turns off hold-to-spin). The renderer
365
+ * reads it; RTS 14D requires a fresh press per cycle (no auto-repeat).
366
+ */
367
+ readonly allowKeyboard: Signal<boolean>;
368
+ /**
369
+ * Remaining free spins (0 = normal play). Set it > 0 and the spin button switches
370
+ * to its free-spins face — a big remaining count over an "FS" label — via the view;
371
+ * back to 0 returns the normal play button. The host drives the loop; this is just
372
+ * the display. Set with {@link setFreeSpins}.
373
+ */
374
+ readonly freeSpins: Signal<number>;
375
+ constructor(opts: {
376
+ id?: string;
377
+ layout: LayoutSpec;
378
+ holdToSpin?: boolean;
379
+ }, bus: EventBus<OpenUIEvents>);
380
+ /**
381
+ * Derived (Charter P6), with the slam-stop guard folded in: when slam-stop is
382
+ * disabled, the in-spin `stop`/`auto` affordance is locked — the button can't be
383
+ * tapped to skip; it just dims.
384
+ */
385
+ get interactable(): boolean;
386
+ busy(): void;
387
+ idle(): void;
388
+ stopState(): void;
389
+ /** Show the autoplay-engaged look (press to stop). */
390
+ auto(): void;
391
+ enable(): void;
392
+ disable(): void;
393
+ activate(): void;
394
+ /** View calls this when a press is held past the hold threshold. Returns whether
395
+ * hold-to-spin engaged (so the view knows to wait for release). */
396
+ holdBegin(): boolean;
397
+ /** View calls this on release after a hold — the host stops the turbo loop. */
398
+ holdEnd(): void;
399
+ /** Switch the button to / from its free-spins face. `n > 0` shows "n FS"; `0`
400
+ * restores the normal play button. Non-number/negative is clamped to 0 (P11). */
401
+ setFreeSpins(n: number): void;
402
+ }
403
+
404
+ /** Currency / crypto descriptor — the code/symbol + how to format it. */
405
+ interface CurrencySpec {
406
+ /** ISO code or crypto name: "USD", "EUR", "BTC", "mBTC", "SATS". */
407
+ code: string;
408
+ /**
409
+ * The minimal-unit precision — how many fractional digits show after the `.`:
410
+ * 2 (USD/EUR cents), 0 (JPY/SATS), 5 (mBTC), 8 (BTC). Values are held + rolled as
411
+ * integer minor units (no float drift). Clamped to 0..8.
412
+ */
413
+ decimals: number;
414
+ /** Symbol for `display: 'symbol'` (e.g. "$", "€", "₿"). */
415
+ symbol?: string;
416
+ /** Show the ISO/crypto `code` (default) or the `symbol` (when one is set). */
417
+ display?: 'code' | 'symbol';
418
+ /** Affix on the left or right of the number. Default 'suffix'. */
419
+ position?: 'prefix' | 'suffix';
420
+ /** Thousands separator char. Default ','. */
421
+ separator?: string;
422
+ /** Decimal point char. Default '.'. */
423
+ decimalChar?: string;
424
+ }
425
+ interface ValueDisplayOptions {
426
+ id: string;
427
+ layout: LayoutSpec;
428
+ currency: CurrencySpec;
429
+ /** Optional caption ("Balance" / "Bet"). */
430
+ label?: string;
431
+ /** Initial value, in major units (e.g. 1234.56). */
432
+ initial?: number;
433
+ /** Optional MINIMUM total column width for the rolling counter (integer + fraction).
434
+ * `0`/unset = auto: the odometer is sized tightly to the value and grows as it does,
435
+ * so the currency symbol hugs the number and large balances never clamp. Set it only
436
+ * to reserve a stable, wider odometer. Clamped 0..18. Default 0 (auto). */
437
+ digits?: number;
438
+ }
439
+ /**
440
+ * A value display: a number plus a currency/crypto code. Not interactive — the
441
+ * host sets the value (`ui.balance.set(...)`) and the renderer animates it.
442
+ * Value is held in major units; `minorUnits` is the integer the counter rolls.
443
+ */
444
+ declare class ValueDisplay extends Control {
445
+ readonly states: StateMap;
446
+ readonly value: Signal<number>;
447
+ readonly currency: Signal<CurrencySpec>;
448
+ readonly label?: string;
449
+ /** Minimum odometer column width (0 = auto: sized tightly to the value, grows as it
450
+ * does). Read by the renderer at build time, so set it before mount (or declaratively
451
+ * via `controls.{id}.digits`). */
452
+ digits: number;
453
+ constructor(opts: ValueDisplayOptions);
454
+ /** Set the minimum odometer column width (clamped 0..18; 0 = auto). Read by the
455
+ * renderer at build time, so set it before mount (or via `controls.{id}.digits`). */
456
+ setDigits(n: number): void;
457
+ /** Never-reject: NaN/Infinity/non-number degrade to a no-op, keeping the last
458
+ * good value — malformed host data never throws into the render loop (P11). */
459
+ set(amount: number): void;
460
+ get(): number;
461
+ /** Keep the prior spec on malformed input; clamp decimals to 0..8 (P11). */
462
+ setCurrency(spec: CurrencySpec): void;
463
+ /** Integer minor units for the counter (no float drift). */
464
+ get minorUnits(): number;
465
+ }
466
+
467
+ interface ButtonOptions {
468
+ id: string;
469
+ layout: LayoutSpec;
470
+ /** Optional text label (e.g. "Rules"). */
471
+ label?: string;
472
+ /** ARIA-ish role; default 'button'. */
473
+ role?: string;
474
+ }
475
+ /**
476
+ * A generic tap button: idle / hover / pressed / disabled, emitting
477
+ * `buttonActivated` on a valid press-release. Used for settings, rules, close, …
478
+ */
479
+ declare class ButtonControl extends Control {
480
+ private readonly bus?;
481
+ readonly states: StateMap;
482
+ readonly label?: string;
483
+ constructor(opts: ButtonOptions, bus?: EventBus<OpenUIEvents> | undefined);
484
+ enable(): void;
485
+ disable(): void;
486
+ /** The view calls this on a valid press-release. */
487
+ activate(): void;
488
+ }
489
+
490
+ interface SliderOptions {
491
+ id: string;
492
+ layout: LayoutSpec;
493
+ /** Initial value, 0..1. Default 0.5. */
494
+ initial?: number;
495
+ label?: string;
496
+ }
497
+ /**
498
+ * A 0..1 slider (sound volume, etc.). The view drives `setNormalized` while
499
+ * dragging; the lib holds the value and emits `valueChanged`. The host applies
500
+ * the actual effect (e.g. sets audio volume).
501
+ */
502
+ declare class SliderControl extends Control {
503
+ private readonly bus?;
504
+ readonly states: StateMap;
505
+ readonly value: Signal<number>;
506
+ readonly label?: string;
507
+ constructor(opts: SliderOptions, bus?: EventBus<OpenUIEvents> | undefined);
508
+ /** Set the normalized value (0..1) and notify. */
509
+ setNormalized(v: number): void;
510
+ beginDrag(): void;
511
+ endDrag(): void;
512
+ }
513
+
514
+ type PanelVariant = 'modal' | 'popover';
515
+ interface PanelOptions {
516
+ id: string;
517
+ variant: PanelVariant;
518
+ layout?: LayoutSpec;
519
+ /** Title shown at the top of a modal. */
520
+ title?: string;
521
+ }
522
+ /**
523
+ * A panel that opens/closes. `modal` = full-screen backdrop + scrollable content
524
+ * + close button; `popover` = small anchored cluster (e.g. the settings flyout).
525
+ * State is the single source of truth (open|closed); the view animates it.
526
+ */
527
+ declare class PanelControl extends Control {
528
+ private readonly bus?;
529
+ readonly states: StateMap;
530
+ readonly variant: PanelVariant;
531
+ readonly title?: string;
532
+ constructor(opts: PanelOptions, bus?: EventBus<OpenUIEvents> | undefined);
533
+ get isOpen(): boolean;
534
+ openPanel(): void;
535
+ closePanel(): void;
536
+ toggle(): void;
537
+ }
538
+
539
+ interface ToggleOptions {
540
+ id: string;
541
+ layout: LayoutSpec;
542
+ /** Initial on/off. Default false. */
543
+ on?: boolean;
544
+ }
545
+ /**
546
+ * An on/off toggle (e.g. Turbo). State IS the value: `off` | `on`. Emits `toggled`.
547
+ * The view skins each state (e.g. the Turbo off/on frames).
548
+ */
549
+ declare class ToggleControl extends Control {
550
+ private readonly bus?;
551
+ readonly states: StateMap;
552
+ constructor(opts: ToggleOptions, bus?: EventBus<OpenUIEvents> | undefined);
553
+ get isOn(): boolean;
554
+ set(on: boolean): void;
555
+ /** Called by the view on a valid press-release. */
556
+ toggle(): void;
557
+ enable(): void;
558
+ disable(): void;
559
+ }
560
+
561
+ interface TurboOptions {
562
+ id: string;
563
+ layout: LayoutSpec;
564
+ /**
565
+ * The ordered modes this switcher cycles through. The FIRST mode is "off".
566
+ * Two modes (`['off','on']`) behave exactly like a plain on/off toggle; three
567
+ * (`['off','turbo','super']`) make a 3-mode switcher. Default `['off','on']`.
568
+ */
569
+ modes?: string[];
570
+ /** Initial mode index. Default 0 (off). */
571
+ index?: number;
572
+ }
573
+ /**
574
+ * Turbo switcher. Each press advances to the next mode and wraps back to "off"
575
+ * — so the SAME control is a 2-mode toggle or a 3-mode (off / turbo / super)
576
+ * switcher purely by configuration. The current FSM state IS the current mode,
577
+ * so look + interactivity + tests all read from one place (Charter P5).
578
+ *
579
+ * Emits both `turboChanged` (mode + index, the full story) and `toggled`
580
+ * (`on` = "any mode past off") so existing 2-mode listeners keep working.
581
+ */
582
+ declare class TurboControl extends Control {
583
+ private readonly bus?;
584
+ states: StateMap;
585
+ private _modes;
586
+ /** Current mode index, observable (0 = off). */
587
+ readonly index: Signal<number>;
588
+ constructor(opts: TurboOptions, bus?: EventBus<OpenUIEvents> | undefined);
589
+ private static buildStates;
590
+ /** The ordered modes (the first is "off"). */
591
+ get modes(): string[];
592
+ /** Number of modes (2 = toggle, 3 = three-mode switcher). */
593
+ get modeCount(): number;
594
+ /** The current mode name. */
595
+ get mode(): string;
596
+ /** True for any mode past "off" — the 2-mode `isOn` semantics, preserved. */
597
+ get isOn(): boolean;
598
+ /** Replace the mode ladder (e.g. switch a game from 2-mode to 3-mode at runtime). */
599
+ setModes(modes: string[]): void;
600
+ /** Advance to the next mode, wrapping back to "off". The view's tap handler. */
601
+ cycle(): void;
602
+ /** Jump to a specific mode index (clamped). */
603
+ setIndex(i: number): void;
604
+ /** 2-mode convenience: off (index 0) or on (index 1). */
605
+ set(on: boolean): void;
606
+ /** Alias kept so old `toggle()` callers compile — same as `cycle()`. */
607
+ toggle(): void;
608
+ private goTo;
609
+ enable(): void;
610
+ disable(): void;
611
+ }
612
+
613
+ interface StepperOptions {
614
+ id: string;
615
+ layout: LayoutSpec;
616
+ /** Ordered list of allowed values (e.g. bet levels). */
617
+ levels: number[];
618
+ /** Starting index into `levels`. Default 0. */
619
+ index?: number;
620
+ /** Optional caption (e.g. "Bet"). */
621
+ label?: string;
622
+ }
623
+ /**
624
+ * Steps through a host-provided ordered list of values (bet levels). `inc`/`dec`
625
+ * move the index and clamp at the ends; `canInc`/`canDec` drive the +/- buttons'
626
+ * enabled state. Emits `valueChanged` with the resolved value.
627
+ */
628
+ declare class StepperControl extends Control {
629
+ private readonly bus?;
630
+ readonly states: StateMap;
631
+ readonly index: Signal<number>;
632
+ /** Optional caption (e.g. "Bet"). */
633
+ readonly label?: string;
634
+ private levels;
635
+ constructor(opts: StepperOptions, bus?: EventBus<OpenUIEvents> | undefined);
636
+ private clampIndex;
637
+ get value(): number;
638
+ get canInc(): boolean;
639
+ get canDec(): boolean;
640
+ inc(): void;
641
+ dec(): void;
642
+ setIndex(i: number): void;
643
+ /** Replace the level list (e.g. when the currency/limits change). */
644
+ setLevels(levels: number[], index?: number): void;
645
+ }
646
+
647
+ /**
648
+ * How tapping autoplay behaves:
649
+ * - `'options'` — tap opens a picker (a bottom drawer) to choose a spin count, then Start.
650
+ * - `'infinite'` — tap starts infinite autoplay immediately; tap again stops. No picker.
651
+ */
652
+ type AutoplayMode = 'options' | 'infinite';
653
+ /** Responsible-gambling stop limits for an autoplay session (multiples of bet). */
654
+ interface AutoplayLimits {
655
+ /** Stop when cumulative net loss reaches `lossLimit × bet`. Infinity = no limit. */
656
+ lossLimit?: number;
657
+ /** Stop on a single win ≥ `singleWinLimit × bet`. Infinity = no limit. */
658
+ singleWinLimit?: number;
659
+ }
660
+ interface AutoplayOptions {
661
+ id: string;
662
+ layout: LayoutSpec;
663
+ /** Count choices shown in the picker. Use Infinity for "∞". */
664
+ options?: number[];
665
+ /** Tap behavior. Default `'options'`. */
666
+ mode?: AutoplayMode;
667
+ /**
668
+ * Optional responsible-gambling limit choices offered in the picker (multiples
669
+ * of the bet; Infinity = "no limit"). When present, the picker shows a
670
+ * "stop if total loss reaches N×" / "stop on a single win ≥ N×" row, and the
671
+ * host must feed each round's outcome via {@link AutoplayControl.reportResult}.
672
+ */
673
+ lossLimitOptions?: number[];
674
+ winLimitOptions?: number[];
675
+ }
676
+ /**
677
+ * Autoplay button. In `'options'` mode, tapping (idle) opens a count picker;
678
+ * choosing a count starts autoplay. In `'infinite'` mode, tapping starts infinite
679
+ * autoplay straight away. While active it shows the live remaining count (fed by
680
+ * the host via `setCount`); tapping stops. open-ui owns the picker UI + the
681
+ * displayed count; the host runs the actual spin loop.
682
+ */
683
+ declare class AutoplayControl extends Control {
684
+ private readonly bus?;
685
+ readonly states: StateMap;
686
+ /** Remaining spins while active (Infinity = ∞). 0 when not active. */
687
+ readonly count: Signal<number>;
688
+ private _options;
689
+ private _lossLimitOptions;
690
+ private _winLimitOptions;
691
+ /** Active stop-on-total-loss (multiples of bet; Infinity = no limit). */
692
+ lossLimit: number;
693
+ /** Active stop-on-single-win (multiples of bet; Infinity = no limit). */
694
+ singleWinLimit: number;
695
+ /** Running net loss this autoplay session (bet − win, summed). */
696
+ private netLoss;
697
+ /** Tap behavior (`'options'` opens the picker, `'infinite'` starts immediately). */
698
+ mode: AutoplayMode;
699
+ constructor(opts: AutoplayOptions, bus?: EventBus<OpenUIEvents> | undefined);
700
+ /** Count choices shown in the picker (Infinity allowed = ∞). */
701
+ get options(): number[];
702
+ /** Loss-limit choices for the picker (multiples of bet; Infinity = no limit). */
703
+ get lossLimitOptions(): number[];
704
+ /** Single-win-stop choices for the picker (multiples of bet; Infinity = no limit). */
705
+ get winLimitOptions(): number[];
706
+ /** Replace the picker's count choices (e.g. when limits change). */
707
+ setOptions(options: number[]): void;
708
+ /** Replace the picker's loss-limit / single-win-stop choices. */
709
+ setLossLimitOptions(options: number[]): void;
710
+ setWinLimitOptions(options: number[]): void;
711
+ get isActive(): boolean;
712
+ openPicker(): void;
713
+ cancelPicker(): void;
714
+ /** Choose a count (+ optional RG limits) from the picker → start autoplay. */
715
+ pick(count: number, limits?: AutoplayLimits): void;
716
+ /** Start autoplay with a count (Infinity = ∞) + optional RG limits, from idle or
717
+ * the picker. The limits are enforced as the host reports each round's outcome
718
+ * via {@link reportResult}. */
719
+ begin(count: number, limits?: AutoplayLimits): void;
720
+ /**
721
+ * The host reports one completed autoplay round's outcome (major units) — this is
722
+ * the responsible-gambling guardrail. It advances the remaining count and stops
723
+ * autoplay if: the count is exhausted, the cumulative net loss reaches the
724
+ * loss-limit, or this single win reaches the single-win-stop. No-op when autoplay
725
+ * isn't active or the inputs are malformed (Charter P11).
726
+ */
727
+ reportResult(win: number, bet: number): void;
728
+ stop(): void;
729
+ /** Host updates the remaining count during play. Infinity is valid (∞); only
730
+ * NaN/non-number is rejected, so bad host data never corrupts the badge (P11). */
731
+ setCount(n: number): void;
732
+ /** The view's tap handler. In `'infinite'` mode, idle starts straight away;
733
+ * in `'options'` mode, idle opens the picker. Active always stops. */
734
+ press(): void;
735
+ disable(): void;
736
+ enable(): void;
737
+ }
738
+
739
+ /**
740
+ * What a readout shows:
741
+ * - `'percent'` → a number + "%" (RTP disclosure — `displayRTP`)
742
+ * - `'currency'` → a signed currency amount (net position — `displayNetPosition`)
743
+ * - `'duration'` → an elapsed clock the view ticks (session timer — `displaySessionTimer`)
744
+ * - `'plain'` → the grouped number
745
+ */
746
+ type ReadoutKind = 'percent' | 'currency' | 'duration' | 'plain';
747
+ interface ReadoutOptions {
748
+ id: string;
749
+ layout: LayoutSpec;
750
+ kind?: ReadoutKind;
751
+ label?: string;
752
+ /** Initial value (percent: 96, currency: major units, duration: seconds). */
753
+ value?: number;
754
+ /** Decimals for `percent`/`plain`. Default 1 for percent, 0 otherwise. */
755
+ decimals?: number;
756
+ /** Currency descriptor for the `currency` kind. */
757
+ currency?: CurrencySpec;
758
+ /** Whether a `currency` readout shows an explicit +/- sign. Default true. */
759
+ signed?: boolean;
760
+ }
761
+ /**
762
+ * A small, non-interactive text readout for the compliance display elements Stake
763
+ * Engine's jurisdiction config can mandate: RTP (`displayRTP`), net position
764
+ * (`displayNetPosition`), and the session timer (`displaySessionTimer`). It is the
765
+ * single source of truth for the value; the renderer formats it (and, for
766
+ * `'duration'`, ticks it). Hidden by default — `applyJurisdiction` reveals the ones
767
+ * a jurisdiction requires. Never-reject inbound boundary (Charter P11).
768
+ */
769
+ declare class ReadoutControl extends Control {
770
+ readonly states: StateMap;
771
+ readonly value: Signal<number>;
772
+ /** Whether a `'duration'` readout is currently advancing. */
773
+ readonly running: Signal<boolean>;
774
+ readonly kind: ReadoutKind;
775
+ readonly label?: string;
776
+ readonly decimals: number;
777
+ readonly signed: boolean;
778
+ /** Present for the `'currency'` kind so the view can format + react to changes. */
779
+ readonly currency?: Signal<CurrencySpec>;
780
+ constructor(opts: ReadoutOptions);
781
+ /** Never-reject: malformed input keeps the last good value (P11). */
782
+ set(v: number): void;
783
+ get(): number;
784
+ setCurrency(spec: CurrencySpec): void;
785
+ /** Advance an elapsed-time readout by `deltaSec` (the renderer calls this each frame). */
786
+ tick(deltaSec: number): void;
787
+ start(): void;
788
+ stop(): void;
789
+ reset(): void;
790
+ }
791
+
792
+ interface CardOptions {
793
+ id: string;
794
+ layout: LayoutSpec;
795
+ /** Card title (e.g. the feature name). */
796
+ title?: string;
797
+ /** Price / cost shown on the card (formatted string or number). */
798
+ price?: string;
799
+ /** Asset id/key for the card image (resolved by the view/host). */
800
+ image?: string;
801
+ }
802
+ /**
803
+ * A selectable buy-feature card: image + title + price + a buy action.
804
+ * Emits `cardActivated` on a valid press-release. Lives inside a buy-feature panel.
805
+ */
806
+ declare class CardControl extends Control {
807
+ private readonly bus?;
808
+ readonly states: StateMap;
809
+ title?: string;
810
+ price?: string;
811
+ image?: string;
812
+ constructor(opts: CardOptions, bus?: EventBus<OpenUIEvents> | undefined);
813
+ enable(): void;
814
+ disable(): void;
815
+ /** The view calls this on a valid press-release. */
816
+ activate(): void;
817
+ }
818
+
819
+ interface SelectOption {
820
+ value: string;
821
+ label: string;
822
+ }
823
+ interface SelectControlOptions {
824
+ id: string;
825
+ layout: LayoutSpec;
826
+ options: SelectOption[];
827
+ index?: number;
828
+ /** Caption shown beside the control (e.g. "Language"). */
829
+ label?: string;
830
+ role?: string;
831
+ }
832
+ /**
833
+ * A single-choice select (language, quality, …). FSM peer of StepperControl:
834
+ * `closed | open | disabled`. Tapping cycles or opens a list; all logic is
835
+ * headless and the renderer just draws it. Emits its OWN typed `optionSelected`
836
+ * event rather than overloading the numeric `valueChanged` (Charter B1, P5).
837
+ */
838
+ declare class SelectControl extends Control {
839
+ private readonly bus?;
840
+ readonly states: StateMap;
841
+ readonly index: Signal<number>;
842
+ /** Caption (e.g. "Language") — distinct from the selected option's label. */
843
+ readonly caption?: string;
844
+ private _options;
845
+ constructor(opts: SelectControlOptions, bus?: EventBus<OpenUIEvents> | undefined);
846
+ private clamp;
847
+ get options(): ReadonlyArray<SelectOption>;
848
+ get value(): string;
849
+ get optionLabel(): string;
850
+ get isOpen(): boolean;
851
+ openList(): void;
852
+ closeList(): void;
853
+ /** Choose by value (the view's list-item handler). Closes the list. */
854
+ choose(value: string): void;
855
+ /** Jump to an index (clamped). Emits only if it changed. */
856
+ setIndex(i: number): void;
857
+ /** Advance to the next option, wrapping (the view's cycle-on-tap handler). */
858
+ cycle(): void;
859
+ /** Replace the option list (e.g. when limits or locale change). */
860
+ setOptions(options: SelectOption[], index?: number): void;
861
+ enable(): void;
862
+ disable(): void;
863
+ }
864
+
865
+ /** Built-in presets. open-ui ships a SINGLE canonical look — black & white with a
866
+ * yellow accent (the `default` token theme). Hosts restyle via `ThemeOverrides`
867
+ * (sanitized colours/radius/font), so the library stays themeable without shipping
868
+ * a palette zoo. Unknown preset names resolve to `default`. */
869
+ type ThemePreset = 'default';
870
+ /**
871
+ * The SAFE, curated surface a host may override (Charter P8 + "configure, don't
872
+ * break"). Deliberately a subset of the full `Theme`: colours, corner radii, and
873
+ * the font family — the look knobs. Structural tokens (spacing, motion timing,
874
+ * font sizes) are NOT exposed, so a theme can restyle the HUD but cannot break its
875
+ * layout or timing. Every value is validated/clamped at runtime too (see below).
876
+ */
877
+ interface ThemeOverrides {
878
+ color?: Partial<Theme['color']>;
879
+ radius?: Partial<Theme['radius']>;
880
+ type?: {
881
+ family?: string;
882
+ };
883
+ }
884
+ /** A reportable theme problem (structurally a `SpecIssue`, kept dependency-free). */
885
+ interface ThemeIssue {
886
+ level: 'warn' | 'error';
887
+ path: string;
888
+ code: string;
889
+ message: string;
890
+ }
891
+ /**
892
+ * What a host can pass for `theme`: a preset name, a safe override (layered on the
893
+ * default), or `{ preset, overrides }` to layer a safe override on a chosen preset.
894
+ */
895
+ type ThemeChoice = ThemePreset | ThemeOverrides | {
896
+ preset?: ThemePreset;
897
+ overrides?: ThemeOverrides;
898
+ };
899
+ /** The one ready-to-use theme: black & white with a yellow accent (`defaultTheme`). */
900
+ declare const themePresets: Readonly<Record<ThemePreset, Theme>>;
901
+ /** True for a colour string the renderer can safely use. */
902
+ declare function isSafeColor(v: unknown): v is string;
903
+ /**
904
+ * Turn host overrides into a guaranteed-safe `DeepPartial<Theme>`: invalid colours
905
+ * are dropped (the base token shows through), radii are clamped to a sane range,
906
+ * and a non-string font family is ignored. Each rejection is reported, never fatal.
907
+ */
908
+ declare function sanitizeThemeOverrides(ov: ThemeOverrides, onIssue?: (i: ThemeIssue) => void): DeepPartial<Theme>;
909
+ /**
910
+ * Resolve any `ThemeChoice` into a concrete, frozen Theme. Overrides are
911
+ * sanitized first, so a host can restyle the HUD but never break it: bad values
912
+ * fall back to the safe token (Charter P8/G5/P11). Pass `onIssue` to surface
913
+ * rejections (createUI forwards these to `HostHooks.onDataIssue`).
914
+ */
915
+ declare function resolveTheme(choice?: ThemeChoice, onIssue?: (i: ThemeIssue) => void): Theme;
916
+
917
+ /**
918
+ * Never-reject inbound boundary helpers (Charter P11). The UI *displays* values
919
+ * the host sets and computes no money — so malformed host data must degrade to a
920
+ * no-op, never throw into the render loop.
921
+ */
922
+ /** Return `n` if it is a finite number, else fall back to the last good value. */
923
+ declare function safeAmount(n: number, lastGood: number): number;
924
+ /** Clamp a currency's fractional digits to a sane 0..8 integer (default 2). */
925
+ declare function clampDecimals(d: number): number;
926
+ /** Clamp the value counter's fit WIDTH budget to a sane 1..18 column count (default 9). */
927
+ declare function clampDigits(n: number): number;
928
+ /** Clamp a value-display's MINIMUM column width: 0..18, where 0 means "auto" — size
929
+ * the odometer tightly to the value. Garbage → 0 (auto). */
930
+ declare function clampMinDigits(n: number): number;
931
+ /** How many integer (whole-part) digits a value needs, minimum 1 (so `0.x` shows a
932
+ * leading `0`). Uses string length, not log10, to dodge the `log10(1000)=2.999…` trap. */
933
+ declare function integerDigits(value: number): number;
934
+ /**
935
+ * Total odometer columns to show `value` at `decimals` precision TIGHTLY — exactly
936
+ * the integer digits the value needs plus the fraction columns, so there are no
937
+ * hidden leading-zero columns padding the number (a fixed reserve would either gap
938
+ * small values like `$1.00` or clamp large ones like `12.34 BTC` on a 9-col budget).
939
+ * Never below `minTotal` (a host-set stable width) nor `decimals + 1` (the counter
940
+ * needs `decimals < digits`); capped at 18. The renderer grows the counter as the
941
+ * value grows, so the magnitude is never clamped.
942
+ */
943
+ declare function displayDigits(value: number, minTotal: number, decimals: number): number;
944
+ /**
945
+ * On-screen width budget (px) the value counter is allowed before `fit` scales it
946
+ * down. Keyed to a fixed display budget (≥ 9 columns, or the host's wider `digits`)
947
+ * so a normal-magnitude readout renders full-size while a very large value scales to
948
+ * fit ("won't go too far"). `+64` leaves room for the currency affix.
949
+ */
950
+ declare function valueFitMaxWidth(digits: number, columnWidth: number): number;
951
+
952
+ /**
953
+ * The unified MENU: one scrollable surface opened by the ☰ button, composed of
954
+ * three ordered sections — Settings (Music/Sound + a Language switch + your
955
+ * extras), Paytable, then Rules. `composeMenu` is pure: it flattens a `MenuSpec`
956
+ * into the ordered `BlockSpec[]` the renderer draws, injecting section headings.
957
+ * Section text is localizable (it flows through `ui.t` in the view).
958
+ */
959
+
960
+ interface MenuSpec {
961
+ /** A banner image shown at the very top of the menu (e.g. a themed title art). */
962
+ banner?: {
963
+ src: string;
964
+ width?: number;
965
+ height?: number;
966
+ };
967
+ /** Extra settings, appended after the built-in Music/Sound (+ Language). */
968
+ settings?: BlockSpec[];
969
+ /** Paytable section content (omit → no Paytable section). */
970
+ paytable?: BlockSpec[];
971
+ /** Rules section content (omit → no Rules section). */
972
+ rules?: BlockSpec[];
973
+ /** Override the section heading text (each is run through `ui.t`). */
974
+ titles?: {
975
+ settings?: string;
976
+ paytable?: string;
977
+ rules?: string;
978
+ };
979
+ }
980
+ declare const DEFAULT_MENU_TITLES: {
981
+ readonly settings: "Settings";
982
+ readonly paytable: "Paytable";
983
+ readonly rules: "Rules";
984
+ };
985
+ /** Native names for the language switch; falls back to the upper-cased code.
986
+ * Covers Stake Engine's locale set (ar de en es fr id ja ko pl pt ru tr vi zh fi hi). */
987
+ declare const LOCALE_LABELS: Record<string, string>;
988
+ interface ComposeMenuOptions {
989
+ /** Locale codes available — when 2+, a Language select is added to Settings. */
990
+ locales?: string[];
991
+ /** The select id that switches locale (must match createUI's localeSelectId). */
992
+ localeSelectId?: string;
993
+ /** Back-compat: a top-level `rules` array used when `menu.rules` is absent. */
994
+ rulesFallback?: BlockSpec[];
995
+ }
996
+ /**
997
+ * Compose the ordered menu blocks. The built-in Music/Sound sliders reuse the
998
+ * real `music`/`sfx` controls (the view's reuse resolver wires them), so the menu
999
+ * drives the actual volume — it doesn't shadow it.
1000
+ */
1001
+ declare function composeMenu(menu: MenuSpec | undefined, opts?: ComposeMenuOptions): BlockSpec[];
1002
+
1003
+ /**
1004
+ * The notice / error modal model.
1005
+ *
1006
+ * The HUD shows a menu-style modal built from declarative {@link BlockSpec} content
1007
+ * plus a row of action buttons. EVERY string — title, message, and each button
1008
+ * label — is passed through the translator (`ui.t`), so the host can either pass the
1009
+ * EXACT literal text they want, or a translation key they localize themselves (their
1010
+ * `locale.messages` dict overrides the built-in English defaults). Nothing is
1011
+ * hard-coded in the renderer, and every default is overridable per call.
1012
+ */
1013
+
1014
+ interface NoticeAction {
1015
+ /** Button label — a literal string OR an i18n key (resolved via the translator). */
1016
+ label: string;
1017
+ /** Called when the button is pressed. */
1018
+ onSelect?: () => void;
1019
+ /** Also emit a `buttonActivated` event with this id (for analytics / host wiring). */
1020
+ emit?: string;
1021
+ /** Visual weight (default: first action `primary`, the rest `secondary`). */
1022
+ variant?: 'primary' | 'secondary';
1023
+ /** Keep the modal open after pressing (default: dismiss it). */
1024
+ keepOpen?: boolean;
1025
+ }
1026
+ interface NoticeOptions {
1027
+ /** Title — literal or i18n key. Default `'openui.error'` for errors. */
1028
+ title?: string;
1029
+ /** Callout tone. Default `'warning'` for errors. */
1030
+ tone?: 'info' | 'warning' | 'bonus';
1031
+ /** Action buttons. Omitted → a single dismiss button (`openui.ok`), or NONE when
1032
+ * `blocking` (so a fatal modal is dismissable only in code). */
1033
+ actions?: NoticeAction[];
1034
+ /** FATAL/blocking modal: locks the HUD, no backdrop/✕ dismiss; remove via `hideNotice`. */
1035
+ blocking?: boolean;
1036
+ }
1037
+ /** Reality-check reminder config (RTS 13). You supply the cadence + text; open-ui
1038
+ * owns the (wall-clock) timer, the event, and the modal. */
1039
+ interface RealityCheckOptions {
1040
+ /** Reminder interval in minutes (wall-clock). */
1041
+ everyMinutes: number;
1042
+ /** Modal title — literal or i18n key. `{{minutes}}`/`{{spent}}`/`{{won}}` interpolate. */
1043
+ title?: string;
1044
+ /** Modal message — literal or i18n key. Same interpolation tokens. */
1045
+ message?: string;
1046
+ /** Show the built-in acknowledge modal (default true). `false` → only the event fires. */
1047
+ showModal?: boolean;
1048
+ /** Custom action buttons (default a single "Continue"). */
1049
+ actions?: NoticeAction[];
1050
+ }
1051
+ /** What `showRgsError` accepts: notice options plus an exact message override. */
1052
+ interface RgsErrorOptions extends NoticeOptions {
1053
+ /** Override the default message for this code with exact text (or your own key). */
1054
+ message?: string;
1055
+ /** Stop an in-progress autoplay session (default true — most RGS errors should). */
1056
+ stopAutoplay?: boolean;
1057
+ }
1058
+ /** RGS status codes open-ui ships a default message for (the TS enum + the HTTP groups). */
1059
+ type RgsErrorCode = 'ERR_IPB' | 'ERR_IS' | 'ERR_ATE' | 'ERR_GLE' | 'ERR_BE' | 'ERR_BNF' | 'ERR_LOC' | 'ERR_MAINTENANCE' | 'ERR_VAL' | 'ERR_GEN' | 'ERR_UE' | 'ERR_TIMEOUT';
1060
+ /**
1061
+ * Code → default title/message i18n KEYS. The English text for these keys lives in
1062
+ * the translator defaults (`openuiDefaults`), so a host localizes errors by adding
1063
+ * those keys to their `messages` dict — or overrides any single one per call via
1064
+ * `showRgsError(code, { title, message })`. Unknown codes fall back to generic.
1065
+ */
1066
+ declare const RGS_ERROR_KEYS: Readonly<Record<string, {
1067
+ title: string;
1068
+ message: string;
1069
+ }>>;
1070
+ /** The default dismiss action used when a notice supplies none. */
1071
+ declare const DEFAULT_NOTICE_ACTION: NoticeAction;
1072
+ /** Build the title + message blocks for an error/notice (used by `showError`). */
1073
+ declare function errorBlocks(message: string, title: string, tone: NonNullable<NoticeOptions['tone']>): BlockSpec[];
1074
+
1075
+ /**
1076
+ * i18n port (Charter B5/B8): the core depends on this tiny interface, NOT on i18next.
1077
+ * Ship adapters (`@open-slot-ui/i18n-i18next`, the built-in `dictionary()`), never a hard dep.
1078
+ */
1079
+ interface Translator {
1080
+ t(key: string, vars?: Record<string, string | number>): string;
1081
+ readonly locale: string;
1082
+ /** Switch the active locale (a no-op for the same locale); fires onChange. */
1083
+ setLocale(next: string): void;
1084
+ onChange(cb: (locale: string) => void): Dispose;
1085
+ }
1086
+ /** open-ui owns this namespace; the host's translator may override any key. */
1087
+ declare const openuiDefaults: Record<string, string>;
1088
+ /**
1089
+ * Zero-dep dictionary translator. Falls back to open-ui defaults, then to the
1090
+ * key itself only if nothing is found — but built-in keys always resolve, so the
1091
+ * library never renders a raw key (the text analog of "never break un-themed").
1092
+ */
1093
+ declare class DictionaryTranslator implements Translator {
1094
+ private readonly messages;
1095
+ private _locale;
1096
+ private readonly subs;
1097
+ constructor(messages: Record<string, Record<string, string>>, locale: string);
1098
+ get locale(): string;
1099
+ setLocale(next: string): void;
1100
+ t(key: string, vars?: Record<string, string | number>): string;
1101
+ onChange(cb: (locale: string) => void): Dispose;
1102
+ }
1103
+ /** Convenience factory for a zero-dep dictionary translator. */
1104
+ declare function dictionary(messages: Record<string, Record<string, string>>, locale: string): DictionaryTranslator;
1105
+
1106
+ interface OpenUIOptions {
1107
+ theme?: Theme;
1108
+ layout?: LayoutConfig;
1109
+ /** i18n port. Default: an empty dictionary on locale 'en' (keys fall through). */
1110
+ translator?: Translator;
1111
+ /** Start with audio muted (the icon reflects it). Unmute restores the volumes. */
1112
+ startMuted?: boolean;
1113
+ }
1114
+ /**
1115
+ * The headless root: owns the control tree, the active theme, the responsive
1116
+ * screen state, and the typed event bus. This is the object the game holds and
1117
+ * "talks to" (Charter P1/P4). Renderer-agnostic — a renderer binds to it.
1118
+ */
1119
+ declare class OpenUI {
1120
+ readonly theme: Theme;
1121
+ readonly layoutConfig: LayoutConfig;
1122
+ readonly bus: EventBus<OpenUIEvents>;
1123
+ readonly screen: Signal<ScreenState>;
1124
+ /** i18n translator (the port — no hard i18next dep, Charter B5/B8). */
1125
+ readonly translator: Translator;
1126
+ /** Current locale, observable — views re-render their text when it changes. */
1127
+ readonly locale: Signal<string>;
1128
+ /**
1129
+ * Ref-counted input lock. While the count is above zero, `locked` is true and
1130
+ * EVERY registered control reports not-interactable through its derived getter
1131
+ * (Charter P6/G7) — no per-button bookkeeping, no desync. Instance-scoped: the
1132
+ * gate each control receives in `register` reads THIS ui's `locked`.
1133
+ */
1134
+ readonly locked: Signal<boolean>;
1135
+ private lockCount;
1136
+ /** Every subscription this ui owns, torn down by `dispose()` (Charter P12). */
1137
+ private readonly disposers;
1138
+ /** Control ids the spec marked hidden — still registered (so they stay
1139
+ * introspectable in `snapshot()`) but skipped by the renderer (Charter P10). */
1140
+ readonly hidden: Set<string>;
1141
+ /** Ids hidden UNCONDITIONALLY (e.g. a jurisdiction `disabled*` flag): `setHidden`
1142
+ * refuses to re-show these, so a responsive resize can't undo a compliance hide. */
1143
+ readonly forceHidden: Set<string>;
1144
+ /** Master mute (music+sfx), observable so an icon can reflect it. */
1145
+ readonly muted: Signal<boolean>;
1146
+ /** Social / sweepstakes mode: swaps gambling wording (Bet/Buy → social terms) and,
1147
+ * paired with a social coin, shows GC/SC. Toggle with {@link setSocial}. */
1148
+ readonly social: Signal<boolean>;
1149
+ /** Declarative blocks backing the menu-style notice/error modal. */
1150
+ readonly noticeBlocks: Signal<BlockSpec[]>;
1151
+ /** Action buttons for the notice/error modal (a single dismiss when empty). */
1152
+ readonly noticeActions: Signal<NoticeAction[]>;
1153
+ /** Whether the open notice is a BLOCKING/fatal modal — no backdrop/✕ dismiss and
1154
+ * the HUD is locked; it can be removed only in code (`hideNotice`). */
1155
+ readonly noticeBlocking: Signal<boolean>;
1156
+ private noticeLockHeld;
1157
+ /** Cumulative amount staked this session (major units) — RTS 12 "money spent". */
1158
+ readonly totalStaked: Signal<number>;
1159
+ /** Cumulative amount won this session (major units). */
1160
+ readonly totalWon: Signal<number>;
1161
+ /** The Stake jurisdiction config currently applied (read-back for guards/queries). */
1162
+ jurisdiction: Readonly<JurisdictionConfig>;
1163
+ /** Replay mode (Stake `replay=true`): the HUD locks + a REPLAY badge shows. */
1164
+ readonly replay: Signal<boolean>;
1165
+ /** Game name + version (shown in the menu footer; for support / certification). */
1166
+ gameInfo: {
1167
+ name?: string;
1168
+ version?: string;
1169
+ };
1170
+ /** Minimum round duration (ms) from jurisdiction — stored for the GAME to enforce. */
1171
+ minimumRoundDuration: number;
1172
+ private sessionNet;
1173
+ private prevVolumes;
1174
+ /** The frozen UISpec `createUI` built this from, if any — authored = run = tested. */
1175
+ spec?: Readonly<UISpec>;
1176
+ /** Typed convenience handles for the controls. */
1177
+ readonly spin: SpinControl;
1178
+ readonly balance: ValueDisplay;
1179
+ readonly bet: ValueDisplay;
1180
+ readonly settingsButton: ButtonControl;
1181
+ readonly settingsPanel: PanelControl;
1182
+ readonly musicSlider: SliderControl;
1183
+ readonly sfxSlider: SliderControl;
1184
+ readonly rulesButton: ButtonControl;
1185
+ readonly infoPanel: PanelControl;
1186
+ readonly infoClose: ButtonControl;
1187
+ readonly turbo: TurboControl;
1188
+ readonly autoplay: AutoplayControl;
1189
+ readonly bonusButton: ButtonControl;
1190
+ readonly betStepper: StepperControl;
1191
+ readonly betPlus: ButtonControl;
1192
+ readonly betMinus: ButtonControl;
1193
+ readonly fullscreenButton: ButtonControl;
1194
+ readonly muteButton: ButtonControl;
1195
+ /** Compliance display readouts — hidden until a jurisdiction reveals them. */
1196
+ readonly rtp: ReadoutControl;
1197
+ readonly netPosition: ReadoutControl;
1198
+ readonly sessionTimer: ReadoutControl;
1199
+ /** Menu-style notice/error modal (content set via `showNotice`). */
1200
+ readonly noticePanel: PanelControl;
1201
+ private readonly controls;
1202
+ constructor(opts?: OpenUIOptions);
1203
+ register(control: Control): void;
1204
+ control(id: string): Control | undefined;
1205
+ all(): Control[];
1206
+ /** Renderer calls this on resize; recomputes breakpoint + fit scale. */
1207
+ setScreen(width: number, height: number): void;
1208
+ /**
1209
+ * Hide or show a control at runtime and tell the renderer (Charter P10). The
1210
+ * control stays registered — still in `snapshot()` — but the view is not drawn.
1211
+ * Used by the responsive layer to drop controls on small screens.
1212
+ */
1213
+ setHidden(id: string, hidden: boolean): void;
1214
+ /** Push the ref-counted input lock (e.g. for the duration of a spin round). */
1215
+ lock(): void;
1216
+ /** Pop the lock; the HUD becomes interactable again only at zero. */
1217
+ unlock(): void;
1218
+ /** Toggle master mute (music + sfx). */
1219
+ toggleMute(): void;
1220
+ /** Mute/unmute music+sfx, remembering the levels to restore on unmute. */
1221
+ setMuted(m: boolean): void;
1222
+ /**
1223
+ * Report one completed round (major units): updates the net-position readout and,
1224
+ * while autoplay is running, advances its count + enforces its RG loss/single-win
1225
+ * limits. One feed point powers both `displayNetPosition` and the autoplay stops.
1226
+ */
1227
+ reportRound(win: number, bet: number): void;
1228
+ /** Reset the running session net + aggregates + timer (e.g. on a fresh session). */
1229
+ resetSession(): void;
1230
+ /** Show the menu-style notice modal: declarative blocks + optional action buttons.
1231
+ * Every string (block text + button labels) runs through `ui.t`, so pass exact
1232
+ * literals OR your own i18n keys. Omit `actions` → a single dismiss button. */
1233
+ showNotice(blocks: BlockSpec[], actions?: NoticeAction[], opts?: {
1234
+ blocking?: boolean;
1235
+ }): void;
1236
+ /** Show a simple error/notice: a title + a tone-coloured message. `message` and
1237
+ * `opts.title` are literal-or-key (localized via `ui.t`); `opts.actions` adds
1238
+ * custom buttons (e.g. a "Reload"). `opts.blocking` → a fatal modal (locks the
1239
+ * HUD, no backdrop/✕ dismiss; removable only via `hideNotice`). */
1240
+ showError(message: string, opts?: NoticeOptions): void;
1241
+ /** Show a FATAL error: a blocking modal that locks the HUD and can be removed only
1242
+ * in code (`hideNotice`) — for unrecoverable RGS states. Pass `actions` (e.g. a
1243
+ * "Reload" that reloads the page) for the only way out. */
1244
+ showFatal(message: string, opts?: NoticeOptions): void;
1245
+ /** Show the default (localizable, overridable) message for an RGS status code.
1246
+ * Override the exact text per call via `opts.title` / `opts.message`. Stops an
1247
+ * active autoplay by default (`opts.stopAutoplay = false` to keep it). Pass
1248
+ * `opts.blocking` for unrecoverable codes (session expiry, maintenance). */
1249
+ showRgsError(code: string, opts?: RgsErrorOptions): void;
1250
+ /** Close the notice / error modal. */
1251
+ hideNotice(): void;
1252
+ /** Enter/leave replay mode (Stake `replay=true`) — locks the HUD so no real bet is
1253
+ * placed; the renderer shows a REPLAY badge while it's on. */
1254
+ setReplay(on: boolean): void;
1255
+ /** Apply a Stake Engine `jurisdiction` config to the live HUD (whole switchboard).
1256
+ * The merged config is stored for `isDisabled()` read-back. */
1257
+ applyJurisdiction(jur: JurisdictionConfig): void;
1258
+ /** Tear down every subscription and control this ui created (Charter P12). */
1259
+ dispose(): void;
1260
+ /**
1261
+ * Switch language. Drives the translator (if it can self-switch) and emits
1262
+ * `localeChanged`; controls' views, subscribed to `locale`, re-render their text.
1263
+ */
1264
+ setLocale(next: string): void;
1265
+ /** Translate a key (or pass plain text through) via the active translator. In
1266
+ * social mode, a `<key>.social` variant wins when one resolves — so gambling
1267
+ * wording (Bet/Buy feature) swaps to sweepstakes terms automatically. */
1268
+ t(key: string, vars?: Record<string, string | number>): string;
1269
+ /**
1270
+ * Turn social / sweepstakes mode on or off (one switch). Swaps gambling wording
1271
+ * (Bet/Buy feature → social terms via `<key>.social` i18n) AND, when a `coin` is
1272
+ * given (e.g. `'GC'`/`'SC'`/`'XGC'`), shows balance/bet/net in that coin. Wording
1273
+ * re-renders live; the host can still override any `.social` key.
1274
+ */
1275
+ setSocial(on: boolean, coin?: string): void;
1276
+ /** Read-back: did the applied jurisdiction disable this feature? (real guard, not
1277
+ * just a hide — `confirmBuy`/autoplay entry points consult this). */
1278
+ isDisabled(feature: 'autoplay' | 'buyFeature' | 'turbo' | 'superTurbo' | 'fullscreen' | 'slamstop' | 'spacebar'): boolean;
1279
+ /**
1280
+ * Start a reality-check reminder (RTS 13). Every `everyMinutes` (WALL-CLOCK, so a
1281
+ * backgrounded tab can't cheat it) it emits a `realityCheck` event, stops autoplay,
1282
+ * and — unless `showModal: false` — shows an acknowledge modal. You provide only
1283
+ * the interval + (optional) title/message text; `{{minutes}}`/`{{spent}}`/`{{won}}`
1284
+ * are interpolated. Returns a disposer (also torn down by `dispose()`).
1285
+ */
1286
+ startRealityCheck(opts: RealityCheckOptions): Dispose;
1287
+ /** Fire one reality check now: emit the event (+ session totals), stop autoplay,
1288
+ * and show the modal unless `showModal:false`. Exposed for manual triggers / tests. */
1289
+ fireRealityCheck(opts: RealityCheckOptions, elapsedMs?: number): void;
1290
+ on<K extends keyof OpenUIEvents>(type: K, fn: (payload: OpenUIEvents[K]) => void): Dispose;
1291
+ /** Serializable snapshot for the introspection / e2e API (Charter P10). */
1292
+ snapshot(): ControlSnapshot[];
1293
+ }
1294
+
1295
+ /**
1296
+ * Stake Engine's per-player jurisdiction config — the object the RGS returns at
1297
+ * `/wallet/authenticate` as `config.jurisdiction`. It is the platform's regulatory
1298
+ * switchboard: each `disabled*` flag must HIDE/limit a feature, each `display*`
1299
+ * flag must SHOW a readout, and `minimumRoundDuration` is a hint the GAME enforces
1300
+ * (open-ui never throttles the round). Every field is optional — an omitted flag
1301
+ * leaves the HUD's configured default untouched.
1302
+ *
1303
+ * @see https://github.com/StakeEngine/web-sdk — packages/rgs-fetcher/src/schema.ts
1304
+ */
1305
+ interface JurisdictionConfig {
1306
+ /** Social / sweepstakes mode (coins shown as GC/SC). */
1307
+ socialCasino?: boolean;
1308
+ disabledFullscreen?: boolean;
1309
+ disabledTurbo?: boolean;
1310
+ disabledSuperTurbo?: boolean;
1311
+ disabledAutoplay?: boolean;
1312
+ disabledSlamstop?: boolean;
1313
+ disabledSpacebar?: boolean;
1314
+ disabledBuyFeature?: boolean;
1315
+ displayNetPosition?: boolean;
1316
+ displayRTP?: boolean;
1317
+ displaySessionTimer?: boolean;
1318
+ /** Minimum ms a round must take — stored on the HUD for the GAME to enforce. */
1319
+ minimumRoundDuration?: number;
1320
+ }
1321
+ /**
1322
+ * Apply a Stake Engine `jurisdiction` config to a live HUD — the whole switchboard
1323
+ * in one call. Safe to run at any time (typically right after the game
1324
+ * authenticates and learns the player's jurisdiction): it only acts on the flags
1325
+ * that are set, so it composes with the game's own `controls`/`responsive` config
1326
+ * and is idempotent. `disabled*` hides are made permanent (resize-proof) via
1327
+ * `ui.forceHidden`; `display*` flags reveal/hide the mandated readouts. It reuses
1328
+ * the primitives open-ui already has, so honoring the platform is one line.
1329
+ */
1330
+ declare function applyJurisdiction(ui: OpenUI, jur: JurisdictionConfig | undefined): void;
1331
+
1332
+ /**
1333
+ * The JSON-serializable UISpec — the single source of truth that builds the
1334
+ * control tree, drives e2e, and powers the docs panel editor (the "SpecKit"
1335
+ * design). Authoring it is type-checked; `validateSpec` guards it at runtime;
1336
+ * `createUI` assembles it over the existing OpenUI machinery.
1337
+ */
1338
+
1339
+ /** The reference-HUD control ids — typed so a typo is a compile error. */
1340
+ type KnownControlId = 'spin' | 'balance' | 'bet' | 'settings' | 'settings-panel' | 'music' | 'sfx' | 'rules' | 'info-panel' | 'info-close' | 'turbo' | 'autoplay' | 'bonus' | 'bet-stepper' | 'bet-plus' | 'bet-minus' | 'fullscreen' | 'mute' | 'rtp' | 'net-position' | 'session-timer' | 'notice-panel';
1341
+ /** One issue from the never-throw validator. */
1342
+ interface SpecIssue {
1343
+ level: 'error' | 'warn';
1344
+ path: string;
1345
+ code: string;
1346
+ message: string;
1347
+ }
1348
+ /** Host-owned side effects, injected (the lib performs none itself — Charter B8). */
1349
+ interface HostHooks {
1350
+ onDataIssue?(issue: SpecIssue): void;
1351
+ onAnalyticsEvent?: <K extends keyof OpenUIEvents>(type: K, payload: OpenUIEvents[K]) => void;
1352
+ }
1353
+ /** A targeted override for one shipped control. Anything omitted keeps the default. */
1354
+ interface ControlOverride {
1355
+ layout?: LayoutSpec;
1356
+ label?: string;
1357
+ hidden?: boolean;
1358
+ disabled?: boolean;
1359
+ currency?: CurrencySpec;
1360
+ initial?: number;
1361
+ /** Value-display only: MINIMUM odometer column width. `0`/unset = auto — the counter
1362
+ * is sized tightly to the value (symbol hugs the number) and grows as it does, so
1363
+ * balances never clamp. Set it only to reserve a stable, wider readout. Clamped
1364
+ * 0..18; ignored by non-value controls. Default 0 (auto). */
1365
+ digits?: number;
1366
+ }
1367
+ /**
1368
+ * Turbo switcher config. Choose how many modes the one Turbo control cycles
1369
+ * through — `2` (off/on) or `3` (off/turbo/super) — or pass explicit mode names.
1370
+ */
1371
+ interface TurboSpec {
1372
+ /** `2` → off/on · `3` → off/turbo/super · or a custom ordered ladder (first = off). */
1373
+ modes?: 2 | 3 | string[];
1374
+ /** Initial mode index (0 = off). */
1375
+ index?: number;
1376
+ }
1377
+ /** How pressing the spin button behaves. */
1378
+ type SpinPress = 'tap' | 'hold-to-spin';
1379
+ interface SpinSpec {
1380
+ /** `'tap'` = one spin per tap · `'hold-to-spin'` = turbo-spin while held. Default `'tap'`. */
1381
+ press?: SpinPress;
1382
+ }
1383
+ /** A responsive bucket the config can target: a device size OR an orientation. */
1384
+ type ResponsiveKey = Breakpoint | Orientation;
1385
+ /** What a responsive bucket may change. Layout + visibility, per control. */
1386
+ interface ResponsiveOverride {
1387
+ controls?: Record<string, Pick<ControlOverride, 'layout' | 'hidden'>>;
1388
+ }
1389
+ /**
1390
+ * A declarative menu/panel content block. Discriminated by `kind` so a "shown but
1391
+ * no payload" bug is uncompilable (Charter B10): a `select` cannot omit options,
1392
+ * a `slider` cannot carry them.
1393
+ */
1394
+ type BlockSpec = {
1395
+ kind: 'slider';
1396
+ id: string;
1397
+ label?: string;
1398
+ initial?: number;
1399
+ } | {
1400
+ kind: 'toggle';
1401
+ id: string;
1402
+ label?: string;
1403
+ on?: boolean;
1404
+ } | {
1405
+ kind: 'button';
1406
+ id: string;
1407
+ label: string;
1408
+ action?: 'closePanel' | 'openPanel' | 'emit';
1409
+ target?: string;
1410
+ role?: string;
1411
+ } | {
1412
+ kind: 'select';
1413
+ id: string;
1414
+ label?: string;
1415
+ options: SelectOption[];
1416
+ index?: number;
1417
+ } | {
1418
+ kind: 'stepper';
1419
+ id: string;
1420
+ label?: string;
1421
+ levels: number[];
1422
+ index?: number;
1423
+ } | {
1424
+ kind: 'value';
1425
+ id: string;
1426
+ label?: string;
1427
+ currency?: CurrencySpec;
1428
+ initial?: number;
1429
+ } | {
1430
+ kind: 'text';
1431
+ id: string;
1432
+ text: string;
1433
+ } | {
1434
+ kind: 'heading';
1435
+ id: string;
1436
+ text: string;
1437
+ } | {
1438
+ kind: 'subheading';
1439
+ id: string;
1440
+ text: string;
1441
+ } | {
1442
+ kind: 'callout';
1443
+ id: string;
1444
+ tone?: 'info' | 'bonus' | 'warning';
1445
+ title?: string;
1446
+ text: string;
1447
+ } | {
1448
+ kind: 'stat-grid';
1449
+ id: string;
1450
+ items: Array<{
1451
+ label: string;
1452
+ value: string;
1453
+ }>;
1454
+ } | {
1455
+ kind: 'steps';
1456
+ id: string;
1457
+ ordered?: boolean;
1458
+ items: string[];
1459
+ } | {
1460
+ kind: 'table';
1461
+ id: string;
1462
+ columns?: string[];
1463
+ rows: string[][];
1464
+ } | {
1465
+ kind: 'paytable';
1466
+ id: string;
1467
+ columns?: number;
1468
+ rows: Array<{
1469
+ symbol?: string;
1470
+ payouts: string;
1471
+ icon?: string;
1472
+ }>;
1473
+ } | {
1474
+ kind: 'image';
1475
+ id: string;
1476
+ src: string;
1477
+ alt?: string;
1478
+ width?: number;
1479
+ height?: number;
1480
+ } | {
1481
+ kind: 'media';
1482
+ id: string;
1483
+ src: string;
1484
+ alt?: string;
1485
+ side?: 'left' | 'right';
1486
+ title?: string;
1487
+ text: string;
1488
+ width?: number;
1489
+ height?: number;
1490
+ } | {
1491
+ kind: 'cards';
1492
+ id: string;
1493
+ items: Array<{
1494
+ icon?: string;
1495
+ title: string;
1496
+ text?: string;
1497
+ }>;
1498
+ } | {
1499
+ kind: 'legal';
1500
+ id: string;
1501
+ text: string;
1502
+ } | {
1503
+ kind: 'divider';
1504
+ id: string;
1505
+ } | {
1506
+ kind: 'group';
1507
+ id: string;
1508
+ title?: string;
1509
+ children: BlockSpec[];
1510
+ };
1511
+ /** All block kinds, for runtime validation of untyped host data. */
1512
+ declare const BLOCK_KINDS: readonly ["slider", "toggle", "button", "select", "stepper", "value", "text", "heading", "subheading", "callout", "stat-grid", "steps", "table", "paytable", "image", "media", "cards", "legal", "divider", "group"];
1513
+ interface PanelSpec {
1514
+ id: string;
1515
+ variant: PanelVariant;
1516
+ title?: string;
1517
+ layout?: LayoutSpec;
1518
+ blocks: BlockSpec[];
1519
+ }
1520
+ /** The whole UI as one optional, JSON-serializable object. Omit any field → default. */
1521
+ interface UISpec {
1522
+ meta?: {
1523
+ id: string;
1524
+ version: number;
1525
+ };
1526
+ theme?: ThemeChoice;
1527
+ layout?: LayoutConfig;
1528
+ /** Balance + bet currency: a full spec, or a code string (e.g. `'JPY'`, `'XGC'`)
1529
+ * auto-resolved (decimals, social coins) via the built-in currency table. */
1530
+ currency?: CurrencySpec | string;
1531
+ betLadder?: {
1532
+ levels: number[];
1533
+ index?: number;
1534
+ };
1535
+ /**
1536
+ * Autoplay count choices + tap behavior (`'options'` drawer or `'infinite'`), plus
1537
+ * optional responsible-gambling limit choices (multiples of bet; Infinity = none).
1538
+ * When `lossLimits`/`winLimits` are set, the picker offers them and the host must
1539
+ * feed each round to `hud.reportRound(win, bet)` so they're enforced.
1540
+ */
1541
+ autoplay?: {
1542
+ options?: number[];
1543
+ mode?: AutoplayMode;
1544
+ lossLimits?: number[];
1545
+ winLimits?: number[];
1546
+ };
1547
+ /** Turbo switcher: 2-mode (off/on) or 3-mode (off/turbo/super). */
1548
+ turbo?: TurboSpec;
1549
+ /** Spin button behavior: single `'tap'` or `'hold-to-spin'` turbo. */
1550
+ spin?: SpinSpec;
1551
+ /**
1552
+ * Per-device / per-orientation overrides, layered on top of `controls`. Keys are
1553
+ * `'mobile' | 'tablet' | 'desktop'` (device size) and `'portrait' | 'landscape'`
1554
+ * (orientation); on each resize the matching buckets re-apply, so the HUD can
1555
+ * reflow or drop controls per screen. Order: base → orientation → size.
1556
+ */
1557
+ responsive?: Partial<Record<ResponsiveKey, ResponsiveOverride>>;
1558
+ controls?: Partial<Record<KnownControlId, ControlOverride>> & Record<string, ControlOverride>;
1559
+ /** The unified scrollable MENU opened by ☰: Settings → Paytable → Rules. */
1560
+ menu?: MenuSpec;
1561
+ /** Back-compat: rules blocks, folded into the menu's Rules section if `menu.rules` is absent. */
1562
+ rules?: BlockSpec[];
1563
+ /** Additional declarative panels. */
1564
+ panels?: PanelSpec[];
1565
+ /** i18n dictionary + starting locale (built into a DictionaryTranslator). */
1566
+ locale?: {
1567
+ messages: Record<string, Record<string, string>>;
1568
+ locale: string;
1569
+ };
1570
+ /** Block/control id whose `optionSelected` switches the locale. Default 'lang'. */
1571
+ localeSelectId?: string;
1572
+ /** Auto-lock the whole HUD while the spin control is spinning. Default true. */
1573
+ lockDuringSpin?: boolean;
1574
+ /**
1575
+ * Stake Engine per-player jurisdiction config — the compliance switchboard. Usually
1576
+ * known only after the game authenticates, so prefer the runtime
1577
+ * `hud.applyJurisdiction(jur)`; set it here when known up front (createUI applies it
1578
+ * before the responsive layer so `disabled*` hides survive resizes).
1579
+ */
1580
+ jurisdiction?: JurisdictionConfig;
1581
+ /** Initial RTP percentage for the RTP readout (e.g. 96 → "96.0%"). */
1582
+ rtp?: number;
1583
+ /** Put the compliance readouts (net · RTP · session) in a thin status bar pinned
1584
+ * to the `'top'` or `'bottom'` edge instead of at screen corners. */
1585
+ statusBar?: 'top' | 'bottom';
1586
+ /** Game name + version — shown in the menu footer (support / certification). */
1587
+ game?: {
1588
+ name?: string;
1589
+ version?: string;
1590
+ };
1591
+ /** Reality-check reminder (RTS 13): open-ui runs a wall-clock timer, emits a
1592
+ * `realityCheck` event, stops autoplay, and (unless `showModal:false`) shows an
1593
+ * acknowledge modal. You supply only the cadence + text. `title`/`message` are
1594
+ * literal-or-key with `{{minutes}}`/`{{spent}}`/`{{won}}` interpolation. */
1595
+ realityCheck?: {
1596
+ everyMinutes: number;
1597
+ title?: string;
1598
+ message?: string;
1599
+ showModal?: boolean;
1600
+ };
1601
+ /** Social / sweepstakes mode: `true` swaps gambling wording (Bet/Buy → social
1602
+ * terms); `{ coin }` also shows balance/bet/net in that coin (e.g. `'GC'`/`'SC'`). */
1603
+ social?: boolean | {
1604
+ coin?: string;
1605
+ };
1606
+ /** Audio start state. `startMuted` boots muted (the mute icon reflects it; unmuting
1607
+ * restores the configured volumes). */
1608
+ audio?: {
1609
+ startMuted?: boolean;
1610
+ };
1611
+ }
1612
+ /** The result of building a PanelSpec: the panel + every leaf control, in render order. */
1613
+ interface BuiltPanel {
1614
+ panel: PanelControl;
1615
+ controls: Control[];
1616
+ blocks: BlockSpec[];
1617
+ }
1618
+
1619
+ /**
1620
+ * Resolves a declarative BlockSpec into a real Control. This is the swap-by-class
1621
+ * seam (Charter P7): the defaults can be overridden per kind, so a game can drop
1622
+ * in its own control class without forking. Keyed by block `kind`.
1623
+ */
1624
+
1625
+ type BlockFactory = (block: BlockSpec, bus: EventBus<OpenUIEvents>) => Control | null;
1626
+ declare class ControlRegistry {
1627
+ private readonly factories;
1628
+ register(kind: string, factory: BlockFactory): void;
1629
+ has(kind: string): boolean;
1630
+ /** Build the Control for a block, or null for non-control blocks / unknown kinds. */
1631
+ resolve(block: BlockSpec, bus: EventBus<OpenUIEvents>): Control | null;
1632
+ /** A registry with every built-in block kind wired to its reference control. */
1633
+ static defaults(): ControlRegistry;
1634
+ }
1635
+
1636
+ /**
1637
+ * Never-throw spec validator (Charter P11/B9). Returns a report; callers
1638
+ * (createUI) drop only the offending pieces and degrade — a malformed HUD still
1639
+ * boots. Every guardrail check lives here; it is pure and unit-tested.
1640
+ */
1641
+
1642
+ declare function validateSpec(spec: UISpec): {
1643
+ ok: boolean;
1644
+ issues: SpecIssue[];
1645
+ };
1646
+
1647
+ /** Resolve a block tree to its flat leaf controls, in render order. text/group carry none. */
1648
+ declare function buildBlocks(blocks: BlockSpec[], bus: EventBus<OpenUIEvents>, registry?: ControlRegistry,
1649
+ /** If a block id already resolves to a control (e.g. a built-in like 'music'),
1650
+ * reuse it instead of building a shadow copy that would overwrite it. */
1651
+ reuse?: (id: string) => Control | undefined): Control[];
1652
+ declare function buildPanel(spec: PanelSpec, bus: EventBus<OpenUIEvents>, registry?: ControlRegistry): BuiltPanel;
1653
+ /** Flatten a block tree to its button blocks (used to wire panel actions). */
1654
+ declare function buttonBlocks(blocks: BlockSpec[]): Array<Extract<BlockSpec, {
1655
+ kind: 'button';
1656
+ }>>;
1657
+
1658
+ /**
1659
+ * createUI(spec) — assemble a complete, configured OpenUI from one JSON UISpec,
1660
+ * over the EXISTING reference HUD (Charter P1/P4/B9). Zero-arg `createUI()`
1661
+ * reproduces today's 16-control HUD. Pure assembly: validate → apply overrides →
1662
+ * wire the ref-counted spin lock → freeze the spec onto `ui.spec`.
1663
+ */
1664
+
1665
+ /** Resolve the turbo `modes` config (`2`/`3` presets or explicit names) to a ladder. */
1666
+ declare function resolveTurboModes(modes: TurboSpec['modes']): string[];
1667
+ /** Identity helper for typed authoring (IDE checks the shape); returns the spec. */
1668
+ declare function defineUI(spec: UISpec): UISpec;
1669
+ declare function createUI(spec?: UISpec, hooks?: HostHooks): OpenUI;
1670
+
1671
+ /**
1672
+ * The responsive layer: re-apply per-device / per-orientation control overrides
1673
+ * every time the screen bucket changes. Pure assembly over the existing layout
1674
+ * machinery — it only ever sets `control.layout` and toggles `ui.setHidden`, both
1675
+ * of which the views already react to (Charter P6/P10). No new rendering concepts.
1676
+ */
1677
+
1678
+ type Responsive = Partial<Record<ResponsiveKey, ResponsiveOverride>>;
1679
+ /**
1680
+ * Install responsive overrides. Snapshots each referenced control's CURRENT layout
1681
+ * + hidden state as the "base" (so callers must apply static `controls` overrides
1682
+ * first), then on every screen change re-derives base → orientation → size and
1683
+ * applies it. Returns a disposer. Runs once immediately for the current screen.
1684
+ */
1685
+ declare function installResponsive(ui: OpenUI, responsive: Responsive): Dispose;
1686
+
1687
+ /**
1688
+ * The reference HUD expressed as data — the knobs `new OpenUI()` ships with.
1689
+ * `createUI(defaultHudSpec)` is byte-equivalent to zero-arg `createUI()` (a test
1690
+ * guards this). It's the canonical starting point for the docs editor and for
1691
+ * games that want to tweak from the default rather than start blank.
1692
+ */
1693
+ declare const defaultHudSpec: Readonly<UISpec>;
1694
+
1695
+ interface LoggedEvent {
1696
+ t: number;
1697
+ type: keyof OpenUIEvents;
1698
+ payload: unknown;
1699
+ }
1700
+ interface EventLogOptions {
1701
+ /** Max entries kept (ring buffer). Default 200. */
1702
+ capacity?: number;
1703
+ /** Monotonic clock; defaults to performance.now/Date.now. Injectable for tests. */
1704
+ now?: () => number;
1705
+ }
1706
+ /**
1707
+ * An opt-in ring buffer over the one typed event bus — backs `BootedHud.events`
1708
+ * and the dev `window.__OPENUI__.events`, so e2e can assert "exactly one
1709
+ * spinRequested fired during the round" without timing guesses (Charter P10/G6).
1710
+ */
1711
+ declare class EventLog {
1712
+ private readonly buf;
1713
+ private readonly cap;
1714
+ private readonly clock;
1715
+ private readonly disposers;
1716
+ constructor(bus: EventBus<OpenUIEvents>, opts?: EventLogOptions);
1717
+ private record;
1718
+ /** The most recent payload of `type`, or undefined. */
1719
+ last<K extends keyof OpenUIEvents>(type: K): OpenUIEvents[K] | undefined;
1720
+ /** How many of `type` are in the buffer. */
1721
+ count(type: keyof OpenUIEvents): number;
1722
+ /** Every event at or after timestamp `t`, in order. */
1723
+ since(t: number): LoggedEvent[];
1724
+ clear(): void;
1725
+ dispose(): void;
1726
+ }
1727
+
1728
+ /**
1729
+ * Currency display metadata + a pure formatter (Charter P11: data, not code).
1730
+ *
1731
+ * Stake Engine supports 35+ currencies — fiat (incl. zero-decimal JPY/KRW/VND…),
1732
+ * crypto, and the social coins XGC/XSC (shown as "GC"/"SC", never locale-formatted
1733
+ * as fiat). A host shouldn't have to hand-tune decimals for each, so this maps a
1734
+ * code → its display precision; anything unknown defaults to 2 (the common case).
1735
+ * The UI computes no money — it only formats what the game sets.
1736
+ */
1737
+
1738
+ interface CurrencyInfo {
1739
+ /** Fractional digits to show (JPY/KRW = 0, USD = 2, BTC = 8). */
1740
+ decimals: number;
1741
+ /** Display code shown to the player when it differs from the code (XGC → "GC"). */
1742
+ display?: string;
1743
+ /** A social / sweepstakes coin — never formatted as a fiat currency. */
1744
+ social?: boolean;
1745
+ }
1746
+ /** Code → display info. Unknown codes fall back to 2 decimals (see {@link resolveCurrency}). */
1747
+ declare const CURRENCY_TABLE: Readonly<Record<string, CurrencyInfo>>;
1748
+ /** True if `code` is a Stake social/sweepstakes coin (display "GC"/"SC"). */
1749
+ declare function isSocialCurrency(code: string): boolean;
1750
+ /**
1751
+ * Resolve a currency code to a complete, display-ready {@link CurrencySpec}: fills
1752
+ * decimals from the table (default 2) and maps social coins to their display code.
1753
+ * Anything in `overrides` wins, so a game can still hand-tune. Never throws.
1754
+ */
1755
+ declare function resolveCurrency(code: string, overrides?: Partial<CurrencySpec>): CurrencySpec;
1756
+ /**
1757
+ * Format an amount (major units) per a {@link CurrencySpec}: grouped integer part,
1758
+ * fixed decimals, the code on the configured side. `signed` adds an explicit "+"
1759
+ * for non-negative values (negatives always show "-") — used by the net-position
1760
+ * readout. Pure + total: a malformed number renders as "0".
1761
+ */
1762
+ declare function formatAmount(value: number, spec: CurrencySpec, opts?: {
1763
+ signed?: boolean;
1764
+ }): string;
1765
+
1766
+ /**
1767
+ * Build / clamp a bet ladder from a Stake Engine RGS bet config so the host doesn't
1768
+ * have to hand-tune `betLadder` (and can't ship a stake outside min/max/step). Pure.
1769
+ */
1770
+ interface RgsBetConfig {
1771
+ /** Allowed stake amounts in API minor units (Stake: dollars × 1_000_000). */
1772
+ betLevels?: number[];
1773
+ minBet?: number;
1774
+ maxBet?: number;
1775
+ stepBet?: number;
1776
+ /** Default stake in minor units (custom bets must be a multiple of this). */
1777
+ defaultBetLevel?: number;
1778
+ }
1779
+ /** Stake's API minor-unit divisor (1_000_000 minor units = 1.00). */
1780
+ declare const API_AMOUNT_DIVISOR = 1000000;
1781
+ /**
1782
+ * Turn an RGS bet config into a `{ levels, index }` ladder in MAJOR units (what
1783
+ * `UISpec.betLadder` wants). Uses `betLevels` if present, else derives from
1784
+ * min/max/step. `divisor` converts minor→major (default Stake's 1_000_000).
1785
+ */
1786
+ declare function buildBetLadder(cfg: RgsBetConfig, divisor?: number): {
1787
+ levels: number[];
1788
+ index: number;
1789
+ };
1790
+ /** Clamp a stake (major units) to the RGS min/max and snap to `stepBet`. Pure. */
1791
+ declare function clampBet(amount: number, cfg: RgsBetConfig, divisor?: number): number;
1792
+
1793
+ /**
1794
+ * Win-celebration classifier — the RTS 14F guardrail. open-ui doesn't render the win
1795
+ * (the game does), but this helper lets a game decide HOW LOUD to celebrate without
1796
+ * ever celebrating a return ≤ the stake (the most-fined responsible-gambling rule).
1797
+ */
1798
+ interface WinTier {
1799
+ name: string;
1800
+ /** Minimum win/stake multiplier for this tier. */
1801
+ minMultiplier: number;
1802
+ }
1803
+ /** Default escalation tiers (multipliers of stake). */
1804
+ declare const DEFAULT_WIN_TIERS: readonly WinTier[];
1805
+ /**
1806
+ * Classify a win for celebration. Returns `'none'` when the win is ≤ the stake (so a
1807
+ * "win" that returns less than was staked is NEVER celebrated — UKGC RTS 14F), else
1808
+ * `'win'` for a positive win below the first tier, or the highest tier whose
1809
+ * multiplier threshold is met. Pure + total.
1810
+ */
1811
+ declare function winTier(win: number, stake: number, tiers?: readonly WinTier[]): string;
1812
+
1813
+ export { API_AMOUNT_DIVISOR, type Anchor, AutoplayControl, type AutoplayLimits, type AutoplayMode, type AutoplayOptions, BLOCK_KINDS, type BlockFactory, type BlockSpec, type Breakpoint, type BuiltPanel, ButtonControl, type ButtonOptions, CURRENCY_TABLE, CardControl, type CardOptions, type ComposeMenuOptions, Control, type ControlOptions, type ControlOverride, ControlRegistry, type ControlSnapshot, type CurrencyInfo, type CurrencySpec, DEFAULT_MENU_TITLES, DEFAULT_NOTICE_ACTION, DEFAULT_WIN_TIERS, type DeepPartial, DictionaryTranslator, type Dispose, EventBus, EventLog, type EventLogOptions, type EventMap, Fade, type HostHooks, type JurisdictionConfig, type KnownControlId, LOCALE_LABELS, type LayoutConfig, type LayoutSpec, type LoggedEvent, type MenuSpec, type NoticeAction, type NoticeOptions, OpenUI, type OpenUIEvents, type OpenUIOptions, type Orientation, PanelControl, type PanelOptions, type PanelSpec, type PanelVariant, Parallel, type Placement, Pulse, RGS_ERROR_KEYS, ReadoutControl, type ReadoutKind, type ReadoutOptions, type RealityCheckOptions, type Rect, type ResponsiveKey, type ResponsiveOverride, type RgsBetConfig, type RgsErrorCode, type RgsErrorOptions, type ScreenState, SelectControl, type SelectControlOptions, type SelectOption, Sequence, Signal, SliderControl, type SliderOptions, type SpecIssue, SpinControl, type SpinPress, type SpinSpec, Squish, type StateDef, type StateMap, StepperControl, type StepperOptions, type Subscribable, type Theme, type ThemeChoice, type ThemeIssue, type ThemeOverrides, type ThemePreset, ToggleControl, type ToggleOptions, type Transition, type Translator, TurboControl, type TurboOptions, type TurboSpec, Turn, type UISpec, ValueDisplay, type ValueDisplayOptions, type ViewInspect, type WinTier, applyJurisdiction, breakpointFor, buildBetLadder, buildBlocks, buildPanel, buttonBlocks, clampBet, clampDecimals, clampDigits, clampMinDigits, composeMenu, computeScreen, createUI, defaultHudSpec, defaultLayoutConfig, defaultTheme, defineUI, dictionary, displayDigits, effect, errorBlocks, extendTheme, formatAmount, installResponsive, integerDigits, isSafeColor, isSocialCurrency, openuiDefaults, resolveCurrency, resolvePlacement, resolveTheme, resolveTurboModes, safeAmount, sanitizeThemeOverrides, themePresets, validateSpec, valueFitMaxWidth, winTier };