@mohasinac/appkit 4.53.2 → 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 (179) hide show
  1. package/dist/.build-stamp.json +5 -5
  2. package/dist/_internal/server/features/auth/capabilities.js +7 -5
  3. package/dist/_internal/server/features/checkout/supplier-identity.js +28 -6
  4. package/dist/_internal/server/features/products/listing-tab-counts.js +81 -25
  5. package/dist/_internal/server/features/search/index-build/sources.js +43 -2
  6. package/dist/_internal/server/jobs/core/jobRunners.js +0 -10
  7. package/dist/_internal/server/jobs/core/onOrderCreate.js +10 -104
  8. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  9. package/dist/_internal/shared/actions/action-registry.js +128 -0
  10. package/dist/_internal/shared/features/categories/bundle-copy.d.ts +1 -1
  11. package/dist/_internal/shared/features/site-settings/supplier-of-record.d.ts +56 -0
  12. package/dist/_internal/shared/features/site-settings/supplier-of-record.js +57 -0
  13. package/dist/_internal/shared/listing-types/tiers.d.ts +94 -0
  14. package/dist/_internal/shared/listing-types/tiers.js +150 -0
  15. package/dist/cli/index.js +0 -2
  16. package/dist/client.d.ts +21 -2
  17. package/dist/client.js +23 -4
  18. package/dist/constants/api-endpoints.d.ts +21 -0
  19. package/dist/constants/api-endpoints.js +7 -0
  20. package/dist/features/account/components/UserOrdersView.js +26 -7
  21. package/dist/features/addresses/utils/postal-autofill.d.ts +39 -0
  22. package/dist/features/addresses/utils/postal-autofill.js +54 -0
  23. package/dist/features/admin/components/AdminStoreEditorView.js +73 -34
  24. package/dist/features/admin/components/OwnerQuickActions.d.ts +29 -0
  25. package/dist/features/admin/components/OwnerQuickActions.js +142 -0
  26. package/dist/features/admin/schemas/admin-editor-forms.d.ts +1 -1
  27. package/dist/features/admin/schemas/admin-editor-forms.js +4 -1
  28. package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +11 -1
  29. package/dist/features/auctions/components/MarketplaceAuctionCard.js +9 -2
  30. package/dist/features/auth/permissions/constants.d.ts +29 -2
  31. package/dist/features/auth/permissions/constants.js +68 -1
  32. package/dist/features/blog/components/BlogFeaturedCard.d.ts +11 -1
  33. package/dist/features/blog/components/BlogFeaturedCard.js +9 -2
  34. package/dist/features/cart/components/CartDrawer.js +3 -2
  35. package/dist/features/cart/components/CartGroupLineRow.js +2 -1
  36. package/dist/features/cart/hooks/useAddToCart.js +10 -5
  37. package/dist/features/cart/utils/pending-ops.d.ts +11 -1
  38. package/dist/features/cart/utils/pending-ops.js +14 -2
  39. package/dist/features/categories/components/BrandDetailPageView.js +2 -1
  40. package/dist/features/categories/components/CategoryDetailPageView.js +2 -1
  41. package/dist/features/categories/components/CategoryGrid.d.ts +11 -1
  42. package/dist/features/categories/components/CategoryGrid.js +9 -2
  43. package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +10 -5
  44. package/dist/features/checkout/actions/checkout-value-otp-actions.js +2 -88
  45. package/dist/features/events/components/EventCard.d.ts +11 -1
  46. package/dist/features/events/components/EventCard.js +9 -2
  47. package/dist/features/layout/AppLayoutShell.js +2 -1
  48. package/dist/features/layout/BottomActions.d.ts +1 -0
  49. package/dist/features/layout/BottomActions.js +330 -0
  50. package/dist/features/layout/BottomChrome.d.ts +45 -0
  51. package/dist/features/layout/BottomChrome.js +5 -17
  52. package/dist/features/layout/SideActionDock.js +36 -13
  53. package/dist/features/layout/index.d.ts +2 -7
  54. package/dist/features/layout/index.js +7 -7
  55. package/dist/features/layout/useIsDesktop.d.ts +36 -0
  56. package/dist/features/layout/useIsDesktop.js +48 -0
  57. package/dist/features/media/messages/en.json +67 -67
  58. package/dist/features/orders/components/YourPurchaseStrip.d.ts +24 -0
  59. package/dist/features/orders/components/YourPurchaseStrip.js +77 -0
  60. package/dist/features/orders/components/index.d.ts +2 -1
  61. package/dist/features/orders/components/index.js +1 -1
  62. package/dist/features/orders/utils/order-announcement-message.d.ts +18 -0
  63. package/dist/features/orders/utils/order-announcement-message.js +16 -0
  64. package/dist/features/orders/utils/payment-proof-message.d.ts +20 -0
  65. package/dist/features/orders/utils/payment-proof-message.js +17 -0
  66. package/dist/features/parts/schemas/firestore.d.ts +227 -0
  67. package/dist/features/parts/schemas/firestore.js +6 -6
  68. package/dist/features/pre-orders/components/MarketplacePreorderCard.d.ts +11 -1
  69. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +10 -3
  70. package/dist/features/products/api/[id]/route.js +8 -1
  71. package/dist/features/products/api/route.js +16 -4
  72. package/dist/features/products/components/AvailabilityScopeHint.js +1 -1
  73. package/dist/features/products/components/InteractiveProductCard.d.ts +7 -1
  74. package/dist/features/products/components/InteractiveProductCard.js +16 -4
  75. package/dist/features/products/components/MarketplaceBundleCard.d.ts +11 -1
  76. package/dist/features/products/components/MarketplaceBundleCard.js +9 -2
  77. package/dist/features/products/components/MarketplacePrizeDrawCard.d.ts +11 -1
  78. package/dist/features/products/components/MarketplacePrizeDrawCard.js +9 -2
  79. package/dist/features/products/components/ProductDetailPageView.js +27 -2
  80. package/dist/features/products/components/ProductGrid.d.ts +14 -1
  81. package/dist/features/products/components/ProductGrid.js +22 -5
  82. package/dist/features/products/components/ProductsIndexListing.js +2 -2
  83. package/dist/features/reviews/api/[id]/route.js +83 -23
  84. package/dist/features/reviews/components/PurchaseReviewAffordance.d.ts +6 -0
  85. package/dist/features/reviews/components/PurchaseReviewAffordance.js +42 -0
  86. package/dist/features/reviews/components/ReviewFormDrawer.d.ts +34 -0
  87. package/dist/features/reviews/components/ReviewFormDrawer.js +127 -0
  88. package/dist/features/reviews/components/index.d.ts +4 -0
  89. package/dist/features/reviews/components/index.js +2 -0
  90. package/dist/features/seller/actions/seller-actions.d.ts +1 -1
  91. package/dist/features/seller/actions/seller-actions.js +49 -3
  92. package/dist/features/seller/components/SellerPayoutSettingsView.js +30 -2
  93. package/dist/features/seller/components/SellerProductShell.d.ts +17 -1
  94. package/dist/features/seller/hooks/useStoreListingEntitlements.d.ts +8 -0
  95. package/dist/features/seller/hooks/useStoreListingEntitlements.js +70 -0
  96. package/dist/features/stores/components/InteractiveStoreCard.d.ts +11 -1
  97. package/dist/features/stores/components/InteractiveStoreCard.js +9 -2
  98. package/dist/features/stores/components/index.d.ts +0 -2
  99. package/dist/features/stores/components/index.js +0 -1
  100. package/dist/features/stores/schemas/index.d.ts +1 -1
  101. package/dist/features/stores/schemas/index.js +10 -1
  102. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +403 -0
  103. package/dist/index.d.ts +8 -27
  104. package/dist/index.js +14 -35
  105. package/dist/next/routing/route-map.d.ts +2 -0
  106. package/dist/next/routing/route-map.js +9 -0
  107. package/dist/providers/shipping-shiprocket/index.d.ts +98 -0
  108. package/dist/providers/shipping-shiprocket/index.js +353 -0
  109. package/dist/schemas/registry.d.ts +2 -2
  110. package/dist/security/csp.d.ts +16 -1
  111. package/dist/security/csp.js +5 -3
  112. package/dist/seed/_helpers/category-forest.js +27 -0
  113. package/dist/seed/_helpers/category-parts-glossary-map.js +1816 -86
  114. package/dist/seed/_helpers/category-parts.d.ts +1 -1
  115. package/dist/seed/_helpers/category-parts.js +13412 -3020
  116. package/dist/seed/categories-seed-data.js +48 -6
  117. package/dist/seed/homepage-sections-seed-data.js +41 -28
  118. package/dist/seed/model-composition-seed-data.js +6 -8
  119. package/dist/seed/part-glossary/chunk0.d.ts +9 -0
  120. package/dist/seed/part-glossary/chunk0.js +35044 -0
  121. package/dist/seed/part-glossary/chunk1.d.ts +9 -0
  122. package/dist/seed/part-glossary/chunk1.js +39955 -0
  123. package/dist/seed/part-glossary/chunk2.d.ts +9 -0
  124. package/dist/seed/part-glossary/chunk2.js +50335 -0
  125. package/dist/seed/part-glossary/chunk3.d.ts +9 -0
  126. package/dist/seed/part-glossary/chunk3.js +52386 -0
  127. package/dist/seed/part-glossary/chunk4.d.ts +9 -0
  128. package/dist/seed/part-glossary/chunk4.js +50549 -0
  129. package/dist/seed/part-glossary-seed-data.d.ts +1 -1
  130. package/dist/seed/part-glossary-seed-data.js +23 -47483
  131. package/dist/server.d.ts +4 -8
  132. package/dist/server.js +8 -20
  133. package/dist/styles.css +516 -358
  134. package/dist/tailwind-utilities.css +1 -1
  135. package/dist/tokens/themes/index.d.ts +3 -1
  136. package/dist/tokens/themes/index.js +11 -1
  137. package/dist/tokens/themes/technical-dark.d.ts +30 -0
  138. package/dist/tokens/themes/technical-dark.js +103 -0
  139. package/dist/tokens/themes/technical-light.d.ts +22 -0
  140. package/dist/tokens/themes/technical-light.js +89 -0
  141. package/dist/tokens/tokens.css +155 -0
  142. package/dist/ui/components/GatedPrice.js +30 -3
  143. package/dist/ui/components/GatedPrice.style.css +22 -19
  144. package/dist/ui/components/QuickCreateModal.style.css +78 -78
  145. package/dist/ui/forms/FormShell.style.css +260 -260
  146. package/package.json +1 -1
  147. package/dist/_internal/server/jobs/core/whatsappCatalogImport.d.ts +0 -27
  148. package/dist/_internal/server/jobs/core/whatsappCatalogImport.js +0 -172
  149. package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +0 -35
  150. package/dist/_internal/server/jobs/core/whatsappNotify.js +0 -140
  151. package/dist/features/admin/components/AdminWhatsAppGuideView.d.ts +0 -2
  152. package/dist/features/admin/components/AdminWhatsAppGuideView.js +0 -35
  153. package/dist/features/orders/components/MarketplaceOrderCard.d.ts +0 -36
  154. package/dist/features/orders/components/MarketplaceOrderCard.js +0 -57
  155. package/dist/features/stores/components/StoreWhatsAppGuideView.d.ts +0 -3
  156. package/dist/features/stores/components/StoreWhatsAppGuideView.js +0 -12
  157. package/dist/features/whatsapp-bot/api/send-status/route.d.ts +0 -12
  158. package/dist/features/whatsapp-bot/api/send-status/route.js +0 -38
  159. package/dist/features/whatsapp-bot/api/webhook/route.d.ts +0 -12
  160. package/dist/features/whatsapp-bot/api/webhook/route.js +0 -43
  161. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +0 -6
  162. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +0 -270
  163. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.d.ts +0 -9
  164. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +0 -7
  165. package/dist/features/whatsapp-bot/components/index.d.ts +0 -3
  166. package/dist/features/whatsapp-bot/components/index.js +0 -2
  167. package/dist/features/whatsapp-bot/helpers/payment-proof-message.d.ts +0 -14
  168. package/dist/features/whatsapp-bot/helpers/payment-proof-message.js +0 -17
  169. package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +0 -52
  170. package/dist/features/whatsapp-bot/helpers/whatsapp.js +0 -283
  171. package/dist/features/whatsapp-bot/index.d.ts +0 -3
  172. package/dist/features/whatsapp-bot/index.js +0 -3
  173. package/dist/features/whatsapp-bot/manifest.d.ts +0 -2
  174. package/dist/features/whatsapp-bot/manifest.js +0 -13
  175. package/dist/features/whatsapp-bot/messages/en.json +0 -7
  176. package/dist/features/whatsapp-bot/server.d.ts +0 -7
  177. package/dist/features/whatsapp-bot/server.js +0 -7
  178. package/dist/features/whatsapp-bot/types/index.d.ts +0 -126
  179. package/dist/features/whatsapp-bot/types/index.js +0 -1
