@tokenoftrust/storefront-runner 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/LICENSE +58 -0
  2. package/README.md +40 -0
  3. package/apps/storefront/.dev.vars.example +41 -0
  4. package/apps/storefront/.env.example +24 -0
  5. package/apps/storefront/astro.config.mjs +245 -0
  6. package/apps/storefront/env.d.ts +155 -0
  7. package/apps/storefront/lib/ensure-preview-reconcile-secret.sh +31 -0
  8. package/apps/storefront/lib/resolve-cf-token.sh +48 -0
  9. package/apps/storefront/lib/resolve-tot-credentials.sh +69 -0
  10. package/apps/storefront/migrations/0000_baseline.sql +54 -0
  11. package/apps/storefront/migrations/0001_products_fts.sql +9 -0
  12. package/apps/storefront/migrations/README.md +54 -0
  13. package/apps/storefront/migrations/meta/0000_snapshot.json +347 -0
  14. package/apps/storefront/migrations/meta/0001_snapshot.json +347 -0
  15. package/apps/storefront/migrations/meta/_journal.json +20 -0
  16. package/apps/storefront/package.json +41 -0
  17. package/apps/storefront/public/brand/northwind-logo-dark.svg +4 -0
  18. package/apps/storefront/public/brand/northwind-logo-light.svg +4 -0
  19. package/apps/storefront/public/brand/northwind-og.svg +13 -0
  20. package/apps/storefront/public/favicon.svg +5 -0
  21. package/apps/storefront/public/js/dashboard-team.js +147 -0
  22. package/apps/storefront/public/js/dashboard-vendor-search.js +115 -0
  23. package/apps/storefront/src/components/Badge.astro +34 -0
  24. package/apps/storefront/src/components/Breadcrumbs.astro +45 -0
  25. package/apps/storefront/src/components/CollectionCard.astro +56 -0
  26. package/apps/storefront/src/components/EmptyState.astro +35 -0
  27. package/apps/storefront/src/components/Img.astro +68 -0
  28. package/apps/storefront/src/components/ProductCard.astro +228 -0
  29. package/apps/storefront/src/components/Section.astro +39 -0
  30. package/apps/storefront/src/components/Seo.astro +70 -0
  31. package/apps/storefront/src/components/WidgetFrame.astro +34 -0
  32. package/apps/storefront/src/components/auth/AuthBadge.astro +44 -0
  33. package/apps/storefront/src/components/chrome/AnnouncementBar.astro +108 -0
  34. package/apps/storefront/src/components/chrome/MegaMenu.astro +26 -0
  35. package/apps/storefront/src/components/chrome/NavDropdown.astro +116 -0
  36. package/apps/storefront/src/components/chrome/SiteFooter.astro +181 -0
  37. package/apps/storefront/src/components/chrome/SiteHeader.astro +181 -0
  38. package/apps/storefront/src/components/chrome/UtilityNav.astro +79 -0
  39. package/apps/storefront/src/components/commerce/FoxyLoader.astro +23 -0
  40. package/apps/storefront/src/components/commerce/PriceDisplay.astro +91 -0
  41. package/apps/storefront/src/components/commerce/ProductCarousel.astro +197 -0
  42. package/apps/storefront/src/components/commerce/ProductGrid.astro +75 -0
  43. package/apps/storefront/src/components/commerce/RatingStars.astro +58 -0
  44. package/apps/storefront/src/components/compliance/NicotineWarning.astro +33 -0
  45. package/apps/storefront/src/components/home/Hero.astro +164 -0
  46. package/apps/storefront/src/components/islands/AgeGate.tsx +105 -0
  47. package/apps/storefront/src/components/islands/ImageGallery.tsx +237 -0
  48. package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +73 -0
  49. package/apps/storefront/src/components/islands/MobileNav.tsx +239 -0
  50. package/apps/storefront/src/components/islands/QuickView.tsx +138 -0
  51. package/apps/storefront/src/components/islands/RecentlyViewed.tsx +122 -0
  52. package/apps/storefront/src/components/islands/SavedList.tsx +99 -0
  53. package/apps/storefront/src/components/islands/SearchTypeahead.tsx +248 -0
  54. package/apps/storefront/src/components/islands/VariantSelector.tsx +378 -0
  55. package/apps/storefront/src/components/marketing/CardGrid.astro +72 -0
  56. package/apps/storefront/src/components/marketing/CodeSample.astro +20 -0
  57. package/apps/storefront/src/components/marketing/CtaBand.astro +24 -0
  58. package/apps/storefront/src/components/marketing/Faq.astro +37 -0
  59. package/apps/storefront/src/components/marketing/Integrations.astro +63 -0
  60. package/apps/storefront/src/components/marketing/MarketingBlocks.astro +53 -0
  61. package/apps/storefront/src/components/marketing/MarketingCtas.astro +48 -0
  62. package/apps/storefront/src/components/marketing/MarketingHero.astro +72 -0
  63. package/apps/storefront/src/components/marketing/ProofStrip.astro +37 -0
  64. package/apps/storefront/src/components/marketing/SplitCompare.astro +53 -0
  65. package/apps/storefront/src/components/marketing/Steps.astro +36 -0
  66. package/apps/storefront/src/components/marketing/Testimonials.astro +38 -0
  67. package/apps/storefront/src/components/marketing/TrustBar.astro +22 -0
  68. package/apps/storefront/src/components/nav/Footer.astro +148 -0
  69. package/apps/storefront/src/components/nav/Header.astro +190 -0
  70. package/apps/storefront/src/components/plp/ActiveFilters.astro +79 -0
  71. package/apps/storefront/src/components/plp/FacetSidebar.astro +146 -0
  72. package/apps/storefront/src/components/plp/Pagination.astro +99 -0
  73. package/apps/storefront/src/components/plp/SortSelect.astro +64 -0
  74. package/apps/storefront/src/config/devTenants.ts +16 -0
  75. package/apps/storefront/src/config/kvTenantResolver.ts +87 -0
  76. package/apps/storefront/src/config/resolver.ts +46 -0
  77. package/apps/storefront/src/config/tenants.ts +193 -0
  78. package/apps/storefront/src/layouts/Layout.astro +303 -0
  79. package/apps/storefront/src/lib/analytics/index.ts +47 -0
  80. package/apps/storefront/src/lib/auth/brokerAssertion.ts +136 -0
  81. package/apps/storefront/src/lib/auth/devAuth.ts +90 -0
  82. package/apps/storefront/src/lib/auth/gatePage.ts +81 -0
  83. package/apps/storefront/src/lib/auth/identityToken.ts +341 -0
  84. package/apps/storefront/src/lib/auth/loginGate.ts +107 -0
  85. package/apps/storefront/src/lib/auth/route.ts +146 -0
  86. package/apps/storefront/src/lib/auth/session.ts +203 -0
  87. package/apps/storefront/src/lib/auth/totAccessClient.ts +503 -0
  88. package/apps/storefront/src/lib/basePath.ts +41 -0
  89. package/apps/storefront/src/lib/cfImage.ts +55 -0
  90. package/apps/storefront/src/lib/chrome/model.ts +65 -0
  91. package/apps/storefront/src/lib/colors.ts +97 -0
  92. package/apps/storefront/src/lib/content-edit/client.ts +230 -0
  93. package/apps/storefront/src/lib/content-edit/index.ts +14 -0
  94. package/apps/storefront/src/lib/content-edit/route.ts +25 -0
  95. package/apps/storefront/src/lib/content-edit/types.ts +19 -0
  96. package/apps/storefront/src/lib/cspStaticHashes.ts +24 -0
  97. package/apps/storefront/src/lib/d1/catalog.ts +289 -0
  98. package/apps/storefront/src/lib/dashboard/staffAdmission.ts +43 -0
  99. package/apps/storefront/src/lib/dashboard/staffVendorAccess.ts +449 -0
  100. package/apps/storefront/src/lib/dashboard/tenantSelection.ts +136 -0
  101. package/apps/storefront/src/lib/dashboard/vendorQuery.ts +61 -0
  102. package/apps/storefront/src/lib/demoGate.ts +113 -0
  103. package/apps/storefront/src/lib/dev/ai-widget-client.js +127 -0
  104. package/apps/storefront/src/lib/dev/cliSignInCode.ts +132 -0
  105. package/apps/storefront/src/lib/diag/redact.ts +20 -0
  106. package/apps/storefront/src/lib/edgeCache.ts +128 -0
  107. package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +62 -0
  108. package/apps/storefront/src/lib/env.ts +60 -0
  109. package/apps/storefront/src/lib/fixtures/sampleProducts.ts +130 -0
  110. package/apps/storefront/src/lib/format.ts +29 -0
  111. package/apps/storefront/src/lib/foxyCommerce.ts +49 -0
  112. package/apps/storefront/src/lib/imageAttrs.ts +55 -0
  113. package/apps/storefront/src/lib/jsonld.ts +136 -0
  114. package/apps/storefront/src/lib/maintenance.ts +71 -0
  115. package/apps/storefront/src/lib/memoryKv.ts +32 -0
  116. package/apps/storefront/src/lib/messaging/messagesClient.ts +143 -0
  117. package/apps/storefront/src/lib/placeholder.ts +99 -0
  118. package/apps/storefront/src/lib/previewReload.ts +125 -0
  119. package/apps/storefront/src/lib/rawMarketingHtml.ts +133 -0
  120. package/apps/storefront/src/lib/search/index.ts +74 -0
  121. package/apps/storefront/src/lib/sections.ts +129 -0
  122. package/apps/storefront/src/lib/securityHeaders.ts +31 -0
  123. package/apps/storefront/src/lib/storyblok/content-model.ts +289 -0
  124. package/apps/storefront/src/lib/storyblok/provider.ts +524 -0
  125. package/apps/storefront/src/lib/tenantRouting.ts +91 -0
  126. package/apps/storefront/src/lib/tot/ToTClient.ts +177 -0
  127. package/apps/storefront/src/lib/tot/d1Client.ts +62 -0
  128. package/apps/storefront/src/lib/tot/query.ts +187 -0
  129. package/apps/storefront/src/lib/tot/totClientInterface.ts +30 -0
  130. package/apps/storefront/src/lib/url.ts +119 -0
  131. package/apps/storefront/src/lib/wishlist.ts +63 -0
  132. package/apps/storefront/src/middleware/index.ts +413 -0
  133. package/apps/storefront/src/pages/404.astro +61 -0
  134. package/apps/storefront/src/pages/[...slug].astro +90 -0
  135. package/apps/storefront/src/pages/api/auth/logout.ts +21 -0
  136. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +107 -0
  137. package/apps/storefront/src/pages/api/auth/send.ts +85 -0
  138. package/apps/storefront/src/pages/api/auth/verify.ts +135 -0
  139. package/apps/storefront/src/pages/api/cache-purge.ts +44 -0
  140. package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +64 -0
  141. package/apps/storefront/src/pages/api/csp-report.ts +25 -0
  142. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +124 -0
  143. package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +120 -0
  144. package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +44 -0
  145. package/apps/storefront/src/pages/api/newsletter.ts +104 -0
  146. package/apps/storefront/src/pages/api/request-access.ts +172 -0
  147. package/apps/storefront/src/pages/auth/login.astro +149 -0
  148. package/apps/storefront/src/pages/auth/magic.astro +105 -0
  149. package/apps/storefront/src/pages/capabilities.astro +292 -0
  150. package/apps/storefront/src/pages/collections/[handle].astro +154 -0
  151. package/apps/storefront/src/pages/collections/index.astro +59 -0
  152. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +100 -0
  153. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +154 -0
  154. package/apps/storefront/src/pages/dashboard/index.astro +160 -0
  155. package/apps/storefront/src/pages/dev.astro +653 -0
  156. package/apps/storefront/src/pages/index.astro +290 -0
  157. package/apps/storefront/src/pages/llms.txt.ts +92 -0
  158. package/apps/storefront/src/pages/products/[handle].astro +443 -0
  159. package/apps/storefront/src/pages/robots.txt.ts +24 -0
  160. package/apps/storefront/src/pages/saved.astro +35 -0
  161. package/apps/storefront/src/pages/search.astro +150 -0
  162. package/apps/storefront/src/pages/sitemap.xml.ts +78 -0
  163. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +646 -0
  164. package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +85 -0
  165. package/apps/storefront/src/pages/style-guide/index.astro +94 -0
  166. package/apps/storefront/src/pages/tenants/[id]/[...path].ts +57 -0
  167. package/apps/storefront/src/styles/global.css +277 -0
  168. package/apps/storefront/src/themes/reference.ts +61 -0
  169. package/apps/storefront/src/themes/schema.ts +111 -0
  170. package/apps/storefront/src/themes/tenants/index.ts +51 -0
  171. package/apps/storefront/tsconfig.json +16 -0
  172. package/apps/storefront/vitest.config.ts +22 -0
  173. package/apps/storefront/wrangler.toml +175 -0
  174. package/package.json +23 -0
  175. package/packages/public-runtime/LICENSE +58 -0
  176. package/packages/public-runtime/package.json +22 -0
  177. package/packages/public-runtime/src/binary-path.ts +68 -0
  178. package/packages/public-runtime/src/capabilities.ts +89 -0
  179. package/packages/public-runtime/src/catalog-api.ts +81 -0
  180. package/packages/public-runtime/src/catalog-d1.ts +192 -0
  181. package/packages/public-runtime/src/csp.ts +109 -0
  182. package/packages/public-runtime/src/customization-preview.ts +197 -0
  183. package/packages/public-runtime/src/customization-reconcile.ts +63 -0
  184. package/packages/public-runtime/src/customization-runtime.ts +157 -0
  185. package/packages/public-runtime/src/customization-scripts.ts +64 -0
  186. package/packages/public-runtime/src/customization-versioning.ts +95 -0
  187. package/packages/public-runtime/src/foxy.ts +184 -0
  188. package/packages/public-runtime/src/index.ts +30 -0
  189. package/packages/public-runtime/src/product.ts +159 -0
  190. package/packages/public-runtime/src/search.ts +45 -0
  191. package/packages/public-runtime/src/tenant-assets.ts +444 -0
  192. package/packages/public-runtime/src/tenant.ts +243 -0
  193. package/packages/public-runtime/tsconfig.json +8 -0
  194. package/packages/public-runtime/vitest.config.ts +8 -0
  195. package/pnpm-workspace.yaml +7 -0
  196. package/scripts/build/copy-tenant-assets.mjs +69 -0
  197. package/scripts/tot-dev.mjs +439 -0
  198. package/tenants/bigdvapor/content/blog.json +52 -0
  199. package/tenants/bigdvapor/content/chrome.html +149 -0
  200. package/tenants/bigdvapor/content/chrome.json +47 -0
  201. package/tenants/bigdvapor/content/home.html +463 -0
  202. package/tenants/bigdvapor/content/home.json +73 -0
  203. package/tenants/bigdvapor/content/pages/about.json +5 -0
  204. package/tenants/bigdvapor/content/pages/privacy.json +6 -0
  205. package/tenants/bigdvapor/content/pages/shipping-returns.json +6 -0
  206. package/tenants/bigdvapor/content/pages-html/blogs/news.html +86 -0
  207. package/tenants/bigdvapor/content/pages-html/pages/about-us.html +106 -0
  208. package/tenants/bigdvapor/content/pages-html/pages/contact-us.html +61 -0
  209. package/tenants/bigdvapor/content/pages-html/pages/privacy-policy.html +72 -0
  210. package/tenants/bigdvapor/content/pages-html/pages/shipping-returns.html +172 -0
  211. package/tenants/bigdvapor/content/themes/bigd-navy.json +109 -0
  212. package/tenants/bigdvapor/public/img/brands/adjust-vape-disposable.png +0 -0
  213. package/tenants/bigdvapor/public/img/brands/again-vape-disposable.png +0 -0
  214. package/tenants/bigdvapor/public/img/brands/al-fakher-vape-disposable.png +0 -0
  215. package/tenants/bigdvapor/public/img/brands/arro-vape-disposable.png +0 -0
  216. package/tenants/bigdvapor/public/img/brands/cookies-vape-disposable.png +0 -0
  217. package/tenants/bigdvapor/public/img/brands/core-vape-disposable.png +0 -0
  218. package/tenants/bigdvapor/public/img/brands/cyclone-vape-disposable.png +0 -0
  219. package/tenants/bigdvapor/public/img/brands/dinner-lady-vape-disposable.png +0 -0
  220. package/tenants/bigdvapor/public/img/brands/extre-bar-vape-disposable.png +0 -0
  221. package/tenants/bigdvapor/public/img/brands/fifty-bar-vape-disposable.jpg +0 -0
  222. package/tenants/bigdvapor/public/img/brands/firerose-vape-disposable.jpg +0 -0
  223. package/tenants/bigdvapor/public/img/brands/flavor-beast-vape-disposable.jpg +0 -0
  224. package/tenants/bigdvapor/public/img/brands/foger-vape-disposable.png +0 -0
  225. package/tenants/bigdvapor/public/img/brands/kk-energy-vape-disposable.png +0 -0
  226. package/tenants/bigdvapor/public/img/brands/kumi-vape-disposable.png +0 -0
  227. package/tenants/bigdvapor/public/img/brands/lost-vape-dispoable.png +0 -0
  228. package/tenants/bigdvapor/public/img/brands/melo-labs-vape-disposable.png +0 -0
  229. package/tenants/bigdvapor/public/img/brands/mixo-vape-disposable.webp +0 -0
  230. package/tenants/bigdvapor/public/img/brands/one-tank-vape-disposable.jpg +0 -0
  231. package/tenants/bigdvapor/public/img/brands/oxbar-vape-disposable.webp +0 -0
  232. package/tenants/bigdvapor/public/img/brands/pillow-talk-vape-disposable.jpg +0 -0
  233. package/tenants/bigdvapor/public/img/brands/pod-king-vape-disposable.jpg +0 -0
  234. package/tenants/bigdvapor/public/img/brands/raz-vape-disposable.png +0 -0
  235. package/tenants/bigdvapor/public/img/hero-texas.jpg +0 -0
  236. package/tenants/bigdvapor/public/img/og.jpg +0 -0
  237. package/tenants/bigdvapor/public/img/pages/AdobeStock_202106312_web.jpg +0 -0
  238. package/tenants/bigdvapor/public/img/pages/CREW_web.jpg +0 -0
  239. package/tenants/bigdvapor/public/img/pages/Hero-Image.jpg +0 -0
  240. package/tenants/bigdvapor/public/img/tiles/adjust.png +0 -0
  241. package/tenants/bigdvapor/public/img/tiles/again.png +0 -0
  242. package/tenants/bigdvapor/public/img/tiles/al-fakher.png +0 -0
  243. package/tenants/bigdvapor/public/img/tiles/arro.png +0 -0
  244. package/tenants/bigdvapor/public/img/tiles/cookies.png +0 -0
  245. package/tenants/bigdvapor/public/img/tiles/core.png +0 -0
  246. package/tenants/bigdvapor/public/img/tiles/cyclone.png +0 -0
  247. package/tenants/bigdvapor/public/img/tiles/disposables.svg +23 -0
  248. package/tenants/bigdvapor/public/logo-wordmark.png +0 -0
  249. package/tenants/bigdvapor/public/pages/home.css +118 -0
  250. package/tenants/bigdvapor/public/pages/mkt.css +185 -0
  251. package/tenants/bigdvapor/public/pages/page.css +148 -0
  252. package/tenants/bigdvapor/public/themes/bigd-navy.css +73 -0
  253. package/tenants/bigdvapor/theme.json +12 -0
  254. package/tenants/giantvapes/content/chrome.html +152 -0
  255. package/tenants/giantvapes/content/chrome.json +94 -0
  256. package/tenants/giantvapes/content/home.html +194 -0
  257. package/tenants/giantvapes/content/home.json +50 -0
  258. package/tenants/giantvapes/content/pages/about.json +10 -0
  259. package/tenants/giantvapes/content/pages/privacy.json +6 -0
  260. package/tenants/giantvapes/content/pages/shipping-returns.json +6 -0
  261. package/tenants/giantvapes/content/pages-html/blogs/news.html +68 -0
  262. package/tenants/giantvapes/content/pages-html/pages/about-us.html +95 -0
  263. package/tenants/giantvapes/content/pages-html/pages/contact-us.html +68 -0
  264. package/tenants/giantvapes/content/pages-html/pages/privacy-policy.html +65 -0
  265. package/tenants/giantvapes/content/pages-html/pages/shipping-returns.html +77 -0
  266. package/tenants/giantvapes/content/themes/giant-navy.json +73 -0
  267. package/tenants/giantvapes/public/img/hero-suicide-bunny.jpg +0 -0
  268. package/tenants/giantvapes/public/img/hero.webp +0 -0
  269. package/tenants/giantvapes/public/img/og.jpg +0 -0
  270. package/tenants/giantvapes/public/logo-wordmark.png +0 -0
  271. package/tenants/giantvapes/public/pages/home.css +120 -0
  272. package/tenants/giantvapes/public/pages/mkt.css +185 -0
  273. package/tenants/giantvapes/public/pages/page.css +155 -0
  274. package/tenants/giantvapes/public/themes/giant-navy.css +76 -0
  275. package/tenants/giantvapes/theme.json +39 -0
  276. package/tenants/home/content/chrome.json +11 -0
  277. package/tenants/home/content/home.html +304 -0
  278. package/tenants/home/content/home.json +13 -0
  279. package/tenants/home/public/js/storefront.js +210 -0
  280. package/tenants/home/public/logo-wordmark.png +0 -0
  281. package/tenants/home/public/pages/storefront.css +212 -0
  282. package/tenants/home/theme.json +6 -0
  283. package/tenants/tokenoftrust/.tot/config.json +9 -0
  284. package/tenants/tokenoftrust/content/chrome.json +55 -0
  285. package/tenants/tokenoftrust/content/home.html +723 -0
  286. package/tenants/tokenoftrust/content/home.json +233 -0
  287. package/tenants/tokenoftrust/content/pages-html/careers.html +231 -0
  288. package/tenants/tokenoftrust/content/pages-html/company.html +275 -0
  289. package/tenants/tokenoftrust/content/pages-html/contact/contact-sales.html +188 -0
  290. package/tenants/tokenoftrust/content/pages-html/contact/startup-apply.html +206 -0
  291. package/tenants/tokenoftrust/content/pages-html/contact.html +178 -0
  292. package/tenants/tokenoftrust/content/pages-html/industries/adult-content-age-verification.html +286 -0
  293. package/tenants/tokenoftrust/content/pages-html/industries/cannabis.html +277 -0
  294. package/tenants/tokenoftrust/content/pages-html/pact-act-pricing.html +289 -0
  295. package/tenants/tokenoftrust/content/pages-html/pricing.html +483 -0
  296. package/tenants/tokenoftrust/content/pages-html/product/age-assurance/age-estimation.html +278 -0
  297. package/tenants/tokenoftrust/content/pages-html/product/age-assurance/age-gate.html +279 -0
  298. package/tenants/tokenoftrust/content/pages-html/product/age-assurance/age-verification.html +265 -0
  299. package/tenants/tokenoftrust/content/pages-html/product/age-assurance.html +173 -0
  300. package/tenants/tokenoftrust/content/pages-html/product/compliance/aml.html +214 -0
  301. package/tenants/tokenoftrust/content/pages-html/product/compliance/pact-act.html +226 -0
  302. package/tenants/tokenoftrust/content/pages-html/product/fraud-prevention.html +254 -0
  303. package/tenants/tokenoftrust/content/pages-html/product/social-profile-verification.html +212 -0
  304. package/tenants/tokenoftrust/content/pages-html/product/tax/excise-tax.html +213 -0
  305. package/tenants/tokenoftrust/content/pages-html/product/verification/aml.html +259 -0
  306. package/tenants/tokenoftrust/content/pages-html/product/verification/biometric-selfie.html +305 -0
  307. package/tenants/tokenoftrust/content/pages-html/product/verification/coverage.html +254 -0
  308. package/tenants/tokenoftrust/content/pages-html/product/verification/document-verification.html +306 -0
  309. package/tenants/tokenoftrust/content/pages-html/product/verification/electronic-identity-verification.html +304 -0
  310. package/tenants/tokenoftrust/content/pages-html/product/verification/government-id-verification.html +308 -0
  311. package/tenants/tokenoftrust/content/pages-html/product/verification.html +183 -0
  312. package/tenants/tokenoftrust/content/pages-html/product.html +228 -0
  313. package/tenants/tokenoftrust/content/pages-html/resources/integrations/bigcommerce-integration.html +220 -0
  314. package/tenants/tokenoftrust/content/pages-html/resources/integrations/shopify-integration.html +221 -0
  315. package/tenants/tokenoftrust/content/pages-html/resources/integrations/wordpress.html +220 -0
  316. package/tenants/tokenoftrust/content/pages-html/resources/integrations.html +172 -0
  317. package/tenants/tokenoftrust/content/pages-html/resources/refer-a-friend-program.html +317 -0
  318. package/tenants/tokenoftrust/content/pages-html/resources.html +177 -0
  319. package/tenants/tokenoftrust/content/pages-html/reviews.html +273 -0
  320. package/tenants/tokenoftrust/content/pages-html/solutions.html +292 -0
  321. package/tenants/tokenoftrust/content/pages-html/vlp-alcohol.html +289 -0
  322. package/tenants/tokenoftrust/content/pages-html/vlp-cigars-and-tobacco.html +286 -0
  323. package/tenants/tokenoftrust/content/pages-html/vlp-firearm-accessories.html +267 -0
  324. package/tenants/tokenoftrust/content/pages-html/vlp-peptides.html +273 -0
  325. package/tenants/tokenoftrust/content/themes/tot-navy.json +88 -0
  326. package/tenants/tokenoftrust/public/favicon.png +0 -0
  327. package/tenants/tokenoftrust/public/logo-icon.png +0 -0
  328. package/tenants/tokenoftrust/public/logo-wordmark.png +0 -0
  329. package/tenants/tokenoftrust/public/pages/company.css +108 -0
  330. package/tenants/tokenoftrust/public/pages/home.css +457 -0
  331. package/tenants/tokenoftrust/public/pages/mkt.css +482 -0
  332. package/tenants/tokenoftrust/public/pages/pricing.css +214 -0
  333. package/tenants/tokenoftrust/public/pages/solutions.css +66 -0
  334. package/tenants/tokenoftrust/public/themes/tot-navy-1.0.0.css +136 -0
  335. package/tenants/tokenoftrust/public/themes/tot-navy.css +358 -0
  336. package/tenants/tokenoftrust/theme.json +31 -0
  337. package/tsconfig.base.json +20 -0
