@mohasinac/appkit 3.2.8 → 3.2.9

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 (268) hide show
  1. package/dist/_internal/client/features/filters/filter-load-options.js +11 -9
  2. package/dist/_internal/client/theme/ThemeProvider.js +14 -4
  3. package/dist/_internal/server/features/auth/firebase-identity.d.ts +12 -0
  4. package/dist/_internal/server/features/auth/firebase-identity.js +25 -0
  5. package/dist/_internal/server/features/site-settings/safe-settings.d.ts +12 -0
  6. package/dist/_internal/server/features/site-settings/safe-settings.js +25 -0
  7. package/dist/_internal/shared/features/layout/config.js +7 -7
  8. package/dist/_internal/shared/styles/skeleton.d.ts +8 -8
  9. package/dist/_internal/shared/styles/skeleton.js +10 -10
  10. package/dist/_internal/shared/styles/themed.d.ts +12 -12
  11. package/dist/_internal/shared/styles/themed.js +12 -12
  12. package/dist/features/about/components/AboutView.js +1 -1
  13. package/dist/features/about/components/FAQPageView.js +1 -1
  14. package/dist/features/about/components/FeesView.js +2 -2
  15. package/dist/features/about/components/HowAuctionsWorkView.js +2 -2
  16. package/dist/features/about/components/HowCheckoutWorksView.js +2 -2
  17. package/dist/features/about/components/HowOffersWorkView.js +2 -2
  18. package/dist/features/about/components/HowOrdersWorkView.js +2 -2
  19. package/dist/features/about/components/HowPayoutsWorkView.js +2 -2
  20. package/dist/features/about/components/HowPreOrdersWorkView.js +2 -2
  21. package/dist/features/about/components/HowReviewsWorkView.js +2 -2
  22. package/dist/features/about/components/PublicProfileView.js +3 -3
  23. package/dist/features/about/components/SecurityPrivacyView.js +2 -2
  24. package/dist/features/account/components/AddressBook.js +2 -2
  25. package/dist/features/account/components/AddressesIndexListing.js +1 -1
  26. package/dist/features/account/components/ChatWindow.js +1 -1
  27. package/dist/features/account/components/MessagesView.js +1 -1
  28. package/dist/features/account/components/NotificationBell.js +2 -2
  29. package/dist/features/account/components/NotificationPreferencesPanel.js +1 -1
  30. package/dist/features/account/components/UserSidebar.js +3 -3
  31. package/dist/features/account/components/UserSupportView.js +3 -3
  32. package/dist/features/admin/components/AdminAdEditorView.js +1 -1
  33. package/dist/features/admin/components/AdminAdsView.js +5 -5
  34. package/dist/features/admin/components/AdminAnalyticsView.js +1 -1
  35. package/dist/features/admin/components/AdminBrandsView.js +1 -1
  36. package/dist/features/admin/components/AdminBundlesView.js +2 -2
  37. package/dist/features/admin/components/AdminCarouselEditorView.js +4 -4
  38. package/dist/features/admin/components/AdminCarouselView.js +2 -2
  39. package/dist/features/admin/components/AdminCategoriesView.js +2 -2
  40. package/dist/features/admin/components/AdminContactEditorView.js +1 -1
  41. package/dist/features/admin/components/AdminCouponEditorView.js +1 -1
  42. package/dist/features/admin/components/AdminEmployeeEditorView.js +3 -3
  43. package/dist/features/admin/components/AdminFaqsView.js +1 -1
  44. package/dist/features/admin/components/AdminFeatureEditorView.js +2 -2
  45. package/dist/features/admin/components/AdminFeatureFlagsView.js +1 -1
  46. package/dist/features/admin/components/AdminFeaturesView.js +2 -2
  47. package/dist/features/admin/components/AdminNavigationView.js +2 -2
  48. package/dist/features/admin/components/AdminNotificationsView.js +1 -1
  49. package/dist/features/admin/components/AdminOrderEditorView.js +1 -1
  50. package/dist/features/admin/components/AdminPageHeader.d.ts +1 -1
  51. package/dist/features/admin/components/AdminPageHeader.js +1 -1
  52. package/dist/features/admin/components/AdminReviewsView.js +1 -1
  53. package/dist/features/admin/components/AdminScammerEditorView.js +3 -3
  54. package/dist/features/admin/components/AdminScammersView.js +1 -1
  55. package/dist/features/admin/components/AdminSectionsView.js +1 -1
  56. package/dist/features/admin/components/AdminSessionsView.js +1 -1
  57. package/dist/features/admin/components/AdminSidebar.js +5 -5
  58. package/dist/features/admin/components/AdminSiteSettingsView.js +3 -3
  59. package/dist/features/admin/components/AdminStoreEditorView.js +3 -3
  60. package/dist/features/admin/components/AdminSupportTicketDetailView.js +3 -3
  61. package/dist/features/admin/components/AdminSupportTicketsView.js +2 -2
  62. package/dist/features/admin/components/AdminViewCards.js +5 -5
  63. package/dist/features/admin/components/DataTable.js +1 -1
  64. package/dist/features/admin/components/DrawerFormFooter.js +1 -1
  65. package/dist/features/admin/components/QuickActionsPanel.js +1 -1
  66. package/dist/features/admin/components/analytics/AdminTopProductsTable.js +1 -1
  67. package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
  68. package/dist/features/auctions/components/CollapsibleBidHistory.js +1 -1
  69. package/dist/features/auctions/components/MarketplaceAuctionCard.js +1 -1
  70. package/dist/features/auth/components/ForgotPasswordView.js +1 -1
  71. package/dist/features/auth/components/Guards.js +1 -1
  72. package/dist/features/auth/components/RegisterForm.js +1 -1
  73. package/dist/features/auth/components/ResetPasswordView.js +1 -1
  74. package/dist/features/auth/components/SocialAuthButtons.js +1 -1
  75. package/dist/features/blog/components/BlogIndexListing.js +1 -1
  76. package/dist/features/blog/components/BlogListView.js +3 -3
  77. package/dist/features/blog/components/BlogPostView.js +1 -1
  78. package/dist/features/cart/components/CartDrawer.js +5 -5
  79. package/dist/features/categories/components/BrandDetailPageView.js +1 -1
  80. package/dist/features/categories/components/BrandDetailTabs.js +1 -1
  81. package/dist/features/categories/components/CategoriesIndexListing.js +2 -2
  82. package/dist/features/categories/components/CategoryDetailPageView.js +1 -1
  83. package/dist/features/categories/components/CategoryDetailTabs.js +1 -1
  84. package/dist/features/categories/components/CategoryGrid.js +3 -3
  85. package/dist/features/categories/components/CategoryTree.js +2 -2
  86. package/dist/features/categories/components/ConcernCard.js +1 -1
  87. package/dist/features/checkout/components/CheckoutStepper.js +1 -1
  88. package/dist/features/classified/components/ClassifiedIndexListing.js +1 -1
  89. package/dist/features/collections/components/CollectionCard.js +1 -1
  90. package/dist/features/collections/hooks/useCollections.js +12 -12
  91. package/dist/features/contact/components/ContactForm.js +1 -1
  92. package/dist/features/copilot/components/AdminCopilotView.js +5 -5
  93. package/dist/features/digital-codes/components/DigitalCodesIndexListing.js +1 -1
  94. package/dist/features/events/components/AdminEventEntriesView.js +1 -1
  95. package/dist/features/events/components/EventCard.js +1 -1
  96. package/dist/features/events/components/EventPollWidget.js +1 -1
  97. package/dist/features/events/components/EventStatusBadge.js +1 -1
  98. package/dist/features/events/components/EventsIndexListing.js +1 -1
  99. package/dist/features/faq/components/ContactCTA.js +2 -2
  100. package/dist/features/faq/components/FAQAccordion.js +4 -4
  101. package/dist/features/faq/components/FAQCategorySidebar.js +2 -2
  102. package/dist/features/faq/components/FAQHelpfulButtons.js +4 -4
  103. package/dist/features/faq/components/RelatedFAQs.js +1 -1
  104. package/dist/features/filters/AsyncFacetSection.js +3 -3
  105. package/dist/features/filters/FilterFacetSection.js +5 -5
  106. package/dist/features/filters/RangeFilter.js +3 -3
  107. package/dist/features/filters/SwitchFilter.js +2 -2
  108. package/dist/features/forms/Checkbox.js +1 -1
  109. package/dist/features/forms/Input.js +2 -2
  110. package/dist/features/forms/Radio.js +2 -2
  111. package/dist/features/forms/Select.js +1 -1
  112. package/dist/features/forms/Slider.js +1 -1
  113. package/dist/features/forms/Textarea.js +1 -1
  114. package/dist/features/forms/Toggle.js +3 -3
  115. package/dist/features/forms/utils.d.ts +4 -4
  116. package/dist/features/forms/utils.js +4 -4
  117. package/dist/features/homepage/components/AdSlot.js +1 -1
  118. package/dist/features/homepage/components/AdvertisementBanner.js +3 -3
  119. package/dist/features/homepage/components/BrandsSection.js +2 -2
  120. package/dist/features/homepage/components/GoogleReviewsSection.js +1 -1
  121. package/dist/features/homepage/components/HeroSection.js +1 -1
  122. package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +1 -1
  123. package/dist/features/homepage/components/HowItWorksSection.js +1 -1
  124. package/dist/features/homepage/components/ShopByCategorySection.js +2 -2
  125. package/dist/features/homepage/components/SiteFeaturesSection.js +1 -1
  126. package/dist/features/homepage/components/SocialPostCard.js +1 -1
  127. package/dist/features/homepage/components/StatsCounterSection.js +1 -1
  128. package/dist/features/homepage/components/TrustIndicatorsSection.js +1 -1
  129. package/dist/features/homepage/constants/hero-carousel-styles.d.ts +3 -3
  130. package/dist/features/homepage/constants/hero-carousel-styles.js +3 -3
  131. package/dist/features/homepage/hooks/useActiveAd.d.ts +1 -3
  132. package/dist/features/homepage/hooks/useActiveAd.js +9 -17
  133. package/dist/features/layout/AppLayoutShell.js +12 -12
  134. package/dist/features/layout/AutoBreadcrumbs.js +1 -1
  135. package/dist/features/layout/BackToTop.js +2 -2
  136. package/dist/features/layout/BottomActions.js +8 -8
  137. package/dist/features/layout/BottomNavItem.js +1 -1
  138. package/dist/features/layout/BottomNavLayout.js +1 -1
  139. package/dist/features/layout/BottomNavbar.d.ts +1 -1
  140. package/dist/features/layout/BottomNavbar.js +2 -2
  141. package/dist/features/layout/BottomSheet.js +1 -1
  142. package/dist/features/layout/Breadcrumbs.js +2 -2
  143. package/dist/features/layout/FooterLayout.js +2 -2
  144. package/dist/features/layout/MainNavbar.js +1 -1
  145. package/dist/features/layout/NavItem.js +1 -1
  146. package/dist/features/layout/NavbarLayout.js +3 -3
  147. package/dist/features/layout/SidebarLayout.js +1 -1
  148. package/dist/features/layout/TitleBarLayout.js +3 -3
  149. package/dist/features/live/components/LiveItemsIndexListing.js +1 -1
  150. package/dist/features/loyalty/components/CoinsDisplay.js +1 -1
  151. package/dist/features/loyalty/hooks/useLoyaltyBalance.js +11 -3
  152. package/dist/features/media/AvatarUpload.js +1 -1
  153. package/dist/features/media/MediaPickerModal.js +1 -1
  154. package/dist/features/media/components/MediaSlider.js +1 -1
  155. package/dist/features/media/finalize.d.ts +12 -1
  156. package/dist/features/media/finalize.js +64 -11
  157. package/dist/features/media/modals/ImageCropModal.js +1 -1
  158. package/dist/features/media/modals/ImageEditor.js +1 -1
  159. package/dist/features/media/repository/media-assets.repository.d.ts +33 -0
  160. package/dist/features/media/repository/media-assets.repository.js +43 -0
  161. package/dist/features/media/upload/MediaUploadField.js +1 -1
  162. package/dist/features/messages/hooks/useConversation.js +12 -17
  163. package/dist/features/messages/hooks/useConversations.js +3 -5
  164. package/dist/features/orders/components/MarketplaceOrderCard.js +1 -1
  165. package/dist/features/orders/components/OrdersList.js +3 -3
  166. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
  167. package/dist/features/pre-orders/components/PreOrderBadge.js +1 -1
  168. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -2
  169. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +1 -1
  170. package/dist/features/pre-orders/hooks/usePreOrders.js +12 -12
  171. package/dist/features/products/columns/productTableColumns.js +3 -3
  172. package/dist/features/products/components/BidHistory.js +1 -1
  173. package/dist/features/products/components/CompareOverlay.js +9 -9
  174. package/dist/features/products/components/CustomSectionTabContent.js +1 -1
  175. package/dist/features/products/components/CustomSectionsEditor.js +1 -1
  176. package/dist/features/products/components/FeatureBadge.js +2 -2
  177. package/dist/features/products/components/GroupSettingsPanel.js +2 -2
  178. package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
  179. package/dist/features/products/components/PrizeDrawsIndexListing.js +1 -1
  180. package/dist/features/products/components/ProductDetailPageView.js +4 -4
  181. package/dist/features/products/components/ProductFeatureBadges.js +2 -2
  182. package/dist/features/products/components/ProductFeaturesSelector.js +4 -4
  183. package/dist/features/products/components/ProductGalleryClient.js +1 -1
  184. package/dist/features/products/components/ProductGrid.js +8 -8
  185. package/dist/features/products/components/ProductTabs.js +1 -1
  186. package/dist/features/products/components/ProductTabsShell.js +1 -1
  187. package/dist/features/products/components/ShareButton.js +1 -1
  188. package/dist/features/products/components/ShowGroupSection.js +2 -2
  189. package/dist/features/products/components/SublistingCarouselSection.js +2 -2
  190. package/dist/features/promotions/components/CouponCard.js +2 -2
  191. package/dist/features/promotions/components/CouponsIndexListing.js +2 -2
  192. package/dist/features/reviews/components/ReviewDetailPageView.js +1 -1
  193. package/dist/features/reviews/components/ReviewDetailShell.js +5 -5
  194. package/dist/features/reviews/components/ReviewModal.js +1 -1
  195. package/dist/features/reviews/components/ReviewSummary.js +2 -2
  196. package/dist/features/reviews/components/ReviewsIndexListing.js +1 -1
  197. package/dist/features/reviews/components/ReviewsList.js +2 -2
  198. package/dist/features/scams/components/ScamRegistryView.js +2 -2
  199. package/dist/features/search/components/Search.js +11 -11
  200. package/dist/features/search/components/SearchFiltersRow.js +1 -1
  201. package/dist/features/search/components/SearchResultsSection.js +2 -2
  202. package/dist/features/seller/components/SellerAddressesView.js +4 -4
  203. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +1 -1
  204. package/dist/features/seller/components/SellerCouponEditorView.js +1 -1
  205. package/dist/features/seller/components/SellerCouponsView.js +1 -1
  206. package/dist/features/seller/components/SellerFeaturesView.js +2 -2
  207. package/dist/features/seller/components/SellerGoogleReviewsView.js +1 -1
  208. package/dist/features/seller/components/SellerOrdersView.js +1 -1
  209. package/dist/features/seller/components/SellerPayoutMethodsView.js +1 -1
  210. package/dist/features/seller/components/SellerSidebar.js +4 -4
  211. package/dist/features/seller/components/analytics/SellerRevenueChart.js +1 -1
  212. package/dist/features/seller/components/analytics/SellerTopProducts.js +1 -1
  213. package/dist/features/site-settings/components/ActionPermissionsManager.js +2 -2
  214. package/dist/features/site-settings/components/NavPermissionsManager.js +1 -1
  215. package/dist/features/stores/components/InteractiveStoreCard.js +3 -3
  216. package/dist/features/stores/components/StoreHeader.js +2 -2
  217. package/dist/features/stores/components/StoreNavTabs.js +2 -2
  218. package/dist/features/stores/components/StorePreOrdersListing.js +1 -1
  219. package/dist/features/stores/components/StoreReviewsListing.js +1 -1
  220. package/dist/features/stores/components/StoresIndexListing.js +1 -1
  221. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +2 -2
  222. package/dist/features/wishlist/components/WishlistPage.js +2 -2
  223. package/dist/features/wishlist/hooks/useWishlistCount.js +5 -13
  224. package/dist/http/index.d.ts +1 -0
  225. package/dist/http/index.js +1 -0
  226. package/dist/http/retry.d.ts +1 -0
  227. package/dist/http/retry.js +26 -0
  228. package/dist/index.d.ts +4 -0
  229. package/dist/index.js +8 -0
  230. package/dist/next/ErrorBoundary.js +1 -1
  231. package/dist/repositories/index.d.ts +2 -0
  232. package/dist/repositories/index.js +2 -0
  233. package/dist/seed/site-settings-seed-data.js +476 -0
  234. package/dist/server.d.ts +4 -0
  235. package/dist/server.js +9 -0
  236. package/dist/styles.css +1 -1
  237. package/dist/tailwind-utilities.css +1 -1
  238. package/dist/tokens/index.d.ts +6 -6
  239. package/dist/tokens/index.js +59 -59
  240. package/dist/ui/DataTable.js +3 -3
  241. package/dist/ui/components/Avatar.js +1 -1
  242. package/dist/ui/components/BaseListingCard.js +7 -7
  243. package/dist/ui/components/Button.js +3 -3
  244. package/dist/ui/components/DetailPageGallery.js +3 -3
  245. package/dist/ui/components/DetailPageHero.js +1 -1
  246. package/dist/ui/components/DetailPageTabs.js +4 -4
  247. package/dist/ui/components/FilterChipGroup.js +2 -2
  248. package/dist/ui/components/Input.js +1 -1
  249. package/dist/ui/components/Layout.d.ts +2 -2
  250. package/dist/ui/components/ListingFilterDrawer.js +2 -2
  251. package/dist/ui/components/ListingToolbar.js +6 -6
  252. package/dist/ui/components/LoginRequiredModal.js +1 -1
  253. package/dist/ui/components/Modal.js +1 -1
  254. package/dist/ui/components/NavbarChevron.js +1 -1
  255. package/dist/ui/components/PageLoader.js +3 -3
  256. package/dist/ui/components/Radio.js +1 -1
  257. package/dist/ui/components/RichTextEditor.js +1 -1
  258. package/dist/ui/components/Select.js +1 -1
  259. package/dist/ui/components/SettingsSection.js +3 -3
  260. package/dist/ui/components/SideModal.js +1 -1
  261. package/dist/ui/components/Slider.js +2 -2
  262. package/dist/ui/components/StatsGrid.js +1 -1
  263. package/dist/ui/components/Typography.js +3 -3
  264. package/dist/ui/components/surface-tokens.d.ts +11 -11
  265. package/dist/ui/components/surface-tokens.js +12 -12
  266. package/dist/utils/safe-fire-forget.d.ts +14 -0
  267. package/dist/utils/safe-fire-forget.js +23 -0
  268. package/package.json +1 -1
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import Link from "next/link";
3
3
  import { Avatar, BlockHeader, Button, Div, Row, Section, SiteLogo, Span } from "../../ui";
