@mohasinac/appkit 4.26.0 → 4.28.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 (277) hide show
  1. package/dist/_internal/client/features/lottery/LotteryAdminEditView.d.ts +16 -16
  2. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +29 -6
  3. package/dist/_internal/client/features/lottery/LotteryListView.js +13 -1
  4. package/dist/_internal/server/features/bundles/data.d.ts +19 -0
  5. package/dist/_internal/server/features/bundles/data.js +32 -0
  6. package/dist/_internal/server/features/bundles/index.d.ts +1 -1
  7. package/dist/_internal/server/features/bundles/index.js +1 -1
  8. package/dist/_internal/server/features/checkout/actions.js +6 -4
  9. package/dist/_internal/server/features/classified/metadata.js +3 -1
  10. package/dist/_internal/server/features/digital-code/metadata.js +3 -1
  11. package/dist/_internal/server/features/live/metadata.js +3 -1
  12. package/dist/_internal/server/features/refunds/actions.js +3 -2
  13. package/dist/_internal/server/features/reviews/actions.js +3 -2
  14. package/dist/_internal/server/features/seo/sitemap.js +189 -40
  15. package/dist/_internal/server/jobs/core/listingProcessor.d.ts +0 -1
  16. package/dist/_internal/server/jobs/core/listingProcessor.js +19 -1
  17. package/dist/_internal/shared/actions/action-registry.js +14 -0
  18. package/dist/_internal/shared/checkout/order-math.d.ts +25 -3
  19. package/dist/_internal/shared/features/checkout/config.d.ts +3 -3
  20. package/dist/_internal/shared/features/checkout/config.js +6 -3
  21. package/dist/client.d.ts +32 -2
  22. package/dist/client.js +37 -2
  23. package/dist/constants/api-endpoints.d.ts +21 -3
  24. package/dist/constants/api-endpoints.js +14 -1
  25. package/dist/constants/field-names.d.ts +2 -2
  26. package/dist/constants/field-names.js +2 -2
  27. package/dist/core/newsletter.repository.js +14 -1
  28. package/dist/features/about/components/FAQPageView.js +1 -1
  29. package/dist/features/about/components/FeesView.js +1 -1
  30. package/dist/features/account/components/AddressForm.d.ts +2 -0
  31. package/dist/features/account/components/AddressForm.js +5 -1
  32. package/dist/features/account/components/UserBidsView.js +1 -1
  33. package/dist/features/account/components/UserSidebar.js +1 -1
  34. package/dist/features/account/components/UserSupportView.js +24 -2
  35. package/dist/features/account/hooks/useAddresses.d.ts +29 -1
  36. package/dist/features/account/hooks/useAddresses.js +4 -1
  37. package/dist/features/account/hooks/useProfile.d.ts +1 -1
  38. package/dist/features/account/hooks/useProfile.js +1 -1
  39. package/dist/features/addresses/repository/addresses.repository.d.ts +13 -0
  40. package/dist/features/addresses/repository/addresses.repository.js +19 -0
  41. package/dist/features/admin/actions/chat-actions.js +5 -1
  42. package/dist/features/admin/components/AdminAdsView.js +1 -1
  43. package/dist/features/admin/components/AdminBidsView.js +6 -1
  44. package/dist/features/admin/components/AdminCarouselGroupEditorView.d.ts +9 -1
  45. package/dist/features/admin/components/AdminCarouselGroupEditorView.js +42 -10
  46. package/dist/features/admin/components/AdminCarouselView.js +1 -1
  47. package/dist/features/admin/components/AdminFeatureEditorView.js +1 -1
  48. package/dist/features/admin/components/AdminGroupedListingsView.js +14 -2
  49. package/dist/features/admin/components/AdminSidebar.js +1 -1
  50. package/dist/features/admin/components/AdminViewCards.js +2 -2
  51. package/dist/features/admin/components/DataTable.js +2 -2
  52. package/dist/features/admin/schemas/firestore.js +9 -3
  53. package/dist/features/auctions/actions/bid-actions.js +45 -4
  54. package/dist/features/auctions/components/AuctionBottomActions.d.ts +21 -6
  55. package/dist/features/auctions/components/AuctionBottomActions.js +42 -11
  56. package/dist/features/auctions/components/AuctionDetailPageView.d.ts +8 -3
  57. package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
  58. package/dist/features/auctions/components/MarketplaceAuctionCard.js +4 -5
  59. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +27 -2
  60. package/dist/features/auctions/components/PlaceBidFormClient.js +42 -20
  61. package/dist/features/auth/components/SocialAuthButtons.js +1 -1
  62. package/dist/features/auth/hooks/useAuth.d.ts +23 -1
  63. package/dist/features/auth/hooks/useAuth.js +27 -1
  64. package/dist/features/auth/permissions/constants.d.ts +1 -1
  65. package/dist/features/auth/permissions/constants.js +3 -0
  66. package/dist/features/auth/repository/session.repository.d.ts +16 -1
  67. package/dist/features/auth/repository/session.repository.js +23 -0
  68. package/dist/features/auth/repository/user.repository.js +13 -2
  69. package/dist/features/blog/components/BlogListView.js +2 -2
  70. package/dist/features/cart/components/CartPriceBreakdown.d.ts +11 -1
  71. package/dist/features/cart/components/CartPriceBreakdown.js +3 -3
  72. package/dist/features/cart/components/StoreAddonsPicker.d.ts +9 -0
  73. package/dist/features/cart/components/StoreAddonsPicker.js +15 -5
  74. package/dist/features/cart/components/index.d.ts +1 -1
  75. package/dist/features/cart/components/index.js +1 -1
  76. package/dist/features/cart/hooks/useGuestCartMerge.d.ts +8 -1
  77. package/dist/features/cart/hooks/useGuestCartMerge.js +2 -1
  78. package/dist/features/cart/utils/line-total.d.ts +25 -0
  79. package/dist/features/cart/utils/line-total.js +26 -0
  80. package/dist/features/catalogue/repository/catalogue.repository.d.ts +12 -0
  81. package/dist/features/catalogue/repository/catalogue.repository.js +15 -0
  82. package/dist/features/categories/components/CategoryTree.js +2 -2
  83. package/dist/features/contact/components/ContactForm.js +1 -1
  84. package/dist/features/copilot/hooks/useCopilotFeedback.d.ts +1 -1
  85. package/dist/features/copilot/hooks/useCopilotFeedback.js +1 -1
  86. package/dist/features/events/components/AdminEventEditorView.js +16 -9
  87. package/dist/features/events/components/AdminEventEntriesView.js +7 -13
  88. package/dist/features/events/components/EventRafflesSection.js +14 -2
  89. package/dist/features/events/components/EventStatusBadge.js +1 -1
  90. package/dist/features/events/components/event-entry-detail-fields.d.ts +24 -0
  91. package/dist/features/events/components/event-entry-detail-fields.js +13 -0
  92. package/dist/features/faq/components/ContactCTA.js +1 -1
  93. package/dist/features/faq/components/FAQHelpfulButtons.js +2 -2
  94. package/dist/features/faq/repository/faqs.repository.d.ts +16 -1
  95. package/dist/features/faq/repository/faqs.repository.js +64 -54
  96. package/dist/features/faq/schemas/firestore.d.ts +3 -2
  97. package/dist/features/faq/schemas/firestore.js +2 -2
  98. package/dist/features/faq/schemas/index.d.ts +3 -3
  99. package/dist/features/faq/schemas/index.js +1 -1
  100. package/dist/features/filters/FilterFacetSection.js +2 -2
  101. package/dist/features/forms/Toggle.js +1 -1
  102. package/dist/features/grouped/components/GroupedListingEditorView.d.ts +18 -0
  103. package/dist/features/grouped/components/GroupedListingEditorView.js +192 -0
  104. package/dist/features/homepage/components/AdvertisementBanner.js +2 -2
  105. package/dist/features/homepage/components/BrandsSection.js +1 -1
  106. package/dist/features/homepage/components/CTABannerSection.js +1 -1
  107. package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +1 -1
  108. package/dist/features/homepage/components/HomepageSectionBoundary.d.ts +39 -0
  109. package/dist/features/homepage/components/HomepageSectionBoundary.js +49 -0
  110. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  111. package/dist/features/homepage/components/WelcomeSection.js +1 -1
  112. package/dist/features/homepage/lib/section-renderer.js +2 -1
  113. package/dist/features/layout/AppLayoutShell.js +5 -5
  114. package/dist/features/layout/BottomActions.js +5 -5
  115. package/dist/features/layout/BottomSheet.js +1 -1
  116. package/dist/features/layout/MainNavbar.js +1 -1
  117. package/dist/features/layout/NavbarLayout.js +1 -1
  118. package/dist/features/layout/TitleBarLayout.js +3 -3
  119. package/dist/features/layout/hooks/useFormBottomActions.d.ts +11 -1
  120. package/dist/features/layout/hooks/useFormBottomActions.js +16 -2
  121. package/dist/features/lottery/schemas/config-write.d.ts +141 -0
  122. package/dist/features/lottery/schemas/config-write.js +139 -0
  123. package/dist/features/media/MediaPickerModal.js +3 -1
  124. package/dist/features/media/MediaVideo.js +4 -2
  125. package/dist/features/media/components/MediaSlider.js +1 -1
  126. package/dist/features/orders/components/OrdersList.js +2 -2
  127. package/dist/features/payments/repository/payout.repository.d.ts +11 -1
  128. package/dist/features/payments/repository/payout.repository.js +14 -6
  129. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -2
  130. package/dist/features/pre-orders/components/PreOrderBadge.js +1 -1
  131. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -1
  132. package/dist/features/products/columns/productTableColumns.js +3 -3
  133. package/dist/features/products/components/CompareOverlay.js +1 -1
  134. package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
  135. package/dist/features/products/components/PrizeDrawEntryActions.js +2 -2
  136. package/dist/features/products/components/PrizeDrawsSection.js +5 -1
  137. package/dist/features/products/components/ProductDetailActions.js +11 -7
  138. package/dist/features/products/components/ProductDetailPageView.js +3 -3
  139. package/dist/features/products/components/ProductFeatureBadges.js +2 -2
  140. package/dist/features/products/components/ProductFeaturesSelector.js +1 -1
  141. package/dist/features/products/components/ProductFilters.js +15 -8
  142. package/dist/features/products/components/ProductGrid.js +8 -17
  143. package/dist/features/products/components/ProductTabs.js +7 -4
  144. package/dist/features/products/components/ProductTabsShell.js +2 -7
  145. package/dist/features/products/constants/action-defs.d.ts +21 -4
  146. package/dist/features/products/constants/action-defs.js +7 -3
  147. package/dist/features/products/hooks/useRelatedProducts.js +2 -1
  148. package/dist/features/products/repository/products.repository.d.ts +1 -1
  149. package/dist/features/products/repository/products.repository.js +27 -13
  150. package/dist/features/products/schemas/firestore.d.ts +3 -2
  151. package/dist/features/products/schemas/firestore.js +1 -1
  152. package/dist/features/reviews/actions/review-actions.d.ts +8 -1
  153. package/dist/features/reviews/actions/review-actions.js +14 -3
  154. package/dist/features/reviews/components/ReviewDetailShell.js +10 -4
  155. package/dist/features/reviews/components/ReviewModal.js +1 -1
  156. package/dist/features/reviews/repository/reviews.repository.d.ts +25 -1
  157. package/dist/features/reviews/repository/reviews.repository.js +53 -13
  158. package/dist/features/reviews/schemas/firestore.d.ts +6 -0
  159. package/dist/features/scams/components/ScamRegistryView.js +2 -2
  160. package/dist/features/search/components/Search.js +3 -3
  161. package/dist/features/search/components/SearchResultsSection.js +1 -1
  162. package/dist/features/seller/components/SellerAddressesView.js +3 -1
  163. package/dist/features/seller/components/SellerBidsView.js +6 -1
  164. package/dist/features/seller/components/SellerSidebar.js +1 -1
  165. package/dist/features/shell/SectionForm.d.ts +21 -1
  166. package/dist/features/shell/SectionForm.js +15 -3
  167. package/dist/features/shell/build-sections.js +10 -1
  168. package/dist/features/shell/field-ui-meta.d.ts +21 -0
  169. package/dist/features/shipments/components/AdminShipmentLotItemsView.js +23 -6
  170. package/dist/features/site-settings/components/ActionPermissionsManager.js +1 -1
  171. package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +21 -4
  172. package/dist/features/store-extensions/repository/store-extensions.repositories.js +24 -4
  173. package/dist/features/store-extensions/schemas/shipping-config-form.js +13 -0
  174. package/dist/features/store-extensions/schemas/store-category-form.js +1 -1
  175. package/dist/features/stores/actions/store-query-actions.js +25 -1
  176. package/dist/features/stores/components/InteractiveStoreCard.js +1 -1
  177. package/dist/features/stores/components/StoreHeader.js +1 -1
  178. package/dist/features/stores/components/StoreNavTabs.js +20 -8
  179. package/dist/features/support/repository/support.repository.d.ts +12 -0
  180. package/dist/features/support/repository/support.repository.js +15 -0
  181. package/dist/features/support/schemas/ticket-create-form.d.ts +33 -0
  182. package/dist/features/support/schemas/ticket-create-form.js +82 -0
  183. package/dist/features/tester/seed-data/products-tester-seed-data.js +108 -2
  184. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +235 -13
  185. package/dist/features/wishlist/components/WishlistHeartButton.d.ts +55 -0
  186. package/dist/features/wishlist/components/WishlistHeartButton.js +52 -0
  187. package/dist/features/wishlist/components/index.d.ts +2 -1
  188. package/dist/features/wishlist/components/index.js +1 -1
  189. package/dist/index.d.ts +10 -3
  190. package/dist/index.js +19 -4
  191. package/dist/monitoring/error-tracking.js +39 -5
  192. package/dist/monitoring/server-logger.js +70 -32
  193. package/dist/next/components/ErrorView.js +1 -1
  194. package/dist/next/routing/route-map.d.ts +39 -7
  195. package/dist/next/routing/route-map.js +34 -4
  196. package/dist/providers/db-firebase/base.repository.d.ts +30 -0
  197. package/dist/providers/db-firebase/base.repository.js +56 -9
  198. package/dist/react/ErrorBoundary.js +9 -1
  199. package/dist/react/contexts/SessionContext.js +6 -2
  200. package/dist/react/hooks/useAuthGate.d.ts +22 -0
  201. package/dist/react/hooks/useAuthGate.js +86 -7
  202. package/dist/schemas/registry.d.ts +3 -3
  203. package/dist/security/index.d.ts +1 -1
  204. package/dist/security/index.js +1 -1
  205. package/dist/security/pii-encrypt.d.ts +13 -5
  206. package/dist/security/pii-encrypt.js +132 -6
  207. package/dist/security/pii-schemas.d.ts +20 -4
  208. package/dist/security/pii-schemas.js +23 -5
  209. package/dist/seed/_helpers/product-search-txt.d.ts +18 -0
  210. package/dist/seed/_helpers/product-search-txt.js +33 -0
  211. package/dist/seed/faq-seed-data.d.ts +16 -0
  212. package/dist/seed/faq-seed-data.js +22 -1
  213. package/dist/seed/products-art-seed-data.js +49 -7
  214. package/dist/seed/products-auctions-seed-data.js +12 -4
  215. package/dist/seed/products-classifieds-seed-data.d.ts +5 -0
  216. package/dist/seed/products-classifieds-seed-data.js +65 -9
  217. package/dist/seed/products-digital-codes-seed-data.d.ts +5 -0
  218. package/dist/seed/products-digital-codes-seed-data.js +65 -9
  219. package/dist/seed/products-live-items-seed-data.d.ts +5 -0
  220. package/dist/seed/products-live-items-seed-data.js +33 -5
  221. package/dist/seed/products-preorders-seed-data.d.ts +1 -1
  222. package/dist/seed/products-preorders-seed-data.js +6 -2
  223. package/dist/seed/products-prize-draws-seed-data.d.ts +5 -0
  224. package/dist/seed/products-prize-draws-seed-data.js +57 -8
  225. package/dist/seed/products-standard-seed-data.js +6 -2
  226. package/dist/seed/products-stickers-seed-data.js +49 -7
  227. package/dist/seed/site-settings-seed-data.js +5 -3
  228. package/dist/seo/json-ld.d.ts +44 -2
  229. package/dist/seo/json-ld.js +57 -11
  230. package/dist/server.d.ts +1 -1
  231. package/dist/server.js +1 -1
  232. package/dist/styles.css +368 -90
  233. package/dist/tailwind-utilities.css +1 -1
  234. package/dist/tokens/index.d.ts +1 -1
  235. package/dist/tokens/index.js +1 -1
  236. package/dist/tokens/themes/default-dark.js +6 -0
  237. package/dist/tokens/themes/default-light.js +5 -0
  238. package/dist/tokens/tokens.css +84 -0
  239. package/dist/ui/DataTable.js +1 -1
  240. package/dist/ui/DataTable.style.css +3 -3
  241. package/dist/ui/components/BulkActionBar.style.css +1 -3
  242. package/dist/ui/components/Button.js +15 -6
  243. package/dist/ui/components/Dropdown.style.css +1 -4
  244. package/dist/ui/components/FilterChipGroup.js +1 -1
  245. package/dist/ui/components/FilterDrawer.style.css +1 -1
  246. package/dist/ui/components/Icon.d.ts +53 -0
  247. package/dist/ui/components/Icon.js +41 -0
  248. package/dist/ui/components/IconButton.d.ts +13 -0
  249. package/dist/ui/components/IconButton.js +13 -0
  250. package/dist/ui/components/IconButton.style.css +18 -10
  251. package/dist/ui/components/ListingLayout.style.css +2 -10
  252. package/dist/ui/components/ListingToolbar.js +6 -6
  253. package/dist/ui/components/Modal.js +1 -1
  254. package/dist/ui/components/PaginatedSelect.style.css +3 -3
  255. package/dist/ui/components/Pagination.style.css +1 -6
  256. package/dist/ui/components/RichTextEditor.js +1 -1
  257. package/dist/ui/components/RowActionMenu.style.css +1 -1
  258. package/dist/ui/components/SideDrawer.style.css +1 -1
  259. package/dist/ui/components/Slider.js +1 -1
  260. package/dist/ui/components/TabBarShell.d.ts +78 -0
  261. package/dist/ui/components/TabBarShell.js +95 -0
  262. package/dist/ui/components/Tabs.js +12 -8
  263. package/dist/ui/components/Tabs.style.css +249 -41
  264. package/dist/ui/components/ViewToggle.style.css +1 -5
  265. package/dist/ui/components/tabs-overflow.d.ts +17 -0
  266. package/dist/ui/components/tabs-overflow.js +142 -0
  267. package/dist/ui/icons/icon-registry.d.ts +38 -0
  268. package/dist/ui/icons/icon-registry.js +37 -0
  269. package/dist/ui/index.d.ts +6 -0
  270. package/dist/ui/index.js +3 -0
  271. package/dist/utils/index.d.ts +1 -1
  272. package/dist/utils/index.js +1 -1
  273. package/dist/utils/media-url.d.ts +27 -4
  274. package/dist/utils/media-url.js +37 -6
  275. package/dist/utils/search-txt.d.ts +71 -0
  276. package/dist/utils/search-txt.js +118 -0
  277. package/package.json +1 -1
