@energy8platform/shell 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pixi.d.ts CHANGED
@@ -643,7 +643,7 @@ declare class ShellController extends EventEmitter<ShellEvents> implements Shell
643
643
  tokens: ShellTokens;
644
644
  layout: ShellLayoutMode;
645
645
  soundOn: boolean;
646
- readonly engineVersion = "0.9.0";
646
+ readonly engineVersion = "0.11.0";
647
647
  readonly actions: ShellActions;
648
648
  private renderer;
649
649
  private i18n;
@@ -787,8 +787,74 @@ interface I18n {
787
787
  }
788
788
  declare function createI18n(opts: I18nOptions): I18n;
789
789
 
790
+ /**
791
+ * The disclaimer body every launch shows, in source English — and the SAME strings that key the
792
+ * translations below. One constant for both on purpose: a host that kept its own copy would drift
793
+ * from these keys by one character and the lookup would miss in silence, leaving a player on any
794
+ * language with English legal text. Certification asks for the opposite ("if multiple languages are
795
+ * supported, the disclaimer is translated and displayed in each language").
796
+ *
797
+ * Brand-free by construction. Stake's own template ends with a seventh line — "TM and © {year}
798
+ * Stake Engine." — and that one belongs to a Stake launch alone: it arrives with the Stake bridge's
799
+ * `INIT.config.disclaimerLines`, which outrank this default, and renders verbatim (see the host's
800
+ * `isBrandLine`). Every other platform gets the same mandated wording without someone else's mark.
801
+ */
802
+ declare const DISCLAIMER_LINES: readonly string[];
803
+
790
804
  /** The @energy8platform/shell package version, stamped into the game-info footer. */
791
- declare const PACKAGE_VERSION = "0.9.0";
805
+ declare const PACKAGE_VERSION = "0.11.0";
806
+
807
+ /**
808
+ * What a scrollable region should ADVERTISE about itself.
809
+ *
810
+ * A slot in a Stake popout is 400×225. At that size the game-info overlay holds twelve screens of
811
+ * content, the buy-bonus switches to a vertical card stack, and the menu popover hides its last
812
+ * row — all of them scroll, and (before this module) none of them said so. macOS makes it worse:
813
+ * overlay scrollbars stay invisible until something is already scrolling, so the very affordance a
814
+ * player needs BEFORE they touch anything is the one the OS withholds. A certification reviewer
815
+ * reads that as content the player can't reach.
816
+ *
817
+ * The maths lives here, apart from both renderers, for one reason: the DOM shell reads
818
+ * `scrollTop`/`scrollHeight` while the Pixi shell tracks its own offset against a mask, and those
819
+ * two must never disagree about whether a fade belongs at the bottom edge. Renderers decide how a
820
+ * thumb LOOKS; this decides when there is one and where it sits.
821
+ */
822
+ /**
823
+ * Shortest thumb we will draw, as a fraction of its track.
824
+ *
825
+ * The honest ratio for game info at Popout S is 8% — an 18px speck on a 226px track, which reads
826
+ * as a rendering artifact rather than a scrollbar. Floored at 18% it stays recognisably a thumb,
827
+ * and it still travels the whole track, so the position it reports remains truthful even though
828
+ * its length no longer is. That is the right trade: length is decoration, position is information.
829
+ */
830
+ declare const SCROLL_THUMB_MIN = 0.18;
831
+ /** One axis of a scroll region. Named for the Y axis because that is the common case; the
832
+ * buy-bonus strip passes its width metrics through the same fields. */
833
+ interface ScrollMetrics {
834
+ scrollTop: number;
835
+ scrollHeight: number;
836
+ clientHeight: number;
837
+ }
838
+ interface ScrollHint {
839
+ /** There is content past an edge — draw the affordance at all. */
840
+ overflowing: boolean;
841
+ /** Nothing above/left of the viewport: suppress the leading fade. */
842
+ atStart: boolean;
843
+ /** Nothing below/right of the viewport: suppress the trailing fade and the chevron. */
844
+ atEnd: boolean;
845
+ /** Thumb length as a fraction of the track, in `[SCROLL_THUMB_MIN, 1]`. */
846
+ thumbSize: number;
847
+ /** Thumb's leading edge as a fraction of the track, in `[0, 1 - thumbSize]`. */
848
+ thumbOffset: number;
849
+ /** Scrollable distance in pixels — 0 when the content fits. */
850
+ maxScroll: number;
851
+ }
852
+ /** Resolve one axis of a scroll region into everything a renderer needs to draw its affordance. */
853
+ declare function scrollHint(m: ScrollMetrics): ScrollHint;
854
+ /** The three-state tag both renderers put on a scroll region, so CSS and tests can name it.
855
+ * `none` when the content fits — the attribute is removed rather than set to it. */
856
+ type ScrollEdge = 'none' | 'start' | 'mid' | 'end';
857
+ declare function scrollEdge(h: ScrollHint): ScrollEdge;
792
858
 
793
859
  /** A shell: the renderer-agnostic controller plus the surface facade (safeArea/barHeight/setVisible)
794
860
  * an embedding host reads. `createShell`, `createGameShell` and `createPixiShell` all return this. */
@@ -924,5 +990,5 @@ declare function createPixiShell(config: PixiShellConfig): PixiGameShell;
924
990
  * Resolves when removed — mirrors `removePixiShell` in the legacy package. */
925
991
  declare function removePixiShell(): Promise<void>;
926
992
 
927
- export { DEFAULT_ACCENT, DEFAULT_MENU, PACKAGE_VERSION, POPOVER, PixiRenderer, SCHEMES, ShellController, createI18n, createPixiShell, createShell, isPresetId, normalizeLang, placePopover, popoverWidth, rangeBounds, removePixiShell, resolveConfig, resolveMenu, resolveTheme, seedMenuValues, socialize };
928
- export type { AutoplayConfig, AutoplayOptions, BonusCardContext, BonusOption, BonusReadout, CellRef, CreateShellOptions, CurrencyConfig, FreeSpinsState, GameInfoContent, GameInfoSection, GameMode, I18n, I18nOptions, Lang, MenuHost, MenuItem, MenuPresetId, MenuRow, ModalAction, ModalOptions, OverlayHandle, OverlayRequest, PaylineDef, PaytableRow, PixiGameShell, PixiShellConfig, PixiShellSurface, PopoverPlacement, Rect as PopoverRect, ReplayModalOptions, ResolvedShellConfig, SafeArea, ShapeDef, Shell, ShellActions, ShellConfig, ShellEvents, ShellFeatures, ShellHost, ShellLayoutMode, ShellMode, ShellRenderer, ShellState, ShellSurface, ShellTokens, ThemeConfig, VolumeKey, VolumeLevels, WinSection };
993
+ export { DEFAULT_ACCENT, DEFAULT_MENU, DISCLAIMER_LINES, PACKAGE_VERSION, POPOVER, PixiRenderer, SCHEMES, SCROLL_THUMB_MIN, ShellController, createI18n, createPixiShell, createShell, isPresetId, normalizeLang, placePopover, popoverWidth, rangeBounds, removePixiShell, resolveConfig, resolveMenu, resolveTheme, scrollEdge, scrollHint, seedMenuValues, socialize };
994
+ export type { AutoplayConfig, AutoplayOptions, BonusCardContext, BonusOption, BonusReadout, CellRef, CreateShellOptions, CurrencyConfig, FreeSpinsState, GameInfoContent, GameInfoSection, GameMode, I18n, I18nOptions, Lang, MenuHost, MenuItem, MenuPresetId, MenuRow, ModalAction, ModalOptions, OverlayHandle, OverlayRequest, PaylineDef, PaytableRow, PixiGameShell, PixiShellConfig, PixiShellSurface, PopoverPlacement, Rect as PopoverRect, ReplayModalOptions, ResolvedShellConfig, SafeArea, ScrollEdge, ScrollHint, ScrollMetrics, ShapeDef, Shell, ShellActions, ShellConfig, ShellEvents, ShellFeatures, ShellHost, ShellLayoutMode, ShellMode, ShellRenderer, ShellState, ShellSurface, ShellTokens, ThemeConfig, VolumeKey, VolumeLevels, WinSection };
package/dist/pixi.esm.js CHANGED
@@ -1307,6 +1307,19 @@ const D = {
1307
1307
  L4: 'The game display is not representative of any physical device and is for illustrative purposes only.',
1308
1308
  L5: 'Winnings are settled according to the amount received from the Remote Game Server and not from events within the web browser.',
1309
1309
  };
1310
+ /**
1311
+ * The disclaimer body every launch shows, in source English — and the SAME strings that key the
1312
+ * translations below. One constant for both on purpose: a host that kept its own copy would drift
1313
+ * from these keys by one character and the lookup would miss in silence, leaving a player on any
1314
+ * language with English legal text. Certification asks for the opposite ("if multiple languages are
1315
+ * supported, the disclaimer is translated and displayed in each language").
1316
+ *
1317
+ * Brand-free by construction. Stake's own template ends with a seventh line — "TM and © {year}
1318
+ * Stake Engine." — and that one belongs to a Stake launch alone: it arrives with the Stake bridge's
1319
+ * `INIT.config.disclaimerLines`, which outrank this default, and renders verbatim (see the host's
1320
+ * `isBrandLine`). Every other platform gets the same mandated wording without someone else's mark.
1321
+ */
1322
+ const DISCLAIMER_LINES = [D.L1, D.L2, D.L3, D.L4, D.L5];
1310
1323
  const DISCLAIMER_LOCALES = {
1311
1324
  da: {
1312
1325
  [D.L1]: 'Fejlfunktion annullerer alle gevinster og spil.',
@@ -1724,7 +1737,7 @@ function keyboardCapable(win = typeof window === 'undefined' ? undefined : windo
1724
1737
 
1725
1738
  // AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
1726
1739
  /** The @energy8platform/shell package version, stamped into the game-info footer. */
1727
- const PACKAGE_VERSION = '0.9.0';
1740
+ const PACKAGE_VERSION = '0.11.0';
1728
1741
 
1729
1742
  /** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
1730
1743
  function resolveConfig(config) {
@@ -2210,6 +2223,66 @@ function placePopover(anchor, surface, size, pointer = null) {
2210
2223
  return { x, y, maxH, arrowX, below };
2211
2224
  }
2212
2225
 
2226
+ /**
2227
+ * What a scrollable region should ADVERTISE about itself.
2228
+ *
2229
+ * A slot in a Stake popout is 400×225. At that size the game-info overlay holds twelve screens of
2230
+ * content, the buy-bonus switches to a vertical card stack, and the menu popover hides its last
2231
+ * row — all of them scroll, and (before this module) none of them said so. macOS makes it worse:
2232
+ * overlay scrollbars stay invisible until something is already scrolling, so the very affordance a
2233
+ * player needs BEFORE they touch anything is the one the OS withholds. A certification reviewer
2234
+ * reads that as content the player can't reach.
2235
+ *
2236
+ * The maths lives here, apart from both renderers, for one reason: the DOM shell reads
2237
+ * `scrollTop`/`scrollHeight` while the Pixi shell tracks its own offset against a mask, and those
2238
+ * two must never disagree about whether a fade belongs at the bottom edge. Renderers decide how a
2239
+ * thumb LOOKS; this decides when there is one and where it sits.
2240
+ */
2241
+ /** Fractions of a pixel are layout rounding, not reachable content. */
2242
+ const EPSILON = 1;
2243
+ /** Landing within half a pixel of an edge counts as arriving: a browser settles a flung scroll on
2244
+ * 199.5 of 200, and a fade left glowing over content the player has already reached reads as a
2245
+ * bug. */
2246
+ const EDGE_EPSILON = 0.5;
2247
+ /**
2248
+ * Shortest thumb we will draw, as a fraction of its track.
2249
+ *
2250
+ * The honest ratio for game info at Popout S is 8% — an 18px speck on a 226px track, which reads
2251
+ * as a rendering artifact rather than a scrollbar. Floored at 18% it stays recognisably a thumb,
2252
+ * and it still travels the whole track, so the position it reports remains truthful even though
2253
+ * its length no longer is. That is the right trade: length is decoration, position is information.
2254
+ */
2255
+ const SCROLL_THUMB_MIN = 0.18;
2256
+ /** Resolve one axis of a scroll region into everything a renderer needs to draw its affordance. */
2257
+ function scrollHint(m) {
2258
+ const view = Math.max(0, m.clientHeight);
2259
+ const content = Math.max(0, m.scrollHeight);
2260
+ const maxScroll = Math.max(0, content - view);
2261
+ if (maxScroll <= EPSILON || view <= 0) {
2262
+ // Both edges are "the edge" when there is nowhere to go — callers gate every fade on the
2263
+ // matching flag, so a region that fits draws nothing without needing to check `overflowing`.
2264
+ return { overflowing: false, atStart: true, atEnd: true, thumbSize: 1, thumbOffset: 0, maxScroll: 0 };
2265
+ }
2266
+ const at = Math.max(0, Math.min(maxScroll, m.scrollTop));
2267
+ const thumbSize = Math.min(1, Math.max(SCROLL_THUMB_MIN, view / content));
2268
+ const progress = at / maxScroll;
2269
+ return {
2270
+ overflowing: true,
2271
+ atStart: at <= EDGE_EPSILON,
2272
+ atEnd: maxScroll - at <= EDGE_EPSILON,
2273
+ thumbSize,
2274
+ thumbOffset: progress * (1 - thumbSize),
2275
+ maxScroll,
2276
+ };
2277
+ }
2278
+ function scrollEdge(h) {
2279
+ if (!h.overflowing)
2280
+ return 'none';
2281
+ if (h.atStart)
2282
+ return 'start';
2283
+ return h.atEnd ? 'end' : 'mid';
2284
+ }
2285
+
2213
2286
  const NO_INSET = { top: 0, right: 0, bottom: 0, left: 0 };
2214
2287
  /** Create a shell with an explicit renderer instance (custom or a built-in HtmlRenderer/PixiRenderer).
2215
2288
  * Built-in renderers also have the createGameShell/createPixiShell sugar in /html and /pixi.
@@ -4135,11 +4208,207 @@ function SPIN_POP_MOBILE() {
4135
4208
  return (SPIN - M_CTRL_H) / 2; // 11
4136
4209
  }
4137
4210
 
4211
+ /** Thickness of the thumb, and how far it is inset from the edge it rides. */
4212
+ const THUMB_THICK = 4;
4213
+ const THUMB_INSET = 3;
4214
+ /** Shortest thumb we will draw in pixels — the fractional floor still degenerates on a tiny frame. */
4215
+ const THUMB_MIN_PX = 22;
4216
+ /** Depth of the edge scrim. */
4217
+ const SCRIM = 34;
4218
+ const SCRIM_ALPHA = 0.72;
4219
+ /** The overlay chrome — plaques, bar, backdrop — is scheme-independent in this shell (see
4220
+ * resolveTheme: those tokens are fixed, only the palette behind them flips). The affordance is
4221
+ * chrome, so it is a fixed neutral too: white marks over a tint of the same hue as the veil, which
4222
+ * makes the fade read as content dissolving INTO the overlay rather than as a grey band on top. */
4223
+ const SCRIM_TINT = 0x0c111c;
4224
+ const CUE_R = 11;
4225
+ const CUE_BOB = 4;
4226
+ const CUE_PERIOD = 1600; // ms
4227
+ /**
4228
+ * The scrim's alpha ramp, densest AT the edge and clear by `depth` inward.
4229
+ *
4230
+ * Exported because the direction is the whole point and is easy to get backwards: an inverted ramp
4231
+ * draws a hard-edged dark band floating over the content instead of a fade into the frame, and it
4232
+ * looks deliberate enough that nobody questions it. The squared falloff keeps the outer half of the
4233
+ * ramp faint, so the scrim reads as content thinning out rather than as a panel laid on top.
4234
+ */
4235
+ function scrimRamp(depth) {
4236
+ const slices = Math.max(1, Math.round(depth));
4237
+ const thickness = depth / slices;
4238
+ const out = [];
4239
+ for (let i = 0; i < slices; i++) {
4240
+ const offset = i * thickness;
4241
+ const t = 1 - (offset + thickness / 2) / depth; // 1 at the edge → 0 at `depth`
4242
+ out.push({ offset, thickness, alpha: SCRIM_ALPHA * t * t });
4243
+ }
4244
+ return out;
4245
+ }
4246
+ class ScrollAffordanceView extends Container {
4247
+ thumb = new Graphics();
4248
+ scrim = new Graphics();
4249
+ cue = new Container();
4250
+ ticker;
4251
+ bounds = null;
4252
+ cueOn = false;
4253
+ // Once the region has moved the player knows the gesture; re-offering it on every return to the
4254
+ // top would nag rather than inform.
4255
+ cueRetired = false;
4256
+ cueBaseY = 0;
4257
+ elapsed = 0;
4258
+ tick;
4259
+ constructor(ticker) {
4260
+ super();
4261
+ this.ticker = ticker;
4262
+ this.eventMode = 'none'; // decoration: never intercept the drag it is advertising
4263
+ this.addChild(this.scrim, this.thumb, this.cue);
4264
+ this.buildCue();
4265
+ this.visible = false;
4266
+ }
4267
+ /** Where the thumb is drawn, in the parent's space — `null` when nothing is drawn. */
4268
+ get thumbBounds() {
4269
+ return this.visible ? this.bounds : null;
4270
+ }
4271
+ get cueVisible() {
4272
+ return this.visible && this.cueOn;
4273
+ }
4274
+ /** Re-draw for a viewport and a hint. Cheap enough to call on every scroll frame. */
4275
+ update(vp, hint) {
4276
+ if (this.destroyed)
4277
+ return;
4278
+ if (!hint.overflowing) {
4279
+ this.visible = false;
4280
+ this.bounds = null;
4281
+ this.showCue(false);
4282
+ return;
4283
+ }
4284
+ this.visible = true;
4285
+ // Any movement off the start retires the cue permanently.
4286
+ if (!hint.atStart)
4287
+ this.cueRetired = true;
4288
+ this.drawThumb(vp, hint);
4289
+ this.drawScrim(vp, hint);
4290
+ this.placeCue(vp);
4291
+ this.showCue(hint.atStart && !this.cueRetired);
4292
+ }
4293
+ drawThumb(vp, hint) {
4294
+ const along = vp.axis === 'y' ? vp.h : vp.w;
4295
+ const track = Math.max(0, along - THUMB_INSET * 2);
4296
+ const len = Math.max(Math.min(track, THUMB_MIN_PX), track * hint.thumbSize);
4297
+ // Recover the travel from the (possibly floored) length so the thumb still reaches both ends.
4298
+ const travel = Math.max(0, track - len);
4299
+ const progress = hint.thumbSize < 1 ? hint.thumbOffset / (1 - hint.thumbSize) : 0;
4300
+ const at = THUMB_INSET + travel * progress;
4301
+ this.bounds =
4302
+ vp.axis === 'y'
4303
+ ? { x: vp.x + vp.w - THUMB_INSET - THUMB_THICK, y: vp.y + at, w: THUMB_THICK, h: len }
4304
+ : { x: vp.x + at, y: vp.y + vp.h - THUMB_INSET - THUMB_THICK, w: len, h: THUMB_THICK };
4305
+ const b = this.bounds;
4306
+ this.thumb.clear();
4307
+ // A faint track behind the thumb: on a dark overlay a lone thumb can read as a stray highlight;
4308
+ // with the groove behind it, it reads as a scrollbar.
4309
+ if (vp.axis === 'y') {
4310
+ this.thumb
4311
+ .roundRect(b.x, vp.y + THUMB_INSET, THUMB_THICK, track, THUMB_THICK / 2)
4312
+ .fill({ color: 0xffffff, alpha: 0.1 });
4313
+ }
4314
+ else {
4315
+ this.thumb
4316
+ .roundRect(vp.x + THUMB_INSET, b.y, track, THUMB_THICK, THUMB_THICK / 2)
4317
+ .fill({ color: 0xffffff, alpha: 0.1 });
4318
+ }
4319
+ this.thumb.roundRect(b.x, b.y, b.w, b.h, THUMB_THICK / 2).fill({ color: 0xffffff, alpha: 0.55 });
4320
+ }
4321
+ /** Stacked 1px slices rather than a gradient fill: no texture, no backend-specific paths, and at
4322
+ * one device pixel per slice there is nothing left to band. */
4323
+ drawScrim(vp, hint) {
4324
+ this.scrim.clear();
4325
+ const depth = Math.min(SCRIM, (vp.axis === 'y' ? vp.h : vp.w) / 3);
4326
+ if (depth <= 0)
4327
+ return;
4328
+ for (const { offset, thickness, alpha } of scrimRamp(depth)) {
4329
+ // `offset` is measured INWARD from whichever edge is being faded.
4330
+ if (!hint.atEnd) {
4331
+ if (vp.axis === 'y')
4332
+ this.scrim.rect(vp.x, vp.y + vp.h - offset - thickness, vp.w, thickness);
4333
+ else
4334
+ this.scrim.rect(vp.x + vp.w - offset - thickness, vp.y, thickness, vp.h);
4335
+ this.scrim.fill({ color: SCRIM_TINT, alpha });
4336
+ }
4337
+ if (!hint.atStart) {
4338
+ if (vp.axis === 'y')
4339
+ this.scrim.rect(vp.x, vp.y + offset, vp.w, thickness);
4340
+ else
4341
+ this.scrim.rect(vp.x + offset, vp.y, thickness, vp.h);
4342
+ this.scrim.fill({ color: SCRIM_TINT, alpha });
4343
+ }
4344
+ }
4345
+ }
4346
+ buildCue() {
4347
+ const disc = new Graphics();
4348
+ disc.circle(0, 0, CUE_R).fill({ color: 0x0b0f18, alpha: 0.9 });
4349
+ disc.circle(0, 0, CUE_R).stroke({ color: 0xffffff, alpha: 0.22, width: 1 });
4350
+ const size = Math.round(CUE_R * 1.3);
4351
+ const glyph = makeIcon('chevronDown', size, '#ffffff');
4352
+ glyph.position.set(-size / 2, -size / 2);
4353
+ this.cue.addChild(disc, glyph);
4354
+ this.cue.visible = false;
4355
+ }
4356
+ placeCue(vp) {
4357
+ // Bottom-centre for a vertical region; hugging the trailing edge for a horizontal one.
4358
+ if (vp.axis === 'y') {
4359
+ this.cue.x = vp.x + vp.w / 2;
4360
+ this.cueBaseY = vp.y + vp.h - CUE_R - 6;
4361
+ }
4362
+ else {
4363
+ this.cue.x = vp.x + vp.w - CUE_R - 6;
4364
+ this.cueBaseY = vp.y + vp.h / 2;
4365
+ this.cue.rotation = -Math.PI / 2; // chevron points the way the strip moves
4366
+ }
4367
+ this.cue.y = this.cueBaseY;
4368
+ }
4369
+ showCue(on) {
4370
+ if (on === this.cueOn)
4371
+ return;
4372
+ this.cueOn = on;
4373
+ this.cue.visible = on;
4374
+ if (on)
4375
+ this.startBob();
4376
+ else
4377
+ this.stopBob();
4378
+ }
4379
+ startBob() {
4380
+ // A player who asked for stillness still needs the hint; they just do not need it moving.
4381
+ if (this.tick || !this.ticker || prefersReducedMotion())
4382
+ return;
4383
+ this.elapsed = 0;
4384
+ this.tick = (t) => {
4385
+ this.elapsed += t.deltaMS;
4386
+ const phase = (this.elapsed % CUE_PERIOD) / CUE_PERIOD;
4387
+ this.cue.y = this.cueBaseY + Math.sin(phase * Math.PI * 2) * (CUE_BOB / 2) + CUE_BOB / 2;
4388
+ };
4389
+ this.ticker.add(this.tick);
4390
+ }
4391
+ stopBob() {
4392
+ if (!this.tick)
4393
+ return;
4394
+ this.ticker?.remove(this.tick);
4395
+ this.tick = undefined;
4396
+ this.cue.y = this.cueBaseY;
4397
+ }
4398
+ destroy(options) {
4399
+ this.stopBob();
4400
+ super.destroy(options);
4401
+ }
4402
+ }
4403
+
4138
4404
  /** A vertically-scrolling viewport — mask + drag + mouse-wheel, the Pixi analogue of the
4139
4405
  * overlay's `overflow-y:auto` scroll region. Add content to `.content`; call `refresh()`
4140
4406
  * after content changes or a resize. */
4141
4407
  class ScrollBox extends Container {
4142
4408
  content = new Container();
4409
+ /** The drawn "this scrolls" marks — thumb, edge scrim, chevron. Public so the overlay that owns
4410
+ * the box (and the tests) can read what a player would see. */
4411
+ affordance;
4143
4412
  maskG = new Graphics();
4144
4413
  viewW = 0;
4145
4414
  viewH = 0;
@@ -4150,10 +4419,13 @@ class ScrollBox extends Container {
4150
4419
  moved = false;
4151
4420
  canvas;
4152
4421
  wheelHandler;
4153
- constructor(canvas) {
4422
+ constructor(canvas, ticker) {
4154
4423
  super();
4155
4424
  this.canvas = canvas;
4156
- this.addChild(this.content);
4425
+ this.affordance = new ScrollAffordanceView(ticker);
4426
+ // Added AFTER content, and never masked: the marks describe the viewport, so clipping them to
4427
+ // the content they describe would hide them exactly when the content overflows.
4428
+ this.addChild(this.content, this.affordance);
4157
4429
  // maskG is added to the scene only while scrolling (see refresh) — a leftover unused mask
4158
4430
  // graphic renders as a white rect. (Masking does NOT gate pointer events to the content, despite
4159
4431
  // what an earlier version of this comment claimed — see the correction in refresh() below.)
@@ -4208,6 +4480,7 @@ class ScrollBox extends Container {
4208
4480
  }
4209
4481
  this.eventMode = scrollable ? 'static' : 'passive';
4210
4482
  this.setScroll(this.scrollY);
4483
+ this.syncAffordance();
4211
4484
  }
4212
4485
  /** Max scrollable distance (0 when content fits) — exposed for tests. */
4213
4486
  get maxScrollY() {
@@ -4224,6 +4497,12 @@ class ScrollBox extends Container {
4224
4497
  return;
4225
4498
  this.scrollY = Math.max(0, Math.min(this.maxScroll, y)) || 0; // || 0 converts -0 to 0
4226
4499
  this.content.y = this.scrollY === 0 ? 0 : -this.scrollY;
4500
+ this.syncAffordance();
4501
+ }
4502
+ syncAffordance() {
4503
+ if (this.destroyed || this.affordance.destroyed)
4504
+ return;
4505
+ this.affordance.update({ x: 0, y: 0, w: this.viewW, h: this.viewH, axis: 'y' }, scrollHint({ scrollTop: this.scrollY, scrollHeight: this.viewH + this.maxScroll, clientHeight: this.viewH }));
4227
4506
  }
4228
4507
  onDown = (e) => {
4229
4508
  this.dragging = true;
@@ -4794,7 +5073,7 @@ class Overlay extends Container {
4794
5073
  this.host = host;
4795
5074
  this.opts = opts;
4796
5075
  this.tag = opts.tag;
4797
- this.scroll = new ScrollBox(host.canvas);
5076
+ this.scroll = new ScrollBox(host.canvas, host.ticker);
4798
5077
  this.addChild(this.veil, this.scroll, this.header);
4799
5078
  this.veil.eventMode = 'static'; // swallow clicks to the game behind
4800
5079
  this.resize(host.screenW, host.screenH);
@@ -5517,6 +5796,10 @@ class BuyBonusOverlay extends Container {
5517
5796
  header = new Container();
5518
5797
  strip = new Container(); // cards live here (drag-scrollable when wide)
5519
5798
  stripMask = new Graphics();
5799
+ /** "the strip moves" — the drawn marks over the card band. Unmasked and outside `strip`, so a
5800
+ * drag moves the cards under it while the marks hold still. Public for the same reason
5801
+ * ScrollBox.affordance is: it is the only readable account of what a player sees here. */
5802
+ scrollCue;
5520
5803
  footer = new Container();
5521
5804
  confirm;
5522
5805
  /** The bonus shown in the current confirm dialog (set by openConfirm, cleared by removeConfirm). */
@@ -5553,7 +5836,8 @@ class BuyBonusOverlay extends Container {
5553
5836
  super();
5554
5837
  this.host = host;
5555
5838
  this.bonuses = bonuses;
5556
- this.addChild(this.veil, this.strip, this.header, this.footer);
5839
+ this.scrollCue = new ScrollAffordanceView(host.ticker);
5840
+ this.addChild(this.veil, this.strip, this.scrollCue, this.header, this.footer);
5557
5841
  this.veil.eventMode = 'static';
5558
5842
  this.strip.mask = this.stripMask;
5559
5843
  this.addChild(this.stripMask);
@@ -5586,6 +5870,7 @@ class BuyBonusOverlay extends Container {
5586
5870
  this.strip.position.x = this.dragBaseX + this.dragX;
5587
5871
  else
5588
5872
  this.strip.position.y = this.dragBaseY + this.dragX;
5873
+ this.syncScrollCue();
5589
5874
  };
5590
5875
  onDragUp = () => {
5591
5876
  this.dragging = false;
@@ -5603,6 +5888,14 @@ class BuyBonusOverlay extends Container {
5603
5888
  this.dragging = false;
5604
5889
  this.strip.eventMode = 'auto'; // visual-only; its children (cards) are still hit-tested in-band
5605
5890
  this.strip.position.set(baseX, baseY);
5891
+ this.syncScrollCue();
5892
+ }
5893
+ /** Re-draw the band's scroll marks from the current drag state. `dragX` runs [-dragMax, 0] as the
5894
+ * strip is pulled, so the scroll position is its negation. */
5895
+ syncScrollCue() {
5896
+ if (this.destroyed || this.scrollCue.destroyed)
5897
+ return;
5898
+ this.scrollCue.update({ x: 0, y: this.bandTop, w: this.w, h: this.bandH, axis: this.dragAxis }, scrollHint({ scrollTop: -this.dragX, scrollHeight: this.bandH + this.dragMax, clientHeight: this.bandH }));
5606
5899
  }
5607
5900
  resize(w, h) {
5608
5901
  this.w = w;
@@ -7183,5 +7476,5 @@ function removePixiShell() {
7183
7476
  return shell.destroy();
7184
7477
  }
7185
7478
 
7186
- export { DEFAULT_ACCENT, DEFAULT_MENU, PACKAGE_VERSION, POPOVER, PixiRenderer, SCHEMES, ShellController, createI18n, createPixiShell, createShell, isPresetId, normalizeLang, placePopover, popoverWidth, rangeBounds, removePixiShell, resolveConfig, resolveMenu, resolveTheme, seedMenuValues, socialize };
7479
+ export { DEFAULT_ACCENT, DEFAULT_MENU, DISCLAIMER_LINES, PACKAGE_VERSION, POPOVER, PixiRenderer, SCHEMES, SCROLL_THUMB_MIN, ShellController, createI18n, createPixiShell, createShell, isPresetId, normalizeLang, placePopover, popoverWidth, rangeBounds, removePixiShell, resolveConfig, resolveMenu, resolveTheme, scrollEdge, scrollHint, seedMenuValues, socialize };
7187
7480
  //# sourceMappingURL=pixi.esm.js.map