@mohasinac/appkit 4.53.0 → 4.53.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 (520) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/client/scaffolds/AppShell.js +1 -1
  3. package/dist/_internal/client/theme/ThemeProvider.js +15 -1
  4. package/dist/_internal/server/features/auctions/og.d.ts +3 -1
  5. package/dist/_internal/server/features/auctions/og.js +3 -1
  6. package/dist/_internal/server/features/bundles/actions.js +1 -1
  7. package/dist/_internal/server/features/business-documents/download.d.ts +41 -0
  8. package/dist/_internal/server/features/business-documents/download.js +91 -0
  9. package/dist/_internal/server/features/categories/adapters.d.ts +1 -1
  10. package/dist/_internal/server/features/categories/adapters.js +10 -0
  11. package/dist/_internal/server/features/categories/price-guidance-resolve.d.ts +66 -0
  12. package/dist/_internal/server/features/categories/price-guidance-resolve.js +138 -0
  13. package/dist/_internal/server/features/checkout/actions.d.ts +14 -0
  14. package/dist/_internal/server/features/checkout/actions.js +106 -12
  15. package/dist/_internal/server/features/checkout/bundle-expansion.js +17 -0
  16. package/dist/_internal/server/features/checkout/data.d.ts +22 -0
  17. package/dist/_internal/server/features/checkout/group-rule-revalidation.d.ts +34 -0
  18. package/dist/_internal/server/features/checkout/group-rule-revalidation.js +124 -0
  19. package/dist/_internal/server/features/checkout/supplier-identity.d.ts +55 -0
  20. package/dist/_internal/server/features/checkout/supplier-identity.js +81 -0
  21. package/dist/_internal/server/features/classified/metadata.js +1 -1
  22. package/dist/_internal/server/features/classified/og.d.ts +3 -1
  23. package/dist/_internal/server/features/classified/og.js +3 -1
  24. package/dist/_internal/server/features/digital-code/metadata.js +1 -1
  25. package/dist/_internal/server/features/digital-code/og.d.ts +3 -1
  26. package/dist/_internal/server/features/digital-code/og.js +3 -1
  27. package/dist/_internal/server/features/digital-code/pool.d.ts +19 -4
  28. package/dist/_internal/server/features/digital-code/pool.js +19 -4
  29. package/dist/_internal/server/features/live/metadata.js +1 -1
  30. package/dist/_internal/server/features/live/og.d.ts +3 -1
  31. package/dist/_internal/server/features/live/og.js +3 -1
  32. package/dist/_internal/server/features/messages/actions.js +1 -1
  33. package/dist/_internal/server/features/parts/og.js +1 -1
  34. package/dist/_internal/server/features/pre-orders/og.d.ts +3 -1
  35. package/dist/_internal/server/features/pre-orders/og.js +3 -1
  36. package/dist/_internal/server/features/price-index/market.d.ts +57 -0
  37. package/dist/_internal/server/features/price-index/market.js +120 -0
  38. package/dist/_internal/server/features/price-index/search.d.ts +18 -0
  39. package/dist/_internal/server/features/price-index/search.js +47 -7
  40. package/dist/_internal/server/features/prize-draws/og.d.ts +3 -1
  41. package/dist/_internal/server/features/prize-draws/og.js +3 -1
  42. package/dist/_internal/server/features/products/group-members.d.ts +26 -0
  43. package/dist/_internal/server/features/products/group-members.js +7 -1
  44. package/dist/_internal/server/features/products/list-public.d.ts +12 -0
  45. package/dist/_internal/server/features/products/list-public.js +10 -1
  46. package/dist/_internal/server/features/products/listing-tab-counts.d.ts +25 -1
  47. package/dist/_internal/server/features/products/listing-tab-counts.js +128 -10
  48. package/dist/_internal/server/features/products/og.d.ts +3 -1
  49. package/dist/_internal/server/features/products/og.js +3 -1
  50. package/dist/_internal/server/features/search/scoped-sources.d.ts +26 -0
  51. package/dist/_internal/server/features/search/scoped-sources.js +252 -0
  52. package/dist/_internal/server/features/search/search-everything.d.ts +57 -0
  53. package/dist/_internal/server/features/search/search-everything.js +70 -20
  54. package/dist/_internal/server/features/seo/sitemap.js +6 -0
  55. package/dist/_internal/server/features/shared/listing-adapters.js +4 -2
  56. package/dist/_internal/server/features/sticker-trace/adapters.d.ts +11 -0
  57. package/dist/_internal/server/features/sticker-trace/adapters.js +1 -0
  58. package/dist/_internal/server/features/sticker-trace/promote-to-listings.d.ts +26 -0
  59. package/dist/_internal/server/features/sticker-trace/promote-to-listings.js +264 -0
  60. package/dist/_internal/server/features/sticker-trace/sheet-actions.d.ts +3 -1
  61. package/dist/_internal/server/features/sticker-trace/sheet-actions.js +32 -0
  62. package/dist/_internal/server/features/sticker-trace/storage.d.ts +20 -1
  63. package/dist/_internal/server/features/sticker-trace/storage.js +20 -1
  64. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  65. package/dist/_internal/server/features/stores/adapters.js +16 -0
  66. package/dist/_internal/server/features/stores/index.d.ts +1 -0
  67. package/dist/_internal/server/features/stores/index.js +1 -0
  68. package/dist/_internal/server/features/stores/tabs.d.ts +30 -0
  69. package/dist/_internal/server/features/stores/tabs.js +124 -0
  70. package/dist/_internal/shared/checkout/rules/_defaults.js +0 -2
  71. package/dist/_internal/shared/checkout/rules/_limits.d.ts +14 -1
  72. package/dist/_internal/shared/checkout/rules/_limits.js +14 -1
  73. package/dist/_internal/shared/checkout/rules/_registry.js +2 -0
  74. package/dist/_internal/shared/checkout/rules/auction.rule.js +9 -1
  75. package/dist/_internal/shared/checkout/rules/bundle.rule.js +13 -0
  76. package/dist/_internal/shared/checkout/rules/index.d.ts +2 -0
  77. package/dist/_internal/shared/checkout/rules/index.js +9 -0
  78. package/dist/_internal/shared/checkout/rules/offer.rule.js +0 -1
  79. package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +26 -0
  80. package/dist/_internal/shared/checkout/rules/payment-gate.js +133 -0
  81. package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +29 -8
  82. package/dist/_internal/shared/checkout/rules/spares.rule.d.ts +16 -0
  83. package/dist/_internal/shared/checkout/rules/spares.rule.js +5 -0
  84. package/dist/_internal/shared/checkout/rules/types.d.ts +26 -2
  85. package/dist/_internal/shared/config/schema.d.ts +122 -0
  86. package/dist/_internal/shared/features/cart/schema.js +3 -2
  87. package/dist/_internal/shared/features/products/group-rules.d.ts +75 -0
  88. package/dist/_internal/shared/features/products/group-rules.js +142 -0
  89. package/dist/_internal/shared/listing-types/_registry.d.ts +24 -0
  90. package/dist/_internal/shared/listing-types/_registry.js +39 -1
  91. package/dist/_internal/shared/listing-types/art/config.d.ts +1 -0
  92. package/dist/_internal/shared/listing-types/art/config.js +2 -0
  93. package/dist/_internal/shared/listing-types/auction/config.d.ts +1 -0
  94. package/dist/_internal/shared/listing-types/auction/config.js +2 -0
  95. package/dist/_internal/shared/listing-types/availability.js +48 -0
  96. package/dist/_internal/shared/listing-types/capabilities.js +19 -0
  97. package/dist/_internal/shared/listing-types/classified/config.d.ts +1 -0
  98. package/dist/_internal/shared/listing-types/classified/config.js +2 -0
  99. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +1 -0
  100. package/dist/_internal/shared/listing-types/digital-code/config.js +2 -0
  101. package/dist/_internal/shared/listing-types/feature-flags.js +1 -0
  102. package/dist/_internal/shared/listing-types/live/config.d.ts +1 -0
  103. package/dist/_internal/shared/listing-types/live/config.js +2 -0
  104. package/dist/_internal/shared/listing-types/pre-order/config.d.ts +1 -0
  105. package/dist/_internal/shared/listing-types/pre-order/config.js +2 -0
  106. package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +1 -0
  107. package/dist/_internal/shared/listing-types/prize-draw/config.js +2 -0
  108. package/dist/_internal/shared/listing-types/spares/config.d.ts +74 -0
  109. package/dist/_internal/shared/listing-types/spares/config.js +60 -0
  110. package/dist/_internal/shared/listing-types/spares/ctas.d.ts +1 -0
  111. package/dist/_internal/shared/listing-types/spares/ctas.js +4 -0
  112. package/dist/_internal/shared/listing-types/spares/og.js +1 -0
  113. package/dist/_internal/shared/listing-types/spares/schema.d.ts +2 -0
  114. package/dist/_internal/shared/listing-types/spares/schema.js +3 -0
  115. package/dist/_internal/shared/listing-types/spares/seed-factory.d.ts +1 -0
  116. package/dist/_internal/shared/listing-types/spares/seed-factory.js +1 -0
  117. package/dist/_internal/shared/listing-types/standard/config.d.ts +1 -0
  118. package/dist/_internal/shared/listing-types/standard/config.js +2 -0
  119. package/dist/_internal/shared/listing-types/stickers/config.d.ts +1 -0
  120. package/dist/_internal/shared/listing-types/stickers/config.js +2 -0
  121. package/dist/_internal/shared/search/match.d.ts +11 -0
  122. package/dist/_internal/shared/search/match.js +30 -3
  123. package/dist/_internal/shared/search/to-groups.d.ts +52 -0
  124. package/dist/_internal/shared/search/to-groups.js +141 -0
  125. package/dist/client.d.ts +8 -2
  126. package/dist/client.js +4 -1
  127. package/dist/constants/api-endpoints.d.ts +46 -15
  128. package/dist/constants/api-endpoints.js +35 -5
  129. package/dist/constants/cache-invalidation.js +3 -1
  130. package/dist/constants/field-names.d.ts +6 -0
  131. package/dist/constants/field-names.js +6 -0
  132. package/dist/features/about/components/PolicyPageView.d.ts +22 -2
  133. package/dist/features/about/components/PolicyPageView.js +14 -2
  134. package/dist/features/about/components/PublicProfileView.js +2 -1
  135. package/dist/features/about/components/ShippingPolicyView.js +42 -2
  136. package/dist/features/account/components/NotificationBell.js +1 -1
  137. package/dist/features/account/hooks/useProfileStats.js +5 -1
  138. package/dist/features/addresses/repository/addresses.repository.d.ts +16 -0
  139. package/dist/features/addresses/repository/addresses.repository.js +21 -0
  140. package/dist/features/admin/actions/admin-actions.js +16 -1
  141. package/dist/features/admin/components/AdminCategoryEditorView.js +19 -2
  142. package/dist/features/admin/components/AdminNavigationView.js +1 -1
  143. package/dist/features/admin/components/AdminPageHeader.js +1 -1
  144. package/dist/features/admin/components/AdminProductEditorView.js +17 -2
  145. package/dist/features/admin/components/AdminSidebar.js +1 -1
  146. package/dist/features/admin/components/AdminSiteSettingsView.js +39 -8
  147. package/dist/features/admin/components/AdminSparesView.d.ts +12 -0
  148. package/dist/features/admin/components/AdminSparesView.js +30 -0
  149. package/dist/features/admin/components/AdminUsersView.js +9 -7
  150. package/dist/features/admin/components/index.d.ts +2 -0
  151. package/dist/features/admin/components/index.js +1 -0
  152. package/dist/features/admin/components/sections/adminSectionsTypes.js +7 -0
  153. package/dist/features/admin/constants/filter-tabs.d.ts +3 -0
  154. package/dist/features/admin/constants/filter-tabs.js +1 -0
  155. package/dist/features/admin/schemas/firestore.d.ts +26 -2
  156. package/dist/features/auctions/components/AuctionDetailPageView.js +12 -18
  157. package/dist/features/auth/repository/user.repository.d.ts +13 -0
  158. package/dist/features/auth/repository/user.repository.js +28 -0
  159. package/dist/features/auth/schemas/firestore.d.ts +2 -0
  160. package/dist/features/auth/schemas/firestore.js +2 -0
  161. package/dist/features/business-documents/repository/business-documents.repository.d.ts +58 -0
  162. package/dist/features/business-documents/repository/business-documents.repository.js +54 -0
  163. package/dist/features/business-documents/schemas/firestore.d.ts +105 -0
  164. package/dist/features/business-documents/schemas/firestore.js +75 -0
  165. package/dist/features/cart/actions/cart-actions.js +83 -6
  166. package/dist/features/cart/repository/cart.repository.js +32 -4
  167. package/dist/features/cart/schemas/firestore.d.ts +20 -0
  168. package/dist/features/cart/schemas/index.d.ts +2 -2
  169. package/dist/features/catalogue/components/PublicCatalogueItemDetailView.js +1 -1
  170. package/dist/features/categories/api/[id]/price-guidance/route.d.ts +42 -0
  171. package/dist/features/categories/api/[id]/price-guidance/route.js +90 -0
  172. package/dist/features/categories/components/BrandDetailPageView.js +32 -8
  173. package/dist/features/categories/components/BrandDetailTabs.d.ts +7 -1
  174. package/dist/features/categories/components/BrandDetailTabs.js +28 -3
  175. package/dist/features/categories/components/BundleCollage.js +3 -2
  176. package/dist/features/categories/components/BundleItemsPicker.js +3 -4
  177. package/dist/features/categories/components/CategoriesIndexListing.js +23 -8
  178. package/dist/features/categories/components/CategoryDetailPageView.js +157 -20
  179. package/dist/features/categories/components/CategoryDetailTabs.d.ts +7 -1
  180. package/dist/features/categories/components/CategoryDetailTabs.js +41 -7
  181. package/dist/features/categories/components/CategoryGrid.js +10 -1
  182. package/dist/features/categories/components/PriceGuidanceHint.d.ts +29 -0
  183. package/dist/features/categories/components/PriceGuidanceHint.js +61 -0
  184. package/dist/features/categories/components/SubcategoryCardRow.d.ts +27 -0
  185. package/dist/features/categories/components/SubcategoryCardRow.js +98 -0
  186. package/dist/features/categories/constants/category-tabs.d.ts +20 -25
  187. package/dist/features/categories/constants/category-tabs.js +22 -39
  188. package/dist/features/categories/repository/categories.repository.d.ts +12 -0
  189. package/dist/features/categories/repository/categories.repository.js +20 -0
  190. package/dist/features/categories/schemas/firestore.d.ts +22 -1
  191. package/dist/features/categories/schemas/firestore.js +10 -0
  192. package/dist/features/categories/schemas/index.d.ts +6 -0
  193. package/dist/features/categories/schemas/index.js +2 -0
  194. package/dist/features/categories/types/index.d.ts +8 -0
  195. package/dist/features/categories/utils/taxonomy-visual.d.ts +16 -3
  196. package/dist/features/categories/utils/taxonomy-visual.js +26 -10
  197. package/dist/features/classified/components/ClassifiedDetailPageView.js +14 -10
  198. package/dist/features/contact/components/ContactPageView.d.ts +33 -1
  199. package/dist/features/contact/components/ContactPageView.js +58 -28
  200. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +14 -10
  201. package/dist/features/grouped/components/GroupedListingDetailView.js +9 -1
  202. package/dist/features/grouped/schemas/firestore.d.ts +5 -0
  203. package/dist/features/homepage/actions/homepage-section-actions.d.ts +3 -3
  204. package/dist/features/homepage/components/CollectionCardsSection.d.ts +28 -6
  205. package/dist/features/homepage/components/CollectionCardsSection.js +43 -8
  206. package/dist/features/homepage/components/HowItWorksSection.d.ts +23 -6
  207. package/dist/features/homepage/components/HowItWorksSection.js +4 -4
  208. package/dist/features/homepage/components/PromoGrid.d.ts +31 -2
  209. package/dist/features/homepage/components/PromoGrid.js +12 -3
  210. package/dist/features/homepage/components/TestimonialsCarousel.d.ts +34 -2
  211. package/dist/features/homepage/components/TestimonialsCarousel.js +1 -1
  212. package/dist/features/homepage/components/index.d.ts +0 -20
  213. package/dist/features/homepage/components/index.js +0 -11
  214. package/dist/features/homepage/index.d.ts +0 -2
  215. package/dist/features/homepage/index.js +0 -2
  216. package/dist/features/homepage/lib/section-renderer.d.ts +9 -0
  217. package/dist/features/homepage/lib/section-renderer.js +202 -58
  218. package/dist/features/homepage/repository/homepage-sections.repository.d.ts +35 -3
  219. package/dist/features/homepage/repository/homepage-sections.repository.js +58 -8
  220. package/dist/features/homepage/schemas/firestore.d.ts +106 -3
  221. package/dist/features/homepage/schemas/firestore.js +32 -2
  222. package/dist/features/homepage/schemas/index.d.ts +36 -7
  223. package/dist/features/homepage/schemas/index.js +39 -27
  224. package/dist/features/homepage/server.d.ts +0 -3
  225. package/dist/features/homepage/server.js +0 -3
  226. package/dist/features/homepage/types/index.d.ts +0 -47
  227. package/dist/features/homepage/types/index.js +49 -0
  228. package/dist/features/layout/AppLayoutShell.js +1 -1
  229. package/dist/features/layout/BottomNavItem.js +1 -1
  230. package/dist/features/layout/BottomSheet.js +1 -1
  231. package/dist/features/layout/TitleBarLayout.js +1 -1
  232. package/dist/features/live/components/LiveItemDetailPageView.js +14 -10
  233. package/dist/features/media/hooks/useMedia.js +17 -0
  234. package/dist/features/media/repository/media-assets.repository.d.ts +46 -0
  235. package/dist/features/media/repository/media-assets.repository.js +31 -0
  236. package/dist/features/media/trust-signals.d.ts +85 -0
  237. package/dist/features/media/trust-signals.js +126 -0
  238. package/dist/features/parts/components/PartDetailPageView.d.ts +14 -1
  239. package/dist/features/parts/components/PartDetailPageView.js +2 -2
  240. package/dist/features/parts/components/PartsIndexPageView.d.ts +11 -2
  241. package/dist/features/parts/components/PartsIndexPageView.js +2 -2
  242. package/dist/features/parts/components/PartsSearchBox.d.ts +9 -0
  243. package/dist/features/parts/components/PartsSearchBox.js +57 -0
  244. package/dist/features/parts/schemas/firestore.d.ts +149 -3
  245. package/dist/features/parts/schemas/firestore.js +184 -0
  246. package/dist/features/payments/repository/payout.repository.d.ts +15 -0
  247. package/dist/features/payments/repository/payout.repository.js +21 -0
  248. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +12 -18
  249. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +1 -1
  250. package/dist/features/price-index/components/PriceIndexToolView.d.ts +24 -1
  251. package/dist/features/price-index/components/PriceIndexToolView.js +299 -17
  252. package/dist/features/price-index/lookup-target.d.ts +48 -0
  253. package/dist/features/price-index/lookup-target.js +107 -0
  254. package/dist/features/products/components/AvailabilityScopeHint.d.ts +22 -0
  255. package/dist/features/products/components/AvailabilityScopeHint.js +27 -0
  256. package/dist/features/products/components/CompareOverlay.d.ts +20 -2
  257. package/dist/features/products/components/CompareOverlay.js +23 -3
  258. package/dist/features/products/components/CustomSectionTabContent.d.ts +31 -0
  259. package/dist/features/products/components/CustomSectionTabContent.js +29 -3
  260. package/dist/features/products/components/GroupMemberPicker.d.ts +19 -1
  261. package/dist/features/products/components/GroupMemberPicker.js +46 -8
  262. package/dist/features/products/components/GroupSettingsPanel.d.ts +11 -1
  263. package/dist/features/products/components/GroupSettingsPanel.js +92 -3
  264. package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
  265. package/dist/features/products/components/PrizeDrawEntryActions.d.ts +15 -1
  266. package/dist/features/products/components/PrizeDrawEntryActions.js +70 -8
  267. package/dist/features/products/components/ProductDetailPageView.js +81 -17
  268. package/dist/features/products/components/ProductForm.d.ts +13 -1
  269. package/dist/features/products/components/ProductForm.js +16 -2
  270. package/dist/features/products/components/ProductsIndexListing.d.ts +16 -1
  271. package/dist/features/products/components/ProductsIndexListing.js +3 -3
  272. package/dist/features/products/components/ShowGroupSection.js +15 -2
  273. package/dist/features/products/components/SparesListView.d.ts +7 -0
  274. package/dist/features/products/components/SparesListView.js +39 -0
  275. package/dist/features/products/components/SublistingCarouselSection.js +2 -1
  276. package/dist/features/products/constants/action-defs.d.ts +1 -0
  277. package/dist/features/products/constants/action-defs.js +1 -0
  278. package/dist/features/products/repository/products.repository.d.ts +99 -0
  279. package/dist/features/products/repository/products.repository.js +240 -18
  280. package/dist/features/products/schemas/firestore.d.ts +97 -4
  281. package/dist/features/products/schemas/firestore.js +104 -0
  282. package/dist/features/products/schemas/index.d.ts +10 -19
  283. package/dist/features/products/schemas/index.js +23 -6
  284. package/dist/features/products/types/index.d.ts +45 -1
  285. package/dist/features/products/utils/group-member-image.d.ts +46 -0
  286. package/dist/features/products/utils/group-member-image.js +4 -0
  287. package/dist/features/products/utils/listing-badge-variant.js +4 -0
  288. package/dist/features/reviews/components/ReviewsList.js +1 -1
  289. package/dist/features/scams/components/ScamProfileView.js +1 -1
  290. package/dist/features/search/components/EntityTypeahead.d.ts +44 -0
  291. package/dist/features/search/components/EntityTypeahead.js +92 -0
  292. package/dist/features/search/components/Search.d.ts +12 -1
  293. package/dist/features/search/components/Search.js +47 -3
  294. package/dist/features/search/components/SearchFacetRail.d.ts +11 -0
  295. package/dist/features/search/components/SearchFacetRail.js +92 -0
  296. package/dist/features/search/components/SearchResultsClient.d.ts +33 -0
  297. package/dist/features/search/components/SearchResultsClient.js +103 -0
  298. package/dist/features/search/components/SearchResultsPageView.d.ts +15 -1
  299. package/dist/features/search/components/SearchResultsPageView.js +2 -2
  300. package/dist/features/search/components/index.d.ts +0 -6
  301. package/dist/features/search/components/index.js +0 -3
  302. package/dist/features/search/hooks/useNavSuggestions.d.ts +13 -0
  303. package/dist/features/search/hooks/useScopedSearch.d.ts +34 -0
  304. package/dist/features/search/hooks/useScopedSearch.js +114 -0
  305. package/dist/features/search/hooks/useSiteSearch.d.ts +26 -0
  306. package/dist/features/search/hooks/useSiteSearch.js +149 -0
  307. package/dist/features/search/index.d.ts +0 -4
  308. package/dist/features/search/index.js +0 -4
  309. package/dist/features/search/manifest.d.ts +12 -0
  310. package/dist/features/search/manifest.js +17 -2
  311. package/dist/features/search/server.d.ts +0 -3
  312. package/dist/features/search/server.js +0 -3
  313. package/dist/features/seller/components/SellerListingTypeView.d.ts +1 -1
  314. package/dist/features/seller/components/SellerListingTypeView.js +20 -0
  315. package/dist/features/seller/components/SellerPayoutSettingsView.js +35 -14
  316. package/dist/features/seller/components/SellerProductShell.d.ts +1 -1
  317. package/dist/features/seller/components/SellerProductShell.js +2 -1
  318. package/dist/features/seller/components/SellerProductsView.js +3 -2
  319. package/dist/features/seller/components/SellerSidebar.js +1 -1
  320. package/dist/features/seller/components/SellerSparesView.d.ts +17 -0
  321. package/dist/features/seller/components/SellerSparesView.js +6 -0
  322. package/dist/features/seller/components/index.d.ts +2 -0
  323. package/dist/features/seller/components/index.js +1 -0
  324. package/dist/features/shell/CommandPalette.js +1 -1
  325. package/dist/features/sticker-trace/schemas/firestore.d.ts +19 -0
  326. package/dist/features/stores/components/StoreAuctionsListing.js +2 -1
  327. package/dist/features/stores/components/StoreDetailLayoutView.d.ts +1 -1
  328. package/dist/features/stores/components/StoreDetailLayoutView.js +7 -82
  329. package/dist/features/stores/components/StoreHeader.js +1 -2
  330. package/dist/features/stores/components/StoreSparesPageView.d.ts +27 -0
  331. package/dist/features/stores/components/StoreSparesPageView.js +48 -0
  332. package/dist/features/stores/schemas/firestore.d.ts +19 -0
  333. package/dist/features/tester/seed-data/authored/buying__spares-and-groups.d.ts +2 -0
  334. package/dist/features/tester/seed-data/authored/buying__spares-and-groups.js +96 -0
  335. package/dist/features/tester/seed-data/authored/content-discovery__authenticity-checker.d.ts +2 -0
  336. package/dist/features/tester/seed-data/authored/content-discovery__authenticity-checker.js +392 -45
  337. package/dist/features/tester/seed-data/authored/content-discovery__category-tabs-franchise-bundles.js +54 -32
  338. package/dist/features/tester/seed-data/authored/content-discovery__parts-database.js +3 -2
  339. package/dist/features/tester/seed-data/authored/content-discovery__price-index-and-compare.d.ts +2 -0
  340. package/dist/features/tester/seed-data/authored/content-discovery__price-index-and-compare.js +73 -0
  341. package/dist/features/tester/seed-data/authored/content-discovery__site-wide-index.d.ts +2 -0
  342. package/dist/features/tester/seed-data/authored/content-discovery__site-wide-index.js +340 -0
  343. package/dist/features/tester/seed-data/authored/index.js +16 -0
  344. package/dist/features/tester/seed-data/authored/search-and-nav__header-search.d.ts +1 -0
  345. package/dist/features/tester/seed-data/authored/search-and-nav__header-search.js +57 -0
  346. package/dist/features/tester/seed-data/authored/selling__spares-and-group-editing.d.ts +2 -0
  347. package/dist/features/tester/seed-data/authored/selling__spares-and-group-editing.js +77 -0
  348. package/dist/features/tester/seed-data/authored/tools__sticker-promote-to-listings.d.ts +2 -0
  349. package/dist/features/tester/seed-data/authored/tools__sticker-promote-to-listings.js +58 -0
  350. package/dist/features/tester/seed-data/authored/tools__sticker-tracer.js +5 -4
  351. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +384 -18
  352. package/dist/index.d.ts +17 -57
  353. package/dist/index.js +35 -96
  354. package/dist/next/routing/route-map.d.ts +42 -1
  355. package/dist/next/routing/route-map.js +45 -0
  356. package/dist/providers/db-firebase/base.repository.d.ts +22 -0
  357. package/dist/providers/db-firebase/base.repository.js +36 -0
  358. package/dist/schemas/registry.d.ts +15 -6
  359. package/dist/security/blind-index-tokens.d.ts +54 -0
  360. package/dist/security/blind-index-tokens.js +152 -0
  361. package/dist/security/index.d.ts +1 -1
  362. package/dist/security/index.js +1 -1
  363. package/dist/security/settings-encryption.d.ts +35 -0
  364. package/dist/security/settings-encryption.js +91 -0
  365. package/dist/seed/_helpers/brand-rows.js +38 -14
  366. package/dist/seed/_helpers/category-forest.js +73 -7
  367. package/dist/seed/_helpers/category-images-generated.d.ts +25 -6
  368. package/dist/seed/_helpers/category-images-generated.js +81 -6
  369. package/dist/seed/_helpers/category-models.d.ts +1 -1
  370. package/dist/seed/_helpers/category-models.js +7 -1
  371. package/dist/seed/_helpers/category-parts-glossary-map.js +8 -0
  372. package/dist/seed/_helpers/category-parts.d.ts +1 -1
  373. package/dist/seed/_helpers/category-parts.js +50 -2
  374. package/dist/seed/_helpers/character-features.d.ts +1 -1
  375. package/dist/seed/_helpers/character-features.js +2 -1
  376. package/dist/seed/_helpers/franchise-rows.js +10 -2
  377. package/dist/seed/_helpers/media.d.ts +47 -0
  378. package/dist/seed/_helpers/media.js +34 -0
  379. package/dist/seed/authenticity-knowledge-seed-data.d.ts +49 -5
  380. package/dist/seed/authenticity-knowledge-seed-data.js +10001 -549
  381. package/dist/seed/categories-seed-data.js +124 -20
  382. package/dist/seed/grouped-listings-seed-data.js +16 -0
  383. package/dist/seed/homepage-sections-seed-data.js +94 -5
  384. package/dist/seed/index.d.ts +2 -3
  385. package/dist/seed/index.js +2 -2
  386. package/dist/seed/manifest.js +3 -0
  387. package/dist/seed/media-assets-seed-data.d.ts +1 -1
  388. package/dist/seed/media-assets-seed-data.js +746 -196
  389. package/dist/seed/model-composition-seed-data.js +122 -6
  390. package/dist/seed/part-glossary-seed-data.d.ts +1 -1
  391. package/dist/seed/part-glossary-seed-data.js +37219 -463
  392. package/dist/seed/products-art-seed-data.js +7 -18
  393. package/dist/seed/products-auctions-seed-data.js +56 -47
  394. package/dist/seed/products-beyblade-x-seed-data.d.ts +2 -0
  395. package/dist/seed/products-beyblade-x-seed-data.js +476 -0
  396. package/dist/seed/products-catalogue-seed-data.d.ts +5 -5
  397. package/dist/seed/products-catalogue-seed-data.js +1500 -1240
  398. package/dist/seed/products-classifieds-seed-data.js +9 -27
  399. package/dist/seed/products-digital-codes-seed-data.js +9 -18
  400. package/dist/seed/products-live-items-seed-data.js +5 -13
  401. package/dist/seed/products-preorders-seed-data.js +67 -33
  402. package/dist/seed/products-prize-draws-seed-data.js +8 -17
  403. package/dist/seed/products-spares-seed-data.d.ts +2 -0
  404. package/dist/seed/products-spares-seed-data.js +368 -0
  405. package/dist/seed/products-standard-seed-data.js +39 -126
  406. package/dist/seed/products-stickers-seed-data.js +59 -15
  407. package/dist/seed/site-settings-seed-data.js +8 -0
  408. package/dist/seo/json-ld.d.ts +25 -0
  409. package/dist/seo/json-ld.js +28 -0
  410. package/dist/server-entry.d.ts +3 -1
  411. package/dist/server-entry.js +2 -2
  412. package/dist/server.d.ts +11 -9
  413. package/dist/server.js +35 -25
  414. package/dist/styles.css +202 -1
  415. package/dist/tailwind-input.css +31 -17
  416. package/dist/tailwind-utilities.css +1 -1
  417. package/dist/ui/components/Accordion.js +1 -1
  418. package/dist/ui/components/Breadcrumb.js +1 -1
  419. package/dist/ui/components/DetailPageGallery.js +0 -2
  420. package/dist/ui/components/DetailPageHero.js +0 -1
  421. package/dist/ui/components/DetailPageTabs.js +1 -1
  422. package/dist/ui/components/Divider.js +3 -3
  423. package/dist/ui/components/GatedPrice.js +5 -1
  424. package/dist/ui/components/HorizontalScroller.js +1 -1
  425. package/dist/ui/components/ListingLayout.js +3 -3
  426. package/dist/ui/components/MediaTabs.style.css +81 -0
  427. package/dist/ui/components/Pagination.js +1 -1
  428. package/dist/ui/components/PendingValue.d.ts +45 -0
  429. package/dist/ui/components/PendingValue.js +7 -0
  430. package/dist/ui/components/Radio.style.css +119 -0
  431. package/dist/ui/components/RatingDisplay.js +2 -2
  432. package/dist/ui/components/SettingsSection.js +0 -2
  433. package/dist/ui/components/SiteLogo.js +3 -0
  434. package/dist/ui/components/SiteMark.js +3 -0
  435. package/dist/ui/components/SortDropdown.js +1 -1
  436. package/dist/ui/components/SpecTable.d.ts +58 -0
  437. package/dist/ui/components/SpecTable.js +8 -0
  438. package/dist/ui/components/StarRating.js +1 -3
  439. package/dist/ui/components/SummaryCard.js +1 -1
  440. package/dist/ui/components/index.style.css +1 -0
  441. package/dist/ui/icons/icon-registry.d.ts +1 -0
  442. package/dist/ui/icons/icon-registry.js +9 -1
  443. package/dist/ui/index.d.ts +4 -0
  444. package/dist/ui/index.js +2 -0
  445. package/dist/utils/search-txt-query.d.ts +12 -0
  446. package/dist/utils/search-txt-query.js +12 -0
  447. package/package.json +1 -1
  448. package/scripts/dedupe-peer-deps.mjs +18 -0
  449. package/scripts/seed-cli.mjs +250 -35
  450. package/scripts/verify-css-build.mjs +118 -37
  451. package/dist/_internal/server/features/search/actions.d.ts +0 -7
  452. package/dist/_internal/server/features/search/actions.js +0 -21
  453. package/dist/_internal/server/features/search/data.d.ts +0 -15
  454. package/dist/_internal/server/features/search/data.js +0 -18
  455. package/dist/_internal/server/features/search/index.d.ts +0 -3
  456. package/dist/_internal/server/features/search/index.js +0 -2
  457. package/dist/features/homepage/api/[id]/route.d.ts +0 -23
  458. package/dist/features/homepage/api/[id]/route.js +0 -103
  459. package/dist/features/homepage/api/route.d.ts +0 -14
  460. package/dist/features/homepage/api/route.js +0 -140
  461. package/dist/features/homepage/api/sections/[id]/route.d.ts +0 -26
  462. package/dist/features/homepage/api/sections/[id]/route.js +0 -95
  463. package/dist/features/homepage/api/sections/route.d.ts +0 -13
  464. package/dist/features/homepage/api/sections/route.js +0 -82
  465. package/dist/features/homepage/components/AdvertisementBanner.d.ts +0 -14
  466. package/dist/features/homepage/components/AdvertisementBanner.js +0 -35
  467. package/dist/features/homepage/components/BeforeAfterCard.d.ts +0 -9
  468. package/dist/features/homepage/components/BeforeAfterCard.js +0 -73
  469. package/dist/features/homepage/components/FeaturedResultsSection.d.ts +0 -17
  470. package/dist/features/homepage/components/FeaturedResultsSection.js +0 -17
  471. package/dist/features/homepage/components/HeroBanner.d.ts +0 -7
  472. package/dist/features/homepage/components/HeroBanner.js +0 -43
  473. package/dist/features/homepage/components/HeroSection.d.ts +0 -7
  474. package/dist/features/homepage/components/HeroSection.js +0 -6
  475. package/dist/features/homepage/components/HomepageSkeleton.d.ts +0 -2
  476. package/dist/features/homepage/components/HomepageSkeleton.js +0 -21
  477. package/dist/features/homepage/components/HomepageView.d.ts +0 -25
  478. package/dist/features/homepage/components/HomepageView.js +0 -5
  479. package/dist/features/homepage/components/NewsletterBanner.d.ts +0 -23
  480. package/dist/features/homepage/components/NewsletterBanner.js +0 -37
  481. package/dist/features/homepage/components/SiteFeaturesSection.d.ts +0 -14
  482. package/dist/features/homepage/components/SiteFeaturesSection.js +0 -11
  483. package/dist/features/homepage/components/TrustBadges.d.ts +0 -6
  484. package/dist/features/homepage/components/TrustBadges.js +0 -46
  485. package/dist/features/homepage/components/TrustIndicatorsSection.d.ts +0 -12
  486. package/dist/features/homepage/components/TrustIndicatorsSection.js +0 -11
  487. package/dist/features/homepage/hooks/useHomepage.d.ts +0 -11
  488. package/dist/features/homepage/hooks/useHomepage.js +0 -17
  489. package/dist/features/homepage/hooks/useHomepageSections.d.ts +0 -2
  490. package/dist/features/homepage/hooks/useHomepageSections.js +0 -10
  491. package/dist/features/homepage/repository/homepage.repository.d.ts +0 -11
  492. package/dist/features/homepage/repository/homepage.repository.js +0 -25
  493. package/dist/features/search/actions/index.d.ts +0 -1
  494. package/dist/features/search/actions/index.js +0 -1
  495. package/dist/features/search/actions/search-actions.d.ts +0 -18
  496. package/dist/features/search/actions/search-actions.js +0 -43
  497. package/dist/features/search/api/route.d.ts +0 -22
  498. package/dist/features/search/api/route.js +0 -128
  499. package/dist/features/search/columns/index.d.ts +0 -17
  500. package/dist/features/search/columns/index.js +0 -49
  501. package/dist/features/search/components/SearchFiltersRow.d.ts +0 -22
  502. package/dist/features/search/components/SearchFiltersRow.js +0 -21
  503. package/dist/features/search/components/SearchResultsSection.d.ts +0 -53
  504. package/dist/features/search/components/SearchResultsSection.js +0 -35
  505. package/dist/features/search/components/SearchView.d.ts +0 -27
  506. package/dist/features/search/components/SearchView.js +0 -8
  507. package/dist/features/search/hooks/useSearch.d.ts +0 -21
  508. package/dist/features/search/hooks/useSearch.js +0 -33
  509. package/dist/features/search/repository/search.repository.d.ts +0 -14
  510. package/dist/features/search/repository/search.repository.js +0 -59
  511. package/dist/features/search/schemas/index.d.ts +0 -70
  512. package/dist/features/search/schemas/index.js +0 -36
  513. package/dist/features/search/types/index.d.ts +0 -48
  514. package/dist/features/stores/components/StoreScopedSearch.d.ts +0 -17
  515. package/dist/features/stores/components/StoreScopedSearch.js +0 -27
  516. package/dist/seed/defaults/homepage-sections.d.ts +0 -2
  517. package/dist/seed/defaults/homepage-sections.js +0 -60
  518. package/dist/seed/factories/homepage-section.factory.d.ts +0 -10
  519. package/dist/seed/factories/homepage-section.factory.js +0 -13
  520. /package/dist/{features/search/types/index.js → _internal/shared/listing-types/spares/og.d.ts} +0 -0