@@ -9,7 +9,7 @@ const __O = {
9
9
  hidden: "overflow-hidden",
10
10
  };
11
11
  const REL_NOOPENER = "noopener noreferrer";
12
- const CLS_REMOVE_BTN = "shrink-0 text-zinc-400 hover:bg-[var(--appkit-color-surface)] hover:text-error hover:bg-[var(--appkit-color-surface-elevated)]";
12
+ const CLS_REMOVE_BTN = "shrink-0 text-zinc-400 hover:text-error hover:bg-surface-hover";
13
13
  import Link from "next/link";
14
14
  import { X } from "lucide-react";
15
15
  import { Button, Div, Heading, IconButton, Row, Span, Stack, Text } from "../../../ui";
@@ -44,7 +44,7 @@ function statusClass(s) {
44
44
  case "open":
45
45
  return "bg-success-surface text-success";
46
46
  case "closed":
47
- return "bg-zinc-200 text-zinc-700 bg-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)]";
47
+ return " bg-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)]";
48
48
  case "pending":
49
49
  default:
50
50
  return "bg-warning-surface text-warning";
@@ -18,7 +18,7 @@ import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
18
18
  */
19
19
  export function PrizeDrawEntryActions({ productId, productSlug, title, pricePerEntry, currency, remainingEntries, revealStatus, prizeGithubFileUrl, }) {
20
20
  const router = useRouter();
21
- const { requireAuth, modalOpen, modalMessage, closeModal } = useAuthGate();
21
+ const { requireAuth, isAuthResolving, modalOpen, modalMessage, closeModal } = useAuthGate();
22
22
  const [consentOpen, setConsentOpen] = useState(false);
23
23
  const closed = revealStatus === "closed" || remainingEntries === 0;
24
24
  const handleEnter = useCallback(() => {
@@ -31,5 +31,5 @@ export function PrizeDrawEntryActions({ productId, productSlug, title, pricePerE
31
31
  const slug = productSlug ?? productId;
32
32
  router.push(`${String(ROUTES.USER.CHECKOUT)}?directItem=${encodeURIComponent(slug)}&type=prize-draw`);
33
33
  }, [productSlug, productId, router]);
34
- return (_jsxs(Stack, { gap: "md", children: [_jsxs(Text, { size: "2xl", weight: "bold", color: "primary", children: [formatCurrency(pricePerEntry, currency), _jsx(Text, { as: "span", className: "ml-1 text-[var(--appkit-color-text-muted)]", size: "xs", weight: "normal", children: "per entry" })] }), _jsx(Button, { action: closed ? undefined : ACTIONS.PRIZE_DRAW["buy-now"], variant: "primary", size: "md", className: "w-full", disabled: closed, onClick: handleEnter, children: closed ? "Draw closed" : undefined }), prizeGithubFileUrl ? (_jsx(Anchor, { href: prizeGithubFileUrl, tone: "none", underline: "hover", size: "xs", weight: "medium", className: "text-left text-primary-600 underline-offset-4 dark:text-primary-400", children: "View RNG source code on GitHub \u2192" })) : null, _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", align: "start", children: "Winners chosen by Node.js crypto.randomInt \u2014 fully auditable. Entries are locked once paid; refunds only if the prize pool is exhausted." }), _jsx(NonRefundableConsentModal, { open: consentOpen, listingType: "prize-draw", itemTitle: title, priceLabel: formatCurrency(pricePerEntry, currency), onCancel: () => setConsentOpen(false), onConfirm: handleConfirm }), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
34
+ return (_jsxs(Stack, { gap: "md", children: [_jsxs(Text, { size: "2xl", weight: "bold", color: "primary", children: [formatCurrency(pricePerEntry, currency), _jsx(Text, { as: "span", className: "ml-1 text-[var(--appkit-color-text-muted)]", size: "xs", weight: "normal", children: "per entry" })] }), _jsx(Button, { action: closed ? undefined : ACTIONS.PRIZE_DRAW["buy-now"], variant: "primary", size: "md", className: "w-full", disabled: closed || isAuthResolving, onClick: handleEnter, children: closed ? "Draw closed" : isAuthResolving ? "Checking…" : undefined }), prizeGithubFileUrl ? (_jsx(Anchor, { href: prizeGithubFileUrl, tone: "none", underline: "hover", size: "xs", weight: "medium", className: "text-left text-primary-600 underline-offset-4 dark:text-primary-400", children: "View RNG source code on GitHub \u2192" })) : null, _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", align: "start", children: "Winners chosen by Node.js crypto.randomInt \u2014 fully auditable. Entries are locked once paid; refunds only if the prize pool is exhausted." }), _jsx(NonRefundableConsentModal, { open: consentOpen, listingType: "prize-draw", itemTitle: title, priceLabel: formatCurrency(pricePerEntry, currency), onCancel: () => setConsentOpen(false), onConfirm: handleConfirm }), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
35
35
  }
@@ -1,5 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { sortBy } from "@mohasinac/appkit/client";
2
+ // Import from the DEFINING module, never `@mohasinac/appkit/client` — that entry
3
+ // is `"use client"`, so in this async Server Component `sortBy` would resolve to
4
+ // a client-reference proxy and calling it throws during the server render
5
+ // (Root Cause #18). That is exactly what took `section-prize-draws` down.
6
+ import { sortBy } from "../../../constants/sort";
3
7
  import { Container, Div, Heading, Section, Stack, Text, } from "../../../ui";
4
8
  import { ROUTES } from "../../../next";
5
9
  import { listPublicProducts } from "../../../_internal/server/features/products/list-public";
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
4
  import { useRouter } from "next/navigation";
5
- import { Button, Stack, LoginRequiredModal } from "../../../ui";
5
+ import { Button, Stack, LoginRequiredModal, Icon } from "../../../ui";
6
6
  import { useToast } from "../../../ui/components/Toast";
7
7
  import { useAddToCart } from "../../cart/hooks/useAddToCart";
8
8
  import { apiClient } from "../../../http";
@@ -22,9 +22,13 @@ import { normalizeError } from "../../../errors/normalize";
22
22
  export function ProductDetailActions({ productId, productTitle, productImage, price, currency, storeId, storeName, inStock, variant = "desktop", }) {
23
23
  const router = useRouter();
24
24
  const { showToast } = useToast();
25
- const { requireAuth, modalOpen, modalMessage, closeModal } = useAuthGate();
25
+ const { requireAuth, isAuthResolving, modalOpen, modalMessage, closeModal } = useAuthGate();
26
26
  const [busy, setBusy] = useState(null);
27
27
  const [wishlisted, setWishlisted] = useState(false);
28
+ // `isAuthResolving` covers the window where the session has not settled and a
29
+ // click would be parked by useAuthGate rather than acted on. Without it the
30
+ // button looks live, swallows the click, and appears to do nothing.
31
+ const blocked = busy !== null || isAuthResolving;
28
32
  // Success toast (item + updated count/total) is now shown by useAddToCart
29
33
  // itself — centralised so every add-to-cart call site gets the same
30
34
  // richer toast, not just this one.
@@ -98,24 +102,24 @@ export function ProductDetailActions({ productId, productTitle, productImage, pr
98
102
  actions: [
99
103
  {
100
104
  id: ACTION_ID.ADD_TO_WISHLIST,
101
- label: wishlisted ? "♥ Saved" : "♡ Wishlist",
105
+ label: wishlisted ? "Saved" : "Wishlist",
102
106
  variant: "ghost",
103
107
  grow: false,
104
- disabled: busy !== null || wishlisted,
108
+ disabled: blocked || wishlisted,
105
109
  onClick: handleWishlist,
106
110
  },
107
111
  {
108
112
  id: ACTION_ID.ADD_TO_CART,
109
113
  label: busy === "cart" ? "Adding…" : "Add to Cart",
110
114
  variant: "outline",
111
- disabled: !inStock || busy !== null,
115
+ disabled: !inStock || blocked,
112
116
  onClick: () => handleAddToCart(),
113
117
  },
114
118
  {
115
119
  id: ACTION_ID.BUY_NOW,
116
120
  label: !inStock ? "Out of Stock" : busy === "buy" ? "Loading…" : "Buy Now",
117
121
  variant: "primary",
118
- disabled: !inStock || busy !== null,
122
+ disabled: !inStock || blocked,
119
123
  onClick: handleBuyNow,
120
124
  },
121
125
  ],
@@ -132,5 +136,5 @@ export function ProductDetailActions({ productId, productTitle, productImage, pr
132
136
  if (variant === "mobile") {
133
137
  return _jsx(_Fragment, { children: authModal });
134
138
  }
135
- return (_jsxs(_Fragment, { children: [authModal, _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: !inStock || busy !== null, onClick: handleBuyNow, children: !inStock ? "Out of Stock" : busy === "buy" ? "Loading…" : "Buy Now" }), _jsx(Button, { variant: "secondary", size: "md", className: "w-full", disabled: !inStock || busy !== null, onClick: () => handleAddToCart(), children: !inStock ? "Out of Stock" : busy === "cart" ? "Adding…" : "Add to Cart" }), _jsx(Button, { variant: "ghost", size: "md", className: "w-full", disabled: busy !== null || wishlisted, onClick: handleWishlist, children: wishlisted ? "♥ In Wishlist" : "♡ Add to Wishlist" })] })] }));
139
+ return (_jsxs(_Fragment, { children: [authModal, _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: !inStock || blocked, onClick: handleBuyNow, children: !inStock ? "Out of Stock" : busy === "buy" ? "Loading…" : "Buy Now" }), _jsx(Button, { variant: "secondary", size: "md", className: "w-full", disabled: !inStock || blocked, onClick: () => handleAddToCart(), children: !inStock ? "Out of Stock" : busy === "cart" ? "Adding…" : "Add to Cart" }), _jsxs(Button, { variant: "ghost", size: "md", className: "w-full", disabled: blocked || wishlisted, onClick: handleWishlist, children: [_jsx(Icon, { name: "wishlist", size: "md", filled: wishlisted }), wishlisted ? "In Wishlist" : "Add to Wishlist"] })] })] }));
136
140
  }
@@ -22,7 +22,7 @@ const CLS_BUNDLE_LABEL = "text-[length:var(--appkit-text-xs)] text-success dark:
22
22
  const CLS_BUNDLE_CTA = "flex-shrink-0 rounded-lg bg-success-solid hover:brightness-110 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-success-on-solid transition-colors";
23
23
  import { ROUTES } from "../../../next";
24
24
  import { getDefaultCurrency } from "../../../core/baseline-resolver";
25
- import { Button, Container, Div, Heading, Main, Nav, PriceDisplay, RichText, Row, Section, Span, Stack, Text, Ul, Ol, Li, Dl, Dt, Dd, } from "../../../ui";
25
+ import { Button, Container, Div, Heading, Main, Nav, PriceDisplay, RichText, Row, Section, Span, Stack, Text, Ul, Ol, Li, Dl, Dt, Dd, Icon, } from "../../../ui";
26
26
  import { normalizeRichTextHtml } from "../../../utils/string.formatter";
27
27
  import { safeDisplayName } from "../../../security";
28
28
  import { ReviewsListingPanel } from "../../reviews/components/ReviewsListingPanel";
@@ -204,7 +204,7 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
204
204
  ? ` — only ${effectiveStock} left`
205
205
  : ""] })] })) : (_jsxs(Span, { layout: "inline-flex", gap: "xs", className: `w-fit .5 ${inStock ? CLS_STOCK_IN : CLS_STOCK_OUT}`, rounded: "full", padding: "pill-md", size: "xs", weight: "medium", children: [inStock ? "✓ In Stock" : "✗ Out of Stock", inStock && effectiveStock !== null && effectiveStock <= 10
206
206
  ? ` — only ${effectiveStock} left`
207
- : ""] })), (categorySlugs.length > 0 || subcategory || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [categorySlugs.map((slug, i) => (_jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(slug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:bg-primary-900/20 dark:hover:text-primary-400", children: categoryNames[i] ?? slug }, slug))), subcategory && (_jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", transform: "capitalize", children: subcategory })), brand && brandSlug && (_jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:bg-primary-900/20 dark:hover:text-primary-400", children: brand })), brand && !brandSlug && (_jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: brand }))] })), bundleMemberships.length > 0 && (_jsx(Row, { gap: "sm", wrap: true, children: bundleMemberships.map((b) => (_jsxs(Link, { href: String(ROUTES.PUBLIC.BUNDLE_DETAIL(b.id)), className: CLS_BUNDLE_PILL, children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDCE6" }), "In bundle: ", b.title] }, b.id))) })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, codAvailable: codEnabled === true, codHref: String(ROUTES.PUBLIC.HOW_CHECKOUT_WORKS), emiAvailable: emiEnabled === true, emiHref: String(ROUTES.PUBLIC.HOW_EMI_WORKS), condition: condition ?? undefined, returnable: returnPolicy != null && returnPolicy.length > 0, labels: {
207
+ : ""] })), (categorySlugs.length > 0 || subcategory || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [categorySlugs.map((slug, i) => (_jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(slug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: categoryNames[i] ?? slug }, slug))), subcategory && (_jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", transform: "capitalize", children: subcategory })), brand && brandSlug && (_jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: brand })), brand && !brandSlug && (_jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: brand }))] })), bundleMemberships.length > 0 && (_jsx(Row, { gap: "sm", wrap: true, children: bundleMemberships.map((b) => (_jsxs(Link, { href: String(ROUTES.PUBLIC.BUNDLE_DETAIL(b.id)), className: CLS_BUNDLE_PILL, children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDCE6" }), "In bundle: ", b.title] }, b.id))) })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, codAvailable: codEnabled === true, codHref: String(ROUTES.PUBLIC.HOW_CHECKOUT_WORKS), emiAvailable: emiEnabled === true, emiHref: String(ROUTES.PUBLIC.HOW_EMI_WORKS), condition: condition ?? undefined, returnable: returnPolicy != null && returnPolicy.length > 0, labels: {
208
208
  featured: "Featured",
209
209
  fasterDelivery: "Faster Delivery",
210
210
  ratedSeller: "Rated Seller",
@@ -232,7 +232,7 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
232
232
  storeName: typeof p.storeName === "string" ? p.storeName : undefined,
233
233
  inStock,
234
234
  variant: "desktop",
235
- })) : (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: true, title: ACTION_NOT_WIRED, children: inStock ? "Buy Now" : "Out of Stock" }), _jsx(Button, { variant: "secondary", size: "md", className: "w-full", disabled: true, title: ACTION_NOT_WIRED, children: inStock ? "Add to Cart" : "Out of Stock" }), _jsx(Button, { variant: "ghost", size: "md", className: "w-full", disabled: true, title: ACTION_NOT_WIRED, children: "\u2661 Add to Wishlist" })] })), allowOffers && offerableType && offersEnabled && price !== null && renderOfferAction?.({
235
+ })) : (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: true, title: ACTION_NOT_WIRED, children: inStock ? "Buy Now" : "Out of Stock" }), _jsx(Button, { variant: "secondary", size: "md", className: "w-full", disabled: true, title: ACTION_NOT_WIRED, children: inStock ? "Add to Cart" : "Out of Stock" }), _jsxs(Button, { variant: "ghost", size: "md", className: "w-full", disabled: true, title: ACTION_NOT_WIRED, children: [_jsx(Icon, { name: "wishlist", size: "md" }), "Add to Wishlist"] })] })), allowOffers && offerableType && offersEnabled && price !== null && renderOfferAction?.({
236
236
  productId: product.id,
237
237
  price,
238
238
  currency,
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Fragment } from "react";
3
- import { Row, Span, TextLink } from "../../../ui";
3
+ import { Row, Span, TextLink, Icon } from "../../../ui";
4
4
  export function ProductFeatureBadges({ featured, fasterDelivery, ratedSeller, condition, returnable, freeShipping, wishlistCount, categoryProductCount, categoryName, codAvailable, codHref, emiAvailable, emiHref, labels, formatCount, categoryBadgeClassName, }) {
5
5
  const badges = [];
6
6
  if (featured) {
@@ -89,7 +89,7 @@ export function ProductFeatureBadges({ featured, fasterDelivery, ratedSeller, co
89
89
  if (wishlistCount && wishlistCount > 0) {
90
90
  badges.push({
91
91
  key: "wishlist",
92
- icon: _jsx(Span, { size: "xs", children: "\u2665" }),
92
+ icon: _jsx(Icon, { name: "wishlist", size: "xs", filled: true }),
93
93
  label: labels.wishlistCount(wishlistCount),
94
94
  colorClass: "text-pink-700 dark:text-pink-300",
95
95
  bgClass: "bg-pink-50 dark:bg-pink-900/30 border-pink-200 dark:border-pink-800",
@@ -11,7 +11,7 @@ const SECTION_LABEL_CLASS = "text-[var(--appkit-font-size-2xs,11px)] font-semibo
11
11
  const ROW_BASE_CLASS = "flex items-start gap-[var(--appkit-space-2)] rounded-lg border px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-xs)] cursor-pointer transition-colors";
12
12
  const ROW_CHECKED_CLASS = "border-primary bg-primary/5 dark:bg-primary/10";
13
13
  const ROW_DISABLED_CLASS = "border-[var(--appkit-color-border)] opacity-50 cursor-not-allowed";
14
- const ROW_DEFAULT_CLASS = "border-[var(--appkit-color-border)] hover:bg-zinc-50 hover:bg-[var(--appkit-color-surface-elevated)]";
14
+ const ROW_DEFAULT_CLASS = "border-[var(--appkit-color-border)] hover:bg-surface-hover";
15
15
  const OVER_LIMIT_BANNER_CLASS = "rounded-lg border border-error/20 bg-error-surface px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-xs)] text-error";
16
16
  const EMPTY_STATE_CLASS = "rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-3)] py-[var(--appkit-space-4)] text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] text-left";
17
17
  function applyProductType(list, productType) {
@@ -75,12 +75,21 @@ export function ProductFilters({ table, categoryOptions = [], brandOptions = [],
75
75
  { value: PRODUCT_FIELDS.CONDITION_VALUES.REFURBISHED, label: t("conditionRefurbished") },
76
76
  { value: PRODUCT_FIELDS.CONDITION_VALUES.BROKEN, label: t("conditionBroken") },
77
77
  ];
78
- const defaultStatusOptions = [
79
- { value: PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED, label: t("statusPublished") },
80
- { value: PRODUCT_FIELDS.STATUS_VALUES.DRAFT, label: t("statusDraft") },
81
- { value: PRODUCT_FIELDS.STATUS_VALUES.IN_REVIEW, label: t("statusInReview") },
82
- { value: PRODUCT_FIELDS.STATUS_VALUES.ARCHIVED, label: t("statusArchived") },
83
- ];
78
+ const resolvedVariant = variant ?? (showStatus ? "admin" : "public");
79
+ const shouldShowStatus = resolvedVariant !== "public" || showStatus;
80
+ // Built ONLY when the status facet actually renders. This used to be
81
+ // constructed unconditionally at the top of the component, so every public
82
+ // listing that never shows a status facet still called t() four times —
83
+ // five such listings mount ProductFilters, which is how one missing key
84
+ // (`statusInReview`) produced a MISSING_MESSAGE storm on every page load.
85
+ const defaultStatusOptions = shouldShowStatus
86
+ ? [
87
+ { value: PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED, label: t("statusPublished") },
88
+ { value: PRODUCT_FIELDS.STATUS_VALUES.DRAFT, label: t("statusDraft") },
89
+ { value: PRODUCT_FIELDS.STATUS_VALUES.IN_REVIEW, label: t("statusInReview") },
90
+ { value: PRODUCT_FIELDS.STATUS_VALUES.ARCHIVED, label: t("statusArchived") },
91
+ ]
92
+ : [];
84
93
  const splitPipe = (key) => table.get(key) ? table.get(key).split("|").filter(Boolean) : [];
85
94
  const selectedCategories = splitPipe(TABLE_KEYS.CATEGORY);
86
95
  const selectedBrands = splitPipe(TABLE_KEYS.BRAND);
@@ -90,7 +99,5 @@ export function ProductFilters({ table, categoryOptions = [], brandOptions = [],
90
99
  const selectedStatuses = splitPipe(TABLE_KEYS.STATUS);
91
100
  const selectedSublistings = splitPipe(TABLE_KEYS.SUBLISTING_CATEGORY);
92
101
  const selectedFeatures = splitPipe(TABLE_KEYS.FEATURES);
93
- const resolvedVariant = variant ?? (showStatus ? "admin" : "public");
94
- const shouldShowStatus = resolvedVariant !== "public" || showStatus;
95
102
  return (_jsxs(Div, { children: [loadCategoryOptions ? (_jsx(AsyncFacetSection, { title: t("category"), loadOptions: loadCategoryOptions, selected: selectedCategories, onChange: (vals) => table.set(TABLE_KEYS.CATEGORY, vals.join("|")), defaultCollapsed: false })) : categoryOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("category"), options: categoryOptions, selected: selectedCategories, onChange: (vals) => table.set(TABLE_KEYS.CATEGORY, vals.join("|")), searchable: true, defaultCollapsed: categoryOptions.length > 6 })) : null, _jsx(FilterFacetSection, { title: t("condition"), options: conditionOptions, selected: selectedConditions, onChange: (vals) => table.set(TABLE_KEYS.CONDITION, vals.join("|")), searchable: false, defaultCollapsed: false }), _jsx(RangeFilter, { title: t("priceRange"), minValue: table.get(TABLE_KEYS.MIN_PRICE), maxValue: table.get(TABLE_KEYS.MAX_PRICE), onMinChange: (v) => table.set(TABLE_KEYS.MIN_PRICE, v), onMaxChange: (v) => table.set(TABLE_KEYS.MAX_PRICE, v), prefix: currencyPrefix, showSlider: true, minBound: 0, maxBound: 500000, step: 500, minPlaceholder: t("minPrice"), maxPlaceholder: t("maxPrice"), defaultCollapsed: false }), loadBrandOptions ? (_jsx(AsyncFacetSection, { title: t("brand"), loadOptions: loadBrandOptions, selected: selectedBrands, onChange: (vals) => table.set(TABLE_KEYS.BRAND, vals.join("|")), defaultCollapsed: false })) : brandOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("brand"), options: brandOptions, selected: selectedBrands, onChange: (vals) => table.set(TABLE_KEYS.BRAND, vals.join("|")), searchable: brandOptions.length > 4, defaultCollapsed: brandOptions.length > 6 })) : null, loadStoreOptions ? (_jsx(AsyncFacetSection, { title: t("store"), loadOptions: loadStoreOptions, selected: selectedSellers, onChange: (vals) => table.set(TABLE_KEYS.STORE_ID, vals.join("|")), defaultCollapsed: false })) : resolvedStoreOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("store"), options: resolvedStoreOptions, selected: selectedSellers, onChange: (vals) => table.set(TABLE_KEYS.STORE_ID, vals.join("|")), searchable: resolvedStoreOptions.length > 4, defaultCollapsed: resolvedStoreOptions.length > 6 })) : null, showShipping && (_jsx(SwitchFilter, { title: t("shipping"), label: t("freeShippingOnly"), checked: table.get(TABLE_KEYS.FREE_SHIPPING) === "true", onChange: (v) => table.set(TABLE_KEYS.FREE_SHIPPING, v ? "true" : ""), defaultCollapsed: false })), tagOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("tags"), options: tagOptions, selected: selectedTags, onChange: (vals) => table.set(TABLE_KEYS.TAGS, vals.join("|")), searchable: tagOptions.length > 4, defaultCollapsed: tagOptions.length > 6 })), shouldShowStatus && (_jsx(FilterFacetSection, { title: t("status"), options: statusOptions ?? defaultStatusOptions, selected: selectedStatuses, onChange: (vals) => table.set(TABLE_KEYS.STATUS, vals.join("|")), searchable: false, defaultCollapsed: false })), sublistingCategoryOptions.length > 0 && (_jsx(FilterFacetSection, { title: "Sublisting Type", options: sublistingCategoryOptions, selected: selectedSublistings, onChange: (vals) => table.set(TABLE_KEYS.SUBLISTING_CATEGORY, vals.join("|")), searchable: sublistingCategoryOptions.length > 4, defaultCollapsed: false })), showBundleFilter && (_jsx(SwitchFilter, { title: "Bundles", label: "Part of a bundle only", checked: table.get(TABLE_KEYS.IS_PART_OF_BUNDLE) === "true", onChange: (v) => table.set(TABLE_KEYS.IS_PART_OF_BUNDLE, v ? "true" : ""), defaultCollapsed: false })), featureOptions.length > 0 && (_jsx(FilterFacetSection, { title: "Features", options: featureOptions, selected: selectedFeatures, onChange: (vals) => table.set(TABLE_KEYS.FEATURES, vals.join("|")), searchable: featureOptions.length > 4, defaultCollapsed: featureOptions.length > 4 }))] }));
