@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
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useCallback, useState } from "react";
3
+ import React, { useCallback, useState } from "react";
4
4
  import { useQuery } from "@tanstack/react-query";
5
5
  import { Search, TrendingUp } from "lucide-react";
6
6
  import { Container, Div, Heading, Row, Section, Span, Stack, Text } from "../../../ui/index.js";
@@ -8,12 +8,20 @@ import { Badge } from "../../../ui/components/Badge.js";
8
8
  import { Button } from "../../../ui/components/Button.js";
9
9
  import { Form } from "../../../ui/components/Form.js";
10
10
  import { FieldInput } from "../../../ui/forms/FieldInput.js";
11
+ import { PaginatedSelect } from "../../../ui/components/PaginatedSelect.js";
12
+ import { TextLink } from "../../../ui/components/TextLink.js";
11
13
  import { MediaImage } from "../../media/MediaImage.js";
12
14
  import { TOOL_ENDPOINTS } from "../../../constants/api-endpoints.js";
13
15
  import { apiClient } from "../../../http/ApiClient.js";
16
+ import { useSiteSearch } from "../../search/hooks/useSiteSearch.js";
17
+ import { useSearchIndex } from "../../search/hooks/useSearchIndex.js";
18
+ import { searchIndex } from "../../../_internal/shared/search/match.js";
19
+ import { useOptionalSession } from "../../../react/contexts/SessionContext.js";
20
+ import { ROUTES } from "../../../next/routing/route-map.js";
14
21
  import { formatCurrency } from "../../../utils/number.formatter.js";
15
22
  import { normalizeError } from "../../../errors/normalize.js";
16
23
  import { toUserMessage } from "../../../errors/error-display-map.js";
24
+ import { PRICE_LOOKUP_INDEX_TYPES, priceLookupTargetFor } from "../lookup-target.js";
17
25
  /** Human labels. Falls back to the raw value so a new condition still renders. */
18
26
  const CONDITION_LABEL = {
19
27
  new: "New",
@@ -46,6 +54,14 @@ const __O = {
46
54
  xAuto: "overflow-x-auto",
47
55
  hidden: "overflow-hidden",
48
56
  };
57
+ /**
58
+ * How many options the dropdown offers.
59
+ *
60
+ * Larger than `PRICE_INDEX_SEARCH_LIMIT`'s 8 because this is a PICK list rather
61
+ * than a result set — the cost of an extra row is a line of text, not a
62
+ * Firestore document, so the bound that matters is what a person will read.
63
+ */
64
+ const DROPDOWN_LIMIT = 20;
49
65
  const money = (v) => formatCurrency(v, "INR");
50
66
  function relativeDay(iso) {
51
67
  if (!iso)
@@ -78,16 +94,72 @@ function BandCard({ band, threshold }) {
78
94
  ? ` · via ${band.lastSold.via}`
79
95
  : ""] }))] })) : (_jsx(LowConfidence, { sampleSize: band.sampleSize, threshold: threshold })), band.byCondition && band.byCondition.length > 0 && (_jsx(ConditionTable, { rows: band.byCondition, threshold: threshold }))] }));
80
96
  }
