@mohasinac/appkit 4.11.2 → 4.11.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 (165) hide show
  1. package/dist/_internal/server/features/checkout/actions.d.ts +4 -0
  2. package/dist/_internal/server/features/checkout/actions.js +111 -4
  3. package/dist/_internal/server/features/checkout/data.d.ts +10 -0
  4. package/dist/_internal/server/features/orders/adapters.js +7 -0
  5. package/dist/_internal/server/features/products/data.d.ts +12 -3
  6. package/dist/_internal/server/features/products/data.js +55 -4
  7. package/dist/_internal/server/features/products/index.d.ts +1 -1
  8. package/dist/_internal/server/features/products/index.js +1 -1
  9. package/dist/_internal/server/features/products/list-public.d.ts +19 -0
  10. package/dist/_internal/server/features/products/list-public.js +39 -6
  11. package/dist/_internal/server/features/reviews/data.d.ts +1 -1
  12. package/dist/_internal/server/features/reviews/data.js +3 -2
  13. package/dist/_internal/server/features/tester/visibility.js +3 -3
  14. package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +1 -1
  15. package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +4 -1
  16. package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +2 -0
  17. package/dist/_internal/server/jobs/handlers/_helpers.js +4 -1
  18. package/dist/_internal/shared/features/promotions/schema.d.ts +0 -16
  19. package/dist/_internal/shared/features/promotions/schema.js +0 -2
  20. package/dist/_internal/shared/features/reviews/config.d.ts +8 -1
  21. package/dist/_internal/shared/features/reviews/config.js +8 -1
  22. package/dist/_internal/shared/listing-types/_registry.d.ts +88 -1
  23. package/dist/_internal/shared/listing-types/_registry.js +63 -1
  24. package/dist/_internal/shared/listing-types/art/config.d.ts +59 -0
  25. package/dist/_internal/shared/listing-types/art/config.js +12 -0
  26. package/dist/_internal/shared/listing-types/auction/config.d.ts +53 -0
  27. package/dist/_internal/shared/listing-types/auction/config.js +17 -0
  28. package/dist/_internal/shared/listing-types/classified/config.d.ts +59 -0
  29. package/dist/_internal/shared/listing-types/classified/config.js +14 -0
  30. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +59 -0
  31. package/dist/_internal/shared/listing-types/digital-code/config.js +12 -0
  32. package/dist/_internal/shared/listing-types/live/config.d.ts +59 -0
  33. package/dist/_internal/shared/listing-types/live/config.js +14 -0
  34. package/dist/_internal/shared/listing-types/pre-order/config.d.ts +110 -0
  35. package/dist/_internal/shared/listing-types/pre-order/config.js +14 -0
  36. package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +44 -0
  37. package/dist/_internal/shared/listing-types/prize-draw/config.js +10 -0
  38. package/dist/_internal/shared/listing-types/standard/config.d.ts +59 -0
  39. package/dist/_internal/shared/listing-types/standard/config.js +9 -0
  40. package/dist/_internal/shared/listing-types/stickers/config.d.ts +59 -0
  41. package/dist/_internal/shared/listing-types/stickers/config.js +12 -0
  42. package/dist/client.d.ts +5 -1
  43. package/dist/client.js +4 -1
  44. package/dist/constants/field-names.d.ts +12 -0
  45. package/dist/constants/field-names.js +12 -0
  46. package/dist/constants/table-keys.d.ts +2 -0
  47. package/dist/constants/table-keys.js +2 -0
  48. package/dist/features/account/components/UserOrdersView.js +27 -5
  49. package/dist/features/admin/components/AdminAddressesView.js +3 -1
  50. package/dist/features/admin/components/AdminCouponEditorView.js +1 -4
  51. package/dist/features/admin/components/AdminPaymentMethodsView.js +3 -1
  52. package/dist/features/admin/components/AdminProductsView.js +34 -18
  53. package/dist/features/admin/constants/filter-tabs.d.ts +26 -9
  54. package/dist/features/admin/constants/filter-tabs.js +22 -9
  55. package/dist/features/auctions/components/AuctionBidsTable.js +1 -1
  56. package/dist/features/auctions/components/AuctionBottomActions.js +1 -0
  57. package/dist/features/auctions/components/AuctionDetailPageView.js +13 -11
  58. package/dist/features/cart/schemas/firestore.d.ts +0 -2
  59. package/dist/features/categories/components/BrandDetailTabs.js +31 -11
  60. package/dist/features/categories/components/CategoryDetailTabs.js +27 -7
  61. package/dist/features/categories/components/CategoryProductsListing.d.ts +9 -1
  62. package/dist/features/categories/components/CategoryProductsListing.js +5 -2
  63. package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -1
  64. package/dist/features/contact/email.js +5 -1
  65. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +17 -16
  66. package/dist/features/homepage/components/FeaturedResultsSection.js +1 -1
  67. package/dist/features/homepage/components/SectionCarousel.js +1 -1
  68. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  69. package/dist/features/layout/AppLayoutShell.js +9 -2
  70. package/dist/features/layout/BottomActions.js +26 -3
  71. package/dist/features/layout/BottomActionsContext.d.ts +14 -0
  72. package/dist/features/layout/BottomActionsContext.js +13 -1
  73. package/dist/features/layout/hooks/useBottomActions.d.ts +6 -1
  74. package/dist/features/layout/hooks/useBottomActions.js +4 -1
  75. package/dist/features/layout/index.d.ts +1 -1
  76. package/dist/features/live/components/LiveItemDetailPageView.js +7 -6
  77. package/dist/features/orders/repository/orders.repository.d.ts +4 -0
  78. package/dist/features/orders/repository/orders.repository.js +4 -0
  79. package/dist/features/orders/types/index.d.ts +16 -0
  80. package/dist/features/pre-orders/components/PreOrderBottomActions.d.ts +1 -1
  81. package/dist/features/pre-orders/components/PreOrderBottomActions.js +3 -19
  82. package/dist/features/products/components/AuctionsIndexListing.js +1 -2
  83. package/dist/features/products/components/ListingBottomActions.d.ts +28 -0
  84. package/dist/features/products/components/ListingBottomActions.js +25 -0
  85. package/dist/features/products/components/PrizeDrawBottomActions.d.ts +1 -1
  86. package/dist/features/products/components/PrizeDrawBottomActions.js +3 -21
  87. package/dist/features/products/components/ProductDetailActions.js +4 -0
  88. package/dist/features/products/components/ProductDetailPageView.js +7 -7
  89. package/dist/features/products/constants/listing-tabs.d.ts +43 -198
  90. package/dist/features/products/constants/listing-tabs.js +71 -58
  91. package/dist/features/products/constants/sieve.d.ts +104 -1
  92. package/dist/features/products/constants/sieve.js +27 -7
  93. package/dist/features/products/repository/products.repository.d.ts +45 -1
  94. package/dist/features/products/repository/products.repository.js +26 -3
  95. package/dist/features/products/utils/listing-badge-variant.d.ts +20 -1
  96. package/dist/features/products/utils/listing-badge-variant.js +26 -1
  97. package/dist/features/products/utils/listing-type.d.ts +24 -0
  98. package/dist/features/products/utils/listing-type.js +48 -0
  99. package/dist/features/promotions/actions/coupon-actions.d.ts +7 -0
  100. package/dist/features/promotions/actions/coupon-actions.js +12 -1
  101. package/dist/features/promotions/actions/coupon-stacking.d.ts +24 -0
  102. package/dist/features/promotions/actions/coupon-stacking.js +21 -0
  103. package/dist/features/promotions/actions/index.d.ts +1 -0
  104. package/dist/features/promotions/actions/index.js +1 -0
  105. package/dist/features/promotions/actions/seller-coupon-actions.d.ts +0 -1
  106. package/dist/features/promotions/actions/seller-coupon-actions.js +0 -1
  107. package/dist/features/promotions/components/CouponHelpDetails.d.ts +20 -0
  108. package/dist/features/promotions/components/CouponHelpDetails.js +17 -0
  109. package/dist/features/promotions/components/CouponsIndexListing.js +2 -1
  110. package/dist/features/promotions/components/index.d.ts +2 -0
  111. package/dist/features/promotions/components/index.js +1 -0
  112. package/dist/features/promotions/constants/coupon-help.d.ts +27 -0
  113. package/dist/features/promotions/constants/coupon-help.js +42 -0
  114. package/dist/features/promotions/repository/coupons.repository.d.ts +10 -1
  115. package/dist/features/promotions/repository/coupons.repository.js +22 -8
  116. package/dist/features/promotions/schemas/firestore.d.ts +0 -1
  117. package/dist/features/promotions/schemas/firestore.js +1 -1
  118. package/dist/features/promotions/schemas/index.d.ts +0 -20
  119. package/dist/features/promotions/schemas/index.js +0 -1
  120. package/dist/features/reviews/actions/review-actions.js +5 -1
  121. package/dist/features/reviews/components/ReviewDetailPageView.js +2 -1
  122. package/dist/features/reviews/components/ReviewFilters.d.ts +7 -1
  123. package/dist/features/reviews/components/ReviewFilters.js +4 -3
  124. package/dist/features/reviews/components/ReviewsIndexListing.d.ts +6 -0
  125. package/dist/features/reviews/components/ReviewsIndexListing.js +9 -109
  126. package/dist/features/reviews/components/ReviewsListingPanel.d.ts +41 -0
  127. package/dist/features/reviews/components/ReviewsListingPanel.js +226 -0
  128. package/dist/features/reviews/components/index.d.ts +2 -0
  129. package/dist/features/reviews/components/index.js +1 -0
  130. package/dist/features/reviews/hooks/useReviews.d.ts +8 -0
  131. package/dist/features/reviews/hooks/useReviews.js +14 -6
  132. package/dist/features/reviews/repository/reviews.repository.d.ts +29 -1
  133. package/dist/features/reviews/repository/reviews.repository.js +77 -41
  134. package/dist/features/reviews/types/index.d.ts +8 -1
  135. package/dist/features/seller/components/SellerProductsCards.js +2 -2
  136. package/dist/features/seller/components/SellerProductsView.js +21 -18
  137. package/dist/features/stores/actions/store-query-actions.js +1 -1
  138. package/dist/features/stores/components/StoreAuctionsListing.js +5 -11
  139. package/dist/features/stores/components/StorePreOrdersListing.js +6 -11
  140. package/dist/features/stores/components/StoreReviewsListing.d.ts +4 -0
  141. package/dist/features/stores/components/StoreReviewsListing.js +7 -108
  142. package/dist/features/stores/schemas/index.d.ts +2 -2
  143. package/dist/features/tester/seed-data/coupons-tester-seed-data.js +3 -3
  144. package/dist/features/tester/seed-data/index.d.ts +1 -0
  145. package/dist/features/tester/seed-data/index.js +1 -0
  146. package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +2 -0
  147. package/dist/features/tester/seed-data/offers-tester-seed-data.js +61 -0
  148. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +337 -1
  149. package/dist/index.d.ts +9 -3
  150. package/dist/index.js +11 -2
  151. package/dist/react/hooks/useListingTypeFlags.d.ts +12 -19
  152. package/dist/react/hooks/useListingTypeFlags.js +4 -9
  153. package/dist/schemas/registry.d.ts +0 -12
  154. package/dist/seed/claimed-coupons-seed-data.js +3 -3
  155. package/dist/seed/coupons-seed-data.js +29 -10
  156. package/dist/seed/faq-seed-data.js +2 -2
  157. package/dist/seed/manifest.js +2 -2
  158. package/dist/styles.css +44 -39
  159. package/dist/tailwind-utilities.css +1 -1
  160. package/dist/ui/components/FilterChipGroup.d.ts +21 -3
  161. package/dist/ui/components/FilterChipGroup.js +33 -4
  162. package/dist/ui/components/HorizontalScroller.d.ts +1 -2
  163. package/dist/ui/components/HorizontalScroller.js +9 -5
  164. package/dist/ui/components/HorizontalScroller.style.css +43 -38
  165. package/package.json +1 -1