96
103
  }
@@ -15,6 +15,7 @@ import { useProductFeatures } from "./ProductFeaturesContext";
15
15
  import { PRODUCT_FEATURE_CARD_MAX_VISIBLE } from "../constants/product-features.constants";
16
16
  import { normalizeListingType } from "../utils/listing-type";
17
17
  import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
18
+ import { WishlistHeartButton } from "../../wishlist/components/WishlistHeartButton";
18
19
  const __P = {
19
20
  p3: "p-[var(--appkit-space-3)]",
20
21
  };
@@ -30,15 +31,11 @@ const CLS_BADGE_LIMITED = "rounded-full bg-info-solid px-[var(--appkit-space-2)]
30
31
  // inverts with the theme. `warning` because success/info/error are each already
31
32
  // taken by a sibling pill in this same stack.
32
33
  const CLS_BADGE_OFFERABLE = "rounded-full bg-warning-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[10px] font-bold text-warning-on-solid shadow-sm";
33
- const CLS_HEART_ACTIVE = "bg-[var(--appkit-color-surface-elevated)]/90 text-error hover:bg-[var(--appkit-color-surface-elevated)]";
34
- const CLS_HEART_IDLE = "bg-[var(--appkit-color-surface-elevated)]/90 text-[var(--appkit-color-text-muted)] hover:text-error hover:bg-[var(--appkit-color-surface-elevated)]";
35
34
  const CLS_STAR = "text-[11px] text-warning";
