@tokenoftrust/storefront-runner 1.3.4-rc.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/apps/storefront/astro.config.mjs +15 -0
  2. package/apps/storefront/dev-plugins/dev-publish.mjs +55 -0
  3. package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +86 -7
  4. package/apps/storefront/drizzle.config.apps.ts +13 -0
  5. package/apps/storefront/env.d.ts +10 -0
  6. package/apps/storefront/migrations/README.md +13 -7
  7. package/apps/storefront/migrations-apps/0000_fast_millenium_guard.sql +129 -0
  8. package/apps/storefront/migrations-apps/meta/0000_snapshot.json +843 -0
  9. package/apps/storefront/migrations-apps/meta/_journal.json +13 -0
  10. package/apps/storefront/package.json +13 -1
  11. package/apps/storefront/perf/README.md +64 -0
  12. package/apps/storefront/perf/assert-budgets.ts +159 -0
  13. package/apps/storefront/playwright.config.ts +23 -0
  14. package/apps/storefront/public/js/dashboard-apps.js +173 -0
  15. package/apps/storefront/public/shared/commerce-marketing.css +221 -0
  16. package/apps/storefront/src/components/CollectionCard.astro +1 -0
  17. package/apps/storefront/src/components/ProductCard.astro +1 -0
  18. package/apps/storefront/src/components/admin/AdminPublishTab.astro +1959 -0
  19. package/apps/storefront/src/components/apps/AppWidgetFrame.astro +30 -0
  20. package/apps/storefront/src/components/chrome/NavDropdown.astro +6 -3
  21. package/apps/storefront/src/components/chrome/SiteFooter.astro +10 -0
  22. package/apps/storefront/src/components/chrome/SiteHeader.astro +10 -0
  23. package/apps/storefront/src/components/commerce/RatingStars.astro +3 -2
  24. package/apps/storefront/src/components/content/Callout.astro +75 -0
  25. package/apps/storefront/src/components/content/NeedsReviewCallout.astro +66 -0
  26. package/apps/storefront/src/components/content/ProseSections.astro +121 -0
  27. package/apps/storefront/src/components/content/ProseToc.astro +34 -0
  28. package/apps/storefront/src/components/content/RichText.astro +44 -0
  29. package/apps/storefront/src/components/content/TrustStrip.astro +46 -0
  30. package/apps/storefront/src/components/home/Hero.astro +14 -0
  31. package/apps/storefront/src/components/islands/CheckoutComplianceGate.tsx +295 -0
  32. package/apps/storefront/src/components/islands/ImageGallery.tsx +42 -21
  33. package/apps/storefront/src/components/islands/VariantSelector.tsx +62 -12
  34. package/apps/storefront/src/components/plp/FacetSidebar.astro +2 -2
  35. package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +1 -0
  36. package/apps/storefront/src/config/compliance/rulesets.ts +79 -0
  37. package/apps/storefront/src/config/storeName.ts +34 -0
  38. package/apps/storefront/src/layouts/Layout.astro +97 -23
  39. package/apps/storefront/src/lib/analytics/budgets.json +69 -0
  40. package/apps/storefront/src/lib/analytics/lighthouseReport.ts +109 -0
  41. package/apps/storefront/src/lib/analytics/perfBudgets.ts +452 -0
  42. package/apps/storefront/src/lib/analytics/rumAlert.ts +179 -0
  43. package/apps/storefront/src/lib/analytics/webVitals.ts +269 -0
  44. package/apps/storefront/src/lib/apps/adminService.ts +106 -0
  45. package/apps/storefront/src/lib/apps/adminSession.ts +205 -0
  46. package/apps/storefront/src/lib/apps/apiAuth.ts +91 -0
  47. package/apps/storefront/src/lib/apps/apiRoute.ts +35 -0
  48. package/apps/storefront/src/lib/apps/catalogMapper.ts +39 -0
  49. package/apps/storefront/src/lib/apps/credentials.ts +153 -0
  50. package/apps/storefront/src/lib/apps/gatewayKeys.ts +156 -0
  51. package/apps/storefront/src/lib/apps/healthAggregate.ts +66 -0
  52. package/apps/storefront/src/lib/apps/orders/attributionService.ts +206 -0
  53. package/apps/storefront/src/lib/apps/orders/customerHash.ts +28 -0
  54. package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +203 -0
  55. package/apps/storefront/src/lib/apps/orders/idempotency.ts +100 -0
  56. package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +117 -0
  57. package/apps/storefront/src/lib/apps/orders/orderMapper.ts +91 -0
  58. package/apps/storefront/src/lib/apps/orders/ordersStore.ts +181 -0
  59. package/apps/storefront/src/lib/apps/registryService.ts +579 -0
  60. package/apps/storefront/src/lib/apps/scopes.ts +79 -0
  61. package/apps/storefront/src/lib/apps/tokenIssuer.ts +121 -0
  62. package/apps/storefront/src/lib/apps/tokenVerifier.ts +148 -0
  63. package/apps/storefront/src/lib/apps/widgets/eligibility.ts +18 -0
  64. package/apps/storefront/src/lib/apps/widgets/frameProps.ts +52 -0
  65. package/apps/storefront/src/lib/apps/widgets/launchToken.ts +84 -0
  66. package/apps/storefront/src/lib/apps/widgets/placements.ts +57 -0
  67. package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +111 -0
  68. package/apps/storefront/src/lib/auth/adminEntry.ts +119 -0
  69. package/apps/storefront/src/lib/auth/identityToken.ts +21 -2
  70. package/apps/storefront/src/lib/auth/loginGate.ts +102 -18
  71. package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +215 -0
  72. package/apps/storefront/src/lib/auth/route.ts +16 -1
  73. package/apps/storefront/src/lib/auth/session.ts +8 -0
  74. package/apps/storefront/src/lib/auth/stepUpChallenge.ts +107 -0
  75. package/apps/storefront/src/lib/auth/totAccessClient.ts +208 -0
  76. package/apps/storefront/src/lib/blog/provider.ts +39 -0
  77. package/apps/storefront/src/lib/blog/types.ts +26 -0
  78. package/apps/storefront/src/lib/checkoutCommerce.ts +39 -1
  79. package/apps/storefront/src/lib/chrome/model.ts +11 -0
  80. package/apps/storefront/src/lib/compliance/enforcement.ts +95 -0
  81. package/apps/storefront/src/lib/content/callout.ts +78 -0
  82. package/apps/storefront/src/lib/content/index.ts +27 -0
  83. package/apps/storefront/src/lib/content/needsReview.ts +58 -0
  84. package/apps/storefront/src/lib/content/prose.ts +186 -0
  85. package/apps/storefront/src/lib/content/richtext.ts +76 -0
  86. package/apps/storefront/src/lib/content/trustStrip.ts +74 -0
  87. package/apps/storefront/src/lib/content-edit/client.ts +70 -14
  88. package/apps/storefront/src/lib/d1/catalog.ts +12 -0
  89. package/apps/storefront/src/lib/d1/schema-apps.ts +213 -0
  90. package/apps/storefront/src/lib/dev/apiBase.ts +8 -2
  91. package/apps/storefront/src/lib/dev/cliSignInCode.ts +65 -115
  92. package/apps/storefront/src/lib/dev/cockpitStore.ts +65 -0
  93. package/apps/storefront/src/lib/dev/obstacleStore.ts +214 -0
  94. package/apps/storefront/src/lib/dev/previewStatus.ts +112 -0
  95. package/apps/storefront/src/lib/dev/rendezvousBroker.ts +241 -0
  96. package/apps/storefront/src/lib/dev/subjectReissue.ts +67 -0
  97. package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +10 -10
  98. package/apps/storefront/src/lib/env.ts +12 -0
  99. package/apps/storefront/src/lib/jsonld.ts +12 -17
  100. package/apps/storefront/src/lib/membership/eligibility.ts +37 -0
  101. package/apps/storefront/src/lib/monitoring/manifest.ts +302 -0
  102. package/apps/storefront/src/lib/privacy/emailHint.ts +13 -5
  103. package/apps/storefront/src/lib/publish/apex-readiness.ts +337 -0
  104. package/apps/storefront/src/lib/publish/dispatchHealth.ts +269 -0
  105. package/apps/storefront/src/lib/publish/domainState.ts +351 -0
  106. package/apps/storefront/src/lib/publish/shipWorkspace.ts +362 -0
  107. package/apps/storefront/src/lib/rawChrome.ts +34 -3
  108. package/apps/storefront/src/lib/storyblok/content-model.ts +34 -2
  109. package/apps/storefront/src/lib/storyblok/provider.ts +11 -4
  110. package/apps/storefront/src/lib/subscription/model.ts +114 -0
  111. package/apps/storefront/src/lib/tot/ToTClient.ts +3 -3
  112. package/apps/storefront/src/lib/tot/query.ts +32 -0
  113. package/apps/storefront/src/lib/webhooks/cloudflareQueueDispatcher.ts +82 -0
  114. package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +245 -0
  115. package/apps/storefront/src/lib/webhooks/deliveryMapper.ts +34 -0
  116. package/apps/storefront/src/lib/webhooks/deliveryStore.ts +668 -0
  117. package/apps/storefront/src/lib/webhooks/dispatcher.ts +168 -0
  118. package/apps/storefront/src/lib/webhooks/emit.ts +167 -0
  119. package/apps/storefront/src/lib/webhooks/endpointGuard.ts +135 -0
  120. package/apps/storefront/src/lib/webhooks/events.ts +98 -0
  121. package/apps/storefront/src/lib/webhooks/getDispatcher.ts +49 -0
  122. package/apps/storefront/src/lib/webhooks/signing.ts +29 -0
  123. package/apps/storefront/src/lib/webhooks/webhookSigningKey.ts +146 -0
  124. package/apps/storefront/src/middleware/index.ts +28 -13
  125. package/apps/storefront/src/pages/404.astro +21 -9
  126. package/apps/storefront/src/pages/[...slug].astro +36 -2
  127. package/apps/storefront/src/pages/admin.astro +29 -3
  128. package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +60 -0
  129. package/apps/storefront/src/pages/api/apps/admin/health.ts +44 -0
  130. package/apps/storefront/src/pages/api/apps/admin/install.ts +100 -0
  131. package/apps/storefront/src/pages/api/apps/admin/list.ts +26 -0
  132. package/apps/storefront/src/pages/api/apps/admin/resume.ts +56 -0
  133. package/apps/storefront/src/pages/api/apps/admin/suspend.ts +57 -0
  134. package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +74 -0
  135. package/apps/storefront/src/pages/api/apps/admin/update.ts +103 -0
  136. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +59 -0
  137. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +51 -0
  138. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +172 -0
  139. package/apps/storefront/src/pages/api/apps/oauth/token.ts +87 -0
  140. package/apps/storefront/src/pages/api/apps/v1/attribution.ts +142 -0
  141. package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +39 -0
  142. package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +49 -0
  143. package/apps/storefront/src/pages/api/apps/v1/health.ts +32 -0
  144. package/apps/storefront/src/pages/api/apps/v1/inventory.ts +59 -0
  145. package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +48 -0
  146. package/apps/storefront/src/pages/api/apps/v1/orders.ts +73 -0
  147. package/apps/storefront/src/pages/api/apps/v1/reports.ts +21 -0
  148. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +62 -0
  149. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +47 -0
  150. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +48 -1
  151. package/apps/storefront/src/pages/api/auth/step-up-send.ts +57 -0
  152. package/apps/storefront/src/pages/api/auth/step-up-verify.ts +129 -0
  153. package/apps/storefront/src/pages/api/auth/verify.ts +23 -0
  154. package/apps/storefront/src/pages/api/compliance/preflight.ts +206 -0
  155. package/apps/storefront/src/pages/api/rum/vitals.ts +54 -0
  156. package/apps/storefront/src/pages/api/test/dev-session.ts +133 -0
  157. package/apps/storefront/src/pages/auth/login.astro +197 -41
  158. package/apps/storefront/src/pages/auth/magic.astro +75 -43
  159. package/apps/storefront/src/pages/blog/[slug].astro +107 -0
  160. package/apps/storefront/src/pages/blog/index.astro +98 -0
  161. package/apps/storefront/src/pages/capabilities.astro +8 -0
  162. package/apps/storefront/src/pages/cockpit.astro +496 -74
  163. package/apps/storefront/src/pages/collections/[handle].astro +8 -0
  164. package/apps/storefront/src/pages/collections/index.astro +10 -2
  165. package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +119 -0
  166. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +5 -0
  167. package/apps/storefront/src/pages/index.astro +57 -0
  168. package/apps/storefront/src/pages/llms.txt.ts +31 -10
  169. package/apps/storefront/src/pages/products/[handle].astro +100 -9
  170. package/apps/storefront/src/pages/sitemap.xml.ts +21 -4
  171. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +198 -0
  172. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +7 -0
  173. package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +7 -0
  174. package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +7 -0
  175. package/apps/storefront/src/pages/style-guide/index.astro +10 -0
  176. package/apps/storefront/src/styles/fonts.css +54 -0
  177. package/apps/storefront/src/styles/global.css +22 -2
  178. package/apps/storefront/src/themes/schema.ts +3 -25
  179. package/apps/storefront/tsconfig.json +1 -1
  180. package/apps/storefront/vitest.config.ts +4 -1
  181. package/package.json +1 -1
  182. package/packages/public-runtime/src/candidate-index.ts +311 -0
  183. package/packages/public-runtime/src/checkout.ts +94 -2
  184. package/packages/public-runtime/src/compliance/evaluate.ts +265 -0
  185. package/packages/public-runtime/src/compliance/evidence-signals.ts +81 -0
  186. package/packages/public-runtime/src/compliance/index.ts +22 -0
  187. package/packages/public-runtime/src/compliance/pact-report.ts +94 -0
  188. package/packages/public-runtime/src/compliance/profile.ts +198 -0
  189. package/packages/public-runtime/src/compliance/ruleset.ts +117 -0
  190. package/packages/public-runtime/src/compliance/verification.ts +81 -0
  191. package/packages/public-runtime/src/csp.ts +27 -3
  192. package/packages/public-runtime/src/customization-reconcile.ts +35 -0
  193. package/packages/public-runtime/src/customization-runtime.ts +8 -0
  194. package/packages/public-runtime/src/customization-versioning.ts +17 -0
  195. package/packages/public-runtime/src/extension-contract.ts +2 -1
  196. package/packages/public-runtime/src/hash.ts +25 -0
  197. package/packages/public-runtime/src/index.ts +6 -0
  198. package/packages/public-runtime/src/membership.ts +353 -0
  199. package/packages/public-runtime/src/product.ts +24 -2
  200. package/packages/public-runtime/src/review-trust-proof.ts +194 -0
  201. package/packages/public-runtime/src/tenant-assets.ts +40 -5
  202. package/packages/public-runtime/src/tenant.ts +236 -0
  203. package/packages/public-runtime/src/widget-postmessage.ts +205 -0
  204. package/scripts/dev/publish.mjs +158 -0
  205. package/scripts/dev/transient-files.mjs +2 -1
  206. package/tenants/home/public/fonts/inter-latin-400-normal.woff2 +0 -0
  207. package/tenants/home/public/fonts/inter-latin-500-normal.woff2 +0 -0
  208. package/tenants/home/public/fonts/inter-latin-600-normal.woff2 +0 -0
  209. package/tenants/home/public/fonts/inter-latin-ext-400-normal.woff2 +0 -0
  210. package/tenants/home/public/fonts/inter-latin-ext-500-normal.woff2 +0 -0
  211. package/tenants/home/public/fonts/inter-latin-ext-600-normal.woff2 +0 -0
  212. package/tenants/home/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  213. package/tenants/home/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
  214. package/tenants/home/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
  215. package/tenants/home/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
  216. package/tenants/home/public/fonts/plus-jakarta-sans-latin-500-normal.woff2 +0 -0
  217. package/tenants/home/public/fonts/plus-jakarta-sans-latin-600-normal.woff2 +0 -0
  218. package/tenants/home/public/fonts/plus-jakarta-sans-latin-700-normal.woff2 +0 -0
  219. package/tenants/home/public/fonts/plus-jakarta-sans-latin-800-normal.woff2 +0 -0
  220. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-500-normal.woff2 +0 -0
  221. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-600-normal.woff2 +0 -0
  222. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-700-normal.woff2 +0 -0
  223. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-800-normal.woff2 +0 -0
  224. package/tenants/home/public/pages/storefront.css +23 -0
  225. package/apps/storefront/src/lib/dev/hostedCockpit.ts +0 -169