@@ -7,8 +7,8 @@ import { categoriesRepository, productRepository, storeRepository, } from "../..
7
7
  import { Container, Div, Heading, ImageMosaic, Main, MOSAIC_MIN, Section, Span, SplitTile, Stack, Text } from "../../../ui/index.js";
8
8
  import { MediaImage } from "../../media/MediaImage.js";
9
9
  import { RichTextRenderer } from "../../../ui/rich-text/RichTextRenderer.js";
10
- import { OverflowRow } from "../../../ui/index.js";
11
10
  import { resolveTaxonomyVisual } from "../utils/taxonomy-visual.js";
11
+ import { SubcategoryCardRow } from "./SubcategoryCardRow.js";
12
12
  import { CategoryDetailTabs } from "./CategoryDetailTabs.js";
13
13
  import { CategoryGrid } from "./CategoryGrid.js";
14
14
  import { CategoryHighlightsAndFaqSection } from "./CategoryHighlightsAndFaqSection.js";
@@ -27,6 +27,7 @@ import { safeRead } from "../../../errors/safe-read.js";
27
27
  import { breadcrumbJsonLd, collectionPageJsonLd, faqJsonLd, } from "../../../seo/json-ld.js";
28
28
  import { toStoreListItem } from "../../../_internal/server/features/stores/adapters.js";
