@mohasinac/appkit 4.9.5 → 4.11.0

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 (395) hide show
  1. package/dist/_internal/client/features/layout/DashboardLayoutClient.d.ts +6 -1
  2. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +4 -1
  3. package/dist/_internal/client/features/lottery/LotteryPullForm.d.ts +1 -1
  4. package/dist/_internal/client/features/lottery/LotteryPullForm.js +2 -2
  5. package/dist/_internal/server/features/admin/daily-digest-actions.d.ts +18 -0
  6. package/dist/_internal/server/features/admin/daily-digest-actions.js +37 -0
  7. package/dist/_internal/server/features/auctions/actions.js +3 -2
  8. package/dist/_internal/server/features/auctions/service.d.ts +4 -3
  9. package/dist/_internal/server/features/auctions/service.js +6 -6
  10. package/dist/_internal/server/features/audit-log/actions.d.ts +25 -0
  11. package/dist/_internal/server/features/audit-log/actions.js +38 -0
  12. package/dist/_internal/server/features/brands/actions.js +0 -2
  13. package/dist/_internal/server/features/brands/og.d.ts +4 -1
  14. package/dist/_internal/server/features/brands/og.js +1 -1
  15. package/dist/_internal/server/features/bundles/data.d.ts +9 -0
  16. package/dist/_internal/server/features/bundles/data.js +14 -0
  17. package/dist/_internal/server/features/bundles/index.d.ts +1 -1
  18. package/dist/_internal/server/features/bundles/index.js +1 -1
  19. package/dist/_internal/server/features/catalogue/og.d.ts +20 -0
  20. package/dist/_internal/server/features/catalogue/og.js +27 -0
  21. package/dist/_internal/server/features/grouped/data.d.ts +16 -0
  22. package/dist/_internal/server/features/grouped/data.js +50 -1
  23. package/dist/_internal/server/features/grouped/index.d.ts +1 -1
  24. package/dist/_internal/server/features/grouped/index.js +1 -1
  25. package/dist/_internal/server/features/orders/actions.js +9 -0
  26. package/dist/_internal/server/features/orders/adapters.js +25 -0
  27. package/dist/_internal/server/features/products/data.d.ts +23 -0
  28. package/dist/_internal/server/features/products/data.js +159 -0
  29. package/dist/_internal/server/features/products/index.d.ts +1 -1
  30. package/dist/_internal/server/features/products/index.js +1 -1
  31. package/dist/_internal/server/functions/scheduled.d.ts +3 -1
  32. package/dist/_internal/server/functions/scheduled.js +17 -1
  33. package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +40 -0
  34. package/dist/_internal/server/jobs/core/dailyStatusDigest.js +164 -0
  35. package/dist/_internal/server/jobs/core/hardBanCascade.js +11 -0
  36. package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +1 -0
  37. package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +4 -1
  38. package/dist/_internal/server/jobs/core/testerSandboxRefresh.d.ts +12 -0
  39. package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +85 -0
  40. package/dist/_internal/server/jobs/core/whatsappNotify.js +16 -0
  41. package/dist/_internal/server/jobs/handlers/dailyStatusDigest.d.ts +2 -0
  42. package/dist/_internal/server/jobs/handlers/dailyStatusDigest.js +4 -0
  43. package/dist/_internal/server/jobs/handlers/index.d.ts +2 -0
  44. package/dist/_internal/server/jobs/handlers/index.js +3 -0
  45. package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.d.ts +2 -0
  46. package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.js +2 -0
  47. package/dist/_internal/server/notifications/channel-health.d.ts +27 -0
  48. package/dist/_internal/server/notifications/channel-health.js +101 -0
  49. package/dist/_internal/shared/features/auctions/config.d.ts +20 -0
  50. package/dist/_internal/shared/features/auctions/config.js +31 -1
  51. package/dist/_internal/shared/features/brands/schema.d.ts +0 -6
  52. package/dist/_internal/shared/features/brands/schema.js +0 -1
  53. package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +7 -0
  54. package/dist/_internal/shared/features/categories/bundle-schemas.js +3 -0
  55. package/dist/_internal/shared/features/orders/schema.d.ts +8 -8
  56. package/dist/_internal/shared/listing-types/art/config.d.ts +1 -1
  57. package/dist/_internal/shared/listing-types/art/config.js +1 -1
  58. package/dist/_internal/shared/listing-types/classified/config.d.ts +1 -1
  59. package/dist/_internal/shared/listing-types/classified/config.js +1 -1
  60. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +1 -1
  61. package/dist/_internal/shared/listing-types/digital-code/config.js +1 -1
  62. package/dist/_internal/shared/listing-types/live/config.d.ts +1 -1
  63. package/dist/_internal/shared/listing-types/live/config.js +1 -1
  64. package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +1 -1
  65. package/dist/_internal/shared/listing-types/prize-draw/config.js +1 -1
  66. package/dist/_internal/shared/listing-types/stickers/config.d.ts +1 -1
  67. package/dist/_internal/shared/listing-types/stickers/config.js +1 -1
  68. package/dist/client.d.ts +21 -12
  69. package/dist/client.js +14 -7
  70. package/dist/constants/api-endpoints.d.ts +9 -9
  71. package/dist/constants/api-endpoints.js +3 -3
  72. package/dist/contracts/client-auth.d.ts +3 -7
  73. package/dist/contracts/email.d.ts +1 -0
  74. package/dist/features/account/components/AddressForm.js +24 -2
  75. package/dist/features/account/components/BecomeSellerView.d.ts +1 -2
  76. package/dist/features/account/components/BecomeSellerView.js +1 -1
  77. package/dist/features/account/components/NotificationBell.js +1 -1
  78. package/dist/features/account/components/UserBidsView.d.ts +1 -0
  79. package/dist/features/account/components/UserBidsView.js +47 -0
  80. package/dist/features/account/components/UserOrdersView.d.ts +4 -11
  81. package/dist/features/account/components/UserOrdersView.js +54 -4
  82. package/dist/features/account/components/UserReturnsView.d.ts +4 -9
  83. package/dist/features/account/components/UserReturnsView.js +37 -3
  84. package/dist/features/account/components/index.d.ts +3 -2
  85. package/dist/features/account/components/index.js +1 -0
  86. package/dist/features/admin/actions/admin-actions.js +22 -0
  87. package/dist/features/admin/actions/admin-coupon-actions.js +10 -0
  88. package/dist/features/admin/actions/notification-actions.js +4 -3
  89. package/dist/features/admin/components/AdminAdsView.d.ts +2 -3
  90. package/dist/features/admin/components/AdminAdsView.js +86 -89
  91. package/dist/features/admin/components/AdminAllEventEntriesView.js +18 -3
  92. package/dist/features/admin/components/AdminAuditLogView.d.ts +11 -0
  93. package/dist/features/admin/components/AdminAuditLogView.js +95 -0
  94. package/dist/features/admin/components/AdminBlogEditorView.js +37 -3
  95. package/dist/features/admin/components/AdminBrandEditorView.js +10 -6
  96. package/dist/features/admin/components/AdminBundleEditorView.js +18 -2
  97. package/dist/features/admin/components/AdminCarouselEditorView.js +2 -2
  98. package/dist/features/admin/components/AdminCarouselGroupEditorView.js +2 -2
  99. package/dist/features/admin/components/AdminCartsView.js +34 -4
  100. package/dist/features/admin/components/AdminCategoryEditorView.js +5 -4
  101. package/dist/features/admin/components/AdminCouponEditorView.js +2 -2
  102. package/dist/features/admin/components/AdminFaqEditorView.js +2 -2
  103. package/dist/features/admin/components/AdminHistoryView.js +11 -1
  104. package/dist/features/admin/components/AdminNotificationsView.js +27 -1
  105. package/dist/features/admin/components/AdminOrderEditorView.d.ts +10 -1
  106. package/dist/features/admin/components/AdminOrderEditorView.js +4 -3
  107. package/dist/features/admin/components/AdminOrdersView.js +92 -24
  108. package/dist/features/admin/components/AdminPayoutMarkPaidModal.d.ts +7 -0
  109. package/dist/features/admin/components/AdminPayoutMarkPaidModal.js +47 -0
  110. package/dist/features/admin/components/AdminPayoutsView.js +15 -31
  111. package/dist/features/admin/components/AdminProductEditorView.js +26 -3
  112. package/dist/features/admin/components/AdminSiteSettingsView.js +31 -4
  113. package/dist/features/admin/components/AdminStoreAddressesView.js +22 -2
  114. package/dist/features/admin/components/AdminStoresView.js +7 -0
  115. package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +2 -2
  116. package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +2 -2
  117. package/dist/features/admin/components/AdminUserEditorView.d.ts +2 -2
  118. package/dist/features/admin/components/AdminUserEditorView.js +4 -2
  119. package/dist/features/admin/components/AdminUsersView.js +1 -1
  120. package/dist/features/admin/components/AdminWishlistsView.js +14 -1
  121. package/dist/features/admin/components/DataListingView.d.ts +2 -2
  122. package/dist/features/admin/components/ViewAuditLogEntryModal.d.ts +19 -0
  123. package/dist/features/admin/components/ViewAuditLogEntryModal.js +22 -0
  124. package/dist/features/admin/components/ViewNotificationModal.d.ts +21 -0
  125. package/dist/features/admin/components/ViewNotificationModal.js +15 -0
  126. package/dist/features/admin/components/analytics/AdminPageViewsReportView.d.ts +4 -7
  127. package/dist/features/admin/components/analytics/AdminPageViewsReportView.js +56 -36
  128. package/dist/features/admin/components/index.d.ts +8 -1
  129. package/dist/features/admin/components/index.js +4 -0
  130. package/dist/features/admin/constants/filter-tabs.d.ts +49 -0
  131. package/dist/features/admin/constants/filter-tabs.js +27 -0
  132. package/dist/features/admin/hooks/useAdminListing.d.ts +6 -0
  133. package/dist/features/admin/hooks/useAdminListing.js +3 -1
  134. package/dist/features/admin/hooks/useAdminListingData.d.ts +9 -1
  135. package/dist/features/admin/hooks/useAdminListingData.js +9 -2
  136. package/dist/features/admin/repository/site-settings.repository.d.ts +5 -0
  137. package/dist/features/admin/schemas/firestore.d.ts +21 -0
  138. package/dist/features/admin/schemas/firestore.js +6 -0
  139. package/dist/features/analytics/types.d.ts +1 -1
  140. package/dist/features/analytics/types.js +8 -0
  141. package/dist/features/auctions/actions/bid-actions.js +4 -1
  142. package/dist/features/auctions/components/AuctionBottomActions.d.ts +2 -1
  143. package/dist/features/auctions/components/AuctionBottomActions.js +14 -1
  144. package/dist/features/auctions/components/AuctionDetailPageView.js +21 -9
  145. package/dist/features/auctions/components/LiveMinIncrement.d.ts +18 -0
  146. package/dist/features/auctions/components/LiveMinIncrement.js +17 -0
  147. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +5 -2
  148. package/dist/features/auctions/components/PlaceBidFormClient.js +7 -3
  149. package/dist/features/auctions/schemas/index.d.ts +5 -0
  150. package/dist/features/audit-log/repository/audit-log.repository.d.ts +45 -0
  151. package/dist/features/audit-log/repository/audit-log.repository.js +30 -0
  152. package/dist/features/audit-log/schemas/firestore.d.ts +59 -0
  153. package/dist/features/audit-log/schemas/firestore.js +35 -0
  154. package/dist/features/auth/actions/index.d.ts +0 -1
  155. package/dist/features/auth/actions/index.js +0 -1
  156. package/dist/features/auth/hooks/useAuth.d.ts +8 -35
  157. package/dist/features/auth/hooks/useAuth.js +21 -37
  158. package/dist/features/auth/permissions/constants.d.ts +1 -1
  159. package/dist/features/auth/permissions/constants.js +2 -0
  160. package/dist/features/blog/schemas/index.d.ts +32 -0
  161. package/dist/features/catalogue/components/AdminCatalogueApprovalsView.js +31 -3
  162. package/dist/features/catalogue/components/CatalogueItemEditorView.js +2 -1
  163. package/dist/features/catalogue/components/PublicCatalogueItemDetailView.d.ts +11 -0
  164. package/dist/features/catalogue/components/PublicCatalogueItemDetailView.js +33 -0
  165. package/dist/features/catalogue/components/PublicCatalogueView.js +2 -1
  166. package/dist/features/catalogue/index.d.ts +2 -0
  167. package/dist/features/catalogue/index.js +1 -0
  168. package/dist/features/categories/components/BrandDetailPageView.js +14 -12
  169. package/dist/features/categories/components/BundleDetailView.d.ts +3 -1
  170. package/dist/features/categories/components/BundleDetailView.js +3 -2
  171. package/dist/features/categories/components/CategoryDetailPageView.js +7 -3
  172. package/dist/features/categories/components/CategoryHighlightsAndFaqSection.d.ts +14 -0
  173. package/dist/features/categories/components/CategoryHighlightsAndFaqSection.js +22 -0
  174. package/dist/features/categories/components/CategoryProductsView.d.ts +1 -5
  175. package/dist/features/categories/components/CategoryProductsView.js +1 -1
  176. package/dist/features/categories/components/CategorySelectorCreate.js +16 -2
  177. package/dist/features/categories/schemas/firestore.d.ts +19 -2
  178. package/dist/features/categories/schemas/index.d.ts +24 -3
  179. package/dist/features/categories/schemas/index.js +3 -1
  180. package/dist/features/categories/types/index.d.ts +7 -0
  181. package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +17 -3
  182. package/dist/features/checkout/actions/checkout-value-otp-actions.js +101 -22
  183. package/dist/features/classified/components/ClassifiedContactSellerPanel.d.ts +12 -0
  184. package/dist/features/classified/components/ClassifiedContactSellerPanel.js +49 -0
  185. package/dist/features/classified/components/ClassifiedDetailPageView.d.ts +7 -0
  186. package/dist/features/classified/components/ClassifiedDetailPageView.js +69 -0
  187. package/dist/features/contact/components/ContactForm.js +17 -4
  188. package/dist/features/contact/components/ContactPageView.js +8 -2
  189. package/dist/features/contact/email.d.ts +1 -0
  190. package/dist/features/contact/email.js +3 -0
  191. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.d.ts +19 -0
  192. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +79 -0
  193. package/dist/features/events/components/AdminEventEditorView.js +54 -2
  194. package/dist/features/events/components/AdminEventEntriesView.d.ts +2 -16
  195. package/dist/features/events/components/AdminEventEntriesView.js +56 -89
  196. package/dist/features/events/components/EventFormDrawer.js +5 -1
  197. package/dist/features/events/components/EventRaffleEntryForm.d.ts +1 -3
  198. package/dist/features/events/components/EventRaffleEntryForm.js +1 -1
  199. package/dist/features/events/schemas/index.d.ts +22 -0
  200. package/dist/features/grouped/components/GroupedListingsCarousel.d.ts +13 -0
  201. package/dist/features/grouped/components/GroupedListingsCarousel.js +31 -0
  202. package/dist/features/grouped/repository/grouped-listings.repository.d.ts +12 -0
  203. package/dist/features/grouped/repository/grouped-listings.repository.js +34 -0
  204. package/dist/features/homepage/components/CharacterHotspotForm.js +202 -187
  205. package/dist/features/homepage/components/SectionCarousel.d.ts +1 -7
  206. package/dist/features/homepage/components/SectionCarousel.js +1 -1
  207. package/dist/features/layout/BottomActions.js +4 -3
  208. package/dist/features/layout/BottomActionsContext.d.ts +10 -0
  209. package/dist/features/layout/BottomActionsContext.js +5 -1
  210. package/dist/features/layout/TitleBarLayout.d.ts +1 -1
  211. package/dist/features/layout/TitleBarLayout.js +3 -3
  212. package/dist/features/layout/hooks/useBottomActions.d.ts +2 -0
  213. package/dist/features/layout/hooks/useBottomActions.js +4 -1
  214. package/dist/features/live/components/LiveItemDetailPageView.d.ts +9 -0
  215. package/dist/features/live/components/LiveItemDetailPageView.js +74 -0
  216. package/dist/features/media/MediaImage.js +39 -3
  217. package/dist/features/media/MediaVideo.js +17 -1
  218. package/dist/features/media/types/index.d.ts +10 -0
  219. package/dist/features/media/types/index.js +1 -0
  220. package/dist/features/media/upload/MediaUploadField.d.ts +7 -1
  221. package/dist/features/media/upload/MediaUploadField.js +11 -7
  222. package/dist/features/media/upload/video-poster.d.ts +2 -0
  223. package/dist/features/media/upload/video-poster.js +16 -1
  224. package/dist/features/media/upload/video-poster.test.js +1 -1
  225. package/dist/features/orders/components/OrderStatusTimeline.d.ts +12 -0
  226. package/dist/features/orders/components/OrderStatusTimeline.js +55 -0
  227. package/dist/features/orders/components/index.d.ts +2 -0
  228. package/dist/features/orders/components/index.js +1 -0
  229. package/dist/features/orders/constants/payment-window.d.ts +25 -0
  230. package/dist/features/orders/constants/payment-window.js +25 -0
  231. package/dist/features/orders/repository/orders.repository.d.ts +32 -0
  232. package/dist/features/orders/repository/orders.repository.js +56 -0
  233. package/dist/features/orders/schemas/index.d.ts +24 -24
  234. package/dist/features/orders/types/index.d.ts +33 -0
  235. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +9 -1
  236. package/dist/features/pre-orders/schemas/index.d.ts +5 -0
  237. package/dist/features/products/components/BidHistory.js +9 -1
  238. package/dist/features/products/components/GroupSettingsPanel.js +25 -3
  239. package/dist/features/products/components/PrizeDrawDetailPageView.js +11 -1
  240. package/dist/features/products/components/PrizeDrawEntryActions.d.ts +1 -4
  241. package/dist/features/products/components/PrizeDrawEntryActions.js +1 -1
  242. package/dist/features/products/components/ProductDetailActions.d.ts +1 -2
  243. package/dist/features/products/components/ProductDetailActions.js +1 -1
  244. package/dist/features/products/components/ProductDetailPageView.js +11 -116
  245. package/dist/features/products/components/ProductGrid.js +15 -1
  246. package/dist/features/products/components/RelatedItemsSection.d.ts +16 -0
  247. package/dist/features/products/components/RelatedItemsSection.js +17 -0
  248. package/dist/features/products/constants/action-defs.d.ts +1 -0
  249. package/dist/features/products/constants/action-defs.js +1 -0
  250. package/dist/features/products/schemas/index.d.ts +5 -0
  251. package/dist/features/promotions/components/CouponsIndexListing.d.ts +1 -3
  252. package/dist/features/promotions/components/CouponsIndexListing.js +1 -1
  253. package/dist/features/search/components/SearchView.d.ts +1 -3
  254. package/dist/features/search/components/SearchView.js +1 -1
  255. package/dist/features/seller/components/QuickProductForm.js +2 -1
  256. package/dist/features/seller/components/SellerBidsView.js +66 -107
  257. package/dist/features/seller/components/SellerGoogleReviewsView.js +17 -1
  258. package/dist/features/seller/components/SellerOrdersView.d.ts +15 -3
  259. package/dist/features/seller/components/SellerOrdersView.js +134 -163
  260. package/dist/features/seller/components/SellerPayoutMethodsView.js +60 -48
  261. package/dist/features/seller/components/SellerPayoutSettingsView.js +69 -4
  262. package/dist/features/seller/components/SellerPayoutsView.d.ts +12 -0
  263. package/dist/features/seller/components/SellerPayoutsView.js +25 -5
  264. package/dist/features/seller/components/SellerProductShell.d.ts +10 -3
  265. package/dist/features/seller/components/SellerProductShell.js +90 -37
  266. package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +2 -7
  267. package/dist/features/seller/components/SellerProductsFilterDrawer.js +4 -4
  268. package/dist/features/seller/components/SellerProductsView.d.ts +2 -4
  269. package/dist/features/seller/components/SellerProductsView.js +131 -189
  270. package/dist/features/seller/components/SellerReviewsView.js +95 -133
  271. package/dist/features/seller/components/SellerShippingConfigsView.js +47 -67
  272. package/dist/features/seller/components/SellerShippingView.js +41 -4
  273. package/dist/features/seller/components/SellerStoreCategoriesView.js +51 -59
  274. package/dist/features/seller/components/SellerStorefrontView.d.ts +6 -1
  275. package/dist/features/seller/components/SellerStorefrontView.js +42 -5
  276. package/dist/features/seller/components/index.d.ts +2 -2
  277. package/dist/features/seller/components/index.js +2 -2
  278. package/dist/features/shell/FormShell.d.ts +24 -4
  279. package/dist/features/shell/FormShell.js +12 -6
  280. package/dist/features/shell/QuickFormDrawer.js +67 -12
  281. package/dist/features/shell/StepForm.d.ts +30 -1
  282. package/dist/features/shell/StepForm.js +47 -4
  283. package/dist/features/shell/index.d.ts +1 -1
  284. package/dist/features/shell/index.js +1 -1
  285. package/dist/features/shipments/components/AdminShipmentEditorView.js +2 -2
  286. package/dist/features/shipments/schemas/validation.d.ts +14 -4
  287. package/dist/features/tester/components/AdminTesterFeedbackListView.js +23 -2
  288. package/dist/features/tester/components/TesterChecklistStepRow.js +1 -1
  289. package/dist/features/tester/components/TesterHubView.js +3 -2
  290. package/dist/features/tester/seed-data/orders-tester-seed-data.js +9 -1
  291. package/dist/features/tester/seed-data/products-tester-seed-data.js +43 -32
  292. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +487 -18
  293. package/dist/index.d.ts +17 -3
  294. package/dist/index.js +23 -5
  295. package/dist/next/routing/route-map.d.ts +13 -0
  296. package/dist/next/routing/route-map.js +12 -0
  297. package/dist/providers/email-resend/provider.js +1 -0
  298. package/dist/providers/firebase-client/auth.d.ts +1 -0
  299. package/dist/providers/firebase-client/auth.js +7 -1
  300. package/dist/repositories/index.d.ts +1 -0
  301. package/dist/repositories/index.js +1 -0
  302. package/dist/schemas/registry.d.ts +61 -25
  303. package/dist/security/pii-mask.js +1 -1
  304. package/dist/seed/bids-seed-data.js +48 -3
  305. package/dist/seed/categories-seed-data.js +69 -0
  306. package/dist/seed/grouped-listings-seed-data.js +167 -0
  307. package/dist/seed/payouts-seed-data.js +4 -4
  308. package/dist/seed/products-art-seed-data.js +188 -5
  309. package/dist/seed/products-auctions-seed-data.js +226 -6
  310. package/dist/seed/products-classifieds-seed-data.js +255 -8
  311. package/dist/seed/products-digital-codes-seed-data.js +258 -19
  312. package/dist/seed/products-live-items-seed-data.js +143 -10
  313. package/dist/seed/products-preorders-seed-data.js +213 -8
  314. package/dist/seed/products-prize-draws-seed-data.js +198 -0
  315. package/dist/seed/products-standard-seed-data.js +234 -4
  316. package/dist/seed/products-stickers-seed-data.js +178 -5
  317. package/dist/seed/site-settings-seed-data.js +23 -30
  318. package/dist/seed/store-extensions-seed-data.js +2 -2
  319. package/dist/server-entry.d.ts +6 -2
  320. package/dist/server-entry.js +24 -3
  321. package/dist/server.d.ts +10 -3
  322. package/dist/server.js +11 -4
  323. package/dist/styles.css +29 -1
  324. package/dist/tailwind-utilities.css +1 -1
  325. package/dist/ui/components/Form.d.ts +11 -2
  326. package/dist/ui/components/Form.js +4 -3
  327. package/dist/ui/components/FormField.d.ts +3 -1
  328. package/dist/ui/components/FormField.js +22 -4
  329. package/dist/ui/components/ImageLightbox.js +37 -3
  330. package/dist/ui/components/ImageLightbox.style.css +28 -0
  331. package/dist/ui/components/ListingLayout.d.ts +1 -2
  332. package/dist/ui/components/ListingLayout.js +1 -1
  333. package/dist/ui/components/RecordDetailModal.d.ts +68 -0
  334. package/dist/ui/components/RecordDetailModal.js +8 -0
  335. package/dist/ui/components/SlottedListingView.js +9 -1
  336. package/dist/ui/forms/FormErrorSummary.d.ts +25 -0
  337. package/dist/ui/forms/FormErrorSummary.js +37 -0
  338. package/dist/ui/forms/FormShell.d.ts +37 -2
  339. package/dist/ui/forms/FormShell.js +37 -7
  340. package/dist/ui/forms/FormShell.style.css +17 -0
  341. package/dist/ui/forms/index.d.ts +2 -0
  342. package/dist/ui/forms/index.js +1 -0
  343. package/dist/ui/index.d.ts +4 -0
  344. package/dist/ui/index.js +5 -0
  345. package/dist/utils/media-url.d.ts +12 -1
  346. package/dist/utils/media-url.js +59 -1
  347. package/dist/validation/schemas.d.ts +4 -4
  348. package/package.json +1 -1
  349. package/dist/_internal/client/features/classified/ClassifiedDetailView.d.ts +0 -9
  350. package/dist/_internal/client/features/classified/ClassifiedDetailView.js +0 -55
  351. package/dist/_internal/client/features/digital-code/DigitalCodeDetailView.d.ts +0 -18
  352. package/dist/_internal/client/features/digital-code/DigitalCodeDetailView.js +0 -21
  353. package/dist/_internal/client/features/live/LiveItemDetailView.d.ts +0 -9
  354. package/dist/_internal/client/features/live/LiveItemDetailView.js +0 -25
  355. package/dist/_internal/server/jobs/core/prizeRevealClose.d.ts +0 -2
  356. package/dist/_internal/server/jobs/core/prizeRevealClose.js +0 -28
  357. package/dist/_internal/server/jobs/core/prizeRevealExpiry.d.ts +0 -2
  358. package/dist/_internal/server/jobs/core/prizeRevealExpiry.js +0 -61
  359. package/dist/_internal/server/jobs/core/prizeRevealOpen.d.ts +0 -2
  360. package/dist/_internal/server/jobs/core/prizeRevealOpen.js +0 -68
  361. package/dist/_internal/server/jobs/core/prizeRevealReminder.d.ts +0 -2
  362. package/dist/_internal/server/jobs/core/prizeRevealReminder.js +0 -49
  363. package/dist/_internal/server/jobs/handlers/prizeRevealClose.d.ts +0 -2
  364. package/dist/_internal/server/jobs/handlers/prizeRevealClose.js +0 -2
  365. package/dist/_internal/server/jobs/handlers/prizeRevealExpiry.d.ts +0 -2
  366. package/dist/_internal/server/jobs/handlers/prizeRevealExpiry.js +0 -2
  367. package/dist/_internal/server/jobs/handlers/prizeRevealOpen.d.ts +0 -2
  368. package/dist/_internal/server/jobs/handlers/prizeRevealOpen.js +0 -2
  369. package/dist/_internal/server/jobs/handlers/prizeRevealReminder.d.ts +0 -2
  370. package/dist/_internal/server/jobs/handlers/prizeRevealReminder.js +0 -2
  371. package/dist/features/auth/consent-otp.d.ts +0 -57
  372. package/dist/features/auth/consent-otp.js +0 -127
  373. package/dist/features/cart/components/CheckoutOtpModal.d.ts +0 -15
  374. package/dist/features/cart/components/CheckoutOtpModal.js +0 -12
  375. package/dist/features/checkout/actions/checkout-actions.d.ts +0 -31
  376. package/dist/features/checkout/actions/checkout-actions.js +0 -124
  377. package/dist/features/classified/components/ClassifiedIndexListing.d.ts +0 -5
  378. package/dist/features/classified/components/ClassifiedIndexListing.js +0 -123
  379. package/dist/features/classified/components/ClassifiedListView.d.ts +0 -6
  380. package/dist/features/classified/components/ClassifiedListView.js +0 -44
  381. package/dist/features/digital-codes/components/DigitalCodesIndexListing.d.ts +0 -5
  382. package/dist/features/digital-codes/components/DigitalCodesIndexListing.js +0 -114
  383. package/dist/features/digital-codes/components/DigitalCodesListView.d.ts +0 -6
  384. package/dist/features/digital-codes/components/DigitalCodesListView.js +0 -38
  385. package/dist/features/live/components/LiveItemsIndexListing.d.ts +0 -5
  386. package/dist/features/live/components/LiveItemsIndexListing.js +0 -115
  387. package/dist/features/live/components/LiveItemsListView.d.ts +0 -6
  388. package/dist/features/live/components/LiveItemsListView.js +0 -44
  389. package/dist/features/seller/components/SellerTemplatesView.d.ts +0 -12
  390. package/dist/features/seller/components/SellerTemplatesView.js +0 -276
  391. package/dist/react/hooks/useVisibleItems.d.ts +0 -37
  392. package/dist/react/hooks/useVisibleItems.js +0 -59
  393. package/dist/ui/components/TabStrip.d.ts +0 -34
  394. package/dist/ui/components/TabStrip.js +0 -56
  395. package/dist/ui/components/TabStrip.style.css +0 -97
