@mohasinac/appkit 4.62.0 → 4.63.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 (391) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +8 -29
  3. package/dist/_internal/client/features/layout/filterNavItems.d.ts +28 -0
  4. package/dist/_internal/client/features/layout/filterNavItems.js +35 -0
  5. package/dist/_internal/server/features/ads/booking.d.ts +118 -0
  6. package/dist/_internal/server/features/ads/booking.js +326 -0
  7. package/dist/_internal/server/features/checkout/actions.d.ts +129 -9
  8. package/dist/_internal/server/features/checkout/actions.js +850 -79
  9. package/dist/_internal/server/features/checkout/expand-order-items.d.ts +1 -2
  10. package/dist/_internal/server/features/checkout/expand-order-items.js +32 -0
  11. package/dist/_internal/server/features/checkout/freeze-parts.d.ts +50 -0
  12. package/dist/_internal/server/features/checkout/freeze-parts.js +94 -0
  13. package/dist/_internal/server/features/checkout/value-otp-gate.d.ts +55 -0
  14. package/dist/_internal/server/features/checkout/value-otp-gate.js +60 -0
  15. package/dist/_internal/server/features/dashboard/attention.d.ts +128 -0
  16. package/dist/_internal/server/features/dashboard/attention.js +183 -0
  17. package/dist/_internal/server/features/dashboard/data.d.ts +47 -0
  18. package/dist/_internal/server/features/dashboard/data.js +82 -0
  19. package/dist/_internal/server/features/inventory/reservations.d.ts +99 -0
  20. package/dist/_internal/server/features/inventory/reservations.js +273 -0
  21. package/dist/_internal/server/features/invoicing/allocate.d.ts +63 -0
  22. package/dist/_internal/server/features/invoicing/allocate.js +162 -0
  23. package/dist/_internal/server/features/invoicing/credit-note.d.ts +57 -0
  24. package/dist/_internal/server/features/invoicing/credit-note.js +126 -0
  25. package/dist/_internal/server/features/invoicing/gstr1.d.ts +74 -0
  26. package/dist/_internal/server/features/invoicing/gstr1.js +251 -0
  27. package/dist/_internal/server/features/invoicing/issue.d.ts +96 -0
  28. package/dist/_internal/server/features/invoicing/issue.js +202 -0
  29. package/dist/_internal/server/features/offers/destinations.d.ts +40 -0
  30. package/dist/_internal/server/features/offers/destinations.js +65 -0
  31. package/dist/_internal/server/features/orders/actions.d.ts +8 -1
  32. package/dist/_internal/server/features/orders/actions.js +81 -8
  33. package/dist/_internal/server/features/orders/adapters.js +54 -0
  34. package/dist/_internal/server/features/orders/amend-delivery.d.ts +56 -0
  35. package/dist/_internal/server/features/orders/amend-delivery.js +124 -0
  36. package/dist/_internal/server/features/orders/balance.d.ts +89 -0
  37. package/dist/_internal/server/features/orders/balance.js +256 -0
  38. package/dist/_internal/server/features/orders/cancellation-refund.d.ts +15 -9
  39. package/dist/_internal/server/features/orders/cancellation-refund.js +15 -9
  40. package/dist/_internal/server/features/points/award.js +46 -4
  41. package/dist/_internal/server/features/pricing/actions.d.ts +58 -0
  42. package/dist/_internal/server/features/pricing/actions.js +144 -0
  43. package/dist/_internal/server/features/refunds/actions.js +26 -0
  44. package/dist/_internal/server/features/shipping/book-split.d.ts +86 -0
  45. package/dist/_internal/server/features/shipping/book-split.js +130 -0
  46. package/dist/_internal/server/features/shipping/book.d.ts +61 -0
  47. package/dist/_internal/server/features/shipping/book.js +176 -0
  48. package/dist/_internal/server/features/shipping/courier-block.d.ts +69 -0
  49. package/dist/_internal/server/features/shipping/courier-block.js +264 -0
  50. package/dist/_internal/server/features/site-settings/adapters.d.ts +2 -2
  51. package/dist/_internal/server/features/site-settings/adapters.js +29 -0
  52. package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
  53. package/dist/_internal/server/features/stores/adapters.js +65 -0
  54. package/dist/_internal/server/functions/scheduled.d.ts +2 -1
  55. package/dist/_internal/server/functions/scheduled.js +95 -2
  56. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +11 -2
  57. package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +4 -0
  58. package/dist/_internal/server/jobs/core/dailyStatusDigest.js +19 -2
  59. package/dist/_internal/server/jobs/core/index.d.ts +2 -1
  60. package/dist/_internal/server/jobs/core/index.js +2 -1
  61. package/dist/_internal/server/jobs/core/jobRunners.js +45 -0
  62. package/dist/_internal/server/jobs/core/onJobCreated.d.ts +30 -7
  63. package/dist/_internal/server/jobs/core/onJobCreated.js +78 -45
  64. package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +7 -0
  65. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +65 -0
  66. package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +33 -0
  67. package/dist/_internal/server/jobs/core/reservationExpiry.d.ts +17 -0
  68. package/dist/_internal/server/jobs/core/reservationExpiry.js +40 -0
  69. package/dist/_internal/server/jobs/core/runJobOnce.d.ts +48 -0
  70. package/dist/_internal/server/jobs/core/runJobOnce.js +195 -0
  71. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +25 -2
  72. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
  73. package/dist/_internal/server/jobs/handlers/index.js +1 -0
  74. package/dist/_internal/server/jobs/handlers/onOrderStatusChange.js +17 -0
  75. package/dist/_internal/server/jobs/handlers/reservationExpiry.d.ts +2 -0
  76. package/dist/_internal/server/jobs/handlers/reservationExpiry.js +2 -0
  77. package/dist/_internal/server/notifications/channel-health.d.ts +26 -3
  78. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  79. package/dist/_internal/shared/actions/action-registry.js +296 -0
  80. package/dist/_internal/shared/checkout/coupon-discount.d.ts +43 -1
  81. package/dist/_internal/shared/checkout/coupon-discount.js +26 -1
  82. package/dist/_internal/shared/checkout/order-math.d.ts +174 -0
  83. package/dist/_internal/shared/checkout/order-math.js +159 -0
  84. package/dist/_internal/shared/checkout/refundable-base.d.ts +25 -0
  85. package/dist/_internal/shared/checkout/refundable-base.js +13 -1
  86. package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
  87. package/dist/_internal/shared/checkout/rules/index.js +1 -1
  88. package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +18 -0
  89. package/dist/_internal/shared/checkout/rules/payment-gate.js +25 -0
  90. package/dist/_internal/shared/checkout/split.d.ts +75 -0
  91. package/dist/_internal/shared/checkout/split.js +120 -0
  92. package/dist/_internal/shared/content/disclaimers.d.ts +88 -0
  93. package/dist/_internal/shared/content/disclaimers.js +217 -0
  94. package/dist/_internal/shared/content/published-values.d.ts +93 -0
  95. package/dist/_internal/shared/content/published-values.js +206 -0
  96. package/dist/_internal/shared/features/admin/integrations.d.ts +139 -0
  97. package/dist/_internal/shared/features/admin/integrations.js +263 -0
  98. package/dist/_internal/shared/features/dashboard/hub-sections.d.ts +72 -0
  99. package/dist/_internal/shared/features/dashboard/hub-sections.js +75 -0
  100. package/dist/_internal/shared/features/emi/choice.d.ts +98 -0
  101. package/dist/_internal/shared/features/emi/choice.js +69 -0
  102. package/dist/_internal/shared/features/emi/group.d.ts +84 -0
  103. package/dist/_internal/shared/features/emi/group.js +126 -0
  104. package/dist/_internal/shared/features/ledger/posting.d.ts +107 -0
  105. package/dist/_internal/shared/features/ledger/posting.js +165 -0
  106. package/dist/_internal/shared/features/ledger/reports.d.ts +76 -0
  107. package/dist/_internal/shared/features/ledger/reports.js +127 -0
  108. package/dist/_internal/shared/features/ledger/rules.d.ts +170 -0
  109. package/dist/_internal/shared/features/ledger/rules.js +221 -0
  110. package/dist/_internal/shared/features/offers/coordination.d.ts +122 -0
  111. package/dist/_internal/shared/features/offers/coordination.js +80 -0
  112. package/dist/_internal/shared/features/orders/delivery-snapshot.d.ts +114 -0
  113. package/dist/_internal/shared/features/orders/delivery-snapshot.js +116 -0
  114. package/dist/_internal/shared/features/orders/dispute-reasons.d.ts +113 -0
  115. package/dist/_internal/shared/features/orders/dispute-reasons.js +181 -0
  116. package/dist/_internal/shared/features/orders/refund-copy.d.ts +1 -1
  117. package/dist/_internal/shared/features/orders/refund-copy.js +18 -1
  118. package/dist/_internal/shared/features/payouts/pay-period.d.ts +91 -0
  119. package/dist/_internal/shared/features/payouts/pay-period.js +104 -0
  120. package/dist/_internal/shared/features/points/accepts.d.ts +29 -0
  121. package/dist/_internal/shared/features/points/accepts.js +49 -0
  122. package/dist/_internal/shared/features/pricing/rate-registry.d.ts +150 -0
  123. package/dist/_internal/shared/features/pricing/rate-registry.js +246 -0
  124. package/dist/_internal/shared/features/shipping/parcel.d.ts +123 -0
  125. package/dist/_internal/shared/features/shipping/parcel.js +139 -0
  126. package/dist/_internal/shared/features/shipping/shared-purchase.d.ts +91 -0
  127. package/dist/_internal/shared/features/shipping/shared-purchase.js +108 -0
  128. package/dist/_internal/shared/features/site-settings/fees.d.ts +16 -8
  129. package/dist/_internal/shared/features/site-settings/fees.js +12 -8
  130. package/dist/_internal/shared/features/site-settings/types.d.ts +11 -0
  131. package/dist/_internal/shared/fees/calculator.d.ts +33 -1
  132. package/dist/_internal/shared/fees/calculator.js +32 -2
  133. package/dist/_internal/shared/fees/explain.d.ts +81 -0
  134. package/dist/_internal/shared/fees/explain.js +101 -0
  135. package/dist/_internal/shared/history/index.d.ts +1 -1
  136. package/dist/_internal/shared/history/index.js +1 -1
  137. package/dist/_internal/shared/history/types.d.ts +7 -1
  138. package/dist/_internal/shared/history/types.js +25 -0
  139. package/dist/client.d.ts +21 -0
  140. package/dist/client.js +43 -0
  141. package/dist/constants/api-endpoints.d.ts +114 -0
  142. package/dist/constants/api-endpoints.js +55 -0
  143. package/dist/constants/field-names.d.ts +42 -0
  144. package/dist/constants/field-names.js +52 -0
  145. package/dist/contracts/index.d.ts +1 -1
  146. package/dist/contracts/shipping.d.ts +45 -0
  147. package/dist/contracts/shipping.js +15 -0
  148. package/dist/features/about/components/AboutView.js +1 -1
  149. package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -1
  150. package/dist/features/account/components/UserOrdersView.js +47 -35
  151. package/dist/features/admin/components/AdminCouponEditorView.js +17 -0
  152. package/dist/features/admin/components/AdminIntegrationsView.d.ts +55 -0
  153. package/dist/features/admin/components/AdminIntegrationsView.js +74 -0
  154. package/dist/features/admin/components/AdminInvoicingView.d.ts +2 -0
  155. package/dist/features/admin/components/AdminInvoicingView.js +65 -0
  156. package/dist/features/admin/components/AdminOrderEditorView.d.ts +33 -1
  157. package/dist/features/admin/components/AdminOrderEditorView.js +81 -3
  158. package/dist/features/admin/components/AdminOrdersGuideView.js +23 -2
  159. package/dist/features/admin/components/AdminOrdersView.js +86 -5
  160. package/dist/features/admin/components/OwnerQuickActions.js +24 -2
  161. package/dist/features/admin/constants/filter-tabs.d.ts +6 -0
  162. package/dist/features/admin/constants/filter-tabs.js +6 -0
  163. package/dist/features/admin/constants/site-settings-tabs.d.ts +1 -1
  164. package/dist/features/admin/hooks/useAdminListingData.js +16 -0
  165. package/dist/features/admin/schemas/admin-user-form.d.ts +2 -2
  166. package/dist/features/admin/schemas/firestore.d.ts +62 -0
  167. package/dist/features/admin/schemas/firestore.js +5 -0
  168. package/dist/features/ads/schemas/booking-write.d.ts +120 -0
  169. package/dist/features/ads/schemas/booking-write.js +75 -0
  170. package/dist/features/ads/schemas/creative-form.d.ts +18 -0
  171. package/dist/features/ads/schemas/creative-form.js +51 -0
  172. package/dist/features/ads/schemas/firestore.d.ts +140 -0
  173. package/dist/features/ads/schemas/firestore.js +109 -0
  174. package/dist/features/auctions/repository/bid.repository.js +3 -1
  175. package/dist/features/audit-log/schemas/firestore.d.ts +140 -0
  176. package/dist/features/audit-log/schemas/firestore.js +89 -0
  177. package/dist/features/auth/actions/profile-actions.d.ts +69 -1
  178. package/dist/features/auth/actions/profile-actions.js +62 -1
  179. package/dist/features/auth/permissions/constants.js +13 -0
  180. package/dist/features/auth/schemas/firestore.d.ts +16 -2
  181. package/dist/features/auth/schemas/firestore.js +2 -1
  182. package/dist/features/blog/actions/blog-actions.d.ts +6 -0
  183. package/dist/features/blog/actions/blog-actions.js +17 -2
  184. package/dist/features/blog/api/route.js +4 -1
  185. package/dist/features/blog/columns/index.js +5 -0
  186. package/dist/features/blog/components/BlogIndexPageView.js +2 -1
  187. package/dist/features/blog/repository/blog.repository.d.ts +4 -0
  188. package/dist/features/blog/repository/blog.repository.js +5 -0
  189. package/dist/features/blog/schemas/blog-form.d.ts +3 -0
  190. package/dist/features/blog/schemas/blog-form.js +1 -0
  191. package/dist/features/blog/schemas/firestore.d.ts +38 -1
  192. package/dist/features/blog/schemas/firestore.js +23 -0
  193. package/dist/features/blog/schemas/index.d.ts +6 -0
  194. package/dist/features/blog/schemas/index.js +2 -0
  195. package/dist/features/blog/types/index.d.ts +2 -0
  196. package/dist/features/cart/components/CartPriceBreakdown.d.ts +25 -1
  197. package/dist/features/cart/components/CartPriceBreakdown.js +29 -6
  198. package/dist/features/cart/components/PointsTenderPicker.d.ts +61 -0
  199. package/dist/features/cart/components/PointsTenderPicker.js +15 -0
  200. package/dist/features/cart/components/StoreAddonsPicker.js +2 -1
  201. package/dist/features/cart/components/index.d.ts +2 -0
  202. package/dist/features/cart/components/index.js +1 -0
  203. package/dist/features/cart/repository/cart.repository.d.ts +33 -0
  204. package/dist/features/cart/repository/cart.repository.js +47 -0
  205. package/dist/features/cart/schemas/firestore.d.ts +116 -0
  206. package/dist/features/contact/email.d.ts +27 -10
  207. package/dist/features/contact/email.js +20 -3
  208. package/dist/features/dashboard/components/DashboardHubView.d.ts +83 -0
  209. package/dist/features/dashboard/components/DashboardHubView.js +56 -0
  210. package/dist/features/dashboard/components/DashboardSectionsClient.d.ts +23 -0
  211. package/dist/features/dashboard/components/DashboardSectionsClient.js +66 -0
  212. package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.d.ts +55 -0
  213. package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.js +175 -0
  214. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +62 -0
  215. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +108 -0
  216. package/dist/features/dispute-reasons/schemas/firestore.d.ts +62 -0
  217. package/dist/features/dispute-reasons/schemas/firestore.js +40 -0
  218. package/dist/features/events/components/EventSaleBanner.js +1 -1
  219. package/dist/features/faq/components/FAQAccordion.d.ts +13 -1
  220. package/dist/features/faq/components/FAQAccordion.js +29 -4
  221. package/dist/features/faq/components/FAQPageContent.js +2 -2
  222. package/dist/features/gateway-events/schemas/index.d.ts +2 -2
  223. package/dist/features/homepage/schemas/index.d.ts +8 -8
  224. package/dist/features/inventory/schemas/stock-reservation.d.ts +114 -0
  225. package/dist/features/inventory/schemas/stock-reservation.js +103 -0
  226. package/dist/features/invoicing/schemas/firestore.d.ts +112 -0
  227. package/dist/features/invoicing/schemas/firestore.js +45 -0
  228. package/dist/features/invoicing/utils/financial-year.d.ts +43 -0
  229. package/dist/features/invoicing/utils/financial-year.js +53 -0
  230. package/dist/features/jobs/actions/enqueue-job.js +18 -1
  231. package/dist/features/jobs/repository/jobs.repository.d.ts +62 -1
  232. package/dist/features/jobs/repository/jobs.repository.js +84 -1
  233. package/dist/features/jobs/schemas/firestore.d.ts +80 -0
  234. package/dist/features/jobs/schemas/firestore.js +37 -0
  235. package/dist/features/layout/AppLayoutShell.d.ts +9 -1
  236. package/dist/features/layout/AppLayoutShell.js +12 -4
  237. package/dist/features/layout/BottomActions.js +2 -1
  238. package/dist/features/layout/BottomNavbar.d.ts +10 -2
  239. package/dist/features/layout/SideActionDock.js +52 -2
  240. package/dist/features/layout/TitleBarLayout.js +13 -4
  241. package/dist/features/layout/TitleBarLayout.style.css +59 -0
  242. package/dist/features/layout/useCtaReveal.d.ts +16 -1
  243. package/dist/features/layout/useCtaReveal.js +17 -26
  244. package/dist/features/ledger/repository/journal.repository.d.ts +110 -0
  245. package/dist/features/ledger/repository/journal.repository.js +312 -0
  246. package/dist/features/ledger/schemas/expenses.d.ts +87 -0
  247. package/dist/features/ledger/schemas/expenses.js +27 -0
  248. package/dist/features/ledger/schemas/firestore.d.ts +183 -0
  249. package/dist/features/ledger/schemas/firestore.js +69 -0
  250. package/dist/features/lottery/schemas/zod.d.ts +6 -6
  251. package/dist/features/media/MediaImage.js +22 -1
  252. package/dist/features/media/upload/ImageUpload.js +1 -1
  253. package/dist/features/obligations/constants/policies.js +24 -1
  254. package/dist/features/orders/components/BalancePaymentPanel.d.ts +12 -0
  255. package/dist/features/orders/components/BalancePaymentPanel.js +94 -0
  256. package/dist/features/orders/components/OrderPaymentSummary.js +10 -1
  257. package/dist/features/orders/components/OrderProvenanceCard.d.ts +8 -0
  258. package/dist/features/orders/components/OrderProvenanceCard.js +185 -0
  259. package/dist/features/orders/components/RaiseDisputeForm.d.ts +14 -0
  260. package/dist/features/orders/components/RaiseDisputeForm.js +119 -0
  261. package/dist/features/orders/components/RefundRequestView.js +19 -1
  262. package/dist/features/orders/components/index.d.ts +4 -0
  263. package/dist/features/orders/components/index.js +2 -0
  264. package/dist/features/orders/constants/order-lane-tabs.d.ts +69 -0
  265. package/dist/features/orders/constants/order-lane-tabs.js +88 -0
  266. package/dist/features/orders/repository/orders.repository.d.ts +85 -1
  267. package/dist/features/orders/repository/orders.repository.js +205 -6
  268. package/dist/features/orders/schemas/balance.d.ts +40 -0
  269. package/dist/features/orders/schemas/balance.js +47 -0
  270. package/dist/features/orders/schemas/firestore.d.ts +398 -1
  271. package/dist/features/orders/schemas/firestore.js +43 -0
  272. package/dist/features/orders/schemas/index.d.ts +56 -8
  273. package/dist/features/orders/schemas/index.js +12 -0
  274. package/dist/features/orders/types/index.d.ts +47 -0
  275. package/dist/features/orders/utils/courier-block.d.ts +25 -0
  276. package/dist/features/orders/utils/courier-block.js +23 -0
  277. package/dist/features/payment-cases/schemas/firestore.d.ts +26 -1
  278. package/dist/features/payment-cases/schemas/firestore.js +1 -0
  279. package/dist/features/payments/repository/payout.repository.js +3 -2
  280. package/dist/features/payments/schemas/firestore.d.ts +92 -1
  281. package/dist/features/payments/schemas/firestore.js +23 -0
  282. package/dist/features/pricing/components/AdminPricingView.d.ts +13 -0
  283. package/dist/features/pricing/components/AdminPricingView.js +157 -0
  284. package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
  285. package/dist/features/products/components/ProductDetailPageView.d.ts +17 -1
  286. package/dist/features/products/components/ProductDetailPageView.js +2 -2
  287. package/dist/features/products/schemas/firestore.d.ts +26 -1
  288. package/dist/features/products/schemas/firestore.js +2 -0
  289. package/dist/features/products/schemas/index.d.ts +2 -2
  290. package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
  291. package/dist/features/promotions/actions/coupon-stacking.d.ts +30 -3
  292. package/dist/features/promotions/actions/coupon-stacking.js +21 -2
  293. package/dist/features/promotions/components/CouponCard.js +13 -0
  294. package/dist/features/promotions/constants/coupon-help.d.ts +2 -2
  295. package/dist/features/promotions/constants/coupon-help.js +9 -1
  296. package/dist/features/promotions/schemas/firestore.d.ts +24 -0
  297. package/dist/features/promotions/schemas/firestore.js +4 -0
  298. package/dist/features/promotions/types/index.d.ts +19 -1
  299. package/dist/features/reviews/repository/reviews.repository.d.ts +14 -0
  300. package/dist/features/reviews/repository/reviews.repository.js +16 -0
  301. package/dist/features/scams/components/ScamProfileView.js +2 -2
  302. package/dist/features/scams/components/ScamRegistryView.js +2 -2
  303. package/dist/features/scams/repository/scammer.repository.js +2 -1
  304. package/dist/features/scams/schemas/firestore.d.ts +2 -1
  305. package/dist/features/scams/schemas/firestore.js +2 -1
  306. package/dist/features/search/action-index/tab-entries.js +1 -1
  307. package/dist/features/seller/actions/offer-actions.js +56 -0
  308. package/dist/features/seller/actions/seller-actions.d.ts +1 -1
  309. package/dist/features/seller/actions/seller-actions.js +37 -3
  310. package/dist/features/seller/components/DispatchForm.d.ts +11 -0
  311. package/dist/features/seller/components/DispatchForm.js +190 -0
  312. package/dist/features/seller/components/SellerOrdersView.js +221 -27
  313. package/dist/features/seller/components/index.d.ts +1 -0
  314. package/dist/features/seller/components/index.js +1 -0
  315. package/dist/features/seller/hooks/useSellerListingData.js +25 -0
  316. package/dist/features/seller/repository/offer.repository.js +4 -3
  317. package/dist/features/seller/schemas/firestore.d.ts +23 -2
  318. package/dist/features/seller/schemas/firestore.js +3 -2
  319. package/dist/features/seller/schemas/index.d.ts +4 -4
  320. package/dist/features/seller/schemas/order-forms.d.ts +61 -4
  321. package/dist/features/seller/schemas/order-forms.js +41 -8
  322. package/dist/features/shipments/schemas/firestore.d.ts +25 -0
  323. package/dist/features/shipments/schemas/validation.d.ts +4 -4
  324. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +82 -0
  325. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +145 -0
  326. package/dist/features/shipping-boxes/schemas/firestore.d.ts +80 -0
  327. package/dist/features/shipping-boxes/schemas/firestore.js +53 -0
  328. package/dist/features/store-extensions/schemas/report-create-form.d.ts +2 -2
  329. package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +2 -2
  330. package/dist/features/stores/components/StoreOrdersGuideView.js +6 -1
  331. package/dist/features/stores/repository/store.repository.d.ts +18 -0
  332. package/dist/features/stores/repository/store.repository.js +27 -1
  333. package/dist/features/stores/schemas/firestore.d.ts +112 -0
  334. package/dist/features/stores/schemas/firestore.js +20 -0
  335. package/dist/features/stores/schemas/index.d.ts +6 -6
  336. package/dist/features/stores/types/index.d.ts +19 -0
  337. package/dist/features/support/repository/support.repository.js +72 -6
  338. package/dist/features/support/schemas/firestore.d.ts +17 -0
  339. package/dist/features/support/schemas/index.d.ts +8 -8
  340. package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
  341. package/dist/index.d.ts +11 -1
  342. package/dist/index.js +33 -1
  343. package/dist/next/routing/route-map.d.ts +112 -0
  344. package/dist/next/routing/route-map.js +63 -0
  345. package/dist/providers/shipping-shiprocket/index.d.ts +27 -1
  346. package/dist/providers/shipping-shiprocket/index.js +84 -1
  347. package/dist/react/contexts/SessionContext.d.ts +17 -1
  348. package/dist/schemas/registry.d.ts +69 -34
  349. package/dist/schemas/sieve.d.ts +2 -2
  350. package/dist/security/pii-schemas.d.ts +1 -1
  351. package/dist/security/pii-schemas.js +45 -0
  352. package/dist/server-entry.d.ts +2 -2
  353. package/dist/server-entry.js +8 -1
  354. package/dist/server.d.ts +36 -1
  355. package/dist/server.js +183 -1
  356. package/dist/styles.css +412 -80
  357. package/dist/tailwind-utilities.css +1 -1
  358. package/dist/tokens/themes/default-dark.js +1 -0
  359. package/dist/tokens/themes/default-light.js +1 -0
  360. package/dist/tokens/themes/required.d.ts +1 -1
  361. package/dist/tokens/themes/required.js +1 -0
  362. package/dist/tokens/tokens.css +15 -0
  363. package/dist/ui/components/Button.d.ts +10 -1
  364. package/dist/ui/components/Button.js +25 -3
  365. package/dist/ui/components/Chart.d.ts +63 -0
  366. package/dist/ui/components/Chart.js +169 -0
  367. package/dist/ui/components/Chart.style.css +214 -0
  368. package/dist/ui/components/Disclaimer.d.ts +19 -0
  369. package/dist/ui/components/Disclaimer.js +83 -0
  370. package/dist/ui/components/OverflowRow.js +54 -2
  371. package/dist/ui/components/OverflowRow.style.css +38 -0
  372. package/dist/ui/components/RichTextEditor.js +99 -8
  373. package/dist/ui/components/chart-spec.d.ts +68 -0
  374. package/dist/ui/components/chart-spec.js +140 -0
  375. package/dist/ui/components/index.style.css +1 -0
  376. package/dist/ui/components/surface-tokens.d.ts +1 -1
  377. package/dist/ui/components/surface-tokens.js +9 -0
  378. package/dist/ui/components/tabs-overflow.js +64 -11
  379. package/dist/ui/components/toolbar-collapse.d.ts +13 -0
  380. package/dist/ui/components/toolbar-collapse.js +10 -1
  381. package/dist/ui/index.d.ts +8 -0
  382. package/dist/ui/index.js +9 -0
  383. package/dist/ui/rich-text/RichText.d.ts +10 -1
  384. package/dist/ui/rich-text/RichText.js +33 -2
  385. package/dist/ui/rich-text/RichText.style.css +83 -79
  386. package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
  387. package/dist/ui/rich-text/RichTextRenderer.js +34 -2
  388. package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
  389. package/dist/ui/rich-text/chart-blocks.js +87 -0
  390. package/package.json +1 -1
  391. package/scripts/seed-cli.mjs +1834 -1822
