@energy8platform/shell 0.6.5 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/html.cjs.js +679 -113
- package/dist/html.cjs.js.map +1 -1
- package/dist/html.d.ts +213 -31
- package/dist/html.esm.js +672 -114
- package/dist/html.esm.js.map +1 -1
- package/dist/index.cjs.js +376 -22
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +204 -29
- package/dist/index.esm.js +369 -23
- package/dist/index.esm.js.map +1 -1
- package/dist/pixi.cjs.js +957 -285
- package/dist/pixi.cjs.js.map +1 -1
- package/dist/pixi.d.ts +208 -32
- package/dist/pixi.esm.js +950 -286
- package/dist/pixi.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ShellController.ts +75 -21
- package/src/core/icon-names.ts +30 -0
- package/src/core/index.ts +4 -0
- package/src/core/menu.ts +301 -0
- package/src/core/popover.ts +81 -0
- package/src/core/renderer.ts +17 -12
- package/src/core/state.ts +2 -1
- package/src/core/types.ts +12 -6
- package/src/core/version.ts +1 -1
- package/src/ui/html/HtmlRenderer.ts +35 -12
- package/src/ui/html/components/GameInfo.ts +1 -1
- package/src/ui/html/components/Menu.ts +153 -0
- package/src/ui/html/icons.ts +16 -15
- package/src/ui/html/primitives.ts +142 -0
- package/src/ui/html/shell.css.ts +21 -1
- package/src/ui/pixi/PixiRenderer.ts +41 -21
- package/src/ui/pixi/components/BottomBar.ts +80 -9
- package/src/ui/pixi/components/GameInfo.ts +1 -1
- package/src/ui/pixi/components/Menu.ts +167 -0
- package/src/ui/pixi/context.ts +3 -2
- package/src/ui/pixi/icons.ts +16 -15
- package/src/ui/pixi/pixi-icon.ts +15 -3
- package/src/ui/pixi/primitives/controls.ts +46 -0
- package/src/ui/pixi/primitives/popover.ts +163 -0
- package/src/ui/pixi/primitives/scroll.ts +9 -5
- package/src/ui/html/components/Settings.ts +0 -68
- package/src/ui/pixi/components/Settings.ts +0 -132
package/dist/html.cjs.js
CHANGED
|
@@ -52,6 +52,240 @@ class EventEmitter {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// AUTO-GENERATED from ../../icons.svg by scripts/gen-icons-from-svg.mjs — do not edit by hand.
|
|
56
|
+
// Sharp monochrome icon set: each glyph is a 24×24 viewBox fragment using currentColor
|
|
57
|
+
// (hollow shapes use fill-rule="evenodd"). Coordinates are baked into the 24×24 space (no
|
|
58
|
+
// <g transform>), so the same fragment renders identically in the DOM <svg> and in Pixi's
|
|
59
|
+
// GraphicsContext.svg. To change an icon: edit icons.svg, then run
|
|
60
|
+
// `node scripts/gen-icons-from-svg.mjs`.
|
|
61
|
+
// The single glyph-name union, shared by core (menu items) and both renderers.
|
|
62
|
+
const ICON_NAMES = [
|
|
63
|
+
'spin',
|
|
64
|
+
'turbo1',
|
|
65
|
+
'autoplay',
|
|
66
|
+
'stop',
|
|
67
|
+
'menu',
|
|
68
|
+
'minus',
|
|
69
|
+
'plus',
|
|
70
|
+
'gift',
|
|
71
|
+
'info',
|
|
72
|
+
'soundOn',
|
|
73
|
+
'soundOff',
|
|
74
|
+
'close',
|
|
75
|
+
'back',
|
|
76
|
+
'chevronRight',
|
|
77
|
+
'ticket',
|
|
78
|
+
'turbo2',
|
|
79
|
+
'turboOff',
|
|
80
|
+
'chevronUp',
|
|
81
|
+
'chevronDown',
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const PRESET_IDS = ['sound', 'music', 'sfx', 'gameInfo'];
|
|
85
|
+
function isPresetId(id) {
|
|
86
|
+
return PRESET_IDS.includes(id);
|
|
87
|
+
}
|
|
88
|
+
/** The rows shown when `ShellConfig.menu` is omitted — today's Settings content, minus master. */
|
|
89
|
+
const DEFAULT_MENU = [
|
|
90
|
+
{ id: 'sound' },
|
|
91
|
+
{ id: 'music' },
|
|
92
|
+
{ id: 'sfx' },
|
|
93
|
+
{ type: 'separator' },
|
|
94
|
+
{ id: 'gameInfo' },
|
|
95
|
+
];
|
|
96
|
+
const isSeparator = (i) => i.type === 'separator';
|
|
97
|
+
/** Range bounds with defaults: 0..1 like a volume slider, step = a twentieth of the span. */
|
|
98
|
+
function rangeBounds(item) {
|
|
99
|
+
const min = item.min ?? 0;
|
|
100
|
+
const max = item.max ?? 1;
|
|
101
|
+
const derivedStep = (max - min) / 20;
|
|
102
|
+
// A declared `step` must be a genuinely positive number. `??` alone doesn't catch this: an
|
|
103
|
+
// explicit 0 (or a negative value) is not null/undefined, so it would sail through unchanged —
|
|
104
|
+
// and a renderer's position math divides by it (Pixi's fromUnit: `(raw-min)/step`), turning the
|
|
105
|
+
// slider's value into NaN, which then reaches `state.menu` and the `settingChange` payload.
|
|
106
|
+
const step = item.step != null && item.step > 0 ? item.step : derivedStep;
|
|
107
|
+
return { min, max, step };
|
|
108
|
+
}
|
|
109
|
+
/** Initial values for CUSTOM items (presets keep their own homes). Values already in `prev` win, so
|
|
110
|
+
* a later `setMenu()` with the same ids does not reset what the player has changed. */
|
|
111
|
+
function seedMenuValues(items, prev = {}) {
|
|
112
|
+
const out = {};
|
|
113
|
+
for (const item of items) {
|
|
114
|
+
if (isSeparator(item))
|
|
115
|
+
continue;
|
|
116
|
+
const type = item.type;
|
|
117
|
+
if (!type || isPresetId(item.id))
|
|
118
|
+
continue;
|
|
119
|
+
// A previous value only carries over when its runtime type still matches this item's kind.
|
|
120
|
+
// Without this guard, reconfiguring the same id from a toggle to a range (or back) would seed
|
|
121
|
+
// a boolean into a slider's position math (or a stale number into a checkbox) — a legitimate
|
|
122
|
+
// reconfiguration, not a config error, so it falls through to the item's own default instead
|
|
123
|
+
// of warning.
|
|
124
|
+
const prevValue = prev[item.id];
|
|
125
|
+
if (type === 'toggle') {
|
|
126
|
+
out[item.id] = typeof prevValue === 'boolean' ? prevValue : (item.value ?? false);
|
|
127
|
+
}
|
|
128
|
+
else if (type === 'range') {
|
|
129
|
+
const r = item;
|
|
130
|
+
out[item.id] = typeof prevValue === 'number' ? prevValue : (r.value ?? rangeBounds(r).min);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
const percent = (v) => `${Math.round(v * 100)}%`;
|
|
136
|
+
function safeIcon(name) {
|
|
137
|
+
return name && ICON_NAMES.includes(name) ? name : undefined;
|
|
138
|
+
}
|
|
139
|
+
/** Every drop/collision warning goes through here so the message style stays uniform. */
|
|
140
|
+
function warn(message) {
|
|
141
|
+
console.warn(`[shell] ${message}`);
|
|
142
|
+
}
|
|
143
|
+
/** Expand the configured list into render-ready rows. A config mistake — an unknown preset id, an
|
|
144
|
+
* unrecognized custom `type`, or an invalid range span — is dropped with one warning rather than
|
|
145
|
+
* silently misbehaving: a typo must be visible, not silently invisible. A custom id that collides
|
|
146
|
+
* with a reserved preset id also warns, but keeps its row (see `custom()`). */
|
|
147
|
+
function resolveMenu(host) {
|
|
148
|
+
const rows = [];
|
|
149
|
+
for (const item of host.menu) {
|
|
150
|
+
if (isSeparator(item)) {
|
|
151
|
+
rows.push({ kind: 'separator' });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const type = item.type;
|
|
155
|
+
if (!type) {
|
|
156
|
+
const row = preset(host, item);
|
|
157
|
+
if (row)
|
|
158
|
+
rows.push(row);
|
|
159
|
+
else
|
|
160
|
+
warn(`unknown menu preset id "${item.id}" — item skipped`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const row = custom(host, item, type);
|
|
164
|
+
if (row)
|
|
165
|
+
rows.push(row);
|
|
166
|
+
}
|
|
167
|
+
return rows;
|
|
168
|
+
}
|
|
169
|
+
function preset(host, item) {
|
|
170
|
+
const disabled = item.disabled ?? false;
|
|
171
|
+
const label = host.t(item.label ?? DEFAULT_LABELS[item.id] ?? item.id);
|
|
172
|
+
switch (item.id) {
|
|
173
|
+
case 'sound':
|
|
174
|
+
return {
|
|
175
|
+
kind: 'toggle',
|
|
176
|
+
id: 'sound',
|
|
177
|
+
label,
|
|
178
|
+
disabled,
|
|
179
|
+
icon: (v) => safeIcon(item.icon) ?? (v ? 'soundOn' : 'soundOff'),
|
|
180
|
+
get: () => host.getMenuValue('sound') !== false,
|
|
181
|
+
set: (v) => host.setMenuValue('sound', v),
|
|
182
|
+
};
|
|
183
|
+
case 'music':
|
|
184
|
+
case 'sfx': {
|
|
185
|
+
const id = item.id;
|
|
186
|
+
return {
|
|
187
|
+
kind: 'range',
|
|
188
|
+
id,
|
|
189
|
+
label,
|
|
190
|
+
icon: safeIcon(item.icon),
|
|
191
|
+
disabled,
|
|
192
|
+
min: 0,
|
|
193
|
+
max: 1,
|
|
194
|
+
step: 0.05,
|
|
195
|
+
get: () => Number(host.getMenuValue(id) ?? 1),
|
|
196
|
+
set: (v) => host.setMenuValue(id, v),
|
|
197
|
+
format: percent,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
case 'gameInfo':
|
|
201
|
+
return {
|
|
202
|
+
kind: 'button',
|
|
203
|
+
id: 'gameInfo',
|
|
204
|
+
label,
|
|
205
|
+
icon: safeIcon(item.icon) ?? 'info',
|
|
206
|
+
disabled,
|
|
207
|
+
chevron: true,
|
|
208
|
+
select: () => host.actions.openInfo(),
|
|
209
|
+
};
|
|
210
|
+
default:
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const DEFAULT_LABELS = {
|
|
215
|
+
sound: 'Sound',
|
|
216
|
+
music: 'Music',
|
|
217
|
+
sfx: 'SFX',
|
|
218
|
+
gameInfo: 'Game info',
|
|
219
|
+
};
|
|
220
|
+
function custom(host, item, type) {
|
|
221
|
+
// Same store, different semantics: the real preset's get() and a custom row's get() disagree
|
|
222
|
+
// (see the preset cases below vs. the toggle case here). Routing does not change — Task 4's
|
|
223
|
+
// ShellHost is what actually owns the value — this warning just makes the clash visible.
|
|
224
|
+
if (isPresetId(item.id)) {
|
|
225
|
+
warn(`menu item id "${item.id}" collides with a built-in preset id — preset ids are reserved`);
|
|
226
|
+
}
|
|
227
|
+
const disabled = item.disabled ?? false;
|
|
228
|
+
const label = host.t(item.label ?? item.id);
|
|
229
|
+
const icon = safeIcon(item.icon);
|
|
230
|
+
if (type === 'toggle') {
|
|
231
|
+
const it = item;
|
|
232
|
+
return {
|
|
233
|
+
kind: 'toggle',
|
|
234
|
+
id: it.id,
|
|
235
|
+
label,
|
|
236
|
+
disabled,
|
|
237
|
+
icon: () => icon,
|
|
238
|
+
get: () => host.getMenuValue(it.id) === true,
|
|
239
|
+
set: (v) => {
|
|
240
|
+
host.setMenuValue(it.id, v);
|
|
241
|
+
it.onChange?.(v);
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (type === 'range') {
|
|
246
|
+
const it = item;
|
|
247
|
+
const { min, max, step } = rangeBounds(it);
|
|
248
|
+
if (max <= min) {
|
|
249
|
+
// A renderer's position math ((value - min) / (max - min)) turns this into Infinity/NaN —
|
|
250
|
+
// drop the row instead, exactly like an unknown preset id.
|
|
251
|
+
warn(`menu item "${it.id}" has invalid range bounds (min ${min}, max ${max}) — item skipped`);
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
kind: 'range',
|
|
256
|
+
id: it.id,
|
|
257
|
+
label,
|
|
258
|
+
icon,
|
|
259
|
+
disabled,
|
|
260
|
+
min,
|
|
261
|
+
max,
|
|
262
|
+
step,
|
|
263
|
+
get: () => Number(host.getMenuValue(it.id) ?? min),
|
|
264
|
+
set: (v) => {
|
|
265
|
+
host.setMenuValue(it.id, v);
|
|
266
|
+
it.onChange?.(v);
|
|
267
|
+
},
|
|
268
|
+
format: it.format ?? (min === 0 && max === 1 ? percent : (v) => String(v)),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (type === 'button') {
|
|
272
|
+
const it = item;
|
|
273
|
+
return {
|
|
274
|
+
kind: 'button',
|
|
275
|
+
id: it.id,
|
|
276
|
+
label,
|
|
277
|
+
icon,
|
|
278
|
+
disabled,
|
|
279
|
+
chevron: it.chevron ?? false,
|
|
280
|
+
select: () => it.onSelect?.(),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
// Neither toggle, range, nor button — a typo'd `type` used to fall through to an unconditional
|
|
284
|
+
// button row with no onSelect. Drop it visibly instead.
|
|
285
|
+
warn(`menu item "${item.id}" has unknown type "${type}" — item skipped`);
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
55
289
|
function createInitialState(config) {
|
|
56
290
|
return {
|
|
57
291
|
mode: config.mode,
|
|
@@ -68,10 +302,10 @@ function createInitialState(config) {
|
|
|
68
302
|
bonus: null,
|
|
69
303
|
activeFeature: null,
|
|
70
304
|
volumes: {
|
|
71
|
-
master: clampVolume(config.volumes?.master),
|
|
72
305
|
music: clampVolume(config.volumes?.music),
|
|
73
306
|
sfx: clampVolume(config.volumes?.sfx),
|
|
74
307
|
},
|
|
308
|
+
menu: seedMenuValues(config.menu ?? DEFAULT_MENU),
|
|
75
309
|
};
|
|
76
310
|
}
|
|
77
311
|
/** Clamp a configured volume to 0..1, defaulting to full (1) when unset/invalid. */
|
|
@@ -1455,7 +1689,7 @@ class KeyboardController {
|
|
|
1455
1689
|
|
|
1456
1690
|
// AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
|
|
1457
1691
|
/** The @energy8platform/shell package version, stamped into the game-info footer. */
|
|
1458
|
-
const PACKAGE_VERSION = '0.
|
|
1692
|
+
const PACKAGE_VERSION = '0.7.0';
|
|
1459
1693
|
|
|
1460
1694
|
/** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
|
|
1461
1695
|
function resolveConfig(config) {
|
|
@@ -1473,6 +1707,7 @@ function resolveConfig(config) {
|
|
|
1473
1707
|
theme: config.theme,
|
|
1474
1708
|
onBonusBuy: config.onBonusBuy,
|
|
1475
1709
|
volumes: config.volumes,
|
|
1710
|
+
menu: config.menu,
|
|
1476
1711
|
version: config.version ?? '1.0.0',
|
|
1477
1712
|
isSocial: config.isSocial ?? false,
|
|
1478
1713
|
replay: config.replay ?? config.mode === 'replay',
|
|
@@ -1493,8 +1728,9 @@ class ShellController extends EventEmitter {
|
|
|
1493
1728
|
i18n;
|
|
1494
1729
|
kbd;
|
|
1495
1730
|
overlay = null;
|
|
1496
|
-
|
|
1497
|
-
|
|
1731
|
+
menuItems;
|
|
1732
|
+
menuRefresh = null;
|
|
1733
|
+
overlayKind = null;
|
|
1498
1734
|
prevBalance;
|
|
1499
1735
|
prevWin;
|
|
1500
1736
|
destroyed = false;
|
|
@@ -1505,6 +1741,7 @@ class ShellController extends EventEmitter {
|
|
|
1505
1741
|
this.config = resolveConfig(config);
|
|
1506
1742
|
this.i18n = createI18n({ language: this.config.language, isSocial: this.config.isSocial });
|
|
1507
1743
|
this.state = createInitialState(this.config);
|
|
1744
|
+
this.menuItems = this.config.menu ?? DEFAULT_MENU;
|
|
1508
1745
|
this.tokens = resolveTheme(this.config.theme);
|
|
1509
1746
|
this.prevBalance = this.state.balance;
|
|
1510
1747
|
this.prevWin = this.state.win;
|
|
@@ -1638,14 +1875,21 @@ class ShellController extends EventEmitter {
|
|
|
1638
1875
|
show(req) {
|
|
1639
1876
|
this.closeModal();
|
|
1640
1877
|
this.overlay = this.renderer.openOverlay(req) ?? null;
|
|
1878
|
+
this.overlayKind = this.overlay ? req.kind : null;
|
|
1641
1879
|
}
|
|
1880
|
+
/** Open the bar menu. Called again while it is open, it closes it — the burger toggles. */
|
|
1642
1881
|
openMenu() {
|
|
1882
|
+
if (this.overlayKind === 'menu') {
|
|
1883
|
+
this.closeModal();
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1643
1886
|
this.emit('menuOpen');
|
|
1644
|
-
this.
|
|
1887
|
+
this.show({ kind: 'menu' });
|
|
1645
1888
|
}
|
|
1889
|
+
/** @deprecated The Settings overlay is gone — this opens the bar menu. */
|
|
1646
1890
|
openSettings() {
|
|
1647
1891
|
this.emit('settingsOpen');
|
|
1648
|
-
this.
|
|
1892
|
+
this.openMenu();
|
|
1649
1893
|
}
|
|
1650
1894
|
openInfo() {
|
|
1651
1895
|
this.emit('infoOpen');
|
|
@@ -1677,35 +1921,72 @@ class ShellController extends EventEmitter {
|
|
|
1677
1921
|
if (!this.overlay)
|
|
1678
1922
|
return;
|
|
1679
1923
|
this.overlay = null;
|
|
1680
|
-
this.
|
|
1681
|
-
this.
|
|
1924
|
+
this.overlayKind = null;
|
|
1925
|
+
this.menuRefresh = null;
|
|
1682
1926
|
this.renderer.closeOverlay();
|
|
1683
1927
|
}
|
|
1684
1928
|
// ── sound ──────────────────────────────────────────────────────────────────
|
|
1685
1929
|
setSound(on) {
|
|
1686
1930
|
this.soundOn = on;
|
|
1687
1931
|
this.emit('settingChange', { key: 'sound', value: on });
|
|
1688
|
-
this.
|
|
1689
|
-
this.renderer.refreshSoundIcon?.(on);
|
|
1690
|
-
}
|
|
1691
|
-
setSoundRefresh(fn) {
|
|
1692
|
-
this.soundRefresh = fn;
|
|
1932
|
+
this.menuRefresh?.('sound', on);
|
|
1693
1933
|
}
|
|
1694
1934
|
// ── volume ─────────────────────────────────────────────────────────────────
|
|
1695
1935
|
getVolume(key) {
|
|
1696
1936
|
return this.state.volumes[key];
|
|
1697
1937
|
}
|
|
1698
1938
|
/** Set a volume slider (0..1). Shared by the slider control (drag) and game code (public API):
|
|
1699
|
-
* clamps, stores so a reopened
|
|
1700
|
-
* live-updates the slider if the
|
|
1939
|
+
* clamps, stores so a reopened menu popover reflects it, emits `settingChange`, and
|
|
1940
|
+
* live-updates the slider if the menu is currently open. */
|
|
1701
1941
|
setVolume(key, value) {
|
|
1702
1942
|
const v = Math.max(0, Math.min(1, value));
|
|
1703
1943
|
this.state.volumes[key] = v;
|
|
1704
1944
|
this.emit('settingChange', { key, value: v });
|
|
1705
|
-
this.
|
|
1945
|
+
this.menuRefresh?.(key, v);
|
|
1946
|
+
}
|
|
1947
|
+
// ── menu ───────────────────────────────────────────────────────────────────
|
|
1948
|
+
get menu() {
|
|
1949
|
+
return this.menuItems;
|
|
1950
|
+
}
|
|
1951
|
+
/** Replace the item list. Values of ids already in state are kept; new ids are seeded. */
|
|
1952
|
+
setMenu(items) {
|
|
1953
|
+
this.menuItems = items;
|
|
1954
|
+
this.state.menu = seedMenuValues(items, this.state.menu);
|
|
1955
|
+
if (this.overlayKind === 'menu')
|
|
1956
|
+
this.show({ kind: 'menu' });
|
|
1957
|
+
}
|
|
1958
|
+
getMenuValue(id) {
|
|
1959
|
+
if (id === 'sound')
|
|
1960
|
+
return this.soundOn;
|
|
1961
|
+
if (id === 'music' || id === 'sfx')
|
|
1962
|
+
return this.state.volumes[id];
|
|
1963
|
+
return this.state.menu[id];
|
|
1964
|
+
}
|
|
1965
|
+
/** Set a menu value. Presets route to their own homes so there is never a second copy. */
|
|
1966
|
+
setMenuValue(id, value) {
|
|
1967
|
+
if (id === 'sound') {
|
|
1968
|
+
this.setSound(value !== false);
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
if (id === 'music' || id === 'sfx') {
|
|
1972
|
+
this.setVolume(id, Number(value));
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
const next = typeof value === 'number' ? this.clampRange(id, value) : value;
|
|
1976
|
+
this.state.menu[id] = next;
|
|
1977
|
+
this.emit('settingChange', { key: id, value: next });
|
|
1978
|
+
this.menuRefresh?.(id, next);
|
|
1979
|
+
}
|
|
1980
|
+
setMenuRefresh(fn) {
|
|
1981
|
+
this.menuRefresh = fn;
|
|
1706
1982
|
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1983
|
+
/** Clamp to the declared bounds of a custom `range` item (a non-range id passes through). */
|
|
1984
|
+
clampRange(id, value) {
|
|
1985
|
+
const item = this.menuItems.find((i) => i.id === id);
|
|
1986
|
+
if (!item || item.type !== 'range')
|
|
1987
|
+
return value;
|
|
1988
|
+
const { min, max } = rangeBounds(item);
|
|
1989
|
+
return Math.max(min, Math.min(max, value));
|
|
1709
1990
|
}
|
|
1710
1991
|
// ── features ─────────────────────────────────────────────────────────────────
|
|
1711
1992
|
activateFeature(bonus) {
|
|
@@ -1722,10 +2003,10 @@ class ShellController extends EventEmitter {
|
|
|
1722
2003
|
this.renderer.renderBar();
|
|
1723
2004
|
}
|
|
1724
2005
|
// ── game-facing public API (mirrors GameShell/PixiGameShell) ───────────────────
|
|
1725
|
-
money(field, from, to) {
|
|
2006
|
+
money(field, from, to, durationMs) {
|
|
1726
2007
|
this.renderer.renderBar();
|
|
1727
2008
|
if (to !== from)
|
|
1728
|
-
this.renderer.animateMoney(field, from, to);
|
|
2009
|
+
this.renderer.animateMoney(field, from, to, durationMs);
|
|
1729
2010
|
}
|
|
1730
2011
|
setBalance(n) {
|
|
1731
2012
|
const from = this.prevBalance;
|
|
@@ -1735,7 +2016,9 @@ class ShellController extends EventEmitter {
|
|
|
1735
2016
|
}
|
|
1736
2017
|
/** Set the WIN readout. Counts up/down from the previous value by default. Pass
|
|
1737
2018
|
* `{ animate: false }` to SNAP instantly (renderBar cancels any in-flight count-up) — used by the
|
|
1738
|
-
* host to clear WIN to 0 at spin start, where an animated count-DOWN would look wrong.
|
|
2019
|
+
* host to clear WIN to 0 at spin start, where an animated count-DOWN would look wrong.
|
|
2020
|
+
* `{ durationMs }` shortens/lengthens the count-up (default 450ms) — a scene reporting a win per
|
|
2021
|
+
* cascade step passes its step length so each count-up finishes before the next step lands. */
|
|
1739
2022
|
setWin(n, opts) {
|
|
1740
2023
|
const from = this.prevWin;
|
|
1741
2024
|
this.state.win = n;
|
|
@@ -1744,7 +2027,7 @@ class ShellController extends EventEmitter {
|
|
|
1744
2027
|
this.renderer.renderBar(); // instant repaint from state; cancels running money anims
|
|
1745
2028
|
return;
|
|
1746
2029
|
}
|
|
1747
|
-
this.money('win', from, n);
|
|
2030
|
+
this.money('win', from, n, opts?.durationMs);
|
|
1748
2031
|
}
|
|
1749
2032
|
setBet(n) {
|
|
1750
2033
|
this.state.bet = n;
|
|
@@ -1813,6 +2096,11 @@ class ShellController extends EventEmitter {
|
|
|
1813
2096
|
if (this.destroyed)
|
|
1814
2097
|
return Promise.resolve();
|
|
1815
2098
|
this.destroyed = true;
|
|
2099
|
+
// With the menu (or any overlay) open at teardown, `menuRefresh` / `overlay` / `overlayKind`
|
|
2100
|
+
// would otherwise survive the renderer's destroy — so a later setVolume()/setSound() call
|
|
2101
|
+
// invokes a stale row updater against already-destroyed Pixi Graphics (or a detached DOM node)
|
|
2102
|
+
// and throws. Run BEFORE the renderer teardown below, while it can still close cleanly.
|
|
2103
|
+
this.closeModal();
|
|
1816
2104
|
if (typeof document !== 'undefined') {
|
|
1817
2105
|
this.kbd?.detach();
|
|
1818
2106
|
document.removeEventListener('pointerdown', this.pullFocus, true);
|
|
@@ -1822,6 +2110,64 @@ class ShellController extends EventEmitter {
|
|
|
1822
2110
|
}
|
|
1823
2111
|
}
|
|
1824
2112
|
|
|
2113
|
+
/** Geometry for the bar-menu popover. Pure math over rectangles so the DOM and Pixi renderers
|
|
2114
|
+
* place it identically — the renderers only supply measured sizes and apply the result. */
|
|
2115
|
+
const POPOVER = {
|
|
2116
|
+
/** Keep-out from the surface edges. */
|
|
2117
|
+
margin: 8,
|
|
2118
|
+
/** Space between the anchor and the card. */
|
|
2119
|
+
gap: 8,
|
|
2120
|
+
/** Minimum distance from the arrow tip to either rounded corner. */
|
|
2121
|
+
arrowInset: 14,
|
|
2122
|
+
/** A card shorter than this does not fit — flip to the other side instead. */
|
|
2123
|
+
minH: 120,
|
|
2124
|
+
minW: 220,
|
|
2125
|
+
maxW: 320,
|
|
2126
|
+
};
|
|
2127
|
+
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
2128
|
+
/** Card width: content width clamped to [minW, maxW] and never wider than the surface. */
|
|
2129
|
+
function popoverWidth(surfaceW, contentW) {
|
|
2130
|
+
const hi = Math.min(POPOVER.maxW, surfaceW - POPOVER.margin * 2);
|
|
2131
|
+
return Math.max(0, clamp(contentW, Math.min(POPOVER.minW, hi), hi));
|
|
2132
|
+
}
|
|
2133
|
+
/** Place the card above the `anchor` (below if it does not fit), left-aligned to it and clamped
|
|
2134
|
+
* inside the surface. `anchor === null` (no bar / hidden shell) centres it, arrow off.
|
|
2135
|
+
*
|
|
2136
|
+
* `anchor` drives PLACEMENT (x, y, maxH, below) — normally the bar's whole plaque ("plate"), so the
|
|
2137
|
+
* card sits flush with the bar as a whole rather than with whichever control opened it. `pointer` is
|
|
2138
|
+
* the (optional) rect the ARROW points at — normally the burger button, which can sit anywhere
|
|
2139
|
+
* inside the plate. Defaults to `anchor` when omitted, so every caller that only ever had one rect
|
|
2140
|
+
* (i.e. every caller before `pointer` existed) keeps behaving exactly as it did before. */
|
|
2141
|
+
function placePopover(anchor, surface, size, pointer = null) {
|
|
2142
|
+
const { margin, gap, arrowInset, minH } = POPOVER;
|
|
2143
|
+
if (!anchor) {
|
|
2144
|
+
const maxH = Math.max(0, surface.h - margin * 2);
|
|
2145
|
+
const h = Math.min(size.h, maxH);
|
|
2146
|
+
const rawY = (surface.h - h) / 2;
|
|
2147
|
+
const y = clamp(rawY, margin, Math.max(margin, surface.h - h - margin));
|
|
2148
|
+
return {
|
|
2149
|
+
x: Math.max(margin, (surface.w - size.w) / 2),
|
|
2150
|
+
y,
|
|
2151
|
+
maxH,
|
|
2152
|
+
arrowX: -1,
|
|
2153
|
+
below: false,
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
const spaceAbove = anchor.y - gap - margin;
|
|
2157
|
+
const spaceBelow = surface.h - (anchor.y + anchor.h) - gap - margin;
|
|
2158
|
+
// Prefer above; flip only when the card would be squeezed below its usable minimum AND there is
|
|
2159
|
+
// genuinely more room on the other side.
|
|
2160
|
+
const below = spaceAbove < Math.min(size.h, minH) && spaceBelow > spaceAbove;
|
|
2161
|
+
const maxH = Math.max(0, below ? spaceBelow : spaceAbove);
|
|
2162
|
+
const h = Math.min(size.h, maxH);
|
|
2163
|
+
const x = clamp(anchor.x, margin, Math.max(margin, surface.w - size.w - margin));
|
|
2164
|
+
const rawY = below ? anchor.y + anchor.h + gap : anchor.y - gap - h;
|
|
2165
|
+
const y = clamp(rawY, margin, Math.max(margin, surface.h - h - margin));
|
|
2166
|
+
const arrowAnchor = pointer ?? anchor;
|
|
2167
|
+
const arrowX = clamp(arrowAnchor.x + arrowAnchor.w / 2 - x, arrowInset, Math.max(arrowInset, size.w - arrowInset));
|
|
2168
|
+
return { x, y, maxH, arrowX, below };
|
|
2169
|
+
}
|
|
2170
|
+
|
|
1825
2171
|
const NO_INSET = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
1826
2172
|
/** Create a shell with an explicit renderer instance (custom or a built-in HtmlRenderer/PixiRenderer).
|
|
1827
2173
|
* Built-in renderers also have the createGameShell/createPixiShell sugar in /html and /pixi.
|
|
@@ -2029,7 +2375,10 @@ const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
2029
2375
|
border-radius:16px; background:var(--shell-plaque-glass); color:#fff; font-size:14px; font-weight:600; }
|
|
2030
2376
|
#${SHELL_ROOT_ID} .ge-ov-row .ge-grow { flex:1; text-align:left; }
|
|
2031
2377
|
#${SHELL_ROOT_ID} button.ge-ov-row { cursor:pointer; font-family:inherit; transition:background .12s ease, color .12s ease; }
|
|
2032
|
-
#${SHELL_ROOT_ID} button.ge-ov-row:hover { background:var(--shell-plaque-glass-hover); color:var(--shell-accent); }
|
|
2378
|
+
#${SHELL_ROOT_ID} button.ge-ov-row:hover:not([disabled]) { background:var(--shell-plaque-glass-hover); color:var(--shell-accent); }
|
|
2379
|
+
/* disabled rows — mirrors Pixi's box.alpha = 0.5 for all three row kinds. A <button> row carries
|
|
2380
|
+
the native attribute directly; a toggle/range row is a <div> wrapper, so it gets .ge-disabled. */
|
|
2381
|
+
#${SHELL_ROOT_ID} .ge-ov-row[disabled], #${SHELL_ROOT_ID} .ge-ov-row.ge-disabled { opacity:.5; cursor:default; }
|
|
2033
2382
|
#${SHELL_ROOT_ID} .ge-ov-row.ge-col { flex-direction:column; align-items:stretch; gap:10px; }
|
|
2034
2383
|
#${SHELL_ROOT_ID} .ge-ov-row .ge-row-head { display:flex; justify-content:space-between; align-items:center; }
|
|
2035
2384
|
#${SHELL_ROOT_ID} .ge-ov-row .ge-row-head .ge-val { color:var(--shell-plaque-label); font-variant-numeric:tabular-nums; font-weight:700; }
|
|
@@ -2040,6 +2389,7 @@ const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
2040
2389
|
#${SHELL_ROOT_ID} .ge-toggle i { position:absolute; top:2px; left:2px; width:20px; height:20px; border-radius:50%;
|
|
2041
2390
|
background:#fff; transition:left .12s ease; }
|
|
2042
2391
|
#${SHELL_ROOT_ID} .ge-toggle.ge-on i { left:20px; }
|
|
2392
|
+
#${SHELL_ROOT_ID} .ge-toggle:disabled { cursor:default; }
|
|
2043
2393
|
/* sound on/off — speaker icon button (replaces the toggle) */
|
|
2044
2394
|
#${SHELL_ROOT_ID} .ge-snd { pointer-events:auto; cursor:pointer; border:none; background:none; padding:0;
|
|
2045
2395
|
width:36px; height:36px; display:flex; align-items:center; justify-content:center; font-size:24px;
|
|
@@ -2048,6 +2398,22 @@ const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
2048
2398
|
#${SHELL_ROOT_ID} .ge-snd:hover { color:var(--shell-accent); }
|
|
2049
2399
|
#${SHELL_ROOT_ID} .ge-snd:active { transform:scale(.92); }
|
|
2050
2400
|
|
|
2401
|
+
/* bar menu popover — light dismiss (no dim, no blur), card anchored to the burger */
|
|
2402
|
+
#${SHELL_ROOT_ID} .ge-pop-layer { position:absolute; inset:0; z-index:55; pointer-events:auto; }
|
|
2403
|
+
#${SHELL_ROOT_ID} .ge-pop { position:absolute; box-sizing:border-box; display:flex; flex-direction:column;
|
|
2404
|
+
padding:8px; border-radius:18px; background:var(--shell-plaque-dark);
|
|
2405
|
+
box-shadow:0 14px 38px rgba(0,0,0,.5); backdrop-filter:blur(12px) saturate(120%);
|
|
2406
|
+
-webkit-backdrop-filter:blur(12px) saturate(120%); animation:ge-ov-in .12s ease-out; }
|
|
2407
|
+
#${SHELL_ROOT_ID} .ge-pop-body { overflow-y:auto; overflow-x:hidden; min-height:0; }
|
|
2408
|
+
#${SHELL_ROOT_ID} .ge-pop .ge-ov-row { padding:10px 12px; margin-bottom:6px; font-size:13px; }
|
|
2409
|
+
#${SHELL_ROOT_ID} .ge-pop .ge-ov-row:last-child { margin-bottom:0; }
|
|
2410
|
+
#${SHELL_ROOT_ID} .ge-pop-sep { height:1px; margin:6px 4px; background:var(--shell-plaque-line); opacity:.5; }
|
|
2411
|
+
#${SHELL_ROOT_ID} .ge-pop-arrow { position:absolute; bottom:-7px; width:14px; height:14px; margin-left:-7px;
|
|
2412
|
+
background:var(--shell-plaque-dark); transform:rotate(45deg); border-radius:3px; }
|
|
2413
|
+
#${SHELL_ROOT_ID} .ge-pop.ge-pop-below .ge-pop-arrow { bottom:auto; top:-7px; }
|
|
2414
|
+
#${SHELL_ROOT_ID} .ge-pop .ge-mi-icon { flex:0 0 auto; width:20px; font-size:20px; display:flex; }
|
|
2415
|
+
#${SHELL_ROOT_ID} .ge-pop .ge-mi-chev { flex:0 0 auto; width:16px; font-size:16px; color:var(--shell-muted); display:flex; }
|
|
2416
|
+
|
|
2051
2417
|
/* game info — each section is its own glass plaque; body text sized for comfortable reading */
|
|
2052
2418
|
#${SHELL_ROOT_ID} .ge-gi-sec { margin-bottom:12px; background:var(--shell-plaque-glass);
|
|
2053
2419
|
border-radius:16px; padding:16px 18px; }
|
|
@@ -2478,25 +2844,25 @@ function countUp(el, from, to, fmt, durationMs = 450) {
|
|
|
2478
2844
|
// GraphicsContext.svg. To change an icon: edit icons.svg, then run
|
|
2479
2845
|
// `node scripts/gen-icons-from-svg.mjs`.
|
|
2480
2846
|
const SVGS = {
|
|
2481
|
-
spin: `<g fill="currentColor" fill-rule="evenodd"><path d="M13.57 8.41C13.3 8.31 13.51 7.09 13.59 6.65C13.61 6.56 13.55 6.48 13.46 6.48C12.32 6.45 5.74 6.61 4.16 14.85C4.15 14.9 4 15.16 3.8 14.91C3.27 14.23 2.43 12.08 3.46 9.16C6.01 1.92 13 3.07 13.79 3.23C13.83 3.23 13.86 3.2 13.86 3.17C13.85 2.86 13.82 1.68 14.17 1.64C14.51 1.6 19.04 5.61 18.96 5.95C18.86 6.36 13.83 8.51 13.57 8.41Z"/><path d="M10.43 15.59C10.7 15.69 10.49 16.91 10.41 17.35C10.39 17.44 10.45 17.52 10.54 17.52C11.68 17.55 18.26 17.39 19.84 9.15C19.85 9.1 20 8.84 20.2 9.09C20.73 9.77 21.57 11.92 20.54 14.85C17.99 22.08 11 20.93 10.21 20.77C10.17 20.77 10.13 20.8 10.14 20.83C10.15 21.14 10.18 22.32 9.83 22.36C9.49 22.4 4.96 18.39 5.04 18.05C5.14 17.64 10.17 15.49 10.43 15.59Z"/></g>`,
|
|
2482
|
-
turbo1: `<g fill="currentColor" fill-rule="evenodd"><path d="M17.81 2.62L14.41 8.21C14.15 8.64 14.46 9.2 14.97 9.2L18.84 9.21C19.42 9.21 19.72 9.9 19.32 10.31L8.02 22.19C7.89 22.33 7.72 22.4 7.53 22.4L7.53 22.4C7.08 22.39 6.77 21.93 6.93 21.5L9.91 13.91C10.08 13.48 9.76 13.01 9.29 13.01L5.11 13.06C4.61 13.06 4.28 12.53 4.53 12.09L10.07 1.94C10.19 1.73 10.41 1.6 10.65 1.6L17.26 1.62C17.77 1.62 18.08 2.18 17.81 2.62Z"/></g>`,
|
|
2483
|
-
autoplay: `<g fill="currentColor" fill-rule="evenodd"><path d="M19.97 12.41C20.22 11.94 20.95 11.88 20.97 12.47C21.14 16.4 18.71 19.92 14.44 20.97C8.59 22.4 5.29 18.55 5.28 18.52C5.2 18.33 4.41 19.77 4.19 19.59C3.81 19.28 3.78 14.9 3.9 14.78C4.01 14.67 8.39 14.95 8.48 15.31C8.58 15.64 7.2 16.1 7.4 16.38C9.14 18.67 15.82 20.29 19.97 12.41Z"/><path d="M9.3 15.36L9.3 8.67C9.3 8.32 9.97 7.74 10.58 8.14C15.83 11.56 16 11.46 16.01 12.04C16.02 12.61 15.12 12.98 10.63 15.91C10.32 16.11 9.29 15.72 9.29 15.36Z"/><path d="M4.03 11.59C3.78 12.06 3.05 12.12 3.03 11.53C2.86 7.6 5.29 4.08 9.56 3.03C15.41 1.6 18.71 5.45 18.72 5.48C18.8 5.67 19.59 4.23 19.81 4.41C20.19 4.72 20.22 9.1 20.1 9.22C19.99 9.33 15.61 9.05 15.52 8.69C15.42 8.36 16.8 7.9 16.6 7.62C14.86 5.33 8.18 3.71 4.03 11.59Z"/></g>`,
|
|
2847
|
+
spin: `<g fill="currentColor" fill-rule="evenodd"><path d="M13.57 8.41C13.3 8.31 13.51 7.09 13.59 6.65C13.61 6.56 13.55 6.48 13.46 6.48C12.32 6.45 5.74 6.61 4.16 14.85C4.15 14.9 4 15.16 3.8 14.91C3.27 14.23 2.43 12.08 3.46 9.16C6.01 1.92 13 3.07 13.79 3.23C13.83 3.23 13.86 3.2 13.86 3.17C13.85 2.86 13.82 1.68 14.17 1.64C14.51 1.6 19.04 5.61 18.96 5.95C18.86 6.36 13.83 8.51 13.57 8.41L13.57 8.41Z"/><path d="M10.43 15.59C10.7 15.69 10.49 16.91 10.41 17.35C10.39 17.44 10.45 17.52 10.54 17.52C11.68 17.55 18.26 17.39 19.84 9.15C19.85 9.1 20 8.84 20.2 9.09C20.73 9.77 21.57 11.92 20.54 14.85C17.99 22.08 11 20.93 10.21 20.77C10.17 20.77 10.13 20.8 10.14 20.83C10.15 21.14 10.18 22.32 9.83 22.36C9.49 22.4 4.96 18.39 5.04 18.05C5.14 17.64 10.17 15.49 10.43 15.59L10.43 15.59L10.43 15.59Z"/></g>`,
|
|
2848
|
+
turbo1: `<g fill="currentColor" fill-rule="evenodd"><path d="M17.81 2.62L14.41 8.21C14.15 8.64 14.46 9.2 14.97 9.2L18.84 9.21C19.42 9.21 19.72 9.9 19.32 10.31L8.02 22.19C7.89 22.33 7.72 22.4 7.53 22.4L7.53 22.4C7.08 22.39 6.77 21.93 6.93 21.5L9.91 13.91C10.08 13.48 9.76 13.01 9.29 13.01L5.11 13.06C4.61 13.06 4.28 12.53 4.53 12.09L10.07 1.94C10.19 1.73 10.41 1.6 10.65 1.6L17.26 1.62C17.77 1.62 18.08 2.18 17.81 2.62L17.81 2.62Z"/></g>`,
|
|
2849
|
+
autoplay: `<g fill="currentColor" fill-rule="evenodd"><path d="M19.97 12.41C20.22 11.94 20.95 11.88 20.97 12.47C21.14 16.4 18.71 19.92 14.44 20.97C8.59 22.4 5.29 18.55 5.28 18.52C5.2 18.33 4.41 19.77 4.19 19.59C3.81 19.28 3.78 14.9 3.9 14.78C4.01 14.67 8.39 14.95 8.48 15.31C8.58 15.64 7.2 16.1 7.4 16.38C9.14 18.67 15.82 20.29 19.97 12.41L19.97 12.41Z"/><path d="M9.3 15.36L9.3 8.67C9.3 8.32 9.97 7.74 10.58 8.14C15.83 11.56 16 11.46 16.01 12.04C16.02 12.61 15.12 12.98 10.63 15.91C10.32 16.11 9.29 15.72 9.29 15.36L9.3 15.36Z"/><path d="M4.03 11.59C3.78 12.06 3.05 12.12 3.03 11.53C2.86 7.6 5.29 4.08 9.56 3.03C15.41 1.6 18.71 5.45 18.72 5.48C18.8 5.67 19.59 4.23 19.81 4.41C20.19 4.72 20.22 9.1 20.1 9.22C19.99 9.33 15.61 9.05 15.52 8.69C15.42 8.36 16.8 7.9 16.6 7.62C14.86 5.33 8.18 3.71 4.03 11.59L4.03 11.59Z"/></g>`,
|
|
2484
2850
|
stop: `<g fill="currentColor" fill-rule="evenodd"><rect x="1.6" y="1.6" width="20.8" height="20.8" rx="2.9"/></g>`,
|
|
2485
2851
|
menu: `<g fill="currentColor" fill-rule="evenodd"><rect x="1.61" y="3.81" width="20.79" height="4.47" rx="2.23"/><rect x="1.6" y="9.77" width="20.79" height="4.46" rx="2.23"/><rect x="1.6" y="15.72" width="20.79" height="4.47" rx="2.23"/></g>`,
|
|
2486
2852
|
minus: `<g fill="currentColor" fill-rule="evenodd"><rect x="1.6" y="9.86" width="20.8" height="4.28" rx="2.11"/></g>`,
|
|
2487
|
-
plus: `<g fill="currentColor" fill-rule="evenodd"><path d="M19.72 14.37L13.84 14.37L13.84 20.33C13.84 21.48 12.89 22.4 11.74 22.36C10.61 22.36 9.7 21.45 9.7 20.33L9.7 14.37L3.67 14.37C2.52 14.37 1.6 13.42 1.63 12.27C1.63 11.15 2.55 10.23 3.67 10.23L9.7 10.23L9.7 4.28C9.71 1.61 13.83 1.6 13.84 4.28L13.84 10.23L19.72 10.23C22.39 10.24 22.4 14.36 19.72 14.37Z"/></g>`,
|
|
2853
|
+
plus: `<g fill="currentColor" fill-rule="evenodd"><path d="M19.72 14.37L13.84 14.37L13.84 20.33C13.84 21.48 12.89 22.4 11.74 22.36C10.61 22.36 9.7 21.45 9.7 20.33L9.7 14.37L3.67 14.37C2.52 14.37 1.6 13.42 1.63 12.27C1.63 11.15 2.55 10.23 3.67 10.23L9.7 10.23L9.7 4.28C9.71 1.61 13.83 1.6 13.84 4.28L13.84 10.23L19.72 10.23C22.39 10.24 22.4 14.36 19.72 14.37L19.72 14.37Z"/></g>`,
|
|
2488
2854
|
gift: `<rect x="4" y="9" width="16" height="11" rx="2" fill="currentColor"/><path d="M9 9c-1.35 -0.31 -2.18 -1.65 -1.87 -3s1.65 -2.18 3 -1.87c0.93 0.22 1.65 0.94 1.87 1.87c0.31 -1.35 1.65 -2.18 3 -1.87c1.35 0.31 2.18 1.65 1.87 3c-0.22 0.93 -0.94 1.65 -1.87 1.87h-6Z" fill="currentColor"/><rect x="11" y="9" width="2" height="11" fill="rgba(0,0,0,.35)"/>`,
|
|
2489
|
-
info: `<g fill="currentColor" fill-rule="evenodd"><circle cx="11.98" cy="4.27" r="2.67"/><path d="M16.05 21.09L16.05 21.54C16.05 22.02 15.67 22.4 15.19 22.4L8.89 22.4C8.42 22.4 8.04 22.02 8.04 21.54L8.04 20.98C8.04 20.5 8.42 20.12 8.89 20.12L8.89 20.12C9.13 20.12 9.32 19.93 9.32 19.7L9.32 11.59C9.32 11.29 9.08 11.05 8.8 11.05C8.33 11.05 7.95 10.67 7.95 10.19L7.95 9.18C7.95 8.7 8.33 8.32 8.8 8.
|
|
2490
|
-
soundOn: `<g fill="currentColor" fill-rule="evenodd"><path d="M1.6 15.04L1.6 9.18C1.6 8.75 1.95 8.4 2.38 8.4L5.03 8.4C5.21 8.4 5.39 8.36 5.56 8.28L11.6 5.26C12.12 5 12.73 5.38 12.73 5.96L12.73 18.27C12.73 18.85 12.12 19.22 11.6 18.96L5.56 15.94C5.39 15.86 5.22 15.82 5.03 15.82L2.38 15.82C1.95 15.82 1.6 15.47 1.6 15.04Z"/><path d="M16.01 12.11C16.04 12.36 15.67 14.7 14.7 15.23C13.92 15.42 13.54 14.77 13.86 14.1C14.89 12.56 14.88 11.66 13.86 10.13C13.54 9.46 13.92 8.8 14.7 8.99C15.67 9.53 16.06 11.81 16.01 12.11Z"/><path d="M19.15 12.12C19.2 12.51 18.6 16.4 17.3 17.05C16.32 17.54 15.99 16.87 16.42 15.81C17.8 13.4 17.59 9.93 16.42 8.41C15.73 7.5 16.34 6.58 17.26 7.18C18.74 8.15 19.21 11.64 19.15 12.12Z"/><path d="M22.4 12.16C22.16 15.18 21 17.95 19.71 18.82C18.73 19.48 18.22 18.42 18.93 17.32C20.96 14.17 21.92 9.95 18.9 6.29C18.13 5.35 19.35 4.52 20.11 5.48C21.43 7.14 22.33 9.14 22.4 12.16Z"/></g>`,
|
|
2855
|
+
info: `<g fill="currentColor" fill-rule="evenodd"><circle cx="11.98" cy="4.27" r="2.67"/><path d="M16.05 21.09L16.05 21.54C16.05 22.02 15.67 22.4 15.19 22.4L8.89 22.4C8.42 22.4 8.04 22.02 8.04 21.54L8.04 20.98C8.04 20.5 8.42 20.12 8.89 20.12L8.89 20.12C9.13 20.12 9.32 19.93 9.32 19.7L9.32 11.59C9.32 11.29 9.08 11.05 8.8 11.05C8.33 11.05 7.95 10.67 7.95 10.19L7.95 9.18C7.95 8.7 8.33 8.32 8.8 8.32L13.8 8.32C14.27 8.32 14.65 8.71 14.65 9.18L14.65 19.7C14.65 19.93 14.84 20.12 15.07 20.12L15.07 20.12C15.61 20.12 16.05 20.56 16.05 21.09L16.05 21.09Z"/></g>`,
|
|
2856
|
+
soundOn: `<g fill="currentColor" fill-rule="evenodd"><path d="M1.6 15.04L1.6 9.18C1.6 8.75 1.95 8.4 2.38 8.4L5.03 8.4C5.21 8.4 5.39 8.36 5.56 8.28L11.6 5.26C12.12 5 12.73 5.38 12.73 5.96L12.73 18.27C12.73 18.85 12.12 19.22 11.6 18.96L5.56 15.94C5.39 15.86 5.22 15.82 5.03 15.82L2.38 15.82C1.95 15.82 1.6 15.47 1.6 15.04L1.6 15.04Z"/><path d="M16.01 12.11C16.04 12.36 15.67 14.7 14.7 15.23C13.92 15.42 13.54 14.77 13.86 14.1C14.89 12.56 14.88 11.66 13.86 10.13C13.54 9.46 13.92 8.8 14.7 8.99C15.67 9.53 16.06 11.81 16.01 12.11Z"/><path d="M19.15 12.12C19.2 12.51 18.6 16.4 17.3 17.05C16.32 17.54 15.99 16.87 16.42 15.81C17.8 13.4 17.59 9.93 16.42 8.41C15.73 7.5 16.34 6.58 17.26 7.18C18.74 8.15 19.21 11.64 19.15 12.12L19.15 12.12Z"/><path d="M22.4 12.16C22.16 15.18 21 17.95 19.71 18.82C18.73 19.48 18.22 18.42 18.93 17.32C20.96 14.17 21.92 9.95 18.9 6.29C18.13 5.35 19.35 4.52 20.11 5.48C21.43 7.14 22.33 9.14 22.4 12.16L22.4 12.16Z"/></g>`,
|
|
2491
2857
|
soundOff: `<g fill="currentColor" fill-rule="evenodd"><path d="M1.6 14.77L1.6 9.23C1.6 8.82 1.93 8.49 2.34 8.49L4.85 8.49C5.02 8.49 5.19 8.45 5.34 8.38L11.06 5.52C11.55 5.27 12.12 5.63 12.12 6.18L12.12 17.82C12.12 18.37 11.55 18.73 11.06 18.48L5.34 15.62C5.19 15.55 5.02 15.51 4.85 15.51L2.34 15.51C1.93 15.51 1.6 15.18 1.6 14.77Z"/><path d="M21.95 16.53C21.5 16.98 20.78 16.98 20.33 16.53L17.79 13.99L15.25 16.54C14.8 16.99 14.07 16.99 13.62 16.54C13.17 16.09 13.17 15.36 13.62 14.92L16.17 12.37L13.63 9.83C13.18 9.38 13.18 8.65 13.63 8.21C13.85 7.98 14.14 7.87 14.44 7.87C14.73 7.87 15.02 7.98 15.25 8.21L17.79 10.75L20.33 8.21C20.77 7.76 21.5 7.76 21.95 8.21C22.17 8.44 22.28 8.73 22.28 9.02C22.28 9.32 22.17 9.61 21.95 9.83L19.41 12.37L21.95 14.91C22.4 15.36 22.4 16.08 21.95 16.53Z"/></g>`,
|
|
2492
|
-
close: `<g fill="currentColor" fill-rule="evenodd"><path d="M21.39 21.37C20.38 22.38 18.74 22.38 17.73 21.37L12.01 15.64L6.27 21.38C5.26 22.39 3.62 22.39 2.61 21.38C1.6 20.38 1.6 18.74 2.61 17.73L8.35 11.99L2.62 6.25C1.61 5.24 1.61 3.61 2.62 2.6C3.12 2.09 3.79 1.84 4.45 1.84C5.11 1.84 5.77 2.09 6.28 2.6L12.01 8.33L17.72 2.62C18.73 1.61 20.37 1.61 21.38 2.62C21.89 3.12 22.14 3.78 22.14 4.44C22.14 5.11 21.89 5.77 21.38 6.27L15.67 11.99L21.39 17.71C22.4 18.72 22.4 20.36 21.39 21.37Z"/></g>`,
|
|
2493
|
-
back: `<g fill="currentColor" fill-rule="evenodd"><path d="M17.49 2.32L17.49 2.32C18.2 3.03 18.2 4.19 17.49 4.9L11.68 10.71C10.97 11.42 10.97 12.58 11.68 13.29L17.49 19.1C18.21 19.81 18.21 20.97 17.49 21.69L17.49 21.69C16.78 22.4 15.62 22.4 14.91 21.69L7.8 14.58L7.8 14.
|
|
2494
|
-
chevronRight: `<g fill="currentColor" fill-rule="evenodd"><path d="M6.51 21.68L6.51 21.68C5.8 20.97 5.8 19.81 6.51 19.1L12.32 13.29C13.03 12.58 13.03 11.42 12.32 10.71L6.51 4.9C5.79 4.19 5.79 3.03 6.51 2.31L6.51 2.31C7.22 1.6 8.38 1.6 9.09 2.31L16.2 9.42L16.2 9.
|
|
2858
|
+
close: `<g fill="currentColor" fill-rule="evenodd"><path d="M21.39 21.37C20.38 22.38 18.74 22.38 17.73 21.37L12.01 15.64L6.27 21.38C5.26 22.39 3.62 22.39 2.61 21.38C1.6 20.38 1.6 18.74 2.61 17.73L8.35 11.99L2.62 6.25C1.61 5.24 1.61 3.61 2.62 2.6C3.12 2.09 3.79 1.84 4.45 1.84C5.11 1.84 5.77 2.09 6.28 2.6L12.01 8.33L17.72 2.62C18.73 1.61 20.37 1.61 21.38 2.62C21.89 3.12 22.14 3.78 22.14 4.44C22.14 5.11 21.89 5.77 21.38 6.27L15.67 11.99L21.39 17.71C22.4 18.72 22.4 20.36 21.39 21.37L21.39 21.37Z"/></g>`,
|
|
2859
|
+
back: `<g fill="currentColor" fill-rule="evenodd"><path d="M17.49 2.32L17.49 2.32C18.2 3.03 18.2 4.19 17.49 4.9L11.68 10.71C10.97 11.42 10.97 12.58 11.68 13.29L17.49 19.1C18.21 19.81 18.21 20.97 17.49 21.69L17.49 21.69C16.78 22.4 15.62 22.4 14.91 21.69L7.8 14.58L7.8 14.58L6.51 13.29C5.79 12.58 5.79 11.42 6.51 10.71L14.9 2.31C15.62 1.6 16.77 1.6 17.49 2.31L17.49 2.31Z"/></g>`,
|
|
2860
|
+
chevronRight: `<g fill="currentColor" fill-rule="evenodd"><path d="M6.51 21.68L6.51 21.68C5.8 20.97 5.8 19.81 6.51 19.1L12.32 13.29C13.03 12.58 13.03 11.42 12.32 10.71L6.51 4.9C5.79 4.19 5.79 3.03 6.51 2.31L6.51 2.31C7.22 1.6 8.38 1.6 9.09 2.31L16.2 9.42L16.2 9.42L17.49 10.71C18.21 11.42 18.21 12.58 17.49 13.29L9.1 21.69C8.38 22.4 7.23 22.4 6.51 21.69L6.51 21.69Z"/></g>`,
|
|
2495
2861
|
ticket: `<g transform="translate(12 12) scale(0.0354) translate(-518.2 -535.2)"><g transform="rotate(-15 512 512)"><path d="M250 392H774L774 462C739 474 720 504 720 536C720 568 739 598 774 610V680H250V610C285 598 304 568 304 536C304 504 285 474 250 462V392Z" fill="none" stroke="currentColor" stroke-width="52" stroke-linejoin="miter" stroke-linecap="square"/><path d="M562.9 408.3L441.5 553.9L506.9 546.3L472.9 664.1L604 504L528.9 514.3Z" fill="currentColor"/></g></g>`,
|
|
2496
|
-
turbo2: `<g fill="currentColor" fill-rule="evenodd"><path d="M8.24 8.81L3.92 7.68C3.88 7.67 3.88 7.61 3.92 7.61L9.66 6.45C9.8 6.43 9.89 6.56 9.83 6.68L8.86 8.35C8.67 8.73 8.33 8.83 8.24 8.81Z"/><path d="M20.58 3.04L17.34 8.38C17.09 8.8 17.39 9.33 17.87 9.33L21.57 9.33C22.12 9.33 22.4 9.99 22.02 10.39L11.24 21.73C11.12 21.86 10.95 21.93 10.77 21.92L10.77 21.92C10.34 21.92 10.04 21.47 10.2 21.07L13.04 13.82C13.21 13.41 12.9 12.96 12.45 12.97L8.46 13.01C7.98 13.01 7.67 12.5 7.9 12.08L13.2 2.4C13.31 2.2 13.52 2.07 13.75 2.07L20.05 2.09C20.54 2.1 20.84 2.63 20.58 3.04Z"/><path d="M5.96 12.68L1.64 11.54C1.6 11.54 1.6 11.47 1.64 11.47L7.38 10.31C7.
|
|
2497
|
-
turboOff: `<g fill="currentColor" fill-rule="evenodd"><path d="M17.81 2.62L14.41 8.21C14.15 8.64 14.46 9.2 14.97 9.2L18.84 9.21C19.42 9.21 19.72 9.9 19.32 10.31L8.02 22.19C7.89 22.33 7.72 22.4 7.53 22.4L7.53 22.4C7.08 22.39 6.77 21.93 6.93 21.5L9.91 13.91C10.08 13.48 9.76 13.01 9.29 13.01L5.11 13.06C4.61 13.06 4.28 12.53 4.53 12.09L10.07 1.94C10.19 1.73 10.41 1.6 10.65 1.6L17.26 1.62C17.77 1.62 18.08 2.18 17.81 2.62Z"/></g>`,
|
|
2498
|
-
chevronUp: `<g fill="currentColor" fill-rule="evenodd"><path d="M21.68 17.49L21.68 17.49C20.97 18.2 19.81 18.2 19.1 17.49L13.29 11.68C12.58 10.97 11.42 10.97 10.71 11.68L4.9 17.49C4.19 18.21 3.03 18.21 2.31 17.49L2.31 17.49C1.6 16.78 1.6 15.62 2.31 14.91L9.42 7.8L9.42 7.
|
|
2499
|
-
chevronDown: `<g fill="currentColor" fill-rule="evenodd"><path d="M2.32 6.51L2.32 6.51C3.03 5.8 4.19 5.8 4.9 6.51L10.71 12.32C11.42 13.03 12.58 13.03 13.29 12.32L19.1 6.51C19.82 5.79 20.97 5.79 21.69 6.51L21.69 6.51C22.4 7.22 22.4 8.38 21.69 9.09L14.58 16.19L14.58 16.
|
|
2862
|
+
turbo2: `<g fill="currentColor" fill-rule="evenodd"><path d="M8.24 8.81L3.92 7.68C3.88 7.67 3.88 7.61 3.92 7.61L9.66 6.45C9.8 6.43 9.89 6.56 9.83 6.68L8.86 8.35C8.67 8.73 8.33 8.83 8.24 8.81L8.24 8.81Z"/><path d="M20.58 3.04L17.34 8.38C17.09 8.8 17.39 9.33 17.87 9.33L21.57 9.33C22.12 9.33 22.4 9.99 22.02 10.39L11.24 21.73C11.12 21.86 10.95 21.93 10.77 21.92L10.77 21.92C10.34 21.92 10.04 21.47 10.2 21.07L13.04 13.82C13.21 13.41 12.9 12.96 12.45 12.97L8.46 13.01C7.98 13.01 7.67 12.5 7.9 12.08L13.2 2.4C13.31 2.2 13.52 2.07 13.75 2.07L20.05 2.09C20.54 2.1 20.84 2.63 20.58 3.04L20.58 3.04Z"/><path d="M5.96 12.68L1.64 11.54C1.6 11.54 1.6 11.47 1.64 11.47L7.38 10.31C7.51 10.3 7.61 10.42 7.55 10.55L6.58 12.22C6.39 12.59 6.05 12.69 5.96 12.68L5.96 12.68Z"/><path d="M9.67 16.66L5.35 15.53C5.31 15.52 5.31 15.46 5.35 15.46L11.09 14.3C11.22 14.28 11.32 14.41 11.26 14.53L10.29 16.2C10.1 16.58 9.76 16.68 9.67 16.66L9.67 16.66Z"/></g>`,
|
|
2863
|
+
turboOff: `<g fill="currentColor" fill-rule="evenodd"><path d="M17.81 2.62L14.41 8.21C14.15 8.64 14.46 9.2 14.97 9.2L18.84 9.21C19.42 9.21 19.72 9.9 19.32 10.31L8.02 22.19C7.89 22.33 7.72 22.4 7.53 22.4L7.53 22.4C7.08 22.39 6.77 21.93 6.93 21.5L9.91 13.91C10.08 13.48 9.76 13.01 9.29 13.01L5.11 13.06C4.61 13.06 4.28 12.53 4.53 12.09L10.07 1.94C10.19 1.73 10.41 1.6 10.65 1.6L17.26 1.62C17.77 1.62 18.08 2.18 17.81 2.62L17.81 2.62Z"/></g>`,
|
|
2864
|
+
chevronUp: `<g fill="currentColor" fill-rule="evenodd"><path d="M21.68 17.49L21.68 17.49C20.97 18.2 19.81 18.2 19.1 17.49L13.29 11.68C12.58 10.97 11.42 10.97 10.71 11.68L4.9 17.49C4.19 18.21 3.03 18.21 2.31 17.49L2.31 17.49C1.6 16.78 1.6 15.62 2.31 14.91L9.42 7.8L9.42 7.8L10.71 6.51C11.42 5.79 12.58 5.79 13.29 6.51L21.69 14.9C22.4 15.62 22.4 16.77 21.69 17.49L21.69 17.49Z"/></g>`,
|
|
2865
|
+
chevronDown: `<g fill="currentColor" fill-rule="evenodd"><path d="M2.32 6.51L2.32 6.51C3.03 5.8 4.19 5.8 4.9 6.51L10.71 12.32C11.42 13.03 12.58 13.03 13.29 12.32L19.1 6.51C19.82 5.79 20.97 5.79 21.69 6.51L21.69 6.51C22.4 7.22 22.4 8.38 21.69 9.09L14.58 16.19L14.58 16.2L13.29 17.49C12.58 18.21 11.42 18.21 10.71 17.49L2.31 9.1C1.6 8.38 1.6 7.23 2.31 6.51L2.32 6.51L2.32 6.51Z"/></g>`,
|
|
2500
2866
|
};
|
|
2501
2867
|
/** Inline SVG string for an icon, sized to 1em (scale via font-size/width). */
|
|
2502
2868
|
function icon(name) {
|
|
@@ -2855,77 +3221,246 @@ function createOverlay(opts) {
|
|
|
2855
3221
|
root.append(head, scroll);
|
|
2856
3222
|
return { root, body, scroll };
|
|
2857
3223
|
}
|
|
3224
|
+
/** A light-dismiss popover: a transparent full-surface layer (closes on pointerdown) holding a
|
|
3225
|
+
* card with an arrow that points at `pointer`. Append rows to `body`; call `position()` after the
|
|
3226
|
+
* card is in the DOM and again on resize. */
|
|
3227
|
+
function createPopover(opts) {
|
|
3228
|
+
const root = document.createElement('div');
|
|
3229
|
+
root.className = 'ge-pop-layer';
|
|
3230
|
+
root.dataset.ge = opts.ge;
|
|
3231
|
+
const card = document.createElement('div');
|
|
3232
|
+
card.className = 'ge-pop';
|
|
3233
|
+
card.dataset.ge = 'menu-card';
|
|
3234
|
+
const body = document.createElement('div');
|
|
3235
|
+
body.className = 'ge-pop-body';
|
|
3236
|
+
const arrow = document.createElement('span');
|
|
3237
|
+
arrow.className = 'ge-pop-arrow';
|
|
3238
|
+
card.append(body, arrow);
|
|
3239
|
+
root.appendChild(card);
|
|
3240
|
+
// Clicks inside the card must not reach the dismiss layer.
|
|
3241
|
+
card.addEventListener('pointerdown', (e) => e.stopPropagation());
|
|
3242
|
+
root.addEventListener('pointerdown', opts.onClose);
|
|
3243
|
+
const resolveEl = (v) => typeof v === 'function' ? v() : (v ?? null);
|
|
3244
|
+
/** A rect in surface coordinates, or null when unresolved/fully zero-sized (a zero-HEIGHT rect —
|
|
3245
|
+
* e.g. a not-yet-laid-out anchor — is still considered valid, matching placePopover's own rule). */
|
|
3246
|
+
const rectOf = (el, surfaceRect) => {
|
|
3247
|
+
if (!el)
|
|
3248
|
+
return null;
|
|
3249
|
+
const r = el.getBoundingClientRect();
|
|
3250
|
+
if (r.width <= 0 && r.height <= 0)
|
|
3251
|
+
return null;
|
|
3252
|
+
return { x: r.left - surfaceRect.left, y: r.top - surfaceRect.top, w: r.width, h: r.height };
|
|
3253
|
+
};
|
|
3254
|
+
const position = () => {
|
|
3255
|
+
const surfaceRect = opts.surface.getBoundingClientRect();
|
|
3256
|
+
const surface = { w: surfaceRect.width || opts.surface.clientWidth, h: surfaceRect.height || opts.surface.clientHeight };
|
|
3257
|
+
if (surface.w <= 0 || surface.h <= 0)
|
|
3258
|
+
return;
|
|
3259
|
+
const s = opts.scale?.() ?? 1;
|
|
3260
|
+
// 1. Clear a prior run's constraints (transform + width + max-height + left) before measuring —
|
|
3261
|
+
// otherwise scrollWidth/offsetHeight report the already-scaled/clamped box (not the natural
|
|
3262
|
+
// content size) on every call after the first, and the card can shrink to fit a
|
|
3263
|
+
// narrower/shorter surface but never grow back when the surface widens/grows again. An uncleared
|
|
3264
|
+
// max-height is the worse of the two: placePopover positions a card sized for the STALE clamped
|
|
3265
|
+
// height, then the un-clamped natural height (restored below) springs back afterward — so the
|
|
3266
|
+
// rendered card can overlap the plate or run off the surface edge until reopened. The transform
|
|
3267
|
+
// doesn't affect layout either way, but clearing it keeps every pass measuring the same way.
|
|
3268
|
+
// `left` matters too: `.ge-pop` is absolutely positioned inside an `inset:0` layer, so with the
|
|
3269
|
+
// previous pass's `left` still applied, its shrink-to-fit width is bounded by (layerWidth − left)
|
|
3270
|
+
// instead of the card's true natural width. Invisible at scale 1; a scale below 1 lays the card
|
|
3271
|
+
// out at up to 1/s its on-screen width, so a stale left — only ever a few hundred px — can clip it.
|
|
3272
|
+
card.style.transform = '';
|
|
3273
|
+
card.style.width = '';
|
|
3274
|
+
card.style.maxHeight = '';
|
|
3275
|
+
card.style.left = '0px';
|
|
3276
|
+
const naturalW = card.scrollWidth || POPOVER.minW;
|
|
3277
|
+
// 2. Resolve the ON-SCREEN width from the natural (unscaled) width scaled up to screen units,
|
|
3278
|
+
// then set the LOCAL style.width so the card renders at that resolved width once scaled — and
|
|
3279
|
+
// re-measure the height at that width (wrapping may have changed it).
|
|
3280
|
+
const resolvedW = popoverWidth(surface.w, naturalW * s);
|
|
3281
|
+
card.style.width = `${resolvedW / s}px`;
|
|
3282
|
+
const naturalH = card.offsetHeight || POPOVER.minH;
|
|
3283
|
+
// 3. Resolve plate/pointer in surface coordinates and place using SCREEN-space size —
|
|
3284
|
+
// placePopover works in surface pixels throughout, so both the anchor rects and `size` here are
|
|
3285
|
+
// screen units even though the card's own layout (width/height above) is still LOCAL/unscaled.
|
|
3286
|
+
const plateEl = resolveEl(opts.plate);
|
|
3287
|
+
const pointerEl = resolveEl(opts.pointer);
|
|
3288
|
+
let plate = rectOf(plateEl, surfaceRect) ?? rectOf(pointerEl, surfaceRect);
|
|
3289
|
+
// Extend the plate's TOP edge upward to a popped-out hero's true top (e.g. the mobile SPIN/FS
|
|
3290
|
+
// control, taller than its row) — bottom edge untouched. Both rects are already in the SAME
|
|
3291
|
+
// surface-coordinate space (post any bar-scale transform), so this is a plain coordinate compare,
|
|
3292
|
+
// no separate unit conversion needed.
|
|
3293
|
+
const overflowEl = resolveEl(opts.plateOverflowTop);
|
|
3294
|
+
if (plate) {
|
|
3295
|
+
const overflowRect = rectOf(overflowEl, surfaceRect);
|
|
3296
|
+
if (overflowRect && overflowRect.y < plate.y) {
|
|
3297
|
+
plate = { ...plate, h: plate.h + (plate.y - overflowRect.y), y: overflowRect.y };
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
const pointer = rectOf(pointerEl, surfaceRect);
|
|
3301
|
+
const p = placePopover(plate, surface, { w: resolvedW, h: naturalH * s }, pointer);
|
|
3302
|
+
// 4. Apply. maxHeight and the arrow's offset are LOCAL (unscaled) too, since both live inside the
|
|
3303
|
+
// scaled card; transform-origin:top left keeps left/top (screen units) as the card's visual
|
|
3304
|
+
// top-left regardless of `s`, and — since a transform doesn't affect layout — the next
|
|
3305
|
+
// measurement pass stays clean without needing any extra bookkeeping.
|
|
3306
|
+
card.style.left = `${p.x}px`;
|
|
3307
|
+
card.style.top = `${p.y}px`;
|
|
3308
|
+
card.style.maxHeight = `${p.maxH / s}px`;
|
|
3309
|
+
card.style.transformOrigin = 'top left';
|
|
3310
|
+
card.style.transform = Math.abs(s - 1) > 0.001 ? `scale(${s})` : '';
|
|
3311
|
+
card.classList.toggle('ge-pop-below', p.below);
|
|
3312
|
+
if (p.arrowX < 0)
|
|
3313
|
+
arrow.style.display = 'none';
|
|
3314
|
+
else {
|
|
3315
|
+
arrow.style.display = '';
|
|
3316
|
+
arrow.style.left = `${p.arrowX / s}px`;
|
|
3317
|
+
}
|
|
3318
|
+
};
|
|
3319
|
+
return { root, card, body, position };
|
|
3320
|
+
}
|
|
2858
3321
|
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
3322
|
+
/** The bar menu, as a light-dismiss popover anchored to the burger. Rows come from the core model,
|
|
3323
|
+
* so DOM and Pixi always show the same list in the same order. */
|
|
3324
|
+
function openMenuPopover(host, surface, getScale) {
|
|
3325
|
+
const pop = createPopover({
|
|
3326
|
+
ge: 'menu-popover',
|
|
3327
|
+
surface,
|
|
3328
|
+
// Resolved lazily on every position() call, not captured once: HtmlRenderer's renderBar() (run
|
|
3329
|
+
// on every resize, and on ~20 other state changes) rebuilds the bottom bar from scratch, so the
|
|
3330
|
+
// plate/burger are brand-new elements after it — a captured reference would already be detached
|
|
3331
|
+
// by the next position() call and the card would silently recentre with its arrow hidden.
|
|
3332
|
+
// The plate is the bar's own plaque — the continuous dark panel wide, the controls row mobile
|
|
3333
|
+
// (NOT the info pill below it) — so the card sits flush with the WHOLE bar, not just the burger.
|
|
3334
|
+
plate: () => surface.querySelector(host.layout === 'mobile' ? '.ge-m-controls' : '.ge-bar-panel'),
|
|
3335
|
+
pointer: () => surface.querySelector('[data-ge="menu"]'),
|
|
3336
|
+
// Mobile only: the SPIN disc / FS counter is taller than `.ge-m-controls` and centred inside it
|
|
3337
|
+
// (`align-items:center` over a shorter fixed-height row), so it pops above the row's own top edge
|
|
3338
|
+
// — see createPopover's `plateOverflowTop` doc comment. Exactly one of the two (or neither, e.g. a
|
|
3339
|
+
// replay with no free spins) is ever rendered, so a combined selector resolves to whichever is
|
|
3340
|
+
// current. Wide's plate already contains its content, so this deliberately resolves to null there.
|
|
3341
|
+
plateOverflowTop: () => host.layout === 'mobile'
|
|
3342
|
+
? surface.querySelector('[data-ge="spin"], [data-ge="fs-counter"]')
|
|
3343
|
+
: null,
|
|
3344
|
+
scale: getScale,
|
|
3345
|
+
onClose: () => host.actions.closeOverlay(),
|
|
3346
|
+
});
|
|
3347
|
+
const updaters = {};
|
|
3348
|
+
for (const row of resolveMenu(host)) {
|
|
3349
|
+
pop.body.appendChild(buildRow(host, row, updaters, () => pop.position()));
|
|
3350
|
+
}
|
|
3351
|
+
// Live updates while open (host.setMenuValue / setSound / setVolume); cleared by the controller.
|
|
3352
|
+
host.setMenuRefresh((id, v) => updaters[id]?.(v));
|
|
3353
|
+
return { root: pop.root, position: pop.position };
|
|
3354
|
+
}
|
|
3355
|
+
function glyph(name, cls) {
|
|
3356
|
+
if (!name)
|
|
3357
|
+
return null;
|
|
3358
|
+
const el = document.createElement('span');
|
|
3359
|
+
el.className = cls;
|
|
3360
|
+
el.innerHTML = icon(name);
|
|
3361
|
+
return el;
|
|
3362
|
+
}
|
|
3363
|
+
function buildRow(host, row, updaters, reposition) {
|
|
3364
|
+
if (row.kind === 'separator') {
|
|
3365
|
+
const sep = document.createElement('div');
|
|
3366
|
+
sep.className = 'ge-pop-sep';
|
|
3367
|
+
sep.dataset.ge = 'menu-sep';
|
|
3368
|
+
return sep;
|
|
3369
|
+
}
|
|
3370
|
+
if (row.kind === 'button') {
|
|
3371
|
+
// The row hook goes on a wrapper so both `menu-row-<id>` (order assertions) and
|
|
3372
|
+
// `menu-item-<id>` (the clickable control) exist, as they do for toggle/range rows.
|
|
3373
|
+
const wrap = document.createElement('div');
|
|
3374
|
+
wrap.dataset.ge = `menu-row-${row.id}`;
|
|
3375
|
+
const btn = document.createElement('button');
|
|
3376
|
+
btn.className = 'ge-ov-row';
|
|
3377
|
+
btn.dataset.ge = `menu-item-${row.id}`;
|
|
3378
|
+
btn.disabled = row.disabled;
|
|
3379
|
+
const label = document.createElement('span');
|
|
3380
|
+
label.className = 'ge-grow';
|
|
3381
|
+
label.textContent = row.label;
|
|
3382
|
+
const ico = glyph(row.icon, 'ge-mi-icon');
|
|
3383
|
+
const chev = row.chevron ? glyph('chevronRight', 'ge-mi-chev') : null;
|
|
3384
|
+
for (const el of [ico, label, chev])
|
|
3385
|
+
if (el)
|
|
3386
|
+
btn.appendChild(el);
|
|
3387
|
+
btn.addEventListener('click', () => {
|
|
3388
|
+
if (btn.disabled)
|
|
3389
|
+
return;
|
|
3390
|
+
host.actions.closeOverlay();
|
|
3391
|
+
row.select();
|
|
3392
|
+
});
|
|
3393
|
+
wrap.appendChild(btn);
|
|
3394
|
+
return wrap;
|
|
3395
|
+
}
|
|
3396
|
+
if (row.kind === 'toggle') {
|
|
3397
|
+
const el = document.createElement('div');
|
|
3398
|
+
el.className = 'ge-ov-row';
|
|
3399
|
+
el.classList.toggle('ge-disabled', row.disabled); // a <div> can't carry [disabled] itself
|
|
3400
|
+
el.dataset.ge = `menu-row-${row.id}`;
|
|
3401
|
+
const ico = glyph(row.icon(row.get()), 'ge-mi-icon');
|
|
3402
|
+
const label = document.createElement('span');
|
|
3403
|
+
label.className = 'ge-grow';
|
|
3404
|
+
label.textContent = row.label;
|
|
2865
3405
|
const btn = document.createElement('button');
|
|
2866
|
-
btn.className = 'ge-
|
|
2867
|
-
btn.dataset.ge =
|
|
2868
|
-
btn.
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
btn.classList.toggle('ge-
|
|
2872
|
-
btn.setAttribute('aria-pressed', String(
|
|
3406
|
+
btn.className = 'ge-toggle';
|
|
3407
|
+
btn.dataset.ge = `menu-item-${row.id}`;
|
|
3408
|
+
btn.disabled = row.disabled;
|
|
3409
|
+
btn.innerHTML = '<i></i>';
|
|
3410
|
+
const paint = (v) => {
|
|
3411
|
+
btn.classList.toggle('ge-on', v);
|
|
3412
|
+
btn.setAttribute('aria-pressed', String(v));
|
|
3413
|
+
const next = row.icon(v);
|
|
3414
|
+
if (ico && next)
|
|
3415
|
+
ico.innerHTML = icon(next);
|
|
2873
3416
|
};
|
|
2874
|
-
paint(
|
|
2875
|
-
btn.addEventListener('click', () =>
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
const row = document.createElement('div');
|
|
2879
|
-
row.className = 'ge-ov-row';
|
|
2880
|
-
row.innerHTML = `<span class="ge-grow">${host.t('Sound')}</span>`;
|
|
2881
|
-
row.appendChild(btn);
|
|
2882
|
-
return row;
|
|
2883
|
-
})();
|
|
2884
|
-
body.appendChild(sound);
|
|
2885
|
-
// Volume sliders — full-width column rows with a live value readout. Positions are read from the
|
|
2886
|
-
// shell's stored volumes (not hardcoded to 100%), so reopening the overlay reflects the last set
|
|
2887
|
-
// value, and `host.setVolume()` from game code updates them live via the registered refreshers.
|
|
2888
|
-
const updaters = {};
|
|
2889
|
-
const slider = (key, label) => {
|
|
2890
|
-
const row = document.createElement('div');
|
|
2891
|
-
row.className = 'ge-ov-row ge-col';
|
|
2892
|
-
const head = document.createElement('div');
|
|
2893
|
-
head.className = 'ge-row-head';
|
|
2894
|
-
const val = document.createElement('span');
|
|
2895
|
-
val.className = 'ge-val';
|
|
2896
|
-
head.innerHTML = `<span>${label}</span>`;
|
|
2897
|
-
head.appendChild(val);
|
|
2898
|
-
const input = document.createElement('input');
|
|
2899
|
-
input.type = 'range';
|
|
2900
|
-
input.min = '0';
|
|
2901
|
-
input.max = '1';
|
|
2902
|
-
input.step = '0.05';
|
|
2903
|
-
input.className = 'ge-slider';
|
|
2904
|
-
input.dataset.ge = `setting-${key}`;
|
|
2905
|
-
const paint = (v) => { input.value = String(v); val.textContent = `${Math.round(v * 100)}%`; };
|
|
2906
|
-
paint(host.getVolume(key));
|
|
2907
|
-
input.addEventListener('input', () => {
|
|
2908
|
-
val.textContent = `${Math.round(Number(input.value) * 100)}%`;
|
|
2909
|
-
host.setVolume(key, Number(input.value));
|
|
3417
|
+
paint(row.get());
|
|
3418
|
+
btn.addEventListener('click', () => {
|
|
3419
|
+
if (!btn.disabled)
|
|
3420
|
+
row.set(!row.get());
|
|
2910
3421
|
});
|
|
2911
|
-
updaters[
|
|
2912
|
-
|
|
2913
|
-
|
|
3422
|
+
updaters[row.id] = (v) => paint(v === true);
|
|
3423
|
+
if (ico)
|
|
3424
|
+
el.appendChild(ico);
|
|
3425
|
+
el.append(label, btn);
|
|
3426
|
+
return el;
|
|
3427
|
+
}
|
|
3428
|
+
// range
|
|
3429
|
+
const el = document.createElement('div');
|
|
3430
|
+
el.className = 'ge-ov-row ge-col';
|
|
3431
|
+
el.classList.toggle('ge-disabled', row.disabled); // a <div> can't carry [disabled] itself
|
|
3432
|
+
el.dataset.ge = `menu-row-${row.id}`;
|
|
3433
|
+
const head = document.createElement('div');
|
|
3434
|
+
head.className = 'ge-row-head';
|
|
3435
|
+
const name = document.createElement('span');
|
|
3436
|
+
name.textContent = row.label;
|
|
3437
|
+
const val = document.createElement('span');
|
|
3438
|
+
val.className = 'ge-val';
|
|
3439
|
+
head.append(name, val);
|
|
3440
|
+
const input = document.createElement('input');
|
|
3441
|
+
input.type = 'range';
|
|
3442
|
+
input.className = 'ge-slider';
|
|
3443
|
+
input.dataset.ge = `menu-item-${row.id}`;
|
|
3444
|
+
input.min = String(row.min);
|
|
3445
|
+
input.max = String(row.max);
|
|
3446
|
+
input.step = String(row.step);
|
|
3447
|
+
input.disabled = row.disabled;
|
|
3448
|
+
const paint = (v) => {
|
|
3449
|
+
input.value = String(v);
|
|
3450
|
+
val.textContent = row.format(v);
|
|
3451
|
+
};
|
|
3452
|
+
paint(row.get());
|
|
3453
|
+
input.addEventListener('input', () => {
|
|
3454
|
+
const v = Number(input.value);
|
|
3455
|
+
val.textContent = row.format(v);
|
|
3456
|
+
row.set(v);
|
|
3457
|
+
});
|
|
3458
|
+
updaters[row.id] = (v) => {
|
|
3459
|
+
paint(Number(v));
|
|
3460
|
+
reposition();
|
|
2914
3461
|
};
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
body.appendChild(slider('sfx', host.t('SFX')));
|
|
2918
|
-
// Live-update sliders when volume changes via host.setVolume (shell clears on close).
|
|
2919
|
-
host.setVolumeRefresh((key, v) => updaters[key]?.(v));
|
|
2920
|
-
// Game info — full-width row button that opens its own overlay
|
|
2921
|
-
const gameInfo = document.createElement('button');
|
|
2922
|
-
gameInfo.className = 'ge-ov-row';
|
|
2923
|
-
gameInfo.dataset.ge = 'game-info-btn';
|
|
2924
|
-
gameInfo.style.marginTop = '6px';
|
|
2925
|
-
gameInfo.innerHTML = `<span style="width:22px;font-size:22px">${icon('info')}</span><span class="ge-grow">${host.t('Game info')}</span><span style="width:20px;font-size:20px;color:var(--shell-muted)">${icon('chevronRight')}</span>`;
|
|
2926
|
-
gameInfo.addEventListener('click', () => { root.remove(); host.actions.openInfo(); });
|
|
2927
|
-
body.appendChild(gameInfo);
|
|
2928
|
-
return root;
|
|
3462
|
+
el.append(head, input);
|
|
3463
|
+
return el;
|
|
2929
3464
|
}
|
|
2930
3465
|
|
|
2931
3466
|
/** Default order key for the auto-injected hotkeys section: just after `controls` (-1). */
|
|
@@ -2935,7 +3470,7 @@ function openGameInfoModal(host) {
|
|
|
2935
3470
|
const { root, body, scroll } = createOverlay({
|
|
2936
3471
|
title: host.t('Game info'),
|
|
2937
3472
|
onClose: () => host.actions.closeOverlay(),
|
|
2938
|
-
onBack: () => { root.remove(); host.actions.
|
|
3473
|
+
onBack: () => { root.remove(); host.actions.openMenu(); },
|
|
2939
3474
|
});
|
|
2940
3475
|
root.dataset.ge = 'info-modal';
|
|
2941
3476
|
const rawSections = host.config.gameInfo.sections ?? [];
|
|
@@ -3890,13 +4425,13 @@ function buildReplayModal(host, opts, reopen) {
|
|
|
3890
4425
|
const REMOVE_FADE_MS = 300;
|
|
3891
4426
|
/** Count-up the value span (.ge-rd-val) of a readout, leaving its label untouched.
|
|
3892
4427
|
* Returns the count-up canceler so the renderer can stop it before the node is replaced. */
|
|
3893
|
-
function animateReadout(el, from, to, fmt) {
|
|
4428
|
+
function animateReadout(el, from, to, fmt, durationMs) {
|
|
3894
4429
|
const val = el.querySelector('.ge-rd-val');
|
|
3895
4430
|
if (!val) {
|
|
3896
4431
|
el.textContent = fmt(to);
|
|
3897
4432
|
return () => { };
|
|
3898
4433
|
}
|
|
3899
|
-
return countUp(val, from, to, fmt);
|
|
4434
|
+
return durationMs == null ? countUp(val, from, to, fmt) : countUp(val, from, to, fmt, durationMs);
|
|
3900
4435
|
}
|
|
3901
4436
|
class HtmlRenderer {
|
|
3902
4437
|
host;
|
|
@@ -3908,7 +4443,11 @@ class HtmlRenderer {
|
|
|
3908
4443
|
ro = null;
|
|
3909
4444
|
moneyAnims = [];
|
|
3910
4445
|
modalOnKey;
|
|
4446
|
+
popoverPosition = null;
|
|
3911
4447
|
destroyed = false;
|
|
4448
|
+
/** The scale factor last applied to the bar by applyFitScale() — exposed so the menu popover can
|
|
4449
|
+
* match it exactly (see getBarScale) rather than re-derive its own, possibly-disagreeing, value. */
|
|
4450
|
+
barScale = 1;
|
|
3912
4451
|
/** MutationObserver for buy-bonus confirm fit — fires fitModals() when nodes are added
|
|
3913
4452
|
* inside the modalHost (e.g. the confirm dialog appended after the grid is open). */
|
|
3914
4453
|
mutObs = null;
|
|
@@ -3951,15 +4490,23 @@ class HtmlRenderer {
|
|
|
3951
4490
|
this.barHost.innerHTML = '';
|
|
3952
4491
|
this.barHost.appendChild(renderBottomBar(this.host));
|
|
3953
4492
|
this.applyFitScale();
|
|
4493
|
+
// renderBar() runs on every resize AND on ~20 other state changes (bet/win/turbo/mode/…), any of
|
|
4494
|
+
// which can change barScale and/or the plate rect (e.g. a WIN pill appearing mid-autoplay
|
|
4495
|
+
// retriggers the overflow-tightening branch). Only the resize hook used to reposition an open
|
|
4496
|
+
// popover, so a live bar-content change while the menu was open left the card at the stale
|
|
4497
|
+
// scale/position until the next resize. `popoverPosition` only reads plate/pointer/scale getters
|
|
4498
|
+
// and writes card styles — no path back into renderBar() — so this cannot recurse; it's `null`
|
|
4499
|
+
// whenever no popover is open (or a different overlay kind is), so this cannot throw either.
|
|
4500
|
+
this.popoverPosition?.();
|
|
3954
4501
|
}
|
|
3955
4502
|
setLayout() {
|
|
3956
4503
|
this.renderBar();
|
|
3957
4504
|
}
|
|
3958
|
-
animateMoney(field, from, to) {
|
|
4505
|
+
animateMoney(field, from, to, durationMs) {
|
|
3959
4506
|
const fmt = (n) => this.host.formatCurrency(n, field === 'win');
|
|
3960
4507
|
const el = this.barHost.querySelector(`[data-ge="${field}"]`);
|
|
3961
4508
|
if (el)
|
|
3962
|
-
this.moneyAnims.push(animateReadout(el, from, to, fmt));
|
|
4509
|
+
this.moneyAnims.push(animateReadout(el, from, to, fmt, durationMs));
|
|
3963
4510
|
}
|
|
3964
4511
|
openOverlay(req) {
|
|
3965
4512
|
const built = this.buildOverlay(req);
|
|
@@ -3970,11 +4517,9 @@ class HtmlRenderer {
|
|
|
3970
4517
|
}
|
|
3971
4518
|
closeOverlay() {
|
|
3972
4519
|
this.modalOnKey = undefined;
|
|
4520
|
+
this.popoverPosition = null;
|
|
3973
4521
|
this.modalHost.innerHTML = '';
|
|
3974
4522
|
}
|
|
3975
|
-
refreshSoundIcon(_on) {
|
|
3976
|
-
// Settings registers via host.setSoundRefresh; nothing extra here
|
|
3977
|
-
}
|
|
3978
4523
|
destroy() {
|
|
3979
4524
|
if (this.destroyed)
|
|
3980
4525
|
return Promise.resolve();
|
|
@@ -3995,6 +4540,10 @@ class HtmlRenderer {
|
|
|
3995
4540
|
}
|
|
3996
4541
|
/** Trigger a bar fit-scale pass (used by tests that stub geometry after the initial render). */
|
|
3997
4542
|
fitBar() { this.applyFitScale(); }
|
|
4543
|
+
/** The scale factor applyFitScale() last applied to the bar — the menu popover multiplies its own
|
|
4544
|
+
* card by this SAME number so its typography/padding/row-heights carry the same visual weight
|
|
4545
|
+
* relationship the bar has, instead of ignoring the viewport like a fixed-px card would. */
|
|
4546
|
+
getBarScale() { return this.barScale; }
|
|
3998
4547
|
// ── private ────────────────────────────────────────────────────────────────
|
|
3999
4548
|
cancelMoneyAnims() {
|
|
4000
4549
|
for (const cancel of this.moneyAnims)
|
|
@@ -4003,8 +4552,9 @@ class HtmlRenderer {
|
|
|
4003
4552
|
}
|
|
4004
4553
|
buildOverlay(req) {
|
|
4005
4554
|
switch (req.kind) {
|
|
4006
|
-
case '
|
|
4007
|
-
const root =
|
|
4555
|
+
case 'menu': {
|
|
4556
|
+
const { root, position } = openMenuPopover(this.host, this.root, () => this.getBarScale());
|
|
4557
|
+
this.popoverPosition = position;
|
|
4008
4558
|
return { root };
|
|
4009
4559
|
}
|
|
4010
4560
|
case 'gameInfo': {
|
|
@@ -4048,6 +4598,7 @@ class HtmlRenderer {
|
|
|
4048
4598
|
this.modalHost.appendChild(el);
|
|
4049
4599
|
this.modalOnKey = onKey;
|
|
4050
4600
|
this.fitModals();
|
|
4601
|
+
this.popoverPosition?.(); // measure + place now that the card is in the DOM
|
|
4051
4602
|
}
|
|
4052
4603
|
/** Uniformly scale every open centred card modal (.ge-sheet) down so it fits a short/narrow
|
|
4053
4604
|
* popout. Covers pickers, generic + replay modals, AND the buy-bonus confirm (which is hosted
|
|
@@ -4098,13 +4649,18 @@ class HtmlRenderer {
|
|
|
4098
4649
|
for (const row of Array.from(bar.children))
|
|
4099
4650
|
need = Math.max(need, row.scrollWidth);
|
|
4100
4651
|
const avail = bar.clientWidth;
|
|
4652
|
+
let s = 1;
|
|
4101
4653
|
if (need > avail + 1 && avail > 0) {
|
|
4654
|
+
s = Math.max(0.4, avail / need);
|
|
4102
4655
|
host.style.transformOrigin = 'bottom left';
|
|
4103
|
-
host.style.transform = `scale(${
|
|
4656
|
+
host.style.transform = `scale(${s.toFixed(4)})`;
|
|
4104
4657
|
}
|
|
4658
|
+
this.barScale = s;
|
|
4105
4659
|
return;
|
|
4106
4660
|
}
|
|
4661
|
+
let sApplied = 1;
|
|
4107
4662
|
const zoomBar = (z) => {
|
|
4663
|
+
sApplied = z;
|
|
4108
4664
|
const v = z < 0.999 ? z.toFixed(4) : '';
|
|
4109
4665
|
const set = (el) => {
|
|
4110
4666
|
if (!el)
|
|
@@ -4124,6 +4680,7 @@ class HtmlRenderer {
|
|
|
4124
4680
|
if (bar.scrollWidth > bar.clientWidth + 1 && bar.scrollWidth > 0) {
|
|
4125
4681
|
zoomBar(s * (bar.clientWidth / bar.scrollWidth));
|
|
4126
4682
|
}
|
|
4683
|
+
this.barScale = sApplied;
|
|
4127
4684
|
this.fitReadouts();
|
|
4128
4685
|
}
|
|
4129
4686
|
/** Unified number-fit: shrink EVERY readout's value span (`.ge-rd-val`) with a transform-scale so
|
|
@@ -4153,6 +4710,7 @@ class HtmlRenderer {
|
|
|
4153
4710
|
this.host.notifyResize(w, h);
|
|
4154
4711
|
this.applyFitScale();
|
|
4155
4712
|
this.fitModals();
|
|
4713
|
+
this.popoverPosition?.(); // re-place the card if the surface itself resized
|
|
4156
4714
|
});
|
|
4157
4715
|
this.ro.observe(this.root);
|
|
4158
4716
|
}
|
|
@@ -4177,17 +4735,25 @@ function removeGameShell() {
|
|
|
4177
4735
|
}
|
|
4178
4736
|
|
|
4179
4737
|
exports.DEFAULT_ACCENT = DEFAULT_ACCENT;
|
|
4738
|
+
exports.DEFAULT_MENU = DEFAULT_MENU;
|
|
4180
4739
|
exports.GameShell = ShellController;
|
|
4181
4740
|
exports.HtmlRenderer = HtmlRenderer;
|
|
4182
4741
|
exports.PACKAGE_VERSION = PACKAGE_VERSION;
|
|
4742
|
+
exports.POPOVER = POPOVER;
|
|
4183
4743
|
exports.SCHEMES = SCHEMES;
|
|
4184
4744
|
exports.ShellController = ShellController;
|
|
4185
4745
|
exports.createGameShell = createGameShell;
|
|
4186
4746
|
exports.createI18n = createI18n;
|
|
4187
4747
|
exports.createShell = createShell;
|
|
4748
|
+
exports.isPresetId = isPresetId;
|
|
4188
4749
|
exports.normalizeLang = normalizeLang;
|
|
4750
|
+
exports.placePopover = placePopover;
|
|
4751
|
+
exports.popoverWidth = popoverWidth;
|
|
4752
|
+
exports.rangeBounds = rangeBounds;
|
|
4189
4753
|
exports.removeGameShell = removeGameShell;
|
|
4190
4754
|
exports.resolveConfig = resolveConfig;
|
|
4755
|
+
exports.resolveMenu = resolveMenu;
|
|
4191
4756
|
exports.resolveTheme = resolveTheme;
|
|
4757
|
+
exports.seedMenuValues = seedMenuValues;
|
|
4192
4758
|
exports.socialize = socialize;
|
|
4193
4759
|
//# sourceMappingURL=html.cjs.js.map
|