4
4
  /** Shared icon-button class for all action buttons in the title bar. */
5
- const iconBtn = "flex items-center justify-center w-9 h-9 rounded-lg text-zinc-600 dark:text-zinc-400 hover:bg-primary-50 hover:text-primary-700 dark:hover:bg-slate-800 dark:hover:text-secondary-400 transition-colors";
5
+ const iconBtn = "flex items-center justify-center w-9 h-9 rounded-lg text-[var(--appkit-color-text-muted)] hover:bg-primary-50 hover:text-primary-700 hover:bg-[var(--appkit-color-surface-elevated)] dark:hover:text-secondary-400 transition-colors";
6
6
  /** Badge counter class for wishlist/cart counts. */
7
7
  const countBadge = "absolute -top-0.5 -right-0.5 flex items-center justify-center w-4 h-4 rounded-full bg-error-surface text-white text-[10px] font-bold leading-none";
8
8
  /**
@@ -30,10 +30,10 @@ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, s
30
30
  const wishlistEl = wishlistHref ? (_jsxs(Link, { href: wishlistHref, "aria-label": `Wishlist${wishlistCount > 0 ? `, ${wishlistCount} items` : ""}`, className: `relative ${iconBtn}`, children: [_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4.318 6.318a4.5 4.5 0 0 0 0 6.364L12 20.364l7.682-7.682a4.5 4.5 0 0 0-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 0 0-6.364 0z" }) }), wishlistCount > 0 && (_jsx(Span, { className: countBadge, children: wishlistCount > 9 ? "9+" : wishlistCount }))] })) : null;
31
31
  const cartEl = cartHref ? (_jsxs(Link, { href: cartHref, "aria-label": `Cart${cartCount > 0 ? `, ${cartCount} items` : ""}`, className: `relative ${iconBtn}`, children: [_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 0 0-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 0 0-16.536-1.84M7.5 14.25 5.106 5.272M6 20.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm12.75 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0z" }) }), cartCount > 0 && (_jsx(Span, { className: countBadge, children: cartCount > 9 ? "9+" : cartCount }))] })) : null;
32
32
  const profileEl = profileHref ? (_jsx(Link, { href: profileHref, "aria-label": user ? `Profile — ${user.displayName ?? user.email}` : "Sign in", className: `relative ${iconBtn}`, children: _jsx(Avatar, { src: user?.photoURL ?? undefined, name: user?.displayName ?? user?.email ?? undefined, alt: user?.displayName ?? "Profile", size: "sm" }) })) : null;
33
- const authButtonsEl = !user && (loginHref || registerHref) ? (_jsxs(Row, { gap: "xs", className: "hidden lg:flex", children: [loginHref && (_jsx(Link, { href: loginHref, className: "px-3 py-1.5 text-sm font-medium text-zinc-700 dark:text-zinc-300 hover:text-primary-700 dark:hover:text-secondary-400 transition-colors rounded-lg hover:bg-primary-50 dark:hover:bg-slate-800", children: "Sign in" })), registerHref && (_jsx(Link, { href: registerHref, className: "px-3 py-1.5 text-sm font-semibold rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors btn-glow shadow-sm", children: "Register" }))] })) : null;
33
+ const authButtonsEl = !user && (loginHref || registerHref) ? (_jsxs(Row, { gap: "xs", className: "hidden lg:flex", children: [loginHref && (_jsx(Link, { href: loginHref, className: "px-3 py-1.5 text-sm font-medium text-[var(--appkit-color-text-muted)] hover:text-primary-700 dark:hover:text-secondary-400 transition-colors rounded-lg hover:bg-primary-50 hover:bg-[var(--appkit-color-surface-elevated)]", children: "Sign in" })), registerHref && (_jsx(Link, { href: registerHref, className: "px-3 py-1.5 text-sm font-semibold rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors btn-glow shadow-sm", children: "Register" }))] })) : null;
34
34
  const hasTb2 = !!(notificationsEl || wishlistEl || cartEl || profileEl);
35
35
  // ── Render ───────────────────────────────────────────────────────────────────
36
- return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-white/95 dark:bg-slate-950/95 backdrop-blur-md border-b border-zinc-100 dark:border-slate-800 shadow-sm ${className}`, children: [promoStripText && (_jsx(Section, { color: "inverse", tone: "accent-banner", className: "text-xs text-center font-medium", padding: "y-2xs", children: promoStripText })), _jsxs(Div, { paddingX: "x-page", className: "container mx-auto max-w-[1920px]", children: [_jsxs(Row, { justify: "between", gap: "none", className: "relative h-14", children: [_jsx(Row, { gap: "3", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { title: brandName, size: "md" }) }) }), siteLogoUrl ? (_jsx(Row, { className: "absolute inset-y-0 left-1/2 -translate-x-1/2", align: "center", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { src: siteLogoUrl, title: brandName, size: "md" }) }) })) : (navSlot && _jsx(Div, { className: "hidden md:flex", children: navSlot })), _jsxs(Row, { gap: "xs", children: [devSlot, compareEl, notificationSlot, notificationsEl && _jsx(Div, { className: "hidden lg:flex", children: notificationsEl }), wishlistEl && _jsx(Div, { className: "hidden lg:flex", children: wishlistEl }), cartEl && _jsx(Div, { className: "hidden lg:flex", children: cartEl }), user
36
+ return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-white/95 bg-[var(--appkit-color-bg)]/95 backdrop-blur-md border-b border-zinc-100 border-[var(--appkit-color-border-subtle)] shadow-sm ${className}`, children: [promoStripText && (_jsx(Section, { color: "inverse", tone: "accent-banner", className: "text-xs text-center font-medium", padding: "y-2xs", children: promoStripText })), _jsxs(Div, { paddingX: "x-page", className: "container mx-auto max-w-[1920px]", children: [_jsxs(Row, { justify: "between", gap: "none", className: "relative h-14", children: [_jsx(Row, { gap: "3", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { title: brandName, size: "md" }) }) }), siteLogoUrl ? (_jsx(Row, { className: "absolute inset-y-0 left-1/2 -translate-x-1/2", align: "center", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { src: siteLogoUrl, title: brandName, size: "md" }) }) })) : (navSlot && _jsx(Div, { className: "hidden md:flex", children: navSlot })), _jsxs(Row, { gap: "xs", children: [devSlot, compareEl, notificationSlot, notificationsEl && _jsx(Div, { className: "hidden lg:flex", children: notificationsEl }), wishlistEl && _jsx(Div, { className: "hidden lg:flex", children: wishlistEl }), cartEl && _jsx(Div, { className: "hidden lg:flex", children: cartEl }), user
37
37
  ? profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })
38
38
  : authButtonsEl ?? (profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })), searchBtn, promotionsEl, tourBtn, themeBtn, hamburgerBtn] })] }), hasTb2 && (_jsxs(Row, { border: "subtle", as: "nav", "aria-label": "Account actions", justify: "end", gap: "xs", className: "flex lg:hidden h-10 border-t px-1", children: [notificationsEl, wishlistEl, cartEl, profileEl] }))] })] }));
39
39
  }
@@ -111,5 +111,5 @@ export function LiveItemsIndexListing({ initialData }) {
111
111
  useBottomActions(selection.selectedCount > 0 ? { bulk: { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [] } } : {});
112
112
  return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search live items\u2026", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: (e) => { if (e.key === "Enter")
113
113
  commitSearch(); }, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: (v) => { if (v !== "table")
114
- setView(v); }, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: products.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [] }), totalPages > 1 && (_jsx(Row, { border: "bottom", className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 backdrop-blur-sm", surface: "default", padding: "toolbar", justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Div, { className: gridClass, children: Array.from({ length: 10 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-square", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-1/3", surface: "subtle", rounded: "default" })] })] }, i))) })) : products.length === 0 ? (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "center", children: "No live item listings found." })) : (_jsx(Div, { className: view === "list" ? "flex flex-col divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800" : gridClass, children: products.map((product) => (_jsx(InteractiveProductCard, { product: product, href: String(ROUTES.PUBLIC.LIVE_DETAIL(product.id)), variant: view === "list" ? "list" : "grid", isWishlisted: wishlistActions.isWishlisted(product.id), onToggleWishlist: handleToggleWishlist, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: onFilterClear, activeCount: filterActiveCount, hideTrigger: true, children: _jsx(LiveItemFilters, { table: pendingTable, categoryOptions: categoryOptions }) }), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
114
+ setView(v); }, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: products.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [] }), totalPages > 1 && (_jsx(Row, { border: "bottom", className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 backdrop-blur-sm", surface: "default", padding: "toolbar", justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Div, { className: gridClass, children: Array.from({ length: 10 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-square", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-1/3", surface: "subtle", rounded: "default" })] })] }, i))) })) : products.length === 0 ? (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "center", children: "No live item listings found." })) : (_jsx(Div, { className: view === "list" ? "flex flex-col divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)] rounded-xl border border-[var(--appkit-color-border-subtle)]" : gridClass, children: products.map((product) => (_jsx(InteractiveProductCard, { product: product, href: String(ROUTES.PUBLIC.LIVE_DETAIL(product.id)), variant: view === "list" ? "list" : "grid", isWishlisted: wishlistActions.isWishlisted(product.id), onToggleWishlist: handleToggleWishlist, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: onFilterClear, activeCount: filterActiveCount, hideTrigger: true, children: _jsx(LiveItemFilters, { table: pendingTable, categoryOptions: categoryOptions }) }), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
115
115
  }
@@ -6,5 +6,5 @@ export function CoinsBadge({ coins, className }) {
6
6
  return (_jsxs(Span, { className: `${CLS_COIN_PILL} ${className ?? ""}`, children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83E\uDE99" }), coins.toLocaleString()] }));
7
7
  }
8
8
  export function CoinsDisplay({ coins, label = "HC Coins" }) {
9
- return (_jsxs(Stack, { align: "center", gap: "xs", children: [_jsx(Text, { className: CLS_COIN_TOTAL, children: coins.toLocaleString() }), _jsx(Text, { className: "text-gray-500", size: "sm", children: label })] }));
9
+ return (_jsxs(Stack, { align: "center", gap: "xs", children: [_jsx(Text, { className: CLS_COIN_TOTAL, children: coins.toLocaleString() }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: label })] }));
10
10
  }
@@ -1,4 +1,7 @@
1
+ "use client";
1
2
  import { useQuery } from "@tanstack/react-query";
3
+ import { apiClient } from "../../../http/ApiClient";
4
+ import { normalizeError } from "../../../errors/normalize";
2
5
  import { LOYALTY_ENDPOINTS } from "../../../constants/api-endpoints";
3
6
  export function useLoyaltyBalance(uid, opts) {
4
7
  return useQuery({
@@ -7,10 +10,15 @@ export function useLoyaltyBalance(uid, opts) {
7
10
  if (!uid)
8
11
  return null;
9
12
  const endpoint = opts?.endpoint ?? LOYALTY_ENDPOINTS.BALANCE(uid);
10
- const res = await fetch(endpoint);
11
- if (!res.ok)
13
+ try {
14
+ return await apiClient.get(endpoint);
15
+ }
16
+ catch (err) {
17
+ // loyalty balance is a display feature — treat any fetch failure as null
18
+ // so the loyalty section degrades gracefully without blocking the page
19
+ void normalizeError(err);
12
20
  return null;
13
- return res.json();
21
+ }
14
22
  },
15
23
  enabled: !!uid,
16
24
  });
@@ -109,6 +109,6 @@ export function AvatarUpload({ currentPhotoURL, currentCropData, userId, display
109
109
  fileInputRef.current.value = "";
110
110
  };
111
111
  const hasPending = pendingCropData !== null && pendingUploadFile !== null;
112
- return (_jsxs(_Fragment, { children: [_jsxs(Stack, { gap: "md", children: [_jsxs(Row, { align: "start", gap: "lg", children: [_jsx(Div, { className: "shrink-0", children: _jsx(AvatarDisplay, { cropData: hasPending ? pendingCropData : cropData, size: "2xl", className: `border-4 ${hasPending ? "border-primary/60 ring-2 ring-primary/20" : "border-zinc-200 dark:border-slate-700"}`, displayName: displayName }) }), _jsxs(Stack, { className: "flex-1", gap: "3", children: [_jsx(Text, { variant: "secondary", size: "xs", children: "JPG, PNG, WEBP or GIF up to 10MB." }), isUploading ? (_jsx(Progress, { value: 50, variant: "primary", size: "sm", label: t.uploading })) : null, hasPending && !isUploading ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Text, { variant: "secondary", className: "text-primary", size: "xs", children: t.readyToSave }), _jsxs(Row, { gap: "3", children: [_jsx(Button, { type: "button", onClick: handleConfirmSave, size: "sm", children: t.saveAvatar }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: handleCancelPending, children: t.cancelChange })] })] })) : null, !hasPending && !isUploading ? (_jsxs(Row, { gap: "3", children: [_jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), size: "sm", children: previewUrl ? t.changePhoto : t.chooseImage }), previewUrl ? (_jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: handleRemovePhoto, children: t.removePhoto })) : null] })) : null, _jsx(Input, { ref: fileInputRef, bare: true, type: "file", accept: "image/*", onChange: handleFileSelect, className: "hidden", "aria-label": t.changePhoto })] })] }), uploadApiError ? (_jsx(Alert, { variant: "error", children: uploadApiError.message })) : null] }), showCropModal && tempImageUrl ? (_jsx(ImageCropModal, { isOpen: showCropModal, imageUrl: tempImageUrl, onClose: handleCropCancel, onSave: handleCropSave, initialCropData: cropData || undefined })) : null] }));
112
+ return (_jsxs(_Fragment, { children: [_jsxs(Stack, { gap: "md", children: [_jsxs(Row, { align: "start", gap: "lg", children: [_jsx(Div, { className: "shrink-0", children: _jsx(AvatarDisplay, { cropData: hasPending ? pendingCropData : cropData, size: "2xl", className: `border-4 ${hasPending ? "border-primary/60 ring-2 ring-primary/20" : "border-[var(--appkit-color-border)]"}`, displayName: displayName }) }), _jsxs(Stack, { className: "flex-1", gap: "3", children: [_jsx(Text, { variant: "secondary", size: "xs", children: "JPG, PNG, WEBP or GIF up to 10MB." }), isUploading ? (_jsx(Progress, { value: 50, variant: "primary", size: "sm", label: t.uploading })) : null, hasPending && !isUploading ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Text, { variant: "secondary", className: "text-primary", size: "xs", children: t.readyToSave }), _jsxs(Row, { gap: "3", children: [_jsx(Button, { type: "button", onClick: handleConfirmSave, size: "sm", children: t.saveAvatar }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: handleCancelPending, children: t.cancelChange })] })] })) : null, !hasPending && !isUploading ? (_jsxs(Row, { gap: "3", children: [_jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), size: "sm", children: previewUrl ? t.changePhoto : t.chooseImage }), previewUrl ? (_jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: handleRemovePhoto, children: t.removePhoto })) : null] })) : null, _jsx(Input, { ref: fileInputRef, bare: true, type: "file", accept: "image/*", onChange: handleFileSelect, className: "hidden", "aria-label": t.changePhoto })] })] }), uploadApiError ? (_jsx(Alert, { variant: "error", children: uploadApiError.message })) : null] }), showCropModal && tempImageUrl ? (_jsx(ImageCropModal, { isOpen: showCropModal, imageUrl: tempImageUrl, onClose: handleCropCancel, onSave: handleCropSave, initialCropData: cropData || undefined })) : null] }));
113
113
  }
114
114
  export default AvatarUpload;
@@ -125,7 +125,7 @@ export function MediaPickerModal({ isOpen, onClose, onSelect, accept = "image/*"
125
125
  "rounded-lg border overflow-hidden text-left transition p-0 h-auto",
126
126
  isSelected
127
127
  ? "border-primary-500 ring-2 ring-primary-300"
128
- : "border-zinc-200 dark:border-slate-700",
128
+ : "border-[var(--appkit-color-border)]",
129
129
  ].join(" "), "aria-pressed": isSelected, children: [_jsx(Row, { className: "aspect-square w-full", surface: "subtle", align: "center", justify: "center", children: isImage ? (
130
130
  // eslint-disable-next-line @next/next/no-img-element
131
131
  _jsx("img", { src: f.downloadURL, alt: f.name, className: "h-full w-full object-cover", loading: "lazy" })) : (_jsx(Text, { size: "sm", color: "muted", children: f.contentType ?? "file" })) }), _jsx(Text, { className: "block truncate p-1.5 font-mono", size: "xs", children: f.name.split("/").pop() })] }, f.name));
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  export function MediaSlider({ value, min = 0, max = 100, step = 1, onChange, onChangeEnd, className = "", }) {
3
3
  return (_jsx("input", { type: "range", min: min, max: max, step: step, value: value, onChange: (e) => onChange?.(parseFloat(e.target.value)), onMouseUp: (e) => onChangeEnd?.(parseFloat(e.target.value)), onTouchEnd: (e) => onChangeEnd?.(parseFloat(e.target.value)), className: [
4
4
  "w-full h-2 appearance-none cursor-pointer rounded-full",
5
- "bg-zinc-200 dark:bg-slate-700",
5
+ "bg-zinc-200 bg-[var(--appkit-color-border)]",
6
6
  "accent-primary-600 dark:accent-secondary-400",
7
7
  className,
8
8
  ]
@@ -3,7 +3,18 @@ import type { MediaField } from "./types";
3
3
  * Extract a storage object path from public/download URLs for the same bucket.
4
4
  */