81
- function ResultCard({ row, threshold }) {
82
- return (_jsxs(Stack, { gap: "md", padding: "card", rounded: "lg", surface: "card", border: "subtle", className: "min-w-0", children: [_jsxs(Row, { gap: "md", align: "start", className: "min-w-0", children: [row.coverImage && (_jsx(Div, { rounded: "lg", className: `w-16 h-16 flex-shrink-0 ${__O.hidden}`, children: _jsx(MediaImage, { src: row.coverImage, alt: row.name, size: "thumbnail" }) })), _jsxs(Stack, { gap: "none", className: "flex-1 min-w-0", children: [_jsx(Heading, { level: 3, children: row.name }), row.path && (_jsx(Text, { size: "xs", color: "muted", className: "min-w-0 break-words", children: row.path }))] })] }), row.bands && row.bands.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Stack, { gap: "sm", className: "min-w-0", children: row.bands.map((b) => (_jsx(BandCard, { band: b, threshold: threshold }, b.listingType))) }), _jsxs(Text, { size: "xs", color: "muted", children: ["Completed sales on LetItRip", row.windowDays ? ` over the last ${row.windowDays} days` : "", row.computedAt ? ` · as of ${new Date(row.computedAt).toLocaleDateString("en-IN")}` : ""] })] })) : (
97
+ /**
98
+ * The competitor-crawl band: what this thing is typically ASKED for.
99
+ *
100
+ * ## 🛑 Side by side with the sold band, never averaged with it
101
+ *
102
+ * `price-bands.md` states the rule this component exists to honour: "They are
103
+ * rendered side by side and never averaged. The gap between them is the most
104
+ * useful thing we can tell a seller." An ask and a sale are different events,
105
+ * and a blended figure would be a statistic describing neither.
106
+ *
107
+ * ## 🛑 "Sold out" is not "sold"
108
+ *
109
+ * 76–93% of the crawled catalogues are sold out, and a sold-out row proves the
110
+ * item is GONE, not that it changed hands at that price. So `soldOutShare` is
111
+ * rendered as prose rather than dropped — without it, a reader takes the median
112
+ * for a clearing price, which is the single wrong conclusion this data invites.
113
+ *
114
+ * ## 🛑 An inherited band says whose band it is
115
+ *
116
+ * `MARKET_BANDS` is keyed on era and part-group leaves, never on the ~216
117
+ * named-model leaves — the crawl measured a generation, not a bey. So a model
118
+ * lookup resolves its era's band, and the card must attribute it: "typical for
119
+ * Burst DB" is true, "typical for Rage Longinus" is not.
120
+ */
121
+ function MarketCard({ market }) {
122
+ const hasRange = market.p25 !== undefined && market.p75 !== undefined;
123
+ const soldOutPct = market.soldOutShare === undefined ? null : Math.round(market.soldOutShare * 100);
124
+ return (_jsxs(Stack, { gap: "sm", padding: "sm", rounded: "lg", border: "default", className: "min-w-0", children: [_jsxs(Row, { align: "center", justify: "between", gap: "sm", wrap: true, className: "min-w-0", children: [_jsx(Badge, { variant: "secondary", children: "Typical asking price" }), market.sampleSize !== undefined && (_jsxs(Text, { size: "xs", color: "muted", children: [market.sampleSize, " ", market.sampleSize === 1 ? "listing" : "listings"] }))] }), _jsxs(Row, { gap: "lg", wrap: true, className: "min-w-0", children: [hasRange && _jsx(Figure, { label: "Usual low", value: money(market.p25) }), _jsx(Figure, { label: "Typical ask", value: money(market.median) }), hasRange && _jsx(Figure, { label: "Usual high", value: money(market.p75) })] }), market.inherited && (_jsxs(Text, { size: "sm", color: "muted", children: ["Measured for", " ", _jsx(Span, { weight: "semibold", children: market.viaName ?? market.viaCategoryId }), " as a whole \u2014 the crawl priced the generation, not this exact item."] })), soldOutPct !== null && (_jsxs(Text, { size: "sm", color: "muted", children: ["These are asking prices from other shops, and about ", soldOutPct, "% of listings at this level are sold out \u2014 so an ask is not proof anything changed hands at it."] })), _jsxs(Text, { size: "xs", color: "muted", className: "min-w-0 break-words", children: [market.source ? `Source: ${market.source}. ` : "", "Tax-inclusive", market.asOf
125
+ ? ` · measured ${new Date(market.asOf).toLocaleDateString("en-IN")}`
126
+ : ""] })] }));
127
+ }
128
+ /**
129
+ * What a signed-out visitor sees where the market band would be.
130
+ *
131
+ * 🛑 This is NOT the guest price gate. The sold band above it is public by
132
+ * decision (see this file's header). The market band is withheld for a
133
+ * different reason, already settled at
134
+ * `categories/api/[id]/price-guidance/route.ts`: it is competitor-derived, and
135
+ * publishing it on an unauthenticated, cacheable surface is the "neighbouring
136
+ * surface publishes the range" leak. The copy therefore says what is behind
137
+ * sign-in and why it is worth having, rather than implying no data exists.
138
+ */
139
+ function MarketSignInPrompt() {
140
+ return (_jsxs(Stack, { gap: "xs", padding: "sm", rounded: "lg", surface: "muted", className: "min-w-0", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Asking prices elsewhere" }), _jsxs(Text, { size: "sm", color: "muted", children: ["We measured what other Indian shops ask for this, from a crawl of fifteen catalogues.", " ", _jsx(TextLink, { href: String(ROUTES.AUTH.LOGIN), children: "Sign in" }), " to see the range."] })] }));
141
+ }
142
+ function ResultCard({ row, threshold, market, canSeeMarket, }) {
143
+ return (_jsxs(Stack, { gap: "md", padding: "card", rounded: "lg", surface: "card", border: "subtle", className: "min-w-0", children: [_jsxs(Row, { gap: "md", align: "start", className: "min-w-0", children: [row.coverImage && (_jsx(Div, { rounded: "lg", className: `w-16 h-16 flex-shrink-0 ${__O.hidden}`, children: _jsx(MediaImage, { src: row.coverImage, alt: row.name, size: "thumbnail" }) })), _jsxs(Stack, { gap: "none", className: "flex-1 min-w-0", children: [_jsx(Heading, { level: 3, children: row.name }), row.path && (_jsx(Text, { size: "xs", color: "muted", className: "min-w-0 break-words", children: row.path }))] })] }), (!row.bands || row.bands.length === 0) &&
144
+ (market ? _jsx(MarketCard, { market: market }) : canSeeMarket ? null : _jsx(MarketSignInPrompt, {})), row.bands && row.bands.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Stack, { gap: "sm", className: "min-w-0", children: row.bands.map((b) => (_jsx(BandCard, { band: b, threshold: threshold }, b.listingType))) }), market ? _jsx(MarketCard, { market: market }) : canSeeMarket ? null : _jsx(MarketSignInPrompt, {}), _jsxs(Text, { size: "xs", color: "muted", children: ["Completed sales on LetItRip", row.windowDays ? ` over the last ${row.windowDays} days` : "", row.computedAt ? ` · as of ${new Date(row.computedAt).toLocaleDateString("en-IN")}` : ""] })] })) : (
83
145
  /*
84
146
  * 🛑 "No sales recorded yet" — never ₹0, and never a blank card. The
85
147
  * absence of a band means nothing has sold here, which is a fact about
86
148
  * our marketplace and not a failure of the lookup.
149
+ *
150
+ * 🛑 The wording is DIFFERENT when a market band is showing above it.
151
+ * "Prices appear once something sells" was written for a card that had
152
+ * nothing else on it; printed under a real asking-price band it reads
153
+ * as though the band were not a price, which is the opposite of what
154
+ * this card now says.
87
155
  */
88
- _jsx(Text, { size: "sm", color: "muted", children: "No completed sales recorded here yet. Prices appear once something sells on LetItRip." }))] }));
156
+ _jsx(Text, { size: "sm", color: "muted", children: market
157
+ ? "Nothing has sold here on LetItRip yet, so the figures above are asking prices elsewhere rather than our own sales."
158
+ : "No completed sales recorded here yet. Prices appear once something sells on LetItRip." }))] }));
89
159
  }
