@mohasinac/appkit 4.53.2 → 4.54.1

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 (350) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/server/features/auth/capabilities.js +7 -5
  3. package/dist/_internal/server/features/checkout/actions.d.ts +22 -6
  4. package/dist/_internal/server/features/checkout/actions.js +96 -51
  5. package/dist/_internal/server/features/checkout/index.d.ts +1 -1
  6. package/dist/_internal/server/features/checkout/index.js +1 -1
  7. package/dist/_internal/server/features/checkout/supplier-identity.js +28 -6
  8. package/dist/_internal/server/features/orders/adapters.js +1 -3
  9. package/dist/_internal/server/features/products/listing-tab-counts.js +81 -25
  10. package/dist/_internal/server/features/search/index-build/sources.js +43 -2
  11. package/dist/_internal/server/features/site-settings/adapters.js +1 -1
  12. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  13. package/dist/_internal/server/features/stores/adapters.js +0 -1
  14. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +2 -5
  15. package/dist/_internal/server/jobs/core/jobRunners.js +0 -10
  16. package/dist/_internal/server/jobs/core/onOrderCreate.js +10 -104
  17. package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +0 -2
  18. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +0 -1
  19. package/dist/_internal/server/jobs/core/onScamReportCreate.js +0 -1
  20. package/dist/_internal/server/jobs/core/onScamReportRejected.js +0 -1
  21. package/dist/_internal/server/jobs/core/onScamReportVerified.js +0 -1
  22. package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +0 -1
  23. package/dist/_internal/server/jobs/core/pendingOrderTimeout.js +0 -1
  24. package/dist/_internal/server/notifications/channel-health.d.ts +7 -3
  25. package/dist/_internal/server/notifications/channel-health.js +2 -2
  26. package/dist/_internal/server/notifications/send-guard.d.ts +1 -1
  27. package/dist/_internal/server/notifications/send-guard.js +2 -4
  28. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  29. package/dist/_internal/shared/actions/action-registry.js +128 -28
  30. package/dist/_internal/shared/checkout/order-math.d.ts +1 -1
  31. package/dist/_internal/shared/checkout/order-math.js +1 -1
  32. package/dist/_internal/shared/features/categories/bundle-copy.d.ts +1 -1
  33. package/dist/_internal/shared/features/checkout/config.d.ts +0 -2
  34. package/dist/_internal/shared/features/checkout/config.js +0 -2
  35. package/dist/_internal/shared/features/messaging/config.d.ts +7 -5
  36. package/dist/_internal/shared/features/messaging/config.js +1 -4
  37. package/dist/_internal/shared/features/site-settings/fees.d.ts +0 -2
  38. package/dist/_internal/shared/features/site-settings/fees.js +0 -2
  39. package/dist/_internal/shared/features/site-settings/supplier-of-record.d.ts +56 -0
  40. package/dist/_internal/shared/features/site-settings/supplier-of-record.js +57 -0
  41. package/dist/_internal/shared/fees/calculator.d.ts +0 -8
  42. package/dist/_internal/shared/fees/calculator.js +0 -7
  43. package/dist/_internal/shared/listing-types/tiers.d.ts +94 -0
  44. package/dist/_internal/shared/listing-types/tiers.js +150 -0
  45. package/dist/cli/index.js +0 -2
  46. package/dist/client.d.ts +25 -4
  47. package/dist/client.js +36 -5
  48. package/dist/constants/api-endpoints.d.ts +21 -15
  49. package/dist/constants/api-endpoints.js +7 -8
  50. package/dist/constants/field-names.d.ts +1 -0
  51. package/dist/constants/field-names.js +1 -0
  52. package/dist/constants/notification-types.d.ts +1 -1
  53. package/dist/constants/notification-types.js +1 -1
  54. package/dist/core/integration-keys.d.ts +5 -5
  55. package/dist/core/integration-keys.js +21 -10
  56. package/dist/features/about/components/FeesView.js +0 -6
  57. package/dist/features/about/components/GuidesHubView.js +15 -2
  58. package/dist/features/account/components/NotificationPreferencesPanel.js +3 -4
  59. package/dist/features/account/components/UserOrdersView.js +26 -7
  60. package/dist/features/account/types/index.d.ts +0 -2
  61. package/dist/features/addresses/utils/postal-autofill.d.ts +39 -0
  62. package/dist/features/addresses/utils/postal-autofill.js +54 -0
  63. package/dist/features/admin/actions/notification-actions.d.ts +2 -8
  64. package/dist/features/admin/actions/notification-actions.js +9 -104
  65. package/dist/features/admin/components/AdminGuideHubView.js +1 -8
  66. package/dist/features/admin/components/AdminOrderEditorView.d.ts +1 -1
  67. package/dist/features/admin/components/AdminOrdersView.js +0 -2
  68. package/dist/features/admin/components/AdminSectionsView.js +3 -2
  69. package/dist/features/admin/components/AdminSiteSettingsView.js +22 -68
  70. package/dist/features/admin/components/AdminStoreEditorView.js +72 -34
  71. package/dist/features/admin/components/OwnerQuickActions.d.ts +29 -0
  72. package/dist/features/admin/components/OwnerQuickActions.js +142 -0
  73. package/dist/features/admin/components/sections/adminSectionsTypes.js +13 -4
  74. package/dist/features/admin/components/sections/icon-options.d.ts +21 -0
  75. package/dist/features/admin/components/sections/icon-options.js +23 -0
  76. package/dist/features/admin/components/sections/sectionTemplates.js +4 -1
  77. package/dist/features/admin/constants/site-settings-tabs.d.ts +7 -12
  78. package/dist/features/admin/constants/site-settings-tabs.js +6 -12
  79. package/dist/features/admin/repository/site-settings.repository.js +1 -1
  80. package/dist/features/admin/schemas/admin-editor-forms.d.ts +1 -1
  81. package/dist/features/admin/schemas/admin-editor-forms.js +4 -1
  82. package/dist/features/admin/schemas/admin-user-form.d.ts +0 -61
  83. package/dist/features/admin/schemas/admin-user-form.js +3 -44
  84. package/dist/features/admin/schemas/firestore.d.ts +28 -54
  85. package/dist/features/admin/schemas/firestore.js +40 -18
  86. package/dist/features/auctions/components/AuctionDetailPageView.js +4 -3
  87. package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +11 -1
  88. package/dist/features/auctions/components/MarketplaceAuctionCard.js +9 -2
  89. package/dist/features/auth/checkout-value-otp.d.ts +1 -1
  90. package/dist/features/auth/checkout-value-otp.js +1 -1
  91. package/dist/features/auth/permissions/constants.d.ts +29 -2
  92. package/dist/features/auth/permissions/constants.js +67 -1
  93. package/dist/features/blog/components/BlogFeaturedCard.d.ts +11 -1
  94. package/dist/features/blog/components/BlogFeaturedCard.js +9 -2
  95. package/dist/features/cart/components/CartDrawer.js +3 -2
  96. package/dist/features/cart/components/CartGroupLineRow.js +2 -1
  97. package/dist/features/cart/components/CartPriceBreakdown.d.ts +0 -2
  98. package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
  99. package/dist/features/cart/components/StoreAddonsPicker.d.ts +2 -5
  100. package/dist/features/cart/components/StoreAddonsPicker.js +4 -5
  101. package/dist/features/cart/hooks/useAddToCart.js +10 -5
  102. package/dist/features/cart/repository/cart.repository.js +1 -2
  103. package/dist/features/cart/schemas/firestore.d.ts +1 -2
  104. package/dist/features/cart/utils/pending-ops.d.ts +11 -1
  105. package/dist/features/cart/utils/pending-ops.js +14 -2
  106. package/dist/features/categories/components/BrandDetailPageView.js +2 -1
  107. package/dist/features/categories/components/BundleDetailView.js +2 -1
  108. package/dist/features/categories/components/CategoryDetailPageView.js +2 -1
  109. package/dist/features/categories/components/CategoryGrid.d.ts +11 -1
  110. package/dist/features/categories/components/CategoryGrid.js +9 -2
  111. package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +10 -5
  112. package/dist/features/checkout/actions/checkout-value-otp-actions.js +2 -88
  113. package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -1
  114. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +2 -1
  115. package/dist/features/events/components/EventCard.d.ts +11 -1
  116. package/dist/features/events/components/EventCard.js +9 -2
  117. package/dist/features/grouped/components/GroupedListingDetailView.js +3 -1
  118. package/dist/features/homepage/components/CTABannerSection.d.ts +3 -1
  119. package/dist/features/homepage/components/CTABannerSection.js +6 -3
  120. package/dist/features/homepage/components/HeroCarousel.js +8 -6
  121. package/dist/features/homepage/components/MarketplaceHomepageView.js +33 -7
  122. package/dist/features/homepage/components/SectionCarousel.js +13 -5
  123. package/dist/features/homepage/components/SecurityHighlightsSection.d.ts +8 -3
  124. package/dist/features/homepage/components/SecurityHighlightsSection.js +2 -2
  125. package/dist/features/homepage/components/StatsCounterSection.d.ts +9 -4
  126. package/dist/features/homepage/components/StatsCounterSection.js +2 -2
  127. package/dist/features/homepage/components/TestimonialsCarousel.js +1 -1
  128. package/dist/features/homepage/components/TrustFeaturesSection.d.ts +18 -4
  129. package/dist/features/homepage/components/TrustFeaturesSection.js +5 -5
  130. package/dist/features/homepage/components/WelcomeSection.d.ts +13 -2
  131. package/dist/features/homepage/components/WelcomeSection.js +3 -3
  132. package/dist/features/homepage/components/WhatsAppCommunitySection.d.ts +28 -0
  133. package/dist/features/homepage/components/WhatsAppCommunitySection.js +42 -6
  134. package/dist/features/homepage/components/WhatsAppGlyph.d.ts +20 -0
  135. package/dist/features/homepage/components/WhatsAppGlyph.js +20 -0
  136. package/dist/features/homepage/constants/hero-carousel-styles.d.ts +8 -6
  137. package/dist/features/homepage/constants/hero-carousel-styles.js +8 -6
  138. package/dist/features/homepage/constants/section-copy.d.ts +12 -10
  139. package/dist/features/homepage/constants/section-copy.js +28 -6
  140. package/dist/features/homepage/constants/stat-icons.d.ts +12 -0
  141. package/dist/features/homepage/constants/stat-icons.js +17 -0
  142. package/dist/features/homepage/constants/trust-icon-aliases.d.ts +8 -0
  143. package/dist/features/homepage/constants/trust-icon-aliases.js +75 -0
  144. package/dist/features/homepage/lib/section-defaults.d.ts +16 -1
  145. package/dist/features/homepage/lib/section-defaults.js +24 -4
  146. package/dist/features/homepage/lib/section-renderer.js +48 -8
  147. package/dist/features/homepage/schemas/firestore.d.ts +18 -1
  148. package/dist/features/layout/AppLayoutShell.js +2 -1
  149. package/dist/features/layout/BottomActions.d.ts +1 -0
  150. package/dist/features/layout/BottomActions.js +330 -0
  151. package/dist/features/layout/BottomChrome.d.ts +45 -0
  152. package/dist/features/layout/BottomChrome.js +5 -17
  153. package/dist/features/layout/SideActionDock.js +36 -13
  154. package/dist/features/layout/index.d.ts +2 -7
  155. package/dist/features/layout/index.js +7 -7
  156. package/dist/features/layout/useIsDesktop.d.ts +36 -0
  157. package/dist/features/layout/useIsDesktop.js +48 -0
  158. package/dist/features/live/components/LiveItemDetailPageView.js +2 -1
  159. package/dist/features/orders/actions/refund-actions.js +0 -1
  160. package/dist/features/orders/components/OrderAddonBadges.d.ts +1 -3
  161. package/dist/features/orders/components/OrderAddonBadges.js +3 -4
  162. package/dist/features/orders/components/OrderPaymentSummary.js +15 -2
  163. package/dist/features/orders/components/YourPurchaseStrip.d.ts +24 -0
  164. package/dist/features/orders/components/YourPurchaseStrip.js +77 -0
  165. package/dist/features/orders/components/index.d.ts +2 -1
  166. package/dist/features/orders/components/index.js +1 -1
  167. package/dist/features/orders/repository/orders.repository.d.ts +0 -8
  168. package/dist/features/orders/repository/orders.repository.js +0 -2
  169. package/dist/features/orders/schemas/firestore.d.ts +17 -9
  170. package/dist/features/orders/types/index.d.ts +2 -5
  171. package/dist/features/orders/utils/order-announcement-message.d.ts +18 -0
  172. package/dist/features/orders/utils/order-announcement-message.js +16 -0
  173. package/dist/features/orders/utils/payment-proof-message.d.ts +20 -0
  174. package/dist/features/orders/utils/payment-proof-message.js +17 -0
  175. package/dist/features/parts/components/PartDetailPageView.js +96 -1
  176. package/dist/features/parts/schemas/firestore.d.ts +247 -0
  177. package/dist/features/parts/schemas/firestore.js +6 -6
  178. package/dist/features/pre-orders/components/MarketplacePreorderCard.d.ts +11 -1
  179. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +10 -3
  180. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +4 -3
  181. package/dist/features/products/api/[id]/route.js +8 -1
  182. package/dist/features/products/api/route.js +16 -4
  183. package/dist/features/products/components/AvailabilityScopeHint.js +1 -1
  184. package/dist/features/products/components/InteractiveProductCard.d.ts +7 -1
  185. package/dist/features/products/components/InteractiveProductCard.js +16 -4
  186. package/dist/features/products/components/MarketplaceBundleCard.d.ts +11 -1
  187. package/dist/features/products/components/MarketplaceBundleCard.js +9 -2
  188. package/dist/features/products/components/MarketplacePrizeDrawCard.d.ts +11 -1
  189. package/dist/features/products/components/MarketplacePrizeDrawCard.js +9 -2
  190. package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -1
  191. package/dist/features/products/components/ProductDetailPageView.js +27 -2
  192. package/dist/features/products/components/ProductGrid.d.ts +14 -1
  193. package/dist/features/products/components/ProductGrid.js +22 -5
  194. package/dist/features/products/components/ProductsIndexListing.js +2 -2
  195. package/dist/features/reviews/api/[id]/route.js +83 -23
  196. package/dist/features/reviews/components/PurchaseReviewAffordance.d.ts +6 -0
  197. package/dist/features/reviews/components/PurchaseReviewAffordance.js +42 -0
  198. package/dist/features/reviews/components/ReviewFormDrawer.d.ts +34 -0
  199. package/dist/features/reviews/components/ReviewFormDrawer.js +127 -0
  200. package/dist/features/reviews/components/index.d.ts +4 -0
  201. package/dist/features/reviews/components/index.js +2 -0
  202. package/dist/features/scams/schemas/index.d.ts +10 -10
  203. package/dist/features/search/action-index/settings-entries.js +0 -9
  204. package/dist/features/seller/actions/seller-actions.d.ts +1 -1
  205. package/dist/features/seller/actions/seller-actions.js +49 -3
  206. package/dist/features/seller/components/SellerPayoutSettingsView.js +30 -2
  207. package/dist/features/seller/components/SellerProductShell.d.ts +17 -1
  208. package/dist/features/seller/hooks/useStoreListingEntitlements.d.ts +8 -0
  209. package/dist/features/seller/hooks/useStoreListingEntitlements.js +70 -0
  210. package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +1 -6
  211. package/dist/features/store-extensions/repository/store-extensions.repositories.js +1 -10
  212. package/dist/features/store-extensions/schemas/firestore.d.ts +1 -17
  213. package/dist/features/store-extensions/schemas/firestore.js +0 -10
  214. package/dist/features/stores/components/InteractiveStoreCard.d.ts +11 -1
  215. package/dist/features/stores/components/InteractiveStoreCard.js +9 -2
  216. package/dist/features/stores/components/StoreCapabilitiesGuideView.js +0 -1
  217. package/dist/features/stores/components/StoreGuideHubView.js +2 -8
  218. package/dist/features/stores/components/StoreSettingsGuideView.js +2 -6
  219. package/dist/features/stores/components/index.d.ts +0 -2
  220. package/dist/features/stores/components/index.js +0 -1
  221. package/dist/features/stores/repository/store.repository.d.ts +1 -26
  222. package/dist/features/stores/repository/store.repository.js +5 -60
  223. package/dist/features/stores/schemas/firestore.d.ts +6 -25
  224. package/dist/features/stores/schemas/firestore.js +6 -3
  225. package/dist/features/stores/schemas/index.d.ts +1 -87
  226. package/dist/features/stores/schemas/index.js +10 -15
  227. package/dist/features/tester/seed-data/authored/admin__site-system.js +0 -48
  228. package/dist/features/tester/seed-data/authored/buying__buying-checkout.js +0 -40
  229. package/dist/features/tester/seed-data/authored/selling__quick-add-minimum-details.js +0 -16
  230. package/dist/features/tester/seed-data/authored/selling__seller-guide.js +1 -1
  231. package/dist/features/tester/seed-data/authored/selling__seller-marketing-extras.js +0 -79
  232. package/dist/features/tester/seed-data/authored/selling__seller-orders.js +1 -1
  233. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +415 -69
  234. package/dist/index.d.ts +20 -32
  235. package/dist/index.js +34 -43
  236. package/dist/next/routing/route-map.d.ts +2 -8
  237. package/dist/next/routing/route-map.js +21 -4
  238. package/dist/providers/db-firebase/base.repository.d.ts +1 -1
  239. package/dist/providers/db-firebase/base.repository.js +1 -1
  240. package/dist/providers/payment-razorpay/index.d.ts +125 -0
  241. package/dist/providers/payment-razorpay/index.js +232 -0
  242. package/dist/providers/payment-razorpay/signature.d.ts +53 -0
  243. package/dist/providers/payment-razorpay/signature.js +71 -0
  244. package/dist/providers/shipping-shiprocket/index.d.ts +98 -0
  245. package/dist/providers/shipping-shiprocket/index.js +356 -0
  246. package/dist/providers.d.ts +3 -0
  247. package/dist/providers.js +3 -0
  248. package/dist/react/hooks/useCanSeePrices.d.ts +0 -27
  249. package/dist/react/hooks/useCanSeePrices.js +25 -0
  250. package/dist/repositories/index.d.ts +1 -1
  251. package/dist/repositories/index.js +1 -1
  252. package/dist/schemas/registry.d.ts +10 -115
  253. package/dist/schemas/registry.js +1 -2
  254. package/dist/security/pii-encrypt.js +1 -1
  255. package/dist/seed/_helpers/category-forest.js +27 -0
  256. package/dist/seed/_helpers/category-parts-glossary-map.js +1816 -88
  257. package/dist/seed/_helpers/category-parts.d.ts +1 -1
  258. package/dist/seed/_helpers/category-parts.js +13399 -3019
  259. package/dist/seed/carousel-slides-seed-data.js +47 -10
  260. package/dist/seed/categories-seed-data.js +48 -6
  261. package/dist/seed/faq-seed-data.js +2 -2
  262. package/dist/seed/homepage-sections-seed-data.js +286 -54
  263. package/dist/seed/index.d.ts +1 -1
  264. package/dist/seed/index.js +1 -1
  265. package/dist/seed/manifest.d.ts +1 -1
  266. package/dist/seed/manifest.js +1 -2
  267. package/dist/seed/model-composition-seed-data.js +6 -8
  268. package/dist/seed/part-glossary/chunk0.d.ts +9 -0
  269. package/dist/seed/part-glossary/chunk0.js +35448 -0
  270. package/dist/seed/part-glossary/chunk1.d.ts +9 -0
  271. package/dist/seed/part-glossary/chunk1.js +40494 -0
  272. package/dist/seed/part-glossary/chunk2.d.ts +9 -0
  273. package/dist/seed/part-glossary/chunk2.js +51242 -0
  274. package/dist/seed/part-glossary/chunk3.d.ts +9 -0
  275. package/dist/seed/part-glossary/chunk3.js +53337 -0
  276. package/dist/seed/part-glossary/chunk4.d.ts +9 -0
  277. package/dist/seed/part-glossary/chunk4.js +51454 -0
  278. package/dist/seed/part-glossary-seed-data.d.ts +1 -1
  279. package/dist/seed/part-glossary-seed-data.js +23 -47483
  280. package/dist/seed/products-catalogue-seed-data.d.ts +5 -5
  281. package/dist/seed/products-catalogue-seed-data.js +63936 -13803
  282. package/dist/seed/site-settings-seed-data.js +34 -35
  283. package/dist/seed/store-extensions-seed-data.d.ts +1 -2
  284. package/dist/seed/store-extensions-seed-data.js +1 -23
  285. package/dist/seed/stores-seed-data.js +0 -1
  286. package/dist/server-entry.d.ts +1 -1
  287. package/dist/server-entry.js +1 -1
  288. package/dist/server.d.ts +8 -10
  289. package/dist/server.js +16 -22
  290. package/dist/styles.css +392 -43
  291. package/dist/tailwind-utilities.css +1 -1
  292. package/dist/tokens/themes/default-dark.js +1 -1
  293. package/dist/tokens/themes/default-light.js +2 -1
  294. package/dist/tokens/tokens.css +6 -2
  295. package/dist/ui/components/Button.d.ts +11 -0
  296. package/dist/ui/components/Button.js +11 -0
  297. package/dist/ui/components/Button.style.css +29 -0
  298. package/dist/ui/components/CarouselNav.d.ts +57 -0
  299. package/dist/ui/components/CarouselNav.js +25 -0
  300. package/dist/ui/components/CarouselNav.style.css +265 -0
  301. package/dist/ui/components/GatedPrice.js +30 -3
  302. package/dist/ui/components/GatedPrice.style.css +43 -40
  303. package/dist/ui/components/HorizontalScroller.d.ts +12 -1
  304. package/dist/ui/components/HorizontalScroller.js +43 -3
  305. package/dist/ui/components/ImageMosaic.d.ts +8 -1
  306. package/dist/ui/components/ImageMosaic.js +3 -2
  307. package/dist/ui/components/ImageMosaic.style.css +22 -0
  308. package/dist/ui/components/Layout.style.css +6 -0
  309. package/dist/ui/components/Semantic.d.ts +3 -2
  310. package/dist/ui/components/Semantic.js +1 -0
  311. package/dist/ui/components/Typography.d.ts +2 -0
  312. package/dist/ui/components/Typography.js +2 -0
  313. package/dist/ui/components/Typography.style.css +19 -0
  314. package/dist/ui/components/index.style.css +1 -0
  315. package/dist/ui/index.d.ts +3 -1
  316. package/dist/ui/index.js +4 -0
  317. package/package.json +3 -2
  318. package/dist/_internal/server/jobs/core/whatsappCatalogImport.d.ts +0 -27
  319. package/dist/_internal/server/jobs/core/whatsappCatalogImport.js +0 -172
  320. package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +0 -35
  321. package/dist/_internal/server/jobs/core/whatsappNotify.js +0 -140
  322. package/dist/features/admin/components/AdminWhatsAppGuideView.d.ts +0 -2
  323. package/dist/features/admin/components/AdminWhatsAppGuideView.js +0 -35
  324. package/dist/features/orders/components/MarketplaceOrderCard.d.ts +0 -36
  325. package/dist/features/orders/components/MarketplaceOrderCard.js +0 -57
  326. package/dist/features/stores/components/StoreWhatsAppGuideView.d.ts +0 -3
  327. package/dist/features/stores/components/StoreWhatsAppGuideView.js +0 -12
  328. package/dist/features/whatsapp-bot/api/send-status/route.d.ts +0 -12
  329. package/dist/features/whatsapp-bot/api/send-status/route.js +0 -38
  330. package/dist/features/whatsapp-bot/api/webhook/route.d.ts +0 -12
  331. package/dist/features/whatsapp-bot/api/webhook/route.js +0 -43
  332. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +0 -6
  333. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +0 -270
  334. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.d.ts +0 -9
  335. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +0 -7
  336. package/dist/features/whatsapp-bot/components/index.d.ts +0 -3
  337. package/dist/features/whatsapp-bot/components/index.js +0 -2
  338. package/dist/features/whatsapp-bot/helpers/payment-proof-message.d.ts +0 -14
  339. package/dist/features/whatsapp-bot/helpers/payment-proof-message.js +0 -17
  340. package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +0 -52
  341. package/dist/features/whatsapp-bot/helpers/whatsapp.js +0 -283
  342. package/dist/features/whatsapp-bot/index.d.ts +0 -3
  343. package/dist/features/whatsapp-bot/index.js +0 -3
  344. package/dist/features/whatsapp-bot/manifest.d.ts +0 -2
  345. package/dist/features/whatsapp-bot/manifest.js +0 -13
  346. package/dist/features/whatsapp-bot/messages/en.json +0 -7
  347. package/dist/features/whatsapp-bot/server.d.ts +0 -7
  348. package/dist/features/whatsapp-bot/server.js +0 -7
  349. package/dist/features/whatsapp-bot/types/index.d.ts +0 -126
  350. package/dist/features/whatsapp-bot/types/index.js +0 -1