5
5
  export declare function extractStoragePathFromUrl(url: string, bucketName: string): string | null;
6
- /** Promote staged tmp/* uploads to canonical media/* path. */
6
+ /**
7
+ * Promote staged tmp/* uploads to canonical media/* path.
8
+ *
9
+ * Handles two URL formats:
10
+ * 1. `/media/{shortId}` — the new SEO-friendly short-ID format.
11
+ * Looks up the asset in the mediaAssets Firestore collection, moves the
12
+ * file from tmp/ → media/, updates the storagePath, and returns the same
13
+ * URL unchanged (the URL is stable across the move).
14
+ * 2. Raw GCS URLs (`https://storage.googleapis.com/...` or Firebase Storage URLs) —
15
+ * legacy path kept for backward compatibility; moves the file and returns a
16
+ * /media/{finalStoragePath} proxy URL.
17
+ */
7
18
  export declare function finalizeStagedMediaUrl(url: string): Promise<string>;
8
19
  export declare function finalizeStagedMediaField(url: string | null | undefined): Promise<string | undefined>;
9
20
  export declare function finalizeStagedMediaArray(urls: string[] | null | undefined): Promise<string[]>;
@@ -1,7 +1,9 @@
1
1
  import { getAdminStorage } from "../../providers/db-firebase";