@@ -0,0 +1,30 @@
1
+ ---
2
+ /**
3
+ * <AppWidgetFrame> — renders one private-app widget placement (PrivateApps
4
+ * epic, D5 Chunk D) as a sandboxed iframe. Its `sandbox`/`loading`/
5
+ * `referrerpolicy` come straight from `widgetFrameAttrs()`
6
+ * (`allow-scripts`, no `allow-same-origin` — see `frameProps.ts`), the SAME
7
+ * primitive `WidgetFrame.astro` uses for tenant `scripts.json` widgets. This
8
+ * component composes the `src` (endpoint + launch-token query param) and
9
+ * stamps `data-tot-widget-origin`/`data-tot-install-id` directly on the
10
+ * `<iframe>` for the parent-side `mountWidgetPostMessageBridge()`
11
+ * (`@tot/public-runtime`) to scan for.
12
+ */
13
+ import { buildAppWidgetFrameAttrs, type AppWidgetFrameProps } from "@/lib/apps/widgets/frameProps.js";
14
+
15
+ type Props = AppWidgetFrameProps;
16
+
17
+ const attrs = buildAppWidgetFrameAttrs(Astro.props);
18
+ ---
19
+
20
+ <iframe
21
+ src={attrs.src}
22
+ title={attrs.title}
23
+ sandbox={attrs.sandbox}
24
+ loading={attrs.loading}
25
+ referrerpolicy={attrs.referrerpolicy}
26
+ data-tot-widget-origin={attrs.dataWidgetOrigin}
27
+ data-tot-install-id={attrs.dataInstallId}
28
+ class="tot-app-widget-frame"
29
+ style={`border:0;width:100%;height:${attrs.height};`}
30
+ ></iframe>
@@ -78,7 +78,7 @@ const groups = isMega
78
78
  <a