@@ -84,6 +84,9 @@ export function BottomActionsProvider({ children, }) {
84
84
  const setInfoLabel = useCallback((infoLabel) => {
85
85
  setState((prev) => ({ ...prev, infoLabel }));
86
86
  }, []);
87
+ const setSecondaryLabel = useCallback((secondaryLabel) => {
88
+ setState((prev) => ({ ...prev, secondaryLabel }));
89
+ }, []);
87
90
  const clearAll = useCallback(() => {
88
91
  actionCallbacksRef.current = new Map();
89
92
  bulkCallbacksRef.current = new Map();
@@ -98,8 +101,9 @@ export function BottomActionsProvider({ children, }) {
98
101
  bulkCallbacksRef,
99
102
  bulkClearRef,
100
103
  setInfoLabel,
104
+ setSecondaryLabel,
101
105
  clearAll,
102
- }), [state, setActions, setBulkConfig, setInfoLabel, clearAll]);
106
+ }), [state, setActions, setBulkConfig, setInfoLabel, setSecondaryLabel, clearAll]);
103
107
  return (_jsx(BottomActionsContext.Provider, { value: value, children: children }));
104
108
  }
105
109
  // --- Hook ---------------------------------------------------------------------
@@ -66,4 +66,4 @@ export interface TitleBarLayoutProps {
66
66
  *
67
67
  * Receives all domain data as props — zero domain imports.
68
68
  */
69
- export declare function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen: _searchOpen, brandName, brandShortName: _brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount, cartHref, cartCount, profileHref, unreadNotificationCount, notificationsHref, loginHref, registerHref, user, notificationSlot, devSlot, navSlot: _navSlot, promoStripText, isDark, onToggleTheme, onTourStart, hasDashboardNav, onToggleDashboardNav, hideSidebarToggle, id, className, }: TitleBarLayoutProps): React.JSX.Element;
69
+ export declare function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount, cartHref, cartCount, profileHref, unreadNotificationCount, notificationsHref, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark, onToggleTheme, onTourStart, hasDashboardNav, onToggleDashboardNav, hideSidebarToggle, id, className, }: TitleBarLayoutProps): React.JSX.Element;
@@ -14,7 +14,7 @@ const countBadge = "absolute -top-0.5 -right-0.5 flex items-center justify-cente
14
14
  *