90
- export function PriceIndexToolView({ initialQuery = "", initialData = null }) {
160
+ /** Stable identity — a `useMemo` dependency inside `useSiteSearch`. */
161
+ const identityLabel = (typeKey) => typeKey;
162
+ export function PriceIndexToolView({ initialQuery = "", initialData = null, searchIndexEnabled, }) {
91
163
  const [draft, setDraft] = useState(initialQuery);
92
164
  /**
93
165
  * The SUBMITTED term, distinct from `draft`.
@@ -97,29 +169,230 @@ export function PriceIndexToolView({ initialQuery = "", initialData = null }) {
97
169
  * keystroke against a route that holds a Firestore query plus a multi-get —
98
170
  * which is Root Cause #94's recurring server bill, and `audit-client-poll-cost`
99
171
  * exists because of it. The query key only moves when somebody submits.
172
+ *
173
+ * 🛑 Still here after the dropdown landed, because it drives the FALLBACK
174
+ * path: the dropdown's options come from the local search artifact, so when
175
+ * that artifact is disabled or fails to load there is nothing to pick from
176
+ * and the tool must still work. Deleting this would have made a kill-switch
177
+ * flip turn the whole page into an empty combobox.
100
178
  */
101
179
  const [term, setTerm] = useState(initialQuery.trim());
180
+ /**
181
+ * What the dropdown picked: the category id to price, plus its label.
182
+ *
183
+ * 🛑 The LABEL is kept alongside the id, not looked up again. The option list
184
+ * is rebuilt per keystroke inside `loadOptions`, so by the time results
185
+ * arrive the option that produced the selection may no longer be in the list
186
+ * — and `PaginatedSelect` renders the trigger from the value it was given.
187
+ * Without this the chosen item's name would vanish from the control the
188
+ * moment the query that found it changed.
189
+ */
190
+ const [picked, setPicked] = useState(null);
191
+ /** Signed in? Decides whether the market half is requested at all. */
192
+ const session = useOptionalSession();
193
+ const canSeeMarket = Boolean(session?.user?.uid);
102
194
  /*
103
- * `useQuery` + `apiClient`, not a hand-rolled `fetch` with its own loading
104
- * and error state — which is what this had, and what `audit-form-mutation-hook`
105
- * correctly rejected. It also deletes three `useState`s: React Query already
106
- * owns in-flight, error and the response, and it dedupes the identical lookup
107
- * rather than issuing it twice.
195
+ * The artifact, for the dropdown. `active` is true as soon as the control is
196
+ * on the page: unlike the old submit-driven flow there is no "intent"
197
+ * threshold to wait for — a combobox the user has just clicked must have
198
+ * options, and the artifact is a one-time ~420 KB load with a module-scope
199
+ * singleton behind it, not a per-keystroke cost.
108
200
  */
109
- const query = useQuery({
201
+ const dropdownIndex = useSearchIndex({
202
+ active: true,
203
+ indexEnabled: searchIndexEnabled,
204
+ });
205
+ /**
206
+ * Options for the dropdown, resolved LOCALLY on every keystroke.
207
+ *
208
+ * 🛑 `async` only to satisfy `loadOptions`' signature — nothing here awaits.
209
+ * `searchIndex` is a linear scan over ~3,700 prepared records, measured at
210
+ * 0.76 ms by `verify-search-local.mjs`, so there is no network round trip to
211
+ * debounce and no Firestore read to bill. This is the whole reason the
212
+ * dropdown can be a dropdown.
213
+ *
214
+ * 🛑 Hits that cannot be priced are DROPPED, not shown disabled. An option
215
+ * that selects to a card reading "no data" is worse than an absent one — it
216
+ * looks like the tool failed rather than like the item is not priceable. See
217
+ * `priceLookupTargetFor` for which types resolve and why.
218
+ */
219
+ const loadOptions = useCallback(async (query, page) => {
220
+ const idx = dropdownIndex.index;
221
+ if (!idx || !query.trim())
222
+ return { items: [], hasMore: false };
223
+ const hits = searchIndex(idx.artifact, idx.prepared, query, {
224
+ types: PRICE_LOOKUP_INDEX_TYPES,
225
+ limit: DROPDOWN_LIMIT,
226
+ });
227
+ const seen = new Set();
228
+ const items = [];
229
+ for (const hit of hits) {
230
+ const target = priceLookupTargetFor(hit.href);
231
+ if (!target || seen.has(target))
232
+ continue;
233
+ seen.add(target);
234
+ items.push({
235
+ value: target,
236
+ /*
237
+ * The type and the detail both earn their place: `detail` is the
238
+ * taxonomy path for a category and the classification for a part,
239
+ * which is how a reader tells three results named "105" apart.
240
+ */
241
+ label: `${hit.title}${hit.detail ? ` — ${hit.detail}` : ""}`,
242
+ });
243
+ }
244
+ // One page: the matcher already ranked and capped. Paging a ranked list
245
+ // would invite scrolling past the answer rather than refining the query.
246
+ return { items, hasMore: false, nextPage: page };
247
+ }, [dropdownIndex.index]);
248
+ /**
249
+ * 🛑 The dropdown is only usable with an artifact behind it. On `disabled` or
250
+ * `failed` the page falls back to the original text box, which hits the
251
+ * server-side `searchTxt` path. Reported as a state rather than silently
252
+ * degrading, because an empty combobox is indistinguishable from an empty
253
+ * catalogue.
254
+ */
255
+ const dropdownUsable = dropdownIndex.status === "ready" || dropdownIndex.status === "loading" ||
256
+ dropdownIndex.status === "idle";
257
+ /*
258
+ * 🛑 TIER 1 ADOPTION, kept for the FALLBACK path only.
259
+ *
260
+ * `searchPriceIndex` pays a `searchTxt` push-down query to turn a free-text
261
+ * term into category leaves — the resolution the local artifact does for
262
+ * free. When the dropdown is driving, no term is ever submitted and this
263
+ * stays idle; it matters when somebody lands on a shared `?q=` link.
264
+ *
265
+ * 🛑 Its `types` now come from `PRICE_LOOKUP_INDEX_TYPES`, not a hand-written
266
+ * `["model", "category"]`. That literal is why spares were unreachable: the
267
+ * index has had a `part` type the whole time and this call never asked for
268
+ * it, so no amount of typing a part name could surface one.
269
+ */
270
+ const local = useSiteSearch(term, {
271
+ active: term.length >= 2,
272
+ indexEnabled: searchIndexEnabled,
273
+ types: PRICE_LOOKUP_INDEX_TYPES,
274
+ label: identityLabel,
275
+ });
276
+ const localIds = React.useMemo(() => {
277
+ if (!local.result)
278
+ return [];
279
+ const ids = new Set();
280
+ for (const group of local.result.groups) {
281
+ for (const hit of group.hits) {
282
+ /*
283
+ * 🛑 `priceLookupTargetFor`, not a bare `/categories/` regex. The old
284
+ * `categoryIdFromHref` matched that one shape and returned `null` for
285
+ * everything else, so a part hit was discarded here even on the rare
286
+ * occasion one arrived.
287
+ */
288
+ const id = priceLookupTargetFor(hit.href);
289
+ if (id)
290
+ ids.add(id);
291
+ }
292
+ }
293
+ return [...ids];
294
+ }, [local.result]);
295
+ const localReady = local.status === "ready";
296
+ /**
297
+ * The ids actually being looked up.
298
+ *
299
+ * A dropdown pick wins outright: it is an explicit choice of ONE thing, and
300
+ * mixing it with the term-resolved set would answer a question the user
301
+ * stopped asking the moment they picked.
302
+ */
303
+ const lookupIds = React.useMemo(() => (picked ? [picked.id] : localReady ? localIds : []), [picked, localReady, localIds]);
304
+ /*
305
+ * The bands-only request. Enabled ONLY once local resolution has actually
306
+ * produced something — an empty `localIds` with the gate open would be a
307
+ * request shaped `?ids=`, which the route already treats as "no results"
308
+ * but which is still a request worth not making.
309
+ */
310
+ const bandsQuery = useQuery({
311
+ queryKey: ["tool", "price-index", "bands", lookupIds],
312
+ queryFn: () => apiClient.get(TOOL_ENDPOINTS.PRICE_INDEX_BANDS(lookupIds)),
313
+ enabled: lookupIds.length > 0,
314
+ staleTime: Infinity,
315
+ refetchOnWindowFocus: false,
316
+ });
317
+ /**
318
+ * The MARKET half — a separate request, on purpose.
319
+ *
320
+ * 🛑 `enabled: canSeeMarket && …`. A guest never issues it, so the gate costs
321
+ * a signed-out visitor nothing and cannot 401-spam the console. The server
322
+ * enforces it too (`auth: true`); this is the client half of one decision,
323
+ * not the decision itself.
324
+ *
325
+ * 🛑 A separate request rather than two more fields on `bandsQuery`, because
326
+ * that route is PUBLIC and edge-cached: folding viewer-dependent content into
327
+ * it would let one signed-in lookup populate the entry every guest then
328
+ * reads. The full argument is in `price-index/market.ts`.
329
+ *
330
+ * 🛑 Its failure is deliberately NOT surfaced as the page's error. The sold
331
+ * band is the page; the ask is context. A 401 or a cold-start blip on the
332
+ * context must not replace a working result with "could not look that up".
333
+ */
334
+ const marketQuery = useQuery({
335
+ queryKey: ["tool", "price-index", "market", lookupIds],
336
+ queryFn: () => apiClient.get(TOOL_ENDPOINTS.PRICE_INDEX_MARKET(lookupIds)),
337
+ enabled: canSeeMarket && lookupIds.length > 0,
338
+ staleTime: Infinity,
339
+ refetchOnWindowFocus: false,
340
+ });
341
+ /** `categoryId` -> its market band, for the per-card join. */
342
+ const marketById = React.useMemo(() => {
343
+ const m = new Map();
344
+ for (const row of marketQuery.data?.results ?? [])
345
+ m.set(row.categoryId, row);
346
+ return m;
347
+ }, [marketQuery.data]);
348
+ /*
349
+ * The FALLBACK. Fires only when the local index is off, failed, or
350
+ * (rarely) resolved to nothing — never alongside the local path, so a
351
+ * working index never pays for both. Same cache posture as it always had.
352
+ */
353
+ const fallbackEnabled = !picked && term.length >= 2 &&
354
+ (local.status === "failed" || local.status === "disabled" ||
355
+ (localReady && localIds.length === 0));
356
+ const fallbackQuery = useQuery({
110
357
  queryKey: ["tool", "price-index", term],
111
358
  queryFn: () => apiClient.get(TOOL_ENDPOINTS.PRICE_INDEX_SEARCH(term)),
112
- enabled: term.length >= 2,
113
- /*
114
- * The endpoint is edge-cached for an hour over a day-stale aggregate, so
115
- * refetching on focus or remount would spend invocations to learn nothing.
116
- */
359
+ enabled: fallbackEnabled,
117
360
  staleTime: Infinity,
118
361
  refetchOnWindowFocus: false,
119
362
  ...(initialData && initialData.query === initialQuery.trim()
120
363
  ? { initialData }
121
364
  : {}),
122
365
  });
366
+ /*
367
+ * One shape out, regardless of which path answered. `truncated` is always
368
+ * `false` on the local path — `PRICE_INDEX_SEARCH_LIMIT` already bounded
369
+ * how many ids were resolved, so there is nothing left uncounted the way
370
+ * the query-based path's search step could leave behind.
371
+ */
372
+ const query = lookupIds.length > 0
373
+ ? {
374
+ data: bandsQuery.data
375
+ ? {
376
+ query: picked ? picked.label : term,
377
+ results: bandsQuery.data.results,
378
+ truncated: false,
379
+ /*
380
+ * The confidence threshold is a server-side constant
381
+ * (`PRICE_INDEX_MIN_CONFIDENT_SAMPLES = 3`), not something
382
+ * resolved per-request — the bands endpoint does not echo it
383
+ * back because it is already baked into each band's own
384
+ * `confident` flag. Hardcoded to match the renderer's own
385
+ * existing fallback below (`data?.minConfidentSamples ?? 3`),
386
+ * so the two paths show the same explanatory copy.
387
+ */
388
+ minConfidentSamples: 3,
389
+ }
390
+ : undefined,
391
+ isFetching: bandsQuery.isFetching,
392
+ isError: bandsQuery.isError,
393
+ error: bandsQuery.error,
394
+ }
395
+ : fallbackQuery;
123
396
  const run = useCallback((q) => setTerm(q.trim()), []);
124
397
  const data = query.data ?? null;
125
398
  const loading = query.isFetching;
@@ -136,5 +409,14 @@ export function PriceIndexToolView({ initialQuery = "", initialData = null }) {
136
409
  : null;
137
410
  const threshold = data?.minConfidentSamples ?? 3;
138
411
  const hasQuery = (data?.query ?? "").length > 0;
139
- return (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "lg", children: _jsxs(Stack, { gap: "lg", className: "min-w-0", children: [_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsxs(Row, { gap: "sm", align: "center", children: [_jsx(TrendingUp, { className: "w-5 h-5 text-primary-600", "aria-hidden": "true" }), _jsx(Heading, { level: 1, children: "What it sold for" })] }), _jsx(Text, { color: "muted", children: "Search any model, part or set to see what it actually sold for on LetItRip \u2014 the range, the median, the average and the most recent sale, broken down by condition." })] }), _jsx(Form, { onSubmit: (e) => e.preventDefault(), children: () => (_jsxs(Row, { gap: "sm", align: "end", wrap: true, className: "min-w-0", children: [_jsx(Div, { className: "flex-1 min-w-[16rem]", children: _jsx(FieldInput, { name: "q", label: "Search", value: draft, onChange: setDraft, placeholder: "e.g. Dragoon G, Storm Pegasus, rubber flat\u2026" }) }), _jsxs(Button, { type: "submit", isLoading: loading, onClick: () => run(draft), children: [_jsx(Search, { className: "w-4 h-4", "aria-hidden": "true" }), "Look up"] })] })) }), error && (_jsx(Text, { size: "sm", color: "error", role: "alert", children: error })), !hasQuery && !loading && !error && (_jsxs(Stack, { gap: "xs", padding: "card", rounded: "lg", surface: "muted", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", children: "Why these prices are different" }), _jsxs(Text, { size: "sm", color: "muted", children: ["Everything here is a completed sale on LetItRip \u2014 not an asking price, and not a guess. We publish a figure only once", " ", threshold, " or more sales support it, and the index is recomputed once a day, so it is deliberately a day behind."] })] })), hasQuery && data && data.results.length === 0 && !loading && (_jsxs(Text, { color: "muted", children: ["Nothing matched \u201C", data.query, "\u201D. Try a shorter term, or the model name on its own."] })), data && data.results.length > 0 && (_jsxs(Stack, { gap: "md", className: "min-w-0", children: [data.results.map((row) => (_jsx(ResultCard, { row: row, threshold: threshold }, row.categoryId))), data.truncated && (_jsxs(Text, { size: "sm", color: "muted", children: ["Showing the closest ", data.results.length, " matches. Narrow the search to see others."] }))] }))] }) }) }));
412
+ return (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "lg", children: _jsxs(Stack, { gap: "lg", className: "min-w-0", children: [_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsxs(Row, { gap: "sm", align: "center", children: [_jsx(TrendingUp, { className: "w-5 h-5 text-primary-600", "aria-hidden": "true" }), _jsx(Heading, { level: 1, children: "What it sold for" })] }), _jsx(Text, { color: "muted", children: "Pick any bey, part or set to see what it actually sold for on LetItRip \u2014 the range, the median, the average and the most recent sale, broken down by condition \u2014 alongside what other Indian shops are asking for the same thing." })] }), dropdownUsable ? (_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsx(PaginatedSelect, { value: picked?.id ?? null, onChange: (value, option) => setPicked(value && option ? { id: value, label: option.label } : null), loadOptions: loadOptions, placeholder: "Pick a bey, part or set\u2026", searchPlaceholder: "Type a name, code or part \u2014 e.g. Dragoon G, 1B, rubber flat", noResultsText: "Nothing in our catalogue matches that.", ariaLabel: "Search the catalogue for a price lookup" }), _jsx(Text, { size: "xs", color: "muted", children: "Models, parts and spares, eras and brands \u2014 straight from our own catalogue, so what you pick is a real thing we can price." })] })) : (
413
+ /*
414
+ * 🛑 The fallback, for a disabled or failed search artifact. Without
415
+ * it a kill-switch flip would leave an empty combobox and no way to
416
+ * search at all — the dropdown has no options without the artifact,
417
+ * but the server-side `searchTxt` path still works.
418
+ */
419
+ _jsx(Form, { onSubmit: (e) => e.preventDefault(), children: () => (_jsxs(Row, { gap: "sm", align: "end", wrap: true, className: "min-w-0", children: [_jsx(Div, { className: "flex-1 min-w-[16rem]", children: _jsx(FieldInput, { name: "q", label: "Search", value: draft, onChange: setDraft, placeholder: "e.g. Dragoon G, Storm Pegasus, rubber flat\u2026" }) }), _jsxs(Button, { type: "submit", isLoading: loading, onClick: () => run(draft), children: [_jsx(Search, { className: "w-4 h-4", "aria-hidden": "true" }), "Look up"] })] })) })), error && (_jsx(Text, { size: "sm", color: "error", role: "alert", children: error })), !hasQuery && !loading && !error && (_jsxs(Stack, { gap: "xs", padding: "card", rounded: "lg", surface: "muted", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", children: "Two numbers, and the gap matters" }), _jsxs(Text, { size: "sm", color: "muted", children: [_jsx(Span, { weight: "semibold", children: "Sold" }), " is a completed sale on LetItRip \u2014 not an asking price and not a guess. We publish a figure only once ", threshold, " or more sales support it, and the index is recomputed once a day, so it is deliberately a day behind."] }), _jsxs(Text, { size: "sm", color: "muted", children: [_jsx(Span, { weight: "semibold", children: "Asked" }), " is what other Indian shops list the same thing for, measured across fifteen catalogues.", canSeeMarket
420
+ ? " Shown beside our own sales — never averaged with them, because an ask is not a sale."
421
+ : " Sign in to see it beside our own sales."] })] })), hasQuery && data && data.results.length === 0 && !loading && (_jsxs(Text, { color: "muted", children: ["Nothing matched \u201C", data.query, "\u201D. Try a shorter term, or the model name on its own."] })), data && data.results.length > 0 && (_jsxs(Stack, { gap: "md", className: "min-w-0", children: [data.results.map((row) => (_jsx(ResultCard, { row: row, threshold: threshold, market: marketById.get(row.categoryId), canSeeMarket: canSeeMarket }, row.categoryId))), data.truncated && (_jsxs(Text, { size: "sm", color: "muted", children: ["Showing the closest ", data.results.length, " matches. Narrow the search to see others."] }))] }))] }) }) }));
140
422
  }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The index types the price tool searches.