2
2
  import { FIREBASE_STORAGE_HOST, GCS_HOST } from "../../utils/media-url";
3
+ import { mediaAssetsRepository } from "./repository/media-assets.repository";
3
4
  const TMP_MEDIA_PREFIX = "tmp/";
4
5
  const FINAL_MEDIA_PREFIX = "media/";
6
+ const PROXY_URL_PREFIX = "/media/";
5
7
  /**
6
8
  * Extract a storage object path from public/download URLs for the same bucket.
7
9
  */
@@ -26,24 +28,75 @@ export function extractStoragePathFromUrl(url, bucketName) {
26
28
  return null;
27
29
  }
28
30
  }
29
- /** Promote staged tmp/* uploads to canonical media/* path. */
30
- export async function finalizeStagedMediaUrl(url) {
31
+ /**
32
+ * Move a file from tmp/ to media/ in Firebase Storage.
33
+ * Returns the new storage path.
34
+ */
35
+ async function moveToFinalPath(sourcePath) {
31
36
  const storage = getAdminStorage();
32
37
  const bucket = storage.bucket();
33
- const sourcePath = extractStoragePathFromUrl(url, bucket.name);
34
- if (!sourcePath || !sourcePath.startsWith(TMP_MEDIA_PREFIX)) {
35
- return url;
36
- }
37
38
  const destinationPath = sourcePath.replace(TMP_MEDIA_PREFIX, FINAL_MEDIA_PREFIX);
38
- if (destinationPath === sourcePath) {
39
- return url;
40
- }
39
+ if (destinationPath === sourcePath)
40
+ return sourcePath;
41
41
  const sourceFile = bucket.file(sourcePath);
42
42
  const destinationFile = bucket.file(destinationPath);
43
43
  await sourceFile.copy(destinationFile);
44
- await destinationFile.makePublic();
45
44
  await sourceFile.delete({ ignoreNotFound: true });
46
- return `https://storage.googleapis.com/${bucket.name}/${destinationPath}`;
45
+ return destinationPath;
46
+ }
47
+ /**
48
+ * Promote staged tmp/* uploads to canonical media/* path.
49
+ *
50
+ * Handles two URL formats:
51
+ * 1. `/media/{shortId}` — the new SEO-friendly short-ID format.
52
+ * Looks up the asset in the mediaAssets Firestore collection, moves the
53
+ * file from tmp/ → media/, updates the storagePath, and returns the same
54
+ * URL unchanged (the URL is stable across the move).
55
+ * 2. Raw GCS URLs (`https://storage.googleapis.com/...` or Firebase Storage URLs) —
56
+ * legacy path kept for backward compatibility; moves the file and returns a
57
+ * /media/{finalStoragePath} proxy URL.
58
+ */
59
+ export async function finalizeStagedMediaUrl(url) {
60
+ // ── Path 1: /media/{shortId} proxy URLs (new system) ──────────────────────
61
+ if (url.startsWith(PROXY_URL_PREFIX)) {
62
+ const afterPrefix = url.slice(PROXY_URL_PREFIX.length);
63
+ // Single-segment = shortId. Multi-segment = legacy raw path embedded in URL.
64
+ if (!afterPrefix.includes("/")) {
65
+ const shortId = afterPrefix;
66
+ try {
67
+ const asset = await mediaAssetsRepository.findById(shortId);
68
+ if (asset && asset.status === "staged" && asset.storagePath.startsWith(TMP_MEDIA_PREFIX)) {
69
+ const finalPath = await moveToFinalPath(asset.storagePath);
70
+ await mediaAssetsRepository.promoteToFinalized(shortId, finalPath);
71
+ }
72
+ }
73
+ catch {
74
+ // Non-fatal: URL is still valid; proxy will serve whatever storagePath points to.
75
+ }
76
+ return url; // URL is stable — same before and after the file move
77
+ }
78
+ // Multi-segment legacy: /media/tmp/folder/uid/file.jpg → move + return proxy URL
79
+ const rawPath = afterPrefix;
80
+ if (rawPath.startsWith(TMP_MEDIA_PREFIX)) {
81
+ try {
82
+ const finalPath = await moveToFinalPath(rawPath);
83
+ return `${PROXY_URL_PREFIX}${finalPath}`;
84
+ }
85
+ catch {
86
+ return url;
87
+ }
88
+ }
89
+ return url; // already at media/ or other path — no-op
90
+ }
91
+ // ── Path 2: Legacy raw GCS URLs ────────────────────────────────────────────
92
+ const storage = getAdminStorage();
93
+ const bucket = storage.bucket();
94
+ const sourcePath = extractStoragePathFromUrl(url, bucket.name);
95
+ if (!sourcePath || !sourcePath.startsWith(TMP_MEDIA_PREFIX)) {
96
+ return url;
97
+ }
98
+ const finalPath = await moveToFinalPath(sourcePath);
99
+ return `${PROXY_URL_PREFIX}${finalPath}`;
47
100
  }
48
101
  export async function finalizeStagedMediaField(url) {
49
102
  if (!url)
@@ -95,5 +95,5 @@ export function ImageCropModal({ isOpen, imageUrl, onClose, onSave, initialCropD
95
95
  left: `${position.x}%`,
96
96
  top: `${position.y}%`,
97
97
  transform: "translate(-50%, -50%)",
98
- } }), _jsxs(Div, { className: "absolute inset-0 pointer-events-none", children: [_jsx(Div, { layout: "grid", className: "absolute w-full h-full grid-cols-3 grid-rows-3", children: Array.from({ length: 9 }).map((_, i) => (_jsx(Div, { className: "border border-white/20 dark:border-slate-600/30" }, i))) }), _jsxs(Row, { className: "absolute inset-0", align: "center", justify: "center", children: [_jsx(Div, { className: "w-6 h-px", surface: "default" }), _jsx(Div, { className: "absolute h-6 w-px", surface: "default" })] })] })] }), _jsxs(Stack, { gap: "xs", children: [_jsxs(Row, { justify: "between", children: [_jsx(Text, { size: "xs", weight: "medium", children: t("cropZoom") }), _jsxs(Text, { size: "xs", color: "muted", children: [Math.round(zoom * 100), "%"] })] }), _jsxs(Row, { gap: "3", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setZoom(Math.max(0.1, zoom - 0.1)), "aria-label": t("cropZoomOut"), children: _jsx("svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10H7" }) }) }), _jsx(MediaSlider, { value: zoom, min: 0.1, max: 3, step: 0.01, onChange: (v) => setZoom(v), className: "flex-1" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setZoom(Math.min(3, zoom + 0.1)), "aria-label": t("cropZoomIn"), children: _jsx("svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7" }) }) })] }), _jsx(Row, { gap: "sm", children: [0.5, 1, 1.5, 2].map((preset) => (_jsxs(Button, { rounded: "lg", type: "button", variant: "ghost", size: "sm", onClick: () => setZoom(preset), className: "px-3 py-1 text-xs bg-zinc-100 dark:bg-slate-800", children: [preset * 100, "%"] }, preset))) })] }), showZoomWarning && (_jsxs(Alert, { variant: "warning", children: [_jsx(Span, { weight: "bold", children: t("cropWarningTitle") }), " ", t("cropWarningMessage")] })), _jsxs(Row, { textSize: "xs", justify: "between", className: "text-zinc-500 dark:text-slate-400", children: [_jsxs(Span, { children: [t("cropPosition"), ": ", Math.round(position.x), "%, ", Math.round(position.y), "%"] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: handleReset, className: "text-primary-600 dark:text-primary-400 hover:underline", children: t("cropReset") })] }), _jsxs(Row, { gap: "sm", padding: "t-xs", children: [_jsx(Button, { onClick: handleSave, variant: "primary", className: "flex-1", children: t("cropSave") }), _jsx(Button, { onClick: onClose, variant: "secondary", className: "flex-1", children: t("cropCancel") })] })] }) }));
98
+ } }), _jsxs(Div, { className: "absolute inset-0 pointer-events-none", children: [_jsx(Div, { layout: "grid", className: "absolute w-full h-full grid-cols-3 grid-rows-3", children: Array.from({ length: 9 }).map((_, i) => (_jsx(Div, { className: "border border-white/20 border-[var(--appkit-color-border)]/30" }, i))) }), _jsxs(Row, { className: "absolute inset-0", align: "center", justify: "center", children: [_jsx(Div, { className: "w-6 h-px", surface: "default" }), _jsx(Div, { className: "absolute h-6 w-px", surface: "default" })] })] })] }), _jsxs(Stack, { gap: "xs", children: [_jsxs(Row, { justify: "between", children: [_jsx(Text, { size: "xs", weight: "medium", children: t("cropZoom") }), _jsxs(Text, { size: "xs", color: "muted", children: [Math.round(zoom * 100), "%"] })] }), _jsxs(Row, { gap: "3", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setZoom(Math.max(0.1, zoom - 0.1)), "aria-label": t("cropZoomOut"), children: _jsx("svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10H7" }) }) }), _jsx(MediaSlider, { value: zoom, min: 0.1, max: 3, step: 0.01, onChange: (v) => setZoom(v), className: "flex-1" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setZoom(Math.min(3, zoom + 0.1)), "aria-label": t("cropZoomIn"), children: _jsx("svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7" }) }) })] }), _jsx(Row, { gap: "sm", children: [0.5, 1, 1.5, 2].map((preset) => (_jsxs(Button, { rounded: "lg", type: "button", variant: "ghost", size: "sm", onClick: () => setZoom(preset), className: "px-3 py-1 text-xs bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)]", children: [preset * 100, "%"] }, preset))) })] }), showZoomWarning && (_jsxs(Alert, { variant: "warning", children: [_jsx(Span, { weight: "bold", children: t("cropWarningTitle") }), " ", t("cropWarningMessage")] })), _jsxs(Row, { textSize: "xs", justify: "between", className: "text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)]", children: [_jsxs(Span, { children: [t("cropPosition"), ": ", Math.round(position.x), "%, ", Math.round(position.y), "%"] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: handleReset, className: "text-primary-600 dark:text-primary-400 hover:underline", children: t("cropReset") })] }), _jsxs(Row, { gap: "sm", padding: "t-xs", children: [_jsx(Button, { onClick: handleSave, variant: "primary", className: "flex-1", children: t("cropSave") }), _jsx(Button, { onClick: onClose, variant: "secondary", className: "flex-1", children: t("cropCancel") })] })] }) }));
99
99
  }
