@energy8platform/shell 0.2.0 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@energy8platform/shell",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Energy8 branded game shell — one logic core, pluggable html/pixi renderers behind a stable contract.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs.js",
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
2
2
  /** The @energy8platform/shell package version, stamped into the game-info footer. */
3
- export const PACKAGE_VERSION = '0.2.0';
3
+ export const PACKAGE_VERSION = '0.2.1';
@@ -167,7 +167,7 @@ export class HtmlRenderer implements ShellRenderer {
167
167
  return { root };
168
168
  }
169
169
  case 'modal': {
170
- const root = buildModal(req.opts);
170
+ const root = buildModal(this.host, req.opts);
171
171
  return { root, onKey: req.opts.onKey };
172
172
  }
173
173
  }
@@ -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: () => root.remove(),
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
- export function buildModal(opts: ModalOptions): HTMLElement {
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: () => ui.root.remove(),
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?.(); ui.root.remove(); });
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: () => root.remove() });
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
@@ -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
- closeLayer: () => self.closeLayer(),
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),
@@ -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
  }