@energy8platform/shell 0.2.0 → 0.2.3
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 +22 -8
- package/dist/html.cjs.js.map +1 -1
- package/dist/html.d.ts +2 -2
- package/dist/html.esm.js +22 -8
- package/dist/html.esm.js.map +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.esm.js +1 -1
- package/dist/pixi.cjs.js +28 -11
- package/dist/pixi.cjs.js.map +1 -1
- package/dist/pixi.d.ts +2 -2
- package/dist/pixi.esm.js +28 -11
- package/dist/pixi.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/core/version.ts +1 -1
- package/src/ui/html/HtmlRenderer.ts +1 -1
- package/src/ui/html/components/GameInfo.ts +1 -1
- package/src/ui/html/components/Modal.ts +8 -4
- package/src/ui/html/components/Settings.ts +1 -1
- package/src/ui/html/shell.css.ts +11 -0
- package/src/ui/pixi/PixiRenderer.ts +4 -1
- package/src/ui/pixi/components/BuyBonus.ts +21 -10
- package/src/ui/pixi/primitives/scroll.ts +3 -0
package/package.json
CHANGED
package/src/core/version.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface GameInfoModal {
|
|
|
18
18
|
export function openGameInfoModal(host: ShellHost): GameInfoModal {
|
|
19
19
|
const { root, body, scroll } = createOverlay({
|
|
20
20
|
title: host.t('Game info'),
|
|
21
|
-
onClose: () =>
|
|
21
|
+
onClose: () => host.actions.closeOverlay(),
|
|
22
22
|
onBack: () => { root.remove(); host.actions.openSettings(); },
|
|
23
23
|
});
|
|
24
24
|
root.dataset.ge = 'info-modal';
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import type { ModalOptions } from '@/core/types';
|
|
2
|
+
import type { ShellHost } from '@/core/renderer';
|
|
2
3
|
import { contrastText } from '@/core/colors';
|
|
3
4
|
import { createCardModal } from '../primitives';
|
|
4
5
|
|
|
5
6
|
/** Build a generic, externally-triggered modal (title + body text + optional action buttons),
|
|
6
7
|
* on the shared card-modal chrome. Each action runs its `on` then closes; the ✕ (if
|
|
7
|
-
* `availableClose`) and the actions are the only ways to dismiss. See GameShell.openModal.
|
|
8
|
-
|
|
8
|
+
* `availableClose`) and the actions are the only ways to dismiss. See GameShell.openModal.
|
|
9
|
+
* Dismissals route through host.actions.closeOverlay() so the controller clears its overlay handle
|
|
10
|
+
* (a bare root.remove() leaves the handle stale → keydowns keep routing to a torn-down layer). */
|
|
11
|
+
export function buildModal(host: ShellHost, opts: ModalOptions): HTMLElement {
|
|
12
|
+
const close = () => host.actions.closeOverlay();
|
|
9
13
|
const ui = createCardModal({
|
|
10
14
|
ge: 'modal',
|
|
11
15
|
title: opts.title,
|
|
12
16
|
closable: opts.availableClose,
|
|
13
17
|
blur: opts.blurLevel,
|
|
14
|
-
onClose:
|
|
18
|
+
onClose: close,
|
|
15
19
|
});
|
|
16
20
|
|
|
17
21
|
const text = document.createElement('p');
|
|
@@ -27,7 +31,7 @@ export function buildModal(opts: ModalOptions): HTMLElement {
|
|
|
27
31
|
btn.textContent = a.title;
|
|
28
32
|
if (a.color) { btn.style.background = a.color; btn.style.color = contrastText(a.color); }
|
|
29
33
|
else btn.classList.add('ge-modal-btn--ghost');
|
|
30
|
-
btn.addEventListener('click', () => { a.on?.();
|
|
34
|
+
btn.addEventListener('click', () => { a.on?.(); close(); });
|
|
31
35
|
actions.appendChild(btn);
|
|
32
36
|
}
|
|
33
37
|
ui.card.appendChild(actions);
|
|
@@ -3,7 +3,7 @@ import { createOverlay } from '../primitives';
|
|
|
3
3
|
import { icon } from '../icons';
|
|
4
4
|
|
|
5
5
|
export function openSettingsModal(host: ShellHost): HTMLElement {
|
|
6
|
-
const { root, body } = createOverlay({ title: host.t('Settings'), onClose: () =>
|
|
6
|
+
const { root, body } = createOverlay({ title: host.t('Settings'), onClose: () => host.actions.closeOverlay() });
|
|
7
7
|
root.dataset.ge = 'settings-modal';
|
|
8
8
|
|
|
9
9
|
// Sound on/off — backed by the shell's shared `soundOn` state so this toggle and the Shift+M
|
package/src/ui/html/shell.css.ts
CHANGED
|
@@ -397,6 +397,17 @@ export const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
397
397
|
#${SHELL_ROOT_ID} [data-ge="buybonus-overlay"] .ge-bb-betval { min-width:clamp(50px,14cqh,66px); }
|
|
398
398
|
#${SHELL_ROOT_ID} [data-ge="buybonus-overlay"] .ge-bb-betval b { font-size:clamp(9px,2.5cqh,11px); }
|
|
399
399
|
#${SHELL_ROOT_ID} [data-ge="buybonus-overlay"] .ge-bb-betval span { font-size:clamp(5px,1.25cqh,6px); }
|
|
400
|
+
/* Popout S (very short landscape, e.g. 400×225): the height-fit shrank the cards' descriptions to an
|
|
401
|
+
unreadable ~4px floor with nothing to scroll. Below a ~340px frame height, drop the shrink-to-fit and
|
|
402
|
+
switch the (non-mobile) buy-bonus to a READABLE vertical stack that scrolls vertically — like mobile.
|
|
403
|
+
Query the overlay's ge-bb-frame size container so it tracks the popout frame, not the browser window. */
|
|
404
|
+
@container ge-bb-frame (max-height: 340px) {
|
|
405
|
+
#${SHELL_ROOT_ID}:not(.ge-mobile) [data-ge="buybonus-overlay"] .ge-ov-scroll { overflow-y:auto; }
|
|
406
|
+
#${SHELL_ROOT_ID}:not(.ge-mobile) [data-ge="buybonus-overlay"] .ge-ov-body { justify-content:flex-start; }
|
|
407
|
+
#${SHELL_ROOT_ID}:not(.ge-mobile) .ge-bb-grid { flex-direction:column; align-items:center; overflow:visible; }
|
|
408
|
+
#${SHELL_ROOT_ID}:not(.ge-mobile) .ge-bb-grid .ge-bonus-card {
|
|
409
|
+
flex:0 0 auto; width:18em; font-size:min(12px, calc(84cqw / 18)); }
|
|
410
|
+
}
|
|
400
411
|
|
|
401
412
|
/* ═══ desktop control bar — one continuous dark panel; white-disc buttons; BUY BONUS outside-left ═══ */
|
|
402
413
|
/* the dark surface fills the row width (BUY BONUS sits outside, to its left) */
|
|
@@ -340,7 +340,10 @@ export class PixiRenderer implements ShellRenderer {
|
|
|
340
340
|
get screenH() { return self.app.screen.height; },
|
|
341
341
|
render: () => self.renderBar(),
|
|
342
342
|
pushLayer: (node) => self.pushLayer(node),
|
|
343
|
-
|
|
343
|
+
// Route component-initiated closes through the controller (not straight to self.closeLayer) so
|
|
344
|
+
// it clears its OverlayHandle. Otherwise the handle goes stale: hasOpenLayer() stays true and
|
|
345
|
+
// keydowns keep routing to onKey on a destroyed overlay → write to a torn-down ScrollBox.
|
|
346
|
+
closeLayer: () => host.actions.closeOverlay(),
|
|
344
347
|
fitModals: () => self.fitModals(),
|
|
345
348
|
fmt: (n) => host.formatCurrency(n),
|
|
346
349
|
fmtWin: (n) => host.formatCurrency(n, true),
|
|
@@ -10,6 +10,11 @@ import { clamp } from '../primitives/overlay';
|
|
|
10
10
|
import { attachHover } from '../primitives/widgets';
|
|
11
11
|
import { roundedPath } from '../primitives/flex';
|
|
12
12
|
|
|
13
|
+
/** Below this frame height (px), a wide/landscape popout (e.g. Popout S 400×225) stacks its cards
|
|
14
|
+
* vertically and scrolls — like mobile — so descriptions stay readable instead of shrinking to a
|
|
15
|
+
* ~4px floor. Wide + taller frames (Popout L and up) keep the centred horizontal row. */
|
|
16
|
+
const SHORT_STACK_H = 340;
|
|
17
|
+
|
|
13
18
|
/** Buy-bonus overlay — art-forward cards (one per option), a live bet footer, and a confirm modal.
|
|
14
19
|
* Returns null when there are no bonus options. */
|
|
15
20
|
export function openBuyBonus(host: PixiComponentContext): ShellLayer | null {
|
|
@@ -46,6 +51,8 @@ class BuyBonusOverlay extends Container implements ShellLayer {
|
|
|
46
51
|
private cardEntries: CardEntry[] = [];
|
|
47
52
|
/** Keyboard focus index into the affordable subset of cardEntries. -1 = none. */
|
|
48
53
|
private focusIndex = -1;
|
|
54
|
+
/** true when cards are stacked vertically (mobile, or a short landscape popout). */
|
|
55
|
+
private stack = false;
|
|
49
56
|
// Drag-scroll state. The drag is handled on the (unmasked) overlay, not the masked strip — a mask
|
|
50
57
|
// prunes pointer events outside its band, stalling globalpointermove mid-drag so later cards never
|
|
51
58
|
// scroll into reach. The overlay sees the whole screen, so the scroll runs the full range.
|
|
@@ -155,7 +162,10 @@ class BuyBonusOverlay extends Container implements ShellLayer {
|
|
|
155
162
|
|
|
156
163
|
private buildCards(): void {
|
|
157
164
|
this.strip.removeChildren().forEach((c) => c.destroy({ children: true }));
|
|
158
|
-
|
|
165
|
+
// Stack (readable vertical list + scroll) on mobile OR on a short landscape popout; a shrink-to-fit
|
|
166
|
+
// horizontal row on wide + tall frames. See SHORT_STACK_H.
|
|
167
|
+
const stack = this.host.layout === 'mobile' || this.h <= SHORT_STACK_H;
|
|
168
|
+
this.stack = stack;
|
|
159
169
|
const top = this.headerH + 6;
|
|
160
170
|
const areaH = this.h - top - this.footerH - 6;
|
|
161
171
|
const gap = 14;
|
|
@@ -166,13 +176,13 @@ class BuyBonusOverlay extends Container implements ShellLayer {
|
|
|
166
176
|
// N cards (+ gaps + 24px side margins) fit the frame width. min() of the two = the binding fit.
|
|
167
177
|
// Floor 4 is a last-resort so a 400×225 popout still shows the CTA (then X-drag scrolls the slack).
|
|
168
178
|
const emH = 3.4 * (areaH / 100);
|
|
169
|
-
const emW =
|
|
179
|
+
const emW = stack
|
|
170
180
|
? (this.w - 48) / 18 // vertical stack: a single card spans the width
|
|
171
181
|
: (this.w - 48 - (n - 1) * gap) / (18 * n); // row: N cards + gaps fit the frame width
|
|
172
|
-
const em =
|
|
182
|
+
const em = stack ? Math.min(12, emW) : clamp(4, Math.min(emH, emW), 12);
|
|
173
183
|
const cardW = Math.min(18 * em, this.w - 48);
|
|
174
184
|
|
|
175
|
-
const cards = this.bonuses.map((b) => this.buildCard(b, cardW, em,
|
|
185
|
+
const cards = this.bonuses.map((b) => this.buildCard(b, cardW, em, stack, areaH));
|
|
176
186
|
const cardH = Math.max(...cards.map((c) => c.height));
|
|
177
187
|
for (const c of cards) c.setHeight(cardH);
|
|
178
188
|
|
|
@@ -193,7 +203,7 @@ class BuyBonusOverlay extends Container implements ShellLayer {
|
|
|
193
203
|
}
|
|
194
204
|
|
|
195
205
|
this.stripMask.clear();
|
|
196
|
-
if (
|
|
206
|
+
if (stack) {
|
|
197
207
|
// vertical stack — scroll vertically if it overflows (simple drag)
|
|
198
208
|
let y = 0;
|
|
199
209
|
const colX = (this.w - cardW) / 2;
|
|
@@ -220,7 +230,7 @@ class BuyBonusOverlay extends Container implements ShellLayer {
|
|
|
220
230
|
}
|
|
221
231
|
|
|
222
232
|
// ── one card ──────────────────────────────────────────────────────────────
|
|
223
|
-
private buildCard(bonus: BonusOption, cardW: number, em: number,
|
|
233
|
+
private buildCard(bonus: BonusOption, cardW: number, em: number, stack: boolean, areaH: number): CardView {
|
|
224
234
|
const accent = effectiveAccent(bonus);
|
|
225
235
|
const ink = contrastText(accent);
|
|
226
236
|
const price = bonus.priceMultiplier * this.host.state.bet;
|
|
@@ -255,7 +265,7 @@ class BuyBonusOverlay extends Container implements ShellLayer {
|
|
|
255
265
|
ctaLabel: this.host.t(bonus.type === 'feature' ? 'Activate' : 'Buy'),
|
|
256
266
|
onSelect: select,
|
|
257
267
|
});
|
|
258
|
-
void
|
|
268
|
+
void stack;
|
|
259
269
|
void areaH;
|
|
260
270
|
return card;
|
|
261
271
|
}
|
|
@@ -435,7 +445,8 @@ class BuyBonusOverlay extends Container implements ShellLayer {
|
|
|
435
445
|
* Browse phase: arrows move focus; +/- step bet; Enter/Space opens confirm; Escape closes.
|
|
436
446
|
* Confirm phase: Enter/Space buys/activates; Escape returns to browse. */
|
|
437
447
|
onKey(e: KeyboardEvent): boolean {
|
|
438
|
-
|
|
448
|
+
// Vertical stack (mobile / short popout) → Up/Down navigate; horizontal row → Left/Right.
|
|
449
|
+
const stack = this.stack;
|
|
439
450
|
|
|
440
451
|
if (this.confirm && this.confirmBonus) {
|
|
441
452
|
// ── Confirm phase ──
|
|
@@ -467,8 +478,8 @@ class BuyBonusOverlay extends Container implements ShellLayer {
|
|
|
467
478
|
if (bet !== null) { this.stepBetBy(bet); return true; }
|
|
468
479
|
|
|
469
480
|
// Determine navigation direction from key code + layout
|
|
470
|
-
const fwdKey = e.code === 'ArrowRight' || (
|
|
471
|
-
const bwdKey = e.code === 'ArrowLeft' || (
|
|
481
|
+
const fwdKey = e.code === 'ArrowRight' || (stack && e.code === 'ArrowDown');
|
|
482
|
+
const bwdKey = e.code === 'ArrowLeft' || (stack && e.code === 'ArrowUp');
|
|
472
483
|
|
|
473
484
|
if (fwdKey) {
|
|
474
485
|
if (last < 0) return true;
|
|
@@ -82,6 +82,9 @@ export class ScrollBox extends Container {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
private setScroll(y: number): void {
|
|
85
|
+
// Ignore late scrolls after teardown: a queued keydown (or resize) can route here once the modal
|
|
86
|
+
// has been destroyed, and writing to a torn-down content Container throws (use-after-destroy).
|
|
87
|
+
if (this.destroyed || !this.content || this.content.destroyed) return;
|
|
85
88
|
this.scrollY = Math.max(0, Math.min(this.maxScroll, y)) || 0; // || 0 converts -0 to 0
|
|
86
89
|
this.content.y = this.scrollY === 0 ? 0 : -this.scrollY;
|
|
87
90
|
}
|