@mohasinac/appkit 4.62.1 → 4.63.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) 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 +51 -11
  8. package/dist/_internal/server/features/checkout/actions.js +335 -80
  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/offers/destinations.d.ts +40 -0
  22. package/dist/_internal/server/features/offers/destinations.js +65 -0
  23. package/dist/_internal/server/features/orders/adapters.js +21 -0
  24. package/dist/_internal/server/features/orders/amend-delivery.d.ts +56 -0
  25. package/dist/_internal/server/features/orders/amend-delivery.js +124 -0
  26. package/dist/_internal/server/features/orders/balance.d.ts +89 -0
  27. package/dist/_internal/server/features/orders/balance.js +256 -0
  28. package/dist/_internal/server/features/pricing/actions.d.ts +58 -0
  29. package/dist/_internal/server/features/pricing/actions.js +144 -0
  30. package/dist/_internal/server/features/shipping/book-split.d.ts +86 -0
  31. package/dist/_internal/server/features/shipping/book-split.js +130 -0
  32. package/dist/_internal/server/features/shipping/book.d.ts +61 -0
  33. package/dist/_internal/server/features/shipping/book.js +176 -0
  34. package/dist/_internal/server/features/shipping/courier-block.js +26 -1
  35. package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
  36. package/dist/_internal/server/features/site-settings/adapters.js +14 -0
  37. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  38. package/dist/_internal/server/features/stores/adapters.js +14 -0
  39. package/dist/_internal/server/functions/scheduled.d.ts +2 -1
  40. package/dist/_internal/server/functions/scheduled.js +57 -1
  41. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +2 -0
  42. package/dist/_internal/server/jobs/core/jobRunners.js +45 -0
  43. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +4 -1
  44. package/dist/_internal/server/jobs/core/reservationExpiry.d.ts +17 -0
  45. package/dist/_internal/server/jobs/core/reservationExpiry.js +40 -0
  46. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +15 -0
  47. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
  48. package/dist/_internal/server/jobs/handlers/index.js +1 -0
  49. package/dist/_internal/server/jobs/handlers/reservationExpiry.d.ts +2 -0
  50. package/dist/_internal/server/jobs/handlers/reservationExpiry.js +2 -0
  51. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  52. package/dist/_internal/shared/actions/action-registry.js +268 -0
  53. package/dist/_internal/shared/checkout/order-math.d.ts +31 -2
  54. package/dist/_internal/shared/checkout/order-math.js +22 -36
  55. package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
  56. package/dist/_internal/shared/checkout/rules/index.js +1 -1
  57. package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +18 -0
  58. package/dist/_internal/shared/checkout/rules/payment-gate.js +25 -0
  59. package/dist/_internal/shared/checkout/split.d.ts +0 -12
  60. package/dist/_internal/shared/checkout/split.js +22 -2
  61. package/dist/_internal/shared/content/disclaimers.js +7 -7
  62. package/dist/_internal/shared/content/published-values.d.ts +6 -1
  63. package/dist/_internal/shared/content/published-values.js +8 -0
  64. package/dist/_internal/shared/features/admin/integrations.d.ts +32 -2
  65. package/dist/_internal/shared/features/admin/integrations.js +19 -9
  66. package/dist/_internal/shared/features/dashboard/hub-sections.d.ts +72 -0
  67. package/dist/_internal/shared/features/dashboard/hub-sections.js +75 -0
  68. package/dist/_internal/shared/features/emi/choice.d.ts +98 -0
  69. package/dist/_internal/shared/features/emi/choice.js +69 -0
  70. package/dist/_internal/shared/features/emi/group.d.ts +84 -0
  71. package/dist/_internal/shared/features/emi/group.js +126 -0
  72. package/dist/_internal/shared/features/ledger/posting.d.ts +107 -0
  73. package/dist/_internal/shared/features/ledger/posting.js +165 -0
  74. package/dist/_internal/shared/features/ledger/reports.d.ts +76 -0
  75. package/dist/_internal/shared/features/ledger/reports.js +127 -0
  76. package/dist/_internal/shared/features/ledger/rules.d.ts +170 -0
  77. package/dist/_internal/shared/features/ledger/rules.js +221 -0
  78. package/dist/_internal/shared/features/offers/coordination.d.ts +122 -0
  79. package/dist/_internal/shared/features/offers/coordination.js +80 -0
  80. package/dist/_internal/shared/features/orders/delivery-snapshot.d.ts +114 -0
  81. package/dist/_internal/shared/features/orders/delivery-snapshot.js +116 -0
  82. package/dist/_internal/shared/features/payouts/pay-period.d.ts +91 -0
  83. package/dist/_internal/shared/features/payouts/pay-period.js +104 -0
  84. package/dist/_internal/shared/features/points/accepts.d.ts +1 -8
  85. package/dist/_internal/shared/features/points/accepts.js +12 -6
  86. package/dist/_internal/shared/features/pricing/rate-registry.d.ts +150 -0
  87. package/dist/_internal/shared/features/pricing/rate-registry.js +246 -0
  88. package/dist/_internal/shared/features/shipping/parcel.d.ts +123 -0
  89. package/dist/_internal/shared/features/shipping/parcel.js +139 -0
  90. package/dist/_internal/shared/features/site-settings/fees.d.ts +16 -8
  91. package/dist/_internal/shared/features/site-settings/fees.js +12 -8
  92. package/dist/_internal/shared/fees/explain.d.ts +81 -0
  93. package/dist/_internal/shared/fees/explain.js +101 -0
  94. package/dist/_internal/shared/history/index.d.ts +1 -1
  95. package/dist/_internal/shared/history/index.js +1 -1
  96. package/dist/_internal/shared/history/types.d.ts +7 -1
  97. package/dist/_internal/shared/history/types.js +25 -0
  98. package/dist/client.d.ts +11 -1
  99. package/dist/client.js +13 -0
  100. package/dist/constants/api-endpoints.d.ts +72 -0
  101. package/dist/constants/api-endpoints.js +41 -0
  102. package/dist/constants/field-names.d.ts +41 -0
  103. package/dist/constants/field-names.js +47 -0
  104. package/dist/features/account/components/UserOrdersView.js +28 -2
  105. package/dist/features/admin/components/AdminIntegrationsView.d.ts +55 -0
  106. package/dist/features/admin/components/AdminIntegrationsView.js +74 -0
  107. package/dist/features/admin/components/AdminOrderEditorView.d.ts +16 -1
  108. package/dist/features/admin/components/AdminOrderEditorView.js +72 -3
  109. package/dist/features/admin/components/AdminOrdersView.js +20 -1
  110. package/dist/features/admin/schemas/firestore.d.ts +32 -0
  111. package/dist/features/ads/schemas/booking-write.d.ts +157 -0
  112. package/dist/features/ads/schemas/booking-write.js +88 -0
  113. package/dist/features/ads/schemas/creative-form.d.ts +18 -0
  114. package/dist/features/ads/schemas/creative-form.js +51 -0
  115. package/dist/features/ads/schemas/firestore.d.ts +140 -0
  116. package/dist/features/ads/schemas/firestore.js +109 -0
  117. package/dist/features/auctions/repository/bid.repository.js +3 -1
  118. package/dist/features/audit-log/schemas/firestore.d.ts +110 -0
  119. package/dist/features/audit-log/schemas/firestore.js +74 -0
  120. package/dist/features/auth/actions/profile-actions.d.ts +69 -1
  121. package/dist/features/auth/actions/profile-actions.js +62 -1
  122. package/dist/features/auth/schemas/firestore.d.ts +16 -2
  123. package/dist/features/auth/schemas/firestore.js +2 -1
  124. package/dist/features/blog/actions/blog-actions.d.ts +6 -0
  125. package/dist/features/blog/actions/blog-actions.js +17 -2
  126. package/dist/features/blog/api/route.js +4 -1
  127. package/dist/features/blog/columns/index.js +5 -0
  128. package/dist/features/blog/components/BlogIndexPageView.js +2 -1
  129. package/dist/features/blog/repository/blog.repository.d.ts +4 -0
  130. package/dist/features/blog/repository/blog.repository.js +5 -0
  131. package/dist/features/blog/schemas/blog-form.d.ts +3 -0
  132. package/dist/features/blog/schemas/blog-form.js +1 -0
  133. package/dist/features/blog/schemas/firestore.d.ts +38 -1
  134. package/dist/features/blog/schemas/firestore.js +23 -0
  135. package/dist/features/blog/schemas/index.d.ts +6 -0
  136. package/dist/features/blog/schemas/index.js +2 -0
  137. package/dist/features/blog/types/index.d.ts +2 -0
  138. package/dist/features/cart/components/CartPriceBreakdown.d.ts +12 -1
  139. package/dist/features/cart/components/CartPriceBreakdown.js +24 -5
  140. package/dist/features/cart/repository/cart.repository.d.ts +14 -0
  141. package/dist/features/cart/repository/cart.repository.js +20 -0
  142. package/dist/features/cart/schemas/firestore.d.ts +24 -0
  143. package/dist/features/dashboard/components/DashboardHubView.d.ts +83 -0
  144. package/dist/features/dashboard/components/DashboardHubView.js +56 -0
  145. package/dist/features/dashboard/components/DashboardSectionsClient.d.ts +23 -0
  146. package/dist/features/dashboard/components/DashboardSectionsClient.js +66 -0
  147. package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.d.ts +55 -0
  148. package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.js +175 -0
  149. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +24 -0
  150. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +28 -0
  151. package/dist/features/dispute-reasons/schemas/firestore.d.ts +3 -5
  152. package/dist/features/events/components/EventSaleBanner.js +1 -1
  153. package/dist/features/faq/components/FAQPageContent.js +2 -2
  154. package/dist/features/gateway-events/schemas/index.d.ts +2 -2
  155. package/dist/features/homepage/schemas/index.d.ts +8 -8
  156. package/dist/features/inventory/schemas/stock-reservation.d.ts +114 -0
  157. package/dist/features/inventory/schemas/stock-reservation.js +103 -0
  158. package/dist/features/layout/AppLayoutShell.d.ts +9 -1
  159. package/dist/features/layout/AppLayoutShell.js +12 -4
  160. package/dist/features/layout/BottomActions.js +2 -1
  161. package/dist/features/layout/BottomNavbar.d.ts +10 -2
  162. package/dist/features/layout/SideActionDock.js +52 -2
  163. package/dist/features/layout/TitleBarLayout.js +13 -4
  164. package/dist/features/layout/TitleBarLayout.style.css +59 -0
  165. package/dist/features/layout/useCtaReveal.d.ts +16 -1
  166. package/dist/features/layout/useCtaReveal.js +17 -26
  167. package/dist/features/ledger/repository/journal.repository.d.ts +110 -0
  168. package/dist/features/ledger/repository/journal.repository.js +312 -0
  169. package/dist/features/ledger/schemas/expenses.d.ts +87 -0
  170. package/dist/features/ledger/schemas/expenses.js +27 -0
  171. package/dist/features/ledger/schemas/firestore.d.ts +183 -0
  172. package/dist/features/ledger/schemas/firestore.js +69 -0
  173. package/dist/features/orders/components/BalancePaymentPanel.d.ts +12 -0
  174. package/dist/features/orders/components/BalancePaymentPanel.js +94 -0
  175. package/dist/features/orders/components/RaiseDisputeForm.js +19 -11
  176. package/dist/features/orders/repository/orders.repository.d.ts +28 -1
  177. package/dist/features/orders/repository/orders.repository.js +60 -6
  178. package/dist/features/orders/schemas/balance.d.ts +40 -0
  179. package/dist/features/orders/schemas/balance.js +47 -0
  180. package/dist/features/orders/schemas/firestore.d.ts +202 -0
  181. package/dist/features/orders/schemas/firestore.js +43 -0
  182. package/dist/features/orders/schemas/index.d.ts +2 -2
  183. package/dist/features/orders/types/index.d.ts +21 -0
  184. package/dist/features/payments/repository/payout.repository.js +3 -2
  185. package/dist/features/payments/schemas/firestore.d.ts +92 -1
  186. package/dist/features/payments/schemas/firestore.js +23 -0
  187. package/dist/features/pricing/components/AdminPricingView.d.ts +13 -0
  188. package/dist/features/pricing/components/AdminPricingView.js +157 -0
  189. package/dist/features/products/schemas/firestore.d.ts +26 -1
  190. package/dist/features/products/schemas/firestore.js +2 -0
  191. package/dist/features/products/schemas/index.d.ts +2 -2
  192. package/dist/features/reviews/repository/reviews.repository.d.ts +14 -0
  193. package/dist/features/reviews/repository/reviews.repository.js +16 -0
  194. package/dist/features/scams/repository/scammer.repository.js +2 -1
  195. package/dist/features/scams/schemas/firestore.d.ts +2 -1
  196. package/dist/features/scams/schemas/firestore.js +2 -1
  197. package/dist/features/seller/actions/offer-actions.js +56 -0
  198. package/dist/features/seller/actions/seller-actions.d.ts +1 -1
  199. package/dist/features/seller/actions/seller-actions.js +4 -0
  200. package/dist/features/seller/components/DispatchForm.d.ts +11 -0
  201. package/dist/features/seller/components/DispatchForm.js +190 -0
  202. package/dist/features/seller/components/SellerOrdersView.js +94 -2
  203. package/dist/features/seller/components/index.d.ts +1 -0
  204. package/dist/features/seller/components/index.js +1 -0
  205. package/dist/features/seller/repository/offer.repository.js +4 -3
  206. package/dist/features/seller/schemas/firestore.d.ts +23 -2
  207. package/dist/features/seller/schemas/firestore.js +3 -2
  208. package/dist/features/seller/schemas/index.d.ts +4 -4
  209. package/dist/features/seller/schemas/order-forms.d.ts +61 -4
  210. package/dist/features/seller/schemas/order-forms.js +41 -8
  211. package/dist/features/shipments/schemas/firestore.d.ts +25 -0
  212. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +39 -0
  213. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +55 -0
  214. package/dist/features/shipping-boxes/schemas/firestore.d.ts +2 -4
  215. package/dist/features/stores/repository/store.repository.d.ts +18 -0
  216. package/dist/features/stores/repository/store.repository.js +27 -1
  217. package/dist/features/stores/schemas/firestore.d.ts +41 -0
  218. package/dist/features/stores/schemas/firestore.js +20 -0
  219. package/dist/features/stores/schemas/index.d.ts +6 -6
  220. package/dist/features/support/repository/support.repository.js +72 -6
  221. package/dist/features/support/schemas/firestore.d.ts +17 -0
  222. package/dist/features/support/schemas/index.d.ts +2 -2
  223. package/dist/index.d.ts +5 -0
  224. package/dist/index.js +19 -0
  225. package/dist/next/routing/route-map.d.ts +92 -0
  226. package/dist/next/routing/route-map.js +53 -0
  227. package/dist/react/contexts/SessionContext.d.ts +17 -1
  228. package/dist/schemas/registry.d.ts +22 -16
  229. package/dist/security/pii-schemas.d.ts +1 -1
  230. package/dist/security/pii-schemas.js +45 -0
  231. package/dist/server-entry.d.ts +1 -1
  232. package/dist/server-entry.js +10 -1
  233. package/dist/server.d.ts +29 -1
  234. package/dist/server.js +158 -0
  235. package/dist/styles.css +349 -1
  236. package/dist/tailwind-utilities.css +1 -1
  237. package/dist/ui/components/Button.d.ts +10 -1
  238. package/dist/ui/components/Button.js +25 -3
  239. package/dist/ui/components/Chart.d.ts +63 -0
  240. package/dist/ui/components/Chart.js +169 -0
  241. package/dist/ui/components/Chart.style.css +214 -0
  242. package/dist/ui/components/Disclaimer.js +10 -1
  243. package/dist/ui/components/OverflowRow.js +54 -2
  244. package/dist/ui/components/OverflowRow.style.css +38 -0
  245. package/dist/ui/components/RichTextEditor.js +99 -8
  246. package/dist/ui/components/chart-spec.d.ts +68 -0
  247. package/dist/ui/components/chart-spec.js +140 -0
  248. package/dist/ui/components/index.style.css +1 -0
  249. package/dist/ui/components/surface-tokens.d.ts +1 -1
  250. package/dist/ui/components/surface-tokens.js +9 -0
  251. package/dist/ui/components/toolbar-collapse.d.ts +13 -0
  252. package/dist/ui/components/toolbar-collapse.js +10 -1
  253. package/dist/ui/index.d.ts +6 -0
  254. package/dist/ui/index.js +3 -0
  255. package/dist/ui/rich-text/RichText.d.ts +10 -1
  256. package/dist/ui/rich-text/RichText.js +33 -2
  257. package/dist/ui/rich-text/RichText.style.css +35 -0
  258. package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
  259. package/dist/ui/rich-text/RichTextRenderer.js +34 -2
  260. package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
  261. package/dist/ui/rich-text/chart-blocks.js +87 -0
  262. package/package.json +1 -1
  263. package/scripts/seed-cli.mjs +3 -0