@@ -75,5 +75,5 @@ export function ImageEditor({ isOpen, imageUrl, onClose, onSave, aspectRatio, ou
75
75
  { label: "4:3", ratio: 4 / 3 },
76
76
  { label: "16:9", ratio: 16 / 9 },
77
77
  { label: "3:2", ratio: 3 / 2 },
78
- ].map((preset) => (_jsx(Button, { rounded: "lg", type: "button", variant: "ghost", size: "sm", onClick: () => applyAspectRatio(preset.ratio), className: "px-3 py-1 text-xs bg-zinc-100 dark:bg-slate-800", children: preset.label }, preset.label))) })), _jsxs(Row, { gap: "sm", padding: "t-xs", children: [_jsx(Button, { onClick: handleSave, variant: "primary", className: "flex-1", disabled: saving, children: saving ? t("cropSaving") : t("cropSave") }), _jsx(Button, { onClick: onClose, variant: "secondary", className: "flex-1", children: t("cropCancel") })] })] }) }));
78
+ ].map((preset) => (_jsx(Button, { rounded: "lg", type: "button", variant: "ghost", size: "sm", onClick: () => applyAspectRatio(preset.ratio), className: "px-3 py-1 text-xs bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)]", children: preset.label }, preset.label))) })), _jsxs(Row, { gap: "sm", padding: "t-xs", children: [_jsx(Button, { onClick: handleSave, variant: "primary", className: "flex-1", disabled: saving, children: saving ? t("cropSaving") : t("cropSave") }), _jsx(Button, { onClick: onClose, variant: "secondary", className: "flex-1", children: t("cropCancel") })] })] }) }));
79
79
  }