36
35
  const CLS_DISCOUNT_TEXT = "mt-1 text-[11px] font-semibold text-error";
37
36
  const CLS_DISCOUNT_TEXT_BARE = "text-[10px] text-error";
38
37
  const CLS_STAR_BARE = "text-warning";
39
38
  const CLS_BID_TEXT = "text-[11px] text-error";
40
- const CLS_HEART_ROSE_ACTIVE = "text-error";
41
- const CLS_HEART_ROSE_IDLE = "text-zinc-300 dark:text-[var(--appkit-color-text-muted)] hover:text-error";
42
39
  export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlisted, onAddToCart, onBuyNow, className = "", selectionMode = false, isSelected = false, onSelect, }) {
43
40
  const discount = product.originalPrice && product.originalPrice > product.price
44
41
  ? Math.round(((product.originalPrice - product.price) / product.originalPrice) *
@@ -55,9 +52,9 @@ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlis
55
52
  ? (e) => { e.preventDefault(); e.stopPropagation(); onSelect?.(product.id); }
56
53
  : onClick && !href ? () => onClick(product) : undefined;
57
54
  const cardBody = (_jsxs(Div, { role: selectionMode ? "checkbox" : onClick ? "button" : undefined, "aria-checked": selectionMode ? isSelected : undefined, tabIndex: selectionMode || (onClick && !href) ? 0 : undefined, onKeyDown: selectionMode
58
- ? (e) => (e.key === "Enter" || e.key === " ") && onSelect?.(product.id)
55
+ ? (e) => (e.key === "Enter" || e.key === "") && onSelect?.(product.id)
59
56
  : onClick && !href
60
- ? (e) => (e.key === "Enter" || e.key === " ") && onClick(product)
57
+ ? (e) => (e.key === "Enter" || e.key === "") && onClick(product)
61
58
  : undefined, onClick: handleCardClick, ...(!selectionMode ? longPress : {}), className: [
62
59
  "group relative flex h-full flex-col overflow-hidden",
63
60
  "rounded-2xl border bg-[var(--appkit-color-surface)]",
@@ -73,17 +70,11 @@ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlis
73
70
  .filter(Boolean)
74
71
  .join(" "), children: [_jsxs(Div, { className: `relative ${__O.hidden} aspect-square`, surface: "subtle", children: [product.mainImage ? (_jsx(MediaImage, { src: product.mainImage, alt: product.title, size: "card", className: "transition-transform duration-500 group-hover:scale-105" })) : (_jsx(Row, { surface: "muted", className: "h-full w-full", align: "center", justify: "center", children: _jsx(Span, { className: "opacity-30", size: "4xl", children: "\uD83D\uDECD\uFE0F" }) })), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => { e.preventDefault(); onSelect(product.id); }, label: isSelected ? "Deselect" : "Select", position: "top-2 left-2", className: selectionMode || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsx(Stack, { className: `absolute top-2 ${onSelect ? "left-10" : "left-2"}`, gap: "xs", children: !selectionMode && !isSelected && (_jsxs(_Fragment, { children: [discount && (_jsxs(Span, { weight: "bold", className: CLS_BADGE_NEW, children: ["-", discount, "%"] })), typeBadge && (_jsx(Span, { weight: "bold", className: `text-[10px] ${typeBadge.cls}`, rounded: "full", padding: "pill-xs", shadow: "sm", children: typeBadge.label })), product.partOfBundleIds && product.partOfBundleIds.length > 0 && (_jsx(Span, { className: CLS_BADGE_SALE, title: product.partOfBundleTitles && product.partOfBundleTitles.length > 0
75
72
  ? `In bundle: ${product.partOfBundleTitles[0]}`
76
- : "In a bundle", children: "Bundled" })), product.groupId && (_jsx(Span, { className: CLS_BADGE_TRENDING, title: product.groupTitle ? `Part of set: ${product.groupTitle}` : "Part of a set", children: product.isGroupParent ? "Set Parent" : "In Set" })), product.allowOffers && (_jsx(Span, { className: CLS_BADGE_OFFERABLE, title: "The seller accepts offers on this listing", children: "Taking Offers" })), product.sublistingCategoryId && (_jsx(Span, { className: CLS_BADGE_LIMITED, title: "Has variants or sub-listings", children: "Has Variants" }))] })) }), _jsx(BaseListingCard.IconOverlay, { groupIcon: product.groupIcon, sublistingIcon: product.sublistingIcon, position: "bottom-2 right-2" }), onAddToWishlist && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: (e) => {
73
+ : "In a bundle", children: "Bundled" })), product.groupId && (_jsx(Span, { className: CLS_BADGE_TRENDING, title: product.groupTitle ? `Part of set: ${product.groupTitle}` : "Part of a set", children: product.isGroupParent ? "Set Parent" : "In Set" })), product.allowOffers && (_jsx(Span, { className: CLS_BADGE_OFFERABLE, title: "The seller accepts offers on this listing", children: "Taking Offers" })), product.sublistingCategoryId && (_jsx(Span, { className: CLS_BADGE_LIMITED, title: "Has variants or sub-listings", children: "Has Variants" }))] })) }), _jsx(BaseListingCard.IconOverlay, { groupIcon: product.groupIcon, sublistingIcon: product.sublistingIcon, position: "bottom-2 right-2" }), onAddToWishlist && (_jsx(WishlistHeartButton, { size: "card", variant: "overlay", inWishlist: !!isWishlisted, addLabel: "Add to wishlist", removeLabel: "Remove from wishlist", className: "absolute right-2 top-2", onToggle: (e) => {
77
74
  e.stopPropagation();
78
75
  e.preventDefault();
79
76
  onAddToWishlist(product.id);
80
- }, "aria-label": isWishlisted ? "Remove from wishlist" : "Add to wishlist", className: [
81
- "absolute right-2 top-2 flex h-8 w-8 items-center justify-center rounded-full shadow-sm",
82
- "transition-all duration-150",
83
- isWishlisted
84
- ? CLS_HEART_ACTIVE
85
- : CLS_HEART_IDLE,
86
- ].join(" "), children: _jsx("svg", { className: "h-4 w-4", fill: isWishlisted ? "currentColor" : "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" }) }) }))] }), _jsxs(Stack, { className: `flex-1 ${__P.p3} pt-[0.625rem]`, children: [_jsx(Text, { className: `leading-snug`, truncate: 2, size: "sm", weight: "semibold", color: "primary", children: product.title }), (() => {
77
+ } }))] }), _jsxs(Stack, { className: `flex-1 ${__P.p3} pt-[0.625rem]`, children: [_jsx(Text, { className: `leading-snug`, truncate: 2, size: "sm", weight: "semibold", color: "primary", children: product.title }), (() => {
87
78
  const categories = Array.isArray(product.categoryNames)
88
79
  ? product.categoryNames.filter(Boolean)
89
80
  : product.categoryName
@@ -142,7 +133,7 @@ function ProductListRow({ product, onClick, onAddToWishlist, isWishlisted, }) {
142
133
  100)
143
134
  : null;
144
135
  return (_jsxs(Div, { role: onClick ? "button" : undefined, tabIndex: onClick ? 0 : undefined, onKeyDown: onClick
145
- ? (e) => (e.key === "Enter" || e.key === " ") && onClick(product)
136
+ ? (e) => (e.key === "Enter" || e.key === "") && onClick(product)
146
137
  : undefined, onClick: onClick ? () => onClick(product) : undefined, className: [
147
138
  "flex items-center gap-[var(--appkit-space-3)] p-[var(--appkit-space-2)] sm:p-[var(--appkit-space-3)]",
148
139
  "hover:bg-[var(--appkit-color-surface)]/60 transition-colors",
@@ -160,10 +151,10 @@ function ProductListRow({ product, onClick, onAddToWishlist, isWishlisted, }) {
160
151
  return (_jsx(Span, { weight: "medium", className: `text-[11px] ${low
161
152
  ? "text-warning"
162
153
  : "text-[var(--appkit-color-text-faint)]"}`, children: low ? `Only ${stock} left` : `${stock} in stock` }));
163
- })()] })] }), onAddToWishlist && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: (e) => {
154
+ })()] })] }), onAddToWishlist && (_jsx(WishlistHeartButton, { size: "inline", inWishlist: !!isWishlisted, addLabel: "Add to wishlist", removeLabel: "Remove from wishlist", onToggle: (e) => {
164
155
  e.stopPropagation();
165
156
  onAddToWishlist(product.id);
166
- }, "aria-label": isWishlisted ? "Remove from wishlist" : "Add to wishlist", className: `flex-shrink-0 h-8 w-8 flex items-center justify-center rounded-full text-[length:var(--appkit-text-base)] leading-none ${isWishlisted ? CLS_HEART_ROSE_ACTIVE : CLS_HEART_ROSE_IDLE}`, children: isWishlisted ? "♥" : "♡" }))] }));
157
+ } }))] }));
167
158
  }