@@ -269,11 +269,11 @@ export const PART_SLOT_CATEGORY = {
269
269
  // Burst — a driver with the disc moulded in is still a driver.
270
270
  "disc-integrated-driver": "category-burst-drivers",
271
271
  // Beyblade X — the CX Custom line, which has its own node.
272
- "lock-chip": "category-x-custom-cx",
273
- "main-blade": "category-x-custom-cx",
274
- "assist-blade": "category-x-custom-cx",
275
- "over-blade": "category-x-custom-cx",
276
- "metal-blade": "category-x-custom-cx",
272
+ "lock-chip": "category-x-lock-chips",
273
+ "main-blade": "category-x-main-blades",
274
+ "assist-blade": "category-x-assist-blades",
275
+ "over-blade": "category-x-over-blades",
276
+ "metal-blade": "category-x-metal-blades",
277
277
  "gear-chip": "category-x-custom-cx",
278
278
  // Beyblade X — integrated parts file under the half a buyer shops for.
279
279
  "ratchet-integrated-blade": "category-x-blades",
@@ -290,7 +290,7 @@ export const PART_SLOT_CATEGORY = {
290
290
  "evolution-gear": "category-original-engine-gears",
291
291
  "bit-protector": "category-original-bit-chips",
292
292
  cew: "category-original-weight-disks",
293
- "support-parts": "category-original-parts",
293
+ "support-parts": "category-original-support-parts",
294
294
  };
295
295
  /** Every slot key, for an index page that must not hand-write the list. */
296
296
  export const ALL_PART_SLOTS = Object.values(PART_SLOT_VALUES);
@@ -23,5 +23,15 @@ export interface MarketplacePreorderCardProps {
23
23
  onAddToCart?: (product: MarketplacePreorderCardData) => void;
24
24
  wishlistActions?: WishlistToggleActions;
25
25
  labels?: MarketplacePreorderCardLabels;
26
+ /**
27
+ * Owner-only pencil / clone / bin. Pass `<OwnerQuickActions>`; it renders
28
+ * nothing for a visitor who does not own the record.
29
+ *
30
+ * 🛑 Rendered OUTSIDE this card's own anchor, as a sibling. A `<button>`
31
+ * inside an `<a>` is invalid HTML that browsers resolve by closing the
32
+ * anchor early, which breaks the CARD's navigation rather than just the
33
+ * button's — `stopPropagation` stops a click but cannot fix a parse.
34
+ */
35
+ renderOwnerActions?: () => React.ReactNode;
26
36
  }
27
- export declare function MarketplacePreorderCard({ product, className, variant, selectable, isSelected, onSelect, inWishlist: initialInWishlist, href, hrefBuilder, onNavigate, onAddToCart, wishlistActions, labels, }: MarketplacePreorderCardProps): import("react").JSX.Element;
37
+ export declare function MarketplacePreorderCard({ product, className, variant, selectable, isSelected, onSelect, inWishlist: initialInWishlist, href, hrefBuilder, onNavigate, onAddToCart, wishlistActions, labels, renderOwnerActions, }: MarketplacePreorderCardProps): import("react").JSX.Element;
@@ -14,7 +14,7 @@ const CLS_PREORDER_BADGE = "inline-flex items-center rounded-full bg-warning-sol
14
14
  const CLS_SALE_BADGE = "rounded-full bg-success-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-bold text-success-on-solid";
15
15
  const CLS_TRENDING_BADGE = "rounded-full bg-indigo-600 px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-bold text-white";
16
16
  const CLS_LIMITED_BADGE = "rounded-full bg-info-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-bold text-info-on-solid";
17
- import { BaseListingCard, Button, GatedPrice, RichText, Row, Span, Stack, Text, TextLink } from "../../../ui/index.js";
17
+ import { Div, BaseListingCard, Button, GatedPrice, RichText, Row, Span, Stack, Text, TextLink } from "../../../ui/index.js";
18
18
  import { normalizeRichTextHtml } from "../../../utils/string.formatter.js";
19
19
  import { PreorderBadge } from "./PreorderCard.js";
20
20
  import { WishlistHeartButton } from "../../wishlist/components/WishlistHeartButton.js";
@@ -34,7 +34,7 @@ function resolveHref(product, href, hrefBuilder) {
34
34
  return hrefBuilder(product);
35
35
  return ROUTES.PUBLIC.PRE_ORDER_DETAIL(product.slug ?? product.id);
36
36
  }
37
- export function MarketplacePreorderCard({ product, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, inWishlist: initialInWishlist = false, href, hrefBuilder, onNavigate, onAddToCart, wishlistActions, labels, }) {
37
+ export function MarketplacePreorderCard({ product, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, inWishlist: initialInWishlist = false, href, hrefBuilder, onNavigate, onAddToCart, wishlistActions, labels, renderOwnerActions, }) {
38
38
  const router = useRouter();
39
39
  const mergedLabels = { ...DEFAULT_LABELS, ...labels };
40
40
  const detailHref = resolveHref(product, href, hrefBuilder);
@@ -61,7 +61,7 @@ export function MarketplacePreorderCard({ product, className = "", variant = "gr
61
61
  void normalizeError(_err);
62
62
  }
63
63
  }, [toggle]);
64
- return (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: detailHref, className: "absolute inset-0 block", children: _jsx(MediaImage, { src: product.mainImage, alt: product.title, size: "card", fallback: "Product image" }) }), _jsxs(Stack, { className: "absolute right-2 top-2", align: "end", gap: "xs", children: [_jsx(Span, { layout: "inline-flex", color: "inverse", size: "xs", weight: "medium", className: "bg-cobalt", rounded: "full", padding: "pill-xs", children: mergedLabels.preOrderBadge }), product.featured && (_jsx(Span, { size: "xs", weight: "medium", className: CLS_PREORDER_BADGE, children: mergedLabels.featuredBadge }))] }), _jsxs(Stack, { className: "absolute left-2 top-2 pointer-events-none", gap: "xs", children: [product.partOfBundleIds && product.partOfBundleIds.length > 0 && (_jsx(Span, { className: CLS_SALE_BADGE, title: product.partOfBundleTitles?.[0] ? `In bundle: ${product.partOfBundleTitles[0]}` : "In a bundle", children: "Bundled" })), product.groupId && (_jsx(Span, { className: CLS_TRENDING_BADGE, title: product.groupTitle ? `Part of set: ${product.groupTitle}` : "Part of a set", children: product.isGroupParent ? "Set Parent" : "In Set" })), product.sublistingCategoryId && (_jsx(Span, { className: CLS_LIMITED_BADGE, title: "Has variants or sub-listings", children: "Has Variants" }))] }), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
64
+ const card = (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: detailHref, className: "absolute inset-0 block", children: _jsx(MediaImage, { src: product.mainImage, alt: product.title, size: "card", fallback: "Product image" }) }), _jsxs(Stack, { className: "absolute right-2 top-2", align: "end", gap: "xs", children: [_jsx(Span, { layout: "inline-flex", color: "inverse", size: "xs", weight: "medium", className: "bg-cobalt", rounded: "full", padding: "pill-xs", children: mergedLabels.preOrderBadge }), product.featured && (_jsx(Span, { size: "xs", weight: "medium", className: CLS_PREORDER_BADGE, children: mergedLabels.featuredBadge }))] }), _jsxs(Stack, { className: "absolute left-2 top-2 pointer-events-none", gap: "xs", children: [product.partOfBundleIds && product.partOfBundleIds.length > 0 && (_jsx(Span, { className: CLS_SALE_BADGE, title: product.partOfBundleTitles?.[0] ? `In bundle: ${product.partOfBundleTitles[0]}` : "In a bundle", children: "Bundled" })), product.groupId && (_jsx(Span, { className: CLS_TRENDING_BADGE, title: product.groupTitle ? `Part of set: ${product.groupTitle}` : "Part of a set", children: product.isGroupParent ? "Set Parent" : "In Set" })), product.sublistingCategoryId && (_jsx(Span, { className: CLS_LIMITED_BADGE, title: "Has variants or sub-listings", children: "Has Variants" }))] }), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
65
65
  event.stopPropagation();