79
79
  href={hrefOf(child)}
80
80
  class="flex items-center gap-1.5 rounded-sm px-2 py-1.5 text-sm text-muted transition-colors hover:bg-bg hover:text-primary"
81
- {...(child.external ? { rel: "noopener noreferrer" } : {})}
81
+ {...(child.external ? { rel: "noopener noreferrer" } : { "data-astro-prefetch": true })}
82
82
  >
83
83
  {child.label}
84
84
  {child.badge && (
@@ -99,6 +99,7 @@ const groups = isMega
99
99
  <li>
100
100
  <a
101
101
  href={itemHref}
102
+ data-astro-prefetch
102
103
  class:list={[
103
104
  "flex items-center gap-1.5 border-b border-border py-2.5 text-sm font-medium text-text transition-colors last:border-b-0 hover:text-accent",
104
105
  item.flag && "text-accent",
@@ -117,6 +118,7 @@ const groups = isMega
117
118
  <li class="group relative">
118
119
  <a
119
120
  href={itemHref}
121
+ data-astro-prefetch
120
122
  class:list={[
121
123
  "inline-flex items-center gap-1 py-2 text-sm transition-colors hover:text-accent group-focus-within:text-accent",
122
124
  item.flag && "font-semibold text-accent",
@@ -148,7 +150,7 @@ const groups = isMega
148
150
  <a
149
151
  href={hrefOf(child)}
150
152
  class="flex items-center gap-1.5 rounded-sm px-2 py-1 text-sm text-muted transition-colors hover:bg-bg hover:text-primary"
151
- {...(child.external ? { rel: "noopener noreferrer" } : {})}
153
+ {...(child.external ? { rel: "noopener noreferrer" } : { "data-astro-prefetch": true })}
152
154
  >
153
155
  {child.label}
154
156
  {child.badge && (
@@ -170,7 +172,7 @@ const groups = isMega
170
172
  <a
171
173
  href={hrefOf(child)}
172
174
  class="block rounded-sm px-3 py-1.5 text-sm text-muted transition-colors hover:bg-bg hover:text-primary"
173
- {...(child.external ? { rel: "noopener noreferrer" } : {})}
175
+ {...(child.external ? { rel: "noopener noreferrer" } : { "data-astro-prefetch": true })}
174
176
  >
175
177
  {child.label}
176
178
  </a>
@@ -184,6 +186,7 @@ const groups = isMega
184
186
  <li>
185
187
  <a
186
188
  href={itemHref}
189
+ data-astro-prefetch
187
190
  class:list={[
188
191
  "inline-flex items-center gap-1 py-2 text-sm transition-colors hover:text-accent",
189
192
  item.flag && "font-semibold text-accent",
@@ -50,11 +50,21 @@ const showLogo = !!brand?.logo && /^(https?:|\/)/.test(brand.logo.src);
50
50
  <div>
51
51
  {
52
52
  showLogo ? (
53
+ /*
54
+ Brand logo: a bare merchant URL shown at a fixed height via
55
+ object-contain. It can't go through <Img> — that forces an
56
+ aspect-ratio crop box + a placeholder background, which would alter
57
+ a transparent logo's appearance. Keep raw, but set explicit
58
+ width/height (reserve space → CLS-free) and, since the footer sits
59
+ below the fold, lazy/async loading hints to defer the fetch.
60
+ */
53
61
  <img
54
62
  src={brand!.logo!.src}
55
63
  alt={brand!.logo!.alt}
56
64
  width={brand!.logo!.width ?? 160}
57
65
  height={brand!.logo!.height ?? 48}
66
+ loading="lazy"
67
+ decoding="async"
58
68
  class="h-9 w-auto max-w-[180px] object-contain"
59
69
  />
60
70
  ) : (
@@ -106,11 +106,21 @@ const mobileNav = nav.map((item) => ({
106
106
  >
107
107
  {
108
108
  showLogo ? (
109
+ /*
110
+ Brand logo: a bare merchant URL shown at a fixed header height via
111
+ object-contain. It can't go through <Img> — that forces an
112
+ aspect-ratio crop box + a placeholder background, which would alter
113
+ a transparent logo's appearance. Keep raw, but set explicit
114
+ width/height (reserve space → CLS-free) and, since it sits
115
+ above the fold, eager/async loading hints.
116
+ */
109
117
  <img
110
118
  src={logo!.src}
111
119
  alt={logo!.alt}
112
120
  width={logo!.width ?? 160}
113
121
  height={logo!.height ?? 48}
122
+ loading="eager"
123
+ decoding="async"
114
124
  class="h-9 w-auto max-w-[180px] object-contain"
115
125
  />
116
126
  ) : (
@@ -2,7 +2,8 @@
2
2
  /**
3
3
  * <RatingStars> — atom. A 0–5 star rating with fractional fill (a single clipped
4
4
  * overlay, no half-star glyph juggling) and an accessible label. Tenant-agnostic:
5
- * filled stars use `--color-accent`, the empty track uses `--color-border`.
5
+ * filled stars use `--color-star` (the trust/review star token, defaults to the
6
+ * accent spark), the empty track uses `--color-border`.
6
7
  *
7
8
  * Presentation only — it renders whatever aggregate you pass. No review data is
8
9
  * invented here (claims-ledger discipline: source ratings before shipping them).
@@ -42,7 +43,7 @@ const label =
42
43
  <span class:list={["relative inline-block leading-none tracking-[0.08em]", dim]} aria-hidden="true">
43
44
  <span class="text-[var(--color-border)]">{stars}</span>
44
45
  <span
45
- class="absolute inset-0 overflow-hidden whitespace-nowrap text-[var(--color-accent)]"
46
+ class="absolute inset-0 overflow-hidden whitespace-nowrap text-[var(--color-star)]"
46
47
  style={`width:${pct}%`}
47
48
  >
48
49
  {stars}
@@ -0,0 +1,75 @@
1
+ ---
2
+ /**
3
+ * <Callout> — a tone-styled aside (see lib/content/callout.ts), the structured
4
+ * form of the f05 sheet's `.disclaimer` (info) and `.warn-band` (warning/legal)
5
+ * page patterns. A tenant supplies a {@link CalloutModel}; this paints it from
6
+ * f06 tokens using the SAME token formulas the raw sheet uses — the info tone
7
+ * mirrors `.disclaimer` (info tint over surface), warning/legal mirror
8
+ * `.warn-band` (warning tint + a warning left border) — so the raw-HTML path and
9
+ * the structured widget look identical.
10
+ *
11
+ * A11y is baked in via calloutPresentation(): role="note" (a static aside, not a
12
+ * live region) + a tone aria-label, and the icon is aria-hidden. The body is a
13
+ * <RichText>, so no HTML is authored. Arbitrary token classes are STATIC
14
+ * per-tone literals so the Tailwind JIT emits them (same discipline as
15
+ * IllustrativeLabel / ComplianceNotice).
16
+ */
17
+ import type { CalloutModel, CalloutTone } from "@/lib/content/callout";
18
+ import { calloutPresentation } from "@/lib/content/callout";
19
+ import RichText from "@/components/content/RichText.astro";
20
+
21
+ interface Props {
22
+ callout: CalloutModel;
23
+ }
24
+ const { callout } = Astro.props;
25
+ const p = calloutPresentation(callout.tone);
26
+
27
+ // Static per-tone token classes (JIT-safe). Mirrors commerce-marketing.css:
28
+ // info → .disclaimer tint; warning/legal → .warn-band tint + left border.
29
+ const TONE: Record<
30
+ CalloutTone,
31
+ { box: string; icon: string }
32
+ > = {
33
+ info: {
34
+ box: "bg-[color-mix(in_srgb,var(--color-info)_10%,var(--color-surface))] border-[color-mix(in_srgb,var(--color-info)_32%,var(--color-border))]",
35
+ icon: "text-[var(--color-info)]",
36
+ },
37
+ warning: {
38
+ box: "bg-[color-mix(in_srgb,var(--color-warning)_12%,var(--color-surface))] border-[color-mix(in_srgb,var(--color-warning)_40%,var(--color-border))] border-l-4 border-l-[var(--color-warning)]",
39
+ icon: "text-[var(--color-warning)]",
40
+ },
41
+ legal: {
42
+ box: "bg-[color-mix(in_srgb,var(--color-warning)_12%,var(--color-surface))] border-[color-mix(in_srgb,var(--color-warning)_40%,var(--color-border))] border-l-4 border-l-[var(--color-warning)]",
43
+ icon: "text-[var(--color-warning)]",
44
+ },
45
+ success: {
46
+ box: "bg-[color-mix(in_srgb,var(--color-success)_10%,var(--color-surface))] border-[color-mix(in_srgb,var(--color-success)_32%,var(--color-border))]",
47
+ icon: "text-[var(--color-success)]",
48
+ },
49
+ };
50
+ const tone = TONE[callout.tone] ?? TONE.info;
51
+ ---
52
+ <div
53
+ class={`my-5 flex max-w-[76ch] items-start gap-2.5 rounded-[var(--radius-md)] border p-3.5 text-sm text-[var(--color-text)] ${tone.box}`}
54
+ role={p.role}
55
+ aria-label={p.label}
56
+ data-widget="callout"
57
+ data-tone={callout.tone}
58
+ >
59
+ <svg
60
+ class={`mt-0.5 h-[18px] w-[18px] flex-none ${tone.icon}`}
61
+ viewBox="0 0 24 24"
62
+ fill="none"
63
+ stroke="currentColor"
64
+ stroke-width="2"
65
+ stroke-linecap="round"
66
+ stroke-linejoin="round"
67
+ aria-hidden="true"
68
+ >
69
+ <path d={p.iconPath} />
70
+ </svg>
71
+ <div>
72
+ {callout.title && <p class="font-semibold">{callout.title}</p>}
73
+ <p class={callout.title ? "mt-1" : ""}><RichText value={callout.body} /></p>
74
+ </div>
75
+ </div>
@@ -0,0 +1,66 @@
1
+ ---
2
+ /**
3
+ * <NeedsReviewCallout> — the structured form of the f05 sheet's `.needs-review`
4
+ * flag + `.nr` badge (see lib/content/needsReview.ts). Marks a placeholder that
5
+ * needs human sign-off before go-live — the honest counterpart to f09's
6
+ * real-or-absent rule (say "needs confirming", never fabricate).
7
+ *
8
+ * This is a REVIEWER affordance: it is shown in preview/dev only. Live shopper
9
+ * pages omit it — <ProseSections includeNeedsReview={false}> drops the section,
10
+ * and the deploy path fails on any `blocker` via auditNeedsReview(). Styled from
11
+ * f06 tokens, mirroring the raw sheet (surface ground + a warning left border +
12
+ * the uppercase `.nr` badge). role="note" + data hooks for audit/e2e.
13
+ */
14
+ import type { NeedsReview } from "@/lib/content/needsReview";
15
+ import { NEEDS_REVIEW_TITLE } from "@/lib/content/needsReview";
16
+
17
+ interface Props {
18
+ flag: NeedsReview;
19
+ }
20
+ const { flag } = Astro.props;
21
+ const severity = flag.severity ?? "note";
22
+ ---
23
+ <div
24
+ class="my-5 flex max-w-[76ch] items-start gap-2.5 rounded-[var(--radius-md)] border border-[var(--color-border)] border-l-4 border-l-[var(--color-warning)] bg-[var(--color-surface)] p-3.5 text-sm text-[var(--color-text)]"
25
+ role="note"
26
+ aria-label={NEEDS_REVIEW_TITLE}
27
+ data-widget="needs-review"
28
+ data-needs-review
29
+ data-severity={severity}
30
+ >
31
+ <svg
32
+ class="mt-0.5 h-[18px] w-[18px] flex-none text-[var(--color-warning)]"
33
+ viewBox="0 0 24 24"
34
+ fill="none"
35
+ stroke="currentColor"
36
+ stroke-width="2"
37
+ stroke-linecap="round"
38
+ stroke-linejoin="round"
39
+ aria-hidden="true"
40
+ >
41
+ <path d="M12 9v4m0 4h.01M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z" />
42
+ </svg>
43
+ <div>
44
+ <p class="font-semibold">
45
+ {NEEDS_REVIEW_TITLE}
46
+ <span class="ml-1.5 inline-block rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-surface-2)] px-1.5 py-px align-middle text-[0.6rem] font-extrabold uppercase tracking-[0.06em] text-[var(--color-warning)]">
47
+ {severity === "blocker" ? "Blocker" : "Review"}
48
+ </span>
49
+ </p>
50
+ <p class="mt-1">{flag.reason}</p>
51
+ {(flag.owner || flag.href) && (
52
+ <p class="mt-1 text-xs text-[var(--color-muted)]">
53
+ {flag.owner && <span>Owner: {flag.owner}</span>}
54
+ {flag.owner && flag.href && <span> · </span>}
55
+ {flag.href && (
56
+ <a
57
+ href={flag.href}
58
+ class="text-[var(--color-accent-ink)] underline underline-offset-[3px]"
59
+ >
60
+ Context
61
+ </a>
62
+ )}
63
+ </p>
64
+ )}
65
+ </div>
66
+ </div>
@@ -0,0 +1,121 @@
1
+ ---
2
+ /**
3
+ * <ProseSections> — renders a structured {@link ProseSection}[] (policy / about /
4
+ * compliance body) as a real document: <h2>/<h3> with anchor ids, <p>, <ul>/<ol>,
5
+ * a semantic <dl> glossary, <address> contact block, tone callouts, and (in
6
+ * preview only) needs-review flags. This is the F4 promotion of the old
7
+ * `set:html(body_html)` policy blob — a tenant drives the whole page from JSON,
8
+ * copying no HTML/CSS. Mirrors the f05 sheet's `.prose` / `.contact-card`
9
+ * patterns using f06 tokens; accessibility lives in the components, not content.
10
+ *
11
+ * `includeNeedsReview` gates the reviewer-only needs-review flags: true in
12
+ * preview/dev (the reviewer sees what still needs sign-off), false on live
13
+ * shopper pages (the flag is scaffolding, never shown to shoppers).
14
+ */
15
+ import type { ProseSection } from "@/lib/content/prose";
16
+ import { normalizeProse, withHeadingIds } from "@/lib/content/prose";
17
+ import RichText from "@/components/content/RichText.astro";
18
+ import Callout from "@/components/content/Callout.astro";
19
+ import NeedsReviewCallout from "@/components/content/NeedsReviewCallout.astro";
20
+
21
+ interface Props {
22
+ sections: ProseSection[];
23
+ /** Show reviewer-only needs-review flags (preview/dev). Default false (live). */
24
+ includeNeedsReview?: boolean;
25
+ }
26
+ const { sections, includeNeedsReview = false } = Astro.props;
27
+ const rendered = withHeadingIds(normalizeProse(sections));
28
+ ---
29
+ <div class="prose-sections max-w-[68ch] text-[var(--color-text)]" data-widget="prose-sections">
30
+ {
31
+ rendered.map((s) => {
32
+ switch (s.kind) {
33
+ case "heading":
34
+ return (s.level ?? 2) === 3 ? (
35
+ <h3 id={s.id} class="mt-6 scroll-mt-24 font-display text-xl font-semibold">
36
+ {s.text}
37
+ </h3>
38
+ ) : (
39
+ <h2 id={s.id} class="mt-8 scroll-mt-24 font-display text-2xl font-semibold">
40
+ {s.text}
41
+ </h2>
42
+ );
43
+ case "paragraph":
44
+ return (
45
+ <p class="mt-3 leading-relaxed">
46
+ <RichText value={s.text} />
47
+ </p>
48
+ );
49
+ case "list":
50
+ return s.ordered ? (
51
+ <ol class="mt-3 list-decimal space-y-2 pl-5">
52
+ {s.items.map((item) => (
53
+ <li><RichText value={item} /></li>
54
+ ))}
55
+ </ol>
56
+ ) : (
57
+ <ul class="mt-3 list-disc space-y-2 pl-5">
58
+ {s.items.map((item) => (
59
+ <li><RichText value={item} /></li>
60
+ ))}
61
+ </ul>
62
+ );
63
+ case "definitions":
64
+ return (
65
+ <dl class="mt-4 space-y-4">
66
+ {s.items.map((d) => (
67
+ <div>
68
+ <dt class="font-semibold text-[var(--color-text)]">{d.term}</dt>
69
+ <dd class="mt-1 text-[var(--color-muted)]">
70
+ <RichText value={d.description} />
71
+ </dd>
72
+ </div>
73
+ ))}
74
+ </dl>
75
+ );
76
+ case "callout":
77
+ return <Callout callout={{ tone: s.tone, title: s.title, body: s.body }} />;
78
+ case "contact":
79
+ return (
80
+ <address class="mt-4 not-italic">
81
+ {s.heading && (
82
+ <p class="font-display text-lg font-semibold">{s.heading}</p>
83
+ )}
84
+ {s.email && (
85
+ <a
86
+ href={`mailto:${s.email}`}
87
+ class="mt-2 inline-flex min-h-[var(--tap-target)] items-center text-[var(--color-accent-ink)] underline underline-offset-[3px]"
88
+ >
89
+ {s.email}
90
+ </a>
91
+ )}
92
+ {s.phone && (
93
+ <a
94
+ href={`tel:${s.phone.replace(/[^+\d]/g, "")}`}
95
+ class="mt-1 flex min-h-[var(--tap-target)] items-center text-[var(--color-accent-ink)] underline underline-offset-[3px]"
96
+ >
97
+ {s.phone}
98
+ </a>
99
+ )}
100
+ {s.address && s.address.length > 0 && (
101
+ <p class="mt-2 text-[var(--color-muted)]">
102
+ {s.address.map((line, i) => (
103
+ <>
104
+ {i > 0 && <br />}
105
+ {line}
106
+ </>
107
+ ))}
108
+ </p>
109
+ )}
110
+ </address>
111
+ );
112
+ case "needs_review":
113
+ return includeNeedsReview ? (
114
+ <NeedsReviewCallout flag={{ reason: s.reason, severity: s.severity, owner: s.owner, href: s.href }} />
115
+ ) : null;
116
+ default:
117
+ return null;
118
+ }
119
+ })
120
+ }
121
+ </div>
@@ -0,0 +1,34 @@
1
+ ---
2
+ /**
3
+ * <ProseToc> — the table-of-contents chip row for a long policy page (the f05
4
+ * sheet's `.toc` pattern), driven by {@link tableOfContents} over the same
5
+ * structured sections <ProseSections> renders. One chip per level-2 heading,
6
+ * anchor-linked to the heading id. Rendered as a real <nav> landmark with an
7
+ * accessible label. Renders nothing below the `min` heading count (a short page
8
+ * needs no TOC). Styled from f06 tokens.
9
+ */
10
+ import type { ProseSection } from "@/lib/content/prose";
11
+ import { normalizeProse, withHeadingIds, tableOfContents } from "@/lib/content/prose";
12
+
13
+ interface Props {
14
+ sections: ProseSection[];
15
+ /** Only render when there are at least this many entries. Default 3. */
16
+ min?: number;
17
+ }
18
+ const { sections, min = 3 } = Astro.props;
19
+ const entries = tableOfContents(withHeadingIds(normalizeProse(sections)));
20
+ ---
21
+ {
22
+ entries.length >= min && (
23
+ <nav class="mt-5 flex flex-wrap gap-2" aria-label="On this page" data-widget="prose-toc">
24
+ {entries.map((e) => (
25
+ <a
26
+ href={`#${e.id}`}
27
+ class="inline-flex min-h-[var(--tap-target)] items-center rounded-[var(--radius-full)] border border-[var(--color-border)] bg-[var(--color-surface)] px-3 text-[0.82rem] font-semibold text-[var(--color-accent-ink)] no-underline hover:border-[var(--color-accent-ink)]"
28
+ >
29
+ {e.text}
30
+ </a>
31
+ ))}
32
+ </nav>
33
+ )
34
+ }
@@ -0,0 +1,44 @@
1
+ ---
2
+ /**
3
+ * <RichText> — renders an inline {@link RichText} (see lib/content/richtext.ts)
4
+ * as REAL DOM: text, <strong>/<em>, and safe <a>. Never uses `set:html`, so a
5
+ * paragraph/list-item/callout body carries typed runs instead of an HTML blob —
6
+ * closing the injection seam f09 + CSP push us to close.
7
+ *
8
+ * Accessibility lives here, not in tenant content: an external link gets
9
+ * rel="noopener noreferrer", target="_blank", and a visually-hidden
10
+ * "(opens in new tab)" so its behavior is announced. Link color/underline come
11
+ * from the f06 token contract via the inherited `.rte a` / token styles.
12
+ */
13
+ import type { RichText } from "@/lib/content/richtext";
14
+ import { toRuns } from "@/lib/content/richtext";
15
+
16
+ interface Props {
17
+ value: RichText;
18
+ }
19
+ const runs = toRuns(Astro.props.value);
20
+ ---
21
+ {
22
+ runs.map((run) =>
23
+ run.kind === "link" ? (
24
+ <a
25
+ href={run.href}
26
+ class="text-[var(--color-accent-ink)] underline underline-offset-[3px]"
27
+ {...(run.external
28
+ ? { rel: "noopener noreferrer", target: "_blank" }
29
+ : {})}
30
+ >
31
+ {run.text}
32
+ {run.external && <span class="sr-only"> (opens in new tab)</span>}
33
+ </a>
34
+ ) : run.strong && run.em ? (
35
+ <strong><em>{run.text}</em></strong>
36
+ ) : run.strong ? (
37
+ <strong>{run.text}</strong>
38
+ ) : run.em ? (
39
+ <em>{run.text}</em>
40
+ ) : (
41
+ <Fragment>{run.text}</Fragment>
42
+ ),
43
+ )
44
+ }
@@ -0,0 +1,46 @@
1
+ ---
2
+ /**
3
+ * <TrustStrip> — the quiet row of proof tokens a store repeats across pages
4
+ * ("Ships from USA", "Lab-tested", a rating). REAL-OR-ABSENT by construction
5
+ * (f09): sanitizeTrustStrip() drops any claim that is scraped review-widget
6
+ * markup and any rating that isn't a real positive aggregate, reusing f09's
7
+ * detector. If nothing real is left the strip renders NOTHING — never a
8
+ * fabricated badge. Plain text + one rating glyph; styled from f06 tokens.
9
+ * data-widget="trust-strip".
10
+ */
11
+ import type { TrustStripItem } from "@/lib/content/trustStrip";
12
+ import { sanitizeTrustStrip, ratingAriaLabel } from "@/lib/content/trustStrip";
13
+
14
+ interface Props {
15
+ items: TrustStripItem[];
16
+ }
17
+ const items = sanitizeTrustStrip(Astro.props.items);
18
+ ---
19
+ {
20
+ items.length > 0 && (
21
+ <div
22
+ class="border-y border-[var(--color-border)] bg-[var(--color-surface)]"
23
+ data-widget="trust-strip"
24
+ >
25
+ <ul class="container-prose flex flex-wrap items-center justify-center gap-x-6 gap-y-2 py-4 text-center font-mono text-[0.72rem] uppercase tracking-[0.1em] text-[var(--color-muted)]">
26
+ {items.map((item) =>
27
+ item.kind === "rating" ? (
28
+ <li
29
+ class="flex items-center gap-1.5 text-[var(--color-text)]"
30
+ data-trust="rating"
31
+ >
32
+ <span aria-hidden="true" class="text-[var(--color-warning)]">★</span>
33
+ <span aria-hidden="true">
34
+ {Math.round(item.average * 10) / 10}
35
+ {item.label ? ` ${item.label}` : ` (${item.count})`}
36
+ </span>
37
+ <span class="sr-only">{ratingAriaLabel(item)}</span>
38
+ </li>
39
+ ) : (
40
+ <li data-trust="claim">{item.label}</li>
41
+ ),
42
+ )}
43
+ </ul>
44
+ </div>
45
+ )
46
+ }
@@ -107,10 +107,24 @@ const hasImage = Boolean(heroImg || (prodImg && featureProduct));
107
107
  light ? "ring-[var(--color-border)]" : "ring-white/10",
108
108
  ]}
109
109
  >
110
+ {/*
111
+ heroImg is a bare merchant URL (HeroBlock.image string) — no
112
+ intrinsic dims and no `rehosted` flag, so it can't go through <Img>
113
+ (that would force a fixed aspect-ratio crop, changing appearance).
114
+ Instead give it the LCP hints the product branch's <Img priority>
115
+ gets — fetchpriority/decoding/eager — plus width/height so the
116
+ browser reserves space (CLS). The `auto` aspect-ratio semantics of
117
+ width/height attributes mean the final render still uses the
118
+ image's natural ratio; the dims below only size the pre-load box.
119
+ */}
110
120
  <img
111
121
  src={heroImg}
112
122
  alt={block.imageAlt ?? block.headline}
123
+ width={1600}
124
+ height={1000}
113
125
  loading="eager"
126
+ fetchpriority="high"
127
+ decoding="sync"
114
128
  class="h-full w-full object-cover"
115
129
  />
116
130
  </div>