@mohasinac/appkit 4.42.1 → 4.42.3

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 (182) hide show
  1. package/dist/_internal/server/features/offers/adapters.d.ts +29 -7
  2. package/dist/_internal/server/features/offers/adapters.js +14 -3
  3. package/dist/_internal/server/features/orders/adapters.js +54 -1
  4. package/dist/client.d.ts +1 -0
  5. package/dist/client.js +1 -0
  6. package/dist/features/account/components/UserOrdersView.js +30 -2
  7. package/dist/features/account/hooks/useAddresses.d.ts +21 -6
  8. package/dist/features/account/hooks/useAddresses.js +1 -1
  9. package/dist/features/addresses/hooks/usePostalLookup.js +84 -12
  10. package/dist/features/admin/components/AdminCategoriesView.js +37 -2
  11. package/dist/features/admin/components/AdminOffersView.js +2 -2
  12. package/dist/features/admin/components/AdminSupportTicketsView.js +27 -1
  13. package/dist/features/admin/constants/filter-tabs.d.ts +25 -0
  14. package/dist/features/admin/constants/filter-tabs.js +30 -0
  15. package/dist/features/admin/hooks/useAdminListingData.d.ts +35 -0
  16. package/dist/features/admin/hooks/useAdminListingData.js +45 -9
  17. package/dist/features/cart/schemas/index.d.ts +15 -0
  18. package/dist/features/cart/schemas/index.js +2 -0
  19. package/dist/features/cart/types/index.d.ts +22 -0
  20. package/dist/features/categories/components/CategoriesIndexListing.js +19 -1
  21. package/dist/features/categories/components/CategoriesIndexPageView.js +14 -1
  22. package/dist/features/classified/components/ClassifiedDetailPageView.d.ts +8 -0
  23. package/dist/features/classified/components/ClassifiedDetailPageView.js +1 -0
  24. package/dist/features/events/actions/event-actions.d.ts +13 -0
  25. package/dist/features/events/actions/event-actions.js +75 -1
  26. package/dist/features/events/repository/event-entry.repository.d.ts +17 -0
  27. package/dist/features/events/repository/event-entry.repository.js +36 -0
  28. package/dist/features/homepage/components/BrandsSection.js +1 -1
  29. package/dist/features/homepage/components/CustomerReviewsSection.js +1 -1
  30. package/dist/features/homepage/components/FeaturedBundlesSection.js +1 -1
  31. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  32. package/dist/features/media/upload/MediaUploadField.js +21 -3
  33. package/dist/features/orders/components/OrdersList.js +2 -1
  34. package/dist/features/orders/index.d.ts +1 -0
  35. package/dist/features/orders/index.js +1 -0
  36. package/dist/features/orders/types/index.d.ts +2 -0
  37. package/dist/features/orders/utils/order-ref.d.ts +45 -0
  38. package/dist/features/orders/utils/order-ref.js +58 -0
  39. package/dist/features/products/components/MakeOfferButton.d.ts +18 -1
  40. package/dist/features/products/components/MakeOfferButton.js +23 -1
  41. package/dist/features/products/components/ProductDetailPageView.d.ts +5 -0
  42. package/dist/features/products/components/ProductDetailPageView.js +3 -2
  43. package/dist/features/products/repository/products.repository.js +29 -2
  44. package/dist/features/seller/actions/offer-actions.js +32 -0
  45. package/dist/features/seller/components/QuickProductForm.js +15 -1
  46. package/dist/features/seller/components/SellerBidsView.js +3 -1
  47. package/dist/features/seller/components/SellerOffersView.js +2 -2
  48. package/dist/features/seller/components/SellerOrdersView.js +49 -6
  49. package/dist/features/seller/components/SellerProductsCards.js +23 -1
  50. package/dist/features/seller/components/SellerProductsView.js +27 -2
  51. package/dist/features/seller/hooks/useSellerListingData.d.ts +0 -1
  52. package/dist/features/seller/hooks/useSellerListingData.js +17 -19
  53. package/dist/features/shell/SectionForm.js +33 -2
  54. package/dist/features/tester/seed-data/_happy-path.d.ts +23 -0
  55. package/dist/features/tester/seed-data/_happy-path.js +710 -0
  56. package/dist/features/tester/seed-data/_money-flows.js +34 -23
  57. package/dist/features/tester/seed-data/authored/account-auth__profile-settings.js +22 -3
  58. package/dist/features/tester/seed-data/authored/addresses__address-filters.js +17 -12
  59. package/dist/features/tester/seed-data/authored/addresses__postal-lookup.js +7 -6
  60. package/dist/features/tester/seed-data/authored/addresses__postal-validation.js +10 -4
  61. package/dist/features/tester/seed-data/authored/addresses__state-picker.js +13 -8
  62. package/dist/features/tester/seed-data/authored/addresses__unban-request.js +49 -11
  63. package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +10 -6
  64. package/dist/features/tester/seed-data/authored/admin__bans-and-trust.js +71 -32
  65. package/dist/features/tester/seed-data/authored/admin__blog-faqs.js +80 -31
  66. package/dist/features/tester/seed-data/authored/admin__bug-hunter-rewards.js +62 -25
  67. package/dist/features/tester/seed-data/authored/admin__bulk-actions.js +61 -31
  68. package/dist/features/tester/seed-data/authored/admin__bundles.js +84 -37
  69. package/dist/features/tester/seed-data/authored/admin__buyer-data-admin.js +90 -57
  70. package/dist/features/tester/seed-data/authored/admin__catalog-listings.js +38 -26
  71. package/dist/features/tester/seed-data/authored/admin__category-brand-authoring.js +47 -28
  72. package/dist/features/tester/seed-data/authored/admin__classifieds-digitalcodes-live.js +118 -31
  73. package/dist/features/tester/seed-data/authored/admin__content-deletes.js +30 -24
  74. package/dist/features/tester/seed-data/authored/admin__content-marketing.js +83 -46
  75. package/dist/features/tester/seed-data/authored/admin__coupons.js +8 -8
  76. package/dist/features/tester/seed-data/authored/admin__events-raffles-spin.js +9 -9
  77. package/dist/features/tester/seed-data/authored/admin__firebase-function-effects.js +19 -4
  78. package/dist/features/tester/seed-data/authored/admin__media-watermark.js +77 -33
  79. package/dist/features/tester/seed-data/authored/admin__orders-fulfillment.js +5 -5
  80. package/dist/features/tester/seed-data/authored/admin__prize-draws-lotteries.js +29 -5
  81. package/dist/features/tester/seed-data/authored/admin__site-system.js +45 -23
  82. package/dist/features/tester/seed-data/authored/admin__uncovered-admin-pages.js +53 -28
  83. package/dist/features/tester/seed-data/authored/admin__users-trust.js +20 -20
  84. package/dist/features/tester/seed-data/authored/buying__bidding.js +19 -2
  85. package/dist/features/tester/seed-data/authored/buying__browsing-search.js +20 -5
  86. package/dist/features/tester/seed-data/authored/buying__buying-checkout.js +28 -10
  87. package/dist/features/tester/seed-data/authored/buying__cart.js +10 -3
  88. package/dist/features/tester/seed-data/authored/buying__image-tile-layout.js +0 -16
  89. package/dist/features/tester/seed-data/authored/buying__my-orders.js +27 -5
  90. package/dist/features/tester/seed-data/authored/buying__offers.js +22 -7
  91. package/dist/features/tester/seed-data/authored/buying__order-detail-actions.js +3 -3
  92. package/dist/features/tester/seed-data/authored/buying__product-detail.js +35 -4
  93. package/dist/features/tester/seed-data/authored/buying__return-request.js +19 -6
  94. package/dist/features/tester/seed-data/authored/buying__reviews-pagination.js +36 -18
  95. package/dist/features/tester/seed-data/authored/buying__reviews.js +16 -1
  96. package/dist/features/tester/seed-data/authored/buying__user-dashboard-navigation.js +2 -1
  97. package/dist/features/tester/seed-data/authored/buying__user-uncovered-pages.js +16 -1
  98. package/dist/features/tester/seed-data/authored/community-support__public-profile.js +3 -2
  99. package/dist/features/tester/seed-data/authored/community-support__support-tickets.js +4 -4
  100. package/dist/features/tester/seed-data/authored/content-discovery__blog.js +19 -2
  101. package/dist/features/tester/seed-data/authored/content-discovery__category-brand-relations.js +4 -4
  102. package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +5 -5
  103. package/dist/features/tester/seed-data/authored/content-discovery__coupons.js +41 -6
  104. package/dist/features/tester/seed-data/authored/content-discovery__event-detail-subroutes.js +63 -8
  105. package/dist/features/tester/seed-data/authored/content-discovery__event-participation.js +29 -29
  106. package/dist/features/tester/seed-data/authored/content-discovery__events.js +2 -2
  107. package/dist/features/tester/seed-data/authored/content-discovery__faq-help.js +23 -1
  108. package/dist/features/tester/seed-data/authored/content-discovery__notifications.js +6 -6
  109. package/dist/features/tester/seed-data/authored/content-discovery__search.js +1 -1
  110. package/dist/features/tester/seed-data/authored/content-discovery__store-reviews-aggregate.js +1 -1
  111. package/dist/features/tester/seed-data/authored/cta-layout__checkout-bottom-bar.js +31 -6
  112. package/dist/features/tester/seed-data/authored/cta-layout__dialog-footers.js +34 -10
  113. package/dist/features/tester/seed-data/authored/cta-layout__editor-action-bar.js +35 -8
  114. package/dist/features/tester/seed-data/authored/cta-layout__navbar-ctas.js +31 -9
  115. package/dist/features/tester/seed-data/authored/cta-layout__product-bottom-bar.js +29 -3
  116. package/dist/features/tester/seed-data/authored/design-ux__back-to-top-button.js +1 -1
  117. package/dist/features/tester/seed-data/authored/design-ux__carousel-arrow-bounds.js +10 -10
  118. package/dist/features/tester/seed-data/authored/design-ux__dashboard-layout.js +3 -3
  119. package/dist/features/tester/seed-data/authored/design-ux__footer-theme.js +2 -2
  120. package/dist/features/tester/seed-data/authored/design-ux__form-validation-errors.js +2 -2
  121. package/dist/features/tester/seed-data/authored/design-ux__general-design.js +4 -4
  122. package/dist/features/tester/seed-data/authored/design-ux__hand-mode-layout.js +1 -1
  123. package/dist/features/tester/seed-data/authored/design-ux__homepage-carousels.js +8 -8
  124. package/dist/features/tester/seed-data/authored/design-ux__status-badge-legibility.js +2 -2
  125. package/dist/features/tester/seed-data/authored/design-ux__sticky-cta-bar.js +2 -2
  126. package/dist/features/tester/seed-data/authored/page-wiring__data-loss.js +39 -16
  127. package/dist/features/tester/seed-data/authored/page-wiring__detail-pages.js +43 -8
  128. package/dist/features/tester/seed-data/authored/page-wiring__drawer-pages.js +40 -14
  129. package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +31 -14
  130. package/dist/features/tester/seed-data/authored/public-pages__auth-error-pages.js +7 -6
  131. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +14 -5
  132. package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.js +3 -3
  133. package/dist/features/tester/seed-data/authored/public-pages__help-how-it-works.js +1 -1
  134. package/dist/features/tester/seed-data/authored/public-pages__help-scams-guides-subpages.js +4 -2
  135. package/dist/features/tester/seed-data/authored/public-pages__legal-policy-pages.js +3 -3
  136. package/dist/features/tester/seed-data/authored/public-pages__newly-wired-browse-indexes.js +5 -5
  137. package/dist/features/tester/seed-data/authored/public-pages__store-tabs.js +5 -5
  138. package/dist/features/tester/seed-data/authored/public-pages__stores-sellers-directories.js +3 -3
  139. package/dist/features/tester/seed-data/authored/search-and-nav__employee-permissions.js +45 -11
  140. package/dist/features/tester/seed-data/authored/search-and-nav__header-search.js +57 -17
  141. package/dist/features/tester/seed-data/authored/search-and-nav__settings-deep-links.js +36 -5
  142. package/dist/features/tester/seed-data/authored/search-and-nav__sidebar-search.js +29 -4
  143. package/dist/features/tester/seed-data/authored/selling__become-seller.js +23 -0
  144. package/dist/features/tester/seed-data/authored/selling__final-sale-authoring.js +14 -13
  145. package/dist/features/tester/seed-data/authored/selling__listing-a-product.js +14 -9
  146. package/dist/features/tester/seed-data/authored/selling__listing-edit-roundtrip.js +34 -7
  147. package/dist/features/tester/seed-data/authored/selling__listing-lifecycle.js +17 -7
  148. package/dist/features/tester/seed-data/authored/selling__listing-type-fields-roundtrip.js +9 -9
  149. package/dist/features/tester/seed-data/authored/selling__media-limits.js +9 -4
  150. package/dist/features/tester/seed-data/authored/selling__product-upload-details.js +3 -3
  151. package/dist/features/tester/seed-data/authored/selling__quick-add-minimum-details.js +3 -3
  152. package/dist/features/tester/seed-data/authored/selling__sectionised-forms.js +25 -1
  153. package/dist/features/tester/seed-data/authored/selling__seller-analytics-payouts.js +27 -6
  154. package/dist/features/tester/seed-data/authored/selling__seller-bids-bundles-filters.js +19 -1
  155. package/dist/features/tester/seed-data/authored/selling__seller-catalog-org.js +63 -22
  156. package/dist/features/tester/seed-data/authored/selling__seller-custom-brands.js +9 -5
  157. package/dist/features/tester/seed-data/authored/selling__seller-guide.js +7 -6
  158. package/dist/features/tester/seed-data/authored/selling__seller-listing-types.js +3 -2
  159. package/dist/features/tester/seed-data/authored/selling__seller-marketing-extras.js +18 -18
  160. package/dist/features/tester/seed-data/authored/selling__seller-ops-comms.js +13 -12
  161. package/dist/features/tester/seed-data/authored/selling__seller-orders-at-scale.js +19 -3
  162. package/dist/features/tester/seed-data/authored/selling__seller-orders.js +22 -4
  163. package/dist/features/tester/seed-data/authored/selling__seller-shipping-payouts-setup.js +20 -8
  164. package/dist/features/tester/seed-data/authored/selling__store-dashboard-navigation.js +4 -4
  165. package/dist/features/tester/seed-data/authored/selling__store-uncovered-pages.js +2 -2
  166. package/dist/features/tester/seed-data/authored/seo__canonical-and-host.js +5 -5
  167. package/dist/features/tester/seed-data/authored/seo__og-images.js +1 -1
  168. package/dist/features/tester/seed-data/authored/seo__page-metadata.js +7 -7
  169. package/dist/features/tester/seed-data/authored/seo__sitemap-and-robots.js +3 -3
  170. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +123 -21
  171. package/dist/features/tester/utils/phases.d.ts +17 -0
  172. package/dist/features/tester/utils/phases.js +29 -2
  173. package/dist/index.d.ts +1 -0
  174. package/dist/index.js +2 -0
  175. package/dist/seo/json-ld.d.ts +23 -0
  176. package/dist/seo/json-ld.js +3 -2
  177. package/dist/server.d.ts +1 -0
  178. package/dist/server.js +2 -0
  179. package/dist/styles.css +1 -1
  180. package/dist/tailwind-utilities.css +1 -1
  181. package/dist/ui/components/PaginatedSelect.js +0 -0
  182. package/package.json +1 -1