66
66
  onSelect(product.id, !isSelected);
67
67
  }, className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsx(BaseListingCard.Info, { variant: variant, children: variant === "list" ? (
@@ -69,4 +69,11 @@ export function MarketplacePreorderCard({ product, className = "", variant = "gr
69
69
  _jsxs(_Fragment, { children: [_jsxs(Row, { className: "min-w-0", align: "start", justify: "between", gap: "sm", children: [_jsx(TextLink, { href: detailHref, className: "min-w-0 flex-1", children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), wishlistActions && (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist }))] }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Text, { className: "text-primary", size: "sm", weight: "bold", children: _jsx(GatedPrice, { children: formatCurrency(product.price, getDefaultCurrency()) }) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "self-start mt-0.5", onClick: handleNavigate, children: mergedLabels.reserveNow })] })) : (
70
70
  /* ── Full grid layout ── */
71
71
  _jsxs(_Fragment, { children: [_jsx(TextLink, { href: detailHref, children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), product.description ? (_jsx(RichText, { html: normalizeRichTextHtml(product.description), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)]" })) : null, _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: _jsx(GatedPrice, { children: formatCurrency(product.price, getDefaultCurrency()) }) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsxs(Row, { justify: "between", className: "mt-auto pt-[var(--appkit-space-2)]", gap: "sm", children: [_jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "flex-1", onClick: handleNavigate, children: mergedLabels.reserveNow }), onAddToCart ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", textSize: "xs", className: "flex-1", onClick: (e) => { e.stopPropagation(); onAddToCart(product); }, children: mergedLabels.addToCart })) : null, wishlistActions ? (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist })) : null] })] })) })] }));
