@mohasinac/appkit 4.53.1 → 4.54.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/dist/.build-stamp.json +5 -5
  2. package/dist/_internal/client/theme/ThemeProvider.js +15 -1
  3. package/dist/_internal/server/features/auth/capabilities.js +7 -5
  4. package/dist/_internal/server/features/categories/price-guidance-resolve.d.ts +66 -0
  5. package/dist/_internal/server/features/categories/price-guidance-resolve.js +138 -0
  6. package/dist/_internal/server/features/checkout/supplier-identity.js +28 -6
  7. package/dist/_internal/server/features/price-index/market.d.ts +57 -0
  8. package/dist/_internal/server/features/price-index/market.js +120 -0
  9. package/dist/_internal/server/features/products/list-public.d.ts +12 -0
  10. package/dist/_internal/server/features/products/list-public.js +10 -1
  11. package/dist/_internal/server/features/products/listing-tab-counts.d.ts +25 -1
  12. package/dist/_internal/server/features/products/listing-tab-counts.js +184 -10
  13. package/dist/_internal/server/features/search/index-build/sources.js +43 -2
  14. package/dist/_internal/server/features/stores/tabs.d.ts +7 -2
  15. package/dist/_internal/server/features/stores/tabs.js +5 -3
  16. package/dist/_internal/server/jobs/core/jobRunners.js +0 -10
  17. package/dist/_internal/server/jobs/core/onOrderCreate.js +10 -104
  18. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  19. package/dist/_internal/shared/actions/action-registry.js +128 -0
  20. package/dist/_internal/shared/features/categories/bundle-copy.d.ts +1 -1
  21. package/dist/_internal/shared/features/site-settings/supplier-of-record.d.ts +56 -0
  22. package/dist/_internal/shared/features/site-settings/supplier-of-record.js +57 -0
  23. package/dist/_internal/shared/listing-types/tiers.d.ts +94 -0
  24. package/dist/_internal/shared/listing-types/tiers.js +150 -0
  25. package/dist/_internal/shared/search/match.js +28 -2
  26. package/dist/cli/index.js +0 -2
  27. package/dist/client.d.ts +21 -2
  28. package/dist/client.js +23 -4
  29. package/dist/constants/api-endpoints.d.ts +28 -0
  30. package/dist/constants/api-endpoints.js +14 -0
  31. package/dist/features/account/components/UserOrdersView.js +26 -7
  32. package/dist/features/addresses/utils/postal-autofill.d.ts +39 -0
  33. package/dist/features/addresses/utils/postal-autofill.js +54 -0
  34. package/dist/features/admin/components/AdminStoreEditorView.js +73 -34
  35. package/dist/features/admin/components/OwnerQuickActions.d.ts +29 -0
  36. package/dist/features/admin/components/OwnerQuickActions.js +142 -0
  37. package/dist/features/admin/schemas/admin-editor-forms.d.ts +1 -1
  38. package/dist/features/admin/schemas/admin-editor-forms.js +4 -1
  39. package/dist/features/auctions/components/AuctionDetailPageView.js +8 -7
  40. package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +11 -1
  41. package/dist/features/auctions/components/MarketplaceAuctionCard.js +9 -2
  42. package/dist/features/auth/permissions/constants.d.ts +29 -2
  43. package/dist/features/auth/permissions/constants.js +68 -1
  44. package/dist/features/blog/components/BlogFeaturedCard.d.ts +11 -1
  45. package/dist/features/blog/components/BlogFeaturedCard.js +9 -2
  46. package/dist/features/cart/components/CartDrawer.js +3 -2
  47. package/dist/features/cart/components/CartGroupLineRow.js +2 -1
  48. package/dist/features/cart/hooks/useAddToCart.js +10 -5
  49. package/dist/features/cart/utils/pending-ops.d.ts +11 -1
  50. package/dist/features/cart/utils/pending-ops.js +14 -2
  51. package/dist/features/categories/api/[id]/price-guidance/route.js +1 -24
  52. package/dist/features/categories/components/BrandDetailPageView.js +15 -6
  53. package/dist/features/categories/components/BrandDetailTabs.d.ts +7 -1
  54. package/dist/features/categories/components/BrandDetailTabs.js +18 -3
  55. package/dist/features/categories/components/BundleCollage.js +3 -2
  56. package/dist/features/categories/components/BundleItemsPicker.js +3 -4
  57. package/dist/features/categories/components/CategoryDetailPageView.js +19 -8
  58. package/dist/features/categories/components/CategoryDetailTabs.d.ts +7 -1
  59. package/dist/features/categories/components/CategoryDetailTabs.js +29 -7
  60. package/dist/features/categories/components/CategoryGrid.d.ts +11 -1
  61. package/dist/features/categories/components/CategoryGrid.js +9 -2
  62. package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +10 -5
  63. package/dist/features/checkout/actions/checkout-value-otp-actions.js +2 -88
  64. package/dist/features/classified/components/ClassifiedDetailPageView.js +10 -1
  65. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +10 -1
  66. package/dist/features/events/components/EventCard.d.ts +11 -1
  67. package/dist/features/events/components/EventCard.js +9 -2
  68. package/dist/features/layout/AppLayoutShell.js +2 -1
  69. package/dist/features/layout/BottomActions.d.ts +1 -0
  70. package/dist/features/layout/BottomActions.js +330 -0
  71. package/dist/features/layout/BottomChrome.d.ts +45 -0
  72. package/dist/features/layout/BottomChrome.js +5 -17
  73. package/dist/features/layout/SideActionDock.js +36 -13
  74. package/dist/features/layout/index.d.ts +2 -7
  75. package/dist/features/layout/index.js +7 -7
  76. package/dist/features/layout/useIsDesktop.d.ts +36 -0
  77. package/dist/features/layout/useIsDesktop.js +48 -0
  78. package/dist/features/live/components/LiveItemDetailPageView.js +10 -1
  79. package/dist/features/media/messages/en.json +67 -67
  80. package/dist/features/orders/components/YourPurchaseStrip.d.ts +24 -0
  81. package/dist/features/orders/components/YourPurchaseStrip.js +77 -0
  82. package/dist/features/orders/components/index.d.ts +2 -1
  83. package/dist/features/orders/components/index.js +1 -1
  84. package/dist/features/orders/utils/order-announcement-message.d.ts +18 -0
  85. package/dist/features/orders/utils/order-announcement-message.js +16 -0
  86. package/dist/features/orders/utils/payment-proof-message.d.ts +20 -0
  87. package/dist/features/orders/utils/payment-proof-message.js +17 -0
  88. package/dist/features/parts/schemas/firestore.d.ts +227 -0
  89. package/dist/features/parts/schemas/firestore.js +6 -6
  90. package/dist/features/pre-orders/components/MarketplacePreorderCard.d.ts +11 -1
  91. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +10 -3
  92. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +8 -7
  93. package/dist/features/price-index/components/PriceIndexToolView.d.ts +21 -0
  94. package/dist/features/price-index/components/PriceIndexToolView.js +230 -27
  95. package/dist/features/price-index/lookup-target.d.ts +48 -0
  96. package/dist/features/price-index/lookup-target.js +107 -0
  97. package/dist/features/products/api/[id]/route.js +8 -1
  98. package/dist/features/products/api/route.js +16 -4
  99. package/dist/features/products/components/AvailabilityScopeHint.d.ts +22 -0
  100. package/dist/features/products/components/AvailabilityScopeHint.js +27 -0
  101. package/dist/features/products/components/CompareOverlay.d.ts +2 -1
  102. package/dist/features/products/components/CompareOverlay.js +2 -1
  103. package/dist/features/products/components/GroupSettingsPanel.js +2 -1
  104. package/dist/features/products/components/InteractiveProductCard.d.ts +7 -1
  105. package/dist/features/products/components/InteractiveProductCard.js +16 -4
  106. package/dist/features/products/components/MarketplaceBundleCard.d.ts +11 -1
  107. package/dist/features/products/components/MarketplaceBundleCard.js +9 -2
  108. package/dist/features/products/components/MarketplacePrizeDrawCard.d.ts +11 -1
  109. package/dist/features/products/components/MarketplacePrizeDrawCard.js +9 -2
  110. package/dist/features/products/components/ProductDetailPageView.js +34 -4
  111. package/dist/features/products/components/ProductGrid.d.ts +14 -1
  112. package/dist/features/products/components/ProductGrid.js +22 -5
  113. package/dist/features/products/components/ProductsIndexListing.js +2 -2
  114. package/dist/features/products/components/SublistingCarouselSection.js +2 -1
  115. package/dist/features/products/repository/products.repository.d.ts +1 -0
  116. package/dist/features/products/repository/products.repository.js +5 -1
  117. package/dist/features/products/utils/group-member-image.d.ts +9 -2
  118. package/dist/features/products/utils/group-member-image.js +2 -1
  119. package/dist/features/reviews/api/[id]/route.js +83 -23
  120. package/dist/features/reviews/components/PurchaseReviewAffordance.d.ts +6 -0
  121. package/dist/features/reviews/components/PurchaseReviewAffordance.js +42 -0
  122. package/dist/features/reviews/components/ReviewFormDrawer.d.ts +34 -0
  123. package/dist/features/reviews/components/ReviewFormDrawer.js +127 -0
  124. package/dist/features/reviews/components/index.d.ts +4 -0
  125. package/dist/features/reviews/components/index.js +2 -0
  126. package/dist/features/seller/actions/seller-actions.d.ts +1 -1
  127. package/dist/features/seller/actions/seller-actions.js +49 -3
  128. package/dist/features/seller/components/SellerPayoutSettingsView.js +30 -2
  129. package/dist/features/seller/components/SellerProductShell.d.ts +17 -1
  130. package/dist/features/seller/components/SellerProductsView.js +3 -2
  131. package/dist/features/seller/hooks/useStoreListingEntitlements.d.ts +8 -0
  132. package/dist/features/seller/hooks/useStoreListingEntitlements.js +70 -0
  133. package/dist/features/stores/components/InteractiveStoreCard.d.ts +11 -1
  134. package/dist/features/stores/components/InteractiveStoreCard.js +9 -2
  135. package/dist/features/stores/components/StoreAuctionsListing.js +2 -1
  136. package/dist/features/stores/components/index.d.ts +0 -2
  137. package/dist/features/stores/components/index.js +0 -1
  138. package/dist/features/stores/schemas/index.d.ts +1 -1
  139. package/dist/features/stores/schemas/index.js +10 -1
  140. package/dist/features/tester/seed-data/authored/content-discovery__authenticity-checker.js +114 -2
  141. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +415 -0
  142. package/dist/index.d.ts +8 -27
  143. package/dist/index.js +14 -35
  144. package/dist/next/routing/route-map.d.ts +2 -0
  145. package/dist/next/routing/route-map.js +9 -0
  146. package/dist/providers/shipping-shiprocket/index.d.ts +98 -0
  147. package/dist/providers/shipping-shiprocket/index.js +353 -0
  148. package/dist/schemas/registry.d.ts +2 -2
  149. package/dist/security/csp.d.ts +16 -1
  150. package/dist/security/csp.js +5 -3
  151. package/dist/seed/_helpers/category-forest.js +27 -0
  152. package/dist/seed/_helpers/category-parts-glossary-map.js +1816 -86
  153. package/dist/seed/_helpers/category-parts.d.ts +1 -1
  154. package/dist/seed/_helpers/category-parts.js +13412 -3020
  155. package/dist/seed/categories-seed-data.js +48 -6
  156. package/dist/seed/homepage-sections-seed-data.js +41 -28
  157. package/dist/seed/model-composition-seed-data.js +6 -8
  158. package/dist/seed/part-glossary/chunk0.d.ts +9 -0
  159. package/dist/seed/part-glossary/chunk0.js +35044 -0
  160. package/dist/seed/part-glossary/chunk1.d.ts +9 -0
  161. package/dist/seed/part-glossary/chunk1.js +39955 -0
  162. package/dist/seed/part-glossary/chunk2.d.ts +9 -0
  163. package/dist/seed/part-glossary/chunk2.js +50335 -0
  164. package/dist/seed/part-glossary/chunk3.d.ts +9 -0
  165. package/dist/seed/part-glossary/chunk3.js +52386 -0
  166. package/dist/seed/part-glossary/chunk4.d.ts +9 -0
  167. package/dist/seed/part-glossary/chunk4.js +50549 -0
  168. package/dist/seed/part-glossary-seed-data.d.ts +1 -1
  169. package/dist/seed/part-glossary-seed-data.js +23 -47483
  170. package/dist/seed/products-beyblade-x-seed-data.js +36 -1
  171. package/dist/server.d.ts +6 -8
  172. package/dist/server.js +16 -20
  173. package/dist/styles.css +516 -358
  174. package/dist/tailwind-utilities.css +1 -1
  175. package/dist/tokens/themes/index.d.ts +3 -1
  176. package/dist/tokens/themes/index.js +11 -1
  177. package/dist/tokens/themes/technical-dark.d.ts +30 -0
  178. package/dist/tokens/themes/technical-dark.js +103 -0
  179. package/dist/tokens/themes/technical-light.d.ts +22 -0
  180. package/dist/tokens/themes/technical-light.js +89 -0
  181. package/dist/tokens/tokens.css +155 -0
  182. package/dist/ui/components/GatedPrice.js +30 -3
  183. package/dist/ui/components/GatedPrice.style.css +22 -19
  184. package/dist/ui/components/QuickCreateModal.style.css +78 -78
  185. package/dist/ui/forms/FormShell.style.css +260 -260
  186. package/package.json +1 -1
  187. package/scripts/seed-cli.mjs +133 -27
  188. package/dist/_internal/server/jobs/core/whatsappCatalogImport.d.ts +0 -27
  189. package/dist/_internal/server/jobs/core/whatsappCatalogImport.js +0 -172
  190. package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +0 -35
  191. package/dist/_internal/server/jobs/core/whatsappNotify.js +0 -140
  192. package/dist/features/admin/components/AdminWhatsAppGuideView.d.ts +0 -2
  193. package/dist/features/admin/components/AdminWhatsAppGuideView.js +0 -35
  194. package/dist/features/orders/components/MarketplaceOrderCard.d.ts +0 -36
  195. package/dist/features/orders/components/MarketplaceOrderCard.js +0 -57
  196. package/dist/features/stores/components/StoreWhatsAppGuideView.d.ts +0 -3
  197. package/dist/features/stores/components/StoreWhatsAppGuideView.js +0 -12
  198. package/dist/features/whatsapp-bot/api/send-status/route.d.ts +0 -12
  199. package/dist/features/whatsapp-bot/api/send-status/route.js +0 -38
  200. package/dist/features/whatsapp-bot/api/webhook/route.d.ts +0 -12
  201. package/dist/features/whatsapp-bot/api/webhook/route.js +0 -43
  202. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +0 -6
  203. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +0 -270
  204. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.d.ts +0 -9
  205. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +0 -7
  206. package/dist/features/whatsapp-bot/components/index.d.ts +0 -3
  207. package/dist/features/whatsapp-bot/components/index.js +0 -2
  208. package/dist/features/whatsapp-bot/helpers/payment-proof-message.d.ts +0 -14
  209. package/dist/features/whatsapp-bot/helpers/payment-proof-message.js +0 -17
  210. package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +0 -52
  211. package/dist/features/whatsapp-bot/helpers/whatsapp.js +0 -283
  212. package/dist/features/whatsapp-bot/index.d.ts +0 -3
  213. package/dist/features/whatsapp-bot/index.js +0 -3
  214. package/dist/features/whatsapp-bot/manifest.d.ts +0 -2
  215. package/dist/features/whatsapp-bot/manifest.js +0 -13
  216. package/dist/features/whatsapp-bot/messages/en.json +0 -7
  217. package/dist/features/whatsapp-bot/server.d.ts +0 -7
  218. package/dist/features/whatsapp-bot/server.js +0 -7
  219. package/dist/features/whatsapp-bot/types/index.d.ts +0 -126
  220. package/dist/features/whatsapp-bot/types/index.js +0 -1