@@ -16,10 +16,11 @@ import { CustomSectionTabContent } from "../../products/components/CustomSection
16
16
  import { ShareButton } from "../../products/components/ShareButton";
17
17
  import { HistoryTracker } from "../../history/components/HistoryTracker";
18
18
  import { RelatedItemsSection } from "../../products/components/RelatedItemsSection";
19
- import { computeRelatedItems, getReviewItemsForProduct } from "../../../_internal/server/features/products/data";
19
+ import { computeRelatedItems, getReviewPageForProduct } from "../../../_internal/server/features/products/data";
20
20
  import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel";
21
21
  import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data";
22
- import { ReviewsList } from "../../reviews/components/ReviewsList";
22
+ import { ReviewsListingPanel } from "../../reviews/components/ReviewsListingPanel";
23
+ import { ListingBottomActions } from "../../products/components/ListingBottomActions";
23
24
  function toDescriptionHtml(raw) {
24
25
  if (!raw)
25
26
  return "";
@@ -50,10 +51,10 @@ export async function DigitalCodeDetailPageView({ slug, initialProduct, renderPr
50
51
  const specs = Array.isArray(p.specifications) ? p.specifications : [];
51
52
  const customSections = Array.isArray(p.customSections) ? p.customSections : [];
52
53
  const descriptionHtml = toDescriptionHtml(p.description);
53
- const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups, reviews] = await Promise.all([
54
+ const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups, initialReviews] = await Promise.all([
54
55
  computeRelatedItems(product),
55
56
  getGroupsWithItemsForProduct(product.id),
56
- getReviewItemsForProduct(product.id),
57
+ getReviewPageForProduct(product.id),
57
58
  ]);
58
59
  return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: String(p.id ?? p.slug ?? ""), productType: "product", snapshot: {
59
60
  title,
@@ -61,19 +62,19 @@ export async function DigitalCodeDetailPageView({ slug, initialProduct, renderPr
61
62
  price: price ?? undefined,
62
63
  storeId: typeof p.storeId === "string" ? p.storeId : undefined,
63
64
  storeName: storeName ?? undefined,
64
- } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.DIGITAL_CODES), className: CLS_BREADCRUMB_LINK, children: "Digital Codes" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_DIGITALCODE_BADGE, children: "Digital Code" }), codesLeft > 0 ? (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: [codesLeft, " available"] })) : (_jsx(Span, { size: "xs", weight: "semibold", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "danger-surface", color: "error", children: "Sold out" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) })), meta?.redemptionInstructions && (_jsx(Text, { size: "xs", color: "muted", children: meta.redemptionInstructions })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsList, { reviews: reviews, context: "listing", emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
65
+ } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.DIGITAL_CODES), className: CLS_BREADCRUMB_LINK, children: "Digital Codes" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_DIGITALCODE_BADGE, children: "Digital Code" }), codesLeft > 0 ? (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: [codesLeft, " available"] })) : (_jsx(Span, { size: "xs", weight: "semibold", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "danger-surface", color: "error", children: "Sold out" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) })), meta?.redemptionInstructions && (_jsx(Text, { size: "xs", color: "muted", children: meta.redemptionInstructions })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsListingPanel, { source: { kind: "product", productId: product.id }, stateMode: "local", context: "listing", initialData: initialReviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
65
66
  id: s.id,
66
67
  label: s.title,
67
68
  content: _jsx(CustomSectionTabContent, { section: s }),
68
- })) })), renderBuyBar: () => renderPrimaryActions?.({
69
- productId: String(product.id),
70
- productSlug: String(p.slug ?? product.id),
71
- productTitle: title,
72
- productImage: images[0],
73
- price,
74
- currency,
75
- storeId: typeof p.storeId === "string" ? p.storeId : undefined,
76
- storeName: storeName ?? undefined,
77
- inStock: codesLeft > 0,
78
- }), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined })] })) })] })] }));
69
+ })) })), renderBuyBar: () => (_jsx(Div, { id: "digital-code-buy-bar", children: renderPrimaryActions?.({
70
+ productId: String(product.id),
71
+ productSlug: String(p.slug ?? product.id),
72
+ productTitle: title,
73
+ productImage: images[0],
74
+ price,
75
+ currency,
76
+ storeId: typeof p.storeId === "string" ? p.storeId : undefined,
77
+ storeName: storeName ?? undefined,
78
+ inStock: codesLeft > 0,
79
+ }) })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined })] })) }), _jsx(ListingBottomActions, { listingType: "digital-code", anchorId: "digital-code-buy-bar", price: price, currency: currency, unavailable: codesLeft <= 0 })] })] }));
79
80
  }