72
+ if (!renderOwnerActions)
73
+ return card;
74
+ /*
75
+ * Sibling of the card, never a child — see the prop's docstring. `z-20`
76
+ * clears this card's own overlays, which all sit at `z-10`.
77
+ */
78
+ return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 bottom-2 z-20", children: renderOwnerActions() })] }));
72
79
  }
@@ -13,6 +13,7 @@ import { NextResponse } from "next/server.js";
13
13
  import { z } from "zod";
14
14
  import { getProviders } from "../../../../contracts/index.js";
15
15
  import { createRouteHandler } from "../../../../next/api/routeHandler.js";
16
+ import { ALL_LISTING_TYPES } from "../../../../_internal/shared/listing-types/feature-flags.js";
16
17
  import { mediaFieldSchema } from "../../../media/types/index.js";
17
18
  import { storeRepository } from "../../../stores/repository/store.repository.js";
18
19
  // SB-UNI-V — bundlesRepository deleted; bundle stock-sync moves to the
@@ -55,8 +56,14 @@ const productUpdateSchema = z
55
56
  isPromoted: z.boolean().optional(),
56
57
  allowShipBeforeEmiComplete: z.boolean().optional(),
57
58
  printMeta: printMetaSchema.optional(),
59
+ /*
60
+ * 🛑 DERIVED from `ALL_LISTING_TYPES`, never restated. This literal was
61
+ * wrong in both directions: `spares` missing (so the type could not be
62
+ * PATCHed onto a listing at all) and `bundle` still present (so a caller
63
+ * could write a value no registry has a branch for). Root Cause #61.
64
+ */
58
65
  listingType: z
59
- .enum(["standard", "auction", "pre-order", "prize-draw", "bundle", "classified", "digital-code", "live", "art", "stickers"])
66
+ .enum(ALL_LISTING_TYPES)
60
67
  .optional(),
61
68
  media: z.array(mediaFieldSchema).optional(),
62
69
  })
@@ -16,6 +16,7 @@ import { NextResponse } from "next/server.js";
16
16
  import { z } from "zod";
17
17
  import { getProviders } from "../../../contracts/index.js";
18
18
  import { createRouteHandler } from "../../../next/api/routeHandler.js";
19
+ import { ALL_LISTING_TYPES } from "../../../_internal/shared/listing-types/feature-flags.js";
19
20
  import { mediaFieldSchema } from "../../media/types/index.js";
20
21
  import { printMetaSchema } from "../schemas/index.js";
21
22
  import { sanitizeProductsForPublic } from "../utils/sanitize.js";
@@ -50,11 +51,22 @@ const productMutateSchema = z
50
51
  sellerName: z.string().optional(),
51
52
  sellerEmail: z.string().email().optional(),
52
53
  slug: z.string().optional(),
53
- // `bundle` is deliberately absent: SB-UNI-D moved bundles onto
54
- // `categories` as a categoryType, so a product written with that value
55
- // would match no listing-type query anywhere.
54
+ /*
55
+ * 🛑 DERIVED from `ALL_LISTING_TYPES`, never restated.
56
+ *
57
+ * The literal that used to sit here is the cautionary tale for why. Its
58
+ * comment correctly explained that `bundle` is absent because SB-UNI-D
59
+ * moved bundles onto `categories` — so the dead value had been noticed and
60
+ * removed — and the list was STILL wrong, because `spares` was added to the
61
+ * union on 2026-10-19 and never reached here. A fix for one half of the
62
+ * drift, documented in a comment, with the other half drifting one field
63
+ * over underneath it.
64
+ *
65
+ * Deriving removes both failure modes at once: a dead value cannot survive
66
+ * and a new member cannot be forgotten. Root Cause #61.
67
+ */
56
68
  listingType: z
57
- .enum(["standard", "auction", "pre-order", "prize-draw", "classified", "digital-code", "live", "art", "stickers"])
69
+ .enum(ALL_LISTING_TYPES)
58
70
  .optional(),
59
71
  media: z.array(mediaFieldSchema).optional(),
60
72
  })
