@energy8platform/shell 0.10.0 → 0.11.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 +280 -10
- package/dist/html.cjs.js.map +1 -1
- package/dist/html.d.ts +56 -4
- package/dist/html.esm.js +278 -11
- package/dist/html.esm.js.map +1 -1
- package/dist/index.cjs.js +81 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +56 -4
- package/dist/index.esm.js +79 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/pixi.cjs.js +314 -10
- package/dist/pixi.cjs.js.map +1 -1
- package/dist/pixi.d.ts +56 -4
- package/dist/pixi.esm.js +312 -11
- package/dist/pixi.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/core/index.ts +3 -0
- package/src/core/keyboard.ts +7 -1
- package/src/core/scrollHint.ts +91 -0
- package/src/core/state.ts +15 -0
- package/src/core/version.ts +1 -1
- package/src/ui/html/components/BottomBar.ts +2 -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/BottomBar.ts +9 -3
- 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.cjs.js
CHANGED
|
@@ -324,6 +324,20 @@ function nextTurbo(current, maxLevels) {
|
|
|
324
324
|
return 0;
|
|
325
325
|
return current >= maxLevels ? 0 : current + 1;
|
|
326
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Is a bonus buy unavailable right now?
|
|
329
|
+
*
|
|
330
|
+
* The three RUNTIME locks, in one place because they used to be in three: both bottom bars spelled
|
|
331
|
+
* them out for the coin, and the Shift+B hotkey spelled out a different, shorter set — so the
|
|
332
|
+
* keyboard opened the buy-bonus overlay mid-round and let a player stake a second bet on top of a
|
|
333
|
+
* round already in flight. A predicate the bars and the hotkey share cannot drift apart again.
|
|
334
|
+
*
|
|
335
|
+
* Runtime only. Whether the feature EXISTS at all is a config question (`features.buyBonus`), and
|
|
336
|
+
* whether this particular surface should offer it (mode, replay) belongs to the caller.
|
|
337
|
+
*/
|
|
338
|
+
function bonusBuyLocked(s) {
|
|
339
|
+
return s.busy || s.autoplay.active || !s.buyBonusEnabled;
|
|
340
|
+
}
|
|
327
341
|
|
|
328
342
|
/** The single brand accent (purple). The bar accent default (theme.ts) and the buy-a-bonus
|
|
329
343
|
* card default both derive from this, so a rebrand only touches one constant. */
|
|
@@ -1626,7 +1640,9 @@ class KeyboardController {
|
|
|
1626
1640
|
}
|
|
1627
1641
|
break;
|
|
1628
1642
|
case 'KeyB':
|
|
1629
|
-
|
|
1643
|
+
// `bonusBuyLocked` is the same predicate the bar's coin uses. Without it this hotkey
|
|
1644
|
+
// reached past a disabled coin and opened the overlay mid-round.
|
|
1645
|
+
if (h.buyBonusEnabled && s.mode === 'base' && !s.replay && !bonusBuyLocked(s)) {
|
|
1630
1646
|
h.openBuyBonus();
|
|
1631
1647
|
return;
|
|
1632
1648
|
}
|
|
@@ -1738,7 +1754,7 @@ function keyboardCapable(win = typeof window === 'undefined' ? undefined : windo
|
|
|
1738
1754
|
|
|
1739
1755
|
// AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
|
|
1740
1756
|
/** The @energy8platform/shell package version, stamped into the game-info footer. */
|
|
1741
|
-
const PACKAGE_VERSION = '0.
|
|
1757
|
+
const PACKAGE_VERSION = '0.11.1';
|
|
1742
1758
|
|
|
1743
1759
|
/** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
|
|
1744
1760
|
function resolveConfig(config) {
|
|
@@ -2224,6 +2240,66 @@ function placePopover(anchor, surface, size, pointer = null) {
|
|
|
2224
2240
|
return { x, y, maxH, arrowX, below };
|
|
2225
2241
|
}
|
|
2226
2242
|
|
|
2243
|
+
/**
|
|
2244
|
+
* What a scrollable region should ADVERTISE about itself.
|
|
2245
|
+
*
|
|
2246
|
+
* A slot in a Stake popout is 400×225. At that size the game-info overlay holds twelve screens of
|
|
2247
|
+
* content, the buy-bonus switches to a vertical card stack, and the menu popover hides its last
|
|
2248
|
+
* row — all of them scroll, and (before this module) none of them said so. macOS makes it worse:
|
|
2249
|
+
* overlay scrollbars stay invisible until something is already scrolling, so the very affordance a
|
|
2250
|
+
* player needs BEFORE they touch anything is the one the OS withholds. A certification reviewer
|
|
2251
|
+
* reads that as content the player can't reach.
|
|
2252
|
+
*
|
|
2253
|
+
* The maths lives here, apart from both renderers, for one reason: the DOM shell reads
|
|
2254
|
+
* `scrollTop`/`scrollHeight` while the Pixi shell tracks its own offset against a mask, and those
|
|
2255
|
+
* two must never disagree about whether a fade belongs at the bottom edge. Renderers decide how a
|
|
2256
|
+
* thumb LOOKS; this decides when there is one and where it sits.
|
|
2257
|
+
*/
|
|
2258
|
+
/** Fractions of a pixel are layout rounding, not reachable content. */
|
|
2259
|
+
const EPSILON = 1;
|
|
2260
|
+
/** Landing within half a pixel of an edge counts as arriving: a browser settles a flung scroll on
|
|
2261
|
+
* 199.5 of 200, and a fade left glowing over content the player has already reached reads as a
|
|
2262
|
+
* bug. */
|
|
2263
|
+
const EDGE_EPSILON = 0.5;
|
|
2264
|
+
/**
|
|
2265
|
+
* Shortest thumb we will draw, as a fraction of its track.
|
|
2266
|
+
*
|
|
2267
|
+
* The honest ratio for game info at Popout S is 8% — an 18px speck on a 226px track, which reads
|
|
2268
|
+
* as a rendering artifact rather than a scrollbar. Floored at 18% it stays recognisably a thumb,
|
|
2269
|
+
* and it still travels the whole track, so the position it reports remains truthful even though
|
|
2270
|
+
* its length no longer is. That is the right trade: length is decoration, position is information.
|
|
2271
|
+
*/
|
|
2272
|
+
const SCROLL_THUMB_MIN = 0.18;
|
|
2273
|
+
/** Resolve one axis of a scroll region into everything a renderer needs to draw its affordance. */
|
|
2274
|
+
function scrollHint(m) {
|
|
2275
|
+
const view = Math.max(0, m.clientHeight);
|
|
2276
|
+
const content = Math.max(0, m.scrollHeight);
|
|
2277
|
+
const maxScroll = Math.max(0, content - view);
|
|
2278
|
+
if (maxScroll <= EPSILON || view <= 0) {
|
|
2279
|
+
// Both edges are "the edge" when there is nowhere to go — callers gate every fade on the
|
|
2280
|
+
// matching flag, so a region that fits draws nothing without needing to check `overflowing`.
|
|
2281
|
+
return { overflowing: false, atStart: true, atEnd: true, thumbSize: 1, thumbOffset: 0, maxScroll: 0 };
|
|
2282
|
+
}
|
|
2283
|
+
const at = Math.max(0, Math.min(maxScroll, m.scrollTop));
|
|
2284
|
+
const thumbSize = Math.min(1, Math.max(SCROLL_THUMB_MIN, view / content));
|
|
2285
|
+
const progress = at / maxScroll;
|
|
2286
|
+
return {
|
|
2287
|
+
overflowing: true,
|
|
2288
|
+
atStart: at <= EDGE_EPSILON,
|
|
2289
|
+
atEnd: maxScroll - at <= EDGE_EPSILON,
|
|
2290
|
+
thumbSize,
|
|
2291
|
+
thumbOffset: progress * (1 - thumbSize),
|
|
2292
|
+
maxScroll,
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
function scrollEdge(h) {
|
|
2296
|
+
if (!h.overflowing)
|
|
2297
|
+
return 'none';
|
|
2298
|
+
if (h.atStart)
|
|
2299
|
+
return 'start';
|
|
2300
|
+
return h.atEnd ? 'end' : 'mid';
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2227
2303
|
const NO_INSET = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
2228
2304
|
/** Create a shell with an explicit renderer instance (custom or a built-in HtmlRenderer/PixiRenderer).
|
|
2229
2305
|
* Built-in renderers also have the createGameShell/createPixiShell sugar in /html and /pixi.
|
|
@@ -2427,6 +2503,68 @@ const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
2427
2503
|
transition:background .12s ease, color .12s ease; }
|
|
2428
2504
|
#${SHELL_ROOT_ID} .ge-ov-nav:hover { background:var(--shell-plaque-glass); color:var(--shell-accent); }
|
|
2429
2505
|
#${SHELL_ROOT_ID} .ge-ov-scroll { flex:1 1 auto; min-height:0; overflow-y:auto; overflow-x:hidden; }
|
|
2506
|
+
|
|
2507
|
+
/* ═══ scroll affordance — "there is more, and you can reach it" ═══════════════════════════════
|
|
2508
|
+
A Stake popout is 400×225. Game info holds twelve screens there, buy-bonus stacks its cards, and
|
|
2509
|
+
the menu popover hides its last row. All of them scrolled already; none of them said so, and
|
|
2510
|
+
Stake rejected the build for it. macOS is the aggravating factor — its overlay scrollbars stay
|
|
2511
|
+
invisible until something is ALREADY scrolling, so the one moment a player needs the hint is the
|
|
2512
|
+
one moment the OS withholds it. Styling the scrollbar at all opts out of that behaviour.
|
|
2513
|
+
|
|
2514
|
+
Three layers, all keyed off data-scroll (written by attachScrollAffordance):
|
|
2515
|
+
1. a persistent thumb — the standing "this scrolls" mark;
|
|
2516
|
+
2. a mask fade at whichever edge hides content — the mask applies to the element's own box, so
|
|
2517
|
+
it does NOT travel with the content the way a child gradient would;
|
|
2518
|
+
3. a chevron, shown only in the start state and retired for good on the first scroll.
|
|
2519
|
+
The end state deliberately gets no trailing fade: there is nothing left below to hint at. */
|
|
2520
|
+
/* Order matters here, and not for cascade reasons. Chromium honours the STANDARD scrollbar
|
|
2521
|
+
properties when they are present and then ignores ::-webkit-scrollbar entirely — and on macOS the
|
|
2522
|
+
standard properties leave the scrollbar in overlay mode, i.e. invisible until it is already
|
|
2523
|
+
moving. Defining ::-webkit-scrollbar with a width is what switches that scroller to a classic,
|
|
2524
|
+
always-painted, still-draggable scrollbar. So the standard properties are quarantined behind an
|
|
2525
|
+
@supports that only Firefox (which has no ::-webkit-scrollbar) satisfies. */
|
|
2526
|
+
@supports not selector(::-webkit-scrollbar) {
|
|
2527
|
+
#${SHELL_ROOT_ID} [data-scroll] { scrollbar-width:thin;
|
|
2528
|
+
scrollbar-color:var(--shell-scrollbar,rgba(255,255,255,.34)) transparent; } }
|
|
2529
|
+
#${SHELL_ROOT_ID} [data-scroll]::-webkit-scrollbar { width:6px; height:6px; }
|
|
2530
|
+
#${SHELL_ROOT_ID} [data-scroll]::-webkit-scrollbar-track { background:transparent; }
|
|
2531
|
+
#${SHELL_ROOT_ID} [data-scroll]::-webkit-scrollbar-thumb { border-radius:999px;
|
|
2532
|
+
background:var(--shell-scrollbar,rgba(255,255,255,.34)); }
|
|
2533
|
+
#${SHELL_ROOT_ID} [data-scroll]::-webkit-scrollbar-thumb:hover { background:var(--shell-accent); }
|
|
2534
|
+
/* Vertical fade. The stop pair is the same on both edges; only which edges are opaque changes.
|
|
2535
|
+
mask-composite-free on purpose — a single multi-stop gradient handles the two-edge case. */
|
|
2536
|
+
#${SHELL_ROOT_ID} [data-scroll="start"]:not([data-scroll-axis="x"]) {
|
|
2537
|
+
-webkit-mask-image:linear-gradient(to bottom, #000 calc(100% - 34px), transparent 100%);
|
|
2538
|
+
mask-image:linear-gradient(to bottom, #000 calc(100% - 34px), transparent 100%); }
|
|
2539
|
+
#${SHELL_ROOT_ID} [data-scroll="mid"]:not([data-scroll-axis="x"]) {
|
|
2540
|
+
-webkit-mask-image:linear-gradient(to bottom, transparent 0, #000 22px, #000 calc(100% - 34px), transparent 100%);
|
|
2541
|
+
mask-image:linear-gradient(to bottom, transparent 0, #000 22px, #000 calc(100% - 34px), transparent 100%); }
|
|
2542
|
+
#${SHELL_ROOT_ID} [data-scroll="end"]:not([data-scroll-axis="x"]) {
|
|
2543
|
+
-webkit-mask-image:linear-gradient(to bottom, transparent 0, #000 22px);
|
|
2544
|
+
mask-image:linear-gradient(to bottom, transparent 0, #000 22px); }
|
|
2545
|
+
/* Horizontal fade — the buy-bonus card strip. */
|
|
2546
|
+
#${SHELL_ROOT_ID} [data-scroll="start"][data-scroll-axis="x"] {
|
|
2547
|
+
-webkit-mask-image:linear-gradient(to right, #000 calc(100% - 30px), transparent 100%);
|
|
2548
|
+
mask-image:linear-gradient(to right, #000 calc(100% - 30px), transparent 100%); }
|
|
2549
|
+
#${SHELL_ROOT_ID} [data-scroll="mid"][data-scroll-axis="x"] {
|
|
2550
|
+
-webkit-mask-image:linear-gradient(to right, transparent 0, #000 20px, #000 calc(100% - 30px), transparent 100%);
|
|
2551
|
+
mask-image:linear-gradient(to right, transparent 0, #000 20px, #000 calc(100% - 30px), transparent 100%); }
|
|
2552
|
+
#${SHELL_ROOT_ID} [data-scroll="end"][data-scroll-axis="x"] {
|
|
2553
|
+
-webkit-mask-image:linear-gradient(to right, transparent 0, #000 20px);
|
|
2554
|
+
mask-image:linear-gradient(to right, transparent 0, #000 20px); }
|
|
2555
|
+
/* The chevron. Its host is whatever box CONTAINS the scroller (overlay root / popover card), so it
|
|
2556
|
+
holds still while the content moves under it. */
|
|
2557
|
+
#${SHELL_ROOT_ID} .ge-scroll-cue { position:absolute; left:0; top:0; z-index:2;
|
|
2558
|
+
width:22px; height:22px; padding:3px; box-sizing:border-box;
|
|
2559
|
+
display:flex; align-items:center; justify-content:center; pointer-events:none;
|
|
2560
|
+
border-radius:50%; color:#fff; background:var(--shell-plaque-dark);
|
|
2561
|
+
box-shadow:0 2px 10px rgba(0,0,0,.45); animation:ge-scroll-cue 1.6s ease-in-out infinite; }
|
|
2562
|
+
@keyframes ge-scroll-cue {
|
|
2563
|
+
0%,100% { transform:translateY(0); opacity:.85; }
|
|
2564
|
+
50% { transform:translateY(4px); opacity:1; } }
|
|
2565
|
+
/* A player who has asked for stillness still needs the hint — keep the chevron, drop the bob. */
|
|
2566
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2567
|
+
#${SHELL_ROOT_ID} .ge-scroll-cue { animation:none; opacity:.95; } }
|
|
2430
2568
|
#${SHELL_ROOT_ID} .ge-ov-body { max-width:800px; margin:0 auto; box-sizing:border-box;
|
|
2431
2569
|
padding:clamp(6px,2vh,16px) clamp(16px,4vw,24px) clamp(16px,4vh,28px); }
|
|
2432
2570
|
|
|
@@ -2788,7 +2926,9 @@ const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
2788
2926
|
card's font-size is the one knob (clamped for readability); everything inside is em-relative so
|
|
2789
2927
|
the whole card scales as a unit. GameShell.fitModal() still transform-scales it down as a
|
|
2790
2928
|
backstop for very short popouts. */
|
|
2791
|
-
|
|
2929
|
+
/* position:relative is here for the scroll cue: the card is the cue's containing block, so a
|
|
2930
|
+
capped, scrolling chip grid hints at its bottom edge rather than the screen's. */
|
|
2931
|
+
#${SHELL_ROOT_ID} .ge-modal-card { position:relative; font-size:clamp(11px, 2cqmin, 15px); width:100%; max-width:28em; box-sizing:border-box;
|
|
2792
2932
|
overflow:hidden; transform-origin:center center; background:var(--shell-plaque-solid); border-radius:1.3em;
|
|
2793
2933
|
display:flex; flex-direction:column; }
|
|
2794
2934
|
/* ✕ pinned to the overlay corner (the screen), not the card */
|
|
@@ -3221,7 +3361,114 @@ function applyBusy(host, bar) {
|
|
|
3221
3361
|
const buy = bar.querySelector('[data-ge="buybonus"]');
|
|
3222
3362
|
// disabled for the whole autoplay run (not just per-spin busy) so it doesn't flicker/pulse
|
|
3223
3363
|
if (buy)
|
|
3224
|
-
buy.disabled =
|
|
3364
|
+
buy.disabled = bonusBuyLocked(host.state);
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
/** Cue diameter, mirrored in the stylesheet. Kept here because the cue is positioned in JS. */
|
|
3368
|
+
const CUE_SIZE = 22;
|
|
3369
|
+
const CUE_GAP = 6;
|
|
3370
|
+
/** The chevron glyph, inline so it needs no icon-set entry and no font. */
|
|
3371
|
+
const CUE_SVG = '<svg viewBox="0 0 24 24" width="100%" height="100%" fill="none" stroke="currentColor" ' +
|
|
3372
|
+
'stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 9l7 7 7-7"/></svg>';
|
|
3373
|
+
function attachScrollAffordance(el, opts = {}) {
|
|
3374
|
+
const axis = opts.axis ?? 'y';
|
|
3375
|
+
let cue = null;
|
|
3376
|
+
// Once the player scrolls they have discovered the gesture; re-offering it every time they
|
|
3377
|
+
// return to the top would nag rather than inform.
|
|
3378
|
+
let cueRetired = false;
|
|
3379
|
+
let destroyed = false;
|
|
3380
|
+
if (axis === 'x')
|
|
3381
|
+
el.dataset.scrollAxis = 'x';
|
|
3382
|
+
const removeCue = () => {
|
|
3383
|
+
cue?.remove();
|
|
3384
|
+
cue = null;
|
|
3385
|
+
};
|
|
3386
|
+
const showCue = () => {
|
|
3387
|
+
if (cue || cueRetired || opts.cue === false)
|
|
3388
|
+
return;
|
|
3389
|
+
const host = opts.cueHost ?? el.parentElement;
|
|
3390
|
+
if (!host)
|
|
3391
|
+
return;
|
|
3392
|
+
cue = document.createElement('div');
|
|
3393
|
+
cue.className = 'ge-scroll-cue';
|
|
3394
|
+
cue.setAttribute('aria-hidden', 'true');
|
|
3395
|
+
cue.innerHTML = CUE_SVG;
|
|
3396
|
+
host.appendChild(cue);
|
|
3397
|
+
positionCue();
|
|
3398
|
+
};
|
|
3399
|
+
/** Pin the cue to the bottom of the SCROLLER, not of its host. The buy-bonus overlay hangs a bet
|
|
3400
|
+
* bar below its scroll region, and a cue pinned to the host's bottom edge lands on top of it. */
|
|
3401
|
+
const positionCue = () => {
|
|
3402
|
+
if (!cue)
|
|
3403
|
+
return;
|
|
3404
|
+
const size = cue.offsetWidth || CUE_SIZE;
|
|
3405
|
+
if (axis === 'x') {
|
|
3406
|
+
cue.style.left = `${el.offsetLeft + el.offsetWidth - size - CUE_GAP}px`;
|
|
3407
|
+
cue.style.top = `${el.offsetTop + (el.offsetHeight - size) / 2}px`;
|
|
3408
|
+
}
|
|
3409
|
+
else {
|
|
3410
|
+
cue.style.left = `${el.offsetLeft + (el.offsetWidth - size) / 2}px`;
|
|
3411
|
+
cue.style.top = `${el.offsetTop + el.offsetHeight - size - CUE_GAP}px`;
|
|
3412
|
+
}
|
|
3413
|
+
};
|
|
3414
|
+
const sync = () => {
|
|
3415
|
+
if (destroyed)
|
|
3416
|
+
return;
|
|
3417
|
+
const h = axis === 'x'
|
|
3418
|
+
? scrollHint({ scrollTop: el.scrollLeft, scrollHeight: el.scrollWidth, clientHeight: el.clientWidth })
|
|
3419
|
+
: scrollHint({ scrollTop: el.scrollTop, scrollHeight: el.scrollHeight, clientHeight: el.clientHeight });
|
|
3420
|
+
const edge = scrollEdge(h);
|
|
3421
|
+
if (edge === 'none') {
|
|
3422
|
+
delete el.dataset.scroll;
|
|
3423
|
+
removeCue();
|
|
3424
|
+
return;
|
|
3425
|
+
}
|
|
3426
|
+
el.dataset.scroll = edge;
|
|
3427
|
+
if (edge === 'start') {
|
|
3428
|
+
showCue();
|
|
3429
|
+
positionCue(); // the scroller can move/resize under a cue that is already up
|
|
3430
|
+
}
|
|
3431
|
+
else {
|
|
3432
|
+
removeCue();
|
|
3433
|
+
}
|
|
3434
|
+
};
|
|
3435
|
+
// Retirement is keyed off actual MOVEMENT, not off the event. A browser also fires `scroll` when
|
|
3436
|
+
// content reflows under a pinned offset, and a hint dismissed by a reflow the player never caused
|
|
3437
|
+
// is a hint they never saw.
|
|
3438
|
+
let lastPos = axis === 'x' ? el.scrollLeft : el.scrollTop;
|
|
3439
|
+
const onScroll = () => {
|
|
3440
|
+
const pos = axis === 'x' ? el.scrollLeft : el.scrollTop;
|
|
3441
|
+
if (pos !== lastPos) {
|
|
3442
|
+
lastPos = pos;
|
|
3443
|
+
cueRetired = true;
|
|
3444
|
+
removeCue();
|
|
3445
|
+
}
|
|
3446
|
+
sync();
|
|
3447
|
+
};
|
|
3448
|
+
el.addEventListener('scroll', onScroll, { passive: true });
|
|
3449
|
+
// Content in these regions is built asynchronously (fonts, images, a rebuilt body on resize), so
|
|
3450
|
+
// a single sync at mount would measure the wrong thing. ResizeObserver is absent in jsdom.
|
|
3451
|
+
const RO = globalThis.ResizeObserver;
|
|
3452
|
+
const ro = typeof RO === 'function' ? new RO(() => sync()) : null;
|
|
3453
|
+
if (ro) {
|
|
3454
|
+
ro.observe(el);
|
|
3455
|
+
if (el.firstElementChild)
|
|
3456
|
+
ro.observe(el.firstElementChild);
|
|
3457
|
+
}
|
|
3458
|
+
sync();
|
|
3459
|
+
return {
|
|
3460
|
+
sync,
|
|
3461
|
+
destroy() {
|
|
3462
|
+
if (destroyed)
|
|
3463
|
+
return;
|
|
3464
|
+
destroyed = true;
|
|
3465
|
+
el.removeEventListener('scroll', onScroll);
|
|
3466
|
+
ro?.disconnect();
|
|
3467
|
+
delete el.dataset.scroll;
|
|
3468
|
+
delete el.dataset.scrollAxis;
|
|
3469
|
+
removeCue();
|
|
3470
|
+
},
|
|
3471
|
+
};
|
|
3225
3472
|
}
|
|
3226
3473
|
|
|
3227
3474
|
/** A centred CARD modal — frosted backdrop + opaque card with an accent title heading and an
|
|
@@ -3258,8 +3505,9 @@ function createCardModal(opts) {
|
|
|
3258
3505
|
}
|
|
3259
3506
|
return { root, card, body };
|
|
3260
3507
|
}
|
|
3261
|
-
/** Full-screen overlay. Returns { root, body, scroll }; append content to body.
|
|
3262
|
-
* The `scroll` element is the scrollable container (overflow-y: auto)
|
|
3508
|
+
/** Full-screen overlay. Returns { root, body, scroll, affordance }; append content to body.
|
|
3509
|
+
* The `scroll` element is the scrollable container (overflow-y: auto); `affordance` marks it as
|
|
3510
|
+
* scrollable once it overflows — call `affordance.sync()` after filling or resizing the body. */
|
|
3263
3511
|
function createOverlay(opts) {
|
|
3264
3512
|
const root = document.createElement('div');
|
|
3265
3513
|
root.className = 'ge-shell-overlay';
|
|
@@ -3297,7 +3545,9 @@ function createOverlay(opts) {
|
|
|
3297
3545
|
body.className = 'ge-ov-body';
|
|
3298
3546
|
scroll.appendChild(body);
|
|
3299
3547
|
root.append(head, scroll);
|
|
3300
|
-
|
|
3548
|
+
// The cue is hosted on `root`, not on `scroll`: `scroll` is the element that moves.
|
|
3549
|
+
const affordance = attachScrollAffordance(scroll, { cueHost: root });
|
|
3550
|
+
return { root, body, scroll, affordance };
|
|
3301
3551
|
}
|
|
3302
3552
|
/** A light-dismiss popover: a transparent full-surface layer (closes on pointerdown) holding a
|
|
3303
3553
|
* card with an arrow that points at `pointer`. Append rows to `body`; call `position()` after the
|
|
@@ -3318,6 +3568,9 @@ function createPopover(opts) {
|
|
|
3318
3568
|
// Clicks inside the card must not reach the dismiss layer.
|
|
3319
3569
|
card.addEventListener('pointerdown', (e) => e.stopPropagation());
|
|
3320
3570
|
root.addEventListener('pointerdown', opts.onClose);
|
|
3571
|
+
// The card clamps itself to `maxHeight` in position(), so the body's overflow is only knowable
|
|
3572
|
+
// after that runs — hence the sync at the end of position().
|
|
3573
|
+
const affordance = attachScrollAffordance(body, { cueHost: card });
|
|
3321
3574
|
const resolveEl = (v) => typeof v === 'function' ? v() : (v ?? null);
|
|
3322
3575
|
/** A rect in surface coordinates, or null when unresolved/fully zero-sized (a zero-HEIGHT rect —
|
|
3323
3576
|
* e.g. a not-yet-laid-out anchor — is still considered valid, matching placePopover's own rule). */
|
|
@@ -3393,8 +3646,9 @@ function createPopover(opts) {
|
|
|
3393
3646
|
arrow.style.display = '';
|
|
3394
3647
|
arrow.style.left = `${p.arrowX / s}px`;
|
|
3395
3648
|
}
|
|
3649
|
+
affordance.sync();
|
|
3396
3650
|
};
|
|
3397
|
-
return { root, card, body, position };
|
|
3651
|
+
return { root, card, body, affordance, position };
|
|
3398
3652
|
}
|
|
3399
3653
|
|
|
3400
3654
|
/** The bar menu, as a light-dismiss popover anchored to the burger. Rows come from the core model,
|
|
@@ -3545,7 +3799,7 @@ function buildRow(host, row, updaters, reposition) {
|
|
|
3545
3799
|
const HOTKEYS_DEFAULT_ORDER = -0.5;
|
|
3546
3800
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
3547
3801
|
function openGameInfoModal(host) {
|
|
3548
|
-
const { root, body, scroll } = createOverlay({
|
|
3802
|
+
const { root, body, scroll, affordance } = createOverlay({
|
|
3549
3803
|
title: host.t('Game info'),
|
|
3550
3804
|
onClose: () => host.actions.closeOverlay(),
|
|
3551
3805
|
onBack: () => { root.remove(); host.actions.openMenu(); },
|
|
@@ -3571,6 +3825,8 @@ function openGameInfoModal(host) {
|
|
|
3571
3825
|
.sort((a, b) => a.k - b.k || a.i - b.i)
|
|
3572
3826
|
.forEach(({ s }) => body.appendChild(renderSection(host, s)));
|
|
3573
3827
|
body.appendChild(versionFooter(host));
|
|
3828
|
+
// The body is filled after createOverlay returned, so its first honest measurement is here.
|
|
3829
|
+
affordance.sync();
|
|
3574
3830
|
const LINE = 60;
|
|
3575
3831
|
const PAGE = () => Math.floor(scroll.clientHeight * 0.9) || Math.floor(540 * 0.9);
|
|
3576
3832
|
const onKey = (e) => {
|
|
@@ -3963,10 +4219,13 @@ function openBuyBonusOverlay(host) {
|
|
|
3963
4219
|
if (bonuses === false || bonuses.length === 0)
|
|
3964
4220
|
return null;
|
|
3965
4221
|
const st = { focusIndex: -1, confirmBonus: undefined };
|
|
3966
|
-
const { root, body } = createOverlay({ title: host.t('Buy bonus'), onClose: () => host.actions.closeOverlay() });
|
|
4222
|
+
const { root, body, affordance } = createOverlay({ title: host.t('Buy bonus'), onClose: () => host.actions.closeOverlay() });
|
|
3967
4223
|
root.dataset.ge = 'buybonus-overlay';
|
|
4224
|
+
// The strip's own X-scroll affordance, rebuilt with the grid it describes.
|
|
4225
|
+
let gridAffordance = null;
|
|
3968
4226
|
// Re-render the grid whenever the bet changes so every card's price stays live.
|
|
3969
4227
|
const renderGrid = () => {
|
|
4228
|
+
gridAffordance?.destroy();
|
|
3970
4229
|
body.innerHTML = '';
|
|
3971
4230
|
const grid = document.createElement('div');
|
|
3972
4231
|
grid.className = 'ge-bb-grid';
|
|
@@ -3992,6 +4251,11 @@ function openBuyBonusOverlay(host) {
|
|
|
3992
4251
|
else {
|
|
3993
4252
|
st.focusIndex = -1;
|
|
3994
4253
|
}
|
|
4254
|
+
// Two axes, two affordances. Below a ~340px frame the CSS stacks the cards and the OVERLAY
|
|
4255
|
+
// scrolls vertically (see the ge-bb-frame container query); above it the STRIP scrolls
|
|
4256
|
+
// horizontally. Each is attached unconditionally and stays silent on the axis that fits.
|
|
4257
|
+
gridAffordance = attachScrollAffordance(grid, { axis: 'x', cue: false });
|
|
4258
|
+
affordance.sync();
|
|
3995
4259
|
};
|
|
3996
4260
|
renderGrid();
|
|
3997
4261
|
root.appendChild(buildBetBar(host, renderGrid)); // thin bottom footer, only as tall as the pill
|
|
@@ -4322,6 +4586,9 @@ function buildSheet(opts) {
|
|
|
4322
4586
|
grid.appendChild(chip);
|
|
4323
4587
|
}
|
|
4324
4588
|
ui.body.appendChild(grid);
|
|
4589
|
+
// A long bet ladder is capped at 50vh and scrolls; in a 225px popout that cap bites after two
|
|
4590
|
+
// rows. The cue hangs off the card, which holds still while the grid moves.
|
|
4591
|
+
attachScrollAffordance(grid, { cueHost: ui.card });
|
|
4325
4592
|
function doConfirm() {
|
|
4326
4593
|
opts.onConfirm(selected);
|
|
4327
4594
|
opts.onClose();
|
|
@@ -4825,6 +5092,7 @@ exports.HtmlRenderer = HtmlRenderer;
|
|
|
4825
5092
|
exports.PACKAGE_VERSION = PACKAGE_VERSION;
|
|
4826
5093
|
exports.POPOVER = POPOVER;
|
|
4827
5094
|
exports.SCHEMES = SCHEMES;
|
|
5095
|
+
exports.SCROLL_THUMB_MIN = SCROLL_THUMB_MIN;
|
|
4828
5096
|
exports.ShellController = ShellController;
|
|
4829
5097
|
exports.createGameShell = createGameShell;
|
|
4830
5098
|
exports.createI18n = createI18n;
|
|
@@ -4838,6 +5106,8 @@ exports.removeGameShell = removeGameShell;
|
|
|
4838
5106
|
exports.resolveConfig = resolveConfig;
|
|
4839
5107
|
exports.resolveMenu = resolveMenu;
|
|
4840
5108
|
exports.resolveTheme = resolveTheme;
|
|
5109
|
+
exports.scrollEdge = scrollEdge;
|
|
5110
|
+
exports.scrollHint = scrollHint;
|
|
4841
5111
|
exports.seedMenuValues = seedMenuValues;
|
|
4842
5112
|
exports.socialize = socialize;
|
|
4843
5113
|
//# sourceMappingURL=html.cjs.js.map
|