@tokenoftrust/storefront-runner 2.2.9 → 2.2.10

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.
@@ -322,7 +322,7 @@ a.article-card__tag:hover { text-decoration: underline; }
322
322
  .footer-brand .brand .b-prime,
323
323
  .footer-brand .brand .b-vapor,
324
324
  .footer-brand .brand .b-club { color: var(--color-primary-contrast); }
325
- .footer-col h4 { color: var(--color-primary-contrast); font-size: .82rem; letter-spacing: .1em; text-transform: uppercase; margin-bottom: 14px; }
325
+ .footer-col .footer-col-title { color: var(--color-primary-contrast); font-size: .82rem; font-weight: bold; letter-spacing: .1em; text-transform: uppercase; margin: 0 0 14px; }
326
326
  .footer-col ul { display: flex; flex-direction: column; gap: 9px; }
327
327
  .footer-col a { color: color-mix(in srgb, var(--color-primary-contrast) 78%, transparent); font-size: .92rem; }
328
328
  .footer-col a:hover { color: var(--color-primary-contrast); text-decoration: underline; }
@@ -480,7 +480,7 @@ function renderFooter(footer: ChromeFooter): string {
480
480
  .join("");
481
481
  return collapsible
482
482
  ? ` <div class="footer-col"><details>\n <summary>${esc(c.title)}</summary>\n <ul>${links}</ul>\n </details></div>`
483
- : ` <div class="footer-col">\n <h4>${esc(c.title)}</h4>\n <ul>${links}</ul>\n </div>`;
483
+ : ` <div class="footer-col">\n <p class="footer-col-title">${esc(c.title)}</p>\n <ul>${links}</ul>\n </div>`;
484
484
  })
485
485
  .join("\n");
486
486
  const trust = footer.trustMarks?.length
@@ -212,9 +212,9 @@ const componentVarsInline = Object.entries(componentVars)
212
212
  </div>
213
213
  <div class="pay-badges"><span>Visa</span><span>Mastercard</span><span>Amex</span><span>PayPal</span></div>
214
214
  </div>
215
- <div class="footer-col"><h4>Shop</h4><ul><li><a href="#">Disposables</a></li><li><a href="#">E-Liquid</a></li><li><a href="#">Devices</a></li></ul></div>
216
- <div class="footer-col"><h4>Support</h4><ul><li><a href="#">Contact</a></li><li><a href="#">Shipping</a></li><li><a href="#">Returns</a></li></ul></div>
217
- <div class="footer-col"><h4>Company</h4><ul><li><a href="#">About</a></li><li><a href="#">Blog</a></li></ul></div>
215
+ <div class="footer-col"><p class="footer-col-title">Shop</p><ul><li><a href="#">Disposables</a></li><li><a href="#">E-Liquid</a></li><li><a href="#">Devices</a></li></ul></div>
216
+ <div class="footer-col"><p class="footer-col-title">Support</p><ul><li><a href="#">Contact</a></li><li><a href="#">Shipping</a></li><li><a href="#">Returns</a></li></ul></div>
217
+ <div class="footer-col"><p class="footer-col-title">Company</p><ul><li><a href="#">About</a></li><li><a href="#">Blog</a></li></ul></div>
218
218
  </div>
219
219
  <div class="trust-marks"><span>🔒 Secure checkout</span><span>✓ Age-verified</span><span>★ 4.9 / 5</span></div>
220
220
  <div class="footer-warning"><strong>WARNING:</strong><p>This product contains nicotine. Nicotine is an addictive chemical.</p></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.2.9",