@@ -23,6 +23,20 @@ export declare const JobStatusValues: {
23
23
  readonly PROCESSING: "processing";
24
24
  readonly DONE: "done";
25
25
  readonly FAILED: "failed";
26
+ /**
27
+ * Phase 8 — attempts exhausted. The dead-letter state.
28
+ *
29
+ * 🛑 `failed` and `dead` are DIFFERENT facts and conflating them is what
30
+ * made a failed job terminal with no signal. `failed` now means "this
31
+ * attempt failed, and a retry is scheduled or the failure was permanent";
32
+ * `dead` means "we have stopped trying" and is the only one that opens a
33
+ * case.
34
+ *
35
+ * A reader that treats every non-`done` as hopeless loses that distinction —
36
+ * which is why the retention sweep excludes `dead` while an open case
37
+ * references it: the row is the evidence.
38
+ */
39
+ readonly DEAD: "dead";
26
40
  };
27
41
  export type JobStatus = (typeof JobStatusValues)[keyof typeof JobStatusValues];
28
42
  export interface JobResult {
@@ -52,7 +66,55 @@ export interface JobDocument extends BaseDocument {
52
66
  error?: string;
53
67
  startedAt?: Date;
54
68
  finishedAt?: Date;
69
+ /**
70
+ * 1-based. Absent means 1 — every job written before this field is a first
71
+ * attempt, which is both true and what every reader should assume.
72
+ */
73
+ attempt?: number;
74
+ /** Defaults to `JOB_MAX_ATTEMPTS`; a jobType may lower it. */
75
+ maxAttempts?: number;
76
+ /**
77
+ * When a retry becomes eligible.
78
+ *
79
+ * 🛑 A retry is a **NEW DOCUMENT**, not a mutation of this one, because
80
+ * `onJobCreated` fires on CREATE — re-running a job by flipping its status
81
+ * back to `pending` would never wake the trigger, and the job would sit
82
+ * `pending` forever looking scheduled. So this field lives on the NEW
83
+ * document and a sweep is what moves it from scheduled to running.
84
+ */
85
+ nextAttemptAt?: Date;
86
+ /**
87
+ * The document this is a retry OF — present on every attempt after the
88
+ * first, and absent on the original.
89
+ *
90
+ * 🛑 Points at the ORIGINAL, not at the immediately previous attempt. A
91
+ * flat chain means "show me every attempt of this job" is one equality
92
+ * query, where a linked list would need N reads to walk; and the question
93
+ * anyone actually asks is "did this job eventually work", which is about the
94
+ * original.
95
+ */
96
+ parentJobId?: string;
97
+ /** Why this attempt failed, kept per attempt so a pattern is visible. */
98
+ lastError?: string;
55
99
  }
100
+ /**
101
+ * Attempts before a job is declared dead.
102
+ *
103
+ * Three, with `JOB_BACKOFF_MS` below: a transient fault usually clears in
104
+ * minutes, and a fourth attempt an hour later is far more likely to be the
105
+ * same permanent fault again than a different outcome.
106
+ */
107
+ export declare const JOB_MAX_ATTEMPTS = 3;
108
+ /**
109
+ * Delay before attempt 2 and attempt 3.
110
+ *
111
+ * 🛑 Indexed by `attempt - 1`, so the array length must be at least
112
+ * `JOB_MAX_ATTEMPTS - 1`. The last value is reused if it is shorter, rather
113
+ * than producing `undefined` — a `nextAttemptAt` of `Invalid Date` would make
114
+ * the sweep's `<= now` comparison false forever and the retry would never run,
115
+ * which is the silent-stall shape this whole phase exists to remove.
116
+ */
117
+ export declare const JOB_BACKOFF_MS: readonly [number, number];
56
118
  export declare const JOB_FIELDS: {
57
119
  readonly JOB_TYPE: "jobType";
58
120
  readonly STATUS: "status";
@@ -63,10 +125,28 @@ export declare const JOB_FIELDS: {
63
125
  readonly FINISHED_AT: "finishedAt";
64
126
  readonly CREATED_AT: "createdAt";
65
127
  readonly UPDATED_AT: "updatedAt";
128
+ readonly ATTEMPT: "attempt";
129
+ readonly MAX_ATTEMPTS: "maxAttempts";
130
+ readonly NEXT_ATTEMPT_AT: "nextAttemptAt";
131
+ readonly PARENT_JOB_ID: "parentJobId";
66
132
  readonly STATUS_VALUES: {
67
133
  readonly PENDING: "pending";
68
134
  readonly PROCESSING: "processing";
69
135
  readonly DONE: "done";
70
136
  readonly FAILED: "failed";
137
+ /**
138
+ * Phase 8 — attempts exhausted. The dead-letter state.
139
+ *
140
+ * 🛑 `failed` and `dead` are DIFFERENT facts and conflating them is what
141
+ * made a failed job terminal with no signal. `failed` now means "this
142
+ * attempt failed, and a retry is scheduled or the failure was permanent";
143
+ * `dead` means "we have stopped trying" and is the only one that opens a
144
+ * case.
145
+ *
146
+ * A reader that treats every non-`done` as hopeless loses that distinction —
147
+ * which is why the retention sweep excludes `dead` while an open case
148
+ * references it: the row is the evidence.
149
+ */
150
+ readonly DEAD: "dead";
71
151
  };
72
152
  };
@@ -21,7 +21,39 @@ export const JobStatusValues = {
21
21
  PROCESSING: "processing",
22
22
  DONE: "done",
23
23
  FAILED: "failed",
24
+ /**
25
+ * Phase 8 — attempts exhausted. The dead-letter state.
26
+ *
27
+ * 🛑 `failed` and `dead` are DIFFERENT facts and conflating them is what
28
+ * made a failed job terminal with no signal. `failed` now means "this
29
+ * attempt failed, and a retry is scheduled or the failure was permanent";
30
+ * `dead` means "we have stopped trying" and is the only one that opens a
31
+ * case.
32
+ *
33
+ * A reader that treats every non-`done` as hopeless loses that distinction —
34
+ * which is why the retention sweep excludes `dead` while an open case
35
+ * references it: the row is the evidence.
36
+ */
37
+ DEAD: "dead",
24
38
  };
39
+ /**
40
+ * Attempts before a job is declared dead.
41
+ *
42
+ * Three, with `JOB_BACKOFF_MS` below: a transient fault usually clears in
43
+ * minutes, and a fourth attempt an hour later is far more likely to be the
44
+ * same permanent fault again than a different outcome.
45
+ */
46
+ export const JOB_MAX_ATTEMPTS = 3;
47
+ /**
48
+ * Delay before attempt 2 and attempt 3.
49
+ *
50
+ * 🛑 Indexed by `attempt - 1`, so the array length must be at least
51
+ * `JOB_MAX_ATTEMPTS - 1`. The last value is reused if it is shorter, rather
52
+ * than producing `undefined` — a `nextAttemptAt` of `Invalid Date` would make
53
+ * the sweep's `<= now` comparison false forever and the retry would never run,
54
+ * which is the silent-stall shape this whole phase exists to remove.
55
+ */
56
+ export const JOB_BACKOFF_MS = [5 * 60 * 1000, 30 * 60 * 1000];
25
57
  export const JOB_FIELDS = {
26
58
  JOB_TYPE: "jobType",
27
59
  STATUS: "status",
@@ -32,5 +64,10 @@ export const JOB_FIELDS = {
32
64
  FINISHED_AT: "finishedAt",
33
65
  CREATED_AT: "createdAt",
34
66
  UPDATED_AT: "updatedAt",
67
+ // Phase 8 retry/DLQ. `NEXT_ATTEMPT_AT` is the sweep's query field.
68
+ ATTEMPT: "attempt",
69
+ MAX_ATTEMPTS: "maxAttempts",
70
+ NEXT_ATTEMPT_AT: "nextAttemptAt",
71
+ PARENT_JOB_ID: "parentJobId",
35
72
  STATUS_VALUES: JobStatusValues,
36
73
  };
@@ -94,6 +94,14 @@ export interface AppLayoutShellProps {
94
94
  userWishlistHref?: string;
95
95
  /** Href for the user settings page — auto-generates PROFILE section in sidebar. */
96
96
  userSettingsHref?: string;
97
+ /**
98
+ * Href for the user's OWN reviews list — the target of the "Reviews" stat.
99
+ *
100
+ * The stat pointed at `${profileHref}#reviews` before, an anchor the profile
101
+ * page does not render, so the count linked to the top of an unrelated page.
102
+ * Omitting this prop hides the tile rather than guessing at a route.
103
+ */
104
+ userReviewsHref?: string;
97
105
  /** Locale switcher node rendered at the bottom of the sidebar. */
98
106
  sidebarLocaleSlot?: React.ReactNode;
99
107
  /** When true, shows theme toggle button in the sidebar footer. */
@@ -148,4 +156,4 @@ export interface AppLayoutShellProps {
148
156
  };
149
157
  };
150
158
  }
151
- export declare function AppLayoutShell({ children, navItems, sidebarItems, sidebarSections, sidebarPrimaryActions, sidebarTitle, hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, messagesHref, unreadMessageCount, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, showFooter, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle, suppressDashboardNav, hideSidebarToggle, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, eventBannerSlot, showBottomNav, contentClassName, lightBackground, darkBackground, }: AppLayoutShellProps): React.JSX.Element;
159
+ export declare function AppLayoutShell({ children, navItems, sidebarItems, sidebarSections, sidebarPrimaryActions, sidebarTitle, hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, messagesHref, unreadMessageCount, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, showFooter, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle, suppressDashboardNav, hideSidebarToggle, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, userReviewsHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, eventBannerSlot, showBottomNav, contentClassName, lightBackground, darkBackground, }: AppLayoutShellProps): React.JSX.Element;
@@ -17,6 +17,7 @@ const ROLE_DOT_BG_CLASS = {
17
17
  import { useTheme } from "../../_internal/client/theme/index.js";
18
18
  import { useAuth } from "../../react/contexts/SessionContext.js";
19
19
  import { useVisualViewportInset } from "../../react/hooks/useVisualViewportInset.js";
20
+ import { useListingTypeFlags } from "../../react/hooks/useListingTypeFlags.js";
20
21
  import { isBuyerUser } from "../auth/role-predicates.js";
21
22
  import { NavbarWithSettings } from "./NavbarWithSettings.js";
22
23
  import SideActionDock from "./SideActionDock.js";
@@ -135,13 +136,20 @@ function SidebarUserHeader({ user, onClose, profileHref, }) {
135
136
  function SidebarGuestHeader({ sidebarTitle, onClose, }) {
136
137
  return (_jsxs(Row, { align: "center", justify: "between", children: [_jsx(Div, { textWeight: "semibold", textSize: "sm", color: "primary", children: sidebarTitle }), _jsx("button", { type: "button", "aria-label": "Close menu", onClick: onClose, className: "rounded-full p-[var(--appkit-space-2)] hover:bg-surface-hover hover:text-zinc-900 text-[var(--appkit-color-text-muted)] dark:hover:text-zinc-100 transition-all hover:rotate-90", children: _jsx("svg", { className: "w-4 h-4 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })] }));
137
138
  }
138
- function SidebarContent({ sidebarItems, sidebarSections, sidebarPrimaryActions, user, activeHref, profileHref, userOrdersHref, userWishlistHref, userSettingsHref, adminHref, storeHref, sellerHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, theme, activeThemeName, toggleTheme, onLogout, onAfterLogout, }) {
139
+ function SidebarContent({ sidebarItems, sidebarSections, sidebarPrimaryActions, user, activeHref, profileHref, userOrdersHref, userWishlistHref, userSettingsHref, userReviewsHref, adminHref, storeHref, sellerHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, theme, activeThemeName, toggleTheme, onLogout, onAfterLogout, }) {
139
140
  const hasLegacyItems = sidebarItems.length > 0;
140
141
  const hasSections = !!(sidebarSections && sidebarSections.length > 0);
141
142
  const isAuthenticated = !!user;
142
143
  const role = user?.role ?? "user";
143
144
  const isAdminOrSeller = role === "admin" || role === "seller";
144
145
  const resolvedStoreHref = storeHref ?? sellerHref;
146
+ /*
147
+ * Defaults to ENABLED while site settings are loading, matching the hook's
148
+ * own contract — hiding the tile on first paint and popping it in a moment
149
+ * later is worse than showing it to the handful of installs that have
150
+ * auctions switched off.
151
+ */
152
+ const auctionsEnabled = useListingTypeFlags().isEnabled("auction");
145
153
  const labels = {
146
154
  sectionTitle: sidebarProfileLabels?.sectionTitle ?? "Profile",
147
155
  profile: sidebarProfileLabels?.profile ?? "My Profile",
@@ -177,10 +185,10 @@ function SidebarContent({ sidebarItems, sidebarSections, sidebarPrimaryActions,
177
185
  action.variant === "outline"
178
186
  ? "border hover:bg-surface-hover border-[var(--appkit-color-border)] text-[var(--appkit-color-text)]"
179
187
  : "bg-primary text-white hover:bg-primary-600 dark:bg-primary dark:hover:bg-primary-600 btn-glow",
180
- ].join(" "), children: action.label }, `${action.href}-${action.label}`))) })), isAuthenticated && (_jsx(CollapsibleNavGroup, { title: labels.sectionTitle, hasActive: !!activeProfileLink, children: _jsxs(Ul, { spacing: "2xs", children: [_jsx(Li, { children: _jsx(TextLink, { href: profileHref, variant: "none", className: activeProfileLink?.href === profileHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === profileHref ? "page" : undefined, children: labels.profile }) }), userOrdersHref && (_jsx(Li, { children: _jsx(TextLink, { href: userOrdersHref, variant: "none", className: activeProfileLink?.href === userOrdersHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userOrdersHref ? "page" : undefined, children: labels.orders }) })), userWishlistHref && (_jsx(Li, { children: _jsx(TextLink, { href: userWishlistHref, variant: "none", className: activeProfileLink?.href === userWishlistHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userWishlistHref ? "page" : undefined, children: labels.wishlist }) })), userSettingsHref && (_jsx(Li, { children: _jsx(TextLink, { href: userSettingsHref, variant: "none", className: activeProfileLink?.href === userSettingsHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userSettingsHref ? "page" : undefined, children: labels.settings }) }))] }) })), isAuthenticated && user?.stats && (_jsxs(Div, { layout: "grid", gap: "2", className: "grid-cols-2", children: [user.stats.totalOrders != null && userOrdersHref && (_jsxs(TextLink, { href: userOrdersHref, variant: "none", className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.totalOrders }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Orders" })] })), user.stats.reviewsCount != null && (_jsxs(TextLink, { href: `${profileHref}#reviews`, variant: "none", className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.reviewsCount }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Reviews" })] })), user.stats.auctionsWon != null && userOrdersHref && (_jsxs(TextLink, { href: `${userOrdersHref}?orderType=auction`, variant: "none", className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.auctionsWon }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Auctions Won" })] })), user.stats.itemsSold != null && resolvedStoreHref && (_jsxs(TextLink, { href: resolvedStoreHref, variant: "none", className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.itemsSold }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Items Sold" })] }))] })), isAuthenticated && isAdminOrSeller && (adminHref || resolvedStoreHref) && (_jsx(CollapsibleNavGroup, { title: labels.dashboardSectionTitle, hasActive: !!activeDashboardLink, children: _jsxs(Ul, { spacing: "2xs", children: [adminHref && role === "admin" && (_jsx(Li, { children: _jsx(TextLink, { href: adminHref, variant: "none", className: activeDashboardLink?.href === adminHref ? navItemActiveClass : navItemClass, "aria-current": activeDashboardLink?.href === adminHref ? "page" : undefined, children: labels.adminDashboard }) })), resolvedStoreHref && isAdminOrSeller && (_jsx(Li, { children: _jsx(TextLink, { href: resolvedStoreHref, variant: "none", className: activeDashboardLink?.href === resolvedStoreHref ? navItemActiveClass : navItemClass, "aria-current": activeDashboardLink?.href === resolvedStoreHref ? "page" : undefined, children: labels.storeDashboard }) }))] }) })), normalizedSections.map((section, sectionIndex) => (_jsx(CollapsibleSidebarSection, { section: section, navItemClass: navItemClass, navItemActiveClass: navItemActiveClass, activeHref: activeHref }, `sidebar-section-${sectionIndex}`))), (sidebarLocaleSlot || showThemeToggleInSidebar || (isAuthenticated && onLogout)) && (_jsxs(Stack, { border: "default", className: "border-t border-[var(--appkit-color-border-subtle)]", padding: "t-md", gap: "3", children: [sidebarLocaleSlot, showThemeToggleInSidebar && (_jsxs("button", { type: "button", onClick: toggleTheme, className: "flex w-full items-center justify-end gap-[var(--appkit-space-3)] rounded-lg px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-muted)] transition-colors hover:bg-primary-surface hover:text-primary-800 dark:hover:text-primary-200", children: [_jsx(Span, { "aria-hidden": "true", children: theme === "dark" ? "☀️" : "🌙" }), activeThemeName] })), isAuthenticated && onLogout && (_jsx("button", { type: "button", onClick: () => { onLogout(); onAfterLogout(); }, className: CLS_LOGOUT_BTN, children: labels.logout }))] }))] }));
188
+ ].join(" "), children: action.label }, `${action.href}-${action.label}`))) })), isAuthenticated && (_jsx(CollapsibleNavGroup, { title: labels.sectionTitle, hasActive: !!activeProfileLink, children: _jsxs(Ul, { spacing: "2xs", children: [_jsx(Li, { children: _jsx(TextLink, { href: profileHref, variant: "none", className: activeProfileLink?.href === profileHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === profileHref ? "page" : undefined, children: labels.profile }) }), userOrdersHref && (_jsx(Li, { children: _jsx(TextLink, { href: userOrdersHref, variant: "none", className: activeProfileLink?.href === userOrdersHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userOrdersHref ? "page" : undefined, children: labels.orders }) })), userWishlistHref && (_jsx(Li, { children: _jsx(TextLink, { href: userWishlistHref, variant: "none", className: activeProfileLink?.href === userWishlistHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userWishlistHref ? "page" : undefined, children: labels.wishlist }) })), userSettingsHref && (_jsx(Li, { children: _jsx(TextLink, { href: userSettingsHref, variant: "none", className: activeProfileLink?.href === userSettingsHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userSettingsHref ? "page" : undefined, children: labels.settings }) }))] }) })), isAuthenticated && user?.stats && (_jsxs(Div, { layout: "grid", gap: "2", className: "grid-cols-2", children: [user.stats.totalOrders != null && userOrdersHref && (_jsxs(TextLink, { href: userOrdersHref, variant: "none", className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.totalOrders }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Orders" })] })), user.stats.reviewsCount != null && userReviewsHref && (_jsxs(TextLink, { href: userReviewsHref, variant: "none", className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.reviewsCount }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Reviews" })] })), auctionsEnabled && user.stats.auctionsWon != null && userOrdersHref && (_jsxs(TextLink, { href: `${userOrdersHref}?orderType=auction`, variant: "none", className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.auctionsWon }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Auctions Won" })] }))] })), isAuthenticated && isAdminOrSeller && (adminHref || resolvedStoreHref) && (_jsx(CollapsibleNavGroup, { title: labels.dashboardSectionTitle, hasActive: !!activeDashboardLink, children: _jsxs(Ul, { spacing: "2xs", children: [adminHref && role === "admin" && (_jsx(Li, { children: _jsx(TextLink, { href: adminHref, variant: "none", className: activeDashboardLink?.href === adminHref ? navItemActiveClass : navItemClass, "aria-current": activeDashboardLink?.href === adminHref ? "page" : undefined, children: labels.adminDashboard }) })), resolvedStoreHref && isAdminOrSeller && (_jsx(Li, { children: _jsx(TextLink, { href: resolvedStoreHref, variant: "none", className: activeDashboardLink?.href === resolvedStoreHref ? navItemActiveClass : navItemClass, "aria-current": activeDashboardLink?.href === resolvedStoreHref ? "page" : undefined, children: labels.storeDashboard }) }))] }) })), normalizedSections.map((section, sectionIndex) => (_jsx(CollapsibleSidebarSection, { section: section, navItemClass: navItemClass, navItemActiveClass: navItemActiveClass, activeHref: activeHref }, `sidebar-section-${sectionIndex}`))), (sidebarLocaleSlot || showThemeToggleInSidebar || (isAuthenticated && onLogout)) && (_jsxs(Stack, { border: "default", className: "border-t border-[var(--appkit-color-border-subtle)]", padding: "t-md", gap: "3", children: [sidebarLocaleSlot, showThemeToggleInSidebar && (_jsxs("button", { type: "button", onClick: toggleTheme, className: "flex w-full items-center justify-end gap-[var(--appkit-space-3)] rounded-lg px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-muted)] transition-colors hover:bg-primary-surface hover:text-primary-800 dark:hover:text-primary-200", children: [_jsx(Span, { "aria-hidden": "true", children: theme === "dark" ? "☀️" : "🌙" }), activeThemeName] })), isAuthenticated && onLogout && (_jsx("button", { type: "button", onClick: () => { onLogout(); onAfterLogout(); }, className: CLS_LOGOUT_BTN, children: labels.logout }))] }))] }));
181
189
  }