@@ -319,7 +319,53 @@ export const ROUTE_PERMISSION_MAP = {
319
319
  "/admin/maintenance/payment-rollbacks": "admin:maintenance:view-payment-rollbacks",
320
320
  "/admin/maintenance/analysis": "admin:maintenance:run-analysis",
321
321
  };
322
- /** Default capabilities granted to every new store on creation. */
322
+ /**
323
+ * Every capability, as a runtime value.
324
+ *
325
+ * 🛑 `Record<StoreCapability, true>` rather than a plain array, for the same
326
+ * reason `ALL_LISTING_TYPES_MAP` is: adding a member to the union without
327
+ * adding it here is then a COMPILE error, not a silent omission. This is what
328
+ * lets `capabilities` be validated as a `z.enum` instead of
329
+ * `z.array(z.string())` — which accepted any string, so a typo'd capability
330
+ * saved successfully and the gate it was meant to open never opened, silently.
331
+ *
332
+ * It also backs the admin editor's grouping, so a new capability cannot be
333
+ * granted in code and remain unreachable from the admin UI.
334
+ */
335
+ const STORE_CAPABILITY_MAP = {
336
+ advanced_listing_types: true,
337
+ host_auctions: true,
338
+ host_preorders: true,
339
+ create_categories: true,
340
+ suggest_brands: true,
341
+ create_coupons: true,
342
+ bulk_listing_import: true,
343
+ extended_return_window: true,
344
+ verified_seller: true,
345
+ featured_placement: true,
346
+ promotional_banner: true,
347
+ priority_support: true,
348
+ multiple_stores: true,
349
+ custom_store_slug: true,
350
+ api_access: true,
351
+ lower_commission_rate: true,
352
+ early_access_features: true,
353
+ advanced_analytics: true,
354
+ whatsapp_catalog_sync: true,
355
+ };
356
+ export const ALL_STORE_CAPABILITIES = Object.keys(STORE_CAPABILITY_MAP);
357
+ /** Narrow an untrusted string to a real capability. */
358
+ export function isStoreCapability(value) {
359
+ return typeof value === "string" && value in STORE_CAPABILITY_MAP;
360
+ }
361
+ /**
362
+ * Default capabilities granted to every new store on creation.
363
+ *
364
+ * 🛑 THE one definition. `AdminStoreEditorView` hardcoded its own copy, which
365
+ * is also the `??` fallback on its load path — so the two could disagree about
366
+ * what a store with no `capabilities` field is entitled to, and the admin
367
+ * editor's copy is the one that gets SAVED BACK. Import this; never restate it.
368
+ */
323
369
  export const DEFAULT_STORE_CAPABILITIES = [
324
370
  "suggest_brands",
325
371
  "create_coupons",
@@ -352,6 +398,27 @@ export function hasAllPermissions(permissions, required) {
352
398
  export function hasCapability(capabilities, required) {
353
399
  return capabilities.includes(required);
354
400
  }
401
+ /**
402
+ * Resolve a store's stored `capabilities` array to its effective set.
403
+ *
404
+ * 🛑 The PURE half of `getStoreCapabilities`, extracted so a caller that
405
+ * already holds the store document does not pay a second Firestore read to ask
406
+ * what it is entitled to (Rule #6). Three WhatsApp routes were reading
407
+ * `store.capabilities ?? []` inline for exactly that reason, which made them a
408
+ * second, divergent reader of the field — they skipped the default merge, so
409
+ * they and `getStoreCapabilities` could disagree about the same store.
410
+ *
411
+ * Note the empty-array case falls back to the defaults, matching
412
+ * `getStoreCapabilities`' long-standing `raw.length > 0` test. That is
413
+ * arguably the wrong call — an admin who deliberately unticks everything gets
414
+ * the defaults handed back — but it is the established behaviour and changing
415
+ * it would silently re-grant capabilities on stores that had them removed.
416
+ */
417
+ export function resolveStoreCapabilities(raw) {
418
+ if (Array.isArray(raw) && raw.length > 0)
419
+ return raw;
420
+ return [...DEFAULT_STORE_CAPABILITIES];
421
+ }
355
422
  // ============================================================================
356
423
  // PERMISSION DOMAINS — display grouping (S-ADMIN-7, 2026-08-19)
357
424
  // ============================================================================
@@ -17,5 +17,15 @@ export interface BlogFeaturedCardProps {
17
17
  showAuthor?: boolean;
18
18
  /** Show the cover image. Defaults to `true`. */
19
19
  showThumbnail?: boolean;
20
+ /**
21
+ * Owner-only pencil / clone / bin, for an admin or whoever owns this record.
22
+ * Pass `<OwnerQuickActions>`; it renders nothing for anyone else.
23
+ *
24
+ * 🛑 Rendered OUTSIDE this card's own `<Link>`/`<TextLink>`, as a sibling.
25
+ * A `<button>` inside an `<a>` is invalid HTML that browsers resolve by
26
+ * closing the anchor early, which breaks the CARD's navigation rather than
27
+ * just the button's — `stopPropagation` stops a click but cannot fix a parse.
28
+ */
29
+ renderOwnerActions?: () => React.ReactNode;
20
30
  }
21
- export declare function BlogFeaturedCard({ post, href, labels, className, selectable, isSelected, onSelect, showReadTime, showAuthor, showThumbnail, }: BlogFeaturedCardProps): React.JSX.Element;
31
+ export declare function BlogFeaturedCard({ post, href, labels, className, selectable, isSelected, onSelect, showReadTime, showAuthor, showThumbnail, renderOwnerActions, }: BlogFeaturedCardProps): React.JSX.Element;
@@ -20,7 +20,7 @@ const CATEGORY_BADGE = {
20
20
  community: "bg-warning-surface text-warning dark:bg-warning-surface dark:text-warning",
21
21
  };
22
22
  const CLS_FEATURED_BADGE = "bg-warning-surface text-warning dark:bg-warning-surface dark:text-warning px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] rounded-full";
23
- export function BlogFeaturedCard({ post, href, labels = {}, className = "", selectable = false, isSelected = false, onSelect, showReadTime = true, showAuthor = true, showThumbnail = true, }) {
23
+ export function BlogFeaturedCard({ post, href, labels = {}, className = "", selectable = false, isSelected = false, onSelect, showReadTime = true, showAuthor = true, showThumbnail = true, renderOwnerActions, }) {
24
24
  const longPress = useLongPress(() => onSelect?.(post.id, !isSelected));
25
25
  const safeTitle = post.title?.trim() || "Untitled post";
26
26
  const coverImageUrl = showThumbnail ? getMediaUrl(post.coverImage) : undefined;
@@ -31,5 +31,12 @@ export function BlogFeaturedCard({ post, href, labels = {}, className = "", sele
31
31
  day: "numeric",
32
32
  })
33
33
  : "";
34
- return (_jsxs(Article, { rounded: "xl", shadow: "hover-md", className: `group relative h-full overflow-hidden border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] ${isSelected ? "ring-2 ring-primary outline outline-2 outline-primary" : ""} ${className}`, onMouseDown: onSelect && !isSelected ? longPress.onMouseDown : undefined, onMouseUp: onSelect && !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: onSelect && !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: onSelect && !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: onSelect && !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: onSelect && !isSelected ? longPress.onTouchCancel : undefined, children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => { e.preventDefault(); onSelect(post.id, !isSelected); }, label: isSelected ? "Deselect post" : "Select post", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsxs(TextLink, { href: href, layout: "flex-col", className: "h-full", children: [showThumbnail && (_jsx(Div, { className: `relative aspect-video ${__O.hidden} flex-shrink-0`, children: coverImageUrl ? (_jsx(MediaImage, { src: coverImageUrl, alt: safeTitle, size: "card", className: "transition-transform duration-300 group-hover:scale-105" })) : (_jsx(Div, { role: "img", "aria-label": safeTitle, surface: "muted", className: "h-full w-full" })) })), _jsxs(Stack, { className: `flex-1 ${__P.p4}`, children: [_jsxs(Row, { gap: "xs", className: "mb-2", wrap: true, children: [_jsx(Span, { size: "xs", weight: "medium", className: `inline-block capitalize ${CATEGORY_BADGE[post.category] ?? "bg-[var(--appkit-color-bg)] text-[var(--appkit-color-text-muted)]"}`, rounded: "full", padding: "pill-xs", children: post.category }), post.isFeatured && (_jsx(Span, { size: "xs", weight: "medium", className: CLS_FEATURED_BADGE, children: labels.featuredBadge ?? "Featured" }))] }), _jsx(Heading, { level: 3, className: `leading-snug mb-1`, color: "primary", truncate: 2, size: "base", weight: "semibold", children: safeTitle }), post.excerpt && (_jsx(Text, { className: `mb-3`, color: "muted", truncate: 2, size: "sm", children: post.excerpt })), _jsxs(Row, { color: "muted", textSize: "xs", className: "mt-auto", gap: "3", wrap: true, children: [showAuthor && post.authorName && (_jsx(Span, { children: safeDisplayName(post.authorName, "Author") })), showReadTime && post.readTimeMinutes != null && (_jsxs(Span, { children: [post.readTimeMinutes, " ", labels.readTime ?? "min read"] })), date && _jsx(Span, { children: date })] })] })] })] }));
34
+ const card = (_jsxs(Article, { rounded: "xl", shadow: "hover-md", className: `group relative h-full overflow-hidden border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] ${isSelected ? "ring-2 ring-primary outline outline-2 outline-primary" : ""} ${className}`, onMouseDown: onSelect && !isSelected ? longPress.onMouseDown : undefined, onMouseUp: onSelect && !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: onSelect && !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: onSelect && !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: onSelect && !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: onSelect && !isSelected ? longPress.onTouchCancel : undefined, children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => { e.preventDefault(); onSelect(post.id, !isSelected); }, label: isSelected ? "Deselect post" : "Select post", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsxs(TextLink, { href: href, layout: "flex-col", className: "h-full", children: [showThumbnail && (_jsx(Div, { className: `relative aspect-video ${__O.hidden} flex-shrink-0`, children: coverImageUrl ? (_jsx(MediaImage, { src: coverImageUrl, alt: safeTitle, size: "card", className: "transition-transform duration-300 group-hover:scale-105" })) : (_jsx(Div, { role: "img", "aria-label": safeTitle, surface: "muted", className: "h-full w-full" })) })), _jsxs(Stack, { className: `flex-1 ${__P.p4}`, children: [_jsxs(Row, { gap: "xs", className: "mb-2", wrap: true, children: [_jsx(Span, { size: "xs", weight: "medium", className: `inline-block capitalize ${CATEGORY_BADGE[post.category] ?? "bg-[var(--appkit-color-bg)] text-[var(--appkit-color-text-muted)]"}`, rounded: "full", padding: "pill-xs", children: post.category }), post.isFeatured && (_jsx(Span, { size: "xs", weight: "medium", className: CLS_FEATURED_BADGE, children: labels.featuredBadge ?? "Featured" }))] }), _jsx(Heading, { level: 3, className: `leading-snug mb-1`, color: "primary", truncate: 2, size: "base", weight: "semibold", children: safeTitle }), post.excerpt && (_jsx(Text, { className: `mb-3`, color: "muted", truncate: 2, size: "sm", children: post.excerpt })), _jsxs(Row, { color: "muted", textSize: "xs", className: "mt-auto", gap: "3", wrap: true, children: [showAuthor && post.authorName && (_jsx(Span, { children: safeDisplayName(post.authorName, "Author") })), showReadTime && post.readTimeMinutes != null && (_jsxs(Span, { children: [post.readTimeMinutes, " ", labels.readTime ?? "min read"] })), date && _jsx(Span, { children: date })] })] })] })] }));
35
+ if (!renderOwnerActions)
36
+ return card;
37
+ /*
38
+ * Sibling of the card, never a child — see the prop's docstring. `z-20`
39
+ * beats this card's own overlays, which all sit at `z-10`.
40
+ */
41
+ return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: renderOwnerActions() })] }));
35
42
  }
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { Aside, Button, Div, Heading, Row, Span, Stack, Text, TextLink } from "../../../ui/index.js";
4
4
  import { MediaImage } from "../../media/MediaImage.js";