@@ -0,0 +1,33 @@
1
+ import { BaseRepository } from "../../../providers/db-firebase";
2
+ export interface MediaAssetDocument {
3
+ /** SEO-friendly slug used in /media/{id} URLs (e.g. "product-image-charizard-1-20260805"). */
4
+ id: string;
5
+ /** Actual path inside the Firebase Storage bucket. Changes from tmp/ → media/ on finalize. */
6
+ storagePath: string;
7
+ /** Firebase Auth UID of the uploader. */
8
+ uploadedBy: string;
9
+ contentType: string;
10
+ /** File size in bytes. */
11
+ size: number;
12
+ isPublic: boolean;
13
+ /** staged = still in tmp/; finalized = moved to media/ by finalizeStagedMediaUrl. */
14
+ status: "staged" | "finalized";
15
+ createdAt: Date;
16
+ }
17
+ export type MediaAssetCreateInput = Omit<MediaAssetDocument, "id" | "createdAt">;
18
+ export declare class MediaAssetsRepository extends BaseRepository<MediaAssetDocument> {
19
+ constructor();
20
+ findByStoragePath(storagePath: string): Promise<MediaAssetDocument | null>;
21
+ /**
22
+ * Create a new mediaAssets document with a collision-safe shortId.
23
+ * Tries `preferredShortId` first, then appends a random 4-char suffix up to
24
+ * MAX_ATTEMPTS times. Falls back to a base-36 timestamp suffix.
25
+ */
26
+ createAsset(preferredShortId: string, data: MediaAssetCreateInput): Promise<MediaAssetDocument>;
27
+ /**
28
+ * Update storagePath + status when the file is promoted from tmp/ to media/.
29
+ * Called by finalizeStagedMediaUrl.
30
+ */
31
+ promoteToFinalized(shortId: string, finalStoragePath: string): Promise<void>;
32
+ }
33
+ export declare const mediaAssetsRepository: MediaAssetsRepository;
@@ -0,0 +1,43 @@
1
+ import { BaseRepository } from "../../../providers/db-firebase";
2
+ function generateRand4() {
3
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
4
+ const indices = new Uint8Array(4);
5
+ globalThis.crypto.getRandomValues(indices);
6
+ return Array.from(indices, (i) => chars[i % chars.length]).join("");
7
+ }
8
+ export class MediaAssetsRepository extends BaseRepository {
9
+ constructor() {
10
+ super("mediaAssets");
11
+ }
12
+ async findByStoragePath(storagePath) {
13
+ return this.findOneBy("storagePath", storagePath);
14
+ }
15
+ /**
16
+ * Create a new mediaAssets document with a collision-safe shortId.
17
+ * Tries `preferredShortId` first, then appends a random 4-char suffix up to
18
+ * MAX_ATTEMPTS times. Falls back to a base-36 timestamp suffix.
19
+ */
20
+ async createAsset(preferredShortId, data) {
21
+ const MAX_ATTEMPTS = 5;
22
+ for (let i = 0; i < MAX_ATTEMPTS; i++) {
23
+ const shortId = i === 0 ? preferredShortId : `${preferredShortId}-${generateRand4()}`;
24
+ const existing = await this.findById(shortId);
25
+ if (!existing) {
26
+ return this.createWithId(shortId, data);
27
+ }
28
+ }
29
+ const fallbackId = `${preferredShortId}-${Date.now().toString(36)}`;
30
+ return this.createWithId(fallbackId, data);
31
+ }
32
+ /**
33
+ * Update storagePath + status when the file is promoted from tmp/ to media/.
34
+ * Called by finalizeStagedMediaUrl.
35
+ */
36
+ async promoteToFinalized(shortId, finalStoragePath) {
37
+ await this.update(shortId, {
38
+ storagePath: finalStoragePath,
39
+ status: "finalized",
40
+ });
41
+ }
42
+ }
43
+ export const mediaAssetsRepository = new MediaAssetsRepository();
@@ -367,5 +367,5 @@ kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText
367
367
  return (_jsxs(Stack, { gap: "sm", children: [_jsx(Label, { className: "block", color: "muted", size: "sm", weight: "medium", children: label }), hasAlternateSources && !disabled && (_jsx(Row, { gap: "none", className: "appkit-media-upload__source-tabs", children: ["upload", ...(showYoutube ? ["youtube"] : []), ...(showExternal ? ["external"] : [])].map((tab) => (_jsx("button", { type: "button", onClick: () => setSourceTab(tab), className: [
368
368
  "appkit-media-upload__source-tab",
369
369
  sourceTab === tab ? "appkit-media-upload__source-tab--active" : "",
370
- ].filter(Boolean).join(" "), children: tab === "upload" ? "Upload" : tab === "youtube" ? "YouTube" : "External URL" }, tab))) })), hasAlternateSources && sourceTab === "youtube" && !disabled && (_jsx(YoutubeTabPanel, { ytInput: ytInput, setYtInput: setYtInput, ytError: ytError, setYtError: setYtError, onApply: handleYtApply, value: value })), hasAlternateSources && sourceTab === "external" && !disabled && (_jsx(ExternalUrlTabPanel, { extInput: extInput, setExtInput: setExtInput, extError: extError, setExtError: setExtError, onApply: handleExtApply })), (!hasAlternateSources || sourceTab === "upload") && value && !isLoading && (_jsx(MediaPreviewPanel, { value: value, label: label, disabled: disabled, enableTrim: enableTrim, enableThumbnail: enableThumbnail, onEditVideo: handleEditVideo, onRemove: handleRemove, tUpload: tUpload, tMediaEditor: tMediaEditor })), !disabled && !isLoading && (!hasAlternateSources || sourceTab === "upload") && (_jsxs(_Fragment, { children: [captureSource === "both" && isCameraSupported && (_jsxs(Row, { gap: "sm", children: [_jsx(Button, { type: "button", variant: inputMode === "file" ? "primary" : "outline", size: "sm", onClick: () => setInputMode("file"), children: t("switchToUpload") }), _jsx(Button, { type: "button", variant: inputMode === "camera" ? "primary" : "outline", size: "sm", onClick: () => setInputMode("camera"), children: t("switchToCamera") })] })), showCamera && isCameraSupported && (_jsx(CameraCapture, { mode: captureMode, facingMode: "environment", onCapture: handleCameraCapture, onError: (msg) => setError(msg) })), showCamera && !isCameraSupported && (_jsx(Button, { rounded: "xl", type: "button", onClick: () => mobileCaptureRef.current?.click(), variant: "ghost", className: "w-full py-3 border-2 border-dashed border-zinc-200 dark:border-zinc-700 text-sm text-zinc-500 dark:text-zinc-400", children: t("switchToCamera") })), showFileInput && (_jsx(Button, { rounded: "xl", type: "button", onClick: () => fileInputRef.current?.click(), variant: "ghost", className: "w-full py-3 border-2 border-dashed border-zinc-200 dark:border-zinc-700 text-sm text-zinc-500 dark:text-zinc-400", children: value ? tUpload("replaceFile") : tUpload("chooseFile") }))] })), isLoading && (_jsxs(Row, { gap: "sm", children: [_jsx(Spinner, { size: "sm" }), _jsx(Text, { size: "sm", variant: "secondary", children: tUpload("uploading") })] })), _jsx("input", { ref: fileInputRef, type: "file", accept: accept, multiple: multiple, onChange: handleFileChange, className: "hidden", "aria-hidden": "true", "data-testid": "media-upload-input" }), showCamera && !isCameraSupported && (_jsx("input", { ref: mobileCaptureRef, type: "file", accept: captureModeAccept, capture: "environment", onChange: handleFileChange, className: "hidden", "aria-hidden": "true", "data-testid": "media-upload-capture-input" })), helperText && !error && (_jsx(Text, { variant: "secondary", size: "xs", children: helperText })), error && _jsx(Alert, { variant: "error", children: error }), pendingVideoUrl && (_jsx(VideoTrimModal, { isOpen: showTrimModal, videoUrl: pendingVideoUrl, onClose: handleTrimClose, onSave: handleTrimSave })), pendingVideoUrl && (_jsx(VideoThumbnailSelector, { isOpen: showThumbnailModal, videoUrl: pendingVideoUrl, onClose: handleThumbnailClose, onSelect: handleThumbnailSelect, onUpload: onUpload }))] }));
370
+ ].filter(Boolean).join(" "), children: tab === "upload" ? "Upload" : tab === "youtube" ? "YouTube" : "External URL" }, tab))) })), hasAlternateSources && sourceTab === "youtube" && !disabled && (_jsx(YoutubeTabPanel, { ytInput: ytInput, setYtInput: setYtInput, ytError: ytError, setYtError: setYtError, onApply: handleYtApply, value: value })), hasAlternateSources && sourceTab === "external" && !disabled && (_jsx(ExternalUrlTabPanel, { extInput: extInput, setExtInput: setExtInput, extError: extError, setExtError: setExtError, onApply: handleExtApply })), (!hasAlternateSources || sourceTab === "upload") && value && !isLoading && (_jsx(MediaPreviewPanel, { value: value, label: label, disabled: disabled, enableTrim: enableTrim, enableThumbnail: enableThumbnail, onEditVideo: handleEditVideo, onRemove: handleRemove, tUpload: tUpload, tMediaEditor: tMediaEditor })), !disabled && !isLoading && (!hasAlternateSources || sourceTab === "upload") && (_jsxs(_Fragment, { children: [captureSource === "both" && isCameraSupported && (_jsxs(Row, { gap: "sm", children: [_jsx(Button, { type: "button", variant: inputMode === "file" ? "primary" : "outline", size: "sm", onClick: () => setInputMode("file"), children: t("switchToUpload") }), _jsx(Button, { type: "button", variant: inputMode === "camera" ? "primary" : "outline", size: "sm", onClick: () => setInputMode("camera"), children: t("switchToCamera") })] })), showCamera && isCameraSupported && (_jsx(CameraCapture, { mode: captureMode, facingMode: "environment", onCapture: handleCameraCapture, onError: (msg) => setError(msg) })), showCamera && !isCameraSupported && (_jsx(Button, { rounded: "xl", type: "button", onClick: () => mobileCaptureRef.current?.click(), variant: "ghost", className: "w-full py-3 border-2 border-dashed border-[var(--appkit-color-border)] text-sm text-[var(--appkit-color-text-muted)]", children: t("switchToCamera") })), showFileInput && (_jsx(Button, { rounded: "xl", type: "button", onClick: () => fileInputRef.current?.click(), variant: "ghost", className: "w-full py-3 border-2 border-dashed border-[var(--appkit-color-border)] text-sm text-[var(--appkit-color-text-muted)]", children: value ? tUpload("replaceFile") : tUpload("chooseFile") }))] })), isLoading && (_jsxs(Row, { gap: "sm", children: [_jsx(Spinner, { size: "sm" }), _jsx(Text, { size: "sm", variant: "secondary", children: tUpload("uploading") })] })), _jsx("input", { ref: fileInputRef, type: "file", accept: accept, multiple: multiple, onChange: handleFileChange, className: "hidden", "aria-hidden": "true", "data-testid": "media-upload-input" }), showCamera && !isCameraSupported && (_jsx("input", { ref: mobileCaptureRef, type: "file", accept: captureModeAccept, capture: "environment", onChange: handleFileChange, className: "hidden", "aria-hidden": "true", "data-testid": "media-upload-capture-input" })), helperText && !error && (_jsx(Text, { variant: "secondary", size: "xs", children: helperText })), error && _jsx(Alert, { variant: "error", children: error }), pendingVideoUrl && (_jsx(VideoTrimModal, { isOpen: showTrimModal, videoUrl: pendingVideoUrl, onClose: handleTrimClose, onSave: handleTrimSave })), pendingVideoUrl && (_jsx(VideoThumbnailSelector, { isOpen: showThumbnailModal, videoUrl: pendingVideoUrl, onClose: handleThumbnailClose, onSelect: handleThumbnailSelect, onUpload: onUpload }))] }));
371
371
  }
@@ -13,6 +13,7 @@ import { useCallback, useEffect, useState } from "react";
13
13
  import { getClientRealtimeProvider } from "../../../contracts/client-realtime";
14
14
  import { conversationPingPath, userConversationsPingPath, } from "../realtime";
15
15
  import { normalizeError } from "../../../errors/normalize";
