@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,901 @@
1
+ ---
2
+ /**
3
+ * Products tab (baseline §Products — catalog readiness and sellability),
4
+ * wired to the live products view model (lib/admin/services/mock, from
5
+ * data-view-contracts) rather than static prototype data.
6
+ */
7
+ import { getProductsViewModel } from "@/lib/admin/services/mock";
8
+ import type { ProductRow } from "@/lib/admin/viewmodels/products";
9
+ import type { AdminTenantEnvelope } from "@/lib/admin/envelope";
10
+
11
+ interface Props {
12
+ envelope: AdminTenantEnvelope;
13
+ }
14
+
15
+ const { envelope } = Astro.props;
16
+ const cspNonce = Astro.locals.cspNonce;
17
+
18
+ const vm = getProductsViewModel(envelope);
19
+
20
+ const STATUS_TONE: Record<ProductRow["status"], string> = {
21
+ active: "green",
22
+ draft: "amber",
23
+ archived: "",
24
+ };
25
+
26
+ function savedViewKeysFor(row: ProductRow): string[] {
27
+ const keys = ["all"];
28
+ if (!row.sellable) keys.push("not_sellable");
29
+ if (row.aiChannelReady) keys.push("ai_channel_ready");
30
+ return keys;
31
+ }
32
+
33
+ const totalVariants = vm.rows.reduce((sum, row) => sum + (vm.detailsById[row.id]?.variants.length ?? 0), 0);
34
+ const needsAttentionCount = vm.rows.filter((r) => r.missingExciseCount > 0).length;
35
+ const aiReadyCount = vm.rows.filter((r) => r.aiChannelReady).length;
36
+ const avgReadiness = vm.rows.length
37
+ ? Math.round(vm.rows.reduce((sum, r) => sum + r.readinessScore, 0) / vm.rows.length)
38
+ : 0;
39
+ const totalExciseGaps = vm.rows.reduce((sum, r) => sum + r.missingExciseCount, 0);
40
+
41
+ const defaultView = vm.savedViews.find((v) => v.key === "not_sellable") && needsAttentionCount > 0 ? "not_sellable" : "all";
42
+ const defaultRow = vm.rows.find((r) => !r.sellable) ?? vm.rows[0] ?? null;
43
+
44
+ ---
45
+
46
+ <section id="products" class="band products-tab" aria-labelledby="admin-tab-products products-title" data-admin-panel data-agent-scope="products" role="tabpanel" data-products-target-tenant={vm.targetTenant}>
47
+ <div class="section-title">
48
+ <div>
49
+ <h2 id="products-title">Products</h2>
50
+ <p>Catalog, variants, pricing, inventory, media, and sellability.</p>
51
+ </div>
52
+ <div class="actions">
53
+ <button class="btn" type="button"><svg><use href="#i-export"></use></svg>Export</button>
54
+ <button class="btn" type="button"><svg><use href="#i-refresh"></use></svg>Import</button>
55
+ <button class="btn primary" type="button"><svg><use href="#i-box"></use></svg>Add product</button>
56
+ </div>
57
+ </div>
58
+
59
+ <div class="grid cols-4">
60
+ <div class="metric"><span>Active products</span><strong>{vm.rows.length}</strong><small>In this catalog</small></div>
61
+ <div class="metric"><span>Variants</span><strong>{totalVariants}</strong><small>Across all products</small></div>
62
+ <div class="metric"><span>Needs attention</span><strong>{needsAttentionCount}</strong><small>Missing excise metadata</small></div>
63
+ <div class="metric"><span>AI channel ready</span><strong>{aiReadyCount}</strong><small>Of {vm.rows.length} products</small></div>
64
+ </div>
65
+
66
+ <div class="panel product-intelligence-panel">
67
+ <div class="panel-head product-panel-head">
68
+ <h3><svg><use href="#i-ask"></use></svg>Catalog Intelligence</h3>
69
+ <span class:list={["chip", totalExciseGaps > 0 ? "amber" : "green"]}>{totalExciseGaps} excise gap{totalExciseGaps === 1 ? "" : "s"}</span>
70
+ </div>
71
+ <div class="panel-body grid">
72
+ <div class="product-signal-grid">
73
+ <div class="product-signal">
74
+ <span class:list={["chip", avgReadiness >= 90 ? "teal" : "amber"]}>Readiness score</span>
75
+ <strong>{avgReadiness}%</strong>
76
+ <p>Catalog quality, sellability, and excise metadata across all products.</p>
77
+ </div>
78
+ <div class="product-signal">
79
+ <span class:list={["chip", totalExciseGaps > 0 ? "amber" : "green"]}>Excise metadata gaps</span>
80
+ <strong>{totalExciseGaps} field{totalExciseGaps === 1 ? "" : "s"}</strong>
81
+ <p>Missing tax class, wholesale basis, or volume/weight needed for regulated sale readiness.</p>
82
+ </div>
83
+ <div class="product-signal">
84
+ <span class="chip violet">AI channel readiness</span>
85
+ <strong>{aiReadyCount} of {vm.rows.length}</strong>
86
+ <p>Products with complete, agentic-commerce-ready listing data.</p>
87
+ </div>
88
+ </div>
89
+ <div class="actions">
90
+ <button class="btn primary" type="button">Review excise gaps</button>
91
+ <button class="btn" type="button">Draft catalog fixes</button>
92
+ <button class="btn" type="button">Schedule readiness audit</button>
93
+ </div>
94
+ </div>
95
+ </div>
96
+
97
+ <div class="product-workspace">
98
+ <div class="panel product-list-panel">
99
+ <div class="panel-head product-panel-head">
100
+ <h3><svg><use href="#i-box"></use></svg>Catalog</h3>
101
+ <span class="chip teal">{vm.rows.length} products</span>
102
+ </div>
103
+ <div class="panel-body grid">
104
+ <div class="tabs" role="tablist" aria-label="Product saved views" data-products-view-list>
105
+ {vm.savedViews.map((view) => (
106
+ <button
107
+ type="button"
108
+ class:list={["tab", { active: view.key === defaultView }]}
109
+ data-products-view-button
110
+ data-view-key={view.key}
111
+ >
112
+ {view.label}
113
+ </button>
114
+ ))}
115
+ </div>
116
+
117
+ <div class="product-toolbar" aria-label="Product controls">
118
+ <label class="product-search">
119
+ <span>Search</span>
120
+ <input type="search" placeholder="Search products, SKUs, category" aria-label="Search products" data-products-search />
121
+ </label>
122
+ <div class="product-filters" aria-label="Product filters">
123
+ <label>
124
+ <span>Status</span>
125
+ <select aria-label="Filter by status" data-products-status-select>
126
+ <option value="">Any status</option>
127
+ <option value="active">Active</option>
128
+ <option value="draft">Draft</option>
129
+ <option value="archived">Archived</option>
130
+ </select>
131
+ </label>
132
+ <label>
133
+ <span>Sellability</span>
134
+ <select aria-label="Filter by sellability" data-products-sellability-select>
135
+ <option value="">Any</option>
136
+ <option value="sellable">Sellable</option>
137
+ <option value="blocked">Not sellable</option>
138
+ </select>
139
+ </label>
140
+ </div>
141
+ </div>
142
+
143
+ <div class="table-wrap products-table-wrap">
144
+ <table class="products-table">
145
+ <thead>
146
+ <tr>
147
+ <th>Product</th>
148
+ <th>Type</th>
149
+ <th>Status</th>
150
+ <th>Readiness</th>
151
+ <th>Sellability</th>
152
+ <th>AI channel</th>
153
+ <th class="action">Action</th>
154
+ </tr>
155
+ </thead>
156
+ <tbody data-products-tbody>
157
+ {vm.rows.map((row) => (
158
+ <tr
159
+ class:list={[{ selected: row.id === defaultRow?.id }]}
160
+ data-products-row
161
+ data-product-id={row.id}
162
+ data-view-keys={savedViewKeysFor(row).join(" ")}
163
+ data-status={row.status}
164
+ data-sellability={row.sellable ? "sellable" : "blocked"}
165
+ data-search-haystack={`${row.name} ${row.id} ${row.category} ${row.productType}`.toLowerCase()}
166
+ >
167
+ <td>
168
+ <div class="product-title-cell">
169
+ <div>
170
+ <strong>{row.name}</strong>
171
+ <span class="subtle">{row.category}</span>
172
+ </div>
173
+ </div>
174
+ </td>
175
+ <td><strong>{row.productType}</strong></td>
176
+ <td><span class:list={["chip", STATUS_TONE[row.status]]}>{row.status[0]!.toUpperCase() + row.status.slice(1)}</span></td>
177
+ <td><strong>{row.readinessScore}%</strong>{row.missingExciseCount > 0 && <span class="subtle">{row.missingExciseCount} gap{row.missingExciseCount === 1 ? "" : "s"}</span>}</td>
178
+ <td><span class:list={["chip", row.sellable ? "green" : "amber"]}>{row.sellable ? "Sellable" : "Excise gaps"}</span></td>
179
+ <td><span class:list={["chip", row.aiChannelReady ? "teal" : ""]}>{row.aiChannelReady ? "Ready" : "Not ready"}</span></td>
180
+ <td class="action"><button type="button" class="btn" data-products-select-row={row.id}>Open</button></td>
181
+ </tr>
182
+ ))}
183
+ </tbody>
184
+ </table>
185
+ <p class="products-empty-state" data-products-empty hidden>No products match the current filters.</p>
186
+ </div>
187
+ </div>
188
+ </div>
189
+
190
+ <aside class="panel product-detail-panel" aria-labelledby="product-detail-title" data-products-detail-root>
191
+ {vm.rows.map((row) => {
192
+ const detail = vm.detailsById[row.id]!;
193
+ return (
194
+ <div
195
+ class="product-detail-record"
196
+ data-products-detail-record={row.id}
197
+ hidden={row.id !== defaultRow?.id}
198
+ >
199
+ <div class="panel-head product-panel-head">
200
+ <h3 id={row.id === defaultRow?.id ? "product-detail-title" : undefined}><svg><use href="#i-file"></use></svg>{row.name}</h3>
201
+ <span class:list={["chip", row.sellable ? "green" : "amber"]}>{row.sellable ? "Sellable" : "Excise gaps"}</span>
202
+ </div>
203
+ <div class="panel-body grid">
204
+ <div class="product-detail-meta">
205
+ <div>
206
+ <span class="eyebrow">Product type</span>
207
+ <strong>{row.productType}</strong>
208
+ </div>
209
+ <div>
210
+ <span class="eyebrow">Category</span>
211
+ <strong>{row.category}</strong>
212
+ </div>
213
+ <div>
214
+ <span class="eyebrow">Readiness score</span>
215
+ <strong>{row.readinessScore}%</strong>
216
+ </div>
217
+ </div>
218
+
219
+ {!row.sellable && detail.complianceNotes.length > 0 && (
220
+ <div class="check-row blocking">
221
+ <div><b>Not sellable</b><p>{detail.complianceNotes[0]}</p></div>
222
+ <strong>{row.missingExciseCount} missing</strong>
223
+ <span class="chip amber">Action</span>
224
+ </div>
225
+ )}
226
+
227
+ <details class="detail-section" open>
228
+ <summary class="detail-section-head">
229
+ <h4>Variants</h4>
230
+ <span class="chip">{detail.variants.length}</span>
231
+ </summary>
232
+ <div class="mini-table" role="table" aria-label="Variants">
233
+ {detail.variants.map((variant) => (
234
+ <div class="mini-row" role="row">
235
+ <div role="cell"><strong>{variant.name}</strong><span class="subtle">{variant.sku}</span></div>
236
+ <div role="cell"><span class="eyebrow">Stock</span><strong>{variant.inventory}</strong></div>
237
+ <div role="cell"><span class="eyebrow">Price</span><strong>${variant.price.toFixed(2)}</strong></div>
238
+ </div>
239
+ ))}
240
+ </div>
241
+ </details>
242
+
243
+ <details class="detail-section">
244
+ <summary class="detail-section-head">
245
+ <h4>Media</h4>
246
+ <span class="chip">{detail.media.length}</span>
247
+ </summary>
248
+ <div class="mini-table" role="table" aria-label="Media">
249
+ {detail.media.map((asset) => (
250
+ <div class="mini-row" role="row">
251
+ <div role="cell"><strong>{asset.kind[0]!.toUpperCase() + asset.kind.slice(1)}</strong><span class="subtle">{asset.alt}</span></div>
252
+ <div role="cell"><span class="subtle">{asset.url}</span></div>
253
+ </div>
254
+ ))}
255
+ </div>
256
+ </details>
257
+
258
+ <details class="detail-section" open={!row.sellable}>
259
+ <summary class="detail-section-head">
260
+ <h4>Excise metadata readiness</h4>
261
+ <span class:list={["chip", row.missingExciseCount > 0 ? "amber" : "green"]}>{row.missingExciseCount} missing</span>
262
+ </summary>
263
+ <div>
264
+ {detail.exciseChecks.map((check) => (
265
+ <div class:list={["check-row", { blocking: !check.present }]}>
266
+ <div><b>{check.label}</b><p>{check.present ? `Set to ${check.value}.` : "Not yet set — required for regulated sale readiness."}</p></div>
267
+ <strong>{check.present ? check.value : "Missing"}</strong>
268
+ <span class:list={["chip", check.present ? "green" : "amber"]}>{check.present ? "OK" : "Fix"}</span>
269
+ </div>
270
+ ))}
271
+ </div>
272
+ </details>
273
+
274
+ <details class="detail-section">
275
+ <summary class="detail-section-head">
276
+ <h4>AI channel readiness</h4>
277
+ <span class:list={["chip", row.aiChannelReady ? "teal" : "amber"]}>{row.aiChannelReady ? "Ready" : "Not ready"}</span>
278
+ </summary>
279
+ <div class="check-row">
280
+ <div><b>Agentic commerce surfaces</b><p>{row.aiChannelReady ? "Listing data is complete enough for AI shopping agents to represent this product." : "Complete excise metadata and sellability first — AI channel readiness depends on a clean listing."}</p></div>
281
+ <strong>{row.aiChannelReady ? "Ready" : "Blocked"}</strong>
282
+ <span class:list={["chip", row.aiChannelReady ? "teal" : "amber"]}>{row.aiChannelReady ? "OK" : "Action"}</span>
283
+ </div>
284
+ </details>
285
+
286
+ <details class="detail-section">
287
+ <summary class="detail-section-head">
288
+ <h4>Activity</h4>
289
+ <span class="chip">{detail.activity.length}</span>
290
+ </summary>
291
+ <div class="mini-table" role="table" aria-label="Activity">
292
+ {detail.activity.map((event) => (
293
+ <div class="mini-row" role="row">
294
+ <div role="cell"><strong>{event.title}</strong><span class="subtle">{event.detail}</span></div>
295
+ <div role="cell"><span class="subtle">{new Date(event.at).toLocaleString([], { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" })}</span></div>
296
+ </div>
297
+ ))}
298
+ </div>
299
+ </details>
300
+ </div>
301
+ </div>
302
+ );
303
+ })}
304
+ </aside>
305
+ </div>
306
+ </section>
307
+
308
+ <script is:inline nonce={cspNonce}>
309
+ (() => {
310
+ const root = document.querySelector('[data-products-target-tenant]');
311
+ if (!root) return;
312
+
313
+ const viewButtons = Array.from(root.querySelectorAll("[data-products-view-button]"));
314
+ const statusSelect = root.querySelector("[data-products-status-select]");
315
+ const sellabilitySelect = root.querySelector("[data-products-sellability-select]");
316
+ const searchInput = root.querySelector("[data-products-search]");
317
+ const rows = Array.from(root.querySelectorAll("[data-products-row]"));
318
+ const emptyState = root.querySelector("[data-products-empty]");
319
+ const detailRoot = root.querySelector("[data-products-detail-root]");
320
+ const detailRecords = detailRoot ? Array.from(detailRoot.querySelectorAll("[data-products-detail-record]")) : [];
321
+
322
+ let activeView = (viewButtons.find((b) => b.classList.contains("active")) || viewButtons[0])?.dataset.viewKey ?? "all";
323
+
324
+ const applyFilters = () => {
325
+ const status = statusSelect ? statusSelect.value : "";
326
+ const sellability = sellabilitySelect ? sellabilitySelect.value : "";
327
+ const term = searchInput ? searchInput.value.trim().toLowerCase() : "";
328
+ let visibleCount = 0;
329
+
330
+ for (const row of rows) {
331
+ const viewKeys = (row.dataset.viewKeys ?? "").split(" ");
332
+ const matchesView = viewKeys.includes(activeView);
333
+ const matchesStatus = !status || row.dataset.status === status;
334
+ const matchesSellability = !sellability || row.dataset.sellability === sellability;
335
+ const matchesSearch = !term || (row.dataset.searchHaystack ?? "").includes(term);
336
+ const visible = matchesView && matchesStatus && matchesSellability && matchesSearch;
337
+ row.hidden = !visible;
338
+ if (visible) visibleCount += 1;
339
+ }
340
+
341
+ if (emptyState) emptyState.hidden = visibleCount > 0;
342
+ };
343
+
344
+ const selectProduct = (productId) => {
345
+ for (const row of rows) {
346
+ row.classList.toggle("selected", row.dataset.productId === productId);
347
+ }
348
+ for (const record of detailRecords) {
349
+ record.hidden = record.dataset.productsDetailRecord !== productId;
350
+ }
351
+ };
352
+
353
+ viewButtons.forEach((button) => {
354
+ button.addEventListener("click", () => {
355
+ activeView = button.dataset.viewKey ?? "all";
356
+ viewButtons.forEach((b) => b.classList.toggle("active", b === button));
357
+ applyFilters();
358
+ });
359
+ });
360
+
361
+ statusSelect?.addEventListener("change", applyFilters);
362
+ sellabilitySelect?.addEventListener("change", applyFilters);
363
+ searchInput?.addEventListener("input", applyFilters);
364
+
365
+ root.querySelectorAll("[data-products-select-row]").forEach((button) => {
366
+ button.addEventListener("click", () => {
367
+ const productId = button.dataset.productsSelectRow;
368
+ if (productId) selectProduct(productId);
369
+ });
370
+ });
371
+
372
+ rows.forEach((row) => {
373
+ row.addEventListener("click", (event) => {
374
+ if (event.target instanceof Element && event.target.closest("[data-products-select-row]")) return;
375
+ const productId = row.dataset.productId;
376
+ if (productId) selectProduct(productId);
377
+ });
378
+ });
379
+
380
+ applyFilters();
381
+ })();
382
+ </script>
383
+
384
+ <style>
385
+ .products-tab {
386
+ --product-split: minmax(0, 1.18fr) minmax(360px, 0.82fr);
387
+ }
388
+
389
+ .products-tab svg {
390
+ width: 18px;
391
+ height: 18px;
392
+ stroke: currentColor;
393
+ stroke-width: 1.9;
394
+ stroke-linecap: round;
395
+ stroke-linejoin: round;
396
+ fill: none;
397
+ flex: 0 0 auto;
398
+ }
399
+
400
+ .products-tab button,
401
+ .products-tab input,
402
+ .products-tab select {
403
+ font: inherit;
404
+ }
405
+
406
+ .products-tab button {
407
+ cursor: pointer;
408
+ }
409
+
410
+ .products-tab .section-title {
411
+ display: flex;
412
+ align-items: center;
413
+ justify-content: space-between;
414
+ gap: 12px;
415
+ min-width: 0;
416
+ }
417
+
418
+ .products-tab .section-title h2 {
419
+ margin: 0;
420
+ font-size: 16px;
421
+ }
422
+
423
+ .products-tab .section-title p {
424
+ margin: 4px 0 0;
425
+ color: var(--admin-muted);
426
+ font-size: 13px;
427
+ }
428
+
429
+ .products-tab .actions {
430
+ display: flex;
431
+ flex-wrap: wrap;
432
+ gap: 8px;
433
+ min-width: 0;
434
+ }
435
+
436
+ .products-tab .btn {
437
+ min-height: 38px;
438
+ max-width: 100%;
439
+ display: inline-flex;
440
+ align-items: center;
441
+ justify-content: center;
442
+ gap: 8px;
443
+ padding: 0 13px;
444
+ border: 1px solid var(--admin-line-strong);
445
+ border-radius: var(--admin-radius-sm);
446
+ background: var(--admin-surface);
447
+ color: var(--admin-ink);
448
+ font-weight: 680;
449
+ text-decoration: none;
450
+ white-space: nowrap;
451
+ }
452
+
453
+ .products-tab .btn.primary {
454
+ border-color: var(--admin-teal-dark);
455
+ background: var(--admin-teal);
456
+ color: #ffffff;
457
+ }
458
+
459
+ .products-tab .grid {
460
+ display: grid;
461
+ grid-template-columns: minmax(0, 1fr);
462
+ gap: 12px;
463
+ min-width: 0;
464
+ }
465
+
466
+ .products-tab .grid.cols-4 {
467
+ grid-template-columns: repeat(4, minmax(0, 1fr));
468
+ }
469
+
470
+ .products-tab .panel {
471
+ min-width: 0;
472
+ border: 1px solid var(--admin-line);
473
+ border-radius: var(--admin-radius);
474
+ background: var(--admin-surface);
475
+ }
476
+
477
+ .products-tab .panel-head {
478
+ display: flex;
479
+ align-items: center;
480
+ justify-content: space-between;
481
+ gap: 12px;
482
+ min-height: 50px;
483
+ padding: 13px 14px;
484
+ border-bottom: 1px solid var(--admin-line);
485
+ }
486
+
487
+ .products-tab .panel-head h3 {
488
+ margin: 0;
489
+ display: flex;
490
+ align-items: center;
491
+ gap: 8px;
492
+ font-size: 15px;
493
+ }
494
+
495
+ .products-tab .panel-body {
496
+ display: grid;
497
+ grid-template-columns: minmax(0, 1fr);
498
+ gap: 12px;
499
+ padding: 14px;
500
+ }
501
+
502
+ .product-intelligence-panel {
503
+ border-color: #b9d8d4;
504
+ background: #fbfefd;
505
+ }
506
+
507
+ .product-signal-grid {
508
+ display: grid;
509
+ grid-template-columns: repeat(3, minmax(0, 1fr));
510
+ gap: 10px;
511
+ }
512
+
513
+ .product-signal {
514
+ display: grid;
515
+ gap: 7px;
516
+ min-width: 0;
517
+ padding: 12px;
518
+ border: 1px solid var(--admin-line);
519
+ border-radius: var(--admin-radius-sm);
520
+ background: var(--admin-surface);
521
+ }
522
+
523
+ .product-signal strong {
524
+ font-size: 20px;
525
+ }
526
+
527
+ .product-signal p {
528
+ margin: 0;
529
+ color: var(--admin-muted);
530
+ font-size: 12px;
531
+ }
532
+
533
+ .products-tab .metric {
534
+ min-height: 84px;
535
+ padding: 14px;
536
+ border: 1px solid var(--admin-line);
537
+ border-radius: var(--admin-radius);
538
+ background: var(--admin-surface);
539
+ }
540
+
541
+ .products-tab .metric span,
542
+ .products-tab .field label {
543
+ display: block;
544
+ color: var(--admin-muted);
545
+ font-size: 11px;
546
+ font-weight: 760;
547
+ text-transform: uppercase;
548
+ }
549
+
550
+ .products-tab .metric strong {
551
+ display: block;
552
+ margin-top: 9px;
553
+ font-size: 24px;
554
+ }
555
+
556
+ .products-tab .metric small {
557
+ display: block;
558
+ margin-top: 3px;
559
+ color: var(--admin-muted);
560
+ font-size: 12px;
561
+ }
562
+
563
+ .products-tab .tabs {
564
+ display: flex;
565
+ gap: 6px;
566
+ max-width: 100%;
567
+ overflow-x: auto;
568
+ border-bottom: 1px solid var(--admin-line);
569
+ }
570
+
571
+ .products-tab .tab {
572
+ flex: 0 0 auto;
573
+ min-height: 38px;
574
+ display: inline-flex;
575
+ align-items: center;
576
+ padding: 0 12px;
577
+ border: 0;
578
+ border-bottom: 2px solid transparent;
579
+ background: transparent;
580
+ color: var(--admin-muted);
581
+ font-weight: 760;
582
+ text-decoration: none;
583
+ }
584
+
585
+ .products-tab .tab.active {
586
+ border-color: var(--admin-teal);
587
+ color: var(--admin-ink);
588
+ }
589
+
590
+ .products-tab .table-wrap {
591
+ min-width: 0;
592
+ max-width: 100%;
593
+ overflow-x: auto;
594
+ border: 1px solid var(--admin-line);
595
+ border-radius: var(--admin-radius);
596
+ background: var(--admin-surface);
597
+ }
598
+
599
+ .products-tab table {
600
+ width: 100%;
601
+ min-width: 860px;
602
+ border-collapse: collapse;
603
+ font-size: 13px;
604
+ }
605
+
606
+ .products-tab th {
607
+ height: 38px;
608
+ padding: 0 12px;
609
+ border-bottom: 1px solid var(--admin-line);
610
+ background: var(--admin-surface-soft);
611
+ color: var(--admin-muted);
612
+ font-size: 11px;
613
+ font-weight: 760;
614
+ text-align: left;
615
+ text-transform: uppercase;
616
+ white-space: nowrap;
617
+ }
618
+
619
+ .products-tab td {
620
+ height: 46px;
621
+ padding: 7px 12px;
622
+ border-bottom: 1px solid var(--admin-line);
623
+ vertical-align: middle;
624
+ }
625
+
626
+ .products-tab tbody tr {
627
+ cursor: pointer;
628
+ }
629
+
630
+ .products-tab tr.selected {
631
+ background: #eef6f4;
632
+ }
633
+
634
+ .products-tab tr:last-child td {
635
+ border-bottom: 0;
636
+ }
637
+
638
+ .products-tab .subtle {
639
+ display: block;
640
+ margin-top: 2px;
641
+ color: var(--admin-muted);
642
+ font-size: 12px;
643
+ }
644
+
645
+ .products-tab .action {
646
+ text-align: right;
647
+ }
648
+
649
+ .products-empty-state {
650
+ margin: 0;
651
+ padding: 24px 14px;
652
+ color: var(--admin-muted);
653
+ font-size: 13px;
654
+ text-align: center;
655
+ }
656
+
657
+ .products-tab .check-row {
658
+ display: grid;
659
+ grid-template-columns: minmax(120px, 0.36fr) minmax(0, 1fr) auto;
660
+ gap: 10px;
661
+ align-items: center;
662
+ padding: 10px 0;
663
+ border-bottom: 1px solid var(--admin-line);
664
+ }
665
+
666
+ .products-tab .check-row:last-child {
667
+ border-bottom: 0;
668
+ }
669
+
670
+ .products-tab .check-row.blocking {
671
+ margin: 4px -6px;
672
+ padding: 10px 6px;
673
+ border: 1px solid #efc477;
674
+ border-radius: var(--admin-radius-sm);
675
+ background: #fff8ec;
676
+ }
677
+
678
+ .products-tab .check-row b {
679
+ font-size: 13px;
680
+ }
681
+
682
+ .products-tab .check-row p {
683
+ margin: 2px 0 0;
684
+ color: var(--admin-muted);
685
+ font-size: 12px;
686
+ }
687
+
688
+ .product-workspace {
689
+ display: grid;
690
+ grid-template-columns: var(--product-split);
691
+ gap: 14px;
692
+ align-items: start;
693
+ min-width: 0;
694
+ }
695
+
696
+ .product-list-panel,
697
+ .product-detail-panel {
698
+ min-width: 0;
699
+ }
700
+
701
+ .product-detail-panel {
702
+ position: sticky;
703
+ top: 86px;
704
+ box-shadow: var(--admin-shadow);
705
+ }
706
+
707
+ .product-panel-head {
708
+ min-width: 0;
709
+ }
710
+
711
+ .product-panel-head h3 {
712
+ min-width: 0;
713
+ overflow-wrap: anywhere;
714
+ }
715
+
716
+ .product-toolbar {
717
+ display: grid;
718
+ gap: 8px;
719
+ align-items: end;
720
+ }
721
+
722
+ .product-search {
723
+ min-width: 0;
724
+ }
725
+
726
+ .product-filters {
727
+ display: grid;
728
+ grid-template-columns: repeat(2, minmax(128px, 1fr));
729
+ gap: 8px;
730
+ min-width: 0;
731
+ }
732
+
733
+ .product-toolbar label,
734
+ .product-filters label {
735
+ display: grid;
736
+ gap: 5px;
737
+ min-width: 0;
738
+ }
739
+
740
+ .product-toolbar span,
741
+ .product-filters span,
742
+ .eyebrow {
743
+ color: var(--admin-muted);
744
+ font-size: 11px;
745
+ font-weight: 760;
746
+ text-transform: uppercase;
747
+ }
748
+
749
+ .product-toolbar input,
750
+ .product-toolbar select,
751
+ .product-filters select {
752
+ min-width: 0;
753
+ width: 100%;
754
+ min-height: 38px;
755
+ padding: 0 10px;
756
+ overflow: hidden;
757
+ border: 1px solid var(--admin-line-strong);
758
+ border-radius: var(--admin-radius-sm);
759
+ background: var(--admin-surface);
760
+ color: var(--admin-ink);
761
+ text-overflow: ellipsis;
762
+ }
763
+
764
+ .products-table {
765
+ min-width: 900px;
766
+ }
767
+
768
+ .products-table td {
769
+ height: 58px;
770
+ }
771
+
772
+ .product-title-cell {
773
+ display: flex;
774
+ align-items: center;
775
+ gap: 8px;
776
+ min-width: 0;
777
+ }
778
+
779
+ .product-detail-meta {
780
+ display: grid;
781
+ grid-template-columns: repeat(3, minmax(0, 1fr));
782
+ gap: 10px;
783
+ }
784
+
785
+ .product-detail-meta > div {
786
+ min-width: 0;
787
+ padding-bottom: 10px;
788
+ border-bottom: 1px solid var(--admin-line);
789
+ }
790
+
791
+ .product-detail-meta strong,
792
+ .mini-row strong {
793
+ display: block;
794
+ margin-top: 3px;
795
+ overflow-wrap: anywhere;
796
+ }
797
+
798
+ .detail-section {
799
+ display: grid;
800
+ gap: 10px;
801
+ padding-top: 2px;
802
+ }
803
+
804
+ .detail-section + .detail-section {
805
+ padding-top: 12px;
806
+ border-top: 1px solid var(--admin-line);
807
+ }
808
+
809
+ .detail-section-head {
810
+ display: flex;
811
+ align-items: center;
812
+ justify-content: space-between;
813
+ gap: 10px;
814
+ cursor: pointer;
815
+ list-style: none;
816
+ }
817
+
818
+ .detail-section-head::-webkit-details-marker {
819
+ display: none;
820
+ }
821
+
822
+ .detail-section-head h4 {
823
+ margin: 0;
824
+ font-size: 13px;
825
+ }
826
+
827
+ .mini-table {
828
+ display: grid;
829
+ margin-top: 10px;
830
+ border: 1px solid var(--admin-line);
831
+ border-radius: var(--admin-radius);
832
+ overflow: hidden;
833
+ }
834
+
835
+ .mini-row {
836
+ display: grid;
837
+ grid-template-columns: minmax(130px, 1.25fr) 90px 90px;
838
+ gap: 10px;
839
+ align-items: center;
840
+ min-height: 58px;
841
+ padding: 9px 10px;
842
+ border-bottom: 1px solid var(--admin-line);
843
+ }
844
+
845
+ .mini-row:last-child {
846
+ border-bottom: 0;
847
+ }
848
+
849
+ @media (max-width: 1180px) {
850
+ .product-workspace {
851
+ grid-template-columns: minmax(0, 1fr);
852
+ }
853
+
854
+ .product-detail-panel {
855
+ position: static;
856
+ }
857
+ }
858
+
859
+ @media (max-width: 920px) {
860
+ .products-tab .section-title,
861
+ .products-tab .panel-head {
862
+ align-items: flex-start;
863
+ flex-direction: column;
864
+ }
865
+
866
+ .products-tab .grid.cols-4,
867
+ .product-signal-grid {
868
+ grid-template-columns: repeat(2, minmax(0, 1fr));
869
+ }
870
+
871
+ .product-filters {
872
+ grid-template-columns: repeat(2, minmax(0, 1fr));
873
+ }
874
+ }
875
+
876
+ @media (max-width: 640px) {
877
+ .products-tab .grid.cols-4,
878
+ .product-signal-grid {
879
+ grid-template-columns: 1fr;
880
+ }
881
+
882
+ .products-tab .actions {
883
+ width: 100%;
884
+ }
885
+
886
+ .products-tab .actions .btn {
887
+ flex: 1 1 150px;
888
+ }
889
+
890
+ .products-tab .check-row {
891
+ grid-template-columns: 1fr;
892
+ }
893
+
894
+ .product-toolbar,
895
+ .product-filters,
896
+ .product-detail-meta,
897
+ .mini-row {
898
+ grid-template-columns: 1fr;
899
+ }
900
+ }
901
+ </style>