@@ -1,6 +1,49 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Section, Container, Stack, Row, Div, Heading, Text, Span, TextLink, Badge, } from "../../../ui/index.js";
3
+ import { SpecTable } from "../../../ui/components/SpecTable.js";
3
4
  import { PART_SLOT_LABEL, } from "../schemas/firestore.js";
5
+ /**
6
+ * The buyer-facing label for each WBO tournament format, in the order a
7
+ * player cares about: the formats they are most likely to enter first.
8
+ *
9
+ * 🛑 A `Record` over the format keys rather than a derived title-case, because
10
+ * "Burst Standard" is not `burstStandard` title-cased and "GT" must not become
11
+ * "Gt". 1,380 of 2,488 parts carry a legality ruling.
12
+ */
13
+ const WBO_FORMAT_LABEL = {
14
+ burstStandard: "Burst Standard",
15
+ burstLimited: "Burst Limited",
16
+ burstClassic: "Burst Classic",
17
+ burstGT: "Burst GT",
18
+ burstSparking: "Burst Sparking",
19
+ burstDynamite: "Burst Dynamite",
20
+ xStandard: "X Standard",
21
+ zeroG: "Zero-G",
22
+ metalFight: "Metal Fight",
23
+ plastic: "Plastic",
24
+ hms: "HMS",
25
+ };
26
+ /**
27
+ * The rating axes, in a fixed order.
28
+ *
29
+ * 🛑 ORDERED EXPLICITLY, never by `Object.keys`. Firestore returns map fields
30
+ * alphabetically, which would put "burst" and "dash" ahead of "attack" and
31
+ * reorder the table per part depending on which axes that part happens to
32
+ * carry. A reader comparing two parts needs the same rows in the same places.
33
+ */
34
+ const RATING_AXES = [
35
+ { key: "attack", label: "Attack" },
36
+ { key: "defense", label: "Defense" },
37
+ { key: "stamina", label: "Stamina" },
38
+ { key: "weight", label: "Weight" },
39
+ { key: "speed", label: "Speed" },
40
+ { key: "burst", label: "Burst" },
41
+ { key: "burstResistance", label: "Burst resistance" },
42
+ { key: "dash", label: "Dash" },
43
+ { key: "power", label: "Power" },
44
+ { key: "height", label: "Height" },
45
+ { key: "runTime", label: "Run time" },
46
+ ];
4
47
  export function PartDetailPageView({ part, usedBy, listings, listingsTruncated, allListingsHref, exactListings = [], exactListingsTruncated, hrefForModel, hrefForClassification, }) {
5
48
  const classification = PART_SLOT_LABEL[part.slot] ?? part.classification ?? part.slot;
6
49
  /*
@@ -10,7 +53,59 @@ export function PartDetailPageView({ part, usedBy, listings, listingsTruncated,
10
53
  * imply we looked it up and found none.
11
54
  */
12
55
  const aliases = (part.aliases ?? []).filter((a) => a && a !== part.name && a !== part.code);
13
- return (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "lg", children: _jsxs(Stack, { gap: "lg", className: "min-w-0", children: [_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsx(Text, { size: "sm", color: "muted", children: _jsx(TextLink, { href: hrefForClassification(part.slot), children: classification }) }), _jsx(Heading, { level: 1, children: part.name }), _jsxs(Row, { gap: "xs", wrap: true, align: "center", className: "min-w-0", children: [part.code && _jsx(Badge, { variant: "info", children: part.code }), typeof part.weightG === "number" && (_jsxs(Badge, { variant: "default", children: [part.weightG, " g"] }))] }), aliases.length > 0 && (_jsxs(Text, { size: "sm", color: "muted", children: ["Also written: ", aliases.join(" · ")] }))] }), _jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: usedBy.length > 0
56
+ /*
57
+ * ── THE SPEC TABLE ────────────────────────────────────────────────────────
58
+ *
59
+ * `specs[]` is the extracted infobox (2,144 of 2,488 parts carry one), and
60
+ * the four rows prepended here are the fields promoted OFF that array
61
+ * because something queries them — so they would otherwise either be
62
+ * missing from the table or duplicated inside it.
63
+ *
64
+ * 🛑 De-duplicated against the extracted rows by label. `Classification`,
65
+ * `System` and `Series` all appear in `specs[]` as well, so prepending them
66
+ * blindly printed each one twice.
67
+ */
68
+ const promoted = [
69
+ { name: "Classification", value: classification },
70
+ ...(part.system ? [{ name: "System", value: part.system }] : []),
71
+ ...(part.series ? [{ name: "Series", value: part.series }] : []),
72
+ ...(typeof part.weightG === "number"
73
+ ? [{ name: "Weight", value: String(part.weightG), unit: "g" }]
74
+ : []),
75
+ ...(part.spinDirection ? [{ name: "Spin direction", value: part.spinDirection }] : []),
76
+ ...(part.partType ? [{ name: "Type", value: part.partType }] : []),
77
+ ...(part.hasbroCode ? [{ name: "Hasbro code", value: part.hasbroCode }] : []),
78
+ ];
79
+ const promotedLabels = new Set(promoted.map((r) => r.name.toLowerCase()));
80
+ const specRows = [
81
+ ...promoted,
82
+ ...(part.specs ?? [])
83
+ .filter((s) => s.label && s.value && !promotedLabels.has(s.label.toLowerCase()))
84
+ .map((s) => ({ name: s.label, value: s.value })),
85
+ ];
86
+ /*
87
+ * ── RATINGS ───────────────────────────────────────────────────────────────
88
+ *
89
+ * 🛑 RENDER THE NORMALISED VECTOR, NEVER THE RAW ONE. Two scales live in
90
+ * these fields — the Beyblade X lines rate 0-100 and everything before them
91
+ * rates 0-6 (with a handful of Burst systems peaking at 7 or 8) — so a raw
92
+ * "Attack 3" means half of maximum for a Burst part and almost nothing for
93
+ * an X part. `ratingsNormalised` is the only comparable form, and the source
94
+ * scale is stated under the bars so the number is not passed off as the
95
+ * wiki's own.
96
+ */
97
+ const ratings = part.ratingsNormalised ?? null;
98
+ const ratingRows = ratings
99
+ ? RATING_AXES.map((a) => ({ ...a, value: ratings[a.key] })).filter((r) => typeof r.value === "number")
100
+ : [];
101
+ /* Only the formats this part is actually ruled on — see the note below. */
102
+ const legalFormats = Object.entries(part.wboLegality ?? {}).filter(([, v]) => typeof v === "boolean");
103
+ return (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "lg", children: _jsxs(Stack, { gap: "lg", className: "min-w-0", children: [_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsx(Text, { size: "sm", color: "muted", children: _jsx(TextLink, { href: hrefForClassification(part.slot), children: classification }) }), _jsx(Heading, { level: 1, children: part.name }), _jsxs(Row, { gap: "xs", wrap: true, align: "center", className: "min-w-0", children: [part.code && _jsx(Badge, { variant: "info", children: part.code }), typeof part.weightG === "number" && (_jsxs(Badge, { variant: "default", children: [part.weightG, " g"] }))] }), aliases.length > 0 && (_jsxs(Text, { size: "sm", color: "muted", children: ["Also written: ", aliases.join(" · ")] }))] }), specRows.length > 1 && (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "Specifications" }), _jsx(SpecTable, { rows: specRows })] })), ratingRows.length > 0 && (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "Ratings" }), _jsx(SpecTable, { rows: ratingRows.map((r) => ({
104
+ name: r.label,
105
+ value: `${r.value} / 100`,
106
+ })) }), _jsxs(Text, { size: "xs", color: "faint", children: ["Rescaled to 0\u2013100 for comparison across generations", typeof part.ratingScale === "number"
107
+ ? `; rated 0–${part.ratingScale} at source`
108
+ : "", "."] })] })), legalFormats.length > 0 && (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "Tournament legality" }), _jsx(Row, { gap: "xs", wrap: true, align: "center", className: "min-w-0", children: legalFormats.map(([format, allowed]) => (_jsxs(Badge, { variant: allowed ? "success" : "danger", children: [WBO_FORMAT_LABEL[format] ?? format, allowed ? " ✓" : " ✗"] }, format))) }), _jsx(Text, { size: "xs", color: "faint", children: "World Beyblade Organization formats this part is ruled on. A format not listed has no ruling for it." })] })), _jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: usedBy.length > 0
14
109
  ? `Used by ${usedBy.length} ${usedBy.length === 1 ? "model" : "models"}`
