@kasuvia/sdk 0.0.8 → 0.0.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 (176) hide show
  1. package/README.md +42 -15
  2. package/dist/AddressAutocomplete-CjcqPElD.d.mts +46 -0
  3. package/dist/AddressAutocomplete-CvJHuRkf.d.ts +46 -0
  4. package/dist/components/index.d.mts +497 -0
  5. package/dist/components/index.d.ts +497 -0
  6. package/dist/components/index.js +4217 -0
  7. package/dist/components/index.js.map +1 -0
  8. package/dist/components/index.mjs +4194 -0
  9. package/dist/components/index.mjs.map +1 -0
  10. package/dist/config/server.d.mts +26 -0
  11. package/dist/config/server.d.ts +26 -0
  12. package/dist/config/server.js +38 -0
  13. package/dist/config/server.js.map +1 -0
  14. package/dist/config/server.mjs +13 -0
  15. package/dist/config/server.mjs.map +1 -0
  16. package/dist/hooks/index.d.mts +393 -160
  17. package/dist/hooks/index.d.ts +393 -160
  18. package/dist/hooks/index.js +2792 -2137
  19. package/dist/hooks/index.js.map +1 -1
  20. package/dist/hooks/index.mjs +2734 -2121
  21. package/dist/hooks/index.mjs.map +1 -1
  22. package/dist/index-CNW_LINy.d.mts +578 -0
  23. package/dist/index-CNW_LINy.d.ts +578 -0
  24. package/dist/{index-C1chUA3P.d.mts → index-F7N6UItS.d.mts} +1 -1
  25. package/dist/{index-C1chUA3P.d.ts → index-F7N6UItS.d.ts} +1 -1
  26. package/dist/index-NkI10J_E.d.mts +40 -0
  27. package/dist/index-NkI10J_E.d.ts +40 -0
  28. package/dist/index.d.mts +20437 -103
  29. package/dist/index.d.ts +20437 -103
  30. package/dist/index.js +2111 -1802
  31. package/dist/index.js.map +1 -1
  32. package/dist/index.mjs +2006 -1786
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/media.service-Cx7dQjzX.d.mts +1393 -0
  35. package/dist/media.service-mrBFJ2UQ.d.ts +1393 -0
  36. package/dist/services/index.d.mts +908 -2
  37. package/dist/services/index.d.ts +908 -2
  38. package/dist/services/index.js +1492 -1686
  39. package/dist/services/index.js.map +1 -1
  40. package/dist/services/index.mjs +1431 -1670
  41. package/dist/services/index.mjs.map +1 -1
  42. package/package.json +46 -6
  43. package/src/components/bespoke/BespokeAvailabilityBadge.tsx +32 -0
  44. package/src/components/bespoke/BespokeProgressTracker.tsx +58 -0
  45. package/src/components/bespoke/index.ts +3 -0
  46. package/src/components/booking/SlotPicker.tsx +97 -0
  47. package/src/components/booking/index.ts +1 -0
  48. package/src/components/cart/CartItem.tsx +81 -0
  49. package/src/components/cart/CartSidebar.tsx +115 -0
  50. package/src/components/cart/CartTypeMismatchBanner.tsx +55 -0
  51. package/src/components/cart/index.ts +3 -0
  52. package/src/components/index.ts +8 -0
  53. package/src/components/orders/DeliverableReviewer.tsx +139 -0
  54. package/src/components/orders/index.ts +1 -0
  55. package/src/components/product/ProductCard.tsx +350 -0
  56. package/src/components/product/ProductGrid.tsx +84 -0
  57. package/src/components/product/index.ts +2 -0
  58. package/src/components/shared/AddressAutocomplete.tsx +212 -0
  59. package/src/components/shared/AnalyticsScripts.tsx +56 -0
  60. package/src/components/shared/CookieConsentBanner.tsx +98 -0
  61. package/src/components/shared/ImageEditorModal.tsx +1066 -0
  62. package/src/components/shared/ImageViewerModal.tsx +598 -0
  63. package/src/components/shared/LucideReactIcon.tsx +52 -0
  64. package/src/components/shared/MapPinDrop.tsx +101 -0
  65. package/src/components/shared/MultiSelectPicker.tsx +193 -0
  66. package/src/components/shared/OfflineBanner.tsx +46 -0
  67. package/src/components/shared/SearchableInput.tsx +153 -0
  68. package/src/components/shared/SearchablePopover.tsx +166 -0
  69. package/src/components/shared/SmartImage.tsx +311 -0
  70. package/src/components/shared/TruncatedValue.tsx +27 -0
  71. package/src/components/shared/UnsavedChangesGuard.tsx +138 -0
  72. package/src/components/shared/index.ts +15 -0
  73. package/src/components/skeletons/CardSkeleton.tsx +26 -0
  74. package/src/components/skeletons/CartItemSkeleton.tsx +26 -0
  75. package/src/components/skeletons/OrderItemSkeleton.tsx +26 -0
  76. package/src/components/skeletons/ProductCardSkeleton.tsx +26 -0
  77. package/src/components/skeletons/TableSkeleton.tsx +42 -0
  78. package/src/components/skeletons/index.ts +5 -0
  79. package/src/config/index.ts +22 -4
  80. package/src/config/server.ts +45 -0
  81. package/src/context/sdk-context.tsx +5 -2
  82. package/src/generated/auth-openapi.ts +2935 -0
  83. package/src/generated/cart-openapi.ts +652 -0
  84. package/src/generated/cms-openapi.ts +1668 -0
  85. package/src/generated/crm-openapi.ts +986 -0
  86. package/src/generated/graphql.ts +126 -0
  87. package/src/generated/index.ts +11 -0
  88. package/src/generated/media-openapi.ts +613 -0
  89. package/src/generated/notification-openapi.ts +862 -0
  90. package/src/generated/payment-openapi.ts +2713 -0
  91. package/src/generated/store-openapi.ts +9510 -0
  92. package/src/graphql/operations/collections.graphql +32 -0
  93. package/src/graphql/operations/products.graphql +197 -0
  94. package/src/graphql/operations/wishlist.graphql +25 -0
  95. package/src/hooks/index.ts +9 -2
  96. package/src/hooks/use-auth.ts +163 -69
  97. package/src/hooks/use-bespoke-availability.ts +39 -22
  98. package/src/hooks/use-business-config.ts +51 -0
  99. package/src/hooks/use-cart.ts +310 -238
  100. package/src/hooks/use-cms.ts +107 -127
  101. package/src/hooks/use-collections.ts +35 -55
  102. package/src/hooks/use-consultations.ts +9 -136
  103. package/src/hooks/use-countries.ts +19 -28
  104. package/src/hooks/use-currencies.ts +8 -20
  105. package/src/hooks/use-customer-address.ts +64 -0
  106. package/src/hooks/use-feedback.ts +34 -0
  107. package/src/hooks/use-languages.ts +26 -39
  108. package/src/hooks/use-media-upload.ts +23 -0
  109. package/src/hooks/use-notification-stream.ts +88 -0
  110. package/src/hooks/use-notifications.ts +163 -0
  111. package/src/hooks/use-order-stream.ts +29 -29
  112. package/src/hooks/use-orders.ts +91 -73
  113. package/src/hooks/use-payment.ts +116 -84
  114. package/src/hooks/use-products.ts +65 -269
  115. package/src/hooks/use-push-notifications.ts +122 -0
  116. package/src/hooks/use-reviews.ts +175 -0
  117. package/src/hooks/use-service-booking.ts +109 -132
  118. package/src/hooks/use-states.ts +16 -30
  119. package/src/hooks/use-wishlist.ts +44 -69
  120. package/src/index.ts +15 -11
  121. package/src/services/auth.service.ts +422 -132
  122. package/src/services/bespoke.service.ts +69 -10
  123. package/src/services/cart.service.ts +155 -186
  124. package/src/services/cms.service.ts +41 -50
  125. package/src/services/collection.service.ts +25 -63
  126. package/src/services/config.service.ts +10 -21
  127. package/src/services/consultation.service.ts +36 -93
  128. package/src/services/feedback.service.ts +76 -0
  129. package/src/services/index.ts +7 -1
  130. package/src/services/legal.service.ts +62 -0
  131. package/src/services/logistics.service.ts +235 -53
  132. package/src/services/media.service.ts +115 -0
  133. package/src/services/notifications.service.ts +204 -20
  134. package/src/services/order.service.ts +498 -127
  135. package/src/services/payment.service.ts +149 -100
  136. package/src/services/product.service.ts +157 -814
  137. package/src/services/review.service.ts +211 -0
  138. package/src/services/service-booking.service.ts +198 -99
  139. package/src/services/wishlist.service.ts +27 -74
  140. package/src/types/auth/index.ts +161 -18
  141. package/src/types/business/index.ts +9 -1
  142. package/src/types/cms/index.ts +91 -6
  143. package/src/types/cms/legal.ts +18 -0
  144. package/src/types/consultations/index.ts +60 -68
  145. package/src/types/index.ts +5 -3
  146. package/src/types/media/index.ts +35 -3
  147. package/src/types/payments/index.ts +80 -16
  148. package/src/types/reviews/index.ts +116 -0
  149. package/src/types/services/index.ts +113 -38
  150. package/src/types/store/checkout.ts +53 -0
  151. package/src/types/store/index.ts +273 -52
  152. package/src/types/store/invoice.ts +31 -0
  153. package/src/types/store/shipping.ts +84 -3
  154. package/src/types/ui/index.ts +1 -1
  155. package/src/utils/checkout.ts +19 -3
  156. package/src/utils/cookieConsent.ts +34 -0
  157. package/src/utils/format.ts +408 -0
  158. package/src/utils/geolocation.ts +134 -0
  159. package/src/utils/graphql-url.ts +47 -0
  160. package/src/utils/guest-session.ts +59 -0
  161. package/src/utils/http-client.ts +147 -0
  162. package/src/utils/index.ts +11 -4
  163. package/src/utils/status-colors.ts +79 -0
  164. package/src/utils/token-refresh.ts +115 -0
  165. package/src/utils/validators.ts +13 -0
  166. package/src/utils/youtube.ts +18 -0
  167. package/dist/index-C5YO9gTl.d.mts +0 -507
  168. package/dist/index-C5YO9gTl.d.ts +0 -507
  169. package/dist/index-CefG7_xq.d.mts +0 -793
  170. package/dist/index-DeHCf3zI.d.ts +0 -793
  171. package/src/graphql/collections.graphql.ts +0 -48
  172. package/src/graphql/index.ts +0 -7
  173. package/src/graphql/products.graphql.ts +0 -860
  174. package/src/graphql/wishlist.graphql.ts +0 -34
  175. package/src/hooks/use-consultation-stream.ts +0 -142
  176. package/src/types/bespoke/index.ts +0 -39
