@energy8platform/shell 0.6.6 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/html.cjs.js +677 -109
- package/dist/html.cjs.js.map +1 -1
- package/dist/html.d.ts +207 -27
- package/dist/html.esm.js +670 -110
- package/dist/html.esm.js.map +1 -1
- package/dist/index.cjs.js +378 -22
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +199 -26
- package/dist/index.esm.js +371 -23
- package/dist/index.esm.js.map +1 -1
- package/dist/pixi.cjs.js +952 -282
- package/dist/pixi.cjs.js.map +1 -1
- package/dist/pixi.d.ts +202 -28
- package/dist/pixi.esm.js +945 -283
- package/dist/pixi.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ShellController.ts +68 -16
- package/src/core/format.ts +8 -4
- 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 +13 -10
- package/src/core/state.ts +2 -1
- package/src/core/types.ts +15 -6
- package/src/core/version.ts +1 -1
- package/src/ui/html/HtmlRenderer.ts +31 -8
- 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 +32 -14
- 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/src/core/state.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ShellConfig, ShellState } from './types';
|
|
2
|
+
import { DEFAULT_MENU, seedMenuValues } from './menu';
|
|
2
3
|
|
|
3
4
|
export function createInitialState(config: ShellConfig): ShellState {
|
|
4
5
|
return {
|
|
@@ -16,10 +17,10 @@ export function createInitialState(config: ShellConfig): ShellState {
|
|
|
16
17
|
bonus: null,
|
|
17
18
|
activeFeature: null,
|
|
18
19
|
volumes: {
|
|
19
|
-
master: clampVolume(config.volumes?.master),
|
|
20
20
|
music: clampVolume(config.volumes?.music),
|
|
21
21
|
sfx: clampVolume(config.volumes?.sfx),
|
|
22
22
|
},
|
|
23
|
+
menu: seedMenuValues(config.menu ?? DEFAULT_MENU),
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
|
package/src/core/types.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import type { MenuItem } from './menu';
|
|
2
|
+
|
|
1
3
|
/** `freeSpins` and `bonus` are the SAME bar layout (host-driven hero + Total Win); `freeSpins` is
|
|
2
4
|
* kept as a back-compat alias for the common case (its readout is derived current/total), while
|
|
3
5
|
* `bonus` pairs with `setBonus()` to show a game-supplied label + value (adventure, hold-and-spin,
|
|
4
6
|
* respins — anything that isn't a plain free-spins counter). */
|
|
5
7
|
export type ShellMode = 'base' | 'bonus' | 'freeSpins' | 'replay';
|
|
6
8
|
|
|
7
|
-
/** The
|
|
8
|
-
export type VolumeKey = '
|
|
9
|
+
/** The two independent volume sliders shown in the bar menu. */
|
|
10
|
+
export type VolumeKey = 'music' | 'sfx';
|
|
9
11
|
export type VolumeLevels = Record<VolumeKey, number>;
|
|
10
12
|
|
|
11
13
|
export interface CurrencyConfig {
|
|
@@ -18,6 +20,9 @@ export interface CurrencyConfig {
|
|
|
18
20
|
* trimmed down to this many places so small wins keep their significant digits (e.g. 0.0673)
|
|
19
21
|
* while round amounts stay compact (e.g. 0.30). Everything else is shown at exactly this many. */
|
|
20
22
|
minDecimals?: number;
|
|
23
|
+
/** Defaults to the platform convention: `{ thousands: ',', decimal: '.' }` → `€1,234.50`.
|
|
24
|
+
* Override only for a jurisdiction that requires another convention — a comma decimal reads
|
|
25
|
+
* as a thousands group to most players and inflates the perceived payout. */
|
|
21
26
|
separator?: { thousands?: string; decimal?: string };
|
|
22
27
|
}
|
|
23
28
|
|
|
@@ -250,8 +255,10 @@ export interface ShellConfig {
|
|
|
250
255
|
onBonusBuy?: () => void;
|
|
251
256
|
/** Initial Settings-overlay volume slider positions (each 0..1, defaults to 1 = 100%). The shell
|
|
252
257
|
* keeps them stateful across opens; read/update at runtime via `shell.getVolume()` /
|
|
253
|
-
* `shell.setVolume()`, and listen to `settingChange` ({ key: '
|
|
258
|
+
* `shell.setVolume()`, and listen to `settingChange` ({ key: 'music'|'sfx' }) to apply. */
|
|
254
259
|
volumes?: Partial<VolumeLevels>;
|
|
260
|
+
/** Bar-menu items, in order. Omit for the default list (sound, music, sfx, ─, game info). */
|
|
261
|
+
menu?: MenuItem[];
|
|
255
262
|
}
|
|
256
263
|
|
|
257
264
|
/** ShellConfig after the controller applies defaults (version, isSocial, replay, theme). No mount. */
|
|
@@ -272,7 +279,7 @@ export type ResolvedShellConfig = Required<
|
|
|
272
279
|
| 'replay'
|
|
273
280
|
>
|
|
274
281
|
> &
|
|
275
|
-
Pick<ShellConfig, 'currentBet' | 'theme' | 'onBonusBuy' | 'volumes'>;
|
|
282
|
+
Pick<ShellConfig, 'currentBet' | 'theme' | 'onBonusBuy' | 'volumes' | 'menu'>;
|
|
276
283
|
|
|
277
284
|
export interface ShellState {
|
|
278
285
|
mode: ShellMode;
|
|
@@ -295,9 +302,11 @@ export interface ShellState {
|
|
|
295
302
|
/** The currently activated `feature` option (e.g. Ante), or null. Drives the
|
|
296
303
|
* effective-bet readout tint and the BUY BONUS → DISABLE toggle on the bar. */
|
|
297
304
|
activeFeature: BonusOption | null;
|
|
298
|
-
/** Volume slider positions (0..1)
|
|
299
|
-
* reopened overlay reflects the last-set positions instead of resetting to 100%. */
|
|
305
|
+
/** Volume slider positions (0..1) for the two sliders in the menu. */
|
|
300
306
|
volumes: VolumeLevels;
|
|
307
|
+
/** Values of CUSTOM menu items, keyed by id. Seeded from the item list; preset values live in
|
|
308
|
+
* their own homes (`soundOn`, `volumes`) and are reached through `getMenuValue`. */
|
|
309
|
+
menu: Record<string, boolean | number>;
|
|
301
310
|
}
|
|
302
311
|
|
|
303
312
|
export interface ShellEvents {
|
package/src/core/version.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { SHELL_CSS, SHELL_ROOT_ID } from './shell.css';
|
|
|
4
4
|
import { buildThemeVars } from './theme-css';
|
|
5
5
|
import { countUp } from './motion-dom';
|
|
6
6
|
import { renderBottomBar } from './components/BottomBar';
|
|
7
|
-
import {
|
|
7
|
+
import { openMenuPopover } from './components/Menu';
|
|
8
8
|
import { openGameInfoModal } from './components/GameInfo';
|
|
9
9
|
import { openBuyBonusOverlay } from './components/BuyBonus';
|
|
10
10
|
import { openBetModal, openAutoplayModal } from './components/pickers';
|
|
@@ -35,7 +35,11 @@ export class HtmlRenderer implements ShellRenderer {
|
|
|
35
35
|
private ro: ResizeObserver | null = null;
|
|
36
36
|
private moneyAnims: Array<() => void> = [];
|
|
37
37
|
private modalOnKey: ((e: KeyboardEvent) => boolean) | undefined;
|
|
38
|
+
private popoverPosition: (() => void) | null = null;
|
|
38
39
|
private destroyed = false;
|
|
40
|
+
/** The scale factor last applied to the bar by applyFitScale() — exposed so the menu popover can
|
|
41
|
+
* match it exactly (see getBarScale) rather than re-derive its own, possibly-disagreeing, value. */
|
|
42
|
+
private barScale = 1;
|
|
39
43
|
|
|
40
44
|
/** MutationObserver for buy-bonus confirm fit — fires fitModals() when nodes are added
|
|
41
45
|
* inside the modalHost (e.g. the confirm dialog appended after the grid is open). */
|
|
@@ -81,6 +85,14 @@ export class HtmlRenderer implements ShellRenderer {
|
|
|
81
85
|
this.barHost.innerHTML = '';
|
|
82
86
|
this.barHost.appendChild(renderBottomBar(this.host));
|
|
83
87
|
this.applyFitScale();
|
|
88
|
+
// renderBar() runs on every resize AND on ~20 other state changes (bet/win/turbo/mode/…), any of
|
|
89
|
+
// which can change barScale and/or the plate rect (e.g. a WIN pill appearing mid-autoplay
|
|
90
|
+
// retriggers the overflow-tightening branch). Only the resize hook used to reposition an open
|
|
91
|
+
// popover, so a live bar-content change while the menu was open left the card at the stale
|
|
92
|
+
// scale/position until the next resize. `popoverPosition` only reads plate/pointer/scale getters
|
|
93
|
+
// and writes card styles — no path back into renderBar() — so this cannot recurse; it's `null`
|
|
94
|
+
// whenever no popover is open (or a different overlay kind is), so this cannot throw either.
|
|
95
|
+
this.popoverPosition?.();
|
|
84
96
|
}
|
|
85
97
|
|
|
86
98
|
setLayout(): void {
|
|
@@ -102,13 +114,10 @@ export class HtmlRenderer implements ShellRenderer {
|
|
|
102
114
|
|
|
103
115
|
closeOverlay(): void {
|
|
104
116
|
this.modalOnKey = undefined;
|
|
117
|
+
this.popoverPosition = null;
|
|
105
118
|
this.modalHost.innerHTML = '';
|
|
106
119
|
}
|
|
107
120
|
|
|
108
|
-
refreshSoundIcon?(_on: boolean): void {
|
|
109
|
-
// Settings registers via host.setSoundRefresh; nothing extra here
|
|
110
|
-
}
|
|
111
|
-
|
|
112
121
|
destroy(): Promise<void> {
|
|
113
122
|
if (this.destroyed) return Promise.resolve();
|
|
114
123
|
this.destroyed = true;
|
|
@@ -130,6 +139,11 @@ export class HtmlRenderer implements ShellRenderer {
|
|
|
130
139
|
/** Trigger a bar fit-scale pass (used by tests that stub geometry after the initial render). */
|
|
131
140
|
fitBar(): void { this.applyFitScale(); }
|
|
132
141
|
|
|
142
|
+
/** The scale factor applyFitScale() last applied to the bar — the menu popover multiplies its own
|
|
143
|
+
* card by this SAME number so its typography/padding/row-heights carry the same visual weight
|
|
144
|
+
* relationship the bar has, instead of ignoring the viewport like a fixed-px card would. */
|
|
145
|
+
getBarScale(): number { return this.barScale; }
|
|
146
|
+
|
|
133
147
|
// ── private ────────────────────────────────────────────────────────────────
|
|
134
148
|
|
|
135
149
|
private cancelMoneyAnims(): void {
|
|
@@ -139,8 +153,9 @@ export class HtmlRenderer implements ShellRenderer {
|
|
|
139
153
|
|
|
140
154
|
private buildOverlay(req: OverlayRequest): { root: HTMLElement; onKey?: (e: KeyboardEvent) => boolean } | null {
|
|
141
155
|
switch (req.kind) {
|
|
142
|
-
case '
|
|
143
|
-
const root =
|
|
156
|
+
case 'menu': {
|
|
157
|
+
const { root, position } = openMenuPopover(this.host, this.root, () => this.getBarScale());
|
|
158
|
+
this.popoverPosition = position;
|
|
144
159
|
return { root };
|
|
145
160
|
}
|
|
146
161
|
case 'gameInfo': {
|
|
@@ -183,6 +198,7 @@ export class HtmlRenderer implements ShellRenderer {
|
|
|
183
198
|
this.modalHost.appendChild(el);
|
|
184
199
|
this.modalOnKey = onKey;
|
|
185
200
|
this.fitModals();
|
|
201
|
+
this.popoverPosition?.(); // measure + place now that the card is in the DOM
|
|
186
202
|
}
|
|
187
203
|
|
|
188
204
|
/** Uniformly scale every open centred card modal (.ge-sheet) down so it fits a short/narrow
|
|
@@ -229,13 +245,18 @@ export class HtmlRenderer implements ShellRenderer {
|
|
|
229
245
|
let need = 0;
|
|
230
246
|
for (const row of Array.from(bar.children) as HTMLElement[]) need = Math.max(need, row.scrollWidth);
|
|
231
247
|
const avail = bar.clientWidth;
|
|
248
|
+
let s = 1;
|
|
232
249
|
if (need > avail + 1 && avail > 0) {
|
|
250
|
+
s = Math.max(0.4, avail / need);
|
|
233
251
|
host.style.transformOrigin = 'bottom left';
|
|
234
|
-
host.style.transform = `scale(${
|
|
252
|
+
host.style.transform = `scale(${s.toFixed(4)})`;
|
|
235
253
|
}
|
|
254
|
+
this.barScale = s;
|
|
236
255
|
return;
|
|
237
256
|
}
|
|
257
|
+
let sApplied = 1;
|
|
238
258
|
const zoomBar = (z: number): void => {
|
|
259
|
+
sApplied = z;
|
|
239
260
|
const v = z < 0.999 ? z.toFixed(4) : '';
|
|
240
261
|
const set = (el: Element | null): void => {
|
|
241
262
|
if (!el) return;
|
|
@@ -252,6 +273,7 @@ export class HtmlRenderer implements ShellRenderer {
|
|
|
252
273
|
if (bar.scrollWidth > bar.clientWidth + 1 && bar.scrollWidth > 0) {
|
|
253
274
|
zoomBar(s * (bar.clientWidth / bar.scrollWidth));
|
|
254
275
|
}
|
|
276
|
+
this.barScale = sApplied;
|
|
255
277
|
this.fitReadouts();
|
|
256
278
|
}
|
|
257
279
|
|
|
@@ -280,6 +302,7 @@ export class HtmlRenderer implements ShellRenderer {
|
|
|
280
302
|
this.host.notifyResize(w, h);
|
|
281
303
|
this.applyFitScale();
|
|
282
304
|
this.fitModals();
|
|
305
|
+
this.popoverPosition?.(); // re-place the card if the surface itself resized
|
|
283
306
|
});
|
|
284
307
|
this.ro.observe(this.root);
|
|
285
308
|
}
|
|
@@ -20,7 +20,7 @@ export function openGameInfoModal(host: ShellHost): GameInfoModal {
|
|
|
20
20
|
const { root, body, scroll } = createOverlay({
|
|
21
21
|
title: host.t('Game info'),
|
|
22
22
|
onClose: () => host.actions.closeOverlay(),
|
|
23
|
-
onBack: () => { root.remove(); host.actions.
|
|
23
|
+
onBack: () => { root.remove(); host.actions.openMenu(); },
|
|
24
24
|
});
|
|
25
25
|
root.dataset.ge = 'info-modal';
|
|
26
26
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { ShellHost } from '@/core/renderer';
|
|
2
|
+
import { resolveMenu, type MenuRow } from '@/core/menu';
|
|
3
|
+
import { createPopover } from '../primitives';
|
|
4
|
+
import { icon, type IconName } from '../icons';
|
|
5
|
+
|
|
6
|
+
/** The bar menu, as a light-dismiss popover anchored to the burger. Rows come from the core model,
|
|
7
|
+
* so DOM and Pixi always show the same list in the same order. */
|
|
8
|
+
export function openMenuPopover(
|
|
9
|
+
host: ShellHost,
|
|
10
|
+
surface: HTMLElement,
|
|
11
|
+
getScale: () => number,
|
|
12
|
+
): { root: HTMLElement; position(): void } {
|
|
13
|
+
const pop = createPopover({
|
|
14
|
+
ge: 'menu-popover',
|
|
15
|
+
surface,
|
|
16
|
+
// Resolved lazily on every position() call, not captured once: HtmlRenderer's renderBar() (run
|
|
17
|
+
// on every resize, and on ~20 other state changes) rebuilds the bottom bar from scratch, so the
|
|
18
|
+
// plate/burger are brand-new elements after it — a captured reference would already be detached
|
|
19
|
+
// by the next position() call and the card would silently recentre with its arrow hidden.
|
|
20
|
+
// The plate is the bar's own plaque — the continuous dark panel wide, the controls row mobile
|
|
21
|
+
// (NOT the info pill below it) — so the card sits flush with the WHOLE bar, not just the burger.
|
|
22
|
+
plate: () => surface.querySelector(host.layout === 'mobile' ? '.ge-m-controls' : '.ge-bar-panel') as HTMLElement | null,
|
|
23
|
+
pointer: () => surface.querySelector('[data-ge="menu"]') as HTMLElement | null,
|
|
24
|
+
// Mobile only: the SPIN disc / FS counter is taller than `.ge-m-controls` and centred inside it
|
|
25
|
+
// (`align-items:center` over a shorter fixed-height row), so it pops above the row's own top edge
|
|
26
|
+
// — see createPopover's `plateOverflowTop` doc comment. Exactly one of the two (or neither, e.g. a
|
|
27
|
+
// replay with no free spins) is ever rendered, so a combined selector resolves to whichever is
|
|
28
|
+
// current. Wide's plate already contains its content, so this deliberately resolves to null there.
|
|
29
|
+
plateOverflowTop: () =>
|
|
30
|
+
host.layout === 'mobile'
|
|
31
|
+
? (surface.querySelector('[data-ge="spin"], [data-ge="fs-counter"]') as HTMLElement | null)
|
|
32
|
+
: null,
|
|
33
|
+
scale: getScale,
|
|
34
|
+
onClose: () => host.actions.closeOverlay(),
|
|
35
|
+
});
|
|
36
|
+
const updaters: Record<string, (v: boolean | number) => void> = {};
|
|
37
|
+
for (const row of resolveMenu(host)) {
|
|
38
|
+
pop.body.appendChild(buildRow(host, row, updaters, () => pop.position()));
|
|
39
|
+
}
|
|
40
|
+
// Live updates while open (host.setMenuValue / setSound / setVolume); cleared by the controller.
|
|
41
|
+
host.setMenuRefresh((id, v) => updaters[id]?.(v));
|
|
42
|
+
return { root: pop.root, position: pop.position };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function glyph(name: IconName | undefined, cls: string): HTMLElement | null {
|
|
46
|
+
if (!name) return null;
|
|
47
|
+
const el = document.createElement('span');
|
|
48
|
+
el.className = cls;
|
|
49
|
+
el.innerHTML = icon(name);
|
|
50
|
+
return el;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildRow(
|
|
54
|
+
host: ShellHost,
|
|
55
|
+
row: MenuRow,
|
|
56
|
+
updaters: Record<string, (v: boolean | number) => void>,
|
|
57
|
+
reposition: () => void,
|
|
58
|
+
): HTMLElement {
|
|
59
|
+
if (row.kind === 'separator') {
|
|
60
|
+
const sep = document.createElement('div');
|
|
61
|
+
sep.className = 'ge-pop-sep';
|
|
62
|
+
sep.dataset.ge = 'menu-sep';
|
|
63
|
+
return sep;
|
|
64
|
+
}
|
|
65
|
+
if (row.kind === 'button') {
|
|
66
|
+
// The row hook goes on a wrapper so both `menu-row-<id>` (order assertions) and
|
|
67
|
+
// `menu-item-<id>` (the clickable control) exist, as they do for toggle/range rows.
|
|
68
|
+
const wrap = document.createElement('div');
|
|
69
|
+
wrap.dataset.ge = `menu-row-${row.id}`;
|
|
70
|
+
const btn = document.createElement('button');
|
|
71
|
+
btn.className = 'ge-ov-row';
|
|
72
|
+
btn.dataset.ge = `menu-item-${row.id}`;
|
|
73
|
+
btn.disabled = row.disabled;
|
|
74
|
+
const label = document.createElement('span');
|
|
75
|
+
label.className = 'ge-grow';
|
|
76
|
+
label.textContent = row.label;
|
|
77
|
+
const ico = glyph(row.icon, 'ge-mi-icon');
|
|
78
|
+
const chev = row.chevron ? glyph('chevronRight', 'ge-mi-chev') : null;
|
|
79
|
+
for (const el of [ico, label, chev]) if (el) btn.appendChild(el);
|
|
80
|
+
btn.addEventListener('click', () => {
|
|
81
|
+
if (btn.disabled) return;
|
|
82
|
+
host.actions.closeOverlay();
|
|
83
|
+
row.select();
|
|
84
|
+
});
|
|
85
|
+
wrap.appendChild(btn);
|
|
86
|
+
return wrap;
|
|
87
|
+
}
|
|
88
|
+
if (row.kind === 'toggle') {
|
|
89
|
+
const el = document.createElement('div');
|
|
90
|
+
el.className = 'ge-ov-row';
|
|
91
|
+
el.classList.toggle('ge-disabled', row.disabled); // a <div> can't carry [disabled] itself
|
|
92
|
+
el.dataset.ge = `menu-row-${row.id}`;
|
|
93
|
+
const ico = glyph(row.icon(row.get()), 'ge-mi-icon');
|
|
94
|
+
const label = document.createElement('span');
|
|
95
|
+
label.className = 'ge-grow';
|
|
96
|
+
label.textContent = row.label;
|
|
97
|
+
const btn = document.createElement('button');
|
|
98
|
+
btn.className = 'ge-toggle';
|
|
99
|
+
btn.dataset.ge = `menu-item-${row.id}`;
|
|
100
|
+
btn.disabled = row.disabled;
|
|
101
|
+
btn.innerHTML = '<i></i>';
|
|
102
|
+
const paint = (v: boolean): void => {
|
|
103
|
+
btn.classList.toggle('ge-on', v);
|
|
104
|
+
btn.setAttribute('aria-pressed', String(v));
|
|
105
|
+
const next = row.icon(v);
|
|
106
|
+
if (ico && next) ico.innerHTML = icon(next);
|
|
107
|
+
};
|
|
108
|
+
paint(row.get());
|
|
109
|
+
btn.addEventListener('click', () => {
|
|
110
|
+
if (!btn.disabled) row.set(!row.get());
|
|
111
|
+
});
|
|
112
|
+
updaters[row.id] = (v) => paint(v === true);
|
|
113
|
+
if (ico) el.appendChild(ico);
|
|
114
|
+
el.append(label, btn);
|
|
115
|
+
return el;
|
|
116
|
+
}
|
|
117
|
+
// range
|
|
118
|
+
const el = document.createElement('div');
|
|
119
|
+
el.className = 'ge-ov-row ge-col';
|
|
120
|
+
el.classList.toggle('ge-disabled', row.disabled); // a <div> can't carry [disabled] itself
|
|
121
|
+
el.dataset.ge = `menu-row-${row.id}`;
|
|
122
|
+
const head = document.createElement('div');
|
|
123
|
+
head.className = 'ge-row-head';
|
|
124
|
+
const name = document.createElement('span');
|
|
125
|
+
name.textContent = row.label;
|
|
126
|
+
const val = document.createElement('span');
|
|
127
|
+
val.className = 'ge-val';
|
|
128
|
+
head.append(name, val);
|
|
129
|
+
const input = document.createElement('input');
|
|
130
|
+
input.type = 'range';
|
|
131
|
+
input.className = 'ge-slider';
|
|
132
|
+
input.dataset.ge = `menu-item-${row.id}`;
|
|
133
|
+
input.min = String(row.min);
|
|
134
|
+
input.max = String(row.max);
|
|
135
|
+
input.step = String(row.step);
|
|
136
|
+
input.disabled = row.disabled;
|
|
137
|
+
const paint = (v: number): void => {
|
|
138
|
+
input.value = String(v);
|
|
139
|
+
val.textContent = row.format(v);
|
|
140
|
+
};
|
|
141
|
+
paint(row.get());
|
|
142
|
+
input.addEventListener('input', () => {
|
|
143
|
+
const v = Number(input.value);
|
|
144
|
+
val.textContent = row.format(v);
|
|
145
|
+
row.set(v);
|
|
146
|
+
});
|
|
147
|
+
updaters[row.id] = (v) => {
|
|
148
|
+
paint(Number(v));
|
|
149
|
+
reposition();
|
|
150
|
+
};
|
|
151
|
+
el.append(head, input);
|
|
152
|
+
return el;
|
|
153
|
+
}
|
package/src/ui/html/icons.ts
CHANGED
|
@@ -5,29 +5,30 @@
|
|
|
5
5
|
// GraphicsContext.svg. To change an icon: edit icons.svg, then run
|
|
6
6
|
// `node scripts/gen-icons-from-svg.mjs`.
|
|
7
7
|
const SVGS: Record<string, string> = {
|
|
8
|
-
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>`,
|
|
9
|
-
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>`,
|
|
10
|
-
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>`,
|
|
8
|
+
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>`,
|
|
9
|
+
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>`,
|
|
10
|
+
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>`,
|
|
11
11
|
stop: `<g fill="currentColor" fill-rule="evenodd"><rect x="1.6" y="1.6" width="20.8" height="20.8" rx="2.9"/></g>`,
|
|
12
12
|
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>`,
|
|
13
13
|
minus: `<g fill="currentColor" fill-rule="evenodd"><rect x="1.6" y="9.86" width="20.8" height="4.28" rx="2.11"/></g>`,
|
|
14
|
-
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>`,
|
|
14
|
+
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>`,
|
|
15
15
|
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)"/>`,
|
|
16
|
-
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.
|
|
17
|
-
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>`,
|
|
16
|
+
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>`,
|
|
17
|
+
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>`,
|
|
18
18
|
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>`,
|
|
19
|
-
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>`,
|
|
20
|
-
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.
|
|
21
|
-
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.
|
|
19
|
+
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>`,
|
|
20
|
+
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>`,
|
|
21
|
+
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>`,
|
|
22
22
|
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>`,
|
|
23
|
-
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.
|
|
24
|
-
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>`,
|
|
25
|
-
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.
|
|
26
|
-
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.
|
|
23
|
+
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>`,
|
|
24
|
+
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>`,
|
|
25
|
+
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>`,
|
|
26
|
+
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>`,
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
export type IconName
|
|
30
|
-
|
|
29
|
+
export type { IconName } from '@/core/icon-names';
|
|
30
|
+
import type { IconName } from '@/core/icon-names';
|
|
31
|
+
export { ICON_NAMES } from '@/core/icon-names';
|
|
31
32
|
|
|
32
33
|
/** Inline SVG string for an icon, sized to 1em (scale via font-size/width). */
|
|
33
34
|
export function icon(name: IconName): string {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { icon } from './icons';
|
|
2
|
+
import { placePopover, popoverWidth, POPOVER, type Rect } from '@/core/popover';
|
|
2
3
|
|
|
3
4
|
/** Render a (possibly socialised) two-word label across two lines — the BUY BONUS badge.
|
|
4
5
|
* Shared so the bottom-bar button and the Game-info control legend break identically. */
|
|
@@ -83,3 +84,144 @@ export function createOverlay(opts: OverlayOpts): { root: HTMLDivElement; body:
|
|
|
83
84
|
root.append(head, scroll);
|
|
84
85
|
return { root, body, scroll };
|
|
85
86
|
}
|
|
87
|
+
|
|
88
|
+
export interface PopoverOpts {
|
|
89
|
+
ge: string;
|
|
90
|
+
/** The shell root — the popover is placed in its coordinate space and clamped to it. */
|
|
91
|
+
surface: HTMLElement;
|
|
92
|
+
/** The plate: the bar's own plaque (`.ge-bar-panel` wide / `.ge-m-controls` mobile). Drives the
|
|
93
|
+
* card's x, y, maxH and above/below flip, so the card sits flush with the WHOLE bar rather than
|
|
94
|
+
* with whichever control opened it. Falls back to `pointer` when it can't be resolved (no plaque
|
|
95
|
+
* found), and to a centred, arrow-less card when neither resolves. A function is re-resolved on
|
|
96
|
+
* EVERY `position()` call rather than captured once — a renderer that rebuilds its DOM on
|
|
97
|
+
* resize/re-render (e.g. HtmlRenderer's `renderBar()`) replaces the element, so a captured
|
|
98
|
+
* reference would go stale and silently fall back to a centred card. Pass a resolver whenever the
|
|
99
|
+
* element can be rebuilt out from under the popover. */
|
|
100
|
+
plate: HTMLElement | null | (() => HTMLElement | null);
|
|
101
|
+
/** The control the arrow points at (the burger button). Defaults to `plate` when omitted — the
|
|
102
|
+
* historical single-rect behaviour, kept so every caller that only ever had one rect (i.e. every
|
|
103
|
+
* caller before `plate`/`pointer` were split) keeps behaving exactly as it did before. Same
|
|
104
|
+
* re-resolve-per-call rule as `plate`. */
|
|
105
|
+
pointer?: HTMLElement | null | (() => HTMLElement | null);
|
|
106
|
+
/** An element that visually pops out ABOVE the plate's own box — e.g. the mobile SPIN/FS hero,
|
|
107
|
+
* taller than the `.ge-m-controls` row and vertically centred, so it overflows the row's own top
|
|
108
|
+
* edge. When present (and its measured top is above the plate's), the plate rect's TOP edge is
|
|
109
|
+
* extended upward to match it — bottom edge untouched — so `placePopover` sees the row's true
|
|
110
|
+
* visual extent on the side that matters, instead of a card whose bottom (only `gap` above the
|
|
111
|
+
* plate's own top) can clip the popped-out control's arc. Omit/return null when nothing pops out
|
|
112
|
+
* (e.g. the wide layout, whose plate already contains its content) — a no-op. Same
|
|
113
|
+
* re-resolve-per-call rule as `plate`/`pointer`. */
|
|
114
|
+
plateOverflowTop?: HTMLElement | null | (() => HTMLElement | null);
|
|
115
|
+
/** The scale factor the bar currently applies to itself (HtmlRenderer.applyFitScale's `s`). The
|
|
116
|
+
* card matches it so its typography/padding/row-heights scale in lockstep with the bar chrome.
|
|
117
|
+
* Defaults to 1 (no scaling) when omitted. */
|
|
118
|
+
scale?: () => number;
|
|
119
|
+
onClose: () => void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** A light-dismiss popover: a transparent full-surface layer (closes on pointerdown) holding a
|
|
123
|
+
* card with an arrow that points at `pointer`. Append rows to `body`; call `position()` after the
|
|
124
|
+
* card is in the DOM and again on resize. */
|
|
125
|
+
export function createPopover(opts: PopoverOpts): {
|
|
126
|
+
root: HTMLDivElement;
|
|
127
|
+
card: HTMLDivElement;
|
|
128
|
+
body: HTMLDivElement;
|
|
129
|
+
position(): void;
|
|
130
|
+
} {
|
|
131
|
+
const root = document.createElement('div');
|
|
132
|
+
root.className = 'ge-pop-layer';
|
|
133
|
+
root.dataset.ge = opts.ge;
|
|
134
|
+
const card = document.createElement('div');
|
|
135
|
+
card.className = 'ge-pop';
|
|
136
|
+
card.dataset.ge = 'menu-card';
|
|
137
|
+
const body = document.createElement('div');
|
|
138
|
+
body.className = 'ge-pop-body';
|
|
139
|
+
const arrow = document.createElement('span');
|
|
140
|
+
arrow.className = 'ge-pop-arrow';
|
|
141
|
+
card.append(body, arrow);
|
|
142
|
+
root.appendChild(card);
|
|
143
|
+
// Clicks inside the card must not reach the dismiss layer.
|
|
144
|
+
card.addEventListener('pointerdown', (e) => e.stopPropagation());
|
|
145
|
+
root.addEventListener('pointerdown', opts.onClose);
|
|
146
|
+
|
|
147
|
+
const resolveEl = (v: HTMLElement | null | (() => HTMLElement | null) | undefined): HTMLElement | null =>
|
|
148
|
+
typeof v === 'function' ? v() : (v ?? null);
|
|
149
|
+
|
|
150
|
+
/** A rect in surface coordinates, or null when unresolved/fully zero-sized (a zero-HEIGHT rect —
|
|
151
|
+
* e.g. a not-yet-laid-out anchor — is still considered valid, matching placePopover's own rule). */
|
|
152
|
+
const rectOf = (el: HTMLElement | null, surfaceRect: DOMRect): Rect | null => {
|
|
153
|
+
if (!el) return null;
|
|
154
|
+
const r = el.getBoundingClientRect();
|
|
155
|
+
if (r.width <= 0 && r.height <= 0) return null;
|
|
156
|
+
return { x: r.left - surfaceRect.left, y: r.top - surfaceRect.top, w: r.width, h: r.height };
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const position = (): void => {
|
|
160
|
+
const surfaceRect = opts.surface.getBoundingClientRect();
|
|
161
|
+
const surface = { w: surfaceRect.width || opts.surface.clientWidth, h: surfaceRect.height || opts.surface.clientHeight };
|
|
162
|
+
if (surface.w <= 0 || surface.h <= 0) return;
|
|
163
|
+
const s = opts.scale?.() ?? 1;
|
|
164
|
+
|
|
165
|
+
// 1. Clear a prior run's constraints (transform + width + max-height + left) before measuring —
|
|
166
|
+
// otherwise scrollWidth/offsetHeight report the already-scaled/clamped box (not the natural
|
|
167
|
+
// content size) on every call after the first, and the card can shrink to fit a
|
|
168
|
+
// narrower/shorter surface but never grow back when the surface widens/grows again. An uncleared
|
|
169
|
+
// max-height is the worse of the two: placePopover positions a card sized for the STALE clamped
|
|
170
|
+
// height, then the un-clamped natural height (restored below) springs back afterward — so the
|
|
171
|
+
// rendered card can overlap the plate or run off the surface edge until reopened. The transform
|
|
172
|
+
// doesn't affect layout either way, but clearing it keeps every pass measuring the same way.
|
|
173
|
+
// `left` matters too: `.ge-pop` is absolutely positioned inside an `inset:0` layer, so with the
|
|
174
|
+
// previous pass's `left` still applied, its shrink-to-fit width is bounded by (layerWidth − left)
|
|
175
|
+
// instead of the card's true natural width. Invisible at scale 1; a scale below 1 lays the card
|
|
176
|
+
// out at up to 1/s its on-screen width, so a stale left — only ever a few hundred px — can clip it.
|
|
177
|
+
card.style.transform = '';
|
|
178
|
+
card.style.width = '';
|
|
179
|
+
card.style.maxHeight = '';
|
|
180
|
+
card.style.left = '0px';
|
|
181
|
+
const naturalW = card.scrollWidth || POPOVER.minW;
|
|
182
|
+
|
|
183
|
+
// 2. Resolve the ON-SCREEN width from the natural (unscaled) width scaled up to screen units,
|
|
184
|
+
// then set the LOCAL style.width so the card renders at that resolved width once scaled — and
|
|
185
|
+
// re-measure the height at that width (wrapping may have changed it).
|
|
186
|
+
const resolvedW = popoverWidth(surface.w, naturalW * s);
|
|
187
|
+
card.style.width = `${resolvedW / s}px`;
|
|
188
|
+
const naturalH = card.offsetHeight || POPOVER.minH;
|
|
189
|
+
|
|
190
|
+
// 3. Resolve plate/pointer in surface coordinates and place using SCREEN-space size —
|
|
191
|
+
// placePopover works in surface pixels throughout, so both the anchor rects and `size` here are
|
|
192
|
+
// screen units even though the card's own layout (width/height above) is still LOCAL/unscaled.
|
|
193
|
+
const plateEl = resolveEl(opts.plate);
|
|
194
|
+
const pointerEl = resolveEl(opts.pointer);
|
|
195
|
+
let plate = rectOf(plateEl, surfaceRect) ?? rectOf(pointerEl, surfaceRect);
|
|
196
|
+
// Extend the plate's TOP edge upward to a popped-out hero's true top (e.g. the mobile SPIN/FS
|
|
197
|
+
// control, taller than its row) — bottom edge untouched. Both rects are already in the SAME
|
|
198
|
+
// surface-coordinate space (post any bar-scale transform), so this is a plain coordinate compare,
|
|
199
|
+
// no separate unit conversion needed.
|
|
200
|
+
const overflowEl = resolveEl(opts.plateOverflowTop);
|
|
201
|
+
if (plate) {
|
|
202
|
+
const overflowRect = rectOf(overflowEl, surfaceRect);
|
|
203
|
+
if (overflowRect && overflowRect.y < plate.y) {
|
|
204
|
+
plate = { ...plate, h: plate.h + (plate.y - overflowRect.y), y: overflowRect.y };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const pointer = rectOf(pointerEl, surfaceRect);
|
|
208
|
+
const p = placePopover(plate, surface, { w: resolvedW, h: naturalH * s }, pointer);
|
|
209
|
+
|
|
210
|
+
// 4. Apply. maxHeight and the arrow's offset are LOCAL (unscaled) too, since both live inside the
|
|
211
|
+
// scaled card; transform-origin:top left keeps left/top (screen units) as the card's visual
|
|
212
|
+
// top-left regardless of `s`, and — since a transform doesn't affect layout — the next
|
|
213
|
+
// measurement pass stays clean without needing any extra bookkeeping.
|
|
214
|
+
card.style.left = `${p.x}px`;
|
|
215
|
+
card.style.top = `${p.y}px`;
|
|
216
|
+
card.style.maxHeight = `${p.maxH / s}px`;
|
|
217
|
+
card.style.transformOrigin = 'top left';
|
|
218
|
+
card.style.transform = Math.abs(s - 1) > 0.001 ? `scale(${s})` : '';
|
|
219
|
+
card.classList.toggle('ge-pop-below', p.below);
|
|
220
|
+
if (p.arrowX < 0) arrow.style.display = 'none';
|
|
221
|
+
else {
|
|
222
|
+
arrow.style.display = '';
|
|
223
|
+
arrow.style.left = `${p.arrowX / s}px`;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
return { root, card, body, position };
|
|
227
|
+
}
|
package/src/ui/html/shell.css.ts
CHANGED
|
@@ -172,7 +172,10 @@ export const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
172
172
|
border-radius:16px; background:var(--shell-plaque-glass); color:#fff; font-size:14px; font-weight:600; }
|
|
173
173
|
#${SHELL_ROOT_ID} .ge-ov-row .ge-grow { flex:1; text-align:left; }
|
|
174
174
|
#${SHELL_ROOT_ID} button.ge-ov-row { cursor:pointer; font-family:inherit; transition:background .12s ease, color .12s ease; }
|
|
175
|
-
#${SHELL_ROOT_ID} button.ge-ov-row:hover { background:var(--shell-plaque-glass-hover); color:var(--shell-accent); }
|
|
175
|
+
#${SHELL_ROOT_ID} button.ge-ov-row:hover:not([disabled]) { background:var(--shell-plaque-glass-hover); color:var(--shell-accent); }
|
|
176
|
+
/* disabled rows — mirrors Pixi's box.alpha = 0.5 for all three row kinds. A <button> row carries
|
|
177
|
+
the native attribute directly; a toggle/range row is a <div> wrapper, so it gets .ge-disabled. */
|
|
178
|
+
#${SHELL_ROOT_ID} .ge-ov-row[disabled], #${SHELL_ROOT_ID} .ge-ov-row.ge-disabled { opacity:.5; cursor:default; }
|
|
176
179
|
#${SHELL_ROOT_ID} .ge-ov-row.ge-col { flex-direction:column; align-items:stretch; gap:10px; }
|
|
177
180
|
#${SHELL_ROOT_ID} .ge-ov-row .ge-row-head { display:flex; justify-content:space-between; align-items:center; }
|
|
178
181
|
#${SHELL_ROOT_ID} .ge-ov-row .ge-row-head .ge-val { color:var(--shell-plaque-label); font-variant-numeric:tabular-nums; font-weight:700; }
|
|
@@ -183,6 +186,7 @@ export const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
183
186
|
#${SHELL_ROOT_ID} .ge-toggle i { position:absolute; top:2px; left:2px; width:20px; height:20px; border-radius:50%;
|
|
184
187
|
background:#fff; transition:left .12s ease; }
|
|
185
188
|
#${SHELL_ROOT_ID} .ge-toggle.ge-on i { left:20px; }
|
|
189
|
+
#${SHELL_ROOT_ID} .ge-toggle:disabled { cursor:default; }
|
|
186
190
|
/* sound on/off — speaker icon button (replaces the toggle) */
|
|
187
191
|
#${SHELL_ROOT_ID} .ge-snd { pointer-events:auto; cursor:pointer; border:none; background:none; padding:0;
|
|
188
192
|
width:36px; height:36px; display:flex; align-items:center; justify-content:center; font-size:24px;
|
|
@@ -191,6 +195,22 @@ export const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
191
195
|
#${SHELL_ROOT_ID} .ge-snd:hover { color:var(--shell-accent); }
|
|
192
196
|
#${SHELL_ROOT_ID} .ge-snd:active { transform:scale(.92); }
|
|
193
197
|
|
|
198
|
+
/* bar menu popover — light dismiss (no dim, no blur), card anchored to the burger */
|
|
199
|
+
#${SHELL_ROOT_ID} .ge-pop-layer { position:absolute; inset:0; z-index:55; pointer-events:auto; }
|
|
200
|
+
#${SHELL_ROOT_ID} .ge-pop { position:absolute; box-sizing:border-box; display:flex; flex-direction:column;
|
|
201
|
+
padding:8px; border-radius:18px; background:var(--shell-plaque-dark);
|
|
202
|
+
box-shadow:0 14px 38px rgba(0,0,0,.5); backdrop-filter:blur(12px) saturate(120%);
|
|
203
|
+
-webkit-backdrop-filter:blur(12px) saturate(120%); animation:ge-ov-in .12s ease-out; }
|
|
204
|
+
#${SHELL_ROOT_ID} .ge-pop-body { overflow-y:auto; overflow-x:hidden; min-height:0; }
|
|
205
|
+
#${SHELL_ROOT_ID} .ge-pop .ge-ov-row { padding:10px 12px; margin-bottom:6px; font-size:13px; }
|
|
206
|
+
#${SHELL_ROOT_ID} .ge-pop .ge-ov-row:last-child { margin-bottom:0; }
|
|
207
|
+
#${SHELL_ROOT_ID} .ge-pop-sep { height:1px; margin:6px 4px; background:var(--shell-plaque-line); opacity:.5; }
|
|
208
|
+
#${SHELL_ROOT_ID} .ge-pop-arrow { position:absolute; bottom:-7px; width:14px; height:14px; margin-left:-7px;
|
|
209
|
+
background:var(--shell-plaque-dark); transform:rotate(45deg); border-radius:3px; }
|
|
210
|
+
#${SHELL_ROOT_ID} .ge-pop.ge-pop-below .ge-pop-arrow { bottom:auto; top:-7px; }
|
|
211
|
+
#${SHELL_ROOT_ID} .ge-pop .ge-mi-icon { flex:0 0 auto; width:20px; font-size:20px; display:flex; }
|
|
212
|
+
#${SHELL_ROOT_ID} .ge-pop .ge-mi-chev { flex:0 0 auto; width:16px; font-size:16px; color:var(--shell-muted); display:flex; }
|
|
213
|
+
|
|
194
214
|
/* game info — each section is its own glass plaque; body text sized for comfortable reading */
|
|
195
215
|
#${SHELL_ROOT_ID} .ge-gi-sec { margin-bottom:12px; background:var(--shell-plaque-glass);
|
|
196
216
|
border-radius:16px; padding:16px 18px; }
|