3
+ *
4
+ * 🛑 Exactly the types that can RESOLVE TO A PRICEABLE NODE, which is a
5
+ * narrower set than "everything in the index" and deliberately so:
6
+ *
7
+ * `model` -> its model leaf (href is `/categories/{categoryId}`)
8
+ * `category` -> itself, incl. the era and part-group leaves the crawl
9
+ * actually measured
10
+ * `part` -> its part-group leaf via `PART_SLOT_CATEGORY` — this is the
11
+ * one that makes spares work, and those leaves
12
+ * (`category-metal-energy-rings`, `category-burst-layers`, …)
13
+ * are precisely where `MARKET_BANDS` has component pricing
14
+ * `brand` -> itself; brands are `categories` rows with
15
+ * `categoryType: "brand"`
16
+ *
17
+ * 🛑 What is NOT here, and why — each of these was asked for and cannot be
18
+ * delivered from the client index as it stands:
19
+ *
20
+ * `group` `groupedListings` is not a category, so no band can
21
+ * attach to it at all.
22
+ * the 10 listing A product record's href is `/products/{slug}` and the
23
+ * types record carries no `categoryId`, so resolving one needs a
24
+ * server read per hit. A price band is a statement about a
25
+ * leaf, not about one seller's listing.
26
+ * `bundle`, NOT IN THE INDEX. No `IndexSource` emits them and the
27
+ * `franchise` `category` source rejects any `categoryType` other than
28
+ * `sublisting`, so adding them is an index-build change,
29
+ * not a filter here.
30
+ *
31
+ * 🛑 `brand` is included as asked, and the honest caveat is that a brand row is
32
+ * a tier-0 node OUTSIDE the forest with no `parentIds`, so it inherits no band
33
+ * and will usually resolve to "no market evidence". That renders as an honest
34
+ * empty rather than a wrong number, but it is a thin result and the reason is
35
+ * the data, not the lookup.
36
+ */
37
+ export declare const PRICE_LOOKUP_INDEX_TYPES: readonly string[];
38
+ /**
39
+ * The category id whose band answers a search hit, or `null` when the hit
40
+ * cannot be priced.
41
+ *
42
+ * 🛑 `null` is a real answer and must not be collapsed into a guess. The
43
+ * alternative — falling back to some ancestor, or to the hit's own slug as if
44
+ * it were a category id — would attach a band to a node that does not have one
45
+ * and render a confident number for the wrong thing. A dropped option is
46
+ * visible; a misattributed price is not.
47
+ */
48
+ export declare function priceLookupTargetFor(href: string): string | null;
@@ -0,0 +1,107 @@
1
+ /*
2
+ * WHY: The realised-price tool looks prices up per CATEGORY leaf, but the thing
3
+ * a person searches for is a model, a part, a brand or an era — four
4
+ * different index types with four different href shapes. Turning a search
5
+ * hit into "the leaf whose band answers this" was previously a single
6
+ * regex over `/categories/...` inside the view, which silently dropped
7
+ * every part hit; the tool asked the index for `["model", "category"]`
8
+ * only, so spares were unreachable even though `part` is indexed.
9
+ * WHAT: One pure function from a search hit to the category id to look up, plus
10
+ * the type list the tool should ask the matcher for.
11
+ *
12
+ * EXPORTS:
13
+ * PRICE_LOOKUP_INDEX_TYPES — which index types the tool searches
14
+ * priceLookupTargetFor — a hit's href -> the category id, or null
15
+ *
16
+ * 🛑 Pure. No React, no Firestore, no fetch — it runs inside a `loadOptions`
17
+ * callback on every keystroke.
18
+ *
19
+ * @tag domain:price-index
20
+ * @tag layer:feature
21
+ * @tag pattern:pure-function
22
+ * @tag access:isomorphic
23
+ * @tag consumers:PriceIndexToolView
24
+ * @tag sideEffects:none
25
+ */
26
+ import { PART_SLOT_CATEGORY } from "../parts/schemas/firestore.js";
27
+ /**
28
+ * The index types the price tool searches.
29
+ *
30
+ * 🛑 Exactly the types that can RESOLVE TO A PRICEABLE NODE, which is a
31
+ * narrower set than "everything in the index" and deliberately so:
32
+ *
33
+ * `model` -> its model leaf (href is `/categories/{categoryId}`)
34
+ * `category` -> itself, incl. the era and part-group leaves the crawl
35
+ * actually measured
36
+ * `part` -> its part-group leaf via `PART_SLOT_CATEGORY` — this is the
37
+ * one that makes spares work, and those leaves
38
+ * (`category-metal-energy-rings`, `category-burst-layers`, …)
39
+ * are precisely where `MARKET_BANDS` has component pricing
40
+ * `brand` -> itself; brands are `categories` rows with
41
+ * `categoryType: "brand"`
42
+ *
43
+ * 🛑 What is NOT here, and why — each of these was asked for and cannot be
44
+ * delivered from the client index as it stands:
45
+ *
46
+ * `group` `groupedListings` is not a category, so no band can
47
+ * attach to it at all.
48
+ * the 10 listing A product record's href is `/products/{slug}` and the
49
+ * types record carries no `categoryId`, so resolving one needs a
50
+ * server read per hit. A price band is a statement about a
51
+ * leaf, not about one seller's listing.
52
+ * `bundle`, NOT IN THE INDEX. No `IndexSource` emits them and the
53
+ * `franchise` `category` source rejects any `categoryType` other than
54
+ * `sublisting`, so adding them is an index-build change,
55
+ * not a filter here.
56
+ *
57
+ * 🛑 `brand` is included as asked, and the honest caveat is that a brand row is
58
+ * a tier-0 node OUTSIDE the forest with no `parentIds`, so it inherits no band
59
+ * and will usually resolve to "no market evidence". That renders as an honest
60
+ * empty rather than a wrong number, but it is a thin result and the reason is
61
+ * the data, not the lookup.
62
+ */
63
+ export const PRICE_LOOKUP_INDEX_TYPES = [
64
+ "model",
65
+ "category",
66
+ "part",
67
+ "brand",
68
+ ];
69
+ /** `/categories/{id}` and `/brands/{id}` — category documents are pure slugs. */
70
+ const DIRECT_CATEGORY_HREF = /^\/(?:categories|brands)\/([^/?#]+)/;
71
+ /**
72
+ * `/parts/{classification}/{code}`.
73
+ *
74
+ * 🛑 TWO segments, and the first is the slot. `ROUTES.PUBLIC.PART_DETAIL`'s own
75
+ * comment explains why a code alone cannot be a URL: "`105` is a Metal Fight
76
+ * spin-track height, a bare number in Burst is a disc, and `1-60` is a Beyblade
77
+ * X ratchet." The slot is what places the part in the taxonomy.
78
+ */
79
+ const PART_HREF = /^\/parts\/([^/?#]+)\/([^/?#]+)/;
80
+ /**
81
+ * The category id whose band answers a search hit, or `null` when the hit
82
+ * cannot be priced.
83
+ *
84
+ * 🛑 `null` is a real answer and must not be collapsed into a guess. The
85
+ * alternative — falling back to some ancestor, or to the hit's own slug as if
86
+ * it were a category id — would attach a band to a node that does not have one
87
+ * and render a confident number for the wrong thing. A dropped option is
88
+ * visible; a misattributed price is not.
89
+ */
90
+ export function priceLookupTargetFor(href) {
91
+ const direct = DIRECT_CATEGORY_HREF.exec(href);
92
+ if (direct)
93
+ return direct[1] ?? null;
94
+ const part = PART_HREF.exec(href);
95
+ if (part) {
96
+ const slot = part[1];
97
+ /*
98
+ * `PART_SLOT_CATEGORY` is a `Record<PartSlot, string>`, so a 21st slot is a
99
+ * compile error there rather than a silent miss here — but the href came
100
+ * off an artifact built from live documents, so a slot this build has never
101
+ * heard of is still possible and resolves to `null` rather than
102
+ * `undefined` leaking into a URL.
103
+ */
104
+ return PART_SLOT_CATEGORY[slot] ?? null;
105
+ }
106
+ return null;
107
+ }
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ import type { ListingType } from "../types/index";
3
+ export interface AvailabilityScopeHintProps {
4
+ /**
5
+ * Every published row in this tab's scope, all availability scopes.
6
+ * `undefined` means the count is unknown — the hint renders nothing rather
7
+ * than guessing (Root Cause #72: unknown must never read as a number).
8
+ */
9
+ total?: number;
10
+ /** Rows the AVAILABLE scope renders. Same `undefined` rule. */
11
+ available?: number;
12
+ /**
13
+ * The listing types this tab spans. The hidden-rows noun ("sold", "ended",
14
+ * "sold & ended") is DERIVED from their plugins via `availabilityTabsFor`,
15
+ * which is the same function that labels the scope tabs — so the hint and
16
+ * the control it points at can never disagree about the word. A hand-written
17
+ * string here would be a tenth place the `ListingType` union is enumerated
18
+ * by hand (Root Cause #61).
19
+ */
20
+ types: readonly ListingType[];
21
+ }
22
+ export declare function AvailabilityScopeHint({ total, available, types, }: AvailabilityScopeHintProps): React.JSX.Element | null;
@@ -0,0 +1,27 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Button, Row, Text } from "../../../ui/index.js";
4
+ import { useUrlTable } from "../../../react/hooks/useUrlTable.js";
5
+ import { AVAILABILITY_VALUES } from "../../../constants/field-names.js";
6
+ import { TABLE_KEYS } from "../../../constants/table-keys.js";
7
+ import { availabilityTabsFor } from "../../../_internal/shared/listing-types/_registry.js";
8
+ export function AvailabilityScopeHint({ total, available, types, }) {
9
+ const table = useUrlTable();
10
+ const scope = table.get(TABLE_KEYS.AVAILABILITY) ||
11
+ AVAILABILITY_VALUES.AVAILABLE;
12
+ // Only the default scope withholds anything. In Ended or All the visitor is
13
+ // already looking at the rows this hint would point them to.
14
+ if (scope !== AVAILABILITY_VALUES.AVAILABLE)
15
+ return null;
16
+ if (total === undefined || available === undefined)
17
+ return null;
18
+ const hidden = total - available;
19
+ if (hidden <= 0)
20
+ return null;
21
+ // availabilityTabsFor()[1] is the unavailable scope — "Sold", "Ended" or
22
+ // "Sold & Ended", already derived per type.
23
+ const noun = (availabilityTabsFor(types)[1]?.label ?? "Sold").toLowerCase();
24
+ return (_jsxs(Row, { align: "center", gap: "sm", padding: "sm", surface: "muted", rounded: "lg", border: "subtle", className: "mb-4 flex-wrap", children: [_jsx(Text, { size: "sm", color: "muted", children: hidden === 1
25
+ ? `1 listing here is ${noun} and hidden by the Available filter.`
26
+ : `${hidden} listings here are ${noun} and hidden by the Available filter.` }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => table.set(TABLE_KEYS.AVAILABILITY, AVAILABILITY_VALUES.ALL), children: "Show all" })] }));
27
+ }