@@ -63,6 +63,41 @@ export function toCurrency(value) {
63
63
  }
64
64
  return formatCurrency(value);
65
65
  }
66
+ /**
67
+ * A timestamp as human-relative text — **in either direction**.
68
+ *
69
+ * 🛑 **Every FUTURE date used to render as "1m ago".** `deltaMs` is
70
+ * `now - date`, which is negative ahead of now; `deltaMs < hour` then matched,
71
+ * and `Math.max(1, Math.floor(negative))` clamped the result to exactly `1`.
72
+ * So a deadline 48 hours away and one 5 minutes away both read "1m ago", and
73
+ * the clamp is what hid it — without it the string would have been
74
+ * "-2880m ago", which nobody could have mistaken for working.
75
+ *
76
+ * Measured live on `/store/offers` as tyson@beybladearena.in: an offer accepted
77
+ * seconds earlier showed **"Buyer must pay by 1m ago"** against a real
78
+ * `checkoutDeadline` of `now + 48h` (`OFFER_CHECKOUT_WINDOW_MS`). The data was
79
+ * correct and the sentence told the seller — and on the admin view, the admin —
80
+ * that a buyer had already missed a window that had barely opened. On the
81
+ * buyer's own side that reads as "too late to pay for the thing you just
82
+ * negotiated", i.e. an abandoned purchase caused entirely by a formatter.
83
+ *
84
+ * Five call sites render a future-capable field today (offer `expiresAt` and
85
+ * `checkoutDeadline` on both the seller and admin views, and session
86
+ * `expiresAt`), and any of the other 61 becomes one the moment its field can
87
+ * hold a future date — which is why this is fixed here rather than at the call
88
+ * sites.
89
+ *
90
+ * The `Math.max(1, …)` floors stay for the PAST direction: they keep a
91
+ * 40-second-old row reading "1m ago" instead of "0m ago". They are applied to
92
+ * an absolute magnitude now, so they can no longer mask a sign error.
93
+ *
94
+ * NOTE ON ITS HOME: a pure date formatter belongs in `ui/` or
95
+ * `_internal/client/`, not in an admin hook module that 44 files reach into.
96
+ * Moving it means rewriting all 44 imports, so it stays put for now and the
97
+ * seller-side duplicate was deleted instead (see `useSellerListingData.ts`) —
98
+ * that copy had already drifted, returning "just now" for the same future date
99
+ * this one called "1m ago". Two answers to one question is the whole argument.
100
+ */
66
101
  export function toRelativeDate(value) {
67
102
  const date = parseDate(value);
68
103
  if (!date) {
@@ -72,17 +107,18 @@ export function toRelativeDate(value) {
72
107
  const minute = 60000;
73
108
  const hour = 60 * minute;
74
109
  const day = 24 * hour;
75
- if (deltaMs < hour) {
76
- const minutes = Math.max(1, Math.floor(deltaMs / minute));
77
- return `${minutes}m ago`;
110
+ const future = deltaMs < 0;
111
+ const magnitude = Math.abs(deltaMs);
112
+ // "in 3h" vs "3h ago" — the only difference between the two directions.
113
+ const phrase = (n, unit) => (future ? `in ${n}${unit}` : `${n}${unit} ago`);
114
+ if (magnitude < hour) {
115
+ return phrase(Math.max(1, Math.floor(magnitude / minute)), "m");
78
116
  }
79
- if (deltaMs < day) {
80
- const hours = Math.max(1, Math.floor(deltaMs / hour));
81
- return `${hours}h ago`;
117
+ if (magnitude < day) {
118
+ return phrase(Math.max(1, Math.floor(magnitude / hour)), "h");
82
119
  }
83
- if (deltaMs < 7 * day) {
84
- const days = Math.max(1, Math.floor(deltaMs / day));
85
- return `${days}d ago`;
120
+ if (magnitude < 7 * day) {
121
+ return phrase(Math.max(1, Math.floor(magnitude / day)), "d");
86
122
  }
87
123
  return date.toLocaleDateString("en-IN", {
88
124
  day: "2-digit",
@@ -5,17 +5,20 @@ export declare const cartItemMetaSchema: z.ZodObject<{
5
5
  price: z.ZodNumber;
6
6
  currency: z.ZodDefault<z.ZodString>;
7
7
  slug: z.ZodOptional<z.ZodString>;
8
+ storeName: z.ZodOptional<z.ZodString>;
8
9
  attributes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
9
10
  }, "strip", z.ZodTypeAny, {
10
11
  currency: string;
11
12
  title: string;
12
13
  price: number;
14
+ storeName?: string | undefined;
13
15
  slug?: string | undefined;
14
16
  attributes?: Record<string, string> | undefined;
15
17
  }, {
16
18
  title: string;
17
19
  price: number;
18
20
  currency?: string | undefined;
21
+ storeName?: string | undefined;
19
22
  slug?: string | undefined;
20
23
  attributes?: Record<string, string> | undefined;
21
24
  }>;
@@ -40,17 +43,20 @@ export declare const cartItemSchema: z.ZodObject<{
40
43
  price: z.ZodNumber;
41
44
  currency: z.ZodDefault<z.ZodString>;
42
45
  slug: z.ZodOptional<z.ZodString>;
46
+ storeName: z.ZodOptional<z.ZodString>;
43
47
  attributes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
44
48
  }, "strip", z.ZodTypeAny, {
45
49
  currency: string;
46
50
  title: string;
47
51
  price: number;
52
+ storeName?: string | undefined;
48
53
  slug?: string | undefined;
49
54
  attributes?: Record<string, string> | undefined;
50
55
  }, {
51
56
  title: string;
52
57
  price: number;
53
58
  currency?: string | undefined;
59
+ storeName?: string | undefined;
54
60
  slug?: string | undefined;
55
61
  attributes?: Record<string, string> | undefined;
56
62
  }>>;
@@ -63,6 +69,7 @@ export declare const cartItemSchema: z.ZodObject<{
63
69
  currency: string;
64
70
  title: string;
65
71
  price: number;
72
+ storeName?: string | undefined;
66
73
  slug?: string | undefined;
67
74
  attributes?: Record<string, string> | undefined;
68
75
  } | undefined;
@@ -76,6 +83,7 @@ export declare const cartItemSchema: z.ZodObject<{
76
83
  title: string;
77
84
  price: number;
78
85
  currency?: string | undefined;
86
+ storeName?: string | undefined;
79
87
  slug?: string | undefined;
80
88
  attributes?: Record<string, string> | undefined;
81
89
  } | undefined;
@@ -96,17 +104,20 @@ export declare const cartSummarySchema: z.ZodObject<{
96
104
  price: z.ZodNumber;
97
105
  currency: z.ZodDefault<z.ZodString>;
98
106
  slug: z.ZodOptional<z.ZodString>;
107
+ storeName: z.ZodOptional<z.ZodString>;
99
108
  attributes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
100
109
  }, "strip", z.ZodTypeAny, {
101
110
  currency: string;
102
111
  title: string;
103
112
  price: number;
113
+ storeName?: string | undefined;
104
114
  slug?: string | undefined;
105
115
  attributes?: Record<string, string> | undefined;
106
116
  }, {
107
117
  title: string;
108
118
  price: number;
109
119
  currency?: string | undefined;
120
+ storeName?: string | undefined;
110
121
  slug?: string | undefined;
111
122
  attributes?: Record<string, string> | undefined;
112
123
  }>>;
@@ -119,6 +130,7 @@ export declare const cartSummarySchema: z.ZodObject<{
119
130
  currency: string;
120
131
  title: string;
121
132
  price: number;
133
+ storeName?: string | undefined;
122
134
  slug?: string | undefined;
123
135
  attributes?: Record<string, string> | undefined;
124
136
  } | undefined;
@@ -132,6 +144,7 @@ export declare const cartSummarySchema: z.ZodObject<{
132
144
  title: string;
133
145
  price: number;
134
146
  currency?: string | undefined;
147
+ storeName?: string | undefined;
135
148
  slug?: string | undefined;
136
149
  attributes?: Record<string, string> | undefined;
137
150
  } | undefined;
@@ -153,6 +166,7 @@ export declare const cartSummarySchema: z.ZodObject<{
153
166
  currency: string;
154
167
  title: string;
155
168
  price: number;
169
+ storeName?: string | undefined;
156
170
  slug?: string | undefined;
157
171
  attributes?: Record<string, string> | undefined;
158
172
  } | undefined;
@@ -170,6 +184,7 @@ export declare const cartSummarySchema: z.ZodObject<{
170
184
  title: string;
171
185
  price: number;
172
186
  currency?: string | undefined;
187
+ storeName?: string | undefined;
173
188
  slug?: string | undefined;
174
189
  attributes?: Record<string, string> | undefined;
175
190
  } | undefined;
@@ -6,6 +6,8 @@ export const cartItemMetaSchema = z.object({
6
6
  price: z.number(),
7
7
  currency: z.string().default(getDefaultCurrency()),
8
8
  slug: z.string().optional(),
9
+ /* Named field, not an `attributes` entry — see CartItemMeta.storeName. */
10
+ storeName: z.string().optional(),
9
11
  attributes: z.record(z.string(), z.string()).optional(),
10
12
  });
11
13
  /**
@@ -1,11 +1,33 @@
1
1
  export interface CartItemMeta {
2
2
  productId: string;
3
3
  storeId?: string;
4
+ /**
5
+ * The seller's display name, for grouping lines by seller and for the cart's
6
+ * own search box.
7
+ *
8
+ * 🛑 It lives here and NOT in `attributes`, which is rendered verbatim.
9
+ * `CartItemRow` prints every attribute entry as `` `${key}: ${value}` ``, so
10
+ * putting the store name in that bag printed the literal string
11
+ * `storeName: Beyblade Arena` under the product title on `/cart` — a field
12
+ * name shown to a buyer on the highest-intent page in the funnel, one line
13
+ * below the same value already rendered properly as `SOLD BY BEYBLADE ARENA`.
14
+ */
15
+ storeName?: string;
4
16
  title: string;
5
17
  image?: string;
6
18
  price: number;
7
19
  currency?: string;
8
20
  slug?: string;
21
+ /**
22
+ * Genuine per-line product attributes — the kind a buyer needs in order to
23
+ * tell two lines of the same product apart (size, colour, edition).
24
+ *
25
+ * 🛑 DISPLAY ONLY. Every entry is rendered to the buyer as
26
+ * `` `${key}: ${value}` ``, joined with commas. Never use this to carry
27
+ * plumbing a component needs: a field put here to be *read* is also a field
28
+ * printed with its own name, and the leak looks like a debug string rather
29
+ * than like a bug. Add a named field to this interface instead.
30
+ */
9
31
  attributes?: Record<string, string>;
10
32
  }
11
33
  export interface CartItem {
@@ -38,9 +38,27 @@ function renderCategoryFilterDrawer(props) {
38
38
  function getNumericParam(table, key) {
39
39
  return table.get(key) ? Number(table.get(key)) : undefined;
40
40
  }
41
- const DEFAULT_SORT = sortBy(CATEGORY_FIELDS.NAME, "ASC");
41
+ /*
42
+ * 🛑 TIER FIRST, NOT NAME. Found by
43
+ * checklist-happy-path-guest-browse-categories-index-lists-roots, 2026-09-29.
44
+ *
45
+ * This was `name ASC`, which sorts 47 categories alphabetically with no
46
+ * awareness of the tree — so BOTH roots fell off page 1. "Spinning Tops" sorts
47
+ * under S onto page 3 and "Living Collectibles" under L onto page 2, while
48
+ * their own descendants filled page 1: Bits, Blades, Discs, Drivers, Energy
49
+ * Rings. A visitor opening /categories met component-level leaves, eight of
50
+ * them reading "0 items", and never saw either root.
51
+ *
52
+ * `tier` is already `canSort: true` in the repository's SIEVE_FIELDS, and a
53
+ * single-field sort needs no composite index, so this costs nothing to deploy.
54
+ * Deliberately NOT `tier ASC, name ASC`: that pairing has no
55
+ * `(tier, name)` index and a missing composite fails as an empty page rather
56
+ * than an error (Root Cause #59).
57
+ */
58
+ const DEFAULT_SORT = sortBy(CATEGORY_FIELDS.TIER, "ASC");
42
59
  const FILTER_KEYS = [TABLE_KEYS.IS_FEATURED, "isBrand", "rootOnly", "tier", "minItemCount", "maxItemCount"];
43
60
  const SORT_OPTIONS = [
61
+ { value: sortBy(CATEGORY_FIELDS.TIER, "ASC"), label: "Top level first" },
44
62
  { value: sortBy(CATEGORY_FIELDS.NAME, "ASC"), label: "Name A–Z" },
45
63
  { value: sortBy(CATEGORY_FIELDS.NAME), label: "Name Z–A" },
46
64
  { value: sortBy("metrics.productCount", "DESC"), label: "Most Products" },
@@ -1,5 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit";
3
+ import { sortBy } from "../../../constants/sort";
4
+ import { CATEGORY_FIELDS } from "../../../constants/field-names";
3
5
  import { categoriesRepository } from "../../../repositories";
4
6
  import { Container, Heading, Main, Section } from "../../../ui";
5
7
  import { AdSlot } from "../../homepage/components/AdSlot";
@@ -38,7 +40,18 @@ function buildCategoryFilters(params) {
38
40
  return parts.join(",");
39
41
  }
40
42
  export async function CategoriesIndexPageView({ searchParams = {} }) {
41
- const sort = sp(searchParams, "sort") || "name";
43
+ /*
44
+ * 🛑 MUST MATCH CategoriesIndexListing's DEFAULT_SORT.
45
+ *
46
+ * This defaulted to "name" while the client listing defaulted to the same,
47
+ * and both were wrong: the roots fell off page 1. Now both default to tier
48
+ * ascending — and they have to agree, because this view hands its result to
49
+ * the listing as initialData and every public listing hook sets
50
+ * `staleTime: Infinity` when given it. A disagreement here is not a
51
+ * transient wrong first paint; it is frozen for that query key
52
+ * (Root Cause #30).
53
+ */
54
+ const sort = sp(searchParams, "sort") || sortBy(CATEGORY_FIELDS.TIER, "ASC");
42
55
  const page = Number(sp(searchParams, "page")) || 1;
43
56
  const filters = buildCategoryFilters(searchParams);
44
57
  const result = await safeRead(() => categoriesRepository.list({
@@ -18,6 +18,14 @@ export interface ClassifiedDetailPageViewProps {
18
18
  currency: string;
19
19
  minOfferPercent: number;
20
20
  bounds: OfferBounds;
21
+ /**
22
+ * The owning store's SLUG, so the control can suppress itself for the
23
+ * seller who owns the listing. Kept identical to `ProductDetailPageView`'s
24
+ * ctx — the comment at the `/classified/[slug]` call site asserts the two
25
+ * contracts match precisely so a consumer snippet can be shared, and an
26
+ * added field on one is drift unless it lands on both.
27
+ */
28
+ listingStoreId?: string;
21
29
  }) => React.ReactNode;
22
30
  }
23
31
  export declare function ClassifiedDetailPageView({ slug, initialProduct, renderOfferAction }: ClassifiedDetailPageViewProps): Promise<import("react").JSX.Element>;
@@ -105,6 +105,7 @@ export async function ClassifiedDetailPageView({ slug, initialProduct, renderOff
105
105
  minOfferPercent: typeof p.minOfferPercent === "number"
106
106
  ? p.minOfferPercent
107
107
  : DEFAULT_MIN_OFFER_PERCENT,
108
+ listingStoreId: typeof p.storeId === "string" ? p.storeId : undefined,
108
109
  bounds: offerBounds,
109
110
  }), _jsx(Text, { size: "xs", color: "muted", children: offerBounds.isBuyRequest
110
111
  ? "Send a request to buy and the seller will confirm. Payment and collection are arranged after they accept."
@@ -101,6 +101,19 @@ export declare function getEventLeaderboard(eventId: string): Promise<import("..
101
101
  * caller can render a generic "Guest" label instead of a raw hash.
102
102
  */
103
103
  export declare function getEventSpinResults(eventId: string, limit?: number): Promise<import("../types").SpinResultEntry[]>;
104
+ /**
105
+ * The CALLER'S OWN winning spins for an event, newest first.
106
+ *
107
+ * The companion to `getEventSpinResults`, which is a public masked feed of
108
+ * everyone's recent spins. That feed cannot answer "what did I win", and a
109
+ * checklist case asserting it did was the thing that surfaced the gap: read as
110
+ * a buyer who had never spun, the page listed three other people's prizes and
111
+ * showed no empty state, because from its point of view nothing was empty.
112
+ *
113
+ * Unmasked on purpose — these are the caller's own results, and `userId` is
114
+ * supplied by the server from a verified session, never by the client.
115
+ */
116
+ export declare function getUserSpinResultsForEvent(eventId: string, userId: string): Promise<import("../types").SpinResultEntry[]>;
104
117
  /**
105
118
  * Per-option vote tally for a poll event — replaces the voter-ranked
106
119
  * leaderboard for event.type === "poll", where ranking "who happened to
@@ -16,6 +16,7 @@ const ERR_EVENT_NOT_FOUND = "Event not found";
16
16
  import { coerceMediaField, getMediaUrl } from "../../../utils";
17
17
  import { finalizeStagedMediaField, finalizeStagedMediaObject, finalizeStagedMediaObjectArray, } from "../../media/finalize";
18
18
  import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility";
19
+ import { maskName } from "../../../security/pii-mask";
19
20
  // --- Admin: Create Event --------------------------------------------------
20
21
  export async function createEvent(adminId, input) {
21
22
  const finalizedCoverUrl = await finalizeStagedMediaField(getMediaUrl(input.coverImage) ??
@@ -194,13 +195,56 @@ export async function getEventSpinResults(eventId, limit = 10) {
194
195
  const entries = await eventEntryRepository.getRecentSpinResults(event.id, limit);
195
196
  return entries.map((entry) => ({
196
197
  id: entry.id,
197
- userDisplayName: entry.userId ? entry.userDisplayName : undefined,
198
+ /*
199
+ * 🛑 MASKED. This is a public feed on a public event page — the route is
200
+ * not viewer-scoped and never has been — so it was publishing real winners'
201
+ * full display names beside the prize each one won, to anybody with the
202
+ * event's URL.
203
+ *
204
+ * Root Cause #50 is the precedent and it is the same shape: a real bidder's
205
+ * display name was going out on public Bid History because `maskPublicBid`
206
+ * spread its input instead of calling `maskName`. Nothing rendered the field
207
+ * conspicuously there either, which is why it sat unnoticed.
208
+ *
209
+ * `maskName` returns "Anonymous" for a missing name, so the guest case still
210
+ * reads sensibly, and `isGuest` below is what the renderer actually branches
211
+ * on for its own label. A caller's OWN spins are served unmasked by
212
+ * `getUserSpinResultsForEvent` — masking is for other people's.
213
+ */
214
+ userDisplayName: entry.userId ? maskName(entry.userDisplayName) : undefined,
198
215
  isGuest: !entry.userId,
199
216
  spinPrizeId: entry.spinPrizeId,
200
217
  spinPrizeTitle: entry.spinPrizeId ? prizeTitleById.get(entry.spinPrizeId) : undefined,
201
218
  spinWonAt: entry.spinWonAt ? new Date(entry.spinWonAt).toISOString() : undefined,
202
219
  }));
203
220
  }
221
+ /**
222
+ * The CALLER'S OWN winning spins for an event, newest first.
223
+ *
224
+ * The companion to `getEventSpinResults`, which is a public masked feed of
225
+ * everyone's recent spins. That feed cannot answer "what did I win", and a
226
+ * checklist case asserting it did was the thing that surfaced the gap: read as
227
+ * a buyer who had never spun, the page listed three other people's prizes and
228
+ * showed no empty state, because from its point of view nothing was empty.
229
+ *
230
+ * Unmasked on purpose — these are the caller's own results, and `userId` is
231
+ * supplied by the server from a verified session, never by the client.
232
+ */
233
+ export async function getUserSpinResultsForEvent(eventId, userId) {
234
+ const event = await eventRepository.findByIdOrSlug(eventId);
235
+ if (!event || event.type !== "spin_wheel")
236
+ return [];
237
+ const prizeTitleById = new Map((event.spinPrizes ?? []).map((p) => [p.id, p.label]));
238
+ const entries = await eventEntryRepository.getUserSpinResults(event.id, userId);
239
+ return entries.map((entry) => ({
240
+ id: entry.id,
241
+ userDisplayName: entry.userDisplayName,
242
+ isGuest: false,
243
+ spinPrizeId: entry.spinPrizeId,
244
+ spinPrizeTitle: entry.spinPrizeId ? prizeTitleById.get(entry.spinPrizeId) : undefined,
245
+ spinWonAt: entry.spinWonAt ? new Date(entry.spinWonAt).toISOString() : undefined,
246
+ }));
247
+ }
204
248
  /**
205
249
  * Per-option vote tally for a poll event — replaces the voter-ranked
206
250
  * leaderboard for event.type === "poll", where ranking "who happened to
@@ -324,6 +368,36 @@ export async function enterEvent(eventId, input, user, request) {
324
368
  throw new ValidationError(ERROR_MESSAGES.EVENT.ALREADY_ENTERED);
325
369
  }
326
370
  }
371
+ /*
372
+ * 🛑 A signed-in user could vote in a poll as many times as they liked, and
373
+ * every vote counted.
374
+ *
375
+ * Three paths reach this point and only two of them guarded. A GUEST is
376
+ * deduped by hashed IP just above; a SURVEY enforces `maxEntriesPerUser`
377
+ * immediately above that; a poll had neither, so the one case that is trivial
378
+ * to abuse — an authenticated voter clicking Cast Vote again — was the
379
+ * unguarded one. Measured on /events/event-favourite-blader-poll: voted for
380
+ * one option, reloaded, voted for a DIFFERENT option, and the second was
381
+ * accepted with the same "Vote recorded!" confirmation. The event's
382
+ * Participants counter went 362 -> 364 on those two votes from one account, so
383
+ * the tally itself was corrupted, not merely the UI.
384
+ *
385
+ * A poll is one vote per person by definition — there is no
386
+ * `maxEntriesPerUser` to consult and no change-your-vote affordance anywhere
387
+ * in the UI — so the rule is simply "not twice". Uses the same
388
+ * `countUserEntries` the survey path already relies on rather than a new
389
+ * query, so the read cost and the index it needs are both already proven.
390
+ *
391
+ * ValidationError, so the caller gets `ALREADY_ENTERED` and a real message:
392
+ * the previous behaviour silently produced a duplicate, which is the failure
393
+ * mode this refusal exists to replace.
394
+ */
395
+ if (user && event.type === "poll") {
396
+ const priorVotes = await eventEntryRepository.countUserEntries(resolvedEventId, user.uid);
397
+ if (priorVotes >= 1) {
398
+ throw new ValidationError(ERROR_MESSAGES.EVENT.ALREADY_ENTERED);
399
+ }
400
+ }
327
401
  if (event.type === "poll" && event.pollConfig) {
328
402
  const validOptionIds = event.pollConfig.options.map((o) => o.id);
329
403
  const votes = input.pollVotes ?? [];
@@ -22,6 +22,23 @@ declare class EventEntryRepository extends BaseRepository<EventEntryDocument> {
22
22
  * (every write path sets `spinWonAt` and `spinPrizeId` together).
23
23
  */
24
24
  getRecentSpinResults(eventId: string, limit?: number): Promise<EventEntryDocument[]>;
25
+ /**
26
+ * One caller's own winning spins for an event, newest first.
27
+ *
28
+ * 🛑 Deliberately the SAME query shape as `countUserEntries` — two equalities
29
+ * on `(eventId, userId)` and no `orderBy` — so it needs no composite index
30
+ * that does not already exist. `getRecentSpinResults` orders by `spinWonAt`,
31
+ * which is fine with its single equality but would demand a new
32
+ * `(eventId, userId, spinWonAt)` index here, and an index that has not been
33
+ * deployed fails with FAILED_PRECONDITION at runtime while looking perfectly
34
+ * correct in source (Root Cause #2).
35
+ *
36
+ * Sorting in memory is safe precisely because this set is tiny and bounded by
37
+ * the event's own `spinMaxPerUser` — it is "my spins", not a feed. Rows with
38
+ * no `spinWonAt` are dropped rather than sorted to an arbitrary end: an entry
39
+ * that never resolved a prize is not a spin result.
40
+ */
41
+ getUserSpinResults(eventId: string, userId: string): Promise<EventEntryDocument[]>;
25
42
  countUserEntries(eventId: string, userId: string): Promise<number>;
26
43
  getLeaderboard(eventId: string, limit?: number): Promise<import("../types").LeaderboardEntry[]>;
27
44
  /**
@@ -92,6 +92,42 @@ class EventEntryRepository extends BaseRepository {
92
92
  throw new DatabaseError(`Failed to get recent spin results for event ${eventId}`, error);
93
93
  }
94
94
  }
95
+ /**
96
+ * One caller's own winning spins for an event, newest first.
97
+ *
98
+ * 🛑 Deliberately the SAME query shape as `countUserEntries` — two equalities
99
+ * on `(eventId, userId)` and no `orderBy` — so it needs no composite index
100
+ * that does not already exist. `getRecentSpinResults` orders by `spinWonAt`,
101
+ * which is fine with its single equality but would demand a new
102
+ * `(eventId, userId, spinWonAt)` index here, and an index that has not been
103
+ * deployed fails with FAILED_PRECONDITION at runtime while looking perfectly
104
+ * correct in source (Root Cause #2).
105
+ *
106
+ * Sorting in memory is safe precisely because this set is tiny and bounded by
107
+ * the event's own `spinMaxPerUser` — it is "my spins", not a feed. Rows with
108
+ * no `spinWonAt` are dropped rather than sorted to an arbitrary end: an entry
109
+ * that never resolved a prize is not a spin result.
110
+ */
111
+ async getUserSpinResults(eventId, userId) {
112
+ try {
113
+ const snapshot = await this.getCollection()
114
+ .where(EVENT_ENTRY_FIELDS.EVENT_ID, "==", eventId)
115
+ .where(EVENT_ENTRY_FIELDS.USER_ID, "==", userId)
116
+ .get();
117
+ return snapshot.docs
118
+ .map((doc) => this.mapDoc(doc))
119
+ .filter((entry) => Boolean(entry.spinWonAt))
120
+ .sort((a, b) => {
121
+ const at = new Date(a.spinWonAt).getTime();
122
+ const bt = new Date(b.spinWonAt).getTime();
123
+ return (Number.isFinite(bt) ? bt : 0) - (Number.isFinite(at) ? at : 0);
124
+ });
125
+ }
126
+ catch (error) {
127
+ void normalizeError(error);
128
+ throw new DatabaseError(`Failed to load spin results for user ${userId} on event ${eventId}`, error);
129
+ }
130
+ }
95
131
  async countUserEntries(eventId, userId) {
96
132
  try {
97
133
  const snapshot = await this.getCollection()
@@ -45,5 +45,5 @@ export function BrandsSection({ title = "Top Brands", subtitle, limit = 12, view
45
45
  });
46
46
  if (!isLoading && allBrands.length === 0)
47
47
  return null;
48
- return (_jsx(Section, { className: `${className}`, surface: "subtle", paddingY: "y-3xl", paddingX: "x-md", children: _jsxs(Div, { className: "mx-auto max-w-7xl", children: [_jsxs(Row, { className: "mb-6", align: "center", justify: "between", reverse: hand === "left", children: [_jsxs(_Fragment, { children: [_jsx(Heading, { level: 2, className: themed.textPrimary, size: "2xl", mdSize: "3xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { variant: "secondary", className: "mt-1", size: "sm", children: subtitle }))] }), !cta && viewMoreHref && (_jsx(Link, { href: viewMoreHref, className: "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-primary)] hover:underline", children: viewMoreLabel }))] }), showFeaturedChip && !isLoading && (_jsxs(Row, { wrap: true, gap: "sm", className: "mb-4", children: [_jsx(BrandFilterChip, { label: "All", active: activeFilter === "all", onClick: () => setActiveFilter("all") }), _jsx(BrandFilterChip, { label: "Featured", active: activeFilter === "featured", onClick: () => setActiveFilter(activeFilter === "featured" ? "all" : "featured") })] })), isLoading ? (_jsx(Div, { layout: "flex", gap: "3", className: `${__O.hidden}`, children: Array.from({ length: 8 }).map((_, i) => (_jsx(Div, { className: "h-32 w-36 flex-none animate-pulse md:h-40", surface: "subtle", rounded: "xl" }, i))) })) : (_jsx(HorizontalScroller, { items: brands, renderItem: (brand) => _jsx(BrandLogo, { brand: brand }), keyExtractor: (brand) => brand.id, gap: 12, showArrows: true, showScrollbar: false, autoScroll: autoScroll, autoScrollInterval: scrollInterval, pauseOnHover: true, loop: loop })), cta && !isLoading && (_jsx(Div, { className: "mt-6 text-left", children: _jsx(Link, { href: cta.href, className: CTA_CLASSES[cta.variant], children: cta.label }) }))] }) }));
48
+ return (_jsx(Section, { className: `${className}`, surface: "subtle", paddingY: "y-3xl", paddingX: "x-md", children: _jsxs(Div, { className: "mx-auto max-w-7xl", children: [_jsxs(Row, { className: "mb-6", align: "center", justify: "between", reverse: hand === "left", children: [_jsxs(_Fragment, { children: [_jsx(Heading, { level: 2, className: themed.textPrimary, size: "2xl", mdSize: "3xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { variant: "secondary", className: "mt-1", size: "sm", children: subtitle }))] }), !cta && viewMoreHref && (_jsx(Link, { href: viewMoreHref, className: "inline-flex items-center gap-[0.375rem] rounded-[var(--appkit-radius-md)] px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-sm)] font-semibold transition-colors bg-primary text-white hover:bg-primary-600", children: viewMoreLabel }))] }), showFeaturedChip && !isLoading && (_jsxs(Row, { wrap: true, gap: "sm", className: "mb-4", children: [_jsx(BrandFilterChip, { label: "All", active: activeFilter === "all", onClick: () => setActiveFilter("all") }), _jsx(BrandFilterChip, { label: "Featured", active: activeFilter === "featured", onClick: () => setActiveFilter(activeFilter === "featured" ? "all" : "featured") })] })), isLoading ? (_jsx(Div, { layout: "flex", gap: "3", className: `${__O.hidden}`, children: Array.from({ length: 8 }).map((_, i) => (_jsx(Div, { className: "h-32 w-36 flex-none animate-pulse md:h-40", surface: "subtle", rounded: "xl" }, i))) })) : (_jsx(HorizontalScroller, { items: brands, renderItem: (brand) => _jsx(BrandLogo, { brand: brand }), keyExtractor: (brand) => brand.id, gap: 12, showArrows: true, showScrollbar: false, autoScroll: autoScroll, autoScrollInterval: scrollInterval, pauseOnHover: true, loop: loop })), cta && !isLoading && (_jsx(Div, { className: "mt-6 text-left", children: _jsx(Link, { href: cta.href, className: CTA_CLASSES[cta.variant], children: cta.label }) }))] }) }));
49
49
  }
@@ -17,5 +17,5 @@ export function CustomerReviewsSection({ title, subtitle, items, renderItem, vie
17
17
  }
18
18
  if (items.length === 0)
19
19
  return null;
20
- return (_jsx(Section, { className: `${className}`, surface: "muted", padding: "y-3xl", children: _jsxs(Div, { className: "w-full max-w-7xl mx-auto md:px-[2rem]", padding: "x-md", children: [_jsxs(Div, { className: "text-left mb-10", children: [_jsx(Heading, { level: 2, className: `${themed.textPrimary} mb-3`, size: "3xl", mdSize: "4xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { className: `${themed.textSecondary}`, size: "base", children: subtitle }))] }), _jsx(HorizontalScroller, { items: items, renderItem: (item) => renderItem(item), perView: perView ?? CAROUSEL_PER_VIEW.reviews, gap: 24, autoScroll: autoScroll, autoScrollInterval: scrollInterval, pauseOnHover: true, snapToItems: true, showArrows: true, keyExtractor: keyExtractor, loop: loop }), viewMoreHref && (_jsx(Div, { className: "text-left mt-8", children: _jsx(TextLink, { href: viewMoreHref, className: "text-primary hover:text-primary/80", size: "sm", weight: "medium", children: viewMoreLabel }) }))] }) }));
20
+ return (_jsx(Section, { className: `${className}`, surface: "muted", padding: "y-3xl", children: _jsxs(Div, { className: "w-full max-w-7xl mx-auto md:px-[2rem]", padding: "x-md", children: [_jsxs(Div, { className: "text-left mb-10", children: [_jsx(Heading, { level: 2, className: `${themed.textPrimary} mb-3`, size: "3xl", mdSize: "4xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { className: `${themed.textSecondary}`, size: "base", children: subtitle }))] }), _jsx(HorizontalScroller, { items: items, renderItem: (item) => renderItem(item), perView: perView ?? CAROUSEL_PER_VIEW.reviews, gap: 24, autoScroll: autoScroll, autoScrollInterval: scrollInterval, pauseOnHover: true, snapToItems: true, showArrows: true, keyExtractor: keyExtractor, loop: loop }), viewMoreHref && (_jsx(Div, { className: "text-left mt-8", children: _jsx(TextLink, { href: viewMoreHref, rounded: "md", align: "center", gap: "xs", className: "inline-flex px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] transition-colors bg-primary text-white hover:bg-primary-600", size: "sm", weight: "semibold", children: viewMoreLabel }) }))] }) }));
21
21
  }
@@ -30,7 +30,7 @@ export function FeaturedBundlesSection({ title = BUNDLE_COPY.featuredDefaultTitl
30
30
  const items = maxItems && maxItems > 0 ? bundles.slice(0, maxItems) : bundles;
31
31
  if (items.length === 0)
32
32
  return null;
33
- return (_jsx(Section, { className: `${className}`, padding: "y-2xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", wrap: true, reverse: hand === "left", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", color: "primary", children: title }), description && (_jsx(Text, { size: "sm", color: "muted", children: description }))] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLES), className: "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-primary)] hover:underline", children: viewMoreLabel })] }), _jsx(Grid, { cols: "cards", gap: "3", children: items.map((bundle) => (_jsx(FeaturedBundleCard, { bundle: bundle, showSavingsBadge: showSavingsBadge, onBuyNow: onBuyNow }, bundle.id))) })] }) }));
33
+ return (_jsx(Section, { className: `${className}`, padding: "y-2xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", wrap: true, reverse: hand === "left", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", color: "primary", children: title }), description && (_jsx(Text, { size: "sm", color: "muted", children: description }))] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLES), className: "inline-flex items-center gap-[0.375rem] rounded-[var(--appkit-radius-md)] px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-sm)] font-semibold transition-colors bg-primary text-white hover:bg-primary-600", children: viewMoreLabel })] }), _jsx(Grid, { cols: "cards", gap: "3", children: items.map((bundle) => (_jsx(FeaturedBundleCard, { bundle: bundle, showSavingsBadge: showSavingsBadge, onBuyNow: onBuyNow }, bundle.id))) })] }) }));
34
34
  }
35
35
  function FeaturedBundleCard({ bundle, showSavingsBadge = true, onBuyNow, }) {
36
36
  const stock = bundle.bundleStockStatus ?? "in_stock";
@@ -69,5 +69,5 @@ export function ShopByCategorySection({ title = "Shop by Category", subtitle, li
69
69
  if (!cat)
70
70
  return null;
71
71
  return (_jsx(FilterChip, { label: cat.name, active: activeFilter === id, onClick: () => setActiveFilter(activeFilter === id ? "all" : id) }, id));
72
- })] })), isLoading ? (_jsx(Div, { layout: "flex", gap: "3", className: `${__O.hidden} px-[0.25rem]`, children: Array.from({ length: 6 }).map((_, i) => (_jsx(Div, { className: "flex-none h-[104px] w-[108px] animate-pulse", surface: "subtle", rounded: "xl" }, i))) })) : (_jsx(HorizontalScroller, { items: categories, renderItem: (cat) => _jsx(CategoryChip, { category: cat }), perView: CAROUSEL_PER_VIEW.standard, gap: 16, keyExtractor: (cat) => cat.id, autoScroll: autoScroll, autoScrollInterval: scrollInterval, showArrows: true, snapToItems: true, showScrollbar: false, loop: loop, pauseOnHover: true })), cta && !isLoading && (_jsx(Div, { className: "mt-6 text-left", children: _jsx(Link, { href: cta.href, className: CTA_CLASSES[cta.variant], children: cta.label }) })), !cta && viewMoreHref && !isLoading && (_jsx(Div, { className: "mt-6 text-left", children: _jsx(Link, { href: viewMoreHref, className: "inline-flex items-center gap-[var(--appkit-space-1)] text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-primary)] hover:opacity-80", children: viewMoreLabel }) }))] }) }));
72
+ })] })), isLoading ? (_jsx(Div, { layout: "flex", gap: "3", className: `${__O.hidden} px-[0.25rem]`, children: Array.from({ length: 6 }).map((_, i) => (_jsx(Div, { className: "flex-none h-[104px] w-[108px] animate-pulse", surface: "subtle", rounded: "xl" }, i))) })) : (_jsx(HorizontalScroller, { items: categories, renderItem: (cat) => _jsx(CategoryChip, { category: cat }), perView: CAROUSEL_PER_VIEW.standard, gap: 16, keyExtractor: (cat) => cat.id, autoScroll: autoScroll, autoScrollInterval: scrollInterval, showArrows: true, snapToItems: true, showScrollbar: false, loop: loop, pauseOnHover: true })), cta && !isLoading && (_jsx(Div, { className: "mt-6 text-left", children: _jsx(Link, { href: cta.href, className: CTA_CLASSES[cta.variant], children: cta.label }) })), !cta && viewMoreHref && !isLoading && (_jsx(Div, { className: "mt-6 text-left", children: _jsx(Link, { href: viewMoreHref, className: "inline-flex items-center gap-[0.375rem] rounded-[var(--appkit-radius-md)] px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-sm)] font-semibold transition-colors bg-primary text-white hover:bg-primary-600", children: viewMoreLabel }) }))] }) }));
73
73
  }
@@ -101,9 +101,27 @@ kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText
101
101
  const [extInput, setExtInput] = useState("");
102
102
  const [extError, setExtError] = useState("");
103
103
  const hasAlternateSources = showYoutube || showExternal;
104
- // PDF mode: derived from the `accept` prop. Hides camera capture + alternate
105
- // URL tabs (YouTube/External never make sense for invoice/payout-doc fields)
106
- // and trim/thumbnail flow.
104
+ /*
105
+ * PDF mode: derived from the `accept` prop. It forces file-only capture
106
+ * (`effectiveCaptureSource` below) and does nothing else.
107
+ *
108
+ * 🛑 This comment used to claim it also hid "alternate URL tabs (YouTube /
109
+ * External never make sense for invoice/payout-doc fields) and trim/thumbnail
110
+ * flow". Two of those three were never implemented — `pdfMode` has exactly one
111
+ * consumer — so a reader checking whether a document field could be given a
112
+ * YouTube link found a comment saying no and code saying yes. The buyer's
113
+ * payment-proof field was offering "YouTube" and "External URL" as ways to
114
+ * evidence a real payment because of it.
115
+ *
116
+ * The intent in that comment is still right, and closing it properly means
117
+ * deciding one rule for all 11 mounts: `showYoutube`/`showExternal` default to
118
+ * TRUE and NOT ONE mount in the codebase opts out, while `AUTO_KIND_DEFAULTS`
119
+ * accepts `application/pdf`, so gating the tabs on `pdfMode` alone would also
120
+ * strip YouTube from every `kind="auto"` field — including the product video
121
+ * field, where the YouTube source is deliberate (Root Cause #49). Until that
122
+ * rule is chosen, a field that must not accept a URL says so explicitly, as
123
+ * the payment-proof page now does.
124
+ */
107
125
  // SB-UNI-Z5 2026-05-13 — fold `kind` defaults under explicit props.
108
126
  const kindDefaults = kind === "auto"
109
127
  ? AUTO_KIND_DEFAULTS
@@ -1,5 +1,6 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Div, Pagination, Row, Span, Stack, Text } from "../../../ui";
3
+ import { shortOrderRef } from "../utils/order-ref";
3
4
  import { MediaImage } from "../../media/MediaImage";
4
5
  import { formatCurrency } from "../../../utils/number.formatter";
5
6
  import { getDefaultLocale } from "../../../core/baseline-resolver";
@@ -31,7 +32,7 @@ export function OrderCard({ order, onClick, labels = {}, renderActions }) {
31
32
  const revealsRemaining = unrevealedPrizeDraws.length;
32
33
  return (_jsxs(Div, { padding: "5", role: onClick ? "button" : undefined, tabIndex: onClick ? 0 : undefined, onKeyDown: onClick
33
34
  ? (e) => (e.key === "Enter" || e.key === "") && onClick(order)
34
- : undefined, onClick: onClick ? () => onClick(order) : undefined, className: `border border-neutral-200 ${onClick ? "cursor-pointer transition hover:shadow-md" : ""}`, rounded: "xl", surface: "default", children: [_jsxs(Row, { wrap: true, align: "start", justify: "between", gap: "3", children: [_jsxs(Div, { children: [_jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: ["Order #", order.id.slice(-8).toUpperCase()] }), date && (_jsx(Text, { className: "mt-0.5", color: "faint", size: "xs", children: date }))] }), _jsx(Span, { size: "xs", weight: "semibold", className: `capitalize ${statusColor}`, rounded: "full", padding: "pill-md", children: labels[order.orderStatus] ?? order.orderStatus.replace(/_/g, " ") })] }), revealsRemaining > 0 && (_jsx(Row, { gap: "sm", className: "mt-2", children: _jsxs(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: [revealsRemaining, " ", revealsRemaining === 1 ? "reveal" : "reveals", " pending"] }) })), _jsxs(Row, { wrap: true, gap: "3", className: "mt-4", children: [order.items.slice(0, 3).map((item, i) => (_jsxs(Row, { gap: "sm", children: [item.image && (_jsx(Div, { className: "relative h-10 w-10 flex-shrink-0", rounded: "lg", overflow: "hidden", children: _jsx(MediaImage, { src: item.image, alt: item.title, size: "thumbnail" }) })), _jsxs(Div, { children: [_jsx(Text, { truncate: true, size: "sm", weight: "medium", color: "primary", children: item.title }), _jsxs(Text, { size: "xs", color: "faint", children: ["\u00D7", item.quantity] })] })] }, i))), order.items.length > 3 && (_jsxs(Span, { size: "xs", className: "self-center", color: "faint", children: ["+", order.items.length - 3, " more"] }))] }), _jsxs(Row, { justify: "between", className: "mt-4 border-t border-neutral-100", padding: "t-sm", children: [_jsxs(Span, { size: "sm", className: "text-[var(--appkit-color-text-muted)]", children: [order.currency ?? "", " Total"] }), _jsx(Span, { weight: "semibold", className: "text-[var(--appkit-color-text)]", children: formatCurrency(order.total, order.currency) })] }), renderActions && (_jsx(Row, { justify: "end", gap: "sm", className: "mt-3", onClick: (e) => e.stopPropagation(), children: renderActions(order) }))] }));
35
+ : undefined, onClick: onClick ? () => onClick(order) : undefined, className: `border border-neutral-200 ${onClick ? "cursor-pointer transition hover:shadow-md" : ""}`, rounded: "xl", surface: "default", children: [_jsxs(Row, { wrap: true, align: "start", justify: "between", gap: "3", children: [_jsxs(Div, { children: [_jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: ["Order #", shortOrderRef(order.id)] }), date && (_jsx(Text, { className: "mt-0.5", color: "faint", size: "xs", children: date }))] }), _jsx(Span, { size: "xs", weight: "semibold", className: `capitalize ${statusColor}`, rounded: "full", padding: "pill-md", children: labels[order.orderStatus] ?? order.orderStatus.replace(/_/g, " ") })] }), revealsRemaining > 0 && (_jsx(Row, { gap: "sm", className: "mt-2", children: _jsxs(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: [revealsRemaining, " ", revealsRemaining === 1 ? "reveal" : "reveals", " pending"] }) })), _jsxs(Row, { wrap: true, gap: "3", className: "mt-4", children: [order.items.slice(0, 3).map((item, i) => (_jsxs(Row, { gap: "sm", children: [item.image && (_jsx(Div, { className: "relative h-10 w-10 flex-shrink-0", rounded: "lg", overflow: "hidden", children: _jsx(MediaImage, { src: item.image, alt: item.title, size: "thumbnail" }) })), _jsxs(Div, { children: [_jsx(Text, { truncate: true, size: "sm", weight: "medium", color: "primary", children: item.title }), _jsxs(Text, { size: "xs", color: "faint", children: ["\u00D7", item.quantity] })] })] }, i))), order.items.length > 3 && (_jsxs(Span, { size: "xs", className: "self-center", color: "faint", children: ["+", order.items.length - 3, " more"] }))] }), _jsxs(Row, { justify: "between", className: "mt-4 border-t border-neutral-100", padding: "t-sm", children: [_jsxs(Span, { size: "sm", className: "text-[var(--appkit-color-text-muted)]", children: [order.currency ?? "", " Total"] }), _jsx(Span, { weight: "semibold", className: "text-[var(--appkit-color-text)]", children: formatCurrency(order.total, order.currency) })] }), renderActions && (_jsx(Row, { justify: "end", gap: "sm", className: "mt-3", onClick: (e) => e.stopPropagation(), children: renderActions(order) }))] }));
35
36
  }
36
37
  export function OrdersList({ orders, isLoading, onOrderClick, totalPages = 1, currentPage = 1, onPageChange, emptyLabel = "No orders found", renderActions, }) {
37
38
  if (isLoading) {
@@ -4,5 +4,6 @@ export * from "./components";
4
4
  export * from "./schemas";
5
5
  export * from "./columns";
6
6
  export * from "./utils/order-splitter";
7
+ export { shortOrderRef } from "./utils/order-ref";
7
8
  export { groupOrderItemsByLine, type LineOrderGroup, type OrderItemForLineGrouping, } from "./utils/bundle-grouping";
8
9
  export { manifest } from "./manifest";
@@ -4,6 +4,7 @@ export * from "./components";
4
4
  export * from "./schemas";
5
5
  export * from "./columns";
6
6
  export * from "./utils/order-splitter";
7
+ export { shortOrderRef } from "./utils/order-ref";
7
8
  // S-SBUNI-5 2026-05-13 — bundle-aware order item grouping.
8
9
  export { groupOrderItemsByLine, } from "./utils/bundle-grouping";
9
10
  export { manifest } from "./manifest";