29
29
  import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility.js";
30
+ import { productImageUrls } from "../../products/schemas/firestore.js";
30
31
  const __O = {
31
32
  hidden: "overflow-hidden",
32
33
  };
@@ -49,17 +50,73 @@ const __W = "max-w-7xl mx-auto";
49
50
  * Admin surfaces that genuinely need both still show both —
50
51
  * `CategoryTableColumns` renders "own (total)" explicitly.
51
52
  */
52
- function toSubcategoryItems(children) {
53
+ /*
54
+ * 🛑 products + auctions, SUMMED.
55
+ *
56
+ * `metrics.productCount` EXCLUDES auctions — `Tally` keeps `productIds` and
57
+ * `auctionIds` as separate arrays and writes `auctionCount` /
58
+ * `totalAuctionCount` alongside, so the comparable "how much is in here"
59
+ * figure is the sum of both. Reading only the product half understated every
60
+ * subcategory holding an auction, and understated it silently: the chip
61
+ * showed a plausible smaller number rather than anything that looked wrong.
62
+ *
63
+ * This page already derives its own header figures correctly that way
64
+ * (`productCount` and `auctionCount` resolved separately further down); this
65
+ * was the one place that took the product half for the whole.
66
+ *
67
+ * Prefer the rollup (`total*`) over the own-count for both halves, because a
68
+ * chip is a navigational promise about the whole subtree you are about to
69
+ * enter, not about what happens to be filed at that exact node. Shared by
70
+ * `toSubcategoryItems` (the chip's badge count) and the scroller's own
71
+ * filter below, so the two can't disagree about what "has listings" means.
72
+ */
73
+ function listingCountFor(child) {
74
+ const products = child.metrics?.totalProductCount ?? child.metrics?.productCount ?? 0;
75
+ const auctions = child.metrics?.totalAuctionCount ?? child.metrics?.auctionCount ?? 0;
76
+ return products + auctions;
77
+ }
78
+ /**
79
+ * Direct children → the Subcategory card row's items.
80
+ *
81
+ * `descendants` is the SAME array the page already fetched, passed in so each
82
+ * card can carry its own direct children's names for the filter. One level
83
+ * only — see `SubcategoryCardRow`'s header for why matching the whole subtree
84
+ * would surface cards for names the visitor never sees.
85
+ */
86
+ function toSubcategoryItems(children, descendants) {
87
+ /*
88
+ * Grandchildren indexed by parent in ONE pass. The alternative — filtering
89
+ * `descendants` inside the map — is O(children x descendants), and on the
90
+ * spinning-tops root that is 5 x ~1,200 per render for a list that never
91
+ * changes.
92
+ *
93
+ * 🛑 Keyed on `parentIds[last]`, the IMMEDIATE parent. `parentIds` holds the
94
+ * full ancestor chain, so testing `.includes(child.id)` would match every
95
+ * descendant at any depth and quietly turn the one-level rule into a
96
+ * whole-subtree match.
97
+ */
98
+ const childNamesByParent = new Map();
99
+ for (const d of descendants) {
100
+ const parent = d.parentIds?.[d.parentIds.length - 1];
101
+ if (!parent)
102
+ continue;
103
+ const list = childNamesByParent.get(parent);
104
+ if (list)
105
+ list.push(d.name);
106
+ else
107
+ childNamesByParent.set(parent, [d.name]);
108
+ }
53
109
  return children.map((child) => {
54
- const count = child.metrics?.totalProductCount ?? child.metrics?.productCount ?? 0;
110
+ const count = listingCountFor(child);
55
111
  return {
56
112
  id: child.id,
57
- label: child.name,
113
+ name: child.name,
58
114
  // Omitted rather than rendered as 0 — `count` is "not counted" when
59
- // undefined, and a chip reading "0" invites a click that lands on an
115
+ // undefined, and a card reading "0" invites a click that lands on an
60
116
  // empty page.
61
117
  count: count > 0 ? count : undefined,
62
118
  href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(child.slug)),
119
+ childNames: childNamesByParent.get(child.id) ?? [],
63
120
  ...resolveTaxonomyVisual(child),
64
121
  };
65
122
  });
@@ -114,7 +171,7 @@ export async function CategoryDetailPageView({ slug }) {
114
171
  // Four of the ten tabs used to have no count at all and could never hide.
115
172
  categoriesIn
116
173
  ? listingTabCounts(CATEGORY_PAGE_TABS, { categoriesIn })
117
- : Promise.resolve({}),
174
+ : Promise.resolve({ available: {}, total: {} }),
118
175
  // SB-UNI-D — bundles fetched from the categories collection. We pull
119
176
  // all active bundle rows; the carousel filters by category affinity.
120
177
  categoriesIn
@@ -216,9 +273,76 @@ export async function CategoryDetailPageView({ slug }) {
216
273
  })
217
274
  : Promise.resolve([]),
218
275
  ]);