@@ -867,16 +867,16 @@ export declare const carouselSlideFirestoreSchema: z.ZodObject<{
867
867
  opacity?: number | undefined;
868
868
  subtitle?: string | undefined;
869
869
  } | undefined;
870
+ settings?: {
871
+ height?: "medium" | "viewport" | "tall" | undefined;
872
+ autoplayDelayMs?: number | undefined;
873
+ } | undefined;
870
874
  analytics?: {
871
875
  views: number;
872
876
  lastViewed?: string | Date | z.objectOutputType<{
873
877
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
874
878
  }, z.ZodTypeAny, "passthrough"> | undefined;
875
879
  } | undefined;
876
- settings?: {
877
- height?: "medium" | "viewport" | "tall" | undefined;
878
- autoplayDelayMs?: number | undefined;
879
- } | undefined;
880
880
  carouselId?: string | null | undefined;
881
881
  }, {
882
882
  id: string;
@@ -976,16 +976,16 @@ export declare const carouselSlideFirestoreSchema: z.ZodObject<{
976
976
  opacity?: number | undefined;
977
977
  subtitle?: string | undefined;
978
978
  } | undefined;
979
+ settings?: {
980
+ height?: "medium" | "viewport" | "tall" | undefined;
981
+ autoplayDelayMs?: number | undefined;
982
+ } | undefined;
979
983
  analytics?: {
980
984
  views: number;
981
985
  lastViewed?: string | Date | z.objectInputType<{
982
986
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
983
987
  }, z.ZodTypeAny, "passthrough"> | undefined;
984
988
  } | undefined;
985
- settings?: {
986
- height?: "medium" | "viewport" | "tall" | undefined;
987
- autoplayDelayMs?: number | undefined;
988
- } | undefined;
989
989
  carouselId?: string | null | undefined;
990
990
  }>;