5
5
  import { QuantityStepper } from "../../../ui/components/QuantityStepper.js";
6
+ import { GatedPrice } from "../../../ui/components/GatedPrice.js";
6
7
  import { formatCurrency } from "../../../utils/number.formatter.js";
7
8
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
8
9
  import { useHandMode } from "../../../_internal/client/hand-mode/index.js";
@@ -19,11 +20,11 @@ export function CartItemRow({ item, onQtyChange, onRemove, href, isOutOfStock =
19
20
  return (_jsxs(Div, { layout: "flex", gap: isRow ? "3" : "4", surface: isRow ? "none" : "card", padding: isRow ? "none" : "sm", className: `min-w-0 transition-opacity ${isOutOfStock ? "opacity-60" : ""}`, children: [_jsx(Div, { surface: "muted", className: `relative ${isRow ? "h-16 w-16" : "h-20 w-20"} flex-shrink-0 ${__O.hidden}`, rounded: "lg", children: item.meta.image && (_jsx(MediaImage, { src: item.meta.image, alt: item.meta.title, size: "thumbnail" })) }), _jsxs(Stack, { justify: "between", className: "flex-1 min-w-0", children: [_jsxs(Row, { gap: "xs", className: "min-w-0", align: "start", children: [href ? (_jsx(TextLink, { href: href, target: "_blank", rel: "noopener noreferrer", weight: "medium", className: "text-[var(--appkit-color-text)] hover:underline underline-offset-2 line-clamp-2", children: item.meta.title })) : (_jsx(Text, { truncate: 2, weight: "medium", color: "primary", children: item.meta.title })), isOutOfStock && (_jsx(Span, { padding: "pill-2xs", color: "error", surface: "danger-surface", weight: "semibold", className: "flex-shrink-0 text-[10px] tracking-wide", rounded: "default", transform: "uppercase", children: "Out of Stock" }))] }), item.meta.attributes &&
20
21
  Object.keys(item.meta.attributes).length > 0 && (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", truncate: 1, children: Object.entries(item.meta.attributes)
21
22
  .map(([k, v]) => `${k}: ${v}`)
22
- .join(", ") })), _jsxs(Row, { justify: "between", className: "min-w-0", gap: "sm", children: [_jsx(Text, { className: "text-[var(--appkit-color-text)]", weight: "semibold", children: formatCurrency(item.meta.price * item.quantity, item.meta.currency) }), onQtyChange && !isOutOfStock && (_jsx(QuantityStepper, { value: item.quantity, onChange: (next) => onQtyChange(item.id, next), min: 1, ariaLabel: `Quantity for ${item.meta.title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel })), isOutOfStock && (_jsxs(Span, { size: "xs", color: "muted", children: ["Qty: ", item.quantity] }))] })] }), onRemove && (_jsx(Button, { onClick: () => onRemove(item.id), variant: "ghost", size: "sm", "aria-label": ACTIONS.CART["remove-item"].ariaLabel, className: CLS_REMOVE_BTN, children: "\u2715" }))] }));
23
+ .join(", ") })), _jsxs(Row, { justify: "between", className: "min-w-0", gap: "sm", children: [_jsx(Text, { className: "text-[var(--appkit-color-text)]", weight: "semibold", children: _jsx(GatedPrice, { children: formatCurrency(item.meta.price * item.quantity, item.meta.currency) }) }), onQtyChange && !isOutOfStock && (_jsx(QuantityStepper, { value: item.quantity, onChange: (next) => onQtyChange(item.id, next), min: 1, ariaLabel: `Quantity for ${item.meta.title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel })), isOutOfStock && (_jsxs(Span, { size: "xs", color: "muted", children: ["Qty: ", item.quantity] }))] })] }), onRemove && (_jsx(Button, { onClick: () => onRemove(item.id), variant: "ghost", size: "sm", "aria-label": ACTIONS.CART["remove-item"].ariaLabel, className: CLS_REMOVE_BTN, children: "\u2715" }))] }));
23
24
  }