@@ -21,7 +21,7 @@ export function AvailabilityScopeHint({ total, available, types, }) {
21
21
  // availabilityTabsFor()[1] is the unavailable scope — "Sold", "Ended" or
22
22
  // "Sold & Ended", already derived per type.
23
23
  const noun = (availabilityTabsFor(types)[1]?.label ?? "Sold").toLowerCase();
24
- return (_jsxs(Row, { align: "center", gap: "sm", padding: "sm", surface: "muted", rounded: "lg", border: "subtle", className: "mb-4 flex-wrap", children: [_jsx(Text, { size: "sm", color: "muted", children: hidden === 1
24
+ return (_jsxs(Row, { align: "center", gap: "sm", padding: "sm", surface: "muted", rounded: "lg", border: "subtle", wrap: true, className: "mb-4", children: [_jsx(Text, { size: "sm", color: "muted", children: hidden === 1
25
25
  ? `1 listing here is ${noun} and hidden by the Available filter.`
26
26
  : `${hidden} listings here are ${noun} and hidden by the Available filter.` }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => table.set(TABLE_KEYS.AVAILABILITY, AVAILABILITY_VALUES.ALL), children: "Show all" })] }));
27
27
  }
@@ -14,5 +14,11 @@ export interface InteractiveProductCardProps {
14
14
  onToggleWishlist?: (productId: string) => void;
15
15
  onAddToCart?: (product: ProductItem) => void;
16
16
  onBuyNow?: (product: ProductItem) => void;
17
+ /**
18
+ * Owner-only pencil / clone / bin. Forwarded to ProductCard, which renders
19
+ * it OUTSIDE its own and this component's `<Link>` wrappers — see its return
20
+ * statement. Renders nothing for a visitor who does not own the listing.
21
+ */
22
+ renderOwnerActions?: () => React.ReactNode;
17
23
  }
18
- export declare function InteractiveProductCard({ product, href, className, selectable, isSelected, onSelect, isWishlisted, onToggleWishlist, onAddToCart, onBuyNow, }: InteractiveProductCardProps): React.JSX.Element;
24
+ export declare function InteractiveProductCard({ product, href, className, selectable, isSelected, onSelect, isWishlisted, onToggleWishlist, onAddToCart, onBuyNow, renderOwnerActions, }: InteractiveProductCardProps): React.JSX.Element;
@@ -1,7 +1,8 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import Link from "next/link.js";
3
3
  import { ProductCard } from "./ProductGrid.js";
4
- export function InteractiveProductCard({ product, href, className, selectable, isSelected, onSelect, isWishlisted = false, onToggleWishlist, onAddToCart, onBuyNow, }) {
4
+ import { Div } from "../../../ui/components/Div.js";
5
+ export function InteractiveProductCard({ product, href, className, selectable, isSelected, onSelect, isWishlisted = false, onToggleWishlist, onAddToCart, onBuyNow, renderOwnerActions, }) {
5
6
  // When selection is enabled, render ProductCard directly so its built-in
6
7
  // checkbox + long-press (BaseListingCard.Checkbox) work. `href` is still
7
8
  // forwarded here (not just in the plain-Link branch below) — ProductCard's
@@ -12,7 +13,18 @@ export function InteractiveProductCard({ product, href, className, selectable, i
12
13
  // `onSelect` is provided at all (see Root Cause Pattern: selectable-card
13
14
  // navigation must not depend on a selection callback being present).
14
15
  if (onSelect) {
15
- return (_jsx(ProductCard, { product: product, href: href, className: className, isWishlisted: isWishlisted, onAddToWishlist: onToggleWishlist, onAddToCart: onAddToCart, onBuyNow: onBuyNow, selectionMode: selectable, isSelected: isSelected, onSelect: (id) => onSelect(id, !isSelected) }));
16
+ return (_jsx(ProductCard, { product: product, href: href, className: className, isWishlisted: isWishlisted, onAddToWishlist: onToggleWishlist, onAddToCart: onAddToCart, onBuyNow: onBuyNow, selectionMode: selectable, isSelected: isSelected, onSelect: (id) => onSelect(id, !isSelected), renderOwnerActions: renderOwnerActions }));
16
17
  }
17
- return (_jsx(Link, { href: href, className: "block", children: _jsx(ProductCard, { product: product, className: className, isWishlisted: isWishlisted, onAddToWishlist: onToggleWishlist, onAddToCart: onAddToCart, onBuyNow: onBuyNow }) }));
18
+ /*
19
+ * 🛑 The owner toolbar is a SIBLING of this Link, not a child of it.
20
+ *
21
+ * ProductCard already keeps the toolbar outside its OWN anchor, but this
22
+ * branch adds a second one around the whole card — so handing the slot down
23
+ * would still nest buttons inside an `<a>`. Rendering it here, over a
24
+ * relative wrapper, is the only placement that escapes both.
25
+ */
26
+ const card = (_jsx(Link, { href: href, className: "block", children: _jsx(ProductCard, { product: product, className: className, isWishlisted: isWishlisted, onAddToWishlist: onToggleWishlist, onAddToCart: onAddToCart, onBuyNow: onBuyNow }) }));
27
+ if (!renderOwnerActions)
28
+ return card;
29
+ return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-14 z-20", children: renderOwnerActions() })] }));
18
30
  }
@@ -17,5 +17,15 @@ export interface MarketplaceBundleCardProps {
17
17
  hrefBuilder?: (bundle: MarketplaceBundleCardData) => string;
18
18
  onNavigate?: (href: string) => void;
19
19
  labels?: MarketplaceBundleCardLabels;
20
+ /**
21
+ * Owner-only pencil / clone / bin. Pass `<OwnerQuickActions>`; it renders
22
+ * nothing for a visitor who does not own the record.
23
+ *
24
+ * 🛑 Rendered OUTSIDE this card's own anchor, as a sibling. A `<button>`
25
+ * inside an `<a>` is invalid HTML that browsers resolve by closing the
26
+ * anchor early, which breaks the CARD's navigation rather than just the
27
+ * button's — `stopPropagation` stops a click but cannot fix a parse.
28
+ */
29
+ renderOwnerActions?: () => React.ReactNode;
20
30
  }
21
- export declare function MarketplaceBundleCard({ bundle, className, variant, selectable, isSelected, onSelect, href, hrefBuilder, onNavigate, labels, }: MarketplaceBundleCardProps): import("react").JSX.Element;
31
+ export declare function MarketplaceBundleCard({ bundle, className, variant, selectable, isSelected, onSelect, href, hrefBuilder, onNavigate, labels, renderOwnerActions, }: MarketplaceBundleCardProps): import("react").JSX.Element;
@@ -27,7 +27,7 @@ function resolveHref(bundle, href, hrefBuilder) {
27
27
  return hrefBuilder(bundle);
28
28
  return ROUTES.PUBLIC.BUNDLE_DETAIL(bundle.slug);
29
29
  }
30
- export function MarketplaceBundleCard({ bundle, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, href, hrefBuilder, onNavigate, labels, }) {
30
+ export function MarketplaceBundleCard({ bundle, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, href, hrefBuilder, onNavigate, labels, renderOwnerActions, }) {
31
31
  const router = useRouter();
32
32
  const mergedLabels = { ...DEFAULT_LABELS, ...labels };
33
33
  const detailHref = resolveHref(bundle, href, hrefBuilder);
@@ -49,7 +49,7 @@ export function MarketplaceBundleCard({ bundle, className = "", variant = "grid"
49
49
  }
50
50
  router.push(String(detailHref));
51
51
  }, [detailHref, onNavigate, router]);
52
- return (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: String(detailHref), className: "absolute inset-0 block", children: showCollage ? (_jsx(Div, { layout: "grid", className: `h-full w-full gap-[0.125rem] bg-[var(--appkit-color-surface-muted)] ${collageTiles.length === 2 ? "grid-cols-2 grid-rows-1" : "grid-cols-2 grid-rows-2"}`, children: collageTiles.map((tile, i) => (_jsxs(Div, { className: "relative overflow-hidden bg-[var(--appkit-color-surface-muted)]", children: [_jsx(MediaImage, { src: tile.imageURL, alt: tile.title ?? `${bundle.name} item ${i + 1}`, size: "card" }), i === collageTiles.length - 1 && overflow > 0 && (_jsxs(Row, { textWeight: "semibold", textSize: "sm", surface: "overlay-md", className: "absolute inset-0 text-white", align: "center", justify: "center", children: ["+", overflow] }))] }, `${tile.productId}-${i}`))) })) : cover ? (_jsx(Div, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", children: _jsx(MediaImage, { src: cover, alt: bundle.name, size: "card" }) })) : (_jsx(Row, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", align: "center", justify: "center", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: "No image" }) })) }), discount && (_jsx(PricesOnly, { children: _jsx(Span, { size: "xs", weight: "bold", className: "absolute left-2 top-2 bg-success-solid text-success-on-solid", rounded: "full", padding: "pill-xs", children: BUNDLE_COPY.detail.discountBadge(discount.percent) }) })), _jsxs(Stack, { className: "absolute right-2 top-2", align: "end", gap: "xs", children: [_jsx(Span, { size: "xs", weight: "medium", className: CLS_BUNDLE_PILL, children: mergedLabels.bundleBadge }), _jsx(Span, { layout: "inline-flex", color: "inverse", className: `${stock === "out_of_stock" ? "bg-neutral-500 " : CLS_STOCK_OK}`, rounded: "full", padding: "pill-xs", size: "xs", weight: "medium", children: stock === "out_of_stock" ? mergedLabels.outOfStockBadge : mergedLabels.inStockBadge })] }), stock === "out_of_stock" && (_jsx(Row, { surface: "overlay-xs", className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { color: "inverse", size: "xs", weight: "bold", className: "bg-neutral-900/80 tracking-wider", rounded: "default", padding: "pill-md", transform: "uppercase", children: mergedLabels.outOfStockBadge }) })), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
52
+ const card = (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: String(detailHref), className: "absolute inset-0 block", children: showCollage ? (_jsx(Div, { layout: "grid", className: `h-full w-full gap-[0.125rem] bg-[var(--appkit-color-surface-muted)] ${collageTiles.length === 2 ? "grid-cols-2 grid-rows-1" : "grid-cols-2 grid-rows-2"}`, children: collageTiles.map((tile, i) => (_jsxs(Div, { className: "relative overflow-hidden bg-[var(--appkit-color-surface-muted)]", children: [_jsx(MediaImage, { src: tile.imageURL, alt: tile.title ?? `${bundle.name} item ${i + 1}`, size: "card" }), i === collageTiles.length - 1 && overflow > 0 && (_jsxs(Row, { textWeight: "semibold", textSize: "sm", surface: "overlay-md", className: "absolute inset-0 text-white", align: "center", justify: "center", children: ["+", overflow] }))] }, `${tile.productId}-${i}`))) })) : cover ? (_jsx(Div, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", children: _jsx(MediaImage, { src: cover, alt: bundle.name, size: "card" }) })) : (_jsx(Row, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", align: "center", justify: "center", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: "No image" }) })) }), discount && (_jsx(PricesOnly, { children: _jsx(Span, { size: "xs", weight: "bold", className: "absolute left-2 top-2 bg-success-solid text-success-on-solid", rounded: "full", padding: "pill-xs", children: BUNDLE_COPY.detail.discountBadge(discount.percent) }) })), _jsxs(Stack, { className: "absolute right-2 top-2", align: "end", gap: "xs", children: [_jsx(Span, { size: "xs", weight: "medium", className: CLS_BUNDLE_PILL, children: mergedLabels.bundleBadge }), _jsx(Span, { layout: "inline-flex", color: "inverse", className: `${stock === "out_of_stock" ? "bg-neutral-500 " : CLS_STOCK_OK}`, rounded: "full", padding: "pill-xs", size: "xs", weight: "medium", children: stock === "out_of_stock" ? mergedLabels.outOfStockBadge : mergedLabels.inStockBadge })] }), stock === "out_of_stock" && (_jsx(Row, { surface: "overlay-xs", className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { color: "inverse", size: "xs", weight: "bold", className: "bg-neutral-900/80 tracking-wider", rounded: "default", padding: "pill-md", transform: "uppercase", children: mergedLabels.outOfStockBadge }) })), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
53
53
  event.stopPropagation();