@@ -0,0 +1,4217 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/components/index.ts
31
+ var components_exports = {};
32
+ __export(components_exports, {
33
+ AddressAutocomplete: () => AddressAutocomplete,
34
+ AnalyticsScripts: () => AnalyticsScripts,
35
+ BespokeAvailabilityBadge: () => BespokeAvailabilityBadge,
36
+ BespokeProgressTracker: () => BespokeProgressTracker,
37
+ CardSkeleton: () => CardSkeleton,
38
+ CartItem: () => CartItem,
39
+ CartItemSkeleton: () => CartItemSkeleton,
40
+ CartSidebar: () => CartSidebar,
41
+ CartTypeMismatchBanner: () => CartTypeMismatchBanner,
42
+ CookieConsentBanner: () => CookieConsentBanner,
43
+ DeliverableReviewer: () => DeliverableReviewer,
44
+ ImageEditorModal: () => ImageEditorModal,
45
+ ImageViewerModal: () => ImageViewerModal,
46
+ LucideReactIcon: () => LucideReactIcon,
47
+ MapPinDrop: () => MapPinDrop,
48
+ MultiSelectPicker: () => MultiSelectPicker,
49
+ OfflineBanner: () => OfflineBanner,
50
+ OrderItemSkeleton: () => OrderItemSkeleton,
51
+ ProductCard: () => ProductCard,
52
+ ProductCardSkeleton: () => ProductCardSkeleton,
53
+ ProductGrid: () => ProductGrid,
54
+ SearchableInput: () => SearchableInput,
55
+ SearchablePopover: () => SearchablePopover,
56
+ SlotPicker: () => SlotPicker,
57
+ SmartImage: () => SmartImage,
58
+ SmartMotionImage: () => SmartMotionImage,
59
+ TableSkeleton: () => TableSkeleton,
60
+ TruncatedValue: () => TruncatedValue,
61
+ UnsavedChangesDialog: () => UnsavedChangesDialog,
62
+ useUnsavedChanges: () => useUnsavedChanges
63
+ });
64
+ module.exports = __toCommonJS(components_exports);
65
+
66
+ // src/components/skeletons/TableSkeleton.tsx
67
+ var import_jsx_runtime = require("react/jsx-runtime");
68
+ function TableSkeleton({
69
+ rows = 5,
70
+ columns = 4,
71
+ className = "w-full border-collapse",
72
+ rowClassName = "border-b"
73
+ }) {
74
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "w-full overflow-x-auto", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", { className, children: [
75
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { className: rowClassName, children: Array.from({ length: columns }).map((_, c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { className: "p-3 text-left", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-4 bg-muted rounded animate-pulse w-20" }) }, c)) }) }),
76
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: Array.from({ length: rows }).map((_, r) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { className: rowClassName, children: Array.from({ length: columns }).map((_2, c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { className: "p-3", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-4 bg-muted rounded animate-pulse w-full max-w-[120px]" }) }, c)) }, r)) })
77
+ ] }) });
78
+ }
79
+
80
+ // src/components/skeletons/CardSkeleton.tsx
81
+ var import_jsx_runtime2 = require("react/jsx-runtime");
82
+ function CardSkeleton({
83
+ count = 4,
84
+ containerClassName = "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6",
85
+ cardClassName = "rounded-xl border p-4 space-y-3 animate-pulse bg-card"
86
+ }) {
87
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: containerClassName, children: Array.from({ length: count }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: cardClassName, children: [
88
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "w-full aspect-square bg-muted rounded-lg" }),
89
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "h-4 bg-muted rounded w-3/4" }),
90
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "h-4 bg-muted rounded w-1/2" }),
91
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "h-8 bg-muted rounded w-full mt-2" })
92
+ ] }, i)) });
93
+ }
94
+
95
+ // src/components/skeletons/CartItemSkeleton.tsx
96
+ var import_jsx_runtime3 = require("react/jsx-runtime");
97
+ function CartItemSkeleton({
98
+ count = 3,
99
+ className = "space-y-4"
100
+ }) {
101
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className, children: Array.from({ length: count }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-4 p-3 border-b animate-pulse", children: [
102
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "w-16 h-16 bg-muted rounded-md shrink-0" }),
103
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex-1 space-y-2", children: [
104
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "h-4 bg-muted rounded w-2/3" }),
105
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "h-3 bg-muted rounded w-1/3" })
106
+ ] }),
107
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "h-6 w-16 bg-muted rounded shrink-0" })
108
+ ] }, i)) });
109
+ }
110
+
111
+ // src/components/skeletons/OrderItemSkeleton.tsx
112
+ var import_jsx_runtime4 = require("react/jsx-runtime");
113
+ function OrderItemSkeleton({
114
+ count = 3,
115
+ className = "space-y-4"
116
+ }) {
117
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className, children: Array.from({ length: count }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "p-4 border rounded-lg animate-pulse space-y-3", children: [
118
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex justify-between items-center", children: [
119
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "h-4 bg-muted rounded w-1/3" }),
120
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "h-5 w-20 bg-muted rounded-full" })
121
+ ] }),
122
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "h-3 bg-muted rounded w-1/4" }),
123
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "h-4 bg-muted rounded w-1/2" })
124
+ ] }, i)) });
125
+ }
126
+
127
+ // src/components/skeletons/ProductCardSkeleton.tsx
128
+ var import_jsx_runtime5 = require("react/jsx-runtime");
129
+ function ProductCardSkeleton({
130
+ className = "flex flex-col h-full rounded-xl border bg-card overflow-hidden shadow-sm animate-pulse",
131
+ imageClassName = "w-full aspect-square bg-muted"
132
+ }) {
133
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className, children: [
134
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: imageClassName }),
135
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "p-4 flex flex-col flex-1 space-y-3", children: [
136
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "h-5 bg-muted rounded w-3/4" }),
137
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "h-3 bg-muted rounded w-full" }),
138
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "h-3 bg-muted rounded w-2/3" }),
139
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex items-center justify-between pt-2 mt-auto", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "h-6 bg-muted rounded w-20" }) }),
140
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "w-full h-10 bg-muted rounded-lg mt-auto" })
141
+ ] })
142
+ ] });
143
+ }
144
+
145
+ // src/components/cart/CartItem.tsx
146
+ var import_jsx_runtime6 = require("react/jsx-runtime");
147
+ function CartItem({
148
+ item,
149
+ onUpdateQty,
150
+ onRemove,
151
+ formatPriceFn,
152
+ className = "flex items-center gap-4 py-3 border-b border-gray-100 dark:border-gray-800",
153
+ imageClassName = "w-16 h-16 object-cover rounded-md bg-gray-100 dark:bg-gray-800 shrink-0",
154
+ titleClassName = "text-sm font-medium text-gray-900 dark:text-gray-100 line-clamp-1",
155
+ priceClassName = "text-sm font-semibold text-gray-900 dark:text-gray-100",
156
+ quantityGroupClassName = "flex items-center border rounded-md border-gray-200 dark:border-gray-700",
157
+ buttonClassName = "px-2 py-1 text-xs hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors disabled:opacity-50",
158
+ removeButtonClassName = "text-xs text-red-600 hover:text-red-700 dark:text-red-400 transition-colors ml-auto"
159
+ }) {
160
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className, children: [
161
+ item.image_url ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("img", { src: item.image_url, alt: item.product_name, className: imageClassName }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: imageClassName }),
162
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex-1 min-w-0 space-y-1", children: [
163
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h4", { className: titleClassName, children: item.product_name }),
164
+ item.variant_attributes && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: typeof item.variant_attributes === "string" ? item.variant_attributes : Object.entries(item.variant_attributes).map(([k, v]) => `${k}: ${v}`).join(", ") }),
165
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: priceClassName, children: formatPriceFn ? formatPriceFn(item.unit_price * item.quantity) : `${item.currency || ""}${item.unit_price * item.quantity}` })
166
+ ] }),
167
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-col items-end gap-2 shrink-0", children: [
168
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: quantityGroupClassName, children: [
169
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
170
+ "button",
171
+ {
172
+ type: "button",
173
+ onClick: () => onUpdateQty(item.id, Math.max(1, item.quantity - 1)),
174
+ disabled: item.quantity <= 1,
175
+ className: buttonClassName,
176
+ children: "-"
177
+ }
178
+ ),
179
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "px-2 text-xs font-medium", children: item.quantity }),
180
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
181
+ "button",
182
+ {
183
+ type: "button",
184
+ onClick: () => onUpdateQty(item.id, item.quantity + 1),
185
+ className: buttonClassName,
186
+ children: "+"
187
+ }
188
+ )
189
+ ] }),
190
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
191
+ "button",
192
+ {
193
+ type: "button",
194
+ onClick: () => onRemove(item.id),
195
+ className: removeButtonClassName,
196
+ children: "Remove"
197
+ }
198
+ )
199
+ ] })
200
+ ] });
201
+ }
202
+
203
+ // src/hooks/use-cart.ts
204
+ var import_react2 = __toESM(require("react"));
205
+ var import_react_query = require("@tanstack/react-query");
206
+
207
+ // src/utils/token-refresh.ts
208
+ var GLOBAL_KEY = "__kasuviaSdkTokenRefreshRegistry__";
209
+ function getRegistry() {
210
+ const g = globalThis;
211
+ if (!g[GLOBAL_KEY]) {
212
+ g[GLOBAL_KEY] = { handler: null, inFlight: null };
213
+ }
214
+ return g[GLOBAL_KEY];
215
+ }
216
+ async function refreshAccessToken() {
217
+ const registry = getRegistry();
218
+ if (!registry.handler) return null;
219
+ if (registry.inFlight) return registry.inFlight;
220
+ registry.inFlight = registry.handler().finally(() => {
221
+ registry.inFlight = null;
222
+ });
223
+ return registry.inFlight;
224
+ }
225
+ function hasRefreshHandler() {
226
+ return getRegistry().handler !== null;
227
+ }
228
+
229
+ // src/utils/http-client.ts
230
+ var ApiError = class extends Error {
231
+ constructor(message, status, body) {
232
+ super(message);
233
+ this.name = "ApiError";
234
+ this.status = status;
235
+ this.body = body;
236
+ }
237
+ };
238
+ function extractApiErrorMessage(body, fallback) {
239
+ if (typeof body !== "object" || body === null) return fallback;
240
+ const record = body;
241
+ if (typeof record.detail === "string") return record.detail;
242
+ if (Array.isArray(record.detail) && record.detail.length > 0) {
243
+ const first = record.detail[0];
244
+ if (first && typeof first.msg === "string") {
245
+ const loc = first.loc;
246
+ const field = Array.isArray(loc) ? loc[loc.length - 1] : void 0;
247
+ return typeof field === "string" || typeof field === "number" ? `${field}: ${first.msg}` : first.msg;
248
+ }
249
+ }
250
+ const firstStringOrArrayItem = (value) => {
251
+ if (typeof value === "string") return value;
252
+ if (Array.isArray(value) && typeof value[0] === "string") return value[0];
253
+ return void 0;
254
+ };
255
+ const nonFieldMessage = firstStringOrArrayItem(record.non_field_errors);
256
+ if (nonFieldMessage) return nonFieldMessage;
257
+ for (const [key, value] of Object.entries(record)) {
258
+ if (key === "detail" || key === "non_field_errors") continue;
259
+ const message = firstStringOrArrayItem(value);
260
+ if (message) return message;
261
+ }
262
+ return fallback;
263
+ }
264
+ function storefrontHeaders(config) {
265
+ return {
266
+ "X-Storefront-Key": config.storefrontApiKey,
267
+ "X-Business-ID": config.businessId
268
+ };
269
+ }
270
+ function authHeaders(config, accessToken) {
271
+ return {
272
+ ...storefrontHeaders(config),
273
+ Authorization: `Bearer ${accessToken}`
274
+ };
275
+ }
276
+ var JSON_HEADERS = { "Content-Type": "application/json" };
277
+ async function apiRequest(url, options = {}) {
278
+ const { fallbackErrorMessage = "Request failed", ...init } = options;
279
+ const headers = new Headers(init.headers);
280
+ const hadBearerToken = headers.get("Authorization")?.startsWith("Bearer ") ?? false;
281
+ const response = await fetch(url, { ...init, headers });
282
+ if (response.status === 401 && hadBearerToken && hasRefreshHandler()) {
283
+ const freshToken = await refreshAccessToken();
284
+ if (freshToken) {
285
+ const retryHeaders = new Headers(headers);
286
+ retryHeaders.set("Authorization", `Bearer ${freshToken}`);
287
+ const retryResponse = await fetch(url, { ...init, headers: retryHeaders });
288
+ if (retryResponse.ok) {
289
+ const retryText = await retryResponse.text();
290
+ return retryText ? JSON.parse(retryText) : void 0;
291
+ }
292
+ const retryBody = await retryResponse.json().catch(() => ({}));
293
+ throw new ApiError(extractApiErrorMessage(retryBody, fallbackErrorMessage), retryResponse.status, retryBody);
294
+ }
295
+ }
296
+ if (!response.ok) {
297
+ const body = await response.json().catch(() => ({}));
298
+ throw new ApiError(extractApiErrorMessage(body, fallbackErrorMessage), response.status, body);
299
+ }
300
+ const text = await response.text();
301
+ return text ? JSON.parse(text) : void 0;
302
+ }
303
+
304
+ // src/services/cart.service.ts
305
+ function parseItems(items) {
306
+ return items.map((item) => ({
307
+ ...item,
308
+ variant_attributes: item.variant_attributes && typeof item.variant_attributes === "string" ? JSON.parse(item.variant_attributes) : item.variant_attributes
309
+ }));
310
+ }
311
+ function guestHeaders(config, guestSessionId, extra) {
312
+ return { ...extra, ...storefrontHeaders(config), "X-Guest-Session": guestSessionId };
313
+ }
314
+ async function getGuestCart(config, guestSessionId) {
315
+ if (!guestSessionId) throw new Error("Guest session ID is required to fetch guest cart");
316
+ const data = await apiRequest(`${config.backendUrls.cart}/cart/`, {
317
+ headers: guestHeaders(config, guestSessionId),
318
+ fallbackErrorMessage: "Failed to fetch guest cart"
319
+ });
320
+ return { ...data, items: parseItems(data.items || []) };
321
+ }
322
+ async function addToGuestCart(config, guestSessionId, item) {
323
+ if (!guestSessionId) throw new Error("Guest session ID is required to add to guest cart");
324
+ const payload = {
325
+ product_id: item.product_id,
326
+ variant_id: item.variant_id ?? void 0,
327
+ quantity: item.quantity,
328
+ custom_measurements: item.custom_measurements ?? void 0
329
+ };
330
+ const data = await apiRequest(`${config.backendUrls.cart}/cart/`, {
331
+ method: "POST",
332
+ headers: guestHeaders(config, guestSessionId, JSON_HEADERS),
333
+ body: JSON.stringify(payload),
334
+ fallbackErrorMessage: "Failed to add to guest cart"
335
+ });
336
+ return { ...data, items: parseItems(data.items || []) };
337
+ }
338
+ async function updateGuestCartItemQuantity(config, guestSessionId, itemId, quantity) {
339
+ if (!guestSessionId) throw new Error("Guest session ID is required to update guest cart item");
340
+ const data = await apiRequest(`${config.backendUrls.cart}/cart/${itemId}`, {
341
+ method: "PATCH",
342
+ headers: guestHeaders(config, guestSessionId, JSON_HEADERS),
343
+ body: JSON.stringify({ quantity }),
344
+ fallbackErrorMessage: "Failed to update guest cart item"
345
+ });
346
+ return { ...data, items: parseItems(data.items || []) };
347
+ }
348
+ async function removeFromGuestCart(config, guestSessionId, itemId) {
349
+ if (!guestSessionId) throw new Error("Guest session ID is required to remove from guest cart");
350
+ await apiRequest(`${config.backendUrls.cart}/cart/${itemId}`, {
351
+ method: "DELETE",
352
+ headers: guestHeaders(config, guestSessionId),
353
+ fallbackErrorMessage: "Failed to remove item from guest cart"
354
+ });
355
+ }
356
+ async function clearGuestCart(config, guestSessionId) {
357
+ if (!guestSessionId) throw new Error("Guest session ID is required to clear guest cart");
358
+ await apiRequest(`${config.backendUrls.cart}/cart/clear`, {
359
+ method: "DELETE",
360
+ headers: guestHeaders(config, guestSessionId),
361
+ fallbackErrorMessage: "Failed to clear guest cart"
362
+ });
363
+ }
364
+ async function getCart(config, accessToken) {
365
+ if (!accessToken) {
366
+ throw new Error("Access token is required to fetch authenticated cart");
367
+ }
368
+ const data = await apiRequest(`${config.backendUrls.cart}/cart/`, {
369
+ headers: authHeaders(config, accessToken),
370
+ fallbackErrorMessage: "Failed to fetch cart"
371
+ });
372
+ return { ...data, items: parseItems(data.items || []) };
373
+ }
374
+ async function addToCart(config, accessToken, item) {
375
+ const payload = {
376
+ product_id: item.product_id,
377
+ variant_id: item.variant_id ?? void 0,
378
+ quantity: item.quantity,
379
+ custom_measurements: item.custom_measurements ?? void 0
380
+ };
381
+ const data = await apiRequest(`${config.backendUrls.cart}/cart/`, {
382
+ method: "POST",
383
+ headers: { ...JSON_HEADERS, ...authHeaders(config, accessToken) },
384
+ body: JSON.stringify(payload),
385
+ fallbackErrorMessage: "Failed to add to cart"
386
+ });
387
+ return { ...data, items: parseItems(data.items || []) };
388
+ }
389
+ async function updateCartItem(config, accessToken, itemId, quantity) {
390
+ const data = await apiRequest(`${config.backendUrls.cart}/cart/${itemId}`, {
391
+ method: "PATCH",
392
+ headers: { ...JSON_HEADERS, ...authHeaders(config, accessToken) },
393
+ body: JSON.stringify({ quantity }),
394
+ fallbackErrorMessage: "Failed to update cart item"
395
+ });
396
+ return { ...data, items: parseItems(data.items || []) };
397
+ }
398
+ async function removeFromCart(config, accessToken, itemId) {
399
+ await apiRequest(`${config.backendUrls.cart}/cart/${itemId}`, {
400
+ method: "DELETE",
401
+ headers: authHeaders(config, accessToken),
402
+ fallbackErrorMessage: "Failed to remove cart item"
403
+ });
404
+ }
405
+ async function clearCart(config, accessToken) {
406
+ await apiRequest(`${config.backendUrls.cart}/cart/clear`, {
407
+ method: "DELETE",
408
+ headers: authHeaders(config, accessToken),
409
+ fallbackErrorMessage: "Failed to clear cart"
410
+ });
411
+ }
412
+ async function mergeGuestCart(config, accessToken, guestSessionId, guestItems) {
413
+ const payloadItems = guestItems.map((item) => ({
414
+ product_id: item.product_id,
415
+ variant_id: item.variant_id ?? void 0,
416
+ quantity: item.quantity,
417
+ custom_measurements: item.custom_measurements ?? void 0
418
+ }));
419
+ const headers = { ...JSON_HEADERS, ...authHeaders(config, accessToken) };
420
+ if (guestSessionId) {
421
+ headers["X-Guest-Session"] = guestSessionId;
422
+ }
423
+ const data = await apiRequest(`${config.backendUrls.cart}/cart/merge`, {
424
+ method: "POST",
425
+ headers,
426
+ body: JSON.stringify({ items: payloadItems }),
427
+ fallbackErrorMessage: "Failed to merge guest cart"
428
+ });
429
+ return { ...data, items: parseItems(data.items || []) };
430
+ }
431
+ async function mergeGuestCartIfPresent(config, accessToken, guestSessionId) {
432
+ const guestCart = await getGuestCart(config, guestSessionId);
433
+ if (!guestCart.items.length) return;
434
+ await mergeGuestCart(config, accessToken, guestSessionId, guestCart.items);
435
+ }
436
+
437
+ // src/utils/logger.ts
438
+ var isDevelopment = true;
439
+ var logger = {
440
+ log: (...args) => {
441
+ if (isDevelopment) {
442
+ console.log(...args);
443
+ }
444
+ },
445
+ warn: (...args) => {
446
+ if (isDevelopment) {
447
+ console.warn(...args);
448
+ }
449
+ },
450
+ /**
451
+ * Logs a warning to the console in development only.
452
+ * Alias for warn() for Python logging compatibility.
453
+ * In production, this is a no-op.
454
+ */
455
+ warning: (...args) => {
456
+ if (isDevelopment) {
457
+ console.warn(...args);
458
+ }
459
+ },
460
+ error: (...args) => {
461
+ if (isDevelopment) {
462
+ console.error(...args);
463
+ }
464
+ },
465
+ info: (...args) => {
466
+ if (isDevelopment) {
467
+ console.info(...args);
468
+ }
469
+ },
470
+ createScope: (scope) => ({
471
+ log: (...args) => {
472
+ if (isDevelopment) {
473
+ console.log(`[${scope}]`, ...args);
474
+ }
475
+ },
476
+ warn: (...args) => {
477
+ if (isDevelopment) {
478
+ console.warn(`[${scope}]`, ...args);
479
+ }
480
+ },
481
+ warning: (...args) => {
482
+ if (isDevelopment) {
483
+ console.warn(`[${scope}]`, ...args);
484
+ }
485
+ },
486
+ error: (...args) => {
487
+ if (isDevelopment) {
488
+ console.error(`[${scope}]`, ...args);
489
+ }
490
+ },
491
+ info: (...args) => {
492
+ if (isDevelopment) {
493
+ console.info(`[${scope}]`, ...args);
494
+ }
495
+ }
496
+ })
497
+ };
498
+
499
+ // src/context/sdk-context.tsx
500
+ var import_react = require("react");
501
+ var import_jsx_runtime7 = require("react/jsx-runtime");
502
+ var SdkContext = (0, import_react.createContext)(null);
503
+ function useSdkContext() {
504
+ return (0, import_react.useContext)(SdkContext);
505
+ }
506
+ function resolveHookArgs(explicitConfig, explicitToken, ctx) {
507
+ const config = explicitConfig ?? ctx?.config;
508
+ if (!config) {
509
+ throw new Error(
510
+ "[kasuvia-sdk] No SdkConfig found. Either pass config explicitly or wrap your app with <SdkProvider config={...}>."
511
+ );
512
+ }
513
+ const accessToken = explicitToken !== void 0 ? explicitToken : ctx?.accessToken ?? null;
514
+ return { config, accessToken };
515
+ }
516
+
517
+ // src/hooks/use-cart.ts
518
+ var CartCtx = import_react2.default.createContext ? import_react2.default.createContext(null) : null;
519
+ function useCart(config, accessToken, guestSessionId) {
520
+ const ctx = useSdkContext();
521
+ const resolved = resolveHookArgs(config, accessToken, ctx);
522
+ const resolvedConfig = resolved.config;
523
+ const resolvedToken = resolved.accessToken;
524
+ guestSessionId = guestSessionId !== void 0 ? guestSessionId : ctx?.guestSessionId ?? null;
525
+ const queryClient = (0, import_react_query.useQueryClient)();
526
+ const context = CartCtx ? (0, import_react2.useContext)(CartCtx) : null;
527
+ const isOpen = context ? context.isOpen : false;
528
+ const setOpen = context ? context.setOpen : () => {
529
+ };
530
+ const productTypeMismatch = context ? context.productTypeMismatch : null;
531
+ const setProductTypeMismatch = context ? context.setProductTypeMismatch : () => {
532
+ };
533
+ const cartQueryKey = ["cart", resolvedToken ?? guestSessionId ?? "anon"];
534
+ const cartQuery = (0, import_react_query.useQuery)({
535
+ queryKey: cartQueryKey,
536
+ queryFn: async () => {
537
+ if (resolvedToken) {
538
+ return getCart(resolvedConfig, resolvedToken);
539
+ }
540
+ if (guestSessionId) {
541
+ return getGuestCart(resolvedConfig, guestSessionId);
542
+ }
543
+ return { items: [], subtotal: 0, total: 0, item_count: 0, product_type: void 0 };
544
+ },
545
+ enabled: !!(resolvedToken || guestSessionId)
546
+ });
547
+ const addToCartMutation = (0, import_react_query.useMutation)({
548
+ mutationFn: async (item) => {
549
+ if (resolvedToken) {
550
+ return addToCart(resolvedConfig, resolvedToken, item);
551
+ }
552
+ if (guestSessionId) {
553
+ return addToGuestCart(resolvedConfig, guestSessionId, item);
554
+ }
555
+ throw new Error("Cannot add to cart: no access token or guest session ID provided.");
556
+ },
557
+ onSuccess: () => {
558
+ setProductTypeMismatch(null);
559
+ queryClient.invalidateQueries({ queryKey: ["cart"] });
560
+ },
561
+ // REAL BUG FIXED (2026-09-24), live-reported ("mixed products between
562
+ // physical to digital"): this read `error.status`/`error.data` - but
563
+ // apiRequest throws a real `ApiError` (utils/http-client.ts), whose
564
+ // fields are `.status`/`.body`/`.message`, not `.data` - `err.data` was
565
+ // always `undefined`, so this branch could never fire even when the
566
+ // backend genuinely sent a 409. Separately, kasuvia-cart-management's
567
+ // error body is a huma ErrorModel (`{detail: "plain string"}`), never a
568
+ // nested `{detail: {code, ...}}` object - this service's own
569
+ // TranslateServiceError doc comment already documents that convention
570
+ // (a bracketed "[CODE] ..." prefix in the message string, matching
571
+ // every other cart error). Parses that same convention here instead of
572
+ // a response shape the backend never produces.
573
+ onError: (error) => {
574
+ const err = error;
575
+ if (err.status !== 409 || !err.message?.startsWith("[CART_PRODUCT_TYPE_MISMATCH]")) return;
576
+ const match = err.message.match(/contains (\w+) items\. Add (\w+) items/i);
577
+ setProductTypeMismatch({
578
+ message: err.message.replace(/^\[CART_PRODUCT_TYPE_MISMATCH\]\s*/, ""),
579
+ existing_type: match?.[1] ?? "unknown",
580
+ new_type: match?.[2] ?? "unknown"
581
+ });
582
+ }
583
+ });
584
+ function recomputeCartTotals(previous, items2) {
585
+ const subtotal2 = items2.reduce((sum, i) => sum + i.unit_price * i.quantity, 0);
586
+ const item_count = items2.reduce((sum, i) => sum + i.quantity, 0);
587
+ return { ...previous, items: items2, subtotal: subtotal2, total: subtotal2, item_count };
588
+ }
589
+ const updateCartItemMutation = (0, import_react_query.useMutation)({
590
+ mutationFn: async ({ itemId, quantity }) => {
591
+ if (resolvedToken) {
592
+ return updateCartItem(resolvedConfig, resolvedToken, itemId, quantity);
593
+ }
594
+ if (guestSessionId) {
595
+ return updateGuestCartItemQuantity(resolvedConfig, guestSessionId, itemId, quantity);
596
+ }
597
+ throw new Error("Cannot update cart: no access token or guest session ID provided.");
598
+ },
599
+ onMutate: async ({ itemId, quantity }) => {
600
+ await queryClient.cancelQueries({ queryKey: cartQueryKey });
601
+ const previous = queryClient.getQueryData(cartQueryKey);
602
+ if (previous) {
603
+ const items2 = quantity > 0 ? previous.items.map((i) => i.id === itemId ? { ...i, quantity } : i) : previous.items.filter((i) => i.id !== itemId);
604
+ queryClient.setQueryData(cartQueryKey, recomputeCartTotals(previous, items2));
605
+ }
606
+ return { previous };
607
+ },
608
+ onError: (error, _variables, context2) => {
609
+ if (context2?.previous) {
610
+ queryClient.setQueryData(cartQueryKey, context2.previous);
611
+ }
612
+ logger.error("[useCart] Failed to update item quantity, rolled back:", error);
613
+ },
614
+ onSettled: () => {
615
+ queryClient.invalidateQueries({ queryKey: ["cart"] });
616
+ }
617
+ });
618
+ const removeFromCartMutation = (0, import_react_query.useMutation)({
619
+ mutationFn: async (itemId) => {
620
+ if (resolvedToken) {
621
+ return removeFromCart(resolvedConfig, resolvedToken, itemId);
622
+ }
623
+ if (guestSessionId) {
624
+ return removeFromGuestCart(resolvedConfig, guestSessionId, itemId);
625
+ }
626
+ throw new Error("Cannot remove from cart: no access token or guest session ID provided.");
627
+ },
628
+ onMutate: async (itemId) => {
629
+ await queryClient.cancelQueries({ queryKey: cartQueryKey });
630
+ const previous = queryClient.getQueryData(cartQueryKey);
631
+ if (previous) {
632
+ const items2 = previous.items.filter((i) => i.id !== itemId);
633
+ queryClient.setQueryData(cartQueryKey, recomputeCartTotals(previous, items2));
634
+ }
635
+ return { previous };
636
+ },
637
+ onError: (error, _itemId, context2) => {
638
+ if (context2?.previous) {
639
+ queryClient.setQueryData(cartQueryKey, context2.previous);
640
+ }
641
+ logger.error("[useCart] Failed to remove item, rolled back:", error);
642
+ },
643
+ onSettled: () => {
644
+ queryClient.invalidateQueries({ queryKey: ["cart"] });
645
+ }
646
+ });
647
+ const clearCartMutation = (0, import_react_query.useMutation)({
648
+ mutationFn: async () => {
649
+ if (resolvedToken) {
650
+ return clearCart(resolvedConfig, resolvedToken);
651
+ }
652
+ if (guestSessionId) {
653
+ return clearGuestCart(resolvedConfig, guestSessionId);
654
+ }
655
+ throw new Error("Cannot clear cart: no access token or guest session ID provided.");
656
+ },
657
+ onSuccess: () => {
658
+ queryClient.invalidateQueries({ queryKey: ["cart"] });
659
+ }
660
+ });
661
+ const mergeGuestCartMutation = (0, import_react_query.useMutation)({
662
+ mutationFn: (sessionId) => {
663
+ if (!resolvedToken) {
664
+ logger.warn("[useCart] mergeGuestCart called without an access token \u2014 skipping merge.");
665
+ return Promise.resolve();
666
+ }
667
+ return mergeGuestCartIfPresent(resolvedConfig, resolvedToken, sessionId);
668
+ },
669
+ onSuccess: () => {
670
+ queryClient.invalidateQueries({ queryKey: ["cart"] });
671
+ }
672
+ });
673
+ const addItem = (product, variantId, quantity = 1, customMeasurements) => {
674
+ return addToCartMutation.mutate({
675
+ product_id: product.id,
676
+ variant_id: variantId,
677
+ quantity,
678
+ custom_measurements: customMeasurements ? JSON.stringify(customMeasurements) : void 0,
679
+ // Below are accepted by the mutation signature for UI convenience but stripped before the fetch:
680
+ product_slug: product.slug || "",
681
+ product_name: product.name,
682
+ image_url: product.images[0] || void 0
683
+ });
684
+ };
685
+ const items = cartQuery.data?.items ?? [];
686
+ const subtotal = cartQuery.data?.subtotal ?? 0;
687
+ const count = items.reduce((total, item) => total + item.quantity, 0);
688
+ const updateQty = (itemId, quantity) => {
689
+ return updateCartItemMutation.mutateAsync({ itemId, quantity });
690
+ };
691
+ const removeItem = (itemId) => {
692
+ return removeFromCartMutation.mutateAsync(itemId);
693
+ };
694
+ const handleClearCartAndAdd = () => {
695
+ clearCartMutation.mutate();
696
+ setProductTypeMismatch(null);
697
+ };
698
+ const handleKeepCurrentCart = () => {
699
+ setProductTypeMismatch(null);
700
+ };
701
+ return {
702
+ cart: cartQuery.data,
703
+ items,
704
+ count,
705
+ isOpen,
706
+ setOpen,
707
+ isLoading: cartQuery.isLoading,
708
+ error: cartQuery.error,
709
+ addToCart: addToCartMutation.mutate,
710
+ updateCartItem: updateCartItemMutation.mutate,
711
+ removeFromCart: removeFromCartMutation.mutate,
712
+ clearCart: clearCartMutation.mutate,
713
+ clear: clearCartMutation.mutate,
714
+ mergeGuestCart: mergeGuestCartMutation.mutate,
715
+ addItem,
716
+ updateQty,
717
+ removeItem,
718
+ subtotal,
719
+ productTypeMismatch,
720
+ handleClearCartAndAdd,
721
+ handleKeepCurrentCart
722
+ };
723
+ }
724
+
725
+ // src/components/cart/CartTypeMismatchBanner.tsx
726
+ var import_jsx_runtime8 = require("react/jsx-runtime");
727
+ function CartTypeMismatchBanner({
728
+ mismatch,
729
+ onClearAndAdd,
730
+ onKeepCurrent,
731
+ className = "p-4 bg-amber-50 dark:bg-amber-950/40 border border-amber-200 dark:border-amber-800 rounded-lg space-y-3",
732
+ titleClassName = "text-sm font-semibold text-amber-800 dark:text-amber-200",
733
+ messageClassName = "text-xs text-amber-700 dark:text-amber-300",
734
+ buttonGroupClassName = "flex items-center gap-2 pt-1",
735
+ confirmButtonClassName = "px-3 py-1.5 text-xs font-medium bg-amber-600 hover:bg-amber-700 text-white rounded transition-colors",
736
+ cancelButtonClassName = "px-3 py-1.5 text-xs font-medium border border-amber-300 dark:border-amber-700 text-amber-800 dark:text-amber-200 rounded hover:bg-amber-100 dark:hover:bg-amber-900/50 transition-colors"
737
+ }) {
738
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className, children: [
739
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: titleClassName, children: "Cart Product Type Conflict" }),
740
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: messageClassName, children: mismatch.message || `Your cart currently contains ${mismatch.existing_type} items. You cannot mix ${mismatch.existing_type} and ${mismatch.new_type} items in the same order.` }),
741
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: buttonGroupClassName, children: [
742
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
743
+ "button",
744
+ {
745
+ type: "button",
746
+ onClick: onClearAndAdd,
747
+ className: confirmButtonClassName,
748
+ children: "Clear Cart & Add New Item"
749
+ }
750
+ ),
751
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
752
+ "button",
753
+ {
754
+ type: "button",
755
+ onClick: onKeepCurrent,
756
+ className: cancelButtonClassName,
757
+ children: "Keep Current Cart"
758
+ }
759
+ )
760
+ ] })
761
+ ] });
762
+ }
763
+
764
+ // src/utils/format.ts
765
+ var BYTE_THRESHOLDS = [
766
+ { value: 1024 ** 5, label: "PB" },
767
+ { value: 1024 ** 4, label: "TB" },
768
+ { value: 1024 ** 3, label: "GB" },
769
+ { value: 1024 ** 2, label: "MB" },
770
+ { value: 1024 ** 1, label: "KB" }
771
+ ];
772
+ function formatTimeString(time) {
773
+ const [hoursStr, minutesStr] = time.split(":");
774
+ const hours = parseInt(hoursStr, 10);
775
+ if (isNaN(hours) || minutesStr === void 0) return time;
776
+ const period = hours >= 12 ? "PM" : "AM";
777
+ const displayHours = hours % 12 || 12;
778
+ return `${displayHours}:${minutesStr} ${period}`;
779
+ }
780
+ function formatPrice(amount, currency, locale = "en-US") {
781
+ if (amount == null || isNaN(amount)) return "";
782
+ if (!currency) return `N/A ${amount.toLocaleString()}`;
783
+ const code = currency.toUpperCase();
784
+ try {
785
+ return new Intl.NumberFormat(locale, {
786
+ style: "currency",
787
+ currency: code,
788
+ minimumFractionDigits: 0,
789
+ maximumFractionDigits: 2
790
+ }).format(amount);
791
+ } catch {
792
+ return `${code} ${amount.toLocaleString()}`;
793
+ }
794
+ }
795
+
796
+ // src/components/cart/CartSidebar.tsx
797
+ var import_jsx_runtime9 = require("react/jsx-runtime");
798
+ function CartSidebar({
799
+ onCheckout,
800
+ onClose,
801
+ formatPriceFn,
802
+ className = "flex flex-col h-full w-full max-w-md bg-white dark:bg-gray-900 border-l border-gray-200 dark:border-gray-800 shadow-xl",
803
+ headerClassName = "flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-800",
804
+ titleClassName = "text-lg font-semibold text-gray-900 dark:text-gray-100",
805
+ bodyClassName = "flex-1 overflow-y-auto p-4 space-y-4",
806
+ footerClassName = "p-4 border-t border-gray-200 dark:border-gray-800 space-y-3 bg-gray-50/50 dark:bg-gray-900/50",
807
+ checkoutButtonClassName = "w-full py-3 px-4 bg-emerald-600 hover:bg-emerald-700 text-white font-medium rounded-lg transition-colors text-center block disabled:opacity-50"
808
+ }) {
809
+ const {
810
+ items,
811
+ count,
812
+ subtotal,
813
+ isLoading,
814
+ updateQty,
815
+ removeItem,
816
+ productTypeMismatch,
817
+ handleClearCartAndAdd,
818
+ handleKeepCurrentCart,
819
+ setOpen
820
+ } = useCart();
821
+ const handleClose = () => {
822
+ setOpen(false);
823
+ if (onClose) onClose();
824
+ };
825
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className, children: [
826
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: headerClassName, children: [
827
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("h3", { className: titleClassName, children: [
828
+ "Shopping Cart (",
829
+ count,
830
+ ")"
831
+ ] }),
832
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
833
+ "button",
834
+ {
835
+ type: "button",
836
+ onClick: handleClose,
837
+ className: "p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200",
838
+ "aria-label": "Close cart",
839
+ children: "\u2715"
840
+ }
841
+ )
842
+ ] }),
843
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: bodyClassName, children: [
844
+ productTypeMismatch && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
845
+ CartTypeMismatchBanner,
846
+ {
847
+ mismatch: productTypeMismatch,
848
+ onClearAndAdd: handleClearCartAndAdd,
849
+ onKeepCurrent: handleKeepCurrentCart
850
+ }
851
+ ),
852
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(CartItemSkeleton, { count: 3 }) : items.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "text-center py-12 text-gray-500 dark:text-gray-400", children: "Your cart is empty" }) : items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
853
+ CartItem,
854
+ {
855
+ item,
856
+ onUpdateQty: updateQty,
857
+ onRemove: removeItem,
858
+ formatPriceFn
859
+ },
860
+ item.id
861
+ ))
862
+ ] }),
863
+ items.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: footerClassName, children: [
864
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex justify-between items-center text-sm font-semibold text-gray-900 dark:text-gray-100", children: [
865
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: "Subtotal" }),
866
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: formatPriceFn ? formatPriceFn(subtotal) : formatPrice(subtotal, void 0) })
867
+ ] }),
868
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
869
+ "button",
870
+ {
871
+ type: "button",
872
+ onClick: onCheckout,
873
+ disabled: items.length === 0,
874
+ className: checkoutButtonClassName,
875
+ children: "Proceed to Checkout"
876
+ }
877
+ )
878
+ ] })
879
+ ] });
880
+ }
881
+
882
+ // src/components/product/ProductCard.tsx
883
+ var import_react4 = require("react");
884
+
885
+ // src/components/shared/SmartImage.tsx
886
+ var import_react3 = require("react");
887
+ var import_framer_motion = require("framer-motion");
888
+ var import_image = __toESM(require("next/image"));
889
+ var import_lucide_react = require("lucide-react");
890
+ var import_jsx_runtime10 = require("react/jsx-runtime");
891
+ var isValidSrc = (url) => {
892
+ if (!url || typeof url !== "string") return false;
893
+ const s = url.trim();
894
+ if (s === "" || s === "null" || s === "undefined") return false;
895
+ return s.startsWith("/") || s.startsWith("http://") || s.startsWith("https://") || s.startsWith("data:image/") || s.startsWith("blob:");
896
+ };
897
+ function useImageState(src) {
898
+ const [isMounted, setIsMounted] = (0, import_react3.useState)(false);
899
+ const [isLoading, setIsLoading] = (0, import_react3.useState)(true);
900
+ const [hasError, setHasError] = (0, import_react3.useState)(false);
901
+ (0, import_react3.useEffect)(() => {
902
+ setIsMounted(true);
903
+ }, []);
904
+ (0, import_react3.useEffect)(() => {
905
+ setIsLoading(true);
906
+ setHasError(false);
907
+ }, [src]);
908
+ const handleLoad = () => setIsLoading(false);
909
+ const handleError = () => {
910
+ setIsLoading(false);
911
+ setHasError(true);
912
+ };
913
+ return { isMounted, isLoading, hasError, handleLoad, handleError };
914
+ }
915
+ function ImagePlaceholder({ className, failed }) {
916
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
917
+ "div",
918
+ {
919
+ className: `relative overflow-hidden flex flex-col items-center justify-center gap-1 bg-muted/40 ${className}`,
920
+ children: [
921
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react.ImageIcon, { className: "w-4 h-4 text-muted-foreground/50 shrink-0" }),
922
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "text-[10px] text-muted-foreground/70 text-center leading-tight px-1", children: failed ? "Failed to load" : "No image" })
923
+ ]
924
+ }
925
+ );
926
+ }
927
+ function ImageShimmer() {
928
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "absolute inset-0 z-10 bg-muted/60 overflow-hidden", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "absolute inset-0 -translate-x-full animate-shimmer bg-linear-to-r from-transparent via-white/10 to-transparent" }) });
929
+ }
930
+ var SmartImage = (0, import_react3.forwardRef)(function SmartImage2({
931
+ src,
932
+ alt,
933
+ className = "",
934
+ imageClassName = "",
935
+ priority = false,
936
+ isRelative = true,
937
+ sizes = "100vw",
938
+ objectFit = "cover",
939
+ fallbackSrc,
940
+ suppressHydrationWarning = false,
941
+ style,
942
+ draggable
943
+ }, ref) {
944
+ const { isMounted, isLoading, hasError, handleLoad, handleError } = useImageState(src);
945
+ const resolvedSrc = isValidSrc(src) ? src.trim() : null;
946
+ const fallback = isValidSrc(fallbackSrc) ? fallbackSrc.trim() : null;
947
+ if (!isMounted || !resolvedSrc && !fallback) {
948
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ImagePlaceholder, { className });
949
+ }
950
+ if (hasError) {
951
+ if (fallback) {
952
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
953
+ "div",
954
+ {
955
+ className: `${isRelative ? "relative" : ""} overflow-hidden ${className}`,
956
+ style,
957
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
958
+ import_image.default,
959
+ {
960
+ ref,
961
+ src: fallback,
962
+ alt,
963
+ fill: true,
964
+ unoptimized: true,
965
+ sizes,
966
+ className: `object-${objectFit} ${imageClassName}`,
967
+ draggable
968
+ }
969
+ )
970
+ }
971
+ );
972
+ }
973
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ImagePlaceholder, { className, failed: true });
974
+ }
975
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
976
+ "div",
977
+ {
978
+ className: `${isRelative ? "relative" : ""} overflow-hidden ${className}`,
979
+ style,
980
+ children: [
981
+ isLoading && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ImageShimmer, {}),
982
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
983
+ import_image.default,
984
+ {
985
+ ref,
986
+ src: resolvedSrc,
987
+ alt,
988
+ fill: true,
989
+ priority,
990
+ sizes,
991
+ unoptimized: true,
992
+ suppressHydrationWarning,
993
+ className: `object-${objectFit} ${imageClassName} transition-opacity duration-300 ${isLoading ? "opacity-0" : "opacity-100"}`,
994
+ onLoad: handleLoad,
995
+ onError: handleError,
996
+ draggable
997
+ }
998
+ )
999
+ ]
1000
+ }
1001
+ );
1002
+ });
1003
+ var SmartMotionImage = (0, import_react3.forwardRef)(function SmartMotionImage2({
1004
+ src,
1005
+ alt,
1006
+ className = "",
1007
+ imageClassName = "",
1008
+ priority = false,
1009
+ isRelative = true,
1010
+ sizes = "100vw",
1011
+ objectFit = "cover",
1012
+ fallbackSrc,
1013
+ style,
1014
+ draggable,
1015
+ ...motionProps
1016
+ }, ref) {
1017
+ const { isMounted, isLoading, hasError, handleLoad, handleError } = useImageState(src);
1018
+ const resolvedSrc = isValidSrc(src) ? src.trim() : null;
1019
+ const fallback = isValidSrc(fallbackSrc) ? fallbackSrc.trim() : null;
1020
+ if (!isMounted || !resolvedSrc && !fallback) {
1021
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ImagePlaceholder, { className });
1022
+ }
1023
+ if (hasError) {
1024
+ if (fallback) {
1025
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1026
+ import_framer_motion.motion.div,
1027
+ {
1028
+ ref,
1029
+ className: `${isRelative ? "relative" : ""} overflow-hidden ${className}`,
1030
+ style,
1031
+ ...motionProps,
1032
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1033
+ import_image.default,
1034
+ {
1035
+ src: fallback,
1036
+ alt,
1037
+ fill: true,
1038
+ unoptimized: true,
1039
+ sizes,
1040
+ className: `object-${objectFit} ${imageClassName}`,
1041
+ draggable
1042
+ }
1043
+ )
1044
+ }
1045
+ );
1046
+ }
1047
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ImagePlaceholder, { className, failed: true });
1048
+ }
1049
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1050
+ import_framer_motion.motion.div,
1051
+ {
1052
+ ref,
1053
+ className: `${isRelative ? "relative" : ""} overflow-hidden ${className}`,
1054
+ style,
1055
+ ...motionProps,
1056
+ children: [
1057
+ isLoading && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ImageShimmer, {}),
1058
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1059
+ import_image.default,
1060
+ {
1061
+ src: resolvedSrc,
1062
+ alt,
1063
+ fill: true,
1064
+ priority,
1065
+ sizes,
1066
+ unoptimized: true,
1067
+ className: `object-${objectFit} ${imageClassName} transition-opacity duration-300 ${isLoading ? "opacity-0" : "opacity-100"}`,
1068
+ onLoad: handleLoad,
1069
+ onError: handleError,
1070
+ draggable
1071
+ }
1072
+ )
1073
+ ]
1074
+ }
1075
+ );
1076
+ });
1077
+ var SmartImage_default = SmartImage;
1078
+
1079
+ // src/components/product/ProductCard.tsx
1080
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1081
+ function StarIcon({ half, gradientId }) {
1082
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("svg", { viewBox: "0 0 20 20", className: "w-3.5 h-3.5", "aria-hidden": "true", children: half ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
1083
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("linearGradient", { id: gradientId, children: [
1084
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("stop", { offset: "50%", stopColor: "currentColor" }),
1085
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("stop", { offset: "50%", stopColor: "transparent", stopOpacity: "1" })
1086
+ ] }) }),
1087
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1088
+ "path",
1089
+ {
1090
+ fill: `url(#${gradientId})`,
1091
+ stroke: "currentColor",
1092
+ strokeWidth: "1",
1093
+ d: "M10 1.5l2.6 5.27 5.82.85-4.21 4.1.99 5.79L10 14.9l-5.2 2.73.99-5.8-4.21-4.09 5.82-.85L10 1.5z"
1094
+ }
1095
+ )
1096
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1097
+ "path",
1098
+ {
1099
+ fill: "currentColor",
1100
+ d: "M10 1.5l2.6 5.27 5.82.85-4.21 4.1.99 5.79L10 14.9l-5.2 2.73.99-5.8-4.21-4.09 5.82-.85L10 1.5z"
1101
+ }
1102
+ ) });
1103
+ }
1104
+ function StarRow({ rating, idPrefix }) {
1105
+ const stars = [];
1106
+ for (let i = 1; i <= 5; i++) {
1107
+ const diff = rating - (i - 1);
1108
+ if (diff >= 0.75) stars.push(/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(StarIcon, { gradientId: `${idPrefix}-star-${i}` }, i));
1109
+ else if (diff >= 0.25) stars.push(/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(StarIcon, { half: true, gradientId: `${idPrefix}-star-${i}` }, i));
1110
+ else stars.push(/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("svg", { viewBox: "0 0 20 20", className: "w-3.5 h-3.5 text-muted-foreground/30", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { fill: "currentColor", d: "M10 1.5l2.6 5.27 5.82.85-4.21 4.1.99 5.79L10 14.9l-5.2 2.73.99-5.8-4.21-4.09 5.82-.85L10 1.5z" }) }, i));
1111
+ }
1112
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_jsx_runtime11.Fragment, { children: stars });
1113
+ }
1114
+ function ProductCard({
1115
+ product,
1116
+ onSelectProduct,
1117
+ onWishlistToggle,
1118
+ isWishlistSaved = false,
1119
+ showWishlistButton = false,
1120
+ priority = false,
1121
+ className = "flex flex-col h-full rounded-xl border bg-card overflow-hidden shadow-sm hover:shadow-md transition-shadow group",
1122
+ imageContainerClassName = "w-full aspect-square",
1123
+ imageClassName = "transition-transform duration-500 group-hover:scale-105",
1124
+ titleClassName = "text-base font-semibold text-foreground line-clamp-1 cursor-pointer hover:underline",
1125
+ priceClassName = "text-lg font-bold text-foreground",
1126
+ buttonClassName = "w-full py-2.5 px-4 bg-primary hover:bg-primary/90 text-primary-foreground text-sm font-medium rounded-lg transition-colors text-center block mt-auto",
1127
+ enableImageCarousel = true,
1128
+ showCarouselDots = true,
1129
+ showCarouselArrows = true,
1130
+ activeDotClassName = "bg-primary w-4 h-1.5 rounded-full transition-all",
1131
+ dotClassName = "bg-white/70 hover:bg-white w-1.5 h-1.5 rounded-full transition-all",
1132
+ formatPriceFn,
1133
+ badgeSlot,
1134
+ showRating = true,
1135
+ ratingClassName = "flex items-center gap-1 text-amber-500",
1136
+ footerSlot
1137
+ }) {
1138
+ const { addItem } = useCart();
1139
+ const ratingIdPrefix = (0, import_react4.useId)();
1140
+ const [currentImageIndex, setCurrentImageIndex] = (0, import_react4.useState)(0);
1141
+ const touchStartX = (0, import_react4.useRef)(null);
1142
+ const touchEndX = (0, import_react4.useRef)(null);
1143
+ const productType = product.product_type;
1144
+ const serviceType = product.service_type || "appointment";
1145
+ const isConsultationRequired = product.service_requires_consultation;
1146
+ const images = product.images && product.images.length > 0 ? product.images : [];
1147
+ const hasMultipleImages = images.length > 1 && enableImageCarousel;
1148
+ const hasRating = showRating && product.average_rating != null && product.rating_count > 0;
1149
+ const handleCTA = () => {
1150
+ if (productType === "physical" || productType === "digital") {
1151
+ addItem(product);
1152
+ return;
1153
+ }
1154
+ onSelectProduct?.(product);
1155
+ };
1156
+ const getCtaText = () => {
1157
+ if (productType === "physical" || productType === "digital") return "Add to Cart";
1158
+ if (productType === "bespoke") return "Start Commission";
1159
+ if (productType === "service") {
1160
+ if (isConsultationRequired || serviceType === "project") return "Get a Quote";
1161
+ if (serviceType === "appointment") return "Book Appointment";
1162
+ return "View Details";
1163
+ }
1164
+ return "View Details";
1165
+ };
1166
+ const handleNextImage = (e) => {
1167
+ e?.stopPropagation();
1168
+ if (!hasMultipleImages) return;
1169
+ setCurrentImageIndex((prev) => (prev + 1) % images.length);
1170
+ };
1171
+ const handlePrevImage = (e) => {
1172
+ e?.stopPropagation();
1173
+ if (!hasMultipleImages) return;
1174
+ setCurrentImageIndex((prev) => (prev - 1 + images.length) % images.length);
1175
+ };
1176
+ const handleTouchStart = (e) => {
1177
+ if (!hasMultipleImages) return;
1178
+ touchStartX.current = e.touches[0].clientX;
1179
+ };
1180
+ const handleTouchMove = (e) => {
1181
+ if (!hasMultipleImages) return;
1182
+ touchEndX.current = e.touches[0].clientX;
1183
+ };
1184
+ const handleTouchEnd = () => {
1185
+ if (!hasMultipleImages || touchStartX.current === null || touchEndX.current === null) return;
1186
+ const distance = touchStartX.current - touchEndX.current;
1187
+ const isSwipeLeft = distance > 40;
1188
+ const isSwipeRight = distance < -40;
1189
+ if (isSwipeLeft) handleNextImage();
1190
+ else if (isSwipeRight) handlePrevImage();
1191
+ touchStartX.current = null;
1192
+ touchEndX.current = null;
1193
+ };
1194
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className, children: [
1195
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1196
+ "div",
1197
+ {
1198
+ className: "relative overflow-hidden cursor-pointer group/image",
1199
+ onClick: () => onSelectProduct?.(product),
1200
+ onTouchStart: handleTouchStart,
1201
+ onTouchMove: handleTouchMove,
1202
+ onTouchEnd: handleTouchEnd,
1203
+ children: [
1204
+ images.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1205
+ SmartImage,
1206
+ {
1207
+ src: images[currentImageIndex],
1208
+ alt: `${product.name} - image ${currentImageIndex + 1}`,
1209
+ className: imageContainerClassName,
1210
+ imageClassName,
1211
+ priority,
1212
+ sizes: "(min-width: 1024px) 25vw, 50vw"
1213
+ }
1214
+ ) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: `${imageContainerClassName} flex items-center justify-center text-muted-foreground text-sm`, children: "No Image" }),
1215
+ badgeSlot ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "absolute top-2 left-2 z-10 flex flex-col gap-1", children: badgeSlot }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "absolute top-2 right-2 px-2 py-0.5 text-xs font-semibold rounded bg-black/60 text-white capitalize z-10", children: productType }),
1216
+ showWishlistButton && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1217
+ "button",
1218
+ {
1219
+ type: "button",
1220
+ "aria-label": isWishlistSaved ? "Remove from wishlist" : "Save to wishlist",
1221
+ onClick: (e) => {
1222
+ e.stopPropagation();
1223
+ onWishlistToggle?.(product, e);
1224
+ },
1225
+ className: "absolute top-2 right-2 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-card/80 backdrop-blur shadow-sm hover:bg-card transition-colors",
1226
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1227
+ "svg",
1228
+ {
1229
+ className: `w-4 h-4 ${isWishlistSaved ? "fill-red-500 text-red-500" : "text-muted-foreground"}`,
1230
+ viewBox: "0 0 24 24",
1231
+ stroke: "currentColor",
1232
+ strokeWidth: "2",
1233
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" })
1234
+ }
1235
+ )
1236
+ }
1237
+ ),
1238
+ hasMultipleImages && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
1239
+ showCarouselArrows && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
1240
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1241
+ "button",
1242
+ {
1243
+ type: "button",
1244
+ "aria-label": "Previous image",
1245
+ onClick: handlePrevImage,
1246
+ className: "absolute left-2 top-1/2 -translate-y-1/2 w-7 h-7 rounded-full bg-black/50 hover:bg-black/75 text-white flex items-center justify-center opacity-0 group-hover/image:opacity-100 transition-opacity z-10",
1247
+ children: "\u2039"
1248
+ }
1249
+ ),
1250
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1251
+ "button",
1252
+ {
1253
+ type: "button",
1254
+ "aria-label": "Next image",
1255
+ onClick: handleNextImage,
1256
+ className: "absolute right-2 top-1/2 -translate-y-1/2 w-7 h-7 rounded-full bg-black/50 hover:bg-black/75 text-white flex items-center justify-center opacity-0 group-hover/image:opacity-100 transition-opacity z-10",
1257
+ children: "\u203A"
1258
+ }
1259
+ )
1260
+ ] }),
1261
+ showCarouselDots && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "absolute bottom-2 left-1/2 -translate-x-1/2 flex items-center gap-1 z-10 px-2 py-1 rounded-full bg-black/30 backdrop-blur-xs", children: images.map((_, idx) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1262
+ "button",
1263
+ {
1264
+ type: "button",
1265
+ "aria-label": `Go to image ${idx + 1}`,
1266
+ onClick: (e) => {
1267
+ e.stopPropagation();
1268
+ setCurrentImageIndex(idx);
1269
+ },
1270
+ className: idx === currentImageIndex ? activeDotClassName : dotClassName
1271
+ },
1272
+ idx
1273
+ )) })
1274
+ ] })
1275
+ ]
1276
+ }
1277
+ ),
1278
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "p-4 flex flex-col flex-1 space-y-3", children: [
1279
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h3", { className: titleClassName, onClick: () => onSelectProduct?.(product), children: product.name }),
1280
+ product.description && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-xs text-muted-foreground line-clamp-2", children: product.description }),
1281
+ hasRating && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: ratingClassName, children: [
1282
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(StarRow, { rating: product.average_rating, idPrefix: ratingIdPrefix }),
1283
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className: "text-xs text-muted-foreground font-medium", children: [
1284
+ product.average_rating.toFixed(1),
1285
+ " (",
1286
+ product.rating_count,
1287
+ ")"
1288
+ ] })
1289
+ ] }),
1290
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex items-center justify-between pt-2 mt-auto", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: priceClassName, children: formatPriceFn ? formatPriceFn(product.sale_price ?? product.base_price) : formatPrice(product.sale_price ?? product.base_price, product.currency) }) }),
1291
+ footerSlot,
1292
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1293
+ "button",
1294
+ {
1295
+ type: "button",
1296
+ onClick: handleCTA,
1297
+ className: buttonClassName,
1298
+ children: getCtaText()
1299
+ }
1300
+ )
1301
+ ] })
1302
+ ] });
1303
+ }
1304
+
1305
+ // src/components/product/ProductGrid.tsx
1306
+ var import_react_query2 = require("@tanstack/react-query");
1307
+
1308
+ // src/utils/graphql-url.ts
1309
+ var import_graphql_request = require("graphql-request");
1310
+ function resolveGraphQLEndpoint(url) {
1311
+ if (typeof window === "undefined") return url;
1312
+ try {
1313
+ new URL(url);
1314
+ return url;
1315
+ } catch {
1316
+ return `${window.location.origin}${url}`;
1317
+ }
1318
+ }
1319
+ function createGraphQLClient(config, accessToken) {
1320
+ const headers = {
1321
+ "Content-Type": "application/json",
1322
+ "Accept": "application/json",
1323
+ "X-Storefront-Key": config.storefrontApiKey,
1324
+ "X-Business-ID": config.businessId
1325
+ };
1326
+ if (accessToken) {
1327
+ headers["Authorization"] = `Bearer ${accessToken}`;
1328
+ }
1329
+ return new import_graphql_request.GraphQLClient(resolveGraphQLEndpoint(`${config.backendUrls.store}/graphql`), { headers });
1330
+ }
1331
+
1332
+ // src/generated/graphql.ts
1333
+ var GetStorefrontProductsDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "GetStorefrontProducts" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "page" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "Int" } } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "pageSize" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "Int" } } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "collectionSlug" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "search" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "productType" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "storefront_products" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "page" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "page" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "pageSize" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "pageSize" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "collectionSlug" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "collectionSlug" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "search" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "search" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "productType" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "productType" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "items" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "FragmentSpread", "name": { "kind": "Name", "value": "ProductFields" } }] } }, { "kind": "Field", "name": { "kind": "Name", "value": "total" } }, { "kind": "Field", "name": { "kind": "Name", "value": "page" } }, { "kind": "Field", "name": { "kind": "Name", "value": "page_size" } }, { "kind": "Field", "name": { "kind": "Name", "value": "total_pages" } }] } }] } }, { "kind": "FragmentDefinition", "name": { "kind": "Name", "value": "ProductFields" }, "typeCondition": { "kind": "NamedType", "name": { "kind": "Name", "value": "Product" } }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "business_id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "slug" } }, { "kind": "Field", "name": { "kind": "Name", "value": "description" } }, { "kind": "Field", "name": { "kind": "Name", "value": "brand" } }, { "kind": "Field", "name": { "kind": "Name", "value": "product_type" } }, { "kind": "Field", "name": { "kind": "Name", "value": "requires_shipping" } }, { "kind": "Field", "name": { "kind": "Name", "value": "is_consumable" } }, { "kind": "Field", "name": { "kind": "Name", "value": "is_bespoke" } }, { "kind": "Field", "name": { "kind": "Name", "value": "is_physical" } }, { "kind": "Field", "name": { "kind": "Name", "value": "is_non_returnable" } }, { "kind": "Field", "name": { "kind": "Name", "value": "deposit_percentage" } }, { "kind": "Field", "name": { "kind": "Name", "value": "base_price" } }, { "kind": "Field", "name": { "kind": "Name", "value": "currency" } }, { "kind": "Field", "name": { "kind": "Name", "value": "sale_price" } }, { "kind": "Field", "name": { "kind": "Name", "value": "sale_start" } }, { "kind": "Field", "name": { "kind": "Name", "value": "sale_end" } }, { "kind": "Field", "name": { "kind": "Name", "value": "images" } }, { "kind": "Field", "name": { "kind": "Name", "value": "tags" } }, { "kind": "Field", "name": { "kind": "Name", "value": "custom_fields" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "label" } }, { "kind": "Field", "name": { "kind": "Name", "value": "unit" } }, { "kind": "Field", "name": { "kind": "Name", "value": "required" } }, { "kind": "Field", "name": { "kind": "Name", "value": "field_type" } }, { "kind": "Field", "name": { "kind": "Name", "value": "options" } }] } }, { "kind": "Field", "name": { "kind": "Name", "value": "is_published" } }, { "kind": "Field", "name": { "kind": "Name", "value": "is_featured" } }, { "kind": "Field", "name": { "kind": "Name", "value": "is_popular" } }, { "kind": "Field", "name": { "kind": "Name", "value": "is_new" } }, { "kind": "Field", "name": { "kind": "Name", "value": "is_temporary" } }, { "kind": "Field", "name": { "kind": "Name", "value": "average_rating" } }, { "kind": "Field", "name": { "kind": "Name", "value": "rating_count" } }, { "kind": "Field", "name": { "kind": "Name", "value": "source_channel" } }, { "kind": "Field", "name": { "kind": "Name", "value": "source_channel_ref_id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "service_type" } }, { "kind": "Field", "name": { "kind": "Name", "value": "service_duration_minutes" } }, { "kind": "Field", "name": { "kind": "Name", "value": "service_location_type" } }, { "kind": "Field", "name": { "kind": "Name", "value": "service_coverage_radius_km" } }, { "kind": "Field", "name": { "kind": "Name", "value": "service_requires_consultation" } }, { "kind": "Field", "name": { "kind": "Name", "value": "service_max_capacity_per_slot" } }, { "kind": "Field", "name": { "kind": "Name", "value": "service_buffer_minutes" } }, { "kind": "Field", "name": { "kind": "Name", "value": "service_is_recurring" } }, { "kind": "Field", "name": { "kind": "Name", "value": "service_recurring_interval" } }, { "kind": "Field", "name": { "kind": "Name", "value": "service_project_based" } }, { "kind": "Field", "name": { "kind": "Name", "value": "digital_files" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "file_key" } }, { "kind": "Field", "name": { "kind": "Name", "value": "display_name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "sort_order" } }] } }, { "kind": "Field", "name": { "kind": "Name", "value": "download_limit" } }, { "kind": "Field", "name": { "kind": "Name", "value": "download_expires_at" } }, { "kind": "Field", "name": { "kind": "Name", "value": "weight_kg" } }, { "kind": "Field", "name": { "kind": "Name", "value": "length_cm" } }, { "kind": "Field", "name": { "kind": "Name", "value": "width_cm" } }, { "kind": "Field", "name": { "kind": "Name", "value": "height_cm" } }, { "kind": "Field", "name": { "kind": "Name", "value": "created_at" } }, { "kind": "Field", "name": { "kind": "Name", "value": "updated_at" } }, { "kind": "Field", "name": { "kind": "Name", "value": "variants" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "product_id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "sku" } }, { "kind": "Field", "name": { "kind": "Name", "value": "attributes" } }, { "kind": "Field", "name": { "kind": "Name", "value": "price" } }, { "kind": "Field", "name": { "kind": "Name", "value": "images" } }, { "kind": "Field", "name": { "kind": "Name", "value": "weight_kg" } }, { "kind": "Field", "name": { "kind": "Name", "value": "is_active" } }, { "kind": "Field", "name": { "kind": "Name", "value": "inventory" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "product_id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "variant_id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "quantity" } }, { "kind": "Field", "name": { "kind": "Name", "value": "reserved" } }, { "kind": "Field", "name": { "kind": "Name", "value": "reorder_point" } }, { "kind": "Field", "name": { "kind": "Name", "value": "reorder_quantity" } }, { "kind": "Field", "name": { "kind": "Name", "value": "track_quantity" } }, { "kind": "Field", "name": { "kind": "Name", "value": "allow_backorder" } }, { "kind": "Field", "name": { "kind": "Name", "value": "available" } }, { "kind": "Field", "name": { "kind": "Name", "value": "is_in_stock" } }, { "kind": "Field", "name": { "kind": "Name", "value": "is_low_stock" } }, { "kind": "Field", "name": { "kind": "Name", "value": "updated_at" } }] } }, { "kind": "Field", "name": { "kind": "Name", "value": "created_at" } }, { "kind": "Field", "name": { "kind": "Name", "value": "updated_at" } }] } }, { "kind": "Field", "name": { "kind": "Name", "value": "collections" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "business_id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "slug" } }, { "kind": "Field", "name": { "kind": "Name", "value": "description" } }, { "kind": "Field", "name": { "kind": "Name", "value": "image_url" } }, { "kind": "Field", "name": { "kind": "Name", "value": "collection_type" } }, { "kind": "Field", "name": { "kind": "Name", "value": "rules" } }, { "kind": "Field", "name": { "kind": "Name", "value": "status" } }, { "kind": "Field", "name": { "kind": "Name", "value": "sort_order" } }, { "kind": "Field", "name": { "kind": "Name", "value": "created_at" } }, { "kind": "Field", "name": { "kind": "Name", "value": "updated_at" } }] } }] } }] };
1334
+
1335
+ // src/services/product.service.ts
1336
+ function mapProductType(raw) {
1337
+ switch (raw) {
1338
+ case "PHYSICAL":
1339
+ return "physical" /* PHYSICAL */;
1340
+ case "DIGITAL":
1341
+ return "digital" /* DIGITAL */;
1342
+ case "SERVICE":
1343
+ return "service" /* SERVICE */;
1344
+ case "BESPOKE":
1345
+ return "bespoke" /* BESPOKE */;
1346
+ default: {
1347
+ const _exhaustive = raw;
1348
+ throw new Error(`Unhandled GraphQL ProductType member: ${_exhaustive}`);
1349
+ }
1350
+ }
1351
+ }
1352
+ function mapCustomFieldType(raw) {
1353
+ if (raw === "text" || raw === "choice") return raw;
1354
+ throw new Error(`Unrecognised custom field field_type from backend: "${raw}"`);
1355
+ }
1356
+ function mapProduct(raw) {
1357
+ return {
1358
+ ...raw,
1359
+ product_type: mapProductType(raw.product_type),
1360
+ custom_fields: raw.custom_fields.map((f) => ({
1361
+ ...f,
1362
+ unit: f.unit ?? void 0,
1363
+ field_type: mapCustomFieldType(f.field_type)
1364
+ })),
1365
+ description: raw.description ?? void 0,
1366
+ brand: raw.brand ?? void 0,
1367
+ is_physical: raw.is_physical ?? void 0,
1368
+ sale_price: raw.sale_price ?? void 0,
1369
+ sale_start: raw.sale_start ?? void 0,
1370
+ sale_end: raw.sale_end ?? void 0,
1371
+ source_channel_ref_id: raw.source_channel_ref_id ?? void 0,
1372
+ download_limit: raw.download_limit ?? void 0,
1373
+ download_expires_at: raw.download_expires_at ?? void 0,
1374
+ service_type: raw.service_type ?? void 0,
1375
+ service_duration_minutes: raw.service_duration_minutes ?? void 0,
1376
+ service_location_type: raw.service_location_type ?? void 0,
1377
+ service_coverage_radius_km: raw.service_coverage_radius_km ?? void 0,
1378
+ service_max_capacity_per_slot: raw.service_max_capacity_per_slot ?? void 0,
1379
+ service_recurring_interval: raw.service_recurring_interval ?? void 0,
1380
+ length_cm: raw.length_cm ?? void 0,
1381
+ width_cm: raw.width_cm ?? void 0,
1382
+ height_cm: raw.height_cm ?? void 0,
1383
+ average_rating: raw.average_rating ?? void 0,
1384
+ created_at: raw.created_at,
1385
+ updated_at: raw.updated_at,
1386
+ variants: raw.variants.map((v) => ({
1387
+ ...v,
1388
+ sku: v.sku ?? void 0,
1389
+ attributes: v.attributes ? JSON.parse(v.attributes) : {},
1390
+ price: v.price ?? void 0,
1391
+ weight_kg: v.weight_kg ?? void 0,
1392
+ inventory: v.inventory ? {
1393
+ ...v.inventory,
1394
+ variant_id: v.inventory.variant_id ?? void 0,
1395
+ updated_at: v.inventory.updated_at
1396
+ } : void 0,
1397
+ created_at: v.created_at,
1398
+ updated_at: v.updated_at
1399
+ })),
1400
+ collections: raw.collections.map((c) => ({
1401
+ ...c,
1402
+ business_id: c.business_id ?? void 0,
1403
+ description: c.description ?? void 0,
1404
+ image_url: c.image_url ?? void 0,
1405
+ rules: c.rules ?? void 0,
1406
+ created_at: c.created_at,
1407
+ updated_at: c.updated_at
1408
+ }))
1409
+ };
1410
+ }
1411
+ function mapProductConnection(connection) {
1412
+ return { ...connection, items: connection.items.map(mapProduct) };
1413
+ }
1414
+ async function getStorefrontProducts(config, params) {
1415
+ const client = createGraphQLClient(config);
1416
+ const data = await client.request(GetStorefrontProductsDocument, {
1417
+ page: params?.page || 1,
1418
+ pageSize: params?.pageSize || 20,
1419
+ collectionSlug: params?.collectionSlug,
1420
+ search: params?.search,
1421
+ productType: params?.productType
1422
+ });
1423
+ return mapProductConnection(data.storefront_products);
1424
+ }
1425
+
1426
+ // src/utils/cache.ts
1427
+ var CACHE_TIERS = {
1428
+ none: { staleTime: 0, gcTime: 0 },
1429
+ infinity: { staleTime: Infinity, gcTime: 0 },
1430
+ low: { staleTime: 1e3 * 30, gcTime: 1e3 * 60 },
1431
+ // 30s / 1m
1432
+ medium: { staleTime: 1e3 * 60 * 5, gcTime: 1e3 * 60 * 10 },
1433
+ // 5m / 10m
1434
+ high: { staleTime: 1e3 * 60 * 30, gcTime: 1e3 * 60 * 60 }
1435
+ // 30m / 1h
1436
+ };
1437
+ var isDevelopment2 = true;
1438
+ var getCacheTier = (tier) => {
1439
+ if (isDevelopment2) return CACHE_TIERS.none;
1440
+ return CACHE_TIERS[tier];
1441
+ };
1442
+
1443
+ // src/components/product/ProductGrid.tsx
1444
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1445
+ function ProductGrid({
1446
+ params,
1447
+ onSelectProduct,
1448
+ onWishlistToggle,
1449
+ isWishlistSavedFn,
1450
+ showWishlistButton = false,
1451
+ formatPriceFn,
1452
+ className = "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6",
1453
+ cardClassName
1454
+ }) {
1455
+ const ctx = useSdkContext();
1456
+ const { config } = resolveHookArgs(void 0, void 0, ctx);
1457
+ const { data, isLoading, error } = (0, import_react_query2.useQuery)({
1458
+ queryKey: ["products", "grid", params],
1459
+ queryFn: () => getStorefrontProducts(config, params),
1460
+ ...getCacheTier("medium")
1461
+ });
1462
+ if (isLoading) {
1463
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(CardSkeleton, { count: 8, containerClassName: className });
1464
+ }
1465
+ if (error) {
1466
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "text-center py-12 text-destructive", children: "Failed to load products. Please try again." });
1467
+ }
1468
+ const products = data?.items || [];
1469
+ if (products.length === 0) {
1470
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "text-center py-12 text-muted-foreground", children: "No products found." });
1471
+ }
1472
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className, children: products.map((product) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1473
+ ProductCard,
1474
+ {
1475
+ product,
1476
+ onSelectProduct,
1477
+ onWishlistToggle,
1478
+ isWishlistSaved: isWishlistSavedFn ? isWishlistSavedFn(product) : false,
1479
+ showWishlistButton,
1480
+ formatPriceFn,
1481
+ className: cardClassName
1482
+ },
1483
+ product.id
1484
+ )) });
1485
+ }
1486
+
1487
+ // src/components/booking/SlotPicker.tsx
1488
+ var import_react5 = require("react");
1489
+
1490
+ // src/hooks/use-service-booking.ts
1491
+ var import_react_query3 = require("@tanstack/react-query");
1492
+
1493
+ // src/services/service-booking.service.ts
1494
+ async function getAvailableSlots(config, productId, date, params) {
1495
+ const query = new URLSearchParams({ date });
1496
+ if (params?.staffId) query.set("staff_id", params.staffId);
1497
+ if (params?.locationId) query.set("location_id", params.locationId);
1498
+ if (params?.customerLatitude !== void 0) query.set("customer_latitude", String(params.customerLatitude));
1499
+ if (params?.customerLongitude !== void 0) query.set("customer_longitude", String(params.customerLongitude));
1500
+ return apiRequest(
1501
+ `${config.backendUrls.store}/catalog/products/${productId}/available-slots?${query.toString()}`,
1502
+ {
1503
+ headers: storefrontHeaders(config),
1504
+ fallbackErrorMessage: "Failed to get available slots"
1505
+ }
1506
+ );
1507
+ }
1508
+
1509
+ // src/hooks/use-service-booking.ts
1510
+ function useAvailableSlots(config, productId, date, params) {
1511
+ const ctx = useSdkContext();
1512
+ const { config: cfg } = resolveHookArgs(config, void 0, ctx);
1513
+ return (0, import_react_query3.useQuery)({
1514
+ queryKey: ["available-slots", productId, date, params?.staffId, params?.locationId],
1515
+ queryFn: () => getAvailableSlots(cfg, productId, date, params),
1516
+ enabled: !!productId && !!date
1517
+ });
1518
+ }
1519
+
1520
+ // src/components/booking/SlotPicker.tsx
1521
+ var import_jsx_runtime13 = require("react/jsx-runtime");
1522
+ function SlotPicker({
1523
+ productId,
1524
+ date,
1525
+ staffId,
1526
+ locationId,
1527
+ onSelectSlot,
1528
+ className = "space-y-4",
1529
+ slotButtonClassName = "p-3 border rounded-lg text-sm text-center hover:border-primary focus:outline-none transition-colors"
1530
+ }) {
1531
+ const { data, isLoading, error } = useAvailableSlots(void 0, productId, date, { staffId, locationId });
1532
+ const [selectedTime, setSelectedTime] = (0, import_react5.useState)(null);
1533
+ if (isLoading) {
1534
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "grid grid-cols-2 sm:grid-cols-3 gap-3 animate-pulse", children: Array.from({ length: 6 }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "h-12 bg-muted rounded-lg" }, i)) });
1535
+ }
1536
+ if (error || !data) {
1537
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "text-xs text-destructive", children: "Failed to load available time slots. Please select a different date." });
1538
+ }
1539
+ if (!data.serviceable) {
1540
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "text-xs text-muted-foreground py-4 text-center", children: data.reason || "This service isn't available for booking right now." });
1541
+ }
1542
+ if (data.available_slots.length === 0) {
1543
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "text-xs text-muted-foreground py-4 text-center", children: "No time slots available on this date." });
1544
+ }
1545
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className, children: [
1546
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("h4", { className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: "Available Time Slots" }),
1547
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "grid grid-cols-2 sm:grid-cols-3 gap-3", children: data.available_slots.map((time) => {
1548
+ const isSelected = selectedTime === time;
1549
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1550
+ "button",
1551
+ {
1552
+ type: "button",
1553
+ onClick: () => {
1554
+ setSelectedTime(time);
1555
+ onSelectSlot(time);
1556
+ },
1557
+ className: `${slotButtonClassName} ${isSelected ? "border-primary bg-primary/10 text-primary font-semibold" : "border-border text-foreground"}`,
1558
+ children: formatTimeString(time)
1559
+ },
1560
+ time
1561
+ );
1562
+ }) })
1563
+ ] });
1564
+ }
1565
+
1566
+ // src/hooks/use-bespoke-availability.ts
1567
+ var import_react_query4 = require("@tanstack/react-query");
1568
+
1569
+ // src/services/bespoke.service.ts
1570
+ async function checkAvailability(config, product_id) {
1571
+ return apiRequest(`${config.backendUrls.store}/bespoke/capacity/availability/${product_id}`, {
1572
+ headers: storefrontHeaders(config),
1573
+ fallbackErrorMessage: "Failed to check bespoke availability"
1574
+ });
1575
+ }
1576
+ async function approveBespokeDeliverable(config, accessToken, orderId) {
1577
+ return apiRequest(`${config.backendUrls.store}/orders/${orderId}/bespoke/approve`, {
1578
+ method: "POST",
1579
+ headers: { ...JSON_HEADERS, ...authHeaders(config, accessToken) },
1580
+ body: JSON.stringify({}),
1581
+ fallbackErrorMessage: "Failed to approve deliverable"
1582
+ });
1583
+ }
1584
+ async function requestBespokeRevision(config, accessToken, orderId, notes) {
1585
+ return apiRequest(`${config.backendUrls.store}/orders/${orderId}/bespoke/request-revision`, {
1586
+ method: "POST",
1587
+ headers: { ...JSON_HEADERS, ...authHeaders(config, accessToken) },
1588
+ body: JSON.stringify({ notes }),
1589
+ fallbackErrorMessage: "Failed to request revision"
1590
+ });
1591
+ }
1592
+
1593
+ // src/hooks/use-bespoke-availability.ts
1594
+ function useBespokeAvailability(product_id, config) {
1595
+ const ctx = useSdkContext();
1596
+ const { config: cfg } = resolveHookArgs(config, void 0, ctx);
1597
+ return (0, import_react_query4.useQuery)({
1598
+ queryKey: ["bespoke-availability", product_id],
1599
+ queryFn: () => checkAvailability(cfg, product_id),
1600
+ enabled: !!product_id,
1601
+ retry: false
1602
+ });
1603
+ }
1604
+ function useApproveBespokeDeliverable(config, accessToken) {
1605
+ const ctx = useSdkContext();
1606
+ const { config: cfg, accessToken: token } = resolveHookArgs(config, accessToken, ctx);
1607
+ const queryClient = (0, import_react_query4.useQueryClient)();
1608
+ return (0, import_react_query4.useMutation)({
1609
+ mutationFn: (orderId) => token ? approveBespokeDeliverable(cfg, token, orderId) : Promise.reject(new Error("No access token available")),
1610
+ onSuccess: (_data, orderId) => {
1611
+ queryClient.invalidateQueries({ queryKey: ["order", orderId] });
1612
+ queryClient.invalidateQueries({ queryKey: ["orders"] });
1613
+ }
1614
+ });
1615
+ }
1616
+ function useRequestBespokeRevision(config, accessToken) {
1617
+ const ctx = useSdkContext();
1618
+ const { config: cfg, accessToken: token } = resolveHookArgs(config, accessToken, ctx);
1619
+ const queryClient = (0, import_react_query4.useQueryClient)();
1620
+ return (0, import_react_query4.useMutation)({
1621
+ mutationFn: ({ orderId, notes }) => token ? requestBespokeRevision(cfg, token, orderId, notes) : Promise.reject(new Error("No access token available")),
1622
+ onSuccess: (_data, variables) => {
1623
+ queryClient.invalidateQueries({ queryKey: ["order", variables.orderId] });
1624
+ queryClient.invalidateQueries({ queryKey: ["orders"] });
1625
+ }
1626
+ });
1627
+ }
1628
+
1629
+ // src/components/bespoke/BespokeAvailabilityBadge.tsx
1630
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1631
+ function BespokeAvailabilityBadge({ productId, className = "inline-flex items-center gap-2 px-3 py-1 text-xs rounded-full font-medium" }) {
1632
+ const { data: availability, isLoading } = useBespokeAvailability(productId);
1633
+ if (isLoading) {
1634
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "h-5 w-28 bg-gray-200 dark:bg-gray-800 animate-pulse rounded-full" });
1635
+ }
1636
+ if (!availability) return null;
1637
+ if (availability.is_available) {
1638
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("span", { className: `${className} bg-emerald-100 dark:bg-emerald-950 text-emerald-700 dark:text-emerald-300 border border-emerald-200 dark:border-emerald-800`, children: [
1639
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "w-2 h-2 rounded-full bg-emerald-500 animate-ping" }),
1640
+ availability.available_slots,
1641
+ " slots available this month"
1642
+ ] });
1643
+ }
1644
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("span", { className: `${className} bg-amber-100 dark:bg-amber-950 text-amber-700 dark:text-amber-300 border border-amber-200 dark:border-amber-800`, children: [
1645
+ "Fully booked \u2014 Next available: ",
1646
+ availability.next_available_date ? new Date(availability.next_available_date).toLocaleDateString() : "Next Month"
1647
+ ] });
1648
+ }
1649
+
1650
+ // src/components/bespoke/BespokeProgressTracker.tsx
1651
+ var import_jsx_runtime15 = require("react/jsx-runtime");
1652
+ function BespokeProgressTracker({
1653
+ stage,
1654
+ className = "flex items-center w-full my-4",
1655
+ activeStepClassName = "bg-emerald-600 text-white font-bold",
1656
+ completedStepClassName = "bg-emerald-500 text-white",
1657
+ inactiveStepClassName = "border border-gray-300 dark:border-gray-700 bg-gray-100 dark:bg-gray-800 text-gray-500"
1658
+ }) {
1659
+ const stages = [
1660
+ { key: "received", label: "Order Received" },
1661
+ { key: "development", label: "In Development" },
1662
+ { key: "finished", label: "Finished" },
1663
+ { key: "dispatched", label: "Ready / Dispatched" }
1664
+ ];
1665
+ const currentIdx = stages.findIndex((s) => s.key === stage);
1666
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className, children: stages.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-1 flex-col items-center", children: [
1667
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex w-full items-center", children: [
1668
+ i > 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: `h-0.5 flex-1 ${i <= currentIdx ? "bg-emerald-500" : "bg-gray-200 dark:bg-gray-800"}` }),
1669
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: `flex h-8 w-8 items-center justify-center rounded-full text-xs font-semibold ${i < currentIdx ? completedStepClassName : i === currentIdx ? activeStepClassName : inactiveStepClassName}`, children: i < currentIdx ? "\u2713" : i + 1 }),
1670
+ i < stages.length - 1 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: `h-0.5 flex-1 ${i < currentIdx ? "bg-emerald-500" : "bg-gray-200 dark:bg-gray-800"}` })
1671
+ ] }),
1672
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: `mt-2 text-center text-xs ${i === currentIdx ? "font-semibold text-gray-900 dark:text-gray-100" : "text-gray-500"}`, children: s.label })
1673
+ ] }, s.key)) });
1674
+ }
1675
+
1676
+ // src/components/orders/DeliverableReviewer.tsx
1677
+ var import_react6 = require("react");
1678
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1679
+ function DeliverableReviewer({ order, className = "space-y-4 p-4 border rounded-xl bg-muted/50" }) {
1680
+ const [revisionNotes, setRevisionNotes] = (0, import_react6.useState)("");
1681
+ const [showRevisionForm, setShowRevisionForm] = (0, import_react6.useState)(false);
1682
+ const approveMutation = useApproveBespokeDeliverable();
1683
+ const revisionMutation = useRequestBespokeRevision();
1684
+ if (!order.bespoke_deliverable_submitted_at) {
1685
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className, children: [
1686
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h4", { className: "text-xs font-bold uppercase tracking-wider text-foreground", children: "Deliverable" }),
1687
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "text-xs text-muted-foreground py-4 text-center", children: "The seller hasn't submitted a deliverable for review yet." })
1688
+ ] });
1689
+ }
1690
+ const awaitingApproval = order.bespoke_status === "awaiting_approval";
1691
+ const handleApprove = async () => {
1692
+ try {
1693
+ await approveMutation.mutateAsync(order.id);
1694
+ } catch (error) {
1695
+ logger.error(error);
1696
+ }
1697
+ };
1698
+ const handleRequestRevision = async () => {
1699
+ if (!revisionNotes.trim() || revisionNotes.trim().length < 10) return;
1700
+ try {
1701
+ await revisionMutation.mutateAsync({ orderId: order.id, notes: revisionNotes.trim() });
1702
+ setShowRevisionForm(false);
1703
+ setRevisionNotes("");
1704
+ } catch (error) {
1705
+ logger.error(error);
1706
+ }
1707
+ };
1708
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className, children: [
1709
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex justify-between items-center", children: [
1710
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h4", { className: "text-xs font-bold uppercase tracking-wider text-foreground", children: "Deliverable" }),
1711
+ order.bespoke_status && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: `px-2 py-0.5 rounded text-[10px] font-semibold uppercase ${order.bespoke_status === "confirmed" ? "bg-primary/10 text-primary" : order.bespoke_status === "awaiting_approval" ? "bg-blue-500/10 text-blue-600 dark:text-blue-400" : "bg-amber-500/10 text-amber-600 dark:text-amber-400"}`, children: order.bespoke_status.replace("_", " ") })
1712
+ ] }),
1713
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "p-3 bg-card border rounded-lg space-y-2", children: [
1714
+ order.bespoke_deliverable_url && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1715
+ "a",
1716
+ {
1717
+ href: order.bespoke_deliverable_url,
1718
+ target: "_blank",
1719
+ rel: "noreferrer",
1720
+ className: "text-xs text-primary hover:underline flex items-center gap-1",
1721
+ children: "\u{1F4CE} View Deliverable"
1722
+ }
1723
+ ),
1724
+ order.bespoke_deliverable_notes && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "text-xs text-muted-foreground", children: order.bespoke_deliverable_notes }),
1725
+ (order.bespoke_revision_count ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("p", { className: "text-[10px] text-muted-foreground", children: [
1726
+ "Revisions requested: ",
1727
+ order.bespoke_revision_count
1728
+ ] }),
1729
+ awaitingApproval && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex items-center gap-2 pt-2", children: [
1730
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1731
+ "button",
1732
+ {
1733
+ type: "button",
1734
+ onClick: handleApprove,
1735
+ disabled: approveMutation.isPending,
1736
+ className: "px-3 py-1.5 bg-primary text-primary-foreground text-xs font-medium rounded hover:bg-primary/90 disabled:opacity-50",
1737
+ children: approveMutation.isPending ? "Approving\u2026" : "Approve Deliverable"
1738
+ }
1739
+ ),
1740
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1741
+ "button",
1742
+ {
1743
+ type: "button",
1744
+ onClick: () => setShowRevisionForm(!showRevisionForm),
1745
+ className: "px-3 py-1.5 border text-xs font-medium rounded hover:bg-muted",
1746
+ children: "Request Revision"
1747
+ }
1748
+ )
1749
+ ] }),
1750
+ showRevisionForm && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "pt-2 space-y-2", children: [
1751
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1752
+ "textarea",
1753
+ {
1754
+ rows: 2,
1755
+ value: revisionNotes,
1756
+ onChange: (e) => setRevisionNotes(e.target.value),
1757
+ placeholder: "Explain what needs to change (at least 10 characters)...",
1758
+ className: "w-full p-2 border rounded text-xs bg-transparent"
1759
+ }
1760
+ ),
1761
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1762
+ "button",
1763
+ {
1764
+ type: "button",
1765
+ onClick: handleRequestRevision,
1766
+ disabled: revisionMutation.isPending || revisionNotes.trim().length < 10,
1767
+ className: "px-3 py-1 bg-amber-600 text-white text-xs font-medium rounded hover:bg-amber-700 disabled:opacity-50",
1768
+ children: revisionMutation.isPending ? "Submitting\u2026" : "Submit Revision Request"
1769
+ }
1770
+ )
1771
+ ] })
1772
+ ] })
1773
+ ] });
1774
+ }
1775
+
1776
+ // src/components/shared/TruncatedValue.tsx
1777
+ var import_jsx_runtime17 = require("react/jsx-runtime");
1778
+ function TruncatedValue({ value, fullValue, className = "", triggerClassName = "" }) {
1779
+ const tooltipText = typeof fullValue === "string" ? fullValue : typeof value === "string" ? value : void 0;
1780
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1781
+ "span",
1782
+ {
1783
+ title: tooltipText,
1784
+ className: `truncate tabular-nums cursor-default inline-block max-w-full ${triggerClassName} ${className}`.trim(),
1785
+ children: value
1786
+ }
1787
+ );
1788
+ }
1789
+
1790
+ // src/components/shared/AddressAutocomplete.tsx
1791
+ var import_react7 = require("react");
1792
+ var import_lucide_react2 = require("lucide-react");
1793
+
1794
+ // src/services/logistics.service.ts
1795
+ async function autocompleteAddress(config, q, country) {
1796
+ const params = new URLSearchParams();
1797
+ params.set("q", q);
1798
+ if (country) params.set("country", country);
1799
+ return apiRequest(`${config.backendUrls.store}/logistics/address/autocomplete?${params.toString()}`, {
1800
+ headers: storefrontHeaders(config),
1801
+ fallbackErrorMessage: "Failed to autocomplete address"
1802
+ });
1803
+ }
1804
+
1805
+ // src/components/shared/AddressAutocomplete.tsx
1806
+ var import_jsx_runtime18 = require("react/jsx-runtime");
1807
+ function AddressAutocomplete({
1808
+ value,
1809
+ onChange,
1810
+ onResolve,
1811
+ country,
1812
+ disabled,
1813
+ placeholder = "Start typing an address\u2026",
1814
+ className = "",
1815
+ id,
1816
+ config,
1817
+ ...rest
1818
+ }) {
1819
+ const ctx = useSdkContext();
1820
+ const generatedId = (0, import_react7.useId)();
1821
+ const fieldId = id ?? generatedId;
1822
+ const [open, setOpen] = (0, import_react7.useState)(false);
1823
+ const [highlight, setHighlight] = (0, import_react7.useState)(-1);
1824
+ const [justPicked, setJustPicked] = (0, import_react7.useState)(false);
1825
+ const [suggestions, setSuggestions] = (0, import_react7.useState)([]);
1826
+ const [isLoading, setIsLoading] = (0, import_react7.useState)(false);
1827
+ const wrapRef = (0, import_react7.useRef)(null);
1828
+ (0, import_react7.useEffect)(() => {
1829
+ if (!open || justPicked || disabled || !value || value.trim().length < 3) {
1830
+ setSuggestions([]);
1831
+ return;
1832
+ }
1833
+ let isMounted = true;
1834
+ const timer = setTimeout(async () => {
1835
+ setIsLoading(true);
1836
+ try {
1837
+ const { config: cfg } = resolveHookArgs(config, void 0, ctx);
1838
+ const { results } = await autocompleteAddress(cfg, value, country);
1839
+ if (isMounted) {
1840
+ setSuggestions(
1841
+ results.map((r) => ({
1842
+ place_id: r.place_id,
1843
+ display_name: r.display_name,
1844
+ line1: r.line1,
1845
+ city: r.city ?? void 0,
1846
+ state: r.state ?? void 0,
1847
+ postal_code: r.postal_code ?? void 0,
1848
+ country: r.country_code ?? r.country ?? void 0,
1849
+ latitude: r.latitude ?? void 0,
1850
+ longitude: r.longitude ?? void 0
1851
+ }))
1852
+ );
1853
+ }
1854
+ } catch {
1855
+ if (isMounted) setSuggestions([]);
1856
+ } finally {
1857
+ if (isMounted) setIsLoading(false);
1858
+ }
1859
+ }, 350);
1860
+ return () => {
1861
+ isMounted = false;
1862
+ clearTimeout(timer);
1863
+ };
1864
+ }, [value, country, open, justPicked, disabled, config, ctx]);
1865
+ (0, import_react7.useEffect)(() => {
1866
+ const onDocClick = (e) => {
1867
+ if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
1868
+ };
1869
+ document.addEventListener("mousedown", onDocClick);
1870
+ return () => document.removeEventListener("mousedown", onDocClick);
1871
+ }, []);
1872
+ const pick = (s) => {
1873
+ onChange(s.line1 || value);
1874
+ onResolve?.(s);
1875
+ setJustPicked(true);
1876
+ setOpen(false);
1877
+ setHighlight(-1);
1878
+ };
1879
+ const showDropdown = open && !justPicked && (isLoading || suggestions.length > 0);
1880
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { ref: wrapRef, className: "relative w-full", children: [
1881
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "relative flex items-center", children: [
1882
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react2.MapPin, { className: "absolute left-3 w-4 h-4 text-muted-foreground pointer-events-none" }),
1883
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1884
+ "input",
1885
+ {
1886
+ id: fieldId,
1887
+ value,
1888
+ disabled,
1889
+ placeholder,
1890
+ autoComplete: "off",
1891
+ className: `w-full h-10 pl-9 pr-9 py-2 text-sm bg-background border border-input rounded-lg focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50 ${className}`.trim(),
1892
+ onChange: (e) => {
1893
+ onChange(e.target.value);
1894
+ setJustPicked(false);
1895
+ setOpen(true);
1896
+ },
1897
+ onFocus: () => {
1898
+ if (!justPicked) setOpen(true);
1899
+ },
1900
+ onKeyDown: (e) => {
1901
+ if (!showDropdown) return;
1902
+ if (e.key === "ArrowDown") {
1903
+ e.preventDefault();
1904
+ setHighlight((h) => Math.min(h + 1, suggestions.length - 1));
1905
+ } else if (e.key === "ArrowUp") {
1906
+ e.preventDefault();
1907
+ setHighlight((h) => Math.max(h - 1, 0));
1908
+ } else if (e.key === "Enter" && highlight >= 0 && suggestions[highlight]) {
1909
+ e.preventDefault();
1910
+ pick(suggestions[highlight]);
1911
+ } else if (e.key === "Escape") {
1912
+ setOpen(false);
1913
+ }
1914
+ },
1915
+ ...rest
1916
+ }
1917
+ ),
1918
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react2.Loader2, { className: "absolute right-3 w-4 h-4 text-muted-foreground animate-spin" }) : justPicked ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react2.Check, { className: "absolute right-3 w-4 h-4 text-primary" }) : null
1919
+ ] }),
1920
+ showDropdown && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "absolute z-50 w-full mt-1 bg-card border rounded-lg shadow-lg max-h-60 overflow-y-auto", children: isLoading && suggestions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex items-center gap-2 px-3 py-3 text-sm text-muted-foreground", children: [
1921
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react2.Loader2, { className: "w-3.5 h-3.5 animate-spin" }),
1922
+ " Searching addresses\u2026"
1923
+ ] }) : suggestions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "px-3 py-3 text-sm text-muted-foreground", children: "No matches found. Enter address manually." }) : suggestions.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
1924
+ "button",
1925
+ {
1926
+ type: "button",
1927
+ onMouseEnter: () => setHighlight(i),
1928
+ onClick: () => pick(s),
1929
+ className: `w-full text-left flex items-start gap-2 px-3 py-2 text-sm transition-colors ${i === highlight ? "bg-muted" : "hover:bg-muted/50"}`,
1930
+ children: [
1931
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react2.MapPin, { className: "w-3.5 h-3.5 mt-0.5 text-muted-foreground shrink-0" }),
1932
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "min-w-0", children: [
1933
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "block font-medium text-foreground truncate", children: s.line1 || s.display_name }),
1934
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "block text-xs text-muted-foreground truncate", children: s.display_name })
1935
+ ] })
1936
+ ]
1937
+ },
1938
+ s.place_id || `${s.display_name}-${i}`
1939
+ )) })
1940
+ ] });
1941
+ }
1942
+
1943
+ // src/components/shared/OfflineBanner.tsx
1944
+ var import_react8 = require("react");
1945
+ var import_lucide_react3 = require("lucide-react");
1946
+ var import_jsx_runtime19 = require("react/jsx-runtime");
1947
+ function OfflineBanner({
1948
+ message = "You are currently offline. Check your network connection.",
1949
+ className = ""
1950
+ }) {
1951
+ const [isOffline, setIsOffline] = (0, import_react8.useState)(false);
1952
+ (0, import_react8.useEffect)(() => {
1953
+ if (typeof window === "undefined") return;
1954
+ const handleOnline = () => setIsOffline(false);
1955
+ const handleOffline = () => setIsOffline(true);
1956
+ setIsOffline(!navigator.onLine);
1957
+ window.addEventListener("online", handleOnline);
1958
+ window.addEventListener("offline", handleOffline);
1959
+ return () => {
1960
+ window.removeEventListener("online", handleOnline);
1961
+ window.removeEventListener("offline", handleOffline);
1962
+ };
1963
+ }, []);
1964
+ if (!isOffline) return null;
1965
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1966
+ "div",
1967
+ {
1968
+ className: `bg-amber-500/10 border-b border-amber-500/20 px-4 py-2 text-amber-700 dark:text-amber-400 text-xs font-medium flex items-center justify-between animate-in slide-in-from-top-2 duration-300 ${className}`.trim(),
1969
+ children: /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-center gap-2", children: [
1970
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react3.WifiOff, { className: "w-4 h-4 text-amber-500 shrink-0" }),
1971
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { children: message })
1972
+ ] })
1973
+ }
1974
+ );
1975
+ }
1976
+
1977
+ // src/components/shared/MapPinDrop.tsx
1978
+ var import_react9 = require("react");
1979
+ var import_lucide_react4 = require("lucide-react");
1980
+ var import_jsx_runtime20 = require("react/jsx-runtime");
1981
+ function MapPinDrop({
1982
+ initialLatitude = 6.5244,
1983
+ initialLongitude = 3.3792,
1984
+ onPinDrop,
1985
+ value,
1986
+ height = "300px",
1987
+ disabled = false
1988
+ }) {
1989
+ const [isMounted, setIsMounted] = (0, import_react9.useState)(false);
1990
+ const [lat, setLat] = (0, import_react9.useState)(value?.latitude ?? initialLatitude);
1991
+ const [lng, setLng] = (0, import_react9.useState)(value?.longitude ?? initialLongitude);
1992
+ (0, import_react9.useEffect)(() => {
1993
+ setIsMounted(true);
1994
+ }, []);
1995
+ (0, import_react9.useEffect)(() => {
1996
+ if (value) {
1997
+ setLat(value.latitude);
1998
+ setLng(value.longitude);
1999
+ }
2000
+ }, [value]);
2001
+ if (!isMounted) {
2002
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2003
+ "div",
2004
+ {
2005
+ className: "w-full rounded-lg bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 flex items-center justify-center",
2006
+ style: { height },
2007
+ children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react4.Loader2, { className: "w-6 h-6 text-gray-400 animate-spin" })
2008
+ }
2009
+ );
2010
+ }
2011
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "w-full rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900", children: [
2012
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "p-4 flex flex-col gap-3", children: [
2013
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex items-center justify-between text-xs text-gray-500", children: [
2014
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { children: "Coordinates Selector" }),
2015
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("span", { children: [
2016
+ lat.toFixed(6),
2017
+ ", ",
2018
+ lng.toFixed(6)
2019
+ ] })
2020
+ ] }),
2021
+ !disabled && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "grid grid-cols-2 gap-2", children: [
2022
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { children: [
2023
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("label", { className: "block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1", children: "Latitude" }),
2024
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2025
+ "input",
2026
+ {
2027
+ type: "number",
2028
+ step: "any",
2029
+ value: lat,
2030
+ onChange: (e) => {
2031
+ const val = parseFloat(e.target.value) || 0;
2032
+ setLat(val);
2033
+ onPinDrop?.({ latitude: val, longitude: lng });
2034
+ },
2035
+ className: "w-full h-9 px-2 text-xs border border-gray-300 dark:border-gray-700 rounded bg-transparent"
2036
+ }
2037
+ )
2038
+ ] }),
2039
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { children: [
2040
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("label", { className: "block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1", children: "Longitude" }),
2041
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2042
+ "input",
2043
+ {
2044
+ type: "number",
2045
+ step: "any",
2046
+ value: lng,
2047
+ onChange: (e) => {
2048
+ const val = parseFloat(e.target.value) || 0;
2049
+ setLng(val);
2050
+ onPinDrop?.({ latitude: lat, longitude: val });
2051
+ },
2052
+ className: "w-full h-9 px-2 text-xs border border-gray-300 dark:border-gray-700 rounded bg-transparent"
2053
+ }
2054
+ )
2055
+ ] })
2056
+ ] })
2057
+ ] }),
2058
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "px-3 py-2 bg-gray-50 dark:bg-gray-800/50 border-t border-gray-200 dark:border-gray-700 text-xs text-gray-500", children: disabled ? "Location view only" : "Adjust coordinates to fine-tune map location" })
2059
+ ] });
2060
+ }
2061
+
2062
+ // src/components/shared/SearchableInput.tsx
2063
+ var import_react10 = require("react");
2064
+ var import_lucide_react5 = require("lucide-react");
2065
+ var import_jsx_runtime21 = require("react/jsx-runtime");
2066
+ function SearchableInput({
2067
+ items,
2068
+ value,
2069
+ onSelect,
2070
+ placeholder = "Search...",
2071
+ disabled = false,
2072
+ isLoading = false,
2073
+ className = "",
2074
+ onSearchChange,
2075
+ debounceMs = 300
2076
+ }) {
2077
+ const [isOpen, setIsOpen] = (0, import_react10.useState)(false);
2078
+ const [search, setSearch] = (0, import_react10.useState)("");
2079
+ const inputRef = (0, import_react10.useRef)(null);
2080
+ const dropdownRef = (0, import_react10.useRef)(null);
2081
+ const debounceRef = (0, import_react10.useRef)(void 0);
2082
+ const selectedItem = items.find((item) => item.value === value);
2083
+ (0, import_react10.useEffect)(() => {
2084
+ if (selectedItem) {
2085
+ setSearch(selectedItem.label);
2086
+ }
2087
+ }, [selectedItem]);
2088
+ const handleSearchChange = (newValue) => {
2089
+ setSearch(newValue);
2090
+ if (debounceRef.current) clearTimeout(debounceRef.current);
2091
+ debounceRef.current = setTimeout(() => {
2092
+ onSearchChange?.(newValue);
2093
+ }, debounceMs);
2094
+ };
2095
+ const handleSelect = (item) => {
2096
+ onSelect(item.value);
2097
+ setSearch(item.label);
2098
+ setIsOpen(false);
2099
+ };
2100
+ const handleClear = () => {
2101
+ setSearch("");
2102
+ onSelect("");
2103
+ inputRef.current?.focus();
2104
+ };
2105
+ const serverSearch = typeof onSearchChange === "function";
2106
+ const filteredItems = serverSearch ? items : items.filter(
2107
+ (item) => item.label.toLowerCase().includes(search.toLowerCase()) || item.description?.toLowerCase().includes(search.toLowerCase())
2108
+ );
2109
+ (0, import_react10.useEffect)(() => {
2110
+ const handleClickOutside = (event) => {
2111
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target) && inputRef.current && !inputRef.current.contains(event.target)) {
2112
+ setIsOpen(false);
2113
+ }
2114
+ };
2115
+ document.addEventListener("mousedown", handleClickOutside);
2116
+ return () => document.removeEventListener("mousedown", handleClickOutside);
2117
+ }, []);
2118
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "relative w-full", children: [
2119
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "relative", children: [
2120
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2121
+ "input",
2122
+ {
2123
+ ref: inputRef,
2124
+ value: search,
2125
+ onChange: (e) => handleSearchChange(e.target.value),
2126
+ onFocus: () => setIsOpen(true),
2127
+ placeholder,
2128
+ disabled,
2129
+ className: `w-full h-10 px-3 pr-8 text-sm bg-background border border-input rounded-lg focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50 ${className}`.trim()
2130
+ }
2131
+ ),
2132
+ search && !disabled && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2133
+ "button",
2134
+ {
2135
+ type: "button",
2136
+ onClick: handleClear,
2137
+ className: "absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground",
2138
+ children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react5.X, { className: "w-4 h-4" })
2139
+ }
2140
+ )
2141
+ ] }),
2142
+ isOpen && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2143
+ "div",
2144
+ {
2145
+ ref: dropdownRef,
2146
+ className: "absolute z-50 w-full mt-1 bg-card border rounded-lg shadow-lg max-h-64 overflow-y-auto",
2147
+ children: isLoading ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "p-3 text-center text-xs text-muted-foreground", children: "Loading options\u2026" }) : filteredItems.length > 0 ? filteredItems.map((item) => /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
2148
+ "div",
2149
+ {
2150
+ onClick: () => handleSelect(item),
2151
+ className: "flex items-center justify-between py-2 px-3 cursor-pointer hover:bg-muted transition-colors",
2152
+ children: [
2153
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-3 flex-1 min-w-0", children: [
2154
+ item.icon,
2155
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex-1 min-w-0", children: [
2156
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "font-medium text-sm text-foreground truncate", children: item.label }),
2157
+ item.description && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "text-xs text-muted-foreground truncate", children: item.description })
2158
+ ] })
2159
+ ] }),
2160
+ value === item.value && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react5.Check, { className: "w-4 h-4 text-primary shrink-0" })
2161
+ ]
2162
+ },
2163
+ item.value
2164
+ )) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "p-4 text-center text-sm text-muted-foreground", children: "No results found" })
2165
+ }
2166
+ )
2167
+ ] });
2168
+ }
2169
+
2170
+ // src/components/shared/SearchablePopover.tsx
2171
+ var import_react11 = require("react");
2172
+ var import_lucide_react6 = require("lucide-react");
2173
+ var import_jsx_runtime22 = require("react/jsx-runtime");
2174
+ function SearchablePopover({
2175
+ items,
2176
+ value,
2177
+ onSelect,
2178
+ placeholder = "Select an option",
2179
+ searchPlaceholder = "Search...",
2180
+ emptyMessage = "No results found.",
2181
+ disabled = false,
2182
+ isLoading = false,
2183
+ triggerClassName = "",
2184
+ contentClassName = "",
2185
+ onSearchChange,
2186
+ searchValue,
2187
+ hasNextPage = false,
2188
+ isFetchingNextPage = false,
2189
+ onFetchNextPage,
2190
+ resolvedSelectedItem
2191
+ }) {
2192
+ const [open, setOpen] = (0, import_react11.useState)(false);
2193
+ const [query, setQuery] = (0, import_react11.useState)("");
2194
+ const popoverRef = (0, import_react11.useRef)(null);
2195
+ const serverSearch = typeof onSearchChange === "function";
2196
+ const activeSearch = serverSearch ? searchValue ?? "" : query;
2197
+ const selectedItem = resolvedSelectedItem ?? items.find((item) => item.value === value);
2198
+ (0, import_react11.useEffect)(() => {
2199
+ const handleClickOutside = (e) => {
2200
+ if (popoverRef.current && !popoverRef.current.contains(e.target)) {
2201
+ setOpen(false);
2202
+ }
2203
+ };
2204
+ document.addEventListener("mousedown", handleClickOutside);
2205
+ return () => document.removeEventListener("mousedown", handleClickOutside);
2206
+ }, []);
2207
+ const handleSearch = (text) => {
2208
+ if (serverSearch) {
2209
+ onSearchChange?.(text);
2210
+ } else {
2211
+ setQuery(text);
2212
+ }
2213
+ };
2214
+ const filteredItems = serverSearch ? items : items.filter(
2215
+ (item) => item.label.toLowerCase().includes(query.toLowerCase()) || item.description?.toLowerCase().includes(query.toLowerCase())
2216
+ );
2217
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { ref: popoverRef, className: "relative w-full", children: [
2218
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
2219
+ "button",
2220
+ {
2221
+ type: "button",
2222
+ disabled,
2223
+ onClick: () => setOpen((prev) => !prev),
2224
+ className: `w-full h-10 px-3 py-2 text-sm bg-background border border-input rounded-lg flex items-center justify-between font-medium text-foreground disabled:opacity-50 ${triggerClassName}`.trim(),
2225
+ children: [
2226
+ selectedItem ? /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex items-center gap-2 truncate", children: [
2227
+ selectedItem.icon,
2228
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { children: selectedItem.label })
2229
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "text-muted-foreground", children: placeholder }),
2230
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_lucide_react6.ChevronsUpDown, { className: "ml-2 h-4 w-4 shrink-0 text-muted-foreground" })
2231
+ ]
2232
+ }
2233
+ ),
2234
+ open && /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
2235
+ "div",
2236
+ {
2237
+ className: `absolute z-50 mt-1 w-full bg-card border rounded-lg shadow-xl overflow-hidden p-2 ${contentClassName}`.trim(),
2238
+ children: [
2239
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2240
+ "input",
2241
+ {
2242
+ type: "text",
2243
+ value: activeSearch,
2244
+ onChange: (e) => handleSearch(e.target.value),
2245
+ placeholder: searchPlaceholder,
2246
+ className: "w-full h-9 px-3 text-xs bg-muted border border-input rounded mb-2 focus:outline-none"
2247
+ }
2248
+ ),
2249
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "max-h-60 overflow-y-auto space-y-1", children: [
2250
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "p-3 text-center text-xs text-muted-foreground flex items-center justify-center gap-2", children: [
2251
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_lucide_react6.Loader2, { className: "w-4 h-4 animate-spin" }),
2252
+ " Loading options\u2026"
2253
+ ] }) : filteredItems.length > 0 ? filteredItems.map((item) => /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
2254
+ "button",
2255
+ {
2256
+ type: "button",
2257
+ onClick: () => {
2258
+ onSelect(item.value);
2259
+ setOpen(false);
2260
+ },
2261
+ className: "w-full text-left flex items-center justify-between py-2 px-3 rounded text-xs hover:bg-muted transition-colors",
2262
+ children: [
2263
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex items-center gap-2 min-w-0", children: [
2264
+ item.icon,
2265
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "truncate", children: [
2266
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "font-semibold text-foreground", children: item.label }),
2267
+ item.description && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "text-[10px] text-muted-foreground", children: item.description })
2268
+ ] })
2269
+ ] }),
2270
+ value === item.value && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_lucide_react6.Check, { className: "h-4 w-4 text-primary shrink-0" })
2271
+ ]
2272
+ },
2273
+ item.value
2274
+ )) : /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "p-3 text-center text-xs text-muted-foreground", children: emptyMessage }),
2275
+ hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2276
+ "button",
2277
+ {
2278
+ type: "button",
2279
+ onClick: () => onFetchNextPage?.(),
2280
+ disabled: isFetchingNextPage,
2281
+ className: "w-full py-1 text-center text-[11px] text-primary font-semibold hover:underline",
2282
+ children: isFetchingNextPage ? "Loading more..." : "Load more..."
2283
+ }
2284
+ )
2285
+ ] })
2286
+ ]
2287
+ }
2288
+ )
2289
+ ] });
2290
+ }
2291
+
2292
+ // src/components/shared/LucideReactIcon.tsx
2293
+ var LucideIcons = __toESM(require("lucide-react"));
2294
+ var import_jsx_runtime23 = require("react/jsx-runtime");
2295
+ var toPascalCase = (str) => {
2296
+ return str.replace(/[-_]+/g, " ").replace(/\s+(.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (_, c) => c.toUpperCase());
2297
+ };
2298
+ var LucideReactIcon = ({ icon, ...props }) => {
2299
+ if (!icon || typeof icon === "boolean" || typeof icon === "number" || Array.isArray(icon)) {
2300
+ return null;
2301
+ }
2302
+ if (typeof icon === "function" || typeof icon === "object" && icon !== null && ("$$typeof" in icon || "render" in icon)) {
2303
+ const Component = icon;
2304
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Component, { size: props.size ?? 24, color: props.color ?? "currentColor", strokeWidth: props.strokeWidth ?? 2, ...props });
2305
+ }
2306
+ const iconObj = typeof icon === "object" && icon !== null ? icon : null;
2307
+ const rawName = typeof icon === "string" ? icon : typeof iconObj?.name === "string" ? iconObj.name : typeof iconObj?.icon === "string" ? iconObj.icon : null;
2308
+ if (!rawName) return null;
2309
+ const pascalName = toPascalCase(rawName);
2310
+ const IconComponent = LucideIcons[pascalName] || LucideIcons[rawName];
2311
+ if (!IconComponent) {
2312
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2313
+ "span",
2314
+ {
2315
+ style: {
2316
+ fontSize: props.size ?? 24,
2317
+ color: props.color ?? "currentColor"
2318
+ },
2319
+ className: "flex items-center justify-center text-xs font-medium text-center break-all leading-tight",
2320
+ children: rawName
2321
+ }
2322
+ );
2323
+ }
2324
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(IconComponent, { size: props.size ?? 24, color: props.color ?? "currentColor", strokeWidth: props.strokeWidth ?? 2, ...props });
2325
+ };
2326
+
2327
+ // src/components/shared/UnsavedChangesGuard.tsx
2328
+ var import_react12 = require("react");
2329
+ var import_lucide_react7 = require("lucide-react");
2330
+ var import_jsx_runtime24 = require("react/jsx-runtime");
2331
+ function useUnsavedChanges(options = {}) {
2332
+ const {
2333
+ browserMessage = "You have unsaved changes. If you leave, your work will be lost.",
2334
+ dialogTitle = "Unsaved Changes",
2335
+ dialogBody = "You have unsaved changes. If you leave now, all your progress will be lost."
2336
+ } = options;
2337
+ const [isDirty, setIsDirty] = (0, import_react12.useState)(false);
2338
+ const [dialogOpen, setDialogOpen] = (0, import_react12.useState)(false);
2339
+ const pendingActionRef = (0, import_react12.useRef)(null);
2340
+ (0, import_react12.useEffect)(() => {
2341
+ if (!isDirty) return;
2342
+ const handler = (e) => {
2343
+ e.preventDefault();
2344
+ e.returnValue = browserMessage;
2345
+ return browserMessage;
2346
+ };
2347
+ window.addEventListener("beforeunload", handler);
2348
+ return () => window.removeEventListener("beforeunload", handler);
2349
+ }, [isDirty, browserMessage]);
2350
+ const markDirty = (0, import_react12.useCallback)(() => setIsDirty(true), []);
2351
+ const resetDirty = (0, import_react12.useCallback)(() => {
2352
+ setIsDirty(false);
2353
+ setDialogOpen(false);
2354
+ pendingActionRef.current = null;
2355
+ }, []);
2356
+ const guardedNavigate = (0, import_react12.useCallback)(
2357
+ (action) => {
2358
+ if (!isDirty) {
2359
+ action();
2360
+ return;
2361
+ }
2362
+ pendingActionRef.current = action;
2363
+ setDialogOpen(true);
2364
+ },
2365
+ [isDirty]
2366
+ );
2367
+ const handleStay = (0, import_react12.useCallback)(() => {
2368
+ setDialogOpen(false);
2369
+ pendingActionRef.current = null;
2370
+ }, []);
2371
+ const handleLeave = (0, import_react12.useCallback)(() => {
2372
+ setDialogOpen(false);
2373
+ setIsDirty(false);
2374
+ const action = pendingActionRef.current;
2375
+ pendingActionRef.current = null;
2376
+ action?.();
2377
+ }, []);
2378
+ return {
2379
+ isDirty,
2380
+ markDirty,
2381
+ resetDirty,
2382
+ guardedNavigate,
2383
+ dialogProps: {
2384
+ open: dialogOpen,
2385
+ onStay: handleStay,
2386
+ onLeave: handleLeave,
2387
+ title: dialogTitle,
2388
+ body: dialogBody
2389
+ }
2390
+ };
2391
+ }
2392
+ function UnsavedChangesDialog({
2393
+ open,
2394
+ onStay,
2395
+ onLeave,
2396
+ title = "Unsaved Changes",
2397
+ body = "You have unsaved changes. If you leave now, all your progress will be lost."
2398
+ }) {
2399
+ if (!open) return null;
2400
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-in fade-in duration-200", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "w-full max-w-md bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl p-6 shadow-2xl space-y-4", children: [
2401
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex flex-col items-center text-center", children: [
2402
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "w-12 h-12 rounded-xl bg-amber-500/10 flex items-center justify-center mb-3 text-amber-500", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_lucide_react7.AlertTriangle, { className: "w-6 h-6" }) }),
2403
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("h3", { className: "text-lg font-bold text-gray-900 dark:text-gray-100", children: title }),
2404
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1 leading-relaxed", children: body })
2405
+ ] }),
2406
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex flex-col sm:flex-row gap-2 pt-2", children: [
2407
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
2408
+ "button",
2409
+ {
2410
+ type: "button",
2411
+ onClick: onStay,
2412
+ className: "flex-1 h-10 px-4 text-xs font-semibold rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 hover:bg-gray-50",
2413
+ children: "Stay & Keep Editing"
2414
+ }
2415
+ ),
2416
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
2417
+ "button",
2418
+ {
2419
+ type: "button",
2420
+ onClick: onLeave,
2421
+ className: "flex-1 h-10 px-4 text-xs font-semibold rounded-xl bg-red-600 text-white hover:bg-red-700",
2422
+ children: "Leave Without Saving"
2423
+ }
2424
+ )
2425
+ ] })
2426
+ ] }) });
2427
+ }
2428
+
2429
+ // src/components/shared/MultiSelectPicker.tsx
2430
+ var import_react13 = require("react");
2431
+ var import_lucide_react8 = require("lucide-react");
2432
+ var import_jsx_runtime25 = require("react/jsx-runtime");
2433
+ function MultiSelectPicker({
2434
+ selected,
2435
+ onChange,
2436
+ options = [],
2437
+ isLoading = false,
2438
+ isFetchingNextPage = false,
2439
+ hasNextPage = false,
2440
+ onFetchNextPage,
2441
+ placeholder = "Search or type to add...",
2442
+ onCreateOption,
2443
+ onSearchChange,
2444
+ searchValue
2445
+ }) {
2446
+ const [open, setOpen] = (0, import_react13.useState)(false);
2447
+ const serverSearch = typeof onSearchChange === "function";
2448
+ const [localSearch, setLocalSearch] = (0, import_react13.useState)("");
2449
+ const search = serverSearch ? searchValue ?? "" : localSearch;
2450
+ const setSearch = serverSearch ? onSearchChange : setLocalSearch;
2451
+ const inputRef = (0, import_react13.useRef)(null);
2452
+ const containerRef = (0, import_react13.useRef)(null);
2453
+ (0, import_react13.useEffect)(() => {
2454
+ const handler = (e) => {
2455
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
2456
+ setOpen(false);
2457
+ setSearch("");
2458
+ }
2459
+ };
2460
+ document.addEventListener("mousedown", handler);
2461
+ return () => document.removeEventListener("mousedown", handler);
2462
+ }, [setSearch]);
2463
+ const filtered = options.filter(
2464
+ (opt) => (serverSearch || opt.name.toLowerCase().includes(search.toLowerCase())) && !selected.includes(opt.name)
2465
+ );
2466
+ const showCustomOption = search.trim().length > 0 && !options.some((opt) => opt.name.toLowerCase() === search.trim().toLowerCase()) && !selected.some((s) => s.toLowerCase() === search.trim().toLowerCase());
2467
+ const addItem = (name) => {
2468
+ onChange([...selected, name]);
2469
+ setSearch("");
2470
+ inputRef.current?.focus();
2471
+ };
2472
+ const removeItem = (name) => {
2473
+ onChange(selected.filter((s) => s !== name));
2474
+ };
2475
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { ref: containerRef, className: "relative w-full", children: [
2476
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
2477
+ "div",
2478
+ {
2479
+ onClick: () => {
2480
+ setOpen(true);
2481
+ inputRef.current?.focus();
2482
+ },
2483
+ className: `relative flex flex-wrap items-center gap-1.5 w-full cursor-text min-h-[42px] rounded-lg px-3 py-1.5 border border-input bg-background text-foreground ${open ? "ring-2 ring-ring border-ring" : ""}`,
2484
+ children: [
2485
+ selected.map((name) => /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
2486
+ "span",
2487
+ {
2488
+ className: "inline-flex items-center gap-1 rounded px-2 py-0.5 text-xs font-semibold bg-muted text-foreground border",
2489
+ children: [
2490
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "truncate max-w-[140px]", children: name }),
2491
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
2492
+ "button",
2493
+ {
2494
+ type: "button",
2495
+ onClick: (e) => {
2496
+ e.stopPropagation();
2497
+ removeItem(name);
2498
+ },
2499
+ className: "rounded p-0.5 hover:bg-accent",
2500
+ children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react8.X, { className: "h-3 w-3" })
2501
+ }
2502
+ )
2503
+ ]
2504
+ },
2505
+ name
2506
+ )),
2507
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
2508
+ "input",
2509
+ {
2510
+ ref: inputRef,
2511
+ value: search,
2512
+ onChange: (e) => {
2513
+ setSearch(e.target.value);
2514
+ setOpen(true);
2515
+ },
2516
+ onFocus: () => setOpen(true),
2517
+ placeholder: selected.length === 0 ? placeholder : "",
2518
+ className: "flex-1 min-w-[120px] bg-transparent outline-none text-xs font-medium h-6 text-foreground placeholder:text-muted-foreground"
2519
+ }
2520
+ ),
2521
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
2522
+ import_lucide_react8.ChevronDown,
2523
+ {
2524
+ className: `h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200 ${open ? "rotate-180" : ""}`
2525
+ }
2526
+ )
2527
+ ]
2528
+ }
2529
+ ),
2530
+ open && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "absolute z-50 mt-1 w-full rounded-lg border bg-card text-foreground shadow-lg overflow-hidden p-1.5", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "max-h-60 overflow-y-auto space-y-1", children: isLoading && options.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "p-3 text-center text-xs text-muted-foreground", children: "Loading options\u2026" }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
2531
+ filtered.length === 0 && !showCustomOption && !isLoading && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "p-3 text-center text-xs text-muted-foreground", children: "No options found" }),
2532
+ filtered.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
2533
+ "button",
2534
+ {
2535
+ type: "button",
2536
+ onClick: () => addItem(opt.name),
2537
+ className: "w-full text-left px-3 py-2 rounded-md flex items-center gap-2 hover:bg-muted transition-colors text-xs",
2538
+ children: [
2539
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "h-6 w-6 shrink-0 rounded bg-muted flex items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react8.Hash, { className: "h-3 w-3 text-muted-foreground" }) }),
2540
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "flex-1 min-w-0", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "font-semibold truncate", children: opt.name }) }),
2541
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react8.Plus, { className: "h-3.5 w-3.5 text-primary shrink-0" })
2542
+ ]
2543
+ },
2544
+ opt.id
2545
+ )),
2546
+ hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
2547
+ "button",
2548
+ {
2549
+ type: "button",
2550
+ onClick: () => onFetchNextPage?.(),
2551
+ disabled: isFetchingNextPage,
2552
+ className: "w-full py-1.5 text-center text-xs text-primary font-semibold hover:underline",
2553
+ children: isFetchingNextPage ? "Loading more..." : "Load more..."
2554
+ }
2555
+ ),
2556
+ showCustomOption && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
2557
+ "button",
2558
+ {
2559
+ type: "button",
2560
+ onClick: () => {
2561
+ if (onCreateOption) {
2562
+ onCreateOption(search.trim());
2563
+ } else {
2564
+ addItem(search.trim());
2565
+ }
2566
+ setSearch("");
2567
+ },
2568
+ className: "w-full text-left px-3 py-2 rounded-md flex items-center gap-2 bg-primary/10 text-primary font-bold text-xs hover:bg-primary/20",
2569
+ children: [
2570
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react8.Plus, { className: "h-3.5 w-3.5" }),
2571
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("span", { children: [
2572
+ 'Create "',
2573
+ search.trim(),
2574
+ '"'
2575
+ ] })
2576
+ ]
2577
+ }
2578
+ )
2579
+ ] }) }) })
2580
+ ] });
2581
+ }
2582
+
2583
+ // src/components/shared/ImageEditorModal.tsx
2584
+ var import_react14 = require("react");
2585
+ var import_framer_motion2 = require("framer-motion");
2586
+ var import_lucide_react9 = require("lucide-react");
2587
+ var import_sonner = require("sonner");
2588
+ var import_jsx_runtime26 = require("react/jsx-runtime");
2589
+ var HANDLE_SIZE = 14;
2590
+ var DEFAULT_CROP = { x: 0.05, y: 0.05, w: 0.9, h: 0.9 };
2591
+ var ASPECT_PRESETS = [
2592
+ { label: "Free", ratio: void 0, icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.Move, { className: "w-3.5 h-3.5" }) },
2593
+ { label: "Original", ratio: -1, icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.Image, { className: "w-3.5 h-3.5" }) },
2594
+ { label: "1 : 1", ratio: 1, icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.Square, { className: "w-3.5 h-3.5" }) },
2595
+ { label: "16 : 9", ratio: 16 / 9, icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.Tv, { className: "w-3.5 h-3.5" }) },
2596
+ { label: "9 : 16", ratio: 9 / 16, icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.Smartphone, { className: "w-3.5 h-3.5" }) },
2597
+ { label: "4 : 3", ratio: 4 / 3, icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.RectangleHorizontal, { className: "w-3.5 h-3.5" }) },
2598
+ { label: "3 : 4", ratio: 3 / 4, icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.RectangleVertical, { className: "w-3.5 h-3.5" }) },
2599
+ { label: "4 : 5", ratio: 4 / 5, icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.RectangleVertical, { className: "w-3.5 h-3.5" }) },
2600
+ { label: "3 : 2", ratio: 3 / 2, icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.RectangleHorizontal, { className: "w-3.5 h-3.5" }) },
2601
+ { label: "2 : 3", ratio: 2 / 3, icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.RectangleVertical, { className: "w-3.5 h-3.5" }) }
2602
+ ];
2603
+ var HANDLE_DEFS = [
2604
+ { key: "nw", cursor: "nwse-resize", style: { left: -7, top: -7 } },
2605
+ { key: "ne", cursor: "nesw-resize", style: { right: -7, top: -7 } },
2606
+ { key: "sw", cursor: "nesw-resize", style: { left: -7, bottom: -7 } },
2607
+ { key: "se", cursor: "nwse-resize", style: { right: -7, bottom: -7 } },
2608
+ { key: "n", cursor: "ns-resize", style: { left: "calc(50% - 7px)", top: -7, width: 14 } },
2609
+ { key: "s", cursor: "ns-resize", style: { left: "calc(50% - 7px)", bottom: -7, width: 14 } },
2610
+ { key: "w", cursor: "ew-resize", style: { top: "calc(50% - 7px)", left: -7, height: 14 } },
2611
+ { key: "e", cursor: "ew-resize", style: { top: "calc(50% - 7px)", right: -7, height: 14 } }
2612
+ ];
2613
+ function InlineSlider({
2614
+ value,
2615
+ onValueChange,
2616
+ min,
2617
+ max,
2618
+ step = 1,
2619
+ className
2620
+ }) {
2621
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2622
+ "input",
2623
+ {
2624
+ type: "range",
2625
+ min,
2626
+ max,
2627
+ step,
2628
+ value: value[0],
2629
+ onChange: (e) => onValueChange([parseFloat(e.target.value)]),
2630
+ className: `w-full h-1.5 bg-muted rounded-lg appearance-none cursor-pointer accent-primary ${className ?? ""}`
2631
+ }
2632
+ );
2633
+ }
2634
+ function ImageEditorModal({
2635
+ open,
2636
+ onOpenChange,
2637
+ onImageSelect,
2638
+ aspectRatio: initialAspectRatio,
2639
+ maxFileSize = 10 * 1024 * 1024,
2640
+ acceptedFormats = ["image/jpeg", "image/png", "image/webp"],
2641
+ maxDimension = 4e3,
2642
+ initialFile = null,
2643
+ outputFormat = "image/jpeg",
2644
+ outputQuality = 0.92,
2645
+ minWidth,
2646
+ minHeight
2647
+ }) {
2648
+ const [imageSrc, setImageSrc] = (0, import_react14.useState)(null);
2649
+ const [fileName, setFileName] = (0, import_react14.useState)("");
2650
+ const [sourceDimensions, setSourceDimensions] = (0, import_react14.useState)(null);
2651
+ const [activeTab, setActiveTab] = (0, import_react14.useState)("crop");
2652
+ const [pan, setPan] = (0, import_react14.useState)({ x: 0, y: 0 });
2653
+ const [zoom, setZoom] = (0, import_react14.useState)(1);
2654
+ const [rotation, setRotation] = (0, import_react14.useState)(0);
2655
+ const [flipX, setFlipX] = (0, import_react14.useState)(false);
2656
+ const [flipY, setFlipY] = (0, import_react14.useState)(false);
2657
+ const [brightness, setBrightness] = (0, import_react14.useState)(0);
2658
+ const [contrast, setContrast] = (0, import_react14.useState)(0);
2659
+ const [saturation, setSaturation] = (0, import_react14.useState)(0);
2660
+ const [blur, setBlur] = (0, import_react14.useState)(0);
2661
+ const [activePresetRatio, setActivePresetRatio] = (0, import_react14.useState)(initialAspectRatio);
2662
+ const [cropBox, setCropBox] = (0, import_react14.useState)(DEFAULT_CROP);
2663
+ const [isDraggingBox, setIsDraggingBox] = (0, import_react14.useState)(false);
2664
+ const [isProcessing, setIsProcessing] = (0, import_react14.useState)(false);
2665
+ const [showCamera, setShowCamera] = (0, import_react14.useState)(false);
2666
+ const [isCameraStarting, setIsCameraStarting] = (0, import_react14.useState)(false);
2667
+ const [cameraSupported] = (0, import_react14.useState)(
2668
+ () => typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia
2669
+ );
2670
+ const frameRef = (0, import_react14.useRef)(null);
2671
+ const canvasRef = (0, import_react14.useRef)(null);
2672
+ const fileInputRef = (0, import_react14.useRef)(null);
2673
+ const videoRef = (0, import_react14.useRef)(null);
2674
+ const cameraStreamRef = (0, import_react14.useRef)(null);
2675
+ const loadedInitialFileRef = (0, import_react14.useRef)(null);
2676
+ const dragStartRef = (0, import_react14.useRef)(null);
2677
+ const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
2678
+ const effectiveAspectRatio = (0, import_react14.useMemo)(() => {
2679
+ if (activePresetRatio === -1 && sourceDimensions) {
2680
+ return sourceDimensions.width / sourceDimensions.height;
2681
+ }
2682
+ return activePresetRatio && activePresetRatio > 0 ? activePresetRatio : void 0;
2683
+ }, [activePresetRatio, sourceDimensions]);
2684
+ const isSourceBelowMin = (0, import_react14.useMemo)(() => {
2685
+ if (!sourceDimensions) return false;
2686
+ return minWidth && sourceDimensions.width < minWidth || minHeight && sourceDimensions.height < minHeight;
2687
+ }, [sourceDimensions, minWidth, minHeight]);
2688
+ const filterStyle = (0, import_react14.useMemo)(() => {
2689
+ const b = 100 + brightness;
2690
+ const c = 100 + contrast;
2691
+ const s = 100 + saturation;
2692
+ return `brightness(${b}%) contrast(${c}%) saturate(${s}%) blur(${blur}px)`;
2693
+ }, [brightness, contrast, saturation, blur]);
2694
+ const resetEdits = (0, import_react14.useCallback)(() => {
2695
+ setPan({ x: 0, y: 0 });
2696
+ setZoom(1);
2697
+ setRotation(0);
2698
+ setFlipX(false);
2699
+ setFlipY(false);
2700
+ setBrightness(0);
2701
+ setContrast(0);
2702
+ setSaturation(0);
2703
+ setBlur(0);
2704
+ setCropBox(DEFAULT_CROP);
2705
+ setActivePresetRatio(initialAspectRatio);
2706
+ }, [initialAspectRatio]);
2707
+ const resetModal = (0, import_react14.useCallback)(() => {
2708
+ setImageSrc(null);
2709
+ setSourceDimensions(null);
2710
+ setFileName("");
2711
+ setShowCamera(false);
2712
+ resetEdits();
2713
+ if (fileInputRef.current) fileInputRef.current.value = "";
2714
+ }, [resetEdits]);
2715
+ const stopCamera = (0, import_react14.useCallback)(() => {
2716
+ cameraStreamRef.current?.getTracks().forEach((t) => t.stop());
2717
+ cameraStreamRef.current = null;
2718
+ }, []);
2719
+ const handleClose = (0, import_react14.useCallback)(() => {
2720
+ stopCamera();
2721
+ resetModal();
2722
+ onOpenChange(false);
2723
+ }, [stopCamera, resetModal, onOpenChange]);
2724
+ (0, import_react14.useEffect)(() => {
2725
+ if (!open) return;
2726
+ const onKey = (e) => {
2727
+ if (e.key === "Escape") handleClose();
2728
+ };
2729
+ window.addEventListener("keydown", onKey);
2730
+ return () => window.removeEventListener("keydown", onKey);
2731
+ }, [open, handleClose]);
2732
+ (0, import_react14.useEffect)(() => {
2733
+ document.body.style.overflow = open ? "hidden" : "";
2734
+ return () => {
2735
+ document.body.style.overflow = "";
2736
+ };
2737
+ }, [open]);
2738
+ (0, import_react14.useEffect)(() => {
2739
+ if (!showCamera) return;
2740
+ let cancelled = false;
2741
+ (async () => {
2742
+ setIsCameraStarting(true);
2743
+ try {
2744
+ const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "user" }, audio: false });
2745
+ if (cancelled) {
2746
+ stream.getTracks().forEach((t) => t.stop());
2747
+ return;
2748
+ }
2749
+ cameraStreamRef.current = stream;
2750
+ if (videoRef.current) {
2751
+ videoRef.current.srcObject = stream;
2752
+ await videoRef.current.play();
2753
+ }
2754
+ } catch (err) {
2755
+ if (!cancelled) {
2756
+ import_sonner.toast.error("Camera access denied", {
2757
+ description: err instanceof Error ? err.message : "Grant camera permission in your browser settings."
2758
+ });
2759
+ setShowCamera(false);
2760
+ }
2761
+ } finally {
2762
+ if (!cancelled) setIsCameraStarting(false);
2763
+ }
2764
+ })();
2765
+ return () => {
2766
+ cancelled = true;
2767
+ };
2768
+ }, [showCamera]);
2769
+ (0, import_react14.useEffect)(() => () => {
2770
+ cameraStreamRef.current?.getTracks().forEach((t) => t.stop());
2771
+ }, []);
2772
+ const loadFile = (0, import_react14.useCallback)(
2773
+ (file) => {
2774
+ if (file.size > maxFileSize) {
2775
+ import_sonner.toast.error("File too large", { description: `Maximum file size is ${(maxFileSize / 1024 / 1024).toFixed(1)} MB.` });
2776
+ return;
2777
+ }
2778
+ if (!acceptedFormats.includes(file.type)) {
2779
+ import_sonner.toast.error("Unsupported file format", {
2780
+ description: `Accepted formats: ${acceptedFormats.map((f) => f.split("/")[1]?.toUpperCase()).join(", ")}`
2781
+ });
2782
+ return;
2783
+ }
2784
+ if (file.type === "image/svg+xml") {
2785
+ onImageSelect(file, file.name);
2786
+ onOpenChange(false);
2787
+ import_sonner.toast.success("Image selected");
2788
+ return;
2789
+ }
2790
+ setFileName(file.name);
2791
+ const reader = new FileReader();
2792
+ reader.onload = (e) => {
2793
+ const dataUrl = e.target?.result;
2794
+ const img = new Image();
2795
+ img.onload = () => {
2796
+ setSourceDimensions({ width: img.naturalWidth, height: img.naturalHeight });
2797
+ setImageSrc(dataUrl);
2798
+ setCropBox(DEFAULT_CROP);
2799
+ setPan({ x: 0, y: 0 });
2800
+ setZoom(1);
2801
+ if (minWidth && img.naturalWidth < minWidth || minHeight && img.naturalHeight < minHeight) {
2802
+ import_sonner.toast.info("Low-resolution image", {
2803
+ description: `Source is ${img.naturalWidth}\xD7${img.naturalHeight}px. It will be upscaled to meet the ${minWidth ?? img.naturalWidth}\xD7${minHeight ?? img.naturalHeight}px target.`
2804
+ });
2805
+ }
2806
+ };
2807
+ img.src = dataUrl;
2808
+ };
2809
+ reader.readAsDataURL(file);
2810
+ },
2811
+ [maxFileSize, acceptedFormats, minWidth, minHeight, onImageSelect, onOpenChange]
2812
+ );
2813
+ const handleFileChange = (0, import_react14.useCallback)(
2814
+ (e) => {
2815
+ const file = e.target.files?.[0];
2816
+ if (file) loadFile(file);
2817
+ },
2818
+ [loadFile]
2819
+ );
2820
+ const capturePhoto = (0, import_react14.useCallback)(() => {
2821
+ const video = videoRef.current;
2822
+ if (!video || video.readyState < 2) return;
2823
+ const canvas = document.createElement("canvas");
2824
+ canvas.width = video.videoWidth;
2825
+ canvas.height = video.videoHeight;
2826
+ const ctx = canvas.getContext("2d");
2827
+ if (!ctx) return;
2828
+ ctx.drawImage(video, 0, 0);
2829
+ canvas.toBlob(
2830
+ (blob) => {
2831
+ if (!blob) return;
2832
+ const file = new File([blob], `capture-${Date.now()}.jpg`, { type: "image/jpeg" });
2833
+ stopCamera();
2834
+ setShowCamera(false);
2835
+ loadFile(file);
2836
+ },
2837
+ "image/jpeg",
2838
+ 0.92
2839
+ );
2840
+ }, [loadFile, stopCamera]);
2841
+ (0, import_react14.useEffect)(() => {
2842
+ if (open && initialFile && loadedInitialFileRef.current !== initialFile) {
2843
+ loadedInitialFileRef.current = initialFile;
2844
+ loadFile(initialFile);
2845
+ }
2846
+ if (!open) loadedInitialFileRef.current = null;
2847
+ }, [open, initialFile, loadFile]);
2848
+ const applyAspectRatioToCropBox = (0, import_react14.useCallback)(
2849
+ (ratio) => {
2850
+ if (!ratio || ratio <= 0 || !frameRef.current) return;
2851
+ const rect = frameRef.current.getBoundingClientRect();
2852
+ if (rect.width <= 0 || rect.height <= 0) return;
2853
+ const vpAR = rect.width / rect.height;
2854
+ const targetFractionAR = ratio / vpAR;
2855
+ setCropBox((cur) => {
2856
+ let { x, y, w, h } = cur;
2857
+ const cx = x + w / 2;
2858
+ const cy = y + h / 2;
2859
+ if (w / h > targetFractionAR) {
2860
+ w = h * targetFractionAR;
2861
+ } else {
2862
+ h = w / targetFractionAR;
2863
+ }
2864
+ w = clamp(w, 0.1, 1);
2865
+ h = clamp(h, 0.1, 1);
2866
+ x = clamp(cx - w / 2, 0, 1 - w);
2867
+ y = clamp(cy - h / 2, 0, 1 - h);
2868
+ return { x, y, w, h };
2869
+ });
2870
+ },
2871
+ []
2872
+ );
2873
+ const handleSelectPreset = (0, import_react14.useCallback)(
2874
+ (preset) => {
2875
+ setActivePresetRatio(preset.ratio);
2876
+ if (preset.ratio === -1 && sourceDimensions) {
2877
+ applyAspectRatioToCropBox(sourceDimensions.width / sourceDimensions.height);
2878
+ } else {
2879
+ applyAspectRatioToCropBox(preset.ratio);
2880
+ }
2881
+ },
2882
+ [sourceDimensions, applyAspectRatioToCropBox]
2883
+ );
2884
+ const startDrag = (0, import_react14.useCallback)(
2885
+ (e, handle) => {
2886
+ e.preventDefault();
2887
+ e.stopPropagation();
2888
+ if (!frameRef.current) return;
2889
+ const frameRect = frameRef.current.getBoundingClientRect();
2890
+ setIsDraggingBox(true);
2891
+ dragStartRef.current = {
2892
+ handle,
2893
+ startBox: { ...cropBox },
2894
+ startMousePx: { x: e.clientX, y: e.clientY },
2895
+ frameRect
2896
+ };
2897
+ const onMove = (me) => {
2898
+ const ref = dragStartRef.current;
2899
+ if (!ref) return;
2900
+ const { handle: h, startBox: sb, startMousePx: sm, frameRect: r } = ref;
2901
+ const dx = (me.clientX - sm.x) / r.width;
2902
+ const dy = (me.clientY - sm.y) / r.height;
2903
+ setCropBox(() => {
2904
+ let { x, y, w, h: _h } = { ...sb };
2905
+ if (h === "body") {
2906
+ x = clamp(x + dx, 0, 1 - w);
2907
+ y = clamp(y + dy, 0, 1 - _h);
2908
+ } else {
2909
+ let x1 = x, y1 = y, x2 = x + w, y2 = y + _h;
2910
+ if (h === "nw" || h === "w" || h === "sw") x1 = clamp(x + dx, 0, x2 - 0.05);
2911
+ if (h === "ne" || h === "e" || h === "se") x2 = clamp(x + w + dx, x1 + 0.05, 1);
2912
+ if (h === "nw" || h === "n" || h === "ne") y1 = clamp(y + dy, 0, y2 - 0.05);
2913
+ if (h === "sw" || h === "s" || h === "se") y2 = clamp(y + _h + dy, y1 + 0.05, 1);
2914
+ w = x2 - x1;
2915
+ _h = y2 - y1;
2916
+ if (effectiveAspectRatio && effectiveAspectRatio > 0) {
2917
+ const vpAR = r.width / r.height;
2918
+ const tAR = effectiveAspectRatio / vpAR;
2919
+ if (h === "n" || h === "s") {
2920
+ w = clamp(_h * tAR, 0.05, 1 - x1);
2921
+ x2 = x1 + w;
2922
+ } else {
2923
+ _h = clamp(w / tAR, 0.05, 1 - y1);
2924
+ y2 = y1 + _h;
2925
+ }
2926
+ }
2927
+ x = clamp(x1, 0, 1);
2928
+ y = clamp(y1, 0, 1);
2929
+ w = clamp(x2 - x, 0.05, 1 - x);
2930
+ _h = clamp(y2 - y, 0.05, 1 - y);
2931
+ }
2932
+ return { x, y, w, h: _h };
2933
+ });
2934
+ };
2935
+ const onUp = () => {
2936
+ setIsDraggingBox(false);
2937
+ dragStartRef.current = null;
2938
+ window.removeEventListener("pointermove", onMove);
2939
+ window.removeEventListener("pointerup", onUp);
2940
+ window.removeEventListener("pointercancel", onUp);
2941
+ };
2942
+ window.addEventListener("pointermove", onMove);
2943
+ window.addEventListener("pointerup", onUp);
2944
+ window.addEventListener("pointercancel", onUp);
2945
+ },
2946
+ [cropBox, effectiveAspectRatio]
2947
+ );
2948
+ const handleFramePointerDown = (0, import_react14.useCallback)(
2949
+ (e) => {
2950
+ if (e.target.closest("[data-crop-handle]")) return;
2951
+ e.preventDefault();
2952
+ const sx = e.clientX, sy = e.clientY;
2953
+ const spx = pan.x, spy = pan.y;
2954
+ const onMove = (me) => setPan({ x: spx + me.clientX - sx, y: spy + me.clientY - sy });
2955
+ const onUp = () => {
2956
+ window.removeEventListener("pointermove", onMove);
2957
+ window.removeEventListener("pointerup", onUp);
2958
+ };
2959
+ window.addEventListener("pointermove", onMove);
2960
+ window.addEventListener("pointerup", onUp);
2961
+ },
2962
+ [pan]
2963
+ );
2964
+ const handleApply = (0, import_react14.useCallback)(async () => {
2965
+ if (!imageSrc || !canvasRef.current || !frameRef.current) return;
2966
+ setIsProcessing(true);
2967
+ try {
2968
+ const image = new Image();
2969
+ image.src = imageSrc;
2970
+ await new Promise((res, rej) => {
2971
+ image.onload = () => res();
2972
+ image.onerror = () => rej(new Error("Image failed to load"));
2973
+ });
2974
+ const origW = image.naturalWidth;
2975
+ const origH = image.naturalHeight;
2976
+ const rad = rotation * Math.PI / 180;
2977
+ const absSin = Math.abs(Math.sin(rad));
2978
+ const absCos = Math.abs(Math.cos(rad));
2979
+ const rotW = origW * absCos + origH * absSin;
2980
+ const rotH = origW * absSin + origH * absCos;
2981
+ const offscreen = document.createElement("canvas");
2982
+ offscreen.width = Math.ceil(rotW);
2983
+ offscreen.height = Math.ceil(rotH);
2984
+ const offCtx = offscreen.getContext("2d");
2985
+ offCtx.imageSmoothingEnabled = true;
2986
+ offCtx.imageSmoothingQuality = "high";
2987
+ offCtx.filter = filterStyle;
2988
+ offCtx.translate(rotW / 2, rotH / 2);
2989
+ offCtx.rotate(rad);
2990
+ offCtx.scale(flipX ? -1 : 1, flipY ? -1 : 1);
2991
+ offCtx.drawImage(image, -origW / 2, -origH / 2);
2992
+ const vpRect = frameRef.current.getBoundingClientRect();
2993
+ const vpW = vpRect.width, vpH = vpRect.height;
2994
+ const baseScale = Math.min(vpW / rotW, vpH / rotH);
2995
+ const effectiveScale = baseScale * zoom;
2996
+ const displayedW = rotW * effectiveScale;
2997
+ const displayedH = rotH * effectiveScale;
2998
+ const marginX = (vpW - displayedW) / 2 + pan.x;
2999
+ const marginY = (vpH - displayedH) / 2 + pan.y;
3000
+ const cropPxX = cropBox.x * vpW, cropPxY = cropBox.y * vpH;
3001
+ const cropPxW = cropBox.w * vpW, cropPxH = cropBox.h * vpH;
3002
+ const sx = clamp((cropPxX - marginX) / effectiveScale, 0, rotW);
3003
+ const sy = clamp((cropPxY - marginY) / effectiveScale, 0, rotH);
3004
+ const sx2 = clamp((cropPxX + cropPxW - marginX) / effectiveScale, 0, rotW);
3005
+ const sy2 = clamp((cropPxY + cropPxH - marginY) / effectiveScale, 0, rotH);
3006
+ const sw = Math.max(1, sx2 - sx);
3007
+ const sh = Math.max(1, sy2 - sy);
3008
+ let outW = sw, outH = sh;
3009
+ const maxSide = Math.max(outW, outH);
3010
+ if (maxDimension && maxSide > maxDimension) {
3011
+ const ds = maxDimension / maxSide;
3012
+ outW *= ds;
3013
+ outH *= ds;
3014
+ }
3015
+ if (minWidth && outW < minWidth) {
3016
+ const us = minWidth / outW;
3017
+ outW = minWidth;
3018
+ outH *= us;
3019
+ }
3020
+ if (minHeight && outH < minHeight) {
3021
+ const us = minHeight / outH;
3022
+ outH = minHeight;
3023
+ outW *= us;
3024
+ }
3025
+ outW = Math.round(Math.max(1, outW));
3026
+ outH = Math.round(Math.max(1, outH));
3027
+ const outputCanvas = canvasRef.current;
3028
+ outputCanvas.width = outW;
3029
+ outputCanvas.height = outH;
3030
+ const outCtx = outputCanvas.getContext("2d");
3031
+ outCtx.imageSmoothingEnabled = true;
3032
+ outCtx.imageSmoothingQuality = "high";
3033
+ if (outputFormat === "image/jpeg") {
3034
+ outCtx.fillStyle = "#fff";
3035
+ outCtx.fillRect(0, 0, outW, outH);
3036
+ }
3037
+ outCtx.drawImage(offscreen, sx, sy, sw, sh, 0, 0, outW, outH);
3038
+ outputCanvas.toBlob(
3039
+ (blob) => {
3040
+ if (!blob) {
3041
+ import_sonner.toast.error("Failed to process image. Please try again.");
3042
+ return;
3043
+ }
3044
+ const ext = outputFormat === "image/png" ? "png" : "jpg";
3045
+ const baseName = fileName.replace(/\.[^./]+$/, "") || "edited-image";
3046
+ onImageSelect(blob, `${baseName}.${ext}`);
3047
+ resetModal();
3048
+ onOpenChange(false);
3049
+ import_sonner.toast.success("Image updated successfully");
3050
+ },
3051
+ outputFormat,
3052
+ outputQuality
3053
+ );
3054
+ } catch (err) {
3055
+ import_sonner.toast.error("Failed to process image", {
3056
+ description: err instanceof Error ? err.message : "Please try again."
3057
+ });
3058
+ } finally {
3059
+ setIsProcessing(false);
3060
+ }
3061
+ }, [
3062
+ imageSrc,
3063
+ rotation,
3064
+ flipX,
3065
+ flipY,
3066
+ zoom,
3067
+ pan,
3068
+ cropBox,
3069
+ fileName,
3070
+ filterStyle,
3071
+ maxDimension,
3072
+ minWidth,
3073
+ minHeight,
3074
+ outputFormat,
3075
+ outputQuality,
3076
+ onImageSelect,
3077
+ onOpenChange,
3078
+ resetModal
3079
+ ]);
3080
+ if (!open) return null;
3081
+ const btnBase = "inline-flex items-center justify-center rounded-md text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50";
3082
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_framer_motion2.AnimatePresence, { children: open && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "fixed inset-0 z-50 select-none flex", children: [
3083
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "absolute inset-0 bg-black/70 backdrop-blur-sm", onClick: handleClose }),
3084
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
3085
+ import_framer_motion2.motion.div,
3086
+ {
3087
+ initial: { opacity: 0, scale: 0.98 },
3088
+ animate: { opacity: 1, scale: 1 },
3089
+ exit: { opacity: 0, scale: 0.98 },
3090
+ transition: { duration: 0.15, ease: "easeOut" },
3091
+ className: "relative z-10 flex flex-col w-full h-full bg-background text-foreground overflow-hidden",
3092
+ role: "dialog",
3093
+ "aria-modal": "true",
3094
+ "aria-label": "Studio Image Editor",
3095
+ children: [
3096
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex items-center gap-2 px-3 sm:px-4 h-11 border-b border-border bg-card shrink-0", children: [
3097
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex items-center gap-2 min-w-0 flex-1", children: [
3098
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.Wand2, { className: "w-4 h-4 text-primary shrink-0" }),
3099
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "text-sm font-semibold text-foreground leading-none truncate", children: "Studio Editor" }),
3100
+ sourceDimensions && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("span", { className: "text-[11px] text-muted-foreground hidden sm:inline truncate", children: [
3101
+ "\xB7 ",
3102
+ sourceDimensions.width,
3103
+ "\xD7",
3104
+ sourceDimensions.height,
3105
+ "px",
3106
+ minWidth && minHeight ? ` (min ${minWidth}\xD7${minHeight})` : ""
3107
+ ] }),
3108
+ isSourceBelowMin && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("span", { className: "hidden sm:inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded-full bg-amber-500/15 text-amber-600 dark:text-amber-400 border border-amber-500/30 shrink-0", children: [
3109
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.Info, { className: "w-3 h-3" }),
3110
+ "Upscale"
3111
+ ] })
3112
+ ] }),
3113
+ imageSrc && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex items-center bg-muted rounded-md p-0.5 gap-0.5 shrink-0", children: [
3114
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
3115
+ "button",
3116
+ {
3117
+ type: "button",
3118
+ id: "editor-tab-crop",
3119
+ onClick: () => setActiveTab("crop"),
3120
+ className: [
3121
+ "flex items-center gap-1 px-2.5 py-1 rounded text-xs font-medium transition-all",
3122
+ activeTab === "crop" ? "bg-background text-foreground shadow-sm font-semibold" : "text-muted-foreground hover:text-foreground"
3123
+ ].join(" "),
3124
+ children: [
3125
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.Crop, { className: "w-3 h-3 text-primary" }),
3126
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { children: "Crop" })
3127
+ ]
3128
+ }
3129
+ ),
3130
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
3131
+ "button",
3132
+ {
3133
+ type: "button",
3134
+ id: "editor-tab-adjust",
3135
+ onClick: () => setActiveTab("adjust"),
3136
+ className: [
3137
+ "flex items-center gap-1 px-2.5 py-1 rounded text-xs font-medium transition-all",
3138
+ activeTab === "adjust" ? "bg-background text-foreground shadow-sm font-semibold" : "text-muted-foreground hover:text-foreground"
3139
+ ].join(" "),
3140
+ children: [
3141
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.Sliders, { className: "w-3 h-3 text-secondary" }),
3142
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { children: "Adjust" })
3143
+ ]
3144
+ }
3145
+ )
3146
+ ] }),
3147
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex items-center gap-1 shrink-0", children: [
3148
+ imageSrc && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
3149
+ "button",
3150
+ {
3151
+ type: "button",
3152
+ onClick: resetEdits,
3153
+ className: `${btnBase} h-7 text-xs px-2.5 text-muted-foreground hover:text-foreground hover:bg-muted border border-transparent rounded-md`,
3154
+ children: [
3155
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.RotateCcw, { className: "w-3 h-3 sm:mr-1" }),
3156
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "hidden sm:inline", children: "Reset" })
3157
+ ]
3158
+ }
3159
+ ),
3160
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
3161
+ "button",
3162
+ {
3163
+ type: "button",
3164
+ onClick: handleClose,
3165
+ id: "editor-close-btn",
3166
+ className: `${btnBase} h-7 w-7 rounded-full text-muted-foreground hover:text-foreground hover:bg-muted`,
3167
+ "aria-label": "Close editor",
3168
+ children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.X, { className: "w-3.5 h-3.5" })
3169
+ }
3170
+ )
3171
+ ] })
3172
+ ] }),
3173
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "flex-1 flex flex-col overflow-hidden min-h-0", children: !imageSrc ? (
3174
+ /* ── Upload / Camera screen ─────────────────────────── */
3175
+ showCamera ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "flex-1 flex flex-col items-center justify-center p-6 bg-background", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "w-full max-w-sm space-y-4", children: [
3176
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "relative aspect-video rounded-xl overflow-hidden border border-border bg-muted flex items-center justify-center", children: [
3177
+ isCameraStarting && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "absolute inset-0 flex items-center justify-center bg-background/80 z-10", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.Loader2, { className: "w-6 h-6 animate-spin text-primary" }) }),
3178
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("video", { ref: videoRef, autoPlay: true, playsInline: true, muted: true, className: "w-full h-full object-cover" })
3179
+ ] }),
3180
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex gap-3", children: [
3181
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
3182
+ "button",
3183
+ {
3184
+ type: "button",
3185
+ onClick: () => {
3186
+ stopCamera();
3187
+ setShowCamera(false);
3188
+ },
3189
+ className: `${btnBase} flex-1 bg-muted border border-border text-foreground hover:bg-muted/80 h-9 px-4 rounded-lg`,
3190
+ children: [
3191
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.X, { className: "w-4 h-4 mr-1.5" }),
3192
+ " Cancel"
3193
+ ]
3194
+ }
3195
+ ),
3196
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
3197
+ "button",
3198
+ {
3199
+ type: "button",
3200
+ onClick: capturePhoto,
3201
+ disabled: isCameraStarting,
3202
+ id: "camera-capture-btn",
3203
+ className: `${btnBase} flex-1 bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 rounded-lg font-medium`,
3204
+ children: [
3205
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.Camera, { className: "w-4 h-4 mr-1.5" }),
3206
+ " Capture"
3207
+ ]
3208
+ }
3209
+ )
3210
+ ] })
3211
+ ] }) }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "flex-1 flex flex-col items-center justify-center p-6 bg-background", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "w-full max-w-sm border-2 border-dashed border-border hover:border-primary/50 transition-colors rounded-2xl p-8 text-center space-y-5 bg-card/60", children: [
3212
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "w-14 h-14 bg-primary/10 text-primary rounded-2xl flex items-center justify-center mx-auto border border-primary/20", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.Upload, { className: "w-7 h-7" }) }),
3213
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "space-y-1", children: [
3214
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { className: "text-base font-semibold text-foreground", children: "Select an Image" }),
3215
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("p", { className: "text-xs text-muted-foreground", children: [
3216
+ "JPEG, PNG or WebP \xB7 up to ",
3217
+ (maxFileSize / 1024 / 1024).toFixed(0),
3218
+ " MB"
3219
+ ] })
3220
+ ] }),
3221
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex flex-wrap items-center justify-center gap-3", children: [
3222
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
3223
+ "button",
3224
+ {
3225
+ type: "button",
3226
+ id: "editor-choose-file-btn",
3227
+ onClick: () => fileInputRef.current?.click(),
3228
+ className: `${btnBase} bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-6 rounded-xl font-medium shadow-md`,
3229
+ children: "Choose File"
3230
+ }
3231
+ ),
3232
+ cameraSupported && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
3233
+ "button",
3234
+ {
3235
+ type: "button",
3236
+ id: "editor-camera-btn",
3237
+ onClick: () => setShowCamera(true),
3238
+ className: `${btnBase} bg-card border border-border text-foreground hover:bg-muted h-9 px-5 rounded-xl font-medium`,
3239
+ children: [
3240
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.Camera, { className: "w-4 h-4 mr-1.5" }),
3241
+ " Camera"
3242
+ ]
3243
+ }
3244
+ )
3245
+ ] }),
3246
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("input", { ref: fileInputRef, type: "file", className: "hidden", accept: acceptedFormats.join(","), onChange: handleFileChange })
3247
+ ] }) })
3248
+ ) : (
3249
+ /* ── Editing workspace ──────────────────────────────── */
3250
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
3251
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex-1 relative bg-muted/30 overflow-hidden select-none min-h-0", children: [
3252
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
3253
+ "div",
3254
+ {
3255
+ className: "absolute inset-0 pointer-events-none opacity-[0.15]",
3256
+ style: {
3257
+ backgroundImage: "radial-gradient(circle, hsl(var(--muted-foreground)/0.6) 1px, transparent 1px)",
3258
+ backgroundSize: "20px 20px"
3259
+ }
3260
+ }
3261
+ ),
3262
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "absolute top-2 left-2 z-30 flex items-center bg-card/90 border border-border rounded-md shadow-sm backdrop-blur-sm", children: [
3263
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
3264
+ "button",
3265
+ {
3266
+ type: "button",
3267
+ id: "editor-zoom-out",
3268
+ onClick: () => setZoom((z) => Math.max(0.2, z - 0.15)),
3269
+ className: "h-6 w-6 flex items-center justify-center text-muted-foreground hover:text-foreground transition-colors",
3270
+ title: "Zoom out",
3271
+ children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.ZoomOut, { className: "w-3 h-3" })
3272
+ }
3273
+ ),
3274
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("span", { className: "w-9 text-center font-mono text-[10px] text-foreground font-medium", children: [
3275
+ Math.round(zoom * 100),
3276
+ "%"
3277
+ ] }),
3278
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
3279
+ "button",
3280
+ {
3281
+ type: "button",
3282
+ id: "editor-zoom-in",
3283
+ onClick: () => setZoom((z) => Math.min(5, z + 0.15)),
3284
+ className: "h-6 w-6 flex items-center justify-center text-muted-foreground hover:text-foreground transition-colors",
3285
+ title: "Zoom in",
3286
+ children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.ZoomIn, { className: "w-3 h-3" })
3287
+ }
3288
+ ),
3289
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "h-3 w-px bg-border mx-0.5" }),
3290
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
3291
+ "button",
3292
+ {
3293
+ type: "button",
3294
+ id: "editor-fit-btn",
3295
+ onClick: () => {
3296
+ setZoom(1);
3297
+ setPan({ x: 0, y: 0 });
3298
+ },
3299
+ className: "h-6 w-6 flex items-center justify-center text-muted-foreground hover:text-foreground transition-colors",
3300
+ title: "Fit to view",
3301
+ children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.Maximize2, { className: "w-3 h-3" })
3302
+ }
3303
+ )
3304
+ ] }),
3305
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
3306
+ "div",
3307
+ {
3308
+ ref: frameRef,
3309
+ onPointerDown: handleFramePointerDown,
3310
+ className: "absolute inset-3 touch-none cursor-grab active:cursor-grabbing",
3311
+ children: [
3312
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
3313
+ "img",
3314
+ {
3315
+ src: imageSrc,
3316
+ alt: "Editor preview",
3317
+ draggable: false,
3318
+ className: "max-w-full max-h-full object-contain pointer-events-none select-none block",
3319
+ style: {
3320
+ transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom * (flipX ? -1 : 1)}, ${zoom * (flipY ? -1 : 1)}) rotate(${rotation}deg)`,
3321
+ filter: filterStyle,
3322
+ transition: isDraggingBox ? "none" : "transform 0.1s ease-out"
3323
+ }
3324
+ }
3325
+ ) }),
3326
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "absolute inset-0 pointer-events-none z-10", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
3327
+ "div",
3328
+ {
3329
+ className: "absolute",
3330
+ style: {
3331
+ left: `${cropBox.x * 100}%`,
3332
+ top: `${cropBox.y * 100}%`,
3333
+ width: `${cropBox.w * 100}%`,
3334
+ height: `${cropBox.h * 100}%`,
3335
+ boxShadow: "0 0 0 9999px rgba(0,0,0,0.55)"
3336
+ },
3337
+ children: [
3338
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
3339
+ "div",
3340
+ {
3341
+ "data-crop-handle": "body",
3342
+ onPointerDown: (e) => startDrag(e, "body"),
3343
+ className: "absolute inset-0 border-2 border-white/90 cursor-move pointer-events-auto group",
3344
+ children: [
3345
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: `absolute inset-0 grid grid-cols-3 grid-rows-3 transition-opacity ${isDraggingBox ? "opacity-50" : "opacity-0 group-hover:opacity-30"}`, children: Array.from({ length: 9 }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: [i % 3 !== 2 ? "border-r border-white/50" : "", i < 6 ? "border-b border-white/50" : ""].join(" ") }, i)) }),
3346
+ [
3347
+ "absolute -top-px -left-px border-t-2 border-l-2",
3348
+ "absolute -top-px -right-px border-t-2 border-r-2",
3349
+ "absolute -bottom-px -left-px border-b-2 border-l-2",
3350
+ "absolute -bottom-px -right-px border-b-2 border-r-2"
3351
+ ].map((cls, i) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: `${cls} w-3.5 h-3.5 border-white pointer-events-none` }, i))
3352
+ ]
3353
+ }
3354
+ ),
3355
+ HANDLE_DEFS.map(({ key, cursor, style }) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
3356
+ "div",
3357
+ {
3358
+ "data-crop-handle": key,
3359
+ onPointerDown: (e) => key && startDrag(e, key),
3360
+ className: "absolute bg-white border border-black/30 rounded-sm shadow-sm pointer-events-auto z-20 hover:scale-110 transition-transform",
3361
+ style: { width: HANDLE_SIZE, height: HANDLE_SIZE, cursor, ...style }
3362
+ },
3363
+ key
3364
+ ))
3365
+ ]
3366
+ }
3367
+ ) })
3368
+ ]
3369
+ }
3370
+ )
3371
+ ] }),
3372
+ activeTab === "crop" ? /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "bg-card border-t border-border shrink-0", children: [
3373
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex items-center gap-2 px-3 sm:px-4 h-10 border-b border-border/50", children: [
3374
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "text-[10px] font-semibold text-muted-foreground uppercase tracking-wider shrink-0", children: "Rotate" }),
3375
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "flex-1 min-w-0", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
3376
+ InlineSlider,
3377
+ {
3378
+ value: [rotation],
3379
+ onValueChange: ([v]) => setRotation(v),
3380
+ min: -180,
3381
+ max: 180,
3382
+ step: 1
3383
+ }
3384
+ ) }),
3385
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("span", { className: "font-mono text-[11px] text-foreground w-8 text-right shrink-0", children: [
3386
+ rotation,
3387
+ "\xB0"
3388
+ ] }),
3389
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "h-4 w-px bg-border mx-1 shrink-0" }),
3390
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
3391
+ "button",
3392
+ {
3393
+ type: "button",
3394
+ id: "editor-rotate-ccw",
3395
+ onClick: () => setRotation((r) => r - 90),
3396
+ title: "Rotate \u221290\xB0",
3397
+ className: "h-7 px-2 flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground border border-border rounded hover:bg-muted transition-colors shrink-0",
3398
+ children: [
3399
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.RotateCcw, { className: "w-3 h-3" }),
3400
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "hidden sm:inline", children: "\u221290\xB0" })
3401
+ ]
3402
+ }
3403
+ ),
3404
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
3405
+ "button",
3406
+ {
3407
+ type: "button",
3408
+ id: "editor-rotate-cw",
3409
+ onClick: () => setRotation((r) => r + 90),
3410
+ title: "Rotate +90\xB0",
3411
+ className: "h-7 px-2 flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground border border-border rounded hover:bg-muted transition-colors shrink-0",
3412
+ children: [
3413
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.RotateCw, { className: "w-3 h-3" }),
3414
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "hidden sm:inline", children: "+90\xB0" })
3415
+ ]
3416
+ }
3417
+ ),
3418
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "h-4 w-px bg-border mx-0.5 shrink-0" }),
3419
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
3420
+ "button",
3421
+ {
3422
+ type: "button",
3423
+ id: "editor-flip-h",
3424
+ onClick: () => setFlipX((f) => !f),
3425
+ title: "Flip Horizontal",
3426
+ className: ["h-7 w-7 flex items-center justify-center rounded border transition-colors shrink-0", flipX ? "bg-primary/10 text-primary border-primary/40" : "border-border text-muted-foreground hover:text-foreground hover:bg-muted"].join(" "),
3427
+ children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.FlipHorizontal, { className: "w-3.5 h-3.5" })
3428
+ }
3429
+ ),
3430
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
3431
+ "button",
3432
+ {
3433
+ type: "button",
3434
+ id: "editor-flip-v",
3435
+ onClick: () => setFlipY((f) => !f),
3436
+ title: "Flip Vertical",
3437
+ className: ["h-7 w-7 flex items-center justify-center rounded border transition-colors shrink-0", flipY ? "bg-primary/10 text-primary border-primary/40" : "border-border text-muted-foreground hover:text-foreground hover:bg-muted"].join(" "),
3438
+ children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.FlipVertical, { className: "w-3.5 h-3.5" })
3439
+ }
3440
+ )
3441
+ ] }),
3442
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "flex items-center gap-1.5 px-3 sm:px-4 h-9 overflow-x-auto scrollbar-hide", children: ASPECT_PRESETS.map((preset) => {
3443
+ const active = activePresetRatio === preset.ratio;
3444
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
3445
+ "button",
3446
+ {
3447
+ type: "button",
3448
+ onClick: () => handleSelectPreset(preset),
3449
+ className: [
3450
+ "flex items-center gap-1 px-2.5 py-1 rounded text-[11px] font-medium border shrink-0 transition-all whitespace-nowrap",
3451
+ active ? "bg-primary/10 border-primary/50 text-primary font-semibold" : "border-border text-muted-foreground hover:text-foreground hover:bg-muted"
3452
+ ].join(" "),
3453
+ children: [
3454
+ preset.icon,
3455
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { children: preset.label })
3456
+ ]
3457
+ },
3458
+ preset.label
3459
+ );
3460
+ }) })
3461
+ ] }) : (
3462
+ /* Adjustments panel — compact 2-col grid */
3463
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "bg-card border-t border-border px-4 py-3 shrink-0", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-3 max-w-4xl mx-auto", children: [
3464
+ { label: "Brightness", icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.Sun, { className: "w-3 h-3 text-amber-500" }), value: brightness, set: setBrightness, min: -100, max: 100, unit: "%" },
3465
+ { label: "Contrast", icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.Contrast, { className: "w-3 h-3 text-blue-500" }), value: contrast, set: setContrast, min: -100, max: 100, unit: "%" },
3466
+ { label: "Saturation", icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.Sparkles, { className: "w-3 h-3 text-violet-500" }), value: saturation, set: setSaturation, min: -100, max: 100, unit: "%" },
3467
+ { label: "Blur", icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.Sliders, { className: "w-3 h-3 text-emerald-500" }), value: blur, set: setBlur, min: 0, max: 10, step: 0.5, unit: "px" }
3468
+ ].map(({ label, icon, value, set, min, max, step = 1, unit }) => /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex items-center gap-2", children: [
3469
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("span", { className: "flex items-center gap-1 text-[11px] font-medium text-foreground w-20 shrink-0", children: [
3470
+ icon,
3471
+ label
3472
+ ] }),
3473
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "flex-1 min-w-0", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(InlineSlider, { value: [value], onValueChange: ([v]) => set(v), min, max, step }) }),
3474
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("span", { className: "font-mono text-[11px] text-muted-foreground w-9 text-right shrink-0", children: [
3475
+ value,
3476
+ unit
3477
+ ] })
3478
+ ] }, label)) }) })
3479
+ ),
3480
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex items-center justify-between gap-3 px-3 sm:px-4 h-10 border-t border-border bg-card shrink-0", children: [
3481
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
3482
+ "button",
3483
+ {
3484
+ type: "button",
3485
+ id: "editor-cancel-btn",
3486
+ onClick: resetModal,
3487
+ className: `${btnBase} flex items-center gap-1.5 h-7 px-3 text-xs text-muted-foreground hover:text-foreground border border-border rounded hover:bg-muted transition-colors`,
3488
+ children: [
3489
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.X, { className: "w-3 h-3" }),
3490
+ "Cancel"
3491
+ ]
3492
+ }
3493
+ ),
3494
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
3495
+ "button",
3496
+ {
3497
+ type: "button",
3498
+ id: "editor-apply-btn",
3499
+ onClick: handleApply,
3500
+ disabled: isProcessing,
3501
+ className: `${btnBase} flex items-center gap-1.5 h-7 px-5 text-xs font-semibold bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors disabled:opacity-60`,
3502
+ children: [
3503
+ isProcessing ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.Loader2, { className: "w-3 h-3 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react9.Check, { className: "w-3 h-3" }),
3504
+ isProcessing ? "Processing\u2026" : "Apply Edits"
3505
+ ]
3506
+ }
3507
+ )
3508
+ ] })
3509
+ ] })
3510
+ ) })
3511
+ ]
3512
+ }
3513
+ ),
3514
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("canvas", { ref: canvasRef, className: "hidden", "aria-hidden": "true" })
3515
+ ] }) });
3516
+ }
3517
+
3518
+ // src/components/shared/ImageViewerModal.tsx
3519
+ var import_react15 = require("react");
3520
+ var import_react_dom = require("react-dom");
3521
+ var import_framer_motion3 = require("framer-motion");
3522
+ var import_react16 = require("@use-gesture/react");
3523
+ var import_lucide_react10 = require("lucide-react");
3524
+ var import_jsx_runtime27 = require("react/jsx-runtime");
3525
+ function ImageViewerModal({
3526
+ isOpen,
3527
+ onClose,
3528
+ images = [],
3529
+ selectedImage,
3530
+ setSelectedImage,
3531
+ productName
3532
+ }) {
3533
+ const scale = (0, import_framer_motion3.useMotionValue)(1);
3534
+ const x = (0, import_framer_motion3.useMotionValue)(0);
3535
+ const y = (0, import_framer_motion3.useMotionValue)(0);
3536
+ const rotation = (0, import_framer_motion3.useMotionValue)(0);
3537
+ const [mounted, setMounted] = (0, import_react15.useState)(false);
3538
+ const [isZoomed, setIsZoomed] = (0, import_react15.useState)(false);
3539
+ const [currentZoomLevel, setCurrentZoomLevel] = (0, import_react15.useState)(1);
3540
+ const [isFullscreen, setIsFullscreen] = (0, import_react15.useState)(false);
3541
+ const [mainImgLoaded, setMainImgLoaded] = (0, import_react15.useState)(false);
3542
+ const containerRef = (0, import_react15.useRef)(null);
3543
+ const prevSelectedImageRef = (0, import_react15.useRef)(selectedImage);
3544
+ const prevIsOpenRef = (0, import_react15.useRef)(isOpen);
3545
+ (0, import_react15.useEffect)(() => {
3546
+ setMounted(true);
3547
+ }, []);
3548
+ (0, import_react15.useEffect)(() => {
3549
+ if (prevSelectedImageRef.current !== selectedImage || prevIsOpenRef.current !== isOpen) {
3550
+ setIsZoomed(false);
3551
+ setCurrentZoomLevel(1);
3552
+ setMainImgLoaded(false);
3553
+ prevSelectedImageRef.current = selectedImage;
3554
+ prevIsOpenRef.current = isOpen;
3555
+ }
3556
+ scale.set(1);
3557
+ x.set(0);
3558
+ y.set(0);
3559
+ rotation.set(0);
3560
+ }, [selectedImage, isOpen, scale, x, y, rotation]);
3561
+ const handleNext = (0, import_react15.useCallback)(() => {
3562
+ if (!images.length) return;
3563
+ setSelectedImage((selectedImage + 1) % images.length);
3564
+ }, [selectedImage, images.length, setSelectedImage]);
3565
+ const handlePrev = (0, import_react15.useCallback)(() => {
3566
+ if (!images.length) return;
3567
+ setSelectedImage((selectedImage - 1 + images.length) % images.length);
3568
+ }, [selectedImage, images.length, setSelectedImage]);
3569
+ const handleZoomIn = (0, import_react15.useCallback)(() => {
3570
+ const next = Math.min(scale.get() + 0.5, 4);
3571
+ scale.set(next);
3572
+ setCurrentZoomLevel(next);
3573
+ setIsZoomed(next > 1);
3574
+ }, [scale]);
3575
+ const handleZoomOut = (0, import_react15.useCallback)(() => {
3576
+ const next = Math.max(scale.get() - 0.5, 1);
3577
+ scale.set(next);
3578
+ setCurrentZoomLevel(next);
3579
+ setIsZoomed(next > 1);
3580
+ if (next === 1) {
3581
+ x.set(0);
3582
+ y.set(0);
3583
+ }
3584
+ }, [scale, x, y]);
3585
+ const handleRotate = (0, import_react15.useCallback)(() => {
3586
+ rotation.set(rotation.get() + 90);
3587
+ }, [rotation]);
3588
+ const handleReset = (0, import_react15.useCallback)(() => {
3589
+ scale.set(1);
3590
+ x.set(0);
3591
+ y.set(0);
3592
+ rotation.set(0);
3593
+ setCurrentZoomLevel(1);
3594
+ setIsZoomed(false);
3595
+ }, [scale, x, y, rotation]);
3596
+ const toggleFullscreen = (0, import_react15.useCallback)(() => {
3597
+ if (!containerRef.current) return;
3598
+ if (!document.fullscreenElement) {
3599
+ containerRef.current.requestFullscreen().catch((err) => {
3600
+ logger.error(`Error attempting to enable full-screen mode: ${err.message}`);
3601
+ });
3602
+ } else {
3603
+ document.exitFullscreen().catch(() => {
3604
+ });
3605
+ }
3606
+ }, []);
3607
+ (0, import_react15.useEffect)(() => {
3608
+ const handleFullscreenChange = () => setIsFullscreen(!!document.fullscreenElement);
3609
+ document.addEventListener("fullscreenchange", handleFullscreenChange);
3610
+ return () => document.removeEventListener("fullscreenchange", handleFullscreenChange);
3611
+ }, []);
3612
+ (0, import_react15.useEffect)(() => {
3613
+ if (!isOpen) return;
3614
+ const handleKeyDown = (e) => {
3615
+ if (e.key === "Escape") onClose();
3616
+ if (e.key === "ArrowLeft") handlePrev();
3617
+ if (e.key === "ArrowRight") handleNext();
3618
+ if (e.key === "+" || e.key === "=") handleZoomIn();
3619
+ if (e.key === "-") handleZoomOut();
3620
+ };
3621
+ window.addEventListener("keydown", handleKeyDown);
3622
+ document.body.style.overflow = "hidden";
3623
+ return () => {
3624
+ window.removeEventListener("keydown", handleKeyDown);
3625
+ document.body.style.overflow = "";
3626
+ };
3627
+ }, [isOpen, onClose, handleNext, handlePrev, handleZoomIn, handleZoomOut]);
3628
+ const bind = (0, import_react16.useGesture)(
3629
+ {
3630
+ onDrag: ({ down, movement: [mx], offset: [ox, oy] }) => {
3631
+ if (scale.get() > 1) {
3632
+ if (down) {
3633
+ x.set(ox);
3634
+ y.set(oy);
3635
+ }
3636
+ } else {
3637
+ if (!down) {
3638
+ if (mx < -60) handleNext();
3639
+ else if (mx > 60) handlePrev();
3640
+ }
3641
+ }
3642
+ },
3643
+ onPinch: ({ offset: [s] }) => {
3644
+ scale.set(s);
3645
+ setCurrentZoomLevel(s);
3646
+ setIsZoomed(s > 1);
3647
+ if (s <= 1) {
3648
+ x.set(0);
3649
+ y.set(0);
3650
+ }
3651
+ },
3652
+ onWheel: ({ delta: [, dy], ctrlKey }) => {
3653
+ if (ctrlKey) {
3654
+ const next = Math.min(Math.max(1, scale.get() - dy / 100), 4);
3655
+ scale.set(next);
3656
+ setCurrentZoomLevel(next);
3657
+ setIsZoomed(next > 1);
3658
+ }
3659
+ },
3660
+ onDoubleClick: ({ event }) => {
3661
+ event.stopPropagation();
3662
+ if (scale.get() > 1) {
3663
+ handleReset();
3664
+ } else {
3665
+ scale.set(2.5);
3666
+ setCurrentZoomLevel(2.5);
3667
+ setIsZoomed(true);
3668
+ }
3669
+ }
3670
+ },
3671
+ {
3672
+ drag: { from: () => [x.get(), y.get()], threshold: 10 },
3673
+ pinch: { scaleBounds: { min: 1, max: 4 }, modifierKey: null }
3674
+ }
3675
+ );
3676
+ if (!images.length) return null;
3677
+ const btnStyle = {
3678
+ background: "rgba(255, 255, 255, 0.1)",
3679
+ border: "none",
3680
+ borderRadius: "9999px",
3681
+ padding: "8px",
3682
+ color: "rgba(255, 255, 255, 0.9)",
3683
+ cursor: "pointer",
3684
+ display: "inline-flex",
3685
+ alignItems: "center",
3686
+ justifyContent: "center",
3687
+ transition: "background-color 0.15s ease, color 0.15s ease"
3688
+ };
3689
+ const navBtnStyle = {
3690
+ position: "absolute",
3691
+ top: "50%",
3692
+ transform: "translateY(-50%)",
3693
+ background: "rgba(0, 0, 0, 0.6)",
3694
+ border: "1px solid rgba(255, 255, 255, 0.2)",
3695
+ borderRadius: "9999px",
3696
+ padding: "14px",
3697
+ color: "#ffffff",
3698
+ cursor: "pointer",
3699
+ backdropFilter: "blur(8px)",
3700
+ zIndex: 40,
3701
+ display: "flex",
3702
+ alignItems: "center",
3703
+ justifyContent: "center",
3704
+ boxShadow: "0 10px 25px rgba(0, 0, 0, 0.5)",
3705
+ transition: "transform 0.15s ease, background-color 0.15s ease"
3706
+ };
3707
+ const modalContent = /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_framer_motion3.AnimatePresence, { children: isOpen && /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
3708
+ import_framer_motion3.motion.div,
3709
+ {
3710
+ ref: containerRef,
3711
+ initial: { opacity: 0 },
3712
+ animate: { opacity: 1 },
3713
+ exit: { opacity: 0 },
3714
+ transition: { duration: 0.15, ease: "easeOut" },
3715
+ style: {
3716
+ position: "fixed",
3717
+ top: 0,
3718
+ left: 0,
3719
+ right: 0,
3720
+ bottom: 0,
3721
+ width: "100vw",
3722
+ height: "100vh",
3723
+ zIndex: 99999,
3724
+ backgroundColor: "#000000",
3725
+ display: "flex",
3726
+ flexDirection: "column",
3727
+ userSelect: "none",
3728
+ WebkitUserSelect: "none",
3729
+ overflow: "hidden",
3730
+ boxSizing: "border-box"
3731
+ },
3732
+ className: "fixed inset-0 z-[99999] bg-black flex flex-col select-none overflow-hidden",
3733
+ onClick: onClose,
3734
+ role: "dialog",
3735
+ "aria-modal": "true",
3736
+ "aria-label": `${productName} image viewer`,
3737
+ children: [
3738
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
3739
+ "div",
3740
+ {
3741
+ style: {
3742
+ display: "flex",
3743
+ justifyContent: "space-between",
3744
+ alignItems: "center",
3745
+ padding: "12px 20px",
3746
+ zIndex: 50,
3747
+ color: "#ffffff",
3748
+ background: "linear-gradient(to bottom, rgba(0,0,0,0.85), rgba(0,0,0,0))",
3749
+ flexShrink: 0,
3750
+ width: "100%",
3751
+ boxSizing: "border-box"
3752
+ },
3753
+ onClick: (e) => e.stopPropagation(),
3754
+ children: [
3755
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "12px" }, children: [
3756
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
3757
+ "span",
3758
+ {
3759
+ style: {
3760
+ fontSize: "13px",
3761
+ fontWeight: 500,
3762
+ letterSpacing: "0.08em",
3763
+ color: "rgba(255, 255, 255, 0.9)",
3764
+ backgroundColor: "rgba(255, 255, 255, 0.12)",
3765
+ padding: "4px 12px",
3766
+ borderRadius: "9999px",
3767
+ border: "1px solid rgba(255, 255, 255, 0.15)"
3768
+ },
3769
+ children: [
3770
+ selectedImage + 1,
3771
+ " / ",
3772
+ images.length
3773
+ ]
3774
+ }
3775
+ ),
3776
+ productName && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3777
+ "span",
3778
+ {
3779
+ style: {
3780
+ fontSize: "13px",
3781
+ fontWeight: 500,
3782
+ color: "rgba(255, 255, 255, 0.7)",
3783
+ whiteSpace: "nowrap",
3784
+ overflow: "hidden",
3785
+ textOverflow: "ellipsis",
3786
+ maxWidth: "300px"
3787
+ },
3788
+ className: "hidden sm:inline",
3789
+ children: productName
3790
+ }
3791
+ )
3792
+ ] }),
3793
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
3794
+ currentZoomLevel > 1 && /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
3795
+ "span",
3796
+ {
3797
+ style: {
3798
+ fontSize: "11px",
3799
+ fontFamily: "monospace",
3800
+ color: "rgba(255, 255, 255, 0.7)",
3801
+ marginRight: "4px"
3802
+ },
3803
+ className: "hidden sm:inline",
3804
+ children: [
3805
+ Math.round(currentZoomLevel * 100),
3806
+ "%"
3807
+ ]
3808
+ }
3809
+ ),
3810
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3811
+ "button",
3812
+ {
3813
+ type: "button",
3814
+ onClick: handleZoomOut,
3815
+ style: btnStyle,
3816
+ title: "Zoom Out (-)",
3817
+ onMouseEnter: (e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.25)",
3818
+ onMouseLeave: (e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.1)",
3819
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_lucide_react10.ZoomOut, { className: "w-5 h-5", style: { width: "20px", height: "20px" } })
3820
+ }
3821
+ ),
3822
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3823
+ "button",
3824
+ {
3825
+ type: "button",
3826
+ onClick: handleZoomIn,
3827
+ style: btnStyle,
3828
+ title: "Zoom In (+)",
3829
+ onMouseEnter: (e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.25)",
3830
+ onMouseLeave: (e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.1)",
3831
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_lucide_react10.ZoomIn, { className: "w-5 h-5", style: { width: "20px", height: "20px" } })
3832
+ }
3833
+ ),
3834
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3835
+ "button",
3836
+ {
3837
+ type: "button",
3838
+ onClick: handleRotate,
3839
+ style: btnStyle,
3840
+ title: "Rotate 90\xB0",
3841
+ onMouseEnter: (e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.25)",
3842
+ onMouseLeave: (e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.1)",
3843
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_lucide_react10.RotateCw, { className: "w-5 h-5", style: { width: "20px", height: "20px" } })
3844
+ }
3845
+ ),
3846
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3847
+ "button",
3848
+ {
3849
+ type: "button",
3850
+ onClick: handleReset,
3851
+ style: btnStyle,
3852
+ title: "Reset View",
3853
+ onMouseEnter: (e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.25)",
3854
+ onMouseLeave: (e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.1)",
3855
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_lucide_react10.RefreshCcw, { className: "w-5 h-5", style: { width: "20px", height: "20px" } })
3856
+ }
3857
+ ),
3858
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { style: { width: "1px", height: "20px", backgroundColor: "rgba(255,255,255,0.2)", margin: "0 6px" } }),
3859
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3860
+ "button",
3861
+ {
3862
+ type: "button",
3863
+ onClick: toggleFullscreen,
3864
+ style: btnStyle,
3865
+ title: isFullscreen ? "Exit Fullscreen" : "Enter Fullscreen",
3866
+ onMouseEnter: (e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.25)",
3867
+ onMouseLeave: (e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.1)",
3868
+ children: isFullscreen ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_lucide_react10.Minimize, { className: "w-5 h-5", style: { width: "20px", height: "20px" } }) : /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_lucide_react10.Maximize, { className: "w-5 h-5", style: { width: "20px", height: "20px" } })
3869
+ }
3870
+ ),
3871
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { style: { width: "1px", height: "20px", backgroundColor: "rgba(255,255,255,0.2)", margin: "0 6px" } }),
3872
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3873
+ "button",
3874
+ {
3875
+ type: "button",
3876
+ onClick: onClose,
3877
+ style: {
3878
+ ...btnStyle,
3879
+ backgroundColor: "rgba(239, 68, 68, 0.25)",
3880
+ color: "#f87171"
3881
+ },
3882
+ title: "Close (Esc)",
3883
+ "aria-label": "Close viewer",
3884
+ onMouseEnter: (e) => e.currentTarget.style.backgroundColor = "rgba(239, 68, 68, 0.45)",
3885
+ onMouseLeave: (e) => e.currentTarget.style.backgroundColor = "rgba(239, 68, 68, 0.25)",
3886
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_lucide_react10.X, { className: "w-6 h-6", style: { width: "24px", height: "24px" } })
3887
+ }
3888
+ )
3889
+ ] })
3890
+ ]
3891
+ }
3892
+ ),
3893
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
3894
+ "div",
3895
+ {
3896
+ style: {
3897
+ flex: 1,
3898
+ position: "relative",
3899
+ display: "flex",
3900
+ alignItems: "center",
3901
+ justifyContent: "center",
3902
+ width: "100%",
3903
+ height: "100%",
3904
+ minHeight: 0,
3905
+ overflow: "hidden",
3906
+ touchAction: "none"
3907
+ },
3908
+ ...bind(),
3909
+ onClick: (e) => e.stopPropagation(),
3910
+ children: [
3911
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
3912
+ import_framer_motion3.motion.div,
3913
+ {
3914
+ style: {
3915
+ scale,
3916
+ rotate: rotation,
3917
+ x,
3918
+ y,
3919
+ cursor: isZoomed ? "grab" : "default",
3920
+ width: "100%",
3921
+ height: "100%",
3922
+ padding: "16px",
3923
+ display: "flex",
3924
+ alignItems: "center",
3925
+ justifyContent: "center",
3926
+ boxSizing: "border-box"
3927
+ },
3928
+ className: "will-change-transform",
3929
+ children: [
3930
+ !mainImgLoaded && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3931
+ "div",
3932
+ {
3933
+ style: {
3934
+ position: "absolute",
3935
+ inset: 0,
3936
+ display: "flex",
3937
+ alignItems: "center",
3938
+ justifyContent: "center",
3939
+ zIndex: 10
3940
+ },
3941
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3942
+ "div",
3943
+ {
3944
+ style: {
3945
+ width: "160px",
3946
+ height: "160px",
3947
+ borderRadius: "16px",
3948
+ backgroundColor: "rgba(255,255,255,0.05)",
3949
+ border: "1px solid rgba(255,255,255,0.1)"
3950
+ }
3951
+ }
3952
+ )
3953
+ }
3954
+ ),
3955
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3956
+ import_framer_motion3.motion.img,
3957
+ {
3958
+ src: images[selectedImage] ?? void 0,
3959
+ alt: `${productName} - Image ${selectedImage + 1}`,
3960
+ style: {
3961
+ maxWidth: "100%",
3962
+ maxHeight: "100%",
3963
+ objectFit: "contain",
3964
+ userSelect: "none",
3965
+ WebkitUserSelect: "none",
3966
+ display: "block",
3967
+ margin: "auto",
3968
+ opacity: mainImgLoaded ? 1 : 0,
3969
+ transition: "opacity 0.25s ease-out",
3970
+ boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.8)"
3971
+ },
3972
+ draggable: false,
3973
+ onLoad: () => setMainImgLoaded(true),
3974
+ onDragStart: (e) => e.preventDefault(),
3975
+ onMouseDown: (e) => e.preventDefault()
3976
+ },
3977
+ images[selectedImage]
3978
+ )
3979
+ ]
3980
+ }
3981
+ ),
3982
+ images.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [
3983
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3984
+ "button",
3985
+ {
3986
+ type: "button",
3987
+ "aria-label": "Previous image",
3988
+ style: { ...navBtnStyle, left: "24px" },
3989
+ onClick: (e) => {
3990
+ e.stopPropagation();
3991
+ handlePrev();
3992
+ },
3993
+ onMouseEnter: (e) => e.currentTarget.style.backgroundColor = "rgba(0,0,0,0.85)",
3994
+ onMouseLeave: (e) => e.currentTarget.style.backgroundColor = "rgba(0,0,0,0.6)",
3995
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_lucide_react10.ChevronLeft, { className: "w-8 h-8", style: { width: "32px", height: "32px" } })
3996
+ }
3997
+ ),
3998
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3999
+ "button",
4000
+ {
4001
+ type: "button",
4002
+ "aria-label": "Next image",
4003
+ style: { ...navBtnStyle, right: "24px" },
4004
+ onClick: (e) => {
4005
+ e.stopPropagation();
4006
+ handleNext();
4007
+ },
4008
+ onMouseEnter: (e) => e.currentTarget.style.backgroundColor = "rgba(0,0,0,0.85)",
4009
+ onMouseLeave: (e) => e.currentTarget.style.backgroundColor = "rgba(0,0,0,0.6)",
4010
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_lucide_react10.ChevronRight, { className: "w-8 h-8", style: { width: "32px", height: "32px" } })
4011
+ }
4012
+ )
4013
+ ] })
4014
+ ]
4015
+ }
4016
+ ),
4017
+ images.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
4018
+ "div",
4019
+ {
4020
+ style: {
4021
+ padding: "16px 20px",
4022
+ zIndex: 50,
4023
+ background: "linear-gradient(to top, rgba(0, 0, 0, 0.95), rgba(0, 0, 0, 0))",
4024
+ display: "flex",
4025
+ justifyContent: "center",
4026
+ flexShrink: 0,
4027
+ width: "100%",
4028
+ boxSizing: "border-box"
4029
+ },
4030
+ onClick: (e) => e.stopPropagation(),
4031
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
4032
+ "div",
4033
+ {
4034
+ style: {
4035
+ display: "flex",
4036
+ gap: "12px",
4037
+ overflowX: "auto",
4038
+ maxWidth: "100%",
4039
+ paddingBottom: "4px"
4040
+ },
4041
+ children: images.map((img, idx) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
4042
+ "button",
4043
+ {
4044
+ type: "button",
4045
+ onClick: () => setSelectedImage(idx),
4046
+ "aria-label": `View image ${idx + 1}`,
4047
+ style: {
4048
+ position: "relative",
4049
+ width: "60px",
4050
+ height: "60px",
4051
+ borderRadius: "10px",
4052
+ overflow: "hidden",
4053
+ flexShrink: 0,
4054
+ border: selectedImage === idx ? "2px solid #ffffff" : "2px solid rgba(255, 255, 255, 0.2)",
4055
+ opacity: selectedImage === idx ? 1 : 0.5,
4056
+ cursor: "pointer",
4057
+ transform: selectedImage === idx ? "scale(1.05)" : "scale(1)",
4058
+ transition: "all 0.2s ease",
4059
+ background: "none",
4060
+ padding: 0
4061
+ },
4062
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
4063
+ SmartImage_default,
4064
+ {
4065
+ src: img,
4066
+ alt: `Thumbnail ${idx + 1}`,
4067
+ className: "w-full h-full object-cover"
4068
+ }
4069
+ )
4070
+ },
4071
+ idx
4072
+ ))
4073
+ }
4074
+ )
4075
+ }
4076
+ )
4077
+ ]
4078
+ }
4079
+ ) });
4080
+ if (!mounted) return null;
4081
+ return (0, import_react_dom.createPortal)(modalContent, document.body);
4082
+ }
4083
+
4084
+ // src/components/shared/AnalyticsScripts.tsx
4085
+ var import_react17 = require("react");
4086
+ var import_script = __toESM(require("next/script"));
4087
+
4088
+ // src/utils/cookieConsent.ts
4089
+ var STORAGE_KEY = "kasuvia_cookie_consent";
4090
+ var COOKIE_CONSENT_CHANGED_EVENT = "kasuvia:cookie-consent-changed";
4091
+ function getCookieConsent() {
4092
+ try {
4093
+ const value = window.localStorage.getItem(STORAGE_KEY);
4094
+ return value === "accepted" || value === "rejected" ? value : null;
4095
+ } catch {
4096
+ return null;
4097
+ }
4098
+ }
4099
+ function setCookieConsent(value) {
4100
+ try {
4101
+ window.localStorage.setItem(STORAGE_KEY, value);
4102
+ window.dispatchEvent(new Event(COOKIE_CONSENT_CHANGED_EVENT));
4103
+ } catch {
4104
+ }
4105
+ }
4106
+
4107
+ // src/components/shared/AnalyticsScripts.tsx
4108
+ var import_jsx_runtime28 = require("react/jsx-runtime");
4109
+ function AnalyticsScripts({ websiteId, scriptUrl, recorderUrl }) {
4110
+ const [accepted, setAccepted] = (0, import_react17.useState)(false);
4111
+ (0, import_react17.useEffect)(() => {
4112
+ const sync = () => setAccepted(getCookieConsent() === "accepted");
4113
+ sync();
4114
+ window.addEventListener(COOKIE_CONSENT_CHANGED_EVENT, sync);
4115
+ window.addEventListener("storage", sync);
4116
+ return () => {
4117
+ window.removeEventListener(COOKIE_CONSENT_CHANGED_EVENT, sync);
4118
+ window.removeEventListener("storage", sync);
4119
+ };
4120
+ }, []);
4121
+ if (!accepted || !websiteId) return null;
4122
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
4123
+ scriptUrl && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_script.default, { async: true, defer: true, "data-website-id": websiteId, src: scriptUrl, strategy: "afterInteractive" }),
4124
+ recorderUrl && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_script.default, { defer: true, "data-website-id": websiteId, src: recorderUrl, strategy: "afterInteractive" })
4125
+ ] });
4126
+ }
4127
+
4128
+ // src/components/shared/CookieConsentBanner.tsx
4129
+ var import_react18 = require("react");
4130
+ var import_jsx_runtime29 = require("react/jsx-runtime");
4131
+ function CookieConsentBanner({
4132
+ privacyUrl = "/privacy",
4133
+ className,
4134
+ acceptButtonClassName,
4135
+ rejectButtonClassName
4136
+ }) {
4137
+ const [visible, setVisible] = (0, import_react18.useState)(false);
4138
+ (0, import_react18.useEffect)(() => {
4139
+ setVisible(getCookieConsent() === null);
4140
+ }, []);
4141
+ if (!visible) return null;
4142
+ const choose = (value) => {
4143
+ setCookieConsent(value);
4144
+ setVisible(false);
4145
+ };
4146
+ const bannerClass = className ?? "fixed inset-x-0 bottom-0 z-50 border-t border-border bg-background/95 backdrop-blur-sm p-4 shadow-[0_-4px_16px_rgba(0,0,0,0.08)]";
4147
+ const rejectClass = rejectButtonClassName ?? "inline-flex items-center justify-center rounded-md border border-input bg-background px-3 h-9 text-sm font-medium hover:bg-accent hover:text-accent-foreground transition-colors";
4148
+ const acceptClass = acceptButtonClassName ?? "inline-flex items-center justify-center rounded-md bg-primary px-3 h-9 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors";
4149
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: bannerClass, children: /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "mx-auto flex max-w-4xl flex-col items-center gap-3 sm:flex-row sm:justify-between", children: [
4150
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("p", { className: "text-center text-sm text-muted-foreground sm:text-left", children: [
4151
+ "We use cookies to understand how you use this site and improve your experience.",
4152
+ " ",
4153
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
4154
+ "a",
4155
+ {
4156
+ href: privacyUrl,
4157
+ className: "underline underline-offset-2 hover:text-foreground",
4158
+ children: "Learn more"
4159
+ }
4160
+ )
4161
+ ] }),
4162
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex shrink-0 gap-2", children: [
4163
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
4164
+ "button",
4165
+ {
4166
+ type: "button",
4167
+ className: rejectClass,
4168
+ onClick: () => choose("rejected"),
4169
+ children: "Reject non-essential"
4170
+ }
4171
+ ),
4172
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
4173
+ "button",
4174
+ {
4175
+ type: "button",
4176
+ className: acceptClass,
4177
+ onClick: () => choose("accepted"),
4178
+ children: "Accept"
4179
+ }
4180
+ )
4181
+ ] })
4182
+ ] }) });
4183
+ }
4184
+ // Annotate the CommonJS export names for ESM import in node:
4185
+ 0 && (module.exports = {
4186
+ AddressAutocomplete,
4187
+ AnalyticsScripts,
4188
+ BespokeAvailabilityBadge,
4189
+ BespokeProgressTracker,
4190
+ CardSkeleton,
4191
+ CartItem,
4192
+ CartItemSkeleton,
4193
+ CartSidebar,
4194
+ CartTypeMismatchBanner,
4195
+ CookieConsentBanner,
4196
+ DeliverableReviewer,
4197
+ ImageEditorModal,
4198
+ ImageViewerModal,
4199
+ LucideReactIcon,
4200
+ MapPinDrop,
4201
+ MultiSelectPicker,
4202
+ OfflineBanner,
4203
+ OrderItemSkeleton,
4204
+ ProductCard,
4205
+ ProductCardSkeleton,
4206
+ ProductGrid,
4207
+ SearchableInput,
4208
+ SearchablePopover,
4209
+ SlotPicker,
4210
+ SmartImage,
4211
+ SmartMotionImage,
4212
+ TableSkeleton,
4213
+ TruncatedValue,
4214
+ UnsavedChangesDialog,
4215
+ useUnsavedChanges
4216
+ });
4217
+ //# sourceMappingURL=index.js.map