991
991
  export type HomepageSectionFromSchema = z.infer<typeof homepageSectionFirestoreSchema>;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * A HOLD on stock that is not yet an order.
3
+ *
4
+ * ## 🛑 Why this has to exist before S5b (Part 6)
5
+ *
6
+ * The plan calls this "the largest hidden cost" of the shared-purchase work,
7
+ * and the reason is that today there is no way to hold stock at all. The only
8
+ * mechanism is: `onOrderCreate` decrements `availableQuantity`, and
9
+ * `pendingOrderTimeout` puts it back if the buyer does not pay inside the
10
+ * 15-minute window. Both are keyed to an ORDER.
11
+ *
12
+ * A shared purchase runs for DAYS while co-buyers pay. With no primitive,
13
+ * either the coordinator places an order up front — which starts a 15-minute
14
+ * payment clock on a multi-day agreement — or nothing is held, and the item is
15
+ * sold out from under the arrangement by an ordinary buyer who happened to
16
+ * check out first. Neither is a product.
17
+ *
18
+ * ## The invariant, and where it already lives
19
+ *
20
+ * `products.repository.ts` already states it:
21
+ *
22
+ * reserved = stockQuantity - availableQuantity
23
+ *
24
+ * …as the comment on `resolveStockResync` puts it, *"the difference between
25
+ * the two IS the outstanding reservation"*. This collection does not invent a
26
+ * new idea; it makes that difference ADDRESSABLE — every held unit gets a row
27
+ * saying who holds it and until when, instead of being an anonymous gap
28
+ * between two numbers that only an order can close.
29
+ *
30
+ * 🛑 So a reservation MUST move `availableQuantity` in the same transaction
31
+ * that writes its row. A row without the decrement holds nothing; a decrement
32
+ * without a row is the anonymous gap again, and nothing can expire it.
33
+ *
34
+ * ## Why a TTL rather than a cancel button
35
+ *
36
+ * A hold with no expiry is a leak: the holder closes the tab, and those units
37
+ * are off the shelf until a human notices. `expiresAt` means the worst case is
38
+ * bounded by the sweep interval, and the seller never has to ask anyone to
39
+ * release anything.
40
+ */
41
+ import type { BaseDocument } from "../../../_internal/shared/types/base-document";
42
+ export declare const STOCK_RESERVATION_COLLECTION = "stockReservations";
43
+ /**
44
+ * Who is holding, and therefore what releases it.
45
+ *
46
+ * 🛑 A plain `holderId: string` was the first draft and it is wrong: the sweep
47
+ * and the release path both need to know WHICH system owns the hold before
48
+ * they can decide whether it is still legitimate. A discriminator makes
49
+ * "release every hold belonging to arrangement X" expressible; an opaque id
50
+ * makes it a string-prefix convention nobody enforces.
51
+ */
52
+ export declare const RESERVATION_HOLDER_KINDS: readonly ["shared-purchase", "manual"];
53
+ export type ReservationHolderKind = (typeof RESERVATION_HOLDER_KINDS)[number];
54
+ /**
55
+ * 🛑 `cart` and `order` are deliberately NOT holder kinds.
56
+ *
57
+ * A cart holds nothing today — adding an item does not decrement anything —
58
+ * and making it hold would silently turn every abandoned cart into held
59
+ * stock. An order already has its own hold through `onOrderCreate` +
60
+ * `pendingOrderTimeout`; a second mechanism over the same units would
61
+ * double-decrement, which is exactly the class of bug `resolveStockResync`'s
62
+ * delta exists to prevent. Add a kind only when the thing that owns it also
63
+ * owns its release.
64
+ */
65
+ export declare const RESERVATION_STATUSES: readonly ["held", "consumed", "released", "expired"];
66
+ export type ReservationStatus = (typeof RESERVATION_STATUSES)[number];
67
+ export interface StockReservationDocument extends BaseDocument {
68
+ productId: string;
69
+ /** Denormalised so the sweep can restore stock without reading the product first. */
70
+ quantity: number;
71
+ holderKind: ReservationHolderKind;
72
+ /** The arrangement / manual-hold id. Scoped by `holderKind`, never globally unique. */
73
+ holderId: string;
74
+ /** Who asked for it — an Auth uid, for the audit trail and for admin release. */
75
+ createdBy: string;
76
+ status: ReservationStatus;
77
+ /**
78
+ * 🛑 The whole point. A hold with no expiry is a leak: the holder closes the
79
+ * tab and the units are gone until a human notices.
80
+ */
81
+ expiresAt: Date;
82
+ /**
83
+ * Set when the hold ENDS, whichever way it ended. One field rather than
84
+ * three (`consumedAt`/`releasedAt`/`expiredAt`) because `status` already
85
+ * says which, and three nullable dates is three chances for the one that
86
+ * matters to be the one nobody set.
87
+ */
88
+ endedAt?: Date;
89
+ /** Free text on a manual release — why an admin took the hold back. */
90
+ endedReason?: string;
91
+ }
92
+ /** Default hold, when a caller names none. */
93
+ export declare const RESERVATION_DEFAULT_TTL_HOURS = 72;
94
+ /**
95
+ * 🛑 A ceiling, and it is load-bearing rather than defensive.
96
+ *
97
+ * A caller may pass any TTL it likes, which makes "hold this for a year" one
98
+ * typo away — and a year-long hold is indistinguishable from permanently
99
+ * deleted stock, because nothing in the UI shows a seller why their own
100
+ * product is unbuyable. 30 days is longer than any agreement window Part 6
101
+ * contemplates and short enough that a mistake self-heals.
102
+ */
103
+ export declare const RESERVATION_MAX_TTL_HOURS: number;
104
+ /** A reservation is only holding stock while it is `held` and unexpired. */
105
+ export declare function isReservationActive(reservation: Pick<StockReservationDocument, "status" | "expiresAt">, now?: Date): boolean;
106
+ /**
107
+ * Firestore hands back a `Timestamp`, a `Date` or an ISO string depending on
108
+ * the path a document took to get here, and the adapter that normalises those
109
+ * is not reachable from this file. Returning `null` on anything unparseable is
110
+ * the honest answer — and callers treat an unparseable expiry as NOT active,
111
+ * so the failure mode is a hold released early rather than one held forever.
112
+ */
113
+ declare function toDate(value: unknown): Date | null;
114
+ export { toDate as reservationExpiryDate };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * A HOLD on stock that is not yet an order.
3
+ *
4
+ * ## 🛑 Why this has to exist before S5b (Part 6)
5
+ *
6
+ * The plan calls this "the largest hidden cost" of the shared-purchase work,
7
+ * and the reason is that today there is no way to hold stock at all. The only
8
+ * mechanism is: `onOrderCreate` decrements `availableQuantity`, and
9
+ * `pendingOrderTimeout` puts it back if the buyer does not pay inside the
10
+ * 15-minute window. Both are keyed to an ORDER.
11
+ *
12
+ * A shared purchase runs for DAYS while co-buyers pay. With no primitive,
13
+ * either the coordinator places an order up front — which starts a 15-minute
14
+ * payment clock on a multi-day agreement — or nothing is held, and the item is
15
+ * sold out from under the arrangement by an ordinary buyer who happened to
16
+ * check out first. Neither is a product.
17
+ *
18
+ * ## The invariant, and where it already lives
19
+ *
20
+ * `products.repository.ts` already states it:
21
+ *
22
+ * reserved = stockQuantity - availableQuantity
23
+ *
24
+ * …as the comment on `resolveStockResync` puts it, *"the difference between
25
+ * the two IS the outstanding reservation"*. This collection does not invent a
26
+ * new idea; it makes that difference ADDRESSABLE — every held unit gets a row
27
+ * saying who holds it and until when, instead of being an anonymous gap
28
+ * between two numbers that only an order can close.
29
+ *
30
+ * 🛑 So a reservation MUST move `availableQuantity` in the same transaction
31
+ * that writes its row. A row without the decrement holds nothing; a decrement
32
+ * without a row is the anonymous gap again, and nothing can expire it.
33
+ *
34
+ * ## Why a TTL rather than a cancel button
35
+ *
36
+ * A hold with no expiry is a leak: the holder closes the tab, and those units
37
+ * are off the shelf until a human notices. `expiresAt` means the worst case is
38
+ * bounded by the sweep interval, and the seller never has to ask anyone to
39
+ * release anything.
40
+ */
41
+ export const STOCK_RESERVATION_COLLECTION = "stockReservations";
42
+ /**
43
+ * Who is holding, and therefore what releases it.
44
+ *
45
+ * 🛑 A plain `holderId: string` was the first draft and it is wrong: the sweep
46
+ * and the release path both need to know WHICH system owns the hold before
47
+ * they can decide whether it is still legitimate. A discriminator makes
48
+ * "release every hold belonging to arrangement X" expressible; an opaque id
49
+ * makes it a string-prefix convention nobody enforces.
50
+ */
51
+ export const RESERVATION_HOLDER_KINDS = ["shared-purchase", "manual"];
52
+ /**
53
+ * 🛑 `cart` and `order` are deliberately NOT holder kinds.
54
+ *
55
+ * A cart holds nothing today — adding an item does not decrement anything —
56
+ * and making it hold would silently turn every abandoned cart into held
57
+ * stock. An order already has its own hold through `onOrderCreate` +
58
+ * `pendingOrderTimeout`; a second mechanism over the same units would
59
+ * double-decrement, which is exactly the class of bug `resolveStockResync`'s
60
+ * delta exists to prevent. Add a kind only when the thing that owns it also
61
+ * owns its release.
62
+ */
63
+ export const RESERVATION_STATUSES = ["held", "consumed", "released", "expired"];
64
+ /** Default hold, when a caller names none. */
65
+ export const RESERVATION_DEFAULT_TTL_HOURS = 72;
66
+ /**
67
+ * 🛑 A ceiling, and it is load-bearing rather than defensive.
68
+ *
69
+ * A caller may pass any TTL it likes, which makes "hold this for a year" one
70
+ * typo away — and a year-long hold is indistinguishable from permanently
71
+ * deleted stock, because nothing in the UI shows a seller why their own
72
+ * product is unbuyable. 30 days is longer than any agreement window Part 6
73
+ * contemplates and short enough that a mistake self-heals.
74
+ */
75
+ export const RESERVATION_MAX_TTL_HOURS = 24 * 30;
76
+ /** A reservation is only holding stock while it is `held` and unexpired. */
77
+ export function isReservationActive(reservation, now = new Date()) {
78
+ if (reservation.status !== "held")
79
+ return false;
80
+ const expires = toDate(reservation.expiresAt);
81
+ return expires !== null && expires.getTime() > now.getTime();
82
+ }
83
+ /**
84
+ * Firestore hands back a `Timestamp`, a `Date` or an ISO string depending on
85
+ * the path a document took to get here, and the adapter that normalises those
86
+ * is not reachable from this file. Returning `null` on anything unparseable is
87
+ * the honest answer — and callers treat an unparseable expiry as NOT active,
88
+ * so the failure mode is a hold released early rather than one held forever.
89
+ */
90
+ function toDate(value) {
91
+ if (value instanceof Date)
92
+ return Number.isNaN(value.getTime()) ? null : value;
93
+ if (value && typeof value.toDate === "function") {
94
+ const d = value.toDate();
95
+ return d instanceof Date && !Number.isNaN(d.getTime()) ? d : null;
96
+ }
97
+ if (typeof value === "string" || typeof value === "number") {
98
+ const d = new Date(value);
99
+ return Number.isNaN(d.getTime()) ? null : d;
100
+ }
101
+ return null;
102
+ }
103
+ export { toDate as reservationExpiryDate };
@@ -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;