@mohasinac/appkit 4.27.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 (271) 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/checkout/actions.js +6 -4
  5. package/dist/_internal/server/features/classified/metadata.js +3 -1
  6. package/dist/_internal/server/features/digital-code/metadata.js +3 -1
  7. package/dist/_internal/server/features/live/metadata.js +3 -1
  8. package/dist/_internal/server/features/refunds/actions.js +3 -2
  9. package/dist/_internal/server/features/reviews/actions.js +3 -2
  10. package/dist/_internal/server/features/seo/sitemap.js +189 -40
  11. package/dist/_internal/server/jobs/core/listingProcessor.d.ts +0 -1
  12. package/dist/_internal/server/jobs/core/listingProcessor.js +19 -1
  13. package/dist/_internal/shared/actions/action-registry.js +14 -0
  14. package/dist/_internal/shared/checkout/order-math.d.ts +25 -3
  15. package/dist/_internal/shared/features/checkout/config.d.ts +3 -3
  16. package/dist/_internal/shared/features/checkout/config.js +6 -3
  17. package/dist/client.d.ts +32 -2
  18. package/dist/client.js +37 -2
  19. package/dist/constants/api-endpoints.d.ts +21 -3
  20. package/dist/constants/api-endpoints.js +14 -1
  21. package/dist/constants/field-names.d.ts +2 -2
  22. package/dist/constants/field-names.js +2 -2
  23. package/dist/core/newsletter.repository.js +14 -1
  24. package/dist/features/about/components/FAQPageView.js +1 -1
  25. package/dist/features/about/components/FeesView.js +1 -1
  26. package/dist/features/account/components/AddressForm.d.ts +2 -0
  27. package/dist/features/account/components/AddressForm.js +5 -1
  28. package/dist/features/account/components/UserBidsView.js +1 -1
  29. package/dist/features/account/components/UserSidebar.js +1 -1
  30. package/dist/features/account/components/UserSupportView.js +24 -2
  31. package/dist/features/account/hooks/useAddresses.d.ts +29 -1
  32. package/dist/features/account/hooks/useAddresses.js +4 -1
  33. package/dist/features/account/hooks/useProfile.d.ts +1 -1
  34. package/dist/features/account/hooks/useProfile.js +1 -1
  35. package/dist/features/addresses/repository/addresses.repository.d.ts +13 -0
  36. package/dist/features/addresses/repository/addresses.repository.js +19 -0
  37. package/dist/features/admin/actions/chat-actions.js +5 -1
  38. package/dist/features/admin/components/AdminAdsView.js +1 -1
  39. package/dist/features/admin/components/AdminBidsView.js +6 -1
  40. package/dist/features/admin/components/AdminCarouselGroupEditorView.d.ts +9 -1
  41. package/dist/features/admin/components/AdminCarouselGroupEditorView.js +42 -10
  42. package/dist/features/admin/components/AdminCarouselView.js +1 -1
  43. package/dist/features/admin/components/AdminFeatureEditorView.js +1 -1
  44. package/dist/features/admin/components/AdminGroupedListingsView.js +14 -2
  45. package/dist/features/admin/components/AdminSidebar.js +1 -1
  46. package/dist/features/admin/components/AdminViewCards.js +2 -2
  47. package/dist/features/admin/components/DataTable.js +2 -2
  48. package/dist/features/admin/schemas/firestore.js +9 -3
  49. package/dist/features/auctions/actions/bid-actions.js +45 -4
  50. package/dist/features/auctions/components/AuctionBottomActions.d.ts +21 -6
  51. package/dist/features/auctions/components/AuctionBottomActions.js +42 -11
  52. package/dist/features/auctions/components/AuctionDetailPageView.d.ts +8 -3
  53. package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
  54. package/dist/features/auctions/components/MarketplaceAuctionCard.js +4 -5
  55. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +27 -2
  56. package/dist/features/auctions/components/PlaceBidFormClient.js +42 -20
  57. package/dist/features/auth/components/SocialAuthButtons.js +1 -1
  58. package/dist/features/auth/hooks/useAuth.d.ts +23 -1
  59. package/dist/features/auth/hooks/useAuth.js +27 -1
  60. package/dist/features/auth/permissions/constants.d.ts +1 -1
  61. package/dist/features/auth/permissions/constants.js +3 -0
  62. package/dist/features/auth/repository/session.repository.d.ts +16 -1
  63. package/dist/features/auth/repository/session.repository.js +23 -0
  64. package/dist/features/auth/repository/user.repository.js +13 -2
  65. package/dist/features/blog/components/BlogListView.js +2 -2
  66. package/dist/features/cart/components/CartPriceBreakdown.d.ts +11 -1
  67. package/dist/features/cart/components/CartPriceBreakdown.js +3 -3
  68. package/dist/features/cart/components/StoreAddonsPicker.d.ts +9 -0
  69. package/dist/features/cart/components/StoreAddonsPicker.js +15 -5
  70. package/dist/features/cart/components/index.d.ts +1 -1
  71. package/dist/features/cart/components/index.js +1 -1
  72. package/dist/features/cart/hooks/useGuestCartMerge.d.ts +8 -1
  73. package/dist/features/cart/hooks/useGuestCartMerge.js +2 -1
  74. package/dist/features/cart/utils/line-total.d.ts +25 -0
  75. package/dist/features/cart/utils/line-total.js +26 -0
  76. package/dist/features/catalogue/repository/catalogue.repository.d.ts +12 -0
  77. package/dist/features/catalogue/repository/catalogue.repository.js +15 -0
  78. package/dist/features/categories/components/CategoryTree.js +2 -2
  79. package/dist/features/contact/components/ContactForm.js +1 -1
  80. package/dist/features/copilot/hooks/useCopilotFeedback.d.ts +1 -1
  81. package/dist/features/copilot/hooks/useCopilotFeedback.js +1 -1
  82. package/dist/features/events/components/AdminEventEditorView.js +16 -9
  83. package/dist/features/events/components/AdminEventEntriesView.js +7 -13
  84. package/dist/features/events/components/EventRafflesSection.js +14 -2
  85. package/dist/features/events/components/EventStatusBadge.js +1 -1
  86. package/dist/features/events/components/event-entry-detail-fields.d.ts +24 -0
  87. package/dist/features/events/components/event-entry-detail-fields.js +13 -0
  88. package/dist/features/faq/components/ContactCTA.js +1 -1
  89. package/dist/features/faq/components/FAQHelpfulButtons.js +2 -2
  90. package/dist/features/faq/repository/faqs.repository.d.ts +16 -1
  91. package/dist/features/faq/repository/faqs.repository.js +64 -54
  92. package/dist/features/faq/schemas/firestore.d.ts +3 -2
  93. package/dist/features/faq/schemas/firestore.js +2 -2
  94. package/dist/features/faq/schemas/index.d.ts +3 -3
  95. package/dist/features/faq/schemas/index.js +1 -1
  96. package/dist/features/filters/FilterFacetSection.js +2 -2
  97. package/dist/features/forms/Toggle.js +1 -1
  98. package/dist/features/grouped/components/GroupedListingEditorView.d.ts +18 -0
  99. package/dist/features/grouped/components/GroupedListingEditorView.js +192 -0
  100. package/dist/features/homepage/components/AdvertisementBanner.js +2 -2
  101. package/dist/features/homepage/components/BrandsSection.js +1 -1
  102. package/dist/features/homepage/components/CTABannerSection.js +1 -1
  103. package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +1 -1
  104. package/dist/features/homepage/components/HomepageSectionBoundary.d.ts +39 -0
  105. package/dist/features/homepage/components/HomepageSectionBoundary.js +49 -0
  106. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  107. package/dist/features/homepage/components/WelcomeSection.js +1 -1
  108. package/dist/features/homepage/lib/section-renderer.js +2 -1
  109. package/dist/features/layout/AppLayoutShell.js +5 -5
  110. package/dist/features/layout/BottomActions.js +5 -5
  111. package/dist/features/layout/BottomSheet.js +1 -1
  112. package/dist/features/layout/MainNavbar.js +1 -1
  113. package/dist/features/layout/NavbarLayout.js +1 -1
  114. package/dist/features/layout/TitleBarLayout.js +3 -3
  115. package/dist/features/layout/hooks/useFormBottomActions.d.ts +11 -1
  116. package/dist/features/layout/hooks/useFormBottomActions.js +16 -2
  117. package/dist/features/lottery/schemas/config-write.d.ts +141 -0
  118. package/dist/features/lottery/schemas/config-write.js +139 -0
  119. package/dist/features/media/MediaPickerModal.js +3 -1
  120. package/dist/features/media/MediaVideo.js +4 -2
  121. package/dist/features/media/components/MediaSlider.js +1 -1
  122. package/dist/features/orders/components/OrdersList.js +2 -2
  123. package/dist/features/payments/repository/payout.repository.d.ts +11 -1
  124. package/dist/features/payments/repository/payout.repository.js +14 -6
  125. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -2
  126. package/dist/features/pre-orders/components/PreOrderBadge.js +1 -1
  127. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -1
  128. package/dist/features/products/columns/productTableColumns.js +3 -3
  129. package/dist/features/products/components/CompareOverlay.js +1 -1
  130. package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
  131. package/dist/features/products/components/PrizeDrawEntryActions.js +2 -2
  132. package/dist/features/products/components/PrizeDrawsSection.js +5 -1
  133. package/dist/features/products/components/ProductDetailActions.js +11 -7
  134. package/dist/features/products/components/ProductDetailPageView.js +3 -3
  135. package/dist/features/products/components/ProductFeatureBadges.js +2 -2
  136. package/dist/features/products/components/ProductFeaturesSelector.js +1 -1
  137. package/dist/features/products/components/ProductFilters.js +15 -8
  138. package/dist/features/products/components/ProductGrid.js +8 -17
  139. package/dist/features/products/components/ProductTabs.js +7 -4
  140. package/dist/features/products/components/ProductTabsShell.js +2 -7
  141. package/dist/features/products/constants/action-defs.d.ts +21 -4
  142. package/dist/features/products/constants/action-defs.js +7 -3
  143. package/dist/features/products/hooks/useRelatedProducts.js +2 -1
  144. package/dist/features/products/repository/products.repository.d.ts +1 -1
  145. package/dist/features/products/repository/products.repository.js +27 -13
  146. package/dist/features/products/schemas/firestore.d.ts +3 -2
  147. package/dist/features/products/schemas/firestore.js +1 -1
  148. package/dist/features/reviews/actions/review-actions.d.ts +8 -1
  149. package/dist/features/reviews/actions/review-actions.js +14 -3
  150. package/dist/features/reviews/components/ReviewDetailShell.js +10 -4
  151. package/dist/features/reviews/components/ReviewModal.js +1 -1
  152. package/dist/features/reviews/repository/reviews.repository.d.ts +25 -1
  153. package/dist/features/reviews/repository/reviews.repository.js +53 -13
  154. package/dist/features/reviews/schemas/firestore.d.ts +6 -0
  155. package/dist/features/scams/components/ScamRegistryView.js +2 -2
  156. package/dist/features/search/components/Search.js +3 -3
  157. package/dist/features/search/components/SearchResultsSection.js +1 -1
  158. package/dist/features/seller/components/SellerAddressesView.js +3 -1
  159. package/dist/features/seller/components/SellerBidsView.js +6 -1
  160. package/dist/features/seller/components/SellerSidebar.js +1 -1
  161. package/dist/features/shell/SectionForm.d.ts +21 -1
  162. package/dist/features/shell/SectionForm.js +15 -3
  163. package/dist/features/shell/build-sections.js +10 -1
  164. package/dist/features/shell/field-ui-meta.d.ts +21 -0
  165. package/dist/features/shipments/components/AdminShipmentLotItemsView.js +23 -6
  166. package/dist/features/site-settings/components/ActionPermissionsManager.js +1 -1
  167. package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +21 -4
  168. package/dist/features/store-extensions/repository/store-extensions.repositories.js +24 -4
  169. package/dist/features/store-extensions/schemas/shipping-config-form.js +13 -0
  170. package/dist/features/store-extensions/schemas/store-category-form.js +1 -1
  171. package/dist/features/stores/actions/store-query-actions.js +25 -1
  172. package/dist/features/stores/components/InteractiveStoreCard.js +1 -1
  173. package/dist/features/stores/components/StoreHeader.js +1 -1
  174. package/dist/features/stores/components/StoreNavTabs.js +20 -8
  175. package/dist/features/support/repository/support.repository.d.ts +12 -0
  176. package/dist/features/support/repository/support.repository.js +15 -0
  177. package/dist/features/support/schemas/ticket-create-form.d.ts +33 -0
  178. package/dist/features/support/schemas/ticket-create-form.js +82 -0
  179. package/dist/features/tester/seed-data/products-tester-seed-data.js +2 -2
  180. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +138 -1
  181. package/dist/features/wishlist/components/WishlistHeartButton.d.ts +55 -0
  182. package/dist/features/wishlist/components/WishlistHeartButton.js +52 -0
  183. package/dist/features/wishlist/components/index.d.ts +2 -1
  184. package/dist/features/wishlist/components/index.js +1 -1
  185. package/dist/index.d.ts +10 -3
  186. package/dist/index.js +19 -4
  187. package/dist/monitoring/error-tracking.js +39 -5
  188. package/dist/monitoring/server-logger.js +70 -32
  189. package/dist/next/components/ErrorView.js +1 -1
  190. package/dist/next/routing/route-map.d.ts +39 -7
  191. package/dist/next/routing/route-map.js +34 -4
  192. package/dist/providers/db-firebase/base.repository.d.ts +30 -0
  193. package/dist/providers/db-firebase/base.repository.js +56 -9
  194. package/dist/react/ErrorBoundary.js +9 -1
  195. package/dist/react/contexts/SessionContext.js +6 -2
  196. package/dist/react/hooks/useAuthGate.d.ts +22 -0
  197. package/dist/react/hooks/useAuthGate.js +86 -7
  198. package/dist/schemas/registry.d.ts +3 -3
  199. package/dist/security/index.d.ts +1 -1
  200. package/dist/security/index.js +1 -1
  201. package/dist/security/pii-encrypt.d.ts +13 -5
  202. package/dist/security/pii-encrypt.js +132 -6
  203. package/dist/security/pii-schemas.d.ts +20 -4
  204. package/dist/security/pii-schemas.js +23 -5
  205. package/dist/seed/_helpers/product-search-txt.d.ts +18 -0
  206. package/dist/seed/_helpers/product-search-txt.js +33 -0
  207. package/dist/seed/faq-seed-data.d.ts +16 -0
  208. package/dist/seed/faq-seed-data.js +22 -1
  209. package/dist/seed/products-art-seed-data.js +49 -7
  210. package/dist/seed/products-auctions-seed-data.js +12 -4
  211. package/dist/seed/products-classifieds-seed-data.d.ts +5 -0
  212. package/dist/seed/products-classifieds-seed-data.js +65 -9
  213. package/dist/seed/products-digital-codes-seed-data.d.ts +5 -0
  214. package/dist/seed/products-digital-codes-seed-data.js +65 -9
  215. package/dist/seed/products-live-items-seed-data.d.ts +5 -0
  216. package/dist/seed/products-live-items-seed-data.js +33 -5
  217. package/dist/seed/products-preorders-seed-data.d.ts +1 -1
  218. package/dist/seed/products-preorders-seed-data.js +6 -2
  219. package/dist/seed/products-prize-draws-seed-data.d.ts +5 -0
  220. package/dist/seed/products-prize-draws-seed-data.js +57 -8
  221. package/dist/seed/products-standard-seed-data.js +6 -2
  222. package/dist/seed/products-stickers-seed-data.js +49 -7
  223. package/dist/seed/site-settings-seed-data.js +5 -3
  224. package/dist/seo/json-ld.d.ts +44 -2
  225. package/dist/seo/json-ld.js +57 -11
  226. package/dist/styles.css +368 -90
  227. package/dist/tailwind-utilities.css +1 -1
  228. package/dist/tokens/index.d.ts +1 -1
  229. package/dist/tokens/index.js +1 -1
  230. package/dist/tokens/themes/default-dark.js +6 -0
  231. package/dist/tokens/themes/default-light.js +5 -0
  232. package/dist/tokens/tokens.css +84 -0
  233. package/dist/ui/DataTable.js +1 -1
  234. package/dist/ui/DataTable.style.css +3 -3
  235. package/dist/ui/components/BulkActionBar.style.css +1 -3
  236. package/dist/ui/components/Button.js +15 -6
  237. package/dist/ui/components/Dropdown.style.css +1 -4
  238. package/dist/ui/components/FilterChipGroup.js +1 -1
  239. package/dist/ui/components/FilterDrawer.style.css +1 -1
  240. package/dist/ui/components/Icon.d.ts +53 -0
  241. package/dist/ui/components/Icon.js +41 -0
  242. package/dist/ui/components/IconButton.d.ts +13 -0
  243. package/dist/ui/components/IconButton.js +13 -0
  244. package/dist/ui/components/IconButton.style.css +18 -10
  245. package/dist/ui/components/ListingLayout.style.css +2 -10
  246. package/dist/ui/components/ListingToolbar.js +6 -6
  247. package/dist/ui/components/Modal.js +1 -1
  248. package/dist/ui/components/PaginatedSelect.style.css +3 -3
  249. package/dist/ui/components/Pagination.style.css +1 -6
  250. package/dist/ui/components/RichTextEditor.js +1 -1
  251. package/dist/ui/components/RowActionMenu.style.css +1 -1
  252. package/dist/ui/components/SideDrawer.style.css +1 -1
  253. package/dist/ui/components/Slider.js +1 -1
  254. package/dist/ui/components/TabBarShell.d.ts +78 -0
  255. package/dist/ui/components/TabBarShell.js +95 -0
  256. package/dist/ui/components/Tabs.js +12 -8
  257. package/dist/ui/components/Tabs.style.css +249 -41
  258. package/dist/ui/components/ViewToggle.style.css +1 -5
  259. package/dist/ui/components/tabs-overflow.d.ts +17 -0
  260. package/dist/ui/components/tabs-overflow.js +142 -0
  261. package/dist/ui/icons/icon-registry.d.ts +38 -0
  262. package/dist/ui/icons/icon-registry.js +37 -0
  263. package/dist/ui/index.d.ts +6 -0
  264. package/dist/ui/index.js +3 -0
  265. package/dist/utils/index.d.ts +1 -1
  266. package/dist/utils/index.js +1 -1
  267. package/dist/utils/media-url.d.ts +27 -4
  268. package/dist/utils/media-url.js +37 -6
  269. package/dist/utils/search-txt.d.ts +71 -0
  270. package/dist/utils/search-txt.js +118 -0
  271. package/package.json +1 -1