15
15
  * Receives all domain data as props — zero domain imports.
16
16
  */
17
- export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen: _searchOpen, brandName, brandShortName: _brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount = 0, cartHref, cartCount = 0, profileHref, unreadNotificationCount = 0, notificationsHref, loginHref, registerHref, user, notificationSlot, devSlot, navSlot: _navSlot, promoStripText, isDark = false, onToggleTheme, onTourStart, hasDashboardNav, onToggleDashboardNav, hideSidebarToggle = false, id = "titlebar", className = "", }) {
17
+ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount = 0, cartHref, cartCount = 0, profileHref, unreadNotificationCount = 0, notificationsHref, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark = false, onToggleTheme, onTourStart, hasDashboardNav, onToggleDashboardNav, hideSidebarToggle = false, id = "titlebar", className = "", }) {
18
18
  // ── Element builders ────────────────────────────────────────────────────────
19
19
  const promotionsEl = promotionsHref ? (_jsxs(Link, { href: promotionsHref, "aria-label": "Today's deals", className: "flex items-center gap-[var(--appkit-space-1)] px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] rounded-full text-[length:var(--appkit-text-xs)] font-bold bg-primary-100 text-primary-700 dark:bg-secondary-900/40 dark:text-secondary-400 hover:bg-primary-200 dark:hover:bg-secondary-900/60 transition-colors border border-primary-200/60 dark:border-secondary-700/40", children: [_jsx("svg", { className: "w-3 h-3", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { d: "M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.37.86.58 1.41.58.55 0 1.05-.21 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z" }) }), _jsx(Span, { className: "hidden sm:inline", children: "Today's Deals" })] })) : null;
