@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,190 @@
1
+ ---
2
+ /**
3
+ * <Header> — sticky site header: optional announcement bar, wordmark logo,
4
+ * desktop nav with CSS-only mega-menus (focus-within / group-hover, no JS),
5
+ * search typeahead island, and a mobile hamburger that mounts the MobileNav
6
+ * island. Theme-agnostic (CSS-var-backed utilities only).
7
+ */
8
+ import type { SiteChrome } from "@/lib/storyblok/content-model";
9
+ import SearchTypeahead from "@/components/islands/SearchTypeahead.tsx";
10
+ import MobileNav from "@/components/islands/MobileNav.tsx";
11
+ import { mergeTheme } from "@/themes/schema";
12
+ import { referenceTheme } from "@/themes/reference";
13
+ import { withBase } from "@/lib/basePath";
14
+ import { readViewerSession } from "@/lib/auth/route";
15
+
16
+ interface Props {
17
+ chrome: SiteChrome;
18
+ }
19
+
20
+ const { chrome } = Astro.props;
21
+ const { tenant, basePath, features } = Astro.locals;
22
+
23
+ // Simple, always-visible auth entry point: "Dashboard" when a session exists,
24
+ // "Sign in" otherwise. Without this there's no discoverable path from the
25
+ // storefront into sign-in / the vendor dashboard (#8425). On login-gated hosts
26
+ // the middleware already resolved the viewer (locals.viewer); reuse it and only
27
+ // read the session KV on open hosts where the gate never ran.
28
+ const viewer = Astro.locals.viewer ?? (await readViewerSession(Astro));
29
+ // Signed-out "Sign in" carries the current path as `next`, so after the email-code
30
+ // round-trip the visitor lands back where they were (e.g. the gated tenant page
31
+ // they were trying to reach) instead of the site root.
32
+ const nextParam = encodeURIComponent(Astro.url.pathname + Astro.url.search);
33
+ const authHref = viewer ? "/dashboard" : `/auth/login?next=${nextParam}`;
34
+ const authLabel = viewer ? "Dashboard" : "Sign in";
35
+
36
+ // Marketing tenants swap the commerce search/saved controls for conversion CTAs.
37
+ const headerCtas = chrome.headerCtas ?? [];
38
+ const ctaClass = (variant?: string) =>
39
+ variant === "secondary" || variant === "ghost"
40
+ ? "inline-flex items-center rounded-full border border-current px-4 py-2 font-display text-sm font-medium transition-opacity hover:opacity-80"
41
+ : "inline-flex items-center rounded-full bg-accent px-4 py-2 font-display text-sm font-medium text-[var(--color-primary-contrast)] transition-transform hover:-translate-y-0.5";
42
+
43
+ // Nav handed to the (client-rendered) MobileNav island with hrefs pre-prefixed,
44
+ // so it doesn't depend on the runtime base path inside the island.
45
+ const mobileNav = chrome.nav.map((item) => ({
46
+ ...item,
47
+ href: withBase(basePath, item.href),
48
+ children: item.children?.map((child) => ({
49
+ ...child,
50
+ href: withBase(basePath, child.href),
51
+ })),
52
+ }));
53
+
54
+ // Per-vendor header treatment from the theme file: a "dark" header renders the
55
+ // bar in the primary color carrying the light logo (e.g. Giant Vapes' navy +
56
+ // white logo); otherwise a light bar with the wordmark.
57
+ const brand = mergeTheme(referenceTheme, tenant.theme_tokens).brand;
58
+ const dark = brand.headerVariant === "dark";
59
+ const logo = dark ? brand.logoLight : brand.logoDark;
60
+ const showLogo = dark && !!logo && /^(https?:|\/)/.test(logo);
61
+ ---
62
+
63
+ <header class="sticky top-0 z-40">
64
+ {
65
+ chrome.announcement && (
66
+ <div class="bg-primary text-primary-contrast">
67
+ <p class="container-prose py-1.5 text-center font-mono text-[0.72rem] tracking-[0.08em]">
68
+ {chrome.announcement}
69
+ </p>
70
+ </div>
71
+ )
72
+ }
73
+
74
+ <div
75
+ class:list={[
76
+ "border-b backdrop-blur",
77
+ dark ? "border-[color-mix(in_srgb,var(--color-primary-contrast)_18%,var(--color-primary))] bg-primary text-primary-contrast" : "border-border bg-surface/95 text-text",
78
+ ]}
79
+ >
80
+ <div class="container-prose flex h-16 items-center justify-between gap-4">
81
+ <!-- Logo / wordmark -->
82
+ <a
83
+ href={withBase(basePath, "/")}
84
+ class="flex shrink-0 items-center transition-opacity hover:opacity-80"
85
+ aria-label={tenant.displayName}
86
+ >
87
+ {showLogo ? (
88
+ <img src={logo} alt={tenant.displayName} class="h-10 w-auto max-w-[160px] object-contain" width="160" height="64" />
89
+ ) : (
90
+ <span class="font-display text-lg font-bold tracking-tight">{tenant.displayName}</span>
91
+ )}
92
+ </a>
93
+
94
+ <!-- Desktop nav -->
95
+ <nav aria-label="Primary" class="hidden md:block">
96
+ <ul class="flex items-center gap-7">
97
+ {
98
+ chrome.nav.map((item) =>
99
+ item.children && item.children.length > 0 ? (
100
+ <li class="group relative">
101
+ <a
102
+ href={withBase(basePath, item.href)}
103
+ class="inline-flex items-center gap-1 py-2 text-sm transition-colors hover:text-accent group-focus-within:text-accent"
104
+ aria-haspopup="true"
105
+ >
106
+ {item.label}
107
+ <span aria-hidden="true" class="text-[0.7em] opacity-60">
108
+
109
+ </span>
110
+ </a>
111
+ <div class="invisible absolute left-0 top-full z-50 min-w-56 translate-y-1 opacity-0 transition-all duration-150 group-hover:visible group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:visible group-focus-within:translate-y-0 group-focus-within:opacity-100">
112
+ <ul class="mt-1 grid gap-0.5 rounded-md border border-border bg-surface p-3 shadow-md">
113
+ {item.children.map((child) => (
114
+ <li>
115
+ <a
116
+ href={withBase(basePath, child.href)}
117
+ class="block rounded-sm px-3 py-1.5 text-sm text-muted transition-colors hover:bg-bg hover:text-primary"
118
+ >
119
+ {child.label}
120
+ </a>
121
+ </li>
122
+ ))}
123
+ </ul>
124
+ </div>
125
+ </li>
126
+ ) : (
127
+ <li>
128
+ <a
129
+ href={withBase(basePath, item.href)}
130
+ class="py-2 text-sm transition-colors hover:text-accent"
131
+ >
132
+ {item.label}
133
+ </a>
134
+ </li>
135
+ ),
136
+ )
137
+ }
138
+ </ul>
139
+ </nav>
140
+
141
+ <!-- Right cluster: search + saved (commerce) or CTAs (marketing) + mobile menu -->
142
+ <div class="flex flex-1 items-center justify-end gap-2 md:max-w-sm">
143
+ {features.productSearch && <SearchTypeahead client:idle />}
144
+ {
145
+ features.savedItems && (
146
+ <a
147
+ href={withBase(basePath, "/saved")}
148
+ class="relative grid h-9 w-9 shrink-0 place-items-center rounded-full transition-colors hover:text-accent"
149
+ aria-label="Saved items"
150
+ >
151
+ <svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true">
152
+ <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" />
153
+ </svg>
154
+ <span
155
+ data-wishlist-count
156
+ hidden
157
+ 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-[var(--color-primary-contrast)]"
158
+ />
159
+ </a>
160
+ )
161
+ }
162
+ {
163
+ headerCtas.length > 0 && (
164
+ <div class="hidden items-center gap-2 sm:flex">
165
+ {headerCtas.map((cta) => (
166
+ <a href={withBase(basePath, cta.href)} class={ctaClass(cta.variant)}>
167
+ {cta.label}
168
+ </a>
169
+ ))}
170
+ </div>
171
+ )
172
+ }
173
+ <!-- Auth entry point: Sign in / Dashboard (#8425) -->
174
+ <a
175
+ href={authHref}
176
+ class="inline-flex shrink-0 items-center rounded-full border border-current px-4 py-2 font-display text-sm font-medium transition-opacity hover:opacity-80"
177
+ >
178
+ {authLabel}
179
+ </a>
180
+ <div class="md:hidden">
181
+ <MobileNav
182
+ client:idle
183
+ nav={mobileNav}
184
+ announcement={chrome.announcement}
185
+ />
186
+ </div>
187
+ </div>
188
+ </div>
189
+ </div>
190
+ </header>
@@ -0,0 +1,79 @@
1
+ ---
2
+ /**
3
+ * Active-filter chips (Shopify-style). Shows what's applied as removable pills
4
+ * above the grid — each chip is a link that drops exactly one filter, so it's
5
+ * URL-addressable and back-button safe like the rest of faceting (src/lib/url.ts).
6
+ */
7
+ import type { ListProductsQuery } from "@tot/public-runtime";
8
+ import { toggleFacet, buildQueryString } from "@/lib/url";
9
+
10
+ interface Props {
11
+ query: ListProductsQuery;
12
+ basePath: string;
13
+ }
14
+ const { query, basePath } = Astro.props;
15
+
16
+ type Chip = { label: string; href: string };
17
+ const chips: Chip[] = [];
18
+
19
+ // Option + tag facet values (toggling an active value removes it).
20
+ for (const [name, values] of Object.entries(query.filters ?? {})) {
21
+ for (const v of values) {
22
+ chips.push({
23
+ label: name === "tags" ? `#${v}` : `${name}: ${v}`,
24
+ href: basePath + toggleFacet(query, name, v),
25
+ });
26
+ }
27
+ }
28
+
29
+ // Price range.
30
+ if (query.priceMin != null || query.priceMax != null) {
31
+ const lo = query.priceMin != null ? `$${query.priceMin}` : "$0";
32
+ const hi = query.priceMax != null ? `$${query.priceMax}` : "any";
33
+ const { priceMin, priceMax, ...rest } = query;
34
+ chips.push({ label: `${lo}–${hi}`, href: basePath + buildQueryString(rest) });
35
+ }
36
+
37
+ // Availability (the meaningful filter is "in-stock"; "all" is the default).
38
+ if (query.availability === "in-stock") {
39
+ const { availability, ...rest } = query;
40
+ chips.push({ label: "In stock", href: basePath + buildQueryString(rest) });
41
+ }
42
+
43
+ const clearHref =
44
+ basePath +
45
+ buildQueryString({ sort: query.sort, pageSize: query.pageSize });
46
+ ---
47
+
48
+ {
49
+ chips.length > 0 && (
50
+ <div class="mb-5 flex flex-wrap items-center gap-2" data-active-filters>
51
+ <span class="sr-only">Active filters:</span>
52
+ {chips.map((chip) => (
53
+ <a
54
+ href={chip.href}
55
+ rel="nofollow"
56
+ class="group inline-flex items-center gap-1.5 rounded-full border border-[var(--color-border)] bg-[var(--color-surface)] py-1 pl-3 pr-2 text-xs transition-colors hover:border-[var(--color-accent-ink)] hover:text-[var(--color-accent-ink)]"
57
+ >
58
+ <span>{chip.label}</span>
59
+ <span
60
+ aria-hidden="true"
61
+ class="grid h-4 w-4 place-items-center rounded-full text-[var(--color-muted)] transition-colors group-hover:bg-[var(--color-accent-ink)] group-hover:text-[var(--color-primary-contrast)]"
62
+ >
63
+ ×
64
+ </span>
65
+ <span class="sr-only">Remove filter</span>
66
+ </a>
67
+ ))}
68
+ {chips.length > 1 && (
69
+ <a
70
+ href={clearHref}
71
+ rel="nofollow"
72
+ class="ml-1 text-xs text-[var(--color-muted)] underline underline-offset-2 hover:text-[var(--color-accent-ink)]"
73
+ >
74
+ Clear all
75
+ </a>
76
+ )}
77
+ </div>
78
+ )
79
+ }
@@ -0,0 +1,146 @@
1
+ ---
2
+ /**
3
+ * Faceted filtering UI (brief §5). URL-ADDRESSABLE and back-button safe: every
4
+ * facet is a real link that toggles a querystring param (no JS required for
5
+ * filtering). The optional mobile drawer toggle is the only island.
6
+ *
7
+ * Encoding handled by src/lib/url.ts (opt.<Name>=<value>, tag=, price, avail).
8
+ */
9
+ import type { Facet, ListProductsQuery } from "@tot/public-runtime";
10
+ import { toggleFacet, buildQueryString } from "@/lib/url";
11
+
12
+ interface Props {
13
+ facets: Facet[];
14
+ query: ListProductsQuery;
15
+ basePath: string; // e.g. /collections/bestsellers
16
+ }
17
+ const { facets, query, basePath } = Astro.props;
18
+
19
+ const selected = (name: string, value: string) =>
20
+ (query.filters?.[name] ?? []).includes(value);
21
+
22
+ // Order facets: option facets first, then tags, availability last.
23
+ const ordered = [...facets].sort((a, b) => {
24
+ const rank = (n: string) => (n === "availability" ? 2 : n === "tags" ? 1 : 0);
25
+ return rank(a.name) - rank(b.name);
26
+ });
27
+
28
+ const hasActiveFilters =
29
+ !!query.filters && Object.keys(query.filters).length > 0;
30
+
31
+ const clearHref =
32
+ basePath +
33
+ buildQueryString({ sort: query.sort, q: query.q, pageSize: query.pageSize });
34
+ ---
35
+
36
+ <aside aria-label="Filters" class="text-sm">
37
+ <div class="mb-4 flex items-center justify-between">
38
+ <h2 class="eyebrow !text-[var(--color-text)]">Filter</h2>
39
+ {hasActiveFilters && (
40
+ <a href={clearHref} class="text-xs text-[var(--color-muted)] underline underline-offset-2 hover:text-[var(--color-accent-ink)]">
41
+ Clear all
42
+ </a>
43
+ )}
44
+ </div>
45
+
46
+ <div class="space-y-7">
47
+ {
48
+ ordered.map((facet) => (
49
+ <fieldset>
50
+ <legend class="mb-2.5 font-display text-[0.95rem] font-medium">
51
+ {facet.name === "tags"
52
+ ? "Tags"
53
+ : facet.name === "availability"
54
+ ? "Availability"
55
+ : facet.name}
56
+ </legend>
57
+ <ul class="space-y-1.5">
58
+ {facet.values.map((fv) => {
59
+ const isSel = selected(facet.name, fv.value);
60
+ const href =
61
+ basePath + toggleFacet(query, facet.name, fv.value);
62
+ return (
63
+ <li>
64
+ <a
65
+ href={href}
66
+ rel="nofollow"
67
+ aria-pressed={isSel}
68
+ class:list={[
69
+ "flex items-center justify-between gap-3 rounded-[var(--radius-sm)] px-2 py-1 transition-colors",
70
+ isSel
71
+ ? "bg-[var(--color-primary)] text-[var(--color-primary-contrast)]"
72
+ : "hover:bg-[color-mix(in_srgb,var(--color-primary)_8%,transparent)]",
73
+ ]}
74
+ >
75
+ <span class="flex items-center gap-2">
76
+ <span
77
+ aria-hidden="true"
78
+ class:list={[
79
+ "grid h-3.5 w-3.5 place-items-center rounded-[2px] border",
80
+ isSel
81
+ ? "border-[var(--color-primary-contrast)] bg-[var(--color-primary-contrast)] text-[var(--color-primary)]"
82
+ : "border-[var(--color-border)]",
83
+ ]}
84
+ >
85
+ {isSel ? "✓" : ""}
86
+ </span>
87
+ {fv.value === "all" ? "All items" : fv.value}
88
+ </span>
89
+ <span class:list={["text-xs", isSel ? "opacity-80" : "text-[var(--color-muted)]"]}>
90
+ {fv.count}
91
+ </span>
92
+ </a>
93
+ </li>
94
+ );
95
+ })}
96
+ </ul>
97
+ </fieldset>
98
+ ))
99
+ }
100
+
101
+ {/* Price range — GET form, preserves other params via hidden inputs. */}
102
+ <form method="get" action={basePath} class="border-t border-[var(--color-border)] pt-5">
103
+ <fieldset>
104
+ <legend class="mb-2.5 font-display text-[0.95rem] font-medium">Price</legend>
105
+ {query.sort && <input type="hidden" name="sort" value={query.sort} />}
106
+ {query.availability && query.availability !== "all" && (
107
+ <input type="hidden" name="availability" value={query.availability} />
108
+ )}
109
+ {Object.entries(query.filters ?? {}).flatMap(([name, values]) =>
110
+ values.map((v) => (
111
+ <input type="hidden" name={name === "tags" ? "tag" : `opt.${name}`} value={v} />
112
+ )),
113
+ )}
114
+ <div class="flex items-center gap-2">
115
+ <label class="sr-only" for="priceMin">Minimum price</label>
116
+ <input
117
+ id="priceMin"
118
+ type="number"
119
+ name="priceMin"
120
+ min="0"
121
+ placeholder="Min"
122
+ value={query.priceMin ?? ""}
123
+ class="w-20 rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-bg)] px-2 py-1"
124
+ />
125
+ <span aria-hidden="true" class="text-[var(--color-muted)]">–</span>
126
+ <label class="sr-only" for="priceMax">Maximum price</label>
127
+ <input
128
+ id="priceMax"
129
+ type="number"
130
+ name="priceMax"
131
+ min="0"
132
+ placeholder="Max"
133
+ value={query.priceMax ?? ""}
134
+ class="w-20 rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-bg)] px-2 py-1"
135
+ />
136
+ <button
137
+ type="submit"
138
+ class="rounded-[var(--radius-sm)] bg-[var(--color-primary)] px-3 py-1 text-xs font-medium text-[var(--color-primary-contrast)]"
139
+ >
140
+ Go
141
+ </button>
142
+ </div>
143
+ </fieldset>
144
+ </form>
145
+ </div>
146
+ </aside>
@@ -0,0 +1,99 @@
1
+ ---
2
+ /**
3
+ * Numbered pagination (SEO-friendly, brief §5 default) + a "Load more" link.
4
+ * Both are plain anchors carrying the full query, so they're crawlable and
5
+ * back-button safe. "Load more" simply links to the next page; with JS it could
6
+ * be progressively enhanced to append, but the no-JS path stays correct.
7
+ */
8
+ import type { ListProductsQuery } from "@tot/public-runtime";
9
+ import { buildQueryString } from "@/lib/url";
10
+
11
+ interface Props {
12
+ query: ListProductsQuery;
13
+ basePath: string;
14
+ page: number;
15
+ pageSize: number;
16
+ total: number;
17
+ }
18
+ const { query, basePath, page, pageSize, total } = Astro.props;
19
+ const pageCount = Math.max(1, Math.ceil(total / pageSize));
20
+ if (pageCount <= 1) {
21
+ // nothing to render
22
+ }
23
+
24
+ const href = (p: number) => basePath + buildQueryString(query, { page: p });
25
+
26
+ // Compact page-number window.
27
+ const windowSize = 5;
28
+ let start = Math.max(1, page - Math.floor(windowSize / 2));
29
+ let end = Math.min(pageCount, start + windowSize - 1);
30
+ start = Math.max(1, end - windowSize + 1);
31
+ const pages = Array.from({ length: end - start + 1 }, (_, i) => start + i);
32
+ const hasNext = page < pageCount;
33
+ ---
34
+
35
+ {pageCount > 1 && (
36
+ <nav aria-label="Pagination" class="mt-12 flex flex-col items-center gap-6">
37
+ {hasNext && (
38
+ <a
39
+ href={href(page + 1)}
40
+ rel="next"
41
+ class="rounded-full border border-[var(--color-border)] bg-[var(--color-surface)] px-8 py-3 font-display text-sm font-medium transition-colors hover:border-[var(--color-accent)] hover:text-[var(--color-accent-ink)]"
42
+ >
43
+ Load more
44
+ </a>
45
+ )}
46
+
47
+ <ul class="flex items-center gap-1">
48
+ <li>
49
+ <a
50
+ href={href(Math.max(1, page - 1))}
51
+ rel="prev"
52
+ aria-label="Previous page"
53
+ aria-disabled={page === 1}
54
+ class:list={[
55
+ "grid h-9 w-9 place-items-center rounded-[var(--radius-sm)]",
56
+ page === 1 ? "pointer-events-none text-[var(--color-muted)] opacity-40" : "hover:bg-[var(--color-surface)]",
57
+ ]}
58
+ >‹</a>
59
+ </li>
60
+ {start > 1 && (
61
+ <>
62
+ <li><a href={href(1)} class="grid h-9 w-9 place-items-center rounded-[var(--radius-sm)] hover:bg-[var(--color-surface)]">1</a></li>
63
+ {start > 2 && <li aria-hidden="true" class="px-1 text-[var(--color-muted)]">…</li>}
64
+ </>
65
+ )}
66
+ {pages.map((p) => (
67
+ <li>
68
+ <a
69
+ href={href(p)}
70
+ aria-current={p === page ? "page" : undefined}
71
+ class:list={[
72
+ "grid h-9 w-9 place-items-center rounded-[var(--radius-sm)] font-display text-sm",
73
+ p === page
74
+ ? "bg-[var(--color-primary)] text-[var(--color-primary-contrast)]"
75
+ : "hover:bg-[var(--color-surface)]",
76
+ ]}
77
+ >{p}</a>
78
+ </li>
79
+ ))}
80
+ {end < pageCount && (
81
+ <>
82
+ {end < pageCount - 1 && <li aria-hidden="true" class="px-1 text-[var(--color-muted)]">…</li>}
83
+ <li><a href={href(pageCount)} class="grid h-9 w-9 place-items-center rounded-[var(--radius-sm)] hover:bg-[var(--color-surface)]">{pageCount}</a></li>
84
+ </>
85
+ )}
86
+ <li>
87
+ <a
88
+ href={href(Math.min(pageCount, page + 1))}
89
+ aria-label="Next page"
90
+ aria-disabled={!hasNext}
91
+ class:list={[
92
+ "grid h-9 w-9 place-items-center rounded-[var(--radius-sm)]",
93
+ !hasNext ? "pointer-events-none text-[var(--color-muted)] opacity-40" : "hover:bg-[var(--color-surface)]",
94
+ ]}
95
+ >›</a>
96
+ </li>
97
+ </ul>
98
+ </nav>
99
+ )}
@@ -0,0 +1,64 @@
1
+ ---
2
+ /**
3
+ * Sort control. A plain <form> select that auto-submits on change — works without
4
+ * a framework, stays URL-addressable. The current query's other params ride
5
+ * along as hidden inputs so sort doesn't drop active filters.
6
+ *
7
+ * Auto-submit is wired by a nonce'd `is:inline` <script> rather than an inline
8
+ * `onchange=` handler, so a strict, ENFORCED CSP allows it without 'unsafe-inline'
9
+ * / 'unsafe-hashes' (Epic S2). The <noscript> Apply button covers the no-JS case.
10
+ */
11
+ import type { ListProductsQuery } from "@tot/public-runtime";
12
+
13
+ interface Props {
14
+ query: ListProductsQuery;
15
+ basePath: string;
16
+ }
17
+ const { query, basePath } = Astro.props;
18
+
19
+ const OPTIONS: { value: NonNullable<ListProductsQuery["sort"]>; label: string }[] = [
20
+ { value: "featured", label: "Featured" },
21
+ { value: "newest", label: "Newest" },
22
+ { value: "price-asc", label: "Price: Low to High" },
23
+ { value: "price-desc", label: "Price: High to Low" },
24
+ { value: "title-asc", label: "Alphabetical (A–Z)" },
25
+ ];
26
+ const current = query.sort ?? "featured";
27
+ ---
28
+
29
+ <form method="get" action={basePath} class="flex items-center gap-2 text-sm">
30
+ {/* Preserve filters/price/availability/q across a sort change. */}
31
+ {query.q && <input type="hidden" name="q" value={query.q} />}
32
+ {query.availability && query.availability !== "all" && (
33
+ <input type="hidden" name="availability" value={query.availability} />
34
+ )}
35
+ {query.priceMin != null && <input type="hidden" name="priceMin" value={query.priceMin} />}
36
+ {query.priceMax != null && <input type="hidden" name="priceMax" value={query.priceMax} />}
37
+ {Object.entries(query.filters ?? {}).flatMap(([name, values]) =>
38
+ values.map((v) => (
39
+ <input type="hidden" name={name === "tags" ? "tag" : `opt.${name}`} value={v} />
40
+ )),
41
+ )}
42
+
43
+ <label for="sort" class="text-[var(--color-muted)]">Sort</label>
44
+ <select
45
+ id="sort"
46
+ name="sort"
47
+ data-sort-autosubmit
48
+ class="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-1.5 font-display"
49
+ >
50
+ {OPTIONS.map((o) => (
51
+ <option value={o.value} selected={o.value === current}>{o.label}</option>
52
+ ))}
53
+ </select>
54
+ <noscript><button type="submit" class="underline">Apply</button></noscript>
55
+ </form>
56
+
57
+ <script is:inline nonce={Astro.locals.cspNonce}>
58
+ // Progressive enhancement: submit the form when the sort changes. Nonce'd
59
+ // inline (the platform pattern for app-authored inline JS) so a strict,
60
+ // ENFORCED CSP allows it without 'unsafe-inline'.
61
+ for (const el of document.querySelectorAll("select[data-sort-autosubmit]")) {
62
+ el.addEventListener("change", () => el.form && el.form.submit());
63
+ }
64
+ </script>
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Dev-only tenant-registry seam for the localhost sign-in bypass.
3
+ *
4
+ * `lib/auth/devAuth` can't import the concrete static tenant map directly
5
+ * (tenancy-resolver-fence — the registry stays behind the `config` boundary so a
6
+ * single-tenant extraction stays a drop-in). This exposes exactly the one thing
7
+ * that bypass needs: the resources a local dev session is granted membership on.
8
+ * In a single-tenant extraction this collapses to that one tenant with no change
9
+ * to devAuth. Same shape of config seam as {@link ./previewTenantLookup}.
10
+ */
11
+ import { staticTenants } from "@/config/tenants";
12
+
13
+ /** Every registered tenant's resource id (appDomain, falling back to tenant_id). */
14
+ export function devTenantResources(): string[] {
15
+ return staticTenants.map((t) => t.appDomain ?? t.tenant_id);
16
+ }