54
54
  onSelect(bundle.id, !isSelected);
55
55
  }, className: selectable || isSelected
@@ -58,4 +58,11 @@ export function MarketplaceBundleCard({ bundle, className = "", variant = "grid"
58
58
  ? formatCurrency(price, getDefaultCurrency())
59
59
  : "—" }), discount && (_jsx(Text, { size: "xs", color: "muted", className: "line-through", children: formatCurrency(discount.originalTotal, getDefaultCurrency()) }))] }) }) }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: mergedLabels.itemsLabel(memberCount) })] }), _jsx(Div, { className: CLS_VIEW_BTN, role: "button", tabIndex: 0, onClick: handleNavigate, onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ")
60
60
  handleNavigate(); }, children: "View Bundle" })] })] }));
61
+ if (!renderOwnerActions)
62
+ return card;
63
+ /*
64
+ * Sibling of the card, never a child — see the prop's docstring. `z-20`
65
+ * clears this card's own overlays, which all sit at `z-10`.
66
+ */
67
+ return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: renderOwnerActions() })] }));
61
68
  }
@@ -20,5 +20,15 @@ export interface MarketplacePrizeDrawCardProps {
20
20
  hrefBuilder?: (product: MarketplacePrizeDrawCardData) => string;
21
21
  onNavigate?: (href: string) => void;
22
22
  labels?: MarketplacePrizeDrawCardLabels;
23
+ /**
24
+ * Owner-only pencil / clone / bin. Pass `<OwnerQuickActions>`; it renders
25
+ * nothing for a visitor who does not own the record.
26
+ *
27
+ * 🛑 Rendered OUTSIDE this card's own anchor, as a sibling. A `<button>`
28
+ * inside an `<a>` is invalid HTML that browsers resolve by closing the
29
+ * anchor early, which breaks the CARD's navigation rather than just the
30
+ * button's — `stopPropagation` stops a click but cannot fix a parse.
31
+ */
32
+ renderOwnerActions?: () => React.ReactNode;
23
33
  }
24
- export declare function MarketplacePrizeDrawCard({ product, className, variant, selectable, isSelected, onSelect, href, hrefBuilder, onNavigate, labels, }: MarketplacePrizeDrawCardProps): import("react").JSX.Element;
34
+ export declare function MarketplacePrizeDrawCard({ product, className, variant, selectable, isSelected, onSelect, href, hrefBuilder, onNavigate, labels, renderOwnerActions, }: MarketplacePrizeDrawCardProps): import("react").JSX.Element;
@@ -65,7 +65,7 @@ function formatCountdown(target) {
65
65
  const minutes = Math.floor((diff % 3600000) / 60000);
66
66
  return `${hours}h ${minutes}m`;
67
67
  }
68
- export function MarketplacePrizeDrawCard({ product, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, href, hrefBuilder, onNavigate, labels, }) {
68
+ export function MarketplacePrizeDrawCard({ product, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, href, hrefBuilder, onNavigate, labels, renderOwnerActions, }) {
69
69
  const router = useRouter();
70
70
  const mergedLabels = { ...DEFAULT_LABELS, ...labels };
71
71
  const detailHref = resolveHref(product, href, hrefBuilder);
@@ -90,7 +90,7 @@ export function MarketplacePrizeDrawCard({ product, className = "", variant = "g
90
90
  }
91
91
  router.push(String(detailHref));
92
92
  }, [detailHref, onNavigate, router]);
93
- return (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: String(detailHref), className: "absolute inset-0 block", children: thumbItems.length > 0 ? (_jsx(Div, { layout: "grid", className: "grid-cols-2 grid-rows-2 gap-[0.125rem] h-full w-full bg-[var(--appkit-color-surface-muted)]", children: Array.from({ length: 4 }).map((_, i) => {
93
+ const card = (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: String(detailHref), className: "absolute inset-0 block", children: thumbItems.length > 0 ? (_jsx(Div, { layout: "grid", className: "grid-cols-2 grid-rows-2 gap-[0.125rem] h-full w-full bg-[var(--appkit-color-surface-muted)]", children: Array.from({ length: 4 }).map((_, i) => {
94
94
  const it = thumbItems[i];
95
95
  const img = it?.images?.[0];
96
96
  return (_jsx(Div, { className: "relative overflow-hidden bg-[var(--appkit-color-surface-muted)]", children: img ? (_jsx(MediaImage, { src: img, alt: it?.title ?? `Prize ${i + 1}`, size: "card" })) : null }, `thumb-${i}`));
@@ -100,4 +100,11 @@ export function MarketplacePrizeDrawCard({ product, className = "", variant = "g
100
100
  }, className: selectable || isSelected
101
101
  ? "opacity-100"
102
102
  : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsxs(BaseListingCard.Info, { variant: variant, children: [_jsx(TextLink, { href: String(detailHref), children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: _jsxs(GatedPrice, { label: "Sign in to see the entry price", children: [formatCurrency(pricePerEntry, getDefaultCurrency()), " ", _jsx(Span, { size: "xs", weight: "normal", className: "text-[var(--appkit-color-text-muted)]", children: mergedLabels.pricePerEntryLabel })] }) }), max > 0 ? (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: mergedLabels.entriesRemainingLabel(remaining, max) })) : null] }), countdown ? (_jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: [status === "pending" ? "Opens in" : "Closes in", " ", countdown] })) : null, _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "mt-2 w-full", onClick: handleNavigate, disabled: status === "closed" || remaining === 0, children: mergedLabels.enterDraw })] })] }));
103
+ if (!renderOwnerActions)
104
+ return card;
105
+ /*
106
+ * Sibling of the card, never a child — see the prop's docstring. `z-20`
107
+ * clears this card's own overlays, which all sit at `z-10`.
108
+ */
109
+ return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: renderOwnerActions() })] }));
103
110
  }