276
+ // 🛑 SAME TIER ONLY. `rootSiblingCategories` spans the whole root subtree —
277
+ // siblings AND cousins across every depth — so without a tier check this
278
+ // rendered deeper descendants (children, grandchildren) as "related",
279
+ // which reads as a second, redundant Subcategories row rather than a set
280
+ // of actual peers. A category with no siblings at its own tier correctly
281
+ // shows nothing here; the tier-grouped descendant tabs above already cover
282
+ // "what's below this category".
219
283
  const relatedCategories = rootSiblingCategories
220
- .filter((c) => c.id !== category?.id && (!c.categoryType || c.categoryType === "category"))
284
+ .filter((c) => c.id !== category?.id &&
285
+ (!c.categoryType || c.categoryType === "category") &&
286
+ c.tier === category?.tier)
221
287
  .slice(0, RELATED_CATEGORY_LIMIT);
288
+ // 🛑 ONLY children that actually have something in them. A direct child
289
+ // with zero listings anywhere in its own subtree is a dead end — clicking
290
+ // it opens an empty category page — so it's dropped from the chip row
291
+ // rather than rendered with no count badge. `listingCountFor` reads the
292
+ // ROLLUP (own + descendants), so a child that merely has no listings of
293
+ // its OWN but does further down is correctly kept.
294
+ const directChildrenWithListings = childCategories
295
+ .filter((c) => c.tier === (category?.tier ?? 0) + 1)
296
+ .filter((c) => listingCountFor(c) > 0);
297
+ /*
298
+ * The ancestor chain as cards, root first.
299
+ *
300
+ * 🛑 `ancestor.id` is used as the slug, which is correct HERE and is not a
301
+ * general licence: categories are one of this codebase's "pure slug"
302
+ * collections (`id === slug`), so the two are interchangeable for a category
303
+ * row. `CategoryAncestor` carries only `{id, name, tier}` — no slug field —
304
+ * so the alternative would be a lookup per ancestor for a value already in
305
+ * hand.
306
+ *
307
+ * `tier` comes from the stored ancestor rather than the array index: the
308
+ * index is only the depth if the chain is complete, and a chain that skipped
309
+ * a tier would silently relabel every level below it (the closure-skip case
310
+ * CLAUDE.md records under the counters invariant).
311
+ */
312
+ /*
313
+ * `CategoryAncestor` is `{id, name, tier}` and carries no display block, so
314
+ * the artwork is looked up in rows this page ALREADY fetched —
315
+ * `rootSiblingCategories` spans the whole root subtree, which is where an
316
+ * ancestor lives. A miss simply yields no visual and the card falls back,
317
+ * which is why this is a plain lookup and not another read.
318
+ */
319
+ const rowsById = new Map();
320
+ for (const c of [...rootSiblingCategories, ...childCategories]) {
321
+ if (c?.id)
322
+ rowsById.set(c.id, c);
323
+ }
324
+ /*
325
+ * 🛑 The IMMEDIATE parent only — one card, never the chain.
326
+ *
327
+ * `ancestors` is ordered root-first, so the last entry is the direct parent
328
+ * and everything before it is a grandparent. Rendering the whole ladder put
329
+ * four cards on a tier-4 page for a path the breadcrumb at the top of the
330
+ * page already spells out in full, and the uncles/grandparents are not
331
+ * places you step to from here — the one move this row needs to afford is
332
+ * "up one".
333
+ */
334
+ const immediateParent = (category?.ancestors ?? []).at(-1);
335
+ const parentLadder = immediateParent
336
+ ? [
337
+ {
338
+ id: immediateParent.id,
339
+ name: immediateParent.name,
340
+ href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(immediateParent.id)),
341
+ level: immediateParent.tier,
342
+ ...resolveTaxonomyVisual(rowsById.get(immediateParent.id) ?? null),
343
+ },
344
+ ]
345
+ : [];
222
346
  // Stores tab — stores whose storeCategory is this category or ANY descendant.
