@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,175 @@
1
+ ---
2
+ /**
3
+ * <BrandGrid> — a "Shop by Brands" logo wall: a grid of brand tiles linking to
4
+ * brand collections, each rendered as a rehosted logo image OR a text-wordmark
5
+ * fallback when no logo is available. Tenant-agnostic — the `brands` array is
6
+ * content, theme tokens are the skin. Replaces per-tenant hand-authored blocks
7
+ * (image-logo walls and text-wordmark tiles alike) with one shared component.
8
+ *
9
+ * Two delivery paths, one look: this component styles itself from the token
10
+ * contract via a scoped <style> (so it renders under the app Layout AND in the
11
+ * standalone style guide, neither of which links the raw sheet). The class-only
12
+ * raw-chrome parity lives in the `.brand-grid` / `.brand-tile` block of
13
+ * public/shared/commerce-chrome.css, for tenants that ship hand-authored HTML
14
+ * through the rawMarketingHtml splice pipeline. Keep the two visually equivalent.
15
+ */
16
+ import Section from "@/components/Section.astro";
17
+ import Img from "@/components/Img.astro";
18
+ import { withBase } from "@/lib/basePath";
19
+
20
+ export interface Brand {
21
+ /** Brand name — the link label, image alt, and text-fallback wordmark. */
22
+ label: string;
23
+ /** Collection (or brand) URL, e.g. /collections/<handle>. Base-prefixed here. */
24
+ href: string;
25
+ /** Rehosted logo image URL. Omit to render the text wordmark fallback. */
26
+ logoSrc?: string;
27
+ /** True when logoSrc is a rehosted asset (enables responsive srcset). */
28
+ logoRehosted?: boolean;
29
+ logoWidth?: number;
30
+ logoHeight?: number;
31
+ }
32
+
33
+ interface Props {
34
+ brands: Brand[];
35
+ title?: string;
36
+ eyebrow?: string;
37
+ tone?: "bg" | "surface";
38
+ /** Optional "View all brands" call-to-action below the grid. */
39
+ viewAllHref?: string;
40
+ viewAllLabel?: string;
41
+ class?: string;
42
+ }
43
+
44
+ const {
45
+ brands,
46
+ title,
47
+ eyebrow,
48
+ tone = "bg",
49
+ viewAllHref,
50
+ viewAllLabel = "View all brands",
51
+ class: className,
52
+ } = Astro.props;
53
+
54
+ const basePath = Astro.locals.basePath ?? "";
55
+ ---
56
+
57
+ <Section title={title} eyebrow={eyebrow} tone={tone} class={className}>
58
+ <ul class="brand-grid" role="list">
59
+ {
60
+ brands.map((brand) => (
61
+ <li>
62
+ <a
63
+ class="brand-tile"
64
+ href={withBase(basePath, brand.href)}
65
+ aria-label={brand.label}
66
+ data-reveal
67
+ >
68
+ {brand.logoSrc ? (
69
+ <Img
70
+ src={brand.logoSrc}
71
+ rehosted={brand.logoRehosted}
72
+ seed={brand.href}
73
+ label={brand.label}
74
+ alt={brand.label}
75
+ width={brand.logoWidth ?? 200}
76
+ height={brand.logoHeight ?? 100}
77
+ sizes="(min-width: 640px) 160px, 44vw"
78
+ />
79
+ ) : (
80
+ <span class="brand-tile__text">{brand.label}</span>
81
+ )}
82
+ </a>
83
+ </li>
84
+ ))
85
+ }
86
+ </ul>
87
+ {
88
+ viewAllHref && (
89
+ <p class="brand-grid__more">
90
+ <a class="brand-grid__cta" href={withBase(basePath, viewAllHref)}>
91
+ {viewAllLabel}
92
+ </a>
93
+ </p>
94
+ )
95
+ }
96
+ </Section>
97
+
98
+ <style>
99
+ /* Mirrors the raw-chrome `.brand-grid` / `.brand-tile` block in
100
+ public/shared/commerce-chrome.css — same token vocabulary, same look. */
101
+ .brand-grid {
102
+ display: grid;
103
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
104
+ gap: clamp(10px, 1.4vw, 16px);
105
+ }
106
+ .brand-tile {
107
+ display: flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ min-height: 92px;
111
+ padding: 18px 16px;
112
+ text-align: center;
113
+ background: var(--color-surface);
114
+ border: 1px solid var(--color-border);
115
+ border-radius: var(--radius-md);
116
+ font-family: var(--font-display);
117
+ font-weight: 700;
118
+ font-size: 0.95rem;
119
+ letter-spacing: 0.01em;
120
+ color: var(--color-text);
121
+ transition:
122
+ transform 0.14s ease,
123
+ box-shadow 0.14s ease,
124
+ border-color 0.14s ease,
125
+ color 0.14s ease;
126
+ }
127
+ .brand-tile:hover {
128
+ transform: translateY(-2px);
129
+ box-shadow: var(--shadow-md);
130
+ border-color: var(--color-primary);
131
+ color: var(--color-primary);
132
+ }
133
+ .brand-tile :global(img) {
134
+ max-height: 46px;
135
+ max-width: 100%;
136
+ width: auto;
137
+ object-fit: contain;
138
+ }
139
+ .brand-grid__more {
140
+ margin-top: clamp(1.25rem, 3vw, 1.75rem);
141
+ text-align: center;
142
+ }
143
+ /* Ghost-button parity with the raw sheet's .btn-ghost, token-only. */
144
+ .brand-grid__cta {
145
+ display: inline-flex;
146
+ align-items: center;
147
+ gap: 0.5em;
148
+ font-family: var(--font-display);
149
+ font-weight: 600;
150
+ font-size: 0.95rem;
151
+ padding: 0.8em 1.6em;
152
+ border: 2px solid var(--color-border);
153
+ border-radius: var(--radius-full);
154
+ background: var(--color-surface);
155
+ color: var(--color-primary);
156
+ transition:
157
+ border-color 0.15s ease,
158
+ transform 0.12s ease;
159
+ }
160
+ .brand-grid__cta:hover {
161
+ border-color: var(--color-primary);
162
+ transform: translateY(-1px);
163
+ }
164
+ @media (max-width: 560px) {
165
+ .brand-grid {
166
+ grid-template-columns: repeat(auto-fill, minmax(44%, 1fr));
167
+ }
168
+ }
169
+ @media (prefers-reduced-motion: reduce) {
170
+ .brand-tile,
171
+ .brand-grid__cta {
172
+ transition: none;
173
+ }
174
+ }
175
+ </style>
@@ -0,0 +1,23 @@
1
+ ---
2
+ /**
3
+ * <FoxyLoader> — injects FoxyCart's loader.js (Track A).
4
+ *
5
+ * Foxy's loader turns our add-to-cart forms/links into the sidecart + hosted
6
+ * checkout ("Powered by FoxyCart"). It's loaded from the store's CDN path and
7
+ * must sit near the end of <body>. Rendered only when the tenant has a store
8
+ * (locals.commerce); marketing tenants ship nothing.
9
+ *
10
+ * Note: Foxy's loader is a mutable, versioned CDN artifact and does NOT support
11
+ * Subresource Integrity — pinning a hash would break it on Foxy's next release.
12
+ * It's constrained instead by the per-tenant CSP allowlist (cdn.foxycart.com +
13
+ * the store origin) set in middleware. It is loaded as a plain cross-origin
14
+ * script (NO `crossorigin` attribute) — cdn.foxycart.com serves it without CORS
15
+ * headers, so `crossorigin="anonymous"` would make the browser block it.
16
+ */
17
+ const { commerce } = Astro.locals;
18
+ ---
19
+ {
20
+ commerce && (
21
+ <script src={commerce.loaderSrc} async defer></script>
22
+ )
23
+ }
@@ -0,0 +1,61 @@
1
+ ---
2
+ /**
3
+ * <FoxySidecartStyles> — brands Foxy's v2.0 sidecart to the tenant's theme.
4
+ *
5
+ * Foxy's loader.js injects the sidecart into OUR page DOM (root `#fc`, light DOM
6
+ * — no iframe, no shadow root), so it inherits the theme's CSS custom properties
7
+ * (`--color-primary`, `--font-*`, …) and can be restyled with plain CSS. We emit
8
+ * a small nonce'd <style> that maps the sidecart's own selectors to those tokens,
9
+ * so a merchant's cart matches their brand out of the box.
10
+ *
11
+ * Two layers: this brand BASELINE, then the tenant's optional `sidecartCss`
12
+ * override (commerce.foxy.sidecartCss) appended verbatim for bespoke tweaks.
13
+ *
14
+ * `!important` is deliberate: Foxy's own responsive_styles.css loads from its CDN
15
+ * AFTER this <head> <style>, so equal-specificity rules would lose the cascade.
16
+ * Everything is scoped under `#fc` so nothing leaks to the storefront itself.
17
+ *
18
+ * Rendered only for commerce tenants with a store (Astro.locals.commerce); the
19
+ * hosted checkout PAGE is a separate origin and is themed in the Foxy admin — out
20
+ * of scope here.
21
+ */
22
+ const { commerce, cspNonce } = Astro.locals;
23
+
24
+ // Selectors verified against the live tot-preview sidecart DOM:
25
+ // #fc — sidecart root (in-page)
26
+ // #fc-cart.fc-container — cart body
27
+ // .fc-action--checkout--button — the primary "Proceed to Checkout" CTA
28
+ // a.fc-btn-action.fc-sidecart-button — the fixed Keep-Shopping/Checkout nav
29
+ const baseline = `
30
+ #fc, #fc input, #fc button, #fc select, #fc textarea {
31
+ font-family: var(--font-body), system-ui, -apple-system, sans-serif;
32
+ }
33
+ #fc h1, #fc h2, #fc h3, #fc .fc-cart-title, #fc [class*="__title"] {
34
+ font-family: var(--font-display), var(--font-body), sans-serif !important;
35
+ }
36
+ #fc #fc-cart { color: var(--color-text); }
37
+ #fc a { color: var(--color-primary); }
38
+ /* Primary checkout CTA(s) → brand primary. */
39
+ #fc .fc-action--checkout--button,
40
+ #fc a.fc-action--checkout--button,
41
+ #fc .fc-btn-action.fc-sidecart-button--checkout {
42
+ background: var(--color-primary) !important;
43
+ border-color: var(--color-primary) !important;
44
+ color: var(--color-primary-contrast) !important;
45
+ border-radius: var(--radius-md) !important;
46
+ }
47
+ #fc .fc-action--checkout--button:hover,
48
+ #fc a.fc-action--checkout--button:hover {
49
+ filter: brightness(1.08);
50
+ }
51
+ /* Quantity / minor actions keep an outline in brand color. */
52
+ #fc .fc-btn-action.fc-sidecart-button {
53
+ border-radius: var(--radius-md) !important;
54
+ }
55
+ `.trim();
56
+
57
+ const css = commerce?.sidecartCss
58
+ ? `${baseline}\n\n/* --- tenant override --- */\n${commerce.sidecartCss}`
59
+ : baseline;
60
+ ---
61
+ {commerce && <style set:html={css} nonce={cspNonce}></style>}
@@ -0,0 +1,91 @@
1
+ ---
2
+ /**
3
+ * <PriceDisplay> — atom. Renders a product price with:
4
+ * - struck compare-at price when on sale,
5
+ * - a "From $X.XX" prefix when the product's variants span a price range,
6
+ * - an optional sale-percent badge.
7
+ *
8
+ * Tenant-agnostic: colours come from theme tokens (`text-sale`, `text-muted`).
9
+ * Pass a `product` (it derives everything) OR the primitives directly, so the
10
+ * atom is usable outside a CatalogProduct context (cart line, PDP, fixtures).
11
+ */
12
+ import type { CatalogProduct } from "@tot/public-runtime";
13
+ import { formatPrice, discountPercent } from "@/lib/format";
14
+ import { fromPrice, isOnSale } from "@/lib/tot/query";
15
+ import Badge from "@/components/Badge.astro";
16
+
17
+ interface Props {
18
+ /** Derive amount/compareAt/from/currency from a product. */
19
+ product?: CatalogProduct;
20
+ /** Explicit amount (overrides product). */
21
+ amount?: number;
22
+ /** Struck original price; only shown when > amount. */
23
+ compareAt?: number | null;
24
+ /** Prefix with "From " (variants span a range). */
25
+ from?: boolean;
26
+ currency?: string;
27
+ locale?: string;
28
+ /** Show the "-N%" sale badge next to the price. */
29
+ showBadge?: boolean;
30
+ /** Type scale. */
31
+ size?: "sm" | "md" | "lg";
32
+ class?: string;
33
+ }
34
+
35
+ const {
36
+ product,
37
+ amount: amountProp,
38
+ compareAt: compareAtProp,
39
+ from: fromProp,
40
+ currency: currencyProp,
41
+ locale = "en-US",
42
+ showBadge = false,
43
+ size = "md",
44
+ class: className,
45
+ } = Astro.props;
46
+
47
+ // Derive from the product when one is supplied; explicit props win.
48
+ const derivedAmount = product ? fromPrice(product) : 0;
49
+ const amount = amountProp ?? derivedAmount;
50
+ const currency = currencyProp ?? product?.price_range.currency ?? "USD";
51
+
52
+ const productHasRange = !!product && product.price_range.min !== product.price_range.max;
53
+ const from = fromProp ?? productHasRange;
54
+
55
+ // Highest compare-at across variants (only meaningful while on sale).
56
+ const derivedCompareAt =
57
+ product && isOnSale(product)
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
+ const compareAt = compareAtProp ?? derivedCompareAt;
65
+
66
+ const onSale = compareAt != null && compareAt > amount;
67
+ const percent = onSale ? discountPercent(amount, compareAt) : null;
68
+
69
+ const amountClass =
70
+ size === "lg" ? "text-lg" : size === "sm" ? "text-[0.8rem]" : "text-sm";
71
+ ---
72
+
73
+ <p class:list={["flex items-baseline gap-2", className]}>
74
+ <span
75
+ class:list={[
76
+ "font-display font-medium tabular-nums",
77
+ amountClass,
78
+ onSale && "text-sale",
79
+ ]}
80
+ >
81
+ {from && <span class="text-muted">From </span>}{formatPrice(amount, currency, locale)}
82
+ </span>
83
+ {
84
+ onSale && compareAt != null && (
85
+ <span class="text-xs text-muted line-through tabular-nums">
86
+ {formatPrice(compareAt, currency, locale)}
87
+ </span>
88
+ )
89
+ }
90
+ {showBadge && percent != null && <Badge variant="sale" label={`-${percent}%`} />}
91
+ </p>
@@ -0,0 +1,197 @@
1
+ ---
2
+ /**
3
+ * <ProductCarousel> — a horizontal-scroll row of <ProductCard>s bound to a
4
+ * SectionSource, with optional CSS-only tabs (each tab is its own SectionSource).
5
+ * Tenant-agnostic: the source(s) decide the products, theme tokens decide the
6
+ * skin. Presets (BestSellers / NewArrivals / OnSale) are just this component with
7
+ * a `SECTION_PRESETS.*` source — there are no bespoke preset components.
8
+ *
9
+ * Tabs are pure CSS (radio + adjacent-sibling reveal, labels pulled to the top
10
+ * row with flex `order`) — zero JS, so it works identically under the app Layout
11
+ * and in the standalone style guide.
12
+ *
13
+ * Data: pass `products` (already-resolved / fixtures) OR a `source` this resolves
14
+ * through `Astro.locals.tot`; or `tabs` for the multi-source tabbed variant.
15
+ */
16
+ import type { CatalogProduct } from "@tot/public-runtime";
17
+ import Section from "@/components/Section.astro";
18
+ import ProductCard from "@/components/ProductCard.astro";
19
+ import {
20
+ resolveSectionSource,
21
+ DEFAULT_SECTION_LIMIT,
22
+ type SectionSource,
23
+ } from "@/lib/sections";
24
+
25
+ interface Tab {
26
+ label: string;
27
+ source?: SectionSource;
28
+ /** Pre-resolved products for this tab (skips resolution). */
29
+ products?: CatalogProduct[];
30
+ }
31
+
32
+ interface Props {
33
+ source?: SectionSource;
34
+ /** Pre-resolved products (skips resolution). Wins over `source`. */
35
+ products?: CatalogProduct[];
36
+ /** Tabbed variant: each tab its own SectionSource. Wins over source/products. */
37
+ tabs?: Tab[];
38
+ limit?: number;
39
+ title?: string;
40
+ eyebrow?: string;
41
+ /** Visible-cards density at the lg breakpoint. */
42
+ columns?: 2 | 3 | 4 | 5;
43
+ /** `compact` tightens the gap. */
44
+ variant?: "default" | "compact";
45
+ tone?: "bg" | "surface";
46
+ class?: string;
47
+ }
48
+
49
+ const {
50
+ source,
51
+ products: provided,
52
+ tabs,
53
+ limit = DEFAULT_SECTION_LIMIT,
54
+ title,
55
+ eyebrow,
56
+ columns = 4,
57
+ variant = "default",
58
+ tone = "bg",
59
+ class: className,
60
+ } = Astro.props;
61
+
62
+ const tot = Astro.locals.tot;
63
+
64
+ async function resolveTab(t: Tab): Promise<CatalogProduct[]> {
65
+ if (t.products) return t.products;
66
+ return t.source ? resolveSectionSource(t.source, tot, limit) : [];
67
+ }
68
+
69
+ // Normalize everything to a list of panels so the template is uniform.
70
+ type Panel = { label: string; products: CatalogProduct[] };
71
+ let panels: Panel[];
72
+ if (tabs && tabs.length > 0) {
73
+ panels = await Promise.all(
74
+ tabs.map(async (t) => ({ label: t.label, products: await resolveTab(t) })),
75
+ );
76
+ } else {
77
+ const products =
78
+ provided ?? (source ? await resolveSectionSource(source, tot, limit) : []);
79
+ panels = [{ label: title ?? "", products }];
80
+ }
81
+
82
+ const showTabs = panels.length > 1;
83
+
84
+ // Unique-per-instance id so multiple carousels on one page don't share radios.
85
+ const uid = "carousel-" + Math.random().toString(36).slice(2, 9);
86
+
87
+ // lg card min-width by density (mobile/sm are fixed for a natural peek).
88
+ const lgWidth: Record<NonNullable<Props["columns"]>, string> = {
89
+ 2: "lg:min-w-[48%]",
90
+ 3: "lg:min-w-[31%]",
91
+ 4: "lg:min-w-[23%]",
92
+ 5: "lg:min-w-[18.4%]",
93
+ };
94
+ const gap = variant === "compact" ? "gap-3" : "gap-5";
95
+ ---
96
+
97
+ <Section title={title} eyebrow={eyebrow} tone={tone} class={className}>
98
+ <div class="tot-carousel">
99
+ {
100
+ panels.map((panel, i) => (
101
+ <Fragment>
102
+ {showTabs && (
103
+ <>
104
+ <input
105
+ class="tot-carousel__radio"
106
+ type="radio"
107
+ name={uid}
108
+ id={`${uid}-t${i}`}
109
+ checked={i === 0}
110
+ aria-label={panel.label}
111
+ />
112
+ <label class="tot-carousel__tab" for={`${uid}-t${i}`}>
113
+ {panel.label}
114
+ </label>
115
+ </>
116
+ )}
117
+ <div class="tot-carousel__panel">
118
+ {panel.products.length > 0 ? (
119
+ <ul
120
+ class:list={[
121
+ "flex snap-x snap-mandatory overflow-x-auto scroll-smooth pb-2",
122
+ gap,
123
+ "[scrollbar-width:thin]",
124
+ ]}
125
+ >
126
+ {panel.products.map((p, idx) => (
127
+ <li
128
+ class:list={[
129
+ "min-w-[72%] shrink-0 snap-start sm:min-w-[42%] md:min-w-[31%]",
130
+ lgWidth[columns],
131
+ ]}
132
+ >
133
+ <ProductCard product={p} position={idx} list={panel.label} />
134
+ </li>
135
+ ))}
136
+ </ul>
137
+ ) : (
138
+ <p class="py-8 text-center text-sm text-muted">No products to show.</p>
139
+ )}
140
+ </div>
141
+ </Fragment>
142
+ ))
143
+ }
144
+ </div>
145
+ </Section>
146
+
147
+ <style>
148
+ /* CSS-only tabs: radios hidden; labels pulled to a top row via flex order; the
149
+ checked radio's adjacent panel is the only one shown. No JS, CSP-clean. */
150
+ .tot-carousel {
151
+ display: flex;
152
+ flex-wrap: wrap;
153
+ align-items: flex-start;
154
+ }
155
+ .tot-carousel__radio {
156
+ position: absolute;
157
+ width: 1px;
158
+ height: 1px;
159
+ opacity: 0;
160
+ pointer-events: none;
161
+ }
162
+ .tot-carousel__tab {
163
+ order: -1;
164
+ margin: 0 0.5rem 1.25rem 0;
165
+ padding: 0.35rem 0.9rem;
166
+ border: 1px solid var(--color-border);
167
+ border-radius: var(--radius-full);
168
+ font-family: var(--font-mono);
169
+ font-size: 0.72rem;
170
+ letter-spacing: 0.08em;
171
+ text-transform: uppercase;
172
+ color: var(--color-muted);
173
+ cursor: pointer;
174
+ transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
175
+ }
176
+ .tot-carousel__tab:hover {
177
+ color: var(--color-text);
178
+ }
179
+ .tot-carousel__radio:focus-visible + .tot-carousel__tab {
180
+ outline: 2px solid var(--color-accent);
181
+ outline-offset: 2px;
182
+ }
183
+ .tot-carousel__radio:checked + .tot-carousel__tab {
184
+ background: var(--color-primary);
185
+ border-color: var(--color-primary);
186
+ color: var(--color-primary-contrast);
187
+ }
188
+ .tot-carousel__panel {
189
+ display: none;
190
+ width: 100%;
191
+ }
192
+ /* Single-panel (untabbed) carousels have no radio sibling — show always. */
193
+ .tot-carousel__panel:first-child,
194
+ .tot-carousel__radio:checked + .tot-carousel__tab + .tot-carousel__panel {
195
+ display: block;
196
+ }
197
+ </style>
@@ -0,0 +1,75 @@
1
+ ---
2
+ /**
3
+ * <ProductGrid> — a paginated-look grid of <ProductCard>s bound to a
4
+ * SectionSource. Thin sibling of <ProductCarousel> (same card, non-scrolling
5
+ * layout). Tenant-agnostic: the source decides the products, theme tokens decide
6
+ * the skin, `columns` tunes density.
7
+ *
8
+ * Data: pass `products` directly (e.g. fixtures / already-resolved) OR a `source`
9
+ * that this component resolves through `Astro.locals.tot`.
10
+ */
11
+ import type { CatalogProduct } from "@tot/public-runtime";
12
+ import Section from "@/components/Section.astro";
13
+ import ProductCard from "@/components/ProductCard.astro";
14
+ import EmptyState from "@/components/EmptyState.astro";
15
+ import {
16
+ resolveSectionSource,
17
+ DEFAULT_SECTION_LIMIT,
18
+ type SectionSource,
19
+ } from "@/lib/sections";
20
+
21
+ interface Props {
22
+ source?: SectionSource;
23
+ /** Pre-resolved products (skips resolution). Wins over `source`. */
24
+ products?: CatalogProduct[];
25
+ limit?: number;
26
+ title?: string;
27
+ eyebrow?: string;
28
+ /** Grid columns at the lg breakpoint (2 on mobile). */
29
+ columns?: 2 | 3 | 4 | 5;
30
+ tone?: "bg" | "surface";
31
+ /** Hide the whole section when there are no products (default true). */
32
+ hideWhenEmpty?: boolean;
33
+ class?: string;
34
+ }
35
+
36
+ const {
37
+ source,
38
+ products: provided,
39
+ limit = DEFAULT_SECTION_LIMIT,
40
+ title,
41
+ eyebrow,
42
+ columns = 4,
43
+ tone = "bg",
44
+ hideWhenEmpty = true,
45
+ class: className,
46
+ } = Astro.props;
47
+
48
+ const products =
49
+ provided ??
50
+ (source ? await resolveSectionSource(source, Astro.locals.tot, limit) : []);
51
+
52
+ const colClass: Record<NonNullable<Props["columns"]>, string> = {
53
+ 2: "grid-cols-2",
54
+ 3: "grid-cols-2 md:grid-cols-3",
55
+ 4: "grid-cols-2 lg:grid-cols-4",
56
+ 5: "grid-cols-2 md:grid-cols-3 lg:grid-cols-5",
57
+ };
58
+ const hidden = hideWhenEmpty && products.length === 0;
59
+ ---
60
+
61
+ {
62
+ !hidden && (
63
+ <Section title={title} eyebrow={eyebrow} tone={tone} class={className}>
64
+ {products.length > 0 ? (
65
+ <div class:list={["grid gap-5", colClass[columns]]}>
66
+ {products.map((p, i) => (
67
+ <ProductCard product={p} position={i} list={title} />
68
+ ))}
69
+ </div>
70
+ ) : (
71
+ <EmptyState title="Nothing here yet" body="Check back soon." />
72
+ )}
73
+ </Section>
74
+ )
75
+ }
@@ -0,0 +1,58 @@
1
+ ---
2
+ /**
3
+ * <RatingStars> — atom. A 0–5 star rating with fractional fill (a single clipped
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`.
6
+ *
7
+ * Presentation only — it renders whatever aggregate you pass. No review data is
8
+ * invented here (claims-ledger discipline: source ratings before shipping them).
9
+ */
10
+ interface Props {
11
+ /** 0–5. */
12
+ rating: number;
13
+ /** Optional review count shown after the stars. */
14
+ count?: number;
15
+ max?: number;
16
+ size?: "sm" | "md";
17
+ /** Show the numeric "(N)" count. */
18
+ showCount?: boolean;
19
+ class?: string;
20
+ }
21
+
22
+ const {
23
+ rating,
24
+ count,
25
+ max = 5,
26
+ size = "sm",
27
+ showCount = true,
28
+ class: className,
29
+ } = Astro.props;
30
+
31
+ const clamped = Math.max(0, Math.min(max, rating));
32
+ const pct = (clamped / max) * 100;
33
+ const dim = size === "md" ? "text-base" : "text-xs";
34
+ const stars = "★".repeat(max);
35
+ const label =
36
+ count != null
37
+ ? `Rated ${clamped} out of ${max} — ${count} review${count === 1 ? "" : "s"}`
38
+ : `Rated ${clamped} out of ${max}`;
39
+ ---
40
+
41
+ <span class:list={["inline-flex items-center gap-1.5", className]} aria-label={label}>
42
+ <span class:list={["relative inline-block leading-none tracking-[0.08em]", dim]} aria-hidden="true">
43
+ <span class="text-[var(--color-border)]">{stars}</span>
44
+ <span
45
+ class="absolute inset-0 overflow-hidden whitespace-nowrap text-[var(--color-accent)]"
46
+ style={`width:${pct}%`}
47
+ >
48
+ {stars}
49
+ </span>
50
+ </span>
51
+ {
52
+ showCount && count != null && (
53
+ <span class="font-mono text-[0.62rem] text-muted" aria-hidden="true">
54
+ ({count})
55
+ </span>
56
+ )
57
+ }
58
+ </span>