@@ -31,6 +31,9 @@ import { ctaAnchorProps } from "../../layout/cta-anchor.js";
31
31
  import { ContextualTrail } from "../../categories/components/ContextualTrail.js";
32
32
  import { productCrumbs, withTrailHead } from "../../categories/utils/breadcrumb-chain.js";
33
33
  import { getDefaultCurrency } from "../../../core/baseline-resolver.js";
34
+ import { OwnerQuickActions } from "../../admin/components/OwnerQuickActions.js";
35
+ import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
36
+ import { PurchaseReviewAffordance } from "../../reviews/components/PurchaseReviewAffordance.js";
34
37
  import { Button, Container, Div, Heading, Main, PriceDisplay, RichText, Row, Section, Span, Stack, Text, Ul, Ol, Li, SpecTable, Icon, } from "../../../ui/index.js";
35
38
  import { normalizeRichTextHtml } from "../../../utils/string.formatter.js";
36
39
  import { safeDisplayName } from "../../../security/index.js";
@@ -350,7 +353,29 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
350
353
  price: product.price,
351
354
  storeId: product.storeId,
352
355
  storeName: product.storeName,
353
- } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsx(ProductDetailView, { renderBreadcrumb: () => (_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsx(ContextualTrail, { trails: trails, alternates: { ids: categoryIds, names: categoryIdNames } }), _jsx(ShareButton, { title: title || undefined })] })), renderGallery: () => (_jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title || undefined })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [condition && (_jsx(Span, { size: "xs", weight: "medium", className: "mb-2 inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", transform: "capitalize", children: condition === "new"
356
+ } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsx(ProductDetailView, { renderBreadcrumb: () => (_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsx(ContextualTrail, { trails: trails, alternates: { ids: categoryIds, names: categoryIdNames } }), _jsx(ShareButton, { title: title || undefined })] })), renderGallery: () => (_jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title || undefined })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsx(OwnerQuickActions, { resource: "PRODUCT", placement: "detail", recordLabel: title || String(product.slug ?? product.id), storeId: product.storeId ? String(product.storeId) : undefined, editHref: String(ROUTES.STORE.PRODUCTS_EDIT(String(product.id))), onClone: async () => {
357
+ const res = await fetch(SELLER_ENDPOINTS.PRODUCT_DUPLICATE(String(product.id)), { method: "POST" });
358
+ if (!res.ok)
359
+ return;
360
+ const json = (await res.json());
361
+ const newId = json?.data?.id;
362
+ /*
363
+ * Land on the COPY's editor. The endpoint returns the new
364
+ * draft's id precisely so a one-click clone ends somewhere
365
+ * useful rather than back on the public page, where the
366
+ * draft is invisible.
367
+ */
368
+ if (newId && typeof window !== "undefined") {
369
+ window.location.assign(String(ROUTES.STORE.PRODUCTS_EDIT(newId)));
370
+ }
371
+ }, onDelete: async () => {
372
+ await fetch(SELLER_ENDPOINTS.PRODUCT_BY_ID(String(product.id)), {
373
+ method: "DELETE",
374
+ });
375
+ if (typeof window !== "undefined") {
376
+ window.location.assign(String(ROUTES.PUBLIC.PRODUCTS));
377
+ }
378
+ } }), _jsxs(Div, { children: [condition && (_jsx(Span, { size: "xs", weight: "medium", className: "mb-2 inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", transform: "capitalize", children: condition === "new"
354
379
  ? "Brand New"
355
380
  : condition === "like_new"
356
381
  ? "Like New"
@@ -421,7 +446,7 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
421
446
  ? () => (_jsx(SublistingCarouselSection, { sublistingCategoryId: sublistingCategoryId, currentListingId: String(product.id) }))
422
447
  : undefined, renderGroupSection: groupId
423
448
  ? () => (_jsx(ShowGroupSection, { groupId: groupId, currentSlug: String(p.slug ?? product.id), isParent: isGroupParent, groupTitle: groupTitle }))
424
- : undefined, renderTabs: () => (_jsx(ProductTabsShell, { partsContent: compositionContent, 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: allSpecs.length > 0 ? (_jsx(SpecTable, { rows: allSpecs })) : undefined, ingredientsContent: ingredients.length > 0 ? (_jsx(Ul, { spacing: "comfortable", children: ingredients.map((item, i) => (_jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-1 flex-shrink-0 h-1.5 w-1.5 bg-primary-400", rounded: "full" }), item] }, i))) })) : undefined, howToUseContent: howToUse.length > 0 ? (_jsx(Ol, { spacing: "loose", children: howToUse.map((step, i) => (_jsxs(Li, { layout: "flex-start", gap: "3", textSize: "sm", color: "muted", children: [_jsx(Span, { layout: "flex-center", size: "xs", weight: "bold", className: "flex-shrink-0 h-6 w-6 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300", rounded: "full", children: i + 1 }), step] }, 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: toCustomSectionTabs(p.customSections) })), renderBundleSection: bundleMemberships.length > 0
449
+ : undefined, renderTabs: () => (_jsx(ProductTabsShell, { partsContent: compositionContent, 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: allSpecs.length > 0 ? (_jsx(SpecTable, { rows: allSpecs })) : undefined, ingredientsContent: ingredients.length > 0 ? (_jsx(Ul, { spacing: "comfortable", children: ingredients.map((item, i) => (_jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-1 flex-shrink-0 h-1.5 w-1.5 bg-primary-400", rounded: "full" }), item] }, i))) })) : undefined, howToUseContent: howToUse.length > 0 ? (_jsx(Ol, { spacing: "loose", children: howToUse.map((step, i) => (_jsxs(Li, { layout: "flex-start", gap: "3", textSize: "sm", color: "muted", children: [_jsx(Span, { layout: "flex-center", size: "xs", weight: "bold", className: "flex-shrink-0 h-6 w-6 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300", rounded: "full", children: i + 1 }), step] }, i))) })) : undefined, reviewsContent: _jsxs(Stack, { gap: "md", children: [_jsx(PurchaseReviewAffordance, { productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined }), _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: toCustomSectionTabs(p.customSections) })), renderBundleSection: bundleMemberships.length > 0
425
450
  ? () => (_jsxs(Div, { className: CLS_BUNDLE_BOX, children: [_jsxs(Row, { className: "mb-3", align: "center", gap: "sm", children: [_jsx(Span, { size: "base", className: CLS_BUNDLE_ICON, "aria-hidden": "true", children: "\uD83D\uDCE6" }), _jsxs(Text, { className: CLS_BUNDLE_TITLE, children: ["Part of ", bundleMemberships.length === 1 ? "a bundle" : `${bundleMemberships.length} bundles`] })] }), _jsx(Stack, { gap: "3", children: bundleMemberships.map((b) => (_jsxs(Div, { className: CLS_BUNDLE_ROW, children: [_jsxs(Div, { className: "min-w-0", children: [_jsx(Text, { className: CLS_BUNDLE_LABEL, children: "Included in bundle" }), _jsx(Text, { className: "truncate", color: "primary", size: "sm", weight: "semibold", children: b.title })] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLE_DETAIL(b.id)), className: CLS_BUNDLE_CTA, children: "View Bundle \u2192" })] }, b.id))) })] }))
426
451
  : undefined, renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryNames[0] || categoryName || category || undefined, brandLabel: brand || undefined, storeLabel: storeName || undefined })] })) }), renderPrimaryActions
427
452
  ? renderPrimaryActions({
@@ -16,8 +16,21 @@ interface ProductCardProps<T extends ProductItem = ProductItem> {
16
16
  selectionMode?: boolean;
17
17
  isSelected?: boolean;
18
18
  onSelect?: (id: string) => void;
19
+ /**
20
+ * Owner-only pencil / clone / bin, for an admin or the seller who owns this
21
+ * listing. Pass `<OwnerQuickActions>`; it renders nothing for anyone else.
22
+ *
23
+ * 🛑 Rendered OUTSIDE this card's `<Link>`, at `top-14 right-2` — below the
24
+ * wishlist heart and clear of the checkbox, badge stack and IconOverlay. See
25
+ * the return statement for why it cannot live inside the card body.
26
+ *
27
+ * Suppressed while a selection is active: a bulk-select gesture and a
28
+ * per-record edit are different intents, and offering both at once invites
29
+ * the wrong one.
30
+ */
31
+ renderOwnerActions?: () => React.ReactNode;
19
32
  }
20
- export declare function ProductCard<T extends ProductItem = ProductItem>({ product, href, onClick, onAddToWishlist, isWishlisted, onAddToCart, onBuyNow, className, selectionMode, isSelected, onSelect, }: ProductCardProps<T>): React.JSX.Element;
33
+ export declare function ProductCard<T extends ProductItem = ProductItem>({ product, href, onClick, onAddToWishlist, isWishlisted, onAddToCart, onBuyNow, className, selectionMode, isSelected, onSelect, renderOwnerActions, }: ProductCardProps<T>): React.JSX.Element;
21
34
  export interface ProductCardContext<T extends ProductItem = ProductItem> {
22
35
  onClick?: (product: T) => void;
23
36
  onWishlistToggle?: (productId: string) => void;
@@ -45,7 +45,7 @@ const CLS_DISCOUNT_TEXT = "mt-1 text-[11px] font-semibold text-error";
45
45
  const CLS_DISCOUNT_TEXT_BARE = "text-[10px] text-error";
46
46
  const CLS_STAR_BARE = "text-warning";
47
47
  const CLS_BID_TEXT = "text-[11px] text-error";
48
- export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlisted, onAddToCart, onBuyNow, className = "", selectionMode = false, isSelected = false, onSelect, }) {
48
+ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlisted, onAddToCart, onBuyNow, className = "", selectionMode = false, isSelected = false, onSelect, renderOwnerActions, }) {
49
49
  const discount = product.originalPrice && product.originalPrice > product.price
50
50
  ? Math.round(((product.originalPrice - product.price) / product.originalPrice) *
51
51
  100)
@@ -124,10 +124,27 @@ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlis
124
124
  e.preventDefault();
125
125
  onAddToCart(product);
126
126
  }, className: "justify-center text-[length:var(--appkit-text-xs)]", children: [_jsx("svg", { className: "h-3.5 w-3.5 flex-shrink-0", fill: "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 3h2l.4 2M7 13h10l4-8H5.4m1.6 8L5 3H3m4 10v7a1 1 0 001 1h8a1 1 0 001-1v-7M9 21h6" }) }), "Cart"] }))] }))] })] })] }));
