@mohasinac/appkit 2.7.57 → 2.7.58

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 (184) hide show
  1. package/dist/_internal/client/features/layout/DashboardLayoutClient.d.ts +7 -1
  2. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +8 -2
  3. package/dist/client.d.ts +1 -1
  4. package/dist/client.js +1 -1
  5. package/dist/configs/tailwind.d.ts +16 -3
  6. package/dist/configs/tailwind.js +18 -5
  7. package/dist/features/account/components/AddressBook.js +1 -1
  8. package/dist/features/account/components/BuyerAccountGuideView.js +7 -7
  9. package/dist/features/account/components/BuyerAuctionsGuideView.js +7 -7
  10. package/dist/features/account/components/BuyerOrdersGuideView.js +6 -6
  11. package/dist/features/account/components/BuyerShoppingGuideView.js +7 -7
  12. package/dist/features/account/components/UserOffersPanel.js +1 -1
  13. package/dist/features/account/components/UserSupportView.js +4 -4
  14. package/dist/features/admin/components/AdminAdsView.js +1 -1
  15. package/dist/features/admin/components/AdminAllEventEntriesView.js +1 -1
  16. package/dist/features/admin/components/AdminAnalyticsGuideView.js +3 -3
  17. package/dist/features/admin/components/AdminBidsView.js +1 -1
  18. package/dist/features/admin/components/AdminBlogView.js +1 -1
  19. package/dist/features/admin/components/AdminBrandsView.js +1 -1
  20. package/dist/features/admin/components/AdminBundlesView.js +1 -1
  21. package/dist/features/admin/components/AdminCarouselView.js +2 -2
  22. package/dist/features/admin/components/AdminCartsView.js +1 -1
  23. package/dist/features/admin/components/AdminCatalogGuideView.js +5 -5
  24. package/dist/features/admin/components/AdminCategoriesView.js +1 -1
  25. package/dist/features/admin/components/AdminContactEditorView.js +2 -2
  26. package/dist/features/admin/components/AdminContactView.js +1 -1
  27. package/dist/features/admin/components/AdminContentGuideView.js +6 -6
  28. package/dist/features/admin/components/AdminCouponsView.js +1 -1
  29. package/dist/features/admin/components/AdminFaqsView.js +1 -1
  30. package/dist/features/admin/components/AdminFeatureFlagsView.js +1 -1
  31. package/dist/features/admin/components/AdminFeaturesView.js +1 -1
  32. package/dist/features/admin/components/AdminGuideHubView.js +1 -1
  33. package/dist/features/admin/components/AdminHistoryView.js +1 -1
  34. package/dist/features/admin/components/AdminListingScaffold.js +1 -1
  35. package/dist/features/admin/components/AdminNewsletterView.js +1 -1
  36. package/dist/features/admin/components/AdminNotificationsView.js +1 -1
  37. package/dist/features/admin/components/AdminOrdersGuideView.js +5 -5
  38. package/dist/features/admin/components/AdminOrdersView.js +1 -1
  39. package/dist/features/admin/components/AdminPayoutsView.js +1 -1
  40. package/dist/features/admin/components/AdminProductsView.js +1 -1
  41. package/dist/features/admin/components/AdminReturnRequestsView.js +1 -1
  42. package/dist/features/admin/components/AdminReviewsView.js +1 -1
  43. package/dist/features/admin/components/AdminScammerEditorView.js +3 -3
  44. package/dist/features/admin/components/AdminScammersView.js +4 -4
  45. package/dist/features/admin/components/AdminSectionsView.js +1 -1
  46. package/dist/features/admin/components/AdminSessionsView.js +1 -1
  47. package/dist/features/admin/components/AdminSiteSettingsView.js +1 -1
  48. package/dist/features/admin/components/AdminStoreAddressesView.js +1 -1
  49. package/dist/features/admin/components/AdminStoresGuideView.js +6 -6
  50. package/dist/features/admin/components/AdminStoresView.js +1 -1
  51. package/dist/features/admin/components/AdminSublistingCategoriesView.js +1 -1
  52. package/dist/features/admin/components/AdminSupportTicketDetailView.js +4 -4
  53. package/dist/features/admin/components/AdminSupportTicketsView.js +7 -7
  54. package/dist/features/admin/components/AdminTeamGuideView.js +4 -4
  55. package/dist/features/admin/components/AdminTeamView.js +1 -1
  56. package/dist/features/admin/components/AdminTrustGuideView.js +5 -5
  57. package/dist/features/admin/components/AdminUserEditorView.js +1 -1
  58. package/dist/features/admin/components/AdminUsersGuideView.js +5 -5
  59. package/dist/features/admin/components/AdminUsersView.js +1 -1
  60. package/dist/features/admin/components/AdminViewCards.js +1 -1
  61. package/dist/features/admin/components/AdminWishlistsView.js +1 -1
  62. package/dist/features/admin/components/DataTable.js +1 -1
  63. package/dist/features/auctions/components/AuctionBidsTable.js +1 -1
  64. package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
  65. package/dist/features/auctions/components/MarketplaceAuctionCard.js +2 -2
  66. package/dist/features/auctions/components/PlaceBidFormClient.js +1 -1
  67. package/dist/features/auth/components/VerifyEmailView.js +1 -1
  68. package/dist/features/blog/components/BlogListView.js +1 -1
  69. package/dist/features/blog/components/BlogPostView.js +1 -1
  70. package/dist/features/cart/components/CartDrawer.js +1 -1
  71. package/dist/features/categories/components/CategoryDetailPageView.js +1 -1
  72. package/dist/features/checkout/components/CheckoutStepper.js +1 -1
  73. package/dist/features/collections/components/CollectionCard.js +1 -1
  74. package/dist/features/copilot/components/AdminCopilotView.js +2 -2
  75. package/dist/features/events/components/AdminEventsView.js +1 -1
  76. package/dist/features/events/components/EventPollWidget.js +2 -2
  77. package/dist/features/events/components/EventStatusBadge.js +4 -4
  78. package/dist/features/events/components/SpinWheelView.js +1 -1
  79. package/dist/features/faq/components/FAQAccordion.js +2 -2
  80. package/dist/features/forms/Input.js +3 -3
  81. package/dist/features/forms/Select.js +2 -2
  82. package/dist/features/forms/Textarea.js +2 -2
  83. package/dist/features/forms/utils.d.ts +1 -1
  84. package/dist/features/forms/utils.js +1 -1
  85. package/dist/features/homepage/components/CharacterHotspotForm.js +2 -2
  86. package/dist/features/layout/BottomActions.js +2 -2
  87. package/dist/features/layout/ListingLayout.js +3 -3
  88. package/dist/features/layout/TitleBarLayout.js +1 -1
  89. package/dist/features/media/index.d.ts +2 -0
  90. package/dist/features/media/index.js +1 -0
  91. package/dist/features/media/modals/ImageCropModal.js +1 -1
  92. package/dist/features/media/modals/ImageEditor.d.ts +12 -0
  93. package/dist/features/media/modals/ImageEditor.js +73 -0
  94. package/dist/features/media/upload/ImageUpload.d.ts +5 -1
  95. package/dist/features/media/upload/ImageUpload.js +33 -8
  96. package/dist/features/media/upload/MediaUploadField.d.ts +5 -1
  97. package/dist/features/media/upload/MediaUploadField.js +26 -12
  98. package/dist/features/orders/components/OrdersList.js +7 -7
  99. package/dist/features/orders/components/RefundHistoryTable.js +2 -2
  100. package/dist/features/orders/components/RefundRequestView.js +3 -3
  101. package/dist/features/pre-orders/components/PreOrderActionsClient.js +1 -1
  102. package/dist/features/pre-orders/components/PreOrderBadge.js +4 -4
  103. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -1
  104. package/dist/features/pre-orders/components/PreorderCard.js +2 -2
  105. package/dist/features/products/components/CustomSectionsEditor.js +1 -1
  106. package/dist/features/products/components/MakeOfferButton.js +2 -2
  107. package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
  108. package/dist/features/products/components/PrizeDrawItemsEditor.js +3 -3
  109. package/dist/features/products/components/PrizeRevealModal.js +3 -3
  110. package/dist/features/products/components/ProductDetailPageView.js +4 -4
  111. package/dist/features/products/components/ProductFeaturesSelector.js +1 -1
  112. package/dist/features/products/components/ProductGrid.js +3 -3
  113. package/dist/features/promotions/components/CouponCard.js +2 -2
  114. package/dist/features/promotions/components/PromotionsView.js +1 -1
  115. package/dist/features/reviews/components/ReviewDetailShell.js +2 -2
  116. package/dist/features/reviews/components/ReviewsList.js +1 -1
  117. package/dist/features/seller/components/SellerAddressesView.js +2 -2
  118. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +2 -2
  119. package/dist/features/seller/components/SellerAuctionsView.js +1 -1
  120. package/dist/features/seller/components/SellerBidsView.js +1 -1
  121. package/dist/features/seller/components/SellerBundlesView.js +2 -2
  122. package/dist/features/seller/components/SellerClassifiedView.js +3 -3
  123. package/dist/features/seller/components/SellerCouponEditorView.js +2 -2
  124. package/dist/features/seller/components/SellerCouponsView.js +1 -1
  125. package/dist/features/seller/components/SellerDigitalCodesView.js +2 -2
  126. package/dist/features/seller/components/SellerFeaturesView.js +1 -1
  127. package/dist/features/seller/components/SellerLiveView.js +4 -4
  128. package/dist/features/seller/components/SellerOffersPanel.js +2 -2
  129. package/dist/features/seller/components/SellerOffersView.js +1 -1
  130. package/dist/features/seller/components/SellerOrdersView.js +2 -2
  131. package/dist/features/seller/components/SellerPayoutMethodsView.js +2 -2
  132. package/dist/features/seller/components/SellerPayoutsView.js +1 -1
  133. package/dist/features/seller/components/SellerProductsCards.js +1 -1
  134. package/dist/features/seller/components/SellerProductsView.js +1 -1
  135. package/dist/features/seller/components/SellerReviewsView.js +1 -1
  136. package/dist/features/seller/components/SellerShippingConfigsView.js +3 -3
  137. package/dist/features/seller/components/SellerStoreCategoriesView.js +2 -2
  138. package/dist/features/seller/components/SellerTemplatesView.js +2 -2
  139. package/dist/features/seller/components/analytics/SellerTopProducts.js +1 -1
  140. package/dist/features/stores/components/StoreFinanceGuideView.js +2 -2
  141. package/dist/features/stores/components/StoreHeader.js +1 -1
  142. package/dist/features/stores/components/StoreListingsGuideView.js +3 -3
  143. package/dist/features/stores/components/StoreOrdersGuideView.js +4 -4
  144. package/dist/features/stores/components/StoreSettingsGuideView.js +2 -2
  145. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +4 -4
  146. package/dist/features/wishlist/components/WishlistPage.js +1 -1
  147. package/dist/index.d.ts +2 -0
  148. package/dist/index.js +3 -0
  149. package/dist/styles.css +286 -3
  150. package/dist/tailwind-utilities.css +1 -1
  151. package/dist/tokens/motion.d.ts +162 -0
  152. package/dist/tokens/motion.js +89 -0
  153. package/dist/ui/DataTable.js +3 -2
  154. package/dist/ui/components/Accordion.js +2 -1
  155. package/dist/ui/components/Card.d.ts +5 -2
  156. package/dist/ui/components/Card.js +29 -11
  157. package/dist/ui/components/Div.d.ts +2 -16
  158. package/dist/ui/components/Div.js +2 -16
  159. package/dist/ui/components/Layout.d.ts +9 -19
  160. package/dist/ui/components/Layout.js +9 -7
  161. package/dist/ui/components/Modal.js +7 -4
  162. package/dist/ui/components/Motion.d.ts +75 -0
  163. package/dist/ui/components/Motion.js +122 -0
  164. package/dist/ui/components/Semantic.d.ts +81 -14
  165. package/dist/ui/components/Semantic.js +75 -10
  166. package/dist/ui/components/Semantic.style.css +261 -2
  167. package/dist/ui/components/SideDrawer.d.ts +1 -1
  168. package/dist/ui/components/SideDrawer.js +8 -6
  169. package/dist/ui/components/SideModal.d.ts +1 -1
  170. package/dist/ui/components/SideModal.js +11 -9
  171. package/dist/ui/components/StackedViewShell.js +6 -1
  172. package/dist/ui/components/Toast.js +5 -2
  173. package/dist/ui/components/Typography.d.ts +44 -3
  174. package/dist/ui/components/Typography.js +15 -30
  175. package/dist/ui/components/Typography.style.css +24 -0
  176. package/dist/ui/components/VacationBanner.js +2 -1
  177. package/dist/ui/components/surface-tokens.d.ts +70 -0
  178. package/dist/ui/components/surface-tokens.js +68 -0
  179. package/dist/ui/forms/FormShell.d.ts +6 -1
  180. package/dist/ui/forms/FormShell.js +32 -4
  181. package/dist/ui/index.d.ts +9 -2
  182. package/dist/ui/index.js +4 -2
  183. package/dist/validation/schemas.d.ts +28 -28
  184. package/package.json +3 -1