@@ -25,7 +25,7 @@ export function BlogCard({ post, href, onClick, className = "" }) {
25
25
  : "";
26
26
  const isInteractive = !!(href || onClick);
27
27
  const card = (_jsxs(Article, { role: onClick && !href ? "button" : undefined, tabIndex: onClick && !href ? 0 : undefined, onKeyDown: onClick && !href
28
- ? (e) => (e.key === "Enter" || e.key === " ") && onClick(post)
28
+ ? (e) => (e.key === "Enter" || e.key === "") && onClick(post)
29
29
  : undefined, onClick: onClick && !href ? () => onClick(post) : undefined, className: `group flex flex-col h-full overflow-hidden rounded-xl border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] shadow-sm transition hover:shadow-md ${isInteractive ? "cursor-pointer" : ""} ${className}`, children: [_jsx(Div, { className: `relative aspect-video w-full ${__O.hidden} bg-[var(--appkit-color-surface)] dark:bg-[var(--appkit-color-surface-elevated)] flex-shrink-0`, children: coverImageUrl ? (_jsx(MediaImage, { src: coverImageUrl, alt: post.title, size: "card", className: "transition-transform duration-300 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", "aria-hidden": "true", children: "\u270D\uFE0F" }) })) }), _jsxs(Stack, { className: `flex-1 ${__P.p5}`, children: [_jsxs(Row, { className: "mb-2", gap: "sm", wrap: true, children: [_jsx(Span, { size: "xs", weight: "medium", className: "bg-primary/10 text-primary", rounded: "full", padding: "pill-xs", transform: "capitalize", children: post.category }), post.isFeatured && (_jsx(Span, { size: "xs", weight: "medium", className: CLS_FEATURED_BADGE, children: "Featured" })), post.readTimeMinutes && (_jsxs(Span, { size: "xs", color: "muted", children: [post.readTimeMinutes, " min read"] }))] }), _jsx(Heading, { level: 3, className: `group-hover:text-primary`, color: "primary", truncate: 2, size: "base", weight: "semibold", children: post.title }), post.excerpt && (_jsx(Text, { className: `mt-2 flex-1`, color: "muted", truncate: 3, size: "sm", children: post.excerpt })), _jsxs(Row, { className: "mt-auto", padding: "t-sm", gap: "3", children: [post.authorAvatar ? (_jsx(Div, { className: "relative h-7 w-7 flex-shrink-0", rounded: "full", overflow: "hidden", children: _jsx(MediaImage, { src: post.authorAvatar, alt: post.authorName ?? "author", size: "avatar" }) })) : post.authorName ? (_jsx(Row, { textWeight: "bold", textSize: "xs", className: "h-7 w-7 flex-shrink-0 bg-primary/10 text-primary", align: "center", justify: "center", rounded: "full", children: post.authorName.charAt(0).toUpperCase() })) : null, _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)] min-w-0", size: "xs", children: [post.authorName && (_jsx(Span, { weight: "medium", className: "text-neutral-700", children: safeDisplayName(post.authorName, "Author") })), date && _jsxs(Span, { className: "ml-1 text-neutral-400", children: ["\u00B7 ", date] })] })] })] })] }));
30
30
  if (href) {
31
31
  return (_jsx(Link, { href: href, className: "block h-full", children: card }));
@@ -33,7 +33,7 @@ export function BlogCard({ post, href, onClick, className = "" }) {
33
33
  return card;
34
34
  }
35
35
  export function BlogCategoryTabs({ categories, active, onSelect, labels = {}, }) {
36
- return (_jsxs(Div, { layout: "flex", gap: "2", className: `scrollbar-none ${__O.xAuto}`, padding: "b-2xs", children: [_jsx(Button, { onClick: () => onSelect(null), variant: !active ? "primary" : "ghost", size: "sm", className: `whitespace-nowrap rounded-full px-[var(--appkit-space-4)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium transition ${!active ? "bg-neutral-900 text-white" : "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:bg-neutral-200 dark:hover:bg-slate-700"}`, children: labels.all ?? "All" }), categories.map((cat) => (_jsx(Button, { onClick: () => onSelect(cat), variant: active === cat ? "primary" : "ghost", size: "sm", className: `whitespace-nowrap rounded-full px-[var(--appkit-space-4)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium capitalize transition ${active === cat ? "bg-neutral-900 text-white" : "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:bg-neutral-200 dark:hover:bg-slate-700"}`, children: labels[cat] ?? cat }, cat)))] }));
36
+ return (_jsxs(Div, { layout: "flex", gap: "2", className: `scrollbar-none ${__O.xAuto}`, padding: "b-2xs", children: [_jsx(Button, { onClick: () => onSelect(null), variant: !active ? "primary" : "ghost", size: "sm", className: `whitespace-nowrap rounded-full px-[var(--appkit-space-4)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium transition ${!active ? "bg-neutral-900 text-white" : "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:bg-surface-hover"}`, children: labels.all ?? "All" }), categories.map((cat) => (_jsx(Button, { onClick: () => onSelect(cat), variant: active === cat ? "primary" : "ghost", size: "sm", className: `whitespace-nowrap rounded-full px-[var(--appkit-space-4)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium capitalize transition ${active === cat ? "bg-neutral-900 text-white" : "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:bg-surface-hover"}`, children: labels[cat] ?? cat }, cat)))] }));
37
37
  }
38
38
  export function BlogListView({ posts, isLoading, totalPages = 1, currentPage = 1, total = 0, onPageChange, onPostClick, emptyLabel = "No posts found", slots, }) {
39
39
  if (isLoading) {
@@ -55,7 +55,17 @@ export interface CartPriceBreakdownProps {
55
55
  unavailableNote?: React.ReactNode;
56
56
  /** Subtotal to show when `preview` is null (guest carts). */
57
57
  fallbackSubtotal?: number;
58
+ /**
59
+ * Shown when the preview request FAILED, as opposed to never having run.
60
+ *
61
+ * Separate from `unavailableNote` because the two say different things and
62
+ * the failure case can coexist with a rendered breakdown: on a refetch error
63
+ * the last good `preview` is deliberately kept, so the buyer sees real
64
+ * figures plus a note that they may be out of date — strictly better than
65
+ * blanking to ₹0, which is what a swallowed error used to produce.
66
+ */
67
+ errorNote?: React.ReactNode;
58
68
  isLoading?: boolean;
59
69
  className?: string;
60
70
  }
61
- export declare function CartPriceBreakdown({ preview, itemCount, currency, unavailableNote, fallbackSubtotal, isLoading, className, }: CartPriceBreakdownProps): React.JSX.Element;
71
+ export declare function CartPriceBreakdown({ preview, itemCount, currency, unavailableNote, fallbackSubtotal, errorNote, isLoading, className, }: CartPriceBreakdownProps): React.JSX.Element;
@@ -9,10 +9,10 @@ function Line({ label, amount, tone = "muted", strong = false, }) {
9
9
  function withStoreCount(label, count) {
10
10
  return count > 1 ? `${label} (${count} stores)` : label;
11
11
  }
12
- export function CartPriceBreakdown({ preview, itemCount, currency = "INR", unavailableNote, fallbackSubtotal = 0, isLoading = false, className = "", }) {
12
+ export function CartPriceBreakdown({ preview, itemCount, currency = "INR", unavailableNote, fallbackSubtotal = 0, errorNote, isLoading = false, className = "", }) {
13
13
  const money = (value) => formatCurrency(value, currency);
14
14
  if (!preview) {
15
- return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: itemCount !== undefined ? `Subtotal (${itemCount} item${itemCount !== 1 ? "s" : ""})` : "Subtotal", amount: money(fallbackSubtotal) }), unavailableNote && (_jsx(Text, { size: "xs", color: "muted", children: unavailableNote }))] }));
15
+ return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: itemCount !== undefined ? `Subtotal (${itemCount} item${itemCount !== 1 ? "s" : ""})` : "Subtotal", amount: money(fallbackSubtotal) }), (errorNote ?? unavailableNote) && (_jsx(Text, { size: "xs", color: errorNote ? "error" : "muted", children: errorNote ?? unavailableNote }))] }));
16
16
  }
17
17
  const storeCount = preview.stores.length;
18
18
  // Count how many stores are actually charged each per-store add-on, so the
@@ -21,5 +21,5 @@ export function CartPriceBreakdown({ preview, itemCount, currency = "INR", unava
21
21
  const subtotalLabel = itemCount !== undefined
22
22
  ? `Subtotal (${itemCount} item${itemCount !== 1 ? "s" : ""}${storeCount > 1 ? `, ${storeCount} stores` : ""})`
23
23
  : "Subtotal";
24
- return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: subtotalLabel, amount: money(preview.subtotal) }), preview.shippingFee > 0 && (_jsx(Line, { label: withStoreCount("Shipping", storeCount), amount: money(preview.shippingFee) })), preview.codHandlingFee > 0 && (_jsx(Line, { label: "COD handling fee", amount: money(preview.codHandlingFee) })), preview.whatsappNotifyFee > 0 && (_jsx(Line, { label: withStoreCount("WhatsApp updates", countCharged((s) => s.whatsappNotifyFee)), amount: money(preview.whatsappNotifyFee) })), preview.giftWrapFee > 0 && (_jsx(Line, { label: withStoreCount("Gift wrap", countCharged((s) => s.giftWrapFee)), amount: money(preview.giftWrapFee) })), preview.shipmentProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Shipment protection", countCharged((s) => s.shipmentProtectionFee)), amount: money(preview.shipmentProtectionFee) })), preview.platformFee > 0 && _jsx(Line, { label: "Platform fee", amount: money(preview.platformFee) }), (preview.gstAmount > 0 || preview.gstOnFee > 0) && (_jsx(Line, { label: "GST", amount: money(preview.gstAmount + preview.gstOnFee) })), preview.couponDiscount > 0 && (_jsx(Line, { label: "Coupon discount", amount: `−${money(preview.couponDiscount)}`, tone: "success" })), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Total", amount: money(preview.total), tone: "primary", strong: true }) }), isLoading && (_jsx(Text, { size: "xs", color: "muted", children: "Updating\u2026" }))] }));
24
+ return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: subtotalLabel, amount: money(preview.subtotal) }), preview.shippingFee > 0 && (_jsx(Line, { label: withStoreCount("Shipping", storeCount), amount: money(preview.shippingFee) })), preview.codHandlingFee > 0 && (_jsx(Line, { label: "COD handling fee", amount: money(preview.codHandlingFee) })), preview.whatsappNotifyFee > 0 && (_jsx(Line, { label: withStoreCount("WhatsApp updates", countCharged((s) => s.whatsappNotifyFee)), amount: money(preview.whatsappNotifyFee) })), preview.giftWrapFee > 0 && (_jsx(Line, { label: withStoreCount("Gift wrap", countCharged((s) => s.giftWrapFee)), amount: money(preview.giftWrapFee) })), preview.shipmentProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Shipment protection", countCharged((s) => s.shipmentProtectionFee)), amount: money(preview.shipmentProtectionFee) })), preview.platformFee > 0 && _jsx(Line, { label: "Platform fee", amount: money(preview.platformFee) }), (preview.gstAmount > 0 || preview.gstOnFee > 0) && (_jsx(Line, { label: "GST", amount: money(preview.gstAmount + preview.gstOnFee) })), preview.couponDiscount > 0 && (_jsx(Line, { label: "Coupon discount", amount: `−${money(preview.couponDiscount)}`, tone: "success" })), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Total", amount: money(preview.total), tone: "primary", strong: true }) }), errorNote ? (_jsx(Text, { size: "xs", color: "error", children: errorNote })) : (isLoading && (_jsx(Text, { size: "xs", color: "muted", children: "Updating\u2026" })))] }));
25
25
  }