@@ -13,5 +13,5 @@ export function FeaturedResultsSection({ title, subtitle, pillLabel, ornamentLab
13
13
  afterImage: item.afterImage,
14
14
  caption: item.caption ?? "",
15
15
  sortOrder: item.sortOrder ?? i,
16
- } }, item.id ?? `result-${i}`) })), keyExtractor: (item, i) => item.id ?? `result-${i}`, perView: CAROUSEL_PER_VIEW.cards, gap: 24, minItemWidth: 260, snapToItems: true, showArrows: true, showFadeEdges: true, loop: true }) })] }));
16
+ } }, item.id ?? `result-${i}`) })), keyExtractor: (item, i) => item.id ?? `result-${i}`, perView: CAROUSEL_PER_VIEW.cards, gap: 24, minItemWidth: 260, snapToItems: true, showArrows: true, loop: true }) })] }));
17
17
  }
@@ -48,5 +48,5 @@ export function SectionCarousel({ title, description, headingVariant = "editoria
48
48
  // Double-row grids (e.g. the homepage Auctions section's
49
49
  // rows:2 config) get the full-height sliver arrows — much
50
50
  // easier to hit than a small circle when the track is tall.
51
- arrowStyle: rows > 1 ? "full-height" : "circle", snapToItems: true, showFadeEdges: true, showScrollbar: false, pauseOnHover: true })), viewMoreHref && !isLoading && (_jsx(Row, { className: "mt-6", justify: hand === "left" ? "end" : "start", children: _jsx(TextLink, { rounded: "lg", paddingX: "xl", paddingY: "sm", href: viewMoreHref, shadow: useLightText ? "lg" : "none", className: "inline-flex items-[center] gap-[0.375rem] transition-colors bg-primary text-white hover:bg-primary-600", size: "sm", weight: "semibold", children: viewMoreLabel }) }))] })] }));
51
+ arrowStyle: rows > 1 ? "full-height" : "circle", snapToItems: true, showScrollbar: false, pauseOnHover: true })), viewMoreHref && !isLoading && (_jsx(Row, { className: "mt-6", justify: hand === "left" ? "end" : "start", children: _jsx(TextLink, { rounded: "lg", paddingX: "xl", paddingY: "sm", href: viewMoreHref, shadow: useLightText ? "lg" : "none", className: "inline-flex items-[center] gap-[0.375rem] transition-colors bg-primary text-white hover:bg-primary-600", size: "sm", weight: "semibold", children: viewMoreLabel }) }))] })] }));
52
52
  }
@@ -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, showFadeEdges: 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-[var(--appkit-space-1)] text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-primary)] hover:opacity-80", children: viewMoreLabel }) }))] }) }));
73
73
  }