24
25
  export function CartDrawer({ isOpen, onClose, items, subtotal = 0, currency, isLoading, onQtyChange, onRemove, onCheckout, labels = {}, }) {
25
26
  const { hand } = useHandMode();
26
27
  if (!isOpen)
27
28
  return null;
28
- return (_jsxs(_Fragment, { children: [_jsx(Div, { surface: "overlay-xs", role: "presentation", className: "fixed inset-0 z-40", onClick: onClose }), _jsx(Aside, { surface: "sidePanel", shadow: "xl", className: `fixed inset-y-0 ${hand === "left" ? "left-0" : "right-0"} z-50 w-full max-w-sm`, children: _jsxs(Stack, { className: "h-full", children: [_jsxs(Row, { justify: "between", border: "bottom", className: __P.p4, reverse: hand === "left", children: [_jsx(Heading, { level: 2, size: "lg", weight: "semibold", children: labels.title ?? "Cart" }), _jsx(Button, { onClick: onClose, variant: "ghost", size: "sm", "aria-label": "Close cart", textColor: "faint", className: "hover:text-[var(--appkit-color-text)] dark:hover:text-[var(--appkit-color-text)]", children: "\u2715" })] }), _jsx(Stack, { className: `flex-1 ${__O.yAuto} ${__P.p4}`, gap: "3", children: isLoading ? (_jsx(Row, { justify: "center", padding: "y-3xl", children: _jsx(Div, { className: "h-8 w-8 animate-spin border-2 border-neutral-300 border-t-neutral-800 dark:border-t-zinc-200", rounded: "full" }) })) : items.length === 0 ? (_jsx(Text, { paddingY: "3xl", className: "text-[var(--appkit-color-text-muted)]", size: "sm", align: "center", children: labels.empty ?? "Your cart is empty" })) : (items.map((item) => (_jsx(CartItemRow, { item: item, onQtyChange: onQtyChange, onRemove: onRemove }, item.id)))) }), items.length > 0 && (_jsxs(Stack, { border: "top", className: __P.p4, gap: "md", children: [_jsxs(Row, { textSize: "sm", justify: "between", children: [_jsx(Span, { className: "text-[var(--appkit-color-text-muted)]", children: labels.subtotal ?? "Subtotal" }), _jsx(Span, { weight: "semibold", children: formatCurrency(subtotal, currency) })] }), onCheckout && (_jsx(Button, { rounded: "xl", onClick: onCheckout, variant: "primary", textSize: "sm", className: "w-full bg-neutral-900 py-[0.75rem] font-[600] text-white transition hover:bg-neutral-800", children: labels.checkout ?? ACTIONS.CART["checkout"].label }))] }))] }) })] }));
29
+ return (_jsxs(_Fragment, { children: [_jsx(Div, { surface: "overlay-xs", role: "presentation", className: "fixed inset-0 z-40", onClick: onClose }), _jsx(Aside, { surface: "sidePanel", shadow: "xl", className: `fixed inset-y-0 ${hand === "left" ? "left-0" : "right-0"} z-50 w-full max-w-sm`, children: _jsxs(Stack, { className: "h-full", children: [_jsxs(Row, { justify: "between", border: "bottom", className: __P.p4, reverse: hand === "left", children: [_jsx(Heading, { level: 2, size: "lg", weight: "semibold", children: labels.title ?? "Cart" }), _jsx(Button, { onClick: onClose, variant: "ghost", size: "sm", "aria-label": "Close cart", textColor: "faint", className: "hover:text-[var(--appkit-color-text)] dark:hover:text-[var(--appkit-color-text)]", children: "\u2715" })] }), _jsx(Stack, { className: `flex-1 ${__O.yAuto} ${__P.p4}`, gap: "3", children: isLoading ? (_jsx(Row, { justify: "center", padding: "y-3xl", children: _jsx(Div, { className: "h-8 w-8 animate-spin border-2 border-neutral-300 border-t-neutral-800 dark:border-t-zinc-200", rounded: "full" }) })) : items.length === 0 ? (_jsx(Text, { paddingY: "3xl", className: "text-[var(--appkit-color-text-muted)]", size: "sm", align: "center", children: labels.empty ?? "Your cart is empty" })) : (items.map((item) => (_jsx(CartItemRow, { item: item, onQtyChange: onQtyChange, onRemove: onRemove }, item.id)))) }), items.length > 0 && (_jsxs(Stack, { border: "top", className: __P.p4, gap: "md", children: [_jsxs(Row, { textSize: "sm", justify: "between", children: [_jsx(Span, { className: "text-[var(--appkit-color-text-muted)]", children: labels.subtotal ?? "Subtotal" }), _jsx(Span, { weight: "semibold", children: _jsx(GatedPrice, { children: formatCurrency(subtotal, currency) }) })] }), onCheckout && (_jsx(Button, { rounded: "xl", onClick: onCheckout, variant: "primary", textSize: "sm", className: "w-full bg-neutral-900 py-[0.75rem] font-[600] text-white transition hover:bg-neutral-800", children: labels.checkout ?? ACTIONS.CART["checkout"].label }))] }))] }) })] }));
29
30
  }
@@ -20,6 +20,7 @@ import { useState } from "react";
20
20
  import { Button, Div, Row, Span, Stack, Text, TextLink } from "../../../ui/index.js";
21
21
  import { QuantityStepper } from "../../../ui/components/QuantityStepper.js";
22
22
  import { MediaImage } from "../../media/MediaImage.js";
23
+ import { GatedPrice, PricesOnly } from "../../../ui/components/GatedPrice.js";
23
24
  import { formatCurrency } from "../../../utils/number.formatter.js";
24
25
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
25
26
  const __O = { hidden: "overflow-hidden" };