127
- if (href && !selectionMode) {
128
- return (_jsx(Link, { href: href, className: "block h-full", children: cardBody }));
129
- }
130
- return cardBody;
127
+ const wrapped = href && !selectionMode ? (_jsx(Link, { href: href, className: "block h-full", children: cardBody })) : (cardBody);
128
+ if (!renderOwnerActions || selectionMode || isSelected)
129
+ return wrapped;
130
+ /*
131
+ * 🛑 OUTSIDE the anchor, as a sibling — never inside `cardBody`.
132
+ *
133
+ * This card wraps its entire body in a `<Link>` whenever `href` is set, and
134
+ * `InteractiveProductCard` wraps it in another one. A `<button>` inside an
135
+ * `<a>` is invalid HTML that browsers resolve by closing the anchor early,
136
+ * which breaks the CARD's navigation rather than just the button's —
137
+ * stopPropagation stops the click but cannot fix the parse.
138
+ *
139
+ * Positioned `top-14 right-2`: directly below the 44px wishlist heart at
140
+ * `top-2 right-2`, so it clears every occupied corner (checkbox and badges
141
+ * top-left, IconOverlay bottom-right) without needing to know the hero's
142
+ * height. The hero is `aspect-square`, so it is always taller than
143
+ * 3.5rem + 44px and the toolbar lands on the photo.
144
+ *
145
+ * `z-20` beats the in-card overlays, which all sit at `z-10`.
146
+ */
147
+ return (_jsxs(Div, { className: "relative h-full", children: [wrapped, _jsx(Div, { className: "absolute right-2 top-14 z-20", children: renderOwnerActions() })] }));
131
148
  }
132
149
  function ProductListRow({ product, onClick, onAddToWishlist, isWishlisted, }) {
133
150
  const discount = product.originalPrice && product.originalPrice > product.price
@@ -221,9 +221,9 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
221
221
  itemCount: items.reduce((sum, it) => sum + it.quantity, 0),
222
222
  totalValue: items.reduce((sum, it) => sum + (it.price ?? 0) * it.quantity, 0),
223
223
  };
224
- showToast(formatCartAddedMessage(detail, (amount) => formatCurrency(amount, getDefaultCurrency())), "success");
224
+ showToast(formatCartAddedMessage(detail, (amount) => formatCurrency(amount, getDefaultCurrency()), canSeePrices), "success");
225
225
  dispatchCartUpdated(detail);
226
- }, [localCart, showToast]);
226
+ }, [localCart, showToast, canSeePrices]);
227
227
  const handleBuyNow = useCallback((product) => {
228
228
  requireAuth(ACTION_ID.BUY_NOW, () => {
229
229
  const snapshot = {