@@ -170,9 +170,16 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
170
170
  const handleBeforeDashboardNavToggle = useCallback(() => {
171
171
  setSidebarOpen(false);
172
172
  }, []);
173
+ // Must stay in sync with BottomActions' own `isVisible` — `secondaryLabel` was missing
174
+ // here, so an auction bar shown by its countdown alone got no bottom clearance.
173
175
  const hasBottomActions = bottomActionsState.actions.length > 0 ||
174
176
  !!(bottomActionsState.bulk && bottomActionsState.bulk.selectedCount > 0) ||
175
- !!bottomActionsState.infoLabel;
177
+ !!bottomActionsState.infoLabel ||
178
+ !!bottomActionsState.secondaryLabel;
179
+ /** The bar overlays desktop content too once a page opts in, so reserve space there. */
180
+ const hasDesktopBottomActions = hasBottomActions &&
181
+ (bottomActionsState.desktop === "always" ||
182
+ bottomActionsState.desktop === "after-scroll");
176
183
  const sidebarContent = (_jsx(SidebarContent, { sidebarItems: sidebarItems, sidebarSections: sidebarSections, sidebarPrimaryActions: sidebarPrimaryActions, user: user, activeHref: pathname ?? "", profileHref: profileHref, userOrdersHref: userOrdersHref, userWishlistHref: userWishlistHref, userSettingsHref: userSettingsHref, adminHref: adminHref, storeHref: storeHref, sellerHref: sellerHref, sidebarLocaleSlot: sidebarLocaleSlot, showThemeToggleInSidebar: showThemeToggleInSidebar, sidebarProfileLabels: sidebarProfileLabels, theme: theme, activeThemeName: activeTheme.name, toggleTheme: toggleTheme, onLogout: onLogout, onAfterLogout: () => setSidebarOpen(false) }));
177
184
  const normalizedLightBackground = {
178
185
  type: lightBackground.type,
@@ -192,5 +199,5 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
192
199
  opacity: darkBackground.overlay?.opacity ?? 0,
193
200
  },
194
201
  };
195
- return (_jsx(_Fragment, { children: _jsxs(Stack, { className: "min-h-screen w-full overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onTourStart: onTourStart, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(NavbarWithSettings, { navItems: navItems, hiddenNavItems: hiddenNavItems, permissions: authUser?.permissions }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot), eventBannerSlot] }), _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { layout: "flex", className: "relative w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsx(SidebarUserHeader, { user: user, onClose: () => setSidebarOpen(false) })) : (_jsx(SidebarGuestHeader, { sidebarTitle: sidebarTitle, onClose: () => setSidebarOpen(false) })), onClose: () => setSidebarOpen(false), children: sidebarContent }), _jsx(Main, { id: "main-content", className: `w-full flex-1 flex flex-col ${hasBottomActions ? "mb-28" : "mb-16"} lg:mb-0`, children: _jsx(Div, { padding: "y-lg", className: `flex-1 ${contentClassName ?? "mx-auto w-full max-w-screen-xl px-[var(--appkit-space-5)] md:px-[var(--appkit-space-6)] lg:px-[var(--appkit-space-8)]"}`, children: children }) })] }), _jsx(BackToTop, {}), _jsx(FooterLayout, { ...footer }), _jsx(BottomActions, {}), _jsx(BottomNavbar, { user: user, userId: userId, homeHref: homeHref, shopHref: shopHref, cartHref: cartHref, wishlistHref: wishlistHref, profileHref: profileHref, loginHref: loginHref, onSearchToggle: () => setSearchOpen((prev) => !prev), navItems: navItems, onMoreToggle: hasDashboardNav ? toggleDashboardNav : handleTogglePublicSidebar }), _jsx(UnsavedChangesModal, {})] }) }));
202
+ return (_jsx(_Fragment, { children: _jsxs(Stack, { className: "min-h-screen w-full overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onTourStart: onTourStart, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(NavbarWithSettings, { navItems: navItems, hiddenNavItems: hiddenNavItems, permissions: authUser?.permissions }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot), eventBannerSlot] }), _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { layout: "flex", className: "relative w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsx(SidebarUserHeader, { user: user, onClose: () => setSidebarOpen(false) })) : (_jsx(SidebarGuestHeader, { sidebarTitle: sidebarTitle, onClose: () => setSidebarOpen(false) })), onClose: () => setSidebarOpen(false), children: sidebarContent }), _jsx(Main, { id: "main-content", className: `w-full flex-1 flex flex-col ${hasBottomActions ? "mb-28" : "mb-16"} ${hasDesktopBottomActions ? "lg:mb-24" : "lg:mb-0"}`, children: _jsx(Div, { padding: "y-lg", className: `flex-1 ${contentClassName ?? "mx-auto w-full max-w-screen-xl px-[var(--appkit-space-5)] md:px-[var(--appkit-space-6)] lg:px-[var(--appkit-space-8)]"}`, children: children }) })] }), _jsx(BackToTop, {}), _jsx(FooterLayout, { ...footer }), _jsx(BottomActions, {}), _jsx(BottomNavbar, { user: user, userId: userId, homeHref: homeHref, shopHref: shopHref, cartHref: cartHref, wishlistHref: wishlistHref, profileHref: profileHref, loginHref: loginHref, onSearchToggle: () => setSearchOpen((prev) => !prev), navItems: navItems, onMoreToggle: hasDashboardNav ? toggleDashboardNav : handleTogglePublicSidebar }), _jsx(UnsavedChangesModal, {})] }) }));
196
203
  }
@@ -18,7 +18,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
18
18
  * styled with the selected action's variant (danger = red, etc.).
19
19
  *
20
20
  * Layout rules:
21
- * - Hidden on lg+ screens (`lg:hidden`) — desktop shows inline action panels.
21
+ * - Hidden on lg+ screens by default — desktop shows inline action panels. A page can
22
+ * opt in by passing `desktop: "after-scroll" | "always"` to `useBottomActions`;
23
+ * "after-scroll" brings the bar back once the primary CTA has scrolled out of view.
22
24
  * - The bar slides up with a 300 ms ease-out transition; `pointer-events-none`
23
25
  * while off-screen.
24
26
  *
@@ -38,6 +40,8 @@ const Z_BOTTOM_ACTIONS = "z-[var(--appkit-z-bottom-nav)]";
38
40
  const BOTTOM_NAV_HEIGHT = "h-14";
39
41
  const FLEX_CENTER = "flex items-center justify-center";