@@ -29,5 +30,5 @@ export function CartGroupLineRow({ id, title, image, currency = "INR", lineKind,
29
30
  const onePrice = members.reduce((sum, m) => sum + m.unitPrice * m.quantity, 0);
30
31
  const lineTotal = onePrice * quantity;
31
32
  const unitCount = members.reduce((sum, m) => sum + m.quantity, 0);
32
- return (_jsxs(Div, { layout: "flex", gap: isRow ? "3" : "4", surface: isRow ? "none" : "card", padding: isRow ? "none" : "sm", className: "min-w-0", children: [_jsx(Div, { surface: "muted", className: `relative ${isRow ? "h-16 w-16" : "h-20 w-20"} flex-shrink-0 ${__O.hidden}`, rounded: "lg", children: image && _jsx(MediaImage, { src: image, alt: title, size: "thumbnail" }) }), _jsxs(Stack, { justify: "between", gap: "xs", className: "flex-1 min-w-0", children: [_jsxs(Row, { gap: "xs", align: "start", className: "min-w-0", children: [href ? (_jsx(TextLink, { href: href, weight: "medium", className: "text-[var(--appkit-color-text)] hover:underline underline-offset-2 line-clamp-2", children: title })) : (_jsx(Text, { truncate: 2, weight: "medium", color: "primary", children: title })), _jsx(Span, { padding: "pill-2xs", surface: "subtle", color: "muted", rounded: "default", className: "flex-shrink-0 text-[10px] tracking-wide", transform: "uppercase", children: lineKind === "bundle" ? "Bundle" : "Group" })] }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setIsOpen((v) => !v), "aria-expanded": isOpen, className: "self-start text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-primary)] hover:underline", children: [isOpen ? "▼" : "▶", " ", members.length, " item", members.length === 1 ? "" : "s", unitCount !== members.length ? ` · ${unitCount} units` : ""] }), isOpen && (_jsx(Stack, { gap: "xs", className: "w-full", children: members.map((m) => (_jsxs(Row, { gap: "sm", align: "center", justify: "between", className: "min-w-0", children: [_jsxs(Row, { gap: "sm", align: "center", className: "min-w-0 flex-1", children: [_jsx(Div, { className: `h-10 w-10 flex-shrink-0 ${__O.hidden}`, rounded: "default", surface: "muted", children: m.image && _jsx(MediaImage, { src: m.image, alt: m.title, size: "thumbnail" }) }), _jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsx(Text, { size: "sm", truncate: 1, color: "primary", children: m.title }), _jsxs(Text, { size: "xs", color: "muted", children: [formatCurrency(m.unitPrice, currency), lineKind === "bundle" ? ` × ${m.quantity}` : ""] })] })] }), lineKind === "group" && onMemberQtyChange && !locked && (_jsxs(Row, { gap: "xs", align: "center", className: "flex-shrink-0", children: [_jsx(QuantityStepper, { value: m.quantity, onChange: (next) => onMemberQtyChange(id, m.productId, next), min: 0, max: maxByProductId?.[m.productId], ariaLabel: `Quantity for ${m.title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": ACTIONS.CART["remove-group-member"].ariaLabel, onClick: () => onMemberQtyChange(id, m.productId, 0), className: "text-[var(--appkit-color-text-faint)] transition hover:text-error", children: "\u2715" })] })), lineKind === "bundle" && (_jsxs(Span, { size: "xs", color: "muted", className: "flex-shrink-0", children: ["\u00D7 ", m.quantity] }))] }, m.productId))) })), _jsxs(Row, { justify: "between", gap: "sm", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", className: "text-[var(--appkit-color-text)]", children: formatCurrency(lineTotal, currency) }), lineKind === "bundle" && onQtyChange && !locked && (_jsx(QuantityStepper, { value: quantity, onChange: (next) => onQtyChange(id, next), min: 1, ariaLabel: `Number of copies of ${title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel }))] })] }), onRemove && !locked && (_jsx(Button, { onClick: () => onRemove(id), variant: "ghost", size: "sm", "aria-label": ACTIONS.CART["remove-item"].ariaLabel, className: "self-start text-[var(--appkit-color-text-faint)] transition hover:text-error", children: "\u2715" }))] }));
33
+ return (_jsxs(Div, { layout: "flex", gap: isRow ? "3" : "4", surface: isRow ? "none" : "card", padding: isRow ? "none" : "sm", className: "min-w-0", children: [_jsx(Div, { surface: "muted", className: `relative ${isRow ? "h-16 w-16" : "h-20 w-20"} flex-shrink-0 ${__O.hidden}`, rounded: "lg", children: image && _jsx(MediaImage, { src: image, alt: title, size: "thumbnail" }) }), _jsxs(Stack, { justify: "between", gap: "xs", className: "flex-1 min-w-0", children: [_jsxs(Row, { gap: "xs", align: "start", className: "min-w-0", children: [href ? (_jsx(TextLink, { href: href, weight: "medium", className: "text-[var(--appkit-color-text)] hover:underline underline-offset-2 line-clamp-2", children: title })) : (_jsx(Text, { truncate: 2, weight: "medium", color: "primary", children: title })), _jsx(Span, { padding: "pill-2xs", surface: "subtle", color: "muted", rounded: "default", className: "flex-shrink-0 text-[10px] tracking-wide", transform: "uppercase", children: lineKind === "bundle" ? "Bundle" : "Group" })] }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setIsOpen((v) => !v), "aria-expanded": isOpen, className: "self-start text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-primary)] hover:underline", children: [isOpen ? "▼" : "▶", " ", members.length, " item", members.length === 1 ? "" : "s", unitCount !== members.length ? ` · ${unitCount} units` : ""] }), isOpen && (_jsx(Stack, { gap: "xs", className: "w-full", children: members.map((m) => (_jsxs(Row, { gap: "sm", align: "center", justify: "between", className: "min-w-0", children: [_jsxs(Row, { gap: "sm", align: "center", className: "min-w-0 flex-1", children: [_jsx(Div, { className: `h-10 w-10 flex-shrink-0 ${__O.hidden}`, rounded: "default", surface: "muted", children: m.image && _jsx(MediaImage, { src: m.image, alt: m.title, size: "thumbnail" }) }), _jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsx(Text, { size: "sm", truncate: 1, color: "primary", children: m.title }), _jsxs(Text, { size: "xs", color: "muted", children: [_jsx(PricesOnly, { children: formatCurrency(m.unitPrice, currency) }), lineKind === "bundle" ? ` × ${m.quantity}` : ""] })] })] }), lineKind === "group" && onMemberQtyChange && !locked && (_jsxs(Row, { gap: "xs", align: "center", className: "flex-shrink-0", children: [_jsx(QuantityStepper, { value: m.quantity, onChange: (next) => onMemberQtyChange(id, m.productId, next), min: 0, max: maxByProductId?.[m.productId], ariaLabel: `Quantity for ${m.title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": ACTIONS.CART["remove-group-member"].ariaLabel, onClick: () => onMemberQtyChange(id, m.productId, 0), className: "text-[var(--appkit-color-text-faint)] transition hover:text-error", children: "\u2715" })] })), lineKind === "bundle" && (_jsxs(Span, { size: "xs", color: "muted", className: "flex-shrink-0", children: ["\u00D7 ", m.quantity] }))] }, m.productId))) })), _jsxs(Row, { justify: "between", gap: "sm", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", className: "text-[var(--appkit-color-text)]", children: _jsx(GatedPrice, { children: formatCurrency(lineTotal, currency) }) }), lineKind === "bundle" && onQtyChange && !locked && (_jsx(QuantityStepper, { value: quantity, onChange: (next) => onQtyChange(id, next), min: 1, ariaLabel: `Number of copies of ${title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel }))] })] }), onRemove && !locked && (_jsx(Button, { onClick: () => onRemove(id), variant: "ghost", size: "sm", "aria-label": ACTIONS.CART["remove-item"].ariaLabel, className: "self-start text-[var(--appkit-color-text-faint)] transition hover:text-error", children: "\u2715" }))] }));
33
34
  }
@@ -8,9 +8,10 @@ import { CART_ENDPOINTS } from "../../../constants/api-endpoints.js";
8
8
  import { dispatchCartUpdated, formatCartAddedMessage } from "../utils/pending-ops.js";
9
9
  import { useToast } from "../../../ui/components/Toast.js";
10
10
  import { formatCurrency } from "../../../utils/number.formatter.js";
11
+ import { useCanSeePrices } from "../../../react/hooks/useCanSeePrices.js";
11
12
  import { getDefaultCurrency } from "../../../core/baseline-resolver.js";
12
- function showAddedToast(showToast, detail) {
13
- showToast(formatCartAddedMessage(detail, (amount) => formatCurrency(amount, getDefaultCurrency())), "success");
13
+ function showAddedToast(showToast, detail, canSeePrices) {
14
+ showToast(formatCartAddedMessage(detail, (amount) => formatCurrency(amount, getDefaultCurrency()), canSeePrices), "success");
14
15
  }
15
16
  /**
16
17
  * useAddToCart
@@ -21,6 +22,10 @@ export function useAddToCart(options) {
21
22
  const queryClient = useQueryClient();
22
23
  const endpoint = options?.endpoint ?? CART_ENDPOINTS.GET;
23
24
  const { showToast } = useToast();
25
+ // The guest fallback below fires precisely when the viewer is NOT signed in,
26
+ // so a toast that quoted the cart total was the one place the price gate was
27
+ // guaranteed to be speaking to a guest.
28
+ const { canSeePrices } = useCanSeePrices();
24
29
  const optionsRef = useRef(options);
25
30
  useEffect(() => {
26
31
  optionsRef.current = options;
@@ -31,7 +36,7 @@ export function useAddToCart(options) {
31
36
  queryClient.invalidateQueries({ queryKey: ["cart"] });
32
37
  const itemCount = data?.itemCount ?? 0;
33
38
  const totalValue = data?.subtotal ?? 0;
34
- showAddedToast(showToast, { productTitle: variables.productTitle, itemCount, totalValue });
39
+ showAddedToast(showToast, { productTitle: variables.productTitle, itemCount, totalValue }, canSeePrices);
35
40
  dispatchCartUpdated({ itemCount, totalValue, productTitle: variables.productTitle });
36
41
  optionsRef.current?.onSuccess?.();
37
42
  },
@@ -55,14 +60,14 @@ export function useAddToCart(options) {
55
60
  const guestItems = getGuestCartItems();
56
61
  const itemCount = guestItems.reduce((sum, it) => sum + it.quantity, 0);
57
62
  const totalValue = guestItems.reduce((sum, it) => sum + (it.price ?? 0) * it.quantity, 0);
58
- showAddedToast(showToast, { productTitle: data.productTitle, itemCount, totalValue });
63
+ showAddedToast(showToast, { productTitle: data.productTitle, itemCount, totalValue }, canSeePrices);
59
64
  dispatchCartUpdated({ itemCount, totalValue, productTitle: data.productTitle });
60
65
  optionsRef.current?.onSuccess?.();
61
66
  return undefined;
62
67
  }
63
68
  throw err;
64
69
  }
65
- }, [serverMutation, showToast]);
70
+ }, [serverMutation, showToast, canSeePrices]);
66
71
  return {
67
72
  mutate,
68
73
  isLoading: serverMutation.isPending,
@@ -30,7 +30,17 @@ export declare function dispatchCartUpdated(detail: CartUpdatedEventDetail): voi
30
30
  * instead of a bare "Added to cart", so callers only need to format
31
31
  * currency, not compose the sentence twice.
32
32
  */
33
- export declare function formatCartAddedMessage(detail: CartUpdatedEventDetail, formatCurrency: (amount: number) => string): string;
33
+ export declare function formatCartAddedMessage(detail: CartUpdatedEventDetail, formatCurrency: (amount: number) => string,
34
+ /**
35
+ * 🛑 A toast is a STRING, so it cannot be wrapped in `<GatedPrice>` — this is
36
+ * the third form of the guest-price gate, read as a value at the call site
37
+ * (CLAUDE.md § "Guest price gating"). Default `true` so no existing caller
38
+ * silently loses the total; the signed-out callers pass `false`.
39
+ *
40
+ * The COUNT is deliberately still shown. It is not money, and a guest who
41
+ * just added something is owed confirmation that it landed.
42
+ */
43
+ includeTotal?: boolean): string;
34
44
  export type CartOpKind = "add" | "remove";
35
45
  export type WishlistOpKind = "add" | "remove";
36
46
  export interface CartOp {
@@ -35,9 +35,21 @@ export function dispatchCartUpdated(detail) {
35
35
  * instead of a bare "Added to cart", so callers only need to format
36
36
  * currency, not compose the sentence twice.
37
37
  */
38
- export function formatCartAddedMessage(detail, formatCurrency) {
38
+ export function formatCartAddedMessage(detail, formatCurrency,
39
+ /**
40
+ * 🛑 A toast is a STRING, so it cannot be wrapped in `<GatedPrice>` — this is
41
+ * the third form of the guest-price gate, read as a value at the call site
42
+ * (CLAUDE.md § "Guest price gating"). Default `true` so no existing caller
43
+ * silently loses the total; the signed-out callers pass `false`.
44
+ *
45
+ * The COUNT is deliberately still shown. It is not money, and a guest who
46
+ * just added something is owed confirmation that it landed.
47
+ */
48
+ includeTotal = true) {
39
49
  const itemLabel = detail.productTitle ? `"${detail.productTitle}"` : "Item";
40
- return `${itemLabel} added to cart — ${detail.itemCount} item${detail.itemCount !== 1 ? "s" : ""}, ${formatCurrency(detail.totalValue)} total`;
50
+ const count = `${detail.itemCount} item${detail.itemCount !== 1 ? "s" : ""}`;
51
+ const total = includeTotal ? `, ${formatCurrency(detail.totalValue)} total` : "";
52
+ return `${itemLabel} added to cart — ${count}${total}`;
41
53
  }
42
54
  function dispatchCartOpsChange() {
43
55
  if (typeof window === "undefined")
@@ -43,30 +43,7 @@ import { getProviders } from "../../../../../contracts/index.js";
43
43
  import { categoriesRepository } from "../../../repository/categories.repository.js";
44
44
  import { isAdminUser } from "../../../../auth/role-predicates.js";
45
45
  import { priceGuidanceOnSellerBasis, describePriceGuidanceBasis, } from "../../../../../_internal/shared/features/categories/price-guidance.js";
46
- const MAX_ANCESTORS = 8;
47
- /**
48
- * Nearest-first ancestor walk for `priceGuidance`, same shape as
49
- * `description-template/route.ts`'s `resolveInherited` — the leaf's own band
50
- * wins, and only a leaf with none falls back to the nearest ancestor that has
51
- * one. ONE bounded `findByIds`, not a sequential walk: a seller form hitting
52
- * this on every category change should not pay up to eight round trips for it.
53
- */
54
- async function resolveInheritedGuidance(leaf) {
55
- if (leaf.productDefaults?.priceGuidance)
56
- return leaf.productDefaults.priceGuidance;
57
- const parentIds = (leaf.parentIds ?? []).slice(0, MAX_ANCESTORS);
58
- if (parentIds.length === 0)
59
- return undefined;
60
- const nearestFirst = [...parentIds].reverse();
61
- const ancestors = await categoriesRepository.findByIds(nearestFirst);
62
- const byId = new Map(ancestors.map((a) => [a.id, a]));
63
- for (const pid of nearestFirst) {
64
- const guidance = byId.get(pid)?.productDefaults?.priceGuidance;
65
- if (guidance)
66
- return guidance;
67
- }
68
- return undefined;
69
- }
46
+ import { resolveInheritedGuidance, } from "../../../../../_internal/server/features/categories/price-guidance-resolve.js";
70
47
  export const GET = createRouteHandler({
71
48
  roles: ["seller", "admin", "moderator"],
72
49
  handler: async ({ user, params }) => {
@@ -5,6 +5,7 @@ import Link from "next/link.js";
5
5
  import { categoriesRepository, productRepository, } from "../../../repositories/index.js";
6
6
  import { ROUTES } from "../../../next/routing/route-map.js";
7
7
  import { Anchor, Container, Div, Dl, Dt, Dd, Heading, Main, Nav, Section, Span, Stack, Text } from "../../../ui/index.js";
8
+ import { OwnerQuickActions } from "../../admin/components/OwnerQuickActions.js";
8
9
  import { DynamicBgDiv } from "../../../ui/components/DynamicBgDiv.js";
9
10
  import { MediaImage } from "../../media/MediaImage.js";
10
11
  import { RichTextRenderer } from "../../../ui/rich-text/RichTextRenderer.js";
@@ -60,7 +61,7 @@ export async function BrandDetailPageView({ slug, initialBrand }) {
60
61
  // therefore never hide, however empty the brand was.
61
62
  brandName
62
63
  ? listingTabCounts(CATEGORY_PAGE_TABS, { brandName })
63
- : Promise.resolve({}),
64
+ : Promise.resolve({ available: {}, total: {} }),
64
65
  // SB-UNI-D — bundles are categoryType:"bundle" rows on the categories
65
66
  // collection, tagged to a brand via the real brandSlug field (added
66
67
  // 2026-08-21 — previously a fragile seo.keywords string-match heuristic).
@@ -108,15 +109,23 @@ export async function BrandDetailPageView({ slug, initialBrand }) {
108
109
  // Keyed by tabSlug. `bundles` is overridden with the brand-scoped figure —
109
110
  // `listingTabCounts` cannot narrow bundles by brand (a bundle's brand lives on
110
111
  // `brandSlug`, which this page already filters on locally).
112
+ // Badge = available scope; hiding + the header pill = total. See the same
113
+ // split in `CategoryDetailPageView` and `ListingTabCountResult`.
111
114
  const counts = {
112
- ...tabCounts,
113
- products: tabCounts.products ?? brand?.metrics?.productCount ?? 0,
114
- auctions: tabCounts.auctions ?? brand?.metrics?.auctionCount ?? 0,
115
+ ...tabCounts.available,
116
+ products: tabCounts.available.products ?? brand?.metrics?.productCount ?? 0,
117
+ auctions: tabCounts.available.auctions ?? brand?.metrics?.auctionCount ?? 0,
118
+ bundles: brandBundles.length,
119
+ };
120
+ const totals = {
121
+ ...tabCounts.total,
122
+ products: tabCounts.total.products ?? brand?.metrics?.productCount ?? 0,
123
+ auctions: tabCounts.total.auctions ?? brand?.metrics?.auctionCount ?? 0,
115
124
  bundles: brandBundles.length,
116
125
  };
117
126
  // Header total — `?? 0` is right here (a pill just doesn't render), unlike the
118
127
  // tab bar where an unknown count must keep its tab visible.
119
- const totalItems = Object.values(counts).reduce((sum, n) => sum + (n ?? 0), 0);
128
+ const totalItems = Object.values(totals).reduce((sum, n) => sum + (n ?? 0), 0);
120
129
  /*
121
130
  * ── D4: structured data on the brand page ───────────────────────────────
122
131
  *
@@ -168,5 +177,5 @@ export async function BrandDetailPageView({ slug, initialBrand }) {
168
177
  const ldFaq = brand?.faqs && brand.faqs.length > 0
169
178
  ? faqJsonLd(brand.faqs.map((f) => ({ question: f.question, answer: f.answer })))
170
179
  : null;
171
- return (_jsxs(Main, { children: [_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldBreadcrumb) } }), _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldCollection) } }), ldFaq && (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldFaq) } })), _jsxs(Section, { className: `relative ${__O.hidden} ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-[var(--appkit-color-bg)]"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: coverImage, alt: "", size: "banner" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), !hasCover && (_jsx(DynamicBgDiv, { color: brandColor, className: "absolute inset-0 opacity-10" })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsxs(Nav, { layout: "flex", gap: "2xs", textSize: "sm", className: "mb-4", "aria-label": "Breadcrumb", children: [_jsx(Link, { href: String(ROUTES.HOME), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Home" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.BRANDS), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Brands" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Span, { weight: "medium", className: hasCover ? "text-white" : "text-[var(--appkit-color-text)]", children: brand?.name ?? slug })] }), _jsxs(Row, { className: "mb-3", align: "center", gap: "md", children: [brand?.display?.icon && (_jsx(Span, { className: "leading-none", size: "5xl", children: brand.display.icon })), _jsxs(_Fragment, { children: [_jsx(Heading, { color: "inverse", level: 1, className: `${hasCover ? "" : "text-[var(--appkit-color-text)]"}`, size: "3xl", mdSize: "4xl", weight: "bold", children: brand?.name ?? slug }), brand?.description && typeof brand.description === "string" && !brand.description.startsWith("{") && (_jsx(Text, { color: hasCover ? "inverse" : "muted", className: `max-w-2xl mt-1 ${hasCover ? "/80" : ""}`, size: "base", children: brand.description }))] })] }), _jsxs(Row, { gap: "sm", wrap: true, className: "mt-3", children: [(counts.products ?? 0) > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: "inverse", size: "xs", weight: "medium", className: `${hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "bg-primary/10 text-primary-700 dark:text-primary-400"}`, rounded: "full", padding: "pill-sm-tall", children: [counts.products.toLocaleString(), " ", counts.products === 1 ? "product" : "products"] })), (counts.auctions ?? 0) > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "warning", surface: hasCover ? undefined : "warning-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [counts.auctions.toLocaleString(), " ", counts.auctions === 1 ? "auction" : "auctions"] })), (counts["pre-orders"] ?? 0) > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "info", surface: hasCover ? undefined : "info-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [counts["pre-orders"].toLocaleString(), " ", counts["pre-orders"] === 1 ? "pre-order" : "pre-orders"] })), totalItems === 0 && (_jsx(Span, { size: "sm", className: hasCover ? "text-white/60" : "text-zinc-400", children: "No items listed yet" }))] })] })] }), (brand?.brandWebsite || brand?.brandCountry || brand?.brandFounded) && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", padding: "y-md", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "sm", weight: "semibold", color: "muted", transform: "uppercase", children: "About this brand" }), _jsxs(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden max-w-lg", children: [brand?.brandWebsite && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Website" }), _jsx(Dd, { className: "flex-1", children: _jsx(Anchor, { href: brand.brandWebsite, tone: "brand", underline: "hover", children: brand.brandWebsite }) })] })), brand?.brandCountry && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Country" }), _jsx(Dd, { className: "flex-1", color: "muted", children: brand.brandCountry })] })), brand?.brandFounded && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Founded" }), _jsx(Dd, { className: "flex-1", color: "muted", children: brand.brandFounded })] }))] })] }) }) })), brandContentBody && brandContentBody.trim().length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(RichTextRenderer, { html: brandContentBody }) }) })), _jsx(CategoryHighlightsAndFaqSection, { highlights: brand?.highlights, faqs: brand?.faqs }), _jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: brandName ? (_jsx(BrandDetailTabs, { brandName: brandName, initialProductsData: productsResult ?? undefined, initialBundles: brandBundles, counts: counts, enabledListingTypes: enabledListingTypes(settings), enabledCategoryTypes: enabledCategoryTypes(settings) })) : (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "Brand not found." })) }) }), groupedListings.length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(GroupedListingsCarousel, { groups: groupedListings }) }) })), relatedBrands.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-t", padding: "y-lg", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 2, className: "mb-4", size: "xl", weight: "semibold", children: "Related Brands" }), _jsx(CategoryGrid, { categories: relatedBrands, getHref: (b) => String(ROUTES.PUBLIC.BRAND_DETAIL(b.slug)) })] }) }))] }));
180
+ return (_jsxs(Main, { children: [_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldBreadcrumb) } }), _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldCollection) } }), ldFaq && (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldFaq) } })), _jsxs(Section, { className: `relative ${__O.hidden} ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-[var(--appkit-color-bg)]"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: coverImage, alt: "", size: "banner" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), !hasCover && (_jsx(DynamicBgDiv, { color: brandColor, className: "absolute inset-0 opacity-10" })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsxs(Nav, { layout: "flex", gap: "2xs", textSize: "sm", className: "mb-4", "aria-label": "Breadcrumb", children: [_jsx(Link, { href: String(ROUTES.HOME), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Home" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.BRANDS), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Brands" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Span, { weight: "medium", className: hasCover ? "text-white" : "text-[var(--appkit-color-text)]", children: brand?.name ?? slug })] }), _jsxs(Row, { className: "mb-3", align: "center", gap: "md", children: [brand?.display?.icon && (_jsx(Span, { className: "leading-none", size: "5xl", children: brand.display.icon })), _jsxs(_Fragment, { children: [brand?.id && (_jsx(OwnerQuickActions, { resource: "BRAND", placement: "detail", className: "mb-2", recordLabel: brand.name ?? slug, editHref: String(ROUTES.ADMIN.BRANDS_EDIT(brand.id)) })), _jsx(Heading, { color: "inverse", level: 1, className: `${hasCover ? "" : "text-[var(--appkit-color-text)]"}`, size: "3xl", mdSize: "4xl", weight: "bold", children: brand?.name ?? slug }), brand?.description && typeof brand.description === "string" && !brand.description.startsWith("{") && (_jsx(Text, { color: hasCover ? "inverse" : "muted", className: `max-w-2xl mt-1 ${hasCover ? "/80" : ""}`, size: "base", children: brand.description }))] })] }), _jsxs(Row, { gap: "sm", wrap: true, className: "mt-3", children: [(counts.products ?? 0) > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: "inverse", size: "xs", weight: "medium", className: `${hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "bg-primary/10 text-primary-700 dark:text-primary-400"}`, rounded: "full", padding: "pill-sm-tall", children: [counts.products.toLocaleString(), " ", counts.products === 1 ? "product" : "products"] })), (counts.auctions ?? 0) > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "warning", surface: hasCover ? undefined : "warning-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [counts.auctions.toLocaleString(), " ", counts.auctions === 1 ? "auction" : "auctions"] })), (counts["pre-orders"] ?? 0) > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "info", surface: hasCover ? undefined : "info-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [counts["pre-orders"].toLocaleString(), " ", counts["pre-orders"] === 1 ? "pre-order" : "pre-orders"] })), totalItems === 0 && (_jsx(Span, { size: "sm", className: hasCover ? "text-white/60" : "text-zinc-400", children: "No items listed yet" }))] })] })] }), (brand?.brandWebsite || brand?.brandCountry || brand?.brandFounded) && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", padding: "y-md", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "sm", weight: "semibold", color: "muted", transform: "uppercase", children: "About this brand" }), _jsxs(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden max-w-lg", children: [brand?.brandWebsite && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Website" }), _jsx(Dd, { className: "flex-1", children: _jsx(Anchor, { href: brand.brandWebsite, tone: "brand", underline: "hover", children: brand.brandWebsite }) })] })), brand?.brandCountry && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Country" }), _jsx(Dd, { className: "flex-1", color: "muted", children: brand.brandCountry })] })), brand?.brandFounded && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Founded" }), _jsx(Dd, { className: "flex-1", color: "muted", children: brand.brandFounded })] }))] })] }) }) })), brandContentBody && brandContentBody.trim().length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(RichTextRenderer, { html: brandContentBody }) }) })), _jsx(CategoryHighlightsAndFaqSection, { highlights: brand?.highlights, faqs: brand?.faqs }), _jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: brandName ? (_jsx(BrandDetailTabs, { brandName: brandName, initialProductsData: productsResult ?? undefined, initialBundles: brandBundles, counts: counts, totals: totals, enabledListingTypes: enabledListingTypes(settings), enabledCategoryTypes: enabledCategoryTypes(settings) })) : (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "Brand not found." })) }) }), groupedListings.length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(GroupedListingsCarousel, { groups: groupedListings }) }) })), relatedBrands.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-t", padding: "y-lg", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 2, className: "mb-4", size: "xl", weight: "semibold", children: "Related Brands" }), _jsx(CategoryGrid, { categories: relatedBrands, getHref: (b) => String(ROUTES.PUBLIC.BRAND_DETAIL(b.slug)) })] }) }))] }));
172
181
  }
@@ -10,10 +10,16 @@ export interface BrandDetailTabsProps {
10
10
  * inventory (Root Cause #59). Keyed by tab id; the previous hand-named shape
11
11
  * covered only 5 of 9 tabs (Root Cause #61).
12
12
  */
13
+ /** Badge counts — what the default (available) scope will render. */
13
14
  counts?: Record<string, number | undefined>;
15
+ /**
16
+ * Every published row per tab, all availability scopes. Drives hide-at-zero
17
+ * and the "N ended — show all" hint; see `ListingTabCountResult`.
18
+ */
19
+ totals?: Record<string, number | undefined>;
14
20
  /** Enabled listing types (e.g. ["standard","auction"]). When omitted, all tabs shown. */
15
21
  enabledListingTypes?: string[];
16
22
  /** Enabled category types (e.g. ["category","brand","bundle"]). When omitted, all tabs shown. */
17
23
  enabledCategoryTypes?: string[];
18
24
  }
19
- export declare function BrandDetailTabs({ brandName, initialProductsData, initialBundles, counts, enabledListingTypes, enabledCategoryTypes, }: BrandDetailTabsProps): React.JSX.Element;
25
+ export declare function BrandDetailTabs({ brandName, initialProductsData, initialBundles, counts, totals, enabledListingTypes, enabledCategoryTypes, }: BrandDetailTabsProps): React.JSX.Element;
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
4
  import { EmptyState, Tabs, TabsList, TabsTrigger } from "../../../ui/index.js";
5
+ import { AvailabilityScopeHint } from "../../products/components/AvailabilityScopeHint.js";
5
6
  import { CategoryProductsListing } from "./CategoryProductsListing.js";
6
7
  import { AuctionsIndexListing } from "../../products/components/AuctionsIndexListing.js";
7
8
  import { PreOrdersIndexListing } from "../../pre-orders/components/PreOrdersIndexListing.js";
@@ -28,6 +29,18 @@ const TAB_TYPE_MAP = {
28
29
  ])),
29
30
  bundles: { kind: "category", type: "bundle" },
30
31
  };
32
+ /**
33
+ * The listing types a tab spans, for the availability-scope hint. Derived
34
+ * from the same maps the tabs render from, never hand-written (Root Cause
35
+ * #61).
36
+ */
37
+ function typesForTab(id) {
38
+ const generic = GENERIC_TAB_LISTING_TYPES[id];
39
+ if (generic)
40
+ return generic;
41
+ const mapping = TAB_TYPE_MAP[id];
42
+ return mapping?.kind === "listing" ? [mapping.type] : [];
43
+ }
31
44
  /** Listing types rendered by the shared products listing rather than a dedicated one. */
32
45
  const GENERIC_TAB_LISTING_TYPES = {
33
46
  [pluginFor("standard").tabSlug]: ["standard"],
@@ -46,8 +59,9 @@ const GENERIC_TAB_LISTING_TYPES = {
46
59
  */
47
60
  [pluginFor("spares").tabSlug]: ["spares"],
48
61
  };
49
- export function BrandDetailTabs({ brandName, initialProductsData, initialBundles = [], counts, enabledListingTypes, enabledCategoryTypes, }) {
62
+ export function BrandDetailTabs({ brandName, initialProductsData, initialBundles = [], counts, totals, enabledListingTypes, enabledCategoryTypes, }) {
50
63
  const countFor = (id) => counts?.[id];
64
+ const totalFor = (id) => totals?.[id] ?? counts?.[id];
51
65
  const visibleTabs = CATEGORY_PAGE_TABS.filter((t) => {
52
66
  const mapping = TAB_TYPE_MAP[t.id];
53
67
  // A tab with no mapping has no render branch below and would show a blank
@@ -67,7 +81,8 @@ export function BrandDetailTabs({ brandName, initialProductsData, initialBundles
67
81
  // Hide a tab only when its count is known and explicitly zero. `undefined`
68
82
  // now means the count query FAILED (every tab is counted), so the tab stays
69
83
  // visible rather than hiding real listings behind a swallowed error.
70
- const count = countFor(t.id);
84
+ // TOTAL, not the badge — see the same note in CategoryDetailTabs.
85
+ const count = totalFor(t.id);
71
86
  return count === undefined || count > 0;
72
87
  });
73
88
  const firstTabId = (visibleTabs[0]?.id ?? "products");
@@ -75,5 +90,5 @@ export function BrandDetailTabs({ brandName, initialProductsData, initialBundles
75
90
  if (visibleTabs.length === 0) {
76
91
  return (_jsx(EmptyState, { title: "Nothing listed here yet", description: "This brand has no active listings right now. Check back soon." }));
77
92
  }
78
- return (_jsxs(_Fragment, { children: [_jsx(Tabs, { value: activeTab, onChange: (v) => setActiveTab(v), className: "mb-6", children: _jsx(TabsList, { children: visibleTabs.map((t) => (_jsx(TabsTrigger, { value: t.id, badge: countFor(t.id), children: t.label }, t.id))) }) }), GENERIC_TAB_LISTING_TYPES[activeTab] && (_jsx(CategoryProductsListing, { categorySlug: "", brandName: brandName, listingTypes: GENERIC_TAB_LISTING_TYPES[activeTab], initialData: activeTab === pluginFor("standard").tabSlug ? initialProductsData : undefined })), activeTab === pluginFor("auction").tabSlug && (_jsx(AuctionsIndexListing, { brandName: brandName })), activeTab === pluginFor("pre-order").tabSlug && (_jsx(PreOrdersIndexListing, { brandName: brandName })), activeTab === pluginFor("prize-draw").tabSlug && (_jsx(PrizeDrawsIndexListing, { brandName: brandName })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles, brandName: brandName }))] }));
93
+ return (_jsxs(_Fragment, { children: [_jsx(Tabs, { value: activeTab, onChange: (v) => setActiveTab(v), className: "mb-6", children: _jsx(TabsList, { children: visibleTabs.map((t) => (_jsx(TabsTrigger, { value: t.id, badge: countFor(t.id), children: t.label }, t.id))) }) }), _jsx(AvailabilityScopeHint, { total: totalFor(activeTab), available: countFor(activeTab), types: typesForTab(activeTab) }), GENERIC_TAB_LISTING_TYPES[activeTab] && (_jsx(CategoryProductsListing, { categorySlug: "", brandName: brandName, listingTypes: GENERIC_TAB_LISTING_TYPES[activeTab], initialData: activeTab === pluginFor("standard").tabSlug ? initialProductsData : undefined })), activeTab === pluginFor("auction").tabSlug && (_jsx(AuctionsIndexListing, { brandName: brandName })), activeTab === pluginFor("pre-order").tabSlug && (_jsx(PreOrdersIndexListing, { brandName: brandName })), activeTab === pluginFor("prize-draw").tabSlug && (_jsx(PrizeDrawsIndexListing, { brandName: brandName })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles, brandName: brandName }))] }));
79
94
  }
@@ -16,10 +16,11 @@ import { MediaImage } from "../../media/MediaImage.js";
16
16
  import { ImageLightbox } from "../../../ui/components/ImageLightbox.js";
17
17
  import { ROUTES } from "../../../next/routing/route-map.js";
18
18
  import { formatCurrency } from "../../../utils/number.formatter.js";
19
+ import { productImageUrls } from "../../products/schemas/firestore.js";
19
20
  const PLACEHOLDER_EMOJI = "📦";
20
21
  function toGalleryImages(members, canSeePrices) {
21
22
  return members.map((p, i) => ({
22
- src: p.mainImage ?? p.images?.[0] ?? "",
23
+ src: p.mainImage || productImageUrls(p.images)[0] || "",
23
24
  alt: p.title,
24
25
  badge: `#${i + 1}`,
25
26
  caption: p.title,
@@ -46,7 +47,7 @@ export function BundleCollage({ members, onItemClick }) {
46
47
  return null;
47
48
  const galleryImages = toGalleryImages(members, canSeePrices);
48
49
  return (_jsxs(_Fragment, { children: [_jsx(Grid, { cols: "categoryCards", gap: "3", children: members.map((p, idx) => {
49
- const cover = p.mainImage ?? p.images?.[0];
50
+ const cover = p.mainImage || productImageUrls(p.images)[0];
50
51
  const href = String(ROUTES.PUBLIC.PRODUCT_DETAIL?.(p.slug ?? p.id) ?? "#");
51
52
  return (_jsxs(Div, { layout: "flex-col", className: "group relative overflow-hidden border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] transition-transform hover:scale-[1.01]", rounded: "lg", children: [_jsx(Button, { variant: "ghost", type: "button", onClick: makeBundleItemClickHandler(p, idx, onItemClick, setLightboxIndex), paddingX: "none", paddingY: "none", rounded: "none", className: "relative aspect-square w-full overflow-hidden focus-visible:ring-2 focus-visible:ring-[var(--appkit-color-primary)]", "aria-label": `View ${p.title} in lightbox`, children: _jsxs(Div, { className: "relative aspect-square w-full", children: [cover ? (_jsx(MediaImage, { src: cover, alt: p.title, size: "card", className: "transition-transform group-hover:scale-105" })) : (_jsx(Row, { textSize: "3xl", surface: "muted", className: "absolute inset-0", align: "center", justify: "center", children: PLACEHOLDER_EMOJI })), _jsxs(Div, { textWeight: "semibold", textSize: "xs", surface: "overlay-xl", padding: "chip-2xs", className: "absolute left-2 top-2 text-white", rounded: "default", children: ["#", idx + 1] })] }) }), _jsxs(Div, { padding: "xs", children: [_jsx(Link, { href: href, className: "line-clamp-2 text-[length:var(--appkit-text-sm)] font-medium hover:underline", children: p.title }), _jsx(Text, { className: "mt-0.5 text-[var(--appkit-color-text-muted)]", size: "xs", children: _jsx(GatedPrice, { children: formatCurrency(p.price ?? 0, p.currency ?? "INR") }) })] })] }, p.id));
52
53
  }) }), _jsx(ImageLightbox, { images: galleryImages, activeIndex: lightboxIndex, onClose: () => setLightboxIndex(null), onNavigate: (i) => setLightboxIndex(i), showThumbnails: true })] }));
@@ -18,6 +18,7 @@ import { Badge, Button, Checkbox, Div, Input, Row, Stack, Text } from "../../../
18
18
  import { BUNDLE_MAX_ITEMS, BUNDLE_MIN_ITEMS, } from "../../../_internal/shared/features/categories/bundle-config.js";
19
19
  import { BUNDLE_COPY } from "../../../_internal/shared/features/categories/bundle-copy.js";
20
20
  import { PRODUCT_ENDPOINTS } from "../../../constants/api-endpoints.js";
21
+ import { productImageUrls } from "../../products/schemas/firestore.js";
21
22
  const __P = {
22
23
  p3: "p-[var(--appkit-space-3)]",
23
24
  };
@@ -137,11 +138,9 @@ export async function defaultBundleItemsFetch(query) {
137
138
  return {
138
139
  id,
139
140
  title,
140
- image: typeof rec.mainImage === "string"
141
+ image: typeof rec.mainImage === "string" && rec.mainImage
141
142
  ? rec.mainImage
142
- : Array.isArray(rec.images) && typeof rec.images[0] === "string"
143
- ? rec.images[0]
144
- : undefined,
143
+ : productImageUrls(rec.images)[0],
145
144
  price: typeof rec.price === "number" ? rec.price : undefined,
146
145
  storeId: typeof rec.storeId === "string" ? rec.storeId : undefined,
147
146
  };