@tokenoftrust/storefront-runner 2.2.126 → 2.3.1
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 +23 -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/checkoutCommerce.ts +26 -3
- 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/checkout.ts +13 -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
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
3
|
* PACT Act (Prevent All Cigarette Trafficking Act, extended to ENDS/vapor in
|
|
4
|
-
* 2021) compliance notice. The federal requirements it summarizes are fixed
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* marketing page.
|
|
4
|
+
* 2021) compliance notice. The federal requirements it summarizes are fixed —
|
|
5
|
+
* age verification, adult signature on delivery, and no U.S. Postal Service
|
|
6
|
+
* carriage — in `pactActNoticeText`, the one copy the raw-HTML path injects too;
|
|
7
|
+
* only whether it renders (and the age) comes from tenant.compliance.pactAct /
|
|
8
|
+
* .minAge. Renders NOTHING when `enabled` is false, so absent data never shows
|
|
9
|
+
* placeholder legal text. Never hand-author this on a marketing page. Layout.astro
|
|
10
|
+
* renders it as a footer band on every page.
|
|
10
11
|
*/
|
|
11
12
|
import ComplianceNotice from "./ComplianceNotice.astro";
|
|
13
|
+
import { pactActNoticeText } from "@/lib/compliance/rawComplianceNotices";
|
|
12
14
|
|
|
13
15
|
interface Props {
|
|
14
16
|
/** From tenant.compliance.pactAct. */
|
|
@@ -20,10 +22,7 @@ interface Props {
|
|
|
20
22
|
|
|
21
23
|
const { enabled = false, minAge = 21, variant = "block" } = Astro.props;
|
|
22
24
|
|
|
23
|
-
const TEXT =
|
|
24
|
-
`In compliance with the federal PACT Act, all vapor and nicotine orders require ` +
|
|
25
|
-
`age verification (${minAge}+) and an adult signature on delivery. These products ` +
|
|
26
|
-
`cannot be shipped through the U.S. Postal Service.`;
|
|
25
|
+
const TEXT = pactActNoticeText(minAge);
|
|
27
26
|
---
|
|
28
27
|
|
|
29
28
|
{
|
|
@@ -32,11 +32,11 @@ const rendered = withHeadingIds(normalizeProse(sections));
|
|
|
32
32
|
switch (s.kind) {
|
|
33
33
|
case "heading":
|
|
34
34
|
return (s.level ?? 2) === 3 ? (
|
|
35
|
-
<h3 id={s.id} class="mt-6 scroll-mt-24 font-display text-
|
|
35
|
+
<h3 id={s.id} class="mt-6 scroll-mt-24 font-display text-[length:var(--heading-h3,1.25rem)] leading-[1.4] font-semibold">
|
|
36
36
|
{s.text}
|
|
37
37
|
</h3>
|
|
38
38
|
) : (
|
|
39
|
-
<h2 id={s.id} class="mt-8 scroll-mt-24 font-display text-
|
|
39
|
+
<h2 id={s.id} class="mt-8 scroll-mt-24 font-display text-[length:var(--heading-h2,1.5rem)] leading-[1.3333] font-semibold">
|
|
40
40
|
{s.text}
|
|
41
41
|
</h2>
|
|
42
42
|
);
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
* visual-parity migrations where the artwork owns the visible copy.
|
|
14
14
|
* The feature image is the merchant's own hero image (block.image) when adopted,
|
|
15
15
|
* else the hero collection's first product.
|
|
16
|
+
*
|
|
17
|
+
* `block.notice` renders a status pill (`.hero__notice`) above the eyebrow in
|
|
18
|
+
* every treatment; the slideshow and banner carry it in a band above the artwork.
|
|
19
|
+
* The eyebrow (`.hero__eyebrow`) reads `--color-hero-eyebrow` first, so a theme
|
|
20
|
+
* recolours it with `color.hero-eyebrow` and tenant CSS can target the hook class.
|
|
16
21
|
*/
|
|
17
22
|
import type { HeroBlock } from "@/lib/storyblok/content-model";
|
|
18
23
|
import type { CatalogProduct } from "@tot/public-runtime";
|
|
@@ -24,8 +29,11 @@ interface Props {
|
|
|
24
29
|
block: HeroBlock;
|
|
25
30
|
featureProduct?: CatalogProduct | null;
|
|
26
31
|
instanceKey?: string;
|
|
32
|
+
/** The page's h1 by default; a page that already has its h1 renders the hero headline as h2. */
|
|
33
|
+
headingLevel?: 1 | 2;
|
|
27
34
|
}
|
|
28
|
-
const { block, featureProduct, instanceKey = "home" } = Astro.props;
|
|
35
|
+
const { block, featureProduct, instanceKey = "home", headingLevel = 1 } = Astro.props;
|
|
36
|
+
const Heading = headingLevel === 2 ? "h2" : "h1";
|
|
29
37
|
const { basePath } = Astro.locals;
|
|
30
38
|
const heroVariant = Astro.locals.tenant?.theme_tokens?.brand?.heroVariant ?? "bold";
|
|
31
39
|
const light = heroVariant === "light";
|
|
@@ -37,18 +45,25 @@ const slideshowView = heroSlideshowView(block, instanceKey, basePath);
|
|
|
37
45
|
const slides = slideshowView.slides;
|
|
38
46
|
const slideshow = slides.length > 0;
|
|
39
47
|
const slideId = slideshowView.id;
|
|
48
|
+
const notice = block.notice?.trim();
|
|
40
49
|
---
|
|
41
50
|
|
|
42
51
|
{slideshow ? (
|
|
43
52
|
<section
|
|
44
53
|
class="hero-slideshow bg-[var(--color-bg)]"
|
|
54
|
+
data-widget="home-hero"
|
|
45
55
|
aria-roledescription="carousel"
|
|
46
56
|
aria-label={block.headline}
|
|
47
57
|
data-hero-slideshow
|
|
48
58
|
data-autoplay={slideshowView.autoplay ? "true" : "false"}
|
|
49
59
|
data-interval={slideshowView.intervalMs}
|
|
50
60
|
>
|
|
51
|
-
<
|
|
61
|
+
<Heading class="sr-only">{block.headline}</Heading>
|
|
62
|
+
{notice && (
|
|
63
|
+
<div class="container-prose py-3">
|
|
64
|
+
<p class="hero__notice">{notice}</p>
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
52
67
|
<div class="hero-slideshow__slides">
|
|
53
68
|
{slides.map((slide) => (
|
|
54
69
|
<Fragment>
|
|
@@ -79,8 +94,13 @@ const slideId = slideshowView.id;
|
|
|
79
94
|
)}
|
|
80
95
|
</section>
|
|
81
96
|
) : banner ? (
|
|
82
|
-
<section class="bg-[var(--color-bg)]">
|
|
83
|
-
<
|
|
97
|
+
<section class="bg-[var(--color-bg)]" data-widget="home-hero">
|
|
98
|
+
<Heading class="sr-only">{block.headline}</Heading>
|
|
99
|
+
{notice && (
|
|
100
|
+
<div class="container-prose py-3">
|
|
101
|
+
<p class="hero__notice">{notice}</p>
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
84
104
|
{block.ctaHref ? (
|
|
85
105
|
<a
|
|
86
106
|
href={withBase(basePath, block.ctaHref)}
|
|
@@ -113,6 +133,7 @@ const slideId = slideshowView.id;
|
|
|
113
133
|
</section>
|
|
114
134
|
) : (
|
|
115
135
|
<section
|
|
136
|
+
data-widget="home-hero"
|
|
116
137
|
class:list={[
|
|
117
138
|
"relative overflow-hidden",
|
|
118
139
|
light
|
|
@@ -127,25 +148,26 @@ const slideId = slideshowView.id;
|
|
|
127
148
|
]}
|
|
128
149
|
>
|
|
129
150
|
<div data-reveal>
|
|
151
|
+
{notice && <p class="hero__notice mb-5">{notice}</p>}
|
|
130
152
|
{
|
|
131
153
|
block.eyebrow && (
|
|
132
154
|
<p
|
|
133
155
|
class:list={[
|
|
134
|
-
"eyebrow eyebrow--tick mb-6",
|
|
156
|
+
"hero__eyebrow eyebrow eyebrow--tick mb-6",
|
|
135
157
|
light
|
|
136
|
-
? "
|
|
137
|
-
: "
|
|
158
|
+
? "text-[var(--color-hero-eyebrow,var(--color-muted))]"
|
|
159
|
+
: "text-[var(--color-hero-eyebrow,color-mix(in_srgb,var(--color-primary-contrast)_72%,transparent))]",
|
|
138
160
|
]}
|
|
139
161
|
>
|
|
140
162
|
{block.eyebrow}
|
|
141
163
|
</p>
|
|
142
164
|
)
|
|
143
165
|
}
|
|
144
|
-
<
|
|
166
|
+
<Heading class="display-hero">
|
|
145
167
|
<span class:list={["block", light && "text-[var(--color-primary)]"]}>{block.headline}</span>
|
|
146
168
|
{
|
|
147
169
|
block.headlineAccent && (
|
|
148
|
-
<span class="relative block w-fit text-[var(--color-accent)]">
|
|
170
|
+
<span class:list={["relative block w-fit", light ? "text-[var(--color-accent-ink)]" : "text-[var(--color-accent)]"]}>
|
|
149
171
|
{block.headlineAccent}
|
|
150
172
|
<span
|
|
151
173
|
aria-hidden="true"
|
|
@@ -154,7 +176,7 @@ const slideId = slideshowView.id;
|
|
|
154
176
|
</span>
|
|
155
177
|
)
|
|
156
178
|
}
|
|
157
|
-
</
|
|
179
|
+
</Heading>
|
|
158
180
|
{
|
|
159
181
|
block.subhead && (
|
|
160
182
|
<p
|
|
@@ -173,7 +195,7 @@ const slideId = slideshowView.id;
|
|
|
173
195
|
block.ctaLabel && block.ctaHref && (
|
|
174
196
|
<a
|
|
175
197
|
href={withBase(basePath, block.ctaHref)}
|
|
176
|
-
class="group mt-9 inline-flex items-center gap-2 rounded-full bg-[var(--color-accent)] px-7 py-3 font-display text-[0.95rem] font-semibold text-
|
|
198
|
+
class="group mt-9 inline-flex items-center gap-2 rounded-full bg-[var(--color-accent)] px-7 py-3 font-display text-[0.95rem] font-semibold text-[var(--color-accent-contrast)] transition-transform duration-200 hover:-translate-y-0.5 focus-visible:-translate-y-0.5"
|
|
177
199
|
>
|
|
178
200
|
{block.ctaLabel}
|
|
179
201
|
<span aria-hidden="true" class="transition-transform duration-200 group-hover:translate-x-1">
|
|
@@ -243,7 +265,7 @@ const slideId = slideshowView.id;
|
|
|
243
265
|
class="absolute bottom-4 left-4 right-4 flex items-center justify-between rounded-[var(--radius-md)] bg-[var(--color-surface)]/95 px-4 py-3 text-[var(--color-text)] backdrop-blur"
|
|
244
266
|
>
|
|
245
267
|
<span class="font-display text-sm font-medium">{featureProduct.title}</span>
|
|
246
|
-
<span class="eyebrow
|
|
268
|
+
<span class="eyebrow text-[var(--color-accent-ink)]">Shop →</span>
|
|
247
269
|
</a>
|
|
248
270
|
</div>
|
|
249
271
|
)
|
|
@@ -48,12 +48,44 @@ function scrollbarGutterWidth(): number {
|
|
|
48
48
|
return Number.isFinite(width) && width > 0 ? width : 0;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
const MAIN_LANDMARK = 'main, [role="main"]';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The elements to make inert so everything behind the gate is unreachable while the
|
|
55
|
+
* page's `<main>` landmark itself stays in the accessibility tree. An element that
|
|
56
|
+
* is or contains the main landmark is descended into rather than inerted, so only
|
|
57
|
+
* its contents go inert; every other element is inerted whole. A page with no main
|
|
58
|
+
* landmark inerts each body child. `containers` are the elements descended into (body,
|
|
59
|
+
* main, and main's ancestors): content added to one of them later must be inerted too.
|
|
60
|
+
*/
|
|
61
|
+
export function ageGateInertTargets(host: HTMLElement): { targets: HTMLElement[]; containers: Element[] } {
|
|
62
|
+
const targets: HTMLElement[] = [];
|
|
63
|
+
const containers: Element[] = [];
|
|
64
|
+
const visit = (parent: Element) => {
|
|
65
|
+
containers.push(parent);
|
|
66
|
+
for (const child of Array.from(parent.children)) {
|
|
67
|
+
if (!(child instanceof HTMLElement) || child === host || child.hasAttribute("inert")) continue;
|
|
68
|
+
if (child.matches(MAIN_LANDMARK) || child.querySelector(MAIN_LANDMARK)) visit(child);
|
|
69
|
+
else targets.push(child);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
visit(document.body);
|
|
73
|
+
return { targets, containers };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Make the storefront behind the gate non-interactive without disturbing prior inert
|
|
78
|
+
* state. The gate is an `aria-modal` dialog with a focus trap; inert takes keyboard,
|
|
79
|
+
* pointer, and assistive-technology reach away from everything else on the page.
|
|
80
|
+
*/
|
|
52
81
|
export function isolateAgeGateBackground(host: HTMLElement): BackgroundIsolation {
|
|
53
82
|
const isolated = new Set<HTMLElement>();
|
|
54
83
|
const previousOverflow = document.documentElement.style.overflow;
|
|
55
84
|
const previousPaddingRight = document.documentElement.style.paddingRight;
|
|
56
85
|
let active = true;
|
|
86
|
+
// Watches the containers kept out of inert, so a child appended to one later is
|
|
87
|
+
// inerted as well.
|
|
88
|
+
const containerObserver = new MutationObserver(() => refresh());
|
|
57
89
|
|
|
58
90
|
const refresh = () => {
|
|
59
91
|
if (!active) return;
|
|
@@ -64,15 +96,17 @@ export function isolateAgeGateBackground(host: HTMLElement): BackgroundIsolation
|
|
|
64
96
|
document.documentElement.style.paddingRight = `${gutter}px`;
|
|
65
97
|
}
|
|
66
98
|
document.documentElement.style.overflow = "hidden";
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
isolated.add(
|
|
99
|
+
const { targets, containers } = ageGateInertTargets(host);
|
|
100
|
+
for (const target of targets) {
|
|
101
|
+
target.setAttribute("inert", "");
|
|
102
|
+
isolated.add(target);
|
|
71
103
|
}
|
|
104
|
+
for (const container of containers) containerObserver.observe(container, { childList: true });
|
|
72
105
|
};
|
|
73
106
|
const release = () => {
|
|
74
107
|
if (!active) return;
|
|
75
108
|
active = false;
|
|
109
|
+
containerObserver.disconnect();
|
|
76
110
|
for (const sibling of isolated) sibling.removeAttribute("inert");
|
|
77
111
|
isolated.clear();
|
|
78
112
|
document.documentElement.style.overflow = previousOverflow;
|
|
@@ -227,7 +227,7 @@ export default function VariantSelector({
|
|
|
227
227
|
stockClass = "text-sale";
|
|
228
228
|
} else if (qty !== undefined && qty !== null && qty <= LOW_STOCK_THRESHOLD) {
|
|
229
229
|
stockLabel = `Only ${qty} left`;
|
|
230
|
-
stockClass = "text-accent";
|
|
230
|
+
stockClass = "text-accent-ink";
|
|
231
231
|
} else {
|
|
232
232
|
stockLabel = "In stock";
|
|
233
233
|
stockClass = "text-primary";
|
|
@@ -300,7 +300,7 @@ export default function VariantSelector({
|
|
|
300
300
|
{memberApplied && (
|
|
301
301
|
<span
|
|
302
302
|
data-member-price
|
|
303
|
-
class="rounded-full bg-accent px-2 py-0.5 font-mono text-[0.68rem] uppercase tracking-[0.08em] text-
|
|
303
|
+
class="rounded-full bg-accent px-2 py-0.5 font-mono text-[0.68rem] uppercase tracking-[0.08em] text-accent-contrast"
|
|
304
304
|
>
|
|
305
305
|
{pricing?.membershipRule.label ?? "Member"} price
|
|
306
306
|
</span>
|
|
@@ -403,7 +403,7 @@ export default function VariantSelector({
|
|
|
403
403
|
{option.label}
|
|
404
404
|
</span>
|
|
405
405
|
{"savingsLabel" in option && option.savingsLabel && (
|
|
406
|
-
<span class="rounded-full bg-accent px-2 py-0.5 font-mono text-[0.68rem] uppercase tracking-[0.08em] text-
|
|
406
|
+
<span class="rounded-full bg-accent px-2 py-0.5 font-mono text-[0.68rem] uppercase tracking-[0.08em] text-accent-contrast">
|
|
407
407
|
{option.savingsLabel}
|
|
408
408
|
</span>
|
|
409
409
|
)}
|
|
@@ -21,7 +21,7 @@ const cols =
|
|
|
21
21
|
const isExternal = (href: string) => /^https?:\/\//i.test(href);
|
|
22
22
|
const hrefOf = (href: string) => (isExternal(href) ? href : withBase(basePath, href));
|
|
23
23
|
---
|
|
24
|
-
<Section id={block.anchor} eyebrow={block.eyebrow} title={block.title} tone="bg">
|
|
24
|
+
<Section id={block.anchor} eyebrow={block.eyebrow} title={block.title} tone="bg" widget="marketing-card-grid">
|
|
25
25
|
{block.subhead && (
|
|
26
26
|
<p class="-mt-4 mb-9 max-w-[60ch] text-[var(--color-muted)]" data-reveal>
|
|
27
27
|
{block.subhead}
|
|
@@ -11,7 +11,7 @@ interface Props {
|
|
|
11
11
|
}
|
|
12
12
|
const { sample } = Astro.props;
|
|
13
13
|
---
|
|
14
|
-
<figure class="overflow-hidden rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-text)]">
|
|
14
|
+
<figure data-widget="marketing-code-sample" class="overflow-hidden rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-text)]">
|
|
15
15
|
<figcaption class="flex items-center justify-between border-b border-white/10 px-4 py-2 font-mono text-[0.68rem] uppercase tracking-[0.12em] text-white/60">
|
|
16
16
|
<span>{sample.caption ?? "Example"}</span>
|
|
17
17
|
{sample.language && <span>{sample.language}</span>}
|
|
@@ -11,7 +11,7 @@ interface Props {
|
|
|
11
11
|
}
|
|
12
12
|
const { block, basePath } = Astro.props;
|
|
13
13
|
---
|
|
14
|
-
<section class="bg-[var(--color-surface)] py-[clamp(3.5rem,8vw,6.5rem)]">
|
|
14
|
+
<section data-widget="marketing-cta-band" class="bg-[var(--color-surface)] py-[clamp(3.5rem,8vw,6.5rem)]">
|
|
15
15
|
<div class="container-prose">
|
|
16
16
|
<div class="mx-auto max-w-2xl text-center" data-reveal>
|
|
17
17
|
<h2 class="display-lg">{block.title}</h2>
|
|
@@ -23,7 +23,7 @@ const faqLd = {
|
|
|
23
23
|
};
|
|
24
24
|
const faqJsonLd = safeJsonScriptContent(faqLd);
|
|
25
25
|
---
|
|
26
|
-
<Section eyebrow={block.eyebrow} title={block.title} tone="surface">
|
|
26
|
+
<Section eyebrow={block.eyebrow} title={block.title} tone="surface" widget="marketing-faq">
|
|
27
27
|
{block.lede && (
|
|
28
28
|
<p class="mb-8 max-w-[42rem] text-[var(--color-muted)]" data-reveal>{block.lede}</p>
|
|
29
29
|
)}
|
|
@@ -16,7 +16,7 @@ interface Props {
|
|
|
16
16
|
const { block, basePath } = Astro.props;
|
|
17
17
|
const isExternal = (href: string) => /^https?:\/\//i.test(href);
|
|
18
18
|
---
|
|
19
|
-
<Section eyebrow={block.eyebrow} title={block.title} tone="surface">
|
|
19
|
+
<Section eyebrow={block.eyebrow} title={block.title} tone="surface" widget="marketing-integrations">
|
|
20
20
|
{block.subhead && (
|
|
21
21
|
<p class="-mt-4 mb-9 max-w-[60ch] text-[var(--color-muted)]" data-reveal>
|
|
22
22
|
{block.subhead}
|
|
@@ -18,7 +18,7 @@ const { ctas = [], basePath, class: className } = Astro.props;
|
|
|
18
18
|
|
|
19
19
|
const styles: Record<NonNullable<MarketingCta["variant"]>, string> = {
|
|
20
20
|
primary:
|
|
21
|
-
"rounded-full bg-accent px-6 py-3 font-display text-sm font-semibold text-
|
|
21
|
+
"rounded-full bg-accent px-6 py-3 font-display text-sm font-semibold text-accent-contrast shadow-[var(--shadow-sm)] transition-transform hover:-translate-y-0.5",
|
|
22
22
|
secondary:
|
|
23
23
|
"rounded-full border border-[var(--color-border)] bg-[var(--color-surface)] px-6 py-3 font-display text-sm font-semibold text-[var(--color-text)] transition-colors hover:border-[var(--color-primary)]",
|
|
24
24
|
ghost:
|
|
@@ -16,7 +16,7 @@ const { block, basePath } = Astro.props;
|
|
|
16
16
|
const hasPanel = !!block.panel?.lines?.length;
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
<section class="bg-[var(--color-bg)] py-[clamp(3.5rem,8vw,7rem)]">
|
|
19
|
+
<section data-widget="marketing-hero" class="bg-[var(--color-bg)] py-[clamp(3.5rem,8vw,7rem)]">
|
|
20
20
|
<div class="container-prose">
|
|
21
21
|
<div
|
|
22
22
|
class:list={[
|
|
@@ -10,7 +10,7 @@ interface Props {
|
|
|
10
10
|
}
|
|
11
11
|
const { block } = Astro.props;
|
|
12
12
|
---
|
|
13
|
-
<section class="bg-[var(--color-primary)] py-[clamp(3rem,7vw,6rem)] text-[var(--color-primary-contrast)]">
|
|
13
|
+
<section data-widget="marketing-proof-strip" class="bg-[var(--color-primary)] py-[clamp(3rem,7vw,6rem)] text-[var(--color-primary-contrast)]">
|
|
14
14
|
<div class="container-prose">
|
|
15
15
|
<div class="mx-auto max-w-3xl text-center" data-reveal>
|
|
16
16
|
{block.eyebrow && (
|
|
@@ -11,7 +11,7 @@ interface Props {
|
|
|
11
11
|
}
|
|
12
12
|
const { block } = Astro.props;
|
|
13
13
|
---
|
|
14
|
-
<Section eyebrow={block.eyebrow} title={block.title} tone="bg">
|
|
14
|
+
<Section eyebrow={block.eyebrow} title={block.title} tone="bg" widget="marketing-split-compare">
|
|
15
15
|
{block.subhead && (
|
|
16
16
|
<p class="-mt-4 mb-9 max-w-[60ch] text-[var(--color-muted)]" data-reveal>
|
|
17
17
|
{block.subhead}
|
|
@@ -10,7 +10,7 @@ interface Props {
|
|
|
10
10
|
}
|
|
11
11
|
const { block } = Astro.props;
|
|
12
12
|
---
|
|
13
|
-
<Section eyebrow={block.eyebrow} title={block.title} tone="surface">
|
|
13
|
+
<Section eyebrow={block.eyebrow} title={block.title} tone="surface" widget="marketing-steps">
|
|
14
14
|
{block.subhead && (
|
|
15
15
|
<p class="-mt-4 mb-9 max-w-[60ch] text-[var(--color-muted)]" data-reveal>
|
|
16
16
|
{block.subhead}
|
|
@@ -10,12 +10,12 @@ interface Props {
|
|
|
10
10
|
}
|
|
11
11
|
const { block } = Astro.props;
|
|
12
12
|
---
|
|
13
|
-
<Section eyebrow={block.eyebrow} title={block.title} tone="bg">
|
|
13
|
+
<Section eyebrow={block.eyebrow} title={block.title} tone="bg" widget="marketing-testimonials">
|
|
14
14
|
<div class="grid gap-5 md:grid-cols-3" data-reveal>
|
|
15
15
|
{block.quotes.map((q) => (
|
|
16
16
|
<figure class="flex flex-col rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface)] p-6">
|
|
17
17
|
{q.stat && (
|
|
18
|
-
<p class="font-display text-2xl font-bold text-[var(--color-accent)]">{q.stat}</p>
|
|
18
|
+
<p class="font-display text-2xl font-bold text-[var(--color-accent-ink)]">{q.stat}</p>
|
|
19
19
|
)}
|
|
20
20
|
<blockquote class="mt-3 flex-1 text-[var(--color-text)]">
|
|
21
21
|
<p>“{q.quote}”</p>
|
|
@@ -9,7 +9,7 @@ interface Props {
|
|
|
9
9
|
}
|
|
10
10
|
const { block } = Astro.props;
|
|
11
11
|
---
|
|
12
|
-
<div class="border-y border-[var(--color-border)] bg-[var(--color-surface)]">
|
|
12
|
+
<div data-widget="marketing-trust-bar" class="border-y border-[var(--color-border)] bg-[var(--color-surface)]">
|
|
13
13
|
<div class="container-prose py-4">
|
|
14
14
|
<ul class="flex flex-wrap items-center justify-center gap-x-5 gap-y-2 text-center font-mono text-[0.72rem] uppercase tracking-[0.1em] text-[var(--color-muted)]">
|
|
15
15
|
{block.items.map((item) => (
|
|
@@ -64,7 +64,7 @@ const anyIllustrative = tiers.some((t) => t.figure && isIllustrative(t));
|
|
|
64
64
|
</caption>
|
|
65
65
|
<thead>
|
|
66
66
|
<tr>
|
|
67
|
-
<th scope="col" class="w-1/3 p-3 text-left align-bottom"
|
|
67
|
+
<th scope="col" class="w-1/3 p-3 text-left align-bottom"><span class="sr-only">Feature</span></th>
|
|
68
68
|
{tiers.map((tier) => {
|
|
69
69
|
const illustrative = Boolean(tier.figure) && isIllustrative(tier);
|
|
70
70
|
return (
|
|
@@ -47,7 +47,7 @@ const clearHref =
|
|
|
47
47
|
|
|
48
48
|
{
|
|
49
49
|
chips.length > 0 && (
|
|
50
|
-
<div class="mb-5 flex flex-wrap items-center gap-2" data-active-filters>
|
|
50
|
+
<div class="mb-5 flex flex-wrap items-center gap-2" data-active-filters data-widget="plp-active-filters">
|
|
51
51
|
<span class="sr-only">Active filters:</span>
|
|
52
52
|
{chips.map((chip) => (
|
|
53
53
|
<a
|
|
@@ -40,9 +40,9 @@ const clearHref =
|
|
|
40
40
|
buildQueryString({ sort: query.sort, q: query.q, pageSize: query.pageSize });
|
|
41
41
|
---
|
|
42
42
|
|
|
43
|
-
<aside aria-label="Filters" class="text-sm">
|
|
43
|
+
<aside aria-label="Filters" data-widget="plp-facet-sidebar" class="text-sm">
|
|
44
44
|
<div class="mb-4 flex items-center justify-between">
|
|
45
|
-
<h2 class="eyebrow
|
|
45
|
+
<h2 class="eyebrow text-[var(--color-text)]">Filter</h2>
|
|
46
46
|
{hasActiveFilters && (
|
|
47
47
|
<a href={clearHref} class="text-xs text-[var(--color-muted)] underline underline-offset-2 hover:text-[var(--color-accent-ink)]">
|
|
48
48
|
Clear all
|
|
@@ -93,7 +93,7 @@ const clearHref =
|
|
|
93
93
|
</span>
|
|
94
94
|
{fv.value === "all" ? "All items" : fv.value}
|
|
95
95
|
</span>
|
|
96
|
-
<span class:list={["text-xs", isSel
|
|
96
|
+
<span class:list={["text-xs", !isSel && "text-[var(--color-muted)]"]}>
|
|
97
97
|
{fv.count}
|
|
98
98
|
</span>
|
|
99
99
|
</a>
|
|
@@ -145,7 +145,7 @@ const clearHref =
|
|
|
145
145
|
</span>
|
|
146
146
|
{bucket.label}
|
|
147
147
|
</span>
|
|
148
|
-
<span class:list={["text-xs", isSel
|
|
148
|
+
<span class:list={["text-xs", !isSel && "text-[var(--color-muted)]"]}>
|
|
149
149
|
{fv.count}
|
|
150
150
|
</span>
|
|
151
151
|
</a>
|
|
@@ -33,7 +33,7 @@ const hasNext = page < pageCount;
|
|
|
33
33
|
---
|
|
34
34
|
|
|
35
35
|
{pageCount > 1 && (
|
|
36
|
-
<nav aria-label="Pagination" class="mt-12 flex flex-col items-center gap-6">
|
|
36
|
+
<nav aria-label="Pagination" data-widget="plp-pagination" class="mt-12 flex flex-col items-center gap-6">
|
|
37
37
|
{hasNext && (
|
|
38
38
|
<a
|
|
39
39
|
href={href(page + 1)}
|
|
@@ -34,7 +34,7 @@ const OPTIONS: { value: NonNullable<ListProductsQuery["sort"]>; label: string }[
|
|
|
34
34
|
const current = query.sort ?? "featured";
|
|
35
35
|
---
|
|
36
36
|
|
|
37
|
-
<form method="get" action={basePath} class="flex items-center gap-2 text-sm">
|
|
37
|
+
<form method="get" action={basePath} data-widget="plp-sort-select" class="flex items-center gap-2 text-sm">
|
|
38
38
|
{/* Preserve filters/price/availability/q across a sort change. */}
|
|
39
39
|
{query.q && <input type="hidden" name="q" value={query.q} />}
|
|
40
40
|
{query.availability && query.availability !== "all" && (
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Persistent left-hand navigation for the preview/dev style-guide surfaces.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Owns its `.sg-nav*` styles: a page's own <style> is scoped to the page's
|
|
6
|
+
* markup and never reaches this component's elements. Like every component
|
|
7
|
+
* style, the build emits them as an external sheet (inlineStylesheets:"never"),
|
|
8
|
+
* so the rail stays CSP-clean. It sits on the dark guide rail and meets the bar
|
|
9
|
+
* the guides enforce: ≥14px text and ≥44px targets. Rendered on the tenant
|
|
8
10
|
* hub, the widget-library guide, and any per-theme guide that wants a persistent
|
|
9
11
|
* rail so the Widget Library Guide is one click away from the top page.
|
|
10
12
|
*
|
|
@@ -93,3 +95,16 @@ const toc = headings.filter((h) => h.depth === 2);
|
|
|
93
95
|
)}
|
|
94
96
|
</ul>
|
|
95
97
|
</nav>
|
|
98
|
+
|
|
99
|
+
<style>
|
|
100
|
+
.sg-nav__eyebrow { font: 800 max(.875rem, 14px)/1.2 -apple-system, sans-serif; letter-spacing: .14em; text-transform: uppercase; color: #7fe3d4; margin: 0 0 12px; padding: 0 10px; }
|
|
101
|
+
.sg-nav__list { list-style: none; margin: 0; padding: 0; }
|
|
102
|
+
.sg-nav__list li { margin: 0; }
|
|
103
|
+
.sg-nav__link { display: flex; align-items: center; min-height: 44px; padding: 8px 10px; border-radius: 8px; color: #bfe0ff; text-decoration: none; font-size: max(.92rem, 14px); }
|
|
104
|
+
.sg-nav__link:hover { background: #16223c; }
|
|
105
|
+
.sg-nav__link.is-active { background: #16223c; color: #fff; font-weight: 700; }
|
|
106
|
+
.sg-nav__link--sub { padding-left: 20px; }
|
|
107
|
+
.sg-nav__toc { list-style: none; margin: 2px 0 8px 16px; padding: 0; border-left: 1px solid #26365a; }
|
|
108
|
+
.sg-nav__toc-link { display: flex; align-items: center; min-height: 44px; padding: 4px 10px 4px 14px; color: #9fb0c8; font-size: max(.82rem, 14px); text-decoration: none; line-height: 1.3; }
|
|
109
|
+
.sg-nav__toc-link:hover { color: #e6edf6; }
|
|
110
|
+
</style>
|
|
@@ -8,18 +8,20 @@ import Layout from "@/layouts/Layout.astro";
|
|
|
8
8
|
import PostAccessBadge from "@/components/blog/PostAccessBadge.astro";
|
|
9
9
|
import type { BlogPostSummary } from "@/lib/blog/types.js";
|
|
10
10
|
import type { TheBuildIndexCopy } from "@/lib/the-build/pagination.js";
|
|
11
|
+
import type { BlogPresentation } from "@/lib/blog/presentation.js";
|
|
11
12
|
import { theBuildPagePath } from "@/lib/the-build/pagination.js";
|
|
12
13
|
|
|
13
14
|
interface Props {
|
|
14
15
|
basePath: string;
|
|
15
16
|
canonicalBase: string;
|
|
16
17
|
copy: TheBuildIndexCopy;
|
|
18
|
+
presentation: BlogPresentation;
|
|
17
19
|
pagePosts: BlogPostSummary[];
|
|
18
20
|
page: number;
|
|
19
21
|
totalPages: number;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
const { basePath, canonicalBase, copy, pagePosts, page, totalPages } = Astro.props;
|
|
24
|
+
const { basePath, canonicalBase, copy, presentation, pagePosts, page, totalPages } = Astro.props;
|
|
23
25
|
|
|
24
26
|
const href = (path: string) => `${basePath}${path}` || "/";
|
|
25
27
|
const path = theBuildPagePath(page);
|
|
@@ -52,11 +54,11 @@ const itemList = {
|
|
|
52
54
|
jsonLd={itemList}
|
|
53
55
|
pagination={pagination}
|
|
54
56
|
>
|
|
55
|
-
<header class="blog-hero">
|
|
57
|
+
<header class:list={["blog-hero", { "blog-hero--plain": !presentation.hero }]}>
|
|
56
58
|
<div class="blog-shell">
|
|
57
|
-
<p class="eyebrow eyebrow--tick">{copy.eyebrow}</p>
|
|
59
|
+
{copy.eyebrow && <p class="eyebrow eyebrow--tick">{copy.eyebrow}</p>}
|
|
58
60
|
<h1>{copy.title}</h1>
|
|
59
|
-
<p>{copy.tagline}</p>
|
|
61
|
+
{copy.tagline && <p class="blog-hero__tagline">{copy.tagline}</p>}
|
|
60
62
|
</div>
|
|
61
63
|
</header>
|
|
62
64
|
|
|
@@ -150,8 +152,10 @@ const itemList = {
|
|
|
150
152
|
.blog-shell { width: min(1180px, calc(100% - 2rem)); margin-inline: auto; }
|
|
151
153
|
.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); }
|
|
152
154
|
.blog-hero h1 { max-width: 12ch; margin-top: .75rem; color: inherit; font-size: clamp(2.9rem, 7vw, 5.6rem); }
|
|
153
|
-
.blog-
|
|
155
|
+
.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; }
|
|
154
156
|
.blog-hero .eyebrow { color: color-mix(in srgb, var(--color-primary-contrast) 75%, transparent); }
|
|
157
|
+
.blog-hero--plain { padding: clamp(2rem, 5vw, 3.5rem) 0 0; background: none; color: inherit; }
|
|
158
|
+
.blog-hero--plain .blog-hero__tagline, .blog-hero--plain .eyebrow { color: var(--color-muted); }
|
|
155
159
|
.post-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.35rem; align-items: start; padding-block: clamp(3rem, 7vw, 6rem); }
|
|
156
160
|
.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; }
|
|
157
161
|
.post-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-md); }
|