40
42
  const CLS_COUNT_BADGE = "bg-error-surface text-white";
43
+ /** How far the user must scroll before the desktop bar reveals itself. */
44
+ const DESKTOP_REVEAL_SCROLL_PX = 400;
41
45
  // ─── Sub-components ───────────────────────────────────────────────────────────
42
46
  function BulkPickerPanel({ bulkActions, selectedActionId, pickerOpen, onSelect, }) {
43
47
  return (_jsx(Div, { role: "listbox", "aria-label": "Bulk actions", className: [
@@ -85,7 +89,23 @@ export default function BottomActions() {
85
89
  const [selectedActionId, setSelectedActionId] = useState(null);
86
90
  const containerRef = useRef(null);
87
91
  useClickOutside(containerRef, () => setPickerOpen(false));
88
- const { actions, bulk, infoLabel, secondaryLabel } = state;
92
+ const { actions, bulk, infoLabel, secondaryLabel, desktop } = state;
93
+ // Desktop reveal-on-scroll. Kept as a plain passive listener to match BackToTop —
94
+ // there is no shared scroll-position hook in the codebase and one consumer doesn't
95
+ // justify inventing one. Only armed when a page actually opts into "after-scroll".
96
+ const [scrolledPastFold, setScrolledPastFold] = useState(false);
97
+ const wantsScrollGate = desktop === "after-scroll";
98
+ useEffect(() => {
99
+ if (!wantsScrollGate) {
100
+ setScrolledPastFold(false);
101
+ return;
102
+ }
103
+ const onScroll = () => setScrolledPastFold(window.scrollY > DESKTOP_REVEAL_SCROLL_PX);
104
+ onScroll();
105
+ window.addEventListener("scroll", onScroll, { passive: true });
106
+ return () => window.removeEventListener("scroll", onScroll);
107
+ }, [wantsScrollGate]);
108
+ const showOnDesktop = desktop === "always" || (wantsScrollGate && scrolledPastFold);
89
109
  const isBulkMode = !!(bulk && bulk.selectedCount > 0);
90
110
  const bulkActions = bulk?.actions ?? [];
91
111
  const pageActions = actions;
@@ -120,7 +140,10 @@ export default function BottomActions() {
120
140
  setPickerOpen(false);
121
141
  };
122
142
  return (_jsxs(Div, { ref: containerRef, role: "toolbar", "aria-label": isBulkMode ? "Bulk actions" : "Page actions", "aria-hidden": !isVisible, className: [
123
- "fixed bottom-[calc(var(--keyboard-inset-height,0px)+var(--bottom-nav-height,4rem))] left-0 right-0 lg:hidden",
143
+ "fixed bottom-[calc(var(--keyboard-inset-height,0px)+var(--bottom-nav-height,4rem))] left-0 right-0",
144
+ // `--bottom-nav-height` resolves to 0px above lg, so the same offset lands the
145
+ // bar on the viewport bottom on desktop with no extra positioning.
146
+ showOnDesktop ? "" : "lg:hidden",
124
147
  Z_BOTTOM_ACTIONS,
125
148
  BOTTOM_NAV_BG,
126
149
  "shadow-[0_-4px_20px_rgba(0,0,0,0.08)] dark:shadow-[0_-4px_20px_rgba(0,0,0,0.30)]",
@@ -69,9 +69,21 @@ export interface BottomBulkConfig {
69
69
  /** Bulk action buttons, rendered right of the selection pill. */
70
70
  actions: BottomAction[];
71
71
  }
72
+ /**
73
+ * Whether the bar is allowed to show on desktop (>= lg), where it has always been
74
+ * `lg:hidden`.
75
+ *
76
+ * - `"hidden"` (default) — mobile/tablet only, the historical behaviour.
77
+ * - `"after-scroll"` — appears once the user has scrolled past the top of the page,
78
+ * so a detail page's primary CTA comes back after the hero scrolls away.
79
+ * - `"always"` — pinned on desktop from first paint.
80
+ */
81
+ export type BottomActionsDesktopMode = "hidden" | "after-scroll" | "always";
72
82
  export interface BottomActionsState {
73
83
  /** Page-level primary actions. */
74
84
  actions: BottomAction[];
85
+ /** Desktop visibility policy. Defaults to "hidden" — opt in per page. */
86
+ desktop?: BottomActionsDesktopMode;
75
87
  /** Bulk-selection config. Bulk mode activates when selectedCount > 0. */
76
88
  bulk?: BottomBulkConfig;
77
89
  /**
@@ -107,6 +119,8 @@ interface BottomActionsContextValue {
107
119
  setInfoLabel: (label: string | undefined) => void;
108
120
  /** Set or clear the secondary label (rendered above infoLabel). */
109
121
  setSecondaryLabel: (label: string | undefined) => void;
122
+ /** Set the desktop visibility policy. */
123
+ setDesktopMode: (mode: BottomActionsDesktopMode | undefined) => void;
110
124
  /** Clear all state (called on feature unmount). */
111
125
  clearAll: () => void;
112
126
  }
@@ -87,6 +87,9 @@ export function BottomActionsProvider({ children, }) {
87
87
  const setSecondaryLabel = useCallback((secondaryLabel) => {
88
88
  setState((prev) => ({ ...prev, secondaryLabel }));
89
89
  }, []);
90
+ const setDesktopMode = useCallback((desktop) => {
91
+ setState((prev) => (prev.desktop === desktop ? prev : { ...prev, desktop }));
92
+ }, []);
90
93
  const clearAll = useCallback(() => {
91
94
  actionCallbacksRef.current = new Map();
92
95
  bulkCallbacksRef.current = new Map();
@@ -102,8 +105,17 @@ export function BottomActionsProvider({ children, }) {
102
105
  bulkClearRef,
103
106
  setInfoLabel,
104
107
  setSecondaryLabel,
108
+ setDesktopMode,
109
+ clearAll,
110
+ }), [
111
+ state,
112
+ setActions,
113
+ setBulkConfig,
114
+ setInfoLabel,
115
+ setSecondaryLabel,
116
+ setDesktopMode,
105
117
  clearAll,
106
- }), [state, setActions, setBulkConfig, setInfoLabel, setSecondaryLabel, clearAll]);
118
+ ]);
107
119
  return (_jsx(BottomActionsContext.Provider, { value: value, children: children }));
108
120
  }
109
121
  // --- Hook ---------------------------------------------------------------------
@@ -1,9 +1,14 @@
1
- import { type BottomAction, type BottomBulkConfig } from "../BottomActionsContext";
1
+ import { type BottomAction, type BottomActionsDesktopMode, type BottomBulkConfig } from "../BottomActionsContext";
2
2
  export interface UseBottomActionsOptions {
3
3
  actions?: BottomAction[];
4
4
  bulk?: BottomBulkConfig;
5
5
  infoLabel?: string;
6
6
  /** Rendered as its own row above infoLabel. See BottomActionsState.secondaryLabel. */
7
7
  secondaryLabel?: string;
8
+ /**
9
+ * Whether the bar may also show on desktop. Defaults to "hidden" (mobile/tablet only),
10
+ * so pages that don't opt in behave exactly as before.
11
+ */
12
+ desktop?: BottomActionsDesktopMode;
8
13
  }
9
14
  export declare function useBottomActions(options?: UseBottomActionsOptions): void;
@@ -2,7 +2,7 @@
2
2
  import { useEffect, useRef } from "react";
3
3
  import { useBottomActionsContext, } from "../BottomActionsContext";
4
4
  export function useBottomActions(options = {}) {
5
- const { setActions, setBulkConfig, setInfoLabel, setSecondaryLabel, clearAll } = useBottomActionsContext();
5
+ const { setActions, setBulkConfig, setInfoLabel, setSecondaryLabel, setDesktopMode, clearAll, } = useBottomActionsContext();
6
6
  const actionsRef = useRef(options.actions ?? []);
7
7
  actionsRef.current = options.actions ?? [];
8
8
  const bulkRef = useRef(options.bulk);
@@ -25,6 +25,9 @@ export function useBottomActions(options = {}) {
25
25
  useEffect(() => {
26
26
  setSecondaryLabel(options.secondaryLabel);
27
27
  }, [setSecondaryLabel, options.secondaryLabel]);
28
+ useEffect(() => {
29
+ setDesktopMode(options.desktop);
30
+ }, [setDesktopMode, options.desktop]);
28
31
  useEffect(() => {
29
32
  return () => clearAll();
30
33
  }, [clearAll]);
@@ -27,7 +27,7 @@ export type { LayoutClientProps, LayoutProvider } from "./LayoutClient";
27
27
  export { AppLayoutShell } from "./AppLayoutShell";
28
28
  export type { AppLayoutShellProps, AppLayoutShellSidebarLink, AppLayoutShellSidebarSection } from "./AppLayoutShell";
29
29
  export { BottomActionsProvider, useBottomActionsContext, } from "./BottomActionsContext";
30
- export type { BottomAction, BottomBulkConfig, BottomActionsState, } from "./BottomActionsContext";
30
+ export type { BottomAction, BottomBulkConfig, BottomActionsState, BottomActionsDesktopMode, } from "./BottomActionsContext";
31
31
  export { useBottomActions } from "./hooks/useBottomActions";
32
32
  export type { UseBottomActionsOptions } from "./hooks/useBottomActions";
33
33
  export { default as BottomActions } from "./BottomActions";
@@ -19,10 +19,11 @@ import { CustomSectionTabContent } from "../../products/components/CustomSection
19
19
  import { ShareButton } from "../../products/components/ShareButton";
20
20
  import { HistoryTracker } from "../../history/components/HistoryTracker";
21
21
  import { RelatedItemsSection } from "../../products/components/RelatedItemsSection";
22
- import { computeRelatedItems, getReviewItemsForProduct } from "../../../_internal/server/features/products/data";
22
+ import { computeRelatedItems, getReviewPageForProduct } from "../../../_internal/server/features/products/data";
23
23
  import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel";
24
24
  import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data";
25
- import { ReviewsList } from "../../reviews/components/ReviewsList";
25
+ import { ReviewsListingPanel } from "../../reviews/components/ReviewsListingPanel";
26
+ import { ListingBottomActions } from "../../products/components/ListingBottomActions";
26
27
  function toDescriptionHtml(raw) {
27
28
  if (!raw)
28
29
  return "";
@@ -55,10 +56,10 @@ export async function LiveItemDetailPageView({ slug, initialProduct, renderActio
55
56
  const specs = Array.isArray(p.specifications) ? p.specifications : [];
56
57
  const customSections = Array.isArray(p.customSections) ? p.customSections : [];
57
58
  const descriptionHtml = toDescriptionHtml(p.description);
58
- const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups, reviews] = await Promise.all([
59
+ const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups, initialReviews] = await Promise.all([
59
60
  computeRelatedItems(product),
60
61
  getGroupsWithItemsForProduct(product.id),
61
- getReviewItemsForProduct(product.id),
62
+ getReviewPageForProduct(product.id),
62
63
  ]);
63
64
  return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: String(p.id ?? p.slug ?? ""), productType: "product", snapshot: {
64
65
  title,
@@ -66,9 +67,9 @@ export async function LiveItemDetailPageView({ slug, initialProduct, renderActio
66
67
  price: price ?? undefined,
67
68
  storeId: typeof p.storeId === "string" ? p.storeId : undefined,
68
69
  storeName: storeName ?? undefined,
69
- } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.LIVE), className: CLS_BREADCRUMB_LINK, children: "Live" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_ITEM_BADGE, children: "Live Item" }), meta?.vendorVerified && (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Verified Seller" })), meta?.cites && (_jsxs(Span, { color: "warning", surface: "warning-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: ["CITES: ", meta.cites] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title }), meta?.species && (_jsxs(Text, { className: "italic mt-1", color: "muted", size: "sm", children: [meta.species, meta.sex && meta.sex !== "n/a" && ` · ${meta.sex}`, meta.ageMonths !== undefined && ` · ${meta.ageMonths}mo`] }))] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) })), jurisdictions.length > 0 && (_jsxs(Div, { className: CLS_WARN_BOX, children: [_jsx(Text, { className: CLS_WARN_TITLE, children: "Delivery restrictions" }), _jsxs(Text, { className: CLS_WARN_BODY, children: ["This item can only be shipped to: ", jurisdictions.join(", ")] })] })), transport && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Transport" }), _jsxs(Text, { className: "text-muted-foreground", children: ["Method: ", transport.method, transport.handlingFee !== undefined && ` · Handling: ${formatCurrency(transport.handlingFee, "INR")}`, transport.insuranceIncluded && " · Insurance included"] })] })), meta?.careInfo && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Care information" }), _jsx(Text, { className: "mt-1 text-muted-foreground", children: meta.careInfo })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsList, { reviews: reviews, context: "listing", emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
70
+ } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.LIVE), className: CLS_BREADCRUMB_LINK, children: "Live" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_ITEM_BADGE, children: "Live Item" }), meta?.vendorVerified && (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Verified Seller" })), meta?.cites && (_jsxs(Span, { color: "warning", surface: "warning-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: ["CITES: ", meta.cites] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title }), meta?.species && (_jsxs(Text, { className: "italic mt-1", color: "muted", size: "sm", children: [meta.species, meta.sex && meta.sex !== "n/a" && ` · ${meta.sex}`, meta.ageMonths !== undefined && ` · ${meta.ageMonths}mo`] }))] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) })), jurisdictions.length > 0 && (_jsxs(Div, { className: CLS_WARN_BOX, children: [_jsx(Text, { className: CLS_WARN_TITLE, children: "Delivery restrictions" }), _jsxs(Text, { className: CLS_WARN_BODY, children: ["This item can only be shipped to: ", jurisdictions.join(", ")] })] })), transport && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Transport" }), _jsxs(Text, { className: "text-muted-foreground", children: ["Method: ", transport.method, transport.handlingFee !== undefined && ` · Handling: ${formatCurrency(transport.handlingFee, "INR")}`, transport.insuranceIncluded && " · Insurance included"] })] })), meta?.careInfo && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Care information" }), _jsx(Text, { className: "mt-1 text-muted-foreground", children: meta.careInfo })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsListingPanel, { source: { kind: "product", productId: product.id }, stateMode: "local", context: "listing", initialData: initialReviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
70
71
  id: s.id,
71
72
  label: s.title,
72
73
  content: _jsx(CustomSectionTabContent, { section: s }),
73
- })) })), renderBuyBar: () => renderActions?.(product), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined })] })) })] })] }));
74
+ })) })), renderBuyBar: () => _jsx(Div, { id: "live-item-buy-bar", children: renderActions?.(product) }), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined })] })) }), _jsx(ListingBottomActions, { listingType: "live", anchorId: "live-item-buy-bar", price: price, currency: currency })] })] }));
74
75
  }