168
159
  function safeHref(href) {
169
160
  if (!href)
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Button, Row } from "@mohasinac/appkit/ui";
3
+ import { Tabs, TabsList, TabsTrigger } from "@mohasinac/appkit/ui";
4
4
  import React from "react";
5
5
  import { Div } from "../../../ui/components/Div";
6
6
  const DEFAULT_TABS = [
@@ -19,7 +19,10 @@ export function ProductTabs({ renderDescription, renderSpecs, renderReviews, ext
19
19
  ...extraTabs,
20
20
  ];
21
21
  const tabs = allTabs.length > 0 ? allTabs : DEFAULT_TABS;
22
- return (_jsxs(Div, { className: className, children: [renderTabBar ? (renderTabBar(activeTab, setActiveTab, tabs)) : (_jsx(Row, { gap: "md", className: "border-b mb-4", children: tabs.map((t) => (_jsx(Button, { variant: "ghost", type: "button", onClick: () => setActiveTab(t.value), paddingX: "none", paddingY: "xs", textSize: "sm", weight: "medium", rounded: "none", className: `border-b-2 transition-colors ${activeTab === t.value
23
- ? "border-current text-current"
24
- : "border-transparent text-[var(--appkit-color-text-muted)]"}`, children: t.label }, t.value))) })), activeTab === "description" && renderDescription?.(), activeTab === "specs" && renderSpecs?.(), activeTab === "reviews" && renderReviews?.(), renderExtraTab?.(activeTab)] }));
22
+ return (_jsxs(Div, { className: className, children: [renderTabBar ? (renderTabBar(activeTab, setActiveTab, tabs)) : (
23
+ // Was a hand-rolled <Row> of ghost <Button>s whose ACTIVE state was
24
+ // `border-current text-current` — the inherited colour, i.e. visually
25
+ // identical to inactive. It also had no role="tablist"/role="tab"/
26
+ // aria-selected at all; routing through Tabs supplies all three.
27
+ _jsx(Tabs, { value: activeTab, onChange: setActiveTab, className: "mb-4", children: _jsx(TabsList, { children: tabs.map((t) => (_jsx(TabsTrigger, { value: t.value, children: t.label }, t.value))) }) })), activeTab === "description" && renderDescription?.(), activeTab === "specs" && renderSpecs?.(), activeTab === "reviews" && renderReviews?.(), renderExtraTab?.(activeTab)] }));
25
28
  }
@@ -1,11 +1,8 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
- import { Button } from "../../../ui/components/Button";
5
4
  import { Div } from "../../../ui/components/Div";
6
- const __O = {
7
- xAuto: "overflow-x-auto",
8
- };
5
+ import { Tabs, TabsList, TabsTrigger } from "../../../ui/components/Tabs";
9
6
  const STATIC_TABS = [
10
7
  { id: "description", label: "Description" },
11
8
  { id: "specs", label: "Specifications" },
@@ -40,7 +37,5 @@ export function ProductTabsShell({ descriptionContent, specsContent, gradingCont
40
37
  if (allTabs.length === 0)
41
38
  return null;
42
39
  const activeContent = allTabs.find((t) => t.id === activeId)?.content;
43
- return (_jsxs(Div, { className: `mt-8 ${className}`, children: [_jsx(Div, { layout: "flex", gap: "1", border: "default", className: `mb-6 ${__O.xAuto} border-b pb-px`, children: allTabs.map((t) => (_jsx(Button, { variant: "ghost", type: "button", onClick: () => setActiveId(t.id), paddingX: "md", paddingY: "none", textSize: "sm", weight: "medium", rounded: "none", className: `flex-shrink-0 -mb-px pb-[var(--appkit-space-3)] border-b-2 transition-colors ${activeId === t.id
44
- ? "border-[var(--appkit-color-primary)] text-[var(--appkit-color-primary)]"
45
- : "border-transparent text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-text-muted)]"}`, children: t.label }, t.id))) }), _jsx("div", { children: activeContent })] }));
40
+ return (_jsxs(Div, { className: `mt-8 ${className}`, children: [_jsx(Tabs, { value: activeId, onChange: setActiveId, className: "mb-6", children: _jsx(TabsList, { children: allTabs.map((t) => (_jsx(TabsTrigger, { value: t.id, children: t.label }, t.id))) }) }), _jsx("div", { children: activeContent })] }));
46
41
  }
@@ -32,6 +32,8 @@ export declare const ACTION_ID: {
32
32
  readonly CANCEL_RESERVATION: "cancel-reservation";
33
33
  readonly CHECKOUT: "checkout";
34
34
  readonly CONTINUE_TO_VERIFY: "continue-to-verification";
35
+ readonly CONTINUE_TO_PAYMENT: "continue-to-payment";
36
+ readonly CHECKOUT_BACK: "checkout-back";
35
37
  readonly VERIFY_OTP: "verify-otp";
36
38
  readonly PAY_ONLINE: "pay-online";
37
39
  readonly PAY_COD: "pay-cod";
@@ -74,10 +76,25 @@ export interface ActionMeta {
74
76
  /** Message shown in LoginRequiredModal. Required when requiresAuth=true. */
75
77
  authMessage?: string;
76
78
  /**
77
- * RBAC permission key required to use this action.
78
- * If set, user must have this permission in addition to being signed in.
79
- * Uses the 85+ permission keys from the RBAC system.
80
- * Example: "products.create", "orders.refund", "admin.users.suspend"
79
+ * RBAC permission key required to use this action, checked by `useAuthGate`
80
+ * against `SessionUser.permissions`.
81
+ *
82
+ * 🛑 Two preconditions, and BOTH must hold or the action is a guaranteed
83
+ * denial that looks like a working permission check:
84
+ *
85
+ * 1. The value must be a **colon**-separated key that actually exists in
86
+ * `DEFAULT_ROLES` (`"admin:blog:read"`), not dot-separated. Three entries
87
+ * here carried dot-form keys (`"seller.payouts.request"`,
88
+ * `"seller.reviews.respond"`, `"user.account.delete"`) that match no role;
89
+ * they were removed 2026-08-27. Same family as the 182 guaranteed-403
90
+ * `permission:` lines deleted 2026-08-19.
91
+ * 2. `SessionUser.permissions` must be populated for the acting user — today
92
+ * that happens only on the employee path
93
+ * (`_internal/server/features/auth/permissions.ts`). Neither
94
+ * `buildSessionUser` nor `fetchUserProfileFromServer` sets it, so for an
95
+ * ordinary buyer/seller this field denies unconditionally.
96
+ *
97
+ * Leave it unset unless you have verified both.
81
98
  */
82
99
  requiredPermission?: string;
83
100
  /**
@@ -53,6 +53,8 @@ export const ACTION_ID = {
53
53
  // ── Checkout / navigation CTAs ───────────────────────────────────────────
54
54
  CHECKOUT: "checkout",
55
55
  CONTINUE_TO_VERIFY: "continue-to-verification",
56
+ CONTINUE_TO_PAYMENT: "continue-to-payment",
57
+ CHECKOUT_BACK: "checkout-back",
56
58
  VERIFY_OTP: "verify-otp",
57
59
  PAY_ONLINE: "pay-online",
58
60
  PAY_COD: "pay-cod",
@@ -98,6 +100,8 @@ export const ACTION_META = {
98
100
  // ── Checkout / navigation CTAs ────────────────────────────────────────────
99
101
  [ACTION_ID.CHECKOUT]: { id: ACTION_ID.CHECKOUT, label: regLabel("CART", "checkout", "Checkout"), variant: "primary", requiresAuth: true, authMessage: "You need to be signed in to checkout." },
100
102
  [ACTION_ID.CONTINUE_TO_VERIFY]: { id: ACTION_ID.CONTINUE_TO_VERIFY, label: regLabel("CHECKOUT", "continue-to-verification", "Continue"), variant: "primary", requiresAuth: true, authMessage: "You need to be signed in to checkout." },
103
+ [ACTION_ID.CONTINUE_TO_PAYMENT]: { id: ACTION_ID.CONTINUE_TO_PAYMENT, label: regLabel("CHECKOUT", "continue-to-payment", "Continue to payment"), variant: "primary", requiresAuth: true, authMessage: "You need to be signed in to checkout." },
104
+ [ACTION_ID.CHECKOUT_BACK]: { id: ACTION_ID.CHECKOUT_BACK, label: regLabel("CHECKOUT", "back", "Back"), variant: "ghost" },
101
105
  [ACTION_ID.VERIFY_OTP]: { id: ACTION_ID.VERIFY_OTP, label: regLabel("CHECKOUT", "verify-otp", "Verify & Continue"), variant: "primary", requiresAuth: true, authMessage: "You need to be signed in to checkout." },
102
106
  [ACTION_ID.PAY_ONLINE]: { id: ACTION_ID.PAY_ONLINE, label: regLabel("CHECKOUT", "pay-online", "Pay Online"), variant: "primary", requiresAuth: true, authMessage: "You need to be signed in to checkout." },
103
107
  [ACTION_ID.PAY_COD]: { id: ACTION_ID.PAY_COD, label: regLabel("CHECKOUT", "pay-cod", "Cash on Delivery"), variant: "secondary", requiresAuth: true, authMessage: "You need to be signed in to checkout." },
@@ -113,8 +117,8 @@ export const ACTION_META = {
113
117
  [ACTION_ID.MESSAGE_SELLER]: { id: ACTION_ID.MESSAGE_SELLER, label: "Message Seller", variant: "outline", requiresAuth: true, authMessage: "You need to be signed in to message a seller." },
114
118
  // ── Seller CTAs ───────────────────────────────────────────────────────────
115
119
  [ACTION_ID.BECOME_SELLER]: { id: ACTION_ID.BECOME_SELLER, label: "Apply as Seller", variant: "primary", requiresAuth: true, authMessage: "You need to be signed in to apply as a seller." },
116
- [ACTION_ID.REQUEST_PAYOUT]: { id: ACTION_ID.REQUEST_PAYOUT, label: regLabel("STORE", "request-payout", "Request Payout"), variant: "outline", requiresAuth: true, authMessage: "You need to be signed in to request a payout.", requiredPermission: "seller.payouts.request" },
117
- [ACTION_ID.RESPOND_TO_REVIEW]: { id: ACTION_ID.RESPOND_TO_REVIEW, label: regLabel("STORE", "reply-review", "Respond to Review"), variant: "outline", requiresAuth: true, authMessage: "You need to be signed in to respond to a review.", requiredPermission: "seller.reviews.respond" },
120
+ [ACTION_ID.REQUEST_PAYOUT]: { id: ACTION_ID.REQUEST_PAYOUT, label: regLabel("STORE", "request-payout", "Request Payout"), variant: "outline", requiresAuth: true, authMessage: "You need to be signed in to request a payout." },
121
+ [ACTION_ID.RESPOND_TO_REVIEW]: { id: ACTION_ID.RESPOND_TO_REVIEW, label: regLabel("STORE", "reply-review", "Respond to Review"), variant: "outline", requiresAuth: true, authMessage: "You need to be signed in to respond to a review." },
118
122
  // ── User account actions ──────────────────────────────────────────────────
119
123
  [ACTION_ID.CANCEL_ORDER]: { id: ACTION_ID.CANCEL_ORDER, label: regLabel("USER", "cancel-order", "Cancel Order"), variant: "danger", requiresAuth: true, authMessage: "You need to be signed in to cancel an order." },
120
124
  [ACTION_ID.REQUEST_RETURN]: { id: ACTION_ID.REQUEST_RETURN, label: regLabel("USER", "request-return", "Request Return"), variant: "outline", requiresAuth: true, authMessage: "You need to be signed in to request a return." },
@@ -125,7 +129,7 @@ export const ACTION_META = {
125
129
  [ACTION_ID.EDIT_ADDRESS]: { id: ACTION_ID.EDIT_ADDRESS, label: "Edit Address", variant: "ghost", requiresAuth: true, authMessage: "You need to be signed in to edit an address." },
126
130
  [ACTION_ID.DELETE_ADDRESS]: { id: ACTION_ID.DELETE_ADDRESS, label: regLabel("USER", "delete-address", "Delete Address"), variant: "danger", requiresAuth: true, authMessage: "You need to be signed in to delete an address." },
127
131
  [ACTION_ID.CHANGE_PASSWORD]: { id: ACTION_ID.CHANGE_PASSWORD, label: regLabel("USER", "update-password", "Change Password"), variant: "outline", requiresAuth: true, authMessage: "You need to be signed in to change your password." },
128
- [ACTION_ID.DELETE_ACCOUNT]: { id: ACTION_ID.DELETE_ACCOUNT, label: "Delete Account", variant: "danger", requiresAuth: true, authMessage: "You need to be signed in to delete your account.", requiredPermission: "user.account.delete" },
132
+ [ACTION_ID.DELETE_ACCOUNT]: { id: ACTION_ID.DELETE_ACCOUNT, label: "Delete Account", variant: "danger", requiresAuth: true, authMessage: "You need to be signed in to delete your account." },
129
133
  };
130
134
  // Detail page action groups — ordered top-to-bottom in the right-hand panel
131
135
  export const DETAIL_ACTIONS = {
@@ -1,4 +1,5 @@
1
- import { sieveAnd, sieveFilter, SIEVE_OP, sortBy } from "@mohasinac/appkit/client";
1
+ import { sieveAnd, sieveFilter, SIEVE_OP } from "../../../utils/sieve-builder";
2
+ import { sortBy } from "../../../constants/sort";
2
3
  import { useQuery } from "@tanstack/react-query";
3
4
  import { apiClient } from "../../../http";
4
5
  import { PRODUCT_ENDPOINTS } from "../../../constants/api-endpoints";
@@ -277,7 +277,7 @@ export declare class ProductRepository extends BaseRepository<ProductDocument> {
277
277
  canFilter: boolean;
278
278
  canSort: boolean;
279
279
  };
280
- searchTokens: {
280
+ searchTxt: {
281
281
  canFilter: boolean;
282
282
  canSort: boolean;
283
283
  };
@@ -4,7 +4,8 @@ import { DatabaseError } from "../../../errors";
4
4
  import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
5
5
  import { cacheManager } from "../../../core";
6
6
  import { serverLogger } from "../../../monitoring";
7
- import { generateUniqueId, slugify, buildSearchTokens, tokenizeQuery, generateBarcodeId } from "../../../utils";
7
+ import { generateUniqueId, slugify, generateBarcodeId } from "../../../utils";
8
+ import { buildSearchTxt, parseSearchTxtQuery } from "../../../utils/search-txt";
8
9
  import { PRODUCT_COLLECTION, ProductStatusValues } from "../schemas";
9
10
  import { PRODUCT_FIELDS } from "../../../constants/field-names";
10
11
  /**
@@ -45,8 +46,21 @@ function buildListingKindClause(kind, inverted) {
45
46
  const op = inverted ? "!=" : "==";
46
47
  return `${PRODUCT_FIELDS.LISTING_TYPE}${op}${canonical}`;
47
48
  }
48
- function buildProductSearchTokens(p) {
49
- return buildSearchTokens(p.title, p.description, p.brand, p.brandSlug, p.categoryNames, p.tags, p.features, p.condition, p.card?.setName, p.card?.cardNumber, p.grading?.service, p.specifications?.map((s) => `${s.name} ${s.value}`));
49
+ function buildProductSearchTxt(p) {
50
+ return buildSearchTxt([
51
+ p.title,
52
+ p.description,
53
+ p.brand,
54
+ p.brandSlug,
55
+ p.categoryNames,
56
+ p.tags,
57
+ p.features,
58
+ p.condition,
59
+ p.card?.setName,
60
+ p.card?.cardNumber,
61
+ p.grading?.service,
62
+ p.specifications?.map((s) => `${s.name} ${s.value}`),
63
+ ]);
50
64
  }
51
65
  export class ProductRepository extends BaseRepository {
52
66
  constructor() {
@@ -97,7 +111,7 @@ export class ProductRepository extends BaseRepository {
97
111
  const merged = { ...current, ...data };
98
112
  const updated = await super.update(id, {
99
113
  ...data,
100
- searchTokens: buildProductSearchTokens(merged),
114
+ searchTxt: buildProductSearchTxt(merged),
101
115
  });
102
116
  this.cacheSet(updated);
103
117
  return updated;
@@ -121,7 +135,7 @@ export class ProductRepository extends BaseRepository {
121
135
  barcodeId,
122
136
  slug: id,
123
137
  availableQuantity: input.stockQuantity,
124
- searchTokens: buildProductSearchTokens(input),
138
+ searchTxt: buildProductSearchTxt(input),
125
139
  createdAt: new Date(),
126
140
  updatedAt: new Date(),
127
141
  };
@@ -359,17 +373,17 @@ export class ProductRepository extends BaseRepository {
359
373
  if (opts?.storeId) {
360
374
  baseQuery = baseQuery.where(PRODUCT_FIELDS.STORE_ID, "==", opts.storeId);
361
375
  }
362
- const searchTokens = tokenizeQuery(opts?.search ?? "");
363
- if (searchTokens.length > 0 && opts?.categoriesIn && opts.categoriesIn.length > 0) {
376
+ const searchTxt = parseSearchTxtQuery(opts?.search ?? "");
377
+ if (searchTxt.length > 0 && opts?.categoriesIn && opts.categoriesIn.length > 0) {
364
378
  // Firestore cannot combine two array operators in one query
365
379
  }
366
- else if (searchTokens.length === 1) {
367
- baseQuery = baseQuery.where(PRODUCT_FIELDS.SEARCH_TOKENS, "array-contains", searchTokens[0]);
380
+ else if (searchTxt.length === 1) {
381
+ baseQuery = baseQuery.where(PRODUCT_FIELDS.SEARCH_TXT, "array-contains", searchTxt[0]);
368
382
  }
369
- else if (searchTokens.length > 1) {
370
- baseQuery = baseQuery.where(PRODUCT_FIELDS.SEARCH_TOKENS, "array-contains-any", searchTokens);
383
+ else if (searchTxt.length > 1) {
384
+ baseQuery = baseQuery.where(PRODUCT_FIELDS.SEARCH_TXT, "array-contains-any", searchTxt);
371
385
  }
372
- if (searchTokens.length === 0 && opts?.categoriesIn && opts.categoriesIn.length > 0) {
386
+ if (searchTxt.length === 0 && opts?.categoriesIn && opts.categoriesIn.length > 0) {
373
387
  let categoriesIn = opts.categoriesIn;
374
388
  if (categoriesIn.length > ProductRepository.MAX_ARRAY_CONTAINS_ANY) {
375
389
  // Products carry their FULL ancestor chain in `categorySlugs`, so a
@@ -559,7 +573,7 @@ ProductRepository.SIEVE_FIELDS = {
559
573
  // field had no entry: `throwExceptions:false` means an unknown field is
560
574
  // silently skipped. The toggle did nothing at all until 2026-08-21.
561
575
  shippingPaidBy: { canFilter: true, canSort: false },
562
- searchTokens: { canFilter: true, canSort: false },
576
+ searchTxt: { canFilter: true, canSort: false },
563
577
  "classified.meetupArea": { canFilter: true, canSort: false },
564
578
  "classified.acceptsShipping": { canFilter: true, canSort: false },
565
579
  "classified.negotiable": { canFilter: true, canSort: false },
@@ -349,7 +349,8 @@ export interface ProductDocument extends BaseDocument {
349
349
  * Auto-generated as `LIR-{8-char-hex}` on product creation when not supplied.
350
350
  * Override by scanning a pre-printed sticker during the create/edit flow. */
351
351
  barcodeId?: string;
352
- searchTokens?: string[];
352
+ /** Normalized edge-n-gram search field. Replaced `searchTokens`. */
353
+ searchTxt?: string[];
353
354
  }
354
355
  /**
355
356
  * Single prize within a prize-draw listing. A draw can have between 3 and 16
@@ -381,7 +382,7 @@ export declare const ProductStatusValues: {
381
382
  readonly ARCHIVED: "archived";
382
383
  };
383
384
  export declare const PRODUCT_COLLECTION: "products";
384
- export declare const PRODUCT_INDEXED_FIELDS: readonly ["storeId", "status", "category", "featured", "listingType", "isPromoted", "isOnSale", "isSold", "searchTokens", "createdAt", "barcodeId"];
385
+ export declare const PRODUCT_INDEXED_FIELDS: readonly ["storeId", "status", "category", "featured", "listingType", "isPromoted", "isOnSale", "isSold", "searchTxt", "createdAt", "barcodeId"];
385
386
  export declare const DEFAULT_PRODUCT_DATA: Partial<ProductDocument>;
386
387
  export declare const PRODUCT_PUBLIC_FIELDS: readonly ["allowOffers", "minOfferPercent", "id", "title", "description", "category", "subcategory", "brand", "price", "currency", "stockQuantity", "availableQuantity", "images", "status", "storeName", "featured", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "listingType", "auctionEndDate", "startingBid", "currentBid", "bidCount", "reservePrice", "buyNowPrice", "minBidIncrement", "autoExtendable", "auctionExtensionMinutes", "auctionShippingPaidBy", "bidsHaveStarted", "grading", "card", "classified", "digitalCode", "liveItem", "catalogProductId", "preOrderDeliveryDate", "preOrderDepositPercent", "preOrderDepositAmount", "preOrderMaxQuantity", "preOrderCurrentCount", "preOrderProductionStatus", "preOrderCancellable", "condition", "insurance", "insuranceCost", "shippingPaidBy", "isPromoted", "isOnSale", "isSold", "slug", "seoTitle", "seoDescription", "seoKeywords", "viewCount", "customFields", "customSections", "sublistingCategoryId", "groupId", "isGroupParent", "groupParentSlug", "groupChildSlugs", "groupTitle", "createdAt"];
387
388
  export declare const PRODUCT_UPDATABLE_FIELDS: readonly ["title", "description", "category", "subcategory", "brand", "price", "stockQuantity", "images", "status", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "pickupAddressId", "condition", "insurance", "shippingPaidBy", "autoExtendable", "auctionExtensionMinutes", "auctionShippingPaidBy", "reservePrice", "buyNowPrice", "minBidIncrement", "bidsHaveStarted", "grading", "card", "classified", "digitalCode", "liveItem", "catalogProductId", "listingType", "preOrderDeliveryDate", "preOrderDepositPercent", "preOrderDepositAmount", "preOrderMaxQuantity", "preOrderProductionStatus", "preOrderCancellable", "isOnSale", "isSold", "allowShipBeforeEmiComplete", "printMeta", "seoTitle", "seoDescription", "seoKeywords", "customFields", "customSections", "sublistingCategoryId", "groupId", "isGroupParent", "groupParentSlug", "groupChildSlugs", "groupTitle", "barcodeId"];
@@ -21,7 +21,7 @@ export const PRODUCT_INDEXED_FIELDS = [
21
21
  "isPromoted",
22
22
  "isOnSale",
23
23
  "isSold",
24
- "searchTokens",
24
+ "searchTxt",
25
25
  "createdAt",
26
26
  "barcodeId",
27
27
  ];