3
+ "version": "2.2.10",
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",
@@ -0,0 +1,131 @@
1
+ /**
2
+ * When the GA4 sandbox iframe may mount.
3
+ *
4
+ * The frame is same-process: once it loads, gtag.js runs on the page main
5
+ * thread. Mounting it on the first idle turn after `load` puts those long
6
+ * tasks inside the window Lighthouse scores as Total Blocking Time. This
7
+ * scheduler waits for the first of:
8
+ *
9
+ * - a real pointer or key input (`pointerdown` / `keydown`, capture), or
10
+ * - `load`, then a fixed delay long enough to outlast that quiet window.
11
+ *
12
+ * The delay is a minimum. Input before `load` still mounts immediately.
13
+ * The callback never runs synchronously on the call. Cancel is idempotent
14
+ * and drops both the listeners and the timer. Commands sent before the
15
+ * frame exists stay queued by the transport, not by this scheduler.
16
+ */
17
+
18
+ /** How long after `load` the sandbox waits when nobody has interacted. */
19
+ export const GA4_SANDBOX_POST_LOAD_DELAY_MS = 5_000;
20
+
21
+ /** The narrow slice of `window` this scheduler depends on. */
22
+ export interface Ga4SandboxScheduleWindow {
23
+ document?: {
24
+ readyState?: string;
25
+ addEventListener?(
26
+ type: string,
27
+ listener: (event: unknown) => void,
28
+ options?: { capture?: boolean },
29
+ ): void;
30
+ removeEventListener?(
31
+ type: string,
32
+ listener: (event: unknown) => void,
33
+ options?: { capture?: boolean },
34
+ ): void;
35
+ };
36
+ addEventListener(type: "load", listener: () => void, options?: { once?: boolean }): void;
37
+ removeEventListener?(type: "load", listener: () => void): void;
38
+ setTimeout(callback: () => void, ms: number): unknown;
39
+ clearTimeout(handle: unknown): void;
40
+ }
41
+
42
+ export interface Ga4SandboxScheduleOptions {
43
+ /** Minimum wait after `load` before the sandbox mounts without input. */
44
+ postLoadDelayMs?: number;
45
+ }
46
+
47
+ export interface Ga4SandboxScheduleHandle {
48
+ /** Idempotent. Prevents the callback from running if it has not run yet. */
49
+ cancel(): void;
50
+ }
51
+
52
+ const INPUT_EVENTS = ["pointerdown", "keydown"] as const;
53
+ const INPUT_CAPTURE = { capture: true } as const;
54
+
55
+ /**
56
+ * Schedule `callback` for the first input, or for `postLoadDelayMs` after
57
+ * `load`, whichever comes first. Never runs on the call itself.
58
+ */
59
+ export function scheduleGa4Sandbox(
60
+ win: Ga4SandboxScheduleWindow,
61
+ callback: () => void,
62
+ options: Ga4SandboxScheduleOptions = {},
63
+ ): Ga4SandboxScheduleHandle {
64
+ const delay = options.postLoadDelayMs ?? GA4_SANDBOX_POST_LOAD_DELAY_MS;
65
+ let cancelled = false;
66
+ let ran = false;
67
+ let timeoutHandle: unknown;
68
+ let loadListener: (() => void) | undefined;
69
+ const inputTarget = win.document;
70
+ const addInput = inputTarget?.addEventListener;
71
+ const removeInput = inputTarget?.removeEventListener;
72
+
73
+ const detachInput = (): void => {
74
+ if (typeof removeInput !== "function") return;
75
+ for (const type of INPUT_EVENTS) removeInput.call(inputTarget, type, onInput, INPUT_CAPTURE);
76
+ };
77
+
78
+ const finish = (): void => {
79
+ if (cancelled || ran) return;
80
+ ran = true;
81
+ detachInput();
82
+ if (loadListener) {
83
+ win.removeEventListener?.("load", loadListener);
84
+ loadListener = undefined;
85
+ }
86
+ if (timeoutHandle !== undefined) {
87
+ win.clearTimeout(timeoutHandle);
88
+ timeoutHandle = undefined;
89
+ }
90
+ callback();
91
+ };
92
+
93
+ function onInput(): void {
94
+ finish();
95
+ }
96
+
97
+ if (typeof addInput === "function") {
98
+ for (const type of INPUT_EVENTS) addInput.call(inputTarget, type, onInput, INPUT_CAPTURE);
99
+ }
100
+
101
+ const armDelay = (): void => {
102
+ if (cancelled || ran) return;
103
+ timeoutHandle = win.setTimeout(() => {
104
+ timeoutHandle = undefined;
105
+ finish();
106
+ }, delay);
107
+ };
108
+
109
+ if (win.document?.readyState === "complete") {
110
+ armDelay();
111
+ } else {
112
+ loadListener = () => {
113
+ loadListener = undefined;
114
+ armDelay();
115
+ };
116
+ win.addEventListener("load", loadListener, { once: true });
117
+ }
118
+
119
+ return {
120
+ cancel() {
121
+ if (cancelled || ran) {
122
+ cancelled = true;
123
+ return;
124
+ }
125
+ cancelled = true;
126
+ detachInput();
127
+ if (loadListener) win.removeEventListener?.("load", loadListener);
128
+ if (timeoutHandle !== undefined) win.clearTimeout(timeoutHandle);
129
+ },
130
+ };
131
+ }
@@ -49,6 +49,7 @@ import {
49
49
  type NativeGa4SandboxScheduler,
50
50
  } from "./native-ga4-sandbox.js";
