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