@@ -146,7 +146,7 @@ export function CharacterHotspotForm({ initial, onUploadImage, onSave, onAfterSa
146
146
  }, children: "Next: Place Pins \u2192" })] })] })), step === "place" && (_jsxs(Div, { className: CLS_STEP, style: {
147
147
  borderColor: "var(--border-ink)",
148
148
  background: "var(--surface-elevated)",
149
- }, children: [_jsx(Heading, { level: 2, className: "text-lg font-bold", children: "Place a Pin" }), _jsxs(Text, { className: "text-sm", style: { color: "var(--color-muted)" }, children: [_jsx("strong", { children: "Click anywhere on the image" }), " to drop a pin, or enter exact coordinates below."] }), _jsxs(Div, { ref: containerRef, className: CLS_IMG_WRAP, style: {
149
+ }, children: [_jsx(Heading, { level: 2, className: "text-lg font-bold", children: "Place a Pin" }), _jsxs(Text, { className: "text-sm", style: { color: "var(--color-muted)" }, children: [_jsx(Span, { weight: "bold", children: "Click anywhere on the image" }), " to drop a pin, or enter exact coordinates below."] }), _jsxs(Div, { ref: containerRef, className: CLS_IMG_WRAP, style: {
150
150
  paddingTop: "56.25%",
151
151
  cursor: "crosshair",
152
152
  background: "#111",
@@ -220,7 +220,7 @@ export function CharacterHotspotForm({ initial, onUploadImage, onSave, onAfterSa
220
220
  }, children: "Continue: Add Details \u2192" })] })] })] })), step === "details" && (_jsxs(Div, { className: CLS_STEP, style: {
221
221
  borderColor: "var(--border-ink)",
222
222
  background: "var(--surface-elevated)",
223
- }, children: [_jsx(Heading, { level: 2, className: "text-lg font-bold", children: "Pin Details" }), _jsxs(Text, { className: "text-sm", style: { color: "var(--color-muted)" }, children: ["Fill in the details for the pin at", " ", _jsxs("strong", { children: [draftPos?.xPct, "%, ", draftPos?.yPct, "%"] }), "."] }), _jsx(Div, { className: "grid grid-cols-2 gap-3", children: [
223
+ }, children: [_jsx(Heading, { level: 2, className: "text-lg font-bold", children: "Pin Details" }), _jsxs(Text, { className: "text-sm", style: { color: "var(--color-muted)" }, children: ["Fill in the details for the pin at", " ", _jsxs(Span, { weight: "bold", children: [draftPos?.xPct, "%, ", draftPos?.yPct, "%"] }), "."] }), _jsx(Div, { className: "grid grid-cols-2 gap-3", children: [
224
224
  {
225
225
  label: "Name *",
226
226
  value: draftName,
@@ -55,7 +55,7 @@ function BulkPickerPanel({ bulkActions, selectedActionId, pickerOpen, onSelect,
55
55
  i > 0 ? "border-t border-zinc-100/80 dark:border-slate-800/80" : "",
56
56
  isSelected ? "bg-zinc-50 dark:bg-slate-800/60" : "",
57
57
  action.variant === "danger"
58
- ? "text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/30"
58
+ ? "text-error hover:bg-error-surface"
59
59
  : "text-zinc-800 dark:text-zinc-100 hover:bg-zinc-50 dark:hover:bg-slate-800/60",
60
60
  action.disabled || action.loading ? "opacity-50 cursor-not-allowed" : "",
61
61
  ]
@@ -133,7 +133,7 @@ export default function BottomActions() {
133
133
  "bg-zinc-50 hover:bg-zinc-100 active:bg-zinc-200 dark:bg-slate-800/60 dark:hover:bg-slate-700/60",
134
134
  "border-zinc-200 dark:border-slate-700",
135
135
  selectedAction?.variant === "danger"
136
- ? "text-red-600 dark:text-red-400"
136
+ ? "text-error"
137
137
  : "text-zinc-800 dark:text-zinc-100",
138
138
  ]
139
139
  .filter(Boolean)
@@ -69,7 +69,7 @@ export function ListingLayout({ headerSlot, statusTabsSlot, filterContent, filte
69
69
  sidebarOpen
70
70
  ? "bg-primary/10 border-primary/30 text-primary dark:bg-primary/15 dark:border-primary/40"
71
71
  : "border-zinc-200 dark:border-slate-700 text-zinc-600 dark:text-slate-300 hover:border-zinc-300 dark:hover:border-slate-600 hover:bg-zinc-50 dark:hover:bg-slate-800/60",
72
- ].join(" "), children: [l.filtersTitle, filterActiveCount > 0 && (_jsx(Span, { className: "inline-flex items-center justify-center w-4 h-4 text-[10px] font-bold rounded-full bg-primary text-white", children: filterActiveCount }))] })), searchSlot && _jsx(Div, { className: "flex-1 min-w-0", children: searchSlot }), _jsxs(Row, { className: "gap-1.5 flex-shrink-0", children: [sortSlot, viewToggleSlot && (_jsx(Row, { className: "gap-0.5 rounded-full border border-zinc-200 dark:border-slate-700 bg-white dark:bg-slate-900 p-0.5 shadow-sm", children: viewToggleSlot })), actionsSlot] }), toolbarPaginationSlot && (_jsx(Div, { className: "ml-auto flex-shrink-0 pl-3 border-l border-zinc-200/70 dark:border-slate-700/70", children: toolbarPaginationSlot }))] }), _jsxs(Div, { className: "flex flex-col gap-2 md:hidden", children: [_jsxs(Row, { className: "gap-2", children: [hasFilter && (_jsxs(Button, { type: "button", onClick: () => setMobileFilterOpen(true), "aria-label": l.filtersTitle, variant: "ghost", size: "sm", className: [
72
+ ].join(" "), children: [l.filtersTitle, filterActiveCount > 0 && (_jsx(Span, { className: "inline-flex items-center justify-center w-4 h-4 text-[10px] font-bold rounded-full bg-primary text-white", children: filterActiveCount }))] })), searchSlot && _jsx(Div, { className: "flex-1 min-w-0", children: searchSlot }), _jsxs(Row, { className: "gap-1.5 flex-shrink-0", children: [sortSlot, viewToggleSlot && (_jsx(Row, { surface: "card", className: "gap-0.5 rounded-full p-0.5", children: viewToggleSlot })), actionsSlot] }), toolbarPaginationSlot && (_jsx(Div, { className: "ml-auto flex-shrink-0 pl-3 border-l border-zinc-200/70 dark:border-slate-700/70", children: toolbarPaginationSlot }))] }), _jsxs(Div, { className: "flex flex-col gap-2 md:hidden", children: [_jsxs(Row, { className: "gap-2", children: [hasFilter && (_jsxs(Button, { type: "button", onClick: () => setMobileFilterOpen(true), "aria-label": l.filtersTitle, variant: "ghost", size: "sm", className: [
73
73
  "flex-shrink-0 flex items-center gap-1.5",
74
74
  "rounded-full h-9 px-3 text-sm font-medium",
75
75
  "border border-zinc-200 dark:border-slate-700",
@@ -85,7 +85,7 @@ export function ListingLayout({ headerSlot, statusTabsSlot, filterContent, filte
85
85
  sidebarOpen
86
86
  ? "w-60 xl:w-64 2xl:w-72 opacity-100"
87
87
  : "w-0 opacity-0 pointer-events-none",
88
- ].join(" "), children: _jsxs(Div, { className: "w-60 xl:w-64 2xl:w-72 rounded-2xl overflow-hidden border border-zinc-200/80 dark:border-slate-700/60 bg-white dark:bg-slate-900 shadow-sm", children: [_jsxs(Row, { justify: "between", className: "px-4 py-3 border-b border-zinc-100 dark:border-slate-800", children: [_jsx(Div, { className: "font-semibold text-sm", children: panelTitle }), filterActiveCount > 0 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: onFilterClear, className: "text-xs text-primary hover:text-primary/80 hover:underline p-0 h-auto leading-none font-medium", children: l.clearAll }))] }), _jsx(Div, { className: "px-3 pt-5 pb-3 max-h-[calc(100vh-15rem)] overflow-y-auto space-y-4", children: filterContent }), _jsx(Div, { className: "px-3 pb-3 pt-2 border-t border-zinc-100 dark:border-slate-800", children: _jsx(Button, { type: "button", variant: "primary", className: "w-full rounded-xl bg-primary text-white py-2 px-3", onClick: onFilterApply, children: l.applyFilters }) })] }) })), _jsxs(Div, { className: "flex-1 min-w-0 space-y-3", children: [activeFiltersSlot, errorSlot ? (errorSlot) : (_jsxs(_Fragment, { children: [children, paginationSlot && _jsx(Div, { className: "pt-2", children: paginationSlot })] }))] })] }), toolbarPaginationSlot && (_jsx(Nav, { "aria-label": "Pagination", className: [
88
+ ].join(" "), children: _jsxs(Div, { surface: "elevated", className: "w-60 xl:w-64 2xl:w-72 overflow-hidden", children: [_jsxs(Row, { justify: "between", className: "px-4 py-3 border-b border-zinc-100 dark:border-slate-800", children: [_jsx(Div, { className: "font-semibold text-sm", children: panelTitle }), filterActiveCount > 0 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: onFilterClear, className: "text-xs text-primary hover:text-primary/80 hover:underline p-0 h-auto leading-none font-medium", children: l.clearAll }))] }), _jsx(Div, { className: "px-3 pt-5 pb-3 max-h-[calc(100vh-15rem)] overflow-y-auto space-y-4", children: filterContent }), _jsx(Div, { className: "px-3 pb-3 pt-2 border-t border-zinc-100 dark:border-slate-800", children: _jsx(Button, { type: "button", variant: "primary", className: "w-full rounded-xl bg-primary text-white py-2 px-3", onClick: onFilterApply, children: l.applyFilters }) })] }) })), _jsxs(Div, { className: "flex-1 min-w-0 space-y-3", children: [activeFiltersSlot, errorSlot ? (errorSlot) : (_jsxs(_Fragment, { children: [children, paginationSlot && _jsx(Div, { className: "pt-2", children: paginationSlot })] }))] })] }), toolbarPaginationSlot && (_jsx(Nav, { "aria-label": "Pagination", className: [
89
89
  "fixed left-0 right-0 md:hidden",
90
90
  isDashboard
91
91
  ? "bottom-0"
@@ -97,7 +97,7 @@ export function ListingLayout({ headerSlot, statusTabsSlot, filterContent, filte
97
97
  "shadow-[0_-2px_8px_rgba(0,0,0,0.06)] dark:shadow-[0_-2px_8px_rgba(0,0,0,0.20)]",
98
98
  "h-10 flex items-center justify-center px-3 overflow-x-auto pb-px",
99
99
  "[scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
100
- ].join(" "), children: toolbarPaginationSlot })), hasFilter && mobileFilterOpen && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "fixed inset-0 z-40 bg-black/40 lg:hidden", onClick: () => setMobileFilterOpen(false), "aria-hidden": "true" }), _jsxs(Div, { ref: mobileOverlayRef, className: "fixed inset-0 z-50 flex flex-col lg:hidden bg-white dark:bg-slate-950", role: "dialog", "aria-modal": "true", "aria-label": panelTitle, children: [_jsxs(Row, { justify: "between", className: "px-4 py-3 border-b border-zinc-100 dark:border-slate-800 flex-shrink-0", children: [_jsxs(Div, { className: "font-semibold", children: [panelTitle, filterActiveCount > 0 && (_jsx(Span, { className: "ml-2 inline-flex items-center justify-center w-5 h-5 text-[11px] font-bold rounded-full bg-primary text-white", children: filterActiveCount }))] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setMobileFilterOpen(false), "aria-label": l.close, className: "rounded-full w-8 h-8 p-0 flex items-center justify-center hover:bg-zinc-100 dark:hover:bg-slate-800", children: "x" })] }), _jsx(Div, { className: "flex-1 overflow-y-auto px-4 pt-6 pb-4 space-y-4", children: filterContent }), _jsxs(Div, { className: "flex-shrink-0 flex gap-3 px-4 py-4 border-t border-zinc-100 dark:border-slate-800 bg-white dark:bg-slate-950", children: [_jsx(Button, { type: "button", variant: "outline", className: "flex-1 rounded-xl bg-zinc-200 dark:bg-slate-700 py-2 px-3", onClick: () => {
100
+ ].join(" "), children: toolbarPaginationSlot })), hasFilter && mobileFilterOpen && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "fixed inset-0 z-40 bg-black/40 lg:hidden", onClick: () => setMobileFilterOpen(false), "aria-hidden": "true" }), _jsxs(Div, { ref: mobileOverlayRef, className: "fixed inset-0 z-50 flex flex-col lg:hidden bg-white dark:bg-slate-950", role: "dialog", "aria-modal": "true", "aria-label": panelTitle, children: [_jsxs(Row, { justify: "between", className: "px-4 py-3 border-b border-zinc-100 dark:border-slate-800 flex-shrink-0", children: [_jsxs(Div, { className: "font-semibold", children: [panelTitle, filterActiveCount > 0 && (_jsx(Span, { className: "ml-2 inline-flex items-center justify-center w-5 h-5 text-[11px] font-bold rounded-full bg-primary text-white", children: filterActiveCount }))] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setMobileFilterOpen(false), "aria-label": l.close, className: "rounded-full w-8 h-8 p-0 flex items-center justify-center hover:bg-zinc-100 dark:hover:bg-slate-800", children: "x" })] }), _jsx(Div, { className: "flex-1 overflow-y-auto px-4 pt-6 pb-4 space-y-4", children: filterContent }), _jsxs(Div, { surface: "muted", className: "flex-shrink-0 flex gap-3 px-4 py-4 border-t border-zinc-100 dark:border-slate-800", children: [_jsx(Button, { type: "button", variant: "outline", className: "flex-1 rounded-xl bg-zinc-200 dark:bg-slate-700 py-2 px-3", onClick: () => {
101
101
  onFilterClear?.();
102
102
  }, children: l.clearAll }), _jsx(Button, { type: "button", variant: "primary", className: "flex-1 rounded-xl bg-primary text-white py-2 px-3", onClick: handleMobileApply, children: l.applyFilters })] })] })] }))] }));
103
103
  }