@@ -175,6 +175,10 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
175
175
  canFilter: boolean;
176
176
  canSort: boolean;
177
177
  };
178
+ orderType: {
179
+ canFilter: boolean;
180
+ canSort: boolean;
181
+ };
178
182
  };
179
183
  listAll(model: SieveModel): Promise<FirebaseSieveResult<OrderDocument>>;
180
184
  /**
@@ -516,6 +516,10 @@ OrderRepository.ADMIN_SIEVE_FIELDS = {
516
516
  // S-SBUNI-RULES 2026-05-13 — refund + batch fields
517
517
  paymentBatchId: { canFilter: true, canSort: false },
518
518
  contestable: { canFilter: true, canSort: false },
519
+ // Lane segregation on /user/orders — "standard"/"auction"/"offer" etc.
520
+ // Never filtered as "standard" via Firestore == (see listForUser callers):
521
+ // orders written before this field existed have no value to match.
522
+ orderType: { canFilter: true, canSort: false },
519
523
  };
520
524
  const orderRepository = new OrderRepository();
521
525
  export { OrderRepository, orderRepository };
@@ -1,6 +1,8 @@
1
1
  import { type PaymentGateway, PaymentGatewayValues } from "../../payments/schemas";
2
2
  export { type PaymentGateway, PaymentGatewayValues };
3
3
  import type { ListingType } from "../../products/types/index";
4
+ import type { OrderType } from "../utils/order-splitter";
5
+ import type { AppliedOrderDiscount } from "../schemas/firestore";
4
6
  export type PaymentStatus = "pending" | "processing" | "paid" | "failed" | "refunded" | "partial_refund";
5
7
  export interface UserAddress {
6
8
  id: string;
@@ -57,6 +59,13 @@ export interface Order {
57
59
  address: UserAddress;
58
60
  orderStatus: OrderStatus;
59
61
  paymentStatus: PaymentStatus;
62
+ /**
63
+ * Which checkout lane produced this order. Absent on orders written before
64
+ * the field existed — treat missing as `"standard"`, never filter it out.
65
+ */
66
+ orderType?: OrderType;
67
+ /** Set when this order settled an accepted Make-an-Offer. */
68
+ offerId?: string;
60
69
  paymentGateway?: PaymentGateway;
