@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
|
@@ -15,7 +15,7 @@ import type { PixiComponentContext, ShellLayer, LayerHandle } from './context';
|
|
|
15
15
|
import { installShellFont, whenFontReady } from './text';
|
|
16
16
|
import { countUpText, tween } from './motion-pixi';
|
|
17
17
|
import { BottomBar } from './components/BottomBar';
|
|
18
|
-
import {
|
|
18
|
+
import { openMenu } from './components/Menu';
|
|
19
19
|
import { openGameInfo } from './components/GameInfo';
|
|
20
20
|
import { openBuyBonus } from './components/BuyBonus';
|
|
21
21
|
import { openBetPicker, openAutoplayPicker } from './components/pickers';
|
|
@@ -96,6 +96,15 @@ export class PixiRenderer implements ShellRenderer {
|
|
|
96
96
|
this.bar = new BottomBar(this.ctx);
|
|
97
97
|
this.barLayer.addChild(this.bar);
|
|
98
98
|
this.bar.applyFit();
|
|
99
|
+
// renderBar() runs on every resize AND on ~20 other state changes (bet/win/turbo/mode/…), any of
|
|
100
|
+
// which can change the bar's own fitScale()/menuPlate() (e.g. a WIN pill appearing mid-autoplay
|
|
101
|
+
// retriggers the wide layout's overflow-tightening branch). Only onResize used to reposition the
|
|
102
|
+
// open layer, so a live bar-content change while the menu was open left it at the stale
|
|
103
|
+
// scale/position until the next resize. Every ShellLayer's resize() only re-reads current
|
|
104
|
+
// geometry and re-fits/re-centres itself — none of them call back into renderBar() (verified:
|
|
105
|
+
// Popover, CardModal, Overlay, BuyBonusOverlay) — so this cannot recurse; `currentLayer` is
|
|
106
|
+
// `null` whenever nothing is open, so this cannot throw either.
|
|
107
|
+
this.currentLayer?.resize?.(this.screenW, this.screenH);
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
setLayout(): void {
|
|
@@ -132,8 +141,18 @@ export class PixiRenderer implements ShellRenderer {
|
|
|
132
141
|
openOverlay(req: OverlayRequest): OverlayHandle | void {
|
|
133
142
|
let layer: ShellLayer | null = null;
|
|
134
143
|
switch (req.kind) {
|
|
135
|
-
case '
|
|
136
|
-
|
|
144
|
+
case 'menu':
|
|
145
|
+
// Getters, not `this.bar` by value: renderBar() destroys/rebuilds the bar on every resize
|
|
146
|
+
// and ~20 other state changes, in the same resize handler that then repositions this popover
|
|
147
|
+
// — see Menu.ts's openMenu doc comment for why this must stay lazy. menuAnchor (the burger)
|
|
148
|
+
// is the arrow's pointer; menuPlate (the plaque) drives placement; fitScale is the same
|
|
149
|
+
// factor BottomBar applies to its own content via `inner.scale`.
|
|
150
|
+
layer = openMenu(
|
|
151
|
+
this.ctx,
|
|
152
|
+
() => this.bar?.menuAnchor() ?? null,
|
|
153
|
+
() => this.bar?.menuPlate() ?? null,
|
|
154
|
+
() => this.bar?.fitScale() ?? 1,
|
|
155
|
+
);
|
|
137
156
|
break;
|
|
138
157
|
case 'gameInfo':
|
|
139
158
|
layer = openGameInfo(this.ctx);
|
|
@@ -155,7 +174,7 @@ export class PixiRenderer implements ShellRenderer {
|
|
|
155
174
|
break;
|
|
156
175
|
}
|
|
157
176
|
if (!layer) return;
|
|
158
|
-
this.pushLayer(layer);
|
|
177
|
+
this.pushLayer(layer, { backdrop: req.kind !== 'menu' });
|
|
159
178
|
const built = layer;
|
|
160
179
|
return {
|
|
161
180
|
onKey: built.onKey ? built.onKey.bind(built) : undefined,
|
|
@@ -167,11 +186,6 @@ export class PixiRenderer implements ShellRenderer {
|
|
|
167
186
|
this.closeLayer();
|
|
168
187
|
}
|
|
169
188
|
|
|
170
|
-
refreshSoundIcon(_on: boolean): void {
|
|
171
|
-
// No-op: the open Settings overlay registers its icon updater via host.setSoundRefresh, which
|
|
172
|
-
// the controller's setSound path drives. The renderer has nothing extra to refresh.
|
|
173
|
-
}
|
|
174
|
-
|
|
175
189
|
/** Fade out (≈250ms, like GameShell's REMOVE_FADE_MS) then tear down; resolves when removed. */
|
|
176
190
|
destroy(): Promise<void> {
|
|
177
191
|
if (this.destroyed) return Promise.resolve();
|
|
@@ -195,9 +209,11 @@ export class PixiRenderer implements ShellRenderer {
|
|
|
195
209
|
}
|
|
196
210
|
|
|
197
211
|
// ── layer stack ────────────────────────────────────────────────────────────
|
|
198
|
-
pushLayer(node: ShellLayer): LayerHandle {
|
|
212
|
+
pushLayer(node: ShellLayer, opts?: { backdrop?: boolean }): LayerHandle {
|
|
199
213
|
this.clearLayer();
|
|
200
|
-
|
|
214
|
+
// Light-dismiss layers (the menu popover) opt out with `{ backdrop: false }` — no frosted
|
|
215
|
+
// snapshot, the game stays visible behind them. Every other caller is unaffected (defaults on).
|
|
216
|
+
if (opts?.backdrop !== false) this.makeBackdrop(); // frosted snapshot (the DOM's backdrop-filter:blur)
|
|
201
217
|
this.currentLayer = node;
|
|
202
218
|
this.modalLayer.addChild(node);
|
|
203
219
|
this.fitModals();
|
|
@@ -324,6 +340,7 @@ export class PixiRenderer implements ShellRenderer {
|
|
|
324
340
|
get tokens() { return host.tokens; },
|
|
325
341
|
get layout() { return host.layout; },
|
|
326
342
|
get soundOn() { return host.soundOn; },
|
|
343
|
+
get menu() { return host.menu; },
|
|
327
344
|
get actions() { return host.actions; },
|
|
328
345
|
openReplay: (opts) => host.openReplay(opts),
|
|
329
346
|
t: (s) => host.t(s),
|
|
@@ -331,17 +348,18 @@ export class PixiRenderer implements ShellRenderer {
|
|
|
331
348
|
emit: host.emit.bind(host),
|
|
332
349
|
notifyResize: (w, h) => host.notifyResize(w, h),
|
|
333
350
|
setSound: (on) => host.setSound(on),
|
|
334
|
-
setSoundRefresh: (fn) => host.setSoundRefresh(fn),
|
|
335
351
|
getVolume: (key) => host.getVolume(key),
|
|
336
352
|
setVolume: (key, v) => host.setVolume(key, v),
|
|
337
|
-
|
|
353
|
+
getMenuValue: (id) => host.getMenuValue(id),
|
|
354
|
+
setMenuValue: (id, v) => host.setMenuValue(id, v),
|
|
355
|
+
setMenuRefresh: (fn) => host.setMenuRefresh(fn),
|
|
338
356
|
// — Pixi-specific surface —
|
|
339
357
|
get ticker() { return self.app.ticker; },
|
|
340
358
|
get canvas() { return self.app.canvas as HTMLCanvasElement | undefined; },
|
|
341
359
|
get screenW() { return self.app.screen.width; },
|
|
342
360
|
get screenH() { return self.app.screen.height; },
|
|
343
361
|
render: () => self.renderBar(),
|
|
344
|
-
pushLayer: (node) => self.pushLayer(node),
|
|
362
|
+
pushLayer: (node, opts) => self.pushLayer(node, opts),
|
|
345
363
|
// Route component-initiated closes through the controller (not straight to self.closeLayer) so
|
|
346
364
|
// it clears its OverlayHandle. Otherwise the handle goes stale: hasOpenLayer() stays true and
|
|
347
365
|
// keydowns keep routing to onKey on a destroyed overlay → write to a torn-down ScrollBox.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Container, Graphics, Text } from 'pixi.js';
|
|
2
2
|
import type { PixiComponentContext } from '../context';
|
|
3
|
+
import type { Rect } from '@/core/popover';
|
|
3
4
|
import { effectiveAccent } from '@/core/colors';
|
|
4
5
|
import { BUY_BONUS_ART, BUY_BONUS_SOCIAL_ART, BUY_BONUS_DISABLED_ART } from '../../buy-bonus-art';
|
|
5
6
|
import { FlexBox } from '../primitives/flex';
|
|
@@ -118,6 +119,11 @@ export class BottomBar extends Container {
|
|
|
118
119
|
private spin?: SpinDisc;
|
|
119
120
|
private autoBtn?: IconButton;
|
|
120
121
|
private turboBtn?: IconButton;
|
|
122
|
+
private menuBtn?: IconButton;
|
|
123
|
+
/** The menu's PLATE, in LOCAL coordinates relative to `inner` — the continuous dark panel (wide)
|
|
124
|
+
* or the controls row (mobile). Set at the point `applyFit()`/`applyFitMobile()` compute it;
|
|
125
|
+
* `menuPlate()` converts it to screen space lazily (see that method's doc comment for why). */
|
|
126
|
+
private plateRect: Rect | null = null;
|
|
121
127
|
|
|
122
128
|
constructor(host: PixiComponentContext) {
|
|
123
129
|
super();
|
|
@@ -127,6 +133,57 @@ export class BottomBar extends Container {
|
|
|
127
133
|
else this.buildWide();
|
|
128
134
|
}
|
|
129
135
|
|
|
136
|
+
/** Screen-space rect of the burger — the menu popover's POINTER (the arrow's target only; see
|
|
137
|
+
* `menuPlate` for what drives placement). `measureSize()` (the button's own nominal box, e.g.
|
|
138
|
+
* 36×36) rather than the built-in `getSize()` (the glyph's ink bounds only) so a burger whose icon
|
|
139
|
+
* doesn't fill its box still reports its true hit-box size; multiplied by `inner.scale` — `getSize`
|
|
140
|
+
* /`measureSize` only reflect the node's OWN scale (always 1 here), not the ancestor `inner.scale`
|
|
141
|
+
* the bar's whole fit-scale lives on, so the UNMULTIPLIED width/height would silently overstate the
|
|
142
|
+
* burger's true on-screen footprint on a scaled-down bar and skew the arrow off-centre. */
|
|
143
|
+
menuAnchor(): Rect | null {
|
|
144
|
+
if (!this.menuBtn || this.menuBtn.destroyed) return null;
|
|
145
|
+
const p = this.menuBtn.getGlobalPosition();
|
|
146
|
+
const s = this.inner.scale.x;
|
|
147
|
+
const size = this.menuBtn.measureSize();
|
|
148
|
+
return { x: p.x, y: p.y, w: size.w * s, h: size.h * s };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Screen-space rect of the menu's PLATE — the continuous dark panel (wide) or the controls row
|
|
152
|
+
* (mobile), NOT the burger itself and NOT (mobile) the info pill below it. Drives the popover's
|
|
153
|
+
* x/y/maxH/below; the burger (`menuAnchor`) drives only the arrow. Resolved lazily on every call,
|
|
154
|
+
* like `menuAnchor` — `PixiRenderer.renderBar()` destroys and rebuilds this BottomBar on every
|
|
155
|
+
* resize and ~20 other state changes, so a value captured once would go stale. `plateRect` is
|
|
156
|
+
* local to `inner`; `getGlobalPosition` + `inner.scale` convert it to screen space (`outer*` on the
|
|
157
|
+
* mobile FlexBox and the wide panel's own drawn rect are both LOCAL sizes, unaffected by `inner`'s
|
|
158
|
+
* ancestor scale, same reasoning as `menuAnchor` above). */
|
|
159
|
+
menuPlate(): Rect | null {
|
|
160
|
+
// Pixi v8's Container.destroy() nulls `_position`/`_scale`, so `inner.scale.x` /
|
|
161
|
+
// `inner.getGlobalPosition()` on a destroyed bar throws — guard for symmetry with menuAnchor()
|
|
162
|
+
// above, which already returns null once its button is destroyed. Not reachable today
|
|
163
|
+
// (PixiRenderer.renderBar() destroys and reassigns `this.bar` synchronously, in the same call),
|
|
164
|
+
// but the asymmetry is a trap for a future caller that holds a reference across a render.
|
|
165
|
+
if (this.destroyed) return null;
|
|
166
|
+
if (!this.plateRect) return null;
|
|
167
|
+
const s = this.inner.scale.x;
|
|
168
|
+
const origin = this.inner.getGlobalPosition();
|
|
169
|
+
return {
|
|
170
|
+
x: origin.x + this.plateRect.x * s,
|
|
171
|
+
y: origin.y + this.plateRect.y * s,
|
|
172
|
+
w: this.plateRect.w * s,
|
|
173
|
+
h: this.plateRect.h * s,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The scale factor the bar currently applies to itself (`inner.scale`) — shared with the menu
|
|
178
|
+
* popover so its typography/padding/row-heights scale in lockstep with the bar's own chrome,
|
|
179
|
+
* instead of ignoring the viewport like a fixed-local-size card would. */
|
|
180
|
+
fitScale(): number {
|
|
181
|
+
// Same destroyed guard as menuPlate()/menuAnchor() above, and the same neutral fallback a
|
|
182
|
+
// scale factor should have (1 = no scaling) rather than throwing.
|
|
183
|
+
if (this.destroyed) return 1;
|
|
184
|
+
return this.inner.scale.x;
|
|
185
|
+
}
|
|
186
|
+
|
|
130
187
|
// ── wide / landscape ──────────────────────────────────────────────────────
|
|
131
188
|
private buildWide(): void {
|
|
132
189
|
const { state, tokens } = this.host;
|
|
@@ -139,15 +196,14 @@ export class BottomBar extends Container {
|
|
|
139
196
|
|
|
140
197
|
// LEFT info group: menu · balance · (Total win) · (Win)
|
|
141
198
|
const left = new FlexBox({ direction: 'row', align: 'center', gap: ZONE_GAP });
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
);
|
|
199
|
+
this.menuBtn = new IconButton('menu', {
|
|
200
|
+
size: 36,
|
|
201
|
+
glyph: 30,
|
|
202
|
+
color: '#ffffff',
|
|
203
|
+
hover: tokens.accent,
|
|
204
|
+
onTap: () => this.host.actions.openMenu(),
|
|
205
|
+
});
|
|
206
|
+
left.add(this.menuBtn);
|
|
151
207
|
if (!state.replay) {
|
|
152
208
|
const bal = readout(this.host, 'Balance', this.host.fmt(state.balance));
|
|
153
209
|
this.balanceValue = bal.valueText;
|
|
@@ -340,6 +396,7 @@ export class BottomBar extends Container {
|
|
|
340
396
|
hover: tokens.accent,
|
|
341
397
|
onTap: () => this.host.actions.openMenu(),
|
|
342
398
|
});
|
|
399
|
+
this.menuBtn = menu;
|
|
343
400
|
// autoplay is a base-mode control only — hidden in free spins / replay (matches the DOM bar)
|
|
344
401
|
if (isBase && config.features.autoplay) {
|
|
345
402
|
this.autoBtn = new IconButton('autoplay', {
|
|
@@ -574,6 +631,8 @@ export class BottomBar extends Container {
|
|
|
574
631
|
this.panelBg.clear();
|
|
575
632
|
roundedPath(this.panelBg, panelX, SPIN_POP, panelRight - panelX, BAR_H, [12, 12, 12, 12]);
|
|
576
633
|
this.panelBg.fill(tokens.bar);
|
|
634
|
+
// The menu's plate — the whole continuous dark panel, local to `inner` (see menuPlate()).
|
|
635
|
+
this.plateRect = { x: panelX, y: SPIN_POP, w: panelRight - panelX, h: BAR_H };
|
|
577
636
|
|
|
578
637
|
if (this.buy) this.buy.position.set(OUTER_PAD, panelCenterY - BUY_W / 2);
|
|
579
638
|
left.position.set(panelX + PANEL_PAD, panelCenterY - left.outerHeight / 2);
|
|
@@ -613,6 +672,18 @@ export class BottomBar extends Container {
|
|
|
613
672
|
// SPIN hero), inflating the controls row so it overlaps the info row below it.
|
|
614
673
|
controls.setLayoutSize(rowW, M_CTRL_H);
|
|
615
674
|
info.setLayoutSize(rowW, M_INFO_H);
|
|
675
|
+
// The menu's plate — the controls row (NOT the info pill below it), local to `inner`. Read from
|
|
676
|
+
// `controls.outerWidth/outerHeight` (its own nominal box), NOT the FlexBox's own bounds (which
|
|
677
|
+
// would include the SPIN/FS hero popping out both above AND below it — see menuPlate()'s doc
|
|
678
|
+
// comment) — but the TOP edge is deliberately extended upward by `pop`, the same hero pop-out
|
|
679
|
+
// amount reserved as `topPad` above, so the plate the popover math sees is the row's true visual
|
|
680
|
+
// extent on the side that matters: without this, the card's bottom (only `gap` above the plate's
|
|
681
|
+
// top) can clip the top ~(pop-gap)px of the hero's arc, since 62px M_CTRL_H is 11px shorter than
|
|
682
|
+
// the 84px hero. The BOTTOM edge is left alone — the hero's symmetric under-pop is a separate,
|
|
683
|
+
// unrelated concern (the info pill sits right below with its own small gap) and extending it too
|
|
684
|
+
// would perturb the `below`-placement branch (spaceBelow) for no benefit. A no-op when `pop` is 0
|
|
685
|
+
// (no hero shown — e.g. replay without free spins).
|
|
686
|
+
this.plateRect = { x: 0, y: topPad - pop, w: controls.outerWidth, h: controls.outerHeight + pop };
|
|
616
687
|
const s = rowW > 0 ? Math.max(0.4, Math.min(1, avail / rowW)) : 1;
|
|
617
688
|
|
|
618
689
|
this.inner.scale.set(s);
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Container, Graphics, Text } from 'pixi.js';
|
|
2
|
+
import { resolveMenu, type MenuRow } from '@/core/menu';
|
|
3
|
+
import type { Rect } from '@/core/popover';
|
|
4
|
+
import type { PixiComponentContext, ShellLayer } from '../context';
|
|
5
|
+
import { Popover } from '../primitives/popover';
|
|
6
|
+
import { FlexBox } from '../primitives/flex';
|
|
7
|
+
import { Slider, Spacer, Toggle } from '../primitives/controls';
|
|
8
|
+
import { makeText } from '../text';
|
|
9
|
+
import { makeIcon } from '../pixi-icon';
|
|
10
|
+
import { attachHover } from '../primitives/widgets';
|
|
11
|
+
|
|
12
|
+
/** The bar menu as a Pixi popover. Same rows, same order as the DOM — both come from resolveMenu.
|
|
13
|
+
*
|
|
14
|
+
* `getAnchor` (the burger — the arrow's POINTER), `getPlate` (the bar's plaque — drives placement)
|
|
15
|
+
* and `getScale` (the bar's own fit-scale) are all FUNCTIONS, resolved lazily on every reposition —
|
|
16
|
+
* never a captured `BottomBar` instance. `PixiRenderer.renderBar()` destroys and rebuilds the
|
|
17
|
+
* BottomBar on every resize AND on ~20 other state changes, in the SAME resize handler that then
|
|
18
|
+
* repositions this popover. A captured instance would already be destroyed by the time `resize()`
|
|
19
|
+
* next runs, so `menuAnchor()`/`menuPlate()` on it would return `null` and the card would silently
|
|
20
|
+
* recentre with its arrow hidden — the exact bug the DOM renderer shipped and fixed the same way
|
|
21
|
+
* (see `html/Menu.ts`'s plate/pointer callbacks). Passing getters means every call reads whichever
|
|
22
|
+
* bar is CURRENT. `getPlate`/`getScale` are optional so every pre-existing caller (which only ever
|
|
23
|
+
* passed `getAnchor`) keeps behaving exactly as it did before the plate/pointer/scale split. */
|
|
24
|
+
export function openMenu(
|
|
25
|
+
host: PixiComponentContext,
|
|
26
|
+
getAnchor?: () => Rect | null,
|
|
27
|
+
getPlate?: () => Rect | null,
|
|
28
|
+
getScale?: () => number,
|
|
29
|
+
): ShellLayer {
|
|
30
|
+
const updaters: Record<string, (v: boolean | number) => void> = {};
|
|
31
|
+
const layer = new Popover(host, {
|
|
32
|
+
tag: 'menu',
|
|
33
|
+
plate: () => getPlate?.() ?? null,
|
|
34
|
+
pointer: () => getAnchor?.() ?? null,
|
|
35
|
+
scale: () => getScale?.() ?? 1,
|
|
36
|
+
onClose: () => host.closeLayer(),
|
|
37
|
+
build: (width) => {
|
|
38
|
+
const col = new FlexBox({ direction: 'column', align: 'stretch', gap: 6 });
|
|
39
|
+
for (const row of resolveMenu(host)) col.add(buildRow(host, row, width, updaters));
|
|
40
|
+
return col;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
host.setMenuRefresh((id, v) => updaters[id]?.(v));
|
|
44
|
+
return layer;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function label(text: string): Text {
|
|
48
|
+
return makeText(text, { size: 13, weight: '600', color: '#ffffff' }) as Text;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function rowBox(host: PixiComponentContext, name: string, column = false): FlexBox {
|
|
52
|
+
const box = new FlexBox({
|
|
53
|
+
direction: column ? 'column' : 'row',
|
|
54
|
+
align: column ? 'stretch' : 'center',
|
|
55
|
+
gap: column ? 8 : 10,
|
|
56
|
+
padding: { top: 10, bottom: 10, left: 12, right: 12 },
|
|
57
|
+
minHeight: column ? undefined : 44,
|
|
58
|
+
background: { fill: host.tokens.plaqueGlass, radius: 14 },
|
|
59
|
+
});
|
|
60
|
+
box.label = name;
|
|
61
|
+
return box;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildRow(
|
|
65
|
+
host: PixiComponentContext,
|
|
66
|
+
row: MenuRow,
|
|
67
|
+
width: number,
|
|
68
|
+
updaters: Record<string, (v: boolean | number) => void>,
|
|
69
|
+
): Container {
|
|
70
|
+
if (row.kind === 'separator') {
|
|
71
|
+
const sep = new Container();
|
|
72
|
+
sep.label = 'menu-sep';
|
|
73
|
+
const line = new Graphics().rect(4, 6, width - 8, 1).fill(host.tokens.plaqueLine);
|
|
74
|
+
line.alpha = 0.5;
|
|
75
|
+
sep.addChild(line);
|
|
76
|
+
return sep;
|
|
77
|
+
}
|
|
78
|
+
if (row.kind === 'button') {
|
|
79
|
+
const box = rowBox(host, `menu-row-${row.id}`);
|
|
80
|
+
if (row.icon) box.add(makeIcon(row.icon, 20, '#ffffff'));
|
|
81
|
+
const text = label(row.label);
|
|
82
|
+
box.add(text);
|
|
83
|
+
box.add(new Spacer(), { grow: 1 });
|
|
84
|
+
if (row.chevron) box.add(makeIcon('chevronRight', 16, host.tokens.muted));
|
|
85
|
+
if (!row.disabled) {
|
|
86
|
+
box.setInteractive(true);
|
|
87
|
+
box.on('pointertap', () => {
|
|
88
|
+
host.closeLayer();
|
|
89
|
+
row.select();
|
|
90
|
+
});
|
|
91
|
+
attachHover(
|
|
92
|
+
box,
|
|
93
|
+
() => { box.setBgFill(host.tokens.plaqueGlassHover); text.style.fill = host.tokens.accent; },
|
|
94
|
+
() => { box.setBgFill(host.tokens.plaqueGlass); text.style.fill = '#ffffff'; },
|
|
95
|
+
);
|
|
96
|
+
} else {
|
|
97
|
+
box.alpha = 0.5;
|
|
98
|
+
}
|
|
99
|
+
return box;
|
|
100
|
+
}
|
|
101
|
+
if (row.kind === 'toggle') {
|
|
102
|
+
const box = rowBox(host, `menu-row-${row.id}`);
|
|
103
|
+
const glyph = row.icon(row.get());
|
|
104
|
+
const iconNode = glyph ? makeIcon(glyph, 20, '#ffffff') : null;
|
|
105
|
+
if (iconNode) box.add(iconNode);
|
|
106
|
+
box.add(label(row.label));
|
|
107
|
+
box.add(new Spacer(), { grow: 1 });
|
|
108
|
+
// Disabled: neutralise the write-through at the source (a no-op onChange) rather than relying
|
|
109
|
+
// solely on eventMode — Toggle wires its own pointertap listener unconditionally in its
|
|
110
|
+
// constructor, so a stray direct emit must still be harmless, not just unreachable by real hits.
|
|
111
|
+
const toggle = new Toggle(
|
|
112
|
+
row.get(),
|
|
113
|
+
row.disabled ? () => {} : (v) => row.set(v),
|
|
114
|
+
host.tokens.accent,
|
|
115
|
+
host.tokens.plaqueLine,
|
|
116
|
+
);
|
|
117
|
+
box.add(toggle);
|
|
118
|
+
updaters[row.id] = (v) => {
|
|
119
|
+
const on = v === true;
|
|
120
|
+
toggle.setValue(on);
|
|
121
|
+
const next = row.icon(on);
|
|
122
|
+
if (iconNode && next) iconNode.setIcon(next);
|
|
123
|
+
};
|
|
124
|
+
if (row.disabled) {
|
|
125
|
+
box.alpha = 0.5;
|
|
126
|
+
toggle.eventMode = 'none'; // dimmed + inert — mirrors the button branch's disabled treatment
|
|
127
|
+
}
|
|
128
|
+
return box;
|
|
129
|
+
}
|
|
130
|
+
const box = rowBox(host, `menu-row-${row.id}`, true);
|
|
131
|
+
const head = new FlexBox({ direction: 'row', align: 'center' });
|
|
132
|
+
const value = makeText(row.format(row.get()), { size: 12, weight: '700', color: host.tokens.plaqueLabel });
|
|
133
|
+
head.add(label(row.label));
|
|
134
|
+
head.add(new Spacer(), { grow: 1 });
|
|
135
|
+
head.add(value);
|
|
136
|
+
// Slider works in 0..1; map to the row's declared bounds, snapping to the MIN-anchored lattice
|
|
137
|
+
// (min + k·step). Snapping to a bare multiple of step instead (Math.round(raw/step)*step) drifts
|
|
138
|
+
// off `min` whenever min isn't itself a multiple of step — the common case, since the default step
|
|
139
|
+
// is (max-min)/20: e.g. min:1,max:10 gives step 0.45, and the bare-multiple formula would emit 0.9
|
|
140
|
+
// at the far left, BELOW the declared min. Clamp guards any float overshoot past either end.
|
|
141
|
+
const toUnit = (v: number): number => (row.max === row.min ? 0 : (v - row.min) / (row.max - row.min));
|
|
142
|
+
const fromUnit = (u: number): number => {
|
|
143
|
+
const raw = row.min + u * (row.max - row.min);
|
|
144
|
+
const snapped = row.min + Math.round((raw - row.min) / row.step) * row.step;
|
|
145
|
+
return Math.max(row.min, Math.min(row.max, snapped));
|
|
146
|
+
};
|
|
147
|
+
// Disabled: same reasoning as the toggle branch above — a no-op onInput, not just eventMode.
|
|
148
|
+
const slider = new Slider(host, toUnit(row.get()), row.disabled ? () => {} : (u) => {
|
|
149
|
+
const v = fromUnit(u);
|
|
150
|
+
value.text = row.format(v);
|
|
151
|
+
head.layout(); // the readout's text just changed width (e.g. "5%" → "100%") — reposition it
|
|
152
|
+
row.set(v);
|
|
153
|
+
});
|
|
154
|
+
updaters[row.id] = (v) => {
|
|
155
|
+
const n = Number(v);
|
|
156
|
+
value.text = row.format(n);
|
|
157
|
+
head.layout();
|
|
158
|
+
slider.setValue(toUnit(n));
|
|
159
|
+
};
|
|
160
|
+
box.add(head);
|
|
161
|
+
box.add(slider);
|
|
162
|
+
if (row.disabled) {
|
|
163
|
+
box.alpha = 0.5;
|
|
164
|
+
slider.eventMode = 'none';
|
|
165
|
+
}
|
|
166
|
+
return box;
|
|
167
|
+
}
|
package/src/ui/pixi/context.ts
CHANGED
|
@@ -25,7 +25,8 @@ export interface LayerHandle {
|
|
|
25
25
|
* PixiRenderer provides (ticker, screen size, layer stack).
|
|
26
26
|
*
|
|
27
27
|
* Members already on core ShellHost (state, config, tokens, layout, soundOn, t, emit, setSound,
|
|
28
|
-
*
|
|
28
|
+
* getVolume, setVolume, menu, getMenuValue, setMenuValue, setMenuRefresh, actions, formatCurrency,
|
|
29
|
+
* notifyResize) are NOT re-declared here.
|
|
29
30
|
*
|
|
30
31
|
* fmt/fmtWin decision: pixi components in pixi-shell use `host.fmt(n)` / `host.fmtWin(n)`.
|
|
31
32
|
* Rather than retargeting all components to `host.formatCurrency(n)` / `host.formatCurrency(n, true)`
|
|
@@ -38,7 +39,7 @@ export interface PixiComponentContext extends ShellHost {
|
|
|
38
39
|
readonly screenW: number;
|
|
39
40
|
readonly screenH: number;
|
|
40
41
|
render(): void;
|
|
41
|
-
pushLayer(node: ShellLayer): LayerHandle;
|
|
42
|
+
pushLayer(node: ShellLayer, opts?: { backdrop?: boolean }): LayerHandle;
|
|
42
43
|
closeLayer(): void;
|
|
43
44
|
fitModals(): void;
|
|
44
45
|
/** Swap the active language at runtime (rebuilds resolver, re-renders bar). Optional. */
|
package/src/ui/pixi/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: `<path d="M1.72 9.48L19.64 4.67L20.28 7.07C19.19 7.8 18.82 9 19.11 10.09C19.41 11.19 20.33 12.04 21.64 12.13L22.28 14.52L4.36 19.32L3.72 16.93C4.81 16.2 5.18 15 4.89 13.91C4.59 12.81 3.67 11.96 2.36 11.87L1.72 9.48Z" fill="none" stroke="currentColor" stroke-width="1.84" stroke-linejoin="miter"/><path d="M12.57 7.17L9.75 13.26L11.92 12.4L11.84 16.74L14.85 10.06L12.38 11.1Z" fill="currentColor"/>`,
|
|
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 {
|
package/src/ui/pixi/pixi-icon.ts
CHANGED
|
@@ -35,11 +35,11 @@ export class IconView extends Container implements Sizable {
|
|
|
35
35
|
private gfx: Graphics;
|
|
36
36
|
private _size: number;
|
|
37
37
|
private _color: string;
|
|
38
|
-
|
|
38
|
+
private _iconName: IconName;
|
|
39
39
|
|
|
40
40
|
constructor(name: IconName, size: number, color = '#ffffff') {
|
|
41
41
|
super();
|
|
42
|
-
this.
|
|
42
|
+
this._iconName = name;
|
|
43
43
|
this._size = size;
|
|
44
44
|
this._color = color;
|
|
45
45
|
this.gfx = new Graphics(context(name, color));
|
|
@@ -58,10 +58,22 @@ export class IconView extends Container implements Sizable {
|
|
|
58
58
|
return this._size;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
get iconName(): IconName {
|
|
62
|
+
return this._iconName;
|
|
63
|
+
}
|
|
64
|
+
|
|
61
65
|
setColor(color: string): void {
|
|
62
66
|
if (color === this._color) return;
|
|
63
67
|
this._color = color;
|
|
64
|
-
this.gfx.context = context(this.
|
|
68
|
+
this.gfx.context = context(this._iconName, color);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Swap the glyph in place (e.g. the menu's sound row flips soundOn/soundOff on toggle) — same
|
|
72
|
+
* cached-context rebuild as setColor, keyed on the new name instead of a new colour. */
|
|
73
|
+
setIcon(name: IconName): void {
|
|
74
|
+
if (name === this._iconName) return;
|
|
75
|
+
this._iconName = name;
|
|
76
|
+
this.gfx.context = context(name, this._color);
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
setSize(size: number): void {
|
|
@@ -198,6 +198,52 @@ export class Slider extends Container implements Sizable {
|
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
/** Pill switch — the Pixi twin of the DOM `.ge-toggle`. 42×24, knob 20, accent when on. */
|
|
202
|
+
export class Toggle extends Container {
|
|
203
|
+
private track = new Graphics();
|
|
204
|
+
private knob = new Graphics();
|
|
205
|
+
private _value: boolean;
|
|
206
|
+
private onChange: (v: boolean) => void;
|
|
207
|
+
private accent: string;
|
|
208
|
+
private offFill: string;
|
|
209
|
+
|
|
210
|
+
constructor(value: boolean, onChange: (v: boolean) => void, accent = '#8b5cf6', off = 'rgba(255,255,255,.22)') {
|
|
211
|
+
super();
|
|
212
|
+
this._value = value;
|
|
213
|
+
this.onChange = onChange;
|
|
214
|
+
this.accent = accent;
|
|
215
|
+
this.offFill = off;
|
|
216
|
+
this.addChild(this.track, this.knob);
|
|
217
|
+
this.eventMode = 'static';
|
|
218
|
+
this.cursor = 'pointer';
|
|
219
|
+
this.hitArea = new Rectangle(0, 0, 42, 24);
|
|
220
|
+
this.on('pointertap', () => this.onChange(!this._value));
|
|
221
|
+
this.paint();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
get value(): boolean {
|
|
225
|
+
return this._value;
|
|
226
|
+
}
|
|
227
|
+
setValue(v: boolean): void {
|
|
228
|
+
this._value = v;
|
|
229
|
+
this.paint();
|
|
230
|
+
}
|
|
231
|
+
private paint(): void {
|
|
232
|
+
this.track.clear();
|
|
233
|
+
this.track.roundRect(0, 0, 42, 24, 12);
|
|
234
|
+
this.track.fill(this._value ? this.accent : this.offFill);
|
|
235
|
+
this.knob.clear();
|
|
236
|
+
this.knob.circle(this._value ? 30 : 12, 12, 10);
|
|
237
|
+
this.knob.fill('#ffffff');
|
|
238
|
+
}
|
|
239
|
+
measureSize(): { w: number; h: number } {
|
|
240
|
+
return { w: 42, h: 24 };
|
|
241
|
+
}
|
|
242
|
+
setLayoutSize(): void {
|
|
243
|
+
/* fixed size */
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
201
247
|
/** Picker chip — `.ge-chip`. Selected = accent bg + accent border. */
|
|
202
248
|
export class Chip extends Container implements Sizable {
|
|
203
249
|
readonly id: string;
|