@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
|
@@ -0,0 +1,1496 @@
|
|
|
1
|
+
# Storefront widget library — user guide
|
|
2
|
+
|
|
3
|
+
**Status:** maintained reference, workstream `shared-ecommerce-widgets`. This is the
|
|
4
|
+
human- and agent-facing guide to the shared regulated-ecommerce widget library that every
|
|
5
|
+
storefront tenant inherits. It is the **source of truth**; the style-guide route
|
|
6
|
+
`/_style-guide/<tenant>/guide/<theme>/` renders this same file as HTML.
|
|
7
|
+
|
|
8
|
+
> **How to keep this accurate.** Every widget entry is enumerated from real code under
|
|
9
|
+
> `apps/storefront/src/components/**` and the shared libs in `apps/storefront/src/lib/`.
|
|
10
|
+
> When you add, rename, or reshape a component's props, update its entry here in the same change.
|
|
11
|
+
> Where a fact is load-bearing (a prop name, a token, a data attribute) it is quoted from the source.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Overview — how the library works
|
|
16
|
+
|
|
17
|
+
The library is a set of **platform-owned Astro/Preact components**. A tenant never forks a
|
|
18
|
+
component: it restyles the whole library by supplying **theme tokens** and drives regulated
|
|
19
|
+
surfaces with **data**. Three ideas underpin everything below.
|
|
20
|
+
|
|
21
|
+
### 1.1 The token contract (the library's public API)
|
|
22
|
+
|
|
23
|
+
Components read only the **`themeToCssVars` `--color-*` vocabulary** (plus `--font-*`, `--fs-*`,
|
|
24
|
+
`--space-*`, `--radius-*`, `--shadow-*`, `--container-max`, `--section-rhythm`). This CSS
|
|
25
|
+
custom-property vocabulary is the library's public API — the full spec, reference values, and the
|
|
26
|
+
rule for aliasing a tenant's own brand vars onto it live in
|
|
27
|
+
[`docs/architecture/token-contract.md`](architecture/token-contract.md).
|
|
28
|
+
|
|
29
|
+
- **Core palette (every theme states these):** `--color-{bg, surface, text, muted, primary,
|
|
30
|
+
primary-contrast, accent, border, success, sale}`.
|
|
31
|
+
- **Semantic / state (bridge-defaulted — always resolvable):** `--color-{surface-2, warning,
|
|
32
|
+
error, info, accent-ink, primary-hover, accent-hover}`.
|
|
33
|
+
- **The rule for components:** read tokens only — `var(--color-…)` or the Tailwind utility that
|
|
34
|
+
maps to it (`bg-bg`, `text-primary`, `text-error`, `rounded-md`, `border-border`). Never
|
|
35
|
+
hard-code a hex; never read a tenant-private brand var (`var(--brand-red)`) — those are aliased
|
|
36
|
+
onto the contract.
|
|
37
|
+
|
|
38
|
+
Two equivalent token-reference styles appear across the library and both resolve to the same
|
|
39
|
+
contract: chrome + several atoms use **Tailwind token utilities** (`bg-primary`, `text-muted`,
|
|
40
|
+
`border-border`), while PLP components and most marketing blocks use **arbitrary
|
|
41
|
+
`[var(--color-*)]` utilities** or scoped `<style>` blocks with `var(--color-*)`.
|
|
42
|
+
|
|
43
|
+
### 1.2 The shared-vs-tenant boundary
|
|
44
|
+
|
|
45
|
+
What is shared platform vs. what is per-tenant is drawn in
|
|
46
|
+
[`docs/architecture/shared-vs-tenant-assets.md`](architecture/shared-vs-tenant-assets.md):
|
|
47
|
+
|
|
48
|
+
- **Tier 1 — shared platform:** every component in this guide + `styles/global.css` + the style
|
|
49
|
+
guide. Driven by theme tokens.
|
|
50
|
+
- **Tier 3 — per-tenant:** theme tokens, content (`home`/pages/catalog), brand assets. Differences
|
|
51
|
+
between tenants must be **data + tokens, never re-authored component CSS**.
|
|
52
|
+
|
|
53
|
+
### 1.3 Two delivery paths, one look
|
|
54
|
+
|
|
55
|
+
Many chrome/commerce widgets ship **both** as a platform component **and** as shared raw-chrome
|
|
56
|
+
CSS, so a tenant that renders hand-authored raw marketing HTML gets the same look without copying
|
|
57
|
+
CSS (decision `raw-chrome-single-stylesheet` / `materialization-compat`):
|
|
58
|
+
|
|
59
|
+
- **Platform component** — the `.astro`/`.tsx` in `components/**`, token-styled, used by
|
|
60
|
+
token-driven storefronts and the style guide.
|
|
61
|
+
- **Shared raw-chrome CSS** — one platform stylesheet, `public/shared/commerce-chrome.css`
|
|
62
|
+
(linked as `/shared/commerce-chrome.css`), whose class vocabulary (`.site-header`, `.announce`,
|
|
63
|
+
`.primary-nav`/`.mega`, `.site-footer`, `.compliance-strip`, `.age-gate`, …) mirrors the
|
|
64
|
+
component DOM. The server-side generator `lib/rawChrome.ts` emits that DOM from a tenant's typed
|
|
65
|
+
`chrome.json`, links exactly that one stylesheet, and emits **no inline chrome CSS**. See §11.
|
|
66
|
+
|
|
67
|
+
Where a component has a raw-chrome twin, its entry notes the matching selectors.
|
|
68
|
+
|
|
69
|
+
### 1.4 Compliance widgets are platform-owned and data-driven (never faked in marketing)
|
|
70
|
+
|
|
71
|
+
Regulated-commerce notices (`components/compliance/**`) are **platform-owned and data-driven from
|
|
72
|
+
`tenant.compliance`** (decision `compliance-widgets-platform-owned`). They:
|
|
73
|
+
|
|
74
|
+
- render **only** from tenant config, and render **nothing** when their `tenant.compliance` field
|
|
75
|
+
is absent;
|
|
76
|
+
- **never fabricate legal copy** — the mandated wording (FDA nicotine warning, PACT Act, Prop 65
|
|
77
|
+
skeleton) is owned by the platform component; only the on/off flag and small parameters
|
|
78
|
+
(`minAge`, chemical names, state lists) come from config;
|
|
79
|
+
- for display-only cues (purchase limit, state eligibility, shipping restriction, excise tax), the
|
|
80
|
+
**enforcement/decision/calculation stays product-owned** — the widget only surfaces the message.
|
|
81
|
+
|
|
82
|
+
Marketing blocks (§7) must never hand-author a fake compliance notice; use the platform widget fed
|
|
83
|
+
from `tenant.compliance`.
|
|
84
|
+
|
|
85
|
+
The parallel discipline for **membership/subscription** figures (§5, §6): figures are
|
|
86
|
+
**illustrative by default** — unless a tier/figure asserts `real: true`, the widget shows a
|
|
87
|
+
first-class "illustrative figures" affordance (`IllustrativeLabel`) so an example number is never
|
|
88
|
+
shown as a live offer.
|
|
89
|
+
|
|
90
|
+
### 1.5 How to read each entry
|
|
91
|
+
|
|
92
|
+
Every widget lists: **Purpose**, **Props** (exact names + TS types), **Dependencies** (components,
|
|
93
|
+
libs, and any tenant config/locals it reads), **Tokens consumed**, **Delivery** (platform component
|
|
94
|
+
+ any raw-chrome twin), and a realistic **Usage** snippet. The live style guide
|
|
95
|
+
`/_style-guide/<tenant>/<theme>/` renders almost every widget below with sample data.
|
|
96
|
+
|
|
97
|
+
### 1.6 Category index
|
|
98
|
+
|
|
99
|
+
- §2 Chrome — `components/chrome/`
|
|
100
|
+
- §3 Commerce — `components/commerce/`
|
|
101
|
+
- §4 Compliance — `components/compliance/`
|
|
102
|
+
- §5 Membership — `components/membership/`
|
|
103
|
+
- §6 Subscription — `components/subscription/`
|
|
104
|
+
- §7 Marketing / widget blocks — `components/marketing/`
|
|
105
|
+
- §8 PLP controls — `components/plp/`
|
|
106
|
+
- §9 Islands — `components/islands/`
|
|
107
|
+
- §10 Foundational atoms + home — `components/*` + `components/home/`
|
|
108
|
+
- §11 Shared raw-chrome library — `lib/rawChrome.ts`, `lib/chrome/`, `public/shared/commerce-chrome.css`
|
|
109
|
+
|
|
110
|
+
Shared chrome model (`lib/chrome/model.ts`), referenced throughout §2:
|
|
111
|
+
|
|
112
|
+
- `ChromeLink` — `{ label: string; href: string; external?: boolean; badge?: string }`
|
|
113
|
+
- `MegaColumn` — `{ title?: string; links: ChromeLink[] }`
|
|
114
|
+
- `ChromeNavItem` — `{ label: string; href: string; badge?: string; flag?: boolean; children?: ChromeLink[]; columns?: MegaColumn[] }` (plain link / single-column dropdown / multi-column mega)
|
|
115
|
+
- `SocialLink` — `{ label: string; href: string; iconPath?: string }`
|
|
116
|
+
- `ChromeLogo` — `{ src: string; alt: string; width?: number; height?: number }`
|
|
117
|
+
- `ChromeCta` — `{ label: string; href: string; variant?: "primary" | "secondary" | "ghost" }`
|
|
118
|
+
- `FooterColumn` — `{ title: string; links: ChromeLink[] }`
|
|
119
|
+
- `FooterNewsletter` — `{ title?: string; body?: string; action?: string; placeholder?: string; source?: string; ageAttestation?: boolean }`
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 2. Chrome — `components/chrome/`
|
|
124
|
+
|
|
125
|
+
The site frame: announcement, utility bar, header, primary/mega nav, footer. All six have a
|
|
126
|
+
raw-chrome twin in `public/shared/commerce-chrome.css`. Every href-emitting chrome component reads
|
|
127
|
+
`Astro.locals.basePath` via `withBase` (`@/lib/basePath`) for path-prefix tenant routing.
|
|
128
|
+
|
|
129
|
+
### AnnouncementBar (`components/chrome/AnnouncementBar.astro`)
|
|
130
|
+
|
|
131
|
+
- **Purpose:** Top promo strip — `static` centered single line or `marquee` continuously-scrolling
|
|
132
|
+
track (duplicated for a seamless loop). Reduced-motion-safe (freezes to static copy).
|
|
133
|
+
- **Props:** `text: string`; `variant?: "static" | "marquee"`; `repeat?: number` (marquee phrase
|
|
134
|
+
repeats, default 6); `tone?: "primary" | "sale" | "accent"`; `label?: string` (region aria-label);
|
|
135
|
+
`class?: string`.
|
|
136
|
+
- **Dependencies:** none (self-contained; no imports, no locals).
|
|
137
|
+
- **Tokens consumed:** `--font-mono`; tone → `bg-primary text-primary-contrast` /
|
|
138
|
+
`bg-[var(--color-sale)]` / `bg-accent`.
|
|
139
|
+
- **Delivery:** platform component + raw-chrome twin `.announce` (solid), `.announce-marquee` /
|
|
140
|
+
`.announce-marquee__track` (marquee), `.announce-bar` / `.announce-bar__msg` /
|
|
141
|
+
`.announce-bar__close` (dismissible/rotating bar).
|
|
142
|
+
- **Usage:**
|
|
143
|
+
```astro
|
|
144
|
+
<AnnouncementBar text="Free US shipping on orders over $100!" variant="marquee" tone="sale" />
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### UtilityNav (`components/chrome/UtilityNav.astro`)
|
|
148
|
+
|
|
149
|
+
- **Purpose:** Thin secondary bar above the header — minor links, locale/currency indicator, social
|
|
150
|
+
icons. Hidden below `md` (the mobile menu carries the links).
|
|
151
|
+
- **Props:** `links?: ChromeLink[]`; `locale?: string`; `currency?: string`; `social?: SocialLink[]`;
|
|
152
|
+
`class?: string`.
|
|
153
|
+
- **Dependencies:** `type ChromeLink, SocialLink` from `@/lib/chrome/model`; `withBase`.
|
|
154
|
+
- **Tokens consumed:** `border-border`, `bg-surface`, `text-muted`, `hover:text-primary`,
|
|
155
|
+
`font-mono`.
|
|
156
|
+
- **Delivery:** platform component + raw-chrome twin `.utility-nav` / `.utility-nav__right` /
|
|
157
|
+
`.sel` / `.social` / `.member-cue`.
|
|
158
|
+
- **Usage:**
|
|
159
|
+
```astro
|
|
160
|
+
<UtilityNav
|
|
161
|
+
links={[{ label: "News", href: "/news" }, { label: "Contact", href: "/contact" }]}
|
|
162
|
+
locale="English"
|
|
163
|
+
currency="United States (USD $)"
|
|
164
|
+
social={social}
|
|
165
|
+
/>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### SiteHeader (`components/chrome/SiteHeader.astro`)
|
|
169
|
+
|
|
170
|
+
- **Purpose:** Canonical site header — logo/wordmark + search typeahead island + saved / marketing
|
|
171
|
+
CTA / auth / account / cart affordances, with `<MegaMenu>` below and a `<MobileNav>` drawer.
|
|
172
|
+
Cart is a **link only** (managed checkout; count is static). Pure component — the caller resolves
|
|
173
|
+
session/theme/feature state and passes props.
|
|
174
|
+
- **Props:** `logo?: ChromeLogo`; `wordmark?: string`; `nav?: ChromeNavItem[]`; `searchAction?:
|
|
175
|
+
string` (omit to hide search); `announcement?: string` (surfaced in the mobile drawer);
|
|
176
|
+
`accountHref?: string`; `authLink?: { label: string; href: string }`; `savedItemsHref?: string`;
|
|
177
|
+
`headerCtas?: ChromeCta[]`; `cartHref?: string`; `cartCount?: number`; `variant?: "light" |
|
|
178
|
+
"dark"`; `sticky?: boolean` (default `true`); `class?: string`.
|
|
179
|
+
- **Dependencies:** `type ChromeNavItem, ChromeLogo, ChromeCta` from `@/lib/chrome/model`;
|
|
180
|
+
`MegaMenu`; islands `SearchTypeahead` (`client:idle`) and `MobileNav` (`client:idle`); `withBase`.
|
|
181
|
+
- **Tokens consumed:** `border-border`, `bg-surface`/`bg-surface/95`, `text-text`, `bg-primary`,
|
|
182
|
+
`text-primary-contrast`, `hover:text-accent`, `bg-accent`, `bg-[var(--color-sale)]`.
|
|
183
|
+
- **Delivery:** platform component + raw-chrome twin `.site-header` / `.header-row`, `.brand`,
|
|
184
|
+
`.primary-nav-bar`.
|
|
185
|
+
- **Usage:**
|
|
186
|
+
```astro
|
|
187
|
+
<SiteHeader
|
|
188
|
+
wordmark="Sample Store"
|
|
189
|
+
nav={nav}
|
|
190
|
+
searchAction="/search"
|
|
191
|
+
accountHref="/account"
|
|
192
|
+
cartHref="/cart"
|
|
193
|
+
cartCount={2}
|
|
194
|
+
/>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### MegaMenu (`components/chrome/MegaMenu.astro`)
|
|
198
|
+
|
|
199
|
+
- **Purpose:** Primary desktop nav bar — a horizontal `<ul>` of items, each rendered by
|
|
200
|
+
`<NavDropdown>`. Hidden below `md`.
|
|
201
|
+
- **Props:** `items: ChromeNavItem[]`; `ariaLabel?: string` (default `"Primary"`); `class?: string`.
|
|
202
|
+
- **Dependencies:** `type ChromeNavItem` from `@/lib/chrome/model`; `NavDropdown`.
|
|
203
|
+
- **Tokens consumed:** none directly (layout utilities only; token styling delegated to
|
|
204
|
+
`NavDropdown`).
|
|
205
|
+
- **Delivery:** platform component + raw-chrome twin `.primary-nav` / `.primary-nav > ul` /
|
|
206
|
+
`.primary-nav .nav-item`.
|
|
207
|
+
- **Usage:**
|
|
208
|
+
```astro
|
|
209
|
+
<MegaMenu items={nav} />
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### NavDropdown (`components/chrome/NavDropdown.astro`)
|
|
213
|
+
|
|
214
|
+
- **Purpose:** One primary-nav item in two modes. `mode="bar"` (default) = desktop item with a
|
|
215
|
+
CSS-only hover/focus single-column dropdown (`children`) or multi-column mega panel (`columns`),
|
|
216
|
+
zero-JS and keyboard accessible. `mode="accordion"` = mobile stacked item using native
|
|
217
|
+
`<details>/<summary>`. Supports caret, `badge`, and highlighted `flag`.
|
|
218
|
+
- **Props:** `item: ChromeNavItem`; `mode?: "bar" | "accordion"` (default `"bar"`).
|
|
219
|
+
- **Dependencies:** `type ChromeNavItem, ChromeLink` from `@/lib/chrome/model`; `withBase`.
|
|
220
|
+
- **Tokens consumed:** `text-text`, `text-muted`, `text-accent`, `text-primary`, `border-border`,
|
|
221
|
+
`bg-surface`, `bg-primary`, `text-primary-contrast`, `hover:bg-bg`, `bg-[var(--color-sale)]`,
|
|
222
|
+
`shadow-md`, `rounded-md`, `rounded-sm`.
|
|
223
|
+
- **Delivery:** platform component + raw-chrome twin `.nav-item`, `.mega` / `.mega--wide` /
|
|
224
|
+
`.mega__cols` / `.mega__col`, `.caret`, `.new-badge`, `.is-flag`.
|
|
225
|
+
- **Usage:**
|
|
226
|
+
```astro
|
|
227
|
+
<NavDropdown item={item} mode="accordion" />
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### SiteFooter (`components/chrome/SiteFooter.astro`)
|
|
231
|
+
|
|
232
|
+
- **Purpose:** Brand block + link columns + newsletter POST form + payment-badge chip row +
|
|
233
|
+
trust/compliance line + legal bottom bar. Payment badges are text chips (CSP-clean, no external
|
|
234
|
+
images). No cart/checkout logic (commerce boundary).
|
|
235
|
+
- **Props:** `columns: FooterColumn[]`; `brand?: { logo?: ChromeLogo; wordmark?: string; contact?:
|
|
236
|
+
string[] }`; `newsletter?: FooterNewsletter`; `payments?: string[]`; `social?: SocialLink[]`;
|
|
237
|
+
`compliance?: string`; `legal?: string`; `bottomLinks?: ChromeLink[]`; `class?: string`.
|
|
238
|
+
- **Dependencies:** `type ChromeLink, FooterColumn, FooterNewsletter, SocialLink, ChromeLogo` from
|
|
239
|
+
`@/lib/chrome/model`; `withBase`.
|
|
240
|
+
- **Tokens consumed:** `border-border`, `bg-surface`, `text-text`, `text-muted`,
|
|
241
|
+
`hover:text-primary`, `bg-bg`, `bg-primary`, `text-primary-contrast`, `font-display`, `font-mono`.
|
|
242
|
+
- **Delivery:** platform component + raw-chrome twin `.site-footer`, `.footer-grid`,
|
|
243
|
+
`.footer-brand`, `.footer-col`, `.footer-news` (+ `__row`), `.footer-attest`, `.trust-marks`,
|
|
244
|
+
`.footer-warning`, `.footer-bottom`.
|
|
245
|
+
- **Usage:**
|
|
246
|
+
```astro
|
|
247
|
+
<SiteFooter
|
|
248
|
+
brand={{ wordmark: "Sample Store", contact: ["hello@example.com"] }}
|
|
249
|
+
columns={footerCols}
|
|
250
|
+
newsletter={{ title: "Join the community", placeholder: "Email" }}
|
|
251
|
+
payments={["Visa", "Mastercard", "Amex", "PayPal"]}
|
|
252
|
+
social={social}
|
|
253
|
+
compliance="Must be 21+ to purchase. Products intended for adults of legal age only."
|
|
254
|
+
legal="© Sample Store. All rights reserved."
|
|
255
|
+
/>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## 3. Commerce — `components/commerce/`
|
|
261
|
+
|
|
262
|
+
Product cards, merchandising rows, price/rating atoms, blog cards, and checkout wiring.
|
|
263
|
+
`ArticleCard`, `BrandGrid`, and `TagFilterCloud` each carry a scoped `<style>` that mirrors a
|
|
264
|
+
class-only block in `public/shared/commerce-chrome.css` (`.article-card`, `.brand-grid`/`.brand-tile`,
|
|
265
|
+
`.tag-cloud`/`.tag-chip`) — keep the two visually equal.
|
|
266
|
+
|
|
267
|
+
### PriceDisplay (`components/commerce/PriceDisplay.astro`)
|
|
268
|
+
|
|
269
|
+
- **Purpose:** Price atom — struck compare-at when on sale, "From $X" prefix for variant ranges,
|
|
270
|
+
optional sale-percent badge. Accepts a `product` (derives everything) or raw primitives.
|
|
271
|
+
- **Props:** `product?: CatalogProduct`; `amount?: number`; `compareAt?: number | null`; `from?:
|
|
272
|
+
boolean`; `currency?: string`; `locale?: string` (default `"en-US"`); `showBadge?: boolean`
|
|
273
|
+
(default `false`); `size?: "sm" | "md" | "lg"` (default `"md"`); `class?: string`.
|
|
274
|
+
- **Dependencies:** `CatalogProduct` from `@tot/public-runtime`; `formatPrice, discountPercent`
|
|
275
|
+
(`@/lib/format`); `fromPrice, isOnSale` (`@/lib/tot/query`); `Badge`.
|
|
276
|
+
- **Tokens consumed:** `text-muted`, `text-sale`, `font-display`; sale badge carries `--color-sale`.
|
|
277
|
+
- **Delivery:** platform component.
|
|
278
|
+
- **Usage:**
|
|
279
|
+
```astro
|
|
280
|
+
<PriceDisplay amount={64} compareAt={96} currency="USD" showBadge />
|
|
281
|
+
<PriceDisplay amount={42} from currency="USD" />
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### RatingStars (`components/commerce/RatingStars.astro`)
|
|
285
|
+
|
|
286
|
+
- **Purpose:** 0–5 star rating with fractional fill (single clipped overlay) + accessible label;
|
|
287
|
+
optional numeric review count. Presentation only.
|
|
288
|
+
- **Props:** `rating: number` (0–5); `count?: number`; `max?: number` (default `5`); `size?: "sm" |
|
|
289
|
+
"md"` (default `"sm"`); `showCount?: boolean` (default `true`); `class?: string`.
|
|
290
|
+
- **Dependencies:** none.
|
|
291
|
+
- **Tokens consumed:** `text-[var(--color-border)]` (empty track), `text-[var(--color-accent)]`
|
|
292
|
+
(filled), `text-muted` (count).
|
|
293
|
+
- **Delivery:** platform component.
|
|
294
|
+
- **Usage:**
|
|
295
|
+
```astro
|
|
296
|
+
<RatingStars rating={4.5} count={128} />
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### ProductCarousel (`components/commerce/ProductCarousel.astro`)
|
|
300
|
+
|
|
301
|
+
- **Purpose:** Horizontal-scroll row of `<ProductCard>`s bound to a `SectionSource`, with optional
|
|
302
|
+
CSS-only tabs (each tab its own source; radio + adjacent-sibling reveal, zero JS). Presets
|
|
303
|
+
(BestSellers / NewArrivals / OnSale) are this + a `SECTION_PRESETS.*` source.
|
|
304
|
+
- **Props:** `source?: SectionSource`; `products?: CatalogProduct[]` (wins over source); `tabs?:
|
|
305
|
+
Tab[]` (wins over both, where `Tab = { label: string; source?: SectionSource; products?:
|
|
306
|
+
CatalogProduct[] }`); `limit?: number` (default `DEFAULT_SECTION_LIMIT`); `title?: string`;
|
|
307
|
+
`eyebrow?: string`; `columns?: 2 | 3 | 4 | 5` (default `4`); `variant?: "default" | "compact"`
|
|
308
|
+
(default `"default"`); `tone?: "bg" | "surface"` (default `"bg"`); `class?: string`.
|
|
309
|
+
- **Dependencies:** `CatalogProduct` from `@tot/public-runtime`; `Section`, `ProductCard`;
|
|
310
|
+
`resolveSectionSource, DEFAULT_SECTION_LIMIT, SectionSource` (`@/lib/sections`). Reads
|
|
311
|
+
`Astro.locals.tot`.
|
|
312
|
+
- **Tokens consumed:** `--color-border`, `--color-muted`, `--color-text`, `--color-primary`,
|
|
313
|
+
`--color-primary-contrast`, `--color-accent`, `--radius-full`, `--font-mono`.
|
|
314
|
+
- **Delivery:** platform component.
|
|
315
|
+
- **Usage:**
|
|
316
|
+
```astro
|
|
317
|
+
<ProductCarousel title="Best Sellers" eyebrow="Shop" products={products} tone="surface" />
|
|
318
|
+
<ProductCarousel title="Featured" tabs={[{ label: "New", products: newest }, { label: "On Sale", products: onSale }]} />
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### ProductGrid (`components/commerce/ProductGrid.astro`)
|
|
322
|
+
|
|
323
|
+
- **Purpose:** Non-scrolling paginated-look grid of `<ProductCard>`s bound to a `SectionSource`.
|
|
324
|
+
Thin sibling of `ProductCarousel`; `columns` tunes density; hides itself when empty.
|
|
325
|
+
- **Props:** `source?: SectionSource`; `products?: CatalogProduct[]` (wins over source); `limit?:
|
|
326
|
+
number` (default `DEFAULT_SECTION_LIMIT`); `title?: string`; `eyebrow?: string`; `columns?: 2 | 3
|
|
327
|
+
| 4 | 5` (default `4`); `tone?: "bg" | "surface"` (default `"bg"`); `hideWhenEmpty?: boolean`
|
|
328
|
+
(default `true`); `class?: string`.
|
|
329
|
+
- **Dependencies:** `CatalogProduct` from `@tot/public-runtime`; `Section`, `ProductCard`,
|
|
330
|
+
`EmptyState`; `@/lib/sections`. Reads `Astro.locals.tot`.
|
|
331
|
+
- **Tokens consumed:** none directly (token surface from child `ProductCard` / `Section` /
|
|
332
|
+
`EmptyState`).
|
|
333
|
+
- **Delivery:** platform component.
|
|
334
|
+
- **Usage:**
|
|
335
|
+
```astro
|
|
336
|
+
<ProductGrid title="All Products" products={products} columns={3} />
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### BrandGrid (`components/commerce/BrandGrid.astro`)
|
|
340
|
+
|
|
341
|
+
- **Purpose:** "Shop by Brands" logo wall — grid of tiles linking to brand collections; each tile is
|
|
342
|
+
a rehosted logo image OR a text-wordmark fallback. Optional "View all brands" CTA.
|
|
343
|
+
- **Props:** `brands: Brand[]` where `export interface Brand { label: string; href: string; logoSrc?:
|
|
344
|
+
string; logoRehosted?: boolean; logoWidth?: number; logoHeight?: number }`; `title?: string`;
|
|
345
|
+
`eyebrow?: string`; `tone?: "bg" | "surface"` (default `"bg"`); `viewAllHref?: string`;
|
|
346
|
+
`viewAllLabel?: string` (default `"View all brands"`); `class?: string`.
|
|
347
|
+
- **Dependencies:** `Section`, `Img`; `withBase`.
|
|
348
|
+
- **Tokens consumed:** `--color-surface`, `--color-border`, `--color-text`, `--color-primary`,
|
|
349
|
+
`--radius-md`, `--radius-full`, `--shadow-md`, `--font-display`.
|
|
350
|
+
- **Delivery:** platform component + raw-chrome twin `.brand-grid` / `.brand-tile`.
|
|
351
|
+
- **Usage:**
|
|
352
|
+
```astro
|
|
353
|
+
<BrandGrid title="Shop by Brands" eyebrow="Find your fav" brands={brands} viewAllHref="/brands" tone="surface" />
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### ArticleCard (`components/commerce/ArticleCard.astro`)
|
|
357
|
+
|
|
358
|
+
- **Purpose:** One blog/article card — featured image + category tag chip + title + excerpt + meta
|
|
359
|
+
(author · date · read-time). Presentational; omits any absent field's row, never fabricates
|
|
360
|
+
authors/dates/excerpts.
|
|
361
|
+
- **Props:** `article: Article` where `export interface Article { title: string; href: string;
|
|
362
|
+
excerpt?: string; image?: ArticleImage; tag?: string; tagHref?: string; author?: string; date?:
|
|
363
|
+
string; dateTime?: string; readMinutes?: number }` and `ArticleImage = { src: string; alt?: string;
|
|
364
|
+
rehosted?: boolean; width?: number; height?: number }`; `as?: "h2" | "h3" | "h4"` (default `"h3"`);
|
|
365
|
+
`class?: string`.
|
|
366
|
+
- **Dependencies:** `Img`; `withBase`.
|
|
367
|
+
- **Tokens consumed:** `--color-surface`, `--color-border`, `--color-surface-2`, `--color-primary`,
|
|
368
|
+
`--color-text`, `--color-muted`, `--radius-lg`, `--shadow-md`, `--font-display`.
|
|
369
|
+
- **Delivery:** platform component + raw-chrome twin `.article-card`.
|
|
370
|
+
- **Usage:**
|
|
371
|
+
```astro
|
|
372
|
+
<ArticleCard article={article} />
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### TagFilterCloud (`components/commerce/TagFilterCloud.astro`)
|
|
376
|
+
|
|
377
|
+
- **Purpose:** Cloud of tag/category chips for a blog/article index, each a plain link to its
|
|
378
|
+
filtered view (URL-addressable, zero-JS). Active tag filled + `aria-current`; optional leading
|
|
379
|
+
"All" chip clears the filter.
|
|
380
|
+
- **Props:** `tags: TagItem[]` where `export interface TagItem { label: string; href: string; count?:
|
|
381
|
+
number; active?: boolean }`; `title?: string`; `allHref?: string`; `allLabel?: string` (default
|
|
382
|
+
`"All"`); `allActive?: boolean` (default `false`); `ariaLabel?: string` (default `"Filter by
|
|
383
|
+
tag"`); `class?: string`.
|
|
384
|
+
- **Dependencies:** `withBase`.
|
|
385
|
+
- **Tokens consumed:** `--color-muted`, `--color-border`, `--color-surface`, `--color-text`,
|
|
386
|
+
`--color-primary`, `--color-primary-contrast`, `--radius-full`, `--font-display`.
|
|
387
|
+
- **Delivery:** platform component + raw-chrome twin `.tag-cloud` / `.tag-chip`.
|
|
388
|
+
- **Usage:**
|
|
389
|
+
```astro
|
|
390
|
+
<TagFilterCloud title="Browse by topic" tags={tags} allHref="/blog" allActive={false} />
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### CheckoutLoader (`components/commerce/CheckoutLoader.astro`)
|
|
394
|
+
|
|
395
|
+
- **Purpose:** Injects the managed checkout loader near end of `<body>` to enable the cart drawer +
|
|
396
|
+
hosted checkout. Rendered only for commerce tenants; marketing tenants ship nothing.
|
|
397
|
+
- **Props:** none. Reads `Astro.locals.commerce` (`commerce.loaderSrc`).
|
|
398
|
+
- **Dependencies:** none.
|
|
399
|
+
- **Tokens consumed:** none.
|
|
400
|
+
- **Delivery:** platform component.
|
|
401
|
+
- **Usage:**
|
|
402
|
+
```astro
|
|
403
|
+
<CheckoutLoader />
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### CartDrawerStyles (`components/commerce/CartDrawerStyles.astro`)
|
|
407
|
+
|
|
408
|
+
- **Purpose:** Emits a nonce'd `<style>` mapping the in-page cart drawer (`#fc` light DOM)
|
|
409
|
+
selectors to the tenant's theme tokens so the cart is brand-matched. Baseline layer plus optional
|
|
410
|
+
tenant `cartDrawerCss` override appended verbatim.
|
|
411
|
+
Commerce tenants only.
|
|
412
|
+
- **Props:** none. Reads `Astro.locals.commerce` (`commerce.cartDrawerCss`) and `Astro.locals.cspNonce`.
|
|
413
|
+
- **Dependencies:** none.
|
|
414
|
+
- **Tokens consumed (scoped under `#fc`):** `--font-body`, `--font-display`, `--color-text`,
|
|
415
|
+
`--color-primary`, `--color-primary-contrast`, `--radius-md`.
|
|
416
|
+
- **Delivery:** platform component.
|
|
417
|
+
- **Usage:**
|
|
418
|
+
```astro
|
|
419
|
+
<CartDrawerStyles />
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## 4. Compliance — `components/compliance/`
|
|
425
|
+
|
|
426
|
+
Platform-owned, data-driven from `tenant.compliance` (decision `compliance-widgets-platform-owned`).
|
|
427
|
+
Every widget renders **nothing** when its config field is absent, **never fabricates legal copy**,
|
|
428
|
+
and emits `role="note"` + a `data-compliance="…"` hook. All share the presentational
|
|
429
|
+
`ComplianceNotice` base except `NicotineWarning` (self-contained). The `tenant.compliance` config
|
|
430
|
+
types live in `packages/public-runtime/src/tenant.ts` (`ComplianceConfig`): `nicotineWarning`,
|
|
431
|
+
`pactAct`, `adultSignature` are `boolean`; `shippingRestriction` is `string`; `minAge` is `number`;
|
|
432
|
+
`prop65`/`purchaseLimit`/`stateEligibility`/`exciseTax` are the config objects below.
|
|
433
|
+
|
|
434
|
+
### ComplianceNotice (`components/compliance/ComplianceNotice.astro`)
|
|
435
|
+
|
|
436
|
+
- **Purpose:** Shared presentational shell for the compliance widgets. Owns only the visual
|
|
437
|
+
treatment; the fixed legal text is supplied by each sibling via `<slot />`. Adds no copy of its own.
|
|
438
|
+
- **Props:** `variant?: "bar" | "block" | "inline" | "footer"` (default `"block"`); `tone?: "warning"
|
|
439
|
+
| "info" | "neutral"` (default `"neutral"`); `hook: string` (required — the `data-compliance`
|
|
440
|
+
value); `label: string` (required — accessible label); `symbol?: string` (optional leading glyph,
|
|
441
|
+
e.g. `⚠`).
|
|
442
|
+
- **`data-compliance`:** emits the caller's `hook`.
|
|
443
|
+
- **Dependencies:** none (leaf base). Reads no tenant field directly — siblings pass data in.
|
|
444
|
+
- **Tokens consumed:** `--color-border`, `--color-surface-2`, `--color-bg`, `--color-text`,
|
|
445
|
+
`--color-muted`, `--color-warning`, `--color-info`, `--radius-md`.
|
|
446
|
+
- **Delivery:** platform base + raw-chrome counterparts `.compliance-strip` (bar/block),
|
|
447
|
+
`.topbar-warning` (top FDA band), `.footer-warning` (footer band).
|
|
448
|
+
- **Usage:** consumed by the siblings below (not used directly).
|
|
449
|
+
|
|
450
|
+
### NicotineWarning (`components/compliance/NicotineWarning.astro`)
|
|
451
|
+
|
|
452
|
+
- **Purpose:** FDA-mandated nicotine warning (21 CFR 1143.3). Wording fixed by law, verbatim.
|
|
453
|
+
Self-contained (does not use `ComplianceNotice`).
|
|
454
|
+
- **Props:** `variant?: "bar" | "block"` (default `"bar"`).
|
|
455
|
+
- **`data-compliance`:** `"nicotine-warning"`. Gated by `tenant.compliance.nicotineWarning`.
|
|
456
|
+
- **Dependencies:** none.
|
|
457
|
+
- **Tokens consumed:** `--color-text` (inverted bg/border), `--color-bg` (text on bar),
|
|
458
|
+
`--radius-md`.
|
|
459
|
+
- **Delivery:** platform component + raw-chrome twin `.topbar-warning`.
|
|
460
|
+
- **Usage:**
|
|
461
|
+
```astro
|
|
462
|
+
<NicotineWarning variant="bar" />
|
|
463
|
+
<NicotineWarning variant="block" />
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### PactActNotice (`components/compliance/PactActNotice.astro`)
|
|
467
|
+
|
|
468
|
+
- **Purpose:** PACT Act compliance notice (ENDS/vapor). Federal-requirement wording fixed here; only
|
|
469
|
+
on/off + age come from config.
|
|
470
|
+
- **Props:** `enabled?: boolean` (default `false`; from `tenant.compliance.pactAct`); `minAge?:
|
|
471
|
+
number` (default `21`; from `tenant.compliance.minAge`); `variant?: "bar" | "block" | "inline" |
|
|
472
|
+
"footer"` (default `"block"`). Passes `tone="info"`.
|
|
473
|
+
- **`data-compliance`:** `"pact-act"`.
|
|
474
|
+
- **Dependencies:** `ComplianceNotice`. Reads `tenant.compliance.pactAct` + `.minAge`.
|
|
475
|
+
- **Tokens consumed:** info tone → `--color-info` (+ base tokens).
|
|
476
|
+
- **Delivery:** platform component + raw-chrome `.compliance-strip` / `.footer-warning`.
|
|
477
|
+
- **Usage:**
|
|
478
|
+
```astro
|
|
479
|
+
<PactActNotice enabled minAge={21} variant="bar" />
|
|
480
|
+
<PactActNotice enabled minAge={21} variant="footer" />
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### AdultSignatureNotice (`components/compliance/AdultSignatureNotice.astro`)
|
|
484
|
+
|
|
485
|
+
- **Purpose:** Adult-signature-on-delivery notice. Fixed wording; age reflects `minAge`.
|
|
486
|
+
- **Props:** `enabled?: boolean` (default `false`; from `tenant.compliance.adultSignature`); `minAge?:
|
|
487
|
+
number` (default `21`); `variant?: "bar" | "block" | "inline" | "footer"` (default `"inline"`).
|
|
488
|
+
Passes `tone="neutral"`.
|
|
489
|
+
- **`data-compliance`:** `"adult-signature"`.
|
|
490
|
+
- **Dependencies:** `ComplianceNotice`. Reads `tenant.compliance.adultSignature` + `.minAge`.
|
|
491
|
+
- **Tokens consumed:** neutral tone → `--color-text` / `--color-muted`.
|
|
492
|
+
- **Delivery:** platform component + raw-chrome `.compliance-strip`.
|
|
493
|
+
- **Usage:**
|
|
494
|
+
```astro
|
|
495
|
+
<AdultSignatureNotice enabled minAge={21} variant="inline" />
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Prop65Warning (`components/compliance/Prop65Warning.astro`)
|
|
499
|
+
|
|
500
|
+
- **Purpose:** California Prop 65 safe-harbor warning. Owns the mandated skeleton, the `⚠` symbol,
|
|
501
|
+
and the official warnings URL. Only harm endpoint(s) + chemical name(s) come from config. Short
|
|
502
|
+
form (`config={true}`) vs long form (when `chemicals` supplied).
|
|
503
|
+
- **Props:** `config?: Prop65Config | boolean` (from `tenant.compliance.prop65`; `true` normalizes to
|
|
504
|
+
`{ harm: "cancer-and-reproductive" }`), where `Prop65Config = { harm: "cancer" | "reproductive" |
|
|
505
|
+
"cancer-and-reproductive"; chemicals?: string[]; url?: string }`; `variant?: "bar" | "block" |
|
|
506
|
+
"inline" | "footer"` (default `"block"`). Passes `tone="warning"`, `symbol="⚠"`.
|
|
507
|
+
- **`data-compliance`:** `"prop65"`.
|
|
508
|
+
- **Dependencies:** `ComplianceNotice`, `type Prop65Config` from `@tot/public-runtime`. Reads
|
|
509
|
+
`tenant.compliance.prop65`.
|
|
510
|
+
- **Tokens consumed:** warning tone → `--color-warning`; underlined `<a>`.
|
|
511
|
+
- **Delivery:** platform component + raw-chrome `.compliance-strip`.
|
|
512
|
+
- **Usage:**
|
|
513
|
+
```astro
|
|
514
|
+
<Prop65Warning config={true} variant="block" />
|
|
515
|
+
<Prop65Warning config={{ harm: "reproductive", chemicals: ["nicotine"] }} variant="block" />
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### PurchaseLimitNotice (`components/compliance/PurchaseLimitNotice.astro`)
|
|
519
|
+
|
|
520
|
+
- **Purpose:** Purchase-quantity-limit **display** (state/PACT order caps). Display surface only —
|
|
521
|
+
enforcement stays product/commerce-owned.
|
|
522
|
+
- **Props:** `config?: PurchaseLimitConfig` (from `tenant.compliance.purchaseLimit`), where
|
|
523
|
+
`PurchaseLimitConfig = { text: string; reason?: string }`; `variant?: "bar" | "block" | "inline" |
|
|
524
|
+
"footer"` (default `"inline"`). Passes `tone="neutral"`.
|
|
525
|
+
- **`data-compliance`:** `"purchase-limit"`.
|
|
526
|
+
- **Dependencies:** `ComplianceNotice`, `type PurchaseLimitConfig` from `@tot/public-runtime`.
|
|
527
|
+
- **Tokens consumed:** neutral tone via base.
|
|
528
|
+
- **Delivery:** platform component + raw-chrome `.compliance-strip`.
|
|
529
|
+
- **Usage:**
|
|
530
|
+
```astro
|
|
531
|
+
<PurchaseLimitNotice config={{ text: "2 units per order", reason: "per state law" }} variant="inline" />
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### StateEligibilityNotice (`components/compliance/StateEligibilityNotice.astro`)
|
|
535
|
+
|
|
536
|
+
- **Purpose:** State/jurisdiction sale-eligibility **display**. Shows tenant-configured messaging;
|
|
537
|
+
does **not** decide eligibility for a cart/address. Precedence: explicit `note` wins, else composes
|
|
538
|
+
from `restrictedStates` or `eligibleStates`.
|
|
539
|
+
- **Props:** `config?: StateEligibilityConfig` (from `tenant.compliance.stateEligibility`), where
|
|
540
|
+
`StateEligibilityConfig = { restrictedStates?: string[]; eligibleStates?: string[]; note?: string }`;
|
|
541
|
+
`variant?: "bar" | "block" | "inline" | "footer"` (default `"block"`). Passes `tone="warning"`,
|
|
542
|
+
`symbol="⚠"`.
|
|
543
|
+
- **`data-compliance`:** `"state-eligibility"`.
|
|
544
|
+
- **Dependencies:** `ComplianceNotice`, `type StateEligibilityConfig` from `@tot/public-runtime`.
|
|
545
|
+
- **Tokens consumed:** warning tone → `--color-warning`.
|
|
546
|
+
- **Delivery:** platform component + raw-chrome `.compliance-strip`.
|
|
547
|
+
- **Usage:**
|
|
548
|
+
```astro
|
|
549
|
+
<StateEligibilityNotice config={{ restrictedStates: ["UT", "AR", "VT"] }} variant="block" />
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### ShippingRestrictionNotice (`components/compliance/ShippingRestrictionNotice.astro`)
|
|
553
|
+
|
|
554
|
+
- **Purpose:** Shipping-restriction **display** (PACT no-ship states, adult signature, carrier
|
|
555
|
+
limits). Renders the tenant's configured text; the ship/no-ship decision stays product-owned. This
|
|
556
|
+
is the platform component behind the footer shipping band the app Layout previously inlined.
|
|
557
|
+
- **Props:** `text?: string` (from `tenant.compliance.shippingRestriction`, display text only);
|
|
558
|
+
`variant?: "bar" | "block" | "inline" | "footer"` (default `"footer"`). Passes `tone="neutral"`.
|
|
559
|
+
- **`data-compliance`:** `"shipping-restriction"`.
|
|
560
|
+
- **Dependencies:** `ComplianceNotice`. Reads `tenant.compliance.shippingRestriction` (`string`).
|
|
561
|
+
- **Tokens consumed:** neutral tone; footer variant uses `--color-bg`, `--color-border`,
|
|
562
|
+
`--color-muted`.
|
|
563
|
+
- **Delivery:** platform component + raw-chrome twin `.footer-warning`.
|
|
564
|
+
- **Usage:**
|
|
565
|
+
```astro
|
|
566
|
+
<ShippingRestrictionNotice text="Adult signature (21+) required on delivery. Not shipped via USPS." variant="footer" />
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### ExciseTaxNotice (`components/compliance/ExciseTaxNotice.astro`)
|
|
570
|
+
|
|
571
|
+
- **Purpose:** Excise-tax **display** cue. Surfaces that excise taxes apply (+ optional
|
|
572
|
+
per-jurisdiction breakdown); **never computes tax** — calculation is product/commerce-owned.
|
|
573
|
+
- **Props:** `config?: ExciseTaxConfig` (from `tenant.compliance.exciseTax`), where `ExciseTaxConfig =
|
|
574
|
+
{ note: string; jurisdictions?: Array<{ label: string; detail: string }> }`; `variant?: "bar" |
|
|
575
|
+
"block" | "inline" | "footer"` (default `"inline"`). Passes `tone="info"`. Renders only if
|
|
576
|
+
`config.note` is non-empty.
|
|
577
|
+
- **`data-compliance`:** `"excise-tax"`.
|
|
578
|
+
- **Dependencies:** `ComplianceNotice`, `type ExciseTaxConfig` from `@tot/public-runtime`.
|
|
579
|
+
- **Tokens consumed:** info tone → `--color-info`; jurisdiction spans use `--color-muted`.
|
|
580
|
+
- **Delivery:** platform component + raw-chrome `.compliance-strip`.
|
|
581
|
+
- **Usage:**
|
|
582
|
+
```astro
|
|
583
|
+
<ExciseTaxNotice config={{ note: "Applicable state vapor excise taxes are calculated at checkout." }} variant="inline" />
|
|
584
|
+
<ExciseTaxNotice config={{ note: "State excise taxes apply:", jurisdictions: [{ label: "CA", detail: "$0.05/ml" }, { label: "NY", detail: "20% wholesale" }] }} variant="block" />
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
## 5. Membership — `components/membership/`
|
|
590
|
+
|
|
591
|
+
Presentational + **education-only** (no pricing/checkout/subscription mechanics; CTAs are plain
|
|
592
|
+
handoff links). Figures are **illustrative by default**: a tier's figure is an example unless the
|
|
593
|
+
tier asserts `real: true` (`isIllustrative(tier)` returns `tier.real !== true`). Model in
|
|
594
|
+
`lib/membership/model.ts`. All emit `data-widget="…"`.
|
|
595
|
+
|
|
596
|
+
Model types (`lib/membership/model.ts`):
|
|
597
|
+
|
|
598
|
+
- `TierFigure` — `{ value: string; caption?: string }` (`value` is display text like `"$0"`,
|
|
599
|
+
`"$19/mo"`, `"Save 15%"` — never a computed number).
|
|
600
|
+
- `TierFeature` — `{ label: string; included?: boolean }` (`true`→check, `false`→dash+strikethrough,
|
|
601
|
+
omitted→plain bullet).
|
|
602
|
+
- `TierCta` — `{ label: string; href: string }` (education/handoff link only).
|
|
603
|
+
- `Tier` — `{ name: string; tagline?: string; figure?: TierFigure; features?: TierFeature[]; cta?:
|
|
604
|
+
TierCta; featured?: boolean; badge?: string; real?: boolean }`.
|
|
605
|
+
- `ComparisonRow` — `{ label: string; cells: Array<boolean | string | null> }` (aligned to `tiers`
|
|
606
|
+
by index: boolean→check/dash, string→text, null→em dash).
|
|
607
|
+
- `isIllustrative(tier: Pick<Tier, "real">): boolean` → `tier.real !== true`.
|
|
608
|
+
|
|
609
|
+
### IllustrativeLabel (`components/membership/IllustrativeLabel.astro`)
|
|
610
|
+
|
|
611
|
+
- **Purpose:** The first-class "these figures are examples, not an offer" affordance shared by
|
|
612
|
+
`TierCards`, `TierComparisonTable`, and `SavingsExplainer`.
|
|
613
|
+
- **Props:** `as?: "badge" | "note"` (default `"badge"`); `text?: string` (override copy).
|
|
614
|
+
- **`data-illustrative`:** `"badge"` or `"note"` (note: `data-illustrative`, not `data-widget`).
|
|
615
|
+
- **Dependencies:** none (leaf; driven by the parent widget's illustrative state).
|
|
616
|
+
- **Tokens consumed:** `--color-muted` (note), `--color-info` (badge border + text), `--radius-full`.
|
|
617
|
+
- **Delivery:** platform component (no raw-chrome twin).
|
|
618
|
+
- **Usage:** rendered internally by the tier/savings widgets; `<IllustrativeLabel as="badge" />`.
|
|
619
|
+
|
|
620
|
+
### TierCards (`components/membership/TierCards.astro`)
|
|
621
|
+
|
|
622
|
+
- **Purpose:** Membership/pricing tier comparison card row (e.g. Guest / Member / Autopilot). Shows
|
|
623
|
+
tiers, headline figures, feature bullets; each CTA is a plain handoff link. When any shown tier is
|
|
624
|
+
illustrative, the header carries an `IllustrativeLabel` badge, each illustrative figure is prefixed
|
|
625
|
+
"e.g." + `data-illustrative="figure"`, and a footnote spells it out.
|
|
626
|
+
- **Props:** `tiers: Tier[]` (required); `eyebrow?: string`; `heading?: string`; `subhead?: string`.
|
|
627
|
+
- **`data-widget`:** `"tier-cards"` (per-tier `data-tier={tier.name}`).
|
|
628
|
+
- **Dependencies:** `type Tier` + `isIllustrative` (`@/lib/membership/model`); `IllustrativeLabel`.
|
|
629
|
+
No tenant-config field (tenant supplies `tiers` on a membership page).
|
|
630
|
+
- **Tokens consumed:** `--color-accent-ink`, `--color-text`, `--color-muted`, `--color-surface`,
|
|
631
|
+
`--color-border`, `--color-primary`, `--color-primary-contrast`, `--color-success`, `--radius-lg`,
|
|
632
|
+
`--radius-full`, `--shadow-md`.
|
|
633
|
+
- **Delivery:** platform component (no raw-chrome twin).
|
|
634
|
+
- **Usage:**
|
|
635
|
+
```astro
|
|
636
|
+
<TierCards eyebrow="Membership" heading="Choose how you shop" subhead="Compare guest checkout with our membership tiers." tiers={tiers} />
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
### TierComparisonTable (`components/membership/TierComparisonTable.astro`)
|
|
640
|
+
|
|
641
|
+
- **Purpose:** Feature matrix companion to `TierCards` — tiers across the top, features down the side,
|
|
642
|
+
one cell per (feature × tier). Same illustrative-figure affordance. Horizontally scrollable.
|
|
643
|
+
- **Props:** `tiers: Tier[]` (required); `rows: ComparisonRow[]` (required, cells align to `tiers` by
|
|
644
|
+
index); `eyebrow?: string`; `heading?: string`.
|
|
645
|
+
- **`data-widget`:** `"tier-comparison-table"`.
|
|
646
|
+
- **Dependencies:** `type Tier, ComparisonRow` + `isIllustrative` (`@/lib/membership/model`);
|
|
647
|
+
`IllustrativeLabel`.
|
|
648
|
+
- **Tokens consumed:** `--color-accent-ink`, `--color-text`, `--color-muted`, `--color-surface-2`,
|
|
649
|
+
`--color-border`, `--color-success`, `--radius-md`.
|
|
650
|
+
- **Delivery:** platform component (no raw-chrome twin).
|
|
651
|
+
- **Usage:**
|
|
652
|
+
```astro
|
|
653
|
+
<TierComparisonTable eyebrow="Compare" heading="What each tier includes" tiers={tiers} rows={comparisonRows} />
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
---
|
|
657
|
+
|
|
658
|
+
## 6. Subscription — `components/subscription/`
|
|
659
|
+
|
|
660
|
+
Presentational + **education-only**: explain recurring/Autopilot ordering and hand off to the
|
|
661
|
+
tenant's product-owned subscribe/manage flow; **no scheduling/billing/cancellation mechanics**. The
|
|
662
|
+
savings figure is illustrative by default (same discipline as §5). Model in
|
|
663
|
+
`lib/subscription/model.ts`. All emit `data-widget="…"`.
|
|
664
|
+
|
|
665
|
+
Model types (`lib/subscription/model.ts`):
|
|
666
|
+
|
|
667
|
+
- `SubscriptionStep` — `{ title: string; description: string }`.
|
|
668
|
+
- `SubscriptionFigure` — `{ value: string; caption?: string; real?: boolean }` (`value` is display
|
|
669
|
+
text like `"Save 15%"` — never computed).
|
|
670
|
+
- `SubscriptionManageEntry` — `{ label: string; description?: string; href: string }` (handoff URL).
|
|
671
|
+
- `isIllustrative(figure: Pick<SubscriptionFigure, "real">): boolean` → `figure.real !== true`.
|
|
672
|
+
|
|
673
|
+
### HowItWorksSteps (`components/subscription/HowItWorksSteps.astro`)
|
|
674
|
+
|
|
675
|
+
- **Purpose:** Autopilot/subscription "how it works" explainer (numbered steps). Ships no
|
|
676
|
+
scheduling/billing mechanics.
|
|
677
|
+
- **Props:** `steps: SubscriptionStep[]` (required); `eyebrow?: string`; `heading?: string`;
|
|
678
|
+
`subhead?: string`.
|
|
679
|
+
- **`data-widget`:** `"subscription-how-it-works"`.
|
|
680
|
+
- **Dependencies:** `type SubscriptionStep` (`@/lib/subscription/model`).
|
|
681
|
+
- **Tokens consumed:** `--color-accent-ink`, `--color-text`, `--color-muted`, `--color-border`,
|
|
682
|
+
`--color-surface`, `--color-primary`, `--color-primary-contrast`, `--radius-lg`, `--radius-full`.
|
|
683
|
+
- **Delivery:** platform component (no raw-chrome twin).
|
|
684
|
+
- **Usage:**
|
|
685
|
+
```astro
|
|
686
|
+
<HowItWorksSteps eyebrow="Autopilot" heading="How Autopilot works" subhead="Set it up once — we handle the rest." steps={steps} />
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
### SavingsExplainer (`components/subscription/SavingsExplainer.astro`)
|
|
690
|
+
|
|
691
|
+
- **Purpose:** Autopilot/subscription savings headline (e.g. "Save 15% — vs. one-time purchase") +
|
|
692
|
+
supporting bullets + plain handoff CTA. Figure illustrative by default → "e.g." affordance +
|
|
693
|
+
`IllustrativeLabel` when illustrative.
|
|
694
|
+
- **Props:** `figure: SubscriptionFigure` (required); `eyebrow?: string`; `heading?: string`;
|
|
695
|
+
`bullets?: string[]`; `cta?: { label: string; href: string }` (education/handoff, never a checkout
|
|
696
|
+
mechanic).
|
|
697
|
+
- **`data-widget`:** `"subscription-savings-explainer"`.
|
|
698
|
+
- **Dependencies:** `type SubscriptionFigure` + `isIllustrative` (`@/lib/subscription/model`);
|
|
699
|
+
`IllustrativeLabel` (cross-imports the membership affordance).
|
|
700
|
+
- **Tokens consumed:** `--color-accent-ink`, `--color-text`, `--color-muted`, `--color-border`,
|
|
701
|
+
`--color-surface`, `--color-success`, `--color-primary`, `--color-primary-contrast`, `--radius-lg`,
|
|
702
|
+
`--radius-full`.
|
|
703
|
+
- **Delivery:** platform component (no raw-chrome twin).
|
|
704
|
+
- **Usage:**
|
|
705
|
+
```astro
|
|
706
|
+
<SavingsExplainer eyebrow="Autopilot savings" heading="Save on every order" figure={{ value: "Save 15%", caption: "vs. one-time purchase" }} bullets={["Free shipping on every Autopilot order", "Pause or cancel anytime"]} cta={{ label: "How Autopilot works", href: "/autopilot" }} />
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
### ManageSubscriptionEntry (`components/subscription/ManageSubscriptionEntry.astro`)
|
|
710
|
+
|
|
711
|
+
- **Purpose:** Set of manage-subscription entry points (e.g. "Update delivery date", "Pause or
|
|
712
|
+
cancel"), each a plain link into the tenant's product-owned manage flow. Renders no controls of its
|
|
713
|
+
own.
|
|
714
|
+
- **Props:** `entries: SubscriptionManageEntry[]` (required); `eyebrow?: string`; `heading?: string`;
|
|
715
|
+
`subhead?: string`.
|
|
716
|
+
- **`data-widget`:** `"subscription-manage-entry"`.
|
|
717
|
+
- **Dependencies:** `type SubscriptionManageEntry` (`@/lib/subscription/model`).
|
|
718
|
+
- **Tokens consumed:** `--color-accent-ink`, `--color-text`, `--color-muted`, `--color-border`,
|
|
719
|
+
`--color-surface`, `--color-primary`, `--radius-lg`.
|
|
720
|
+
- **Delivery:** platform component (no raw-chrome twin).
|
|
721
|
+
- **Usage:**
|
|
722
|
+
```astro
|
|
723
|
+
<ManageSubscriptionEntry eyebrow="Manage" heading="Manage your Autopilot" entries={entries} />
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
---
|
|
727
|
+
|
|
728
|
+
## 7. Marketing / widget blocks — `components/marketing/`
|
|
729
|
+
|
|
730
|
+
The block library a `siteType: "marketing"` tenant composes pages from — token-driven, distinct from
|
|
731
|
+
the ported `mkt.css` composition layer. Every block takes a discriminated-union `block` view-model
|
|
732
|
+
from `lib/storyblok/content-model.ts` (the `MarketingBlock` union) plus, where it emits links,
|
|
733
|
+
`basePath: string`. `MarketingCta = { label: string; href: string; variant?: "primary" |
|
|
734
|
+
"secondary" | "ghost" }`.
|
|
735
|
+
|
|
736
|
+
### MarketingBlocks (`components/marketing/MarketingBlocks.astro`)
|
|
737
|
+
|
|
738
|
+
- **Purpose:** The marketing page renderer — dispatches a `MarketingBlock[]` discriminated union to
|
|
739
|
+
its block components by `block.component`. Tenant-agnostic; reused by homepage + future marketing
|
|
740
|
+
pages.
|
|
741
|
+
- **Props:** `blocks: MarketingBlock[]`; `basePath: string`.
|
|
742
|
+
- **Dispatch table** (`block.component` → component): `"marketing_hero"` → `MarketingHero`;
|
|
743
|
+
`"trust_bar"` → `TrustBar`; `"split_compare"` → `SplitCompare`; `"steps"` → `Steps`; `"card_grid"`
|
|
744
|
+
→ `CardGrid`; `"integrations"` → `Integrations`; `"proof_strip"` → `ProofStrip`; `"testimonials"`
|
|
745
|
+
→ `Testimonials`; `"faq"` → `Faq`; `"cta_band"` → `CtaBand`; default → `null`.
|
|
746
|
+
- **Dependencies:** all 10 block components; `MarketingBlock` type.
|
|
747
|
+
- **Tokens consumed:** none directly (delegates to children).
|
|
748
|
+
- **Delivery:** platform component.
|
|
749
|
+
- **Usage:**
|
|
750
|
+
```astro
|
|
751
|
+
<MarketingBlocks blocks={blocks} basePath={basePath} />
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
### MarketingHero (`components/marketing/MarketingHero.astro`)
|
|
755
|
+
|
|
756
|
+
- **Purpose:** Signature top-of-page hero for marketing tenants — eyebrow + oversized headline +
|
|
757
|
+
subhead/body + CTAs + trust chips; optional right-hand "product moment" panel of mono log lines.
|
|
758
|
+
- **Props:** `block: MarketingHeroBlock` (`{ component: "marketing_hero"; eyebrow?: string; headline:
|
|
759
|
+
string; subhead?: string; body?: string; trust?: string[]; ctas?: MarketingCta[]; panel?: { title?:
|
|
760
|
+
string; lines: string[] } }`); `basePath: string`.
|
|
761
|
+
- **Dependencies:** `MarketingCtas`.
|
|
762
|
+
- **Tokens consumed:** `--color-bg`, `--color-text`, `--color-muted`, `--color-accent`,
|
|
763
|
+
`--color-border`, `--color-surface`, `--radius-lg`, `--shadow-lg`; helper classes `container-prose`,
|
|
764
|
+
`display-xl`, `eyebrow eyebrow--tick`.
|
|
765
|
+
- **Delivery:** platform component.
|
|
766
|
+
- **Usage:**
|
|
767
|
+
```astro
|
|
768
|
+
<MarketingHero block={block} basePath={basePath} />
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
### MarketingCtas (`components/marketing/MarketingCtas.astro`)
|
|
772
|
+
|
|
773
|
+
- **Purpose:** Row of CTA buttons for marketing blocks. External `http[s]` hrefs open a new tab with
|
|
774
|
+
`rel="noopener noreferrer"`; root-absolute same-origin hrefs get the base-path prefix. Three variant
|
|
775
|
+
styles.
|
|
776
|
+
- **Props:** `ctas?: MarketingCta[]` (default `[]`); `basePath: string`; `class?: string`.
|
|
777
|
+
- **Dependencies:** `withBase`.
|
|
778
|
+
- **Tokens consumed:** primary → `bg-accent` + `--color-primary-contrast` + `--shadow-sm`; secondary
|
|
779
|
+
→ `--color-border` + `--color-surface` + `--color-text` + hover `--color-primary`; ghost →
|
|
780
|
+
`--color-primary`; `rounded-full`.
|
|
781
|
+
- **Delivery:** platform component (used internally by `MarketingHero` / `CtaBand`).
|
|
782
|
+
- **Usage:**
|
|
783
|
+
```astro
|
|
784
|
+
<MarketingCtas ctas={block.ctas} basePath={basePath} class="mt-8" />
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
### TrustBar (`components/marketing/TrustBar.astro`)
|
|
788
|
+
|
|
789
|
+
- **Purpose:** Quiet horizontal strip of proof tokens (countries, standards, ratings). Plain text,
|
|
790
|
+
mono, dot-separated.
|
|
791
|
+
- **Props:** `block: TrustBarBlock` (`{ component: "trust_bar"; items: string[] }`).
|
|
792
|
+
- **Dependencies:** `TrustBarBlock` type.
|
|
793
|
+
- **Tokens consumed:** `--color-border`, `--color-surface`, `--color-muted`.
|
|
794
|
+
- **Delivery:** platform component.
|
|
795
|
+
- **Usage:**
|
|
796
|
+
```astro
|
|
797
|
+
<TrustBar block={{ component: "trust_bar", items: ["200+ countries", "SOC 2 Type II"] }} />
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
### SplitCompare (`components/marketing/SplitCompare.astro`)
|
|
801
|
+
|
|
802
|
+
- **Purpose:** Before/after two-column comparison — `before` reads as the problem (muted, struck
|
|
803
|
+
items), `after` as the solution (accent border, ✓).
|
|
804
|
+
- **Props:** `block: SplitCompareBlock` (`{ component: "split_compare"; eyebrow?: string; title:
|
|
805
|
+
string; subhead?: string; before: { title: string; items: string[] }; after: { title: string;
|
|
806
|
+
items: string[] } }`).
|
|
807
|
+
- **Dependencies:** `Section`.
|
|
808
|
+
- **Tokens consumed:** `--color-muted`, `--color-border`, `--color-surface`, `--color-primary`,
|
|
809
|
+
`--color-text`, `--color-accent`, `--radius-lg`, `--shadow-md`.
|
|
810
|
+
- **Delivery:** platform component.
|
|
811
|
+
- **Usage:**
|
|
812
|
+
```astro
|
|
813
|
+
<SplitCompare block={block} />
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
### Steps (`components/marketing/Steps.astro`)
|
|
817
|
+
|
|
818
|
+
- **Purpose:** Numbered/emoji journey (e.g. a checkout verification flow) — a grid of step cards, each
|
|
819
|
+
with a circular number/icon badge + label + optional detail.
|
|
820
|
+
- **Props:** `block: StepsBlock` (`{ component: "steps"; eyebrow?: string; title: string; subhead?:
|
|
821
|
+
string; steps: { icon?: string; label: string; detail?: string }[] }`).
|
|
822
|
+
- **Dependencies:** `Section`.
|
|
823
|
+
- **Tokens consumed:** `--color-muted`, `--color-border`, `--color-bg`, `--color-primary`,
|
|
824
|
+
`--color-primary-contrast`, `--color-text`, `--radius-lg`.
|
|
825
|
+
- **Delivery:** platform component.
|
|
826
|
+
- **Usage:**
|
|
827
|
+
```astro
|
|
828
|
+
<Steps block={block} />
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
### CardGrid (`components/marketing/CardGrid.astro`)
|
|
832
|
+
|
|
833
|
+
- **Purpose:** Responsive grid of capability/industry/feature cards (one component, three variants
|
|
834
|
+
tuning density/columns). Cards optionally link internally (base-path) or externally.
|
|
835
|
+
- **Props:** `block: CardGridBlock` (`{ component: "card_grid"; anchor?: string; eyebrow?: string;
|
|
836
|
+
title: string; subhead?: string; variant?: "capability" | "industry" | "feature"; cards: { icon?:
|
|
837
|
+
string; title: string; body?: string; meta?: string; href?: string }[]; footnote?: string }`);
|
|
838
|
+
`basePath: string`.
|
|
839
|
+
- **Dependencies:** `Section`; `withBase`.
|
|
840
|
+
- **Tokens consumed:** `--color-muted`, `--color-border`, `--color-surface`, `--color-text`,
|
|
841
|
+
`--radius-lg`, `--shadow-md` (hover).
|
|
842
|
+
- **Delivery:** platform component.
|
|
843
|
+
- **Usage:**
|
|
844
|
+
```astro
|
|
845
|
+
<CardGrid block={block} basePath={basePath} />
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
### Integrations (`components/marketing/Integrations.astro`)
|
|
849
|
+
|
|
850
|
+
- **Purpose:** "Fits the store you already run" section — a strip of platform name/link chips,
|
|
851
|
+
optional implementation points (✓ list), and an optional safe code sample.
|
|
852
|
+
- **Props:** `block: IntegrationsBlock` (`{ component: "integrations"; eyebrow?: string; title:
|
|
853
|
+
string; subhead?: string; platforms: { label: string; href?: string }[]; points?: string[]; code?:
|
|
854
|
+
CodeSample }`); `basePath: string`.
|
|
855
|
+
- **Dependencies:** `Section`; `CodeSample`; `withBase`.
|
|
856
|
+
- **Tokens consumed:** `--color-muted`, `--color-border`, `--color-bg`, `--color-text`,
|
|
857
|
+
`--color-primary` (hover), `--color-accent` (✓).
|
|
858
|
+
- **Delivery:** platform component.
|
|
859
|
+
- **Usage:**
|
|
860
|
+
```astro
|
|
861
|
+
<Integrations block={block} basePath={basePath} />
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
### CodeSample (`components/marketing/CodeSample.astro`)
|
|
865
|
+
|
|
866
|
+
- **Purpose:** Display-only fenced code block; code rendered as **escaped** text inside `<pre><code>`
|
|
867
|
+
(never `set:html`). Never embed secrets. Caption + language label.
|
|
868
|
+
- **Props:** `sample: CodeSample` where `export interface CodeSample { language?: string; code: string;
|
|
869
|
+
caption?: string }`.
|
|
870
|
+
- **Dependencies:** `CodeSample` type.
|
|
871
|
+
- **Tokens consumed:** `--radius-lg`, `--color-border`, `--color-text` (dark code surface); code text
|
|
872
|
+
colors are deliberately not tokenized.
|
|
873
|
+
- **Delivery:** platform component (usually nested inside `Integrations`).
|
|
874
|
+
- **Usage:**
|
|
875
|
+
```astro
|
|
876
|
+
<CodeSample sample={{ language: "js", caption: "Client-side embed", code: "await verify({ minAge: 21 });" }} />
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
### ProofStrip (`components/marketing/ProofStrip.astro`)
|
|
880
|
+
|
|
881
|
+
- **Purpose:** Privacy/trust proof band painted in the primary color (a single strong moment) — bold
|
|
882
|
+
statement + supporting copy + grid of proof bullets.
|
|
883
|
+
- **Props:** `block: ProofStripBlock` (`{ component: "proof_strip"; eyebrow?: string; title: string;
|
|
884
|
+
body?: string; items?: string[] }`).
|
|
885
|
+
- **Dependencies:** `ProofStripBlock` type.
|
|
886
|
+
- **Tokens consumed:** `--color-primary`, `--color-primary-contrast`, `--radius-md`; chips use
|
|
887
|
+
`color-mix(... --color-primary-contrast ...)`; helper classes `container-prose`, `display-lg`.
|
|
888
|
+
- **Delivery:** platform component.
|
|
889
|
+
- **Usage:**
|
|
890
|
+
```astro
|
|
891
|
+
<ProofStrip block={block} />
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
### Testimonials (`components/marketing/Testimonials.astro`)
|
|
895
|
+
|
|
896
|
+
- **Purpose:** Outcome/quote cards with optional stat lead-in + attribution, plus an optional row of
|
|
897
|
+
rating tokens.
|
|
898
|
+
- **Props:** `block: TestimonialsBlock` (`{ component: "testimonials"; eyebrow?: string; title?:
|
|
899
|
+
string; quotes: { stat?: string; quote: string; attribution?: string }[]; ratings?: string[] }`).
|
|
900
|
+
- **Dependencies:** `Section`.
|
|
901
|
+
- **Tokens consumed:** `--color-border`, `--color-surface`, `--color-accent` (stat), `--color-text`,
|
|
902
|
+
`--color-muted`, `--radius-lg`.
|
|
903
|
+
- **Delivery:** platform component.
|
|
904
|
+
- **Usage:**
|
|
905
|
+
```astro
|
|
906
|
+
<Testimonials block={block} />
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
### Faq (`components/marketing/Faq.astro`)
|
|
910
|
+
|
|
911
|
+
- **Purpose:** No-JS accordion of Q/A pairs using native `<details>`/`<summary>`; emits FAQPage
|
|
912
|
+
JSON-LD for rich results. Answers plain text (auto-escaped).
|
|
913
|
+
- **Props:** `block: FaqBlock` (`{ component: "faq"; eyebrow?: string; title: string; lede?: string;
|
|
914
|
+
items: { q: string; a: string }[] }`).
|
|
915
|
+
- **Dependencies:** `Section`.
|
|
916
|
+
- **Tokens consumed:** `divide-[var(--color-border)]`, `--color-text`, `--color-accent` (the `+`
|
|
917
|
+
toggle), `--color-muted`.
|
|
918
|
+
- **Delivery:** platform component.
|
|
919
|
+
- **Usage:**
|
|
920
|
+
```astro
|
|
921
|
+
<Faq block={block} />
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
### CtaBand (`components/marketing/CtaBand.astro`)
|
|
925
|
+
|
|
926
|
+
- **Purpose:** Final conversion band on the surface tone — headline + optional subhead + centered CTA
|
|
927
|
+
row.
|
|
928
|
+
- **Props:** `block: CtaBandBlock` (`{ component: "cta_band"; title: string; subhead?: string; ctas?:
|
|
929
|
+
MarketingCta[] }`); `basePath: string`.
|
|
930
|
+
- **Dependencies:** `MarketingCtas`.
|
|
931
|
+
- **Tokens consumed:** `--color-surface`, `--color-muted`; helper classes `container-prose`,
|
|
932
|
+
`display-lg`; CTA tokens via `MarketingCtas`.
|
|
933
|
+
- **Delivery:** platform component.
|
|
934
|
+
- **Usage:**
|
|
935
|
+
```astro
|
|
936
|
+
<CtaBand block={block} basePath={basePath} />
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
---
|
|
940
|
+
|
|
941
|
+
## 8. PLP controls — `components/plp/`
|
|
942
|
+
|
|
943
|
+
URL-addressable faceted-search chrome for a collection page — every control is a real link or GET
|
|
944
|
+
form carrying the query, so filtering needs no JS and is back-button safe. All take `query:
|
|
945
|
+
ListProductsQuery` + `basePath: string` (from `@tot/public-runtime`) and share `@/lib/url`
|
|
946
|
+
(`toggleFacet`, `buildQueryString`). None have a raw-chrome twin (PLP-body, not site chrome).
|
|
947
|
+
|
|
948
|
+
### FacetSidebar (`components/plp/FacetSidebar.astro`)
|
|
949
|
+
|
|
950
|
+
- **Purpose:** Faceted filtering UI — each facet value is a real link toggling a querystring param;
|
|
951
|
+
price range is a GET form preserving other params via hidden inputs. Facets ordered: options, then
|
|
952
|
+
tags, availability last.
|
|
953
|
+
- **Props:** `facets: Facet[]` (from `@tot/public-runtime`); `query: ListProductsQuery`; `basePath:
|
|
954
|
+
string`.
|
|
955
|
+
- **Dependencies:** `type Facet, ListProductsQuery` from `@tot/public-runtime`; `toggleFacet,
|
|
956
|
+
buildQueryString` (`@/lib/url`).
|
|
957
|
+
- **Tokens consumed:** `--color-text`, `--color-muted`, `--color-accent-ink` (hover), `--radius-sm`,
|
|
958
|
+
`--color-primary`, `--color-primary-contrast`, `--color-border`, `--color-bg`; `.eyebrow` class.
|
|
959
|
+
- **Delivery:** platform component.
|
|
960
|
+
- **Usage:**
|
|
961
|
+
```astro
|
|
962
|
+
<FacetSidebar facets={facets} query={query} basePath="/collections/bestsellers" />
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
### ActiveFilters (`components/plp/ActiveFilters.astro`)
|
|
966
|
+
|
|
967
|
+
- **Purpose:** Removable filter chips above the grid; each chip is a link that drops exactly one
|
|
968
|
+
filter, plus a "Clear all" link.
|
|
969
|
+
- **Props:** `query: ListProductsQuery`; `basePath: string`.
|
|
970
|
+
- **Dependencies:** `type ListProductsQuery` from `@tot/public-runtime`; `toggleFacet,
|
|
971
|
+
buildQueryString` (`@/lib/url`).
|
|
972
|
+
- **Tokens consumed:** `--color-border`, `--color-surface`, `--color-accent-ink` (hover),
|
|
973
|
+
`--color-muted`, `--color-primary-contrast` (group-hover fill).
|
|
974
|
+
- **Delivery:** platform component.
|
|
975
|
+
- **Usage:**
|
|
976
|
+
```astro
|
|
977
|
+
<ActiveFilters query={query} basePath="/collections/bestsellers" />
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
### SortSelect (`components/plp/SortSelect.astro`)
|
|
981
|
+
|
|
982
|
+
- **Purpose:** Sort control — a plain GET `<form>` `<select>` that auto-submits on change (nonce'd
|
|
983
|
+
`is:inline` script; `<noscript>` Apply fallback). Other query params ride along as hidden inputs.
|
|
984
|
+
Fixed options: `featured`, `newest`, `price-asc`, `price-desc`, `title-asc`.
|
|
985
|
+
- **Props:** `query: ListProductsQuery`; `basePath: string`.
|
|
986
|
+
- **Dependencies:** `type ListProductsQuery` from `@tot/public-runtime`. Reads `Astro.locals.cspNonce`.
|
|
987
|
+
- **Tokens consumed:** `--color-muted`, `--radius-sm`, `--color-border`, `--color-surface`;
|
|
988
|
+
`font-display`.
|
|
989
|
+
- **Delivery:** platform component.
|
|
990
|
+
- **Usage:**
|
|
991
|
+
```astro
|
|
992
|
+
<SortSelect query={query} basePath="/collections/bestsellers" />
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
### Pagination (`components/plp/Pagination.astro`)
|
|
996
|
+
|
|
997
|
+
- **Purpose:** SEO-friendly numbered pagination (compact 5-page window with first/last + ellipses) +
|
|
998
|
+
a "Load more" next-page link. All plain anchors carrying the full query — crawlable, back-button
|
|
999
|
+
safe, no-JS correct.
|
|
1000
|
+
- **Props:** `query: ListProductsQuery`; `basePath: string`; `page: number`; `pageSize: number`;
|
|
1001
|
+
`total: number`.
|
|
1002
|
+
- **Dependencies:** `type ListProductsQuery` from `@tot/public-runtime`; `buildQueryString`
|
|
1003
|
+
(`@/lib/url`).
|
|
1004
|
+
- **Tokens consumed:** `--color-border`, `--color-surface`, `--color-accent` (hover),
|
|
1005
|
+
`--color-accent-ink`, `--radius-sm`, `--color-muted`, `--color-primary`, `--color-primary-contrast`.
|
|
1006
|
+
- **Delivery:** platform component.
|
|
1007
|
+
- **Usage:**
|
|
1008
|
+
```astro
|
|
1009
|
+
<Pagination query={query} basePath="/collections/bestsellers" page={2} pageSize={12} total={58} />
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
---
|
|
1013
|
+
|
|
1014
|
+
## 9. Islands — `components/islands/`
|
|
1015
|
+
|
|
1016
|
+
Client-hydrated Preact `.tsx`. Every island is tenant-agnostic and reads only fixtures / `localStorage`
|
|
1017
|
+
/ its own props — **no live catalog, cart, or ToT calls** (`SearchTypeahead` fetches the tenant's own
|
|
1018
|
+
`/search` endpoint). Cross-island wiring uses `window` CustomEvents; `localStorage` keys in play:
|
|
1019
|
+
`"tot:age-affirmed"`, `"tot:recently-viewed"`, `"tot:wishlist"`.
|
|
1020
|
+
|
|
1021
|
+
### AgeGate (`components/islands/AgeGate.tsx`)
|
|
1022
|
+
|
|
1023
|
+
- **Purpose:** Soft `minAge`+ affirmation overlay, shown once per device for regulated tenants.
|
|
1024
|
+
Front-door affirmation only — real ToT identity/age verification is a later phase (the "Yes" path is
|
|
1025
|
+
where that handoff will live).
|
|
1026
|
+
- **Props:** `{ minAge: number; brand: string }`. Directive: `client:load`.
|
|
1027
|
+
- **State:** reads/writes `localStorage` `"tot:age-affirmed"` (`"yes"`); local state machine; locks
|
|
1028
|
+
page scroll while asking. No network.
|
|
1029
|
+
- **Dependencies:** `preact/hooks`.
|
|
1030
|
+
- **Tokens consumed:** `--color-text` (backdrop mix), `--radius-lg`, `--color-surface`, `--shadow-lg`,
|
|
1031
|
+
`--radius-md`, `--color-primary`, `--color-primary-contrast`, `--color-border`, `--color-muted`;
|
|
1032
|
+
classes `eyebrow eyebrow--tick`, `display-md`, `link-accent`.
|
|
1033
|
+
- **Delivery:** platform island + raw-chrome twin `.age-gate` / `.age-gate__panel` / `__mark` /
|
|
1034
|
+
`__actions` / `__deny` / `__fine` / `.is-open` / `.is-denied` (same `localStorage` key + closed
|
|
1035
|
+
shadow-DOM tamper pattern, produced server-side by `lib/rawChrome.ts`).
|
|
1036
|
+
- **Usage:**
|
|
1037
|
+
```astro
|
|
1038
|
+
<AgeGate client:load minAge={21} brand={tenant.brandName} />
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
### IsolatedAgeGate (`components/islands/IsolatedAgeGate.tsx`)
|
|
1042
|
+
|
|
1043
|
+
- **Purpose:** Tamper-resistant wrapper that mounts `<AgeGate>` inside a **closed shadow root** so no
|
|
1044
|
+
tenant CSS/JS can select, restyle, or hide it. A light-DOM host `<div data-tot-compliance>` is
|
|
1045
|
+
re-asserted by a `MutationObserver` (restored if detached / `display:none` / `hidden`). "Customize
|
|
1046
|
+
anything except what keeps you legal," as a technical guarantee — this is the production age-gate
|
|
1047
|
+
mount for component pages.
|
|
1048
|
+
- **Props:** `{ minAge: number; brand: string }` (passed through). Returns `null`; mounts via
|
|
1049
|
+
`useEffect`. Directive: `client:load`.
|
|
1050
|
+
- **State:** no own state; clones `link[rel=stylesheet]` into the shadow root (`:root` tokens inherit
|
|
1051
|
+
across the boundary); inherits AgeGate's localStorage behavior. No network.
|
|
1052
|
+
- **Dependencies:** `preact` (`render`), `preact/hooks`; `./AgeGate`.
|
|
1053
|
+
- **Tokens consumed:** none of its own (delegates to AgeGate; relies on `:root` token inheritance).
|
|
1054
|
+
- **Delivery:** platform island (server/raw-chrome equivalent is the closed-shadow gate in
|
|
1055
|
+
`CHROME_BEHAVIOR_JS`).
|
|
1056
|
+
- **Usage:**
|
|
1057
|
+
```astro
|
|
1058
|
+
<IsolatedAgeGate client:load minAge={21} brand={tenant.brandName} />
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
### VariantSelector (`components/islands/VariantSelector.tsx`)
|
|
1062
|
+
|
|
1063
|
+
- **Purpose:** PDP variant picker (the centerpiece interaction) — one radiogroup per product option;
|
|
1064
|
+
resolves the matching variant, disables unbuyable combinations, updates a polite live region
|
|
1065
|
+
(price/compare-at/SKU/stock), and drives cross-island events. Add-to-cart is a real checkout form
|
|
1066
|
+
when a signed payload is wired, else a disabled placeholder.
|
|
1067
|
+
- **Props:** `{ variants: CatalogVariant[]; options: OptionDef[]; currency: string; locale?: string;
|
|
1068
|
+
checkoutCart?: { action: string; purchaseOptions: ProductPurchaseOption[]; byVariantId: Record<string, Record<string, VariantCartPayload>> } }` where local
|
|
1069
|
+
`OptionDef = { name: string; position: number; values: string[] }`; `CatalogVariant` /
|
|
1070
|
+
`VariantCartPayload` from `@tot/public-runtime`. Directive: `client:load`.
|
|
1071
|
+
- **State:** props only. Default selection = first available variant. Dispatches `window` CustomEvents
|
|
1072
|
+
`variant:image { imageId }` (consumed by `ImageGallery`) and `variant:change { variantId, available
|
|
1073
|
+
}`. No storage; no catalog/ToT fetch.
|
|
1074
|
+
- **Dependencies:** `preact/hooks`; `formatPrice` (`@/lib/format`); types from `@tot/public-runtime`.
|
|
1075
|
+
- **Tokens consumed:** `text-muted`, `text-sale`, `text-accent`, `text-primary`, `text-text`,
|
|
1076
|
+
`border-border`, `bg-surface`, `border-primary`/`bg-primary`/`text-primary-contrast`,
|
|
1077
|
+
`outline-primary`, `bg-border/40`.
|
|
1078
|
+
- **Delivery:** platform island (PDP).
|
|
1079
|
+
- **Usage:**
|
|
1080
|
+
```astro
|
|
1081
|
+
<VariantSelector client:load variants={product.variants} options={product.options} currency={product.price_range.currency} locale="en-US" />
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
### ImageGallery (`components/islands/ImageGallery.tsx`)
|
|
1085
|
+
|
|
1086
|
+
- **Purpose:** PDP image gallery — main image + thumbnail strip; thumbnails swap the main image, main
|
|
1087
|
+
image has click-to-toggle zoom (reduced-motion safe). First image is the LCP candidate until first
|
|
1088
|
+
interaction.
|
|
1089
|
+
- **Props:** `{ images: CatalogImage[]; title: string; productHandle: string; activeImageId?: string }`
|
|
1090
|
+
(`CatalogImage` from `@tot/public-runtime`). Realistic directive: `client:load`.
|
|
1091
|
+
- **State:** props only. Listens on `window` for `variant:image { imageId }` (dispatched by
|
|
1092
|
+
`VariantSelector`) and switches to the matching image. Dead fixture URLs fall back to a deterministic
|
|
1093
|
+
placeholder. No network.
|
|
1094
|
+
- **Dependencies:** `preact/hooks`; `placeholderDataUri, isUsableImageUrl` (`@/lib/placeholder`);
|
|
1095
|
+
type from `@tot/public-runtime`.
|
|
1096
|
+
- **Tokens consumed:** `--color-border` (placeholder bg); `outline-primary` (focus), `border-primary`
|
|
1097
|
+
/ `border-border` (thumbnails).
|
|
1098
|
+
- **Delivery:** platform island (PDP).
|
|
1099
|
+
- **Usage:**
|
|
1100
|
+
```astro
|
|
1101
|
+
<ImageGallery client:load images={product.images} title={product.title} productHandle={product.handle} />
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
### QuickView (`components/islands/QuickView.tsx`)
|
|
1105
|
+
|
|
1106
|
+
- **Purpose:** One global native `<dialog>` any product card opens via a `[data-quickview]` button
|
|
1107
|
+
(event-delegated, so cards stay zero-JS). Shows a peek — image, price, colors, short description —
|
|
1108
|
+
and routes to the full PDP. No cart action (Phase 1 browse-only).
|
|
1109
|
+
- **Props:** `{ locale: string }`. Directive: `client:idle`. The per-card payload is parsed from the
|
|
1110
|
+
button's `data-quickview` JSON attribute.
|
|
1111
|
+
- **State:** props + DOM-delegated clicks. Reads product data from the clicked button's attribute, not
|
|
1112
|
+
storage/network.
|
|
1113
|
+
- **Dependencies:** `preact/hooks`; `formatPrice` (`@/lib/format`), `@/lib/placeholder`, `withClientBase`
|
|
1114
|
+
(`@/lib/basePath`).
|
|
1115
|
+
- **Tokens consumed:** `--radius-lg`, `--color-surface`, `--color-text` (+ backdrop mix),
|
|
1116
|
+
`--color-border`, `--color-muted`, `--color-sale`, `--color-bg`, `--radius-md`, `--color-primary`,
|
|
1117
|
+
`--color-primary-contrast`.
|
|
1118
|
+
- **Delivery:** platform island (global, mounted once per page).
|
|
1119
|
+
- **Usage:**
|
|
1120
|
+
```astro
|
|
1121
|
+
<QuickView client:idle locale="en-US" />
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
### SearchTypeahead (`components/islands/SearchTypeahead.tsx`)
|
|
1125
|
+
|
|
1126
|
+
- **Purpose:** Compact header search with debounced typeahead dropdown. Fetches `GET
|
|
1127
|
+
{action}?q=<q>&format=json` and renders up to 6 results as an ARIA combobox/listbox; submitting
|
|
1128
|
+
navigates to the full results page.
|
|
1129
|
+
- **Props:** `{ action?: string }` (default `"/search"`). Directive: `client:idle`. `TypeaheadDoc`
|
|
1130
|
+
from `@/lib/search`.
|
|
1131
|
+
- **State:** local state only; debounced `fetch` (150 ms, min 2 chars, `AbortController`) against the
|
|
1132
|
+
tenant's own `/search` endpoint (base-path prefixed) — a same-app fetch, not a catalog/cart/ToT API.
|
|
1133
|
+
Failures degrade to "no dropdown". No storage.
|
|
1134
|
+
- **Dependencies:** `preact/hooks`; `type TypeaheadDoc` (`@/lib/search`), `@/lib/format`,
|
|
1135
|
+
`@/lib/placeholder`, `withClientBase` (`@/lib/basePath`).
|
|
1136
|
+
- **Tokens consumed:** `border-border`, `bg-surface`, `border-primary`/`focus-within:border-primary`,
|
|
1137
|
+
`text-muted`, `text-text`, `placeholder:text-muted`, `shadow-lg`, `bg-border/50`; thumb uses
|
|
1138
|
+
`--color-border`.
|
|
1139
|
+
- **Delivery:** platform island (header).
|
|
1140
|
+
- **Usage:**
|
|
1141
|
+
```astro
|
|
1142
|
+
<SearchTypeahead client:idle />
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
### MobileNav (`components/islands/MobileNav.tsx`)
|
|
1146
|
+
|
|
1147
|
+
- **Purpose:** Hamburger button + slide-in drawer for small screens (`md:hidden`). WCAG 2.2 AA:
|
|
1148
|
+
`aria-expanded`/`aria-controls`, focus trap, Escape + backdrop close, body-scroll lock, focus
|
|
1149
|
+
restore, reduced-motion guard.
|
|
1150
|
+
- **Props:** `{ nav: NavItem[]; announcement?: string }` where `NavItem = { label: string; href: string;
|
|
1151
|
+
children?: { label: string; href: string }[] }`. Directive: `client:idle`.
|
|
1152
|
+
- **State:** props only; local `open` state; toggles body scroll. No storage/network.
|
|
1153
|
+
- **Dependencies:** `preact/hooks`.
|
|
1154
|
+
- **Tokens consumed:** `text-text`, `bg-surface`, `outline-primary`, `bg-text/40` (backdrop),
|
|
1155
|
+
`border-border`, `text-muted`, `bg-primary`, `text-primary-contrast`, `text-primary`, `bg-border/40`,
|
|
1156
|
+
`shadow-xl`.
|
|
1157
|
+
- **Delivery:** platform island (mobile chrome; raw-chrome analog is the hamburger/`#primaryNav` toggle
|
|
1158
|
+
in `CHROME_BEHAVIOR_JS` + the off-canvas `.primary-nav`/`.hamburger` rules).
|
|
1159
|
+
- **Usage:**
|
|
1160
|
+
```astro
|
|
1161
|
+
<MobileNav client:idle nav={nav} announcement="Free US shipping on orders over $100" />
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
### RecentlyViewed (`components/islands/RecentlyViewed.tsx`)
|
|
1165
|
+
|
|
1166
|
+
- **Purpose:** "Recently viewed" discovery rail. On a PDP it records the current product to
|
|
1167
|
+
localStorage and renders the rest as a quiet strip. Renders nothing until it has something to show
|
|
1168
|
+
(no layout shift / empty-state noise).
|
|
1169
|
+
- **Props:** `{ current?: RVItem; locale: string; title?: string; max?: number }` (`title` default
|
|
1170
|
+
`"Recently viewed"`, `max` default `5`) where `export interface RVItem { handle: string; title:
|
|
1171
|
+
string; price: number; currency: string; vendor?: string; img?: string; rehosted?: boolean; seed:
|
|
1172
|
+
string }`. Directive: `client:idle`.
|
|
1173
|
+
- **State:** reads/writes `localStorage` `"tot:recently-viewed"` (JSON array, capped at 12); excludes
|
|
1174
|
+
`current`. No network.
|
|
1175
|
+
- **Dependencies:** `preact/hooks`; `formatPrice` (`@/lib/format`), `@/lib/placeholder`,
|
|
1176
|
+
`withClientBase` (`@/lib/basePath`).
|
|
1177
|
+
- **Tokens consumed:** `--color-border`, `--color-muted`, `--color-primary`; class `eyebrow
|
|
1178
|
+
eyebrow--tick`, `container-prose`.
|
|
1179
|
+
- **Delivery:** platform island.
|
|
1180
|
+
- **Usage:**
|
|
1181
|
+
```astro
|
|
1182
|
+
<RecentlyViewed client:idle locale="en-US" />
|
|
1183
|
+
<!-- On a PDP, pass current so it's recorded + excluded: -->
|
|
1184
|
+
<RecentlyViewed client:idle current={rvItem} locale="en-US" />
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
### SavedList (`components/islands/SavedList.tsx`)
|
|
1188
|
+
|
|
1189
|
+
- **Purpose:** Renders the wishlist from localStorage on the `/saved` page. Client-only; each item
|
|
1190
|
+
links to its PDP and can be removed inline; empty state invites browsing. Phase 2 swaps the store for
|
|
1191
|
+
an account-backed list without touching the component.
|
|
1192
|
+
- **Props:** `{ locale: string }`. Directive: `client:only="preact"` (no SSR — the list only exists
|
|
1193
|
+
client-side). `SavedItem` from `@/lib/wishlist`.
|
|
1194
|
+
- **State:** reads `localStorage` `"tot:wishlist"` via `readWishlist()`; listens for `WISHLIST_EVENT`
|
|
1195
|
+
(`"tot:wishlist-changed"`) to refresh; removes via `toggleSaved()`. Renders `null` pre-hydration.
|
|
1196
|
+
- **Dependencies:** `preact/hooks`; `readWishlist, toggleSaved, WISHLIST_EVENT, type SavedItem`
|
|
1197
|
+
(`@/lib/wishlist`), `@/lib/format`, `@/lib/placeholder`, `@/lib/basePath`.
|
|
1198
|
+
- **Tokens consumed:** `--radius-lg`, `--color-border`, `--color-surface`, `--color-muted`,
|
|
1199
|
+
`--radius-md`, `--color-primary`, `--color-primary-contrast`, `--color-sale` (heart).
|
|
1200
|
+
- **Delivery:** platform island (`/saved` page).
|
|
1201
|
+
- **Usage:**
|
|
1202
|
+
```astro
|
|
1203
|
+
<SavedList client:only="preact" locale="en-US" />
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
---
|
|
1207
|
+
|
|
1208
|
+
## 10. Foundational atoms + home — `components/*`, `components/home/`
|
|
1209
|
+
|
|
1210
|
+
Shared primitives composed by the higher-level widgets, plus the homepage hero.
|
|
1211
|
+
|
|
1212
|
+
### Badge (`components/Badge.astro`)
|
|
1213
|
+
|
|
1214
|
+
- **Purpose:** Tiny mono-uppercase pill used over product imagery and inline (sale %, New, low-stock,
|
|
1215
|
+
sold-out, compliance-verified).
|
|
1216
|
+
- **Props:** `variant: "sale" | "new" | "low-stock" | "soldout" | "compliant"`; `label?: string` (or
|
|
1217
|
+
default slot); `class?: string`.
|
|
1218
|
+
- **Dependencies:** none.
|
|
1219
|
+
- **Tokens consumed:** `bg-sale`, `bg-primary text-primary-contrast`, `bg-accent`, `bg-surface
|
|
1220
|
+
text-muted border-border`, `bg-[var(--color-success)]`; `font-mono`, `rounded-full`.
|
|
1221
|
+
- **Delivery:** platform component.
|
|
1222
|
+
- **Usage:**
|
|
1223
|
+
```astro
|
|
1224
|
+
<Badge variant="compliant" label="Compliant" />
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
### Breadcrumbs (`components/Breadcrumbs.astro`)
|
|
1228
|
+
|
|
1229
|
+
- **Purpose:** Semantic breadcrumb trail; last crumb is the current page (`aria-current`, not a link).
|
|
1230
|
+
- **Props:** `crumbs: { label: string; href: string }[]`.
|
|
1231
|
+
- **Dependencies:** `withBase`.
|
|
1232
|
+
- **Tokens consumed:** `text-muted`, `text-border`, `text-text`, `hover:text-primary`.
|
|
1233
|
+
- **Delivery:** platform component (`.crumbs` exists in the marketing `mkt.css` layer, not
|
|
1234
|
+
commerce-chrome.css).
|
|
1235
|
+
- **Usage:**
|
|
1236
|
+
```astro
|
|
1237
|
+
<Breadcrumbs crumbs={[{ label: "Home", href: "/" }, { label: "Bestsellers", href: "/collections/bestsellers" }]} />
|
|
1238
|
+
```
|
|
1239
|
+
|
|
1240
|
+
### ProductCard (`components/ProductCard.astro`)
|
|
1241
|
+
|
|
1242
|
+
- **Purpose:** PLP/grid card using the stretched-link pattern (title is the link; an `::after` overlay
|
|
1243
|
+
makes the whole card clickable so wishlist/quick-view controls sit above it). Image hover-zoom +
|
|
1244
|
+
optional second image; status badges; color swatches; price with struck compare-at; optional rating.
|
|
1245
|
+
- **Props:** `product: CatalogProduct`; `position?: number`; `list?: string`; `priority?: boolean`;
|
|
1246
|
+
`rating?: number`; `reviewCount?: number`.
|
|
1247
|
+
- **Dependencies:** `CatalogProduct` from `@tot/public-runtime`; `discountPercent, truncate`
|
|
1248
|
+
(`@/lib/format`); `isOnSale, isNew, isAvailable, fromPrice` (`@/lib/tot/query`); `colorOptionValues,
|
|
1249
|
+
resolveSwatch, isLightFill` (`@/lib/colors`); `Img`, `Badge`, `PriceDisplay`, `RatingStars`;
|
|
1250
|
+
`withBase`. Reads `Astro.locals.tenant` (`.locale`) + `Astro.locals.basePath`. The compliance badge
|
|
1251
|
+
is tag-driven (`compliant`/`lab-tested`/`age-verified`) — tenant-agnostic, no per-merchant branch.
|
|
1252
|
+
- **Tokens consumed:** `bg-border`, `ring-[var(--color-border)]`, `shadow-[var(--shadow-md)]`,
|
|
1253
|
+
`rounded-md`, `rounded-[var(--radius-sm)]`, `text-[var(--color-text)]`, `hover:text-[var(--color-sale)]`,
|
|
1254
|
+
`text-muted`, `group-hover:text-primary`, `font-display`, `font-mono`.
|
|
1255
|
+
- **Delivery:** platform component.
|
|
1256
|
+
- **Usage:**
|
|
1257
|
+
```astro
|
|
1258
|
+
<ProductCard product={p} rating={4.5} reviewCount={128} />
|
|
1259
|
+
```
|
|
1260
|
+
|
|
1261
|
+
### CollectionCard (`components/CollectionCard.astro`)
|
|
1262
|
+
|
|
1263
|
+
- **Purpose:** Editorial collection tile with a dark gradient scrim so the overlaid title (+ optional
|
|
1264
|
+
piece count) reads on any image. `data-reveal` scroll-in.
|
|
1265
|
+
- **Props:** `collection: CatalogCollection` (from `@tot/public-runtime`); `productCount?: number`.
|
|
1266
|
+
- **Dependencies:** `CatalogCollection` from `@tot/public-runtime`; `Img`; `withBase`.
|
|
1267
|
+
- **Tokens consumed:** `bg-border`, `rounded-md`, `font-display`, `font-mono` (overlay uses fixed
|
|
1268
|
+
`text-white` / `from-black/75` scrim).
|
|
1269
|
+
- **Delivery:** platform component.
|
|
1270
|
+
- **Usage:**
|
|
1271
|
+
```astro
|
|
1272
|
+
<CollectionCard collection={collection} productCount={24} />
|
|
1273
|
+
```
|
|
1274
|
+
|
|
1275
|
+
### Section (`components/Section.astro`)
|
|
1276
|
+
|
|
1277
|
+
- **Purpose:** Section wrapper with consistent vertical rhythm + optional eyebrow/title header +
|
|
1278
|
+
alternating band background.
|
|
1279
|
+
- **Props:** `title?: string`; `eyebrow?: string`; `tone?: "bg" | "surface"`; `class?: string`; `id?:
|
|
1280
|
+
string`. Slots: default + named `header-aside`.
|
|
1281
|
+
- **Dependencies:** none.
|
|
1282
|
+
- **Tokens consumed:** `bg-[var(--color-surface)]` / `bg-[var(--color-bg)]`; classes `container-prose`,
|
|
1283
|
+
`eyebrow eyebrow--tick`, `display-lg`.
|
|
1284
|
+
- **Delivery:** platform component.
|
|
1285
|
+
- **Usage:**
|
|
1286
|
+
```astro
|
|
1287
|
+
<Section eyebrow="Shop" title="Best Sellers" tone="surface"> … </Section>
|
|
1288
|
+
```
|
|
1289
|
+
|
|
1290
|
+
### EmptyState (`components/EmptyState.astro`)
|
|
1291
|
+
|
|
1292
|
+
- **Purpose:** Intentional empty result (search/PLP). Copy passed in by the parent; optional CTA.
|
|
1293
|
+
- **Props:** `title: string`; `body?: string`; `ctaLabel?: string`; `ctaHref?: string`.
|
|
1294
|
+
- **Dependencies:** `withBase`.
|
|
1295
|
+
- **Tokens consumed:** `text-muted`, `bg-primary`, `text-primary-contrast`, `font-display`,
|
|
1296
|
+
`rounded-full`; class `display-md`.
|
|
1297
|
+
- **Delivery:** platform component.
|
|
1298
|
+
- **Usage:**
|
|
1299
|
+
```astro
|
|
1300
|
+
<EmptyState title="No results" body="Try broadening your filters." ctaLabel="Browse all" ctaHref="/collections/all" />
|
|
1301
|
+
```
|
|
1302
|
+
|
|
1303
|
+
### Img (`components/Img.astro`)
|
|
1304
|
+
|
|
1305
|
+
- **Purpose:** CLS-free, theme-agnostic image with offline placeholder fallback — dead fixture URLs
|
|
1306
|
+
render a deterministic gradient SVG keyed by `seed`; real rehosted / Shopify `?width=` URLs get
|
|
1307
|
+
responsive srcset/sizes. Explicit width/height always set.
|
|
1308
|
+
- **Props:** `src?: string`; `alt: string`; `width: number`; `height: number`; `seed: string`; `label?:
|
|
1309
|
+
string`; `rehosted?: boolean`; `priority?: boolean` (LCP: eager+high); `sizes?: string`; `class?:
|
|
1310
|
+
string`; `loading?: "eager" | "lazy"`.
|
|
1311
|
+
- **Dependencies:** `resolveImage` (`@/lib/imageAttrs`).
|
|
1312
|
+
- **Tokens consumed:** `--color-border` (placeholder backdrop).
|
|
1313
|
+
- **Delivery:** platform component.
|
|
1314
|
+
- **Usage:**
|
|
1315
|
+
```astro
|
|
1316
|
+
<Img src={image?.url} rehosted={image?.rehosted} seed={image?.id ?? handle} alt={title} width={800} height={1000} sizes="(min-width:1024px) 25vw, 100vw" priority />
|
|
1317
|
+
```
|
|
1318
|
+
|
|
1319
|
+
### WidgetFrame (`components/WidgetFrame.astro`)
|
|
1320
|
+
|
|
1321
|
+
- **Purpose:** Renders a tenant-registered widget (`isolation: "sandbox"`) in a sandboxed iframe —
|
|
1322
|
+
`allow-scripts` without `allow-same-origin` puts it in an opaque origin (can't read page
|
|
1323
|
+
cookies/DOM/storage); bundle hash-pinned in CSP.
|
|
1324
|
+
- **Props:** `src: string` (same-origin bundle URL); `title: string` (required a11y); `strategy?:
|
|
1325
|
+
ScriptStrategy` (from `@tot/public-runtime`, default `"on-idle"`); `height?: string` (default
|
|
1326
|
+
`"auto"`).
|
|
1327
|
+
- **Dependencies:** `widgetFrameAttrs, type ScriptStrategy` from `@tot/public-runtime`.
|
|
1328
|
+
- **Tokens consumed:** none (`.tot-widget-frame` + inline sizing; no theme tokens).
|
|
1329
|
+
- **Delivery:** platform component.
|
|
1330
|
+
- **Usage:**
|
|
1331
|
+
```astro
|
|
1332
|
+
<WidgetFrame src="/widgets/tenant-abc/reviews.html" title="Customer reviews" height="480px" />
|
|
1333
|
+
```
|
|
1334
|
+
|
|
1335
|
+
### Seo (`components/Seo.astro`)
|
|
1336
|
+
|
|
1337
|
+
- **Purpose:** Per-page `<title>`/meta, Open Graph, Twitter card, canonical, JSON-LD. Tenant-aware —
|
|
1338
|
+
OG image + site name from the resolved tenant. Head-only (renders no styled markup).
|
|
1339
|
+
- **Props:** `title: string`; `description?: string`; `path: string` (path-only canonical, joined to
|
|
1340
|
+
the tenant canonical base); `ogType?: "website" | "product" | "article"`; `ogImage?: string`;
|
|
1341
|
+
`noindex?: boolean`; `jsonLd?: JsonLd | JsonLd[]` (`JsonLd = { [key: string]: unknown }`).
|
|
1342
|
+
- **Dependencies:** none. Reads `Astro.locals.tenant` (`.displayName`, `.theme_tokens?.brand?.ogImage`)
|
|
1343
|
+
+ `Astro.locals.canonicalBase`.
|
|
1344
|
+
- **Tokens consumed:** none.
|
|
1345
|
+
- **Delivery:** platform component.
|
|
1346
|
+
- **Usage:**
|
|
1347
|
+
```astro
|
|
1348
|
+
<Seo title="Merino Zip Hoodie" description="Soft, warm, everyday." path="/products/merino-zip-hoodie" ogType="product" jsonLd={productSchema} />
|
|
1349
|
+
```
|
|
1350
|
+
|
|
1351
|
+
### Hero (`components/home/Hero.astro`)
|
|
1352
|
+
|
|
1353
|
+
- **Purpose:** The signature homepage moment ("hero is a thesis"). Treatments: `"bold"` (default —
|
|
1354
|
+
full-bleed primary background, contrast text), `"light"` (page background, brand color as
|
|
1355
|
+
headline/CTA accent), `"banner"` (merchant artwork edge-to-edge); non-empty `slides` render a
|
|
1356
|
+
slideshow. Feature image is the merchant's own hero slide (`block.image`) when adopted, else the
|
|
1357
|
+
hero collection's first product. `block.notice` renders a `.hero__notice` status pill in every
|
|
1358
|
+
treatment.
|
|
1359
|
+
- **Props:** `block: HeroBlock` (from `@/lib/storyblok/content-model`: `{ component: "hero"; notice?:
|
|
1360
|
+
string; eyebrow?: string; headline: string; headlineAccent?: string; subhead?: string; ctaLabel?: string; ctaHref?:
|
|
1361
|
+
string; featureCollection?: string; image?: string; imageAlt?: string }`); `featureProduct?:
|
|
1362
|
+
CatalogProduct | null`.
|
|
1363
|
+
- **Dependencies:** `Img`; `withBase`; `HeroBlock` + `CatalogProduct` (`@tot/public-runtime`).
|
|
1364
|
+
- **Note:** the bold/light variant is read from
|
|
1365
|
+
`Astro.locals.tenant?.theme_tokens?.brand?.heroVariant` — **not a prop**. Also reads
|
|
1366
|
+
`Astro.locals.basePath`.
|
|
1367
|
+
- **Tokens consumed:** `--color-bg`, `--color-text`, `--color-primary`, `--color-primary-contrast`,
|
|
1368
|
+
`--color-muted`, `--color-accent`, `--radius-lg`, `--shadow-lg`, `--radius-md`, `--color-surface`,
|
|
1369
|
+
`--color-border`, `--color-hero-eyebrow`, `--heading-hero`; classes `display-hero`,
|
|
1370
|
+
`hero__eyebrow eyebrow eyebrow--tick`, `hero__notice`, `container-prose`.
|
|
1371
|
+
- **Delivery:** platform component (SSR, homepage).
|
|
1372
|
+
- **Usage:**
|
|
1373
|
+
```astro
|
|
1374
|
+
---
|
|
1375
|
+
// tenant sets Astro.locals.tenant.theme_tokens.brand.heroVariant = "bold" | "light" | "banner"
|
|
1376
|
+
---
|
|
1377
|
+
<Hero block={heroBlock} featureProduct={featureProduct} />
|
|
1378
|
+
```
|
|
1379
|
+
|
|
1380
|
+
---
|
|
1381
|
+
|
|
1382
|
+
## 11. Shared raw-chrome library — `lib/rawChrome.ts`, `lib/chrome/`, `public/shared/commerce-chrome.css`
|
|
1383
|
+
|
|
1384
|
+
The raw-chrome path exists so a tenant that ships **hand-authored raw marketing HTML** still gets the
|
|
1385
|
+
platform chrome look without copying CSS. It is the second delivery path referenced throughout §2–§4.
|
|
1386
|
+
|
|
1387
|
+
### `lib/rawChrome.ts` — the canonical raw-chrome generator
|
|
1388
|
+
|
|
1389
|
+
Replaces the three divergent hand-authored `content/<tenant>/chrome.html` documents with **one
|
|
1390
|
+
deterministic server-side generator** driven entirely by a tenant's `chrome.json` (typed
|
|
1391
|
+
`TenantChrome`) — **content only, no styling**.
|
|
1392
|
+
|
|
1393
|
+
- **Single-stylesheet model** (`raw-chrome-single-stylesheet`): the wrapper `<link>`s exactly one
|
|
1394
|
+
platform stylesheet, `SHARED_CHROME_STYLESHEET = "/shared/commerce-chrome.css"`, and emits **no
|
|
1395
|
+
inline chrome CSS**. Every color/space/type comes from theme tokens the tenant's own linked
|
|
1396
|
+
`*-theme.css` sets on `:root`. The markup mirrors, class-for-class, the DOM the F2 stylesheet styles.
|
|
1397
|
+
- **Splice-marker output** (`materialization-compat`): returns a full `<!DOCTYPE html>` document
|
|
1398
|
+
carrying `<!--PAGE_HEAD-->` / `<!--PAGE_BODY-->` markers (`CHROME_HEAD_MARKER` / `CHROME_BODY_MARKER`)
|
|
1399
|
+
so body-only page fragments get spliced in and the result is nonce-stamped + base-path-prefixed
|
|
1400
|
+
downstream. `rawMarketingHtml.prefixTenantLinks` exempts `/shared/` so the shared link survives
|
|
1401
|
+
path-prefix routing.
|
|
1402
|
+
- **Security:** all authored values are HTML-escaped (`esc`/`escSingle`) — defense-in-depth for
|
|
1403
|
+
first-party config.
|
|
1404
|
+
|
|
1405
|
+
Exports:
|
|
1406
|
+
|
|
1407
|
+
- `renderRawChrome(chrome: TenantChrome): string` — the entry point. Builds `<head>` (theme-color,
|
|
1408
|
+
favicon, tenant `themeStylesheets`, then the shared stylesheet), a skip link, then conditional
|
|
1409
|
+
sections in order: `warningBar` → `announcement` → `utilityNav` → header; the body marker; then
|
|
1410
|
+
`complianceStrip` → footer; then the age-gate JSON `<script>` + the inlined behavior script.
|
|
1411
|
+
- `CHROME_BEHAVIOR_JS: string` — the ONE shared, nonce-safe progressive-enhancement script (never
|
|
1412
|
+
copy-pasted per tenant). Three guarded blocks: (1) mobile nav drawer toggle (`#hamburger`/
|
|
1413
|
+
`#primaryNav`), (2) announcement-bar dismiss (remembered in `localStorage`, default key
|
|
1414
|
+
`tot_announce_dismissed`) + message rotation, (3) the 21+ age gate built inside a **closed shadow
|
|
1415
|
+
root** with a `data-tot-compliance` light-DOM host + `MutationObserver` re-assert — the same
|
|
1416
|
+
tamper-resistance pattern as `IsolatedAgeGate.tsx`, keyed on `tot:age-affirmed`.
|
|
1417
|
+
- `SHARED_CHROME_STYLESHEET: string` = `"/shared/commerce-chrome.css"`.
|
|
1418
|
+
|
|
1419
|
+
### `lib/chrome/model.ts` — typed chrome config
|
|
1420
|
+
|
|
1421
|
+
Re-exports `@tot/public-runtime`'s `ChromeConfig` (`packages/public-runtime/src/chrome.ts`) — the
|
|
1422
|
+
ONE schema for a tenant's site chrome, content-only, skinned by tokens. `header` and `footer` are
|
|
1423
|
+
independent, separately-evolvable sub-shapes; every nav item, header CTA and footer link carries a
|
|
1424
|
+
stable `id` (a governed-action-registry substitute, independent of the visible `label`). In addition
|
|
1425
|
+
to `ChromeHeader`/`ChromeFooter`, it exports `ChromeBrand`, `ChromeSearch`, `ChromeUtilityNav`,
|
|
1426
|
+
`ChromeAnnouncement` (`{ variant: "marquee" | "bar" | "solid"; messages: string[]; label?;
|
|
1427
|
+
dismissible?; rotateMs?; storageKey? }`), `ChromeAgeGate` (`{ minAge: number; storageKey?; title?;
|
|
1428
|
+
lede?; yesLabel?; noLabel?; denyText?; fineText?; mark? }`), `ChromeFooterBrand`, `ChromeTrustMark`,
|
|
1429
|
+
`ChromeHead`, `ChromeCta`, `ChromeNavItem`, and `ChromeConfig` itself.
|
|
1430
|
+
|
|
1431
|
+
### `lib/chrome/parseConfig.ts` — the validation gate
|
|
1432
|
+
|
|
1433
|
+
Re-exports `@tot/public-runtime`'s `validateChromeConfig`/`parseChromeConfig`. `parseChromeConfig(raw:
|
|
1434
|
+
unknown): ChromeConfig | null` returns a config only when `raw.header` and `raw.footer` both validate
|
|
1435
|
+
(header: a valid `variant`, `brand.label` string, `nav` array; footer: a valid `variant`, `columns`
|
|
1436
|
+
array — see {@link validateChromeConfig} for the full per-field rules). Any other shape (missing,
|
|
1437
|
+
malformed, or half-authored) returns `null`, so the caller keeps serving the tenant's hand-authored
|
|
1438
|
+
`chrome.html`. `validateChromeConfig(raw)` returns the actionable, path-prefixed error list instead of
|
|
1439
|
+
collapsing to `null` — this is what `@tot/private-controlplane`'s write-side `ArtifactValidator` for
|
|
1440
|
+
`tenants/<id>/content/chrome.json` calls, so a malformed `chrome.json` is rejected with real diagnostics
|
|
1441
|
+
at candidate-submission time.
|
|
1442
|
+
|
|
1443
|
+
### `public/shared/commerce-chrome.css` — the one shared stylesheet (F2)
|
|
1444
|
+
|
|
1445
|
+
ONE platform-shipped, token-driven sheet that generalizes the three per-tenant `mkt.css` files into a
|
|
1446
|
+
single themed layer. **Contract-token-only** — reads only `--color-*` / `--font-*` / `--radius-*` /
|
|
1447
|
+
`--shadow-*` / `--container-max`, no tenant-private brand vars or hard-coded hex; on-dark surfaces
|
|
1448
|
+
(footer, age-gate overlay, category cards) derive from `--color-text` + `--color-primary-contrast` via
|
|
1449
|
+
`color-mix`. Its header documents the old-var → contract-token mapping (`--navy`/`--brand-red` →
|
|
1450
|
+
`--color-primary`, `--sale` → `--color-sale`, `--brand-blue` → `--color-accent`). Main selector groups:
|
|
1451
|
+
|
|
1452
|
+
- **Layout:** `.container`, `.section` / `.section--tight` / `.section--surface`.
|
|
1453
|
+
- **Buttons:** `.btn` + `.btn-primary` / `.btn-secondary` / `.btn-line` / `.btn-ghost` / `.btn-outline`.
|
|
1454
|
+
- **Announcement:** `.announce`, `.announce-marquee` / `__track` (`@keyframes chrome-marquee`,
|
|
1455
|
+
reduced-motion stops it), `.announce-bar` / `__msg` / `__close`.
|
|
1456
|
+
- **Utility / top bar:** `.topbar-warning` (FDA strip), `.utility-nav` / `__right` / `.sel` / `.social`,
|
|
1457
|
+
`.member-cue`.
|
|
1458
|
+
- **Header / nav:** `.site-header` (sticky) / `.header-row`, `.brand` (+ `.b-prime`/`.b-vapor`/`.b-club`),
|
|
1459
|
+
`.header-search`, `.header-actions` / `.cart` / `.count`, `.hamburger`.
|
|
1460
|
+
- **Primary nav + mega:** `.primary-nav` / `> ul`, `.is-flag`, `.nav-item`, `.caret`, `.new-badge`,
|
|
1461
|
+
`.primary-nav-bar`, `.mega` / `.mega--wide` / `.mega__cols` / `.mega__col`.
|
|
1462
|
+
- **Section headers:** `.sec-head` / `--between` / `.eyebrow` / `.link-more`.
|
|
1463
|
+
- **Tiles / grids:** `.tile-grid` / `.tile`, `.brand-grid` / `.brand-tile` / `__text`, `.article-grid`
|
|
1464
|
+
/ `.article-card` (+ `__media`/`__body`/`__tag`/`__title`/`__excerpt`/`__meta`), `.tag-cloud` /
|
|
1465
|
+
`.tag-chip`.
|
|
1466
|
+
- **Category cards:** `.cat-grid` / `.cat-card` (gradient primary→primary-hover).
|
|
1467
|
+
- **Compliance:** `.compliance-strip` (source-copied legal band).
|
|
1468
|
+
- **Age gate:** `.age-gate` / `.is-open` / `__panel` / `__mark` (+ `.is-red`) / `__actions` / `__deny` /
|
|
1469
|
+
`.is-denied` / `__fine` / `__lede`.
|
|
1470
|
+
- **Footer:** `.site-footer` / `.footer-grid` / `.footer-brand` / `.footer-col` / `.footer-news`
|
|
1471
|
+
(+ `__row`, `.footer-attest`) / `.pay-badges` / `.trust-marks` / `.footer-warning` / `.footer-bottom`.
|
|
1472
|
+
- **Reveal-on-scroll:** `.reveal` / `.reveal.in`.
|
|
1473
|
+
- **Responsive:** breakpoints at 1000 / 960 / 860 (off-canvas `.primary-nav` drawer, `.hamburger`,
|
|
1474
|
+
mega→inline accordion) / 560 px.
|
|
1475
|
+
|
|
1476
|
+
---
|
|
1477
|
+
|
|
1478
|
+
## Appendix — cross-cutting reference
|
|
1479
|
+
|
|
1480
|
+
**Cross-island `window` CustomEvents:** `variant:image` (VariantSelector → ImageGallery),
|
|
1481
|
+
`variant:change` (VariantSelector → any listener), `tot:wishlist-changed` / `WISHLIST_EVENT`
|
|
1482
|
+
(`lib/wishlist` → SavedList + header count), and delegated `[data-quickview]` buttons → QuickView.
|
|
1483
|
+
|
|
1484
|
+
**`localStorage` keys:** `"tot:age-affirmed"` (age gate), `"tot:recently-viewed"` (RecentlyViewed),
|
|
1485
|
+
`"tot:wishlist"` (SavedList/wishlist), `tot_announce_dismissed` (raw-chrome announce dismissal).
|
|
1486
|
+
|
|
1487
|
+
**`data-compliance` hooks (9):** `nicotine-warning`, `pact-act`, `adult-signature`, `prop65`,
|
|
1488
|
+
`purchase-limit`, `state-eligibility`, `shipping-restriction`, `excise-tax`, plus `ComplianceNotice`
|
|
1489
|
+
re-emitting the caller's `hook`.
|
|
1490
|
+
|
|
1491
|
+
**`data-widget` hooks (5):** `tier-cards`, `tier-comparison-table`, `subscription-how-it-works`,
|
|
1492
|
+
`subscription-savings-explainer`, `subscription-manage-entry`. Illustrative surfaces are marked
|
|
1493
|
+
`data-illustrative="badge" | "note" | "figure"`.
|
|
1494
|
+
|
|
1495
|
+
**Related architecture docs:** [`token-contract.md`](architecture/token-contract.md),
|
|
1496
|
+
[`shared-vs-tenant-assets.md`](architecture/shared-vs-tenant-assets.md).
|