@@ -31,7 +31,7 @@ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, s
31
31
  const profileEl = profileHref ? (_jsx(Link, { href: profileHref, "aria-label": user ? `Profile — ${user.displayName ?? user.email}` : "Sign in", className: `relative ${iconBtn}`, children: user?.photoURL ? (
32
32
  // eslint-disable-next-line @next/next/no-img-element
33
33
  _jsx("img", { src: user.photoURL, alt: user.displayName ?? "Profile", width: 28, height: 28, loading: "lazy", className: "w-7 h-7 rounded-full object-cover" })) : (_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: "M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0zM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632z" }) })) })) : null;
34
- const authButtonsEl = !user && (loginHref || registerHref) ? (_jsxs(Row, { gap: "xs", className: "hidden lg:flex items-center", 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;
34
+ 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;
35
35
  const hasTb2 = !!(notificationsEl || wishlistEl || cartEl || profileEl);
36
36
  // ── Render ───────────────────────────────────────────────────────────────────
37
37
  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(Div, { className: "bg-gradient-to-r from-primary-700 to-secondary-600 dark:from-primary-700 dark:to-cobalt-700 text-white text-xs py-1 text-center font-medium", children: promoStripText })), _jsxs(Div, { className: "container mx-auto px-4 sm:px-6 lg:px-8 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, className: "h-7 md:h-9 lg:h-10" }) }) }), siteLogoUrl ? (_jsx(Div, { className: "absolute inset-y-0 left-1/2 -translate-x-1/2 flex items-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, className: "h-7 md:h-9 lg:h-10" }) }) })) : (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
@@ -14,6 +14,8 @@ export { MediaPickerModal } from "./MediaPickerModal";
14
14
  export type { MediaPickerModalProps } from "./MediaPickerModal";
15
15
  export { ImageCropModal } from "./modals/ImageCropModal";
16
16
  export type { ImageCropData, ImageCropModalProps, } from "./modals/ImageCropModal";
17
+ export { ImageEditor } from "./modals/ImageEditor";
18
+ export type { ImageEditorProps } from "./modals/ImageEditor";
17
19
  export { VideoTrimModal } from "./modals/VideoTrimModal";
18
20
  export type { VideoTrimModalProps } from "./modals/VideoTrimModal";
19
21
  export { VideoThumbnailSelector } from "./modals/VideoThumbnailSelector";
@@ -11,6 +11,7 @@ export { useMediaUpload, useMediaAbort, useMediaCrop, useMediaTrim, useMediaClea
11
11
  // Modals
12
12
  export { MediaPickerModal } from "./MediaPickerModal";
13
13
  export { ImageCropModal } from "./modals/ImageCropModal";
14
+ export { ImageEditor } from "./modals/ImageEditor";
14
15
  export { VideoTrimModal } from "./modals/VideoTrimModal";
15
16
  export { VideoThumbnailSelector } from "./modals/VideoThumbnailSelector";
16
17
  // Upload components
@@ -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, { className: "absolute w-full h-full grid 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(Div, { className: "absolute inset-0 flex items-center justify-center", children: [_jsx(Div, { className: "w-6 h-px bg-white/50" }), _jsx(Div, { className: "absolute h-6 w-px bg-white/50" })] })] })] }), _jsxs(Div, { className: "space-y-1", children: [_jsxs(Row, { justify: "between", children: [_jsx(Text, { className: "text-xs font-medium", children: t("cropZoom") }), _jsxs(Text, { className: "text-xs text-zinc-500 dark:text-slate-400", children: [Math.round(zoom * 100), "%"] })] }), _jsxs(Row, { className: "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(Div, { className: "flex gap-2", children: [0.5, 1, 1.5, 2].map((preset) => (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setZoom(preset), className: "px-3 py-1 text-xs rounded-lg bg-zinc-100 dark:bg-slate-800", children: [preset * 100, "%"] }, preset))) })] }), showZoomWarning && (_jsxs(Alert, { variant: "warning", children: [_jsx("strong", { children: t("cropWarningTitle") }), " ", t("cropWarningMessage")] })), _jsxs(Row, { justify: "between", className: "text-xs 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(Div, { className: "flex gap-2 pt-2", 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, { className: "absolute w-full h-full grid 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(Div, { className: "absolute inset-0 flex items-center justify-center", children: [_jsx(Div, { className: "w-6 h-px bg-white/50" }), _jsx(Div, { className: "absolute h-6 w-px bg-white/50" })] })] })] }), _jsxs(Div, { className: "space-y-1", children: [_jsxs(Row, { justify: "between", children: [_jsx(Text, { className: "text-xs font-medium", children: t("cropZoom") }), _jsxs(Text, { className: "text-xs text-zinc-500 dark:text-slate-400", children: [Math.round(zoom * 100), "%"] })] }), _jsxs(Row, { className: "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(Div, { className: "flex gap-2", children: [0.5, 1, 1.5, 2].map((preset) => (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setZoom(preset), className: "px-3 py-1 text-xs rounded-lg 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, { justify: "between", className: "text-xs 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(Div, { className: "flex gap-2 pt-2", 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
  }
@@ -0,0 +1,12 @@
1
+ import "react-advanced-cropper/dist/style.css";
2
+ export interface ImageEditorProps {
3
+ isOpen: boolean;
4
+ imageUrl: string;
5
+ onClose: () => void;
6
+ onSave: (croppedFile: File) => void;
7
+ aspectRatio?: number;
8
+ outputFormat?: "image/jpeg" | "image/png" | "image/webp";
9
+ outputQuality?: number;
10
+ maxOutputWidth?: number;
11
+ }
12
+ export declare function ImageEditor({ isOpen, imageUrl, onClose, onSave, aspectRatio, outputFormat, outputQuality, maxOutputWidth, }: ImageEditorProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,73 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * ImageEditor — WhatsApp-style crop/rotate/zoom editor.
5
+ *
6
+ * Uses react-advanced-cropper for real pixel cropping (not just focal-point).
7
+ * Outputs a cropped File ready for upload.
8
+ */
9
+ import { useState, useRef, useCallback } from "react";
10
+ import { useTranslations } from "next-intl";
11
+ import { Cropper } from "react-advanced-cropper";
12
+ import "react-advanced-cropper/dist/style.css";
13
+ import { Button, Div, Modal, Row, Text } from "../../../ui";
14
+ export function ImageEditor({ isOpen, imageUrl, onClose, onSave, aspectRatio, outputFormat = "image/webp", outputQuality = 0.9, maxOutputWidth = 2048, }) {
15
+ const t = useTranslations("mediaEditor");
16
+ const cropperRef = useRef(null);
17
+ const [rotation, setRotation] = useState(0);
18
+ const [saving, setSaving] = useState(false);
19
+ const handleRotate = useCallback((degrees) => {
20
+ setRotation((r) => (r + degrees) % 360);
21
+ cropperRef.current?.rotateImage(degrees);
22
+ }, []);
23
+ const handleFlipH = useCallback(() => {
24
+ cropperRef.current?.flipImage(true, false);
25
+ }, []);
26
+ const handleFlipV = useCallback(() => {
27
+ cropperRef.current?.flipImage(false, true);
28
+ }, []);
29
+ const handleReset = useCallback(() => {
30
+ setRotation(0);
31
+ cropperRef.current?.reset();
32
+ }, []);
33
+ const applyAspectRatio = useCallback((ratio) => {
34
+ if (!ratio) {
35
+ cropperRef.current?.reset();
36
+ return;
37
+ }
38
+ const coords = cropperRef.current?.getCoordinates();
39
+ if (!coords)
40
+ return;
41
+ cropperRef.current?.setCoordinates({ ...coords, height: coords.width / ratio });
42
+ }, []);
43
+ const handleSave = useCallback(async () => {
44
+ const cropper = cropperRef.current;
45
+ if (!cropper)
46
+ return;
47
+ setSaving(true);
48
+ try {
49
+ const canvas = cropper.getCanvas({
50
+ maxWidth: maxOutputWidth,
51
+ maxHeight: maxOutputWidth,
52
+ });
53
+ if (!canvas)
54
+ return;
55
+ const blob = await new Promise((resolve) => canvas.toBlob(resolve, outputFormat, outputQuality));
56
+ if (!blob)
57
+ return;
58
+ const ext = outputFormat.split("/")[1] || "webp";
59
+ const file = new File([blob], `cropped.${ext}`, { type: outputFormat });
60
+ onSave(file);
61
+ }
62
+ finally {
63
+ setSaving(false);
64
+ }
65
+ }, [maxOutputWidth, outputFormat, outputQuality, onSave]);
66
+ return (_jsx(Modal, { isOpen: isOpen, onClose: onClose, title: t("cropTitle"), size: "lg", children: _jsxs(Div, { className: "space-y-3", children: [_jsx(Text, { variant: "secondary", size: "xs", children: t("cropInstruction") }), _jsx(Div, { className: "relative w-full max-h-[400px] overflow-hidden rounded-lg bg-zinc-100 dark:bg-slate-800", children: _jsx(Cropper, { ref: cropperRef, src: imageUrl, stencilProps: aspectRatio ? { aspectRatio } : undefined, className: "max-h-[400px]" }) }), _jsxs(Row, { gap: "sm", justify: "center", className: "flex-wrap", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => handleRotate(-90), "aria-label": t("cropRotateLeft"), children: _jsxs("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: "M3 10h10a4 4 0 014 4v0a4 4 0 01-4 4H3" }), _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M7 6l-4 4 4 4" })] }) }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => handleRotate(90), "aria-label": t("cropRotateRight"), children: _jsxs("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 10H11a4 4 0 00-4 4v0a4 4 0 004 4h10" }), _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M17 6l4 4-4 4" })] }) }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: handleFlipH, "aria-label": t("cropFlipH"), 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: "M7 16V4l-4 4m0 4l4 4M17 16V4l4 4m0 4l-4 4M12 2v20" }) }) }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: handleFlipV, "aria-label": t("cropFlipV"), children: _jsx("svg", { className: "w-4 h-4 rotate-90", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M7 16V4l-4 4m0 4l4 4M17 16V4l4 4m0 4l-4 4M12 2v20" }) }) }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: handleReset, children: t("cropReset") }), _jsxs(Text, { size: "xs", variant: "secondary", className: "ml-auto", children: [rotation, "\u00B0"] })] }), !aspectRatio && (_jsx(Row, { gap: "sm", justify: "center", className: "flex-wrap", children: [
67
+ { label: "Free", ratio: undefined },
68
+ { label: "1:1", ratio: 1 },
69
+ { label: "4:3", ratio: 4 / 3 },
70
+ { label: "16:9", ratio: 16 / 9 },
71
+ { label: "3:2", ratio: 3 / 2 },
72
+ ].map((preset) => (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => applyAspectRatio(preset.ratio), className: "px-3 py-1 text-xs rounded-lg bg-zinc-100 dark:bg-slate-800", children: preset.label }, preset.label))) })), _jsxs(Div, { className: "flex gap-2 pt-2", 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") })] })] }) }));
73
+ }
@@ -9,6 +9,10 @@ export interface ImageUploadProps {
9
9
  helperText?: string;
10
10
  captureSource?: "file-only" | "camera-only" | "both";
11
11
  enableCrop?: boolean;
12
+ /** Use the advanced pixel cropper instead of the focal-point editor. */
13
+ enableAdvancedCrop?: boolean;
14
+ /** Lock the crop aspect ratio (e.g. 1 for square, 16/9 for widescreen). */
15
+ cropAspectRatio?: number;
12
16
  onCropDataChange?: (cropData: ImageCropData) => void;
13
17
  }
14
- export declare function ImageUpload({ currentImage, onUpload, onChange, accept, maxSizeMB, label, helperText, captureSource, enableCrop, onCropDataChange, }: ImageUploadProps): import("react/jsx-runtime").JSX.Element;
18
+ export declare function ImageUpload({ currentImage, onUpload, onChange, accept, maxSizeMB, label, helperText, captureSource, enableCrop, enableAdvancedCrop, cropAspectRatio, onCropDataChange, }: ImageUploadProps): import("react/jsx-runtime").JSX.Element;
@@ -13,6 +13,7 @@ import { useCamera } from "../../../react";
13
13
  import { Alert, Button, Div, Label, Progress, Row, Spinner, Text, } from "../../../ui";
14
14
  import { MediaImage } from "../MediaImage";
15
15
  import { ImageCropModal } from "../modals/ImageCropModal";
16
+ import { ImageEditor } from "../modals/ImageEditor";
16
17
  import CameraCapture from "./CameraCapture";
17
18
  function formatFileSize(bytes) {
18
19
  if (bytes < 1024 * 1024)
@@ -30,12 +31,13 @@ function matchesMimeAccept(fileType, accept) {
30
31
  return pSub === "*" || pSub === fSub;
31
32
  });
32
33
  }
