@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,239 @@
1
+ /**
2
+ * <MobileNav> — hamburger button + slide-in drawer for small screens.
3
+ *
4
+ * Interactive island (Preact). The parent Header renders this inside a
5
+ * `md:hidden` wrapper; we also add `md:hidden` defensively so it never shows on
6
+ * desktop even if mounted standalone.
7
+ *
8
+ * A11y (WCAG 2.2 AA): hamburger exposes aria-expanded/aria-controls; the drawer
9
+ * traps focus, closes on Escape and backdrop click, locks body scroll, and
10
+ * restores focus to the hamburger on close. Motion is guarded by
11
+ * prefers-reduced-motion.
12
+ */
13
+ import { useCallback, useEffect, useRef, useState } from "preact/hooks";
14
+
15
+ interface NavChild {
16
+ label: string;
17
+ href: string;
18
+ }
19
+ interface NavItem {
20
+ label: string;
21
+ href: string;
22
+ children?: NavChild[];
23
+ }
24
+
25
+ interface Props {
26
+ nav: NavItem[];
27
+ announcement?: string;
28
+ }
29
+
30
+ const DRAWER_ID = "mobile-drawer";
31
+
32
+ function prefersReducedMotion(): boolean {
33
+ return (
34
+ typeof window !== "undefined" &&
35
+ typeof window.matchMedia === "function" &&
36
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches
37
+ );
38
+ }
39
+
40
+ export default function MobileNav({ nav, announcement }: Props) {
41
+ const [open, setOpen] = useState(false);
42
+ const buttonRef = useRef<HTMLButtonElement>(null);
43
+ const panelRef = useRef<HTMLDivElement>(null);
44
+ const reduced = prefersReducedMotion();
45
+
46
+ const close = useCallback(() => setOpen(false), []);
47
+
48
+ // Body scroll lock while open.
49
+ useEffect(() => {
50
+ if (!open) return;
51
+ const prev = document.body.style.overflow;
52
+ document.body.style.overflow = "hidden";
53
+ return () => {
54
+ document.body.style.overflow = prev;
55
+ };
56
+ }, [open]);
57
+
58
+ // Focus management: move focus into the panel on open, restore on close.
59
+ useEffect(() => {
60
+ if (open) {
61
+ // Focus the first focusable element (the close button) once painted.
62
+ const id = window.requestAnimationFrame(() => {
63
+ const first = panelRef.current?.querySelector<HTMLElement>(
64
+ "[data-autofocus]",
65
+ );
66
+ first?.focus();
67
+ });
68
+ return () => window.cancelAnimationFrame(id);
69
+ }
70
+ // On close, return focus to the hamburger.
71
+ buttonRef.current?.focus();
72
+ return undefined;
73
+ }, [open]);
74
+
75
+ // Escape to close + focus trap (Tab cycling) while open.
76
+ const onKeyDown = useCallback(
77
+ (e: KeyboardEvent) => {
78
+ if (e.key === "Escape") {
79
+ e.stopPropagation();
80
+ close();
81
+ return;
82
+ }
83
+ if (e.key !== "Tab") return;
84
+ const panel = panelRef.current;
85
+ if (!panel) return;
86
+ const focusable = Array.from(
87
+ panel.querySelectorAll<HTMLElement>(
88
+ 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])',
89
+ ),
90
+ ).filter((el) => el.offsetParent !== null || el === document.activeElement);
91
+ if (focusable.length === 0) return;
92
+ const firstEl = focusable[0]!;
93
+ const lastEl = focusable[focusable.length - 1]!;
94
+ const active = document.activeElement as HTMLElement | null;
95
+ if (e.shiftKey) {
96
+ if (active === firstEl || !panel.contains(active)) {
97
+ e.preventDefault();
98
+ lastEl.focus();
99
+ }
100
+ } else if (active === lastEl) {
101
+ e.preventDefault();
102
+ firstEl.focus();
103
+ }
104
+ },
105
+ [close],
106
+ );
107
+
108
+ return (
109
+ <div class="md:hidden">
110
+ <button
111
+ ref={buttonRef}
112
+ type="button"
113
+ aria-label="Open menu"
114
+ aria-haspopup="dialog"
115
+ aria-expanded={open}
116
+ aria-controls={DRAWER_ID}
117
+ onClick={() => setOpen(true)}
118
+ class="inline-flex h-10 w-10 items-center justify-center rounded-md text-text transition-colors hover:bg-surface focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
119
+ >
120
+ <svg
121
+ width="22"
122
+ height="22"
123
+ viewBox="0 0 24 24"
124
+ fill="none"
125
+ stroke="currentColor"
126
+ stroke-width="1.75"
127
+ stroke-linecap="round"
128
+ aria-hidden="true"
129
+ >
130
+ <line x1="3" y1="6" x2="21" y2="6" />
131
+ <line x1="3" y1="12" x2="21" y2="12" />
132
+ <line x1="3" y1="18" x2="21" y2="18" />
133
+ </svg>
134
+ </button>
135
+
136
+ {open && (
137
+ <div
138
+ class="fixed inset-0 z-50"
139
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
140
+ onKeyDown={onKeyDown}
141
+ >
142
+ {/* Backdrop */}
143
+ <div
144
+ class={`absolute inset-0 bg-text/40 ${
145
+ reduced ? "" : "transition-opacity duration-200"
146
+ }`}
147
+ onClick={close}
148
+ aria-hidden="true"
149
+ />
150
+ {/* Panel */}
151
+ <div
152
+ ref={panelRef}
153
+ id={DRAWER_ID}
154
+ role="dialog"
155
+ aria-modal="true"
156
+ aria-label="Site menu"
157
+ class="absolute inset-y-0 right-0 flex w-[85vw] max-w-sm flex-col overflow-y-auto bg-surface text-text shadow-xl"
158
+ style={
159
+ reduced
160
+ ? undefined
161
+ : "transform:translateX(100%);animation:tot-slidein .22s ease-out forwards"
162
+ }
163
+ >
164
+ <div class="flex items-center justify-between border-b border-border px-5 py-4">
165
+ <span class="font-display text-sm font-medium uppercase tracking-[0.14em] text-muted">
166
+ Menu
167
+ </span>
168
+ <button
169
+ type="button"
170
+ data-autofocus
171
+ aria-label="Close menu"
172
+ onClick={close}
173
+ class="inline-flex h-10 w-10 items-center justify-center rounded-md text-text transition-colors hover:bg-border/40 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
174
+ >
175
+ <svg
176
+ width="22"
177
+ height="22"
178
+ viewBox="0 0 24 24"
179
+ fill="none"
180
+ stroke="currentColor"
181
+ stroke-width="1.75"
182
+ stroke-linecap="round"
183
+ aria-hidden="true"
184
+ >
185
+ <line x1="6" y1="6" x2="18" y2="18" />
186
+ <line x1="18" y1="6" x2="6" y2="18" />
187
+ </svg>
188
+ </button>
189
+ </div>
190
+
191
+ {announcement && (
192
+ <p class="border-b border-border bg-primary px-5 py-2.5 text-center font-body text-sm text-primary-contrast">
193
+ {announcement}
194
+ </p>
195
+ )}
196
+
197
+ <nav aria-label="Mobile" class="flex-1 px-2 py-4">
198
+ <ul class="space-y-1">
199
+ {nav.map((item) => (
200
+ <li key={item.href}>
201
+ {item.children && item.children.length > 0 ? (
202
+ <div class="px-3 py-2">
203
+ <a
204
+ href={item.href}
205
+ class="block font-display text-base font-medium text-text hover:text-primary"
206
+ >
207
+ {item.label}
208
+ </a>
209
+ <ul class="mt-1 space-y-0.5 border-l border-border pl-3">
210
+ {item.children.map((child) => (
211
+ <li key={child.href}>
212
+ <a
213
+ href={child.href}
214
+ class="block rounded px-2 py-1.5 font-body text-sm text-muted hover:bg-border/40 hover:text-text"
215
+ >
216
+ {child.label}
217
+ </a>
218
+ </li>
219
+ ))}
220
+ </ul>
221
+ </div>
222
+ ) : (
223
+ <a
224
+ href={item.href}
225
+ class="block rounded px-3 py-2.5 font-display text-base font-medium text-text hover:bg-border/40 hover:text-primary"
226
+ >
227
+ {item.label}
228
+ </a>
229
+ )}
230
+ </li>
231
+ ))}
232
+ </ul>
233
+ </nav>
234
+ </div>
235
+ </div>
236
+ )}
237
+ </div>
238
+ );
239
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * <QuickView> — one global native <dialog> that any product card opens via a
3
+ * `[data-quickview]` button (event-delegated, so cards stay zero-JS). Shows a
4
+ * peek — image, price, colors, short description — and routes to the full PDP
5
+ * for variant selection. No cart action (Phase 1 browse-only). Native <dialog>
6
+ * gives us focus-trap + Esc + backdrop for free; we add backdrop-click + the
7
+ * reduced-motion-safe entrance via CSS.
8
+ */
9
+ import { useEffect, useRef, useState } from "preact/hooks";
10
+ import { formatPrice } from "@/lib/format";
11
+ import { placeholderDataUri, isUsableImageUrl } from "@/lib/placeholder";
12
+ import { withClientBase } from "@/lib/basePath";
13
+
14
+ interface QV {
15
+ handle: string;
16
+ title: string;
17
+ price: number;
18
+ currency: string;
19
+ vendor?: string;
20
+ desc?: string;
21
+ colors?: string[];
22
+ onSale?: boolean;
23
+ available?: boolean;
24
+ maxCompareAt?: number | null;
25
+ sku?: string | null;
26
+ images?: { url?: string; rehosted?: boolean; seed: string }[];
27
+ }
28
+
29
+ interface Props {
30
+ locale: string;
31
+ }
32
+
33
+ function imgSrc(im: { url?: string; rehosted?: boolean; seed: string }, title: string) {
34
+ return isUsableImageUrl(im.url, im.rehosted)
35
+ ? im.url!
36
+ : placeholderDataUri({ seed: im.seed, width: 700, height: 875, label: title });
37
+ }
38
+
39
+ export default function QuickView({ locale }: Props) {
40
+ const dialogRef = useRef<HTMLDialogElement>(null);
41
+ const [qv, setQv] = useState<QV | null>(null);
42
+
43
+ useEffect(() => {
44
+ function onClick(e: MouseEvent) {
45
+ const btn = (e.target as HTMLElement)?.closest?.("[data-quickview]");
46
+ if (!btn) return;
47
+ e.preventDefault();
48
+ try {
49
+ setQv(JSON.parse(btn.getAttribute("data-quickview")!) as QV);
50
+ dialogRef.current?.showModal();
51
+ } catch {
52
+ /* malformed payload — ignore, card link still works */
53
+ }
54
+ }
55
+ document.addEventListener("click", onClick);
56
+ return () => document.removeEventListener("click", onClick);
57
+ }, []);
58
+
59
+ const close = () => dialogRef.current?.close();
60
+
61
+ return (
62
+ <dialog
63
+ ref={dialogRef}
64
+ class="quickview m-auto w-[min(46rem,92vw)] rounded-[var(--radius-lg)] bg-[var(--color-surface)] p-0 text-[var(--color-text)] backdrop:bg-[color-mix(in_srgb,var(--color-text)_55%,transparent)]"
65
+ aria-label="Product quick view"
66
+ onClick={(e) => {
67
+ // Backdrop click closes (clicks inside the panel stop at the panel).
68
+ if (e.target === dialogRef.current) close();
69
+ }}
70
+ >
71
+ {qv && (
72
+ <div class="grid gap-0 sm:grid-cols-2">
73
+ <div class="aspect-[4/5] overflow-hidden bg-[var(--color-border)] sm:rounded-l-[var(--radius-lg)]">
74
+ <img
75
+ src={imgSrc(qv.images?.[0] ?? { seed: qv.handle }, qv.title)}
76
+ alt={qv.title}
77
+ width={700}
78
+ height={875}
79
+ class="h-full w-full object-cover"
80
+ />
81
+ </div>
82
+ <div class="relative p-6 sm:p-7">
83
+ <button
84
+ type="button"
85
+ onClick={close}
86
+ aria-label="Close quick view"
87
+ class="absolute right-3 top-3 grid h-9 w-9 place-items-center rounded-full text-[var(--color-muted)] hover:bg-[var(--color-bg)] hover:text-[var(--color-text)]"
88
+ >
89
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
90
+ <line x1="5" y1="5" x2="19" y2="19" /><line x1="19" y1="5" x2="5" y2="19" />
91
+ </svg>
92
+ </button>
93
+
94
+ {qv.vendor && (
95
+ <p class="font-mono text-[0.7rem] uppercase tracking-[0.14em] text-[var(--color-muted)]">
96
+ {qv.vendor}
97
+ </p>
98
+ )}
99
+ <h2 class="mt-1.5 font-display text-2xl font-semibold leading-tight">{qv.title}</h2>
100
+
101
+ <p class="mt-3 flex items-baseline gap-2">
102
+ <span class={`font-display text-xl tabular-nums ${qv.onSale ? "text-[var(--color-sale)]" : ""}`}>
103
+ {formatPrice(qv.price, qv.currency, locale)}
104
+ </span>
105
+ {qv.onSale && qv.maxCompareAt != null && (
106
+ <span class="text-sm text-[var(--color-muted)] line-through tabular-nums">
107
+ {formatPrice(qv.maxCompareAt, qv.currency, locale)}
108
+ </span>
109
+ )}
110
+ {qv.available === false && (
111
+ <span class="text-sm text-[var(--color-muted)]">· Sold out</span>
112
+ )}
113
+ </p>
114
+
115
+ {qv.colors && qv.colors.length > 0 && (
116
+ <p class="mt-3 text-sm text-[var(--color-muted)]">
117
+ {qv.colors.length} {qv.colors.length === 1 ? "color" : "colors"}: {qv.colors.join(", ")}
118
+ </p>
119
+ )}
120
+
121
+ {qv.desc && <p class="mt-4 text-sm leading-relaxed text-[var(--color-text)]">{qv.desc}</p>}
122
+
123
+ <a
124
+ href={withClientBase(`/products/${qv.handle}`)}
125
+ class="mt-6 inline-flex items-center gap-2 rounded-[var(--radius-md)] bg-[var(--color-primary)] px-5 py-2.5 font-display text-sm font-semibold text-[var(--color-primary-contrast)] transition-transform hover:gap-3"
126
+ >
127
+ View full details
128
+ <span aria-hidden="true">→</span>
129
+ </a>
130
+ {qv.sku && (
131
+ <p class="mt-4 font-mono text-[0.7rem] text-[var(--color-muted)]">SKU {qv.sku}</p>
132
+ )}
133
+ </div>
134
+ </div>
135
+ )}
136
+ </dialog>
137
+ );
138
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Recently viewed (Shopify-style discovery rail). A small client island that
3
+ * records the current product to localStorage on a PDP and renders the rest as
4
+ * a quiet strip. No server round-trip — snapshots are self-contained, so it
5
+ * works against fixtures or live ToT identically. Renders nothing until it has
6
+ * something to show (no layout shift, no empty state noise).
7
+ */
8
+ import { useEffect, useState } from "preact/hooks";
9
+ import { formatPrice } from "@/lib/format";
10
+ import { placeholderDataUri, isUsableImageUrl } from "@/lib/placeholder";
11
+ import { withClientBase } from "@/lib/basePath";
12
+
13
+ export interface RVItem {
14
+ handle: string;
15
+ title: string;
16
+ price: number;
17
+ currency: string;
18
+ vendor?: string;
19
+ img?: string;
20
+ rehosted?: boolean;
21
+ seed: string;
22
+ }
23
+
24
+ interface Props {
25
+ /** When set (on a PDP), this product is recorded and excluded from the rail. */
26
+ current?: RVItem;
27
+ locale: string;
28
+ title?: string;
29
+ max?: number;
30
+ }
31
+
32
+ const KEY = "tot:recently-viewed";
33
+ const CAP = 12;
34
+
35
+ function read(): RVItem[] {
36
+ try {
37
+ const raw = localStorage.getItem(KEY);
38
+ if (!raw) return [];
39
+ const arr = JSON.parse(raw);
40
+ return Array.isArray(arr) ? (arr as RVItem[]) : [];
41
+ } catch {
42
+ return [];
43
+ }
44
+ }
45
+
46
+ export default function RecentlyViewed({
47
+ current,
48
+ locale,
49
+ title = "Recently viewed",
50
+ max = 5,
51
+ }: Props) {
52
+ const [items, setItems] = useState<RVItem[]>([]);
53
+
54
+ useEffect(() => {
55
+ let list = read();
56
+ if (current) {
57
+ list = [current, ...list.filter((i) => i.handle !== current.handle)].slice(
58
+ 0,
59
+ CAP,
60
+ );
61
+ try {
62
+ localStorage.setItem(KEY, JSON.stringify(list));
63
+ } catch {
64
+ /* storage full / disabled — degrade silently */
65
+ }
66
+ }
67
+ // Exclude the current product from what we *show*.
68
+ setItems(
69
+ list.filter((i) => i.handle !== current?.handle).slice(0, max),
70
+ );
71
+ }, [current?.handle]);
72
+
73
+ if (items.length === 0) return null;
74
+
75
+ return (
76
+ <section
77
+ class="container-prose border-t border-[var(--color-border)] py-12"
78
+ aria-label={title}
79
+ >
80
+ <p class="eyebrow eyebrow--tick mb-5">{title}</p>
81
+ <ul class="grid grid-cols-2 gap-5 sm:grid-cols-3 lg:grid-cols-5">
82
+ {items.map((p) => {
83
+ const src = isUsableImageUrl(p.img, p.rehosted)
84
+ ? p.img!
85
+ : placeholderDataUri({
86
+ seed: p.seed,
87
+ width: 400,
88
+ height: 500,
89
+ label: p.title,
90
+ });
91
+ return (
92
+ <li>
93
+ <a href={withClientBase(`/products/${p.handle}`)} class="group block">
94
+ <div class="aspect-[4/5] overflow-hidden rounded-md bg-[var(--color-border)] ring-1 ring-[var(--color-border)]">
95
+ <img
96
+ src={src}
97
+ alt={p.title}
98
+ width={400}
99
+ height={500}
100
+ loading="lazy"
101
+ class="h-full w-full object-cover transition-transform duration-500 motion-safe:group-hover:scale-[1.03]"
102
+ />
103
+ </div>
104
+ {p.vendor && (
105
+ <p class="mt-2.5 font-mono text-[0.62rem] uppercase tracking-[0.14em] text-[var(--color-muted)]">
106
+ {p.vendor}
107
+ </p>
108
+ )}
109
+ <h3 class="mt-1 font-display text-[0.9rem] font-medium leading-snug transition-colors motion-safe:group-hover:text-[var(--color-primary)]">
110
+ {p.title}
111
+ </h3>
112
+ <p class="mt-1 font-display text-sm tabular-nums">
113
+ {formatPrice(p.price, p.currency, locale)}
114
+ </p>
115
+ </a>
116
+ </li>
117
+ );
118
+ })}
119
+ </ul>
120
+ </section>
121
+ );
122
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * <SavedList> — renders the wishlist from localStorage on the /saved page.
3
+ * Client-only (the list lives in the browser). Each item links to its PDP and
4
+ * can be removed inline; empty state invites browsing. Phase 2 swaps the store
5
+ * for an account-backed list without touching this component.
6
+ */
7
+ import { useEffect, useState } from "preact/hooks";
8
+ import {
9
+ readWishlist,
10
+ toggleSaved,
11
+ WISHLIST_EVENT,
12
+ type SavedItem,
13
+ } from "@/lib/wishlist";
14
+ import { formatPrice } from "@/lib/format";
15
+ import { placeholderDataUri, isUsableImageUrl } from "@/lib/placeholder";
16
+ import { withClientBase } from "@/lib/basePath";
17
+
18
+ interface Props {
19
+ locale: string;
20
+ }
21
+
22
+ export default function SavedList({ locale }: Props) {
23
+ const [items, setItems] = useState<SavedItem[] | null>(null);
24
+
25
+ useEffect(() => {
26
+ const refresh = () => setItems(readWishlist());
27
+ refresh();
28
+ window.addEventListener(WISHLIST_EVENT, refresh);
29
+ return () => window.removeEventListener(WISHLIST_EVENT, refresh);
30
+ }, []);
31
+
32
+ // null = pre-hydration (avoid a flash of the empty state).
33
+ if (items === null) return null;
34
+
35
+ if (items.length === 0) {
36
+ return (
37
+ <div class="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface)] px-6 py-16 text-center">
38
+ <p class="display-md">Nothing saved yet</p>
39
+ <p class="mx-auto mt-3 max-w-md text-[var(--color-muted)]">
40
+ Tap the heart on any product to keep it here for later. Saved items
41
+ live on this device.
42
+ </p>
43
+ <a
44
+ href={withClientBase("/collections/bestsellers")}
45
+ class="mt-6 inline-flex items-center gap-2 rounded-[var(--radius-md)] bg-[var(--color-primary)] px-5 py-2.5 font-display text-sm font-semibold text-[var(--color-primary-contrast)]"
46
+ >
47
+ Browse the shop <span aria-hidden="true">→</span>
48
+ </a>
49
+ </div>
50
+ );
51
+ }
52
+
53
+ return (
54
+ <ul class="grid grid-cols-2 gap-5 sm:grid-cols-3 lg:grid-cols-4">
55
+ {items.map((p) => {
56
+ const src = isUsableImageUrl(p.img, p.rehosted)
57
+ ? p.img!
58
+ : placeholderDataUri({ seed: p.seed, width: 400, height: 500, label: p.title });
59
+ return (
60
+ <li class="group relative">
61
+ <div class="relative aspect-[4/5] overflow-hidden rounded-md bg-[var(--color-border)] ring-1 ring-[var(--color-border)]">
62
+ <img
63
+ src={src}
64
+ alt={p.title}
65
+ width={400}
66
+ height={500}
67
+ loading="lazy"
68
+ class="h-full w-full object-cover transition-transform duration-500 motion-safe:group-hover:scale-[1.03]"
69
+ />
70
+ <button
71
+ type="button"
72
+ onClick={() => toggleSaved(p)}
73
+ aria-label={`Remove ${p.title} from saved`}
74
+ class="absolute right-2 top-2 z-10 grid h-9 w-9 place-items-center rounded-full bg-[color-mix(in_srgb,var(--color-surface)_88%,transparent)] text-[var(--color-sale)] backdrop-blur"
75
+ >
76
+ <svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
77
+ <path d="M12 21s-7.5-4.6-10-9.2C.4 8.6 2 5 5.5 5 8 5 9.4 6.6 12 9c2.6-2.4 4-4 6.5-4C22 5 23.6 8.6 22 11.8 19.5 16.4 12 21 12 21z" />
78
+ </svg>
79
+ </button>
80
+ </div>
81
+ <a href={withClientBase(`/products/${p.handle}`)} class="mt-3 block after:absolute after:inset-0">
82
+ {p.vendor && (
83
+ <span class="block font-mono text-[0.62rem] uppercase tracking-[0.14em] text-[var(--color-muted)]">
84
+ {p.vendor}
85
+ </span>
86
+ )}
87
+ <span class="mt-1 block font-display text-[0.95rem] font-medium leading-snug">
88
+ {p.title}
89
+ </span>
90
+ <span class="mt-1 block font-display text-sm tabular-nums">
91
+ {formatPrice(p.price, p.currency, locale)}
92
+ </span>
93
+ </a>
94
+ </li>
95
+ );
96
+ })}
97
+ </ul>
98
+ );
99
+ }