182
190
  // ─── Main component ────────────────────────────────────────────────────────────
183
- export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarSections, sidebarPrimaryActions, sidebarTitle = "Navigation", hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, messagesHref, unreadMessageCount, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, showFooter = true, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle = false, suppressDashboardNav = false, hideSidebarToggle = false, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar = false, sidebarProfileLabels, eventBannerSlot, showBottomNav = true, contentClassName, lightBackground = DEFAULT_LIGHT_BG, darkBackground = DEFAULT_DARK_BG, }) {
191
+ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarSections, sidebarPrimaryActions, sidebarTitle = "Navigation", hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, messagesHref, unreadMessageCount, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, showFooter = true, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle = false, suppressDashboardNav = false, hideSidebarToggle = false, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, userReviewsHref, sidebarLocaleSlot, showThemeToggleInSidebar = false, sidebarProfileLabels, eventBannerSlot, showBottomNav = true, contentClassName, lightBackground = DEFAULT_LIGHT_BG, darkBackground = DEFAULT_DARK_BG, }) {
184
192
  // QueryClientProvider used to be created here so AppLayoutShell could stand
185
193
  // alone. The real cause of the 2026-06-10/11 "No QueryClient set" prod crash
186
194
  // turned out to be a peer-dep duplicate of @tanstack/query-core under
@@ -228,7 +236,7 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
228
236
  // by its countdown alone got no clearance). That mirror is gone: BottomChrome
229
237
  // measures the real tier and publishes --bottom-chrome-height, and the footer
230
238
  // reserves space from that. Nothing needs to guess any more.
231
- const sidebarContent = (_jsx(SidebarContent, { sidebarItems: sidebarItems, sidebarSections: sidebarSections, sidebarPrimaryActions: sidebarPrimaryActions, user: user, activeHref: pathname ?? "", profileHref: profileHref, userOrdersHref: userOrdersHref, userWishlistHref: userWishlistHref, userSettingsHref: userSettingsHref, adminHref: adminHref, storeHref: storeHref, sellerHref: sellerHref, sidebarLocaleSlot: sidebarLocaleSlot, showThemeToggleInSidebar: showThemeToggleInSidebar, sidebarProfileLabels: sidebarProfileLabels, theme: theme, activeThemeName: activeTheme.name, toggleTheme: toggleTheme, onLogout: onLogout, onAfterLogout: () => setSidebarOpen(false) }));
239
+ const sidebarContent = (_jsx(SidebarContent, { sidebarItems: sidebarItems, sidebarSections: sidebarSections, sidebarPrimaryActions: sidebarPrimaryActions, user: user, activeHref: pathname ?? "", profileHref: profileHref, userOrdersHref: userOrdersHref, userWishlistHref: userWishlistHref, userSettingsHref: userSettingsHref, userReviewsHref: userReviewsHref, adminHref: adminHref, storeHref: storeHref, sellerHref: sellerHref, sidebarLocaleSlot: sidebarLocaleSlot, showThemeToggleInSidebar: showThemeToggleInSidebar, sidebarProfileLabels: sidebarProfileLabels, theme: theme, activeThemeName: activeTheme.name, toggleTheme: toggleTheme, onLogout: onLogout, onAfterLogout: () => setSidebarOpen(false) }));
232
240
  const normalizedLightBackground = {
233
241
  type: lightBackground.type,
234
242
  value: lightBackground.value,
@@ -233,7 +233,8 @@ export default function BottomActions() {
233
233
  * own reveal — bulk mode is a state the user just entered deliberately, and
234
234
  * making them scroll to act on it would be a puzzle.
235
235
  */
236
- const revealed = useCtaReveal(hasContent && !isBulkMode, ctaAnchorId) || isBulkMode;
236
+ const { revealed: ctaRevealed } = useCtaReveal(hasContent && !isBulkMode, ctaAnchorId);
237
+ const revealed = ctaRevealed || isBulkMode;
237
238
  const isVisible = hasContent && revealed;
238
239
  // A panel left open after its content is withdrawn (bulk mode taking over, or
239
240
  // the page clearing it) would sit there as an empty sheet.
@@ -32,12 +32,20 @@ export interface BottomNavbarUser {
32
32
  };
33
33
  zoom: number;
34
34
  } | null;
35
+ /**
36
+ * The account holder's own activity AS A BUYER, derived live by
37
+ * `/api/user/profile`. `undefined` on a member means the count could not be
38
+ * taken — render nothing, never 0.
39
+ *
40
+ * 🛑 `itemsSold` and `rating` were removed, not forgotten. `itemsSold` is a
41
+ * SELLER number and had no business in the account holder's own profile
42
+ * block (see `AppLayoutShell`); `rating` had no writer anywhere in either
43
+ * repo, so its `!= null` guard meant the tile could never render.
44
+ */
35
45
  stats?: {
36
46
  totalOrders?: number;
37
47
  auctionsWon?: number;
38
- itemsSold?: number;
39
48
  reviewsCount?: number;
40
- rating?: number;
41
49
  } | null;
42
50
  }
43
51
  export interface BottomNavbarProps {
@@ -54,7 +54,7 @@ import { useClickOutside } from "../../react/index.js";
54
54
  import { useHandMode } from "../../_internal/client/hand-mode/index.js";
55
55
  import { useCtaReveal } from "./useCtaReveal.js";
56
56
  import { useIsDesktop } from "./useIsDesktop.js";
57
- import { ActionRow, Div, IconButton, Row, Span, Stack, Text, Button } from "../../ui/index.js";
57
+ import { ActionRow, Div, ICON_SIZE, IconButton, Row, Span, Stack, Text, Button } from "../../ui/index.js";
58
58
  const PANEL_SURFACE = "bg-[color-mix(in_srgb,var(--appkit-color-bg)_94%,transparent)] backdrop-blur-md border border-[var(--appkit-color-border)]";
59
59
  const FLEX_CENTER = "flex items-center justify-center";
60
60
  const CLS_COUNT_BADGE = "bg-error-solid text-error-on-solid";
@@ -143,8 +143,40 @@ export default function SideActionDock() {
143
143
  !!infoLabel ||
144
144
  !!secondaryLabel;
145
145
  // Rule 3 — a selection is its own reveal.
146
- const revealed = useCtaReveal(hasContent && !isBulkMode, ctaAnchorId) || isBulkMode;
146
+ const { revealed: ctaRevealed, hasAnchor, scrollToAnchor, } = useCtaReveal(hasContent && !isBulkMode, ctaAnchorId);
147
+ const revealed = ctaRevealed || isBulkMode;
147
148
  const isVisible = isDesktop && hasContent && revealed;
149
+ /*
150
+ * ── Rule 4 — a page with a REAL Buy button gets a way BACK to it ─────────
151
+ *
152
+ * On a detail page the dock was a second copy of the CTA you had just
153
+ * scrolled past, which is the one thing rule 2 says a floating control must
154
+ * not be: `useCtaReveal` already means "you have met the Buy button and left
155
+ * it", so re-offering the purchase mid-screen answers a question the user
156
+ * did not ask. Taking them back to the real control answers the one they
157
+ * did — and it keeps the purchase decision on the page's own CTA, beside
158
+ * the price, the variant and the stock state that qualify it.
159
+ *
160
+ * 🛑 Gated on `hasAnchor`, NOT on a route or a listing type. A bulk screen
161
+ * marks no anchor, so it keeps the centred dock carrying its actions — which
162
+ * is correct, because there was never an inline control there to return to.
163
+ * `ctaAnchorProps` has exactly one caller today (`ProductDetailPageView`),
164
+ * so this branch is opt-in by construction and ~70 admin bulk bars cannot
165
+ * reach it.
166
+ *
167
+ * It sits in the bottom-right cluster above `BackToTop` and shares that
168
+ * control's bottom-tier `calc()` verbatim, so the two stack rather than
169
+ * overlap — and rule 1's objection (a bottom-anchored bar reads as a browser
170
+ * download bar) does not apply to a single round puck in the corner that is
171
+ * already where the scroll affordance lives.
172
+ *
173
+ * 🛑 The RETURN for this branch is deliberately deferred to just above the
174
+ * main one. An early return here would sit above this component's remaining
175
+ * `useEffect`s, so the hook order would differ between a detail page and a
176
+ * bulk screen — React's "rendered fewer hooks than expected" crash, and the
177
+ * kind that only shows up on the page nobody opened during review.
178
+ */
179
+ const isBackToCta = isDesktop && hasAnchor && !isBulkMode;
148
180
  // Rule 1 — opposite the sidebar, which docks to the hand-mode side.
149
181
  const dockSideClass = hand === "left" ? "right-0" : "left-0";
150
182
  const panelAlignClass = hand === "left" ? "items-end" : "items-start";
@@ -216,6 +248,24 @@ export default function SideActionDock() {
216
248
  const fabLabel = isBulkMode
217
249
  ? `${bulk?.selectedCount ?? 0} selected — bulk actions`
218
250
  : (primaryAction?.label ?? infoLabel ?? "Page actions");
251
+ // Rule 4's deferred return — below every hook, for the reason given above.
252
+ if (isBackToCta) {
253
+ if (!ctaRevealed)
254
+ return null;
255
+ return (_jsx(Div, { className: [
256
+ "fixed pointer-events-auto z-[var(--appkit-z-back-to-top)]",
257
+ hand === "left" ? "left-4" : "right-4",
258
+ /*
259
+ * `BackToTop`'s own expression verbatim — including its `4rem`
260
+ * default for `--bottom-nav-height`, not the `0px` the dock's other
261
+ * branch would imply — plus 3.5rem to clear that control's touch
262
+ * box and its gap. Copying the expression rather than inventing one
263
+ * is what keeps the two stacked when the tier changes height.
264
+ */
265
+ "bottom-[calc(var(--keyboard-inset-height,0px)+var(--bottom-nav-height,4rem)+var(--bottom-chrome-height,0px)+var(--appkit-space-4)+3.5rem)]",
266
+ "transition-[bottom] duration-300 ease-out",
267
+ ].join(" "), "data-section": "back-to-cta", children: _jsxs(Button, { type: "button", variant: "primary", size: "sm", onClick: scrollToAnchor, "aria-label": `Back to ${primaryAction?.label ?? "the buy button"}`, children: [_jsx(ChevronUp, { "aria-hidden": "true", size: ICON_SIZE.sm }), `Back to ${primaryAction?.label ?? "buy"}`] }) }));
268
+ }
219
269
  return (_jsxs(Div, { ref: containerRef, role: "toolbar", "aria-label": isBulkMode ? "Bulk actions" : "Page actions", className: [
220
270
  "fixed z-[var(--appkit-z-overlay)] pointer-events-auto flex flex-col",
221
271
  panelAlignClass,
@@ -23,6 +23,15 @@ const iconBtn = "flex items-center justify-center w-11 h-11 rounded-lg text-[var
23
23
  * `important: true`, so the utility would beat the unlayered rule and the badge
24
24
  * would silently stop mirroring. The vertical `-top-0.5` is hand-neutral and stays. */
25
25
  const countBadge = "appkit-hand-badge absolute -top-0.5 flex items-center justify-center w-4 h-4 rounded-full bg-error-solid text-error-on-solid text-[10px] font-bold leading-none";
26
+ /**
27
+ * Applied to the GLYPH, never the link, when the icon is carrying something.
28
+ *
29
+ * On the glyph so the count badge stays put: animating the link would swing
30
+ * the number with it, which turns a readable badge into a moving target. The
31
+ * keyframes and the `prefers-reduced-motion` opt-out live in
32
+ * `TitleBarLayout.style.css`.
33
+ */
34
+ const nudgeWhenCarrying = (count) => count > 0 ? "appkit-nav-icon--has-items" : "";
26
35
  /**
27
36
  * TitleBarLayout — generic top sticky title-bar shell.
28
37
  *
@@ -50,13 +59,13 @@ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, s
50
59
  // Dedicated notification bell — rendered immediately before the wishlist
51
60
  // icon (per layout request). Falls back to invisible when no href is set;
52
61
  // the count badge mirrors the wishlist/cart pattern.
53
- const notificationsEl = notificationsHref ? (_jsxs(Link, { href: notificationsHref, "aria-label": `Notifications${unreadNotificationCount > 0 ? `, ${unreadNotificationCount} unread` : ""}`, className: `relative ${iconBtn}`, children: [_jsx(Icon, { name: "notification", size: "lg" }), unreadNotificationCount > 0 && (_jsx(Span, { className: countBadge, children: unreadNotificationCount > 99 ? "99+" : unreadNotificationCount }))] })) : null;
62
+ const notificationsEl = notificationsHref ? (_jsxs(Link, { href: notificationsHref, "aria-label": `Notifications${unreadNotificationCount > 0 ? `, ${unreadNotificationCount} unread` : ""}`, className: `relative ${iconBtn}`, children: [_jsx(Icon, { name: "notification", size: "lg", className: nudgeWhenCarrying(unreadNotificationCount) }), unreadNotificationCount > 0 && (_jsx(Span, { className: countBadge, children: unreadNotificationCount > 99 ? "99+" : unreadNotificationCount }))] })) : null;
54
63
  // D9 — chat. Same shape as the notification bell: absent entirely when no
55
64
  // href is supplied (the consumer only passes one when
56
65
  // `siteSettings.chat.enabled === true`), never rendered-but-disabled.
57
- const messagesEl = messagesHref ? (_jsxs(Link, { href: messagesHref, "aria-label": `Messages${unreadMessageCount > 0 ? `, ${unreadMessageCount} unread` : ""}`, className: `relative ${iconBtn}`, children: [_jsx(Icon, { name: "message", size: "lg" }), unreadMessageCount > 0 && (_jsx(Span, { className: countBadge, children: unreadMessageCount > 99 ? "99+" : unreadMessageCount }))] })) : null;
58
- const wishlistEl = wishlistHref ? (_jsxs(Link, { href: wishlistHref, "aria-label": `Wishlist${wishlistCount > 0 ? `, ${wishlistCount} items` : ""}`, className: `relative ${iconBtn}`, "data-tour": "nav-wishlist", children: [_jsx(Icon, { name: "wishlist", size: "lg" }), wishlistCount > 0 && (_jsx(Span, { className: countBadge, children: wishlistCount > 9 ? "9+" : wishlistCount }))] })) : null;
59
- const cartEl = cartHref ? (_jsxs(Link, { href: cartHref, "aria-label": `Cart${cartCount > 0 ? `, ${cartCount} items` : ""}`, className: `relative ${iconBtn}`, "data-tour": "nav-cart", children: [_jsx(Icon, { name: "cart", size: "lg" }), cartCount > 0 && (_jsx(Span, { className: countBadge, children: cartCount > 9 ? "9+" : cartCount }))] })) : null;
66
+ const messagesEl = messagesHref ? (_jsxs(Link, { href: messagesHref, "aria-label": `Messages${unreadMessageCount > 0 ? `, ${unreadMessageCount} unread` : ""}`, className: `relative ${iconBtn}`, children: [_jsx(Icon, { name: "message", size: "lg", className: nudgeWhenCarrying(unreadMessageCount) }), unreadMessageCount > 0 && (_jsx(Span, { className: countBadge, children: unreadMessageCount > 99 ? "99+" : unreadMessageCount }))] })) : null;
67
+ const wishlistEl = wishlistHref ? (_jsxs(Link, { href: wishlistHref, "aria-label": `Wishlist${wishlistCount > 0 ? `, ${wishlistCount} items` : ""}`, className: `relative ${iconBtn}`, "data-tour": "nav-wishlist", children: [_jsx(Icon, { name: "wishlist", size: "lg", className: nudgeWhenCarrying(wishlistCount) }), wishlistCount > 0 && (_jsx(Span, { className: countBadge, children: wishlistCount > 9 ? "9+" : wishlistCount }))] })) : null;
68
+ const cartEl = cartHref ? (_jsxs(Link, { href: cartHref, "aria-label": `Cart${cartCount > 0 ? `, ${cartCount} items` : ""}`, className: `relative ${iconBtn}`, "data-tour": "nav-cart", children: [_jsx(Icon, { name: "cart", size: "lg", className: nudgeWhenCarrying(cartCount) }), cartCount > 0 && (_jsx(Span, { className: countBadge, children: cartCount > 9 ? "9+" : cartCount }))] })) : null;
60
69
  const profileEl = profileHref ? (_jsx(Link, { href: profileHref, "aria-label": user ? `Profile — ${user.displayName ?? user.email}` : "Sign in", className: `relative ${iconBtn}`, "data-tour": "nav-profile", children: _jsx(Avatar, { src: user?.photoURL ?? undefined, name: user?.displayName ?? user?.email ?? undefined, alt: user?.displayName ?? "Profile", size: "sm" }) })) : null;
61
70
  const authButtonsEl = !user && (loginHref || registerHref) ? (
62
71
  // Mirrors with TB1 so Register (the primary CTA) keeps the outer, more
@@ -0,0 +1,59 @@
1
+ /*
2
+ * Title-bar icons that are carrying something.
3
+ *
4
+ * The count badge already states the fact, but it is a 16px dot in the corner
5
+ * of a dense bar — easy to miss on a phone, and it never changes once drawn.
6
+ * A small periodic nudge is what makes "you have three things in your cart"
7
+ * noticeable without adding a second place to read the number.
8
+ *
9
+ * 🛑 It is a PERIODIC nudge, not a loop. The movement lasts about half a
10
+ * second at the END of a 10s cycle and the icon is still for the other ~9.5s,
11
+ * so it reads as an occasional glance-catcher rather than a permanent
12
+ * animation sitting in the chrome of every page. A continuously-moving element
13
+ * in a fixed header is the kind of thing people learn to tune out — or leave.
14
+ *
15
+ * 🛑 The percentages and the duration are ONE number, not two. The keyframe
16
+ * stops are a fraction of the cycle, so changing `10s` alone silently
17
+ * stretches the nudge itself — at 20s the same stops would swing the bell over
18
+ * a full second, which reads as a glitch rather than a nudge. Rescale the
19
+ * stops with it: the window is 4.8% here, which is ~480ms of a 10s cycle.
20
+ *
21
+ * The pivot is near the TOP of the glyph so the motion reads as a bell swing
22
+ * rather than a wobble around the middle, which is what the notification bell
23
+ * wants and what the cart and wishlist tolerate.
24
+ */
25
+ @keyframes appkit-nav-icon-nudge {
26
+ 0%,
27
+ 95.2%,
28
+ 100% {
29
+ transform: rotate(0deg);
30
+ }
31
+ 96.1% {
32
+ transform: rotate(-12deg);
33
+ }
34
+ 97.3% {
35
+ transform: rotate(9deg);
36
+ }
37
+ 98.5% {
38
+ transform: rotate(-5deg);
39
+ }
40
+ 99.4% {
41
+ transform: rotate(2deg);
42
+ }
43
+ }
44
+
45
+ .appkit-nav-icon--has-items {
46
+ transform-origin: 50% 15%;
47
+ animation: appkit-nav-icon-nudge 10s ease-in-out infinite;
48
+ }
49
+
50
+ /*
51
+ * 🛑 Honoured, not approximated. This is decoration carrying no information
52
+ * the badge does not already carry, so for a reader who has asked for less
53
+ * motion the correct amount is none — not a shorter or gentler version.
54
+ */
55
+ @media (prefers-reduced-motion: reduce) {
56
+ .appkit-nav-icon--has-items {
57
+ animation: none;
58
+ }
59
+ }
@@ -21,4 +21,19 @@
21
21
  * page that owns it mounts AFTER this dock — the dock lives in the app shell
22
22
  * and the CTA arrives with the route.
23
23
  */
24
- export declare function useCtaReveal(enabled?: boolean, anchorId?: string): boolean;
24
+ export interface CtaRevealState {
25
+ /** Should the floating dock / bar be on screen? */
26
+ revealed: boolean;
27
+ /**
28
+ * Does this page mark an inline CTA at all?
29
+ *
30
+ * The discriminator between a DETAIL page (a real Buy button exists, so the
31
+ * floating control's job is to take you back to it) and a BULK screen (no
32
+ * Buy button was ever there, so the floating control IS the only affordance
33
+ * and must carry the actions itself).
34
+ */
35
+ hasAnchor: boolean;
36
+ /** Scroll the inline CTA back into view. A no-op when there is no anchor. */
37
+ scrollToAnchor: () => void;
38
+ }
39
+ export declare function useCtaReveal(enabled?: boolean, anchorId?: string): CtaRevealState;
@@ -1,29 +1,6 @@
1
1
  "use client";
2
2
  import { useEffect, useState } from "react";
3
3
  import { CTA_ANCHOR_ATTR } from "./cta-anchor.js";
4
- /**
5
- * useCtaReveal — "has the user met the Buy button and scrolled past it?"
6
- *
7
- * Returns true when the floating action dock should be on screen:
8
- *
9
- * - the page marks NO CTA anchor → true immediately (nothing to wait
10
- * for; an admin bulk bar has no Buy button)
11
- * - the page marks one, not yet seen → false (the inline CTA is doing the
12
- * job; a second copy of it beside itself is noise)
13
- * - seen, and now out of view → true
14
- * - seen, and scrolled back into view → false again
15
- *
16
- * 🛑 `IntersectionObserver`, never a scroll-position threshold. The bar this
17
- * replaces used `window.scrollY > 400`, which is a guess about where a CTA sits
18
- * — wrong on a short page (the dock appears while the real button is still on
19
- * screen) and wrong on a long gallery (it appears before the CTA is reached).
20
- * Observing the element itself is the same rule stated exactly, and it costs no
21
- * scroll handler.
22
- *
23
- * The anchor is looked up on an interval-free `MutationObserver` because the
24
- * page that owns it mounts AFTER this dock — the dock lives in the app shell
25
- * and the CTA arrives with the route.
26
- */
27
4
  export function useCtaReveal(enabled = true, anchorId) {
28
5
  const [anchor, setAnchor] = useState(null);
29
6
  const [anchorExists, setAnchorExists] = useState(false);
@@ -74,9 +51,23 @@ export function useCtaReveal(enabled = true, anchorId) {
74
51
  io.observe(anchor);
75
52
  return () => io.disconnect();
76
53
  }, [anchor]);
54
+ /*
55
+ * Re-read at CLICK time rather than closing over `anchor`. A client-side
56
+ * navigation swaps the page under a dock that never unmounts, and the
57
+ * element captured when the effect last ran may already be detached — a
58
+ * `scrollIntoView` on a detached node silently does nothing.
59
+ */
60
+ const scrollToAnchor = () => {
61
+ if (typeof document === "undefined")
62
+ return;
63
+ const el = anchorId
64
+ ? document.getElementById(anchorId)
65
+ : document.querySelector(`[${CTA_ANCHOR_ATTR}]`);
66
+ el?.scrollIntoView({ behavior: "smooth", block: "center" });
67
+ };
77
68
  if (!enabled)
78
- return false;
69
+ return { revealed: false, hasAnchor: anchorExists, scrollToAnchor };
79
70
  if (!anchorExists)
80
- return true;
81
- return seen && !inView;
71
+ return { revealed: true, hasAnchor: false, scrollToAnchor };
72
+ return { revealed: seen && !inView, hasAnchor: true, scrollToAnchor };
82
73
  }