20
20
  const themeBtn = onToggleTheme ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": isDark ? "Switch to light mode" : "Switch to dark mode", onClick: onToggleTheme, className: iconBtn, children: isDark ? (_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 3v1m0 16v1m8.66-9h-1M4.34 12h-1m15.07-6.07-.71.71M6.34 17.66l-.71.71m12.73 0-.71-.71M6.34 6.34l-.71-.71M12 5a7 7 0 1 0 0 14A7 7 0 0 0 12 5z" }) })) : (_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 12.79A9 9 0 1 1 11.21 3a7 7 0 1 0 9.79 9.79z" }) })) })) : null;
@@ -22,7 +22,7 @@ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, s
22
22
  const hamburgerBtn = !hideSidebarToggle ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": sidebarOpen ? "Close menu" : hasDashboardNav ? "Open dashboard navigation" : "Open menu", "aria-expanded": sidebarOpen, "aria-controls": "secondary-sidebar", onClick: hasDashboardNav && onToggleDashboardNav ? onToggleDashboardNav : onToggleSidebar, className: iconBtn, children: _jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: sidebarOpen ? (_jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" })) : (_jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 6h16M4 12h16M4 18h16" })) }) })) : null;
23
23
  // Compare is always lg+ (desktop-only feature, less critical on mobile)
24
24
  const compareEl = compareHref ? (_jsx(Link, { href: compareHref, "aria-label": "Compare items", className: `${iconBtn} hidden lg:flex`, children: _jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" }) }) })) : null;
25
- const searchBtn = (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": "Search", onClick: onSearchToggle, className: iconBtn, "data-tour": "nav-search", children: _jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z" }) }) }));
25
+ const searchBtn = (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": "Search", "aria-pressed": searchOpen, onClick: onSearchToggle, className: iconBtn, "data-tour": "nav-search", children: _jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z" }) }) }));
26
26
  // Dedicated notification bell — rendered immediately before the wishlist
27
27
  // icon (per layout request). Falls back to invisible when no href is set;
28
28
  // the count badge mirrors the wishlist/cart pattern.
@@ -33,7 +33,7 @@ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, s
33
33
  const authButtonsEl = !user && (loginHref || registerHref) ? (_jsxs(Row, { gap: "xs", className: "hidden lg:flex", children: [loginHref && (_jsx(Link, { href: loginHref, className: "px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-text-muted)] hover:text-primary-700 dark:hover:text-secondary-400 transition-colors rounded-lg hover:bg-primary-50 dark:hover:bg-[var(--appkit-color-surface-elevated)]", children: "Sign in" })), registerHref && (_jsx(Link, { href: registerHref, className: "px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-semibold rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors btn-glow shadow-sm", children: "Register" }))] })) : null;
34
34
  const hasTb2 = !!(notificationsEl || wishlistEl || cartEl || profileEl);
35
35
  // ── Render ───────────────────────────────────────────────────────────────────
36
- return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-[var(--appkit-color-bg)]/95 backdrop-blur-md border-b border-[var(--appkit-color-border-subtle)] shadow-sm ${className}`, children: [promoStripText && (_jsx(Section, { color: "inverse", tone: "accent-banner", className: "text-[length:var(--appkit-text-xs)] text-center font-medium", padding: "y-2xs", children: promoStripText })), _jsxs(Div, { paddingX: "x-page", className: "container mx-auto max-w-[1920px]", children: [_jsxs(Row, { justify: "between", gap: "none", className: "relative h-14", children: [_jsx(Row, { gap: "3", children: _jsxs(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center gap-[var(--appkit-space-2)] transition-opacity hover:opacity-80", children: [_jsx(Div, { className: "flex md:hidden", children: _jsx(SiteMark, { title: brandName, size: "sm" }) }), _jsx(SiteLogo, { title: brandName, size: "md" })] }) }), _jsx(Row, { className: "hidden md:flex absolute inset-y-0 left-1/2 -translate-x-1/2", align: "center", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteMark, { src: siteLogoUrl, title: brandName, size: "lg" }) }) }), _jsxs(Row, { gap: "xs", children: [devSlot, compareEl, notificationSlot, notificationsEl && _jsx(Div, { className: "hidden lg:flex", children: notificationsEl }), wishlistEl && _jsx(Div, { className: "hidden lg:flex", children: wishlistEl }), cartEl && _jsx(Div, { className: "hidden lg:flex", children: cartEl }), user
36
+ return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-[var(--appkit-color-bg)]/95 backdrop-blur-md border-b border-[var(--appkit-color-border-subtle)] shadow-sm ${className}`, children: [promoStripText && (_jsx(Section, { color: "inverse", tone: "accent-banner", className: "text-[length:var(--appkit-text-xs)] text-center font-medium", padding: "y-2xs", children: promoStripText })), _jsxs(Div, { paddingX: "x-page", className: "container mx-auto max-w-[1920px]", children: [_jsxs(Row, { justify: "between", gap: "none", className: "relative h-14", children: [_jsx(Row, { gap: "3", children: _jsxs(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center gap-[var(--appkit-space-2)] transition-opacity hover:opacity-80", children: [_jsx(Div, { className: "flex md:hidden", children: _jsx(SiteMark, { title: brandShortName ?? brandName, size: "sm" }) }), _jsx(SiteLogo, { title: brandName, size: "md" })] }) }), _jsx(Row, { className: "hidden md:flex absolute inset-y-0 left-1/2 -translate-x-1/2", align: "center", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteMark, { src: siteLogoUrl, title: brandName, size: "lg" }) }) }), _jsxs(Row, { gap: "xs", children: [devSlot, navSlot, compareEl, notificationSlot, notificationsEl && _jsx(Div, { className: "hidden lg:flex", children: notificationsEl }), wishlistEl && _jsx(Div, { className: "hidden lg:flex", children: wishlistEl }), cartEl && _jsx(Div, { className: "hidden lg:flex", children: cartEl }), user
37
37
  ? profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })
38
38
  : authButtonsEl ?? (profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })), searchBtn, promotionsEl, tourBtn, themeBtn, hamburgerBtn] })] }), hasTb2 && (_jsxs(Row, { border: "subtle", as: "nav", "aria-label": "Account actions", justify: "end", gap: "xs", className: "flex lg:hidden h-10 border-t px-[var(--appkit-space-1)]", children: [notificationsEl, wishlistEl, cartEl, profileEl] }))] })] }));
