@tokenoftrust/storefront-runner 2.0.14 → 2.0.15
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/public/shared/commerce-chrome.css +5 -0
- package/apps/storefront/src/components/chrome/SiteFooter.astro +4 -1
- package/apps/storefront/src/layouts/Layout.astro +8 -6
- package/apps/storefront/src/lib/chrome/footerLegal.ts +52 -0
- package/apps/storefront/src/lib/chrome/trackingPlacements.ts +4 -3
- package/apps/storefront/src/lib/rawChrome.ts +26 -4
- package/package.json +1 -1
|
@@ -339,6 +339,11 @@ a.article-card__tag:hover { text-decoration: underline; }
|
|
|
339
339
|
.footer-warning p { color: color-mix(in srgb, var(--color-primary-contrast) 78%, transparent); font-size: .82rem; }
|
|
340
340
|
.footer-bottom { margin-top: 28px; padding-top: 18px; border-top: 1px solid color-mix(in srgb, var(--color-primary-contrast) 16%, transparent); display: flex; flex-wrap: wrap; gap: 10px 24px; justify-content: space-between; align-items: center; }
|
|
341
341
|
.footer-bottom p, .footer-bottom a { color: color-mix(in srgb, var(--color-primary-contrast) 60%, transparent); font-size: .82rem; }
|
|
342
|
+
/* The Legal link row (a "Legal"-titled footer column, moved here — see
|
|
343
|
+
lib/chrome/footerLegal.ts). Wraps under the copyright at narrow widths
|
|
344
|
+
rather than overflowing the bar. */
|
|
345
|
+
.footer-bottom nav ul { display: flex; flex-wrap: wrap; gap: 6px 20px; list-style: none; margin: 0; padding: 0; }
|
|
346
|
+
.footer-bottom nav a:hover { color: var(--color-primary-contrast); text-decoration: underline; }
|
|
342
347
|
|
|
343
348
|
/* --- Reveal-on-scroll (raw-chrome markup uses .reveal/.in) ---------------- */
|
|
344
349
|
.reveal { opacity: 0; transform: translateY(18px); transition: opacity .5s ease, transform .5s ease; }
|
|
@@ -27,7 +27,10 @@ interface Props {
|
|
|
27
27
|
compliance?: string;
|
|
28
28
|
/** Legal bottom-bar line (e.g. a composed "© 2026 Acme · some legal note" string). */
|
|
29
29
|
legal?: string;
|
|
30
|
-
/** Bottom-bar policy links (e.g. Privacy Policy, Terms), shown alongside `legal`.
|
|
30
|
+
/** Bottom-bar policy links (e.g. Privacy Policy, Terms), shown alongside `legal`.
|
|
31
|
+
* The links of a `Legal`-titled footer column, which the caller MOVES out of
|
|
32
|
+
* `columns` (lib/chrome/footerLegal.ts) — passing them in both places would
|
|
33
|
+
* render each one twice. */
|
|
31
34
|
bottomLinks?: ChromeLink[];
|
|
32
35
|
class?: string;
|
|
33
36
|
}
|
|
@@ -25,6 +25,7 @@ import { organizationLd, websiteLd } from "@/lib/jsonld";
|
|
|
25
25
|
import { nonceInlineTags, CHROME_BODY_MARKER } from "@/lib/rawMarketingHtml";
|
|
26
26
|
import { reportTrackingChainHealth } from "@/lib/tracking/interactionHealth";
|
|
27
27
|
import { resolveHeaderVariant, resolveFooterVariant } from "@/lib/chrome/variant";
|
|
28
|
+
import { splitFooterLegal } from "@/lib/chrome/footerLegal";
|
|
28
29
|
import type { HeaderVariant, FooterVariant } from "@/lib/chrome/model";
|
|
29
30
|
import { renderIntegrationPhases, INTERACTION_TRACKING_BOOT_JS } from "@tot/public-runtime";
|
|
30
31
|
import { readViewerSession } from "@/lib/auth/route";
|
|
@@ -208,11 +209,12 @@ const headerLogo = brandLogoValid ?? themeHeaderLogo;
|
|
|
208
209
|
const headerWordmark = brand?.label ?? tenant.displayName;
|
|
209
210
|
const headerBrandHref = brand?.href;
|
|
210
211
|
|
|
211
|
-
// Footer bottom bar: a "Legal"-titled column
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
const
|
|
215
|
-
|
|
212
|
+
// Footer bottom bar: a "Legal"-titled column MOVES out of the grid and its
|
|
213
|
+
// links render beside the copyright — one shared split with the raw renderer
|
|
214
|
+
// (lib/chrome/footerLegal.ts), so neither surface can duplicate them.
|
|
215
|
+
const { gridColumns: footerColumns, legalLinks: bottomLinks } = splitFooterLegal(
|
|
216
|
+
chrome.footer.columns,
|
|
217
|
+
);
|
|
216
218
|
const legalBase = chrome.footer.bottom?.copyright ?? `© ${new Date().getFullYear()} ${tenant.displayName}`;
|
|
217
219
|
const legalLine = `${legalBase}${chrome.footer.bottom?.note ? ` · ${chrome.footer.bottom.note}` : ""}`;
|
|
218
220
|
|
|
@@ -386,7 +388,7 @@ const footerWidgets = await renderWidgetSlot(
|
|
|
386
388
|
contact: chrome.footer.brand?.text ? [chrome.footer.brand.text] : undefined,
|
|
387
389
|
}}
|
|
388
390
|
variant={resolvedFooterVariant}
|
|
389
|
-
columns={
|
|
391
|
+
columns={footerColumns}
|
|
390
392
|
newsletter={features.newsletter ? { title: "Newsletter", placeholder: "you@example.com" } : undefined}
|
|
391
393
|
social={chrome.footer.social}
|
|
392
394
|
legal={legalLine}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The footer's Legal split — ONE source of truth for both renderers (the
|
|
3
|
+
* raw-string footer in lib/rawChrome.ts and the component footer in
|
|
4
|
+
* components/chrome/SiteFooter.astro), so a tenant's `Legal` column can never
|
|
5
|
+
* land in the grid on one surface and in the bottom bar on the other.
|
|
6
|
+
*
|
|
7
|
+
* Contract, title-based (no extra schema field): a footer column titled
|
|
8
|
+
* `Legal` — case-insensitive, surrounding whitespace ignored — is not a grid
|
|
9
|
+
* column. Its links render in the bottom bar beside the copyright, inside a
|
|
10
|
+
* single `<nav aria-label="Legal">`.
|
|
11
|
+
*
|
|
12
|
+
* The column is MOVED, never copied: it leaves `.footer-grid` entirely, so
|
|
13
|
+
* each legal link appears exactly once in the DOM. Each link keeps its own
|
|
14
|
+
* governed identity (`data-tot-el`) and action (`data-tot-action`); only the
|
|
15
|
+
* placement changes, to `footer-bottom`, which is what that placement means.
|
|
16
|
+
*
|
|
17
|
+
* A tenant with no `Legal` column gets a text-only bottom bar — the other
|
|
18
|
+
* columns stay in the grid rather than being flattened into a second copy of
|
|
19
|
+
* themselves under the copyright.
|
|
20
|
+
*/
|
|
21
|
+
import type { ChromeLink, FooterColumn } from "./model.js";
|
|
22
|
+
|
|
23
|
+
/** The bottom-bar nav's accessible name. Exactly one is emitted per footer. */
|
|
24
|
+
export const LEGAL_NAV_LABEL = "Legal";
|
|
25
|
+
|
|
26
|
+
/** The title-based contract: `Legal`, case- and surrounding-whitespace-insensitive. */
|
|
27
|
+
export function isLegalColumnTitle(title: string): boolean {
|
|
28
|
+
return title.trim().toLowerCase() === LEGAL_NAV_LABEL.toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface FooterLegalSplit {
|
|
32
|
+
/** The columns that stay in `.footer-grid`, in authored order. */
|
|
33
|
+
gridColumns: FooterColumn[];
|
|
34
|
+
/** The extracted bottom-bar links, in authored order. Empty when no Legal column. */
|
|
35
|
+
legalLinks: ChromeLink[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Partition footer columns into what the grid renders and what the bottom bar
|
|
40
|
+
* renders. Every `Legal`-titled column is extracted (a config with two is a
|
|
41
|
+
* tenant mistake, not a reason to emit a second nav) and its links concatenated
|
|
42
|
+
* in authored order.
|
|
43
|
+
*/
|
|
44
|
+
export function splitFooterLegal(columns: readonly FooterColumn[]): FooterLegalSplit {
|
|
45
|
+
const gridColumns: FooterColumn[] = [];
|
|
46
|
+
const legalLinks: ChromeLink[] = [];
|
|
47
|
+
for (const column of columns) {
|
|
48
|
+
if (isLegalColumnTitle(column.title)) legalLinks.push(...column.links);
|
|
49
|
+
else gridColumns.push(column);
|
|
50
|
+
}
|
|
51
|
+
return { gridColumns, legalLinks };
|
|
52
|
+
}
|
|
@@ -24,9 +24,10 @@ export const CHROME_PLACEMENT = {
|
|
|
24
24
|
/** Footer link columns. */
|
|
25
25
|
footer: "footer",
|
|
26
26
|
/**
|
|
27
|
-
* Footer bottom legal/policy link row
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* Footer bottom legal/policy link row, on either surface — the links of a
|
|
28
|
+
* `Legal`-titled footer column, which both renderers move out of the grid and
|
|
29
|
+
* into the bottom bar (lib/chrome/footerLegal.ts). `ChromeFooter.bottom`
|
|
30
|
+
* itself carries text only.
|
|
30
31
|
*/
|
|
31
32
|
footerBottom: "footer-bottom",
|
|
32
33
|
/** The mobile nav drawer (MobileNav island) and its no-JS accordion fallback. */
|
|
@@ -28,6 +28,7 @@ import { CHROME_BODY_MARKER, CHROME_HEAD_MARKER } from "./rawMarketingHtml.js";
|
|
|
28
28
|
import { isCurrentNavHref } from "./chrome/navCurrent.js";
|
|
29
29
|
import { ctaIdentityAttr } from "./chrome/ctaIdentity.js";
|
|
30
30
|
import { CHROME_PLACEMENT } from "./chrome/trackingPlacements.js";
|
|
31
|
+
import { LEGAL_NAV_LABEL, splitFooterLegal } from "./chrome/footerLegal.js";
|
|
31
32
|
import { trackingAttributesHtml } from "@tot/public-runtime";
|
|
32
33
|
import type {
|
|
33
34
|
ChromeAgeGate,
|
|
@@ -391,7 +392,11 @@ function renderFooter(footer: ChromeFooter): string {
|
|
|
391
392
|
return ` <div class="footer-brand">${logo}${text}${news}</div>\n`;
|
|
392
393
|
})()
|
|
393
394
|
: "";
|
|
394
|
-
|
|
395
|
+
// A `Legal`-titled column moves to the bottom bar (lib/chrome/footerLegal.ts)
|
|
396
|
+
// — scope indices count RENDERED grid columns, so they describe where a link
|
|
397
|
+
// actually is rather than where it sat in the authored array.
|
|
398
|
+
const { gridColumns, legalLinks } = splitFooterLegal(footer.columns);
|
|
399
|
+
const columns = gridColumns
|
|
395
400
|
.map((c, colIndex) => {
|
|
396
401
|
const links = c.links
|
|
397
402
|
.map((l, i) => {
|
|
@@ -425,11 +430,28 @@ function renderFooter(footer: ChromeFooter): string {
|
|
|
425
430
|
const warning = footer.warning
|
|
426
431
|
? `\n <div class="footer-warning"><p><strong>WARNING:</strong> ${esc(footer.warning)}</p></div>`
|
|
427
432
|
: "";
|
|
428
|
-
const
|
|
429
|
-
?
|
|
433
|
+
const legalNav = legalLinks.length
|
|
434
|
+
? `<nav aria-label="${LEGAL_NAV_LABEL}"><ul>${legalLinks
|
|
435
|
+
.map((l, i) => {
|
|
436
|
+
const stamp = trackingAttributesHtml({
|
|
437
|
+
el: l.id,
|
|
438
|
+
action: l.action,
|
|
439
|
+
tracking: l.tracking,
|
|
440
|
+
placement: CHROME_PLACEMENT.footerBottom,
|
|
441
|
+
scope: "footer-bottom",
|
|
442
|
+
index: i,
|
|
443
|
+
});
|
|
444
|
+
return `<li><a href="${esc(l.href)}"${relFor(l)}${stamp}>${esc(l.label)}</a></li>`;
|
|
445
|
+
})
|
|
446
|
+
.join("")}</ul></nav>`
|
|
447
|
+
: "";
|
|
448
|
+
const bottomText = footer.bottom
|
|
449
|
+
? `${footer.bottom.copyright ? `<p>${esc(footer.bottom.copyright)}</p>` : ""}${
|
|
430
450
|
footer.bottom.note ? `<p>${esc(footer.bottom.note)}</p>` : ""
|
|
431
|
-
}
|
|
451
|
+
}`
|
|
432
452
|
: "";
|
|
453
|
+
const bottom =
|
|
454
|
+
bottomText || legalNav ? `\n <div class="footer-bottom">${bottomText}${legalNav}</div>` : "";
|
|
433
455
|
return (
|
|
434
456
|
`<footer class="site-footer">\n` +
|
|
435
457
|
` <div class="container">\n` +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.15",
|
|
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",
|