@@ -0,0 +1,181 @@
1
+ ---
2
+ /**
3
+ * <SiteHeader> — logo/wordmark + search input + account + cart-count affordance,
4
+ * with the primary <MegaMenu> nav below and a zero-JS mobile disclosure.
5
+ *
6
+ * COMMERCE BOUNDARY: the cart is a LINK only (cart/checkout is FoxyCart, Phase 2)
7
+ * — no cart state, no add-to-cart. The count is a static affordance the FoxyCart
8
+ * layer hydrates later. Search is a plain GET form (progressive; the typeahead
9
+ * island can enhance it elsewhere). Tenant-agnostic; theme-token skinned.
10
+ */
11
+ import type { ChromeNavItem, ChromeLogo } from "@/lib/chrome/model";
12
+ import MegaMenu from "@/components/chrome/MegaMenu.astro";
13
+ import NavDropdown from "@/components/chrome/NavDropdown.astro";
14
+ import { withBase } from "@/lib/basePath";
15
+
16
+ interface Props {
17
+ logo?: ChromeLogo;
18
+ /** Fallback wordmark text when no logo image. */
19
+ wordmark?: string;
20
+ nav?: ChromeNavItem[];
21
+ /** Search form action (GET). Omit to hide search. */
22
+ searchAction?: string;
23
+ accountHref?: string;
24
+ /** Cart link target (FoxyCart, Phase 2). Omit to hide the cart affordance. */
25
+ cartHref?: string;
26
+ cartCount?: number;
27
+ sticky?: boolean;
28
+ class?: string;
29
+ }
30
+
31
+ const {
32
+ logo,
33
+ wordmark,
34
+ nav = [],
35
+ searchAction,
36
+ accountHref,
37
+ cartHref,
38
+ cartCount = 0,
39
+ sticky = true,
40
+ class: className,
41
+ } = Astro.props;
42
+
43
+ const { basePath } = Astro.locals;
44
+ const homeHref = withBase(basePath, "/");
45
+ const showLogo = !!logo && /^(https?:|\/)/.test(logo.src);
46
+ ---
47
+
48
+ <header class:list={[sticky && "sticky top-0 z-40", className]}>
49
+ <div class="border-b border-border bg-surface/95 text-text backdrop-blur">
50
+ <div class="container-prose flex h-16 items-center gap-4">
51
+ <!-- Logo / wordmark -->
52
+ <a
53
+ href={homeHref}
54
+ class="flex shrink-0 items-center transition-opacity hover:opacity-80"
55
+ aria-label={logo?.alt ?? wordmark ?? "Home"}
56
+ >
57
+ {
58
+ showLogo ? (
59
+ <img
60
+ src={logo!.src}
61
+ alt={logo!.alt}
62
+ width={logo!.width ?? 160}
63
+ height={logo!.height ?? 48}
64
+ class="h-9 w-auto max-w-[180px] object-contain"
65
+ />
66
+ ) : (
67
+ <span class="font-display text-lg font-bold tracking-tight">
68
+ {wordmark ?? logo?.alt ?? "Home"}
69
+ </span>
70
+ )
71
+ }
72
+ </a>
73
+
74
+ <!-- Search (progressive GET form) -->
75
+ {
76
+ searchAction && (
77
+ <form
78
+ action={withBase(basePath, searchAction)}
79
+ method="get"
80
+ role="search"
81
+ class="hidden flex-1 items-center md:flex"
82
+ >
83
+ <label class="sr-only" for="chrome-q">Search</label>
84
+ <div class="relative w-full max-w-md">
85
+ <input
86
+ id="chrome-q"
87
+ type="search"
88
+ name="q"
89
+ placeholder="Search for anything"
90
+ autocomplete="off"
91
+ class="w-full rounded-full border border-border bg-bg px-4 py-2 pr-10 text-sm text-text placeholder:text-muted focus-visible:border-accent"
92
+ />
93
+ <button
94
+ type="submit"
95
+ aria-label="Search"
96
+ class="absolute right-1 top-1/2 grid h-8 w-8 -translate-y-1/2 place-items-center rounded-full text-muted transition-colors hover:text-primary"
97
+ >
98
+ <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
99
+ <circle cx="11" cy="11" r="7" />
100
+ <path d="M21 21l-4.3-4.3" />
101
+ </svg>
102
+ </button>
103
+ </div>
104
+ </form>
105
+ )
106
+ }
107
+
108
+ <!-- Actions: account + cart (link only) + mobile menu -->
109
+ <div class="ml-auto flex items-center gap-1">
110
+ {
111
+ accountHref && (
112
+ <a
113
+ href={withBase(basePath, accountHref)}
114
+ aria-label="Account"
115
+ class="grid h-9 w-9 place-items-center rounded-full text-text transition-colors hover:text-accent"
116
+ >
117
+ <svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true">
118
+ <circle cx="12" cy="8" r="4" />
119
+ <path d="M4 21c0-4.4 3.6-7 8-7s8 2.6 8 7" />
120
+ </svg>
121
+ </a>
122
+ )
123
+ }
124
+ {
125
+ cartHref && (
126
+ <a
127
+ href={withBase(basePath, cartHref)}
128
+ aria-label="Cart"
129
+ class="relative grid h-9 w-9 place-items-center rounded-full text-text transition-colors hover:text-accent"
130
+ >
131
+ <svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true">
132
+ <path d="M6 6h15l-1.5 9h-12z" />
133
+ <circle cx="9" cy="20" r="1.4" />
134
+ <circle cx="18" cy="20" r="1.4" />
135
+ <path d="M6 6L5 3H2" />
136
+ </svg>
137
+ <span
138
+ data-cart-count
139
+ class="absolute -right-0.5 -top-0.5 grid h-4 min-w-4 place-items-center rounded-full bg-[var(--color-sale)] px-1 font-mono text-[0.6rem] font-medium text-white"
140
+ >
141
+ {cartCount}
142
+ </span>
143
+ </a>
144
+ )
145
+ }
146
+
147
+ <!-- Mobile menu: native <details>, zero JS -->
148
+ {
149
+ nav.length > 0 && (
150
+ <details class="relative md:hidden">
151
+ <summary
152
+ class="grid h-9 w-9 cursor-pointer list-none place-items-center rounded-full text-text transition-colors hover:text-accent [&::-webkit-details-marker]:hidden"
153
+ aria-label="Menu"
154
+ >
155
+ <svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
156
+ <path d="M4 7h16M4 12h16M4 17h16" />
157
+ </svg>
158
+ </summary>
159
+ <div class="absolute right-0 top-full z-50 mt-1 w-64 rounded-md border border-border bg-surface p-3 shadow-md">
160
+ <ul class="grid gap-0.5">
161
+ {nav.map((item) => <NavDropdown item={item} />)}
162
+ </ul>
163
+ </div>
164
+ </details>
165
+ )
166
+ }
167
+ </div>
168
+ </div>
169
+ </div>
170
+
171
+ <!-- Primary nav bar -->
172
+ {
173
+ nav.length > 0 && (
174
+ <div class="border-b border-border bg-surface">
175
+ <div class="container-prose">
176
+ <MegaMenu items={nav} />
177
+ </div>
178
+ </div>
179
+ )
180
+ }
181
+ </header>
@@ -0,0 +1,79 @@
1
+ ---
2
+ /**
3
+ * <UtilityNav> — the thin secondary bar above the header: minor links (News,
4
+ * About, Contact…), a locale/currency indicator, and social icons. Tenant-
5
+ * agnostic; theme-token skinned. Hidden on small screens (it's a desktop
6
+ * convenience — the mobile menu carries these links).
7
+ */
8
+ import type { ChromeLink, SocialLink } from "@/lib/chrome/model";
9
+ import { withBase } from "@/lib/basePath";
10
+
11
+ interface Props {
12
+ links?: ChromeLink[];
13
+ locale?: string;
14
+ currency?: string;
15
+ social?: SocialLink[];
16
+ class?: string;
17
+ }
18
+
19
+ const { links = [], locale, currency, social = [], class: className } = Astro.props;
20
+ const { basePath } = Astro.locals;
21
+ const hrefOf = (l: ChromeLink) => (l.external ? l.href : withBase(basePath, l.href));
22
+ ---
23
+
24
+ <div
25
+ class:list={[
26
+ "hidden border-b border-border bg-surface text-muted md:block",
27
+ className,
28
+ ]}
29
+ >
30
+ <div class="container-prose flex h-9 items-center justify-between gap-4 text-[0.72rem]">
31
+ <ul class="flex items-center gap-5">
32
+ {
33
+ links.map((l) => (
34
+ <li>
35
+ <a
36
+ href={hrefOf(l)}
37
+ class="transition-colors hover:text-primary"
38
+ {...(l.external ? { rel: "noopener noreferrer" } : {})}
39
+ >
40
+ {l.label}
41
+ </a>
42
+ </li>
43
+ ))
44
+ }
45
+ </ul>
46
+
47
+ <div class="flex items-center gap-4">
48
+ {locale && <span class="font-mono uppercase tracking-[0.08em]">{locale}</span>}
49
+ {currency && <span class="font-mono">{currency}</span>}
50
+ {
51
+ social.length > 0 && (
52
+ <span class="flex items-center gap-3">
53
+ {social.map((s) => (
54
+ <a
55
+ href={s.href}
56
+ aria-label={s.label}
57
+ rel="noopener noreferrer"
58
+ class="transition-colors hover:text-primary"
59
+ >
60
+ {s.iconPath ? (
61
+ <svg
62
+ class="h-4 w-4"
63
+ viewBox="0 0 24 24"
64
+ fill="currentColor"
65
+ aria-hidden="true"
66
+ >
67
+ <path d={s.iconPath} />
68
+ </svg>
69
+ ) : (
70
+ <span>{s.label}</span>
71
+ )}
72
+ </a>
73
+ ))}
74
+ </span>
75
+ )
76
+ }
77
+ </div>
78
+ </div>
79
+ </div>
@@ -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,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
+ }