39
39
  }
@@ -3,5 +3,7 @@ export interface UseBottomActionsOptions {
3
3
  actions?: BottomAction[];
4
4
  bulk?: BottomBulkConfig;
5
5
  infoLabel?: string;
6
+ /** Rendered as its own row above infoLabel. See BottomActionsState.secondaryLabel. */
7
+ secondaryLabel?: string;
6
8
  }
7
9
  export declare function useBottomActions(options?: UseBottomActionsOptions): void;
@@ -2,7 +2,7 @@
2
2
  import { useEffect, useRef } from "react";
3
3
  import { useBottomActionsContext, } from "../BottomActionsContext";
4
4
  export function useBottomActions(options = {}) {
5
- const { setActions, setBulkConfig, setInfoLabel, clearAll } = useBottomActionsContext();
5
+ const { setActions, setBulkConfig, setInfoLabel, setSecondaryLabel, clearAll } = useBottomActionsContext();
6
6
  const actionsRef = useRef(options.actions ?? []);
7
7
  actionsRef.current = options.actions ?? [];
8
8
  const bulkRef = useRef(options.bulk);
@@ -22,6 +22,9 @@ export function useBottomActions(options = {}) {
22
22
  useEffect(() => {
23
23
  setInfoLabel(options.infoLabel);
24
24
  }, [setInfoLabel, options.infoLabel]);
25
+ useEffect(() => {
26
+ setSecondaryLabel(options.secondaryLabel);
27
+ }, [setSecondaryLabel, options.secondaryLabel]);
25
28
  useEffect(() => {
26
29
  return () => clearAll();
27
30
  }, [clearAll]);
@@ -0,0 +1,9 @@
1
+ import type { ProductDocument } from "../../products/schemas/firestore";
2
+ export interface LiveItemDetailPageViewProps {
3
+ slug: string;
4
+ /** Pre-fetched product document — dedupes with generateMetadata() via React.cache(). */
5
+ initialProduct?: ProductDocument | null;
6
+ /** Render-prop for the add-to-cart CTA — wired by the page shim (carries the vendor-verification gate). */
7
+ renderActions?: (product: ProductDocument) => React.ReactNode;
8
+ }
9
+ export declare function LiveItemDetailPageView({ slug, initialProduct, renderActions }: LiveItemDetailPageViewProps): Promise<import("react").JSX.Element>;
@@ -0,0 +1,74 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import Link from "next/link";
3
+ import { getLiveItemForDetail } from "../../../_internal/server/features/live/data";
4
+ const CLS_BREADCRUMB_LINK = "hover:text-primary-600 transition-colors";
5
+ const CLS_LIVE_ITEM_BADGE = "inline-block rounded-full bg-lime-100 dark:bg-lime-900/30 px-[var(--appkit-space-2-5)] py-[var(--appkit-space-0-5)] text-lime-700 dark:text-lime-300";
6
+ const CLS_WARN_BOX = "rounded-lg border border-warning bg-warning-surface p-[var(--appkit-space-4)] text-[length:var(--appkit-text-sm)]";
7
+ const CLS_WARN_TITLE = "font-medium text-warning";
8
+ const CLS_WARN_BODY = "mt-1 text-warning";
9
+ import { ROUTES } from "../../../next";
10
+ import { getDefaultCurrency } from "../../../core/baseline-resolver";
11
+ import { formatCurrency } from "../../../utils/number.formatter";
12
+ import { normalizeRichTextHtml } from "../../../utils/string.formatter";
13
+ import { safeDisplayName } from "../../../security";
14
+ import { Div, Container, Heading, Main, Nav, RichText, Row, Section, Span, Stack, Text, Dl, Dt, Dd, } from "../../../ui";
15
+ import { PreOrderDetailView } from "../../products/components/PreOrderDetailView";
16
+ import { ProductGalleryClient } from "../../products/components/ProductGalleryClient";
17
+ import { ProductTabsShell } from "../../products/components/ProductTabsShell";
18
+ import { CustomSectionTabContent } from "../../products/components/CustomSectionTabContent";
19
+ import { ShareButton } from "../../products/components/ShareButton";
20
+ import { HistoryTracker } from "../../history/components/HistoryTracker";
21
+ import { RelatedItemsSection } from "../../products/components/RelatedItemsSection";
22
+ import { computeRelatedItems, getReviewItemsForProduct } from "../../../_internal/server/features/products/data";
23
+ import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel";
24
+ import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data";
25
+ import { ReviewsList } from "../../reviews/components/ReviewsList";
26
+ function toDescriptionHtml(raw) {
27
+ if (!raw)
28
+ return "";
29
+ const s = typeof raw === "string" ? raw : JSON.stringify(raw);
30
+ return normalizeRichTextHtml(s);
31
+ }
32
+ export async function LiveItemDetailPageView({ slug, initialProduct, renderActions }) {
33
+ const product = initialProduct !== undefined
34
+ ? (initialProduct ?? undefined)
35
+ : await getLiveItemForDetail(slug).catch(() => undefined);
36
+ if (!product) {
37
+ return (_jsx(Main, { children: _jsx(Section, { padding: "y-5xl", children: _jsx(Container, { size: "md", children: _jsxs(Stack, { align: "start", gap: "md", className: "text-left", children: [_jsx(Heading, { level: 1, size: "2xl", weight: "semibold", color: "primary", children: "Live Listing Not Found" }), _jsx(Text, { color: "muted", children: "The live listing you are looking for may have been removed." }), _jsx(Link, { href: String(ROUTES.PUBLIC.LIVE), className: "text-[length:var(--appkit-text-sm)] font-medium text-primary-600 hover:underline", children: "Browse Live Listings" })] }) }) }) }));
38
+ }
39
+ const p = product;
40
+ const currency = p.currency || getDefaultCurrency();
41
+ const title = String(p.title ?? p.name ?? "Live Listing");
42
+ const price = typeof p.price === "number" ? p.price : null;
43
+ const images = Array.isArray(p.images) ? p.images : typeof p.mainImage === "string" ? [p.mainImage] : [];
44
+ const productVideo = p.video;
45
+ const meta = product.liveItem;
46
+ const jurisdictions = meta?.jurisdictionAllowed ?? [];
47
+ const transport = meta?.transport;
48
+ const category = Array.isArray(p.categorySlugs) && p.categorySlugs.length > 0 ? String(p.categorySlugs[0]) : (typeof p.category === "string" ? p.category : null);
49
+ const categoryName = Array.isArray(p.categoryNames) && p.categoryNames.length > 0 ? String(p.categoryNames[0]) : (typeof p.categoryName === "string" ? p.categoryName : null);
50
+ const brand = typeof p.brand === "string" ? p.brand : undefined;
51
+ const storeName = typeof p.storeName === "string" ? p.storeName : null;
52
+ const safeSeller = storeName ? safeDisplayName(storeName, "") : null;
53
+ const storeSlug = (typeof p.storeSlug === "string" ? p.storeSlug : null) || (typeof p.storeId === "string" ? p.storeId : null);
54
+ const storeHref = storeSlug ? String(ROUTES.PUBLIC.STORE_DETAIL(storeSlug)) : null;
55
+ const specs = Array.isArray(p.specifications) ? p.specifications : [];
56
+ const customSections = Array.isArray(p.customSections) ? p.customSections : [];
57
+ const descriptionHtml = toDescriptionHtml(p.description);
58
+ const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups, reviews] = await Promise.all([
59
+ computeRelatedItems(product),
60
+ getGroupsWithItemsForProduct(product.id),
61
+ getReviewItemsForProduct(product.id),
62
+ ]);
63
+ return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: String(p.id ?? p.slug ?? ""), productType: "product", snapshot: {
64
+ title,
65
+ thumb: images[0],
66
+ price: price ?? undefined,
67
+ storeId: typeof p.storeId === "string" ? p.storeId : undefined,
68
+ storeName: storeName ?? undefined,
69
+ } }), _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.LIVE), className: CLS_BREADCRUMB_LINK, children: "Live" }), 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: () => (_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_LIVE_ITEM_BADGE, children: "Live Item" }), meta?.vendorVerified && (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Verified Seller" })), meta?.cites && (_jsxs(Span, { color: "warning", surface: "warning-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: ["CITES: ", meta.cites] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title }), meta?.species && (_jsxs(Text, { className: "italic mt-1", color: "muted", size: "sm", children: [meta.species, meta.sex && meta.sex !== "n/a" && ` · ${meta.sex}`, meta.ageMonths !== undefined && ` · ${meta.ageMonths}mo`] }))] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) })), jurisdictions.length > 0 && (_jsxs(Div, { className: CLS_WARN_BOX, children: [_jsx(Text, { className: CLS_WARN_TITLE, children: "Delivery restrictions" }), _jsxs(Text, { className: CLS_WARN_BODY, children: ["This item can only be shipped to: ", jurisdictions.join(", ")] })] })), transport && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Transport" }), _jsxs(Text, { className: "text-muted-foreground", children: ["Method: ", transport.method, transport.handlingFee !== undefined && ` · Handling: ${formatCurrency(transport.handlingFee, "INR")}`, transport.insuranceIncluded && " · Insurance included"] })] })), meta?.careInfo && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Care information" }), _jsx(Text, { className: "mt-1 text-muted-foreground", children: meta.careInfo })] })), 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, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", 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, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsList, { reviews: reviews, context: "listing", emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
70
+ id: s.id,
71
+ label: s.title,
72
+ content: _jsx(CustomSectionTabContent, { section: s }),
73
+ })) })), renderBuyBar: () => renderActions?.(product), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined })] })) })] })] }));
74
+ }
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import Image from "next/image";
4
- import { useState } from "react";
4
+ import { useEffect, useRef, useState } from "react";
5
5
  import { Div, Row, Span } from "../../ui";
