@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
@@ -15,6 +15,7 @@
15
15
  * `GET /api/products?ids=<csv>` (BK3 listByIds endpoint).
16
16
  */
17
17
  import type { ListingType } from "../types";
18
+ import type { ProductImage } from "../schemas/firestore";
18
19
  /** Subset of ProductDocument the overlay needs. Kept loose so it can be fed
19
20
  * sanitized public payloads directly. */
20
21
  export interface CompareProductLike {
@@ -24,7 +25,7 @@ export interface CompareProductLike {
24
25
  price?: number;
25
26
  currency?: string;
26
27
  mainImage?: string;
27
- images?: string[];
28
+ images?: readonly (string | ProductImage)[];
28
29
  condition?: string;
29
30
  brand?: string;
30
31
  category?: string;
@@ -60,6 +61,8 @@ export interface CompareOverlayLabels {
60
61
  close?: string;
61
62
  remove?: string;
62
63
  view?: string;
64
+ /** Placeholder/aria-label for the "add another" typeahead. Only rendered when `onAdd` is passed. */
65
+ addAnother?: string;
63
66
  field?: Partial<CompareFieldLabels>;
64
67
  }
65
68
  export interface CompareOverlayProps {
@@ -73,7 +76,22 @@ export interface CompareOverlayProps {
73
76
  onClose: () => void;
74
77
  /** Called when a column's Remove button is clicked. */
75
78
  onRemove?: (id: string) => void;
79
+ /**
80
+ * Called with a resolved slug when the user picks a result from the
81
+ * "Add another" typeahead. Omit to leave the overlay exactly as it was —
82
+ * every existing caller keeps working unchanged.
83
+ *
84
+ * 🛑 Resolution only, no fetch. The overlay already knows how to turn an
85
+ * id into a full `CompareProductLike` via `productIds` (`useFetchedProducts`
86
+ * fetches `/api/products?ids=…`); this hands back ONE more id for that same
87
+ * mechanism, rather than teaching the overlay a second way to load a
88
+ * product. The caller decides whether to append to its own `productIds`
89
+ * state or ignore it.
90
+ */
91
+ onAdd?: (id: string) => void;
92
+ /** `siteSettings.search?.indexEnabled`. Threaded through to the typeahead. */
93
+ searchIndexEnabled?: boolean;
76
94
  labels?: CompareOverlayLabels;
77
95
  }
78
- export declare function CompareOverlay({ isOpen, items: passedItems, productIds, productType, onClose, onRemove, labels: labelOverrides, }: CompareOverlayProps): import("react").JSX.Element | null;
96
+ export declare function CompareOverlay({ isOpen, items: passedItems, productIds, productType, onClose, onRemove, onAdd, searchIndexEnabled, labels: labelOverrides, }: CompareOverlayProps): import("react").JSX.Element | null;
79
97
  export {};
@@ -20,12 +20,15 @@ import { COMPARE_MAX_ITEMS } from "../constants/action-defs.js";
20
20
  import { normalizeListingType } from "../utils/listing-type.js";
21
21
  import { pluginFor } from "../../../_internal/shared/listing-types/_registry.js";
22
22
  import { PRODUCT_ENDPOINTS } from "../../../constants/api-endpoints.js";
23
+ import { EntityTypeahead } from "../../search/components/EntityTypeahead.js";
24
+ import { productImageUrls } from "../schemas/firestore.js";
23
25
  const DEFAULT_LABELS = {
24
26
  title: "Compare items",
25
27
  empty: "Select up to 4 items from the listing to compare.",
26
28
  close: "Close",
27
29
  remove: "Remove",
28
30
  view: "View",
31
+ addAnother: "Add another to compare",
29
32
  field: {
30
33
  image: "Photo",
31
34
  price: "Price",
@@ -35,6 +38,12 @@ const DEFAULT_LABELS = {
35
38
  store: "Store",
36
39
  },
37
40
  };
41
+ /** Index type keys for each `productType` discriminator the overlay accepts. */
42
+ const TYPEAHEAD_TYPES = {
43
+ product: ["standard"],
44
+ auction: ["auction"],
45
+ preorder: ["pre-order"],
46
+ };
38
47
  const OVERLAY_CLASS = "fixed inset-0 flex flex-col bg-[var(--appkit-color-bg)] overflow-hidden z-[var(--appkit-z-modal,60)]";
39
48
  const HEADER_CLASS = "flex items-center justify-between gap-[var(--appkit-space-3)] border-b border-[var(--appkit-color-border-subtle)] px-[var(--appkit-space-4)] py-[var(--appkit-space-3)]";
40
49
  const FIELD_LABEL_CLASS = "text-[length:var(--appkit-text-xs)] font-medium uppercase tracking-wider text-[var(--appkit-color-text-muted)]";
@@ -66,7 +75,7 @@ function customFieldValue(field) {
66
75
  }
67
76
  function CompareColumn({ item, productType, labels, onRemove, onClose, customFieldKeys }) {
68
77
  const href = detailHref(item, productType);
69
- const img = item.mainImage ?? item.images?.[0];
78
+ const img = item.mainImage || productImageUrls(item.images)[0];
70
79
  return (_jsxs(Stack, { gap: "md", className: COLUMN_CARD_CLASS, children: [_jsxs(Row, { justify: "between", align: "start", gap: "sm", children: [_jsx(Link, { href: href, target: "_blank", rel: REL_NOOPENER, className: "text-[length:var(--appkit-text-sm)] font-semibold text-[var(--appkit-color-text)] hover:underline line-clamp-2", children: item.title ?? item.id }), onRemove && (_jsx(IconButton, { type: "button", onClick: () => onRemove(item.id), "aria-label": `${labels.remove} ${item.title ?? item.id}`, variant: "ghost", rounded: "full", className: CLS_REMOVE_BTN, children: _jsx(X, { className: "h-4 w-4" }) }))] }), _jsx(FieldRow, { label: labels.field.image, children: _jsx(Link, { href: href, target: "_blank", rel: REL_NOOPENER, children: _jsx(Div, { className: `relative aspect-square ${__O.hidden}`, rounded: "lg", surface: "subtle", children: img ? (_jsx(MediaImage, { src: img, alt: item.title ?? item.id, size: "card" })) : null }) }) }), _jsx(FieldRow, { label: labels.field.price, children: _jsx(Text, { size: "base", weight: "semibold", color: "primary", children: _jsx(GatedPrice, { children: priceLabel(item) }) }) }), _jsx(FieldRow, { label: labels.field.condition, children: _jsx(Text, { children: item.condition ? _jsx(Span, { className: CHIP_CLASS, children: item.condition }) : "—" }) }), _jsx(FieldRow, { label: labels.field.brand, children: _jsx(Text, { children: item.brand ? _jsx(Span, { className: CHIP_CLASS, children: item.brand }) : "—" }) }), _jsx(FieldRow, { label: labels.field.category, children: _jsx(Text, { children: (Array.isArray(item.categoryNames) ? item.categoryNames[0] : item.categoryName) || (Array.isArray(item.categorySlugs) ? item.categorySlugs[0] : item.category) ? (_jsx(Span, { className: CHIP_CLASS, children: (Array.isArray(item.categoryNames) ? item.categoryNames[0] : item.categoryName) ?? (Array.isArray(item.categorySlugs) ? item.categorySlugs[0] : item.category) })) : ("—") }) }), _jsx(FieldRow, { label: labels.field.store, children: _jsx(Text, { size: "sm", color: "muted", children: item.storeName ?? "—" }) }), customFieldKeys.map((key) => (_jsx(FieldRow, { label: key, children: _jsx(Text, { size: "sm", children: customFieldValue(item.customFields?.find((f) => f.key === key)) }) }, key))), _jsx(Button, { asChild: true, variant: "primary", size: "sm", className: "mt-auto", children: _jsx(Link, { href: href, target: "_blank", rel: REL_NOOPENER, onClick: onClose, children: labels.view }) })] }));
71
80
  }
72
81
  function useFetchedProducts(productIds, passedItems, isOpen) {
@@ -108,7 +117,7 @@ function useFetchedProducts(productIds, passedItems, isOpen) {
108
117
  isLoading: !passedItems && isLoading,
109
118
  };
110
119
  }
111
- export function CompareOverlay({ isOpen, items: passedItems, productIds, productType = "product", onClose, onRemove, labels: labelOverrides, }) {
120
+ export function CompareOverlay({ isOpen, items: passedItems, productIds, productType = "product", onClose, onRemove, onAdd, searchIndexEnabled, labels: labelOverrides, }) {
112
121
  const labels = useMemo(() => ({
113
122
  ...DEFAULT_LABELS,
114
123
  ...labelOverrides,
@@ -167,7 +176,18 @@ export function CompareOverlay({ isOpen, items: passedItems, productIds, product
167
176
  }, [onRemove]);
168
177
  if (!isOpen)
169
178
  return null;
170
- return (_jsxs(Div, { className: OVERLAY_CLASS, role: "dialog", "aria-modal": "true", "aria-label": labels.title, children: [_jsxs(Row, { justify: "between", align: "center", className: HEADER_CLASS, children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: labels.title }), _jsx(IconButton, { type: "button", onClick: onClose, "aria-label": labels.close, variant: "ghost", rounded: "full", className: "text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface-elevated)]", children: _jsx(X, { className: "h-5 w-5" }) })] }), _jsx(Div, { className: `flex-1 ${__O.auto} ${__P.p4}`, children: isLoading ? (
179
+ return (_jsxs(Div, { className: OVERLAY_CLASS, role: "dialog", "aria-modal": "true", "aria-label": labels.title, children: [_jsxs(Row, { justify: "between", align: "center", className: HEADER_CLASS, children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: labels.title }), _jsx(IconButton, { type: "button", onClick: onClose, "aria-label": labels.close, variant: "ghost", rounded: "full", className: "text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface-elevated)]", children: _jsx(X, { className: "h-5 w-5" }) })] }), onAdd && trimmed.length < COMPARE_MAX_ITEMS && (_jsx(Div, { className: `${__P.p4} border-b border-[var(--appkit-color-border-subtle)]`, children: _jsx(EntityTypeahead, { types: TYPEAHEAD_TYPES[productType], indexEnabled: searchIndexEnabled, label: labels.addAnother, placeholder: labels.addAnother, limit: 5, onPick: (pick) => {
180
+ /*
181
+ * The LAST path segment, not a type-specific pattern. The
182
+ * three listing types this overlay accepts have three
183
+ * different detail routes (/products, /auctions,
184
+ * /pre-orders), and every one of them ends in the slug — the
185
+ * same thing `productIds` already expects.
186
+ */
187
+ const slug = pick.href.split("/").filter(Boolean).pop();
188
+ if (slug)
189
+ onAdd(slug);
190
+ } }) })), _jsx(Div, { className: `flex-1 ${__O.auto} ${__P.p4}`, children: isLoading ? (
171
191
  // audit-content-alignment-ok: loading-state text inside a transient overlay panel, not marketing content
172
192
  _jsx(Text, { paddingY: "3xl", variant: "secondary", align: "center", children: "Loading\u00E2\u20AC\u00A6" })) : trimmed.length === 0 ? (
173
193
  // audit-content-alignment-ok: empty-state inside a transient overlay panel, not marketing content
@@ -1,5 +1,36 @@
1
1
  import React from "react";
2
2
  import type { CustomSection } from "../schemas/firestore";
3
+ /**
4
+ * Shape `ProductTabsShell` expects for `customTabs`. Duplicated here rather
5
+ * than imported from `ProductTabsShell.tsx` — that file is `"use client"` and
6
+ * this one is a plain function, so importing its type keeps this module
7
+ * server-renderable. The two must stay structurally compatible; they already
8
+ * do (`{ id, label, content }`).
9
+ */
10
+ interface CustomSectionTab {
11
+ id: string;
12
+ label: string;
13
+ content: React.ReactNode;
14
+ }
15
+ /**
16
+ * `customSections` → `ProductTabsShell`'s `customTabs` prop, in one place.
17
+ *
18
+ * Six detail views (`ProductDetailPageView`, `AuctionDetailPageView`,
19
+ * `PreOrderDetailPageView`, `ClassifiedDetailPageView`,
20
+ * `DigitalCodeDetailPageView`, `LiveItemDetailPageView`) had this EXACT
21
+ * five-line block — the same `Array.isArray` guard and the same `.map` —
22
+ * copy-pasted six times. Rule-of-Three material by the Duplication Decision
23
+ * Framework (same prop surface, same shape, a bug fix would have needed six
24
+ * commits), consolidated here rather than left as six places that could
25
+ * silently drift the next time one of them is touched and the other five are
26
+ * not.
27
+ *
28
+ * Takes the raw, possibly-untyped `customSections` field directly (every call
29
+ * site reads it off a cast Firestore document) so callers do one line instead
30
+ * of two: `customTabs={toCustomSectionTabs(p.customSections)}`.
31
+ */
32
+ export declare function toCustomSectionTabs(input: unknown): CustomSectionTab[];
3
33
  export declare function CustomSectionTabContent({ section }: {
4
34
  section: CustomSection;
5
35
  }): React.JSX.Element;
36
+ export {};
@@ -1,14 +1,40 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Row } from "@mohasinac/appkit/ui";
3
- import { Anchor, Dd, Dl, Dt, RichText, Stack, Text } from "../../../ui/index.js";
2
+ import { RichText, SpecTable, Stack, Text } from "../../../ui/index.js";
4
3
  import { normalizeRichTextHtml } from "../../../utils/string.formatter.js";
5
4
  function renderFieldValue(f) {
6
5
  if (f.type === "boolean")
7
6
  return f.value === "true" ? "Yes" : "No";
8
7
  return f.value + (f.unit ? ` ${f.unit}` : "");
9
8
  }
9
+ /**
10
+ * `customSections` → `ProductTabsShell`'s `customTabs` prop, in one place.
11
+ *
12
+ * Six detail views (`ProductDetailPageView`, `AuctionDetailPageView`,
13
+ * `PreOrderDetailPageView`, `ClassifiedDetailPageView`,
14
+ * `DigitalCodeDetailPageView`, `LiveItemDetailPageView`) had this EXACT
15
+ * five-line block — the same `Array.isArray` guard and the same `.map` —
16
+ * copy-pasted six times. Rule-of-Three material by the Duplication Decision
17
+ * Framework (same prop surface, same shape, a bug fix would have needed six
18
+ * commits), consolidated here rather than left as six places that could
19
+ * silently drift the next time one of them is touched and the other five are
20
+ * not.
21
+ *
22
+ * Takes the raw, possibly-untyped `customSections` field directly (every call
23
+ * site reads it off a cast Firestore document) so callers do one line instead
24
+ * of two: `customTabs={toCustomSectionTabs(p.customSections)}`.
25
+ */
26
+ export function toCustomSectionTabs(input) {
27
+ const sections = Array.isArray(input)
28
+ ? input
29
+ : [];
30
+ return sections.map((s) => ({
31
+ id: s.id,
32
+ label: s.title,
33
+ content: _jsx(CustomSectionTabContent, { section: s }),
34
+ }));
35
+ }
10
36
  export function CustomSectionTabContent({ section }) {
11
37
  const html = section.text ? normalizeRichTextHtml(section.text) : null;
12
38
  const fields = section.fields?.filter((f) => f.key && f.value) ?? [];
13
- return (_jsxs(Stack, { gap: "md", children: [html && (_jsx(RichText, { html: html, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })), fields.length > 0 && (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: fields.map((f, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { weight: "medium", className: "w-36 flex-shrink-0 capitalize", color: "primary", children: f.key }), _jsx(Dd, { className: "flex-1", color: "muted", children: f.type === "url" ? (_jsx(Anchor, { href: f.value, tone: "none", underline: "hover", className: "text-primary-600 dark:text-primary-400 break-all", children: f.value })) : (renderFieldValue(f)) })] }, i))) })), !html && fields.length === 0 && (_jsx(Text, { size: "sm", color: "faint", children: "No content in this section." }))] }));
39
+ return (_jsxs(Stack, { gap: "md", children: [html && (_jsx(RichText, { html: html, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })), fields.length > 0 && (_jsx(SpecTable, { rows: fields.map((f) => ({ name: f.key, value: String(f.value), unit: f.unit })) })), !html && fields.length === 0 && (_jsx(Text, { size: "sm", color: "faint", children: "No content in this section." }))] }));
14
40
  }
@@ -16,6 +16,7 @@
16
16
  * consumer pages untouched.
17
17
  */
18
18
  import React from "react";
19
+ import { type GroupRules } from "../../../_internal/shared/features/products/group-rules";
19
20
  import type { ListingType } from "../types";
20
21
  export interface GroupPickerMember {
21
22
  id: string;
@@ -29,6 +30,14 @@ export interface GroupPickerMember {
29
30
  isGroupParent?: boolean;
30
31
  condition?: string;
31
32
  availableQuantity?: number;
33
+ /**
34
+ * Printed or built when ordered — no stock ceiling.
35
+ *
36
+ * 🛑 Both the "Out of stock" refusal and the stepper's `max` read
37
+ * `availableQuantity`, so without this a print-on-demand member at quantity 0
38
+ * renders as unselectable while being perfectly buyable.
39
+ */
40
+ madeToOrder?: boolean;
32
41
  storeId?: string;
33
42
  storeName?: string;
34
43
  isSold?: boolean;
@@ -38,9 +47,18 @@ export interface GroupMemberPickerProps {
38
47
  groupId: string;
39
48
  groupSource: "product-group" | "grouped-listing";
40
49
  members: GroupPickerMember[];
50
+ /**
51
+ * The group's own selection rules, from whichever home owns them.
52
+ *
53
+ * 🛑 Absent means UNRESTRICTED, which is what every group that exists today
54
+ * is. The server enforces the same rules through `checkGroupRules` whatever
55
+ * is passed here, so omitting them costs correctness nothing — the buyer just
56
+ * finds out on submit instead of while choosing.
57
+ */
58
+ rules?: GroupRules;
41
59
  /** Called after a successful add so the host can close its overlay. */
42
60
  onAdded?: () => void;
43
61
  /** Called when the API reports the caller isn't signed in. */
44
62
  onAuthRequired?: () => void;
45
63
  }
46
- export declare function GroupMemberPicker({ groupId, groupSource, members, onAdded, onAuthRequired, }: GroupMemberPickerProps): React.JSX.Element;
64
+ export declare function GroupMemberPicker({ groupId, groupSource, members, rules, onAdded, onAuthRequired, }: GroupMemberPickerProps): React.JSX.Element;
@@ -27,10 +27,10 @@ import { CART_ENDPOINTS } from "../../../constants/api-endpoints.js";
27
27
  import { normalizeError } from "../../../errors/normalize.js";
28
28
  import { pluginFor } from "../../../_internal/shared/listing-types/_registry.js";
29
29
  import { normalizeListingType } from "../utils/listing-type.js";
30
+ import { resolveGroupMemberImage } from "../utils/group-member-image.js";
31
+ import { checkGroupRules, describeGroupRules, EMPTY_GROUP_RULES, } from "../../../_internal/shared/features/products/group-rules.js";
30
32
  const __O = { xAuto: "overflow-x-auto", hidden: "overflow-hidden" };
31
- function memberImage(m) {
32
- return m.image ?? m.images?.[0] ?? "";
33
- }
33
+ const memberImage = resolveGroupMemberImage;
34
34
  function memberHref(m) {
35
35
  return pluginFor(normalizeListingType(m)).detailRoute(m.slug ?? m.id);
36
36
  }
@@ -40,7 +40,10 @@ function blockedReason(m) {
40
40
  return "Sold";
41
41
  if (m.status && m.status !== "published")
42
42
  return "Unavailable";
43
- if ((m.availableQuantity ?? 0) <= 0)
43
+ // Made to order has no stock to be out of — checked BEFORE the quantity
44
+ // test, and after `isSold`, which stays the seller's kill switch whatever
45
+ // the production model.
46
+ if (m.madeToOrder !== true && (m.availableQuantity ?? 0) <= 0)
44
47
  return "Out of stock";
45
48
  // `cartLine: "blocked"` is how a classified / live listing says "contact the
46
49
  // seller instead" — those can never join a cart line.
@@ -48,7 +51,7 @@ function blockedReason(m) {
48
51
  return "Not purchasable here";
49
52
  return null;
50
53
  }
51
- export function GroupMemberPicker({ groupId, groupSource, members, onAdded, onAuthRequired, }) {
54
+ export function GroupMemberPicker({ groupId, groupSource, members, rules = EMPTY_GROUP_RULES, onAdded, onAuthRequired, }) {
52
55
  const [qty, setQty] = useState({});
53
56
  const [isSaving, setIsSaving] = useState(false);
54
57
  const [error, setError] = useState(null);
@@ -64,6 +67,32 @@ export function GroupMemberPicker({ groupId, groupSource, members, onAdded, onAu
64
67
  const selectedTotal = selected.reduce((sum, s) => sum + s.member.price * s.quantity, 0);
65
68
  const selectedUnits = selected.reduce((sum, s) => sum + s.quantity, 0);
66
69
  const currency = members[0]?.currency ?? "INR";
70
+ /*
71
+ * The SAME predicate the server applies, so the button's enabled state and
72
+ * the API's answer cannot disagree about what a valid selection is. A
73
+ * server-only rule reads as a broken button; a client-only one is not a rule.
74
+ */
75
+ const violations = useMemo(() => checkGroupRules(rules, selected.map((s) => ({
76
+ productId: s.member.id,
77
+ quantity: s.quantity,
78
+ title: s.member.title,
79
+ }))), [rules, selected]);
80
+ /*
81
+ * The discount, shown but NOT the number charged.
82
+ *
83
+ * 🛑 This is display arithmetic over a quoted percentage; the authoritative
84
+ * price is computed server-side inside `deriveLinePrice`, the single writer
85
+ * of a grouped line's `price`. Showing it here is honest (it is the figure
86
+ * the buyer is being offered) and computing it here is unavoidable (there is
87
+ * no cart line yet) — but it must never become a second money rule. The 2dp
88
+ * rounding deliberately matches `deriveLinePrice`'s so the two agree to the
89
+ * paisa rather than drifting by a rounding step.
90
+ */
91
+ const discountPercent = Math.min(100, Math.max(0, rules.groupDiscountPercent ?? 0));
92
+ const discountedTotal = discountPercent > 0
93
+ ? Math.round(selectedTotal * (1 - discountPercent / 100) * 100) / 100
94
+ : selectedTotal;
95
+ const ruleNotes = useMemo(() => describeGroupRules(rules), [rules]);
67
96
  const handleAdd = async () => {
68
97
  setError(null);
69
98
  setIsSaving(true);
@@ -101,8 +130,17 @@ export function GroupMemberPicker({ groupId, groupSource, members, onAdded, onAu
101
130
  };
102
131
  return (_jsxs(Stack, { gap: "sm", children: [isCrossStore && (_jsx(Alert, { variant: "info", children: "These items are sold by different sellers, so they can't be combined into one cart line. Open an item to buy it on its own." })), _jsx(Div, { className: __O.xAuto, children: _jsxs(Table, { className: "text-left min-w-[440px]", children: [_jsx(Thead, { children: _jsxs(Tr, { border: "default", children: [_jsx(Th, { paddingY: "sm", paddingSide: "pr-sm", color: "muted", size: "xs", weight: "semibold", children: "Image" }), _jsx(Th, { paddingY: "sm", paddingSide: "pr-sm", color: "muted", size: "xs", weight: "semibold", children: "Name" }), _jsx(Th, { paddingY: "sm", paddingSide: "pr-sm", color: "muted", size: "xs", weight: "semibold", children: "Price" }), !isCrossStore && (_jsx(Th, { paddingY: "sm", paddingSide: "pr-sm", color: "muted", size: "xs", weight: "semibold", children: "Qty" })), _jsx(Th, { paddingY: "sm", paddingSide: "pb-sm", color: "muted", size: "xs", weight: "semibold" })] }) }), _jsx(Tbody, { children: members.map((m) => {
103
132
  const blocked = blockedReason(m);
104
- return (_jsxs(Tr, { className: "last:border-0", border: "subtle", children: [_jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Div, { className: `w-10 h-10 ${__O.hidden}`, rounded: "full", border: "default", children: _jsx(MediaImage, { src: memberImage(m), alt: m.title, size: "thumbnail" }) }) }), _jsxs(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: [_jsx(Text, { className: "line-clamp-2", color: "primary", size: "sm", weight: "medium", children: m.title }), m.isGroupParent && (_jsx(Span, { weight: "semibold", className: "text-[10px] text-[var(--appkit-color-primary)]", children: "Parent" }))] }), _jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Text, { size: "sm", color: "muted", children: _jsx(GatedPrice, { children: formatCurrency(m.price, m.currency ?? currency) }) }) }), !isCrossStore && (_jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: blocked ? (_jsx(Span, { size: "xs", color: "muted", surface: "muted", rounded: "default", padding: "pill-2xs", children: blocked })) : (_jsx(QuantityStepper, { value: qty[m.id] ?? 0, onChange: (next) => setQty((prev) => ({ ...prev, [m.id]: next })), min: 0, max: m.availableQuantity, ariaLabel: `Quantity for ${m.title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel })) })), _jsx(Td, { padding: "xs-tall", children: _jsx("a", { href: memberHref(m), className: "text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-primary)] hover:underline", children: "View \u2192" }) })] }, m.id));
105
- }) })] }) }), !isCrossStore && (_jsxs(_Fragment, { children: [error && _jsx(Alert, { variant: "error", children: error }), _jsxs(Row, { justify: "between", align: "center", gap: "sm", className: "min-w-0", children: [_jsxs(Text, { size: "sm", color: "muted", children: [selectedUnits > 0
133
+ return (_jsxs(Tr, { className: "last:border-0", border: "subtle", children: [_jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Div, { className: `w-10 h-10 ${__O.hidden}`, rounded: "full", border: "default", children: _jsx(MediaImage, { src: memberImage(m), alt: m.title, size: "thumbnail" }) }) }), _jsxs(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: [_jsx(Text, { className: "line-clamp-2", color: "primary", size: "sm", weight: "medium", children: m.title }), m.isGroupParent && (_jsx(Span, { weight: "semibold", className: "text-[10px] text-[var(--appkit-color-primary)]", children: "Parent" }))] }), _jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Text, { size: "sm", color: "muted", children: _jsx(GatedPrice, { children: formatCurrency(m.price, m.currency ?? currency) }) }) }), !isCrossStore && (_jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: blocked ? (_jsx(Span, { size: "xs", color: "muted", surface: "muted", rounded: "default", padding: "pill-2xs", children: blocked })) : (_jsx(QuantityStepper, { value: qty[m.id] ?? 0, onChange: (next) => setQty((prev) => ({ ...prev, [m.id]: next })), min: 0,
134
+ /*
135
+ * `undefined` means uncapped — `QuantityStepper`
136
+ * only disables its increment when a `max` is given.
137
+ * A made-to-order member has no ceiling; the line's
138
+ * own 99-per-line cap still applies at the cart.
139
+ */
140
+ max: m.madeToOrder === true ? undefined : m.availableQuantity, ariaLabel: `Quantity for ${m.title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel })) })), _jsx(Td, { padding: "xs-tall", children: _jsx("a", { href: memberHref(m), className: "text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-primary)] hover:underline", children: "View \u2192" }) })] }, m.id));
141
+ }) })] }) }), !isCrossStore && (_jsxs(_Fragment, { children: [ruleNotes.length > 0 && (_jsx(Text, { size: "xs", color: "muted", children: ruleNotes.join(" · ") })), error && _jsx(Alert, { variant: "error", children: error }), selectedUnits > 0 && violations.length > 0 && (_jsx(Alert, { variant: "warning", children: _jsx(Stack, { gap: "dense", children: violations.map((v) => (_jsx(Text, { size: "sm", children: v.message }, v.code + v.message))) }) })), _jsxs(Row, { justify: "between", align: "center", gap: "sm", className: "min-w-0", children: [_jsxs(Text, { size: "sm", color: "muted", children: [selectedUnits > 0
106
142
  ? `${selectedUnits} item${selectedUnits === 1 ? "" : "s"}`
107
- : "Nothing selected yet", selectedUnits > 0 && (_jsx(PricesOnly, { children: ` · ${formatCurrency(selectedTotal, currency)}` }))] }), _jsx(Button, { action: ACTIONS.CART["add-group-to-cart"], disabled: selectedUnits === 0, isLoading: isSaving, onClick: handleAdd })] })] }))] }));
143
+ : "Nothing selected yet", selectedUnits > 0 && (_jsx(PricesOnly, { children: discountPercent > 0
144
+ ? ` · ${formatCurrency(discountedTotal, currency)} (${discountPercent}% off ${formatCurrency(selectedTotal, currency)})`
145
+ : ` · ${formatCurrency(selectedTotal, currency)}` }))] }), _jsx(Button, { action: ACTIONS.CART["add-group-to-cart"], disabled: selectedUnits === 0 || violations.length > 0, isLoading: isSaving, onClick: handleAdd })] })] }))] }));
108
146
  }
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import type { GroupRules } from "../../../_internal/shared/features/products/group-rules";
2
3
  export interface GroupSettingsPanelProps {
3
4
  productId: string;
4
5
  productSlug: string;
@@ -7,8 +8,17 @@ export interface GroupSettingsPanelProps {
7
8
  groupParentSlug?: string;
8
9
  groupChildSlugs?: string[];
9
10
  groupTitle?: string;
11
+ /**
12
+ * The group's current selection rules, for seeding the form.
13
+ *
14
+ * 🛑 Seeded from the PARENT PRODUCT the editor is already showing — not
15
+ * re-fetched. The rules live on the parent and this panel is mounted from the
16
+ * parent's own editor, so the values are already in hand; a second read would
17
+ * be a second source of truth for the same five fields.
18
+ */
19
+ groupRules?: GroupRules;
10
20
  isAuction?: boolean;
11
21
  storeProductsEndpoint: string;
12
22
  onGroupChanged: () => void;
13
23
  }
14
- export declare function GroupSettingsPanel({ productId, productSlug, groupId, isGroupParent, groupParentSlug, groupChildSlugs, groupTitle, isAuction, storeProductsEndpoint, onGroupChanged, }: GroupSettingsPanelProps): React.JSX.Element | null;
24
+ export declare function GroupSettingsPanel({ productId, productSlug, groupId, isGroupParent, groupParentSlug, groupChildSlugs, groupTitle, groupRules, isAuction, storeProductsEndpoint, onGroupChanged, }: GroupSettingsPanelProps): React.JSX.Element | null;
@@ -3,8 +3,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { normalizeError } from "../../../errors/normalize.js";
4
4
  import React, { useEffect, useState } from "react";
5
5
  import { z } from "zod";
6
- import { Button, Code, ConfirmDeleteModal, Div, Heading, Modal, Row, SideDrawer, Span, Stack, Tabs, TabsContent, TabsList, TabsTrigger, Text, useToast } from "../../../ui/index.js";
6
+ import { Button, Checkbox, Code, ConfirmDeleteModal, Div, Grid, Heading, Input, Modal, Row, SideDrawer, Span, Stack, Tabs, TabsContent, TabsList, TabsTrigger, Text, useToast } from "../../../ui/index.js";
7
7
  import { FormShellContext, useFormShellState, FormErrorSummary } from "../../../ui/forms/index.js";
8
+ import { FieldInput } from "../../../ui/forms/FieldInput.js";
8
9
  import { MediaImage } from "../../media/MediaImage.js";
9
10
  import { apiClient } from "../../../http/index.js";
10
11
  import { PRODUCT_ENDPOINTS } from "../../../constants/api-endpoints.js";
@@ -14,6 +15,7 @@ import { PRODUCT_FIELDS } from "../../../constants/field-names.js";
14
15
  import { annotate } from "../../shell/field-ui-meta.js";
15
16
  import { SectionForm, useSectionFormNav, buildSectionsFromSchema, visibleValues, } from "../../shell/index.js";
16
17
  import { applyZodIssues } from "../../../ui/forms/apply-zod-issues.js";
18
+ import { productImageUrls } from "../schemas/firestore.js";
17
19
  const groupTitleSchema = z.object({
18
20
  groupTitle: annotate(z.string().min(1, "Group title is required"), {
19
21
  section: "group", sectionLabel: "Group", sectionRequired: true, quick: true,
@@ -50,7 +52,7 @@ const CONDITION_OPTIONS = [
50
52
  { value: "graded", label: "Graded" },
51
53
  { value: "refurbished", label: "Refurbished" },
52
54
  ];
53
- export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupParent, groupParentSlug, groupChildSlugs, groupTitle, isAuction, storeProductsEndpoint, onGroupChanged, }) {
55
+ export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupParent, groupParentSlug, groupChildSlugs, groupTitle, groupRules, isAuction, storeProductsEndpoint, onGroupChanged, }) {
54
56
  const { showToast } = useToast();
55
57
  const [open, setOpen] = useState(false);
56
58
  const [loading, setLoading] = useState(false);
@@ -64,6 +66,21 @@ export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupPar
64
66
  });
65
67
  const [linkTargets, setLinkTargets] = useState([]);
66
68
  const [children, setChildren] = useState(null);
69
+ /*
70
+ * Rule drafts as STRINGS, not numbers. An empty input has to be
71
+ * distinguishable from zero — "" means "no limit" and `0` would mean "pick at
72
+ * most none" — and a numeric state cannot express the empty case without a
73
+ * separate null, which is the same thing with extra steps.
74
+ */
75
+ const [ruleDraft, setRuleDraft] = useState({
76
+ min: groupRules?.groupMinMembers?.toString() ?? "",
77
+ max: groupRules?.groupMaxMembers?.toString() ?? "",
78
+ perMember: groupRules?.groupMaxUnitsPerMember?.toString() ?? "",
79
+ discount: groupRules?.groupDiscountPercent?.toString() ?? "",
80
+ });
81
+ /** Per-member stock edits, keyed by product id. Only touched rows are sent. */
82
+ const [stockDraft, setStockDraft] = useState({});
83
+ const [motDraft, setMotDraft] = useState({});
67
84
  const [confirmAction, setConfirmAction] = useState(null);
68
85
  const titleSections = React.useMemo(() => buildSectionsFromSchema(groupTitleSchema), []);
69
86
  const titleNav = useSectionFormNav(titleSections, titleDraft, { scope: "store:group-title" });
@@ -97,6 +114,77 @@ export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupPar
97
114
  setLoading(false);
98
115
  }