16
+ import { apiClient, ApiClientError } from "../../../http/ApiClient";
16
17
  /**
17
18
  * @deprecated Re-exported for backwards compat — import `conversationPingPath`
18
19
  * from `@mohasinac/appkit` instead.
@@ -27,13 +28,14 @@ const DETAIL_ENDPOINT = (id) => `/api/user/conversations/${encodeURIComponent(id
27
28
  const SEND_ENDPOINT = (id) => `/api/user/conversations/${encodeURIComponent(id)}/messages`;
28
29
  const READ_ENDPOINT = (id) => `/api/user/conversations/${encodeURIComponent(id)}/read`;
29
30
  async function fetchDetail(id) {
30
- const res = await fetch(DETAIL_ENDPOINT(id), { credentials: "include" });
31
- if (res.status === 404)
32
- return null;
33
- if (!res.ok)
34
- throw new Error(`Failed to load conversation (${res.status})`);
35
- const json = (await res.json());
36
- return json.data ?? null;
31
+ try {
32
+ return await apiClient.get(DETAIL_ENDPOINT(id));
33
+ }
34
+ catch (err) {
35
+ if (err instanceof ApiClientError && err.status === 404)
36
+ return null;
37
+ throw err;
38
+ }
37
39
  }
38
40
  export function useConversation(conversationId) {
39
41
  const [conversation, setConversation] = useState(null);
@@ -88,15 +90,7 @@ export function useConversation(conversationId) {
88
90
  const trimmed = body.trim();
89
91
  if (!trimmed)
90
92
  return;
91
- const res = await fetch(SEND_ENDPOINT(conversationId), {
92
- method: "POST",
93
- credentials: "include",
94
- headers: { "Content-Type": "application/json" },
95
- body: JSON.stringify({ body: trimmed }),
96
- });
97
- if (!res.ok) {
98
- throw new Error(`Failed to send message (${res.status})`);
99
- }
93
+ await apiClient.post(SEND_ENDPOINT(conversationId), { body: trimmed });
100
94
  // RTDB ping will trigger refetch — but kick off one immediately in case
101
95
  // the ping path isn't subscribed (e.g. provider not registered).
102
96
  await refetch();
@@ -104,7 +98,8 @@ export function useConversation(conversationId) {
104
98
  const markRead = useCallback(() => {
105
99
  if (!conversationId)
106
100
  return Promise.resolve();
107
- return fetch(READ_ENDPOINT(conversationId), { method: "POST", credentials: "include" })
101
+ return apiClient
102
+ .post(READ_ENDPOINT(conversationId))
108
103
  .then(() => void refetch())
109
104
  .catch((err) => void normalizeError(err));
110
105
  }, [conversationId, refetch]);
@@ -8,13 +8,11 @@ import { useCallback, useEffect, useState } from "react";
8
8
  import { getClientRealtimeProvider } from "../../../contracts/client-realtime";
9
9
  import { userConversationsPingPath } from "../realtime";
10
10
  import { normalizeError } from "../../../errors/normalize";
11
+ import { apiClient } from "../../../http/ApiClient";
11
12
  const LIST_ENDPOINT = "/api/user/conversations";
12
13
  async function fetchList() {
13
- const res = await fetch(LIST_ENDPOINT, { credentials: "include" });
14
- if (!res.ok)
15
- throw new Error(`Failed to load conversations (${res.status})`);
16
- const json = (await res.json());
17
- return json.data?.items ?? [];
14
+ const data = await apiClient.get(LIST_ENDPOINT);
15
+ return data.items ?? [];
18
16
  }
19
17
  export function useConversations(userId) {
20
18
  const [conversations, setConversations] = useState([]);
@@ -48,7 +48,7 @@ export function MarketplaceOrderCard({ order, links, labels, className = "", var
48
48
  const longPress = useLongPress(() => onSelect?.(orderId, !isSelected));
49
49
  const isListVariant = variant === "list";
50
50
  const itemCount = order.items?.length ?? 0;
51
- return (_jsxs("div", { className: `group relative overflow-hidden rounded-xl border border-zinc-200 bg-white transition-all duration-200 hover:shadow-md dark:border-slate-800 dark:bg-slate-900 ${isSelected ? "ring-2 ring-primary-500 dark:ring-primary-400" : ""} ${className}`, onMouseDown: onSelect && !isSelected ? longPress.onMouseDown : undefined, onMouseUp: onSelect && !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: onSelect && !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: onSelect && !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: onSelect && !isSelected ? longPress.onTouchEnd : undefined, "data-section": "marketplaceordercard-div-411", children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: handleSelect, label: isSelected ? "Deselect order" : "Select order", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 group-hover:opacity-100 transition-opacity" })), _jsxs("div", { className: `flex gap-4 p-4 ${isListVariant ? "flex-row items-center justify-between" : "flex-col"}`, "data-section": "marketplaceordercard-div-412", children: [_jsxs("div", { className: `flex items-start gap-3 ${selectable ? "pl-8" : ""} ${isListVariant ? "min-w-0 flex-1" : ""}`, "data-section": "marketplaceordercard-div-413", children: [_jsx(Row, { className: "h-10 w-10 flex-shrink-0", surface: "subtle", align: "center", justify: "center", rounded: "lg", children: _jsx(Package, { className: "h-5 w-5 text-zinc-500 dark:text-slate-400" }) }), _jsxs(Stack, { className: "min-w-0", gap: "xs", children: [_jsx(TextLink, { href: detailHref, className: "leading-tight", children: _jsx(Text, { weight: "semibold", className: "truncate", children: order.productTitle ?? shortId }) }), _jsxs(Caption, { className: "font-mono", children: [mergedLabels.orderNumber, " ", shortId] }), _jsxs(Caption, { children: [mergedLabels.placedOn, " ", formatDate(order.orderDate)] }), itemCount > 1 && (_jsxs(Caption, { children: [mergedLabels.items, ": ", _jsx(Span, { weight: "medium", children: itemCount })] }))] })] }), _jsxs("div", { className: `flex ${isListVariant ? "flex-shrink-0 items-center gap-4" : "flex-col gap-3"}`, "data-section": "marketplaceordercard-div-416", children: [_jsxs("div", { className: `flex ${isListVariant ? "items-center gap-4" : "flex-wrap items-center justify-between gap-3"}`, "data-section": "marketplaceordercard-div-417", children: [_jsx(StatusBadge, { status: status, label: statusLabel }), _jsx(Text, { weight: "semibold", className: "tabular-nums", children: formatCurrency(order.totalPrice, order.currency) })] }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [(isShipped || isDelivered) &&
51
+ return (_jsxs("div", { className: `group relative overflow-hidden rounded-xl border border-zinc-200 bg-[var(--appkit-color-surface)] transition-all duration-200 hover:shadow-md border-[var(--appkit-color-border-subtle)] bg-[var(--appkit-color-surface)] ${isSelected ? "ring-2 ring-primary-500 dark:ring-primary-400" : ""} ${className}`, onMouseDown: onSelect && !isSelected ? longPress.onMouseDown : undefined, onMouseUp: onSelect && !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: onSelect && !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: onSelect && !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: onSelect && !isSelected ? longPress.onTouchEnd : undefined, "data-section": "marketplaceordercard-div-411", children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: handleSelect, label: isSelected ? "Deselect order" : "Select order", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 group-hover:opacity-100 transition-opacity" })), _jsxs("div", { className: `flex gap-4 p-4 ${isListVariant ? "flex-row items-center justify-between" : "flex-col"}`, "data-section": "marketplaceordercard-div-412", children: [_jsxs("div", { className: `flex items-start gap-3 ${selectable ? "pl-8" : ""} ${isListVariant ? "min-w-0 flex-1" : ""}`, "data-section": "marketplaceordercard-div-413", children: [_jsx(Row, { className: "h-10 w-10 flex-shrink-0", surface: "subtle", align: "center", justify: "center", rounded: "lg", children: _jsx(Package, { className: "h-5 w-5 text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)]" }) }), _jsxs(Stack, { className: "min-w-0", gap: "xs", children: [_jsx(TextLink, { href: detailHref, className: "leading-tight", children: _jsx(Text, { weight: "semibold", className: "truncate", children: order.productTitle ?? shortId }) }), _jsxs(Caption, { className: "font-mono", children: [mergedLabels.orderNumber, " ", shortId] }), _jsxs(Caption, { children: [mergedLabels.placedOn, " ", formatDate(order.orderDate)] }), itemCount > 1 && (_jsxs(Caption, { children: [mergedLabels.items, ": ", _jsx(Span, { weight: "medium", children: itemCount })] }))] })] }), _jsxs("div", { className: `flex ${isListVariant ? "flex-shrink-0 items-center gap-4" : "flex-col gap-3"}`, "data-section": "marketplaceordercard-div-416", children: [_jsxs("div", { className: `flex ${isListVariant ? "items-center gap-4" : "flex-wrap items-center justify-between gap-3"}`, "data-section": "marketplaceordercard-div-417", children: [_jsx(StatusBadge, { status: status, label: statusLabel }), _jsx(Text, { weight: "semibold", className: "tabular-nums", children: formatCurrency(order.totalPrice, order.currency) })] }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [(isShipped || isDelivered) &&
52
52
  trackHref &&
53
53
  order.trackingNumber && (_jsx(Button, { variant: "ghost", size: "sm", className: CLS_ACTION_BUTTON, onClick: () => navigate(trackHref), children: mergedLabels.trackOrder })), isDelivered && reviewHref && (_jsx(Button, { variant: "outline", size: "sm", className: CLS_ACTION_BUTTON, onClick: () => navigate(reviewHref), children: mergedLabels.writeReview })), _jsx(Button, { variant: "secondary", size: "sm", className: CLS_ACTION_BUTTON, onClick: () => navigate(detailHref), children: mergedLabels.viewOrder })] })] })] })] }));
54
54
  }
@@ -12,7 +12,7 @@ const STATUS_COLORS = {
12
12
  cancelled: "bg-error-surface text-error",
13
13
  refunded: "bg-warning-surface text-warning",
14
14
  return_requested: "bg-warning-surface text-warning",
15
- returned: "bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-300",
15
+ returned: "bg-[var(--appkit-color-surface)] text-[var(--appkit-color-text-muted)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]",
16
16
  };
17
17
  export function OrderCard({ order, onClick, labels = {} }) {
18
18
  const date = order.createdAt
@@ -22,7 +22,7 @@ export function OrderCard({ order, onClick, labels = {} }) {
22
22
  day: "numeric",
23
23
  })
24
24
  : "";
25
- const statusColor = STATUS_COLORS[order.orderStatus] ?? "bg-neutral-100 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300";
25
+ const statusColor = STATUS_COLORS[order.orderStatus] ?? "bg-[var(--appkit-color-surface)] text-neutral-700 bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]";
26
26
  // SB8-F — count unrevealed prize-draw entries to surface a "reveals pending" badge.
27
27
  const unrevealedPrizeDraws = order.items.filter((it) => it.listingType === "prize-draw" &&
28
28
  it.prizeRevealStatus !== undefined &&
@@ -34,7 +34,7 @@ export function OrderCard({ order, onClick, labels = {} }) {
34
34
  .sort()[0];
35
35
  return (_jsxs(Div, { padding: "5", role: onClick ? "button" : undefined, tabIndex: onClick ? 0 : undefined, onKeyDown: onClick
36
36
  ? (e) => (e.key === "Enter" || e.key === " ") && onClick(order)
37
- : undefined, onClick: onClick ? () => onClick(order) : undefined, className: `border border-neutral-200 ${onClick ? "cursor-pointer transition hover:shadow-md" : ""}`, rounded: "xl", surface: "default", children: [_jsxs(Row, { wrap: true, align: "start", justify: "between", gap: "3", children: [_jsxs(Div, { children: [_jsxs(Text, { className: "text-neutral-500 dark:text-neutral-400", size: "xs", children: ["Order #", order.id.slice(-8).toUpperCase()] }), date && (_jsx(Text, { className: "mt-0.5", color: "faint", size: "xs", children: date }))] }), _jsx(Span, { size: "xs", weight: "semibold", className: `capitalize ${statusColor}`, rounded: "full", padding: "pill-md", children: labels[order.orderStatus] ?? order.orderStatus.replace(/_/g, " ") })] }), revealsRemaining > 0 && (_jsxs(Row, { gap: "sm", className: "mt-2", children: [_jsxs(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: [revealsRemaining, " ", revealsRemaining === 1 ? "reveal" : "reveals", " pending"] }), earliestDeadline && (_jsxs(Span, { size: "xs", color: "muted", children: ["before ", new Date(earliestDeadline).toLocaleDateString(getDefaultLocale(), { month: "short", day: "numeric" })] }))] })), _jsxs(Row, { wrap: true, gap: "3", className: "mt-4", children: [order.items.slice(0, 3).map((item, i) => (_jsxs(Row, { gap: "sm", children: [item.image && (_jsx("img", { src: item.image, alt: item.title, className: "h-10 w-10 object-cover rounded-lg" })), _jsxs(Div, { children: [_jsx(Text, { truncate: true, size: "sm", weight: "medium", color: "primary", children: item.title }), _jsxs(Text, { size: "xs", color: "faint", children: ["\u00D7", item.quantity] })] })] }, i))), order.items.length > 3 && (_jsxs(Span, { size: "xs", className: "self-center", color: "faint", children: ["+", order.items.length - 3, " more"] }))] }), _jsxs(Row, { justify: "between", className: "mt-4 border-t border-neutral-100", padding: "t-sm", children: [_jsxs(Span, { size: "sm", className: "text-neutral-500 dark:text-neutral-400", children: [order.currency ?? "", " Total"] }), _jsx(Span, { weight: "semibold", className: "text-neutral-900 dark:text-neutral-100", children: formatCurrency(order.total, order.currency) })] })] }));
37
+ : undefined, onClick: onClick ? () => onClick(order) : undefined, className: `border border-neutral-200 ${onClick ? "cursor-pointer transition hover:shadow-md" : ""}`, rounded: "xl", surface: "default", children: [_jsxs(Row, { wrap: true, align: "start", justify: "between", gap: "3", children: [_jsxs(Div, { children: [_jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: ["Order #", order.id.slice(-8).toUpperCase()] }), date && (_jsx(Text, { className: "mt-0.5", color: "faint", size: "xs", children: date }))] }), _jsx(Span, { size: "xs", weight: "semibold", className: `capitalize ${statusColor}`, rounded: "full", padding: "pill-md", children: labels[order.orderStatus] ?? order.orderStatus.replace(/_/g, " ") })] }), revealsRemaining > 0 && (_jsxs(Row, { gap: "sm", className: "mt-2", children: [_jsxs(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: [revealsRemaining, " ", revealsRemaining === 1 ? "reveal" : "reveals", " pending"] }), earliestDeadline && (_jsxs(Span, { size: "xs", color: "muted", children: ["before ", new Date(earliestDeadline).toLocaleDateString(getDefaultLocale(), { month: "short", day: "numeric" })] }))] })), _jsxs(Row, { wrap: true, gap: "3", className: "mt-4", children: [order.items.slice(0, 3).map((item, i) => (_jsxs(Row, { gap: "sm", children: [item.image && (_jsx("img", { src: item.image, alt: item.title, className: "h-10 w-10 object-cover rounded-lg" })), _jsxs(Div, { children: [_jsx(Text, { truncate: true, size: "sm", weight: "medium", color: "primary", children: item.title }), _jsxs(Text, { size: "xs", color: "faint", children: ["\u00D7", item.quantity] })] })] }, i))), order.items.length > 3 && (_jsxs(Span, { size: "xs", className: "self-center", color: "faint", children: ["+", order.items.length - 3, " more"] }))] }), _jsxs(Row, { justify: "between", className: "mt-4 border-t border-neutral-100", padding: "t-sm", children: [_jsxs(Span, { size: "sm", className: "text-[var(--appkit-color-text-muted)]", children: [order.currency ?? "", " Total"] }), _jsx(Span, { weight: "semibold", className: "text-[var(--appkit-color-text)]", children: formatCurrency(order.total, order.currency) })] })] }));
38
38
  }
39
39
  export function OrdersList({ orders, isLoading, onOrderClick, totalPages = 1, currentPage = 1, onPageChange, emptyLabel = "No orders found", }) {
40
40
  if (isLoading) {
@@ -62,5 +62,5 @@ export function MarketplacePreorderCard({ product, className = "", variant = "gr
62
62
  /* ── Compact list layout ── */
