@tokenoftrust/storefront-runner 2.2.78 → 2.2.80
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.80",
|
|
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",
|
|
@@ -797,6 +797,49 @@ function renderControlState(root: HTMLElement, state: ViewerAnalyticsConsentStat
|
|
|
797
797
|
}
|
|
798
798
|
}
|
|
799
799
|
|
|
800
|
+
function firstEnabledButton(root: HTMLElement): HTMLButtonElement | null {
|
|
801
|
+
return root.querySelector<HTMLButtonElement>("button:not(:disabled)");
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Bind each persistent "Analytics choices" disclosure to the control its
|
|
806
|
+
* `aria-controls` names: a click opens the control and moves focus into it; a second
|
|
807
|
+
* click, the control's Close button, or Escape inside it closes it again, and the
|
|
808
|
+
* latter two return focus to the toggle.
|
|
809
|
+
*/
|
|
810
|
+
function bindViewerAnalyticsChoicesToggles(win: ViewerConsentWindow): void {
|
|
811
|
+
const doc = win.document;
|
|
812
|
+
doc?.querySelectorAll<HTMLButtonElement>("[data-tot-analytics-choices-toggle]").forEach((toggle) => {
|
|
813
|
+
if (toggle.dataset["totAnalyticsChoicesBound"] === "true") return;
|
|
814
|
+
const controlId = toggle.getAttribute("aria-controls");
|
|
815
|
+
const control = controlId ? doc.getElementById(controlId) : null;
|
|
816
|
+
if (!control) return;
|
|
817
|
+
toggle.dataset["totAnalyticsChoicesBound"] = "true";
|
|
818
|
+
|
|
819
|
+
const open = () => {
|
|
820
|
+
control.hidden = false;
|
|
821
|
+
toggle.setAttribute("aria-expanded", "true");
|
|
822
|
+
firstEnabledButton(control)?.focus();
|
|
823
|
+
};
|
|
824
|
+
const close = (returnFocus: boolean) => {
|
|
825
|
+
control.hidden = true;
|
|
826
|
+
toggle.setAttribute("aria-expanded", "false");
|
|
827
|
+
if (returnFocus) toggle.focus();
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
toggle.addEventListener("click", () => (control.hidden ? open() : close(false)));
|
|
831
|
+
control.addEventListener("click", (event) => {
|
|
832
|
+
const target = event.target;
|
|
833
|
+
if (target instanceof Element && target.closest("[data-tot-analytics-consent-close]")) close(true);
|
|
834
|
+
});
|
|
835
|
+
control.addEventListener("keydown", (event) => {
|
|
836
|
+
if (event.key !== "Escape" || control.hidden) return;
|
|
837
|
+
event.preventDefault();
|
|
838
|
+
close(true);
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
|
|
800
843
|
/** Bind the renderer-owned control once; safe when a document contains no analytics plan. */
|
|
801
844
|
export function bootViewerAnalyticsConsentControl(
|
|
802
845
|
win: ViewerConsentWindow,
|
|
@@ -816,8 +859,11 @@ export function bootViewerAnalyticsConsentControl(
|
|
|
816
859
|
const decision = button?.dataset["totAnalyticsConsentDecision"];
|
|
817
860
|
if (decision !== "granted" && decision !== "denied") return;
|
|
818
861
|
renderControlState(root, setViewerAnalyticsConsent(win, decision, now()));
|
|
862
|
+
// The pressed button is now disabled; keep keyboard focus inside the control.
|
|
863
|
+
if (button?.disabled) firstEnabledButton(root)?.focus();
|
|
819
864
|
});
|
|
820
865
|
});
|
|
866
|
+
bindViewerAnalyticsChoicesToggles(win);
|
|
821
867
|
|
|
822
868
|
const storageBoundGlobal = "__TOT_VIEWER_ANALYTICS_CONSENT_STORAGE_BOUND__";
|
|
823
869
|
if (win.addEventListener && win[storageBoundGlobal] !== true) {
|
|
@@ -177,6 +177,7 @@ export function renderIntegrationPhases(
|
|
|
177
177
|
const bodyDeferred =
|
|
178
178
|
phaseBoundary("document.deferred") +
|
|
179
179
|
`<div data-tot-integration-deferred hidden></div>` +
|
|
180
|
+
viewerAnalyticsChoicesLink(plan) +
|
|
180
181
|
lifecycleHostScript();
|
|
181
182
|
|
|
182
183
|
return { head, bodyStart, bodyDeferred };
|
|
@@ -234,20 +235,37 @@ function inlineScript(source: string, nonce: string): string {
|
|
|
234
235
|
|
|
235
236
|
/** Renderer-owned, provider-neutral viewer control: identical in both document mounts. */
|
|
236
237
|
function viewerAnalyticsConsentStyle(nonce: string): string {
|
|
237
|
-
const style = `.tot-analytics-consent{position:fixed;z-index:900;right:1rem;bottom:1rem;display:flex;max-width:min(30rem,calc(100vw - 2rem));align-items:center;gap:.65rem;padding:.7rem .8rem;border:1px solid var(--color-border,#cbd5e1);border-radius:.75rem;background:var(--color-bg,#fff);color:var(--color-text,#111827);box-shadow:0 8px 30px rgba(15,23,42,.18);font:500 .75rem/1.25 system-ui,sans-serif}.tot-analytics-consent__copy{display:grid;gap:.15rem;min-width:8rem}.tot-analytics-consent__detail{color:var(--color-muted,#475569);font-size:.68rem}.tot-analytics-consent__actions{display:flex;gap:.4rem}.tot-analytics-consent button{border:1px solid currentColor;border-radius:999px;background:transparent;color:inherit;padding:.4rem .65rem;font:inherit;cursor:pointer}.tot-analytics-consent button:disabled{cursor:default;opacity:.45}@media(max-width:34rem){.tot-analytics-consent{left:1rem;right:1rem;align-items:flex-start;flex-direction:column}.tot-analytics-consent__actions{width:100%}.tot-analytics-consent button{flex:1}}.tot-analytics-consent[hidden]{display:none}`;
|
|
238
|
+
const style = `.tot-analytics-consent{position:fixed;z-index:900;right:1rem;bottom:1rem;display:flex;max-width:min(30rem,calc(100vw - 2rem));align-items:center;gap:.65rem;padding:.7rem .8rem;border:1px solid var(--color-border,#cbd5e1);border-radius:.75rem;background:var(--color-bg,#fff);color:var(--color-text,#111827);box-shadow:0 8px 30px rgba(15,23,42,.18);font:500 .75rem/1.25 system-ui,sans-serif}.tot-analytics-consent__copy{display:grid;gap:.15rem;min-width:8rem}.tot-analytics-consent__detail{color:var(--color-muted,#475569);font-size:.68rem}.tot-analytics-consent__actions{display:flex;gap:.4rem}.tot-analytics-consent button{border:1px solid currentColor;border-radius:999px;background:transparent;color:inherit;padding:.4rem .65rem;font:inherit;cursor:pointer}.tot-analytics-consent button:disabled{cursor:default;opacity:.45}@media(max-width:34rem){.tot-analytics-consent{left:1rem;right:1rem;align-items:flex-start;flex-direction:column}.tot-analytics-consent__actions{width:100%}.tot-analytics-consent button{flex:1}}.tot-analytics-consent[hidden]{display:none}.tot-analytics-choices{display:flex;justify-content:center;padding:.75rem 1rem;background:var(--color-bg,#fff)}.tot-analytics-choices__toggle{border:0;background:none;padding:.25rem;color:var(--color-muted,#475569);font:500 .75rem/1.25 system-ui,sans-serif;text-decoration:underline;cursor:pointer}`;
|
|
238
239
|
return `<style${nonce ? ` nonce="${nonce}"` : ""}>${style}</style>`;
|
|
239
240
|
}
|
|
240
241
|
|
|
242
|
+
/** The id the persistent "Analytics choices" toggle names in `aria-controls`. */
|
|
243
|
+
const VIEWER_ANALYTICS_CONSENT_CONTROL_ID = "tot-analytics-consent";
|
|
244
|
+
|
|
241
245
|
function viewerAnalyticsConsentControl(): string {
|
|
242
|
-
return `<aside class="tot-analytics-consent" data-tot-analytics-consent-control aria-label="Analytics privacy settings" hidden>` +
|
|
246
|
+
return `<aside id="${VIEWER_ANALYTICS_CONSENT_CONTROL_ID}" class="tot-analytics-consent" data-tot-analytics-consent-control aria-label="Analytics privacy settings" hidden>` +
|
|
243
247
|
`<span class="tot-analytics-consent__copy"><strong data-tot-analytics-consent-status aria-live="polite">Analytics off</strong>` +
|
|
244
|
-
`<span class="tot-analytics-consent__detail">Choose whether
|
|
248
|
+
`<span class="tot-analytics-consent__detail">Choose whether analytics may run on this site.</span></span>` +
|
|
245
249
|
`<span class="tot-analytics-consent__actions">` +
|
|
246
250
|
`<button type="button" data-tot-analytics-consent-decision="granted" aria-pressed="false" disabled>Allow</button>` +
|
|
247
251
|
`<button type="button" data-tot-analytics-consent-decision="denied" aria-pressed="true" disabled>Decline</button>` +
|
|
252
|
+
`<button type="button" data-tot-analytics-consent-close>Close</button>` +
|
|
248
253
|
`</span></aside>`;
|
|
249
254
|
}
|
|
250
255
|
|
|
256
|
+
/**
|
|
257
|
+
* The persistent, platform-owned way to reach the control: a disclosure button placed
|
|
258
|
+
* after the tenant's own footer, on every page whose plan asks for analytics consent.
|
|
259
|
+
* The lifecycle host binds it (`bootViewerAnalyticsConsentControl`); no inline handler.
|
|
260
|
+
*/
|
|
261
|
+
function viewerAnalyticsChoicesLink(plan: EffectivePlan): string {
|
|
262
|
+
if (!plan.consentNeeds.includes("analytics")) return "";
|
|
263
|
+
return `<div class="tot-analytics-choices">` +
|
|
264
|
+
`<button type="button" class="tot-analytics-choices__toggle" data-tot-analytics-choices-toggle ` +
|
|
265
|
+
`aria-controls="${VIEWER_ANALYTICS_CONSENT_CONTROL_ID}" aria-expanded="false">Analytics choices</button>` +
|
|
266
|
+
`</div>`;
|
|
267
|
+
}
|
|
268
|
+
|
|
251
269
|
/**
|
|
252
270
|
* Replace every {@link INTEGRATION_NONCE_PLACEHOLDER} in `text` (an HTML body or a
|
|
253
271
|
* CSP header value) with one fresh per-response `nonce`. Idempotent on text with no
|