15
110
  : "Used by" }), usedBy.length === 0 ? (_jsx(Text, { color: "muted", size: "sm", children: "No model in our database records this part yet." })) : (_jsx(Stack, { gap: "xs", className: "min-w-0", children: usedBy.map((m) => (_jsxs(Row, { gap: "sm", align: "center", padding: "inline", surface: "card", rounded: "md", border: "subtle", className: "min-w-0", children: [_jsxs(Div, { className: "flex-1 min-w-0", children: [m.categoryId ? (_jsx(TextLink, { href: hrefForModel(m.categoryId), children: m.name })) : (
16
111
  /* 49 of 259 compositions did not join to a category.
@@ -210,6 +210,67 @@ export interface PartColourway {
210
210
  /** The bey this colourway was released in. */
211
211
  inBey: string;
212
212
  }
213
+ /**
214
+ * The infobox stat grid, as NUMBERS.
215
+ *
216
+ * 🛑 This is a deliberate second representation of the `Stats` row in
217
+ * `specs[]`, not a mirror to keep in sync. The row is a RENDERING — one line,
218
+ * because six rows of single digits crowd out every real specification on a
219
+ * part page — and this is the VALUE, which is what a sort, a filter, a
220
+ * side-by-side comparison and the simulation all need. Neither can do the
221
+ * other's job, and both are produced by one parse in one pass, so there is no
222
+ * second write path to forget (unlike Root Cause #42's shape).
223
+ *
224
+ * Every key is OPTIONAL because the source states them per part: a page that
225
+ * gives Attack and Burst and nothing else yields exactly those two. A missing
226
+ * key is "not stated", never zero — `weight: 0` is a real rating on a
227
+ * featherweight disc and must stay distinguishable from silence.
228
+ *
229
+ * The scale is 0–6 and is the wiki's own editorial judgement, not a
230
+ * measurement. It is useful for comparison between parts of the same slot and
231
+ * should not be presented as a physical quantity.
232
+ */
233
+ export interface PartRatings {
234
+ attack?: number;
235
+ defense?: number;
236
+ stamina?: number;
237
+ weight?: number;
238
+ speed?: number;
239
+ burst?: number;
240
+ dash?: number;
241
+ burstResistance?: number;
242
+ power?: number;
243
+ height?: number;
244
+ runTime?: number;
245
+ }
246
+ /**
247
+ * WBO tournament-format legality.
248
+ *
249
+ * 🛑 A format the source does not rule on is ABSENT, never `false`. Three
250
+ * states exist — legal, banned, and not stated — and collapsing the third into
251
+ * the second would publish "banned in Burst Classic" as a fact about every
252
+ * part whose page simply never mentions Burst Classic.
253
+ */
254
+ export interface PartWboLegality {
255
+ burst?: boolean;
256
+ burstGt?: boolean;
257
+ burstLimited?: boolean;
258
+ burstClassic?: boolean;
259
+ xStandard?: boolean;
260
+ xMasters?: boolean;
261
+ xOpen?: boolean;
262
+ mfbStandard?: boolean;
263
+ mfbLimited?: boolean;
264
+ mfbZeroG?: boolean;
265
+ mfb4d?: boolean;
266
+ }
267
+ /**
268
+ * OUR role vocabulary — 10 values covering all 59 slots.
269
+ *
270
+ * Mirrors `PART_ROLE` in `scripts/lib/part-slot-vocab.mjs`, which is the
271
+ * source. A slot missing from that map yields no `role` rather than a guess.
272
+ */
273
+ export type PartRole = "chip" | "ring" | "wheel" | "weight" | "base" | "blade" | "track" | "tip" | "gear" | "support";
213
274
  export interface PartGlossaryDocument extends BaseDocument {
214
275
  /** `${slot}--${codeOrSlug}` — both halves, because a code is not unique. */
215
276
  id: string;
@@ -294,6 +355,192 @@ export interface PartGlossaryDocument extends BaseDocument {
294
355
  * it is a DIFFERENT axis from colour and must not be folded into it.
295
356
  */
296
357
  hasMoldVariations?: boolean;
358
+ /**
359
+ * The 0–6 stat grid as numbers. Present on 1,149 of 2,490 rows — the source
360
+ * states it per part, so absence is "not stated", not zero.
361
+ */
362
+ ratings?: PartRatings;
363
+ /**
364
+ * The ceiling `ratings` are out of — 6, 8 or 100.
365
+ *
366
+ * 🛑 LOAD-BEARING, because each generation rates on its own scale and they
367
+ * share these fields. Measured: Beyblade X rates 0-100 (237 rows) while
368
+ * Burst rates 0-6 or 0-8 (945 rows), so mean `attack` by slot reads
369
+ * `blade 31.8` beside `driver 1.78`. Anything that sorts, compares or
370
+ * simulates on a rating MUST divide by this first; without it a Beyblade X
371
+ * blade outranks every Burst layer five times over.
372
+ *
373
+ * Derived per system from the highest rating observed for it, rounded up to
374
+ * a standard step — not from a hand-written table, which was wrong within
375
+ * one run (`warrior-wheel--dragoon` carries attack 7 against a declared 6).
376
+ */
377
+ ratingScale?: number;
378
+ /**
379
+ * The same axes rescaled to 0-100, so a part can be compared ACROSS
380
+ * generations — which the game needs, since it has cross-gen battles.
381
+ *
382
+ * 🛑 This equalises the EDITORIAL JUDGEMENT, not the capability. A Burst
383
+ * layer at 6/6 and a Beyblade X blade at 100/100 both land on 100 and are
384
+ * not equally strong — that blade is 30 g against the layer's 13 g. So it
385
+ * answers "how good is this part for its generation", which is the right
386
+ * basis for a balanced roster and the wrong basis for claiming physical
387
+ * equivalence. `weightG` carries the latter, in grams, unnormalised.
388
+ *
389
+ * `ratings` stays raw beside it: a normalised value can be recomputed from
390
+ * the raw pair, a discarded raw value cannot.
391
+ *
392
+ * 🛑 FOR COMPARING OPPONENTS, NEVER FOR SUGGESTING A BUILD. A bey can only
393
+ * be ASSEMBLED within one generation — a Burst layer does not fit a Beyblade
394
+ * X ratchet — while battles are cross-generation. So this field belongs in a
395
+ * matchup, a leaderboard or a side-by-side, and a "build your own" picker
396
+ * must constrain to a single system's stack regardless of what it says.
397
+ */
398
+ ratingsNormalised?: PartRatings;
399
+ /** WBO format legality across 11 formats; an unstated format is absent. */
400
+ wboLegality?: PartWboLegality;
401
+ /**
402
+ * Other names for this exact part — the community abbreviation (`Armor Tip
403
+ * - Adventure` is `Ad`) and the other vendor's name. On 1,072 of 2,490 rows.
404
+ * Feeds search, so a buyer can find a part by the code printed on it.
405
+ */
406
+ aka?: string[];
407
+ /**
408
+ * Hasbro's own product code, a SEPARATE field from `productCodes`. On 309
409
+ * rows, and it is the Hasbro <-> Takara-Tomy equivalence stated outright.
410
+ */
411
+ hasbroCode?: string;
412
+ /** Attack / Defense / Stamina / Balance, per part. 793 rows. */
413
+ partType?: string;
414
+ /** Right-Spin / Left-Spin / Dual. 756 rows. */
415
+ spinDirection?: string;
416
+ /** Beyblade X's attribute label — "Sky" and siblings. 29 rows. */
417
+ element?: string;
418
+ /**
419
+ * The character this part belongs to in the story — `Bit Chip - Black
420
+ * Dranzer` is Kai Hiwatari's. Only 27 rows, but it is the sole place a PART
421
+ * is tied to a character.
422
+ */
423
+ owner?: string;
424
+ /** The episode this part appeared in. 22 rows. */
425
+ appearAnime?: string;
426
+ /**
427
+ * OUR OWN classification — what the part DOES, across generations.
428
+ *
429
+ * 🛑 `slot` cannot carry this. It mirrors the source's `Classification=`
430
+ * spelling and it is half of every document id (`${slot}--${code}`), and
431
+ * those ids are LIVE URLS (`/parts/{classification}/{code}`). Renaming a
432
+ * slot re-keys 2,490 rows and breaks every inbound link — a slug is the one
433
+ * field that cannot be quietly corrected later. So this is additive.
434
+ *
435
+ * It collapses the source's line-specific names onto one role:
436
+ * `bu-blade` / `qs-blade` / `blade` are all `blade`; the six Burst chip
437
+ * variants are all `chip`. Derived in `scripts/lib/part-slot-vocab.mjs`,
438
+ * which covers all 59 slots with 10 roles.
439
+ */
440
+ /**
441
+ * The part SYSTEM — "Dynamite Battle Layer System", "4D System", "Basic
442
+ * Line". Stated on 1,961 of 2,087 source pages (94%).
443
+ *
444
+ * 🛑 This is the generation key, and it is load-bearing rather than
445
+ * decorative:
446
+ *
447
+ * 1. ASSEMBLY IS WITHIN-GENERATION ONLY. No bey can be built across
448
+ * generations, and Plastic and HMS do not interchange either, so a build
449
+ * validator needs each part's system. Nothing else on the row answers it.
450
+ *
451
+ * 2. ONE SLOT CAN HOLD TWO DIFFERENT PARTS. `blade` is the measured case:
452
+ * Beyblade X blades are metal at a ~32 g median while Burst QuadDrive and
453
+ * Dynamite Battle blades are plastic at ~7-11 g. A per-slot mass band
454
+ * cannot be correct for both, and without this field nothing downstream
455
+ * could tell them apart.
456
+ */
457
+ system?: string;
458
+ /** The retail series — "Beyblade Burst QuadStrike", "Beyblade: Metal Fury". */
459
+ series?: string;
460
+ role?: PartRole;
461
+ /**
462
+ * Every stack role one moulding fills, listed TOP-TO-BOTTOM. A part that
463
+ * covers a single position does not carry this field at all.
464
+ *
465
+ * Integration is real and spans generations: HMS merges spin gear and blade
466
+ * base into a Running Core, a 4D Bottom (`D:D`, `X:D`) is a spin track and
467
+ * a performance tip in one piece, a Burst `disc-integrated-driver` is a disc
468
+ * and a driver, and `fusion-wheel--variares` is an energy ring and a fusion
469
+ * wheel. A Plastic-era base can integrate its Sub Attack Ring.
470
+ *
471
+ * 🛑 Load-bearing for any stack validation. `role` files an integrated part
472
+ * under its PRIMARY function, so a validator reading only that believes the
473
+ * other position is still empty and offers a part that cannot physically go
474
+ * there — it will refuse a legal build, and an exploded 3D view will show a
475
+ * gap where a solid piece is.
476
+ *
477
+ * 🛑 PER PART, NOT PER SLOT — that is the whole reason this is a field and
478
+ * not a lookup. `fusion-wheel--variares` integrates the ring and
479
+ * `fusion-wheel--jade` does not; same slot, different moulding. Jade is
480
+ * multi-PIECE, which is a different fact (see `pieceCount` when it lands).
481
+ *
482
+ * 🛑 Do NOT re-spell this as "fused". "Fusion Wheel" is a literal MFB slot
483
+ * name, so `fusedRoles` reads as *the roles of the fusion wheel* — the
484
+ * opposite of the meaning. The source's own slot names already use the
485
+ * right word: `disc-integrated-driver`, `ratchet-integrated-bit`.
486
+ *
487
+ * Populated by `scripts/derive-part-integration.mjs`, which reads it off the
488
+ * compositions each part actually appears in — the source states a 4D
489
+ * Bottom's `Classification` as plain "Performance Tip", so this cannot be
490
+ * copied from an infobox and is not guessed either.
491
+ */
492
+ integratedRoles?: PartRole[];
493
+ /**
494
+ * An owner-uploaded 3D model. Empty by default and deliberately so — the
495
+ * models are supplied by the owner, not ingested from anywhere.
496
+ *
497
+ * 🛑 NEVER served through `/api/media/[...slug]`. That route applies NO
498
+ * authentication, raw-pipes non-images with a 30-day immutable cache, and
499
+ * sits over a bucket whose storage rule is `allow read: if true` — so a GLB
500
+ * there is a public download. A mesh is the asset someone could build a
501
+ * competing game from, so it needs a signed, short-lived URL and an
502
+ * authenticated route of its own.
503
+ *
504
+ * 🛑 And a browser viewer cannot make a mesh unextractable — WebGL buffers
505
+ * can be dumped from any page that renders them. The honest mitigation is to
506
+ * publish a DECIMATED preview and keep the full-resolution mesh server-side;
507
+ * "protected" here means inconvenient, never impossible.
508
+ */
509
+ model3d?: {
510
+ url: string;
511
+ format: "glb";
512
+ sizeBytes: number;
513
+ /** A low-poly public preview, when the full mesh is held back. */
514
+ previewUrl?: string;
515
+ credit?: string;
516
+ creditUrl?: string;
517
+ };
518
+ /**
519
+ * An owner-uploaded sound, with its waveform for display.
520
+ *
521
+ * Renders through the existing `<MediaAudio>` primitive, so this needs no
522
+ * new UI primitive. `waveform` is a coarse amplitude array for drawing the
523
+ * trace without decoding the audio client-side.
524
+ */
525
+ sound?: {
526
+ url: string;
527
+ durationMs?: number;
528
+ waveform?: number[];
529
+ label?: string;
530
+ };
531
+ /**
532
+ * The Japanese name, the romanisation, and the kanji where the source gives
533
+ * one.
534
+ *
535
+ * 🛑 Top-level fields AS WELL AS `specs[]` rows, which is not duplication for
536
+ * its own sake: a `<title>`, an `og:title`, an hreflang alternate and a
537
+ * JSON-LD `alternateName` all need a FIELD. None of them can scan an array
538
+ * of label/value pairs for one whose label happens to read "Japanese name",
539
+ * and that is where these 1,068 values previously lived and died.
540
+ */
541
+ jpName?: string;
542
+ romaji?: string;
543
+ kanjiName?: string;
297
544
  /** `/media/<slug>`. 🛑 Never a raw Storage host URL. */
298
545
  image?: string;
299
546
  /** The wiki page the photo came from — the CC-BY-SA attribution target. */
@@ -269,11 +269,11 @@ export const PART_SLOT_CATEGORY = {
269
269
  // Burst — a driver with the disc moulded in is still a driver.
270
270
  "disc-integrated-driver": "category-burst-drivers",
271
271
  // Beyblade X — the CX Custom line, which has its own node.
272
- "lock-chip": "category-x-custom-cx",
273
- "main-blade": "category-x-custom-cx",
274
- "assist-blade": "category-x-custom-cx",
275
- "over-blade": "category-x-custom-cx",
276
- "metal-blade": "category-x-custom-cx",
272
+ "lock-chip": "category-x-lock-chips",
273
+ "main-blade": "category-x-main-blades",
274
+ "assist-blade": "category-x-assist-blades",
275
+ "over-blade": "category-x-over-blades",
276
+ "metal-blade": "category-x-metal-blades",
277
277
  "gear-chip": "category-x-custom-cx",
278
278
  // Beyblade X — integrated parts file under the half a buyer shops for.
279
279
  "ratchet-integrated-blade": "category-x-blades",
@@ -290,7 +290,7 @@ export const PART_SLOT_CATEGORY = {
290
290
  "evolution-gear": "category-original-engine-gears",
291
291
  "bit-protector": "category-original-bit-chips",
292
292
  cew: "category-original-weight-disks",
293
- "support-parts": "category-original-parts",
293
+ "support-parts": "category-original-support-parts",
294
294
  };
295
295
  /** Every slot key, for an index page that must not hand-write the list. */
296
296
  export const ALL_PART_SLOTS = Object.values(PART_SLOT_VALUES);
@@ -23,5 +23,15 @@ export interface MarketplacePreorderCardProps {
23
23
  onAddToCart?: (product: MarketplacePreorderCardData) => void;
24
24
  wishlistActions?: WishlistToggleActions;
25
25
  labels?: MarketplacePreorderCardLabels;
26
+ /**
27
+ * Owner-only pencil / clone / bin. Pass `<OwnerQuickActions>`; it renders
28
+ * nothing for a visitor who does not own the record.
29
+ *
30
+ * 🛑 Rendered OUTSIDE this card's own anchor, as a sibling. A `<button>`
31
+ * inside an `<a>` is invalid HTML that browsers resolve by closing the
32
+ * anchor early, which breaks the CARD's navigation rather than just the
33
+ * button's — `stopPropagation` stops a click but cannot fix a parse.
34
+ */
35
+ renderOwnerActions?: () => React.ReactNode;
26
36
  }
27
- export declare function MarketplacePreorderCard({ product, className, variant, selectable, isSelected, onSelect, inWishlist: initialInWishlist, href, hrefBuilder, onNavigate, onAddToCart, wishlistActions, labels, }: MarketplacePreorderCardProps): import("react").JSX.Element;
37
+ export declare function MarketplacePreorderCard({ product, className, variant, selectable, isSelected, onSelect, inWishlist: initialInWishlist, href, hrefBuilder, onNavigate, onAddToCart, wishlistActions, labels, renderOwnerActions, }: MarketplacePreorderCardProps): import("react").JSX.Element;
@@ -14,7 +14,7 @@ const CLS_PREORDER_BADGE = "inline-flex items-center rounded-full bg-warning-sol
14
14
  const CLS_SALE_BADGE = "rounded-full bg-success-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-bold text-success-on-solid";
15
15
  const CLS_TRENDING_BADGE = "rounded-full bg-indigo-600 px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-bold text-white";
16
16
  const CLS_LIMITED_BADGE = "rounded-full bg-info-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-bold text-info-on-solid";
17
- import { BaseListingCard, Button, GatedPrice, RichText, Row, Span, Stack, Text, TextLink } from "../../../ui/index.js";
17
+ import { Div, BaseListingCard, Button, GatedPrice, RichText, Row, Span, Stack, Text, TextLink } from "../../../ui/index.js";
18
18
  import { normalizeRichTextHtml } from "../../../utils/string.formatter.js";
19
19
  import { PreorderBadge } from "./PreorderCard.js";
20
20
  import { WishlistHeartButton } from "../../wishlist/components/WishlistHeartButton.js";
@@ -34,7 +34,7 @@ function resolveHref(product, href, hrefBuilder) {
34
34
  return hrefBuilder(product);
35
35
  return ROUTES.PUBLIC.PRE_ORDER_DETAIL(product.slug ?? product.id);
36
36
  }
37
- export function MarketplacePreorderCard({ product, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, inWishlist: initialInWishlist = false, href, hrefBuilder, onNavigate, onAddToCart, wishlistActions, labels, }) {
37
+ export function MarketplacePreorderCard({ product, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, inWishlist: initialInWishlist = false, href, hrefBuilder, onNavigate, onAddToCart, wishlistActions, labels, renderOwnerActions, }) {
38
38
  const router = useRouter();
39
39
  const mergedLabels = { ...DEFAULT_LABELS, ...labels };
40
40
  const detailHref = resolveHref(product, href, hrefBuilder);
@@ -61,7 +61,7 @@ export function MarketplacePreorderCard({ product, className = "", variant = "gr
61
61
  void normalizeError(_err);
62
62
  }
63
63
  }, [toggle]);
64
- return (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: detailHref, className: "absolute inset-0 block", children: _jsx(MediaImage, { src: product.mainImage, alt: product.title, size: "card", fallback: "Product image" }) }), _jsxs(Stack, { className: "absolute right-2 top-2", align: "end", gap: "xs", children: [_jsx(Span, { layout: "inline-flex", color: "inverse", size: "xs", weight: "medium", className: "bg-cobalt", rounded: "full", padding: "pill-xs", children: mergedLabels.preOrderBadge }), product.featured && (_jsx(Span, { size: "xs", weight: "medium", className: CLS_PREORDER_BADGE, children: mergedLabels.featuredBadge }))] }), _jsxs(Stack, { className: "absolute left-2 top-2 pointer-events-none", gap: "xs", children: [product.partOfBundleIds && product.partOfBundleIds.length > 0 && (_jsx(Span, { className: CLS_SALE_BADGE, title: product.partOfBundleTitles?.[0] ? `In bundle: ${product.partOfBundleTitles[0]}` : "In a bundle", children: "Bundled" })), product.groupId && (_jsx(Span, { className: CLS_TRENDING_BADGE, title: product.groupTitle ? `Part of set: ${product.groupTitle}` : "Part of a set", children: product.isGroupParent ? "Set Parent" : "In Set" })), product.sublistingCategoryId && (_jsx(Span, { className: CLS_LIMITED_BADGE, title: "Has variants or sub-listings", children: "Has Variants" }))] }), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
64
+ const card = (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: detailHref, className: "absolute inset-0 block", children: _jsx(MediaImage, { src: product.mainImage, alt: product.title, size: "card", fallback: "Product image" }) }), _jsxs(Stack, { className: "absolute right-2 top-2", align: "end", gap: "xs", children: [_jsx(Span, { layout: "inline-flex", color: "inverse", size: "xs", weight: "medium", className: "bg-cobalt", rounded: "full", padding: "pill-xs", children: mergedLabels.preOrderBadge }), product.featured && (_jsx(Span, { size: "xs", weight: "medium", className: CLS_PREORDER_BADGE, children: mergedLabels.featuredBadge }))] }), _jsxs(Stack, { className: "absolute left-2 top-2 pointer-events-none", gap: "xs", children: [product.partOfBundleIds && product.partOfBundleIds.length > 0 && (_jsx(Span, { className: CLS_SALE_BADGE, title: product.partOfBundleTitles?.[0] ? `In bundle: ${product.partOfBundleTitles[0]}` : "In a bundle", children: "Bundled" })), product.groupId && (_jsx(Span, { className: CLS_TRENDING_BADGE, title: product.groupTitle ? `Part of set: ${product.groupTitle}` : "Part of a set", children: product.isGroupParent ? "Set Parent" : "In Set" })), product.sublistingCategoryId && (_jsx(Span, { className: CLS_LIMITED_BADGE, title: "Has variants or sub-listings", children: "Has Variants" }))] }), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
65
65
  event.stopPropagation();
66
66
  onSelect(product.id, !isSelected);
67
67
  }, className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsx(BaseListingCard.Info, { variant: variant, children: variant === "list" ? (
@@ -69,4 +69,11 @@ export function MarketplacePreorderCard({ product, className = "", variant = "gr
69
69
  _jsxs(_Fragment, { children: [_jsxs(Row, { className: "min-w-0", align: "start", justify: "between", gap: "sm", children: [_jsx(TextLink, { href: detailHref, className: "min-w-0 flex-1", children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), wishlistActions && (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist }))] }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Text, { className: "text-primary", size: "sm", weight: "bold", children: _jsx(GatedPrice, { children: formatCurrency(product.price, getDefaultCurrency()) }) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "self-start mt-0.5", onClick: handleNavigate, children: mergedLabels.reserveNow })] })) : (
70
70
  /* ── Full grid layout ── */
71
71
  _jsxs(_Fragment, { children: [_jsx(TextLink, { href: detailHref, children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), product.description ? (_jsx(RichText, { html: normalizeRichTextHtml(product.description), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)]" })) : null, _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: _jsx(GatedPrice, { children: formatCurrency(product.price, getDefaultCurrency()) }) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsxs(Row, { justify: "between", className: "mt-auto pt-[var(--appkit-space-2)]", gap: "sm", children: [_jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "flex-1", onClick: handleNavigate, children: mergedLabels.reserveNow }), onAddToCart ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", textSize: "xs", className: "flex-1", onClick: (e) => { e.stopPropagation(); onAddToCart(product); }, children: mergedLabels.addToCart })) : null, wishlistActions ? (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist })) : null] })] })) })] }));
72
+ if (!renderOwnerActions)
73
+ return card;
74
+ /*
75
+ * Sibling of the card, never a child — see the prop's docstring. `z-20`
76
+ * clears this card's own overlays, which all sit at `z-10`.
77
+ */
78
+ return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 bottom-2 z-20", children: renderOwnerActions() })] }));
72
79
  }
@@ -36,6 +36,7 @@ import { RelatedItemsSection } from "../../products/components/RelatedItemsSecti
36
36
  import { computeRelatedItems } from "../../../_internal/server/features/products/data.js";
37
37
  import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel.js";
38
38
  import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data.js";
39
+ import { OwnerQuickActions } from "../../admin/components/OwnerQuickActions.js";
39
40
  function toDescriptionHtml(raw) {
40
41
  if (!raw)
41
42
  return "";
@@ -47,8 +48,8 @@ const PRODUCTION_STATUS_LABELS = {
47
48
  in_production: "In Production",
48
49
  ready_to_ship: "Ready to Ship",
49
50
  };
50
- function PreOrderInfoSection({ title, productionStatus, maxPerUser, deliveryDate, featured, freeShipping, condition, isCancellable, finalSale, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref, }) {
51
- return (_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_PREORDER_BADGE, children: "Pre-Order" }), productionStatus && (_jsx(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: PRODUCTION_STATUS_LABELS[productionStatus] ?? productionStatus })), maxPerUser !== null && (_jsxs(Span, { size: "xs", weight: "medium", className: CLS_STATUS_BADGE, children: ["Limit: ", maxPerUser, " per customer"] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), deliveryDate && (_jsxs(Row, { color: "muted", textSize: "sm", align: "center", gap: "xs", children: [_jsx(Span, { children: "\uD83D\uDCC5" }), _jsx(Span, { children: "Estimated delivery:" }), _jsx(Span, { weight: "medium", children: deliveryDate.toLocaleDateString(undefined, { year: "numeric", month: "long" }) })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, returnable: isCancellable, finalSale: finalSale, labels: {
51
+ function PreOrderInfoSection({ title, productId, storeId, productionStatus, maxPerUser, deliveryDate, featured, freeShipping, condition, isCancellable, finalSale, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref, }) {
52
+ return (_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_PREORDER_BADGE, children: "Pre-Order" }), productionStatus && (_jsx(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: PRODUCTION_STATUS_LABELS[productionStatus] ?? productionStatus })), maxPerUser !== null && (_jsxs(Span, { size: "xs", weight: "medium", className: CLS_STATUS_BADGE, children: ["Limit: ", maxPerUser, " per customer"] }))] }), productId && (_jsx(OwnerQuickActions, { resource: "PRODUCT", placement: "detail", recordLabel: title, storeId: storeId, editHref: String(ROUTES.STORE.PRODUCTS_EDIT(productId)) })), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), deliveryDate && (_jsxs(Row, { color: "muted", textSize: "sm", align: "center", gap: "xs", children: [_jsx(Span, { children: "\uD83D\uDCC5" }), _jsx(Span, { children: "Estimated delivery:" }), _jsx(Span, { weight: "medium", children: deliveryDate.toLocaleDateString(undefined, { year: "numeric", month: "long" }) })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, returnable: isCancellable, finalSale: finalSale, labels: {
52
53
  featured: "Featured",
53
54
  fasterDelivery: "Faster Delivery",
54
55
  ratedSeller: "Rated Seller",
@@ -162,7 +163,7 @@ export async function PreOrderDetailPageView({ id, initialPreOrder, onReserveNow
162
163
  price: typeof p.price === "number" ? p.price : undefined,
163
164
  storeId: typeof p.storeId === "string" ? p.storeId : undefined,
164
165
  storeName: typeof p.storeName === "string" ? p.storeName : undefined,
165
- } }), _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", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRE_ORDERS), className: CLS_BREADCRUMB_LINK, children: "Pre-Orders" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _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(ProductGalleryClient, { images: images, video: productVideo, productName: title })), renderInfo: () => (_jsx(PreOrderInfoSection, { title: title, productionStatus: productionStatus, maxPerUser: maxPerUser, deliveryDate: deliveryDate, featured: featured, freeShipping: freeShipping, condition: condition, isCancellable: isCancellable, finalSale: isFinalSale(p), category: category, categoryName: categoryName, brand: brand, brandSlug: brandSlug, productFeatures: productFeatures, features: features, descriptionHtml: descriptionHtml, safeSeller: safeSeller, storeHref: storeHref })), renderSublistingSection: sublistingCategoryId
166
+ } }), _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", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRE_ORDERS), className: CLS_BREADCRUMB_LINK, children: "Pre-Orders" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _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(ProductGalleryClient, { images: images, video: productVideo, productName: title })), renderInfo: () => (_jsx(PreOrderInfoSection, { title: title, productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined, productionStatus: productionStatus, maxPerUser: maxPerUser, deliveryDate: deliveryDate, featured: featured, freeShipping: freeShipping, condition: condition, isCancellable: isCancellable, finalSale: isFinalSale(p), category: category, categoryName: categoryName, brand: brand, brandSlug: brandSlug, productFeatures: productFeatures, features: features, descriptionHtml: descriptionHtml, safeSeller: safeSeller, storeHref: storeHref })), renderSublistingSection: sublistingCategoryId
166
167
  ? () => (_jsx(SublistingCarouselSection, { sublistingCategoryId: sublistingCategoryId, currentListingId: String(product.id) }))
167
168
  : undefined, renderGroupSection: groupId
168
169
  ? () => (_jsx(ShowGroupSection, { groupId: groupId, currentSlug: String(p.slug ?? product.id), isParent: isGroupParent, groupTitle: groupTitle }))
@@ -13,6 +13,7 @@ import { NextResponse } from "next/server.js";
13
13
  import { z } from "zod";
14
14
  import { getProviders } from "../../../../contracts/index.js";
15
15
  import { createRouteHandler } from "../../../../next/api/routeHandler.js";
16
+ import { ALL_LISTING_TYPES } from "../../../../_internal/shared/listing-types/feature-flags.js";
16
17
  import { mediaFieldSchema } from "../../../media/types/index.js";
17
18
  import { storeRepository } from "../../../stores/repository/store.repository.js";
18
19
  // SB-UNI-V — bundlesRepository deleted; bundle stock-sync moves to the
@@ -55,8 +56,14 @@ const productUpdateSchema = z
55
56
  isPromoted: z.boolean().optional(),
56
57
  allowShipBeforeEmiComplete: z.boolean().optional(),
57
58
  printMeta: printMetaSchema.optional(),
59
+ /*
60
+ * 🛑 DERIVED from `ALL_LISTING_TYPES`, never restated. This literal was
61
+ * wrong in both directions: `spares` missing (so the type could not be
62
+ * PATCHed onto a listing at all) and `bundle` still present (so a caller
63
+ * could write a value no registry has a branch for). Root Cause #61.
64
+ */
58
65
  listingType: z
59
- .enum(["standard", "auction", "pre-order", "prize-draw", "bundle", "classified", "digital-code", "live", "art", "stickers"])
66
+ .enum(ALL_LISTING_TYPES)
60
67
  .optional(),
61
68
  media: z.array(mediaFieldSchema).optional(),
62
69
  })
@@ -16,6 +16,7 @@ import { NextResponse } from "next/server.js";
16
16
  import { z } from "zod";
17
17
  import { getProviders } from "../../../contracts/index.js";
18
18
  import { createRouteHandler } from "../../../next/api/routeHandler.js";
19
+ import { ALL_LISTING_TYPES } from "../../../_internal/shared/listing-types/feature-flags.js";
19
20
  import { mediaFieldSchema } from "../../media/types/index.js";
20
21
  import { printMetaSchema } from "../schemas/index.js";
21
22
  import { sanitizeProductsForPublic } from "../utils/sanitize.js";
@@ -50,11 +51,22 @@ const productMutateSchema = z
50
51
  sellerName: z.string().optional(),
51
52
  sellerEmail: z.string().email().optional(),
52
53
  slug: z.string().optional(),
53
- // `bundle` is deliberately absent: SB-UNI-D moved bundles onto
54
- // `categories` as a categoryType, so a product written with that value
55
- // would match no listing-type query anywhere.
54
+ /*
55
+ * 🛑 DERIVED from `ALL_LISTING_TYPES`, never restated.
56
+ *
57
+ * The literal that used to sit here is the cautionary tale for why. Its
58
+ * comment correctly explained that `bundle` is absent because SB-UNI-D
59
+ * moved bundles onto `categories` — so the dead value had been noticed and
60
+ * removed — and the list was STILL wrong, because `spares` was added to the
61
+ * union on 2026-10-19 and never reached here. A fix for one half of the
62
+ * drift, documented in a comment, with the other half drifting one field
63
+ * over underneath it.
64
+ *
65
+ * Deriving removes both failure modes at once: a dead value cannot survive
66
+ * and a new member cannot be forgotten. Root Cause #61.
67
+ */
56
68
  listingType: z
57
- .enum(["standard", "auction", "pre-order", "prize-draw", "classified", "digital-code", "live", "art", "stickers"])
69
+ .enum(ALL_LISTING_TYPES)
58
70
  .optional(),
59
71
  media: z.array(mediaFieldSchema).optional(),
60
72
  })
@@ -21,7 +21,7 @@ export function AvailabilityScopeHint({ total, available, types, }) {
21
21
  // availabilityTabsFor()[1] is the unavailable scope — "Sold", "Ended" or
22
22
  // "Sold & Ended", already derived per type.
23
23
  const noun = (availabilityTabsFor(types)[1]?.label ?? "Sold").toLowerCase();
24
- return (_jsxs(Row, { align: "center", gap: "sm", padding: "sm", surface: "muted", rounded: "lg", border: "subtle", className: "mb-4 flex-wrap", children: [_jsx(Text, { size: "sm", color: "muted", children: hidden === 1
24
+ return (_jsxs(Row, { align: "center", gap: "sm", padding: "sm", surface: "muted", rounded: "lg", border: "subtle", wrap: true, className: "mb-4", children: [_jsx(Text, { size: "sm", color: "muted", children: hidden === 1
25
25
  ? `1 listing here is ${noun} and hidden by the Available filter.`
26
26
  : `${hidden} listings here are ${noun} and hidden by the Available filter.` }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => table.set(TABLE_KEYS.AVAILABILITY, AVAILABILITY_VALUES.ALL), children: "Show all" })] }));
27
27
  }
@@ -14,5 +14,11 @@ export interface InteractiveProductCardProps {
14
14
  onToggleWishlist?: (productId: string) => void;
15
15
  onAddToCart?: (product: ProductItem) => void;
16
16
  onBuyNow?: (product: ProductItem) => void;
17
+ /**
18
+ * Owner-only pencil / clone / bin. Forwarded to ProductCard, which renders
19
+ * it OUTSIDE its own and this component's `<Link>` wrappers — see its return
20
+ * statement. Renders nothing for a visitor who does not own the listing.
21
+ */
22
+ renderOwnerActions?: () => React.ReactNode;
17
23
  }
18
- export declare function InteractiveProductCard({ product, href, className, selectable, isSelected, onSelect, isWishlisted, onToggleWishlist, onAddToCart, onBuyNow, }: InteractiveProductCardProps): React.JSX.Element;
24
+ export declare function InteractiveProductCard({ product, href, className, selectable, isSelected, onSelect, isWishlisted, onToggleWishlist, onAddToCart, onBuyNow, renderOwnerActions, }: InteractiveProductCardProps): React.JSX.Element;
@@ -1,7 +1,8 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import Link from "next/link.js";
3
3
  import { ProductCard } from "./ProductGrid.js";
4
- export function InteractiveProductCard({ product, href, className, selectable, isSelected, onSelect, isWishlisted = false, onToggleWishlist, onAddToCart, onBuyNow, }) {
4
+ import { Div } from "../../../ui/components/Div.js";
5
+ export function InteractiveProductCard({ product, href, className, selectable, isSelected, onSelect, isWishlisted = false, onToggleWishlist, onAddToCart, onBuyNow, renderOwnerActions, }) {
5
6
  // When selection is enabled, render ProductCard directly so its built-in
6
7
  // checkbox + long-press (BaseListingCard.Checkbox) work. `href` is still
7
8
  // forwarded here (not just in the plain-Link branch below) — ProductCard's
@@ -12,7 +13,18 @@ export function InteractiveProductCard({ product, href, className, selectable, i
12
13
  // `onSelect` is provided at all (see Root Cause Pattern: selectable-card
13
14
  // navigation must not depend on a selection callback being present).
14
15
  if (onSelect) {
15
- return (_jsx(ProductCard, { product: product, href: href, className: className, isWishlisted: isWishlisted, onAddToWishlist: onToggleWishlist, onAddToCart: onAddToCart, onBuyNow: onBuyNow, selectionMode: selectable, isSelected: isSelected, onSelect: (id) => onSelect(id, !isSelected) }));
16
+ return (_jsx(ProductCard, { product: product, href: href, className: className, isWishlisted: isWishlisted, onAddToWishlist: onToggleWishlist, onAddToCart: onAddToCart, onBuyNow: onBuyNow, selectionMode: selectable, isSelected: isSelected, onSelect: (id) => onSelect(id, !isSelected), renderOwnerActions: renderOwnerActions }));
16
17
  }
17
- return (_jsx(Link, { href: href, className: "block", children: _jsx(ProductCard, { product: product, className: className, isWishlisted: isWishlisted, onAddToWishlist: onToggleWishlist, onAddToCart: onAddToCart, onBuyNow: onBuyNow }) }));
18
+ /*
19
+ * 🛑 The owner toolbar is a SIBLING of this Link, not a child of it.
20
+ *
21
+ * ProductCard already keeps the toolbar outside its OWN anchor, but this
22
+ * branch adds a second one around the whole card — so handing the slot down
23
+ * would still nest buttons inside an `<a>`. Rendering it here, over a
24
+ * relative wrapper, is the only placement that escapes both.
25
+ */
26
+ const card = (_jsx(Link, { href: href, className: "block", children: _jsx(ProductCard, { product: product, className: className, isWishlisted: isWishlisted, onAddToWishlist: onToggleWishlist, onAddToCart: onAddToCart, onBuyNow: onBuyNow }) }));
27
+ if (!renderOwnerActions)
28
+ return card;
29
+ return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-14 z-20", children: renderOwnerActions() })] }));
18
30
  }