63
63
  _jsxs(_Fragment, { children: [_jsxs(Row, { className: "min-w-0", align: "start", justify: "between", gap: "sm", children: [_jsx(TextLink, { href: detailHref, className: "min-w-0 flex-1", children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), wishlistActions && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: `shrink-0 p-[0.25rem] leading-none ${inWishlist ? "text-primary" : "text-[var(--appkit-color-text-faint)]"}`, onClick: handleWishlist, "aria-label": inWishlist ? mergedLabels.removeFromWishlist : mergedLabels.addToWishlist, children: inWishlist ? "♥" : "♡" }))] }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Text, { className: "text-primary", size: "sm", weight: "bold", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "self-start mt-0.5", onClick: handleNavigate, children: mergedLabels.reserveNow })] })) : (
64
64
  /* ── Full grid layout ── */
65
- _jsxs(_Fragment, { children: [_jsx(TextLink, { href: detailHref, children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), product.description ? (_jsx(RichText, { html: normalizeRichTextHtml(product.description), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "line-clamp-2 text-xs text-zinc-500 dark:text-zinc-400" })) : null, _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsxs(Row, { justify: "between", className: "mt-2", gap: "sm", children: [_jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "flex-1", onClick: handleNavigate, children: mergedLabels.reserveNow }), onAddToCart ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", textSize: "xs", className: "flex-1", onClick: (e) => { e.stopPropagation(); onAddToCart(product); }, children: mergedLabels.addToCart })) : null, wishlistActions ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: `${inWishlist ? "text-primary" : "text-[var(--appkit-color-text-muted)]"}`, onClick: handleWishlist, "aria-label": inWishlist ? mergedLabels.removeFromWishlist : mergedLabels.addToWishlist, children: inWishlist ? "♥" : "♡" })) : null] })] })) })] }));
65
+ _jsxs(_Fragment, { children: [_jsx(TextLink, { href: detailHref, children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), product.description ? (_jsx(RichText, { html: normalizeRichTextHtml(product.description), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "line-clamp-2 text-xs text-[var(--appkit-color-text-muted)]" })) : null, _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsxs(Row, { justify: "between", className: "mt-2", gap: "sm", children: [_jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "flex-1", onClick: handleNavigate, children: mergedLabels.reserveNow }), onAddToCart ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", textSize: "xs", className: "flex-1", onClick: (e) => { e.stopPropagation(); onAddToCart(product); }, children: mergedLabels.addToCart })) : null, wishlistActions ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: `${inWishlist ? "text-primary" : "text-[var(--appkit-color-text-muted)]"}`, onClick: handleWishlist, "aria-label": inWishlist ? mergedLabels.removeFromWishlist : mergedLabels.addToWishlist, children: inWishlist ? "♥" : "♡" })) : null] })] })) })] }));
66
66
  }
@@ -5,7 +5,7 @@ const STATUS_MAP = {
5
5
  pending: { label: "Pending", color: "bg-warning-surface text-warning" },
6
6
  confirmed: { label: "Confirmed", color: "bg-info-surface text-info" },
7
7
  ready: { label: "Ready", color: "bg-success-surface text-success" },
8
- fulfilled: { label: "Fulfilled", color: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300" },
8
+ fulfilled: { label: "Fulfilled", color: "bg-[var(--appkit-color-surface)] text-gray-700 bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]" },
9
9
  cancelled: { label: "Cancelled", color: "bg-error-surface text-error" },
10
10
  };
11
11
  export function PreOrderBadge({ status, className = "" }) {
@@ -56,7 +56,7 @@ function PreOrderInfoSection({ title, productionStatus, maxPerUser, deliveryDate
56
56
  codAvailable: "Cash on Delivery",
57
57
  wishlistCount: (n) => `${n} wishlisted`,
58
58
  categoryProductCount: (n, cat) => `${n} in ${cat}`,
59
- } }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && (_jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50 px-2.5 py-1 text-xs font-medium text-zinc-600 dark:text-zinc-300 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: categoryName || category })), !category && categoryName && (_jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: categoryName })), brand && brandSlug && (_jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50 px-2.5 py-1 text-xs font-medium text-zinc-600 dark:text-zinc-300 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 }))] })), productFeatures && features.length > 0 && (_jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures })), !productFeatures && features.length > 0 && (_jsxs(Div, { border: "subtle", surface: "muted", padding: "inline", rounded: "xl", children: [_jsx(Text, { className: "mb-2 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "About this product" }), _jsx(Ul, { spacing: "comfortable", size: "sm", color: "primary", children: features.map((f, i) => (_jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i))) })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-sm leading-relaxed text-zinc-600 dark:text-zinc-400 line-clamp-4" })), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-3 py-1.5 text-xs font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] }));
59
+ } }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && (_jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-2.5 py-1 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: categoryName || category })), !category && categoryName && (_jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: categoryName })), 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-2.5 py-1 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 }))] })), productFeatures && features.length > 0 && (_jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures })), !productFeatures && features.length > 0 && (_jsxs(Div, { border: "subtle", surface: "muted", padding: "inline", rounded: "xl", children: [_jsx(Text, { className: "mb-2 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "About this product" }), _jsx(Ul, { spacing: "comfortable", size: "sm", color: "primary", children: features.map((f, i) => (_jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i))) })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-sm leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-3 py-1.5 text-xs font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] }));
60
60
  }
61
61
  function PreOrderBuyBarPanel({ reserveTarget, reservedCount, progressPct, productId, price, currency, depositAmount, depositPercent, isCancellable, tags, onReserveNow, }) {
62
62
  return (_jsxs(Stack, { id: "pre-order-buy-bar", className: `${__P.p5}`, border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: [reserveTarget > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Text, { size: "xs", color: "muted", children: [reservedCount, " of ", reserveTarget, " reserved"] }), _jsxs(Span, { size: "xs", weight: "semibold", className: "text-primary-600 dark:text-primary-400", children: [progressPct, "%"] })] }), _jsx(Div, { className: `h-2 w-full ${__O.hidden}`, rounded: "full", surface: "subtle", children: _jsx(ProgressBarFill, { pct: progressPct, className: "h-full bg-primary transition-all rounded-full" }) })] })), onReserveNow ? (_jsx(PreOrderActionsClient, { productId: productId, price: price, currency: currency, depositAmount: depositAmount, depositPercent: depositPercent, isCancellable: isCancellable, tags: tags, onReserveNow: onReserveNow })) : (_jsxs(_Fragment, { children: [price !== null && (_jsxs(Div, { children: [_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) }), depositAmount !== null && (_jsxs(Text, { className: "mt-0.5", color: "muted", size: "xs", children: ["Reserve with ", formatCurrency(depositAmount, currency), depositPercent !== null ? ` (${depositPercent}% deposit)` : ""] }))] })), _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", children: "Reserve Now" }), isCancellable && (_jsx(Text, { size: "xs", align: "center", color: "muted", children: "\u2713 Free cancellation before production" }))] }), tags.length > 0 && (_jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: tag }, tag))) }) })), _jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "sm", justify: "center", className: "text-center", children: [
@@ -153,7 +153,7 @@ export async function PreOrderDetailPageView({ id, initialPreOrder, onReserveNow
153
153
  ? () => (_jsx(SublistingCarouselSection, { sublistingCategoryId: sublistingCategoryId, currentListingId: String(product.id) }))
154
154
  : undefined, renderGroupSection: groupId
155
155
  ? () => (_jsx(ShowGroupSection, { groupId: groupId, currentSlug: String(p.slug ?? product.id), isParent: isGroupParent, groupTitle: groupTitle }))
156
- : undefined, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-zinc-700 dark:text-zinc-300" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, customTabs: customSections.map((s) => ({
156
+ : undefined, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, customTabs: customSections.map((s) => ({
157
157
  id: s.id,
158
158
  label: s.title,
159
159
  content: _jsx(CustomSectionTabContent, { section: s }),