6
6
  import { ROUNDED_MAP, SHADOW_MAP } from "../../ui/components/surface-tokens";
7
7
  import { resolveMediaUrl } from "../../utils/media-url";
@@ -25,6 +25,13 @@ const FALLBACK_ICONS = {
25
25
  gallery: "📦",
26
26
  avatar: "👤",
27
27
  };
28
+ // A freshly-uploaded media slug can legitimately 404 for a brief window
29
+ // right after upload — the Firestore doc write and the Storage object write
30
+ // don't land atomically (see src/app/api/media/[...slug]/route.ts's own
31
+ // comment on this race). Retry with backoff before latching the fallback
32
+ // icon, instead of failing permanently on the very first request.
33
+ const MAX_LOAD_RETRIES = 3;
34
+ const RETRY_BACKOFF_MS = [600, 1200, 2400];
28
35
  /** Resolve every URL in a comma-separated srcset string through the media proxy. */
29
36
  function resolveSrcSet(input) {
30
37
  return input
@@ -44,15 +51,39 @@ function resolveSrcSet(input) {
44
51
  export function MediaImage({ src, alt, size = "card", priority = false, loading, objectFit = "cover", fallback, className, rounded, shadow, style, sources, onError, }) {
45
52
  const [hasError, setHasError] = useState(false);
46
53
  const [isLoaded, setIsLoaded] = useState(false);
54
+ const [retryCount, setRetryCount] = useState(0);
55
+ const retryTimeoutRef = useRef(null);
47
56
  const icon = fallback ?? FALLBACK_ICONS[size];
48
57
  const fitClass = objectFit === "contain" ? "object-contain" : "object-cover";
49
58
  const resolvedSrc = resolveMediaUrl(src);
59
+ // Reset error/load/retry state whenever the resolved src actually changes.
60
+ // Without this, a component instance that hit a load failure once (e.g.
61
+ // lost the post-upload propagation race above) latches hasError forever,
62
+ // even across a later prop change to a src that would now succeed.
63
+ useEffect(() => {
64
+ setHasError(false);
65
+ setIsLoaded(false);
66
+ setRetryCount(0);
67
+ return () => {
68
+ if (retryTimeoutRef.current) {
69
+ clearTimeout(retryTimeoutRef.current);
70
+ retryTimeoutRef.current = null;
71
+ }
72
+ };
73
+ // eslint-disable-next-line react-hooks/exhaustive-deps
74
+ }, [resolvedSrc]);
50
75
  const wrapperExtra = [
51
76
  rounded ? ROUNDED_MAP[rounded] : "",
52
77
  shadow ? SHADOW_MAP[shadow] : "",
53
78
  className ?? "",
54
79
  ].filter(Boolean).join(" ");
55
80
  const handleError = () => {
81
+ if (retryCount < MAX_LOAD_RETRIES) {
82
+ retryTimeoutRef.current = setTimeout(() => {
83
+ setRetryCount((count) => count + 1);
84
+ }, RETRY_BACKOFF_MS[retryCount]);
85
+ return;
86
+ }
56
87
  setHasError(true);
57
88
  onError?.();
58
89
  };
@@ -62,15 +93,20 @@ export function MediaImage({ src, alt, size = "card", priority = false, loading,
62
93
  const isSvg = resolvedSrc.toLowerCase().endsWith(".svg") ||
63
94
  resolvedSrc.includes("image/svg") ||
64
95
  /[./]svg(\?|$)/i.test(resolvedSrc);
96
+ // Cache-bust the retried request so a browser-cached 404 response isn't
97
+ // silently re-served — the underlying URL is otherwise byte-identical.
98
+ const requestSrc = retryCount > 0
99
+ ? `${resolvedSrc}${resolvedSrc.includes("?") ? "&" : "?"}retry=${retryCount}`
100
+ : resolvedSrc;
65
101
  // Art-directed `<picture>` path — bypasses Next.js `<Image>` because the
66
102
  // browser must pick the matching `<source>` itself. Loading + decoding hints
67
103
  // come from the consumer's preset.
68
104
  if (sources && sources.length > 0) {
69
- return (_jsxs(Div, { className: `relative w-full h-full overflow-hidden${wrapperExtra ? ` ${wrapperExtra}` : ""}`, style: style, children: [!isLoaded && (_jsx(Div, { className: "absolute inset-0 animate-pulse", surface: "subtle", "aria-hidden": "true" })), _jsxs("picture", { children: [sources.map((source, index) => (_jsx("source", { srcSet: resolveSrcSet(source.srcSet), type: source.type, media: source.media }, `${source.media ?? ""}-${source.type ?? ""}-${index}`))), _jsx("img", { src: resolvedSrc, alt: alt, loading: loading ?? (priority ? "eager" : "lazy"), decoding: "async",
105
+ return (_jsxs(Div, { className: `relative w-full h-full overflow-hidden${wrapperExtra ? ` ${wrapperExtra}` : ""}`, style: style, children: [!isLoaded && (_jsx(Div, { className: "absolute inset-0 animate-pulse", surface: "subtle", "aria-hidden": "true" })), _jsxs("picture", { children: [sources.map((source, index) => (_jsx("source", { srcSet: resolveSrcSet(source.srcSet), type: source.type, media: source.media }, `${source.media ?? ""}-${source.type ?? ""}-${index}`))), _jsx("img", { src: requestSrc, alt: alt, loading: loading ?? (priority ? "eager" : "lazy"), decoding: "async",
70
106
  // catalogued primitive for media (variant catalogue allows the inner
71
107
  // <img> here). Object-fit + className come from typed props.
72
108
  className: `absolute inset-0 w-full h-full ${fitClass}`, onLoad: () => setIsLoaded(true), onError: handleError })] })] }));
73
109
  }
74
- return (_jsxs(Div, { className: `relative w-full h-full overflow-hidden${wrapperExtra ? ` ${wrapperExtra}` : ""}`, style: style, children: [!isLoaded && (_jsx(Div, { className: "absolute inset-0 animate-pulse", surface: "subtle", "aria-hidden": "true" })), _jsx(Image, { src: resolvedSrc, alt: alt, fill: true, priority: priority, loading: loading ?? (priority ? "eager" : "lazy"), className: fitClass, sizes: SIZE_HINTS[size], onLoad: () => setIsLoaded(true), onError: () => setHasError(true), unoptimized: isSvg })] }));
110
+ return (_jsxs(Div, { className: `relative w-full h-full overflow-hidden${wrapperExtra ? ` ${wrapperExtra}` : ""}`, style: style, children: [!isLoaded && (_jsx(Div, { className: "absolute inset-0 animate-pulse", surface: "subtle", "aria-hidden": "true" })), _jsx(Image, { src: requestSrc, alt: alt, fill: true, priority: priority, loading: loading ?? (priority ? "eager" : "lazy"), className: fitClass, sizes: SIZE_HINTS[size], onLoad: () => setIsLoaded(true), onError: handleError, unoptimized: isSvg })] }));
75
111
  }
76
112
  export default MediaImage;
@@ -2,13 +2,14 @@
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useRef, useEffect } from "react";
4
4
  import { Div, Row, Span } from "../../ui";
5
- import { resolveMediaUrl } from "../../utils/media-url";
5
+ import { resolveMediaUrl, getYouTubeVideoId } from "../../utils/media-url";
6
6
  import { useSiteSettings } from "../../core/hooks/useSiteSettings";
7
7
  export function MediaVideo({ src, thumbnailUrl, alt = "Video", controls = true, autoPlayMuted = false, loop = false, trimStart, trimEnd, objectFit = "cover", watermark, }) {
8
8
  const videoRef = useRef(null);
9
9
  const fitClass = objectFit === "contain" ? "object-contain" : "object-cover";
10
10
  const resolvedSrc = resolveMediaUrl(src);
11
11
  const resolvedPoster = resolveMediaUrl(thumbnailUrl);
12
+ const youtubeId = getYouTubeVideoId(src);
12
13
  // Apply trimStart on load
13
14
  useEffect(() => {
14
15
  const el = videoRef.current;
@@ -46,6 +47,21 @@ export function MediaVideo({ src, thumbnailUrl, alt = "Video", controls = true,
46
47
  // audit-color-pair-function-ok: deliberate faded placeholder-icon shade, not a readability regression
47
48
  className: "absolute inset-0 text-zinc-400 text-[length:var(--appkit-text-4xl)]", surface: "subtle", align: "center", justify: "center", role: "img", "aria-label": alt, children: _jsx(Span, { "aria-hidden": "true", children: "\uD83C\uDFAC" }) }));
48
49
  }
50
+ // A YouTube watch/share URL (MediaUploadField's "YouTube" tab writes one)
51
+ // is never a raw playable file — <video src> fails with "no supported
52
+ // format" for it. Render the iframe embed instead of the native player.
53
+ if (youtubeId) {
54
+ const embedParams = new URLSearchParams({ playsinline: "1", rel: "0" });
55
+ if (autoPlayMuted) {
56
+ embedParams.set("autoplay", "1");
57
+ embedParams.set("mute", "1");
58
+ }
59
+ if (loop) {
60
+ embedParams.set("loop", "1");
61
+ embedParams.set("playlist", youtubeId);
62
+ }
63
+ return (_jsx("iframe", { src: `https://www.youtube-nocookie.com/embed/${youtubeId}?${embedParams.toString()}`, title: alt, allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share", allowFullScreen: true, className: "absolute inset-0 w-full h-full border-0" }));
64
+ }
49
65
  return (_jsxs(_Fragment, { children: [_jsx("video", { ref: videoRef, src: resolvedSrc, poster: resolvedPoster, controls: controls, autoPlay: autoPlayMuted, muted: autoPlayMuted, loop: loop, playsInline: true, "aria-label": alt, className: `absolute inset-0 w-full h-full ${fitClass}` }), effectiveWatermark ? _jsx(MediaVideoWatermarkLayer, { config: effectiveWatermark }) : null] }));
50
66
  }
51
67
  /**
@@ -12,6 +12,13 @@ export interface MediaField {
12
12
  source?: MediaSource;
13
13
  /** YouTube video ID. Only set when source === "youtube". */
14
14
  youtubeId?: string;
15
+ /**
16
+ * Duration in seconds, for `type: "video"`. Captured automatically for
17
+ * direct uploads (off-DOM `<video>` element, see `video-poster.ts`) — not
18
+ * obtainable client-side for `source: "youtube" | "external"` without a
19
+ * server round-trip, so it stays undefined for those sources.
20
+ */
21
+ duration?: number;
15
22
  }
16
23
  export declare const mediaFieldSchema: z.ZodObject<{
17
24
  url: z.ZodString;
@@ -20,6 +27,7 @@ export declare const mediaFieldSchema: z.ZodObject<{
20
27
  thumbnailUrl: z.ZodOptional<z.ZodString>;
21
28
  source: z.ZodOptional<z.ZodEnum<["upload", "youtube", "external"]>>;
22
29
  youtubeId: z.ZodOptional<z.ZodString>;
30
+ duration: z.ZodOptional<z.ZodNumber>;
23
31
  }, "strip", z.ZodTypeAny, {
24
32
  type: "video" | "image" | "file";
25
33
  url: string;
@@ -27,6 +35,7 @@ export declare const mediaFieldSchema: z.ZodObject<{
27
35
  alt?: string | undefined;
28
36
  thumbnailUrl?: string | undefined;
29
37
  youtubeId?: string | undefined;
38
+ duration?: number | undefined;
30
39
  }, {
31
40
  type: "video" | "image" | "file";
32
41
  url: string;
@@ -34,6 +43,7 @@ export declare const mediaFieldSchema: z.ZodObject<{
34
43
  alt?: string | undefined;
35
44
  thumbnailUrl?: string | undefined;
36
45
  youtubeId?: string | undefined;
46
+ duration?: number | undefined;
37
47
  }>;
38
48
  export declare function coerceMediaField(value: MediaFieldInput, fallbackType?: MediaFieldType): MediaField | null;
39
49
  export declare function coerceMediaFieldArray(values: Array<MediaField | string> | null | undefined, fallbackType?: MediaFieldType): MediaField[];
@@ -7,6 +7,7 @@ export const mediaFieldSchema = z.object({
7
7
  thumbnailUrl: z.string().optional(),
8
8
  source: z.enum(["upload", "youtube", "external"]).optional(),
9
9
  youtubeId: z.string().optional(),
10
+ duration: z.number().optional(),
10
11
  });
11
12
  export function coerceMediaField(value, fallbackType = "image") {
12
13
  if (!value) {
@@ -28,6 +28,12 @@ export interface MediaUploadFieldProps {
28
28
  enableTrim?: boolean;
29
29
  enableThumbnail?: boolean;
30
30
  onThumbnailChange?: (url: string) => void;
31
+ /**
32
+ * Fired with the captured duration (seconds) for a direct video upload —
33
+ * mirrors `onThumbnailChange`. Never fires for the YouTube/External-URL
34
+ * tabs (no client-side way to read those without a server round-trip).
35
+ */
36
+ onDurationChange?: (duration: number) => void;
31
37
  /** Show a "YouTube URL" tab to embed a YouTube video by ID or URL. */
32
38
  showYoutube?: boolean;
33
39
  /** Show an "External URL" tab to link a third-party image/video URL. */
@@ -63,4 +69,4 @@ export interface MediaUploadFieldProps {
63
69
  */
64
70
  autoCapturePoster?: boolean;
65
71
  }
66
- export declare function MediaUploadField({ label, value, onChange, onChangeField, onUpload, kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled, helperText, captureSource, captureMode, enableTrim, enableThumbnail, onThumbnailChange, showYoutube, showExternal, onAbort, onStagedUrlsChange, isPersisted, multiple, onAddUrl, autoCapturePoster, }: MediaUploadFieldProps): import("react").JSX.Element;
72
+ export declare function MediaUploadField({ label, value, onChange, onChangeField, onUpload, kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled, helperText, captureSource, captureMode, enableTrim, enableThumbnail, onThumbnailChange, onDurationChange, showYoutube, showExternal, onAbort, onStagedUrlsChange, isPersisted, multiple, onAddUrl, autoCapturePoster, }: MediaUploadFieldProps): import("react").JSX.Element;
@@ -88,7 +88,7 @@ export function MediaUploadField({ label, value, onChange, onChangeField, onUplo
88
88
  // SB-UNI-Z5 2026-05-13 — `kind` derives accept/maxSizeMB when not set
89
89
  // explicitly; explicit props still win. "*" + 50 stay as the floor for
90
90
  // back-compat with callers that pass neither.
91
- kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText, captureSource = "both", captureMode = "photo", enableTrim = true, enableThumbnail = true, onThumbnailChange, showYoutube = true, showExternal = true, onAbort, onStagedUrlsChange, isPersisted = false, multiple = false, onAddUrl, autoCapturePoster = true, }) {
91
+ kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText, captureSource = "both", captureMode = "photo", enableTrim = true, enableThumbnail = true, onThumbnailChange, onDurationChange, showYoutube = true, showExternal = true, onAbort, onStagedUrlsChange, isPersisted = false, multiple = false, onAddUrl, autoCapturePoster = true, }) {
92
92
  const [isLoading, setIsLoading] = useState(false);
93
93
  const [error, setError] = useState(null);
94
94
  const [pendingVideoUrl, setPendingVideoUrl] = useState(null);
@@ -198,14 +198,18 @@ kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText
198
198
  : captureMode === "both"
199
199
  ? "image/*,video/*"
200
200
  : "image/*";
201
- const afterUpload = (url, fileType, autoPosterUrl) => {
201
+ const afterUpload = (url, fileType, autoPosterUrl, duration) => {
202
202
  onChangeField?.({
203
203
  url,
204
204
  type: inferMediaTypeFromMime(fileType, url),
205
+ source: "upload",
205
206
  ...(autoPosterUrl ? { thumbnailUrl: autoPosterUrl } : {}),
207
+ ...(duration ? { duration } : {}),
206
208
  });
207
209
  if (autoPosterUrl)
208
210
  onThumbnailChange?.(autoPosterUrl);
211
+ if (duration)
212
+ onDurationChange?.(duration);
209
213
  const isVideoFile = fileType.startsWith("video/");
210
214
  if (isVideoFile && enableTrim) {
211
215
  setPendingVideoUrl(url);
@@ -283,7 +287,7 @@ kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText
283
287
  const posterFile = posterBlobAsFile(poster, file.name);
284
288
  const posterUrl = await onUpload(posterFile);
285
289
  stageUrl(posterUrl);
286
- return posterUrl;
290
+ return { posterUrl, duration: poster.duration };
287
291
  }
288
292
  catch (err) {
289
293
  // The upload-poster step is best-effort. Log via normalizeError so any
@@ -309,8 +313,8 @@ kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText
309
313
  else {
310
314
  const result = await uploadSingleFile(files[0]);
311
315
  if (result) {
312
- const posterUrl = (await maybeCaptureVideoPoster(files[0])) ?? undefined;
313
- afterUpload(result.url, result.type, posterUrl);
316
+ const poster = await maybeCaptureVideoPoster(files[0]);
317
+ afterUpload(result.url, result.type, poster?.posterUrl, poster?.duration);
314
318
  }
315
319
  }
316
320
  }
@@ -353,8 +357,8 @@ kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText
353
357
  try {
354
358
  const url = await onUpload(file);
355
359
  stageUrl(url);
356
- const posterUrl = (await maybeCaptureVideoPoster(file)) ?? undefined;
357
- afterUpload(url, blob.type || mimeType, posterUrl);
360
+ const poster = await maybeCaptureVideoPoster(file);
361
+ afterUpload(url, blob.type || mimeType, poster?.posterUrl, poster?.duration);
358
362
  }
359
363
  catch (err) {
360
364
  void normalizeError(err);
@@ -26,6 +26,8 @@ export interface ExtractedPoster {
26
26
  /** Frame dimensions in pixels (also the JPEG's pixel dimensions). */
27
27
  width: number;
28
28
  height: number;
29
+ /** Video duration in seconds, read from the same off-DOM element. */
30
+ duration: number;
29
31
  }
30
32
  /**
31
33
  * Build a deterministic filename for the captured poster. We mirror the
@@ -25,6 +25,20 @@ const TARGET_FRAME_TIME_SECONDS = 0.5;
25
25
  const JPEG_QUALITY = 0.85;
26
26
  const LOAD_TIMEOUT_MS = 10000;
27
27
  const POSTER_MIME = "image/jpeg";
28
+ /** Bounded wait for `video.duration` to become a finite, positive number. */
29
+ async function readDuration(video) {
30
+ if (Number.isFinite(video.duration) && video.duration > 0)
31
+ return video.duration;
32
+ return new Promise((resolve) => {
33
+ const timer = window.setTimeout(() => resolve(0), 2000);
34
+ video.addEventListener("durationchange", () => {
35
+ if (Number.isFinite(video.duration) && video.duration > 0) {
36
+ window.clearTimeout(timer);
37
+ resolve(video.duration);
38
+ }
39
+ }, { once: true });
40
+ });
41
+ }
28
42
  /**
29
43
  * Build a deterministic filename for the captured poster. We mirror the
30
44
  * source video's basename so downstream code can correlate the two assets
@@ -86,6 +100,7 @@ export async function extractVideoPosterFrame(file) {
86
100
  const height = video.videoHeight;
87
101
  if (!width || !height)
88
102
  return null;
103
+ const duration = await readDuration(video);
89
104
  const canvas = document.createElement("canvas");
90
105
  canvas.width = width;
91
106
  canvas.height = height;
@@ -96,7 +111,7 @@ export async function extractVideoPosterFrame(file) {
96
111
  const blob = await new Promise((resolve) => canvas.toBlob(resolve, POSTER_MIME, JPEG_QUALITY));
97
112
  if (!blob)
98
113
  return null;
99
- return { blob, width, height };
114
+ return { blob, width, height, duration };
100
115
  }
101
116
  catch (_err) {
102
117
  void normalizeError(_err);
@@ -19,7 +19,7 @@ describe("posterBlobAsFile", () => {
19
19
  const blob = new Blob([new Uint8Array([0xff, 0xd8, 0xff])], {
20
20
  type: "image/jpeg",
21
21
  });
22
- const file = posterBlobAsFile({ blob, width: 320, height: 180 }, "promo-reel.mp4");
22
+ const file = posterBlobAsFile({ blob, width: 320, height: 180, duration: 12.5 }, "promo-reel.mp4");
23
23
  expect(file).toBeInstanceOf(File);
24
24
  expect(file.name).toBe("promo-reel.poster.jpg");
25
25
  expect(file.type).toBe("image/jpeg");
@@ -0,0 +1,12 @@
1
+ export interface OrderStatusTimelineProps {
2
+ orderStatus: string;
3
+ orderDate?: string;
4
+ shippingDate?: string;
5
+ deliveryDate?: string;
6
+ cancellationDate?: string;
7
+ trackingNumber?: string;
8
+ shippingCarrier?: string;
9
+ trackingUrl?: string;
10
+ className?: string;
11
+ }
12
+ export declare function OrderStatusTimeline({ orderStatus, orderDate, shippingDate, deliveryDate, cancellationDate, trackingNumber, shippingCarrier, trackingUrl, className, }: OrderStatusTimelineProps): import("react").JSX.Element;
@@ -0,0 +1,55 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * OrderStatusTimeline — a genuine (not fabricated/estimated) status timeline
5
+ * built from the real per-status timestamps already written by the
6
+ * order-status-update path (`orderDate`/`shippingDate`/`deliveryDate`/
7
+ * `cancellationDate`). No live courier-API integration — `trackingUrl` is
8
+ * just the passthrough link a seller/admin entered, rendered as an external
9
+ * link that opens in a new tab. Statuses with no dedicated date field
10
+ * (confirmed/processing/return_requested/returned/refunded) render as a step
11
+ * without a timestamp rather than showing a made-up date.
12
+ */
13
+ import { Anchor, Badge, Div, Heading, Row, Stack, Text } from "../../../ui";
14
+ const STAGE_SEQUENCE = ["pending", "confirmed", "processing", "shipped", "delivered"];
15
+ const STAGE_LABELS = {
16
+ pending: "Order placed",
17
+ confirmed: "Confirmed",
18
+ processing: "Processing",
19
+ shipped: "Shipped",
20
+ delivered: "Delivered",
21
+ cancelled: "Cancelled",
22
+ return_requested: "Return requested",
23
+ returned: "Returned",
24
+ refunded: "Refunded",
25
+ };
26
+ function buildSteps(props) {
27
+ const status = (props.orderStatus ?? "").toLowerCase();
28
+ const currentIndex = STAGE_SEQUENCE.indexOf(status);
29
+ const steps = [{ key: "pending", label: STAGE_LABELS.pending, date: props.orderDate }];
30
+ if ((status === "confirmed" || status === "processing") && !props.shippingDate) {
31
+ steps.push({ key: status, label: STAGE_LABELS[status] });
32
+ }
33
+ if (props.shippingDate || currentIndex >= 3) {
34
+ steps.push({ key: "shipped", label: STAGE_LABELS.shipped, date: props.shippingDate });
35
+ }
36
+ if (props.deliveryDate || status === "delivered") {
37
+ steps.push({ key: "delivered", label: STAGE_LABELS.delivered, date: props.deliveryDate });
38
+ }
39
+ if (status === "cancelled" || props.cancellationDate) {
40
+ steps.push({ key: "cancelled", label: STAGE_LABELS.cancelled, date: props.cancellationDate });
41
+ }
42
+ else if (status === "return_requested" || status === "returned" || status === "refunded") {
43
+ steps.push({ key: status, label: STAGE_LABELS[status] ?? status });
44
+ }
45
+ return steps;
46
+ }
47
+ export function OrderStatusTimeline({ orderStatus, orderDate, shippingDate, deliveryDate, cancellationDate, trackingNumber, shippingCarrier, trackingUrl, className = "", }) {
48
+ const steps = buildSteps({ orderStatus, orderDate, shippingDate, deliveryDate, cancellationDate });
49
+ const isTerminalNegative = steps[steps.length - 1]?.key === "cancelled";
50
+ return (_jsxs(Div, { className: className, rounded: "xl", padding: "md", surface: "muted", border: "default", children: [_jsx(Heading, { level: 3, size: "sm", weight: "semibold", color: "muted", className: "mb-3", children: "Tracking" }), _jsx(Stack, { gap: "none", children: steps.map((step, i) => {
51
+ const isLast = i === steps.length - 1;
52
+ const isNegative = step.key === "cancelled";
53
+ return (_jsxs(Row, { gap: "sm", padding: isLast ? "none" : "b-md", children: [_jsxs(Stack, { gap: "none", align: "center", className: "shrink-0", children: [_jsx(Div, { className: `h-2.5 w-2.5 ${isNegative ? "" : "bg-[var(--appkit-color-primary)]"}`, rounded: "full", surface: isNegative ? "danger-surface" : "none" }), !isLast && _jsx(Div, { className: "w-px flex-1 min-h-[24px]", surface: "subtle" })] }), _jsxs(Stack, { gap: "none", className: "min-w-0 flex-1", children: [_jsx(Text, { size: "sm", weight: "medium", className: isNegative ? "text-error" : undefined, children: step.label }), _jsx(Text, { size: "xs", color: "muted", children: step.date ? new Date(step.date).toLocaleString() : "—" })] })] }, step.key));
54
+ }) }), (trackingNumber || shippingCarrier || trackingUrl) && !isTerminalNegative && (_jsx(Div, { className: "mt-3 border-t border-[var(--appkit-color-border)]", padding: "t-sm", children: _jsxs(Stack, { gap: "xs", children: [shippingCarrier && (_jsxs(Row, { justify: "between", children: [_jsx(Text, { size: "sm", color: "muted", children: "Carrier" }), _jsx(Text, { size: "sm", weight: "medium", children: shippingCarrier })] })), trackingNumber && (_jsxs(Row, { justify: "between", children: [_jsx(Text, { size: "sm", color: "muted", children: "Tracking number" }), _jsx(Text, { size: "sm", weight: "medium", children: trackingNumber })] })), trackingUrl && (_jsx(Row, { justify: "end", children: _jsx(Anchor, { href: trackingUrl, tone: "brand", weight: "semibold", size: "sm", children: "Track with carrier \u2192" }) }))] }) })), !trackingNumber && !trackingUrl && !isTerminalNegative && (_jsx(Badge, { variant: "default", className: "mt-3", children: "No tracking details yet" }))] }));
55
+ }
@@ -6,6 +6,8 @@ export { OrderSiblingPayments } from "./OrderSiblingPayments";
6
6
  export type { OrderSiblingPaymentsProps } from "./OrderSiblingPayments";
7
7
  export { OrderPaymentSummary } from "./OrderPaymentSummary";
8
8
  export type { OrderPaymentSummaryProps } from "./OrderPaymentSummary";
9
+ export { OrderStatusTimeline } from "./OrderStatusTimeline";
10
+ export type { OrderStatusTimelineProps } from "./OrderStatusTimeline";
9
11
  export { RefundHistoryTable } from "./RefundHistoryTable";
10
12
  export type { RefundHistoryTableProps } from "./RefundHistoryTable";
11
13
  export { RefundRequestView } from "./RefundRequestView";
@@ -3,5 +3,6 @@ export { MarketplaceOrderCard, } from "./MarketplaceOrderCard";
3
3
  export { OrderFilters, ORDER_FILTER_KEYS, ORDER_ADMIN_SORT_OPTIONS, ORDER_SELLER_SORT_OPTIONS, ORDER_USER_SORT_OPTIONS, getOrderFilterKeys, getOrderSortOptions, } from "./OrderFilters";
4
4
  export { OrderSiblingPayments } from "./OrderSiblingPayments";
5
5
  export { OrderPaymentSummary } from "./OrderPaymentSummary";
6
+ export { OrderStatusTimeline } from "./OrderStatusTimeline";
6
7
  export { RefundHistoryTable } from "./RefundHistoryTable";
7
8
  export { RefundRequestView } from "./RefundRequestView";