223
347
  //
224
348
  // `storeCategory` is a single slug with no ancestor chain (unlike a product's
@@ -282,18 +406,36 @@ export async function CategoryDetailPageView({ slug }) {
282
406
  * and it cast `createdAt` through `as unknown as string` where the adapter
283
407
  * converts a Timestamp properly.
284
408
  */
285
- .map((s) => toStoreListItem(s));
409
+ .map((s) => toStoreListItem(s))
410
+ // 🛑 `storeCategory` is a one-time "primarily sells this" tag, not proof
411
+ // of current stock — a store can match the category and still have
412
+ // nothing listed right now (the exact shape of the empty-store report
413
+ // this filter closes). Listing it here sends a visitor into a store page
414
+ // with nothing to buy, so only stores that actually have products are
415
+ // kept. `totalProducts` is the denormalised `stats.totalProducts`
416
+ // counter, incremented atomically on create/delete — cheap to read here,
417
+ // unlike a per-store query.
418
+ .filter((s) => (s.totalProducts ?? 0) > 0);
286
419
  const storeCount = categoryStores.length;
287
420
  // `tabCounts` is keyed by tabSlug and already covers every listing type plus
288
421
  // bundles; `stores` is added here because this page resolves its store list
289
422
  // eagerly to render the tab body anyway.
290
- const counts = { ...tabCounts, stores: storeCount };
423
+ // Two maps, not one: the BADGE is what the default (available) scope will
424
+ // render, while HIDING must stay keyed on the total — a tab whose rows have
425
+ // all ended still has a browsable archive behind the Ended/All scopes.
426
+ // `stores` is added to both because this page resolves its store list
427
+ // eagerly to render the tab body anyway, and a store has no availability
428
+ // scope of its own.
429
+ const counts = { ...tabCounts.available, stores: storeCount };
430
+ const totals = { ...tabCounts.total, stores: storeCount };
291
431
  // Header pills read the same numbers the tabs do. `?? 0` is safe here (a pill
292
432
  // simply doesn't render) — unlike the tab bar, where an unknown count must
293
433
  // stay visible rather than collapse to "hide me".
294
- const productCount = counts.products ?? category?.metrics?.totalProductCount ?? category?.metrics?.productCount ?? 0;
295
- const auctionCount = counts.auctions ?? category?.metrics?.totalAuctionCount ?? category?.metrics?.auctionCount ?? 0;
296
- const preOrderCount = counts["pre-orders"] ?? 0;
434
+ // Header pills read TOTALS, deliberately: "47 products" is a statement about
435
+ // what this category holds, not about what one scope is currently showing.
436
+ const productCount = totals.products ?? category?.metrics?.totalProductCount ?? category?.metrics?.productCount ?? 0;
437
+ const auctionCount = totals.auctions ?? category?.metrics?.totalAuctionCount ?? category?.metrics?.auctionCount ?? 0;
438
+ const preOrderCount = totals["pre-orders"] ?? 0;
297
439
  /*
298
440
  * G3b — OWN vs CHILDREN, so one number stops answering two questions.
299
441
  *
@@ -333,7 +475,7 @@ export async function CategoryDetailPageView({ slug }) {
333
475
  const mosaicImages = hasCover
334
476
  ? []
335
477
  : (productsResult?.items ?? [])
336
- .map((row) => row.mainImage ?? row.images?.[0] ?? null)
478
+ .map((row) => row.mainImage || productImageUrls(row.images)[0] || null)
337
479
  .filter((src) => !!src)
338
480
  .slice(0, 5);
339
481
  const hasMosaic = mosaicImages.length >= MOSAIC_MIN;
@@ -430,7 +572,7 @@ export async function CategoryDetailPageView({ slug }) {
430
572
  const ldFaq = category?.faqs && category.faqs.length > 0
431
573
  ? faqJsonLd(category.faqs.map((f) => ({ question: f.question, answer: f.answer })))
432
574
  : null;
433
- return (_jsxs(Main, { children: [_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldBreadcrumb) } }), _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldCollection) } }), ldFaq && (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldFaq) } })), _jsxs(Section, { className: `relative ${__O.hidden} ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-[var(--appkit-color-bg)]"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: coverImage, alt: "", size: "banner" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), !hasCover && hasMosaic && (_jsx(Div, { className: "relative z-10 max-w-7xl mx-auto", padding: "x-md", children: _jsx(Div, { padding: "t-lg", children: _jsx(ImageMosaic, { images: mosaicImages, alt: `${category?.name ?? slug} on LetItRip` }) }) })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsx(BreadcrumbTrail, { crumbs: trail, className: "mb-4", tone: hasCover ? "on-media" : "default" }), category?.id && _jsx(LastCategoryBeacon, { categoryId: category.id }), _jsx(Heading, { color: "inverse", level: 1, className: `mb-2 ${hasCover ? "" : "text-[var(--appkit-color-text)]"}`, size: "3xl", mdSize: "4xl", weight: "bold", children: category?.name ?? slug }), category?.description && typeof category.description === "string" && !category.description.startsWith("{") && (_jsx(Text, { color: hasCover ? "inverse" : "muted", className: `max-w-2xl mb-4 ${hasCover ? "/80" : ""}`, size: "base", children: category.description })), _jsxs(Row, { wrap: true, gap: "sm", children: [productCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: "inverse", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "bg-primary/10 text-primary-700 dark:text-primary-400", rounded: "full", padding: "pill-sm-tall", children: [productCount.toLocaleString(), " ", productCount === 1 ? "product" : "products"] })), showCountSplit && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "muted", size: "xs", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [ownHere.toLocaleString(), " here \u00B7 ", inSubcategories.toLocaleString(), " in subcategories"] })), auctionCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "warning", surface: hasCover ? undefined : "warning-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [auctionCount.toLocaleString(), " ", auctionCount === 1 ? "auction" : "auctions"] })), preOrderCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "info", surface: hasCover ? undefined : "info-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [preOrderCount.toLocaleString(), " ", preOrderCount === 1 ? "pre-order" : "pre-orders"] })), storeCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "success", surface: hasCover ? undefined : "success-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [storeCount.toLocaleString(), " ", storeCount === 1 ? "store" : "stores"] }))] })] })] }), contentBody && contentBody.trim().length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(RichTextRenderer, { html: contentBody }) }) })), _jsx(CategoryHighlightsAndFaqSection, { highlights: category?.highlights, faqs: category?.faqs }), compositions.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: __W, padding: "section", children: _jsxs(Stack, { gap: "md", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "What it is made of" }), _jsx(ModelCompositionPanel, { compositions: compositions, hrefForPart: (slot, tail) => String(ROUTES.PUBLIC.PART_DETAIL(slot, tail)) })] }) }) })), (() => {
575
+ return (_jsxs(Main, { children: [_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldBreadcrumb) } }), _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldCollection) } }), ldFaq && (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldFaq) } })), _jsxs(Section, { className: `relative ${__O.hidden} ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-[var(--appkit-color-bg)]"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: coverImage, alt: "", size: "banner" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), !hasCover && hasMosaic && (_jsx(Div, { className: "relative z-10 max-w-7xl mx-auto", padding: "x-md", children: _jsx(Div, { padding: "t-lg", children: _jsx(ImageMosaic, { images: mosaicImages, alt: `${category?.name ?? slug} on LetItRip` }) }) })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsx(BreadcrumbTrail, { crumbs: trail, className: "mb-4", tone: hasCover ? "on-media" : "default" }), category?.id && _jsx(LastCategoryBeacon, { categoryId: category.id }), _jsx(Heading, { color: "inverse", level: 1, className: `mb-2 ${hasCover ? "" : "text-[var(--appkit-color-text)]"}`, size: "3xl", mdSize: "4xl", weight: "bold", children: category?.name ?? slug }), category?.description && typeof category.description === "string" && !category.description.startsWith("{") && (_jsx(Text, { color: hasCover ? "inverse" : "muted", className: `max-w-2xl mb-4 ${hasCover ? "/80" : ""}`, size: "base", children: category.description })), _jsxs(Row, { wrap: true, gap: "sm", children: [productCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: "inverse", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "bg-primary/10 text-primary-700 dark:text-primary-400", rounded: "full", padding: "pill-sm-tall", children: [productCount.toLocaleString(), " ", productCount === 1 ? "product" : "products"] })), showCountSplit && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "muted", size: "xs", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [ownHere.toLocaleString(), " here \u00B7 ", inSubcategories.toLocaleString(), " in subcategories"] })), auctionCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "warning", surface: hasCover ? undefined : "warning-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [auctionCount.toLocaleString(), " ", auctionCount === 1 ? "auction" : "auctions"] })), preOrderCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "info", surface: hasCover ? undefined : "info-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [preOrderCount.toLocaleString(), " ", preOrderCount === 1 ? "pre-order" : "pre-orders"] })), storeCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "success", surface: hasCover ? undefined : "success-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [storeCount.toLocaleString(), " ", storeCount === 1 ? "store" : "stores"] }))] })] })] }), category?.hasbroName && (_jsx(Section, { padding: "y-md", children: _jsx(Container, { size: "xl", children: _jsxs(Row, { gap: "md", align: "center", wrap: true, children: [category.hasbroCoverImage && (_jsx(MediaImage, { src: category.hasbroCoverImage, alt: `${category.hasbroName} logo`, size: "thumbnail", className: "h-12 w-auto flex-shrink-0" })), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", color: "muted", transform: "uppercase", weight: "medium", children: "Also known as" }), _jsx(Text, { weight: "semibold", children: category.hasbroName }), _jsxs(Text, { size: "xs", color: "muted", children: ["The same wave, named differently outside Japan \u2014 Takara-Tomy releases it as ", category.name, ", Hasbro as ", category.hasbroName, ". Both names find these listings."] })] })] }) }) })), contentBody && contentBody.trim().length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(RichTextRenderer, { html: contentBody }) }) })), _jsx(CategoryHighlightsAndFaqSection, { highlights: category?.highlights, faqs: category?.faqs }), compositions.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: __W, padding: "section", children: _jsxs(Stack, { gap: "md", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "What it is made of" }), _jsx(ModelCompositionPanel, { compositions: compositions, hrefForPart: (slot, tail) => String(ROUTES.PUBLIC.PART_DETAIL(slot, tail)) })] }) }) })), (() => {
434
576
  if (category?.tier !== 0)
435
577
  return null;
436
578
  const branches = childCategories.filter((c) => c.tier === 1);
@@ -455,10 +597,5 @@ export async function CategoryDetailPageView({ slug }) {
455
597
  imageUrl: coverOf(b),
456
598
  countLabel: countOf(b),
457
599
  } }) }) }));
458
- })(), childCategories.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: __W, padding: "inline", children: (() => {
459
- const directChildren = childCategories.filter((c) => c.tier === (category?.tier ?? 0) + 1);
460
- if (directChildren.length === 0)
461
- return null;
462
- return (_jsxs(Div, { padding: "b-2xs", children: [_jsx(Text, { size: "xs", color: "muted", weight: "medium", className: "mb-[var(--appkit-space-1-5)]", children: "Subcategories" }), _jsx(OverflowRow, { items: toSubcategoryItems(directChildren), ariaLabel: "Subcategories", variant: "nav", placeholder: "Browse subcategories", searchPlaceholder: "Search subcategories\u2026", noResultsText: "No matching subcategory" })] }));
463
- })() }) })), _jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(CategoryDetailTabs, { categorySlug: slug, categoryId: category?.id, initialProductsData: productsResult ?? undefined, initialBundles: bundlesResult ?? [], initialStores: categoryStores, counts: counts, enabledListingTypes: enabledListingTypes(settings), enabledCategoryTypes: enabledCategoryTypes(settings) }) }) }), groupedListings.length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(GroupedListingsCarousel, { groups: groupedListings }) }) })), relatedCategories.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-t", padding: "y-lg", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 2, className: "mb-4", size: "xl", weight: "semibold", children: "Related Categories" }), _jsx(CategoryGrid, { categories: relatedCategories, getHref: (c) => String(ROUTES.PUBLIC.CATEGORY_DETAIL(c.slug)) })] }) }))] }));
600
+ })(), directChildrenWithListings.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: __W, padding: "inline", children: _jsxs(Div, { padding: "b-2xs", children: [parentLadder.length > 0 && (_jsx(Div, { padding: "b-2xs", children: _jsx(SubcategoryCardRow, { items: parentLadder, label: "Parent category" }) })), _jsx(SubcategoryCardRow, { items: toSubcategoryItems(directChildrenWithListings, childCategories) })] }) }) })), _jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(CategoryDetailTabs, { categorySlug: slug, categoryId: category?.id, initialProductsData: productsResult ?? undefined, initialBundles: bundlesResult ?? [], initialStores: categoryStores, counts: counts, totals: totals, enabledListingTypes: enabledListingTypes(settings), enabledCategoryTypes: enabledCategoryTypes(settings) }) }) }), groupedListings.length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(GroupedListingsCarousel, { groups: groupedListings }) }) })), relatedCategories.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-t", padding: "y-lg", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 2, className: "mb-4", size: "xl", weight: "semibold", children: "Related Categories" }), _jsx(CategoryGrid, { categories: relatedCategories, getHref: (c) => String(ROUTES.PUBLIC.CATEGORY_DETAIL(c.slug)) })] }) }))] }));
464
601
  }
@@ -17,9 +17,15 @@ export interface CategoryDetailTabsProps {
17
17
  * were structurally uncountable and could never hide (Root Cause #61).
18
18
  */
19
19
  counts?: Record<string, number | undefined>;
20
+ /**
21
+ * Every published row per tab, all availability scopes. Drives hide-at-zero
22
+ * and the "N ended — show all" hint; see `ListingTabCountResult`. Omitted,
23
+ * it falls back to `counts`, i.e. the pre-split behaviour.
24
+ */
25
+ totals?: Record<string, number | undefined>;
20
26
  /** Enabled listing types (e.g. ["standard","auction","pre-order"]). When omitted, all tabs shown. */
21
27
  enabledListingTypes?: string[];
22
28
  /** Enabled category types (e.g. ["category","brand","bundle"]). When omitted, all tabs shown. */
23
29
  enabledCategoryTypes?: string[];
24
30
  }