61
70
  subtotal: number;
62
71
  shippingCost?: number;
@@ -64,7 +73,14 @@ export interface Order {
64
73
  tax?: number;
65
74
  total: number;
66
75
  currency: string;
76
+ /**
77
+ * First applied coupon's code only. Kept for back-compat; prefer
78
+ * `appliedDiscounts` when rendering, since a cart may stack one coupon per
79
+ * store plus one platform-wide coupon and this scalar shows only one of them.
80
+ */
67
81
  couponCode?: string;
82
+ /** Every discount applied to this order — the full stack, not just the first. */
83
+ appliedDiscounts?: AppliedOrderDiscount[];
68
84
  trackingNumber?: string;
69
85
  shippingCarrier?: string;
70
86
  /** Carrier tracking page URL — rendered as an external link that opens in a new tab. No live-tracking API integration; this is just the passthrough URL a seller/admin entered. */
@@ -2,4 +2,4 @@ export interface PreOrderBottomActionsProps {
2
2
  price: number | null;
3
3
  currency: string;
4
4
  }
5
- export declare function PreOrderBottomActions({ price, currency, }: PreOrderBottomActionsProps): null;
5
+ export declare function PreOrderBottomActions({ price, currency, }: PreOrderBottomActionsProps): import("react").JSX.Element;
@@ -1,22 +1,6 @@
1
1
  "use client";
2
- import { useBottomActions } from "../../layout/hooks/useBottomActions";
3
- import { formatCurrency } from "../../../utils/number.formatter";
4
- import { ACTION_ID } from "../../products/constants/action-defs";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { ListingBottomActions } from "../../products/components/ListingBottomActions";
5
4
  export function PreOrderBottomActions({ price, currency, }) {
6
- useBottomActions({
7
- actions: [
8
- {
9
- id: ACTION_ID.RESERVE_NOW,
10
- label: "Reserve Now",
11
- variant: "primary",
12
- onClick: () => {
13
- document
14
- .getElementById("pre-order-buy-bar")
15
- ?.scrollIntoView({ behavior: "smooth" });
16
- },
17
- },
18
- ],
19
- infoLabel: price !== null ? formatCurrency(price, currency) : undefined,
20
- });
21
- return null;
5
+ return (_jsx(ListingBottomActions, { listingType: "preorder", anchorId: "pre-order-buy-bar", price: price, currency: currency }));
22
6
  }
@@ -24,7 +24,6 @@ const __O = {
24
24
  hidden: "overflow-hidden",
25
25
  };
26
26
  const DEFAULT_SORT = AUCTION_PUBLIC_SORT_OPTIONS[0].value;
27
- const AUCTION_SORT_OPTIONS = AUCTION_PUBLIC_SORT_OPTIONS;
28
27
  const FILTER_KEYS = [TABLE_KEYS.CATEGORY, TABLE_KEYS.BRAND, TABLE_KEYS.MIN_BID, TABLE_KEYS.MAX_BID, TABLE_KEYS.STORE_ID, TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO];
29
28
  export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
30
29
  const table = useUrlTable({ defaults: { pageSize: "24", sort: DEFAULT_SORT } });
@@ -132,7 +131,7 @@ export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
132
131
  },