51
51
  import { schedulePostPaint, type PostPaintWindow } from "./post-paint.js";
52
+ import { scheduleGa4Sandbox, type Ga4SandboxScheduleWindow } from "./ga4-sandbox-schedule.js";
52
53
  import { ManagedAnalyticsEventBrokerBridge } from "./managed-analytics-bridge.js";
53
54
  import { minimizePageAttribution, type ManagedPageViewInput } from "./managed-analytics.js";
54
55
  import { managedAnalyticsOpaqueRevision } from "./managed-analytics-value-safety.js";
@@ -124,14 +125,19 @@ export interface BootLifecycleOptions {
124
125
  /** Reviewed isolated-origin GA4 transport; injectable for DOM-free tests. */
125
126
  nativeGa4Transport?: NativeGa4Transport;
126
127
  /**
127
- * Defers non-essential platform-adapter work (the GA4 sandbox iframe mount and
128
- * the Amplitude HTTP capability) until after first paint and idle — see
129
- * `post-paint.ts`. Defaults to the real post-paint scheduler over `window`
130
- * (a guarded synchronous no-op off the browser). Injectable so a test can
131
- * assert nothing runs before the scheduler fires with a fake, controllable
132
- * scheduler instead of real paint/idle timing.
128
+ * Defers the Amplitude HTTP capability until after first paint and idle —
129
+ * see `post-paint.ts`. Defaults to that scheduler over `window` (synchronous
130
+ * off the browser). Injectable so a test can hold the flush.
133
131
  */
134
132
  schedulePlatformAdapterStart?: NativeGa4SandboxScheduler;
133
+ /**
134
+ * Mounts the GA4 sandbox iframe on the first pointer or key input, or a
135
+ * fixed delay after `load`, whichever comes first — see
136
+ * `ga4-sandbox-schedule.ts`. Defaults to that scheduler over `window`
137
+ * (synchronous off the browser). Commands sent before the frame exists stay
138
+ * queued in the transport. Injectable so a test can hold the mount.
139
+ */
140
+ scheduleGa4Sandbox?: NativeGa4SandboxScheduler;
135
141
  }
136
142
 
137
143
  /**
@@ -328,7 +334,8 @@ function defaultConsentGate(
328
334
  /**
329
335
  * The default `schedulePlatformAdapterStart`: the real post-paint scheduler over the
330
336
  * global `window`, or a synchronous immediate start off the browser (SSR, tests that
331
- * import this module without a DOM) — never a hang.
337
+ * import this module without a DOM) — never a hang. Amplitude uses this. The GA4
338
+ * sandbox does not.
332
339
  */