99
116
  }
117
+ /**
118
+ * Save the selection rules.
119
+ *
120
+ * 🛑 An empty input sends `null`, not an omitted key — "no limit" has to be
121
+ * expressible. The route writes only keys PRESENT in the body, so omitting
122
+ * one means "leave it alone", which is the opposite of what clearing a field
123
+ * means. Those are two different intents and the body has to carry both.
124
+ */
125
+ async function saveRules() {
126
+ setLoading(true);
127
+ try {
128
+ const n = (v) => (v.trim() === "" ? null : Number(v));
129
+ await apiClient.patch(groupEndpoint, {
130
+ groupMinMembers: n(ruleDraft.min),
131
+ groupMaxMembers: n(ruleDraft.max),
132
+ groupMaxUnitsPerMember: n(ruleDraft.perMember),
133
+ groupDiscountPercent: n(ruleDraft.discount),
134
+ });
135
+ showToast("Group rules saved.", "success");
136
+ onGroupChanged();
137
+ }
138
+ catch (e) {
139
+ void normalizeError(e);
140
+ showToast(e?.message ?? "Failed to save group rules.", "error");
141
+ }
142
+ finally {
143
+ setLoading(false);
144
+ }
145
+ }
146
+ /**
147
+ * Save stock and made-to-order across the group.
148
+ *
149
+ * 🛑 Sends ONLY the rows the admin touched. Sending every member would make
150
+ * an untouched draft child's stock a write on every save — and the route
151
+ * refuses an entry that names neither field anyway, so a blanket send would
152
+ * 400 as soon as one row was left alone.
153
+ */
154
+ async function saveGroupStock() {
155
+ const touched = new Set([...Object.keys(stockDraft), ...Object.keys(motDraft)]);
156
+ if (touched.size === 0) {
157
+ showToast("Nothing changed.", "info");
158
+ return;
159
+ }
160
+ setLoading(true);
161
+ try {
162
+ const res = (await apiClient.patch(`${groupEndpoint}/stock`, {
163
+ members: [...touched].map((productId) => ({
164
+ productId,
165
+ ...(stockDraft[productId] !== undefined && stockDraft[productId].trim() !== ""
166
+ ? { stockQuantity: Number(stockDraft[productId]) }
167
+ : {}),
168
+ ...(motDraft[productId] !== undefined ? { madeToOrder: motDraft[productId] } : {}),
169
+ })),
170
+ }));
171
+ const rejected = res.data?.rejected?.length ?? 0;
172
+ showToast(rejected > 0
173
+ ? `Saved, but ${rejected} listing${rejected === 1 ? "" : "s"} could not be updated.`
174
+ : "Group stock saved.", rejected > 0 ? "warning" : "success");
175
+ setStockDraft({});
176
+ setMotDraft({});
177
+ void loadChildren();
178
+ onGroupChanged();
179
+ }
180
+ catch (e) {
181
+ void normalizeError(e);
182
+ showToast(e?.message ?? "Failed to save group stock.", "error");
183
+ }
184
+ finally {
185
+ setLoading(false);
186
+ }
187
+ }
100
188
  async function startGroup() {
101
189
  setLoading(true);
102
190
  try {
@@ -264,7 +352,8 @@ export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupPar
264
352
  setOpen(next);
265
353
  if (next && isGroupParent && children === null)
266
354
  loadChildren();
267
- }, paddingX: "none", paddingY: "xs", justify: "between", className: "w-full text-left group", "aria-expanded": open, children: [_jsx(Heading, { level: 3, size: "sm", weight: "semibold", color: "muted", children: "Group Settings" }), _jsx(Span, { size: "xs", className: "group-hover:text-[var(--appkit-color-text-muted)] dark:group-hover:text-[var(--appkit-color-text-faint)] transition-colors", color: "faint", children: open ? "▲" : "▼" })] }), open && (_jsxs(Stack, { gap: "sm", className: `mt-3 ${__P.p4}`, surface: "muted", rounded: "lg", border: "default", children: [!groupId && !groupParentSlug && (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", color: "muted", children: "Group related listings together \u2014 e.g. a set, bundle, or multi-part item. Parts can be sold individually but shown together." }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: startGroup, isLoading: loading, children: "Start a group" })] })), isGroupParent && groupId && (_jsxs(Stack, { gap: "md", children: [_jsxs(FormShellContext.Provider, { value: titleShellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: titleSections, values: titleDraft, onChange: (partial) => setTitleDraft((d) => ({ ...d, ...partial })), onSubmit: () => void saveTitle(), onValidationChange: () => titleForm.validate(titleDraft), schema: groupTitleSchema, openIds: titleNav.openIds, onOpenChange: titleNav.setOpenIds, submitLabel: "Save title", isLoading: loading })] }), _jsxs(Div, { children: [_jsxs(Text, { className: "mb-2", color: "muted", size: "xs", weight: "semibold", children: ["Members (", childSlugsCount + 1, " \u2014 including this listing)"] }), loading && !children ? (_jsx(Text, { size: "xs", color: "faint", children: "Loading\u2026" })) : (_jsxs(Div, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", children: [_jsxs(Row, { align: "center", gap: "sm", padding: "y-xs", children: [_jsx(Span, { padding: "pill-2xs", weight: "semibold", className: "bg-[var(--appkit-color-primary)]/10 text-[var(--appkit-color-primary)] text-[10px]", rounded: "default", children: "Parent" }), _jsx(Text, { className: "flex-1", color: "primary", size: "sm", children: productSlug })] }), (children ?? []).filter((c) => c.id !== productId).map((child) => (_jsxs(Row, { align: "center", gap: "sm", padding: "y-xs", children: [child.images?.[0] ? (_jsx(Div, { className: "relative w-8 h-8 flex-shrink-0 border border-[var(--appkit-color-border)]", rounded: "full", overflow: "hidden", children: _jsx(MediaImage, { src: child.images[0], alt: child.title, size: "thumbnail" }) })) : (_jsx(Div, { className: "w-8 h-8", surface: "subtle", rounded: "full" })), _jsx(Text, { className: "flex-1 truncate", color: "primary", size: "sm", children: child.title }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => requestUnlinkChild(child.id), isLoading: loading, className: `${CLS_DELETE_LINK} text-[length:var(--appkit-text-xs)]`, children: "Unlink" })] }, child.id)))] }))] }), _jsxs(Row, { gap: "sm", wrap: true, children: [_jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => setShowAddModal(true), children: "Add child listing" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: requestDissolveGroup, isLoading: loading, className: CLS_DELETE_LINK, children: "Dissolve group" })] })] })), !isGroupParent && groupParentSlug && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "sm", color: "muted", children: ["Part of:", " ", _jsx(Span, { weight: "medium", color: "primary", children: groupTitle ?? groupParentSlug })] }), _jsxs(Text, { size: "xs", color: "muted", children: ["Parent listing: ", _jsx(Code, { className: "font-mono", children: groupParentSlug })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: requestLeaveGroup, isLoading: loading, className: `${CLS_DELETE_LINK} w-fit`, children: "Leave group" })] }))] })), (() => {
355
+ }, paddingX: "none", paddingY: "xs", justify: "between", className: "w-full text-left group", "aria-expanded": open, children: [_jsx(Heading, { level: 3, size: "sm", weight: "semibold", color: "muted", children: "Group Settings" }), _jsx(Span, { size: "xs", className: "group-hover:text-[var(--appkit-color-text-muted)] dark:group-hover:text-[var(--appkit-color-text-faint)] transition-colors", color: "faint", children: open ? "▲" : "▼" })] }), open && (_jsxs(Stack, { gap: "sm", className: `mt-3 ${__P.p4}`, surface: "muted", rounded: "lg", border: "default", children: [!groupId && !groupParentSlug && (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", color: "muted", children: "Group related listings together \u2014 e.g. a set, bundle, or multi-part item. Parts can be sold individually but shown together." }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: startGroup, isLoading: loading, children: "Start a group" })] })), isGroupParent && groupId && (_jsxs(Stack, { gap: "md", children: [_jsxs(FormShellContext.Provider, { value: titleShellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: titleSections, values: titleDraft, onChange: (partial) => setTitleDraft((d) => ({ ...d, ...partial })), onSubmit: () => void saveTitle(), onValidationChange: () => titleForm.validate(titleDraft), schema: groupTitleSchema, openIds: titleNav.openIds, onOpenChange: titleNav.setOpenIds, submitLabel: "Save title", isLoading: loading })] }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-2", color: "muted", size: "xs", weight: "semibold", children: "Selection rules" }), _jsxs(Grid, { cols: 2, gap: "sm", children: [_jsx(FieldInput, { name: "groupMinMembers", label: "Min items in a selection", type: "number", value: ruleDraft.min, onChange: (v) => setRuleDraft((d) => ({ ...d, min: v })), hint: "Blank = no minimum" }), _jsx(FieldInput, { name: "groupMaxMembers", label: "Max items in a selection", type: "number", value: ruleDraft.max, onChange: (v) => setRuleDraft((d) => ({ ...d, max: v })), hint: "Blank = no maximum" }), _jsx(FieldInput, { name: "groupMaxUnitsPerMember", label: "Max units of any one item", type: "number", value: ruleDraft.perMember, onChange: (v) => setRuleDraft((d) => ({ ...d, perMember: v })), hint: "Blank = no per-item cap" }), _jsx(FieldInput, { name: "groupDiscountPercent", label: "Group discount (%)", type: "number", value: ruleDraft.discount, onChange: (v) => setRuleDraft((d) => ({ ...d, discount: v })), hint: "Off the selection subtotal. Applies to new cart lines; a line already in a cart keeps the price it was quoted and is re-checked at checkout." })] }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", className: "mt-2", onClick: () => void saveRules(), isLoading: loading, children: "Save rules" })] }), _jsxs(Div, { children: [_jsxs(Text, { className: "mb-2", color: "muted", size: "xs", weight: "semibold", children: ["Members (", childSlugsCount + 1, " \u2014 including this listing)"] }), _jsx(Text, { className: "mb-2", color: "faint", size: "xs", children: "Each listing has its own stock. Editing here saves them together \u2014 it does not pool them." }), loading && !children ? (_jsx(Text, { size: "xs", color: "faint", children: "Loading\u2026" })) : (_jsxs(Div, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", children: [_jsxs(Row, { align: "center", gap: "sm", padding: "y-xs", children: [_jsx(Span, { padding: "pill-2xs", weight: "semibold", className: "bg-[var(--appkit-color-primary)]/10 text-[var(--appkit-color-primary)] text-[10px]", rounded: "default", children: "Parent" }), _jsx(Text, { className: "flex-1", color: "primary", size: "sm", children: productSlug })] }), (children ?? []).filter((c) => c.id !== productId).map((child) => (_jsxs(Row, { align: "center", gap: "sm", padding: "y-xs", children: [productImageUrls(child.images)[0] ? (_jsx(Div, { className: "relative w-8 h-8 flex-shrink-0 border border-[var(--appkit-color-border)]", rounded: "full", overflow: "hidden", children: _jsx(MediaImage, { src: productImageUrls(child.images)[0], alt: child.title, size: "thumbnail" }) })) : (_jsx(Div, { className: "w-8 h-8", surface: "subtle", rounded: "full" })), _jsx(Text, { className: "flex-1 truncate", color: "primary", size: "sm", children: child.title }), child.status && child.status !== "published" ? (_jsx(Span, { size: "xs", surface: "warning-surface", className: "text-warning", padding: "pill-2xs", rounded: "default", children: child.status })) : null, (motDraft[child.id] ?? child.madeToOrder) === true ? (_jsx(Text, { size: "xs", color: "muted", children: "Made to order" })) : (_jsx(Input, { type: "number", "aria-label": `Stock for ${child.title}`, className: "w-20", value: stockDraft[child.id] ??
356
+ (child.availableQuantity ?? 0).toString(), onChange: (e) => setStockDraft((d) => ({ ...d, [child.id]: e.target.value })) })), _jsx(Checkbox, { label: "MTO", "aria-label": `Made to order for ${child.title}`, checked: motDraft[child.id] ?? child.madeToOrder === true, onChange: (e) => setMotDraft((d) => ({ ...d, [child.id]: e.target.checked })) }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => requestUnlinkChild(child.id), isLoading: loading, className: `${CLS_DELETE_LINK} text-[length:var(--appkit-text-xs)]`, children: "Unlink" })] }, child.id)))] })), (children ?? []).length > 0 && (_jsx(Button, { type: "button", variant: "secondary", size: "sm", className: "mt-2", onClick: () => void saveGroupStock(), isLoading: loading, children: "Save group stock" }))] }), _jsxs(Row, { gap: "sm", wrap: true, children: [_jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => setShowAddModal(true), children: "Add child listing" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: requestDissolveGroup, isLoading: loading, className: CLS_DELETE_LINK, children: "Dissolve group" })] })] })), !isGroupParent && groupParentSlug && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "sm", color: "muted", children: ["Part of:", " ", _jsx(Span, { weight: "medium", color: "primary", children: groupTitle ?? groupParentSlug })] }), _jsxs(Text, { size: "xs", color: "muted", children: ["Parent listing: ", _jsx(Code, { className: "font-mono", children: groupParentSlug })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: requestLeaveGroup, isLoading: loading, className: `${CLS_DELETE_LINK} w-fit`, children: "Leave group" })] }))] })), (() => {
268
357
  const content = (_jsx(AddChildContent, { addTab: addTab, setAddTab: setAddTab, createForm: createForm, setCreateForm: setCreateForm, productSlug: productSlug, storeProductsEndpoint: storeProductsEndpoint, linkTargets: linkTargets, setLinkTargets: setLinkTargets, onAddCreate: addCreateChild, onAddLink: addLinkChild, loading: loading }));
269
358
  const close = () => setShowAddModal(false);
270
359
  return (groupChildSlugs?.length ?? 0) >= 4 ? (_jsx(SideDrawer, { isOpen: showAddModal, onClose: close, title: "Add child listing", children: content })) : (_jsx(Modal, { open: showAddModal, onClose: close, title: "Add child listing", size: "lg", children: content }));
@@ -131,5 +131,5 @@ export async function PrizeDrawDetailPageView({ id, initialPrizeDraw, currentUse
131
131
  price: pricePerEntry,
132
132
  storeId: typeof p.storeId === "string" ? p.storeId : undefined,
133
133
  storeName: storeName ?? undefined,
134
- } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs("nav", { "aria-label": "Breadcrumb", className: "flex items-center gap-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] flex-wrap", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary-600 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRIZE_DRAWS), className: "hover:text-primary-600 transition-colors", children: "Prize Draws" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => (_jsx(PrizeDrawCollage, { items: items, hideWonState: true })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: "Prize Draw" }), _jsx(Span, { size: "xs", weight: "semibold", className: `inline-block ${statusClass(revealStatus)}`, padding: "pill-sm", rounded: "full", children: statusLabel(revealStatus) }), maxPerUser !== null && (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: ["Limit: ", maxPerUser, " entries per customer"] })), maxPerUser !== null && userEntriesUsed !== null && (_jsxs(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: ["You have used ", userEntriesUsed, "/", maxPerUser] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsx(Div, { className: `${__P.p4}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Entries" }), _jsxs(Text, { size: "sm", weight: "semibold", children: [current, " / ", max, " (", remaining, " left)"] })] }), revealStart && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window opens" }), _jsx(Text, { size: "sm", weight: "medium", children: revealStart.toLocaleString() })] })), revealEnd && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window closes" }), _jsx(Text, { size: "sm", weight: "medium", children: revealEnd.toLocaleString() })] }))] }) }), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined })), renderBuyBar: () => (_jsx(Div, { padding: "5", id: "prize-draw-buy-bar", border: "subtle", surface: "muted", rounded: "xl", children: _jsx(PrizeDrawEntryActions, { productId: String(product.id), productSlug: slug, title: title, pricePerEntry: pricePerEntry, currency: currency, remainingEntries: remaining, revealStatus: revealStatus, prizeGithubFileUrl: githubUrl }) })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName, brandLabel: brand, storeLabel: storeName ?? undefined })] })) }), _jsx(PrizeDrawBottomActions, { pricePerEntry: pricePerEntry, currency: currency, closed: revealStatus === "closed" || remaining === 0 })] })] }));
134
+ } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs("nav", { "aria-label": "Breadcrumb", className: "flex items-center gap-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] flex-wrap", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRIZE_DRAWS), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Prize Draws" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => (_jsx(PrizeDrawCollage, { items: items, hideWonState: true })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: "Prize Draw" }), _jsx(Span, { size: "xs", weight: "semibold", className: `inline-block ${statusClass(revealStatus)}`, padding: "pill-sm", rounded: "full", children: statusLabel(revealStatus) }), maxPerUser !== null && (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: ["Limit: ", maxPerUser, " entries per customer"] })), maxPerUser !== null && userEntriesUsed !== null && (_jsxs(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: ["You have used ", userEntriesUsed, "/", maxPerUser] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsx(Div, { className: `${__P.p4}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Entries" }), _jsxs(Text, { size: "sm", weight: "semibold", children: [current, " / ", max, " (", remaining, " left)"] })] }), revealStart && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window opens" }), _jsx(Text, { size: "sm", weight: "medium", children: revealStart.toLocaleString() })] })), revealEnd && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window closes" }), _jsx(Text, { size: "sm", weight: "medium", children: revealEnd.toLocaleString() })] }))] }) }), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined })), renderBuyBar: () => (_jsx(Div, { padding: "5", id: "prize-draw-buy-bar", border: "subtle", surface: "muted", rounded: "xl", children: _jsx(PrizeDrawEntryActions, { productId: String(product.id), productSlug: slug, title: title, pricePerEntry: pricePerEntry, currency: currency, remainingEntries: remaining, revealStatus: revealStatus, prizeGithubFileUrl: githubUrl }) })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName, brandLabel: brand, storeLabel: storeName ?? undefined })] })) }), _jsx(PrizeDrawBottomActions, { pricePerEntry: pricePerEntry, currency: currency, closed: revealStatus === "closed" || remaining === 0 })] })] }));
135
135
  }
