@tokenoftrust/storefront-runner 2.0.41 → 2.0.43
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/apps/storefront/integrations/lifecycle-host-asset.mjs +20 -6
- package/apps/storefront/package.json +1 -0
- package/apps/storefront/platform-reconcile-tenants.json +3 -0
- package/apps/storefront/public/favicon.ico +0 -0
- package/apps/storefront/public/shared/commerce-chrome.css +147 -16
- package/apps/storefront/src/components/AsyncStylesheet.astro +26 -0
- package/apps/storefront/src/components/Breadcrumbs.astro +8 -1
- package/apps/storefront/src/components/Img.astro +6 -5
- package/apps/storefront/src/components/ProductCard.astro +6 -4
- package/apps/storefront/src/components/Seo.astro +2 -1
- package/apps/storefront/src/components/chrome/SiteFooter.astro +45 -11
- package/apps/storefront/src/components/chrome/SiteHeader.astro +83 -13
- package/apps/storefront/src/components/commerce/RatingStars.astro +1 -1
- package/apps/storefront/src/components/compliance/ComplianceNotice.astro +2 -2
- package/apps/storefront/src/components/content/PromoTiles.astro +107 -0
- package/apps/storefront/src/components/content/TrustStrip.astro +30 -14
- package/apps/storefront/src/components/home/Hero.astro +156 -4
- package/apps/storefront/src/components/islands/AgeGate.tsx +135 -26
- package/apps/storefront/src/components/islands/ImageGallery.tsx +6 -9
- package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +194 -46
- package/apps/storefront/src/components/islands/SearchTypeahead.tsx +10 -4
- package/apps/storefront/src/components/marketing/Faq.astro +9 -2
- package/apps/storefront/src/config/devTenantSeed.ts +1 -1
- package/apps/storefront/src/config/kvTenantResolver.ts +7 -1
- package/apps/storefront/src/config/publicationSpaceConfig.ts +12 -1
- package/apps/storefront/src/config/publicationSpaceRegistry.ts +5 -0
- package/apps/storefront/src/config/tenants.ts +22 -0
- package/apps/storefront/src/layouts/Layout.astro +69 -44
- package/apps/storefront/src/lib/activity/changeActorAttribution.ts +24 -8
- package/apps/storefront/src/lib/activity/recordActivity.ts +15 -0
- package/apps/storefront/src/lib/asyncCss.ts +23 -0
- package/apps/storefront/src/lib/auth/loginGate.ts +22 -11
- package/apps/storefront/src/lib/basePath.ts +70 -9
- package/apps/storefront/src/lib/chrome/managedCart.ts +50 -0
- package/apps/storefront/src/lib/chrome/model.ts +4 -0
- package/apps/storefront/src/lib/chrome/newsletterAgeAttestation.ts +9 -0
- package/apps/storefront/src/lib/content/trustStrip.ts +38 -3
- package/apps/storefront/src/lib/cspStaticHashes.ts +4 -1
- package/apps/storefront/src/lib/diag/capture.ts +161 -0
- package/apps/storefront/src/lib/diag/consoleLine.ts +76 -0
- package/apps/storefront/src/lib/diag/debugConsole.ts +118 -0
- package/apps/storefront/src/lib/diag/debugMode.ts +163 -0
- package/apps/storefront/src/lib/diag/debugWindow.ts +111 -0
- package/apps/storefront/src/lib/diag/reference.ts +148 -0
- package/apps/storefront/src/lib/diag/referenceStore.ts +113 -0
- package/apps/storefront/src/lib/diag/report.ts +109 -0
- package/apps/storefront/src/lib/diag/session.ts +93 -0
- package/apps/storefront/src/lib/featuredProductsPresentation.ts +5 -0
- package/apps/storefront/src/lib/heroSlideshow.ts +41 -0
- package/apps/storefront/src/lib/homeVisualParity.ts +107 -0
- package/apps/storefront/src/lib/imageAttrs.ts +9 -6
- package/apps/storefront/src/lib/integration/nonceStamp.ts +23 -1
- package/apps/storefront/src/lib/integration/renderIntegrationState.ts +36 -0
- package/apps/storefront/src/lib/integration/tenantAnalyticsDeclarations.ts +17 -12
- package/apps/storefront/src/lib/jsonScript.ts +13 -0
- package/apps/storefront/src/lib/pinnedRouteRewriteBoundary.ts +31 -0
- package/apps/storefront/src/lib/previewReload.ts +53 -9
- package/apps/storefront/src/lib/rawChrome.ts +148 -19
- package/apps/storefront/src/lib/rawMarketingHtml.ts +120 -7
- package/apps/storefront/src/lib/storyblok/content-model.ts +46 -3
- package/apps/storefront/src/lib/styleGuideThemes.ts +107 -0
- package/apps/storefront/src/lib/tenantRouting.ts +15 -4
- package/apps/storefront/src/lib/tot/syncState.ts +21 -0
- package/apps/storefront/src/lib/tracking/publishSafetyNet.ts +29 -3
- package/apps/storefront/src/middleware/controlPlane.ts +9 -5
- package/apps/storefront/src/middleware/index.ts +13 -1
- package/apps/storefront/src/middleware/renderChain.ts +54 -11
- package/apps/storefront/src/middleware/routeBranch.ts +58 -12
- package/apps/storefront/src/middleware/staticBundle.ts +37 -3
- package/apps/storefront/src/pages/[...slug].astro +6 -2
- package/apps/storefront/src/pages/api/request-access.ts +5 -4
- package/apps/storefront/src/pages/blog/[slug].astro +11 -1
- package/apps/storefront/src/pages/cart.ts +23 -0
- package/apps/storefront/src/pages/index.astro +64 -85
- package/apps/storefront/src/pages/products/[handle].astro +8 -6
- package/apps/storefront/src/pages/sitemap.xml.ts +8 -2
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +14 -25
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +10 -17
- package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +9 -16
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +9 -15
- package/apps/storefront/src/pages/the-build/[slug].astro +7 -1
- package/apps/storefront/src/styles/fonts.css +7 -0
- package/apps/storefront/src/styles/global.css +397 -3
- package/package.json +4 -1
- package/packages/cli/src/visual-parity.mjs +321 -0
- package/packages/public-runtime/package.json +7 -0
- package/packages/public-runtime/schemas/publication-space/v1/manifest.schema.json +1 -1
- package/packages/public-runtime/schemas/publication-space/v1/normalized-mount.schema.json +1 -1
- package/packages/public-runtime/src/chrome.ts +44 -7
- package/packages/public-runtime/src/csp.ts +12 -1
- package/packages/public-runtime/src/extension-contract-values.d.mts +19 -0
- package/packages/public-runtime/src/extension-contract-values.mjs +62 -0
- package/packages/public-runtime/src/extension-contract.ts +20 -6
- package/packages/public-runtime/src/image-dependencies.ts +1438 -0
- package/packages/public-runtime/src/index.ts +8 -0
- package/packages/public-runtime/src/integration/consent-broker.ts +10 -7
- package/packages/public-runtime/src/integration/consent-config.ts +9 -0
- package/packages/public-runtime/src/integration/contact-sales-action-source.ts +56 -10
- package/packages/public-runtime/src/integration/governed-event-properties.ts +62 -0
- package/packages/public-runtime/src/integration/index.ts +1 -0
- package/packages/public-runtime/src/integration/lifecycle-boot.ts +30 -2
- package/packages/public-runtime/src/integration/lifecycle-host-asset.d.mts +2 -0
- package/packages/public-runtime/src/integration/lifecycle-host-asset.mjs +17 -3
- package/packages/public-runtime/src/integration/native-amplitude.ts +12 -3
- package/packages/public-runtime/src/integration/native-ga4-sandbox-asset.mjs +9 -0
- package/packages/public-runtime/src/integration/native-ga4-sandbox-entry.ts +27 -3
- package/packages/public-runtime/src/integration/native-ga4-sandbox.ts +13 -0
- package/packages/public-runtime/src/integration/native-ga4.ts +11 -0
- package/packages/public-runtime/src/integration/platform-adapters.ts +5 -1
- package/packages/public-runtime/src/integration/render-phases.ts +28 -2
- package/packages/public-runtime/src/publication-redirects.d.mts +45 -0
- package/packages/public-runtime/src/publication-redirects.mjs +379 -0
- package/packages/public-runtime/src/publication-space.ts +24 -3
- package/packages/public-runtime/src/static-bundle.ts +145 -26
- package/packages/public-runtime/src/tenant-embed-defaults.d.mts +3 -0
- package/packages/public-runtime/src/tenant-embed-defaults.mjs +40 -0
- package/packages/public-runtime/src/tenant.ts +59 -1
- package/pnpm-lock.runner.yaml +19 -0
- package/pnpm-workspace.yaml +4 -0
- package/scripts/dev/git-status.mjs +1 -1
- package/scripts/dev/publish.mjs +6 -0
- package/scripts/lib/tenant-asset-copy.mjs +4 -2
- package/scripts/tenant/materialize-tenant-content.mjs +1 -0
- package/scripts/tenant/validate.mjs +171 -8
- package/apps/storefront/src/lib/cfImage.ts +0 -55
- package/apps/storefront/src/workers/pruneRollupWorker.ts +0 -120
- package/tenants/home/public/fonts/inter-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-800-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-800-normal.woff2 +0 -0
- package/tenants/home/public/js/storefront.js +0 -129
- package/tenants/home/public/logo-wordmark.png +0 -0
- package/tenants/home/public/pages/storefront.css +0 -235
- package/tenants/home/theme.json +0 -6
|
@@ -29,6 +29,7 @@ import { isCurrentNavHref } from "./chrome/navCurrent.js";
|
|
|
29
29
|
import { ctaIdentityAttr } from "./chrome/ctaIdentity.js";
|
|
30
30
|
import { CHROME_PLACEMENT } from "./chrome/trackingPlacements.js";
|
|
31
31
|
import { LEGAL_NAV_LABEL, splitFooterLegal } from "./chrome/footerLegal.js";
|
|
32
|
+
import { resolveNewsletterAgeAttestation } from "./chrome/newsletterAgeAttestation.js";
|
|
32
33
|
import { trackingAttributesHtml } from "@tot/public-runtime";
|
|
33
34
|
import type {
|
|
34
35
|
ChromeAgeGate,
|
|
@@ -44,7 +45,7 @@ import type {
|
|
|
44
45
|
HeaderVariant,
|
|
45
46
|
SocialLink,
|
|
46
47
|
} from "./chrome/model.js";
|
|
47
|
-
import { resolveChromeAssignment } from "./chrome/model.js";
|
|
48
|
+
import { resolveAgeGateCopy, resolveChromeAssignment } from "./chrome/model.js";
|
|
48
49
|
import { resolveHeaderVariant } from "./chrome/variant.js";
|
|
49
50
|
|
|
50
51
|
/** HTML-escape text/attribute content. Covers the five significant characters. */
|
|
@@ -289,6 +290,7 @@ function renderHeader(header: ChromeHeader, variant: HeaderVariant, currentPath?
|
|
|
289
290
|
const cart = header.cartHref
|
|
290
291
|
? `<a class="cart" href="${esc(header.cartHref)}" aria-label="Cart">${ICON_CART}<span class="count">0</span></a>`
|
|
291
292
|
: "";
|
|
293
|
+
const compact = variant === "compact-centered";
|
|
292
294
|
// "minimal" (e.g. checkout/policy pages): brand + a reduced action set only —
|
|
293
295
|
// no primary nav bar, no search, no mobile-menu toggle. "primary": the full header.
|
|
294
296
|
const minimal = variant === "minimal";
|
|
@@ -323,24 +325,49 @@ function renderHeader(header: ChromeHeader, variant: HeaderVariant, currentPath?
|
|
|
323
325
|
const navItems = header.nav
|
|
324
326
|
.map((item, i) => renderNavItem(item, i, currentPath))
|
|
325
327
|
.join("\n ");
|
|
328
|
+
const primaryNav =
|
|
329
|
+
`<nav class="primary-nav" id="primaryNav" aria-label="Primary">\n` +
|
|
330
|
+
` <ul>\n ${navItems}\n </ul>\n` +
|
|
331
|
+
` </nav>`;
|
|
326
332
|
const navBar = minimal
|
|
327
333
|
? ""
|
|
334
|
+
: compact
|
|
335
|
+
? ""
|
|
328
336
|
: ` <nav class="primary-nav-bar" aria-label="Primary">\n` +
|
|
329
337
|
` <div class="container">\n` +
|
|
330
|
-
`
|
|
331
|
-
` <ul>\n ${navItems}\n </ul>\n` +
|
|
332
|
-
` </nav>\n` +
|
|
338
|
+
` ${primaryNav}\n` +
|
|
333
339
|
` </div>\n` +
|
|
334
340
|
` </nav>\n`;
|
|
341
|
+
const compactSearch =
|
|
342
|
+
compact && header.search
|
|
343
|
+
? `<a class="header-search-trigger" href="${esc(header.search.action ?? "/search")}" aria-label="${esc(header.search.label ?? "Search")}">${ICON_SEARCH}</a>`
|
|
344
|
+
: "";
|
|
345
|
+
const mobileBottom =
|
|
346
|
+
compact
|
|
347
|
+
? `<nav class="mobile-bottom-nav" aria-label="Quick links">` +
|
|
348
|
+
`<a href="/" aria-label="Home"><span aria-hidden="true">⌂</span><small>Home</small></a>` +
|
|
349
|
+
(header.search
|
|
350
|
+
? `<a href="${esc(header.search.action ?? "/search")}" aria-label="${esc(header.search.label ?? "Search")}">${ICON_SEARCH}<small>Search</small></a>`
|
|
351
|
+
: "") +
|
|
352
|
+
(header.accountHref
|
|
353
|
+
? `<a href="${esc(header.accountHref)}" aria-label="Account">${ICON_ACCOUNT}<small>Account</small></a>`
|
|
354
|
+
: "") +
|
|
355
|
+
(header.cartHref
|
|
356
|
+
? `<a href="${esc(header.cartHref)}" aria-label="Cart">${ICON_CART}<small>Cart</small></a>`
|
|
357
|
+
: "") +
|
|
358
|
+
`</nav>`
|
|
359
|
+
: "";
|
|
335
360
|
return (
|
|
336
361
|
`<header class="site-header" data-variant="${esc(variant)}">\n` +
|
|
337
362
|
` <div class="container header-row">\n` +
|
|
363
|
+
(compact ? ` ${primaryNav}\n` : "") +
|
|
338
364
|
` ${renderBrand(header.brand)}\n` +
|
|
339
|
-
(!minimal && search ? ` ${search}\n` : "") +
|
|
340
|
-
` <div class="header-actions">${memberCue}${ctas}${account}${cart}${hamburger}</div>\n` +
|
|
365
|
+
(!minimal && !compact && search ? ` ${search}\n` : "") +
|
|
366
|
+
` <div class="header-actions">${memberCue}${ctas}${compactSearch}${account}${cart}${hamburger}</div>\n` +
|
|
341
367
|
` </div>\n` +
|
|
342
368
|
navBar +
|
|
343
|
-
`</header>`
|
|
369
|
+
`</header>` +
|
|
370
|
+
mobileBottom
|
|
344
371
|
);
|
|
345
372
|
}
|
|
346
373
|
|
|
@@ -352,13 +379,52 @@ function renderHeader(header: ChromeHeader, variant: HeaderVariant, currentPath?
|
|
|
352
379
|
* can't close the tag early.
|
|
353
380
|
*/
|
|
354
381
|
function renderAgeGate(g: ChromeAgeGate): string {
|
|
355
|
-
const
|
|
382
|
+
const resolved = {
|
|
383
|
+
...g,
|
|
384
|
+
title: resolveAgeGateCopy(g.title, g.minAge),
|
|
385
|
+
lede: resolveAgeGateCopy(g.lede, g.minAge),
|
|
386
|
+
yesLabel: resolveAgeGateCopy(g.yesLabel, g.minAge),
|
|
387
|
+
noLabel: resolveAgeGateCopy(g.noLabel, g.minAge),
|
|
388
|
+
denyText: resolveAgeGateCopy(g.denyText, g.minAge),
|
|
389
|
+
fineText: resolveAgeGateCopy(g.fineText, g.minAge),
|
|
390
|
+
};
|
|
391
|
+
const json = JSON.stringify(resolved).replace(/</g, "\\u003c");
|
|
356
392
|
return `<script type="application/json" id="tot-age-gate-config">${json}</script>`;
|
|
357
393
|
}
|
|
358
394
|
|
|
395
|
+
/** Remove a wrapper's raw gate payload before Layout mounts its governed component gate. */
|
|
396
|
+
export function stripRawChromeAgeGateConfig(html: string): string {
|
|
397
|
+
return html.replace(
|
|
398
|
+
/<script\b(?=[^>]*\bid=["']tot-age-gate-config["'])[^>]*>[\s\S]*?<\/script>\s*/gi,
|
|
399
|
+
"",
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Neutralize the reserved light-DOM marker before tenant-authored chrome is inserted. */
|
|
404
|
+
export function stripReservedComplianceMarkers(html: string): string {
|
|
405
|
+
return html.replace(
|
|
406
|
+
/\sdata-tot-compliance(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?/gi,
|
|
407
|
+
"",
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Remove executable tenant script elements, including unterminated tags. */
|
|
412
|
+
export function stripTenantScripts(html: string): string {
|
|
413
|
+
return html.replace(
|
|
414
|
+
/<script\b[^>]*>(?:[\s\S]*?<\/script\s*>|[\s\S]*$)/gi,
|
|
415
|
+
"",
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Compliance-safe shared-chrome fragment for Layout's component-rendered path. */
|
|
420
|
+
export function sanitizeSharedChromeFragment(html: string): string {
|
|
421
|
+
return stripReservedComplianceMarkers(stripTenantScripts(html));
|
|
422
|
+
}
|
|
423
|
+
|
|
359
424
|
function renderFooterNewsletter(n: NonNullable<ChromeFooter["newsletter"]>): string {
|
|
360
|
-
const
|
|
361
|
-
|
|
425
|
+
const ageAttestation = resolveNewsletterAgeAttestation(n.ageAttestation);
|
|
426
|
+
const attest = ageAttestation
|
|
427
|
+
? `\n <label class="footer-attest"><input type="checkbox" name="${ageAttestation.name}" value="${ageAttestation.value}" required> ${esc(ageAttestation.label)}</label>`
|
|
362
428
|
: "";
|
|
363
429
|
const title = n.title ? `<h4>${esc(n.title)}</h4>` : "";
|
|
364
430
|
const body = n.body ? `<p>${esc(n.body)}</p>` : "";
|
|
@@ -376,6 +442,7 @@ function renderFooterNewsletter(n: NonNullable<ChromeFooter["newsletter"]>): str
|
|
|
376
442
|
}
|
|
377
443
|
|
|
378
444
|
function renderFooter(footer: ChromeFooter): string {
|
|
445
|
+
const collapsible = footer.variant === "light-collapsible";
|
|
379
446
|
const brand = footer.brand;
|
|
380
447
|
const brandBlock =
|
|
381
448
|
brand || footer.newsletter
|
|
@@ -411,7 +478,9 @@ function renderFooter(footer: ChromeFooter): string {
|
|
|
411
478
|
return `<li><a href="${esc(l.href)}"${relFor(l)}${stamp}>${esc(l.label)}</a></li>`;
|
|
412
479
|
})
|
|
413
480
|
.join("");
|
|
414
|
-
return
|
|
481
|
+
return collapsible
|
|
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>`;
|
|
415
484
|
})
|
|
416
485
|
.join("\n");
|
|
417
486
|
const trust = footer.trustMarks?.length
|
|
@@ -453,7 +522,7 @@ function renderFooter(footer: ChromeFooter): string {
|
|
|
453
522
|
const bottom =
|
|
454
523
|
bottomText || legalNav ? `\n <div class="footer-bottom">${bottomText}${legalNav}</div>` : "";
|
|
455
524
|
return (
|
|
456
|
-
`<footer class="site-footer">\n` +
|
|
525
|
+
`<footer class="site-footer" data-variant="${esc(footer.variant)}">\n` +
|
|
457
526
|
` <div class="container">\n` +
|
|
458
527
|
` <div class="footer-grid">\n${brandBlock}${columns}\n </div>` +
|
|
459
528
|
`${trust}${social}${payments}${warning}${bottom}\n` +
|
|
@@ -511,8 +580,8 @@ export const CHROME_BEHAVIOR_JS = `(function () {
|
|
|
511
580
|
// DOM (data-tot-compliance, the same marker components/islands/IsolatedAgeGate.tsx
|
|
512
581
|
// uses); its contents sit behind a closed shadow root, so tenant CSS/JS can't
|
|
513
582
|
// select, restyle, or hide the dialog. A MutationObserver re-asserts the host the
|
|
514
|
-
// same way that component does. Config comes from the
|
|
515
|
-
//
|
|
583
|
+
// same way that component does. Config comes from the validated ChromeAgeGate
|
|
584
|
+
// JSON payload rawChrome.ts renders.
|
|
516
585
|
var gateConfigEl = document.getElementById('tot-age-gate-config');
|
|
517
586
|
if (gateConfigEl && !document.querySelector('[data-tot-compliance]')) {
|
|
518
587
|
var gcfg; try { gcfg = JSON.parse(gateConfigEl.textContent || 'null'); } catch (e) { gcfg = null; }
|
|
@@ -524,10 +593,39 @@ export const CHROME_BEHAVIOR_JS = `(function () {
|
|
|
524
593
|
|
|
525
594
|
var host = document.createElement('div');
|
|
526
595
|
host.setAttribute('data-tot-compliance', '');
|
|
596
|
+
var gharden = function () {
|
|
597
|
+
var floor = { all: 'initial', display: 'block', position: 'fixed', inset: '0', 'z-index': '2147483647', visibility: 'visible', opacity: '1', 'pointer-events': 'none' };
|
|
598
|
+
Object.keys(floor).forEach(function (property) {
|
|
599
|
+
if (host.style.getPropertyValue(property) !== floor[property] || host.style.getPropertyPriority(property) !== 'important') {
|
|
600
|
+
host.style.setProperty(property, floor[property], 'important');
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
};
|
|
604
|
+
gharden();
|
|
527
605
|
var shadow = host.attachShadow({ mode: 'closed' });
|
|
528
|
-
|
|
529
|
-
|
|
606
|
+
var gcopiedSheets = {};
|
|
607
|
+
var gcopyPlatformSheet = function (l) {
|
|
608
|
+
var href = l.getAttribute('href') || '';
|
|
609
|
+
var sheetUrl; try { sheetUrl = new URL(href, document.baseURI); } catch (e) { sheetUrl = null; }
|
|
610
|
+
var platformPath = sheetUrl && sheetUrl.origin === location.origin &&
|
|
611
|
+
(sheetUrl.pathname.indexOf('/_astro/') === 0 || sheetUrl.pathname.indexOf('/shared/') === 0);
|
|
612
|
+
if (platformPath && !gcopiedSheets[sheetUrl.href]) {
|
|
613
|
+
gcopiedSheets[sheetUrl.href] = true;
|
|
614
|
+
shadow.appendChild(l.cloneNode(true));
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
document.head.querySelectorAll('link[rel="stylesheet"]').forEach(gcopyPlatformSheet);
|
|
618
|
+
// Astro may append an extracted island stylesheet after this behavior
|
|
619
|
+
// script runs. Copy the late, same-origin platform link into the closed
|
|
620
|
+
// root; never synthesize an inline style element, which strict CSP would block.
|
|
621
|
+
var gsheetObserver = new MutationObserver(function (records) {
|
|
622
|
+
records.forEach(function (record) {
|
|
623
|
+
record.addedNodes.forEach(function (node) {
|
|
624
|
+
if (node.nodeType === 1 && node.tagName === 'LINK' && node.rel === 'stylesheet') gcopyPlatformSheet(node);
|
|
625
|
+
});
|
|
626
|
+
});
|
|
530
627
|
});
|
|
628
|
+
gsheetObserver.observe(document.head, { childList: true });
|
|
531
629
|
var mount = document.createElement('div');
|
|
532
630
|
shadow.appendChild(mount);
|
|
533
631
|
|
|
@@ -554,10 +652,41 @@ export const CHROME_BEHAVIOR_JS = `(function () {
|
|
|
554
652
|
'</div></div>';
|
|
555
653
|
|
|
556
654
|
var gate = mount.querySelector('#ageGate');
|
|
655
|
+
gate.style.pointerEvents = 'auto';
|
|
656
|
+
if (gcfg.variant === 'compact-modal') {
|
|
657
|
+
gate.style.background = 'rgba(0,0,0,.5)';
|
|
658
|
+
gate.style.padding = '16px';
|
|
659
|
+
gate.style.alignItems = 'flex-start';
|
|
660
|
+
var panel = mount.querySelector('.age-gate__panel');
|
|
661
|
+
panel.style.width = 'min(600px, calc(100vw - 32px))';
|
|
662
|
+
panel.style.minHeight = 'min(382px, calc(100vh - 32px))';
|
|
663
|
+
panel.style.borderRadius = '5px';
|
|
664
|
+
panel.style.boxShadow = '0 20px 60px rgba(0,0,0,.35)';
|
|
665
|
+
var actions = mount.querySelector('.age-gate__actions');
|
|
666
|
+
actions.style.flexDirection = 'column';
|
|
667
|
+
Array.prototype.forEach.call(actions.querySelectorAll('button'), function (button) {
|
|
668
|
+
button.style.width = '100%';
|
|
669
|
+
button.style.minHeight = '48px';
|
|
670
|
+
button.style.borderRadius = '3px';
|
|
671
|
+
});
|
|
672
|
+
}
|
|
557
673
|
var gok = false;
|
|
558
674
|
try { gok = localStorage.getItem(gkey) === 'yes'; } catch (e) {}
|
|
675
|
+
// Hiding overflow REMOVES the scrollbar, which widens the viewport and reflows the whole
|
|
676
|
+
// document behind the gate. Measured at CLS 0.82-0.90 on every page long enough to
|
|
677
|
+
// scroll — pages that never scrolled were untouched — costing ~25 Lighthouse performance
|
|
678
|
+
// points while every timing metric stayed green. Reserve the gutter first so the viewport
|
|
679
|
+
// keeps its width; measure BEFORE hiding, because afterwards the gutter reads 0.
|
|
680
|
+
var ggutter = function () {
|
|
681
|
+
var w = window.innerWidth - document.documentElement.clientWidth;
|
|
682
|
+
return (isFinite(w) && w > 0) ? w : 0;
|
|
683
|
+
};
|
|
559
684
|
if (!gok) {
|
|
560
685
|
gate.classList.add('is-open');
|
|
686
|
+
var gw = ggutter();
|
|
687
|
+
if (gw > 0 && !document.documentElement.style.paddingRight) {
|
|
688
|
+
document.documentElement.style.paddingRight = gw + 'px';
|
|
689
|
+
}
|
|
561
690
|
document.documentElement.style.overflow = 'hidden';
|
|
562
691
|
}
|
|
563
692
|
var gyes = mount.querySelector('#ageYes');
|
|
@@ -566,16 +695,16 @@ export const CHROME_BEHAVIOR_JS = `(function () {
|
|
|
566
695
|
try { localStorage.setItem(gkey, 'yes'); } catch (e) {}
|
|
567
696
|
gate.classList.remove('is-open');
|
|
568
697
|
document.documentElement.style.overflow = '';
|
|
698
|
+
document.documentElement.style.paddingRight = '';
|
|
569
699
|
});
|
|
570
700
|
if (gno) gno.addEventListener('click', function () { gate.classList.add('is-denied'); });
|
|
571
701
|
|
|
572
702
|
document.body.appendChild(host);
|
|
573
703
|
|
|
574
704
|
var gensure = function () {
|
|
575
|
-
if (!host.isConnected) document.body.appendChild(host);
|
|
576
|
-
if (host.style.display === 'none') host.style.display = '';
|
|
577
|
-
if (host.style.visibility === 'hidden') host.style.visibility = '';
|
|
705
|
+
if (!host.isConnected || document.body.lastElementChild !== host) document.body.appendChild(host);
|
|
578
706
|
if (host.hasAttribute('hidden')) host.removeAttribute('hidden');
|
|
707
|
+
gharden();
|
|
579
708
|
};
|
|
580
709
|
var gobserver = new MutationObserver(gensure);
|
|
581
710
|
gobserver.observe(document.body, { childList: true });
|
|
@@ -16,12 +16,15 @@
|
|
|
16
16
|
* sanitizer. Untrusted vendor HTML must be sandboxed/sanitized instead.
|
|
17
17
|
*/
|
|
18
18
|
import {
|
|
19
|
+
normalizePublicationPath,
|
|
19
20
|
renderIntegrationPhases,
|
|
20
21
|
INTERACTION_TRACKING_BOOT_JS,
|
|
21
22
|
type EffectivePlan,
|
|
22
23
|
type ConsentSnapshot,
|
|
23
24
|
} from "@tot/public-runtime";
|
|
24
25
|
import { reportTrackingChainHealth } from "./tracking/interactionHealth.js";
|
|
26
|
+
import { ASYNC_CSS_SWAP_JS } from "./asyncCss.js";
|
|
27
|
+
import { ROOT_SERVED_HREF_NAMESPACES } from "./basePath.js";
|
|
25
28
|
|
|
26
29
|
export { rawHtmlPolicy } from "@tot/public-runtime";
|
|
27
30
|
|
|
@@ -133,6 +136,21 @@ export function interactionTrackingScript(nonce: string): string {
|
|
|
133
136
|
return `<script${nonceAttr}>${INTERACTION_TRACKING_BOOT_JS}</script>`;
|
|
134
137
|
}
|
|
135
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Swaps every `data-async-css`-tagged `<link rel="preload">` to
|
|
141
|
+
* `rel="stylesheet"` once fetched — the raw-mount half of the
|
|
142
|
+
* `<AsyncStylesheet>` primitive (lib/asyncCss.ts; Layout.astro is the other,
|
|
143
|
+
* same one-engine-two-mounts discipline as {@link interactionTrackingScript}).
|
|
144
|
+
* A raw-HTML tenant hand-authors the plain `<link rel="preload" as="style"
|
|
145
|
+
* data-async-css>` + `<noscript>` markup directly (no Astro component
|
|
146
|
+
* needed for that part); this script is what makes it actually async instead
|
|
147
|
+
* of a permanently-inert preload.
|
|
148
|
+
*/
|
|
149
|
+
export function asyncCssSwapScript(nonce: string): string {
|
|
150
|
+
const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
|
|
151
|
+
return `<script${nonceAttr}>${ASYNC_CSS_SWAP_JS}</script>`;
|
|
152
|
+
}
|
|
153
|
+
|
|
136
154
|
/** Stamp `nonce` onto every inline `<style>`/`<script>` so a strict CSP admits them. */
|
|
137
155
|
export function nonceInlineTags(html: string, nonce: string): string {
|
|
138
156
|
return html
|
|
@@ -140,20 +158,105 @@ export function nonceInlineTags(html: string, nonce: string): string {
|
|
|
140
158
|
.replaceAll("<script", `<script nonce="${nonce}"`);
|
|
141
159
|
}
|
|
142
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Matches `href="/` NOT followed by `/` or a {@link ROOT_SERVED_HREF_NAMESPACES}
|
|
163
|
+
* entry. A namespace must be followed by `/`, a query, a fragment, or the closing
|
|
164
|
+
* quote — `favicon.svg` is a file, and `/favicon.svg?v=2` is still that file.
|
|
165
|
+
*/
|
|
166
|
+
const ROOT_SERVED_HREF_RE = new RegExp(
|
|
167
|
+
`href="/(?!/|(?:${ROOT_SERVED_HREF_NAMESPACES.join("|").replace(/\./g, "\\.")})(?:[/?#"]))`,
|
|
168
|
+
"g",
|
|
169
|
+
);
|
|
170
|
+
|
|
143
171
|
/**
|
|
144
172
|
* Prefix same-origin root-absolute hrefs (`href="/…"`) with the tenant base path,
|
|
145
173
|
* so `/pricing/` becomes `/<domain>/pricing/` under path-prefix routing. No-op when
|
|
146
|
-
* basePath is empty (host-routed prod). Skips protocol-relative `//…` and
|
|
147
|
-
*
|
|
148
|
-
* must NOT get the tenant prefix:
|
|
149
|
-
* - `/tenants/…` — per-tenant static assets (logo, favicon, theme/mkt css);
|
|
150
|
-
* - `/shared/…` — platform-shipped shared assets (e.g. the shared commerce-chrome
|
|
151
|
-
* stylesheet) that every tenant links.
|
|
174
|
+
* basePath is empty (host-routed prod). Skips protocol-relative `//…` and every
|
|
175
|
+
* namespace in {@link ROOT_SERVED_HREF_NAMESPACES}.
|
|
152
176
|
* Asset `src="…"` isn't matched at all (href-only).
|
|
153
177
|
*/
|
|
154
178
|
export function prefixTenantLinks(html: string, basePath: string): string {
|
|
155
179
|
if (!basePath) return html;
|
|
156
|
-
return html.replace(
|
|
180
|
+
return html.replace(ROOT_SERVED_HREF_RE, `href="${basePath}/`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Raw pages with a robots `noindex` directive remain routable but stay out of discovery. */
|
|
184
|
+
export function isIndexableHtml(html: string): boolean {
|
|
185
|
+
for (const match of html.matchAll(/<meta\b([^>]*)>/gi)) {
|
|
186
|
+
const attrs: Record<string, string> = {};
|
|
187
|
+
for (const attr of (match[1] ?? "").matchAll(
|
|
188
|
+
/([a-zA-Z_:][\w:.-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/g,
|
|
189
|
+
)) {
|
|
190
|
+
const name = attr[1];
|
|
191
|
+
if (name) attrs[name.toLowerCase()] = attr[2] ?? attr[3] ?? attr[4] ?? "";
|
|
192
|
+
}
|
|
193
|
+
if (attrs.name?.trim().toLowerCase() !== "robots") continue;
|
|
194
|
+
if ((attrs.content ?? "").toLowerCase().split(/[\s,]+/).includes("noindex")) return false;
|
|
195
|
+
}
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The one canonical URL constructor for raw marketing subpages.
|
|
201
|
+
*
|
|
202
|
+
* Raw content providers supply slugs, not trusted URL paths. Normalize those
|
|
203
|
+
* slugs with the publication-space path contract, preserve a path-prefixed
|
|
204
|
+
* preview base, and add the raw-page route's single trailing slash. Both the
|
|
205
|
+
* page renderer and sitemap call this function so they cannot disagree.
|
|
206
|
+
*/
|
|
207
|
+
export function rawPageCanonicalUrl(canonicalBase: string, slug: string): string {
|
|
208
|
+
const path = normalizePublicationPath(`/${slug}`, { mode: "mount" });
|
|
209
|
+
if (!path.ok || path.value === "/") {
|
|
210
|
+
throw new Error(`Invalid canonical raw-page path: ${slug}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let base: URL;
|
|
214
|
+
try {
|
|
215
|
+
base = new URL(canonicalBase);
|
|
216
|
+
} catch {
|
|
217
|
+
throw new Error(`Invalid canonical base URL: ${canonicalBase}`);
|
|
218
|
+
}
|
|
219
|
+
if (
|
|
220
|
+
!["http:", "https:"].includes(base.protocol) ||
|
|
221
|
+
base.username ||
|
|
222
|
+
base.password ||
|
|
223
|
+
base.search ||
|
|
224
|
+
base.hash
|
|
225
|
+
) {
|
|
226
|
+
throw new Error(`Invalid canonical base URL: ${canonicalBase}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const basePath = normalizePublicationPath(base.pathname || "/", { mode: "mount" });
|
|
230
|
+
if (!basePath.ok) throw new Error(`Invalid canonical base URL: ${canonicalBase}`);
|
|
231
|
+
|
|
232
|
+
const prefix = basePath.value === "/" ? "" : basePath.value;
|
|
233
|
+
base.pathname = `${prefix}${path.value}/`;
|
|
234
|
+
base.search = "";
|
|
235
|
+
base.hash = "";
|
|
236
|
+
return base.toString();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Resolve the raw document's `<link rel="canonical">` to an absolute URL.
|
|
241
|
+
* The caller supplies the renderer-owned canonical route URL; this function
|
|
242
|
+
* defensively preserves the raw-subpage trailing-slash contract. No-op when
|
|
243
|
+
* `canonicalUrl` is omitted: preview/candidate renders don't pass one, so
|
|
244
|
+
* their canonical stays exactly as tenant-authored (relative), which
|
|
245
|
+
* `prefixTenantLinks` then base-path-prefixes as before — unchanged
|
|
246
|
+
* behavior there, since those responses are already `noindex`.
|
|
247
|
+
*/
|
|
248
|
+
export function resolveCanonicalHref(html: string, canonicalUrl: string | undefined): string {
|
|
249
|
+
if (!canonicalUrl) return html;
|
|
250
|
+
const resolved = new URL(canonicalUrl);
|
|
251
|
+
if (resolved.pathname !== "/" && !resolved.pathname.endsWith("/")) {
|
|
252
|
+
resolved.pathname += "/";
|
|
253
|
+
}
|
|
254
|
+
const href = resolved.toString();
|
|
255
|
+
return html.replace(/<link\b[^>]*\brel=["']canonical["'][^>]*>/i, (tag) =>
|
|
256
|
+
/\bhref=["'][^"']*["']/i.test(tag)
|
|
257
|
+
? tag.replace(/\bhref=["'][^"']*["']/i, `href="${href}"`)
|
|
258
|
+
: tag.replace(/<link\b/i, `<link href="${href}"`),
|
|
259
|
+
);
|
|
157
260
|
}
|
|
158
261
|
|
|
159
262
|
/**
|
|
@@ -188,6 +291,14 @@ export function rawHtmlResponse(
|
|
|
188
291
|
* config (candidate/preview renders).
|
|
189
292
|
*/
|
|
190
293
|
consentSnapshot?: ConsentSnapshot;
|
|
294
|
+
/**
|
|
295
|
+
* The absolute production URL for this page (`tenant.canonicalDomain` +
|
|
296
|
+
* path), matching Seo.astro's `canonical` derivation. Pass this only from
|
|
297
|
+
* production render paths (index.astro/[...slug].astro); preview/candidate
|
|
298
|
+
* callers omit it so their canonical stays relative and base-path-prefixed,
|
|
299
|
+
* unchanged from today (see resolveCanonicalHref).
|
|
300
|
+
*/
|
|
301
|
+
canonicalUrl?: string;
|
|
191
302
|
},
|
|
192
303
|
): Response {
|
|
193
304
|
let composed =
|
|
@@ -239,6 +350,8 @@ export function rawHtmlResponse(
|
|
|
239
350
|
// `chrome.html`, or a fragment spliced into the generated `renderRawChrome`
|
|
240
351
|
// wrapper — is already present when the delegated listener binds.
|
|
241
352
|
composed = injectBodyEnd(composed, interactionTrackingScript(opts.nonce));
|
|
353
|
+
composed = injectBodyEnd(composed, asyncCssSwapScript(opts.nonce));
|
|
354
|
+
composed = resolveCanonicalHref(composed, opts.canonicalUrl);
|
|
242
355
|
const doc = prefixTenantLinks(composed, opts.basePath);
|
|
243
356
|
return new Response(doc, {
|
|
244
357
|
headers: { "content-type": "text/html; charset=utf-8" },
|
|
@@ -8,25 +8,53 @@
|
|
|
8
8
|
* pilot CHILD space holds the content instances. See DECISIONS.md for the
|
|
9
9
|
* parent/child + Shared Components plan.
|
|
10
10
|
*/
|
|
11
|
-
import type { ProductSectionSource } from "@tot/public-runtime";
|
|
11
|
+
import type { ProductSectionSource, FeaturedProductPresentation } from "@tot/public-runtime";
|
|
12
12
|
import type { ProseSection } from "@/lib/content/prose";
|
|
13
13
|
import type { Tier } from "@/lib/membership/model";
|
|
14
|
+
import type { TrustStripItem } from "@/lib/content/trustStrip";
|
|
15
|
+
import type { PromoTilePresentation } from "@tot/public-runtime";
|
|
14
16
|
|
|
15
17
|
export type HomeBlock =
|
|
16
18
|
| HeroBlock
|
|
17
19
|
| PromoTilesBlock
|
|
20
|
+
| CommerceTrustStripBlock
|
|
18
21
|
| FeaturedCollectionsBlock
|
|
19
22
|
| FeaturedProductsBlock
|
|
20
23
|
| MembershipBandBlock
|
|
21
24
|
| EditorialBlock
|
|
22
|
-
| NewsletterBlock
|
|
25
|
+
| NewsletterBlock
|
|
26
|
+
| FaqBlock;
|
|
27
|
+
|
|
28
|
+
export interface CommerceTrustStripBlock {
|
|
29
|
+
component: "trust_strip";
|
|
30
|
+
items: TrustStripItem[];
|
|
31
|
+
/** Optional small-screen treatment. Claims remain governed by real-or-absent sanitization. */
|
|
32
|
+
mobile?: {
|
|
33
|
+
hidden?: boolean;
|
|
34
|
+
/** Preferred item labels first; unmatched items retain their authored order. */
|
|
35
|
+
order?: string[];
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PromoTile {
|
|
40
|
+
label: string;
|
|
41
|
+
href: string;
|
|
42
|
+
image?: string;
|
|
43
|
+
imageAlt?: string;
|
|
44
|
+
meta?: string;
|
|
45
|
+
}
|
|
23
46
|
|
|
24
47
|
export interface PromoTilesBlock {
|
|
25
48
|
component: "promo_tiles";
|
|
49
|
+
eyebrow?: string;
|
|
26
50
|
title?: string;
|
|
27
51
|
/** A row of promotional tiles (e.g. "New Additions", "On Sale") that link to
|
|
28
52
|
* collections — captured from the merchant's homepage. */
|
|
29
|
-
tiles:
|
|
53
|
+
tiles: PromoTile[];
|
|
54
|
+
/** Defaults to the original image-overlay card. */
|
|
55
|
+
presentation?: PromoTilePresentation;
|
|
56
|
+
ctaLabel?: string;
|
|
57
|
+
ctaHref?: string;
|
|
30
58
|
}
|
|
31
59
|
|
|
32
60
|
export interface HeroBlock {
|
|
@@ -44,6 +72,14 @@ export interface HeroBlock {
|
|
|
44
72
|
/** Direct hero image URL (e.g. the merchant's own homepage hero slide). */
|
|
45
73
|
image?: string;
|
|
46
74
|
imageAlt?: string;
|
|
75
|
+
/** Optional static, governed campaign slides. The first slide is the LCP image. */
|
|
76
|
+
slides?: Array<{
|
|
77
|
+
image: string;
|
|
78
|
+
imageAlt: string;
|
|
79
|
+
headline?: string;
|
|
80
|
+
ctaLabel?: string;
|
|
81
|
+
ctaHref?: string;
|
|
82
|
+
}>;
|
|
47
83
|
}
|
|
48
84
|
|
|
49
85
|
export interface FeaturedCollectionsBlock {
|
|
@@ -60,6 +96,8 @@ export interface FeaturedProductsBlock {
|
|
|
60
96
|
source?: ProductSectionSource;
|
|
61
97
|
handles?: string[];
|
|
62
98
|
limit?: number;
|
|
99
|
+
/** Defaults to the existing grid. Carousel is static/CSS scroll output. */
|
|
100
|
+
presentation?: FeaturedProductPresentation;
|
|
63
101
|
}
|
|
64
102
|
|
|
65
103
|
/**
|
|
@@ -84,6 +122,9 @@ export interface EditorialBlock {
|
|
|
84
122
|
title: string;
|
|
85
123
|
body_html: string;
|
|
86
124
|
imageSeed?: string;
|
|
125
|
+
/** Rehosted editorial photography; preferred over a generated placeholder. */
|
|
126
|
+
image?: string;
|
|
127
|
+
imageAlt?: string;
|
|
87
128
|
align?: "left" | "right";
|
|
88
129
|
ctaLabel?: string;
|
|
89
130
|
ctaHref?: string;
|
|
@@ -211,6 +252,8 @@ export interface FaqBlock {
|
|
|
211
252
|
component: "faq";
|
|
212
253
|
eyebrow?: string;
|
|
213
254
|
title: string;
|
|
255
|
+
/** Optional plain-text introduction rendered below the FAQ heading. */
|
|
256
|
+
lede?: string;
|
|
214
257
|
items: { q: string; a: string }[];
|
|
215
258
|
}
|
|
216
259
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { tenantDirSegment } from "@tot/public-runtime";
|
|
2
|
+
import type { ContentProvider } from "@/lib/storyblok/provider";
|
|
3
|
+
|
|
4
|
+
export type StyleGuideTheme = Record<string, unknown> & {
|
|
5
|
+
id: string;
|
|
6
|
+
role?: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
status?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const localThemeModules = import.meta.glob<Record<string, unknown>>(
|
|
13
|
+
[
|
|
14
|
+
"../../../../tenants/*/content/themes/*.json",
|
|
15
|
+
"../../../../tenants/e2e/*/content/themes/*.json",
|
|
16
|
+
],
|
|
17
|
+
{ eager: true, import: "default" },
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const localChromeModules = import.meta.glob<Record<string, unknown>>(
|
|
21
|
+
[
|
|
22
|
+
"../../../../tenants/*/content/chrome.json",
|
|
23
|
+
"../../../../tenants/e2e/*/content/chrome.json",
|
|
24
|
+
],
|
|
25
|
+
{ eager: true, import: "default" },
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
function isTheme(value: unknown): value is StyleGuideTheme {
|
|
29
|
+
return Boolean(
|
|
30
|
+
value &&
|
|
31
|
+
typeof value === "object" &&
|
|
32
|
+
typeof (value as { id?: unknown }).id === "string",
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function localThemes(tenant: string): StyleGuideTheme[] {
|
|
37
|
+
const segment = `${tenantDirSegment(tenant)}content/themes/`;
|
|
38
|
+
return Object.entries(localThemeModules)
|
|
39
|
+
.filter(([path]) => path.includes(segment))
|
|
40
|
+
.map(([, value]) => value)
|
|
41
|
+
.filter(isTheme);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function contentBelongsToRequestedTenant(
|
|
45
|
+
requestedTenant: string,
|
|
46
|
+
resolvedTenant: { tenant_id: string; appDomain?: string },
|
|
47
|
+
): boolean {
|
|
48
|
+
const requested = requestedTenant.toLowerCase();
|
|
49
|
+
return [resolvedTenant.tenant_id, resolvedTenant.appDomain]
|
|
50
|
+
.filter((value): value is string => Boolean(value))
|
|
51
|
+
.some((value) => value.toLowerCase() === requested);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Read registered-tenant descriptors from the request's pinned/published
|
|
56
|
+
* content before falling back to build-time colocated tenants.
|
|
57
|
+
*/
|
|
58
|
+
export async function loadStyleGuideThemes(
|
|
59
|
+
requestedTenant: string,
|
|
60
|
+
resolvedTenant: { tenant_id: string; appDomain?: string },
|
|
61
|
+
content: Pick<ContentProvider, "listArtifactPaths" | "readArtifact">,
|
|
62
|
+
): Promise<StyleGuideTheme[]> {
|
|
63
|
+
if (contentBelongsToRequestedTenant(requestedTenant, resolvedTenant)) {
|
|
64
|
+
try {
|
|
65
|
+
const paths = await content.listArtifactPaths("themes/");
|
|
66
|
+
if (paths !== null) {
|
|
67
|
+
const themes = (
|
|
68
|
+
await Promise.all(
|
|
69
|
+
paths
|
|
70
|
+
.filter((path) => path.startsWith("themes/") && path.endsWith(".json"))
|
|
71
|
+
.map(async (path) => {
|
|
72
|
+
const raw = await content.readArtifact(path);
|
|
73
|
+
if (!raw) return null;
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(raw) as unknown;
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}),
|
|
80
|
+
)
|
|
81
|
+
).filter(isTheme);
|
|
82
|
+
if (themes.length > 0) return themes;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// A colocated build-time descriptor is the availability fallback.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return localThemes(requestedTenant);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function loadStyleGuideChrome(
|
|
93
|
+
requestedTenant: string,
|
|
94
|
+
resolvedTenant: { tenant_id: string; appDomain?: string },
|
|
95
|
+
content: Pick<ContentProvider, "getChrome">,
|
|
96
|
+
): Promise<Record<string, unknown> | null> {
|
|
97
|
+
if (contentBelongsToRequestedTenant(requestedTenant, resolvedTenant)) {
|
|
98
|
+
try {
|
|
99
|
+
return (await content.getChrome()) as unknown as Record<string, unknown>;
|
|
100
|
+
} catch {
|
|
101
|
+
// A colocated build-time descriptor is the availability fallback.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const segment = `${tenantDirSegment(requestedTenant)}content/chrome.json`;
|
|
106
|
+
return Object.entries(localChromeModules).find(([path]) => path.includes(segment))?.[1] ?? null;
|
|
107
|
+
}
|