@tokenoftrust/storefront-runner 1.3.2

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 (310) hide show
  1. package/LICENSE +58 -0
  2. package/README.md +42 -0
  3. package/apps/storefront/astro.config.mjs +66 -0
  4. package/apps/storefront/dev-plugins/dev-loop-state.mjs +147 -0
  5. package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +238 -0
  6. package/apps/storefront/env.d.ts +162 -0
  7. package/apps/storefront/migrations/0000_baseline.sql +54 -0
  8. package/apps/storefront/migrations/0001_products_fts.sql +9 -0
  9. package/apps/storefront/migrations/README.md +54 -0
  10. package/apps/storefront/migrations/meta/0000_snapshot.json +347 -0
  11. package/apps/storefront/migrations/meta/0001_snapshot.json +347 -0
  12. package/apps/storefront/migrations/meta/_journal.json +20 -0
  13. package/apps/storefront/package.json +42 -0
  14. package/apps/storefront/public/brand/northwind-logo-dark.svg +4 -0
  15. package/apps/storefront/public/brand/northwind-logo-light.svg +4 -0
  16. package/apps/storefront/public/brand/northwind-og.svg +13 -0
  17. package/apps/storefront/public/favicon.svg +5 -0
  18. package/apps/storefront/public/js/dashboard-team.js +147 -0
  19. package/apps/storefront/public/js/dashboard-vendor-search.js +115 -0
  20. package/apps/storefront/public/shared/commerce-chrome.css +369 -0
  21. package/apps/storefront/src/components/Badge.astro +34 -0
  22. package/apps/storefront/src/components/Breadcrumbs.astro +45 -0
  23. package/apps/storefront/src/components/CollectionCard.astro +56 -0
  24. package/apps/storefront/src/components/EmptyState.astro +55 -0
  25. package/apps/storefront/src/components/Img.astro +68 -0
  26. package/apps/storefront/src/components/ProductCard.astro +228 -0
  27. package/apps/storefront/src/components/Section.astro +39 -0
  28. package/apps/storefront/src/components/Seo.astro +70 -0
  29. package/apps/storefront/src/components/WidgetFrame.astro +34 -0
  30. package/apps/storefront/src/components/admin/AdminAIWorkflowsTab.astro +531 -0
  31. package/apps/storefront/src/components/admin/AdminCustomersTab.astro +597 -0
  32. package/apps/storefront/src/components/admin/AdminFoxyCartSettingsPanel.astro +227 -0
  33. package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +465 -0
  34. package/apps/storefront/src/components/admin/AdminProductsTab.astro +901 -0
  35. package/apps/storefront/src/components/admin/AdminReportingTab.astro +435 -0
  36. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +782 -0
  37. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +1223 -0
  38. package/apps/storefront/src/components/auth/AuthBadge.astro +44 -0
  39. package/apps/storefront/src/components/chrome/AnnouncementBar.astro +108 -0
  40. package/apps/storefront/src/components/chrome/MegaMenu.astro +26 -0
  41. package/apps/storefront/src/components/chrome/NavDropdown.astro +201 -0
  42. package/apps/storefront/src/components/chrome/SiteFooter.astro +201 -0
  43. package/apps/storefront/src/components/chrome/SiteHeader.astro +234 -0
  44. package/apps/storefront/src/components/chrome/UtilityNav.astro +79 -0
  45. package/apps/storefront/src/components/commerce/ArticleCard.astro +242 -0
  46. package/apps/storefront/src/components/commerce/BrandGrid.astro +175 -0
  47. package/apps/storefront/src/components/commerce/FoxyLoader.astro +23 -0
  48. package/apps/storefront/src/components/commerce/FoxySidecartStyles.astro +61 -0
  49. package/apps/storefront/src/components/commerce/PriceDisplay.astro +91 -0
  50. package/apps/storefront/src/components/commerce/ProductCarousel.astro +197 -0
  51. package/apps/storefront/src/components/commerce/ProductGrid.astro +75 -0
  52. package/apps/storefront/src/components/commerce/RatingStars.astro +58 -0
  53. package/apps/storefront/src/components/commerce/ReviewList.astro +54 -0
  54. package/apps/storefront/src/components/commerce/ReviewSummary.astro +44 -0
  55. package/apps/storefront/src/components/commerce/TagFilterCloud.astro +149 -0
  56. package/apps/storefront/src/components/compliance/AdultSignatureNotice.astro +37 -0
  57. package/apps/storefront/src/components/compliance/ComplianceNotice.astro +99 -0
  58. package/apps/storefront/src/components/compliance/ExciseTaxNotice.astro +45 -0
  59. package/apps/storefront/src/components/compliance/NicotineWarning.astro +33 -0
  60. package/apps/storefront/src/components/compliance/PactActNotice.astro +35 -0
  61. package/apps/storefront/src/components/compliance/Prop65Warning.astro +80 -0
  62. package/apps/storefront/src/components/compliance/PurchaseLimitNotice.astro +38 -0
  63. package/apps/storefront/src/components/compliance/ShippingRestrictionNotice.astro +35 -0
  64. package/apps/storefront/src/components/compliance/StateEligibilityNotice.astro +53 -0
  65. package/apps/storefront/src/components/home/Hero.astro +164 -0
  66. package/apps/storefront/src/components/islands/AgeGate.tsx +109 -0
  67. package/apps/storefront/src/components/islands/ImageGallery.tsx +237 -0
  68. package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +73 -0
  69. package/apps/storefront/src/components/islands/MobileNav.tsx +239 -0
  70. package/apps/storefront/src/components/islands/QuickView.tsx +138 -0
  71. package/apps/storefront/src/components/islands/RecentlyViewed.tsx +122 -0
  72. package/apps/storefront/src/components/islands/SavedList.tsx +99 -0
  73. package/apps/storefront/src/components/islands/SearchTypeahead.tsx +248 -0
  74. package/apps/storefront/src/components/islands/VariantSelector.tsx +375 -0
  75. package/apps/storefront/src/components/marketing/CardGrid.astro +72 -0
  76. package/apps/storefront/src/components/marketing/CodeSample.astro +20 -0
  77. package/apps/storefront/src/components/marketing/CtaBand.astro +24 -0
  78. package/apps/storefront/src/components/marketing/Faq.astro +37 -0
  79. package/apps/storefront/src/components/marketing/Integrations.astro +63 -0
  80. package/apps/storefront/src/components/marketing/MarketingBlocks.astro +53 -0
  81. package/apps/storefront/src/components/marketing/MarketingCtas.astro +48 -0
  82. package/apps/storefront/src/components/marketing/MarketingHero.astro +72 -0
  83. package/apps/storefront/src/components/marketing/ProofStrip.astro +37 -0
  84. package/apps/storefront/src/components/marketing/SplitCompare.astro +53 -0
  85. package/apps/storefront/src/components/marketing/Steps.astro +36 -0
  86. package/apps/storefront/src/components/marketing/Testimonials.astro +38 -0
  87. package/apps/storefront/src/components/marketing/TrustBar.astro +22 -0
  88. package/apps/storefront/src/components/membership/IllustrativeLabel.astro +56 -0
  89. package/apps/storefront/src/components/membership/TierCards.astro +164 -0
  90. package/apps/storefront/src/components/membership/TierComparisonTable.astro +148 -0
  91. package/apps/storefront/src/components/plp/ActiveFilters.astro +79 -0
  92. package/apps/storefront/src/components/plp/FacetSidebar.astro +199 -0
  93. package/apps/storefront/src/components/plp/Pagination.astro +99 -0
  94. package/apps/storefront/src/components/plp/SortSelect.astro +64 -0
  95. package/apps/storefront/src/components/style-guide/StyleGuideNav.astro +95 -0
  96. package/apps/storefront/src/components/subscription/HowItWorksSteps.astro +67 -0
  97. package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +67 -0
  98. package/apps/storefront/src/components/subscription/SavingsExplainer.astro +101 -0
  99. package/apps/storefront/src/config/adminTargetTenant.ts +28 -0
  100. package/apps/storefront/src/config/devTenantSeed.ts +78 -0
  101. package/apps/storefront/src/config/devTenants.ts +16 -0
  102. package/apps/storefront/src/config/kvTenantResolver.ts +87 -0
  103. package/apps/storefront/src/config/resolver.ts +102 -0
  104. package/apps/storefront/src/config/tenants.ts +87 -0
  105. package/apps/storefront/src/layouts/Layout.astro +368 -0
  106. package/apps/storefront/src/lib/admin/README.md +122 -0
  107. package/apps/storefront/src/lib/admin/ai/approvalMode.ts +21 -0
  108. package/apps/storefront/src/lib/admin/ai/askEnvelope.ts +48 -0
  109. package/apps/storefront/src/lib/admin/ai/audit.ts +14 -0
  110. package/apps/storefront/src/lib/admin/ai/index.ts +6 -0
  111. package/apps/storefront/src/lib/admin/ai/operationalRuns.ts +155 -0
  112. package/apps/storefront/src/lib/admin/ai/recommendation.ts +20 -0
  113. package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +172 -0
  114. package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +65 -0
  115. package/apps/storefront/src/lib/admin/envelope.ts +10 -0
  116. package/apps/storefront/src/lib/admin/evidence/evidenceReference.ts +32 -0
  117. package/apps/storefront/src/lib/admin/memoizeByEnvelope.ts +27 -0
  118. package/apps/storefront/src/lib/admin/orderClient.ts +146 -0
  119. package/apps/storefront/src/lib/admin/seed.ts +54 -0
  120. package/apps/storefront/src/lib/admin/services/mock/aiWorkflows.ts +59 -0
  121. package/apps/storefront/src/lib/admin/services/mock/customers.ts +86 -0
  122. package/apps/storefront/src/lib/admin/services/mock/evidence.ts +99 -0
  123. package/apps/storefront/src/lib/admin/services/mock/foxyCartSettings.ts +78 -0
  124. package/apps/storefront/src/lib/admin/services/mock/fulfillment.ts +62 -0
  125. package/apps/storefront/src/lib/admin/services/mock/index.ts +12 -0
  126. package/apps/storefront/src/lib/admin/services/mock/orders.ts +184 -0
  127. package/apps/storefront/src/lib/admin/services/mock/products.ts +105 -0
  128. package/apps/storefront/src/lib/admin/services/mock/reporting.ts +51 -0
  129. package/apps/storefront/src/lib/admin/services/mock/settings.ts +70 -0
  130. package/apps/storefront/src/lib/admin/services/mock/tenants.ts +29 -0
  131. package/apps/storefront/src/lib/admin/services/orders.ts +300 -0
  132. package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +67 -0
  133. package/apps/storefront/src/lib/admin/viewmodels/customers.ts +56 -0
  134. package/apps/storefront/src/lib/admin/viewmodels/fulfillment.ts +49 -0
  135. package/apps/storefront/src/lib/admin/viewmodels/orders.ts +92 -0
  136. package/apps/storefront/src/lib/admin/viewmodels/products.ts +74 -0
  137. package/apps/storefront/src/lib/admin/viewmodels/reporting.ts +34 -0
  138. package/apps/storefront/src/lib/admin/viewmodels/settings.ts +64 -0
  139. package/apps/storefront/src/lib/adminTenantEnvelope.ts +90 -0
  140. package/apps/storefront/src/lib/analytics/index.ts +47 -0
  141. package/apps/storefront/src/lib/auth/brokerAssertion.ts +136 -0
  142. package/apps/storefront/src/lib/auth/devAuth.ts +90 -0
  143. package/apps/storefront/src/lib/auth/gatePage.ts +81 -0
  144. package/apps/storefront/src/lib/auth/identityToken.ts +341 -0
  145. package/apps/storefront/src/lib/auth/loginGate.ts +119 -0
  146. package/apps/storefront/src/lib/auth/route.ts +146 -0
  147. package/apps/storefront/src/lib/auth/session.ts +203 -0
  148. package/apps/storefront/src/lib/auth/totAccessClient.ts +503 -0
  149. package/apps/storefront/src/lib/basePath.ts +41 -0
  150. package/apps/storefront/src/lib/cfImage.ts +55 -0
  151. package/apps/storefront/src/lib/chrome/model.ts +244 -0
  152. package/apps/storefront/src/lib/chrome/parseConfig.ts +34 -0
  153. package/apps/storefront/src/lib/colors.ts +97 -0
  154. package/apps/storefront/src/lib/commerce/foxyCartSettings.ts +253 -0
  155. package/apps/storefront/src/lib/content-edit/applyDiff.ts +91 -0
  156. package/apps/storefront/src/lib/content-edit/client.ts +230 -0
  157. package/apps/storefront/src/lib/content-edit/index.ts +14 -0
  158. package/apps/storefront/src/lib/content-edit/route.ts +25 -0
  159. package/apps/storefront/src/lib/content-edit/types.ts +19 -0
  160. package/apps/storefront/src/lib/cspStaticHashes.ts +24 -0
  161. package/apps/storefront/src/lib/d1/catalog.ts +366 -0
  162. package/apps/storefront/src/lib/dashboard/staffAdmission.ts +43 -0
  163. package/apps/storefront/src/lib/dashboard/staffVendorAccess.ts +449 -0
  164. package/apps/storefront/src/lib/dashboard/tenantSelection.ts +136 -0
  165. package/apps/storefront/src/lib/dashboard/vendorQuery.ts +61 -0
  166. package/apps/storefront/src/lib/demoGate.ts +113 -0
  167. package/apps/storefront/src/lib/dev/activityIngestToken.ts +78 -0
  168. package/apps/storefront/src/lib/dev/activityStore.ts +72 -0
  169. package/apps/storefront/src/lib/dev/ai-widget-client.js +308 -0
  170. package/apps/storefront/src/lib/dev/apiBase.ts +14 -0
  171. package/apps/storefront/src/lib/dev/cliSignInCode.ts +155 -0
  172. package/apps/storefront/src/lib/dev/devDraft.ts +133 -0
  173. package/apps/storefront/src/lib/dev/hostedCockpit.ts +169 -0
  174. package/apps/storefront/src/lib/dev/livePreview.ts +77 -0
  175. package/apps/storefront/src/lib/dev/runtimeStore.ts +120 -0
  176. package/apps/storefront/src/lib/dev/storeStatus.ts +142 -0
  177. package/apps/storefront/src/lib/diag/redact.ts +20 -0
  178. package/apps/storefront/src/lib/edgeCache.ts +128 -0
  179. package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +62 -0
  180. package/apps/storefront/src/lib/env.ts +60 -0
  181. package/apps/storefront/src/lib/fixtures/sampleProducts.ts +130 -0
  182. package/apps/storefront/src/lib/format.ts +38 -0
  183. package/apps/storefront/src/lib/foxyCommerce.ts +49 -0
  184. package/apps/storefront/src/lib/imageAttrs.ts +55 -0
  185. package/apps/storefront/src/lib/jsonld.ts +166 -0
  186. package/apps/storefront/src/lib/maintenance.ts +71 -0
  187. package/apps/storefront/src/lib/membership/model.ts +97 -0
  188. package/apps/storefront/src/lib/memoryKv.ts +32 -0
  189. package/apps/storefront/src/lib/messaging/messagesClient.ts +143 -0
  190. package/apps/storefront/src/lib/placeholder.ts +99 -0
  191. package/apps/storefront/src/lib/previewReload.ts +125 -0
  192. package/apps/storefront/src/lib/priceBuckets.ts +33 -0
  193. package/apps/storefront/src/lib/rawChrome.ts +505 -0
  194. package/apps/storefront/src/lib/rawMarketingHtml.ts +136 -0
  195. package/apps/storefront/src/lib/search/index.ts +74 -0
  196. package/apps/storefront/src/lib/sections.ts +129 -0
  197. package/apps/storefront/src/lib/securityHeaders.ts +31 -0
  198. package/apps/storefront/src/lib/storyblok/content-model.ts +289 -0
  199. package/apps/storefront/src/lib/storyblok/provider.ts +607 -0
  200. package/apps/storefront/src/lib/subscription/model.ts +68 -0
  201. package/apps/storefront/src/lib/tenantRouting.ts +91 -0
  202. package/apps/storefront/src/lib/tot/ToTClient.ts +177 -0
  203. package/apps/storefront/src/lib/tot/d1Client.ts +62 -0
  204. package/apps/storefront/src/lib/tot/query.ts +249 -0
  205. package/apps/storefront/src/lib/tot/totClientInterface.ts +30 -0
  206. package/apps/storefront/src/lib/url.ts +142 -0
  207. package/apps/storefront/src/lib/wishlist.ts +63 -0
  208. package/apps/storefront/src/middleware/index.ts +455 -0
  209. package/apps/storefront/src/pages/404.astro +61 -0
  210. package/apps/storefront/src/pages/[...slug].astro +90 -0
  211. package/apps/storefront/src/pages/admin.astro +1675 -0
  212. package/apps/storefront/src/pages/api/admin/orders/[id].ts +53 -0
  213. package/apps/storefront/src/pages/api/admin/orders.ts +71 -0
  214. package/apps/storefront/src/pages/api/auth/logout.ts +21 -0
  215. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +107 -0
  216. package/apps/storefront/src/pages/api/auth/send.ts +85 -0
  217. package/apps/storefront/src/pages/api/auth/verify.ts +135 -0
  218. package/apps/storefront/src/pages/api/cache-purge.ts +44 -0
  219. package/apps/storefront/src/pages/api/checkout-style.css.ts +43 -0
  220. package/apps/storefront/src/pages/api/cli/policy.ts +70 -0
  221. package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +164 -0
  222. package/apps/storefront/src/pages/api/csp-report.ts +25 -0
  223. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +124 -0
  224. package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +120 -0
  225. package/apps/storefront/src/pages/api/dev/activity.ts +124 -0
  226. package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +73 -0
  227. package/apps/storefront/src/pages/api/dev/diff.ts +68 -0
  228. package/apps/storefront/src/pages/api/dev/file.ts +57 -0
  229. package/apps/storefront/src/pages/api/dev/locate.ts +125 -0
  230. package/apps/storefront/src/pages/api/dev/publish.ts +128 -0
  231. package/apps/storefront/src/pages/api/dev/tree.ts +70 -0
  232. package/apps/storefront/src/pages/api/dev/write.ts +116 -0
  233. package/apps/storefront/src/pages/api/newsletter.ts +104 -0
  234. package/apps/storefront/src/pages/api/request-access.ts +172 -0
  235. package/apps/storefront/src/pages/auth/login.astro +149 -0
  236. package/apps/storefront/src/pages/auth/magic.astro +105 -0
  237. package/apps/storefront/src/pages/capabilities.astro +292 -0
  238. package/apps/storefront/src/pages/collections/[handle].astro +177 -0
  239. package/apps/storefront/src/pages/collections/index.astro +59 -0
  240. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +100 -0
  241. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +154 -0
  242. package/apps/storefront/src/pages/dashboard/index.astro +160 -0
  243. package/apps/storefront/src/pages/index.astro +290 -0
  244. package/apps/storefront/src/pages/llms.txt.ts +92 -0
  245. package/apps/storefront/src/pages/products/[handle].astro +402 -0
  246. package/apps/storefront/src/pages/robots.txt.ts +24 -0
  247. package/apps/storefront/src/pages/saved.astro +35 -0
  248. package/apps/storefront/src/pages/search.astro +150 -0
  249. package/apps/storefront/src/pages/sitemap.xml.ts +78 -0
  250. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +1280 -0
  251. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +229 -0
  252. package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +144 -0
  253. package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +116 -0
  254. package/apps/storefront/src/pages/style-guide/index.astro +98 -0
  255. package/apps/storefront/src/pages/tenants/[id]/[...path].ts +57 -0
  256. package/apps/storefront/src/styles/global.css +292 -0
  257. package/apps/storefront/src/themes/reference.ts +68 -0
  258. package/apps/storefront/src/themes/schema.ts +111 -0
  259. package/apps/storefront/src/themes/tenants/index.ts +51 -0
  260. package/apps/storefront/tsconfig.json +16 -0
  261. package/apps/storefront/vitest.config.ts +22 -0
  262. package/package.json +26 -0
  263. package/packages/public-runtime/LICENSE +58 -0
  264. package/packages/public-runtime/package.json +22 -0
  265. package/packages/public-runtime/src/binary-path.ts +68 -0
  266. package/packages/public-runtime/src/capabilities.ts +89 -0
  267. package/packages/public-runtime/src/catalog-api.ts +81 -0
  268. package/packages/public-runtime/src/catalog-d1.ts +192 -0
  269. package/packages/public-runtime/src/checkout-style-publish.ts +200 -0
  270. package/packages/public-runtime/src/checkout-style.ts +375 -0
  271. package/packages/public-runtime/src/csp.ts +109 -0
  272. package/packages/public-runtime/src/customization-preview.ts +197 -0
  273. package/packages/public-runtime/src/customization-reconcile.ts +63 -0
  274. package/packages/public-runtime/src/customization-runtime.ts +157 -0
  275. package/packages/public-runtime/src/customization-scripts.ts +64 -0
  276. package/packages/public-runtime/src/customization-versioning.ts +95 -0
  277. package/packages/public-runtime/src/foxy-orders.ts +136 -0
  278. package/packages/public-runtime/src/foxy.ts +196 -0
  279. package/packages/public-runtime/src/hot-standby-readiness.ts +140 -0
  280. package/packages/public-runtime/src/index.ts +34 -0
  281. package/packages/public-runtime/src/product.ts +159 -0
  282. package/packages/public-runtime/src/search.ts +45 -0
  283. package/packages/public-runtime/src/tenant-assets.ts +444 -0
  284. package/packages/public-runtime/src/tenant.ts +384 -0
  285. package/packages/public-runtime/tsconfig.json +8 -0
  286. package/packages/public-runtime/vitest.config.ts +8 -0
  287. package/pnpm-workspace.yaml +7 -0
  288. package/scripts/build/copy-tenant-assets.mjs +69 -0
  289. package/scripts/dev/ai-edit.mjs +183 -0
  290. package/scripts/dev/checkout-watch.mjs +116 -0
  291. package/scripts/dev/file-browser.mjs +159 -0
  292. package/scripts/dev/file-writer.mjs +141 -0
  293. package/scripts/dev/git-status.mjs +171 -0
  294. package/scripts/dev/locate-handler.mjs +163 -0
  295. package/scripts/dev/locate.mjs +112 -0
  296. package/scripts/dev/port-check.mjs +48 -0
  297. package/scripts/dev/safe-path.mjs +109 -0
  298. package/scripts/dev/shot.mjs +46 -0
  299. package/scripts/dev/tenant-source-root.mjs +76 -0
  300. package/scripts/dev/unified-diff.mjs +81 -0
  301. package/scripts/tot-dev.mjs +376 -0
  302. package/tenants/README.md +14 -0
  303. package/tenants/home/content/chrome.json +7 -0
  304. package/tenants/home/content/home.html +58 -0
  305. package/tenants/home/content/home.json +11 -0
  306. package/tenants/home/public/js/storefront.js +210 -0
  307. package/tenants/home/public/logo-wordmark.png +0 -0
  308. package/tenants/home/public/pages/storefront.css +212 -0
  309. package/tenants/home/theme.json +6 -0
  310. package/tsconfig.base.json +20 -0