33
- export function ImageUpload({ currentImage, onUpload, onChange, accept = "image/*", maxSizeMB = 10, label = "Upload Image", helperText, captureSource = "both", enableCrop = true, onCropDataChange, }) {
34
+ export function ImageUpload({ currentImage, onUpload, onChange, accept = "image/*", maxSizeMB = 10, label = "Upload Image", helperText, captureSource = "both", enableCrop = true, enableAdvancedCrop = false, cropAspectRatio, onCropDataChange, }) {
34
35
  const [uploading, setUploading] = useState(false);
35
36
  const [preview, setPreview] = useState(currentImage || "");
36
37
  const [error, setError] = useState("");
37
38
  const [progress, setProgress] = useState(0);
38
39
  const [showCropModal, setShowCropModal] = useState(false);
40
+ const [showAdvancedEditor, setShowAdvancedEditor] = useState(false);
39
41
  const [cropPreviewUrl, setCropPreviewUrl] = useState(null);
40
42
  const [pendingFile, setPendingFile] = useState(null);
41
43
  const [captureMode, setCaptureMode] = useState("file");
@@ -71,7 +73,7 @@ export function ImageUpload({ currentImage, onUpload, onChange, accept = "image/
71
73
  }
72
74
  };
73
75
  const prepareFile = (file) => {
74
- if (!enableCrop) {
76
+ if (!enableCrop && !enableAdvancedCrop) {
75
77
  void performUpload(file);
76
78
  return;
77
79
  }
@@ -80,7 +82,12 @@ export function ImageUpload({ currentImage, onUpload, onChange, accept = "image/
80
82
  if (event.target?.result) {
81
83
  setCropPreviewUrl(event.target.result);
82
84
  setPendingFile(file);
83
- setShowCropModal(true);
85
+ if (enableAdvancedCrop) {
86
+ setShowAdvancedEditor(true);
87
+ }
88
+ else {
89
+ setShowCropModal(true);
90
+ }
84
91
  }
85
92
  };
86
93
  reader.readAsDataURL(file);
@@ -100,6 +107,27 @@ export function ImageUpload({ currentImage, onUpload, onChange, accept = "image/
100
107
  setPendingFile(null);
101
108
  }
102
109
  };
110
+ const handleAdvancedEditorSave = (croppedFile) => {
111
+ setShowAdvancedEditor(false);
112
+ setCropPreviewUrl(null);
113
+ setPendingFile(null);
114
+ void performUpload(croppedFile);
115
+ };
116
+ const openEditor = (url) => {
117
+ setCropPreviewUrl(url);
118
+ if (enableAdvancedCrop)
119
+ setShowAdvancedEditor(true);
120
+ else
121
+ setShowCropModal(true);
122
+ };
123
+ const handleAdvancedEditorClose = () => {
124
+ setShowAdvancedEditor(false);
125
+ setCropPreviewUrl(null);
126
+ if (pendingFile) {
127
+ void performUpload(pendingFile);
128
+ setPendingFile(null);
129
+ }
130
+ };
103
131
  const handleCameraCapture = (blob) => {
104
132
  // blob.type can be empty from some camera implementations; hard-coded
105
133
  // image/webp is always correct here since ImageUpload is photo-only.
@@ -134,10 +162,7 @@ export function ImageUpload({ currentImage, onUpload, onChange, accept = "image/
134
162
  fileInputRef.current.value = "";
135
163
  onChange?.("");
136
164
  };
137
- return (_jsxs(Div, { className: "space-y-3", children: [label && (_jsx(Label, { className: "block text-sm font-medium text-zinc-600 dark:text-zinc-400", children: label })), _jsx(Div, { className: "relative", children: preview ? (_jsxs(Div, { className: "space-y-2", children: [_jsxs(Div, { className: "relative h-32 max-w-xs mx-auto overflow-hidden rounded-lg border border-zinc-200 dark:border-zinc-700", children: [_jsx(MediaImage, { src: preview, alt: "Preview", size: "card" }), uploading && progress > 0 && (_jsx(Div, { className: "absolute inset-x-0 bottom-0", children: _jsx(Progress, { value: progress, size: "sm" }) }))] }), !uploading && (_jsxs(Row, { wrap: true, gap: "sm", children: [enableCrop && (_jsx(Button, { type: "button", onClick: () => {
138
- setCropPreviewUrl(preview);
139
- setShowCropModal(true);
140
- }, variant: "secondary", size: "sm", children: tMediaEditor("editImage") })), _jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), variant: "outline", size: "sm", children: tUpload("change") }), _jsx(Button, { type: "button", onClick: handleRemove, variant: "danger", size: "sm", children: tUpload("remove") })] }))] })) : (_jsxs(_Fragment, { children: [showCamera && isCameraSupported && (_jsx(CameraCapture, { mode: "photo", facingMode: "environment", onCapture: handleCameraCapture, onError: setError })), !showCamera && (_jsxs(Div, { className: "space-y-2", children: [_jsxs(Row, { gap: "sm", className: "flex-wrap", children: [showFileInput && (_jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), disabled: uploading, variant: "outline", size: "sm", children: uploading ? tUpload("uploading") : tUpload("clickToUpload") })), captureSource !== "file-only" && isCameraSupported && (_jsx(Button, { type: "button", onClick: () => setCaptureMode("camera"), disabled: uploading, variant: "ghost", size: "sm", children: t("switchToCamera") })), captureSource !== "file-only" && !isCameraSupported && (_jsx(Button, { type: "button", onClick: () => mobileCaptureRef.current?.click(), disabled: uploading, variant: "ghost", size: "sm", children: t("switchToCamera") }))] }), _jsxs(Text, { size: "xs", variant: "secondary", children: [accept === "image/*"
165
+ return (_jsxs(Div, { className: "space-y-3", children: [label && (_jsx(Label, { className: "block text-sm font-medium text-zinc-600 dark:text-zinc-400", children: label })), _jsx(Div, { className: "relative", children: preview ? (_jsxs(Div, { className: "space-y-2", children: [_jsxs(Div, { className: "relative h-32 max-w-xs mx-auto overflow-hidden rounded-lg border border-zinc-200 dark:border-zinc-700", children: [_jsx(MediaImage, { src: preview, alt: "Preview", size: "card" }), uploading && progress > 0 && (_jsx(Div, { className: "absolute inset-x-0 bottom-0", children: _jsx(Progress, { value: progress, size: "sm" }) }))] }), !uploading && (_jsxs(Row, { wrap: true, gap: "sm", children: [(enableCrop || enableAdvancedCrop) && (_jsx(Button, { type: "button", onClick: () => openEditor(preview), variant: "secondary", size: "sm", children: tMediaEditor("editImage") })), _jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), variant: "outline", size: "sm", children: tUpload("change") }), _jsx(Button, { type: "button", onClick: handleRemove, variant: "danger", size: "sm", children: tUpload("remove") })] }))] })) : (_jsxs(_Fragment, { children: [showCamera && isCameraSupported && (_jsx(CameraCapture, { mode: "photo", facingMode: "environment", onCapture: handleCameraCapture, onError: setError })), !showCamera && (_jsxs(Div, { className: "space-y-2", children: [_jsxs(Row, { gap: "sm", className: "flex-wrap", children: [showFileInput && (_jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), disabled: uploading, variant: "outline", size: "sm", children: uploading ? tUpload("uploading") : tUpload("clickToUpload") })), captureSource !== "file-only" && isCameraSupported && (_jsx(Button, { type: "button", onClick: () => setCaptureMode("camera"), disabled: uploading, variant: "ghost", size: "sm", children: t("switchToCamera") })), captureSource !== "file-only" && !isCameraSupported && (_jsx(Button, { type: "button", onClick: () => mobileCaptureRef.current?.click(), disabled: uploading, variant: "ghost", size: "sm", children: t("switchToCamera") }))] }), _jsxs(Text, { size: "xs", variant: "secondary", children: [accept === "image/*"
141
166
  ? `JPG PNG GIF WebP`
142
- : accept.split(",").map((a) => a.split("/")[1]?.toUpperCase() ?? a).join(" "), " ", "\u2014 max ", maxSizeMB, "MB"] })] }))] })) }), _jsx("input", { ref: fileInputRef, type: "file", accept: accept, onChange: handleFileChange, className: "hidden", "aria-hidden": "true", "data-testid": "media-upload-input" }), showCamera && !isCameraSupported && (_jsx("input", { ref: mobileCaptureRef, type: "file", accept: "image/*", 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 }), uploading && (_jsxs(Row, { className: "gap-2", children: [_jsx(Spinner, { size: "sm" }), _jsx(Text, { size: "sm", variant: "secondary", children: tUpload("uploadingProgress", { progress }) })] })), cropPreviewUrl && (_jsx(ImageCropModal, { isOpen: showCropModal, imageUrl: cropPreviewUrl, onClose: handleCropClose, onSave: handleCropSave }))] }));
167
+ : accept.split(",").map((a) => a.split("/")[1]?.toUpperCase() ?? a).join(" "), " ", "\u2014 max ", maxSizeMB, "MB"] })] }))] })) }), _jsx("input", { ref: fileInputRef, type: "file", accept: accept, onChange: handleFileChange, className: "hidden", "aria-hidden": "true", "data-testid": "media-upload-input" }), showCamera && !isCameraSupported && (_jsx("input", { ref: mobileCaptureRef, type: "file", accept: "image/*", 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 }), uploading && (_jsxs(Row, { className: "gap-2", children: [_jsx(Spinner, { size: "sm" }), _jsx(Text, { size: "sm", variant: "secondary", children: tUpload("uploadingProgress", { progress }) })] })), cropPreviewUrl && !enableAdvancedCrop && (_jsx(ImageCropModal, { isOpen: showCropModal, imageUrl: cropPreviewUrl, onClose: handleCropClose, onSave: handleCropSave })), cropPreviewUrl && enableAdvancedCrop && (_jsx(ImageEditor, { isOpen: showAdvancedEditor, imageUrl: cropPreviewUrl, onClose: handleAdvancedEditorClose, onSave: handleAdvancedEditorSave, aspectRatio: cropAspectRatio }))] }));
143
168
  }