@@ -24,6 +24,15 @@ export interface StoreAddonsRates {
24
24
  shipmentProtectionFeePercent?: number;
25
25
  shipmentProtectionFeeMin?: number;
26
26
  }
27
+ /**
28
+ * True when the site offers at least one paid add-on.
29
+ *
30
+ * Exported because the picker renders NOTHING when the answer is false, and a
31
+ * parent that doesn't know that ends up drawing a bare "Add-ons" heading over
32
+ * empty space. Callers use this to decide between mounting the picker and
33
+ * showing an explanatory line instead.
34
+ */
35
+ export declare function hasAnyStoreAddon(rates: StoreAddonsRates | null | undefined): boolean;
27
36
  export interface StoreAddonsPickerProps {
28
37
  storeId: string;
29
38
  /** This store's subtotal — shipment protection is a percentage of it. */
@@ -6,12 +6,22 @@ import { IconBox } from "../../../ui/components/IconBox";
6
6
  import { FieldCheckbox } from "../../../ui/forms/FieldCheckbox";
7
7
  import { FieldTextarea } from "../../../ui/forms/FieldTextarea";
8
8
  import { formatCurrency } from "../../../utils/number.formatter";
9
- export function StoreAddonsPicker({ storeId, storeSubtotal, value, onChange, rates, disabled = false, disabledReason, showGiftMessage = false, currency = "INR", className = "", }) {
10
- const anyEnabled = !!rates &&
11
- (rates.whatsappNotifyFeeEnabled ||
9
+ /**
10
+ * True when the site offers at least one paid add-on.
11
+ *
12
+ * Exported because the picker renders NOTHING when the answer is false, and a
13
+ * parent that doesn't know that ends up drawing a bare "Add-ons" heading over
14
+ * empty space. Callers use this to decide between mounting the picker and
15
+ * showing an explanatory line instead.
16
+ */
17
+ export function hasAnyStoreAddon(rates) {
18
+ return (!!rates &&
19
+ !!(rates.whatsappNotifyFeeEnabled ||
12
20
  rates.giftWrapFeeEnabled ||
13
- rates.shipmentProtectionFeeEnabled);
14
- if (!anyEnabled)
21
+ rates.shipmentProtectionFeeEnabled));
22
+ }
23
+ export function StoreAddonsPicker({ storeId, storeSubtotal, value, onChange, rates, disabled = false, disabledReason, showGiftMessage = false, currency = "INR", className = "", }) {
24
+ if (!hasAnyStoreAddon(rates) || !rates)
15
25
  return null;
16
26
  const money = (value) => formatCurrency(value, currency);
17
27
  const set = (patch) => onChange(storeId, { ...value, ...patch });
@@ -5,7 +5,7 @@ export { CartView } from "./CartView";
5
5
  export type { CartViewProps, CartOrderGroup } from "./CartView";
6
6
  export { CartSummary } from "./CartSummary";
7
7
  export type { CartSummaryProps } from "./CartSummary";
8
- export { StoreAddonsPicker } from "./StoreAddonsPicker";
8
+ export { StoreAddonsPicker, hasAnyStoreAddon } from "./StoreAddonsPicker";
9
9
  export type { StoreAddonsPickerProps, StoreAddonsValue, StoreAddonsRates, } from "./StoreAddonsPicker";
10
10
  export { CartPriceBreakdown } from "./CartPriceBreakdown";
11
11
  export type { CartPriceBreakdownProps, CartPriceBreakdownData, CartPriceBreakdownStore, } from "./CartPriceBreakdown";
@@ -2,7 +2,7 @@ export { CartItemRow, CartDrawer } from "./CartDrawer";
2
2
  export { CartGroupLineRow } from "./CartGroupLineRow";
3
3
  export { CartView } from "./CartView";
4
4
  export { CartSummary } from "./CartSummary";
5
- export { StoreAddonsPicker } from "./StoreAddonsPicker";
5
+ export { StoreAddonsPicker, hasAnyStoreAddon } from "./StoreAddonsPicker";
6
6
  export { CartPriceBreakdown } from "./CartPriceBreakdown";
7
7
  export { CheckoutView } from "./CheckoutView";
8
8
  export { CheckoutAddressStep } from "./CheckoutAddressStep";
@@ -1,11 +1,18 @@
1
1
  import { type GuestCartItem } from "../utils/guest-cart";
2
+ import { type NormalizedError } from "../../../errors/normalize";
2
3
  export interface UseGuestCartMergeOptions {
3
4
  userId: string | null | undefined;
4
5
  mergeEndpoint?: string;
5
6
  onNavigate?: (url: string) => void;
6
7
  mergeFn?: (items: GuestCartItem[]) => Promise<void>;
7
8
  onMerged?: (count: number) => void;
8
- onMergeFailed?: (error: unknown) => void;
9
+ /**
10
+ * Receives a typed NormalizedError, not the raw `unknown` — the hook is the
11
+ * boundary that knows how to classify the failure, so every consumer would
12
+ * otherwise repeat the same narrowing. Zero external consumers at the time of
13
+ * narrowing, so this broke nothing.
14
+ */
15
+ onMergeFailed?: (error: NormalizedError) => void;
9
16
  }
10
17
  /**
11
18
  * useGuestCartMerge
@@ -3,6 +3,7 @@ import { useEffect, useRef } from "react";
3
3
  import { apiClient } from "../../../http";
4
4
  import { clearGuestCart, clearGuestReturnTo, getGuestCartItems, getGuestReturnTo, } from "../utils/guest-cart";
5
5
  import { CART_ENDPOINTS } from "../../../constants/api-endpoints";
6
+ import { normalizeError } from "../../../errors/normalize";
6
7
  /**
7
8
  * useGuestCartMerge
8
9
  *
@@ -42,7 +43,7 @@ export function useGuestCartMerge(options) {
42
43
  }
43
44
  })
44
45
  .catch((err) => {
45
- options.onMergeFailed?.(err);
46
+ options.onMergeFailed?.(normalizeError(err));
46
47
  if (returnTo)
47
48
  clearGuestReturnTo();
48
49
  });
@@ -0,0 +1,25 @@
1
+ import { type PricedCartLine } from "../../../_internal/shared/checkout/order-math";
2
+ /**
3
+ * What one cart line contributes to a subtotal, computed on the CLIENT — where
4
+ * no `ProductDocument` is available.
5
+ *
6
+ * Delegates to `lineTotalFor(item, null)` so the priority order that actually
7
+ * matters stays in one place (`order-math.ts`): a `lockedPrice` beats
8
+ * everything, a bundle is charged its own discounted price and is checked
9
+ * BEFORE its members, a grouped line is the sum of what the buyer picked, and
10
+ * anything else falls back to the cart's snapshotted price.
11
+ *
12
+ * 🛑 Do not re-derive this at a call site. That rule has been hand-rolled six
13
+ * times already and two of the copies silently omitted the `lockedPrice`
14
+ * branch, so an accepted offer was charged at the seller's list price
15
+ * (Recurrent Root Cause #75).
16
+ *
17
+ * The one thing it cannot know is a live listing price that has moved since the
18
+ * line was added — `lineTotalFor` would read that off the product document.
19
+ * That is fine for the surfaces this serves (the cart and the checkout
20
+ * add-ons/fees step): both exist to render immediately with a good figure, and
21
+ * the server pricing preview overwrites it with the authoritative one the
22
+ * moment it lands.
23
+ */
24
+ export declare function clientLineTotal(item: PricedCartLine): number;
25
+ export type { PricedCartLine };
@@ -0,0 +1,26 @@
1
+ import { lineTotalFor } from "../../../_internal/shared/checkout/order-math";
2
+ /**
3
+ * What one cart line contributes to a subtotal, computed on the CLIENT — where
4
+ * no `ProductDocument` is available.
5
+ *
6
+ * Delegates to `lineTotalFor(item, null)` so the priority order that actually
7
+ * matters stays in one place (`order-math.ts`): a `lockedPrice` beats
8
+ * everything, a bundle is charged its own discounted price and is checked
9
+ * BEFORE its members, a grouped line is the sum of what the buyer picked, and
10
+ * anything else falls back to the cart's snapshotted price.
11
+ *
12
+ * 🛑 Do not re-derive this at a call site. That rule has been hand-rolled six
13
+ * times already and two of the copies silently omitted the `lockedPrice`
14
+ * branch, so an accepted offer was charged at the seller's list price
15
+ * (Recurrent Root Cause #75).
16
+ *
17
+ * The one thing it cannot know is a live listing price that has moved since the
18
+ * line was added — `lineTotalFor` would read that off the product document.
19
+ * That is fine for the surfaces this serves (the cart and the checkout
20
+ * add-ons/fees step): both exist to render immediately with a good figure, and
21
+ * the server pricing preview overwrites it with the authoritative one the
22
+ * moment it lands.
23
+ */
24
+ export function clientLineTotal(item) {
25
+ return lineTotalFor(item, null);
26
+ }
@@ -1,3 +1,4 @@
1
+ import type { DocumentSnapshot } from "firebase-admin/firestore";
1
2
  /**
2
3
  * Personal Catalogue Repository (Feature B).
3
4
  */
@@ -12,6 +13,17 @@ export interface CatalogueWriteContext {
12
13
  note?: string;
13
14
  }
14
15
  export declare class CatalogueRepository extends BaseRepository<CatalogueItemDocument> {
16
+ /**
17
+ * `ownerEmail` only — NOT `ownerName`.
18
+ *
19
+ * Per decision D1 names stay partial-match searchable, and catalogue items are
20
+ * searchable by owner in the admin approvals queue. `CATALOGUE_HISTORY_PII_FIELDS`
21
+ * lists both, but that constant scrubs status *history*, which has no search
22
+ * requirement — the two lists are intentionally different.
23
+ */
24
+ protected piiFields: readonly ["ownerEmail"];
25
+ /** Decrypt on read so callers see the shape they wrote. */
26
+ protected mapDoc<D = CatalogueItemDocument>(snap: DocumentSnapshot): D;
15
27
  constructor();
16
28
  create(input: CatalogueItemCreateInput): Promise<CatalogueItemDocument>;
17
29
  /**
@@ -1,3 +1,4 @@
1
+ import { decryptPiiFields } from "../../../security/pii-encrypt";
1
2
  /**
2
3
  * Personal Catalogue Repository (Feature B).
3
4
  */
@@ -5,8 +6,22 @@ import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../..
5
6
  import { CATALOGUE_COLLECTION, CATALOGUE_FIELDS, CATALOGUE_TRACKED_FIELDS, CATALOGUE_HISTORY_PII_FIELDS, createCatalogueItemId, } from "../schemas/firestore";
6
7
  import { withHistory } from "../../../_internal/shared/history/index";
7
8
  export class CatalogueRepository extends BaseRepository {
9
+ /** Decrypt on read so callers see the shape they wrote. */
10
+ mapDoc(snap) {
11
+ const raw = super.mapDoc(snap);
12
+ return decryptPiiFields(raw, [...this.piiFields]);
13
+ }
8
14
  constructor() {
9
15
  super(CATALOGUE_COLLECTION);
16
+ /**
17
+ * `ownerEmail` only — NOT `ownerName`.
18
+ *
19
+ * Per decision D1 names stay partial-match searchable, and catalogue items are
20
+ * searchable by owner in the admin approvals queue. `CATALOGUE_HISTORY_PII_FIELDS`
21
+ * lists both, but that constant scrubs status *history*, which has no search
22
+ * requirement — the two lists are intentionally different.
23
+ */
24
+ this.piiFields = ["ownerEmail"];
10
25
  }
11
26
  async create(input) {
12
27
  const id = createCatalogueItemId({ ownerSlug: input.ownerId, title: input.title });
@@ -10,7 +10,7 @@ function CategoryTreeNode({ item, children = [], activeId, onSelect, depth = 0,
10
10
  const DEPTH_PL = ["pl-2", "pl-6", "pl-10", "pl-14", "pl-[72px]"];
11
11
  const depthPl = DEPTH_PL[Math.min(depth, 4)];
12
12
  return (_jsxs(Div, { children: [_jsxs(Row, { textWeight: "semibold", textSize: "sm", role: "treeitem", "aria-expanded": hasChildren ? expanded : undefined, "aria-selected": isActive, tabIndex: 0, onKeyDown: (e) => {
13
- if (e.key === "Enter" || e.key === " ") {
13
+ if (e.key === "Enter" || e.key === "") {
14
14
  if (hasChildren)
15
15
  setExpanded((p) => !p);
16
16
  onSelect?.(item);
@@ -20,7 +20,7 @@ function CategoryTreeNode({ item, children = [], activeId, onSelect, depth = 0,
20
20
  if (hasChildren)
21
21
  setExpanded((p) => !p);
22
22
  onSelect?.(item);
23
- }, className: `cursor-pointer py-[var(--appkit-space-1-5)] transition select-none ${depthPl} ${isActive ? "bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400" : "text-neutral-700 hover:bg-neutral-50 text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface-elevated)]"}`, align: "center", gap: "xs", padding: "x-xs", rounded: "md", children: [hasChildren && (_jsx(Span, { size: "xs", "aria-hidden": "true", className: `transition-transform duration-150 ${expanded ? "rotate-90" : ""}`, color: "faint", children: "\u25B6" })), !hasChildren && _jsx(Span, { className: "w-3" }), item.display?.icon && (_jsx(Span, { size: "base", "aria-hidden": "true", className: "flex-shrink-0", children: item.display.icon })), _jsx(Text, { className: "flex-1 truncate", children: item.name }), item.metrics && (_jsx(Span, { size: "xs", className: "ml-auto flex-shrink-0 tabular-nums", color: "faint", children: item.metrics.productCount }))] }), hasChildren && expanded && (_jsx(Div, { role: "group", children: children.map((child) => (_jsx(CategoryTreeNode, { item: child, activeId: activeId, onSelect: onSelect, depth: depth + 1 }, child.id))) }))] }));
23
+ }, className: `cursor-pointer py-[var(--appkit-space-1-5)] transition select-none ${depthPl} ${isActive ? "bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400" : " hover:bg-surface-hover text-[var(--appkit-color-text-muted)]"}`, align: "center", gap: "xs", padding: "x-xs", rounded: "md", children: [hasChildren && (_jsx(Span, { size: "xs", "aria-hidden": "true", className: `transition-transform duration-150 ${expanded ? "rotate-90" : ""}`, color: "faint", children: "\u25B6" })), !hasChildren && _jsx(Span, { className: "w-3" }), item.display?.icon && (_jsx(Span, { size: "base", "aria-hidden": "true", className: "flex-shrink-0", children: item.display.icon })), _jsx(Text, { className: "flex-1 truncate", children: item.name }), item.metrics && (_jsx(Span, { size: "xs", className: "ml-auto flex-shrink-0 tabular-nums", color: "faint", children: item.metrics.productCount }))] }), hasChildren && expanded && (_jsx(Div, { role: "group", children: children.map((child) => (_jsx(CategoryTreeNode, { item: child, activeId: activeId, onSelect: onSelect, depth: depth + 1 }, child.id))) }))] }));
24
24
  }
25
25
  /**
26
26
  * CategoryTree — hierarchical expandable navigation for categories.
@@ -68,7 +68,7 @@ export function ContactForm({ onSubmit, labels = {}, className = "", }) {
68
68
  if (submitted) {
69
69
  return (_jsxs(Div, { className: `text-center ${className}`, padding: "y-3xl", children: [_jsx(Div, { textSize: "5xl", className: "mb-4", children: "\u2705" }), _jsx(Heading, { level: 2, className: "mb-2", children: labels.successTitle ?? "Message Sent!" }), _jsx(Text, { variant: "secondary", className: "mb-6", children: labels.successDescription ?? "We'll get back to you shortly." }), _jsx(Button, { type: "button", variant: "outline", onClick: () => setSubmitted(false), children: labels.sendAnotherButton ?? "Send another message" })] }));
70
70
  }
71
- const field = (id, label, placeholder, multiline) => (_jsxs(Stack, { gap: "xs", children: [_jsx("label", { htmlFor: id, className: "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-text-muted)]", children: label }), multiline ? (_jsx(Textarea, { id: id, value: form[id], onChange: (e) => setForm((f) => ({ ...f, [id]: e.target.value })), placeholder: placeholder, rows: 5, className: "w-full rounded-lg border border-neutral-200 border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] outline-none focus:ring-2 ring-primary/20 transition" })) : (_jsx(Input, { id: id, type: id === "email" ? "email" : "text", value: form[id], onChange: (e) => setForm((f) => ({ ...f, [id]: e.target.value })), placeholder: placeholder, className: "h-10" })), errors[id] && _jsx(Span, { size: "xs", className: CLS_FIELD_ERROR, children: errors[id] })] }));
71
+ const field = (id, label, placeholder, multiline) => (_jsxs(Stack, { gap: "xs", children: [_jsx("label", { htmlFor: id, className: "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-text-muted)]", children: label }), multiline ? (_jsx(Textarea, { id: id, value: form[id], onChange: (e) => setForm((f) => ({ ...f, [id]: e.target.value })), placeholder: placeholder, rows: 5, className: "w-full rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] outline-none focus:ring-2 ring-primary/20 transition" })) : (_jsx(Input, { id: id, type: id === "email" ? "email" : "text", value: form[id], onChange: (e) => setForm((f) => ({ ...f, [id]: e.target.value })), placeholder: placeholder, className: "h-10" })), errors[id] && _jsx(Span, { size: "xs", className: CLS_FIELD_ERROR, children: errors[id] })] }));
72
72
  return (_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Form, { onSubmit: handleSubmit, noValidate: true, className: className, spacing: "md", children: [_jsx(Heading, { level: 2, className: "mb-6", children: labels.title ?? "Send us a message" }), submitError && (_jsx(Div, { className: CLS_ERROR_BANNER, children: submitError })), field("name", labels.nameLabel ?? "Your Name", labels.namePlaceholder ?? "Enter your name"), field("email", labels.emailLabel ?? "Email", labels.emailPlaceholder ?? "your@email.com"), field("subject", labels.subjectLabel ?? "Subject", labels.subjectPlaceholder ?? "How can we help?"), field("message", labels.messageLabel ?? "Message", labels.messagePlaceholder ?? "Tell us more…", true), _jsx(FormErrorSummary, {}), _jsx(Button, { type: "submit", variant: "primary", disabled: isSubmitting, className: "w-full sm:w-auto", children: isSubmitting
73
73
  ? (labels.submittingButton ?? "Sending…")
74
74
  : (labels.submitButton ?? "Send Message") })] }) }));
@@ -9,7 +9,7 @@ export interface UseCopilotFeedbackOptions {
9
9
  * Sends thumbs-up / thumbs-down feedback on a copilot response.
10
10
  * The consumer injects the endpoint builder so appkit stays route-agnostic.
11
11
  */
12
- export declare function useCopilotFeedback({ feedbackEndpoint, }: UseCopilotFeedbackOptions): import("@tanstack/react-query").UseMutationResult<void, import("@mohasinac/appkit/client").ApiError, {
12
+ export declare function useCopilotFeedback({ feedbackEndpoint, }: UseCopilotFeedbackOptions): import("@tanstack/react-query").UseMutationResult<void, import("../../../client").ApiError, {
13
13
  logId: string;
14
14
  feedback: CopilotFeedback;
15
15
  }, unknown>;
@@ -1,4 +1,4 @@
1
- import { useApiMutation } from "@mohasinac/appkit/client";
1
+ import { useApiMutation } from "../../../client/api/useApiMutation";
2
2
  import { apiClient } from "../../../http";
3
3
  /**
4
4
  * useCopilotFeedback
@@ -8,6 +8,7 @@ import { Alert, Button, Div, Grid, Heading, Input, Label, RichTextEditor, Row, S
8
8
  import { FormShellContext, useFormShellState, applyZodIssues, FormErrorSummary } from "../../../ui/forms";
9
9
  import { apiClient } from "../../../http";
10
10
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
11
+ import { ALL_EVENT_TYPES, EVENT_TYPE_LABELS } from "../types";
11
12
  import { SectionForm, useSectionFormNav } from "../../shell";
12
13
  import { eventDraftSchema } from "../schemas/event-form";
13
14
  import { ProductInlineSelect } from "../../seller/components/ProductInlineSelect";
@@ -31,15 +32,21 @@ const CLS_REMOVE_BTN_LG = "text-zinc-400 hover:text-error text-[length:var(--app
31
32
  const CLS_RAFFLE_PANEL = "border border-warning bg-warning-surface dark:border-warning dark:bg-warning-surface";
32
33
  const CLS_RAFFLE_HEADING = "text-warning dark:text-warning";
33
34
  const CLS_RAFFLE_BODY = "text-warning dark:text-warning";
34
- const EVENT_TYPE_OPTIONS = [
35
- { label: "Sale", value: "sale" },
36
- { label: "Offer / Coupon", value: "offer" },
37
- { label: "Poll", value: "poll" },
38
- { label: "Survey", value: "survey" },
39
- { label: "Feedback", value: "feedback" },
40
- { label: "Raffle", value: "raffle" },
41
- { label: "Spin Wheel", value: "spin_wheel" },
42
- ];
35
+ /*
36
+ * DERIVED. This was a hand-written 7-entry array and `EVENT_TYPE_LABELS`
37
+ * — a `Record<EventType, string>` sitting one import away — already held the
38
+ * same labels for all 8. It was missing `lottery`, and `allowedTypes` filters
39
+ * AGAINST this list, so passing `"lottery"` in still could not surface it:
40
+ * lottery events had no create path in the UI at all and could only come from
41
+ * `npm run seed`.
42
+ *
43
+ * Deriving from the Record means a new EventType cannot be omitted here
44
+ * without failing to compile (Root Cause #61).
45
+ */
46
+ const EVENT_TYPE_OPTIONS = ALL_EVENT_TYPES.map((value) => ({
47
+ value,
48
+ label: EVENT_TYPE_LABELS[value],
49
+ }));
43
50
  const RAFFLE_TYPE_OPTIONS = [
44
51
  { label: "Open raffle (all confirmed entries)", value: "open_raffle" },
45
52
  { label: "Top N scorers", value: "top_n_scorers" },
@@ -9,6 +9,9 @@ import { normalizeError } from "../../../errors/normalize";
9
9
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
10
10
  import { DataListingView } from "../../admin/components/DataListingView";
11
11
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
12
+ import { buildEventEntryDetailFields } from "./event-entry-detail-fields";
13
+ import { TextLink } from "../../../ui";
14
+ import { ROUTES } from "../../../constants/index";
12
15
  const CLS_RESPONSE_TEXT = "whitespace-pre-wrap break-words";
13
16
  const REVIEW_STATUS_OPTIONS = [
14
17
  { label: "All", value: "all" },
@@ -151,19 +154,10 @@ export function AdminEventEntriesView({ eventId, entriesEndpoint, entryReviewEnd
151
154
  toolbarExtra: eventId ? (_jsx(Button, { variant: "ghost", action: ACTIONS.ADMIN["export-event-entries"], onClick: handleExportReport })) : undefined,
152
155
  renderAboveContent: () => (_jsxs(Stack, { gap: "3", className: "px-[var(--appkit-space-3)] pb-[var(--appkit-space-2)]", children: [!eventId ? (_jsx(Alert, { variant: "warning", title: "Missing event id", children: "Event id is required to load entries." })) : null, statsQuery.error ? (_jsx(Alert, { variant: "error", title: "Could not load event stats", children: statsQuery.error instanceof Error ? statsQuery.error.message : "Unknown error" })) : null, _jsxs(Div, { layout: "grid", gap: "3", className: "grid-cols-1 sm:grid-cols-3", children: [_jsx(Alert, { variant: "info", title: "Total entries", children: String(statsQuery.data?.stats?.totalEntries ?? 0) }), _jsx(Alert, { variant: "success", title: "Approved", children: String(statsQuery.data?.stats?.approvedEntries ?? 0) }), _jsx(Alert, { variant: "warning", title: "Flagged", children: String(statsQuery.data?.stats?.flaggedEntries ?? 0) })] })] })),
153
156
  };
154
- return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(RecordDetailModal, { isOpen: detail !== null, onClose: () => setDetail(null), title: detail?.userDisplayName || detail?.userEmail || "Entry", badges: detail ? [{ label: detail.reviewStatus }] : undefined, description: detail?.pollComment, fields: detail
155
- ? [
156
- { label: "Entrant", value: detail.userDisplayName ?? detail.userId ?? "—" },
157
- { label: "Email", value: detail.userEmail ?? "—" },
158
- { label: "Review status", value: detail.reviewStatus },
159
- { label: "Points", value: detail.points != null ? String(detail.points) : "—" },
160
- { label: "Raffle eligible", value: detail.raffleEligible ? "Yes" : "No" },
161
- { label: "Poll votes", value: detail.pollVotes?.join(", ") || "—" },
162
- { label: "Reviewed by", value: detail.reviewedBy ?? "—" },
163
- { label: "Review note", value: detail.reviewNote ?? "—" },
164
- { label: "Entry ID", value: detail.id },
165
- ]
166
- : undefined,
157
+ return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(RecordDetailModal, { isOpen: detail !== null, onClose: () => setDetail(null), title: detail?.userDisplayName || detail?.userEmail || "Entry", badges: detail ? [{ label: detail.reviewStatus }] : undefined, description: detail?.pollComment, fields: detail ? buildEventEntryDetailFields(detail) : undefined, footer: detail ? (
158
+ // The full page — a modal cannot be linked, bookmarked or reloaded
159
+ // into, which is the whole reason the page exists.
160
+ _jsx(TextLink, { href: String(ROUTES.ADMIN.EVENT_ENTRY_DETAIL(detail.id)), children: "Open full page \u2192" })) : undefined,
167
161
  // The survey/feedback answers — the actual submission being judged.
168
162
  metadata: detail?.formResponses })] }));
169
163
  }
@@ -1,7 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { normalizeError } from "../../../errors/normalize";
3
- import { sieveFilter, sieveAnd, SIEVE_OP } from "@mohasinac/appkit/client";
4
- import { sortBy } from "@mohasinac/appkit/client";
3
+ import { serverLogger } from "../../../monitoring/server-logger";
4
+ // Import from the DEFINING modules, never `@mohasinac/appkit/client` — that
5
+ // entry is `"use client"`, so in this async Server Component these would resolve
6
+ // to client-reference proxies and calling them throws during the server render
7
+ // (Root Cause #18). The `try/catch` below swallowed that throw, which is why
8
+ // this section silently rendered nothing instead of failing loudly.
9
+ import { sieveFilter, sieveAnd, SIEVE_OP } from "../../../utils/sieve-builder";
10
+ import { sortBy } from "../../../constants/sort";
5
11
  import { Container, Div, Heading, Section, Stack, Text, } from "../../../ui";
6
12
  import { eventRepository } from "../repository/events.repository";
7
13
  import { EventCard } from "./EventCard";
@@ -24,6 +30,12 @@ export async function EventRafflesSection({ config, }) {
24
30
  }
25
31
  catch (_err) {
26
32
  void normalizeError(_err);
33
+ // Loud on purpose: a swallowed failure here is indistinguishable from "no
34
+ // active raffles", which is how this section stayed invisible in production
35
+ // without anyone noticing (Root Cause #59).
36
+ serverLogger.error("EventRafflesSection query failed", {
37
+ error: _err instanceof Error ? _err.message : String(_err),
38
+ });
27
39
  events = [];
28
40
  }
29
41
  if (events.length === 0)
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Span } from "../../../ui";
3
3
  const STATUS_MAP = {
4
- draft: { label: "Draft", color: "bg-[var(--appkit-color-surface)] text-gray-700 bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]" },
4
+ draft: { label: "Draft", color: "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]" },
5
5
  active: { label: "Active", color: "bg-success-surface text-success" },
6
6
  paused: { label: "Paused", color: "bg-warning-surface text-warning" },
7
7
  ended: { label: "Ended", color: "bg-error-surface text-error" },
@@ -0,0 +1,24 @@
1
+ import type { EventEntryItem } from "../types";
2
+ /**
3
+ * The scalar rows for one event entry.
4
+ *
5
+ * Extracted 2026-08-26 (W22) so `AdminEventEntriesView`'s detail modal and
6
+ * `/admin/event-entries/[id]/view` render the same thing. The list had these
7
+ * inline; a page that re-listed them would be a second copy, and the first
8
+ * field added to one and not the other is the drift.
9
+ *
10
+ * Same shape as `buildBidDetailFields` in the auctions feature — that one also
11
+ * takes a viewer, because a bid's bidder identity is portal-dependent. An
12
+ * event entry has no such split: only admins and moderators reach this at all,
13
+ * so there is no viewer parameter to add and adding one "for symmetry" would
14
+ * imply a distinction that does not exist.
15
+ *
16
+ * `formResponses` is deliberately NOT a row. It is the survey/feedback
17
+ * submission being judged — open-ended and often long — and belongs in the
18
+ * `metadata` slot, not squeezed into a definition list.
19
+ */
20
+ export interface EntryFieldRow {
21
+ label: string;
22
+ value: string;
23
+ }
24
+ export declare function buildEventEntryDetailFields(entry: EventEntryItem): EntryFieldRow[];
@@ -0,0 +1,13 @@
1
+ export function buildEventEntryDetailFields(entry) {
2
+ return [
3
+ { label: "Entrant", value: entry.userDisplayName ?? entry.userId ?? "—" },
4
+ { label: "Email", value: entry.userEmail ?? "—" },
5
+ { label: "Review status", value: entry.reviewStatus },
6
+ { label: "Points", value: entry.points != null ? String(entry.points) : "—" },
7
+ { label: "Raffle eligible", value: entry.raffleEligible ? "Yes" : "No" },
8
+ { label: "Poll votes", value: entry.pollVotes?.join(", ") || "—" },
9
+ { label: "Reviewed by", value: entry.reviewedBy ?? "—" },
10
+ { label: "Review note", value: entry.reviewNote ?? "—" },
11
+ { label: "Entry ID", value: entry.id },
12
+ ];
13
+ }
@@ -3,7 +3,7 @@ import { Card, Div, Heading, Row, Span, Text, TextLink } from "../../../ui";
3
3
  const __P = {
4
4
  p8: "p-[var(--appkit-space-8)]",
5
5
  };
6
- const CLS_CONTACT_CARD = "flex items-center gap-[var(--appkit-space-3)] rounded-lg border border-zinc-100 border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-elevated)] px-[var(--appkit-space-4)] py-[var(--appkit-space-3)] hover:border-primary/40 transition-colors";
6
+ const CLS_CONTACT_CARD = "flex items-center gap-[var(--appkit-space-3)] rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-elevated)] px-[var(--appkit-space-4)] py-[var(--appkit-space-3)] hover:border-primary/40 transition-colors";
7
7
  const CLS_CONTACT_LABEL = "text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)]";
8
8
  export function ContactCTA({ email, phone, contactHref, labels, }) {
9
9
  return (_jsxs(Card, { variant: "outlined", padding: "lg", className: "text-center", children: [_jsx(Row, { className: "mb-6", justify: "center", children: _jsx(Div, { className: `bg-primary/10 ${__P.p8}`, rounded: "full", children: _jsx("svg", { className: "h-12 w-12 text-primary", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) }) }) }), _jsx(Heading, { level: 2, className: "mb-3", size: "2xl", weight: "semibold", children: labels?.title ?? "Need More Help?" }), _jsx(Text, { className: "mx-auto mb-6 max-w-2xl", color: "muted", children: labels?.description ?? "Our support team is here to help you." }), _jsxs(Div, { layout: "grid", gap: "4", className: "mb-8 grid-cols-1 md:grid-cols-3", children: [_jsxs(TextLink, { href: `mailto:${email}`, variant: "none", className: CLS_CONTACT_CARD, children: [_jsx(Text, { className: CLS_CONTACT_LABEL, children: labels?.emailUs ?? "Email Us" }), _jsx(Text, { size: "xs", color: "muted", children: email })] }), _jsxs(TextLink, { href: `tel:${phone}`, variant: "none", className: CLS_CONTACT_CARD, children: [_jsx(Text, { className: CLS_CONTACT_LABEL, children: labels?.callUs ?? "Call Us" }), _jsx(Text, { size: "xs", color: "muted", children: phone })] }), _jsxs(TextLink, { href: contactHref, className: CLS_CONTACT_CARD, children: [_jsx(Text, { className: CLS_CONTACT_LABEL, children: labels?.contactForm ?? "Contact Form" }), _jsx(Text, { size: "xs", color: "muted", children: labels?.submitRequest ?? "Submit a request" })] })] }), _jsxs(TextLink, { rounded: "xl", href: contactHref, layout: "inline-flex", align: "center", gap: "sm", className: "bg-primary p-[1.5rem] text-white transition-colors hover:bg-primary/90", weight: "medium", children: [_jsx(Span, { children: labels?.contactTeam ?? "Contact Team" }), _jsx("svg", { className: "h-5 w-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) })] })] }));
@@ -44,9 +44,9 @@ export function FAQHelpfulButtons({ faqId, initialHelpful, initialNotHelpful, on
44
44
  ? "bg-success-solid text-success-on-solid"
45
45
  : userVote
46
46
  ? "cursor-not-allowed bg-zinc-200 text-[var(--appkit-color-text-muted)] opacity-50 dark:bg-slate-600 text-[var(--appkit-color-text-muted)]"
47
- : "bg-[var(--appkit-color-surface)] text-zinc-800 hover:bg-green-50 bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text)] dark:hover:bg-green-900/20"}`, children: [_jsx(Span, { size: "sm", children: labels?.yes ?? "Yes" }), _jsxs(Span, { size: "sm", color: "muted", children: ["(", helpful, ")"] })] }), _jsxs(Button, { variant: "ghost", onClick: () => handleVote(false), disabled: isPending || userVote !== null, className: `flex-1 gap-[var(--appkit-space-2)] rounded-lg p-[var(--appkit-space-4)] transition-all ${userVote === "not-helpful"
47
+ : "bg-[var(--appkit-color-surface)] hover:bg-success-surface bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text)]"}`, children: [_jsx(Span, { size: "sm", children: labels?.yes ?? "Yes" }), _jsxs(Span, { size: "sm", color: "muted", children: ["(", helpful, ")"] })] }), _jsxs(Button, { variant: "ghost", onClick: () => handleVote(false), disabled: isPending || userVote !== null, className: `flex-1 gap-[var(--appkit-space-2)] rounded-lg p-[var(--appkit-space-4)] transition-all ${userVote === "not-helpful"
48
48
  ? "bg-error-solid text-error-on-solid"
49
49
  : userVote
50
50
  ? "cursor-not-allowed bg-zinc-200 text-[var(--appkit-color-text-muted)] opacity-50 dark:bg-slate-600 text-[var(--appkit-color-text-muted)]"
51
- : "bg-[var(--appkit-color-surface)] text-zinc-800 hover:bg-red-50 bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text)] dark:hover:bg-red-900/20"}`, children: [_jsx(Span, { size: "sm", children: labels?.no ?? "No" }), _jsxs(Span, { size: "sm", color: "muted", children: ["(", notHelpful, ")"] })] })] })] }));
51
+ : "bg-[var(--appkit-color-surface)] hover:bg-error-surface bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text)]"}`, children: [_jsx(Span, { size: "sm", children: labels?.no ?? "No" }), _jsxs(Span, { size: "sm", color: "muted", children: ["(", notHelpful, ")"] })] })] })] }));
52
52
  }
@@ -15,10 +15,25 @@ export declare class FAQsRepository {
15
15
  update(id: string, data: Partial<FAQ>): Promise<FAQ>;
16
16
  delete(id: string): Promise<void>;
17
17
  }
18
+ /**
19
+ * FAQ `searchTxt`. Exported so the seed derives byte-identical tokens — the
20
+ * seed writes documents raw via batch.set() and never goes through this
21
+ * repository, which is exactly why all 63 seeded FAQs had no tokens at all.
22
+ *
23
+ * EVERY field is prefix-expanded, including the answer body. An earlier version
24
+ * whole-worded the body to protect the token budget, and a seed test caught what
25
+ * that costs: searching "ship" returned FEWER FAQs than "shipping", because an
26
+ * answer mentioning shipping had only the whole word indexed. Partial match that
27
+ * silently excludes the body is not partial match.
28
+ *
29
+ * Measured over the real seed corpus: max 449 tokens, average 200, nothing near
30
+ * the 600 cap — so nothing is truncated. Re-measure before reusing this shape on
31
+ * a long-form collection (blog posts), where bodies are far larger.
32
+ */
33
+ export declare function buildFaqSearchTxt(input: Partial<FAQDocument>): string[];
18
34
  export declare class FirebaseFAQsRepository extends BaseRepository<FAQDocument> {
19
35
  static readonly SIEVE_FIELDS: FirebaseSieveFields;
20
36
  constructor();
21
- private buildSearchTokens;
22
37
  create(data: Partial<FAQDocument>): Promise<FAQDocument>;
23
38
  update(id: string, data: Partial<FAQDocument>): Promise<FAQDocument>;
24
39
  list(model: SieveModel, opts?: {