@tokenoftrust/storefront-runner 2.4.36 → 2.4.37

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.
@@ -375,17 +375,20 @@ export default function CheckoutComplianceGate({ preflightUrl, minAge }: Props)
375
375
 
376
376
  return (
377
377
  <div
378
- class="fixed inset-0 z-[110] grid place-items-center bg-[color-mix(in_srgb,var(--color-text)_75%,transparent)] p-5 backdrop-blur-sm"
378
+ class="fixed inset-0 z-[110] bg-[color-mix(in_srgb,var(--color-text)_75%,transparent)] backdrop-blur-sm"
379
379
  role="dialog"
380
380
  aria-modal="true"
381
381
  aria-labelledby="cgate-title"
382
382
  onClick={(e) => {
383
- if (e.target === e.currentTarget) close();
383
+ if (e.target === e.currentTarget || (e.target as HTMLElement).hasAttribute("data-viewport-frame")) close();
384
384
  }}
385
385
  >
386
+ {/* The fixed box is the LAYOUT viewport, which content wider than a phone
387
+ stretches; place the dialog in a frame sized to what the visitor sees. */}
388
+ <div data-viewport-frame class="absolute left-0 top-0 flex h-[100svh] w-[100vw] overflow-y-auto p-5">
386
389
  <div
387
390
  ref={dialogRef}
388
- class="w-[min(32rem,100%)] rounded-[var(--radius-lg)] bg-[var(--color-surface)] p-7 text-left shadow-[var(--shadow-lg)]"
391
+ class="m-auto w-[min(32rem,100%)] rounded-[var(--radius-lg)] bg-[var(--color-surface)] p-7 text-left shadow-[var(--shadow-lg)]"
389
392
  >
390
393
  <p class="eyebrow eyebrow--tick mb-3 w-fit">Compliance check</p>
391
394
  <h2 id="cgate-title" class="display-sm">
@@ -606,6 +609,7 @@ export default function CheckoutComplianceGate({ preflightUrl, minAge }: Props)
606
609
  </button>
607
610
  </div>
608
611
  </div>
612
+ </div>
609
613
  </div>
610
614
  );
611
615
  }
@@ -118,6 +118,7 @@ export function isolateAgeGateBackground(host: HTMLElement): BackgroundIsolation
118
118
  }
119
119
 
120
120
  const HOST_MARKER = "data-tot-compliance";
121
+ const HOST_MOVES_PER_TASK = 20;
121
122
  let mountedHost: HTMLElement | null = null;
122
123
  const HOST_STYLE_FLOOR = {
123
124
  all: "initial",
@@ -221,6 +222,25 @@ export function mountIsolatedAgeGate({ minAge, brand, tenantId, presentation }:
221
222
  // writes exactly what this observer watches, so it runs disconnected: a repair
222
223
  // that is not perfectly idempotent can never re-trigger itself and spin the
223
224
  // main thread in an unbounded microtask loop.
225
+ //
226
+ // Another script that also keeps itself last in <body> would answer every move
227
+ // with one of its own, and two observers can ping-pong forever without either
228
+ // re-triggering itself. Moves are budgeted per task; past the budget the repair
229
+ // yields to the event loop and resumes on the next task.
230
+ let active = true;
231
+ let moves = 0;
232
+ let resumeScheduled = false;
233
+ const spendMove = () => {
234
+ if (!resumeScheduled) {
235
+ resumeScheduled = true;
236
+ setTimeout(() => {
237
+ resumeScheduled = false;
238
+ moves = 0;
239
+ if (active) ensure();
240
+ }, 0);
241
+ }
242
+ return ++moves <= HOST_MOVES_PER_TASK;
243
+ };
224
244
  const observeHost = () => {
225
245
  observer.observe(document.body, { childList: true });
226
246
  observer.observe(host, {
@@ -231,7 +251,7 @@ export function mountIsolatedAgeGate({ minAge, brand, tenantId, presentation }:
231
251
  const ensure = () => {
232
252
  observer.disconnect();
233
253
  try {
234
- if (!host.isConnected || document.body.lastElementChild !== host) {
254
+ if ((!host.isConnected || document.body.lastElementChild !== host) && spendMove()) {
235
255
  // Moving the host blurs whatever the gate had focused; give it back so a
236
256
  // keyboard visitor is never stranded by a script appending to <body>.
237
257
  const focused = shadow.activeElement as HTMLElement | null;
@@ -249,6 +269,7 @@ export function mountIsolatedAgeGate({ minAge, brand, tenantId, presentation }:
249
269
  observeHost();
250
270
 
251
271
  return () => {
272
+ active = false;
252
273
  observer.disconnect();
253
274
  stylesheetObserver.disconnect();
254
275
  backgroundIsolation?.release();
@@ -167,7 +167,9 @@ export default function MobileNav({ nav, announcement, currentPath }: Props) {
167
167
 
168
168
  {open && (
169
169
  <div
170
- class="fixed inset-0 z-50"
170
+ // Sized to what the visitor sees, not `inset-0`: on a phone the fixed box is the
171
+ // layout viewport, which wide page content stretches past the screen's edge.
172
+ class="fixed left-0 top-0 z-50 h-[100dvh] w-[100vw]"
171
173
  // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
172
174
  onKeyDown={onKeyDown}
173
175
  >
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.4.36",
3
+ "version": "2.4.37",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",