@@ -45,5 +45,9 @@ export interface MediaUploadFieldProps {
45
45
  * Set true after successful save to prevent auto-cleanup on unmount.
46
46
  */
47
47
  isPersisted?: boolean;
48
+ /** Allow selecting multiple files at once. Each is uploaded individually. */
49
+ multiple?: boolean;
50
+ /** Called with each successfully uploaded URL when `multiple` is true. */
51
+ onAddUrl?: (url: string) => void;
48
52
  }
49
- export declare function MediaUploadField({ label, value, onChange, onChangeField, onUpload, kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled, helperText, captureSource, captureMode, enableTrim, enableThumbnail, onThumbnailChange, showYoutube, showExternal, onAbort, onStagedUrlsChange, isPersisted, }: MediaUploadFieldProps): import("react/jsx-runtime").JSX.Element;
53
+ export declare function MediaUploadField({ label, value, onChange, onChangeField, onUpload, kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled, helperText, captureSource, captureMode, enableTrim, enableThumbnail, onThumbnailChange, showYoutube, showExternal, onAbort, onStagedUrlsChange, isPersisted, multiple, onAddUrl, }: MediaUploadFieldProps): import("react/jsx-runtime").JSX.Element;
@@ -72,13 +72,13 @@ function ExternalUrlTabPanel({ extInput, setExtInput, extError, setExtError, onA
72
72
  return (_jsxs(Div, { className: "space-y-2", children: [_jsxs(Row, { gap: "sm", children: [_jsx("input", { type: "url", value: extInput, onChange: (e) => { setExtInput(e.target.value); setExtError(""); }, placeholder: "https://example.com/image.jpg", className: "appkit-input flex-1" }), _jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: onApply, children: "Apply" })] }), extError && _jsx(Alert, { variant: "error", children: extError }), _jsx(Text, { size: "xs", variant: "secondary", children: "External URLs are stored as-is and are not watermarked." })] }));