333
340
  function defaultPlatformAdapterScheduler(start: () => void): { cancel(): void } {
334
341
  if (typeof window === "undefined") {
@@ -338,12 +345,24 @@ function defaultPlatformAdapterScheduler(start: () => void): { cancel(): void }
338
345
  return schedulePostPaint(window as unknown as PostPaintWindow, start);
339
346
  }
340
347
 
348
+ /**
349
+ * The default GA4 sandbox scheduler. Off the browser it starts synchronously so
350
+ * SSR and DOM-free tests do not hang. In the browser it never starts on this
351
+ * turn: first input, or the post-load delay, whichever comes first.
352
+ */
353
+ function defaultGa4SandboxScheduler(start: () => void): { cancel(): void } {
354
+ if (typeof window === "undefined") {
355
+ start();
356
+ return { cancel() {} };
357
+ }
358
+ return scheduleGa4Sandbox(window as unknown as Ga4SandboxScheduleWindow, start);
359
+ }
360
+
341
361
  /**
342
362
  * Wrap a `fetch` capability so calls queue instead of running immediately, and are
343
- * all issued together once `schedule` fires. This is what keeps the Amplitude
344
- * adapter's HTTP collection off the pre-paint critical path exactly like the GA4
345
- * sandbox's deferred iframe mount, without touching `native-amplitude.ts` (which
346
- * stays paint-timing-agnostic — it just calls the `fetch` capability it was given).
363
+ * all issued together once `schedule` fires. This keeps the Amplitude adapter's
364
+ * HTTP collection off the pre-paint critical path. `native-amplitude.ts` stays
365
+ * paint-timing-agnostic — it just calls the `fetch` capability it was given.
347
366
  */
348
367
  function deferFetchCapability(
349
368
  fetchCapability: typeof fetch,
@@ -395,6 +414,7 @@ export function bootIntegrationLifecycle(
395
414
  (typeof win.fetch === "function" ? win.fetch.bind(win) : undefined);
396
415
  const schedulePlatformAdapterStart =
397
416
  options.schedulePlatformAdapterStart ?? defaultPlatformAdapterScheduler;
417
+ const scheduleGa4SandboxStart = options.scheduleGa4Sandbox ?? defaultGa4SandboxScheduler;
398
418
  const fallbackLoader = options.importModule
399
419
  ? makeSameOriginEntryLoader(options.importModule)
400
420
  : sameOriginEntryLoader;
@@ -419,7 +439,7 @@ export function bootIntegrationLifecycle(
419
439
  return createNativeGa4SandboxTransport(
420
440
  createBrowserNativeGa4SandboxHost(document, MessageChannel),
421
441
  resolveNativeGa4SandboxUrl(win.location.href),
422
- schedulePlatformAdapterStart,
442
+ scheduleGa4SandboxStart,
423
443
  );
424
444
  },
425
445
  });
@@ -111,10 +111,11 @@ const IMMEDIATE_SCHEDULER: NativeGa4SandboxScheduler = (start) => {
111
111
  * needs a real same-site origin. The exact origin, iframe window, and one-shot transfer
112
112
  * are the authority boundary. No general parent `message` listener is installed.
113
113
  *
114
- * The frame is not created or appended until `schedule` fires (default: immediately,
115
- * i.e. unchanged from before) — that append is what starts gtag.js loading, so gating
116
- * it behind a paint-aware scheduler (see `post-paint.ts`) keeps it off the LCP path.
117
- * `configure`/`send` calls before the frame mounts queue exactly as they always have.
114
+ * The frame is not created or appended until `schedule` fires. Boot passes the
115
+ * GA4 sandbox schedule (`ga4-sandbox-schedule.ts`); a caller that omits
116
+ * `schedule` still mounts immediately. That append is what starts gtag.js.
117
+ * `configure`/`send` before the frame mounts stay queued and flush in order
118
+ * when the frame loads.
118
119
  */
119
120
  export function createNativeGa4SandboxTransport(
120
121
  host: NativeGa4SandboxHost,
@@ -1,19 +1,18 @@
1
1
  /**
2
- * Paint-aware scheduling for non-essential platform work (ext-kernel-05 follow-on).
3
- *
4
- * Nothing in the lifecycle boot chain waited for a real paint before starting
5
- * heavy adapter transports (GA4's isolated-origin iframe, Amplitude's HTTP
6
- * collection), so Lighthouse's simulated dependency graph charged their bytes
7
- * and main-thread time straight to LCP. This scheduler defers a callback until
8
- * AFTER the document has painted and the main thread is otherwise idle:
2
+ * Paint-aware scheduling for platform adapter work that starts once the
3
+ * document has painted and the main thread is idle (Amplitude's HTTP
4
+ * collection). The GA4 sandbox uses `ga4-sandbox-schedule.ts` instead: its
5
+ * iframe shares the page process, so an idle start still runs gtag.js inside
6
+ * the Total Blocking Time window.
9
7
  *
10
8
  * - if the document has already finished loading, schedule on idle immediately;
11
9
  * - otherwise wait for `load`, then schedule on idle;
12
10
  * - "idle" is `requestIdleCallback` with a bounded timeout, falling back to a
13
11
  * macrotask (`setTimeout(0)`) where `requestIdleCallback` does not exist.
14
12
  *
15
- * Cancellable at every stage (before `load`, and while idle-queued), so a
16
- * document torn down before the deferred work starts never runs it.
13
+ * The idle timeout is a maximum, not a minimum. Cancellable at every stage
14
+ * (before `load`, and while idle-queued), so a document torn down before the
15
+ * deferred work starts never runs it.
17
16
  */
18
17
 
19
18
  /** The narrow slice of `window` this scheduler depends on — structural, edge-safe. */