@@ -12,7 +12,21 @@ export interface PrizeDrawEntryActionsProps {
12
12
  * Client buy panel for a prize-draw detail page (SB4-G).
13
13
  *
14
14
  * - Enter Draw → NonRefundableConsentModal (listingType="prize-draw")
15
- * - On confirm → add 1 entry to guest cart and route to /user/cart
15
+ * - On confirm → add the chosen number of entries to the cart, then /user/cart
16
16
  * - "View RNG source" link → prizeGithubFileUrl
17
+ *
18
+ * 🛑 THIS FLOW DID NOT WORK AT ALL UNTIL 2026-10-19, and the docstring above
19
+ * described the behaviour it was supposed to have. `handleConfirm` pushed to
20
+ * `/checkout?directItem=<slug>&type=prize-draw` — and NOTHING anywhere reads
21
+ * `directItem` (CheckoutRouteClient reads only `coupon` and `lane`), and
22
+ * nothing added to the cart. A buyer pressing "Enter draw" landed on a
23
+ * checkout of whatever was already in their basket, with no entry in it.
24
+ * Buying an entry was impossible.
25
+ *
26
+ * 🛑 MULTI-ENTRY NEEDED NO SERVER WORK. `prizeDrawRule.splitMultipleOrders`
27
+ * already chunks a quantity into batches of PRIZE_DRAW_MAX_REVEALS_PER_ORDER
28
+ * (7 entries → [3,3,1]), `enforceMaxPerUserForCart` already caps per buyer, and
29
+ * the rule's preflight already checks `prizeMaxEntries`. The whole server half
30
+ * was written and unreachable because the client never sent a quantity.
17
31
  */
18
32
  export declare function PrizeDrawEntryActions({ productId, productSlug, title, pricePerEntry, currency, remainingEntries, revealStatus, prizeGithubFileUrl, }: PrizeDrawEntryActionsProps): import("react").JSX.Element;