73
73
  }
74
74
  function MediaPreviewPanel({ value, label, disabled, enableTrim, enableThumbnail, onEditVideo, onRemove, tUpload, tMediaEditor, }) {
75
- return (_jsxs(Div, { className: "rounded-lg border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-900 p-3", children: [isVideo(value) ? (_jsx(Div, { className: "relative aspect-video overflow-hidden rounded-lg", children: _jsx(MediaVideo, { src: value, alt: label, controls: true, objectFit: "contain" }) })) : isImage(value) ? (_jsx(Div, { className: "relative aspect-video overflow-hidden rounded-lg", children: _jsx(MediaImage, { src: value, alt: label, size: "card", objectFit: "contain" }) })) : isPdf(value) ? (_jsxs(Row, { gap: "md", align: "center", children: [_jsx(Div, { "aria-hidden": true, className: "flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300", children: _jsx("span", { className: "text-xs font-bold", children: "PDF" }) }), _jsx("a", { href: value, target: "_blank", rel: "noopener noreferrer", className: "text-sm font-medium text-blue-600 underline break-all dark:text-blue-400", children: filenameFromUrl(value) })] })) : (_jsx("a", { href: value, target: "_blank", rel: "noopener noreferrer", className: "text-sm underline break-all text-blue-600 dark:text-blue-400", children: filenameFromUrl(value) })), !disabled && (_jsxs(Row, { wrap: true, gap: "sm", className: "mt-2", children: [isVideo(value) && (enableTrim || enableThumbnail) && (_jsx(Button, { type: "button", onClick: () => onEditVideo(value), variant: "secondary", size: "sm", children: tMediaEditor("editVideo") })), _jsx(Button, { type: "button", onClick: onRemove, variant: "danger", size: "sm", children: tUpload("remove") })] }))] }));
75
+ return (_jsxs(Div, { className: "rounded-lg border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-900 p-3", children: [isVideo(value) ? (_jsxs(Div, { className: "space-y-2", children: [_jsx(Div, { className: "relative aspect-video overflow-hidden rounded-lg", children: _jsx(MediaVideo, { src: value, alt: label, controls: true, objectFit: "contain" }) }), _jsx(Text, { size: "xs", variant: "secondary", children: "Video uploaded successfully. Large videos may take longer to process." })] })) : isImage(value) ? (_jsx(Div, { className: "relative aspect-video overflow-hidden rounded-lg", children: _jsx(MediaImage, { src: value, alt: label, size: "card", objectFit: "contain" }) })) : isPdf(value) ? (_jsxs(Div, { className: "space-y-2", children: [_jsx(Div, { className: "relative w-full overflow-hidden rounded-lg border border-zinc-200 dark:border-zinc-700", style: { height: 280 }, children: _jsx("iframe", { src: value, title: filenameFromUrl(value), className: "w-full h-full border-0" }) }), _jsxs(Row, { gap: "md", align: "center", children: [_jsx(Div, { "aria-hidden": true, className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300", children: _jsx("span", { className: "text-xs font-bold", children: "PDF" }) }), _jsx("a", { href: value, target: "_blank", rel: "noopener noreferrer", className: "text-sm font-medium text-blue-600 underline break-all dark:text-blue-400", children: filenameFromUrl(value) })] })] })) : (_jsx("a", { href: value, target: "_blank", rel: "noopener noreferrer", className: "text-sm underline break-all text-blue-600 dark:text-blue-400", children: filenameFromUrl(value) })), !disabled && (_jsxs(Row, { wrap: true, gap: "sm", className: "mt-2", children: [isVideo(value) && (enableTrim || enableThumbnail) && (_jsx(Button, { type: "button", onClick: () => onEditVideo(value), variant: "secondary", size: "sm", children: tMediaEditor("editVideo") })), _jsx(Button, { type: "button", onClick: onRemove, variant: "danger", size: "sm", children: tUpload("remove") })] }))] }));
76
76
  }
77
77
  export function MediaUploadField({ label, value, onChange, onChangeField, onUpload,
78
78
  // SB-UNI-Z5 2026-05-13 — `kind` derives accept/maxSizeMB when not set
79
79
  // explicitly; explicit props still win. "*" + 50 stay as the floor for
80
80
  // back-compat with callers that pass neither.
81
- kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText, captureSource = "both", captureMode = "photo", enableTrim = true, enableThumbnail = true, onThumbnailChange, showYoutube = true, showExternal = true, onAbort, onStagedUrlsChange, isPersisted = false, }) {
81
+ kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText, captureSource = "both", captureMode = "photo", enableTrim = true, enableThumbnail = true, onThumbnailChange, showYoutube = true, showExternal = true, onAbort, onStagedUrlsChange, isPersisted = false, multiple = false, onAddUrl, }) {
82
82
  const [isLoading, setIsLoading] = useState(false);
83
83
  const [error, setError] = useState(null);
84
84
  const [pendingVideoUrl, setPendingVideoUrl] = useState(null);
@@ -240,21 +240,35 @@ kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText
240
240
  onChange(pendingVideoUrl);
241
241
  setPendingVideoUrl(null);
242
242
  };
243
- const handleFileChange = async (e) => {
244
- const file = e.target.files?.[0];
245
- if (!file)
246
- return;
243
+ const uploadSingleFile = async (file) => {
247
244
  const fileSizeMB = file.size / 1024 / 1024;
248
245
  if (fileSizeMB > maxSizeMB) {
249
- setError(`File size must be less than ${maxSizeMB}MB`);
250
- return;
246
+ setError(`${file.name}: exceeds ${maxSizeMB}MB limit`);
247
+ return null;
251
248
  }
249
+ const url = await onUpload(file);
250
+ stageUrl(url);
251
+ return { url, type: file.type };
252
+ };
253
+ const handleFileChange = async (e) => {
254
+ const files = e.target.files;
255
+ if (!files || files.length === 0)
256
+ return;
252
257
  setError(null);
253
258
  setIsLoading(true);
254
259
  try {
255
- const url = await onUpload(file);
256
- stageUrl(url);
257
- afterUpload(url, file.type);
260
+ if (multiple && files.length > 1) {
261
+ for (const file of Array.from(files)) {
262
+ const result = await uploadSingleFile(file);
263
+ if (result)
264
+ onAddUrl?.(result.url);
265
+ }
266
+ }
267
+ else {
268
+ const result = await uploadSingleFile(files[0]);
269
+ if (result)
270
+ afterUpload(result.url, result.type);
271
+ }
258
272
  }
259
273
  catch (err) {
260
274
  setError(err instanceof Error ? err.message : "Upload failed");
@@ -306,5 +320,5 @@ kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText
306
320
  return (_jsxs(Div, { className: "space-y-2", children: [_jsx(Label, { className: "block text-sm font-medium text-zinc-600 dark:text-zinc-400", 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: [
307
321
  "appkit-media-upload__source-tab",
308
322
  sourceTab === tab ? "appkit-media-upload__source-tab--active" : "",
309
- ].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, { className: "gap-2", 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, { type: "button", onClick: () => mobileCaptureRef.current?.click(), variant: "ghost", className: "w-full py-3 border-2 border-dashed border-zinc-200 dark:border-zinc-700 rounded-xl text-sm text-zinc-500 dark:text-zinc-400", children: t("switchToCamera") })), showFileInput && (_jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), variant: "ghost", className: "w-full py-3 border-2 border-dashed border-zinc-200 dark:border-zinc-700 rounded-xl text-sm text-zinc-500 dark:text-zinc-400", children: value ? tUpload("replaceFile") : tUpload("chooseFile") }))] })), isLoading && (_jsxs(Row, { className: "gap-2", children: [_jsx(Spinner, { size: "sm" }), _jsx(Text, { size: "sm", variant: "secondary", children: tUpload("uploading") })] })), _jsx("input", { ref: fileInputRef, type: "file", accept: accept, 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 }))] }));
323
+ ].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, { className: "gap-2", 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, { type: "button", onClick: () => mobileCaptureRef.current?.click(), variant: "ghost", className: "w-full py-3 border-2 border-dashed border-zinc-200 dark:border-zinc-700 rounded-xl text-sm text-zinc-500 dark:text-zinc-400", children: t("switchToCamera") })), showFileInput && (_jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), variant: "ghost", className: "w-full py-3 border-2 border-dashed border-zinc-200 dark:border-zinc-700 rounded-xl text-sm text-zinc-500 dark:text-zinc-400", children: value ? tUpload("replaceFile") : tUpload("chooseFile") }))] })), isLoading && (_jsxs(Row, { className: "gap-2", 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 }))] }));
310
324
  }
