@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,57 @@
1
+ /**
2
+ * GET /tenants/<id>/<path> — serve a tenant's versioned static asset from R2.
3
+ *
4
+ * The version-scoped replacement for the build-time `copy-tenant-assets.mjs`
5
+ * baking (decision `assets-r2-versioned`). Resolves the tenant's active
6
+ * channel→snapshot versionId from the `cust:assets-pointer:<id>` KV record and
7
+ * streams `tenants/<id>/<versionId>/<rel>` from the TENANT_ASSETS R2 bucket with
8
+ * the right content-type + a revalidating cache header + a versionId ETag.
9
+ *
10
+ * BACK-COMPAT: this SSR route only runs when NO baked static file matched — the
11
+ * Cloudflare/Vite static layer serves `public/tenants/<id>/…` first. So a
12
+ * NON-versioned tenant (still baked by copy-tenant-assets) never reaches here,
13
+ * and a versioned tenant (skipped from baking via a `.tot/assets-versioned`
14
+ * marker) falls through to R2. When the tenant has no published asset version,
15
+ * `serveTenantAsset` returns null → 404 (nothing published yet).
16
+ */
17
+ import type { APIContext } from "astro";
18
+ import {
19
+ serveTenantAsset,
20
+ type R2BucketLike,
21
+ } from "@tot/public-runtime";
22
+ import { readR2, readKv } from "@/lib/env";
23
+
24
+ export const prerender = false;
25
+
26
+ export async function GET(context: APIContext): Promise<Response> {
27
+ const [bucket, kv] = await Promise.all([
28
+ readR2("TENANT_ASSETS"),
29
+ readKv("TENANT_CACHE"),
30
+ ]);
31
+ // No R2/KV binding (plain `astro dev`, or unconfigured) → nothing to serve.
32
+ if (!bucket || !kv) {
33
+ return new Response("Not found", {
34
+ status: 404,
35
+ headers: { "cache-control": "no-store" },
36
+ });
37
+ }
38
+
39
+ const res = await serveTenantAsset(
40
+ {
41
+ // Runtime binding structurally satisfies the shared-runtime R2 seam.
42
+ bucket: bucket as unknown as R2BucketLike,
43
+ kvGet: (key) => kv.get(key),
44
+ channel: context.locals.customizationEnv ?? "live",
45
+ ifNoneMatch: context.request.headers.get("if-none-match"),
46
+ },
47
+ new URL(context.request.url).pathname,
48
+ );
49
+
50
+ return (
51
+ res ??
52
+ new Response("Not found", {
53
+ status: 404,
54
+ headers: { "cache-control": "no-store" },
55
+ })
56
+ );
57
+ }
@@ -0,0 +1,292 @@
1
+ @import "tailwindcss";
2
+
3
+ /*
4
+ * Theme bridge (brief §6/§7).
5
+ *
6
+ * The per-tenant identity arrives as CSS custom properties in a :root{} block
7
+ * injected by the middleware (src/themes/schema.ts -> Layout.astro). Here we map
8
+ * those vars into Tailwind v4 theme tokens so utilities like `bg-bg`,
9
+ * `text-primary`, `font-display`, `rounded-md` resolve to the LIVE tenant value.
10
+ * Components stay theme-agnostic: they use utilities, the utilities read vars,
11
+ * the vars come from the tenant. Swap a tenant -> the whole UI reskins, no
12
+ * component edits.
13
+ *
14
+ * Fallback values below keep the design coherent before/without the injected
15
+ * block (e.g. error pages, prerendered routes).
16
+ */
17
+ @theme {
18
+ --color-bg: var(--color-bg, #f4f1ea);
19
+ --color-surface: var(--color-surface, #fcfaf5);
20
+ --color-text: var(--color-text, #1c2624);
21
+ --color-muted: var(--color-muted, #5c6663);
22
+ --color-primary: var(--color-primary, #1f3a34);
23
+ --color-primary-contrast: var(--color-primary-contrast, #f4f1ea);
24
+ --color-accent: var(--color-accent, #b5612f);
25
+ /* Darkened accent for SMALL text/links on light bg/surface (the bright
26
+ `accent` is reserved for large display type on the spruce hero, where it
27
+ reads as the brass spark). Derived from the tenant's own accent so it stays
28
+ theme-agnostic. Reference value resolves to ~#985127 (≥5:1 on bg/surface).
29
+ A tenant MAY hand-tune it for exact AA by emitting --color-accent-ink; else
30
+ it derives from the tenant's own accent so it stays theme-agnostic. */
31
+ --color-accent-ink: var(--color-accent-ink, color-mix(in srgb, var(--color-accent) 84%, #000));
32
+ --color-border: var(--color-border, #dcd6c9);
33
+ --color-success: var(--color-success, #3f6b4f);
34
+ --color-sale: var(--color-sale, #9e3b2e);
35
+
36
+ /* Semantic/state extensions (token-contract.md). Platform defaults kept
37
+ resolvable here so components can always read them; a tenant theme overrides
38
+ any by emitting the matching --color-* via themeToCssVars. */
39
+ --color-surface-2: var(--color-surface-2, #ece7db);
40
+ --color-warning: var(--color-warning, #8a5717);
41
+ --color-error: var(--color-error, #a22014);
42
+ --color-info: var(--color-info, #2c5b73);
43
+
44
+ /* Hover policy: components read --color-{primary,accent}-hover; the default is
45
+ a derived darken of the base, overridable per tenant for hand-tuned values. */
46
+ --color-primary-hover: var(--color-primary-hover, color-mix(in srgb, var(--color-primary) 88%, #000));
47
+ --color-accent-hover: var(--color-accent-hover, color-mix(in srgb, var(--color-accent) 88%, #000));
48
+
49
+ --font-display: var(--font-display, "Space Grotesk"), system-ui, sans-serif;
50
+ --font-body: var(--font-body, "Source Serif 4"), Georgia, serif;
51
+ --font-mono: var(--font-mono, "Space Mono"), ui-monospace, monospace;
52
+
53
+ --radius-sm: var(--radius-sm, 3px);
54
+ --radius-md: var(--radius-md, 6px);
55
+ --radius-lg: var(--radius-lg, 12px);
56
+ --radius-full: var(--radius-full, 999px);
57
+
58
+ --shadow-sm: var(--shadow-sm, 0 1px 2px rgba(28, 38, 36, 0.06));
59
+ --shadow-md: var(--shadow-md, 0 6px 24px -8px rgba(28, 38, 36, 0.14));
60
+ --shadow-lg: var(--shadow-lg, 0 24px 60px -20px rgba(28, 38, 36, 0.22));
61
+
62
+ --container-max: var(--container-max, 1240px);
63
+ }
64
+
65
+ /* ---- base ---------------------------------------------------------------- */
66
+ @layer base {
67
+ :root {
68
+ /* Fluid modular type scale; overridden by injected --fs-* when present. */
69
+ color-scheme: light;
70
+ }
71
+
72
+ html {
73
+ font-family: var(--font-body);
74
+ color: var(--color-text);
75
+ background: var(--color-bg);
76
+ -webkit-font-smoothing: antialiased;
77
+ text-rendering: optimizeLegibility;
78
+ scroll-behavior: smooth;
79
+ }
80
+
81
+ body {
82
+ font-size: var(--fs-base, 1.0625rem);
83
+ line-height: 1.6;
84
+ letter-spacing: var(--type-tracking, -0.011em);
85
+ }
86
+
87
+ h1, h2, h3, h4, h5 {
88
+ font-family: var(--font-display);
89
+ font-weight: 600;
90
+ line-height: 1.05;
91
+ letter-spacing: -0.02em;
92
+ color: var(--color-text);
93
+ text-wrap: balance;
94
+ }
95
+
96
+ /* Body copy rhythm: a comfortable measure + paragraph spacing for prose-y
97
+ blocks that aren't inside .rte (e.g. editorial subheads). */
98
+ p { text-wrap: pretty; }
99
+
100
+ a {
101
+ color: inherit;
102
+ text-decoration: none;
103
+ }
104
+
105
+ /* Visible keyboard focus everywhere (WCAG 2.2 AA, brief §8). */
106
+ :focus-visible {
107
+ outline: var(--border-thick, 2px) solid var(--color-accent);
108
+ outline-offset: 2px;
109
+ border-radius: 2px;
110
+ }
111
+
112
+ ::selection {
113
+ background: var(--color-primary);
114
+ color: var(--color-primary-contrast);
115
+ }
116
+
117
+ img { max-width: 100%; height: auto; }
118
+
119
+ /* Honor reduced motion (brief §6/§8). */
120
+ @media (prefers-reduced-motion: reduce) {
121
+ *, *::before, *::after {
122
+ animation-duration: 0.001ms !important;
123
+ animation-iteration-count: 1 !important;
124
+ transition-duration: 0.001ms !important;
125
+ scroll-behavior: auto !important;
126
+ }
127
+ }
128
+ }
129
+
130
+ /* ---- shared component primitives ----------------------------------------- */
131
+ @layer components {
132
+ .container-prose {
133
+ width: 100%;
134
+ max-width: var(--container-max);
135
+ margin-inline: auto;
136
+ padding-inline: clamp(1rem, 4vw, 2.5rem);
137
+ }
138
+
139
+ .eyebrow {
140
+ font-family: var(--font-mono);
141
+ font-size: 0.72rem;
142
+ letter-spacing: 0.16em;
143
+ text-transform: uppercase;
144
+ color: var(--color-muted);
145
+ }
146
+
147
+ /* Eyebrow with the signature leading tick — a single short amber rule, the
148
+ "one brass rivet" motif from DESIGN.md, applied as a quiet structural mark.
149
+ Opt-in via .eyebrow.eyebrow--tick so it never fires where it'd read as noise. */
150
+ .eyebrow--tick {
151
+ display: inline-flex;
152
+ align-items: center;
153
+ gap: 0.6em;
154
+ }
155
+ .eyebrow--tick::before {
156
+ content: "";
157
+ width: 1.6em;
158
+ height: 2px;
159
+ background: var(--color-accent);
160
+ flex: none;
161
+ }
162
+
163
+ /* Fluid display scale, derived from the tenant's emitted modular --fs-* steps
164
+ (the major-third scale in DESIGN.md). One rhythm across every page instead
165
+ of ad-hoc per-component clamps. min = small step, max = large step. */
166
+ .display-hero {
167
+ font-family: var(--font-display);
168
+ font-weight: 600;
169
+ line-height: 0.95;
170
+ letter-spacing: -0.03em;
171
+ font-size: clamp(var(--fs-3xl, 3.05rem), 8vw, var(--fs-5xl, 4.77rem));
172
+ }
173
+ .display-xl {
174
+ font-family: var(--font-display);
175
+ font-weight: 600;
176
+ line-height: 1.02;
177
+ letter-spacing: -0.025em;
178
+ font-size: clamp(var(--fs-2xl, 2.44rem), 5vw, var(--fs-4xl, 3.81rem));
179
+ }
180
+ .display-lg {
181
+ font-family: var(--font-display);
182
+ font-weight: 600;
183
+ line-height: 1.05;
184
+ letter-spacing: -0.02em;
185
+ font-size: clamp(var(--fs-xl, 1.95rem), 3.5vw, var(--fs-3xl, 3.05rem));
186
+ }
187
+ .display-md {
188
+ font-family: var(--font-display);
189
+ font-weight: 600;
190
+ line-height: 1.1;
191
+ letter-spacing: -0.018em;
192
+ font-size: clamp(var(--fs-lg, 1.56rem), 3vw, var(--fs-2xl, 2.44rem));
193
+ }
194
+
195
+ /* Tasteful scroll-reveal; opt-in via [data-reveal], JS toggles .is-in. */
196
+ [data-reveal] {
197
+ opacity: 0;
198
+ transform: translateY(14px);
199
+ transition: opacity 0.6s ease, transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
200
+ }
201
+ [data-reveal].is-in {
202
+ opacity: 1;
203
+ transform: none;
204
+ }
205
+ @media (prefers-reduced-motion: reduce) {
206
+ [data-reveal] { opacity: 1; transform: none; }
207
+ }
208
+
209
+ /* Island animations (MobileNav drawer slide-in). Globally neutralized by the
210
+ reduced-motion guard above; islands also guard in JS. */
211
+ @keyframes tot-slidein {
212
+ from { transform: translateX(100%); }
213
+ to { transform: translateX(0); }
214
+ }
215
+
216
+ /* Rich text from CMS/description. */
217
+ .rte > * + * { margin-top: 1em; }
218
+ .rte h2 { font-size: var(--fs-xl, 1.6rem); margin-top: 1.6em; }
219
+ .rte h3 { font-size: var(--fs-lg, 1.3rem); margin-top: 1.3em; }
220
+ .rte ul, .rte ol { padding-left: 1.4em; }
221
+ .rte ul { list-style: disc; }
222
+ .rte ol { list-style: decimal; }
223
+ .rte li + li { margin-top: 0.35em; }
224
+ .rte strong { font-weight: 600; }
225
+ .rte a { color: var(--color-accent-ink); text-decoration: underline; text-underline-offset: 3px; }
226
+
227
+ /* Accent text-link with an animated underline — restrained micro-interaction
228
+ for inline CTAs. Uses accent-ink (AA on light surfaces). */
229
+ .link-accent {
230
+ color: var(--color-text);
231
+ background-image: linear-gradient(var(--color-accent-ink), var(--color-accent-ink));
232
+ background-position: 0 100%;
233
+ background-repeat: no-repeat;
234
+ background-size: 100% 1.5px;
235
+ transition: color 0.2s ease, background-size 0.25s cubic-bezier(0.22, 1, 0.36, 1);
236
+ }
237
+ .link-accent:hover,
238
+ .link-accent:focus-visible {
239
+ color: var(--color-accent-ink);
240
+ }
241
+ @media (prefers-reduced-motion: no-preference) {
242
+ .link-accent { background-size: 0% 1.5px; }
243
+ .link-accent:hover,
244
+ .link-accent:focus-visible { background-size: 100% 1.5px; }
245
+ }
246
+
247
+ /* Collapsible PDP info sections (native <details>). Zero-JS, accessible. */
248
+ .accordion {
249
+ border-bottom: 1px solid var(--color-border);
250
+ }
251
+ .accordion__summary {
252
+ display: flex;
253
+ align-items: center;
254
+ justify-content: space-between;
255
+ gap: 1rem;
256
+ padding: 1rem 0;
257
+ cursor: pointer;
258
+ list-style: none;
259
+ font-family: var(--font-display);
260
+ font-weight: 500;
261
+ font-size: 0.95rem;
262
+ color: var(--color-text);
263
+ transition: color 0.15s ease;
264
+ }
265
+ .accordion__summary::-webkit-details-marker { display: none; }
266
+ .accordion__summary:hover { color: var(--color-primary); }
267
+ .accordion__chevron {
268
+ font-family: var(--font-mono);
269
+ font-size: 1.1rem;
270
+ line-height: 1;
271
+ color: var(--color-muted);
272
+ transition: transform 0.2s ease;
273
+ }
274
+ .accordion[open] .accordion__chevron { transform: rotate(45deg); }
275
+ .accordion__body { padding-bottom: 1.1rem; }
276
+
277
+ /* Wishlist saved state (robust, no JS text swap): fill the heart and relabel
278
+ when aria-pressed. Applies to card hearts and the PDP "Save for later". */
279
+ .wishlist-toggle[aria-pressed="true"] .wishlist-heart { fill: currentColor; }
280
+ .wishlist-toggle[aria-pressed="true"] .wl-label { font-size: 0; }
281
+ .wishlist-toggle[aria-pressed="true"] .wl-label::after {
282
+ content: "Saved";
283
+ font-size: 0.875rem;
284
+ }
285
+
286
+ /* Quick-view dialog entrance (reduced-motion neutralized by the base guard). */
287
+ .quickview[open] { animation: tot-pop 0.18s cubic-bezier(0.22, 1, 0.36, 1); }
288
+ @keyframes tot-pop {
289
+ from { opacity: 0; transform: translateY(8px) scale(0.98); }
290
+ to { opacity: 1; transform: none; }
291
+ }
292
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Reference theme — "Northwind" pilot identity. See DESIGN.md for full rationale.
3
+ *
4
+ * Thesis: a Pacific-Northwest maker of considered apparel + home goods.
5
+ * The voice is overcast-day calm with one sharp accent — like a single brass
6
+ * rivet on a waxed-canvas bag.
7
+ *
8
+ * Deliberate anti-AI-default moves (brief §6):
9
+ * - Palette is INK + SPRUCE on a warm GREIGE surface — NOT cream/terracotta,
10
+ * NOT near-black/acid-green. Sale red is a muted brick, not a siren.
11
+ * - Type pairing INVERTS the tired "serif headline" cliché: a tight geometric
12
+ * grotesque (Space Grotesk) carries display; a warm humanist serif
13
+ * (Source Serif 4) carries body copy. Personality up top, readability below.
14
+ * - Boldness is spent in ONE place: the homepage hero's oversized split-type
15
+ * lockup. Everything else stays quiet.
16
+ */
17
+ import type { ThemeTokens } from "@tot/public-runtime";
18
+
19
+ export const referenceTheme: ThemeTokens = {
20
+ color: {
21
+ bg: "#F4F1EA", // warm greige paper — NOT cream, lower chroma + cooler
22
+ surface: "#FCFAF5", // raised card surface, a touch lighter than bg
23
+ text: "#1C2624", // spruce-ink, near-black with a green undertone
24
+ muted: "#5C6663", // slate-sage for secondary text
25
+ primary: "#1F3A34", // deep spruce green — the brand anchor
26
+ "primary-contrast": "#F4F1EA",
27
+ accent: "#B5612F", // burnt amber / brass — the single warm spark
28
+ border: "#DCD6C9", // soft sand hairline (NOT broadsheet black)
29
+ success: "#3F6B4F",
30
+ sale: "#9E3B2E", // muted brick red, not fire-engine
31
+ // Semantic/state extensions (contract optional; stated here as the canonical
32
+ // reference values). accent-ink + *-hover are intentionally omitted so the
33
+ // reference identity exercises the derived @theme defaults.
34
+ "surface-2": "#ECE7DB", // inset greige well, a step deeper than bg
35
+ warning: "#8A5717", // deep amber — AA on the light greige surfaces
36
+ error: "#A22014", // deep true-red, kept distinct from the brick `sale`
37
+ info: "#2C5B73", // calm slate-blue, distinct from the spruce `primary`
38
+ },
39
+ type: {
40
+ display: { family: "Space Grotesk", weights: [500, 600, 700] },
41
+ body: { family: "Source Serif 4", weights: [400, 500, 600] },
42
+ mono: { family: "Space Mono", weights: [400] },
43
+ scale: 1.25, // major third — generous but not theatrical
44
+ tracking: "-0.011em",
45
+ },
46
+ space: {
47
+ base: 4,
48
+ scale: [0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64],
49
+ containerMaxWidth: "1240px",
50
+ sectionRhythm: "clamp(4rem, 9vw, 8rem)",
51
+ },
52
+ shape: {
53
+ radius: { sm: "3px", md: "6px", lg: "12px", full: "999px" },
54
+ borderWeights: { thin: "1px", medium: "1.5px", thick: "2px" },
55
+ shadow: {
56
+ sm: "0 1px 2px rgba(28, 38, 36, 0.06)",
57
+ md: "0 6px 24px -8px rgba(28, 38, 36, 0.14)",
58
+ lg: "0 24px 60px -20px rgba(28, 38, 36, 0.22)",
59
+ },
60
+ },
61
+ brand: {
62
+ logoLight: "/brand/northwind-logo-light.svg",
63
+ logoDark: "/brand/northwind-logo-dark.svg",
64
+ favicon: "/favicon.svg",
65
+ ogImage: "/brand/northwind-og.svg",
66
+ motion: "full",
67
+ },
68
+ };
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Theme token -> CSS custom property serialization.
3
+ *
4
+ * Components are theme-AGNOSTIC: they only ever read var(--color-primary),
5
+ * var(--font-display), etc. This module turns a resolved ThemeTokens object
6
+ * into the `<style>` block the middleware injects per request, so one component
7
+ * library renders every tenant's identity. See DESIGN.md.
8
+ */
9
+ import type { DeepPartial, ThemeTokens } from "@tot/public-runtime";
10
+
11
+ /** Deep-merge tenant overrides over the reference defaults. */
12
+ export function mergeTheme(
13
+ base: ThemeTokens,
14
+ override?: DeepPartial<ThemeTokens>,
15
+ ): ThemeTokens {
16
+ if (!override) return base;
17
+ return deepMerge(base, override) as ThemeTokens;
18
+ }
19
+
20
+ function deepMerge<T>(base: T, over: DeepPartial<T>): T {
21
+ if (Array.isArray(base)) return (over as unknown as T) ?? base;
22
+ if (typeof base === "object" && base !== null) {
23
+ const out: Record<string, unknown> = { ...(base as Record<string, unknown>) };
24
+ for (const [k, v] of Object.entries(over as Record<string, unknown>)) {
25
+ if (v === undefined) continue;
26
+ const bv = (base as Record<string, unknown>)[k];
27
+ out[k] =
28
+ bv && typeof bv === "object" && !Array.isArray(bv) && typeof v === "object"
29
+ ? deepMerge(bv, v as DeepPartial<unknown>)
30
+ : v;
31
+ }
32
+ return out as T;
33
+ }
34
+ return (over as unknown as T) ?? base;
35
+ }
36
+
37
+ /**
38
+ * Serialize ThemeTokens into a `:root { --x: y }` declaration block.
39
+ * Returns the INNER CSS (no <style> wrapper) so the caller controls nonce/attrs.
40
+ */
41
+ export function themeToCssVars(t: ThemeTokens): string {
42
+ const lines: string[] = [];
43
+ const push = (name: string, value: string | number) =>
44
+ lines.push(` --${name}: ${value};`);
45
+
46
+ // Color
47
+ for (const [k, v] of Object.entries(t.color)) push(`color-${k}`, v);
48
+
49
+ // Type
50
+ push("font-display", `"${t.type.display.family}"`);
51
+ push("font-body", `"${t.type.body.family}"`);
52
+ if (t.type.mono) push("font-mono", `"${t.type.mono.family}"`);
53
+ push("type-scale", t.type.scale);
54
+ if (t.type.tracking) push("type-tracking", t.type.tracking);
55
+ // Emit a modular type scale (rem) from the ratio.
56
+ const ratio = t.type.scale;
57
+ const steps: Record<string, number> = {
58
+ "xs": -2, "sm": -1, "base": 0, "md": 1, "lg": 2,
59
+ "xl": 3, "2xl": 4, "3xl": 5, "4xl": 6, "5xl": 7,
60
+ };
61
+ for (const [name, step] of Object.entries(steps)) {
62
+ push(`fs-${name}`, `${(1 * Math.pow(ratio, step)).toFixed(4)}rem`);
63
+ }
64
+
65
+ // Space
66
+ push("space-base", `${t.space.base}px`);
67
+ t.space.scale.forEach((mult, i) =>
68
+ push(`space-${i}`, `${(t.space.base * mult).toFixed(2)}px`),
69
+ );
70
+ push("container-max", t.space.containerMaxWidth);
71
+ push("section-rhythm", t.space.sectionRhythm);
72
+
73
+ // Shape
74
+ push("radius-sm", t.shape.radius.sm);
75
+ push("radius-md", t.shape.radius.md);
76
+ push("radius-lg", t.shape.radius.lg);
77
+ push("radius-full", t.shape.radius.full);
78
+ push("border-thin", t.shape.borderWeights.thin);
79
+ push("border-medium", t.shape.borderWeights.medium);
80
+ push("border-thick", t.shape.borderWeights.thick);
81
+ push("shadow-sm", t.shape.shadow.sm);
82
+ push("shadow-md", t.shape.shadow.md);
83
+ push("shadow-lg", t.shape.shadow.lg);
84
+
85
+ return `:root{\n${lines.join("\n")}\n}`;
86
+ }
87
+
88
+ /** Google Fonts <link> for a theme's display+body+mono families (if Google-hosted). */
89
+ export function themeFontHref(t: ThemeTokens): string | null {
90
+ const families = new Map<string, number[]>();
91
+ families.set(t.type.display.family, t.type.display.weights);
92
+ families.set(t.type.body.family, t.type.body.weights);
93
+ if (t.type.mono) families.set(t.type.mono.family, t.type.mono.weights);
94
+
95
+ const params = [...families]
96
+ .filter(([fam]) => !SYSTEM_FONTS.has(fam))
97
+ .map(([fam, weights]) => {
98
+ const w = [...new Set(weights)].sort((a, b) => a - b).join(";");
99
+ return `family=${encodeURIComponent(fam).replace(/%20/g, "+")}:wght@${w}`;
100
+ });
101
+ if (!params.length) return null;
102
+ return `https://fonts.googleapis.com/css2?${params.join("&")}&display=swap`;
103
+ }
104
+
105
+ const SYSTEM_FONTS = new Set([
106
+ "system-ui",
107
+ "-apple-system",
108
+ "Arial",
109
+ "Helvetica",
110
+ "Georgia",
111
+ ]);
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Per-vendor theme registry.
3
+ *
4
+ * A tenant's theme override lives at the colocated repo-root path
5
+ * `tenants/<tenant_id>/theme.json` (a DeepPartial<ThemeTokens> — only the tokens
6
+ * you want to override; the reference theme fills the rest) and it's
7
+ * auto-discovered at build time. This is the seam the brand adopter targets: its
8
+ * `out/branding/<tenant>.theme.json` output is written to `tenants/<id>/theme.json`.
9
+ *
10
+ * Files may include documentation keys (`_vendor`, `_note`, `$schema`); those are
11
+ * stripped so only real tokens reach the theme merge.
12
+ *
13
+ * Depth: from src/themes/tenants/ the repo root is five levels up
14
+ * (tenants→themes→src→storefront→apps→root).
15
+ */
16
+ import type { DeepPartial, ThemeTokens } from "@tot/public-runtime";
17
+
18
+ const modules = import.meta.glob<Record<string, unknown>>(
19
+ "../../../../../tenants/*/theme.json",
20
+ {
21
+ eager: true,
22
+ import: "default",
23
+ },
24
+ );
25
+
26
+ function stripMeta(obj: Record<string, unknown>): DeepPartial<ThemeTokens> {
27
+ const out: Record<string, unknown> = {};
28
+ for (const [k, v] of Object.entries(obj)) {
29
+ if (k.startsWith("_") || k.startsWith("$")) continue;
30
+ out[k] = v;
31
+ }
32
+ return out as DeepPartial<ThemeTokens>;
33
+ }
34
+
35
+ const THEMES: Record<string, DeepPartial<ThemeTokens>> = {};
36
+ for (const [path, mod] of Object.entries(modules)) {
37
+ // Path looks like ".../tenants/<id>/theme.json" — the id is the dir segment.
38
+ const id = /\/tenants\/([^/]+)\/theme\.json$/.exec(path)?.[1];
39
+ if (!id) continue;
40
+ THEMES[id] = stripMeta(mod as Record<string, unknown>);
41
+ }
42
+
43
+ /** Theme-token overrides for a tenant, or {} if the vendor has no theme file. */
44
+ export function getTenantTheme(tenantId: string): DeepPartial<ThemeTokens> {
45
+ return THEMES[tenantId] ?? {};
46
+ }
47
+
48
+ /** Tenant ids that ship a theme file (for diagnostics / listing). */
49
+ export function tenantsWithThemes(): string[] {
50
+ return Object.keys(THEMES);
51
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "astro/tsconfigs/strict",
3
+ "compilerOptions": {
4
+ "strict": true,
5
+ "noUncheckedIndexedAccess": true,
6
+ "verbatimModuleSyntax": false,
7
+ "jsx": "react-jsx",
8
+ "jsxImportSource": "preact",
9
+ "baseUrl": ".",
10
+ "paths": {
11
+ "@/*": ["src/*"]
12
+ }
13
+ },
14
+ "include": [".astro/types.d.ts", "src/**/*", "*.config.mjs", "env.d.ts"],
15
+ "exclude": ["dist", "node_modules"]
16
+ }
@@ -0,0 +1,22 @@
1
+ import { defineConfig } from "vitest/config";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ // Storefront unit tests — pure TS logic (tenant routing, search, query engine,
5
+ // image-attr resolution, edge cache, etc.). We deliberately do NOT render .astro
6
+ // components here: the Astro Container API needs Astro's Vite pipeline, whose
7
+ // vite-plugin-head crashes under vitest@2 + astro@7 (configureServer reads an
8
+ // undefined `ssr`). Render *logic* is extracted into testable modules instead
9
+ // (e.g. lib/imageAttrs.ts), and rendered output is verified on a live preview.
10
+ export default defineConfig({
11
+ // Mirror the `@` -> src alias from tsconfig so tests can import aliased modules
12
+ // (e.g. src/config/tenants.ts, which pulls the theme registry).
13
+ resolve: {
14
+ alias: {
15
+ "@": fileURLToPath(new URL("./src", import.meta.url)),
16
+ },
17
+ },
18
+ test: {
19
+ include: ["src/**/*.test.ts"],
20
+ environment: "node",
21
+ },
22
+ });
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@tokenoftrust/storefront-runner",
3
+ "version": "1.3.2",
4
+ "license": "SEE LICENSE IN LICENSE",
5
+ "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
+ "packageManager": "pnpm@11.9.0",
7
+ "engines": {
8
+ "node": ">=24"
9
+ },
10
+ "scripts": {
11
+ "dev": "pnpm --filter @tot/storefront-runner dev",
12
+ "build": "pnpm --filter @tot/storefront-runner build",
13
+ "typecheck": "pnpm -r typecheck",
14
+ "depcruise": "pnpm --filter @tot/storefront-runner depcruise"
15
+ },
16
+ "devDependencies": {
17
+ "typescript": "^5.6.3"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public",
21
+ "registry": "https://registry.npmjs.org"
22
+ },
23
+ "bin": {
24
+ "storefront-runner": "scripts/tot-dev.mjs"
25
+ }
26
+ }