@tokenoftrust/storefront-runner 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) hide show
  1. package/LICENSE +58 -0
  2. package/README.md +42 -0
  3. package/apps/storefront/astro.config.mjs +66 -0
  4. package/apps/storefront/dev-plugins/dev-loop-state.mjs +147 -0
  5. package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +238 -0
  6. package/apps/storefront/env.d.ts +162 -0
  7. package/apps/storefront/migrations/0000_baseline.sql +54 -0
  8. package/apps/storefront/migrations/0001_products_fts.sql +9 -0
  9. package/apps/storefront/migrations/README.md +54 -0
  10. package/apps/storefront/migrations/meta/0000_snapshot.json +347 -0
  11. package/apps/storefront/migrations/meta/0001_snapshot.json +347 -0
  12. package/apps/storefront/migrations/meta/_journal.json +20 -0
  13. package/apps/storefront/package.json +42 -0
  14. package/apps/storefront/public/brand/northwind-logo-dark.svg +4 -0
  15. package/apps/storefront/public/brand/northwind-logo-light.svg +4 -0
  16. package/apps/storefront/public/brand/northwind-og.svg +13 -0
  17. package/apps/storefront/public/favicon.svg +5 -0
  18. package/apps/storefront/public/js/dashboard-team.js +147 -0
  19. package/apps/storefront/public/js/dashboard-vendor-search.js +115 -0
  20. package/apps/storefront/public/shared/commerce-chrome.css +369 -0
  21. package/apps/storefront/src/components/Badge.astro +34 -0
  22. package/apps/storefront/src/components/Breadcrumbs.astro +45 -0
  23. package/apps/storefront/src/components/CollectionCard.astro +56 -0
  24. package/apps/storefront/src/components/EmptyState.astro +55 -0
  25. package/apps/storefront/src/components/Img.astro +68 -0
  26. package/apps/storefront/src/components/ProductCard.astro +228 -0
  27. package/apps/storefront/src/components/Section.astro +39 -0
  28. package/apps/storefront/src/components/Seo.astro +70 -0
  29. package/apps/storefront/src/components/WidgetFrame.astro +34 -0
  30. package/apps/storefront/src/components/admin/AdminAIWorkflowsTab.astro +531 -0
  31. package/apps/storefront/src/components/admin/AdminCustomersTab.astro +597 -0
  32. package/apps/storefront/src/components/admin/AdminFoxyCartSettingsPanel.astro +227 -0
  33. package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +465 -0
  34. package/apps/storefront/src/components/admin/AdminProductsTab.astro +901 -0
  35. package/apps/storefront/src/components/admin/AdminReportingTab.astro +435 -0
  36. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +782 -0
  37. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +1223 -0
  38. package/apps/storefront/src/components/auth/AuthBadge.astro +44 -0
  39. package/apps/storefront/src/components/chrome/AnnouncementBar.astro +108 -0
  40. package/apps/storefront/src/components/chrome/MegaMenu.astro +26 -0
  41. package/apps/storefront/src/components/chrome/NavDropdown.astro +201 -0
  42. package/apps/storefront/src/components/chrome/SiteFooter.astro +201 -0
  43. package/apps/storefront/src/components/chrome/SiteHeader.astro +234 -0
  44. package/apps/storefront/src/components/chrome/UtilityNav.astro +79 -0
  45. package/apps/storefront/src/components/commerce/ArticleCard.astro +242 -0
  46. package/apps/storefront/src/components/commerce/BrandGrid.astro +175 -0
  47. package/apps/storefront/src/components/commerce/FoxyLoader.astro +23 -0
  48. package/apps/storefront/src/components/commerce/FoxySidecartStyles.astro +61 -0
  49. package/apps/storefront/src/components/commerce/PriceDisplay.astro +91 -0
  50. package/apps/storefront/src/components/commerce/ProductCarousel.astro +197 -0
  51. package/apps/storefront/src/components/commerce/ProductGrid.astro +75 -0
  52. package/apps/storefront/src/components/commerce/RatingStars.astro +58 -0
  53. package/apps/storefront/src/components/commerce/ReviewList.astro +54 -0
  54. package/apps/storefront/src/components/commerce/ReviewSummary.astro +44 -0
  55. package/apps/storefront/src/components/commerce/TagFilterCloud.astro +149 -0
  56. package/apps/storefront/src/components/compliance/AdultSignatureNotice.astro +37 -0
  57. package/apps/storefront/src/components/compliance/ComplianceNotice.astro +99 -0
  58. package/apps/storefront/src/components/compliance/ExciseTaxNotice.astro +45 -0
  59. package/apps/storefront/src/components/compliance/NicotineWarning.astro +33 -0
  60. package/apps/storefront/src/components/compliance/PactActNotice.astro +35 -0
  61. package/apps/storefront/src/components/compliance/Prop65Warning.astro +80 -0
  62. package/apps/storefront/src/components/compliance/PurchaseLimitNotice.astro +38 -0
  63. package/apps/storefront/src/components/compliance/ShippingRestrictionNotice.astro +35 -0
  64. package/apps/storefront/src/components/compliance/StateEligibilityNotice.astro +53 -0
  65. package/apps/storefront/src/components/home/Hero.astro +164 -0
  66. package/apps/storefront/src/components/islands/AgeGate.tsx +109 -0
  67. package/apps/storefront/src/components/islands/ImageGallery.tsx +237 -0
  68. package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +73 -0
  69. package/apps/storefront/src/components/islands/MobileNav.tsx +239 -0
  70. package/apps/storefront/src/components/islands/QuickView.tsx +138 -0
  71. package/apps/storefront/src/components/islands/RecentlyViewed.tsx +122 -0
  72. package/apps/storefront/src/components/islands/SavedList.tsx +99 -0
  73. package/apps/storefront/src/components/islands/SearchTypeahead.tsx +248 -0
  74. package/apps/storefront/src/components/islands/VariantSelector.tsx +375 -0
  75. package/apps/storefront/src/components/marketing/CardGrid.astro +72 -0
  76. package/apps/storefront/src/components/marketing/CodeSample.astro +20 -0
  77. package/apps/storefront/src/components/marketing/CtaBand.astro +24 -0
  78. package/apps/storefront/src/components/marketing/Faq.astro +37 -0
  79. package/apps/storefront/src/components/marketing/Integrations.astro +63 -0
  80. package/apps/storefront/src/components/marketing/MarketingBlocks.astro +53 -0
  81. package/apps/storefront/src/components/marketing/MarketingCtas.astro +48 -0
  82. package/apps/storefront/src/components/marketing/MarketingHero.astro +72 -0
  83. package/apps/storefront/src/components/marketing/ProofStrip.astro +37 -0
  84. package/apps/storefront/src/components/marketing/SplitCompare.astro +53 -0
  85. package/apps/storefront/src/components/marketing/Steps.astro +36 -0
  86. package/apps/storefront/src/components/marketing/Testimonials.astro +38 -0
  87. package/apps/storefront/src/components/marketing/TrustBar.astro +22 -0
  88. package/apps/storefront/src/components/membership/IllustrativeLabel.astro +56 -0
  89. package/apps/storefront/src/components/membership/TierCards.astro +164 -0
  90. package/apps/storefront/src/components/membership/TierComparisonTable.astro +148 -0
  91. package/apps/storefront/src/components/plp/ActiveFilters.astro +79 -0
  92. package/apps/storefront/src/components/plp/FacetSidebar.astro +199 -0
  93. package/apps/storefront/src/components/plp/Pagination.astro +99 -0
  94. package/apps/storefront/src/components/plp/SortSelect.astro +64 -0
  95. package/apps/storefront/src/components/style-guide/StyleGuideNav.astro +95 -0
  96. package/apps/storefront/src/components/subscription/HowItWorksSteps.astro +67 -0
  97. package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +67 -0
  98. package/apps/storefront/src/components/subscription/SavingsExplainer.astro +101 -0
  99. package/apps/storefront/src/config/adminTargetTenant.ts +28 -0
  100. package/apps/storefront/src/config/devTenantSeed.ts +78 -0
  101. package/apps/storefront/src/config/devTenants.ts +16 -0
  102. package/apps/storefront/src/config/kvTenantResolver.ts +87 -0
  103. package/apps/storefront/src/config/resolver.ts +102 -0
  104. package/apps/storefront/src/config/tenants.ts +87 -0
  105. package/apps/storefront/src/layouts/Layout.astro +368 -0
  106. package/apps/storefront/src/lib/admin/README.md +122 -0
  107. package/apps/storefront/src/lib/admin/ai/approvalMode.ts +21 -0
  108. package/apps/storefront/src/lib/admin/ai/askEnvelope.ts +48 -0
  109. package/apps/storefront/src/lib/admin/ai/audit.ts +14 -0
  110. package/apps/storefront/src/lib/admin/ai/index.ts +6 -0
  111. package/apps/storefront/src/lib/admin/ai/operationalRuns.ts +155 -0
  112. package/apps/storefront/src/lib/admin/ai/recommendation.ts +20 -0
  113. package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +172 -0
  114. package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +65 -0
  115. package/apps/storefront/src/lib/admin/envelope.ts +10 -0
  116. package/apps/storefront/src/lib/admin/evidence/evidenceReference.ts +32 -0
  117. package/apps/storefront/src/lib/admin/memoizeByEnvelope.ts +27 -0
  118. package/apps/storefront/src/lib/admin/orderClient.ts +146 -0
  119. package/apps/storefront/src/lib/admin/seed.ts +54 -0
  120. package/apps/storefront/src/lib/admin/services/mock/aiWorkflows.ts +59 -0
  121. package/apps/storefront/src/lib/admin/services/mock/customers.ts +86 -0
  122. package/apps/storefront/src/lib/admin/services/mock/evidence.ts +99 -0
  123. package/apps/storefront/src/lib/admin/services/mock/foxyCartSettings.ts +78 -0
  124. package/apps/storefront/src/lib/admin/services/mock/fulfillment.ts +62 -0
  125. package/apps/storefront/src/lib/admin/services/mock/index.ts +12 -0
  126. package/apps/storefront/src/lib/admin/services/mock/orders.ts +184 -0
  127. package/apps/storefront/src/lib/admin/services/mock/products.ts +105 -0
  128. package/apps/storefront/src/lib/admin/services/mock/reporting.ts +51 -0
  129. package/apps/storefront/src/lib/admin/services/mock/settings.ts +70 -0
  130. package/apps/storefront/src/lib/admin/services/mock/tenants.ts +29 -0
  131. package/apps/storefront/src/lib/admin/services/orders.ts +300 -0
  132. package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +67 -0
  133. package/apps/storefront/src/lib/admin/viewmodels/customers.ts +56 -0
  134. package/apps/storefront/src/lib/admin/viewmodels/fulfillment.ts +49 -0
  135. package/apps/storefront/src/lib/admin/viewmodels/orders.ts +92 -0
  136. package/apps/storefront/src/lib/admin/viewmodels/products.ts +74 -0
  137. package/apps/storefront/src/lib/admin/viewmodels/reporting.ts +34 -0
  138. package/apps/storefront/src/lib/admin/viewmodels/settings.ts +64 -0
  139. package/apps/storefront/src/lib/adminTenantEnvelope.ts +90 -0
  140. package/apps/storefront/src/lib/analytics/index.ts +47 -0
  141. package/apps/storefront/src/lib/auth/brokerAssertion.ts +136 -0
  142. package/apps/storefront/src/lib/auth/devAuth.ts +90 -0
  143. package/apps/storefront/src/lib/auth/gatePage.ts +81 -0
  144. package/apps/storefront/src/lib/auth/identityToken.ts +341 -0
  145. package/apps/storefront/src/lib/auth/loginGate.ts +119 -0
  146. package/apps/storefront/src/lib/auth/route.ts +146 -0
  147. package/apps/storefront/src/lib/auth/session.ts +203 -0
  148. package/apps/storefront/src/lib/auth/totAccessClient.ts +503 -0
  149. package/apps/storefront/src/lib/basePath.ts +41 -0
  150. package/apps/storefront/src/lib/cfImage.ts +55 -0
  151. package/apps/storefront/src/lib/chrome/model.ts +244 -0
  152. package/apps/storefront/src/lib/chrome/parseConfig.ts +34 -0
  153. package/apps/storefront/src/lib/colors.ts +97 -0
  154. package/apps/storefront/src/lib/commerce/foxyCartSettings.ts +253 -0
  155. package/apps/storefront/src/lib/content-edit/applyDiff.ts +91 -0
  156. package/apps/storefront/src/lib/content-edit/client.ts +230 -0
  157. package/apps/storefront/src/lib/content-edit/index.ts +14 -0
  158. package/apps/storefront/src/lib/content-edit/route.ts +25 -0
  159. package/apps/storefront/src/lib/content-edit/types.ts +19 -0
  160. package/apps/storefront/src/lib/cspStaticHashes.ts +24 -0
  161. package/apps/storefront/src/lib/d1/catalog.ts +366 -0
  162. package/apps/storefront/src/lib/dashboard/staffAdmission.ts +43 -0
  163. package/apps/storefront/src/lib/dashboard/staffVendorAccess.ts +449 -0
  164. package/apps/storefront/src/lib/dashboard/tenantSelection.ts +136 -0
  165. package/apps/storefront/src/lib/dashboard/vendorQuery.ts +61 -0
  166. package/apps/storefront/src/lib/demoGate.ts +113 -0
  167. package/apps/storefront/src/lib/dev/activityIngestToken.ts +78 -0
  168. package/apps/storefront/src/lib/dev/activityStore.ts +72 -0
  169. package/apps/storefront/src/lib/dev/ai-widget-client.js +308 -0
  170. package/apps/storefront/src/lib/dev/apiBase.ts +14 -0
  171. package/apps/storefront/src/lib/dev/cliSignInCode.ts +155 -0
  172. package/apps/storefront/src/lib/dev/devDraft.ts +133 -0
  173. package/apps/storefront/src/lib/dev/hostedCockpit.ts +169 -0
  174. package/apps/storefront/src/lib/dev/livePreview.ts +77 -0
  175. package/apps/storefront/src/lib/dev/runtimeStore.ts +120 -0
  176. package/apps/storefront/src/lib/dev/storeStatus.ts +142 -0
  177. package/apps/storefront/src/lib/diag/redact.ts +20 -0
  178. package/apps/storefront/src/lib/edgeCache.ts +128 -0
  179. package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +62 -0
  180. package/apps/storefront/src/lib/env.ts +60 -0
  181. package/apps/storefront/src/lib/fixtures/sampleProducts.ts +130 -0
  182. package/apps/storefront/src/lib/format.ts +38 -0
  183. package/apps/storefront/src/lib/foxyCommerce.ts +49 -0
  184. package/apps/storefront/src/lib/imageAttrs.ts +55 -0
  185. package/apps/storefront/src/lib/jsonld.ts +166 -0
  186. package/apps/storefront/src/lib/maintenance.ts +71 -0
  187. package/apps/storefront/src/lib/membership/model.ts +97 -0
  188. package/apps/storefront/src/lib/memoryKv.ts +32 -0
  189. package/apps/storefront/src/lib/messaging/messagesClient.ts +143 -0
  190. package/apps/storefront/src/lib/placeholder.ts +99 -0
  191. package/apps/storefront/src/lib/previewReload.ts +125 -0
  192. package/apps/storefront/src/lib/priceBuckets.ts +33 -0
  193. package/apps/storefront/src/lib/rawChrome.ts +505 -0
  194. package/apps/storefront/src/lib/rawMarketingHtml.ts +136 -0
  195. package/apps/storefront/src/lib/search/index.ts +74 -0
  196. package/apps/storefront/src/lib/sections.ts +129 -0
  197. package/apps/storefront/src/lib/securityHeaders.ts +31 -0
  198. package/apps/storefront/src/lib/storyblok/content-model.ts +289 -0
  199. package/apps/storefront/src/lib/storyblok/provider.ts +607 -0
  200. package/apps/storefront/src/lib/subscription/model.ts +68 -0
  201. package/apps/storefront/src/lib/tenantRouting.ts +91 -0
  202. package/apps/storefront/src/lib/tot/ToTClient.ts +177 -0
  203. package/apps/storefront/src/lib/tot/d1Client.ts +62 -0
  204. package/apps/storefront/src/lib/tot/query.ts +249 -0
  205. package/apps/storefront/src/lib/tot/totClientInterface.ts +30 -0
  206. package/apps/storefront/src/lib/url.ts +142 -0
  207. package/apps/storefront/src/lib/wishlist.ts +63 -0
  208. package/apps/storefront/src/middleware/index.ts +455 -0
  209. package/apps/storefront/src/pages/404.astro +61 -0
  210. package/apps/storefront/src/pages/[...slug].astro +90 -0
  211. package/apps/storefront/src/pages/admin.astro +1675 -0
  212. package/apps/storefront/src/pages/api/admin/orders/[id].ts +53 -0
  213. package/apps/storefront/src/pages/api/admin/orders.ts +71 -0
  214. package/apps/storefront/src/pages/api/auth/logout.ts +21 -0
  215. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +107 -0
  216. package/apps/storefront/src/pages/api/auth/send.ts +85 -0
  217. package/apps/storefront/src/pages/api/auth/verify.ts +135 -0
  218. package/apps/storefront/src/pages/api/cache-purge.ts +44 -0
  219. package/apps/storefront/src/pages/api/checkout-style.css.ts +43 -0
  220. package/apps/storefront/src/pages/api/cli/policy.ts +70 -0
  221. package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +164 -0
  222. package/apps/storefront/src/pages/api/csp-report.ts +25 -0
  223. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +124 -0
  224. package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +120 -0
  225. package/apps/storefront/src/pages/api/dev/activity.ts +124 -0
  226. package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +73 -0
  227. package/apps/storefront/src/pages/api/dev/diff.ts +68 -0
  228. package/apps/storefront/src/pages/api/dev/file.ts +57 -0
  229. package/apps/storefront/src/pages/api/dev/locate.ts +125 -0
  230. package/apps/storefront/src/pages/api/dev/publish.ts +128 -0
  231. package/apps/storefront/src/pages/api/dev/tree.ts +70 -0
  232. package/apps/storefront/src/pages/api/dev/write.ts +116 -0
  233. package/apps/storefront/src/pages/api/newsletter.ts +104 -0
  234. package/apps/storefront/src/pages/api/request-access.ts +172 -0
  235. package/apps/storefront/src/pages/auth/login.astro +149 -0
  236. package/apps/storefront/src/pages/auth/magic.astro +105 -0
  237. package/apps/storefront/src/pages/capabilities.astro +292 -0
  238. package/apps/storefront/src/pages/collections/[handle].astro +177 -0
  239. package/apps/storefront/src/pages/collections/index.astro +59 -0
  240. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +100 -0
  241. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +154 -0
  242. package/apps/storefront/src/pages/dashboard/index.astro +160 -0
  243. package/apps/storefront/src/pages/index.astro +290 -0
  244. package/apps/storefront/src/pages/llms.txt.ts +92 -0
  245. package/apps/storefront/src/pages/products/[handle].astro +402 -0
  246. package/apps/storefront/src/pages/robots.txt.ts +24 -0
  247. package/apps/storefront/src/pages/saved.astro +35 -0
  248. package/apps/storefront/src/pages/search.astro +150 -0
  249. package/apps/storefront/src/pages/sitemap.xml.ts +78 -0
  250. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +1280 -0
  251. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +229 -0
  252. package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +144 -0
  253. package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +116 -0
  254. package/apps/storefront/src/pages/style-guide/index.astro +98 -0
  255. package/apps/storefront/src/pages/tenants/[id]/[...path].ts +57 -0
  256. package/apps/storefront/src/styles/global.css +292 -0
  257. package/apps/storefront/src/themes/reference.ts +68 -0
  258. package/apps/storefront/src/themes/schema.ts +111 -0
  259. package/apps/storefront/src/themes/tenants/index.ts +51 -0
  260. package/apps/storefront/tsconfig.json +16 -0
  261. package/apps/storefront/vitest.config.ts +22 -0
  262. package/package.json +26 -0
  263. package/packages/public-runtime/LICENSE +58 -0
  264. package/packages/public-runtime/package.json +22 -0
  265. package/packages/public-runtime/src/binary-path.ts +68 -0
  266. package/packages/public-runtime/src/capabilities.ts +89 -0
  267. package/packages/public-runtime/src/catalog-api.ts +81 -0
  268. package/packages/public-runtime/src/catalog-d1.ts +192 -0
  269. package/packages/public-runtime/src/checkout-style-publish.ts +200 -0
  270. package/packages/public-runtime/src/checkout-style.ts +375 -0
  271. package/packages/public-runtime/src/csp.ts +109 -0
  272. package/packages/public-runtime/src/customization-preview.ts +197 -0
  273. package/packages/public-runtime/src/customization-reconcile.ts +63 -0
  274. package/packages/public-runtime/src/customization-runtime.ts +157 -0
  275. package/packages/public-runtime/src/customization-scripts.ts +64 -0
  276. package/packages/public-runtime/src/customization-versioning.ts +95 -0
  277. package/packages/public-runtime/src/foxy-orders.ts +136 -0
  278. package/packages/public-runtime/src/foxy.ts +196 -0
  279. package/packages/public-runtime/src/hot-standby-readiness.ts +140 -0
  280. package/packages/public-runtime/src/index.ts +34 -0
  281. package/packages/public-runtime/src/product.ts +159 -0
  282. package/packages/public-runtime/src/search.ts +45 -0
  283. package/packages/public-runtime/src/tenant-assets.ts +444 -0
  284. package/packages/public-runtime/src/tenant.ts +384 -0
  285. package/packages/public-runtime/tsconfig.json +8 -0
  286. package/packages/public-runtime/vitest.config.ts +8 -0
  287. package/pnpm-workspace.yaml +7 -0
  288. package/scripts/build/copy-tenant-assets.mjs +69 -0
  289. package/scripts/dev/ai-edit.mjs +183 -0
  290. package/scripts/dev/checkout-watch.mjs +116 -0
  291. package/scripts/dev/file-browser.mjs +159 -0
  292. package/scripts/dev/file-writer.mjs +141 -0
  293. package/scripts/dev/git-status.mjs +171 -0
  294. package/scripts/dev/locate-handler.mjs +163 -0
  295. package/scripts/dev/locate.mjs +112 -0
  296. package/scripts/dev/port-check.mjs +48 -0
  297. package/scripts/dev/safe-path.mjs +109 -0
  298. package/scripts/dev/shot.mjs +46 -0
  299. package/scripts/dev/tenant-source-root.mjs +76 -0
  300. package/scripts/dev/unified-diff.mjs +81 -0
  301. package/scripts/tot-dev.mjs +376 -0
  302. package/tenants/README.md +14 -0
  303. package/tenants/home/content/chrome.json +7 -0
  304. package/tenants/home/content/home.html +58 -0
  305. package/tenants/home/content/home.json +11 -0
  306. package/tenants/home/public/js/storefront.js +210 -0
  307. package/tenants/home/public/logo-wordmark.png +0 -0
  308. package/tenants/home/public/pages/storefront.css +212 -0
  309. package/tenants/home/theme.json +6 -0
  310. package/tsconfig.base.json +20 -0