@@ -0,0 +1,55 @@
1
+ ---
2
+ /**
3
+ * <EmptyState> — intentional empty result (search/PLP). Copy is passed in by
4
+ * the parent (kept in the tenant's editorial voice).
5
+ */
6
+ import { withBase } from "@/lib/basePath";
7
+
8
+ interface Props {
9
+ title: string;
10
+ body?: string;
11
+ ctaLabel?: string;
12
+ ctaHref?: string;
13
+ /** Extra, lower-emphasis recovery links (e.g. "clear just the price range"),
14
+ * shown under the primary CTA. Optional — most callers have only one CTA. */
15
+ secondaryActions?: { label: string; href: string }[];
16
+ }
17
+
18
+ const { title, body, ctaLabel, ctaHref, secondaryActions } = Astro.props;
19
+ const { basePath } = Astro.locals;
20
+ ---
21
+
22
+ <div
23
+ class="mx-auto flex max-w-md flex-col items-center px-6 py-20 text-center sm:py-28"
24
+ data-reveal
25
+ >
26
+ <h2 class="display-md">{title}</h2>
27
+ {body && <p class="mt-3 max-w-prose text-muted">{body}</p>}
28
+ {
29
+ ctaLabel && ctaHref && (
30
+ <a
31
+ href={withBase(basePath, ctaHref)}
32
+ class="mt-7 inline-flex items-center rounded-full bg-primary px-5 py-2 font-display text-sm font-medium text-primary-contrast transition-opacity hover:opacity-90"
33
+ >
34
+ {ctaLabel}
35
+ </a>
36
+ )
37
+ }
38
+ {
39
+ secondaryActions && secondaryActions.length > 0 && (
40
+ <ul class="mt-4 flex flex-wrap items-center justify-center gap-x-4 gap-y-1.5">
41
+ {secondaryActions.map((action) => (
42
+ <li>
43
+ <a
44
+ href={withBase(basePath, action.href)}
45
+ rel="nofollow"
46
+ class="text-sm text-muted underline underline-offset-2 hover:text-[var(--color-accent-ink)]"
47
+ >
48
+ {action.label}
49
+ </a>
50
+ </li>
51
+ ))}
52
+ </ul>
53
+ )
54
+ }
55
+ </div>
@@ -0,0 +1,68 @@
1
+ ---
2
+ /**
3
+ * <Img> — CLS-free, theme-agnostic image with offline placeholder fallback.
4
+ *
5
+ * Fixture image URLs are dead Shopify-CDN paths (brief: images note). When a
6
+ * URL is unusable, we render a deterministic, aspect-correct gradient SVG keyed
7
+ * by a stable seed so the site looks polished offline AND stays CLS-free
8
+ * (explicit width/height always set). When real rehosted URLs land
9
+ * (CatalogImage.rehosted === true), the same component serves them with
10
+ * responsive srcset/sizes.
11
+ *
12
+ * Responsive srcset is emitted for any URL whose CDN accepts `?width=`: the
13
+ * rehost target AND Shopify's image CDN (so referenced Shopify URLs from a live
14
+ * migration are responsive without rehosting).
15
+ */
16
+ import { resolveImage } from "@/lib/imageAttrs";
17
+
18
+ interface Props {
19
+ src?: string;
20
+ alt: string;
21
+ width: number;
22
+ height: number;
23
+ /** Seed for the placeholder (image id or product handle). */
24
+ seed: string;
25
+ /** Short label drawn into the placeholder (e.g. product title). */
26
+ label?: string;
27
+ rehosted?: boolean;
28
+ /** LCP hint: eager + high priority + no lazy. */
29
+ priority?: boolean;
30
+ sizes?: string;
31
+ class?: string;
32
+ loading?: "eager" | "lazy";
33
+ }
34
+
35
+ const {
36
+ src,
37
+ alt,
38
+ width,
39
+ height,
40
+ seed,
41
+ label,
42
+ rehosted = false,
43
+ priority = false,
44
+ sizes,
45
+ class: className,
46
+ loading,
47
+ } = Astro.props;
48
+
49
+ // Render decision (placeholder / Cloudflare resizing / Shopify ?width=) lives in
50
+ // lib/imageAttrs.ts so it's unit-testable without rendering this component (D4).
51
+ const { src: resolvedSrc, srcset } = resolveImage({ src, rehosted, width, height, seed, label });
52
+
53
+ const loadingAttr = loading ?? (priority ? "eager" : "lazy");
54
+ ---
55
+
56
+ <img
57
+ src={resolvedSrc}
58
+ srcset={srcset}
59
+ sizes={srcset ? (sizes ?? "(min-width: 768px) 50vw, 100vw") : undefined}
60
+ alt={alt}
61
+ width={width}
62
+ height={height}
63
+ loading={loadingAttr}
64
+ decoding={priority ? "sync" : "async"}
65
+ fetchpriority={priority ? "high" : "auto"}
66
+ class={className}
67
+ style={`aspect-ratio:${width}/${height};background:var(--color-border)`}
68
+ />
@@ -0,0 +1,228 @@
1
+ ---
2
+ /**
3
+ * <ProductCard> — PLP/grid card. Uses the stretched-link pattern (the title is
4
+ * the link; an ::after overlay makes the whole card clickable) so interactive
5
+ * controls — wishlist heart, quick-view — can sit ABOVE the link without nesting
6
+ * buttons in an anchor. Image hover-zoom + optional second image; status badges;
7
+ * color swatches; price with struck compare-at. Theme-agnostic.
8
+ */
9
+ import type { CatalogProduct } from "@tot/public-runtime";
10
+ import { discountPercent, truncate } from "@/lib/format";
11
+ import { isOnSale, isNew, isAvailable, fromPrice } from "@/lib/tot/query";
12
+ import { colorOptionValues, resolveSwatch, isLightFill } from "@/lib/colors";
13
+ import Img from "@/components/Img.astro";
14
+ import Badge from "@/components/Badge.astro";
15
+ import PriceDisplay from "@/components/commerce/PriceDisplay.astro";
16
+ import RatingStars from "@/components/commerce/RatingStars.astro";
17
+ import { withBase } from "@/lib/basePath";
18
+
19
+ interface Props {
20
+ product: CatalogProduct;
21
+ position?: number;
22
+ list?: string;
23
+ priority?: boolean;
24
+ /** Optional aggregate rating (0–5) — renders <RatingStars> when present. */
25
+ rating?: number;
26
+ /** Optional review count shown with the stars. */
27
+ reviewCount?: number;
28
+ }
29
+
30
+ const { product, priority = false, rating, reviewCount } = Astro.props;
31
+ const { tenant, basePath } = Astro.locals;
32
+
33
+ // A compliance-verified item (age/lab/regulatory) earns a "Compliant" badge —
34
+ // tag-driven so it stays tenant-agnostic (no per-merchant branch).
35
+ const isCompliant = product.tags.some((t) =>
36
+ ["compliant", "lab-tested", "age-verified"].includes(t),
37
+ );
38
+
39
+ const primary = product.images[0];
40
+ const secondary = product.images[1];
41
+
42
+ const onSale = isOnSale(product);
43
+ const available = isAvailable(product);
44
+ const isNewProduct = !onSale && isNew(product);
45
+
46
+ const saleVariant = product.variants
47
+ .filter((v) => v.compare_at_price != null && v.compare_at_price > v.price)
48
+ .sort(
49
+ (a, b) =>
50
+ discountPercent(b.price, b.compare_at_price)! -
51
+ discountPercent(a.price, a.compare_at_price)!,
52
+ )[0];
53
+ const salePercent = saleVariant
54
+ ? discountPercent(saleVariant.price, saleVariant.compare_at_price)
55
+ : null;
56
+
57
+ const maxCompareAt = onSale
58
+ ? Math.max(
59
+ ...product.variants
60
+ .filter((v) => v.compare_at_price != null)
61
+ .map((v) => v.compare_at_price!),
62
+ )
63
+ : null;
64
+
65
+ const price = fromPrice(product);
66
+ const currency = product.price_range.currency;
67
+ const locale = tenant.locale;
68
+
69
+ const colors = colorOptionValues(product.options);
70
+ const MAX_SWATCHES = 5;
71
+ const swatches = colors.slice(0, MAX_SWATCHES).map(resolveSwatch);
72
+ const extraColors = Math.max(0, colors.length - MAX_SWATCHES);
73
+
74
+ // Compact snapshot for the wishlist heart + recently-viewed parity.
75
+ const snapshot = {
76
+ handle: product.handle,
77
+ title: product.title,
78
+ price,
79
+ currency,
80
+ vendor: product.vendor,
81
+ img: primary?.url,
82
+ rehosted: primary?.rehosted,
83
+ seed: primary?.id ?? product.handle,
84
+ };
85
+
86
+ // Richer payload for the quick-view dialog (no variant picker — that's the PDP).
87
+ const quickview = {
88
+ ...snapshot,
89
+ images: product.images.slice(0, 4).map((im) => ({
90
+ url: im.url,
91
+ rehosted: im.rehosted,
92
+ seed: im.id ?? product.handle,
93
+ })),
94
+ desc: truncate(product.description_text, 180),
95
+ colors,
96
+ onSale,
97
+ available,
98
+ maxCompareAt,
99
+ sku: product.variants[0]?.sku ?? null,
100
+ };
101
+ ---
102
+
103
+ <div class="group relative" data-reveal>
104
+ <div class="relative aspect-[4/5] overflow-hidden rounded-md bg-border ring-1 ring-[var(--color-border)] transition-shadow duration-300 motion-safe:group-hover:shadow-[var(--shadow-md)]">
105
+ {
106
+ primary ? (
107
+ <Img
108
+ src={primary.url}
109
+ rehosted={primary.rehosted}
110
+ seed={primary.id ?? product.handle}
111
+ label={product.title}
112
+ alt={primary.alt ?? product.title}
113
+ width={primary.width ?? 800}
114
+ height={primary.height ?? 1000}
115
+ sizes="(min-width:1024px) 25vw, (min-width:640px) 50vw, 100vw"
116
+ priority={priority}
117
+ class="h-full w-full object-cover transition-transform duration-500 motion-safe:group-hover:scale-[1.03]"
118
+ />
119
+ ) : (
120
+ <Img
121
+ seed={product.handle}
122
+ label={product.title}
123
+ alt={product.title}
124
+ width={800}
125
+ height={1000}
126
+ sizes="(min-width:1024px) 25vw, (min-width:640px) 50vw, 100vw"
127
+ priority={priority}
128
+ class="h-full w-full object-cover"
129
+ />
130
+ )
131
+ }
132
+ {
133
+ secondary && (
134
+ <Img
135
+ src={secondary.url}
136
+ rehosted={secondary.rehosted}
137
+ seed={secondary.id ?? `${product.handle}-2`}
138
+ label={product.title}
139
+ alt=""
140
+ width={secondary.width ?? 800}
141
+ height={secondary.height ?? 1000}
142
+ sizes="(min-width:1024px) 25vw, (min-width:640px) 50vw, 100vw"
143
+ class="absolute inset-0 h-full w-full object-cover opacity-0 transition-opacity duration-500 motion-safe:group-hover:opacity-100"
144
+ />
145
+ )
146
+ }
147
+
148
+ {/* Status badges */}
149
+ <div class="pointer-events-none absolute left-2 top-2 z-10 flex flex-col items-start gap-1">
150
+ {onSale && <Badge variant="sale" label={salePercent ? `-${salePercent}%` : "Sale"} />}
151
+ {isNewProduct && <Badge variant="new" label="New" />}
152
+ {isCompliant && <Badge variant="compliant" label="Compliant" />}
153
+ {!available && <Badge variant="soldout" label="Sold out" />}
154
+ </div>
155
+
156
+ {/* Wishlist heart — above the stretched link (z-10). State hydrated by the
157
+ global wishlist script; aria-pressed reflects saved state. */}
158
+ <button
159
+ type="button"
160
+ class="wishlist-toggle absolute right-2 top-2 z-10 grid h-9 w-9 place-items-center rounded-full bg-[color-mix(in_srgb,var(--color-surface)_88%,transparent)] text-[var(--color-text)] backdrop-blur transition-colors hover:text-[var(--color-sale)] focus-visible:text-[var(--color-sale)] aria-pressed:text-[var(--color-sale)]"
161
+ aria-pressed="false"
162
+ aria-label={`Save ${product.title}`}
163
+ data-wishlist={JSON.stringify(snapshot)}
164
+ >
165
+ <svg class="wishlist-heart h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true">
166
+ <path d="M12 21s-7.5-4.6-10-9.2C.4 8.6 2 5 5.5 5 8 5 9.4 6.6 12 9c2.6-2.4 4-4 6.5-4C22 5 23.6 8.6 22 11.8 19.5 16.4 12 21 12 21z" />
167
+ </svg>
168
+ </button>
169
+
170
+ {/* Quick view — reveals on hover/focus-within; opens the global dialog. */}
171
+ <button
172
+ type="button"
173
+ class="quickview-trigger absolute inset-x-2 bottom-2 z-10 rounded-[var(--radius-sm)] bg-[color-mix(in_srgb,var(--color-surface)_92%,transparent)] py-2 text-center font-display text-xs font-medium text-[var(--color-text)] opacity-0 backdrop-blur transition-opacity duration-200 hover:bg-[var(--color-surface)] focus-visible:opacity-100 motion-safe:group-hover:opacity-100 md:opacity-0"
174
+ data-quickview={JSON.stringify(quickview)}
175
+ >
176
+ Quick view
177
+ </button>
178
+ </div>
179
+
180
+ <div class="mt-3.5">
181
+ {
182
+ product.vendor && (
183
+ <p class="font-mono text-[0.62rem] uppercase tracking-[0.14em] text-muted">
184
+ {product.vendor}
185
+ </p>
186
+ )
187
+ }
188
+ <h3 class="mt-1 font-display text-[0.95rem] font-medium leading-snug">
189
+ <a
190
+ href={withBase(basePath, `/products/${product.handle}`)}
191
+ class="after:absolute after:inset-0 after:z-0 transition-colors motion-safe:group-hover:text-primary"
192
+ >
193
+ {product.title}
194
+ </a>
195
+ </h3>
196
+ <PriceDisplay product={product} locale={locale} class="mt-1.5" />
197
+
198
+ {rating != null && <RatingStars rating={rating} count={reviewCount} class="mt-1.5" />}
199
+
200
+ {
201
+ swatches.length > 1 && (
202
+ <ul class="mt-2.5 flex items-center gap-1.5" aria-label={`Available in ${colors.length} colors`}>
203
+ {swatches.map((s) => (
204
+ <li
205
+ title={s.label}
206
+ class:list={[
207
+ "h-3.5 w-3.5 rounded-full",
208
+ s.fill && isLightFill(s.fill) ? "ring-1 ring-inset ring-[var(--color-border)]" : "",
209
+ ]}
210
+ style={
211
+ s.fill
212
+ ? `background:${s.fill}`
213
+ : "background:repeating-linear-gradient(45deg,var(--color-surface),var(--color-surface)2px,var(--color-border)2px,var(--color-border)4px)"
214
+ }
215
+ >
216
+ <span class="sr-only">{s.label}</span>
217
+ </li>
218
+ ))}
219
+ {extraColors > 0 && (
220
+ <li class="ml-0.5 font-mono text-[0.62rem] text-muted" aria-hidden="true">
221
+ +{extraColors}
222
+ </li>
223
+ )}
224
+ </ul>
225
+ )
226
+ }
227
+ </div>
228
+ </div>
@@ -0,0 +1,39 @@
1
+ ---
2
+ /**
3
+ * Section wrapper — consistent vertical rhythm + optional titled header.
4
+ * Structure encodes meaning (brief §6): an eyebrow/title pair only when there's
5
+ * a real section heading; otherwise just rhythm.
6
+ */
7
+ interface Props {
8
+ title?: string;
9
+ eyebrow?: string;
10
+ /** "bg" (default) | "surface" — alternate band background. */
11
+ tone?: "bg" | "surface";
12
+ class?: string;
13
+ id?: string;
14
+ }
15
+ const { title, eyebrow, tone = "bg", class: className, id } = Astro.props;
16
+ const bg = tone === "surface" ? "bg-[var(--color-surface)]" : "bg-[var(--color-bg)]";
17
+ ---
18
+
19
+ <section
20
+ id={id}
21
+ class:list={[bg, "py-[clamp(3rem,7vw,6rem)]", className]}
22
+ >
23
+ <div class="container-prose">
24
+ {(title || eyebrow) && (
25
+ <header class="mb-9 flex flex-wrap items-end justify-between gap-4" data-reveal>
26
+ <div>
27
+ {eyebrow && <p class="eyebrow eyebrow--tick mb-3">{eyebrow}</p>}
28
+ {title && (
29
+ <h2 class="display-lg">
30
+ {title}
31
+ </h2>
32
+ )}
33
+ </div>
34
+ <slot name="header-aside" />
35
+ </header>
36
+ )}
37
+ <slot />
38
+ </div>
39
+ </section>
@@ -0,0 +1,70 @@
1
+ ---
2
+ /**
3
+ * <Seo> — per-page title/meta, Open Graph, Twitter card, canonical, JSON-LD.
4
+ * (brief §8). Tenant-aware: OG image + site name come from the resolved tenant.
5
+ */
6
+ interface JsonLd {
7
+ [key: string]: unknown;
8
+ }
9
+
10
+ interface Props {
11
+ title: string;
12
+ description?: string;
13
+ /** Path-only canonical, e.g. /products/foo. Joined to tenant canonical base. */
14
+ path: string;
15
+ ogType?: "website" | "product" | "article";
16
+ ogImage?: string;
17
+ noindex?: boolean;
18
+ /** One or more JSON-LD graphs (Product, BreadcrumbList, Organization…). */
19
+ jsonLd?: JsonLd | JsonLd[];
20
+ }
21
+
22
+ const {
23
+ title,
24
+ description,
25
+ path,
26
+ ogType = "website",
27
+ ogImage,
28
+ noindex = false,
29
+ jsonLd,
30
+ } = Astro.props;
31
+
32
+ const { tenant, canonicalBase } = Astro.locals;
33
+ const canonical = new URL(path, canonicalBase + "/").toString();
34
+ // Don't double-append the brand: merchant SEO titles (title_tag) often already
35
+ // end with the store name.
36
+ const fullTitle = title.includes(tenant.displayName)
37
+ ? title
38
+ : `${title} — ${tenant.displayName}`;
39
+ const image = ogImage
40
+ ? new URL(ogImage, canonicalBase + "/").toString()
41
+ : new URL(tenant.theme_tokens?.brand?.ogImage ?? "/brand/northwind-og.svg", canonicalBase + "/").toString();
42
+
43
+ const graphs = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : [];
44
+ ---
45
+
46
+ <title>{fullTitle}</title>
47
+ {description && <meta name="description" content={description} />}
48
+ <link rel="canonical" href={canonical} />
49
+ {noindex && <meta name="robots" content="noindex,nofollow" />}
50
+
51
+ <meta property="og:type" content={ogType} />
52
+ <meta property="og:site_name" content={tenant.displayName} />
53
+ <meta property="og:title" content={fullTitle} />
54
+ {description && <meta property="og:description" content={description} />}
55
+ <meta property="og:url" content={canonical} />
56
+ <meta property="og:image" content={image} />
57
+
58
+ <meta name="twitter:card" content="summary_large_image" />
59
+ <meta name="twitter:title" content={fullTitle} />
60
+ {description && <meta name="twitter:description" content={description} />}
61
+ <meta name="twitter:image" content={image} />
62
+
63
+ {
64
+ graphs.map((g) => (
65
+ <script
66
+ type="application/ld+json"
67
+ set:html={JSON.stringify(g)}
68
+ />
69
+ ))
70
+ }
@@ -0,0 +1,34 @@
1
+ ---
2
+ /**
3
+ * <WidgetFrame> — renders a tenant-registered widget (isolation:"sandbox") in a
4
+ * sandboxed iframe. The sandbox token list (`allow-scripts`, no
5
+ * `allow-same-origin`) puts the widget in an opaque origin, so it cannot read the
6
+ * page's cookies/DOM or its own storage, even though the bundle is served from our
7
+ * own origin. The widget bundle is hash-pinned in CSP and placed into a
8
+ * platform-defined slot.
9
+ */
10
+ import { widgetFrameAttrs, type ScriptStrategy } from "@tot/public-runtime";
11
+
12
+ interface Props {
13
+ /** Same-origin URL that loads the tenant's widget bundle. */
14
+ src: string;
15
+ /** Accessible title for the iframe (required for a11y). */
16
+ title: string;
17
+ strategy?: ScriptStrategy;
18
+ /** Optional fixed height; widgets may also self-size via postMessage later. */
19
+ height?: string;
20
+ }
21
+
22
+ const { src, title, strategy = "on-idle", height = "auto" } = Astro.props;
23
+ const attrs = widgetFrameAttrs(strategy);
24
+ ---
25
+
26
+ <iframe
27
+ src={src}
28
+ title={title}
29
+ sandbox={attrs.sandbox}
30
+ loading={attrs.loading}
31
+ referrerpolicy={attrs.referrerpolicy}
32
+ class="tot-widget-frame"
33
+ style={`border:0;width:100%;height:${height};`}
34
+ ></iframe>