25
- export declare function CategoryDetailTabs({ categorySlug, categoryId, initialProductsData, initialBundles, initialStores, counts, enabledListingTypes, enabledCategoryTypes, }: CategoryDetailTabsProps): React.JSX.Element;
31
+ export declare function CategoryDetailTabs({ categorySlug, categoryId, initialProductsData, initialBundles, initialStores, counts, totals, enabledListingTypes, enabledCategoryTypes, }: CategoryDetailTabsProps): React.JSX.Element;
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
4
  import { EmptyState, Tabs, TabsList, TabsTrigger } from "../../../ui/index.js";
5
+ import { AvailabilityScopeHint } from "../../products/components/AvailabilityScopeHint.js";
5
6
  import { CategoryProductsListing } from "./CategoryProductsListing.js";
6
7
  import { AuctionsIndexListing } from "../../products/components/AuctionsIndexListing.js";
7
8
  import { PreOrdersIndexListing } from "../../pre-orders/components/PreOrdersIndexListing.js";
@@ -29,6 +30,21 @@ const TAB_TYPE_MAP = {
29
30
  bundles: { kind: "category", type: "bundle" },
30
31
  stores: { kind: "entity", type: "stores" },
31
32
  };
33
+ /**
34
+ * The listing types a tab spans, for the availability-scope hint.
35
+ *
36
+ * Derived from `TAB_TYPE_MAP` rather than hand-written: that map is already
37
+ * built from `ALL_LISTING_TYPES`, so a tenth type is covered without a second
38
+ * enumeration (Root Cause #61). The combined Art & Stickers tab is the one
39
+ * entry that spans two, and `GENERIC_TAB_LISTING_TYPES` already says so.
40
+ */
41
+ function typesForTab(id) {
42
+ const generic = GENERIC_TAB_LISTING_TYPES[id];
43
+ if (generic)
44
+ return generic;
45
+ const mapping = TAB_TYPE_MAP[id];
46
+ return mapping?.kind === "listing" ? [mapping.type] : [];
47
+ }
32
48
  /** Listing types rendered by the shared products listing rather than a dedicated one. */
