@tokenoftrust/storefront-runner 2.2.126 → 2.3.0
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/package.json +3 -1
- package/apps/storefront/public/shared/commerce-chrome.css +57 -15
- package/apps/storefront/public/shared/commerce-marketing.css +7 -7
- package/apps/storefront/src/components/Badge.astro +1 -1
- package/apps/storefront/src/components/Breadcrumbs.astro +8 -3
- package/apps/storefront/src/components/Section.astro +4 -1
- package/apps/storefront/src/components/Seo.astro +7 -5
- package/apps/storefront/src/components/blog/BlogIndexView.astro +22 -13
- package/apps/storefront/src/components/blog/ShareRow.astro +43 -0
- package/apps/storefront/src/components/chrome/AnnouncementBar.astro +1 -1
- package/apps/storefront/src/components/chrome/NavDropdown.astro +7 -7
- package/apps/storefront/src/components/chrome/SiteFooter.astro +17 -0
- package/apps/storefront/src/components/chrome/SiteHeader.astro +5 -5
- package/apps/storefront/src/components/commerce/TagFilterCloud.astro +1 -1
- package/apps/storefront/src/components/compliance/NicotineWarning.astro +12 -22
- package/apps/storefront/src/components/compliance/PactActNotice.astro +9 -10
- package/apps/storefront/src/components/content/ProseSections.astro +2 -2
- package/apps/storefront/src/components/home/Hero.astro +34 -12
- package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +39 -5
- package/apps/storefront/src/components/islands/VariantSelector.tsx +3 -3
- package/apps/storefront/src/components/marketing/CardGrid.astro +1 -1
- package/apps/storefront/src/components/marketing/CodeSample.astro +1 -1
- package/apps/storefront/src/components/marketing/CtaBand.astro +1 -1
- package/apps/storefront/src/components/marketing/Faq.astro +1 -1
- package/apps/storefront/src/components/marketing/Integrations.astro +1 -1
- package/apps/storefront/src/components/marketing/MarketingCtas.astro +1 -1
- package/apps/storefront/src/components/marketing/MarketingHero.astro +1 -1
- package/apps/storefront/src/components/marketing/ProofStrip.astro +1 -1
- package/apps/storefront/src/components/marketing/SplitCompare.astro +1 -1
- package/apps/storefront/src/components/marketing/Steps.astro +1 -1
- package/apps/storefront/src/components/marketing/Testimonials.astro +2 -2
- package/apps/storefront/src/components/marketing/TrustBar.astro +1 -1
- package/apps/storefront/src/components/membership/TierComparisonTable.astro +1 -1
- package/apps/storefront/src/components/plp/ActiveFilters.astro +1 -1
- package/apps/storefront/src/components/plp/FacetSidebar.astro +4 -4
- package/apps/storefront/src/components/plp/Pagination.astro +1 -1
- package/apps/storefront/src/components/plp/SortSelect.astro +1 -1
- package/apps/storefront/src/components/style-guide/StyleGuideNav.astro +18 -3
- package/apps/storefront/src/components/the-build/TheBuildIndexView.astro +9 -5
- package/apps/storefront/src/config/devTenantSeed.ts +20 -34
- package/apps/storefront/src/config/resolver.ts +0 -38
- package/apps/storefront/src/layouts/Layout.astro +8 -1
- package/apps/storefront/src/lib/assets/preview-version-cookie.ts +52 -5
- package/apps/storefront/src/lib/blog/pagination.ts +20 -10
- package/apps/storefront/src/lib/blog/presentation.ts +94 -0
- package/apps/storefront/src/lib/blog/rss.ts +1 -1
- package/apps/storefront/src/lib/blog/types.ts +24 -7
- package/apps/storefront/src/lib/breadcrumbs.ts +25 -0
- package/apps/storefront/src/lib/chrome/model.ts +4 -0
- package/apps/storefront/src/lib/cloudflare-workers.d.ts +53 -0
- package/apps/storefront/src/lib/compliance/rawComplianceNotices.ts +38 -18
- package/apps/storefront/src/lib/dashboard/delegateGate.ts +28 -0
- package/apps/storefront/src/lib/homeVisualParity.ts +8 -5
- package/apps/storefront/src/lib/jsonld.ts +5 -5
- package/apps/storefront/src/lib/pinnedRouteRewriteBoundary.ts +20 -2
- package/apps/storefront/src/lib/rawChrome.ts +74 -12
- package/apps/storefront/src/lib/runtimeEnv.ts +1 -1
- package/apps/storefront/src/lib/seo/documentTitle.ts +19 -0
- package/apps/storefront/src/lib/shared/addToRelease.ts +32 -0
- package/apps/storefront/src/lib/storyblok/content-model.ts +3 -0
- package/apps/storefront/src/lib/styleGuideThemes.ts +20 -22
- package/apps/storefront/src/pages/[...slug].astro +13 -8
- package/apps/storefront/src/pages/blog/[slug].astro +56 -24
- package/apps/storefront/src/pages/blog/author/[author].astro +17 -9
- package/apps/storefront/src/pages/blog/category/[category].astro +18 -10
- package/apps/storefront/src/pages/blog/index.astro +2 -0
- package/apps/storefront/src/pages/blog/page/[n].astro +2 -0
- package/apps/storefront/src/pages/blog/tag/[tag].astro +18 -10
- package/apps/storefront/src/pages/collections/[handle].astro +5 -4
- package/apps/storefront/src/pages/collections/index.astro +4 -3
- package/apps/storefront/src/pages/index.astro +34 -27
- package/apps/storefront/src/pages/products/[handle].astro +5 -4
- package/apps/storefront/src/pages/saved.astro +4 -3
- package/apps/storefront/src/pages/search.astro +4 -3
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +129 -118
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +19 -15
- package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +23 -27
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +7 -14
- package/apps/storefront/src/pages/style-guide/index.astro +6 -5
- package/apps/storefront/src/pages/tenants/[id]/[...path].ts +32 -7
- package/apps/storefront/src/pages/the-build/[slug].astro +44 -20
- package/apps/storefront/src/pages/the-build/index.astro +2 -0
- package/apps/storefront/src/pages/the-build/page/[n].astro +2 -0
- package/apps/storefront/src/styles/admin.css +47 -0
- package/apps/storefront/src/styles/global.css +104 -56
- package/apps/storefront/src/themes/derivedTokens.ts +34 -0
- package/apps/storefront/src/themes/schema.ts +6 -0
- package/apps/storefront/test/pipeline/harness.ts +164 -0
- package/apps/storefront/test/pipeline/intake-worker.mjs +14 -0
- package/apps/storefront/test/pipeline/pipeline-host.ts +77 -0
- package/apps/storefront/test/pipeline/synthetic-job.ts +156 -0
- package/apps/storefront/vitest.workers.config.cts +67 -0
- package/docs/widget-library-guide.md +1496 -0
- package/package.json +1 -1
- package/packages/cli/src/declared-config.mjs +400 -5
- package/packages/public-runtime/package.json +1 -0
- package/packages/public-runtime/src/chrome.ts +93 -2
- package/packages/public-runtime/src/compliance-coherence.d.mts +17 -0
- package/packages/public-runtime/src/compliance-coherence.mjs +84 -0
- package/packages/public-runtime/src/declared-tenant-config.d.mts +1 -1
- package/packages/public-runtime/src/declared-tenant-config.mjs +6 -2
- package/packages/public-runtime/src/extension-contract-values.d.mts +4 -1
- package/packages/public-runtime/src/extension-contract-values.mjs +14 -1
- package/packages/public-runtime/src/extension-contract.ts +2 -0
- package/packages/public-runtime/src/tenant.ts +48 -4
- package/pnpm-lock.runner.yaml +469 -0
- package/scripts/tenant/validate.mjs +27 -1
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"preview": "wrangler dev",
|
|
15
15
|
"typecheck": "astro check && tsc --noEmit",
|
|
16
16
|
"astro": "astro",
|
|
17
|
-
"test": "NODE_OPTIONS=--experimental-sqlite vitest run"
|
|
17
|
+
"test": "NODE_OPTIONS=--experimental-sqlite vitest run && pnpm run test:pipeline"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@astrojs/cloudflare": "^14.0.1",
|
|
@@ -40,11 +40,13 @@
|
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@astrojs/check": "^0.9.4",
|
|
42
42
|
"@axe-core/playwright": "^4.12.1",
|
|
43
|
+
"@cloudflare/vitest-pool-workers": "0.12.21",
|
|
43
44
|
"@playwright/test": "^1.61.1",
|
|
44
45
|
"axe-core": "^4.12.1",
|
|
45
46
|
"dependency-cruiser": "^18.0.0",
|
|
46
47
|
"drizzle-kit": "^0.31.10",
|
|
47
48
|
"drizzle-orm": "^0.45.2",
|
|
49
|
+
"esbuild": "0.28.1",
|
|
48
50
|
"tsx": "^4.22.4",
|
|
49
51
|
"typescript": "^5.6.3",
|
|
50
52
|
"vitest": "2.1.8",
|
|
@@ -6,6 +6,22 @@
|
|
|
6
6
|
This linked platform asset is served at /shared/commerce-chrome.css.
|
|
7
7
|
rawMarketingHtml.prefixTenantLinks leaves /shared/ paths unprefixed. */
|
|
8
8
|
|
|
9
|
+
/* Accent-derived token defaults for pages that load this sheet without the
|
|
10
|
+
app's global.css (raw pages). Same definitions as global.css; layered, so a
|
|
11
|
+
tenant theme's unlayered value always wins. */
|
|
12
|
+
@layer tot-token-defaults {
|
|
13
|
+
:root {
|
|
14
|
+
--color-accent-ink: color-mix(in srgb, var(--color-accent) 84%, black);
|
|
15
|
+
--color-accent-contrast: var(--color-primary-contrast);
|
|
16
|
+
}
|
|
17
|
+
@supports (color: oklch(from red l c h)) {
|
|
18
|
+
:root {
|
|
19
|
+
--color-accent-ink: oklch(from var(--color-accent) min(l, 0.5) c h);
|
|
20
|
+
--color-accent-contrast: oklch(from var(--color-accent) clamp(0, (0.58 - l) * 1000, 1) 0 0);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
9
25
|
/* --- Layout --------------------------------------------------------------- */
|
|
10
26
|
/* Clip (not hidden) so a closed off-canvas mobile drawer never creates a
|
|
11
27
|
horizontal scrollbar, while position:sticky on the header still works. */
|
|
@@ -31,13 +47,13 @@ body { overflow-x: clip; }
|
|
|
31
47
|
.btn:hover { transform: translateY(-1px); }
|
|
32
48
|
.btn-primary { background: var(--color-primary); color: var(--color-primary-contrast); }
|
|
33
49
|
.btn-primary:hover { background: var(--color-primary-hover); box-shadow: var(--shadow-md); }
|
|
34
|
-
.btn-secondary { background: var(--color-accent); color: var(--color-
|
|
50
|
+
.btn-secondary { background: var(--color-accent); color: var(--color-accent-contrast); }
|
|
35
51
|
.btn-secondary:hover { background: var(--color-accent-hover); }
|
|
36
52
|
.btn-line { border-color: var(--color-primary-contrast); color: var(--color-primary-contrast); }
|
|
37
53
|
.btn-line:hover { background: var(--color-primary-contrast); color: var(--color-primary); }
|
|
38
54
|
.btn-ghost { border-color: var(--color-border); color: var(--color-primary); background: var(--color-surface); }
|
|
39
55
|
.btn-ghost:hover { border-color: var(--color-primary); }
|
|
40
|
-
.btn-outline { background: transparent; color: var(--color-accent); border-color: var(--color-border); }
|
|
56
|
+
.btn-outline { background: transparent; color: var(--color-accent-ink); border-color: var(--color-border); }
|
|
41
57
|
.btn-outline:hover { border-color: var(--color-accent); }
|
|
42
58
|
|
|
43
59
|
/* --- Announcement variants ------------------------------------------------ */
|
|
@@ -58,11 +74,9 @@ body { overflow-x: clip; }
|
|
|
58
74
|
@media (prefers-reduced-motion: reduce) {
|
|
59
75
|
.announce-marquee__track { animation: none; justify-content: center; width: 100%; }
|
|
60
76
|
}
|
|
61
|
-
/* (3) dismissible promo bar
|
|
62
|
-
token
|
|
63
|
-
|
|
64
|
-
caught as a real color-contrast failure once accent is a deep brand hue). */
|
|
65
|
-
.announce-bar { background: var(--color-accent); color: var(--color-primary-contrast); }
|
|
77
|
+
/* (3) dismissible promo bar: accent fill with --color-accent-contrast text, the
|
|
78
|
+
token that reads on the accent whether the accent is light or deep. */
|
|
79
|
+
.announce-bar { background: var(--color-accent); color: var(--color-accent-contrast); }
|
|
66
80
|
.announce-bar[hidden] { display: none; }
|
|
67
81
|
/* A returning visitor who already dismissed the bar gets `.announce-dismissed`
|
|
68
82
|
stamped on <html> pre-paint (see rawChrome.ts's head script) so it never
|
|
@@ -70,9 +84,9 @@ body { overflow-x: clip; }
|
|
|
70
84
|
.announce-dismissed .announce-bar { display: none; }
|
|
71
85
|
.announce-bar .container { display: flex; align-items: center; justify-content: center; gap: 14px; padding-block: 9px; position: relative; }
|
|
72
86
|
.announce-bar__msg { font-size: .875rem; font-weight: 700; letter-spacing: .01em; text-align: center; }
|
|
73
|
-
.announce-bar__msg a { color: var(--color-
|
|
74
|
-
.announce-bar__close { position: absolute; right: clamp(16px, 4vw, 40px); background: none; border: 0; color: var(--color-
|
|
75
|
-
.announce-bar__close:hover { background: color-mix(in srgb, var(--color-
|
|
87
|
+
.announce-bar__msg a { color: var(--color-accent-contrast); text-decoration: underline; }
|
|
88
|
+
.announce-bar__close { position: absolute; right: clamp(16px, 4vw, 40px); background: none; border: 0; color: var(--color-accent-contrast); font-size: 1.1rem; line-height: 1; padding: 4px 6px; border-radius: var(--radius-sm); }
|
|
89
|
+
.announce-bar__close:hover { background: color-mix(in srgb, var(--color-accent-contrast) 12%, transparent); }
|
|
76
90
|
|
|
77
91
|
/* --- Top utility bar (FDA warning + utility nav) -------------------------- */
|
|
78
92
|
.topbar-warning {
|
|
@@ -98,9 +112,9 @@ body { overflow-x: clip; }
|
|
|
98
112
|
.utility-nav__right .social a svg { width: 16px; height: 16px; fill: var(--color-text); }
|
|
99
113
|
|
|
100
114
|
/* Member-price affordance (persistent, guests) */
|
|
101
|
-
.member-cue { display: inline-flex; align-items: center; gap: 7px; font-size: .8rem; font-weight: 700; color: var(--color-accent); }
|
|
115
|
+
.member-cue { display: inline-flex; align-items: center; gap: 7px; font-size: .8rem; font-weight: 700; color: var(--color-accent-ink); }
|
|
102
116
|
.member-cue::before { content: ""; width: 8px; height: 8px; border-radius: var(--radius-full); background: var(--color-accent); }
|
|
103
|
-
.member-cue:hover { color: var(--color-accent-
|
|
117
|
+
.member-cue:hover { color: var(--color-accent-ink); text-decoration: underline; }
|
|
104
118
|
|
|
105
119
|
/* --- Header / nav --------------------------------------------------------- */
|
|
106
120
|
.site-header { position: sticky; top: 0; z-index: 100; background: var(--color-surface); border-bottom: 1px solid var(--color-border); }
|
|
@@ -142,11 +156,25 @@ body { overflow-x: clip; }
|
|
|
142
156
|
white-space: nowrap; transition: transform .15s ease, opacity .15s ease;
|
|
143
157
|
}
|
|
144
158
|
.header-cta--primary {
|
|
145
|
-
background: var(--color-accent); color: var(--color-
|
|
159
|
+
background: var(--color-accent); color: var(--color-accent-contrast);
|
|
146
160
|
}
|
|
147
|
-
.header-actions .header-cta--primary:hover { color: var(--color-
|
|
161
|
+
.header-actions .header-cta--primary:hover { color: var(--color-accent-contrast); transform: translateY(-1px); }
|
|
148
162
|
.header-cta--secondary, .header-cta--ghost { border: 1px solid currentColor; }
|
|
149
163
|
.header-actions .header-cta--secondary:hover, .header-actions .header-cta--ghost:hover { opacity: .8; }
|
|
164
|
+
/* Labelled action links (chrome header.actionLinks): text with an optional leading icon. */
|
|
165
|
+
.header-action { gap: 6px; font-size: .875rem; font-weight: 600; white-space: nowrap; }
|
|
166
|
+
.header-actions .header-action svg { width: 20px; height: 20px; }
|
|
167
|
+
.header-actions .header-action.cart .count { position: static; margin-left: 2px; }
|
|
168
|
+
/* A search form with a visible text submit (header.search.buttonLabel). */
|
|
169
|
+
.header-search--labelled { display: flex; }
|
|
170
|
+
.header-search--labelled input { padding-right: 18px; border-radius: var(--radius-full) 0 0 var(--radius-full); }
|
|
171
|
+
.header-search--labelled button {
|
|
172
|
+
position: static; transform: none; flex: none; padding: 0 18px;
|
|
173
|
+
border: 1px solid var(--color-border); border-left: 0; border-radius: 0 var(--radius-full) var(--radius-full) 0;
|
|
174
|
+
background: var(--color-surface-2); color: var(--color-text); font-size: .8rem; font-weight: 700; letter-spacing: .04em;
|
|
175
|
+
}
|
|
176
|
+
/* The menu-drawer copy of search (header.mobile.search "menu") shows only at narrow widths. */
|
|
177
|
+
.header-search--menu { display: none; }
|
|
150
178
|
.hamburger { display: none; flex-direction: column; gap: 5px; background: none; border: 0; padding: 6px; min-width: 44px; min-height: 44px; align-items: center; justify-content: center; }
|
|
151
179
|
.hamburger span { width: 26px; height: 2px; background: var(--color-text); border-radius: 2px; }
|
|
152
180
|
|
|
@@ -372,6 +400,9 @@ a.article-card__tag:hover { text-decoration: underline; }
|
|
|
372
400
|
.footer-warning { margin-top: 30px; padding: 16px 18px; border: 1px solid color-mix(in srgb, var(--color-primary-contrast) 22%, transparent); border-radius: var(--radius-md); }
|
|
373
401
|
.footer-warning strong { color: var(--color-primary-contrast); }
|
|
374
402
|
.footer-warning p { color: color-mix(in srgb, var(--color-primary-contrast) 78%, transparent); font-size: max(.875rem, 14px); }
|
|
403
|
+
/* The merchant's own legal copy (footer.disclaimer): rendered as written, no label. */
|
|
404
|
+
.footer-disclaimer { margin-top: 30px; }
|
|
405
|
+
.footer-disclaimer p { color: color-mix(in srgb, var(--color-primary-contrast) 78%, transparent); font-size: max(.875rem, 14px); }
|
|
375
406
|
.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; }
|
|
376
407
|
.footer-bottom p, .footer-bottom a { color: color-mix(in srgb, var(--color-primary-contrast) 60%, transparent); font-size: .875rem; }
|
|
377
408
|
/* The Legal link row (a "Legal"-titled footer column, moved here — see
|
|
@@ -410,7 +441,8 @@ a.article-card__tag:hover { text-decoration: underline; }
|
|
|
410
441
|
}
|
|
411
442
|
.site-footer[data-variant="light-collapsible"] .footer-warning { border-color: var(--color-border); }
|
|
412
443
|
.site-footer[data-variant="light-collapsible"] .footer-warning strong,
|
|
413
|
-
.site-footer[data-variant="light-collapsible"] .footer-warning p
|
|
444
|
+
.site-footer[data-variant="light-collapsible"] .footer-warning p,
|
|
445
|
+
.site-footer[data-variant="light-collapsible"] .footer-disclaimer p { color: var(--color-text); }
|
|
414
446
|
|
|
415
447
|
/* --- Reveal-on-scroll (raw-chrome markup uses .reveal/.in) ---------------- */
|
|
416
448
|
.reveal { opacity: 0; transform: translateY(18px); transition: opacity .5s ease, transform .5s ease; }
|
|
@@ -441,6 +473,16 @@ a.article-card__tag:hover { text-decoration: underline; }
|
|
|
441
473
|
PRESENTATION, never its reachability. */
|
|
442
474
|
.js-nav .primary-nav { position: fixed; inset: 0 0 0 auto; width: min(84vw, 340px); background: var(--color-surface); box-shadow: var(--shadow-lg); transform: translateX(100%); transition: transform .25s ease; padding: 78px 20px 24px; overflow-y: auto; z-index: 120; }
|
|
443
475
|
.js-nav .primary-nav.open { transform: none; }
|
|
476
|
+
/* header.mobile — menu button side, brand alignment, search placement. */
|
|
477
|
+
.js-nav .site-header[data-mobile-menu="left"] .primary-nav { inset: 0 auto 0 0; transform: translateX(-100%); }
|
|
478
|
+
.js-nav .site-header[data-mobile-menu="left"] .primary-nav.open { transform: none; }
|
|
479
|
+
.site-header[data-mobile-menu="left"] .header-actions,
|
|
480
|
+
.site-header[data-mobile-brand="center"] .header-actions { margin-left: auto; }
|
|
481
|
+
.site-header[data-mobile-brand="center"] .header-row { position: relative; }
|
|
482
|
+
.site-header[data-mobile-brand="center"] .brand { position: absolute; left: 50%; top: 14px; transform: translateX(-50%); }
|
|
483
|
+
.site-header[data-mobile-search="menu"] .header-search:not(.header-search--menu) { display: none; }
|
|
484
|
+
.site-header[data-mobile-search="menu"] .header-search--menu { display: block; max-width: none; margin-bottom: 12px; }
|
|
485
|
+
.site-header[data-mobile-search="menu"] .header-search--menu.header-search--labelled { display: flex; }
|
|
444
486
|
/* Mega panels: inline accordion on mobile */
|
|
445
487
|
.mega { position: static; opacity: 1; visibility: visible; transform: none; box-shadow: none; border: 0; border-radius: 0; padding: 4px 4px 8px 14px; min-width: 0; }
|
|
446
488
|
.mega--wide { min-width: 0; }
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
--border -> --color-border
|
|
41
41
|
--ink / --ink-2 -> --color-text (contract has ONE body/heading ink)
|
|
42
42
|
--muted -> --color-muted
|
|
43
|
-
--brand-blue -> --color-accent
|
|
43
|
+
--brand-blue -> --color-accent (fills) / --color-accent-ink (small text, links)
|
|
44
44
|
--accent (amber fill) -> semantic tint: --color-info (illustrative note) /
|
|
45
45
|
--color-warning (compliance + needs-review flag) mixed
|
|
46
46
|
over --color-surface, so the callout is token-driven
|
|
@@ -76,8 +76,8 @@
|
|
|
76
76
|
|
|
77
77
|
/* --- Breadcrumbs + eyebrow ------------------------------------------------ */
|
|
78
78
|
.crumbs { font-size: .82rem; color: var(--color-muted); margin-bottom: 14px; }
|
|
79
|
-
.crumbs a { color: var(--color-accent); }
|
|
80
|
-
.eyebrow { display: inline-block; font-family: var(--font-display); font-weight: 700; font-size: .76rem; letter-spacing: .12em; text-transform: uppercase; color: var(--color-accent); margin-bottom: .5rem; }
|
|
79
|
+
.crumbs a { color: var(--color-accent-ink); }
|
|
80
|
+
.eyebrow { display: inline-block; font-family: var(--font-display); font-weight: 700; font-size: .76rem; letter-spacing: .12em; text-transform: uppercase; color: var(--color-accent-ink); margin-bottom: .5rem; }
|
|
81
81
|
|
|
82
82
|
/* --- Section heads (marketing-page variant) -------------------------------
|
|
83
83
|
Distinct from chrome's centered home-section .sec-head: left by default, with a
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
.sec-head--center { max-width: 62ch; margin-inline: auto; text-align: center; }
|
|
89
89
|
.sec-head h2 { font-size: clamp(1.5rem, 3vw, 2.15rem); }
|
|
90
90
|
.sec-head p { margin-top: .5rem; color: var(--color-muted); }
|
|
91
|
-
.sec-head .eyebrow { color: var(--color-accent); }
|
|
91
|
+
.sec-head .eyebrow { color: var(--color-accent-ink); }
|
|
92
92
|
|
|
93
93
|
/* --- Prose (policy / long-form copy) -------------------------------------- */
|
|
94
94
|
.prose { max-width: 76ch; }
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
.prose p { margin-top: .7rem; color: var(--color-text); }
|
|
97
97
|
.prose ul { margin-top: .7rem; padding-left: 1.2rem; display: flex; flex-direction: column; gap: .45rem; }
|
|
98
98
|
.prose li { list-style: disc; color: var(--color-text); }
|
|
99
|
-
.prose a { color: var(--color-accent); }
|
|
99
|
+
.prose a { color: var(--color-accent-ink); }
|
|
100
100
|
|
|
101
101
|
/* --- Callouts ------------------------------------------------------------- */
|
|
102
102
|
/* Illustrative / informational note (e.g. "illustrative demo — figures unconfirmed").
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
|
|
117
117
|
/* --- Table-of-contents chips (long policy pages) -------------------------- */
|
|
118
118
|
.toc { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 20px; }
|
|
119
|
-
.toc a { font-size: .82rem; font-weight: 600; color: var(--color-accent); border: 1px solid var(--color-border); border-radius: var(--radius-full); padding: 6px 12px; background: var(--color-surface); }
|
|
119
|
+
.toc a { font-size: .82rem; font-weight: 600; color: var(--color-accent-ink); border: 1px solid var(--color-border); border-radius: var(--radius-full); padding: 6px 12px; background: var(--color-surface); }
|
|
120
120
|
.toc a:hover { border-color: var(--color-accent); text-decoration: none; }
|
|
121
121
|
|
|
122
122
|
/* --- Value / feature card grid (about) ------------------------------------ */
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
.contact-card { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: 20px; margin-bottom: 14px; }
|
|
131
131
|
.contact-card h3 { font-size: 1.05rem; }
|
|
132
132
|
.contact-card p { margin-top: .4rem; color: var(--color-text); font-size: .92rem; }
|
|
133
|
-
.contact-card a { color: var(--color-accent); }
|
|
133
|
+
.contact-card a { color: var(--color-accent-ink); }
|
|
134
134
|
.contact-form { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: 24px; display: flex; flex-direction: column; gap: 12px; }
|
|
135
135
|
.contact-form h2 { font-size: 1.25rem; margin-bottom: 4px; }
|
|
136
136
|
.contact-form .row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
@@ -17,7 +17,7 @@ const { variant, label, class: className } = Astro.props;
|
|
|
17
17
|
const variants: Record<Props["variant"], string> = {
|
|
18
18
|
sale: "bg-sale text-white",
|
|
19
19
|
new: "bg-primary text-primary-contrast",
|
|
20
|
-
"low-stock": "bg-accent text-
|
|
20
|
+
"low-stock": "bg-accent text-accent-contrast",
|
|
21
21
|
soldout: "bg-surface text-muted border border-border",
|
|
22
22
|
compliant: "bg-[var(--color-success)] text-white",
|
|
23
23
|
};
|
|
@@ -9,18 +9,23 @@
|
|
|
9
9
|
* no surrounding sentence that growing the link would break. Measured 2026-09-21 at 118x20,
|
|
10
10
|
* failing even 2.5.8's 24px floor on every page that renders a trail. The extra height centres
|
|
11
11
|
* around the text, so the crumb does not move.
|
|
12
|
+
*
|
|
13
|
+
* Build `crumbs` with `siteTrail` (lib/breadcrumbs.ts) and pass the SAME array to `breadcrumbLd`.
|
|
14
|
+
* `class` adds a hook for the page's own styles (`breadcrumbs` on the blog templates).
|
|
12
15
|
*/
|
|
13
16
|
import { withBase } from "@/lib/basePath";
|
|
17
|
+
import type { Crumb } from "@/lib/breadcrumbs";
|
|
14
18
|
|
|
15
19
|
interface Props {
|
|
16
|
-
crumbs:
|
|
20
|
+
crumbs: Crumb[];
|
|
21
|
+
class?: string;
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
const { crumbs } = Astro.props;
|
|
24
|
+
const { crumbs, class: className } = Astro.props;
|
|
20
25
|
const { basePath } = Astro.locals;
|
|
21
26
|
---
|
|
22
27
|
|
|
23
|
-
<nav aria-label="Breadcrumb" class="text-sm text-muted">
|
|
28
|
+
<nav aria-label="Breadcrumb" class:list={["text-sm text-muted", className]}>
|
|
24
29
|
<ol class="flex flex-wrap items-center gap-x-1 gap-y-0.5">
|
|
25
30
|
{
|
|
26
31
|
crumbs.map((crumb, i) => {
|
|
@@ -11,13 +11,16 @@ interface Props {
|
|
|
11
11
|
tone?: "bg" | "surface";
|
|
12
12
|
class?: string;
|
|
13
13
|
id?: string;
|
|
14
|
+
/** Stable `data-widget` name, so the rendered page says which block this is. */
|
|
15
|
+
widget?: string;
|
|
14
16
|
}
|
|
15
|
-
const { title, eyebrow, tone = "bg", class: className, id } = Astro.props;
|
|
17
|
+
const { title, eyebrow, tone = "bg", class: className, id, widget } = Astro.props;
|
|
16
18
|
const bg = tone === "surface" ? "bg-[var(--color-surface)]" : "bg-[var(--color-bg)]";
|
|
17
19
|
---
|
|
18
20
|
|
|
19
21
|
<section
|
|
20
22
|
id={id}
|
|
23
|
+
data-widget={widget}
|
|
21
24
|
class:list={[bg, "py-[clamp(3rem,7vw,6rem)]", className]}
|
|
22
25
|
>
|
|
23
26
|
<div class="container-prose">
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { articleSeoMetaTags, resolveOgImage, type ArticleMeta } from "@/lib/seo/meta.js";
|
|
7
7
|
import { withXDefault, type Alternate } from "@/lib/seo/alternates.js";
|
|
8
8
|
import { safeJsonScriptContent } from "@/lib/jsonScript";
|
|
9
|
+
import { documentTitle } from "@/lib/seo/documentTitle";
|
|
9
10
|
|
|
10
11
|
interface JsonLd {
|
|
11
12
|
[key: string]: unknown;
|
|
@@ -13,6 +14,10 @@ interface JsonLd {
|
|
|
13
14
|
|
|
14
15
|
interface Props {
|
|
15
16
|
title: string;
|
|
17
|
+
/** The page's own complete `<title>` (a page/post `seoTitle`). Rendered verbatim —
|
|
18
|
+
* no store-name suffix — so a migrated page keeps its source title exactly.
|
|
19
|
+
* Absent ⇒ `title`, suffixed with the store's display name. */
|
|
20
|
+
seoTitle?: string;
|
|
16
21
|
description?: string;
|
|
17
22
|
/** Path-only canonical, e.g. /products/foo. Joined to tenant canonical base. */
|
|
18
23
|
path: string;
|
|
@@ -47,6 +52,7 @@ interface Props {
|
|
|
47
52
|
|
|
48
53
|
const {
|
|
49
54
|
title,
|
|
55
|
+
seoTitle,
|
|
50
56
|
description,
|
|
51
57
|
path,
|
|
52
58
|
ogType = "website",
|
|
@@ -68,11 +74,7 @@ const {
|
|
|
68
74
|
const { tenant, canonicalBase } = Astro.locals;
|
|
69
75
|
const canonical =
|
|
70
76
|
canonicalOverride ?? new URL(path, canonicalBase + "/").toString();
|
|
71
|
-
|
|
72
|
-
// end with the store name.
|
|
73
|
-
const fullTitle = title.includes(tenant.displayName)
|
|
74
|
-
? title
|
|
75
|
-
: `${title} — ${tenant.displayName}`;
|
|
77
|
+
const fullTitle = documentTitle(title, seoTitle, tenant.displayName);
|
|
76
78
|
const image = resolveOgImage({
|
|
77
79
|
ogImage, themeOgImage: tenant.theme_tokens?.brand?.ogImage ?? null, canonicalBase,
|
|
78
80
|
});
|
|
@@ -8,18 +8,22 @@ import PostAccessBadge from "@/components/blog/PostAccessBadge.astro";
|
|
|
8
8
|
import type { BlogPostSummary } from "@/lib/blog/types.js";
|
|
9
9
|
import type { BlogIndexCopy } from "@/lib/blog/pagination.js";
|
|
10
10
|
import { blogPagePath } from "@/lib/blog/pagination.js";
|
|
11
|
+
import type { BlogPresentation } from "@/lib/blog/presentation.js";
|
|
12
|
+
import { categorySlug } from "@/lib/blog/collection.js";
|
|
11
13
|
|
|
12
14
|
interface Props {
|
|
13
15
|
basePath: string;
|
|
14
16
|
canonicalBase: string;
|
|
15
17
|
copy: BlogIndexCopy;
|
|
18
|
+
presentation: BlogPresentation;
|
|
16
19
|
tags: string[];
|
|
17
20
|
pagePosts: BlogPostSummary[];
|
|
18
21
|
page: number;
|
|
19
22
|
totalPages: number;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
const { basePath, canonicalBase, copy, tags, pagePosts, page, totalPages } = Astro.props;
|
|
25
|
+
const { basePath, canonicalBase, copy, presentation, tags, pagePosts, page, totalPages } = Astro.props;
|
|
26
|
+
const showTopics = presentation.topics && tags.length > 0;
|
|
23
27
|
|
|
24
28
|
const href = (path: string) => `${basePath}${path}` || "/";
|
|
25
29
|
const path = blogPagePath(page);
|
|
@@ -53,21 +57,23 @@ const itemList = {
|
|
|
53
57
|
jsonLd={itemList}
|
|
54
58
|
pagination={pagination}
|
|
55
59
|
>
|
|
56
|
-
<header class="blog-hero">
|
|
60
|
+
<header class:list={["blog-hero", { "blog-hero--plain": !presentation.hero }]}>
|
|
57
61
|
<div class="blog-shell">
|
|
58
|
-
<p class="eyebrow eyebrow--tick">{copy.eyebrow}</p>
|
|
62
|
+
{copy.eyebrow && <p class="eyebrow eyebrow--tick">{copy.eyebrow}</p>}
|
|
59
63
|
<h1>{copy.title}</h1>
|
|
60
|
-
<p>{copy.tagline}</p>
|
|
64
|
+
{copy.tagline && <p class="blog-hero__tagline">{copy.tagline}</p>}
|
|
61
65
|
</div>
|
|
62
66
|
</header>
|
|
63
67
|
|
|
64
|
-
<div class="blog-shell blog-layout">
|
|
65
|
-
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
<div class:list={["blog-shell blog-layout", { "blog-layout--single": !showTopics }]}>
|
|
69
|
+
{showTopics && (
|
|
70
|
+
<aside aria-label="Blog topics">
|
|
71
|
+
<p class="filter-label">Explore topics</p>
|
|
72
|
+
<div class="tag-list">
|
|
73
|
+
{tags.map((tag) => <a href={href(`/blog/tag/${categorySlug(tag)}/`)}>{tag}</a>)}
|
|
74
|
+
</div>
|
|
75
|
+
</aside>
|
|
76
|
+
)}
|
|
71
77
|
|
|
72
78
|
<section class="post-grid" aria-label="Blog posts" id="blog-posts">
|
|
73
79
|
{pagePosts.map((post, index) => (
|
|
@@ -113,13 +119,16 @@ const itemList = {
|
|
|
113
119
|
.blog-shell { width: min(1180px, calc(100% - 2rem)); margin-inline: auto; }
|
|
114
120
|
.blog-hero { padding: clamp(4.5rem, 9vw, 8rem) 0 clamp(3rem, 6vw, 5rem); background: linear-gradient(145deg, var(--color-primary), color-mix(in srgb, var(--color-primary) 82%, var(--color-accent))); color: var(--color-primary-contrast); }
|
|
115
121
|
.blog-hero h1 { max-width: 12ch; margin-top: .75rem; color: inherit; font-size: clamp(2.9rem, 7vw, 5.6rem); }
|
|
116
|
-
.blog-
|
|
122
|
+
.blog-hero__tagline { max-width: 58ch; margin-top: 1.25rem; color: color-mix(in srgb, var(--color-primary-contrast) 82%, transparent); font-size: 1.12rem; }
|
|
117
123
|
.blog-hero .eyebrow { color: color-mix(in srgb, var(--color-primary-contrast) 75%, transparent); }
|
|
124
|
+
.blog-hero--plain { padding: clamp(2rem, 5vw, 3.5rem) 0 0; background: none; color: inherit; }
|
|
125
|
+
.blog-hero--plain .blog-hero__tagline, .blog-hero--plain .eyebrow { color: var(--color-muted); }
|
|
118
126
|
.blog-layout { display: grid; grid-template-columns: 210px minmax(0, 1fr); gap: clamp(2rem, 5vw, 4rem); padding-block: clamp(3rem, 7vw, 6rem); }
|
|
127
|
+
.blog-layout--single { grid-template-columns: minmax(0, 1fr); }
|
|
119
128
|
aside { align-self: start; position: sticky; top: 7rem; }
|
|
120
129
|
.filter-label { margin-bottom: .85rem; font: 700 .76rem/1 var(--font-mono); letter-spacing: .12em; text-transform: uppercase; color: var(--color-muted); }
|
|
121
130
|
.tag-list { display: flex; flex-direction: column; align-items: stretch; gap: .3rem; }
|
|
122
|
-
.tag-list
|
|
131
|
+
.tag-list a { text-decoration: none; padding: .45rem .7rem; border: 1px solid var(--color-border); border-radius: var(--radius-full); background: var(--color-surface); color: var(--color-muted); font-size: .76rem; }
|
|
123
132
|
.post-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.35rem; align-items: start; }
|
|
124
133
|
.post-card { height: 100%; overflow: hidden; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-surface); box-shadow: var(--shadow-sm); transition: transform .2s ease, box-shadow .2s ease; }
|
|
125
134
|
.post-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-md); }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Share row after a post's tags, in the order the collection's `settings.json` `share` declares.
|
|
4
|
+
* Share actions are plain links; Print is a button a nonce'd script wires to `window.print()`, so it
|
|
5
|
+
* works under the strict CSP (no inline handler, no `javascript:` URL) on every serving plane.
|
|
6
|
+
* Each item carries `share-row__item--<target>` so tenant CSS can give it the source theme's icon.
|
|
7
|
+
*/
|
|
8
|
+
import type { ShareLink } from "@/lib/blog/presentation.js";
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
links: ShareLink[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { links } = Astro.props;
|
|
15
|
+
const { cspNonce } = Astro.locals;
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
{links.length > 0 && (
|
|
19
|
+
<ul class="share-row" aria-label="Share this article">
|
|
20
|
+
{links.map((link) => (
|
|
21
|
+
<li class={`share-row__item share-row__item--${link.target}`}>
|
|
22
|
+
{link.href === null
|
|
23
|
+
? <button type="button" class="share-row__action" data-share-print aria-label={link.label}><span class="share-row__label">{link.text}</span></button>
|
|
24
|
+
: <a class="share-row__action" href={link.href} aria-label={link.label} rel="noopener nofollow" target={link.target === "email" ? undefined : "_blank"}><span class="share-row__label">{link.text}</span></a>}
|
|
25
|
+
</li>
|
|
26
|
+
))}
|
|
27
|
+
</ul>
|
|
28
|
+
)}
|
|
29
|
+
|
|
30
|
+
<style>
|
|
31
|
+
.share-row { display: flex; flex-wrap: wrap; gap: .5rem; margin: 1.5rem 0 0; padding: 0; list-style: none; }
|
|
32
|
+
.share-row__action { display: inline-flex; min-height: 2.75rem; align-items: center; padding: .4rem .85rem; border: 1px solid var(--color-border); border-radius: var(--radius-full); background: var(--color-surface); color: var(--color-muted); font: inherit; font-size: .82rem; font-weight: 700; text-decoration: none; cursor: pointer; }
|
|
33
|
+
.share-row__action:hover { color: var(--color-accent-ink); border-color: var(--color-accent); }
|
|
34
|
+
@media print { .share-row { display: none; } }
|
|
35
|
+
</style>
|
|
36
|
+
|
|
37
|
+
{links.some((link) => link.href === null) && (
|
|
38
|
+
<script is:inline nonce={cspNonce}>
|
|
39
|
+
for (const button of document.querySelectorAll("[data-share-print]")) {
|
|
40
|
+
button.addEventListener("click", () => window.print());
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
)}
|
|
@@ -31,7 +31,7 @@ const {
|
|
|
31
31
|
const toneClass: Record<NonNullable<Props["tone"]>, string> = {
|
|
32
32
|
primary: "bg-primary text-primary-contrast",
|
|
33
33
|
sale: "bg-[var(--color-sale)] text-white",
|
|
34
|
-
accent: "bg-accent text-
|
|
34
|
+
accent: "bg-accent text-accent-contrast",
|
|
35
35
|
};
|
|
36
36
|
const copies = Array.from({ length: Math.max(2, repeat) });
|
|
37
37
|
---
|
|
@@ -94,7 +94,7 @@ const childStamp = (link: ChromeLink, childIndex: number) =>
|
|
|
94
94
|
<li>
|
|
95
95
|
<details class="group border-b border-border last:border-b-0">
|
|
96
96
|
<summary class="flex cursor-pointer list-none items-center justify-between gap-2 py-2.5 text-sm font-medium text-text [&::-webkit-details-marker]:hidden">
|
|
97
|
-
<span class:list={["inline-flex items-center gap-1.5", item.flag && "text-accent"]}>
|
|
97
|
+
<span class:list={["inline-flex items-center gap-1.5", item.flag && "text-accent-ink"]}>
|
|
98
98
|
{item.label}
|
|
99
99
|
{item.badge && (
|
|
100
100
|
<span class="rounded-full bg-[var(--color-sale)] px-1.5 py-0.5 font-mono text-[0.55rem] uppercase leading-none tracking-[0.08em] text-white">
|
|
@@ -146,8 +146,8 @@ const childStamp = (link: ChromeLink, childIndex: number) =>
|
|
|
146
146
|
data-astro-prefetch
|
|
147
147
|
aria-current={itemCurrent ? "page" : undefined}
|
|
148
148
|
class:list={[
|
|
149
|
-
"flex items-center gap-1.5 border-b border-border py-2.5 text-sm font-medium text-text transition-colors last:border-b-0 hover:text-accent",
|
|
150
|
-
item.flag && "text-accent",
|
|
149
|
+
"flex items-center gap-1.5 border-b border-border py-2.5 text-sm font-medium text-text transition-colors last:border-b-0 hover:text-accent-ink",
|
|
150
|
+
item.flag && "text-accent-ink",
|
|
151
151
|
]}
|
|
152
152
|
{...itemStamp}
|
|
153
153
|
>
|
|
@@ -167,8 +167,8 @@ const childStamp = (link: ChromeLink, childIndex: number) =>
|
|
|
167
167
|
data-astro-prefetch
|
|
168
168
|
aria-current={itemCurrent ? "page" : undefined}
|
|
169
169
|
class:list={[
|
|
170
|
-
"inline-flex items-center gap-1 py-2 text-sm transition-colors hover:text-accent group-focus-within:text-accent",
|
|
171
|
-
item.flag && "font-semibold text-accent",
|
|
170
|
+
"inline-flex items-center gap-1 py-2 text-sm transition-colors hover:text-accent-ink group-focus-within:text-accent-ink",
|
|
171
|
+
item.flag && "font-semibold text-accent-ink",
|
|
172
172
|
]}
|
|
173
173
|
aria-haspopup="true"
|
|
174
174
|
{...itemStamp}
|
|
@@ -241,8 +241,8 @@ const childStamp = (link: ChromeLink, childIndex: number) =>
|
|
|
241
241
|
data-astro-prefetch
|
|
242
242
|
aria-current={itemCurrent ? "page" : undefined}
|
|
243
243
|
class:list={[
|
|
244
|
-
"inline-flex items-center gap-1 py-2 text-sm transition-colors hover:text-accent",
|
|
245
|
-
item.flag && "font-semibold text-accent",
|
|
244
|
+
"inline-flex items-center gap-1 py-2 text-sm transition-colors hover:text-accent-ink",
|
|
245
|
+
item.flag && "font-semibold text-accent-ink",
|
|
246
246
|
]}
|
|
247
247
|
{...itemStamp}
|
|
248
248
|
>
|
|
@@ -22,6 +22,8 @@ interface Props {
|
|
|
22
22
|
/** Payment method labels rendered as chips (e.g. "Visa", "PayPal"). */
|
|
23
23
|
payments?: string[];
|
|
24
24
|
social?: SocialLink[];
|
|
25
|
+
/** The merchant's own legal/disclaimer copy, rendered verbatim (line breaks kept). */
|
|
26
|
+
disclaimer?: string;
|
|
25
27
|
/** Trust / compliance line (plain text, e.g. an age/nicotine notice). */
|
|
26
28
|
compliance?: string;
|
|
27
29
|
/** Legal bottom-bar line (e.g. a composed "© 2026 Acme · some legal note" string). */
|
|
@@ -41,6 +43,7 @@ const {
|
|
|
41
43
|
newsletter,
|
|
42
44
|
payments = [],
|
|
43
45
|
social = [],
|
|
46
|
+
disclaimer,
|
|
44
47
|
compliance,
|
|
45
48
|
legal,
|
|
46
49
|
bottomLinks = [],
|
|
@@ -233,6 +236,20 @@ const collapsible = variant === "light-collapsible";
|
|
|
233
236
|
)
|
|
234
237
|
}
|
|
235
238
|
|
|
239
|
+
<!-- Merchant legal copy (verbatim) -->
|
|
240
|
+
{
|
|
241
|
+
disclaimer?.trim() && (
|
|
242
|
+
<p class="mt-8 border-t border-border pt-6 text-xs text-muted" data-footer-disclaimer>
|
|
243
|
+
{disclaimer.trim().split(/\r?\n/).map((line, i) => (
|
|
244
|
+
<>
|
|
245
|
+
{i > 0 && <br />}
|
|
246
|
+
{line}
|
|
247
|
+
</>
|
|
248
|
+
))}
|
|
249
|
+
</p>
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
236
253
|
<!-- Compliance / trust line -->
|
|
237
254
|
{
|
|
238
255
|
compliance && (
|
|
@@ -98,7 +98,7 @@ const compact = layout === "compact-centered";
|
|
|
98
98
|
const ctaClass = (v?: string) =>
|
|
99
99
|
v === "secondary" || v === "ghost"
|
|
100
100
|
? "inline-flex items-center rounded-full border border-current px-4 py-2 font-display text-sm font-medium transition-opacity hover:opacity-80"
|
|
101
|
-
: "inline-flex items-center rounded-full bg-accent px-4 py-2 font-display text-sm font-medium text-
|
|
101
|
+
: "inline-flex items-center rounded-full bg-accent px-4 py-2 font-display text-sm font-medium text-accent-contrast transition-transform hover:-translate-y-0.5";
|
|
102
102
|
|
|
103
103
|
// Nav handed to the (client-rendered) MobileNav island with hrefs pre-prefixed,
|
|
104
104
|
// so it doesn't depend on the runtime base path inside the island. MobileNav's
|
|
@@ -224,7 +224,7 @@ const currentPath = Astro.url.pathname;
|
|
|
224
224
|
<a
|
|
225
225
|
href={withBase(basePath, searchAction)}
|
|
226
226
|
aria-label={searchLabel ?? "Search"}
|
|
227
|
-
class="hidden h-9 w-9 place-items-center rounded-full text-text transition-colors hover:text-accent md:grid"
|
|
227
|
+
class="hidden h-9 w-9 place-items-center rounded-full text-text transition-colors hover:text-accent-ink md:grid"
|
|
228
228
|
>
|
|
229
229
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true">
|
|
230
230
|
<circle cx="11" cy="11" r="7" />
|
|
@@ -238,7 +238,7 @@ const currentPath = Astro.url.pathname;
|
|
|
238
238
|
<a
|
|
239
239
|
href={withBase(basePath, savedItemsHref)}
|
|
240
240
|
aria-label="Saved items"
|
|
241
|
-
class="relative grid h-9 w-9 shrink-0 place-items-center rounded-full text-text transition-colors hover:text-accent"
|
|
241
|
+
class="relative grid h-9 w-9 shrink-0 place-items-center rounded-full text-text transition-colors hover:text-accent-ink"
|
|
242
242
|
>
|
|
243
243
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true">
|
|
244
244
|
<path d="M12 21s-7.5-4.6-10-9.2C.4 8.6 2 5 5.5 5 8 5 9.4 6.6 12 9c2.6-2.4 4-4 6.5-4C22 5 23.6 8.6 22 11.8 19.5 16.4 12 21 12 21z" />
|
|
@@ -293,7 +293,7 @@ const currentPath = Astro.url.pathname;
|
|
|
293
293
|
<a
|
|
294
294
|
href={withBase(basePath, accountHref)}
|
|
295
295
|
aria-label="Account"
|
|
296
|
-
class="grid h-9 w-9 place-items-center rounded-full text-text transition-colors hover:text-accent"
|
|
296
|
+
class="grid h-9 w-9 place-items-center rounded-full text-text transition-colors hover:text-accent-ink"
|
|
297
297
|
>
|
|
298
298
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true">
|
|
299
299
|
<circle cx="12" cy="8" r="4" />
|
|
@@ -307,7 +307,7 @@ const currentPath = Astro.url.pathname;
|
|
|
307
307
|
<a
|
|
308
308
|
href={withBase(basePath, cartHref)}
|
|
309
309
|
aria-label={`Cart, ${cartCount} ${cartCount === 1 ? "item" : "items"}`}
|
|
310
|
-
class="relative grid h-9 w-9 place-items-center rounded-full text-text transition-colors hover:text-accent"
|
|
310
|
+
class="relative grid h-9 w-9 place-items-center rounded-full text-text transition-colors hover:text-accent-ink"
|
|
311
311
|
>
|
|
312
312
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true">
|
|
313
313
|
<path d="M6 6h15l-1.5 9h-12z" />
|
|
@@ -135,7 +135,7 @@ const chip = (t: TagItem) => ({
|
|
|
135
135
|
color: var(--color-muted);
|
|
136
136
|
}
|
|
137
137
|
.tag-chip.is-active .tag-chip__count {
|
|
138
|
-
color:
|
|
138
|
+
color: var(--color-primary-contrast);
|
|
139
139
|
}
|
|
140
140
|
.tag-chip:focus-visible {
|
|
141
141
|
outline: 2px solid var(--color-primary);
|
|
@@ -1,33 +1,23 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
3
|
* FDA-mandated nicotine warning (21 CFR 1143.3). The wording is fixed by law and
|
|
4
|
-
* must be conspicuous — do not paraphrase. Two
|
|
5
|
-
* - "bar": a persistent
|
|
4
|
+
* must be conspicuous — do not paraphrase. Two placements:
|
|
5
|
+
* - "bar": a persistent strip at the very top of every page.
|
|
6
6
|
* - "block": a reinforced box on the PDP (regulation also requires it on the
|
|
7
7
|
* product's own page/ads).
|
|
8
|
-
* Rendered only when the tenant's compliance.nicotineWarning is set.
|
|
8
|
+
* Rendered only when the tenant's compliance.nicotineWarning is set. Its colours,
|
|
9
|
+
* case, weight and size come from that declaration (a validated preset, or an
|
|
10
|
+
* owner-reviewed custom style), never below the conspicuity floor and never
|
|
11
|
+
* hideable. The markup is nicotineWarningHtml's, shared with the raw-HTML serve path.
|
|
9
12
|
*/
|
|
13
|
+
import { nicotineWarningHtml } from "../../../../../packages/cli/src/declared-config.mjs";
|
|
14
|
+
|
|
10
15
|
interface Props {
|
|
11
16
|
variant?: "bar" | "block";
|
|
17
|
+
/** The tenant's `compliance.nicotineWarning` declaration. */
|
|
18
|
+
declaration: unknown;
|
|
12
19
|
}
|
|
13
|
-
const { variant = "bar" } = Astro.props;
|
|
14
|
-
|
|
15
|
-
const TEXT =
|
|
16
|
-
"WARNING: This product contains nicotine. Nicotine is an addictive chemical.";
|
|
20
|
+
const { variant = "bar", declaration } = Astro.props;
|
|
17
21
|
---
|
|
18
22
|
|
|
19
|
-
{
|
|
20
|
-
variant === "bar" ? (
|
|
21
|
-
<div class="border-b-2 border-[var(--color-text)] bg-[var(--color-text)] text-[var(--color-bg)]" role="note" aria-label="Health warning" data-compliance="nicotine-warning">
|
|
22
|
-
<p class="container-prose py-2 text-center text-[0.72rem] font-semibold uppercase tracking-[0.06em] sm:text-xs">
|
|
23
|
-
{TEXT}
|
|
24
|
-
</p>
|
|
25
|
-
</div>
|
|
26
|
-
) : (
|
|
27
|
-
<div class="rounded-[var(--radius-md)] border-2 border-[var(--color-text)] px-4 py-3" role="note" aria-label="Health warning" data-compliance="nicotine-warning">
|
|
28
|
-
<p class="text-sm font-semibold uppercase tracking-[0.04em] text-[var(--color-text)]">
|
|
29
|
-
{TEXT}
|
|
30
|
-
</p>
|
|
31
|
-
</div>
|
|
32
|
-
)
|
|
33
|
-
}
|
|
23
|
+
<Fragment set:html={nicotineWarningHtml(declaration, variant)} />
|