133
132
  },
134
133
  ] } } : {});
135
- return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search auctions...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: AUCTION_SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: auctions.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection, toggles: [
134
+ return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search auctions...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: AUCTION_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: auctions.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection, toggles: [
136
135
  { label: "Show ended", active: showEnded, onChange: (next) => table.set(TABLE_KEYS.SHOW_ENDED, next ? "true" : "") },
137
136
  ] }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
138
137
  {
@@ -0,0 +1,28 @@
1
+ import { MOBILE_PRIMARY_ACTIONS } from "../constants/action-defs";
2
+ /**
3
+ * The sticky-CTA registrar shared by every listing type whose bottom bar is just
4
+ * "primary CTA that scrolls to the buy/contact panel, plus a price label".
5
+ *
6
+ * Pre-order, prize-draw, classified, digital-code and live all fit that shape, which put
7
+ * it well past the Rule of Three for a per-type copy. Auction and standard product keep
8
+ * their own registrars — the auction ticks a live countdown into `secondaryLabel`, and
9
+ * the product bar carries three buttons whose labels track cart/wishlist state.
10
+ *
11
+ * Renders nothing; it exists purely to register into `BottomActionsContext`.
12
+ */
13
+ export interface ListingBottomActionsProps {
14
+ /** Selects the CTA set from the ACTIONS registry (Rule #7 — never inline actions). */
15
+ listingType: keyof typeof MOBILE_PRIMARY_ACTIONS;
16
+ /** `id` of the element the CTA scrolls to. */
17
+ anchorId: string;
18
+ price: number | null;
19
+ currency: string;
20
+ /**
21
+ * Register nothing at all — for ended/closed/sold-out listings, matching how the
22
+ * auction and prize-draw registrars bail out.
23
+ */
24
+ unavailable?: boolean;
25
+ /** Appended after the formatted price, e.g. "per entry". */
26
+ infoSuffix?: string;
27
+ }
28
+ export declare function ListingBottomActions({ listingType, anchorId, price, currency, unavailable, infoSuffix, }: ListingBottomActionsProps): null;
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { useBottomActions } from "../../layout/hooks/useBottomActions";
3
+ import { formatCurrency } from "../../../utils/number.formatter";
4
+ import { ACTION_META, MOBILE_PRIMARY_ACTIONS } from "../constants/action-defs";
5
+ export function ListingBottomActions({ listingType, anchorId, price, currency, unavailable = false, infoSuffix, }) {
6
+ const actionIds = MOBILE_PRIMARY_ACTIONS[listingType] ?? [];
7
+ const infoLabel = price !== null
8
+ ? `${formatCurrency(price, currency)}${infoSuffix ? ` ${infoSuffix}` : ""}`
9
+ : undefined;
10
+ useBottomActions(unavailable
11
+ ? {}
12
+ : {
13
+ actions: actionIds.map((id) => ({
14
+ ...ACTION_META[id],
15
+ onClick: () => {
16
+ document
17
+ .getElementById(anchorId)
18
+ ?.scrollIntoView({ behavior: "smooth" });
19
+ },
20
+ })),
21
+ infoLabel,
22
+ desktop: "after-scroll",
23
+ });
24
+ return null;
25
+ }
@@ -3,4 +3,4 @@ export interface PrizeDrawBottomActionsProps {
3
3
  currency: string;
4
4
  closed: boolean;
5
5
  }
6
- export declare function PrizeDrawBottomActions({ pricePerEntry, currency, closed, }: PrizeDrawBottomActionsProps): null;
6
+ export declare function PrizeDrawBottomActions({ pricePerEntry, currency, closed, }: PrizeDrawBottomActionsProps): import("react").JSX.Element;
@@ -1,24 +1,6 @@
1
1
  "use client";
2
- import { useBottomActions } from "../../layout/hooks/useBottomActions";
3
- import { formatCurrency } from "../../../utils/number.formatter";
4
- import { ACTION_ID } from "../constants/action-defs";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { ListingBottomActions } from "./ListingBottomActions";
5
4
  export function PrizeDrawBottomActions({ pricePerEntry, currency, closed, }) {
6
- useBottomActions(closed
7
- ? {}
8
- : {
9
- actions: [
10
- {
11
- id: ACTION_ID.BUY_NOW,
12
- label: "Buy Now",
13
- variant: "primary",
14
- onClick: () => {
15
- document
16
- .getElementById("prize-draw-buy-bar")
17
- ?.scrollIntoView({ behavior: "smooth" });
18
- },
19
- },
20
- ],
21
- infoLabel: `${formatCurrency(pricePerEntry, currency)} per entry`,
22
- });
23
- return null;
5
+ return (_jsx(ListingBottomActions, { listingType: "prize-draw", anchorId: "prize-draw-buy-bar", price: pricePerEntry, currency: currency, unavailable: closed, infoSuffix: "per entry" }));
24
6
  }
@@ -122,6 +122,10 @@ export function ProductDetailActions({ productId, productTitle, productImage, pr
122
122
  infoLabel: price != null && currency
123
123
  ? formatCurrency(price, currency)
124
124
  : undefined,
125
+ // The bar itself owns responsive visibility now, so this one registrar serves
126
+ // both breakpoints. The desktop branch below must NOT also register —
127
+ // useBottomActions is last-mounted-wins and the two would clobber each other.
128
+ desktop: "after-scroll",
125
129
  }
126
130
  : {});
127
131
  const authModal = (_jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage }));