@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/html.cjs.js +276 -8
- package/dist/html.cjs.js.map +1 -1
- package/dist/html.d.ts +70 -4
- package/dist/html.esm.js +273 -9
- package/dist/html.esm.js.map +1 -1
- package/dist/index.cjs.js +78 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +70 -4
- package/dist/index.esm.js +75 -2
- package/dist/index.esm.js.map +1 -1
- package/dist/pixi.cjs.js +302 -5
- package/dist/pixi.cjs.js.map +1 -1
- package/dist/pixi.d.ts +70 -4
- package/dist/pixi.esm.js +299 -6
- package/dist/pixi.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/core/index.ts +4 -0
- package/src/core/locales.ts +14 -0
- package/src/core/scrollHint.ts +91 -0
- package/src/core/version.ts +1 -1
- package/src/ui/html/components/BuyBonus.ts +11 -1
- package/src/ui/html/components/GameInfo.ts +3 -1
- package/src/ui/html/components/pickers.ts +4 -0
- package/src/ui/html/primitives.ts +16 -5
- package/src/ui/html/scroll-affordance.ts +144 -0
- package/src/ui/html/shell.css.ts +65 -1
- package/src/ui/pixi/components/BuyBonus.ts +20 -1
- package/src/ui/pixi/primitives/overlay.ts +1 -1
- package/src/ui/pixi/primitives/scroll-affordance.ts +244 -0
- package/src/ui/pixi/primitives/scroll.ts +21 -3
package/dist/html.d.ts
CHANGED
|
@@ -641,7 +641,7 @@ declare class ShellController extends EventEmitter<ShellEvents> implements Shell
|
|
|
641
641
|
tokens: ShellTokens;
|
|
642
642
|
layout: ShellLayoutMode;
|
|
643
643
|
soundOn: boolean;
|
|
644
|
-
readonly engineVersion = "0.
|
|
644
|
+
readonly engineVersion = "0.11.0";
|
|
645
645
|
readonly actions: ShellActions;
|
|
646
646
|
private renderer;
|
|
647
647
|
private i18n;
|
|
@@ -785,8 +785,74 @@ interface I18n {
|
|
|
785
785
|
}
|
|
786
786
|
declare function createI18n(opts: I18nOptions): I18n;
|
|
787
787
|
|
|
788
|
+
/**
|
|
789
|
+
* The disclaimer body every launch shows, in source English — and the SAME strings that key the
|
|
790
|
+
* translations below. One constant for both on purpose: a host that kept its own copy would drift
|
|
791
|
+
* from these keys by one character and the lookup would miss in silence, leaving a player on any
|
|
792
|
+
* language with English legal text. Certification asks for the opposite ("if multiple languages are
|
|
793
|
+
* supported, the disclaimer is translated and displayed in each language").
|
|
794
|
+
*
|
|
795
|
+
* Brand-free by construction. Stake's own template ends with a seventh line — "TM and © {year}
|
|
796
|
+
* Stake Engine." — and that one belongs to a Stake launch alone: it arrives with the Stake bridge's
|
|
797
|
+
* `INIT.config.disclaimerLines`, which outrank this default, and renders verbatim (see the host's
|
|
798
|
+
* `isBrandLine`). Every other platform gets the same mandated wording without someone else's mark.
|
|
799
|
+
*/
|
|
800
|
+
declare const DISCLAIMER_LINES: readonly string[];
|
|
801
|
+
|
|
788
802
|
/** The @energy8platform/shell package version, stamped into the game-info footer. */
|
|
789
|
-
declare const PACKAGE_VERSION = "0.
|
|
803
|
+
declare const PACKAGE_VERSION = "0.11.0";
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* What a scrollable region should ADVERTISE about itself.
|
|
807
|
+
*
|
|
808
|
+
* A slot in a Stake popout is 400×225. At that size the game-info overlay holds twelve screens of
|
|
809
|
+
* content, the buy-bonus switches to a vertical card stack, and the menu popover hides its last
|
|
810
|
+
* row — all of them scroll, and (before this module) none of them said so. macOS makes it worse:
|
|
811
|
+
* overlay scrollbars stay invisible until something is already scrolling, so the very affordance a
|
|
812
|
+
* player needs BEFORE they touch anything is the one the OS withholds. A certification reviewer
|
|
813
|
+
* reads that as content the player can't reach.
|
|
814
|
+
*
|
|
815
|
+
* The maths lives here, apart from both renderers, for one reason: the DOM shell reads
|
|
816
|
+
* `scrollTop`/`scrollHeight` while the Pixi shell tracks its own offset against a mask, and those
|
|
817
|
+
* two must never disagree about whether a fade belongs at the bottom edge. Renderers decide how a
|
|
818
|
+
* thumb LOOKS; this decides when there is one and where it sits.
|
|
819
|
+
*/
|
|
820
|
+
/**
|
|
821
|
+
* Shortest thumb we will draw, as a fraction of its track.
|
|
822
|
+
*
|
|
823
|
+
* The honest ratio for game info at Popout S is 8% — an 18px speck on a 226px track, which reads
|
|
824
|
+
* as a rendering artifact rather than a scrollbar. Floored at 18% it stays recognisably a thumb,
|
|
825
|
+
* and it still travels the whole track, so the position it reports remains truthful even though
|
|
826
|
+
* its length no longer is. That is the right trade: length is decoration, position is information.
|
|
827
|
+
*/
|
|
828
|
+
declare const SCROLL_THUMB_MIN = 0.18;
|
|
829
|
+
/** One axis of a scroll region. Named for the Y axis because that is the common case; the
|
|
830
|
+
* buy-bonus strip passes its width metrics through the same fields. */
|
|
831
|
+
interface ScrollMetrics {
|
|
832
|
+
scrollTop: number;
|
|
833
|
+
scrollHeight: number;
|
|
834
|
+
clientHeight: number;
|
|
835
|
+
}
|
|
836
|
+
interface ScrollHint {
|
|
837
|
+
/** There is content past an edge — draw the affordance at all. */
|
|
838
|
+
overflowing: boolean;
|
|
839
|
+
/** Nothing above/left of the viewport: suppress the leading fade. */
|
|
840
|
+
atStart: boolean;
|
|
841
|
+
/** Nothing below/right of the viewport: suppress the trailing fade and the chevron. */
|
|
842
|
+
atEnd: boolean;
|
|
843
|
+
/** Thumb length as a fraction of the track, in `[SCROLL_THUMB_MIN, 1]`. */
|
|
844
|
+
thumbSize: number;
|
|
845
|
+
/** Thumb's leading edge as a fraction of the track, in `[0, 1 - thumbSize]`. */
|
|
846
|
+
thumbOffset: number;
|
|
847
|
+
/** Scrollable distance in pixels — 0 when the content fits. */
|
|
848
|
+
maxScroll: number;
|
|
849
|
+
}
|
|
850
|
+
/** Resolve one axis of a scroll region into everything a renderer needs to draw its affordance. */
|
|
851
|
+
declare function scrollHint(m: ScrollMetrics): ScrollHint;
|
|
852
|
+
/** The three-state tag both renderers put on a scroll region, so CSS and tests can name it.
|
|
853
|
+
* `none` when the content fits — the attribute is removed rather than set to it. */
|
|
854
|
+
type ScrollEdge = 'none' | 'start' | 'mid' | 'end';
|
|
855
|
+
declare function scrollEdge(h: ScrollHint): ScrollEdge;
|
|
790
856
|
|
|
791
857
|
/** A shell: the renderer-agnostic controller plus the surface facade (safeArea/barHeight/setVisible)
|
|
792
858
|
* an embedding host reads. `createShell`, `createGameShell` and `createPixiShell` all return this. */
|
|
@@ -866,5 +932,5 @@ declare function createGameShell(config: HtmlShellConfig): ShellController;
|
|
|
866
932
|
/** Tear down the active shell (no argument — singleton). Resolves immediately when nothing is active. */
|
|
867
933
|
declare function removeGameShell(): Promise<void>;
|
|
868
934
|
|
|
869
|
-
export { DEFAULT_ACCENT, DEFAULT_MENU, ShellController as GameShell, HtmlRenderer, PACKAGE_VERSION, POPOVER, SCHEMES, ShellController, createGameShell, createI18n, createShell, isPresetId, normalizeLang, placePopover, popoverWidth, rangeBounds, removeGameShell, resolveConfig, resolveMenu, resolveTheme, seedMenuValues, socialize };
|
|
870
|
-
export type { AutoplayConfig, AutoplayOptions, BonusCardContext, BonusOption, BonusReadout, CellRef, CreateShellOptions, CurrencyConfig, FreeSpinsState, GameInfoContent, GameInfoSection, GameMode, HtmlShellConfig, I18n, I18nOptions, Lang, MenuHost, MenuItem, MenuPresetId, MenuRow, ModalAction, ModalOptions, OverlayHandle, OverlayRequest, PaylineDef, PaytableRow, PopoverPlacement, Rect as PopoverRect, ReplayModalOptions, ResolvedShellConfig, SafeArea, ShapeDef, Shell, ShellActions, HtmlShellConfig as ShellConfig, ShellEvents, ShellFeatures, ShellHost, ShellLayoutMode, ShellMode, ShellRenderer, ShellState, ShellSurface, ShellTokens, ThemeConfig, VolumeKey, VolumeLevels, WinSection };
|
|
935
|
+
export { DEFAULT_ACCENT, DEFAULT_MENU, DISCLAIMER_LINES, ShellController as GameShell, HtmlRenderer, PACKAGE_VERSION, POPOVER, SCHEMES, SCROLL_THUMB_MIN, ShellController, createGameShell, createI18n, createShell, isPresetId, normalizeLang, placePopover, popoverWidth, rangeBounds, removeGameShell, resolveConfig, resolveMenu, resolveTheme, scrollEdge, scrollHint, seedMenuValues, socialize };
|
|
936
|
+
export type { AutoplayConfig, AutoplayOptions, BonusCardContext, BonusOption, BonusReadout, CellRef, CreateShellOptions, CurrencyConfig, FreeSpinsState, GameInfoContent, GameInfoSection, GameMode, HtmlShellConfig, I18n, I18nOptions, Lang, MenuHost, MenuItem, MenuPresetId, MenuRow, ModalAction, ModalOptions, OverlayHandle, OverlayRequest, PaylineDef, PaytableRow, PopoverPlacement, Rect as PopoverRect, ReplayModalOptions, ResolvedShellConfig, SafeArea, ScrollEdge, ScrollHint, ScrollMetrics, ShapeDef, Shell, ShellActions, HtmlShellConfig as ShellConfig, ShellEvents, ShellFeatures, ShellHost, ShellLayoutMode, ShellMode, ShellRenderer, ShellState, ShellSurface, ShellTokens, ThemeConfig, VolumeKey, VolumeLevels, WinSection };
|
package/dist/html.esm.js
CHANGED
|
@@ -1306,6 +1306,19 @@ const D = {
|
|
|
1306
1306
|
L4: 'The game display is not representative of any physical device and is for illustrative purposes only.',
|
|
1307
1307
|
L5: 'Winnings are settled according to the amount received from the Remote Game Server and not from events within the web browser.',
|
|
1308
1308
|
};
|
|
1309
|
+
/**
|
|
1310
|
+
* The disclaimer body every launch shows, in source English — and the SAME strings that key the
|
|
1311
|
+
* translations below. One constant for both on purpose: a host that kept its own copy would drift
|
|
1312
|
+
* from these keys by one character and the lookup would miss in silence, leaving a player on any
|
|
1313
|
+
* language with English legal text. Certification asks for the opposite ("if multiple languages are
|
|
1314
|
+
* supported, the disclaimer is translated and displayed in each language").
|
|
1315
|
+
*
|
|
1316
|
+
* Brand-free by construction. Stake's own template ends with a seventh line — "TM and © {year}
|
|
1317
|
+
* Stake Engine." — and that one belongs to a Stake launch alone: it arrives with the Stake bridge's
|
|
1318
|
+
* `INIT.config.disclaimerLines`, which outrank this default, and renders verbatim (see the host's
|
|
1319
|
+
* `isBrandLine`). Every other platform gets the same mandated wording without someone else's mark.
|
|
1320
|
+
*/
|
|
1321
|
+
const DISCLAIMER_LINES = [D.L1, D.L2, D.L3, D.L4, D.L5];
|
|
1309
1322
|
const DISCLAIMER_LOCALES = {
|
|
1310
1323
|
da: {
|
|
1311
1324
|
[D.L1]: 'Fejlfunktion annullerer alle gevinster og spil.',
|
|
@@ -1723,7 +1736,7 @@ function keyboardCapable(win = typeof window === 'undefined' ? undefined : windo
|
|
|
1723
1736
|
|
|
1724
1737
|
// AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
|
|
1725
1738
|
/** The @energy8platform/shell package version, stamped into the game-info footer. */
|
|
1726
|
-
const PACKAGE_VERSION = '0.
|
|
1739
|
+
const PACKAGE_VERSION = '0.11.0';
|
|
1727
1740
|
|
|
1728
1741
|
/** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
|
|
1729
1742
|
function resolveConfig(config) {
|
|
@@ -2209,6 +2222,66 @@ function placePopover(anchor, surface, size, pointer = null) {
|
|
|
2209
2222
|
return { x, y, maxH, arrowX, below };
|
|
2210
2223
|
}
|
|
2211
2224
|
|
|
2225
|
+
/**
|
|
2226
|
+
* What a scrollable region should ADVERTISE about itself.
|
|
2227
|
+
*
|
|
2228
|
+
* A slot in a Stake popout is 400×225. At that size the game-info overlay holds twelve screens of
|
|
2229
|
+
* content, the buy-bonus switches to a vertical card stack, and the menu popover hides its last
|
|
2230
|
+
* row — all of them scroll, and (before this module) none of them said so. macOS makes it worse:
|
|
2231
|
+
* overlay scrollbars stay invisible until something is already scrolling, so the very affordance a
|
|
2232
|
+
* player needs BEFORE they touch anything is the one the OS withholds. A certification reviewer
|
|
2233
|
+
* reads that as content the player can't reach.
|
|
2234
|
+
*
|
|
2235
|
+
* The maths lives here, apart from both renderers, for one reason: the DOM shell reads
|
|
2236
|
+
* `scrollTop`/`scrollHeight` while the Pixi shell tracks its own offset against a mask, and those
|
|
2237
|
+
* two must never disagree about whether a fade belongs at the bottom edge. Renderers decide how a
|
|
2238
|
+
* thumb LOOKS; this decides when there is one and where it sits.
|
|
2239
|
+
*/
|
|
2240
|
+
/** Fractions of a pixel are layout rounding, not reachable content. */
|
|
2241
|
+
const EPSILON = 1;
|
|
2242
|
+
/** Landing within half a pixel of an edge counts as arriving: a browser settles a flung scroll on
|
|
2243
|
+
* 199.5 of 200, and a fade left glowing over content the player has already reached reads as a
|
|
2244
|
+
* bug. */
|
|
2245
|
+
const EDGE_EPSILON = 0.5;
|
|
2246
|
+
/**
|
|
2247
|
+
* Shortest thumb we will draw, as a fraction of its track.
|
|
2248
|
+
*
|
|
2249
|
+
* The honest ratio for game info at Popout S is 8% — an 18px speck on a 226px track, which reads
|
|
2250
|
+
* as a rendering artifact rather than a scrollbar. Floored at 18% it stays recognisably a thumb,
|
|
2251
|
+
* and it still travels the whole track, so the position it reports remains truthful even though
|
|
2252
|
+
* its length no longer is. That is the right trade: length is decoration, position is information.
|
|
2253
|
+
*/
|
|
2254
|
+
const SCROLL_THUMB_MIN = 0.18;
|
|
2255
|
+
/** Resolve one axis of a scroll region into everything a renderer needs to draw its affordance. */
|
|
2256
|
+
function scrollHint(m) {
|
|
2257
|
+
const view = Math.max(0, m.clientHeight);
|
|
2258
|
+
const content = Math.max(0, m.scrollHeight);
|
|
2259
|
+
const maxScroll = Math.max(0, content - view);
|
|
2260
|
+
if (maxScroll <= EPSILON || view <= 0) {
|
|
2261
|
+
// Both edges are "the edge" when there is nowhere to go — callers gate every fade on the
|
|
2262
|
+
// matching flag, so a region that fits draws nothing without needing to check `overflowing`.
|
|
2263
|
+
return { overflowing: false, atStart: true, atEnd: true, thumbSize: 1, thumbOffset: 0, maxScroll: 0 };
|
|
2264
|
+
}
|
|
2265
|
+
const at = Math.max(0, Math.min(maxScroll, m.scrollTop));
|
|
2266
|
+
const thumbSize = Math.min(1, Math.max(SCROLL_THUMB_MIN, view / content));
|
|
2267
|
+
const progress = at / maxScroll;
|
|
2268
|
+
return {
|
|
2269
|
+
overflowing: true,
|
|
2270
|
+
atStart: at <= EDGE_EPSILON,
|
|
2271
|
+
atEnd: maxScroll - at <= EDGE_EPSILON,
|
|
2272
|
+
thumbSize,
|
|
2273
|
+
thumbOffset: progress * (1 - thumbSize),
|
|
2274
|
+
maxScroll,
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
function scrollEdge(h) {
|
|
2278
|
+
if (!h.overflowing)
|
|
2279
|
+
return 'none';
|
|
2280
|
+
if (h.atStart)
|
|
2281
|
+
return 'start';
|
|
2282
|
+
return h.atEnd ? 'end' : 'mid';
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2212
2285
|
const NO_INSET = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
2213
2286
|
/** Create a shell with an explicit renderer instance (custom or a built-in HtmlRenderer/PixiRenderer).
|
|
2214
2287
|
* Built-in renderers also have the createGameShell/createPixiShell sugar in /html and /pixi.
|
|
@@ -2412,6 +2485,68 @@ const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
2412
2485
|
transition:background .12s ease, color .12s ease; }
|
|
2413
2486
|
#${SHELL_ROOT_ID} .ge-ov-nav:hover { background:var(--shell-plaque-glass); color:var(--shell-accent); }
|
|
2414
2487
|
#${SHELL_ROOT_ID} .ge-ov-scroll { flex:1 1 auto; min-height:0; overflow-y:auto; overflow-x:hidden; }
|
|
2488
|
+
|
|
2489
|
+
/* ═══ scroll affordance — "there is more, and you can reach it" ═══════════════════════════════
|
|
2490
|
+
A Stake popout is 400×225. Game info holds twelve screens there, buy-bonus stacks its cards, and
|
|
2491
|
+
the menu popover hides its last row. All of them scrolled already; none of them said so, and
|
|
2492
|
+
Stake rejected the build for it. macOS is the aggravating factor — its overlay scrollbars stay
|
|
2493
|
+
invisible until something is ALREADY scrolling, so the one moment a player needs the hint is the
|
|
2494
|
+
one moment the OS withholds it. Styling the scrollbar at all opts out of that behaviour.
|
|
2495
|
+
|
|
2496
|
+
Three layers, all keyed off data-scroll (written by attachScrollAffordance):
|
|
2497
|
+
1. a persistent thumb — the standing "this scrolls" mark;
|
|
2498
|
+
2. a mask fade at whichever edge hides content — the mask applies to the element's own box, so
|
|
2499
|
+
it does NOT travel with the content the way a child gradient would;
|
|
2500
|
+
3. a chevron, shown only in the start state and retired for good on the first scroll.
|
|
2501
|
+
The end state deliberately gets no trailing fade: there is nothing left below to hint at. */
|
|
2502
|
+
/* Order matters here, and not for cascade reasons. Chromium honours the STANDARD scrollbar
|
|
2503
|
+
properties when they are present and then ignores ::-webkit-scrollbar entirely — and on macOS the
|
|
2504
|
+
standard properties leave the scrollbar in overlay mode, i.e. invisible until it is already
|
|
2505
|
+
moving. Defining ::-webkit-scrollbar with a width is what switches that scroller to a classic,
|
|
2506
|
+
always-painted, still-draggable scrollbar. So the standard properties are quarantined behind an
|
|
2507
|
+
@supports that only Firefox (which has no ::-webkit-scrollbar) satisfies. */
|
|
2508
|
+
@supports not selector(::-webkit-scrollbar) {
|
|
2509
|
+
#${SHELL_ROOT_ID} [data-scroll] { scrollbar-width:thin;
|
|
2510
|
+
scrollbar-color:var(--shell-scrollbar,rgba(255,255,255,.34)) transparent; } }
|
|
2511
|
+
#${SHELL_ROOT_ID} [data-scroll]::-webkit-scrollbar { width:6px; height:6px; }
|
|
2512
|
+
#${SHELL_ROOT_ID} [data-scroll]::-webkit-scrollbar-track { background:transparent; }
|
|
2513
|
+
#${SHELL_ROOT_ID} [data-scroll]::-webkit-scrollbar-thumb { border-radius:999px;
|
|
2514
|
+
background:var(--shell-scrollbar,rgba(255,255,255,.34)); }
|
|
2515
|
+
#${SHELL_ROOT_ID} [data-scroll]::-webkit-scrollbar-thumb:hover { background:var(--shell-accent); }
|
|
2516
|
+
/* Vertical fade. The stop pair is the same on both edges; only which edges are opaque changes.
|
|
2517
|
+
mask-composite-free on purpose — a single multi-stop gradient handles the two-edge case. */
|
|
2518
|
+
#${SHELL_ROOT_ID} [data-scroll="start"]:not([data-scroll-axis="x"]) {
|
|
2519
|
+
-webkit-mask-image:linear-gradient(to bottom, #000 calc(100% - 34px), transparent 100%);
|
|
2520
|
+
mask-image:linear-gradient(to bottom, #000 calc(100% - 34px), transparent 100%); }
|
|
2521
|
+
#${SHELL_ROOT_ID} [data-scroll="mid"]:not([data-scroll-axis="x"]) {
|
|
2522
|
+
-webkit-mask-image:linear-gradient(to bottom, transparent 0, #000 22px, #000 calc(100% - 34px), transparent 100%);
|
|
2523
|
+
mask-image:linear-gradient(to bottom, transparent 0, #000 22px, #000 calc(100% - 34px), transparent 100%); }
|
|
2524
|
+
#${SHELL_ROOT_ID} [data-scroll="end"]:not([data-scroll-axis="x"]) {
|
|
2525
|
+
-webkit-mask-image:linear-gradient(to bottom, transparent 0, #000 22px);
|
|
2526
|
+
mask-image:linear-gradient(to bottom, transparent 0, #000 22px); }
|
|
2527
|
+
/* Horizontal fade — the buy-bonus card strip. */
|
|
2528
|
+
#${SHELL_ROOT_ID} [data-scroll="start"][data-scroll-axis="x"] {
|
|
2529
|
+
-webkit-mask-image:linear-gradient(to right, #000 calc(100% - 30px), transparent 100%);
|
|
2530
|
+
mask-image:linear-gradient(to right, #000 calc(100% - 30px), transparent 100%); }
|
|
2531
|
+
#${SHELL_ROOT_ID} [data-scroll="mid"][data-scroll-axis="x"] {
|
|
2532
|
+
-webkit-mask-image:linear-gradient(to right, transparent 0, #000 20px, #000 calc(100% - 30px), transparent 100%);
|
|
2533
|
+
mask-image:linear-gradient(to right, transparent 0, #000 20px, #000 calc(100% - 30px), transparent 100%); }
|
|
2534
|
+
#${SHELL_ROOT_ID} [data-scroll="end"][data-scroll-axis="x"] {
|
|
2535
|
+
-webkit-mask-image:linear-gradient(to right, transparent 0, #000 20px);
|
|
2536
|
+
mask-image:linear-gradient(to right, transparent 0, #000 20px); }
|
|
2537
|
+
/* The chevron. Its host is whatever box CONTAINS the scroller (overlay root / popover card), so it
|
|
2538
|
+
holds still while the content moves under it. */
|
|
2539
|
+
#${SHELL_ROOT_ID} .ge-scroll-cue { position:absolute; left:0; top:0; z-index:2;
|
|
2540
|
+
width:22px; height:22px; padding:3px; box-sizing:border-box;
|
|
2541
|
+
display:flex; align-items:center; justify-content:center; pointer-events:none;
|
|
2542
|
+
border-radius:50%; color:#fff; background:var(--shell-plaque-dark);
|
|
2543
|
+
box-shadow:0 2px 10px rgba(0,0,0,.45); animation:ge-scroll-cue 1.6s ease-in-out infinite; }
|
|
2544
|
+
@keyframes ge-scroll-cue {
|
|
2545
|
+
0%,100% { transform:translateY(0); opacity:.85; }
|
|
2546
|
+
50% { transform:translateY(4px); opacity:1; } }
|
|
2547
|
+
/* A player who has asked for stillness still needs the hint — keep the chevron, drop the bob. */
|
|
2548
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2549
|
+
#${SHELL_ROOT_ID} .ge-scroll-cue { animation:none; opacity:.95; } }
|
|
2415
2550
|
#${SHELL_ROOT_ID} .ge-ov-body { max-width:800px; margin:0 auto; box-sizing:border-box;
|
|
2416
2551
|
padding:clamp(6px,2vh,16px) clamp(16px,4vw,24px) clamp(16px,4vh,28px); }
|
|
2417
2552
|
|
|
@@ -2773,7 +2908,9 @@ const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
2773
2908
|
card's font-size is the one knob (clamped for readability); everything inside is em-relative so
|
|
2774
2909
|
the whole card scales as a unit. GameShell.fitModal() still transform-scales it down as a
|
|
2775
2910
|
backstop for very short popouts. */
|
|
2776
|
-
|
|
2911
|
+
/* position:relative is here for the scroll cue: the card is the cue's containing block, so a
|
|
2912
|
+
capped, scrolling chip grid hints at its bottom edge rather than the screen's. */
|
|
2913
|
+
#${SHELL_ROOT_ID} .ge-modal-card { position:relative; font-size:clamp(11px, 2cqmin, 15px); width:100%; max-width:28em; box-sizing:border-box;
|
|
2777
2914
|
overflow:hidden; transform-origin:center center; background:var(--shell-plaque-solid); border-radius:1.3em;
|
|
2778
2915
|
display:flex; flex-direction:column; }
|
|
2779
2916
|
/* ✕ pinned to the overlay corner (the screen), not the card */
|
|
@@ -3209,6 +3346,113 @@ function applyBusy(host, bar) {
|
|
|
3209
3346
|
buy.disabled = busy || auto || !host.state.buyBonusEnabled;
|
|
3210
3347
|
}
|
|
3211
3348
|
|
|
3349
|
+
/** Cue diameter, mirrored in the stylesheet. Kept here because the cue is positioned in JS. */
|
|
3350
|
+
const CUE_SIZE = 22;
|
|
3351
|
+
const CUE_GAP = 6;
|
|
3352
|
+
/** The chevron glyph, inline so it needs no icon-set entry and no font. */
|
|
3353
|
+
const CUE_SVG = '<svg viewBox="0 0 24 24" width="100%" height="100%" fill="none" stroke="currentColor" ' +
|
|
3354
|
+
'stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 9l7 7 7-7"/></svg>';
|
|
3355
|
+
function attachScrollAffordance(el, opts = {}) {
|
|
3356
|
+
const axis = opts.axis ?? 'y';
|
|
3357
|
+
let cue = null;
|
|
3358
|
+
// Once the player scrolls they have discovered the gesture; re-offering it every time they
|
|
3359
|
+
// return to the top would nag rather than inform.
|
|
3360
|
+
let cueRetired = false;
|
|
3361
|
+
let destroyed = false;
|
|
3362
|
+
if (axis === 'x')
|
|
3363
|
+
el.dataset.scrollAxis = 'x';
|
|
3364
|
+
const removeCue = () => {
|
|
3365
|
+
cue?.remove();
|
|
3366
|
+
cue = null;
|
|
3367
|
+
};
|
|
3368
|
+
const showCue = () => {
|
|
3369
|
+
if (cue || cueRetired || opts.cue === false)
|
|
3370
|
+
return;
|
|
3371
|
+
const host = opts.cueHost ?? el.parentElement;
|
|
3372
|
+
if (!host)
|
|
3373
|
+
return;
|
|
3374
|
+
cue = document.createElement('div');
|
|
3375
|
+
cue.className = 'ge-scroll-cue';
|
|
3376
|
+
cue.setAttribute('aria-hidden', 'true');
|
|
3377
|
+
cue.innerHTML = CUE_SVG;
|
|
3378
|
+
host.appendChild(cue);
|
|
3379
|
+
positionCue();
|
|
3380
|
+
};
|
|
3381
|
+
/** Pin the cue to the bottom of the SCROLLER, not of its host. The buy-bonus overlay hangs a bet
|
|
3382
|
+
* bar below its scroll region, and a cue pinned to the host's bottom edge lands on top of it. */
|
|
3383
|
+
const positionCue = () => {
|
|
3384
|
+
if (!cue)
|
|
3385
|
+
return;
|
|
3386
|
+
const size = cue.offsetWidth || CUE_SIZE;
|
|
3387
|
+
if (axis === 'x') {
|
|
3388
|
+
cue.style.left = `${el.offsetLeft + el.offsetWidth - size - CUE_GAP}px`;
|
|
3389
|
+
cue.style.top = `${el.offsetTop + (el.offsetHeight - size) / 2}px`;
|
|
3390
|
+
}
|
|
3391
|
+
else {
|
|
3392
|
+
cue.style.left = `${el.offsetLeft + (el.offsetWidth - size) / 2}px`;
|
|
3393
|
+
cue.style.top = `${el.offsetTop + el.offsetHeight - size - CUE_GAP}px`;
|
|
3394
|
+
}
|
|
3395
|
+
};
|
|
3396
|
+
const sync = () => {
|
|
3397
|
+
if (destroyed)
|
|
3398
|
+
return;
|
|
3399
|
+
const h = axis === 'x'
|
|
3400
|
+
? scrollHint({ scrollTop: el.scrollLeft, scrollHeight: el.scrollWidth, clientHeight: el.clientWidth })
|
|
3401
|
+
: scrollHint({ scrollTop: el.scrollTop, scrollHeight: el.scrollHeight, clientHeight: el.clientHeight });
|
|
3402
|
+
const edge = scrollEdge(h);
|
|
3403
|
+
if (edge === 'none') {
|
|
3404
|
+
delete el.dataset.scroll;
|
|
3405
|
+
removeCue();
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
3408
|
+
el.dataset.scroll = edge;
|
|
3409
|
+
if (edge === 'start') {
|
|
3410
|
+
showCue();
|
|
3411
|
+
positionCue(); // the scroller can move/resize under a cue that is already up
|
|
3412
|
+
}
|
|
3413
|
+
else {
|
|
3414
|
+
removeCue();
|
|
3415
|
+
}
|
|
3416
|
+
};
|
|
3417
|
+
// Retirement is keyed off actual MOVEMENT, not off the event. A browser also fires `scroll` when
|
|
3418
|
+
// content reflows under a pinned offset, and a hint dismissed by a reflow the player never caused
|
|
3419
|
+
// is a hint they never saw.
|
|
3420
|
+
let lastPos = axis === 'x' ? el.scrollLeft : el.scrollTop;
|
|
3421
|
+
const onScroll = () => {
|
|
3422
|
+
const pos = axis === 'x' ? el.scrollLeft : el.scrollTop;
|
|
3423
|
+
if (pos !== lastPos) {
|
|
3424
|
+
lastPos = pos;
|
|
3425
|
+
cueRetired = true;
|
|
3426
|
+
removeCue();
|
|
3427
|
+
}
|
|
3428
|
+
sync();
|
|
3429
|
+
};
|
|
3430
|
+
el.addEventListener('scroll', onScroll, { passive: true });
|
|
3431
|
+
// Content in these regions is built asynchronously (fonts, images, a rebuilt body on resize), so
|
|
3432
|
+
// a single sync at mount would measure the wrong thing. ResizeObserver is absent in jsdom.
|
|
3433
|
+
const RO = globalThis.ResizeObserver;
|
|
3434
|
+
const ro = typeof RO === 'function' ? new RO(() => sync()) : null;
|
|
3435
|
+
if (ro) {
|
|
3436
|
+
ro.observe(el);
|
|
3437
|
+
if (el.firstElementChild)
|
|
3438
|
+
ro.observe(el.firstElementChild);
|
|
3439
|
+
}
|
|
3440
|
+
sync();
|
|
3441
|
+
return {
|
|
3442
|
+
sync,
|
|
3443
|
+
destroy() {
|
|
3444
|
+
if (destroyed)
|
|
3445
|
+
return;
|
|
3446
|
+
destroyed = true;
|
|
3447
|
+
el.removeEventListener('scroll', onScroll);
|
|
3448
|
+
ro?.disconnect();
|
|
3449
|
+
delete el.dataset.scroll;
|
|
3450
|
+
delete el.dataset.scrollAxis;
|
|
3451
|
+
removeCue();
|
|
3452
|
+
},
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3212
3456
|
/** A centred CARD modal — frosted backdrop + opaque card with an accent title heading and an
|
|
3213
3457
|
* overlay ✕ in the top-right. The shared chrome for every centred modal (buy-bonus confirm,
|
|
3214
3458
|
* bet, autoplay, generic openModal). Append content to `body`; append full-bleed footer
|
|
@@ -3243,8 +3487,9 @@ function createCardModal(opts) {
|
|
|
3243
3487
|
}
|
|
3244
3488
|
return { root, card, body };
|
|
3245
3489
|
}
|
|
3246
|
-
/** Full-screen overlay. Returns { root, body, scroll }; append content to body.
|
|
3247
|
-
* The `scroll` element is the scrollable container (overflow-y: auto)
|
|
3490
|
+
/** Full-screen overlay. Returns { root, body, scroll, affordance }; append content to body.
|
|
3491
|
+
* The `scroll` element is the scrollable container (overflow-y: auto); `affordance` marks it as
|
|
3492
|
+
* scrollable once it overflows — call `affordance.sync()` after filling or resizing the body. */
|
|
3248
3493
|
function createOverlay(opts) {
|
|
3249
3494
|
const root = document.createElement('div');
|
|
3250
3495
|
root.className = 'ge-shell-overlay';
|
|
@@ -3282,7 +3527,9 @@ function createOverlay(opts) {
|
|
|
3282
3527
|
body.className = 'ge-ov-body';
|
|
3283
3528
|
scroll.appendChild(body);
|
|
3284
3529
|
root.append(head, scroll);
|
|
3285
|
-
|
|
3530
|
+
// The cue is hosted on `root`, not on `scroll`: `scroll` is the element that moves.
|
|
3531
|
+
const affordance = attachScrollAffordance(scroll, { cueHost: root });
|
|
3532
|
+
return { root, body, scroll, affordance };
|
|
3286
3533
|
}
|
|
3287
3534
|
/** A light-dismiss popover: a transparent full-surface layer (closes on pointerdown) holding a
|
|
3288
3535
|
* card with an arrow that points at `pointer`. Append rows to `body`; call `position()` after the
|
|
@@ -3303,6 +3550,9 @@ function createPopover(opts) {
|
|
|
3303
3550
|
// Clicks inside the card must not reach the dismiss layer.
|
|
3304
3551
|
card.addEventListener('pointerdown', (e) => e.stopPropagation());
|
|
3305
3552
|
root.addEventListener('pointerdown', opts.onClose);
|
|
3553
|
+
// The card clamps itself to `maxHeight` in position(), so the body's overflow is only knowable
|
|
3554
|
+
// after that runs — hence the sync at the end of position().
|
|
3555
|
+
const affordance = attachScrollAffordance(body, { cueHost: card });
|
|
3306
3556
|
const resolveEl = (v) => typeof v === 'function' ? v() : (v ?? null);
|
|
3307
3557
|
/** A rect in surface coordinates, or null when unresolved/fully zero-sized (a zero-HEIGHT rect —
|
|
3308
3558
|
* e.g. a not-yet-laid-out anchor — is still considered valid, matching placePopover's own rule). */
|
|
@@ -3378,8 +3628,9 @@ function createPopover(opts) {
|
|
|
3378
3628
|
arrow.style.display = '';
|
|
3379
3629
|
arrow.style.left = `${p.arrowX / s}px`;
|
|
3380
3630
|
}
|
|
3631
|
+
affordance.sync();
|
|
3381
3632
|
};
|
|
3382
|
-
return { root, card, body, position };
|
|
3633
|
+
return { root, card, body, affordance, position };
|
|
3383
3634
|
}
|
|
3384
3635
|
|
|
3385
3636
|
/** The bar menu, as a light-dismiss popover anchored to the burger. Rows come from the core model,
|
|
@@ -3530,7 +3781,7 @@ function buildRow(host, row, updaters, reposition) {
|
|
|
3530
3781
|
const HOTKEYS_DEFAULT_ORDER = -0.5;
|
|
3531
3782
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
3532
3783
|
function openGameInfoModal(host) {
|
|
3533
|
-
const { root, body, scroll } = createOverlay({
|
|
3784
|
+
const { root, body, scroll, affordance } = createOverlay({
|
|
3534
3785
|
title: host.t('Game info'),
|
|
3535
3786
|
onClose: () => host.actions.closeOverlay(),
|
|
3536
3787
|
onBack: () => { root.remove(); host.actions.openMenu(); },
|
|
@@ -3556,6 +3807,8 @@ function openGameInfoModal(host) {
|
|
|
3556
3807
|
.sort((a, b) => a.k - b.k || a.i - b.i)
|
|
3557
3808
|
.forEach(({ s }) => body.appendChild(renderSection(host, s)));
|
|
3558
3809
|
body.appendChild(versionFooter(host));
|
|
3810
|
+
// The body is filled after createOverlay returned, so its first honest measurement is here.
|
|
3811
|
+
affordance.sync();
|
|
3559
3812
|
const LINE = 60;
|
|
3560
3813
|
const PAGE = () => Math.floor(scroll.clientHeight * 0.9) || Math.floor(540 * 0.9);
|
|
3561
3814
|
const onKey = (e) => {
|
|
@@ -3948,10 +4201,13 @@ function openBuyBonusOverlay(host) {
|
|
|
3948
4201
|
if (bonuses === false || bonuses.length === 0)
|
|
3949
4202
|
return null;
|
|
3950
4203
|
const st = { focusIndex: -1, confirmBonus: undefined };
|
|
3951
|
-
const { root, body } = createOverlay({ title: host.t('Buy bonus'), onClose: () => host.actions.closeOverlay() });
|
|
4204
|
+
const { root, body, affordance } = createOverlay({ title: host.t('Buy bonus'), onClose: () => host.actions.closeOverlay() });
|
|
3952
4205
|
root.dataset.ge = 'buybonus-overlay';
|
|
4206
|
+
// The strip's own X-scroll affordance, rebuilt with the grid it describes.
|
|
4207
|
+
let gridAffordance = null;
|
|
3953
4208
|
// Re-render the grid whenever the bet changes so every card's price stays live.
|
|
3954
4209
|
const renderGrid = () => {
|
|
4210
|
+
gridAffordance?.destroy();
|
|
3955
4211
|
body.innerHTML = '';
|
|
3956
4212
|
const grid = document.createElement('div');
|
|
3957
4213
|
grid.className = 'ge-bb-grid';
|
|
@@ -3977,6 +4233,11 @@ function openBuyBonusOverlay(host) {
|
|
|
3977
4233
|
else {
|
|
3978
4234
|
st.focusIndex = -1;
|
|
3979
4235
|
}
|
|
4236
|
+
// Two axes, two affordances. Below a ~340px frame the CSS stacks the cards and the OVERLAY
|
|
4237
|
+
// scrolls vertically (see the ge-bb-frame container query); above it the STRIP scrolls
|
|
4238
|
+
// horizontally. Each is attached unconditionally and stays silent on the axis that fits.
|
|
4239
|
+
gridAffordance = attachScrollAffordance(grid, { axis: 'x', cue: false });
|
|
4240
|
+
affordance.sync();
|
|
3980
4241
|
};
|
|
3981
4242
|
renderGrid();
|
|
3982
4243
|
root.appendChild(buildBetBar(host, renderGrid)); // thin bottom footer, only as tall as the pill
|
|
@@ -4307,6 +4568,9 @@ function buildSheet(opts) {
|
|
|
4307
4568
|
grid.appendChild(chip);
|
|
4308
4569
|
}
|
|
4309
4570
|
ui.body.appendChild(grid);
|
|
4571
|
+
// A long bet ladder is capped at 50vh and scrolls; in a 225px popout that cap bites after two
|
|
4572
|
+
// rows. The cue hangs off the card, which holds still while the grid moves.
|
|
4573
|
+
attachScrollAffordance(grid, { cueHost: ui.card });
|
|
4310
4574
|
function doConfirm() {
|
|
4311
4575
|
opts.onConfirm(selected);
|
|
4312
4576
|
opts.onClose();
|
|
@@ -4802,5 +5066,5 @@ function removeGameShell() {
|
|
|
4802
5066
|
return shell.destroy();
|
|
4803
5067
|
}
|
|
4804
5068
|
|
|
4805
|
-
export { DEFAULT_ACCENT, DEFAULT_MENU, ShellController as GameShell, HtmlRenderer, PACKAGE_VERSION, POPOVER, SCHEMES, ShellController, createGameShell, createI18n, createShell, isPresetId, normalizeLang, placePopover, popoverWidth, rangeBounds, removeGameShell, resolveConfig, resolveMenu, resolveTheme, seedMenuValues, socialize };
|
|
5069
|
+
export { DEFAULT_ACCENT, DEFAULT_MENU, DISCLAIMER_LINES, ShellController as GameShell, HtmlRenderer, PACKAGE_VERSION, POPOVER, SCHEMES, SCROLL_THUMB_MIN, ShellController, createGameShell, createI18n, createShell, isPresetId, normalizeLang, placePopover, popoverWidth, rangeBounds, removeGameShell, resolveConfig, resolveMenu, resolveTheme, scrollEdge, scrollHint, seedMenuValues, socialize };
|
|
4806
5070
|
//# sourceMappingURL=html.esm.js.map
|