@@ -4,14 +4,14 @@ import { formatCurrency } from "../../../utils/number.formatter";
4
4
  import { getDefaultLocale } from "../../../core/baseline-resolver";
5
5
  import { THEME_CONSTANTS } from "../../../tokens";
6
6
  const STATUS_COLORS = {
7
- pending: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300",
8
- confirmed: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300",
9
- processing: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300",
7
+ pending: "bg-warning-surface text-warning",
8
+ confirmed: "bg-info-surface text-info",
9
+ processing: "bg-info-surface text-info",
10
10
  shipped: "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300",
11
- delivered: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300",
12
- cancelled: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300",
13
- refunded: "bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300",
14
- return_requested: "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300",
11
+ delivered: "bg-success-surface text-success",
12
+ cancelled: "bg-error-surface text-error",
13
+ refunded: "bg-warning-surface text-warning",
14
+ return_requested: "bg-warning-surface text-warning",
15
15
  returned: "bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-300",
16
16
  };
17
17
  export function OrderCard({ order, onClick, labels = {} }) {
@@ -12,12 +12,12 @@ function RefundRow({ event, currency }) {
12
12
  }
13
13
  /** Warning triangle icon — inline SVG kept here since it's a single-use decorative element. */
14
14
  function WarningIcon() {
15
- return (_jsx("svg", { className: "h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: _jsx("path", { fillRule: "evenodd", d: "M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z", clipRule: "evenodd" }) }));
15
+ return (_jsx("svg", { className: "h-4 w-4 shrink-0 text-warning", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: _jsx("path", { fillRule: "evenodd", d: "M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z", clipRule: "evenodd" }) }));
16
16
  }
17
17
  export function RefundHistoryTable({ order, className = "" }) {
18
18
  const events = order.refunds ?? [];
19
19
  const currency = order.currency ?? "INR";
20
20
  if (events.length === 0 && order.contestable !== false)
21
21
  return null;
22
- return (_jsxs(Div, { className: `rounded-xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900 ${className}`, children: [order.contestable === false && (_jsx(Div, { className: "rounded-t-xl bg-amber-50 px-4 py-3 dark:bg-amber-950/30", children: _jsxs(Row, { gap: "sm", align: "center", children: [_jsx(WarningIcon, {}), _jsx(Text, { size: "sm", weight: "semibold", className: "text-amber-700 dark:text-amber-400", children: REFUND_COPY.history.nonContestableBanner })] }) })), events.length > 0 && (_jsxs(Div, { className: "p-4", children: [_jsx(Heading, { level: 4, className: "mb-3 text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: REFUND_COPY.history.heading }), _jsx(Stack, { gap: "none", children: events.map((e) => (_jsx(RefundRow, { event: e, currency: currency }, e.refundId))) })] }))] }));
22
+ return (_jsxs(Div, { className: `rounded-xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900 ${className}`, children: [order.contestable === false && (_jsx(Div, { className: "rounded-t-xl bg-warning-surface px-4 py-3", children: _jsxs(Row, { gap: "sm", align: "center", children: [_jsx(WarningIcon, {}), _jsx(Text, { size: "sm", weight: "semibold", className: "text-warning", children: REFUND_COPY.history.nonContestableBanner })] }) })), events.length > 0 && (_jsxs(Div, { className: "p-4", children: [_jsx(Heading, { level: 4, className: "mb-3 text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: REFUND_COPY.history.heading }), _jsx(Stack, { gap: "none", children: events.map((e) => (_jsx(RefundRow, { event: e, currency: currency }, e.refundId))) })] }))] }));
23
23
  }
@@ -12,7 +12,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
12
  * does not talk to the API directly.
13
13
  */
14
14
  import { useState } from "react";
15
- import { Button, Checkbox, Div, Stack, Text, Textarea } from "../../../ui";
15
+ import { Button, Checkbox, Div, Span, Stack, Text, Textarea } from "../../../ui";
16
16
  import { REFUND_COPY } from "../../../_internal/shared/features/orders/refund-copy";
17
17
  import { formatCurrency } from "../../../utils/number.formatter";
18
18
  const ACK_COUNT = REFUND_COPY.request.acknowledgments.length;
@@ -26,7 +26,7 @@ export function RefundRequestView({ order, onSubmitRequest, isLoading = false, c
26
26
  return (_jsx(Div, { className: `rounded-xl border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-900 ${className}`, children: _jsx(Text, { size: "sm", color: "muted", children: REFUND_COPY.request.nonRefundableMessage }) }));
27
27
  }
28
28
  if (order.contestable === false) {
29
- return (_jsx(Div, { className: `rounded-xl border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-950/30 ${className}`, children: _jsx(Text, { size: "sm", className: "text-amber-700 dark:text-amber-400", children: REFUND_COPY.request.alreadyRefundedMessage }) }));
29
+ return (_jsx(Div, { className: `rounded-xl border border-amber-200 bg-warning-surface p-4 dark:border-amber-800 ${className}`, children: _jsx(Text, { size: "sm", className: "text-warning", children: REFUND_COPY.request.alreadyRefundedMessage }) }));
30
30
  }
31
31
  const handleToggle = (i) => setAcknowledged((prev) => prev.map((v, idx) => (idx === i ? !v : v)));
32
32
  const handleSubmit = async () => {
@@ -38,5 +38,5 @@ export function RefundRequestView({ order, onSubmitRequest, isLoading = false, c
38
38
  setError(err instanceof Error ? err.message : REFUND_COPY.request.errorFallback);
39
39
  }
40
40
  };
41
- return (_jsxs(Stack, { gap: "md", className: className, children: [_jsxs(Text, { size: "sm", color: "muted", children: [REFUND_COPY.request.orderTotalLabel, " ", _jsx("strong", { children: formatCurrency(order.totalPrice / 100, order.currency ?? "INR") })] }), _jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "sm", weight: "semibold", children: [REFUND_COPY.request.reasonLabel, " ", _jsx("span", { "aria-hidden": true, children: "*" })] }), _jsx(Textarea, { value: reason, onChange: (e) => setReason(e.target.value), placeholder: REFUND_COPY.request.reasonPlaceholder, rows: 3, "aria-required": "true", "aria-label": REFUND_COPY.request.reasonLabel })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", children: REFUND_COPY.request.acknowledgeHeading }), REFUND_COPY.request.acknowledgments.map((text, i) => (_jsx(Checkbox, { checked: acknowledged[i], onChange: () => handleToggle(i), label: _jsx(Text, { size: "sm", className: "leading-snug", children: text }), "aria-label": `Acknowledgment ${i + 1}` }, i)))] }), error && (_jsx(Text, { size: "sm", color: "danger", role: "alert", children: error })), _jsx(Button, { variant: "danger", onClick: handleSubmit, disabled: !canSubmit, "aria-busy": isLoading, children: isLoading ? REFUND_COPY.request.submittingLabel : REFUND_COPY.request.submitLabel })] }));
41
+ return (_jsxs(Stack, { gap: "md", className: className, children: [_jsxs(Text, { size: "sm", color: "muted", children: [REFUND_COPY.request.orderTotalLabel, " ", _jsx(Span, { weight: "bold", children: formatCurrency(order.totalPrice / 100, order.currency ?? "INR") })] }), _jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "sm", weight: "semibold", children: [REFUND_COPY.request.reasonLabel, " ", _jsx("span", { "aria-hidden": true, children: "*" })] }), _jsx(Textarea, { value: reason, onChange: (e) => setReason(e.target.value), placeholder: REFUND_COPY.request.reasonPlaceholder, rows: 3, "aria-required": "true", "aria-label": REFUND_COPY.request.reasonLabel })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", children: REFUND_COPY.request.acknowledgeHeading }), REFUND_COPY.request.acknowledgments.map((text, i) => (_jsx(Checkbox, { checked: acknowledged[i], onChange: () => handleToggle(i), label: _jsx(Text, { size: "sm", className: "leading-snug", children: text }), "aria-label": `Acknowledgment ${i + 1}` }, i)))] }), error && (_jsx(Text, { size: "sm", color: "danger", role: "alert", children: error })), _jsx(Button, { variant: "danger", onClick: handleSubmit, disabled: !canSubmit, "aria-busy": isLoading, children: isLoading ? REFUND_COPY.request.submittingLabel : REFUND_COPY.request.submitLabel })] }));
42
42
  }
@@ -27,7 +27,7 @@ export function PreOrderActionsClient({ productId, price, currency, depositAmoun
27
27
  }
28
28
  });
29
29
  }
30
- return (_jsxs(Div, { className: "space-y-4", children: [price !== null && (_jsxs(Div, { children: [_jsx(Text, { className: "text-2xl font-bold text-zinc-900 dark:text-zinc-50", children: formatCurrency(price, currency) }), depositAmount !== null && (_jsxs(Text, { className: "mt-0.5 text-xs text-zinc-500 dark:text-zinc-400", children: ["Reserve with ", formatCurrency(depositAmount, currency), depositPercent !== null ? ` (${depositPercent}% deposit)` : ""] }))] })), error && (_jsx(Text, { className: "text-xs text-red-600 dark:text-red-400", children: error })), success && (_jsx(Text, { className: "text-xs text-emerald-600 dark:text-emerald-400", children: "\u2713 Reserved! Redirecting to checkout\u2026" })), _jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to reserve this pre-order. Please log in or create an account to continue." }), _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isPending, onClick: handleReserve, children: isPending ? "Processing…" : "Reserve Now" }), isCancellable && (_jsx(Text, { className: "text-center text-xs text-zinc-500 dark:text-zinc-400", children: "\u2713 Free cancellation before production" }))] }), tags.length > 0 && (_jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx("div", { className: "flex flex-wrap gap-1.5", children: tags.map((tag) => (_jsx("span", { className: "rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-1 text-xs text-zinc-600 dark:text-zinc-300", children: tag }, tag))) }) })), _jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx("div", { className: "flex flex-wrap gap-4 justify-center text-center", children: [
30
+ return (_jsxs(Div, { className: "space-y-4", children: [price !== null && (_jsxs(Div, { children: [_jsx(Text, { className: "text-2xl font-bold text-zinc-900 dark:text-zinc-50", children: formatCurrency(price, currency) }), depositAmount !== null && (_jsxs(Text, { className: "mt-0.5 text-xs text-zinc-500 dark:text-zinc-400", children: ["Reserve with ", formatCurrency(depositAmount, currency), depositPercent !== null ? ` (${depositPercent}% deposit)` : ""] }))] })), error && (_jsx(Text, { className: "text-xs text-error", children: error })), success && (_jsx(Text, { className: "text-xs text-success", children: "\u2713 Reserved! Redirecting to checkout\u2026" })), _jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to reserve this pre-order. Please log in or create an account to continue." }), _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isPending, onClick: handleReserve, children: isPending ? "Processing…" : "Reserve Now" }), isCancellable && (_jsx(Text, { className: "text-center text-xs text-zinc-500 dark:text-zinc-400", children: "\u2713 Free cancellation before production" }))] }), tags.length > 0 && (_jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx("div", { className: "flex flex-wrap gap-1.5", children: tags.map((tag) => (_jsx("span", { className: "rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-1 text-xs text-zinc-600 dark:text-zinc-300", children: tag }, tag))) }) })), _jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx("div", { className: "flex flex-wrap gap-4 justify-center text-center", children: [
31
31
  { icon: "🔒", label: "Secure\nPayment" },
32
32
  { icon: "📅", label: "Guaranteed\nDelivery" },
33
33
  { icon: "↩", label: "Free\nCancellation" },
@@ -1,11 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Div, Span } from "../../../ui";
3
3
  const STATUS_MAP = {
4
- pending: { label: "Pending", color: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300" },
5
- confirmed: { label: "Confirmed", color: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300" },
6
- ready: { label: "Ready", color: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300" },
4
+ pending: { label: "Pending", color: "bg-warning-surface text-warning" },
5
+ confirmed: { label: "Confirmed", color: "bg-info-surface text-info" },
6
+ ready: { label: "Ready", color: "bg-success-surface text-success" },
7
7
  fulfilled: { label: "Fulfilled", color: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300" },
8
- cancelled: { label: "Cancelled", color: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300" },
8
+ cancelled: { label: "Cancelled", color: "bg-error-surface text-error" },
9
9
  };
10
10
  export function PreOrderBadge({ status, className = "" }) {
11
11
  const { label, color } = STATUS_MAP[status] ?? STATUS_MAP.pending;