33
49
  const GENERIC_TAB_LISTING_TYPES = {
34
50
  [pluginFor("standard").tabSlug]: ["standard"],
@@ -37,9 +53,25 @@ const GENERIC_TAB_LISTING_TYPES = {
37
53
  [pluginFor("live").tabSlug]: ["live"],
38
54
  // The combined Art & Stickers tab spans both types under the `art` slug.
39
55
  [pluginFor("art").tabSlug]: ART_STICKERS_LISTING_TYPES,
56
+ /*
57
+ * Spares render generically — there is nothing type-specific to show beyond
58
+ * a product grid, unlike an auction (countdown, bid history) or a prize draw
59
+ * (entries remaining).
60
+ *
61
+ * 🛑 This map is hand-written and a missing entry is a tab you can CLICK that
62
+ * renders an empty panel. It is covered — `audit-tab-body-coverage` exists
63
+ * for precisely this, after `CategoryDetailTabs` showed a blank panel for
64
+ * four types while `BrandDetailTabs` silently dropped the same four — so the
65
+ * eleventh type will be caught rather than discovered.
66
+ */
67
+ [pluginFor("spares").tabSlug]: ["spares"],
40
68
  };
41
- export function CategoryDetailTabs({ categorySlug, categoryId, initialProductsData, initialBundles = [], initialStores = [], counts, enabledListingTypes, enabledCategoryTypes, }) {
69
+ export function CategoryDetailTabs({ categorySlug, categoryId, initialProductsData, initialBundles = [], initialStores = [], counts, totals, enabledListingTypes, enabledCategoryTypes, }) {
42
70
  const countFor = (id) => counts?.[id];
71
+ // Falls back to the badge when no totals map is supplied, so a caller that
72
+ // has not been migrated keeps today's hide behaviour rather than hiding
73
+ // everything.
74
+ const totalFor = (id) => totals?.[id] ?? counts?.[id];
43
75
  const visibleTabs = CATEGORY_PAGE_TABS.filter((t) => {
44
76
  const mapping = TAB_TYPE_MAP[t.id];
45
77
  if (mapping) {
@@ -52,11 +84,13 @@ export function CategoryDetailTabs({ categorySlug, categoryId, initialProductsDa
52
84
  return false;
53
85
  }
54
86
  }
55
- // Hide a tab only when its count is known and explicitly zero. `undefined`
56
- // now means the count query FAILED (every tab is counted), so keeping it
57
- // visible is the fail-open choice: an empty tab is a far smaller error than
58
- // hiding a tab that holds real listings.
59
- const count = countFor(t.id);
87
+ // 🛑 TOTAL, not the badge. The badge counts what the default (available)
88
+ // scope renders, and a tab whose rows have all ended badges 0 while still
89
+ // holding a browsable archive behind the Ended/All scopes — hiding it
90
+ // would strand that archive. `undefined` still means the count query
91
+ // FAILED, so the tab stays visible: an empty tab is a far smaller error
92
+ // than hiding a tab that holds real listings (Root Cause #59).
93
+ const count = totalFor(t.id);
60
94
  return count === undefined || count > 0;
61
95
  });
62
96
  const firstTabId = (visibleTabs[0]?.id ?? "products");
@@ -66,5 +100,5 @@ export function CategoryDetailTabs({ categorySlug, categoryId, initialProductsDa
66
100
  if (visibleTabs.length === 0) {
67
101
  return (_jsx(EmptyState, { title: "Nothing listed here yet", description: "This category has no active listings right now. Check back soon." }));
68
102
  }
69
- return (_jsxs(_Fragment, { children: [_jsx(Tabs, { value: activeTab, onChange: (v) => setActiveTab(v), className: "mb-6", children: _jsx(TabsList, { children: visibleTabs.map((t) => (_jsx(TabsTrigger, { value: t.id, badge: countFor(t.id), children: t.label }, t.id))) }) }), GENERIC_TAB_LISTING_TYPES[activeTab] && (_jsx(CategoryProductsListing, { categorySlug: categorySlug, categoryId: categoryId, listingTypes: GENERIC_TAB_LISTING_TYPES[activeTab], initialData: activeTab === pluginFor("standard").tabSlug ? initialProductsData : undefined })), activeTab === pluginFor("auction").tabSlug && (_jsx(AuctionsIndexListing, { categorySlug: categorySlug })), activeTab === pluginFor("pre-order").tabSlug && (_jsx(PreOrdersIndexListing, { categorySlug: categorySlug })), activeTab === pluginFor("prize-draw").tabSlug && (_jsx(PrizeDrawsIndexListing, { categorySlug: categorySlug })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles })), activeTab === "stores" && (_jsx(CategoryStoresListing, { stores: initialStores }))] }));
103
+ return (_jsxs(_Fragment, { children: [_jsx(Tabs, { value: activeTab, onChange: (v) => setActiveTab(v), className: "mb-6", children: _jsx(TabsList, { children: visibleTabs.map((t) => (_jsx(TabsTrigger, { value: t.id, badge: countFor(t.id), children: t.label }, t.id))) }) }), _jsx(AvailabilityScopeHint, { total: totalFor(activeTab), available: countFor(activeTab), types: typesForTab(activeTab) }), GENERIC_TAB_LISTING_TYPES[activeTab] && (_jsx(CategoryProductsListing, { categorySlug: categorySlug, categoryId: categoryId, listingTypes: GENERIC_TAB_LISTING_TYPES[activeTab], initialData: activeTab === pluginFor("standard").tabSlug ? initialProductsData : undefined })), activeTab === pluginFor("auction").tabSlug && (_jsx(AuctionsIndexListing, { categorySlug: categorySlug })), activeTab === pluginFor("pre-order").tabSlug && (_jsx(PreOrdersIndexListing, { categorySlug: categorySlug })), activeTab === pluginFor("prize-draw").tabSlug && (_jsx(PrizeDrawsIndexListing, { categorySlug: categorySlug })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles })), activeTab === "stores" && (_jsx(CategoryStoresListing, { stores: initialStores }))] }));
70
104
  }
@@ -18,7 +18,16 @@ export function CategoryCard({ category, href, onClick, className = "", }) {
18
18
  // own-node-only productCount so a parent category's card shows the same
19
19
  // "N products" a visitor actually sees once they open the page.
20
20
  const productCount = category.metrics?.totalProductCount ?? category.metrics?.productCount ?? category.productCount ?? 0;
21
- const inner = (_jsxs(Stack, { className: "h-full", children: [_jsxs(Div, { surface: "muted", className: `relative aspect-[4/3] w-full ${__O.hidden} flex-shrink-0`, children: [(category.display?.mosaicImages?.length ?? 0) >= MOSAIC_MIN ? (_jsx(ImageMosaic, { images: category.display.mosaicImages, alt: category.name, className: "h-full w-full" })) : category.display?.coverImage ? (_jsx(MediaImage, { src: category.display.coverImage, alt: category.name, size: "card", className: "transition-transform duration-300 group-hover:scale-105" })) : category.display?.color ? (_jsx(DynamicBgDiv, { color: category.display.color, className: "h-full w-full opacity-80" })) : null, category.display?.icon && (_jsx(Row, { textSize: "4xl", className: "absolute inset-0", align: "center", justify: "center", children: category.display.icon })), category.isFeatured && (_jsx(Span, { size: "xs", className: CLS_FEATURED_DOT, children: "\u2605" }))] }), _jsxs(Stack, { className: `flex-1 ${__P.p3}.5`, children: [_jsx(Text, { color: "inverse", className: `leading-snug text-[var(--appkit-color-text)] dark:`, truncate: 2, size: "sm", weight: "semibold", children: category.name }), category.description && (_jsx(Text, { className: `mt-1 flex-1`, color: "muted", truncate: 2, size: "xs", children: category.description })), _jsxs(Row, { className: "mt-2", align: "center", justify: "between", gap: "sm", children: [_jsxs(Text, { size: "xs", color: "faint", children: [productCount.toLocaleString(), " ", productCount === 1 ? "item" : "items"] }), _jsxs(Span, { layout: "inline-flex", gap: "xs", size: "xs", weight: "medium", border: "default", className: "group-hover:bg-primary group-hover:border-primary group-hover:text-white transition-colors", rounded: "md", padding: "pill-sm-tall", color: "muted", children: ["Browse ", _jsx(ArrowRight, { className: "h-3 w-3" })] })] })] })] }));
21
+ /*
22
+ * Is a real picture going to render in the image area? Drives the emoji
23
+ * overlay below, which must not paint over artwork. Deliberately mirrors the
24
+ * first two branches of the chain rather than being derived from it — the
25
+ * `color` branch is a flat tint, which an emoji on top of is still the
26
+ * intended look.
27
+ */
28
+ const hasArtwork = (category.display?.mosaicImages?.length ?? 0) >= MOSAIC_MIN ||
29
+ Boolean(category.display?.coverImage);
30
+ const inner = (_jsxs(Stack, { className: "h-full", children: [_jsxs(Div, { surface: "muted", className: `relative aspect-[4/3] w-full ${__O.hidden} flex-shrink-0`, children: [(category.display?.mosaicImages?.length ?? 0) >= MOSAIC_MIN ? (_jsx(ImageMosaic, { images: category.display.mosaicImages, alt: category.name, className: "h-full w-full" })) : category.display?.coverImage ? (_jsx(MediaImage, { src: category.display.coverImage, alt: category.name, size: "card", className: "transition-transform duration-300 group-hover:scale-105" })) : category.display?.color ? (_jsx(DynamicBgDiv, { color: category.display.color, className: "h-full w-full opacity-80" })) : null, category.display?.icon && !hasArtwork && (_jsx(Row, { textSize: "4xl", className: "absolute inset-0", align: "center", justify: "center", children: category.display.icon })), category.isFeatured && (_jsx(Span, { size: "xs", className: CLS_FEATURED_DOT, children: "\u2605" }))] }), _jsxs(Stack, { className: `flex-1 ${__P.p3}.5`, children: [_jsx(Text, { color: "inverse", className: `leading-snug text-[var(--appkit-color-text)] dark:`, truncate: 2, size: "sm", weight: "semibold", children: category.name }), category.description && (_jsx(Text, { className: `mt-1 flex-1`, color: "muted", truncate: 2, size: "xs", children: category.description })), _jsxs(Row, { className: "mt-2", align: "center", justify: "between", gap: "sm", children: [_jsxs(Text, { size: "xs", color: "faint", children: [productCount.toLocaleString(), " ", productCount === 1 ? "item" : "items"] }), _jsxs(Span, { layout: "inline-flex", gap: "xs", size: "xs", weight: "medium", border: "default", className: "group-hover:bg-primary group-hover:border-primary group-hover:text-white transition-colors", rounded: "md", padding: "pill-sm-tall", color: "muted", children: ["Browse ", _jsx(ArrowRight, { className: "h-3 w-3" })] })] })] })] }));
22
31
  const cardClass = `group relative flex flex-col overflow-hidden rounded-xl border border-neutral-200 bg-[var(--appkit-color-surface)] border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] shadow-sm transition hover:shadow-md h-full ${className}`;
23
32
  if (href) {
24
33
  return (_jsx(Link, { href: href, className: cardClass, children: inner }));
@@ -0,0 +1,29 @@
1
+ import React from "react";
2
+ export interface PriceGuidanceHintProps {
3
+ /** Leaf category id/slug the listing is filed under. */
4
+ categoryId: string;
5
+ currency?: string;
6
+ className?: string;
7
+ }
8
+ /**
9
+ * The seller-facing price hint `description-template/route.ts`'s own
10
+ * docstring names as "C1's job, from a surface that knows who is asking" —
11
+ * renders the inherited competitor-derived market band beside the price
12
+ * field, on the SELLER'S own tax basis (the `/price-guidance` endpoint does
13
+ * that conversion server-side; this component only formats what it returns).
14
+ *
15
+ * Mounted via `ProductForm`'s `renderPriceGuidance` slot rather than fetched
16
+ * unconditionally inside the form — `ProductForm` has no data-fetching of its
17
+ * own (every app-specific concern is a render-prop), and this component is
18
+ * seller/admin-only, not something the form itself should assume a caller
19
+ * wants.
20
+ *
21
+ * 🛑 NEVER mount this on a public page (category/brand detail). A
22
+ * competitor-derived market band is exactly the "a neighbouring surface
23
+ * publishes the range" leak the guest price gate exists to close (CLAUDE.md
24
+ * § "Guest price gating"), even though the figure is advisory rather than a
25
+ * real listing price — which is also why `/price-guidance` itself is
26
+ * seller/admin/moderator-gated, not a public, edge-cached endpoint like its
27
+ * `description-template` sibling.
28
+ */
29
+ export declare function PriceGuidanceHint({ categoryId, currency, className, }: PriceGuidanceHintProps): React.JSX.Element | null;
@@ -0,0 +1,61 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { PricesOnly, Stack, Text } from "../../../ui/index.js";
5
+ import { apiClient } from "../../../http/index.js";
6
+ import { CATEGORY_ENDPOINTS } from "../../../constants/api-endpoints.js";
7
+ import { formatCurrency } from "../../../utils/number.formatter.js";
8
+ function unwrap(res) {
9
+ const body = res?.data ?? res;
10
+ return body && typeof body === "object" && "guidance" in body ? body : null;
11
+ }
12
+ /**
13
+ * The seller-facing price hint `description-template/route.ts`'s own
14
+ * docstring names as "C1's job, from a surface that knows who is asking" —
15
+ * renders the inherited competitor-derived market band beside the price
16
+ * field, on the SELLER'S own tax basis (the `/price-guidance` endpoint does
17
+ * that conversion server-side; this component only formats what it returns).
18
+ *
19
+ * Mounted via `ProductForm`'s `renderPriceGuidance` slot rather than fetched
20
+ * unconditionally inside the form — `ProductForm` has no data-fetching of its
21
+ * own (every app-specific concern is a render-prop), and this component is
22
+ * seller/admin-only, not something the form itself should assume a caller
23
+ * wants.
24
+ *
25
+ * 🛑 NEVER mount this on a public page (category/brand detail). A
26
+ * competitor-derived market band is exactly the "a neighbouring surface
27
+ * publishes the range" leak the guest price gate exists to close (CLAUDE.md
28
+ * § "Guest price gating"), even though the figure is advisory rather than a
29
+ * real listing price — which is also why `/price-guidance` itself is
30
+ * seller/admin/moderator-gated, not a public, edge-cached endpoint like its
31
+ * `description-template` sibling.
32
+ */
33
+ export function PriceGuidanceHint({ categoryId, currency = "INR", className = "", }) {
34
+ const query = useQuery({
35
+ queryKey: ["category-price-guidance", categoryId],
36
+ enabled: categoryId.length > 0,
37
+ staleTime: 60 * 60 * 1000,
38
+ queryFn: async () => unwrap(await apiClient.get(CATEGORY_ENDPOINTS.PRICE_GUIDANCE(categoryId))),
39
+ });
40
+ const guidance = query.data?.guidance;
41
+ const basisNote = query.data?.basisNote;
42
+ if (!guidance?.market)
43
+ return null;
44
+ const { median, p25, p75 } = guidance.market;
45
+ const fmt = (n) => formatCurrency(n, currency);
46
+ // p25/p75 are independently optional (see CategoryPriceGuidance's own
47
+ // docstring) — a range renders only when BOTH are present, never a
48
+ // half-range that would misreport the band's shape.
49
+ const range = p25 !== undefined && p75 !== undefined ? ` (${fmt(p25)}–${fmt(p75)})` : "";
50
+ /*
51
+ * `<PricesOnly>` rather than `<GatedPrice>`: this is advisory guidance
52
+ * BESIDE the seller's own price input, not a listing's primary price, so a
53
+ * viewer who may not see prices should get nothing here — a "Sign in to see
54
+ * price" prompt inside a seller form would be nonsense. In practice it
55
+ * never renders for a guest anyway, because the data source
56
+ * (`/api/categories/[id]/price-guidance`) is seller/admin/moderator-gated
57
+ * and 403s before there is a number to format; the wrapper makes that
58
+ * guarantee visible at the render site instead of implied by the route.
59
+ */
60
+ return (_jsx(PricesOnly, { children: _jsxs(Stack, { gap: "xs", className: className, children: [_jsxs(Text, { size: "xs", color: "muted", children: ["Market median:", " ", _jsx(Text, { as: "span", weight: "medium", color: "primary", children: fmt(median) }), range, basisNote?.suffix ? ` ${basisNote.suffix}` : ""] }), basisNote?.note && _jsx(Text, { size: "xs", color: "faint", children: basisNote.note })] }) }));
61
+ }
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+ export interface SubcategoryCardItem {
3
+ id: string;
4
+ name: string;
5
+ href: string;
6
+ /** Rollup listing count. Omitted when zero — a card reading "0" invites a dead click. */
7
+ count?: number;
8
+ /** A real picture, when the row resolved one. */
9
+ imageUrl?: string;
10
+ /** An emoji, when that is all the row has. Never passed to an `<img>`. */
11
+ emoji?: string;
12
+ /** Direct children's names — searched, never rendered. */
13
+ childNames?: readonly string[];
14
+ /**
15
+ * Depth in the forest: 0 is a root, 1 its child, and so on.
16
+ *
17
+ * Rendered as a small "L0 / L1 / L2" marker so a row of ancestors reads as a
18
+ * LADDER rather than as five unrelated cards. Omitted on the subcategory row
19
+ * itself, where every card is the same depth by construction and the marker
20
+ * would be noise repeated N times.
21
+ */
22
+ level?: number;
23
+ }
24
+ export declare function SubcategoryCardRow({ items, label, }: {
25
+ items: readonly SubcategoryCardItem[];
26
+ label?: string;
27
+ }): React.JSX.Element | null;