@@ -0,0 +1,597 @@
1
+ ---
2
+ /**
3
+ * Customers tab (baseline §Customers), wired to the live customers view
4
+ * model (lib/admin/services/mock, from data-view-contracts) rather than
5
+ * static prototype data. Cross-links to a customer's orders use the shell's
6
+ * plain #orders hash route (no hard dependency on the orders-tab unit's
7
+ * component code).
8
+ */
9
+ import { getCustomersViewModel } from "@/lib/admin/services/mock";
10
+ import type { CustomerRow, VerificationState } from "@/lib/admin/viewmodels/customers";
11
+ import type { AdminTenantEnvelope } from "@/lib/admin/envelope";
12
+
13
+ interface Props {
14
+ envelope: AdminTenantEnvelope;
15
+ }
16
+
17
+ const { envelope } = Astro.props;
18
+ const cspNonce = Astro.locals.cspNonce;
19
+
20
+ const vm = getCustomersViewModel(envelope);
21
+
22
+ const VERIFICATION_TONE: Record<VerificationState, string> = {
23
+ verified: "green",
24
+ pending: "amber",
25
+ blocked: "red",
26
+ not_required: "",
27
+ };
28
+
29
+ const VERIFICATION_LABEL: Record<VerificationState, string> = {
30
+ verified: "Verified",
31
+ pending: "Pending review",
32
+ blocked: "Blocked",
33
+ not_required: "Not required",
34
+ };
35
+
36
+ const SEGMENT_TONE: Record<CustomerRow["segment"], string> = {
37
+ vip: "violet",
38
+ returning: "teal",
39
+ new: "",
40
+ };
41
+
42
+ function viewKeysFor(row: CustomerRow): string[] {
43
+ const keys = ["all", row.segment];
44
+ if (row.verificationState === "blocked" || row.verificationState === "pending") keys.push("needs_review");
45
+ if (row.supportFlags.length > 0) keys.push("support_flagged");
46
+ return keys;
47
+ }
48
+
49
+ function formatDate(iso: string | null): string {
50
+ if (!iso) return "—";
51
+ return new Date(iso).toLocaleDateString([], { month: "short", day: "numeric", year: "numeric" });
52
+ }
53
+
54
+ const totalValue = vm.directory.reduce((sum, c) => sum + c.lifetimeValue, 0);
55
+ const vipCount = vm.directory.filter((c) => c.segment === "vip").length;
56
+
57
+ const defaultRow = vm.directory.find((c) => c.id === vm.valuableButBlocked[0]?.customerId) ?? vm.directory[0] ?? null;
58
+
59
+ const SAVED_VIEWS: { key: string; label: string }[] = [
60
+ { key: "all", label: "All customers" },
61
+ { key: "vip", label: "VIP" },
62
+ { key: "returning", label: "Returning" },
63
+ { key: "new", label: "New" },
64
+ { key: "needs_review", label: "Needs review" },
65
+ { key: "support_flagged", label: "Support flags" },
66
+ ];
67
+ ---
68
+
69
+ <section id="customers" class="band customers-tab" aria-labelledby="admin-tab-customers customers-title" data-admin-panel data-agent-scope="customers" role="tabpanel" data-customers-target-tenant={vm.targetTenant}>
70
+ <div class="section-title">
71
+ <div>
72
+ <h2 id="customers-title">Customers</h2>
73
+ <p>Buyer value, purchase history, support context, and account access in one working surface.</p>
74
+ </div>
75
+ <div class="actions">
76
+ <button class="btn" type="button"><svg><use href="#i-export"></use></svg>Export</button>
77
+ <button class="btn primary" type="button"><svg><use href="#i-users"></use></svg>Create segment</button>
78
+ </div>
79
+ </div>
80
+
81
+ <div class="grid cols-4">
82
+ <div class="metric"><span>Customers</span><strong>{vm.directory.length}</strong><small>In this directory</small></div>
83
+ <div class="metric"><span>Lifetime value</span><strong>${totalValue.toFixed(2)}</strong><small>Across all known buyers</small></div>
84
+ <div class="metric"><span>VIP customers</span><strong>{vipCount}</strong><small>7+ orders</small></div>
85
+ <div class="metric"><span>Valuable but blocked</span><strong>{vm.valuableButBlocked.length}</strong><small>Recoverable with a direct action</small></div>
86
+ </div>
87
+
88
+ {vm.valuableButBlocked.length > 0 && (
89
+ <div class="panel valuable-blocked-panel">
90
+ <div class="panel-head">
91
+ <h3><svg><use href="#i-ask"></use></svg>Valuable But Blocked</h3>
92
+ <span class="chip amber">Reach-out queue</span>
93
+ </div>
94
+ <div class="panel-body grid">
95
+ <div class="blocked-customer-grid">
96
+ {vm.valuableButBlocked.map((row) => (
97
+ <div class="blocked-customer">
98
+ <div>
99
+ <strong>{row.name}</strong>
100
+ <span class="subtle">${row.recoverableValue.toFixed(2)} recoverable value</span>
101
+ </div>
102
+ <div>
103
+ <span class="eyebrow">Blocker</span>
104
+ <strong>{row.blockerDetail}</strong>
105
+ </div>
106
+ <div>
107
+ <span class="eyebrow">Next action</span>
108
+ <strong>{row.action}</strong>
109
+ </div>
110
+ <button type="button" class="btn" data-customers-select-row={row.customerId}>View customer</button>
111
+ </div>
112
+ ))}
113
+ </div>
114
+ </div>
115
+ </div>
116
+ )}
117
+
118
+ <div class="panel">
119
+ <div class="panel-head">
120
+ <h3><svg><use href="#i-users"></use></svg>Customer Directory</h3>
121
+ <span class="chip teal">{vm.directory.length} customers</span>
122
+ </div>
123
+ <div class="panel-body grid">
124
+ <div class="tabs" role="tablist" aria-label="Customer saved views" data-customers-view-list>
125
+ {SAVED_VIEWS.map((view) => (
126
+ <button type="button" class:list={["tab", { active: view.key === "all" }]} data-customers-view-button data-view-key={view.key}>{view.label}</button>
127
+ ))}
128
+ </div>
129
+
130
+ <div class="customer-toolbar" aria-label="Customer filters">
131
+ <label class="customer-search">
132
+ <span>Search</span>
133
+ <input type="search" placeholder="Search customers, email" aria-label="Search customers" data-customers-search />
134
+ </label>
135
+ <label>
136
+ <span>Verification</span>
137
+ <select aria-label="Filter by verification" data-customers-verification-select>
138
+ <option value="">Any status</option>
139
+ <option value="verified">Verified</option>
140
+ <option value="pending">Pending review</option>
141
+ <option value="blocked">Blocked</option>
142
+ </select>
143
+ </label>
144
+ </div>
145
+
146
+ <div class="table-wrap">
147
+ <table class="customer-table">
148
+ <thead>
149
+ <tr>
150
+ <th>Customer</th>
151
+ <th>Customer value</th>
152
+ <th>Last purchase</th>
153
+ <th>Segment</th>
154
+ <th>Support</th>
155
+ <th>Verification</th>
156
+ <th class="action">Action</th>
157
+ </tr>
158
+ </thead>
159
+ <tbody data-customers-tbody>
160
+ {vm.directory.map((row) => (
161
+ <tr
162
+ class:list={[{ selected: row.id === defaultRow?.id }]}
163
+ data-customers-row
164
+ data-customer-id={row.id}
165
+ data-view-keys={viewKeysFor(row).join(" ")}
166
+ data-verification={row.verificationState}
167
+ data-search-haystack={`${row.name} ${row.email}`.toLowerCase()}
168
+ >
169
+ <td>
170
+ <div class="customer-cell">
171
+ <span class="customer-avatar" aria-hidden="true">{row.name.split(" ").map((p) => p[0]).join("").slice(0, 2).toUpperCase()}</span>
172
+ <span>
173
+ <strong>{row.name}</strong>
174
+ <span class="subtle">{row.email}</span>
175
+ </span>
176
+ </div>
177
+ </td>
178
+ <td>
179
+ <strong>${row.lifetimeValue.toFixed(2)}</strong>
180
+ <span class="subtle">{row.orderCount} order{row.orderCount === 1 ? "" : "s"}</span>
181
+ </td>
182
+ <td><strong>{formatDate(row.lastOrderAt)}</strong></td>
183
+ <td><span class:list={["chip", SEGMENT_TONE[row.segment]]}>{row.segment[0]!.toUpperCase() + row.segment.slice(1)}</span></td>
184
+ <td>{row.supportFlags.length > 0 ? <span class="chip blue">{row.supportFlags[0]}</span> : <span class="subtle">Clear</span>}</td>
185
+ <td><span class:list={["chip", VERIFICATION_TONE[row.verificationState]]}>{VERIFICATION_LABEL[row.verificationState]}</span></td>
186
+ <td class="action"><button type="button" class="btn" data-customers-select-row={row.id}>Open</button></td>
187
+ </tr>
188
+ ))}
189
+ </tbody>
190
+ </table>
191
+ <p class="customers-empty-state" data-customers-empty hidden>No customers match the current filters.</p>
192
+ </div>
193
+ </div>
194
+ </div>
195
+
196
+ <div class="customer-detail-grid" data-customers-detail-root>
197
+ {vm.directory.map((row) => {
198
+ const detail = vm.detailsById[row.id]!;
199
+ return (
200
+ <div class="customer-detail-record" data-customers-detail-record={row.id} hidden={row.id !== defaultRow?.id}>
201
+ <div class="panel">
202
+ <div class="panel-head">
203
+ <h3><svg><use href="#i-card"></use></svg>{row.name}</h3>
204
+ <div class="actions">
205
+ <span class:list={["chip", SEGMENT_TONE[row.segment]]}>{row.segment[0]!.toUpperCase() + row.segment.slice(1)} buyer</span>
206
+ <span class:list={["chip", VERIFICATION_TONE[row.verificationState]]}>{VERIFICATION_LABEL[row.verificationState]}</span>
207
+ </div>
208
+ </div>
209
+ <div class="panel-body grid">
210
+ <div class="customer-profile-head">
211
+ <div class="customer-cell">
212
+ <span class="customer-avatar large" aria-hidden="true">{row.name.split(" ").map((p) => p[0]).join("").slice(0, 2).toUpperCase()}</span>
213
+ <span>
214
+ <strong>{row.email}</strong>
215
+ <span class="subtle">{row.orderCount} order{row.orderCount === 1 ? "" : "s"} - last purchase {formatDate(row.lastOrderAt)}</span>
216
+ </span>
217
+ </div>
218
+ </div>
219
+
220
+ <details class="detail-section" open>
221
+ <summary class="detail-section-head"><h4>Profile</h4><span class="chip">{row.orderCount} orders</span></summary>
222
+ <div class="profile-facts">
223
+ <div><span>Lifetime value</span><strong>${row.lifetimeValue.toFixed(2)}</strong></div>
224
+ <div><span>Order count</span><strong>{row.orderCount}</strong></div>
225
+ <div><span>Segment</span><strong>{row.segment[0]!.toUpperCase() + row.segment.slice(1)}</strong></div>
226
+ </div>
227
+ </details>
228
+
229
+ <details class="detail-section" open={detail.orders.length > 0}>
230
+ <summary class="detail-section-head"><h4>Orders</h4><span class="chip">{detail.orders.length}</span></summary>
231
+ <div class="table-wrap compact-table">
232
+ <table>
233
+ <thead><tr><th>Order</th><th>Date</th><th class="money">Total</th></tr></thead>
234
+ <tbody>
235
+ {detail.orders.map((order) => (
236
+ <tr>
237
+ <td><a class="btn" href="#orders">{order.orderId}</a></td>
238
+ <td><strong>{formatDate(order.placedAt)}</strong></td>
239
+ <td class="money"><strong>${order.total.toFixed(2)}</strong></td>
240
+ </tr>
241
+ ))}
242
+ </tbody>
243
+ </table>
244
+ </div>
245
+ </details>
246
+
247
+ <details class="detail-section">
248
+ <summary class="detail-section-head"><h4>Activity</h4><span class="chip">{detail.activity.length}</span></summary>
249
+ <div class="customer-activity-list">
250
+ {detail.activity.map((event) => (
251
+ <div>
252
+ <time>{formatDate(event.at)}</time>
253
+ <div><b>{event.title}</b><p>{event.detail}</p></div>
254
+ </div>
255
+ ))}
256
+ </div>
257
+ </details>
258
+
259
+ <details class="detail-section">
260
+ <summary class="detail-section-head"><h4>Verification</h4><span class:list={["chip", VERIFICATION_TONE[row.verificationState]]}>{VERIFICATION_LABEL[row.verificationState]}</span></summary>
261
+ <div class:list={["check-row", { blocking: row.verificationState === "blocked" || row.verificationState === "pending" }]}>
262
+ <div><b>Identity and age</b><p>{row.verificationState === "verified" ? "Verification is current." : row.verificationState === "pending" ? "Verification needs a refresh before the next shipment." : "Verification is blocked — resolve before this customer can order again."}</p></div>
263
+ <strong>{VERIFICATION_LABEL[row.verificationState]}</strong>
264
+ <span class:list={["chip", VERIFICATION_TONE[row.verificationState]]}>{row.verificationState === "verified" ? "OK" : "Action"}</span>
265
+ </div>
266
+ </details>
267
+
268
+ <details class="detail-section">
269
+ <summary class="detail-section-head"><h4>Notes</h4><span class="chip">{detail.notes.length}</span></summary>
270
+ {detail.notes.length > 0 ? (
271
+ <ul class="customer-notes-list">
272
+ {detail.notes.map((note) => <li>{note}</li>)}
273
+ </ul>
274
+ ) : (
275
+ <p class="subtle">No notes yet.</p>
276
+ )}
277
+ </details>
278
+ </div>
279
+ </div>
280
+ </div>
281
+ );
282
+ })}
283
+ </div>
284
+ </section>
285
+
286
+ <script is:inline nonce={cspNonce}>
287
+ (() => {
288
+ const root = document.querySelector('[data-customers-target-tenant]');
289
+ if (!root) return;
290
+
291
+ const viewButtons = Array.from(root.querySelectorAll("[data-customers-view-button]"));
292
+ const verificationSelect = root.querySelector("[data-customers-verification-select]");
293
+ const searchInput = root.querySelector("[data-customers-search]");
294
+ const rows = Array.from(root.querySelectorAll("[data-customers-row]"));
295
+ const emptyState = root.querySelector("[data-customers-empty]");
296
+ const detailRoot = root.querySelector("[data-customers-detail-root]");
297
+ const detailRecords = detailRoot ? Array.from(detailRoot.querySelectorAll("[data-customers-detail-record]")) : [];
298
+
299
+ let activeView = "all";
300
+
301
+ const applyFilters = () => {
302
+ const verification = verificationSelect ? verificationSelect.value : "";
303
+ const term = searchInput ? searchInput.value.trim().toLowerCase() : "";
304
+ let visibleCount = 0;
305
+
306
+ for (const row of rows) {
307
+ const viewKeys = (row.dataset.viewKeys ?? "").split(" ");
308
+ const matchesView = viewKeys.includes(activeView);
309
+ const matchesVerification = !verification || row.dataset.verification === verification;
310
+ const matchesSearch = !term || (row.dataset.searchHaystack ?? "").includes(term);
311
+ const visible = matchesView && matchesVerification && matchesSearch;
312
+ row.hidden = !visible;
313
+ if (visible) visibleCount += 1;
314
+ }
315
+
316
+ if (emptyState) emptyState.hidden = visibleCount > 0;
317
+ };
318
+
319
+ const selectCustomer = (customerId) => {
320
+ for (const row of rows) {
321
+ row.classList.toggle("selected", row.dataset.customerId === customerId);
322
+ }
323
+ for (const record of detailRecords) {
324
+ record.hidden = record.dataset.customersDetailRecord !== customerId;
325
+ }
326
+ document.querySelector(`[data-customers-detail-record="${customerId}"]`)?.scrollIntoView({ block: "nearest" });
327
+ };
328
+
329
+ viewButtons.forEach((button) => {
330
+ button.addEventListener("click", () => {
331
+ activeView = button.dataset.viewKey ?? "all";
332
+ viewButtons.forEach((b) => b.classList.toggle("active", b === button));
333
+ applyFilters();
334
+ });
335
+ });
336
+
337
+ verificationSelect?.addEventListener("change", applyFilters);
338
+ searchInput?.addEventListener("input", applyFilters);
339
+
340
+ root.querySelectorAll("[data-customers-select-row]").forEach((button) => {
341
+ button.addEventListener("click", () => {
342
+ const customerId = button.dataset.customersSelectRow;
343
+ if (customerId) selectCustomer(customerId);
344
+ });
345
+ });
346
+
347
+ rows.forEach((row) => {
348
+ row.addEventListener("click", (event) => {
349
+ if (event.target instanceof Element && event.target.closest("[data-customers-select-row]")) return;
350
+ const customerId = row.dataset.customerId;
351
+ if (customerId) selectCustomer(customerId);
352
+ });
353
+ });
354
+
355
+ applyFilters();
356
+ })();
357
+ </script>
358
+
359
+ <style>
360
+ .customers-tab .section-title .actions {
361
+ align-items: center;
362
+ }
363
+
364
+ .valuable-blocked-panel {
365
+ border-color: #f1c883;
366
+ background: #fffdf8;
367
+ }
368
+
369
+ .blocked-customer-grid {
370
+ display: grid;
371
+ grid-template-columns: minmax(0, 1fr);
372
+ gap: 10px;
373
+ }
374
+
375
+ .blocked-customer {
376
+ display: grid;
377
+ grid-template-columns: minmax(0, 1fr) minmax(120px, 0.72fr) minmax(130px, 0.86fr) auto;
378
+ gap: 10px;
379
+ align-items: center;
380
+ min-width: 0;
381
+ padding: 11px;
382
+ border: 1px solid var(--admin-line);
383
+ border-radius: var(--admin-radius-sm);
384
+ background: var(--admin-surface);
385
+ }
386
+
387
+ .blocked-customer > div {
388
+ min-width: 0;
389
+ }
390
+
391
+ .blocked-customer strong {
392
+ display: block;
393
+ overflow-wrap: anywhere;
394
+ font-size: 13px;
395
+ }
396
+
397
+ .blocked-customer .eyebrow {
398
+ display: block;
399
+ color: var(--admin-muted);
400
+ font-size: 11px;
401
+ font-weight: 760;
402
+ text-transform: uppercase;
403
+ }
404
+
405
+ .customer-toolbar {
406
+ display: grid;
407
+ grid-template-columns: minmax(220px, 1.4fr) minmax(150px, 0.8fr);
408
+ gap: 10px;
409
+ align-items: end;
410
+ }
411
+
412
+ .customer-toolbar label {
413
+ min-width: 0;
414
+ display: grid;
415
+ gap: 5px;
416
+ }
417
+
418
+ .customer-toolbar span,
419
+ .profile-facts span {
420
+ color: var(--admin-muted);
421
+ font-size: 11px;
422
+ font-weight: 760;
423
+ text-transform: uppercase;
424
+ }
425
+
426
+ .customer-toolbar input,
427
+ .customer-toolbar select {
428
+ width: 100%;
429
+ min-height: 38px;
430
+ padding: 0 10px;
431
+ border: 1px solid var(--admin-line-strong);
432
+ border-radius: var(--admin-radius-sm);
433
+ background: var(--admin-surface);
434
+ color: var(--admin-ink);
435
+ }
436
+
437
+ .customer-table {
438
+ min-width: 1000px;
439
+ }
440
+
441
+ .customer-table tbody tr {
442
+ cursor: pointer;
443
+ }
444
+
445
+ .customer-table tr.selected td {
446
+ background: #fffaf0;
447
+ }
448
+
449
+ .customers-empty-state {
450
+ margin: 0;
451
+ padding: 24px 14px;
452
+ color: var(--admin-muted);
453
+ font-size: 13px;
454
+ text-align: center;
455
+ }
456
+
457
+ .customer-cell {
458
+ display: flex;
459
+ align-items: center;
460
+ gap: 10px;
461
+ min-width: 0;
462
+ }
463
+
464
+ .customer-avatar {
465
+ width: 36px;
466
+ height: 36px;
467
+ display: grid;
468
+ place-items: center;
469
+ border-radius: 50%;
470
+ background: var(--admin-surface-tint);
471
+ color: var(--admin-teal-dark);
472
+ font-size: 12px;
473
+ font-weight: 780;
474
+ }
475
+
476
+ .customer-avatar.large {
477
+ width: 44px;
478
+ height: 44px;
479
+ font-size: 13px;
480
+ }
481
+
482
+ .customer-detail-grid {
483
+ display: grid;
484
+ grid-template-columns: minmax(0, 1fr);
485
+ gap: 12px;
486
+ }
487
+
488
+ .customer-profile-head {
489
+ display: flex;
490
+ justify-content: space-between;
491
+ gap: 12px;
492
+ align-items: center;
493
+ }
494
+
495
+ .profile-facts {
496
+ display: grid;
497
+ grid-template-columns: repeat(3, minmax(0, 1fr));
498
+ gap: 10px;
499
+ }
500
+
501
+ .profile-facts > div {
502
+ min-width: 0;
503
+ padding: 10px;
504
+ border: 1px solid var(--admin-line);
505
+ border-radius: var(--admin-radius-sm);
506
+ background: var(--admin-surface-soft);
507
+ }
508
+
509
+ .profile-facts strong {
510
+ display: block;
511
+ margin-top: 4px;
512
+ font-size: 14px;
513
+ }
514
+
515
+ .compact-table table {
516
+ min-width: 400px;
517
+ }
518
+
519
+ .detail-section {
520
+ display: grid;
521
+ gap: 10px;
522
+ padding-top: 2px;
523
+ }
524
+
525
+ .detail-section + .detail-section {
526
+ padding-top: 12px;
527
+ border-top: 1px solid var(--admin-line);
528
+ }
529
+
530
+ .detail-section-head {
531
+ display: flex;
532
+ align-items: center;
533
+ justify-content: space-between;
534
+ gap: 10px;
535
+ cursor: pointer;
536
+ list-style: none;
537
+ }
538
+
539
+ .detail-section-head::-webkit-details-marker {
540
+ display: none;
541
+ }
542
+
543
+ .detail-section-head h4 {
544
+ margin: 0;
545
+ font-size: 13px;
546
+ }
547
+
548
+ .customer-activity-list {
549
+ display: grid;
550
+ gap: 10px;
551
+ }
552
+
553
+ .customer-activity-list > div {
554
+ display: grid;
555
+ grid-template-columns: 90px minmax(0, 1fr);
556
+ gap: 10px;
557
+ }
558
+
559
+ .customer-activity-list time {
560
+ color: var(--admin-muted);
561
+ font-size: 12px;
562
+ }
563
+
564
+ .customer-activity-list p {
565
+ margin: 2px 0 0;
566
+ color: var(--admin-muted);
567
+ font-size: 12px;
568
+ }
569
+
570
+ .customer-notes-list {
571
+ margin: 0;
572
+ padding-left: 18px;
573
+ font-size: 13px;
574
+ }
575
+
576
+ @media (max-width: 1120px) {
577
+ .customer-toolbar {
578
+ grid-template-columns: repeat(2, minmax(0, 1fr));
579
+ }
580
+ }
581
+
582
+ @media (max-width: 760px) {
583
+ .blocked-customer {
584
+ grid-template-columns: 1fr;
585
+ }
586
+
587
+ .customer-toolbar,
588
+ .profile-facts {
589
+ grid-template-columns: minmax(0, 1fr);
590
+ }
591
+
592
+ .customer-profile-head {
593
+ align-items: flex-start;
594
+ flex-direction: column;
595
+ }
596
+ }
597
+ </style>