@manyrows/appkit-react 0.1.6 → 0.1.8

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.
package/dist/index.cjs CHANGED
@@ -10,6 +10,12 @@ var React__default = /*#__PURE__*/_interopDefault(React);
10
10
  // src/AppKit.tsx
11
11
 
12
12
  // src/runtime.ts
13
+ function isSafeOrigin(url) {
14
+ const s = (url || "").trim().toLowerCase();
15
+ if (s.startsWith("https://")) return true;
16
+ if (s.startsWith("http://localhost") || s.startsWith("http://127.0.0.1") || s.startsWith("http://0.0.0.0")) return true;
17
+ return false;
18
+ }
13
19
  function getManyRowsAppKitRuntime() {
14
20
  const w = window;
15
21
  if (w.ManyRows?.AppKit?.init) return w.ManyRows.AppKit;
@@ -28,7 +34,14 @@ function ensureScriptLoaded(src, timeoutMs) {
28
34
  resolve();
29
35
  return;
30
36
  }
31
- if (document.querySelector(`script[data-manyrows-appkit="true"][src="${src}"]`)) {
37
+ if (!isSafeOrigin(src)) {
38
+ reject(new Error(`Refused to load AppKit script from non-HTTPS origin: ${src}`));
39
+ return;
40
+ }
41
+ const existing = Array.from(document.querySelectorAll('script[data-manyrows-appkit="true"]')).find(
42
+ (el) => el.getAttribute("src") === src
43
+ );
44
+ if (existing) {
32
45
  resolve();
33
46
  return;
34
47
  }
@@ -89,9 +102,28 @@ function isProbablyProdBuild() {
89
102
  return env === "production";
90
103
  }
91
104
  function looksLikeLocalhost(url) {
92
- const s = (url || "").trim().toLowerCase();
105
+ const s = (url).trim().toLowerCase();
93
106
  return s.startsWith("http://localhost") || s.startsWith("https://localhost") || s.startsWith("http://127.0.0.1") || s.startsWith("https://127.0.0.1") || s.startsWith("http://0.0.0.0") || s.startsWith("https://0.0.0.0");
94
107
  }
108
+ function assertSafeURLs(baseURL, src, report) {
109
+ if (!isSafeOrigin(baseURL)) {
110
+ report(
111
+ mkErr("BASE_URL_NOT_ALLOWED_IN_PROD", "baseURL must use HTTPS (or localhost for development).", {
112
+ baseURL
113
+ })
114
+ );
115
+ return false;
116
+ }
117
+ if (!isSafeOrigin(src)) {
118
+ report(
119
+ mkErr("SCRIPT_LOAD_FAILED", "Script src must use HTTPS (or localhost for development).", {
120
+ src
121
+ })
122
+ );
123
+ return false;
124
+ }
125
+ return true;
126
+ }
95
127
  function isAuthedSnapshot(s) {
96
128
  if (!s || typeof s !== "object") return false;
97
129
  const status = s.status;
@@ -111,6 +143,7 @@ function AppKitAuthed(props) {
111
143
  if (!isAuthenticated) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: props.fallback ?? null });
112
144
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: props.children });
113
145
  }
146
+ var DEFAULT_BASE_URL = "https://app.manyrows.com";
114
147
  function mkErr(code, message, details) {
115
148
  return { code, message, details };
116
149
  }
@@ -134,7 +167,7 @@ function DefaultError({ err }) {
134
167
  ": ",
135
168
  err.message
136
169
  ] }),
137
- err.details !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
170
+ err.details !== void 0 && !isProbablyProdBuild() ? /* @__PURE__ */ jsxRuntime.jsx(
138
171
  "pre",
139
172
  {
140
173
  style: {
@@ -154,14 +187,16 @@ function DefaultError({ err }) {
154
187
  function AppKit(props) {
155
188
  const autoId = React.useId();
156
189
  const containerId = props.containerId ?? `manyrows-appkit-${autoId.replace(/[:]/g, "")}`;
190
+ const resolvedBaseURL = props.baseURL?.trim() || DEFAULT_BASE_URL;
191
+ const resolvedSrc = props.src?.trim() || `${resolvedBaseURL}/appkit/assets/appkit.js`;
157
192
  const hasChildren = React__default.default.Children.count(props.children) > 0;
158
193
  const initKey = React.useMemo(
159
194
  () => [
160
195
  containerId,
161
196
  props.workspace,
162
197
  props.appId,
163
- props.baseURL ?? "",
164
- props.src ?? "",
198
+ resolvedBaseURL,
199
+ resolvedSrc,
165
200
  props.runtimeMinVersion ?? "",
166
201
  props.runtimeExactVersion ?? "",
167
202
  props.allowDevEnvInProd ? "1" : "0",
@@ -172,8 +207,8 @@ function AppKit(props) {
172
207
  containerId,
173
208
  props.workspace,
174
209
  props.appId,
175
- props.baseURL,
176
- props.src,
210
+ resolvedBaseURL,
211
+ resolvedSrc,
177
212
  props.runtimeMinVersion,
178
213
  props.runtimeExactVersion,
179
214
  props.allowDevEnvInProd,
@@ -182,6 +217,7 @@ function AppKit(props) {
182
217
  ]
183
218
  );
184
219
  const handleRef = React.useRef(null);
220
+ const [handle, setHandle] = React.useState(null);
185
221
  const [status, setStatus] = React.useState("idle");
186
222
  const [lastError, setLastError] = React.useState(null);
187
223
  const [readyInfo, setReadyInfo] = React.useState(null);
@@ -193,6 +229,7 @@ function AppKit(props) {
193
229
  setReadyInfo(null);
194
230
  setSnapshot(null);
195
231
  handleRef.current = null;
232
+ setHandle(null);
196
233
  const report = (err) => {
197
234
  setLastError(err);
198
235
  setStatus("error");
@@ -211,22 +248,21 @@ function AppKit(props) {
211
248
  );
212
249
  return;
213
250
  }
251
+ if (!assertSafeURLs(resolvedBaseURL, resolvedSrc, report)) return;
214
252
  const prod = isProbablyProdBuild();
215
- if (prod && props.blockLocalhostBaseURLInProd !== false && props.baseURL && looksLikeLocalhost(props.baseURL)) {
253
+ if (prod && props.blockLocalhostBaseURLInProd !== false && looksLikeLocalhost(resolvedBaseURL)) {
216
254
  report(
217
255
  mkErr(
218
256
  "BASE_URL_NOT_ALLOWED_IN_PROD",
219
257
  "localhost baseURL is not allowed in production builds.",
220
- { baseURL: props.baseURL }
258
+ { baseURL: resolvedBaseURL }
221
259
  )
222
260
  );
223
261
  return;
224
262
  }
225
263
  try {
226
- const timeoutMs = props.timeoutMs ?? (props.src ? 4e3 : 2500);
227
- if (props.src) {
228
- await ensureScriptLoaded(props.src, timeoutMs);
229
- }
264
+ const timeoutMs = props.timeoutMs ?? 4e3;
265
+ await ensureScriptLoaded(resolvedSrc, timeoutMs);
230
266
  const start = Date.now();
231
267
  while (!getManyRowsAppKitRuntime() && Date.now() - start < timeoutMs) {
232
268
  if (cancelled) return;
@@ -236,9 +272,9 @@ function AppKit(props) {
236
272
  if (!api) {
237
273
  report(
238
274
  mkErr(
239
- props.src ? "SCRIPT_TIMEOUT" : "RUNTIME_NOT_FOUND",
240
- props.src ? `AppKit runtime not found after loading script: ${props.src}` : "AppKit runtime not found.",
241
- { src: props.src }
275
+ "SCRIPT_TIMEOUT",
276
+ `AppKit runtime not found after loading script: ${resolvedSrc}`,
277
+ { src: resolvedSrc }
242
278
  )
243
279
  );
244
280
  return;
@@ -278,15 +314,15 @@ function AppKit(props) {
278
314
  return;
279
315
  }
280
316
  }
281
- const handle = api.init({
317
+ const h = api.init({
282
318
  containerId,
283
319
  workspace: props.workspace.trim(),
284
320
  appId: props.appId.trim(),
285
- baseURL: props.baseURL,
321
+ baseURL: resolvedBaseURL,
286
322
  theme: props.theme,
287
323
  silent: props.silent,
288
324
  throwOnError: props.throwOnError,
289
- // If host provides children, hide runtime default authed UI.
325
+ // If host provides children, hide runtime default authed UI.
290
326
  // Runtime will still render login / errors / forbidden screens as normal.
291
327
  renderAuthed: hasChildren ? (() => null) : void 0,
292
328
  onReady: (info) => {
@@ -309,7 +345,8 @@ function AppKit(props) {
309
345
  report(mkErr("RUNTIME_ERROR", "AppKit runtime error.", e));
310
346
  }
311
347
  });
312
- handleRef.current = handle ?? null;
348
+ handleRef.current = h ?? null;
349
+ setHandle(h ?? null);
313
350
  } catch (e) {
314
351
  report(mkErr("SCRIPT_LOAD_FAILED", "Failed to load AppKit script.", { error: e }));
315
352
  }
@@ -327,52 +364,52 @@ function AppKit(props) {
327
364
  } catch {
328
365
  }
329
366
  handleRef.current = null;
367
+ setHandle(null);
330
368
  };
331
369
  }, [initKey]);
332
370
  const showLoading = status === "loading" && !props.hideLoadingUI;
333
371
  const showError = status === "error" && !!lastError && !props.hideErrorUI;
334
372
  const ctx = React.useMemo(() => {
335
- const h = handleRef.current;
336
373
  return {
337
374
  status,
338
375
  error: lastError,
339
376
  readyInfo,
340
377
  snapshot,
341
378
  isAuthenticated: isAuthedSnapshot(snapshot),
342
- handle: h,
379
+ handle,
343
380
  refresh: () => {
344
381
  try {
345
- h?.refresh?.();
382
+ handle?.refresh?.();
346
383
  } catch {
347
384
  }
348
385
  },
349
386
  logout: async () => {
350
387
  try {
351
- await h?.logout?.();
388
+ await handle?.logout?.();
352
389
  } catch {
353
390
  }
354
391
  },
355
392
  setToken: (tok) => {
356
393
  try {
357
- h?.setToken?.(tok);
394
+ handle?.setToken?.(tok);
358
395
  } catch {
359
396
  }
360
397
  },
361
398
  destroy: () => {
362
399
  try {
363
- h?.destroy?.();
400
+ handle?.destroy?.();
364
401
  } catch {
365
402
  }
366
403
  },
367
404
  info: () => {
368
405
  try {
369
- return h?.info?.() ?? null;
406
+ return handle?.info?.() ?? null;
370
407
  } catch {
371
408
  return null;
372
409
  }
373
410
  }
374
411
  };
375
- }, [status, lastError, readyInfo, snapshot]);
412
+ }, [status, lastError, readyInfo, snapshot, handle]);
376
413
  return /* @__PURE__ */ jsxRuntime.jsx(Ctx.Provider, { value: ctx, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: props.className, style: props.style, children: [
377
414
  showLoading && props.loading ? props.loading : null,
378
415
  showError && lastError ? props.errorUI ? props.errorUI(lastError) : /* @__PURE__ */ jsxRuntime.jsx(DefaultError, { err: lastError }) : null,
@@ -813,9 +850,3831 @@ function ProfileModal({
813
850
  );
814
851
  }
815
852
 
853
+ // src/hooks.ts
854
+ function useUser() {
855
+ const { snapshot } = useAppKit();
856
+ return snapshot?.appData?.account ?? null;
857
+ }
858
+ function useProject() {
859
+ const { snapshot } = useAppKit();
860
+ return snapshot?.appData?.project ?? null;
861
+ }
862
+ function useRoles() {
863
+ const { snapshot } = useAppKit();
864
+ return snapshot?.appData?.project?.roles ?? [];
865
+ }
866
+ function usePermissions() {
867
+ const { snapshot } = useAppKit();
868
+ return snapshot?.appData?.project?.permissions ?? [];
869
+ }
870
+ function usePermission(permission) {
871
+ const perms = usePermissions();
872
+ return perms.includes(permission);
873
+ }
874
+ function useRole(role) {
875
+ const roles = useRoles();
876
+ return roles.includes(role);
877
+ }
878
+ function useFeatureFlags() {
879
+ const { snapshot } = useAppKit();
880
+ return snapshot?.appData?.featureFlags ?? [];
881
+ }
882
+ function useFeatureFlag(key) {
883
+ const flags = useFeatureFlags();
884
+ const flag = flags.find((f) => f.key === key);
885
+ return flag?.enabled ?? false;
886
+ }
887
+ function useConfig() {
888
+ const { snapshot } = useAppKit();
889
+ return snapshot?.appData?.config ?? [];
890
+ }
891
+ function useConfigValue(key, fallback) {
892
+ const config = useConfig();
893
+ const entry = config.find((c) => c.key === key);
894
+ return entry?.value !== void 0 ? entry.value : fallback;
895
+ }
896
+ function useToken() {
897
+ const { snapshot } = useAppKit();
898
+ return snapshot?.jwtToken ?? null;
899
+ }
900
+
901
+ // src/appResource.ts
902
+ var cache = /* @__PURE__ */ new Map();
903
+ var CACHE_TTL = 5 * 60 * 1e3;
904
+ var inflight = /* @__PURE__ */ new Map();
905
+ async function fetchAppResource(workspaceBaseURL, appId) {
906
+ const key = `${workspaceBaseURL}|${appId}`;
907
+ const cached = cache.get(key);
908
+ if (cached && Date.now() - cached.fetchedAt < CACHE_TTL) return cached.data;
909
+ const pending = inflight.get(key);
910
+ if (pending) return pending;
911
+ const p = fetch(`${workspaceBaseURL}/apps/${appId}`).then((res) => {
912
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
913
+ return res.json();
914
+ }).then((data) => {
915
+ const rawImages = data.imagesBaseURL || null;
916
+ const rawFiles = data.filesBaseURL || null;
917
+ const result = {
918
+ imagesBaseURL: rawImages && isSafeOrigin(rawImages) ? rawImages : null,
919
+ filesBaseURL: rawFiles && isSafeOrigin(rawFiles) ? rawFiles : null
920
+ };
921
+ cache.set(key, { data: result, fetchedAt: Date.now() });
922
+ inflight.delete(key);
923
+ return result;
924
+ }).catch((err) => {
925
+ inflight.delete(key);
926
+ throw err;
927
+ });
928
+ inflight.set(key, p);
929
+ return p;
930
+ }
931
+
932
+ // src/images/config.ts
933
+ function useImagesConfig() {
934
+ const { snapshot } = useAppKit();
935
+ const [imagesBaseURL, setImagesBaseURL] = React.useState(null);
936
+ const [loading, setLoading] = React.useState(true);
937
+ const [error, setError] = React.useState(null);
938
+ const fetchedRef = React.useRef("");
939
+ const retryCountRef = React.useRef(0);
940
+ const maxRetries = 3;
941
+ const retryDelayMs = 5e3;
942
+ const workspaceBaseURL = snapshot?.workspaceBaseURL;
943
+ const appId = snapshot?.appId;
944
+ React.useEffect(() => {
945
+ const cacheKey = workspaceBaseURL && appId ? `${workspaceBaseURL}|${appId}` : "";
946
+ if (fetchedRef.current !== cacheKey) {
947
+ fetchedRef.current = "";
948
+ retryCountRef.current = 0;
949
+ }
950
+ }, [workspaceBaseURL, appId]);
951
+ React.useEffect(() => {
952
+ if (!workspaceBaseURL || !appId) {
953
+ setLoading(false);
954
+ return;
955
+ }
956
+ const cacheKey = `${workspaceBaseURL}|${appId}`;
957
+ if (fetchedRef.current === cacheKey) return;
958
+ let cancelled = false;
959
+ let retryTimer = null;
960
+ setLoading(true);
961
+ setError(null);
962
+ const doFetch = () => {
963
+ fetchAppResource(workspaceBaseURL, appId).then((data) => {
964
+ if (cancelled) return;
965
+ setImagesBaseURL(data.imagesBaseURL);
966
+ setLoading(false);
967
+ fetchedRef.current = cacheKey;
968
+ retryCountRef.current = 0;
969
+ }).catch((err) => {
970
+ if (cancelled) return;
971
+ retryCountRef.current += 1;
972
+ if (retryCountRef.current < maxRetries) {
973
+ retryTimer = setTimeout(doFetch, retryDelayMs);
974
+ } else {
975
+ setError(err.message);
976
+ setLoading(false);
977
+ }
978
+ });
979
+ };
980
+ doFetch();
981
+ return () => {
982
+ cancelled = true;
983
+ if (retryTimer != null) clearTimeout(retryTimer);
984
+ };
985
+ }, [workspaceBaseURL, appId]);
986
+ return { imagesBaseURL, loading, error };
987
+ }
988
+
989
+ // src/images/api.ts
990
+ function headers(jwt) {
991
+ return { Authorization: `Bearer ${jwt}` };
992
+ }
993
+ function apiBase(p) {
994
+ return `${p.imagesBaseURL}/api/${encodeURIComponent(p.appId)}`;
995
+ }
996
+ async function listImages(p, opts) {
997
+ const url = new URL(`${apiBase(p)}/images`);
998
+ if (opts?.page != null) url.searchParams.set("page", String(opts.page));
999
+ if (opts?.pageSize != null)
1000
+ url.searchParams.set("pageSize", String(opts.pageSize));
1001
+ if (opts?.q) url.searchParams.set("q", opts.q);
1002
+ if (opts?.refType) url.searchParams.set("refType", opts.refType);
1003
+ if (opts?.refId) url.searchParams.set("refId", opts.refId);
1004
+ const res = await fetch(url.toString(), { headers: headers(p.jwtToken) });
1005
+ if (!res.ok) throw new Error(`List images failed: HTTP ${res.status}`);
1006
+ return res.json();
1007
+ }
1008
+ async function getImage(p, imageId) {
1009
+ const res = await fetch(`${apiBase(p)}/images/${encodeURIComponent(imageId)}`, {
1010
+ headers: headers(p.jwtToken)
1011
+ });
1012
+ if (!res.ok) throw new Error(`Get image failed: HTTP ${res.status}`);
1013
+ return res.json();
1014
+ }
1015
+ function uploadImage(p, opts, onProgress, signal) {
1016
+ return new Promise((resolve, reject) => {
1017
+ const xhr = new XMLHttpRequest();
1018
+ xhr.open("POST", `${apiBase(p)}/upload`);
1019
+ xhr.setRequestHeader("Authorization", `Bearer ${p.jwtToken}`);
1020
+ if (signal) {
1021
+ signal.addEventListener("abort", () => xhr.abort(), { once: true });
1022
+ }
1023
+ xhr.upload.onprogress = (e) => {
1024
+ if (e.lengthComputable && onProgress) {
1025
+ const pct = Math.round(e.loaded / e.total * 100);
1026
+ onProgress(pct, e.loaded, e.total);
1027
+ }
1028
+ };
1029
+ xhr.onload = () => {
1030
+ let body = null;
1031
+ try {
1032
+ body = JSON.parse(xhr.responseText);
1033
+ } catch {
1034
+ }
1035
+ resolve({ status: xhr.status, body });
1036
+ };
1037
+ xhr.onerror = () => reject(new Error("Upload network error"));
1038
+ xhr.onabort = () => reject(new Error("Upload aborted"));
1039
+ const fd = new FormData();
1040
+ fd.append("file", opts.file);
1041
+ fd.append("title", opts.title);
1042
+ if (opts.description) fd.append("description", opts.description);
1043
+ if (opts.variants) fd.append("variants", opts.variants);
1044
+ if (opts.refType) fd.append("refType", opts.refType);
1045
+ if (opts.refId) fd.append("refId", opts.refId);
1046
+ xhr.send(fd);
1047
+ });
1048
+ }
1049
+ async function updateImage(p, imageId, opts) {
1050
+ const res = await fetch(`${apiBase(p)}/images/${encodeURIComponent(imageId)}`, {
1051
+ method: "POST",
1052
+ headers: { ...headers(p.jwtToken), "Content-Type": "application/json" },
1053
+ body: JSON.stringify(opts)
1054
+ });
1055
+ if (!res.ok) throw new Error(`Update image failed: HTTP ${res.status}`);
1056
+ }
1057
+ async function deleteImage(p, imageId) {
1058
+ const res = await fetch(`${apiBase(p)}/images/${encodeURIComponent(imageId)}`, {
1059
+ method: "DELETE",
1060
+ headers: headers(p.jwtToken)
1061
+ });
1062
+ if (!res.ok) throw new Error(`Delete image failed: HTTP ${res.status}`);
1063
+ }
1064
+
1065
+ // src/images/useImages.ts
1066
+ function useImages(opts) {
1067
+ const { snapshot } = useAppKit();
1068
+ const { imagesBaseURL, loading: configLoading } = useImagesConfig();
1069
+ const [images, setImages] = React.useState([]);
1070
+ const [page, setPageState] = React.useState(opts?.page ?? 0);
1071
+ const [pageSize] = React.useState(opts?.pageSize ?? 50);
1072
+ const [total, setTotal] = React.useState(0);
1073
+ const [pageCount, setPageCount] = React.useState(0);
1074
+ const [query, setQueryState] = React.useState(opts?.q ?? "");
1075
+ const [loading, setLoading] = React.useState(false);
1076
+ const [error, setError] = React.useState(null);
1077
+ const fetchIdRef = React.useRef(0);
1078
+ const enabled = opts?.enabled !== false;
1079
+ const jwtToken = snapshot?.jwtToken;
1080
+ const appId = snapshot?.appId;
1081
+ const available = !!imagesBaseURL;
1082
+ const refType = opts?.refType;
1083
+ const refId = opts?.refId;
1084
+ const doFetch = React.useCallback(() => {
1085
+ if (!imagesBaseURL || !appId || !jwtToken || !enabled) return;
1086
+ const id = ++fetchIdRef.current;
1087
+ setLoading(true);
1088
+ setError(null);
1089
+ listImages(
1090
+ { imagesBaseURL, appId, jwtToken },
1091
+ { page, pageSize, q: query || void 0, refType, refId }
1092
+ ).then((res) => {
1093
+ if (id !== fetchIdRef.current) return;
1094
+ setImages(res.images ?? []);
1095
+ setTotal(res.total);
1096
+ setPageCount(res.pageCount);
1097
+ setLoading(false);
1098
+ }).catch((err) => {
1099
+ if (id !== fetchIdRef.current) return;
1100
+ setError(err.message);
1101
+ setLoading(false);
1102
+ });
1103
+ }, [imagesBaseURL, appId, jwtToken, page, pageSize, query, refType, refId, enabled]);
1104
+ React.useEffect(() => {
1105
+ doFetch();
1106
+ return () => {
1107
+ fetchIdRef.current++;
1108
+ };
1109
+ }, [doFetch]);
1110
+ const setPage = React.useCallback((p) => setPageState(p), []);
1111
+ const setQuery = React.useCallback((q) => {
1112
+ setQueryState(q);
1113
+ setPageState(0);
1114
+ }, []);
1115
+ const removeImage = React.useCallback(
1116
+ async (imageId) => {
1117
+ if (!imagesBaseURL || !appId || !jwtToken) return;
1118
+ await deleteImage({ imagesBaseURL, appId, jwtToken }, imageId);
1119
+ doFetch();
1120
+ },
1121
+ [imagesBaseURL, appId, jwtToken, doFetch]
1122
+ );
1123
+ const doUpdateImage = React.useCallback(
1124
+ async (imageId, updateOpts) => {
1125
+ if (!imagesBaseURL || !appId || !jwtToken) return;
1126
+ await updateImage(
1127
+ { imagesBaseURL, appId, jwtToken },
1128
+ imageId,
1129
+ updateOpts
1130
+ );
1131
+ doFetch();
1132
+ },
1133
+ [imagesBaseURL, appId, jwtToken, doFetch]
1134
+ );
1135
+ return {
1136
+ images,
1137
+ page,
1138
+ pageSize,
1139
+ total,
1140
+ pageCount,
1141
+ loading: loading || configLoading,
1142
+ error,
1143
+ refetch: doFetch,
1144
+ setPage,
1145
+ setQuery,
1146
+ available,
1147
+ removeImage,
1148
+ updateImage: doUpdateImage
1149
+ };
1150
+ }
1151
+ var IDLE = {
1152
+ status: "idle",
1153
+ progress: 0,
1154
+ bytesUploaded: 0,
1155
+ bytesTotal: 0,
1156
+ error: null
1157
+ };
1158
+ function useImageUpload() {
1159
+ const { snapshot } = useAppKit();
1160
+ const { imagesBaseURL } = useImagesConfig();
1161
+ const [progress, setProgress] = React.useState(IDLE);
1162
+ const abortRef = React.useRef(null);
1163
+ const jwtToken = snapshot?.jwtToken;
1164
+ const appId = snapshot?.appId;
1165
+ const available = !!imagesBaseURL;
1166
+ React.useEffect(() => {
1167
+ if (!jwtToken && progress.status === "uploading") {
1168
+ abortRef.current?.abort();
1169
+ }
1170
+ }, [jwtToken, progress.status]);
1171
+ const upload = React.useCallback(
1172
+ async (opts) => {
1173
+ if (!imagesBaseURL || !appId || !jwtToken) {
1174
+ setProgress({
1175
+ ...IDLE,
1176
+ status: "error",
1177
+ error: "Images service not available"
1178
+ });
1179
+ return;
1180
+ }
1181
+ abortRef.current?.abort();
1182
+ const controller = new AbortController();
1183
+ abortRef.current = controller;
1184
+ setProgress({
1185
+ status: "uploading",
1186
+ progress: 0,
1187
+ bytesUploaded: 0,
1188
+ bytesTotal: opts.file.size,
1189
+ error: null
1190
+ });
1191
+ try {
1192
+ const result = await uploadImage(
1193
+ { imagesBaseURL, appId, jwtToken },
1194
+ opts,
1195
+ (pct, loaded, total) => {
1196
+ setProgress((prev) => ({
1197
+ ...prev,
1198
+ progress: pct,
1199
+ bytesUploaded: loaded,
1200
+ bytesTotal: total
1201
+ }));
1202
+ },
1203
+ controller.signal
1204
+ );
1205
+ if (result.status === 204) {
1206
+ setProgress({
1207
+ status: "success",
1208
+ progress: 100,
1209
+ bytesUploaded: opts.file.size,
1210
+ bytesTotal: opts.file.size,
1211
+ error: null
1212
+ });
1213
+ } else if (result.status === 409) {
1214
+ setProgress({
1215
+ status: "conflict",
1216
+ progress: 100,
1217
+ bytesUploaded: opts.file.size,
1218
+ bytesTotal: opts.file.size,
1219
+ error: result.body?.message || "An image with identical content already exists",
1220
+ existingImageId: result.body?.imageId
1221
+ });
1222
+ } else if (result.status === 413) {
1223
+ throw new Error("File exceeds 4MB limit");
1224
+ } else if (result.status === 415) {
1225
+ throw new Error(
1226
+ "Unsupported file type. Allowed: PNG, JPEG, GIF, WEBP, AVIF"
1227
+ );
1228
+ } else if (result.status === 429) {
1229
+ throw new Error(result.body?.message || "Storage limit reached");
1230
+ } else {
1231
+ throw new Error(`Upload failed: HTTP ${result.status}`);
1232
+ }
1233
+ } catch (err) {
1234
+ if (err.message === "Upload aborted") return;
1235
+ setProgress({
1236
+ status: "error",
1237
+ progress: 0,
1238
+ bytesUploaded: 0,
1239
+ bytesTotal: opts.file.size,
1240
+ error: err.message || "Upload failed"
1241
+ });
1242
+ }
1243
+ },
1244
+ [imagesBaseURL, appId, jwtToken]
1245
+ );
1246
+ const cancel = React.useCallback(() => {
1247
+ abortRef.current?.abort();
1248
+ setProgress(IDLE);
1249
+ }, []);
1250
+ const reset = React.useCallback(() => {
1251
+ setProgress(IDLE);
1252
+ }, []);
1253
+ return { upload, cancel, progress, reset, available };
1254
+ }
1255
+ function useImage(imageId, opts) {
1256
+ const { snapshot } = useAppKit();
1257
+ const { imagesBaseURL, loading: configLoading } = useImagesConfig();
1258
+ const [image, setImage] = React.useState(null);
1259
+ const [loading, setLoading] = React.useState(false);
1260
+ const [error, setError] = React.useState(null);
1261
+ const fetchIdRef = React.useRef(0);
1262
+ const enabled = opts?.enabled !== false;
1263
+ const jwtToken = snapshot?.jwtToken;
1264
+ const appId = snapshot?.appId;
1265
+ const available = !!imagesBaseURL;
1266
+ const doFetch = React.useCallback(() => {
1267
+ if (!imagesBaseURL || !appId || !jwtToken || !imageId || !enabled) return;
1268
+ const id = ++fetchIdRef.current;
1269
+ setLoading(true);
1270
+ setError(null);
1271
+ getImage({ imagesBaseURL, appId, jwtToken }, imageId).then((res) => {
1272
+ if (id !== fetchIdRef.current) return;
1273
+ setImage(res);
1274
+ setLoading(false);
1275
+ }).catch((err) => {
1276
+ if (id !== fetchIdRef.current) return;
1277
+ setError(err.message);
1278
+ setLoading(false);
1279
+ });
1280
+ }, [imagesBaseURL, appId, jwtToken, imageId, enabled]);
1281
+ React.useEffect(() => {
1282
+ doFetch();
1283
+ return () => {
1284
+ fetchIdRef.current++;
1285
+ };
1286
+ }, [doFetch]);
1287
+ return {
1288
+ image,
1289
+ loading: loading || configLoading,
1290
+ error,
1291
+ refetch: doFetch,
1292
+ available
1293
+ };
1294
+ }
1295
+ var FONT = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
1296
+ var ACCEPT_DEFAULT = "image/png,image/jpeg,image/gif,image/webp,image/avif";
1297
+ var MAX_SIZE_DEFAULT = 4 * 1024 * 1024;
1298
+ var VARIANT_PRESETS = [
1299
+ { key: "sq200", label: "sq200", desc: "Thumbnail (200x200)", required: true },
1300
+ { key: "sq400", label: "sq400", desc: "Square (400x400)" },
1301
+ { key: "w400", label: "w400", desc: "Small (400w)" },
1302
+ { key: "w800", label: "w800", desc: "Medium (800w)" },
1303
+ { key: "w1200", label: "w1200", desc: "Large (1200w)" },
1304
+ { key: "w1600", label: "w1600", desc: "Retina (1600w)" },
1305
+ { key: "w1920", label: "w1920", desc: "Full HD (1920w)" }
1306
+ ];
1307
+ var DEFAULT_VARIANTS = /* @__PURE__ */ new Set(["sq200", "w400", "w800"]);
1308
+ function formatSize(bytes) {
1309
+ if (bytes < 1024) return `${bytes} B`;
1310
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1311
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1312
+ }
1313
+ function parseVariants(csv) {
1314
+ if (!csv) return new Set(DEFAULT_VARIANTS);
1315
+ return new Set(csv.split(",").map((s) => s.trim()).filter(Boolean));
1316
+ }
1317
+ function ImageUploader({
1318
+ onUpload,
1319
+ onError,
1320
+ defaultTitle,
1321
+ defaultDescription = "",
1322
+ variants: variantsProp,
1323
+ showFields = true,
1324
+ showVariantPicker = false,
1325
+ accept = ACCEPT_DEFAULT,
1326
+ maxSize = MAX_SIZE_DEFAULT,
1327
+ refType,
1328
+ refId,
1329
+ className,
1330
+ style,
1331
+ label = "Drop an image here or click to select",
1332
+ disabled = false
1333
+ }) {
1334
+ const { upload, cancel, progress, reset, available } = useImageUpload();
1335
+ const fileInputRef = React.useRef(null);
1336
+ const [dragOver, setDragOver] = React.useState(false);
1337
+ const [title, setTitle] = React.useState(defaultTitle ?? "");
1338
+ const [description, setDescription] = React.useState(defaultDescription);
1339
+ const [selectedFile, setSelectedFile] = React.useState(null);
1340
+ const [validationError, setValidationError] = React.useState(null);
1341
+ const [selectedVariants, setSelectedVariants] = React.useState(
1342
+ () => parseVariants(variantsProp)
1343
+ );
1344
+ const [previewURL, setPreviewURL] = React.useState(null);
1345
+ const [showAdvanced, setShowAdvanced] = React.useState(false);
1346
+ const [showSuccess, setShowSuccess] = React.useState(false);
1347
+ React.useEffect(() => {
1348
+ if (!selectedFile) {
1349
+ setPreviewURL(null);
1350
+ return;
1351
+ }
1352
+ const url = URL.createObjectURL(selectedFile);
1353
+ setPreviewURL(url);
1354
+ return () => URL.revokeObjectURL(url);
1355
+ }, [selectedFile]);
1356
+ const handleFile = React.useCallback(
1357
+ (file) => {
1358
+ setValidationError(null);
1359
+ if (file.size > maxSize) {
1360
+ setValidationError(
1361
+ `File too large (${(file.size / 1024 / 1024).toFixed(1)}MB). Max ${(maxSize / 1024 / 1024).toFixed(0)}MB.`
1362
+ );
1363
+ return;
1364
+ }
1365
+ setSelectedFile(file);
1366
+ if (!title) {
1367
+ setTitle(file.name.replace(/\.[^.]+$/, ""));
1368
+ }
1369
+ },
1370
+ [maxSize, title]
1371
+ );
1372
+ const handleDrop = React.useCallback(
1373
+ (e) => {
1374
+ e.preventDefault();
1375
+ setDragOver(false);
1376
+ const file = e.dataTransfer.files[0];
1377
+ if (file) handleFile(file);
1378
+ },
1379
+ [handleFile]
1380
+ );
1381
+ const toggleVariant = React.useCallback((key) => {
1382
+ setSelectedVariants((prev) => {
1383
+ const next = new Set(prev);
1384
+ if (next.has(key)) next.delete(key);
1385
+ else next.add(key);
1386
+ next.add("sq200");
1387
+ return next;
1388
+ });
1389
+ }, []);
1390
+ const handleSubmit = React.useCallback(async () => {
1391
+ if (!selectedFile || !title.trim()) return;
1392
+ reset();
1393
+ const variantCsv = showVariantPicker ? Array.from(selectedVariants).join(",") : variantsProp;
1394
+ const opts = {
1395
+ file: selectedFile,
1396
+ title: title.trim(),
1397
+ description: description.trim() || void 0,
1398
+ variants: variantCsv,
1399
+ refType,
1400
+ refId
1401
+ };
1402
+ await upload(opts);
1403
+ }, [selectedFile, title, description, variantsProp, showVariantPicker, selectedVariants, refType, refId, upload, reset]);
1404
+ const prevStatus = React.useRef(progress.status);
1405
+ React.useEffect(() => {
1406
+ if (prevStatus.current === progress.status) return;
1407
+ prevStatus.current = progress.status;
1408
+ if (progress.status === "success") {
1409
+ setSelectedFile(null);
1410
+ setTitle(defaultTitle ?? "");
1411
+ setDescription(defaultDescription);
1412
+ setShowSuccess(true);
1413
+ onUpload?.();
1414
+ }
1415
+ if (progress.status === "conflict") {
1416
+ setSelectedFile(null);
1417
+ setTitle(defaultTitle ?? "");
1418
+ setDescription(defaultDescription);
1419
+ }
1420
+ if (progress.status === "error" && progress.error) {
1421
+ onError?.(progress.error);
1422
+ }
1423
+ }, [progress.status, progress.error, onUpload, onError, defaultTitle, defaultDescription]);
1424
+ React.useEffect(() => {
1425
+ if (!showSuccess) return;
1426
+ const t = setTimeout(() => setShowSuccess(false), 4e3);
1427
+ return () => clearTimeout(t);
1428
+ }, [showSuccess]);
1429
+ if (!available) return null;
1430
+ const isUploading = progress.status === "uploading";
1431
+ const isDisabled = disabled || isUploading;
1432
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: { fontFamily: FONT, ...style }, children: [
1433
+ /* @__PURE__ */ jsxRuntime.jsxs(
1434
+ "div",
1435
+ {
1436
+ onDragEnter: (e) => {
1437
+ e.preventDefault();
1438
+ setDragOver(true);
1439
+ },
1440
+ onDragOver: (e) => {
1441
+ e.preventDefault();
1442
+ setDragOver(true);
1443
+ },
1444
+ onDragLeave: () => setDragOver(false),
1445
+ onDrop: handleDrop,
1446
+ onClick: () => !isDisabled && fileInputRef.current?.click(),
1447
+ style: {
1448
+ border: `2px dashed ${dragOver ? "#1976d2" : "#ccc"}`,
1449
+ borderRadius: 8,
1450
+ padding: "24px 16px",
1451
+ textAlign: "center",
1452
+ cursor: isDisabled ? "default" : "pointer",
1453
+ backgroundColor: dragOver ? "#e3f2fd" : "#fafafa",
1454
+ transition: "all 0.15s ease",
1455
+ opacity: isDisabled ? 0.6 : 1
1456
+ },
1457
+ children: [
1458
+ /* @__PURE__ */ jsxRuntime.jsx(
1459
+ "input",
1460
+ {
1461
+ ref: fileInputRef,
1462
+ type: "file",
1463
+ accept,
1464
+ style: { display: "none" },
1465
+ onChange: (e) => {
1466
+ const file = e.target.files?.[0];
1467
+ if (file) handleFile(file);
1468
+ e.target.value = "";
1469
+ }
1470
+ }
1471
+ ),
1472
+ !selectedFile && /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#999", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", style: { marginBottom: 6 }, children: [
1473
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
1474
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "17 8 12 3 7 8" }),
1475
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "3", x2: "12", y2: "15" })
1476
+ ] }),
1477
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 14, color: "#666" }, children: selectedFile ? selectedFile.name : label }),
1478
+ selectedFile && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { fontSize: 12, color: "#999", marginTop: 4 }, children: [
1479
+ (selectedFile.size / 1024).toFixed(0),
1480
+ " KB"
1481
+ ] })
1482
+ ]
1483
+ }
1484
+ ),
1485
+ previewURL && selectedFile && /* @__PURE__ */ jsxRuntime.jsx(
1486
+ "div",
1487
+ {
1488
+ style: {
1489
+ marginTop: 12,
1490
+ borderRadius: 8,
1491
+ overflow: "hidden",
1492
+ backgroundColor: "#f5f5f5",
1493
+ textAlign: "center"
1494
+ },
1495
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1496
+ "img",
1497
+ {
1498
+ src: previewURL,
1499
+ alt: title || selectedFile.name,
1500
+ style: {
1501
+ maxWidth: "100%",
1502
+ maxHeight: 300,
1503
+ objectFit: "contain",
1504
+ display: "block",
1505
+ margin: "0 auto"
1506
+ }
1507
+ }
1508
+ )
1509
+ }
1510
+ ),
1511
+ validationError && /* @__PURE__ */ jsxRuntime.jsx(
1512
+ "div",
1513
+ {
1514
+ style: { color: "#d32f2f", fontSize: 13, marginTop: 8 },
1515
+ children: validationError
1516
+ }
1517
+ ),
1518
+ showFields && selectedFile && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 12, display: "flex", flexDirection: "column", gap: 8 }, children: [
1519
+ /* @__PURE__ */ jsxRuntime.jsx(
1520
+ "input",
1521
+ {
1522
+ type: "text",
1523
+ placeholder: "Title",
1524
+ value: title,
1525
+ onChange: (e) => setTitle(e.target.value),
1526
+ disabled: isDisabled,
1527
+ maxLength: 250,
1528
+ style: {
1529
+ padding: "8px 12px",
1530
+ borderRadius: 6,
1531
+ border: "1px solid #ddd",
1532
+ fontSize: 14,
1533
+ fontFamily: FONT,
1534
+ outline: "none"
1535
+ }
1536
+ }
1537
+ ),
1538
+ /* @__PURE__ */ jsxRuntime.jsx(
1539
+ "input",
1540
+ {
1541
+ type: "text",
1542
+ placeholder: "Description (optional)",
1543
+ value: description,
1544
+ onChange: (e) => setDescription(e.target.value),
1545
+ disabled: isDisabled,
1546
+ maxLength: 500,
1547
+ style: {
1548
+ padding: "8px 12px",
1549
+ borderRadius: 6,
1550
+ border: "1px solid #ddd",
1551
+ fontSize: 14,
1552
+ fontFamily: FONT,
1553
+ outline: "none"
1554
+ }
1555
+ }
1556
+ )
1557
+ ] }),
1558
+ showVariantPicker && selectedFile && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 12 }, children: [
1559
+ /* @__PURE__ */ jsxRuntime.jsxs(
1560
+ "button",
1561
+ {
1562
+ type: "button",
1563
+ onClick: () => setShowAdvanced(!showAdvanced),
1564
+ style: {
1565
+ background: "none",
1566
+ border: "none",
1567
+ padding: 0,
1568
+ fontSize: 13,
1569
+ color: "#666",
1570
+ cursor: "pointer",
1571
+ fontFamily: FONT,
1572
+ display: "flex",
1573
+ alignItems: "center",
1574
+ gap: 4
1575
+ },
1576
+ children: [
1577
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: {
1578
+ display: "inline-block",
1579
+ transition: "transform 0.15s ease",
1580
+ transform: showAdvanced ? "rotate(90deg)" : "rotate(0deg)",
1581
+ fontSize: 10
1582
+ }, children: "\u25B6" }),
1583
+ "Advanced"
1584
+ ]
1585
+ }
1586
+ ),
1587
+ showAdvanced && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 8 }, children: [
1588
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 12, color: "#888", marginBottom: 6 }, children: "Variants:" }),
1589
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6 }, children: VARIANT_PRESETS.map((v) => {
1590
+ const active = selectedVariants.has(v.key);
1591
+ return /* @__PURE__ */ jsxRuntime.jsx(
1592
+ "button",
1593
+ {
1594
+ type: "button",
1595
+ onClick: () => !v.required && toggleVariant(v.key),
1596
+ disabled: isDisabled || v.required,
1597
+ title: v.desc,
1598
+ style: {
1599
+ padding: "4px 10px",
1600
+ borderRadius: 12,
1601
+ border: active ? "1px solid #1976d2" : "1px solid #ccc",
1602
+ backgroundColor: active ? "#e3f2fd" : "#fff",
1603
+ color: active ? "#1976d2" : "#666",
1604
+ fontSize: 12,
1605
+ fontFamily: FONT,
1606
+ cursor: v.required ? "default" : isDisabled ? "default" : "pointer",
1607
+ opacity: isDisabled ? 0.6 : v.required ? 0.8 : 1
1608
+ },
1609
+ children: v.label
1610
+ },
1611
+ v.key
1612
+ );
1613
+ }) })
1614
+ ] })
1615
+ ] }),
1616
+ isUploading && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 12 }, children: [
1617
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { fontSize: 12, color: "#666", marginBottom: 4 }, children: [
1618
+ "Uploading... \u2014 ",
1619
+ Math.round(progress.progress),
1620
+ "%",
1621
+ progress.bytesTotal > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1622
+ " \xB7 ",
1623
+ formatSize(progress.bytesUploaded),
1624
+ " / ",
1625
+ formatSize(progress.bytesTotal)
1626
+ ] })
1627
+ ] }),
1628
+ /* @__PURE__ */ jsxRuntime.jsx(
1629
+ "div",
1630
+ {
1631
+ style: {
1632
+ height: 6,
1633
+ borderRadius: 3,
1634
+ backgroundColor: "#e0e0e0",
1635
+ overflow: "hidden"
1636
+ },
1637
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1638
+ "div",
1639
+ {
1640
+ style: {
1641
+ width: `${progress.progress}%`,
1642
+ height: "100%",
1643
+ backgroundColor: "#1976d2",
1644
+ transition: "width 0.2s ease"
1645
+ }
1646
+ }
1647
+ )
1648
+ }
1649
+ )
1650
+ ] }),
1651
+ selectedFile && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 12, display: "flex", gap: 8 }, children: [
1652
+ /* @__PURE__ */ jsxRuntime.jsx(
1653
+ "button",
1654
+ {
1655
+ onClick: handleSubmit,
1656
+ disabled: isDisabled || !title.trim(),
1657
+ style: {
1658
+ padding: "8px 20px",
1659
+ borderRadius: 6,
1660
+ border: "none",
1661
+ backgroundColor: isDisabled ? "#bbb" : "#1976d2",
1662
+ color: "#fff",
1663
+ fontSize: 14,
1664
+ fontFamily: FONT,
1665
+ cursor: isDisabled ? "default" : "pointer",
1666
+ fontWeight: 500
1667
+ },
1668
+ children: "Upload"
1669
+ }
1670
+ ),
1671
+ isUploading && /* @__PURE__ */ jsxRuntime.jsx(
1672
+ "button",
1673
+ {
1674
+ onClick: cancel,
1675
+ style: {
1676
+ padding: "8px 16px",
1677
+ borderRadius: 6,
1678
+ border: "1px solid #ddd",
1679
+ backgroundColor: "#fff",
1680
+ fontSize: 14,
1681
+ fontFamily: FONT,
1682
+ cursor: "pointer",
1683
+ color: "#666"
1684
+ },
1685
+ children: "Cancel"
1686
+ }
1687
+ )
1688
+ ] }),
1689
+ progress.status === "error" && progress.error && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "#d32f2f", fontSize: 13, marginTop: 8 }, children: progress.error }),
1690
+ progress.status === "conflict" && /* @__PURE__ */ jsxRuntime.jsxs(
1691
+ "div",
1692
+ {
1693
+ style: {
1694
+ marginTop: 12,
1695
+ padding: "10px 14px",
1696
+ borderRadius: 8,
1697
+ backgroundColor: "#fff3e0",
1698
+ border: "1px solid #ffcc80",
1699
+ color: "#e65100",
1700
+ fontSize: 13,
1701
+ fontFamily: FONT,
1702
+ display: "flex",
1703
+ alignItems: "center",
1704
+ justifyContent: "space-between"
1705
+ },
1706
+ children: [
1707
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: progress.error || "An image with identical content already exists" }),
1708
+ /* @__PURE__ */ jsxRuntime.jsx(
1709
+ "button",
1710
+ {
1711
+ onClick: () => reset(),
1712
+ style: {
1713
+ background: "none",
1714
+ border: "none",
1715
+ color: "#e65100",
1716
+ cursor: "pointer",
1717
+ fontSize: 16,
1718
+ lineHeight: 1,
1719
+ padding: "0 2px"
1720
+ },
1721
+ children: "\xD7"
1722
+ }
1723
+ )
1724
+ ]
1725
+ }
1726
+ ),
1727
+ showSuccess && /* @__PURE__ */ jsxRuntime.jsxs(
1728
+ "div",
1729
+ {
1730
+ style: {
1731
+ marginTop: 12,
1732
+ padding: "10px 14px",
1733
+ borderRadius: 8,
1734
+ backgroundColor: "#e8f5e9",
1735
+ border: "1px solid #a5d6a7",
1736
+ color: "#2e7d32",
1737
+ fontSize: 13,
1738
+ fontFamily: FONT,
1739
+ display: "flex",
1740
+ alignItems: "center",
1741
+ justifyContent: "space-between"
1742
+ },
1743
+ children: [
1744
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Image uploaded successfully" }),
1745
+ /* @__PURE__ */ jsxRuntime.jsx(
1746
+ "button",
1747
+ {
1748
+ onClick: () => setShowSuccess(false),
1749
+ style: {
1750
+ background: "none",
1751
+ border: "none",
1752
+ color: "#2e7d32",
1753
+ cursor: "pointer",
1754
+ fontSize: 16,
1755
+ lineHeight: 1,
1756
+ padding: "0 2px"
1757
+ },
1758
+ children: "\xD7"
1759
+ }
1760
+ )
1761
+ ]
1762
+ }
1763
+ )
1764
+ ] });
1765
+ }
1766
+ function pickVariant(variants, opts) {
1767
+ if (!variants || variants.length === 0) return null;
1768
+ if (opts.variant) {
1769
+ const exact = variants.find((v) => v.variant === opts.variant);
1770
+ if (exact) return exact;
1771
+ }
1772
+ if (!opts.width && !opts.height) {
1773
+ return variants.find((v) => v.variant === "orig") || variants[0];
1774
+ }
1775
+ const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
1776
+ const targetW = (opts.width || 0) * dpr;
1777
+ const targetH = (opts.height || 0) * dpr;
1778
+ const targetMax = Math.max(targetW, targetH);
1779
+ const candidates = variants.filter((v) => v.width > 0).sort((a, b) => a.width - b.width);
1780
+ if (candidates.length === 0) return variants[0];
1781
+ for (const v of candidates) {
1782
+ if (v.width >= targetMax) return v;
1783
+ }
1784
+ return candidates[candidates.length - 1];
1785
+ }
1786
+ function buildSrcSet(variants) {
1787
+ const widthVariants = variants.filter(
1788
+ (v) => v.objectURL && v.width > 0 && v.variant !== "orig"
1789
+ );
1790
+ if (widthVariants.length <= 1) return void 0;
1791
+ return widthVariants.sort((a, b) => a.width - b.width).map((v) => `${v.objectURL} ${v.width}w`).join(", ");
1792
+ }
1793
+ function MrImage({
1794
+ image,
1795
+ width,
1796
+ height,
1797
+ variant: variantName,
1798
+ alt,
1799
+ className,
1800
+ style,
1801
+ loading = "lazy",
1802
+ objectFit = "cover",
1803
+ sizes,
1804
+ onLoad,
1805
+ onError
1806
+ }) {
1807
+ const chosen = React.useMemo(
1808
+ () => pickVariant(image.variants, { width, height, variant: variantName }),
1809
+ [image.variants, width, height, variantName]
1810
+ );
1811
+ const srcSet = React.useMemo(() => {
1812
+ if (variantName) return void 0;
1813
+ return buildSrcSet(image.variants);
1814
+ }, [image.variants, variantName]);
1815
+ const [loaded, setLoaded] = React.useState(false);
1816
+ const url = chosen?.objectURL;
1817
+ React.useEffect(() => {
1818
+ setLoaded(false);
1819
+ }, [url]);
1820
+ if (!chosen?.objectURL) {
1821
+ return /* @__PURE__ */ jsxRuntime.jsx(
1822
+ "div",
1823
+ {
1824
+ className,
1825
+ role: "img",
1826
+ "aria-label": alt ?? image.title,
1827
+ style: {
1828
+ display: "flex",
1829
+ alignItems: "center",
1830
+ justifyContent: "center",
1831
+ backgroundColor: "#f0f0f0",
1832
+ color: "#bbb",
1833
+ width,
1834
+ height,
1835
+ maxWidth: "100%",
1836
+ ...style
1837
+ },
1838
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
1839
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
1840
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
1841
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "21 15 16 10 5 21" })
1842
+ ] })
1843
+ }
1844
+ );
1845
+ }
1846
+ return /* @__PURE__ */ jsxRuntime.jsx(
1847
+ "img",
1848
+ {
1849
+ src: chosen.objectURL,
1850
+ srcSet,
1851
+ sizes,
1852
+ alt: alt ?? image.title,
1853
+ width,
1854
+ height,
1855
+ className,
1856
+ loading,
1857
+ onLoad: () => {
1858
+ setLoaded(true);
1859
+ onLoad?.();
1860
+ },
1861
+ onError,
1862
+ style: {
1863
+ objectFit,
1864
+ maxWidth: "100%",
1865
+ opacity: loaded ? 1 : 0,
1866
+ transition: "opacity 0.2s ease",
1867
+ backgroundColor: loaded ? void 0 : "#f0f0f0",
1868
+ ...style
1869
+ }
1870
+ }
1871
+ );
1872
+ }
1873
+ var FONT2 = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
1874
+ var VARIANT_DESCRIPTIONS = {
1875
+ orig: "Original upload",
1876
+ sq200: "Thumbnail (200x200)",
1877
+ sq400: "Square (400x400)",
1878
+ w400: "Small (400w)",
1879
+ w800: "Medium (800w)",
1880
+ w1200: "Large (1200w)",
1881
+ w1600: "Retina (1600w)",
1882
+ w1920: "Full HD (1920w)"
1883
+ };
1884
+ function describeVariant(name) {
1885
+ if (VARIANT_DESCRIPTIONS[name]) return VARIANT_DESCRIPTIONS[name];
1886
+ const sqMatch = name.match(/^sq(\d+)$/);
1887
+ if (sqMatch) return `Square (${sqMatch[1]}x${sqMatch[1]})`;
1888
+ const wMatch = name.match(/^w(\d+)$/);
1889
+ if (wMatch) return `Width ${wMatch[1]}px`;
1890
+ return name;
1891
+ }
1892
+ function formatBytes(bytes) {
1893
+ if (bytes < 1024) return `${bytes} B`;
1894
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1895
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1896
+ }
1897
+ function isSafeURL(url) {
1898
+ return url.startsWith("https://") || url.startsWith("http://localhost");
1899
+ }
1900
+ function sanitizeFilename(name) {
1901
+ return name.replace(/[/\\:*?"<>|\x00-\x1f]/g, "_");
1902
+ }
1903
+ function preferredVariant(variants) {
1904
+ const prefs = ["w1600", "w800", "w400", "orig"];
1905
+ for (const p of prefs) {
1906
+ const v = variants.find((v2) => v2.variant === p);
1907
+ if (v) return v;
1908
+ }
1909
+ return variants[0];
1910
+ }
1911
+ function ImageDetails({
1912
+ image,
1913
+ onClose,
1914
+ onEdit,
1915
+ onDelete,
1916
+ className,
1917
+ style
1918
+ }) {
1919
+ const [selected, setSelected] = React.useState(
1920
+ () => preferredVariant(image.variants)
1921
+ );
1922
+ const [copiedField, setCopiedField] = React.useState(null);
1923
+ const sortedVariants = React.useMemo(
1924
+ () => [...image.variants].sort((a, b) => {
1925
+ if (a.variant === "orig") return -1;
1926
+ if (b.variant === "orig") return 1;
1927
+ return a.width - b.width;
1928
+ }),
1929
+ [image.variants]
1930
+ );
1931
+ const copyText = React.useCallback((text, field) => {
1932
+ navigator.clipboard?.writeText(text).then(() => {
1933
+ setCopiedField(field);
1934
+ setTimeout(() => setCopiedField(null), 1200);
1935
+ }).catch(() => {
1936
+ });
1937
+ }, []);
1938
+ const handleDownload = React.useCallback(async () => {
1939
+ if (!selected?.objectURL) return;
1940
+ try {
1941
+ const res = await fetch(selected.objectURL);
1942
+ const blob = await res.blob();
1943
+ const url = URL.createObjectURL(blob);
1944
+ const a = document.createElement("a");
1945
+ a.href = url;
1946
+ const ext = selected.format || selected.content_type?.split("/")[1] || "jpg";
1947
+ a.download = sanitizeFilename(`${image.title}-${selected.variant}.${ext}`);
1948
+ document.body.appendChild(a);
1949
+ a.click();
1950
+ document.body.removeChild(a);
1951
+ URL.revokeObjectURL(url);
1952
+ } catch {
1953
+ if (isSafeURL(selected.objectURL)) {
1954
+ window.open(selected.objectURL, "_blank");
1955
+ }
1956
+ }
1957
+ }, [selected, image.title]);
1958
+ return /* @__PURE__ */ jsxRuntime.jsx(
1959
+ "div",
1960
+ {
1961
+ style: {
1962
+ position: "fixed",
1963
+ inset: 0,
1964
+ zIndex: 9999,
1965
+ display: "flex",
1966
+ alignItems: "center",
1967
+ justifyContent: "center",
1968
+ backgroundColor: "rgba(0,0,0,0.5)"
1969
+ },
1970
+ onClick: (e) => {
1971
+ if (e.target === e.currentTarget) onClose();
1972
+ },
1973
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
1974
+ "div",
1975
+ {
1976
+ className,
1977
+ style: {
1978
+ backgroundColor: "#fff",
1979
+ borderRadius: 12,
1980
+ maxWidth: 720,
1981
+ width: "95vw",
1982
+ maxHeight: "90vh",
1983
+ overflow: "auto",
1984
+ boxShadow: "0 8px 32px rgba(0,0,0,0.2)",
1985
+ fontFamily: FONT2,
1986
+ ...style
1987
+ },
1988
+ children: [
1989
+ /* @__PURE__ */ jsxRuntime.jsxs(
1990
+ "div",
1991
+ {
1992
+ style: {
1993
+ display: "flex",
1994
+ alignItems: "center",
1995
+ justifyContent: "space-between",
1996
+ padding: "16px 20px 12px",
1997
+ borderBottom: "1px solid #eee"
1998
+ },
1999
+ children: [
2000
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontWeight: 600, fontSize: 16 }, children: image.title }),
2001
+ /* @__PURE__ */ jsxRuntime.jsx(
2002
+ "button",
2003
+ {
2004
+ onClick: onClose,
2005
+ style: {
2006
+ background: "none",
2007
+ border: "none",
2008
+ fontSize: 22,
2009
+ cursor: "pointer",
2010
+ color: "#666",
2011
+ lineHeight: 1,
2012
+ padding: "4px 8px"
2013
+ },
2014
+ children: "\xD7"
2015
+ }
2016
+ )
2017
+ ]
2018
+ }
2019
+ ),
2020
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "16px 20px", textAlign: "center" }, children: selected?.objectURL && /* @__PURE__ */ jsxRuntime.jsx(
2021
+ "img",
2022
+ {
2023
+ src: selected.objectURL,
2024
+ alt: image.title,
2025
+ style: {
2026
+ maxWidth: "100%",
2027
+ maxHeight: 500,
2028
+ borderRadius: 6,
2029
+ objectFit: "contain"
2030
+ }
2031
+ }
2032
+ ) }),
2033
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "0 20px 12px", display: "flex", flexWrap: "wrap", gap: 6 }, children: sortedVariants.map((v) => {
2034
+ const active = v.variant === selected.variant;
2035
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2036
+ "button",
2037
+ {
2038
+ onClick: () => setSelected(v),
2039
+ style: {
2040
+ padding: "4px 12px",
2041
+ borderRadius: 14,
2042
+ border: active ? "1px solid #1976d2" : "1px solid #ddd",
2043
+ backgroundColor: active ? "#e3f2fd" : "#fff",
2044
+ color: active ? "#1976d2" : "#555",
2045
+ fontSize: 12,
2046
+ fontFamily: FONT2,
2047
+ cursor: "pointer"
2048
+ },
2049
+ title: describeVariant(v.variant),
2050
+ children: [
2051
+ v.variant,
2052
+ v.size_bytes > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { opacity: 0.6, marginLeft: 4 }, children: formatBytes(v.size_bytes) })
2053
+ ]
2054
+ },
2055
+ v.variant
2056
+ );
2057
+ }) }),
2058
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "0 20px 16px" }, children: [
2059
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: 8, fontSize: 13 }, children: [
2060
+ /* @__PURE__ */ jsxRuntime.jsx(MetaItem, { label: "Size", value: formatBytes(selected.size_bytes) }),
2061
+ selected.width > 0 && /* @__PURE__ */ jsxRuntime.jsx(MetaItem, { label: "Dimensions", value: `${selected.width} x ${selected.height}` }),
2062
+ /* @__PURE__ */ jsxRuntime.jsx(MetaItem, { label: "Format", value: selected.format || selected.content_type }),
2063
+ /* @__PURE__ */ jsxRuntime.jsx(
2064
+ MetaItem,
2065
+ {
2066
+ label: "ID",
2067
+ value: image.imageId.slice(0, 12) + "...",
2068
+ onClick: () => copyText(image.imageId, "id"),
2069
+ actionLabel: copiedField === "id" ? "Copied" : "Copy"
2070
+ }
2071
+ ),
2072
+ selected?.objectURL && /* @__PURE__ */ jsxRuntime.jsx(
2073
+ MetaItem,
2074
+ {
2075
+ label: "URL",
2076
+ value: new URL(selected.objectURL).pathname.split("/").pop() || "...",
2077
+ onClick: () => copyText(selected.objectURL, "url"),
2078
+ actionLabel: copiedField === "url" ? "Copied" : "Copy"
2079
+ }
2080
+ )
2081
+ ] }),
2082
+ image.description && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginTop: 10, fontSize: 13, color: "#666" }, children: image.description }),
2083
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 6, fontSize: 12, color: "#999" }, children: [
2084
+ "Uploaded ",
2085
+ new Date(image.uploadedAt).toLocaleDateString()
2086
+ ] })
2087
+ ] }),
2088
+ /* @__PURE__ */ jsxRuntime.jsxs(
2089
+ "div",
2090
+ {
2091
+ style: {
2092
+ display: "flex",
2093
+ justifyContent: "space-between",
2094
+ padding: "12px 20px 16px",
2095
+ borderTop: "1px solid #eee",
2096
+ gap: 8
2097
+ },
2098
+ children: [
2099
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 8 }, children: [
2100
+ onEdit && /* @__PURE__ */ jsxRuntime.jsx(
2101
+ "button",
2102
+ {
2103
+ onClick: () => onEdit(image),
2104
+ style: {
2105
+ padding: "6px 14px",
2106
+ borderRadius: 6,
2107
+ border: "1px solid #ddd",
2108
+ backgroundColor: "#fff",
2109
+ fontSize: 13,
2110
+ fontFamily: FONT2,
2111
+ cursor: "pointer",
2112
+ color: "#333"
2113
+ },
2114
+ children: "Edit"
2115
+ }
2116
+ ),
2117
+ onDelete && /* @__PURE__ */ jsxRuntime.jsx(
2118
+ "button",
2119
+ {
2120
+ onClick: () => onDelete(image),
2121
+ style: {
2122
+ padding: "6px 14px",
2123
+ borderRadius: 6,
2124
+ border: "1px solid #ffcdd2",
2125
+ backgroundColor: "#fff",
2126
+ fontSize: 13,
2127
+ fontFamily: FONT2,
2128
+ cursor: "pointer",
2129
+ color: "#d32f2f"
2130
+ },
2131
+ children: "Delete"
2132
+ }
2133
+ )
2134
+ ] }),
2135
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 8 }, children: [
2136
+ selected?.objectURL && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2137
+ isSafeURL(selected.objectURL) && /* @__PURE__ */ jsxRuntime.jsx(
2138
+ "a",
2139
+ {
2140
+ href: selected.objectURL,
2141
+ target: "_blank",
2142
+ rel: "noopener noreferrer",
2143
+ style: {
2144
+ padding: "6px 14px",
2145
+ borderRadius: 6,
2146
+ border: "1px solid #ddd",
2147
+ backgroundColor: "#fff",
2148
+ fontSize: 13,
2149
+ fontFamily: FONT2,
2150
+ cursor: "pointer",
2151
+ color: "#333",
2152
+ textDecoration: "none",
2153
+ display: "inline-flex",
2154
+ alignItems: "center"
2155
+ },
2156
+ children: "Open"
2157
+ }
2158
+ ),
2159
+ /* @__PURE__ */ jsxRuntime.jsx(
2160
+ "button",
2161
+ {
2162
+ onClick: handleDownload,
2163
+ style: {
2164
+ padding: "6px 14px",
2165
+ borderRadius: 6,
2166
+ border: "1px solid #ddd",
2167
+ backgroundColor: "#fff",
2168
+ fontSize: 13,
2169
+ fontFamily: FONT2,
2170
+ cursor: "pointer",
2171
+ color: "#333"
2172
+ },
2173
+ children: "Download"
2174
+ }
2175
+ )
2176
+ ] }),
2177
+ /* @__PURE__ */ jsxRuntime.jsx(
2178
+ "button",
2179
+ {
2180
+ onClick: onClose,
2181
+ style: {
2182
+ padding: "6px 14px",
2183
+ borderRadius: 6,
2184
+ border: "1px solid #ddd",
2185
+ backgroundColor: "#fafafa",
2186
+ fontSize: 13,
2187
+ fontFamily: FONT2,
2188
+ cursor: "pointer",
2189
+ color: "#333"
2190
+ },
2191
+ children: "Close"
2192
+ }
2193
+ )
2194
+ ] })
2195
+ ]
2196
+ }
2197
+ )
2198
+ ]
2199
+ }
2200
+ )
2201
+ }
2202
+ );
2203
+ }
2204
+ function MetaItem({
2205
+ label,
2206
+ value,
2207
+ onClick,
2208
+ actionLabel
2209
+ }) {
2210
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2211
+ "span",
2212
+ {
2213
+ style: {
2214
+ display: "inline-flex",
2215
+ alignItems: "center",
2216
+ gap: 4,
2217
+ padding: "3px 10px",
2218
+ borderRadius: 10,
2219
+ backgroundColor: "#f5f5f5",
2220
+ color: "#555",
2221
+ fontSize: 12
2222
+ },
2223
+ children: [
2224
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 500, color: "#888" }, children: [
2225
+ label,
2226
+ ":"
2227
+ ] }),
2228
+ " ",
2229
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontFamily: "monospace" }, children: value }),
2230
+ onClick && /* @__PURE__ */ jsxRuntime.jsx(
2231
+ "button",
2232
+ {
2233
+ onClick,
2234
+ style: {
2235
+ background: "none",
2236
+ border: "none",
2237
+ color: "#1976d2",
2238
+ cursor: "pointer",
2239
+ fontSize: 11,
2240
+ padding: "0 2px",
2241
+ fontFamily: FONT2
2242
+ },
2243
+ children: actionLabel
2244
+ }
2245
+ )
2246
+ ]
2247
+ }
2248
+ );
2249
+ }
2250
+ var FONT3 = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
2251
+ var SKELETON_STYLE_ID = "mr-skeleton-keyframes";
2252
+ function useSkeletonStyle() {
2253
+ React.useEffect(() => {
2254
+ if (document.getElementById(SKELETON_STYLE_ID)) return;
2255
+ const style = document.createElement("style");
2256
+ style.id = SKELETON_STYLE_ID;
2257
+ style.textContent = "@keyframes mr-skeleton-pulse{0%,100%{opacity:1}50%{opacity:.4}}";
2258
+ document.head.appendChild(style);
2259
+ }, []);
2260
+ }
2261
+ function ImagePicker({
2262
+ onSelect,
2263
+ onClose,
2264
+ mode = "inline",
2265
+ pageSize = 20,
2266
+ showSearch = true,
2267
+ searchPlaceholder = "Search images...",
2268
+ selectedImageId,
2269
+ columns = 4,
2270
+ showActions = false,
2271
+ refType,
2272
+ refId,
2273
+ refreshSignal,
2274
+ className,
2275
+ style
2276
+ }) {
2277
+ const {
2278
+ images,
2279
+ page,
2280
+ pageCount,
2281
+ total,
2282
+ loading,
2283
+ error,
2284
+ setPage,
2285
+ setQuery,
2286
+ refetch,
2287
+ available,
2288
+ removeImage,
2289
+ updateImage: updateImage2
2290
+ } = useImages({ pageSize, refType, refId });
2291
+ useSkeletonStyle();
2292
+ const [searchInput, setSearchInput] = React.useState("");
2293
+ const [activeTile, setActiveTile] = React.useState(null);
2294
+ const [detailImage, setDetailImage] = React.useState(null);
2295
+ const [editImage, setEditImage] = React.useState(null);
2296
+ const [editTitle, setEditTitle] = React.useState("");
2297
+ const [editDesc, setEditDesc] = React.useState("");
2298
+ const [editSaving, setEditSaving] = React.useState(false);
2299
+ const [deleteConfirm, setDeleteConfirm] = React.useState(null);
2300
+ const [deleting, setDeleting] = React.useState(false);
2301
+ const [actionError, setActionError] = React.useState(null);
2302
+ React.useEffect(() => {
2303
+ const timer = setTimeout(() => setQuery(searchInput), 300);
2304
+ return () => clearTimeout(timer);
2305
+ }, [searchInput, setQuery]);
2306
+ const prevSignal = React.useRef(refreshSignal);
2307
+ React.useEffect(() => {
2308
+ if (refreshSignal != null && refreshSignal !== prevSignal.current) {
2309
+ prevSignal.current = refreshSignal;
2310
+ refetch();
2311
+ }
2312
+ }, [refreshSignal, refetch]);
2313
+ React.useEffect(() => {
2314
+ if (!activeTile) return;
2315
+ const handler = () => setActiveTile(null);
2316
+ document.addEventListener("click", handler);
2317
+ return () => document.removeEventListener("click", handler);
2318
+ }, [activeTile]);
2319
+ const handleKeyDown = React.useCallback(
2320
+ (e) => {
2321
+ if (e.key === "Escape") onClose?.();
2322
+ },
2323
+ [onClose]
2324
+ );
2325
+ const handleDelete = React.useCallback(
2326
+ async (img) => {
2327
+ setDeleting(true);
2328
+ setActionError(null);
2329
+ try {
2330
+ await removeImage(img.imageId);
2331
+ setDeleteConfirm(null);
2332
+ setDetailImage(null);
2333
+ } catch (err) {
2334
+ setActionError(err.message || "Delete failed");
2335
+ } finally {
2336
+ setDeleting(false);
2337
+ }
2338
+ },
2339
+ [removeImage]
2340
+ );
2341
+ const handleEditSave = React.useCallback(async () => {
2342
+ if (!editImage || !editTitle.trim()) return;
2343
+ setEditSaving(true);
2344
+ setActionError(null);
2345
+ try {
2346
+ await updateImage2(editImage.imageId, {
2347
+ title: editTitle.trim(),
2348
+ description: editDesc.trim() || void 0
2349
+ });
2350
+ setEditImage(null);
2351
+ } catch (err) {
2352
+ setActionError(err.message || "Update failed");
2353
+ } finally {
2354
+ setEditSaving(false);
2355
+ }
2356
+ }, [editImage, editTitle, editDesc, updateImage2]);
2357
+ const openEdit = React.useCallback((img) => {
2358
+ setEditTitle(img.title);
2359
+ setEditDesc(img.description || "");
2360
+ setEditImage(img);
2361
+ setDetailImage(null);
2362
+ setActionError(null);
2363
+ }, []);
2364
+ const openDelete = React.useCallback((img) => {
2365
+ setDeleteConfirm(img);
2366
+ setDetailImage(null);
2367
+ setActionError(null);
2368
+ }, []);
2369
+ if (!available) return null;
2370
+ const skeletonCount = Math.min(pageSize, 8);
2371
+ const content = /* @__PURE__ */ jsxRuntime.jsxs(
2372
+ "div",
2373
+ {
2374
+ className,
2375
+ style: {
2376
+ fontFamily: FONT3,
2377
+ display: "flex",
2378
+ flexDirection: "column",
2379
+ gap: 12,
2380
+ ...!mode || mode === "inline" ? style : {}
2381
+ },
2382
+ onKeyDown: handleKeyDown,
2383
+ children: [
2384
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
2385
+ showSearch && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { flex: 1, position: "relative" }, children: [
2386
+ /* @__PURE__ */ jsxRuntime.jsx(
2387
+ "input",
2388
+ {
2389
+ type: "text",
2390
+ placeholder: searchPlaceholder,
2391
+ value: searchInput,
2392
+ onChange: (e) => setSearchInput(e.target.value),
2393
+ style: {
2394
+ width: "100%",
2395
+ padding: "8px 12px",
2396
+ paddingRight: searchInput ? 32 : 12,
2397
+ borderRadius: 6,
2398
+ border: "1px solid #ddd",
2399
+ fontSize: 14,
2400
+ fontFamily: FONT3,
2401
+ outline: "none",
2402
+ boxSizing: "border-box"
2403
+ }
2404
+ }
2405
+ ),
2406
+ searchInput && /* @__PURE__ */ jsxRuntime.jsx(
2407
+ "button",
2408
+ {
2409
+ onClick: () => setSearchInput(""),
2410
+ style: {
2411
+ position: "absolute",
2412
+ right: 6,
2413
+ top: "50%",
2414
+ transform: "translateY(-50%)",
2415
+ background: "none",
2416
+ border: "none",
2417
+ fontSize: 16,
2418
+ cursor: "pointer",
2419
+ color: "#999",
2420
+ lineHeight: 1,
2421
+ padding: "2px 4px"
2422
+ },
2423
+ children: "\xD7"
2424
+ }
2425
+ )
2426
+ ] }),
2427
+ mode === "modal" && onClose && /* @__PURE__ */ jsxRuntime.jsx(
2428
+ "button",
2429
+ {
2430
+ onClick: onClose,
2431
+ style: {
2432
+ background: "none",
2433
+ border: "none",
2434
+ fontSize: 20,
2435
+ cursor: "pointer",
2436
+ padding: "4px 8px",
2437
+ color: "#666",
2438
+ lineHeight: 1
2439
+ },
2440
+ children: "\xD7"
2441
+ }
2442
+ )
2443
+ ] }),
2444
+ actionError && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "#d32f2f", fontSize: 13, padding: "4px 0" }, children: actionError }),
2445
+ loading && /* @__PURE__ */ jsxRuntime.jsx(
2446
+ "div",
2447
+ {
2448
+ style: {
2449
+ display: "grid",
2450
+ gridTemplateColumns: `repeat(${columns}, 1fr)`,
2451
+ gap: 8
2452
+ },
2453
+ children: Array.from({ length: skeletonCount }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(
2454
+ "div",
2455
+ {
2456
+ style: {
2457
+ aspectRatio: "1",
2458
+ borderRadius: 6,
2459
+ backgroundColor: "#f0f0f0",
2460
+ animation: "mr-skeleton-pulse 1.5s ease-in-out infinite"
2461
+ }
2462
+ },
2463
+ i
2464
+ ))
2465
+ }
2466
+ ),
2467
+ error && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { textAlign: "center", padding: 20, color: "#d32f2f", fontSize: 14 }, children: error }),
2468
+ !loading && !error && images.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { textAlign: "center", padding: "40px 20px", color: "#999" }, children: [
2469
+ /* @__PURE__ */ jsxRuntime.jsxs(
2470
+ "svg",
2471
+ {
2472
+ width: "36",
2473
+ height: "36",
2474
+ viewBox: "0 0 24 24",
2475
+ fill: "none",
2476
+ stroke: "currentColor",
2477
+ strokeWidth: "1.2",
2478
+ strokeLinecap: "round",
2479
+ strokeLinejoin: "round",
2480
+ style: { opacity: 0.4, marginBottom: 8 },
2481
+ children: [
2482
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
2483
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
2484
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "21 15 16 10 5 21" })
2485
+ ]
2486
+ }
2487
+ ),
2488
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 14, fontWeight: 500, color: "#666" }, children: "No images yet" }),
2489
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 13, marginTop: 4 }, children: searchInput ? "Try a different search term" : "Upload an image to get started" })
2490
+ ] }),
2491
+ !loading && images.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
2492
+ "div",
2493
+ {
2494
+ style: {
2495
+ display: "grid",
2496
+ gridTemplateColumns: `repeat(${columns}, 1fr)`,
2497
+ gap: 8
2498
+ },
2499
+ children: images.map((img) => {
2500
+ const isSelected = img.imageId === selectedImageId;
2501
+ const isActive = activeTile === img.imageId;
2502
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2503
+ "div",
2504
+ {
2505
+ style: {
2506
+ position: "relative",
2507
+ borderRadius: 6,
2508
+ overflow: "hidden",
2509
+ border: isSelected ? "2px solid #1976d2" : "2px solid transparent",
2510
+ transition: "border-color 0.15s ease",
2511
+ aspectRatio: "1"
2512
+ },
2513
+ onMouseEnter: () => setActiveTile(img.imageId),
2514
+ onMouseLeave: () => setActiveTile(null),
2515
+ onClick: (e) => {
2516
+ e.stopPropagation();
2517
+ },
2518
+ children: [
2519
+ /* @__PURE__ */ jsxRuntime.jsx(
2520
+ "div",
2521
+ {
2522
+ onClick: () => {
2523
+ if (showActions && !isActive) {
2524
+ setActiveTile(img.imageId);
2525
+ } else {
2526
+ onSelect?.(img);
2527
+ }
2528
+ },
2529
+ style: {
2530
+ cursor: "pointer",
2531
+ width: "100%",
2532
+ height: "100%"
2533
+ },
2534
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2535
+ MrImage,
2536
+ {
2537
+ image: img,
2538
+ variant: "sq200",
2539
+ style: {
2540
+ width: "100%",
2541
+ height: "100%",
2542
+ objectFit: "cover",
2543
+ display: "block"
2544
+ },
2545
+ alt: img.title
2546
+ }
2547
+ )
2548
+ }
2549
+ ),
2550
+ /* @__PURE__ */ jsxRuntime.jsx(
2551
+ "div",
2552
+ {
2553
+ style: {
2554
+ position: "absolute",
2555
+ bottom: 0,
2556
+ left: 0,
2557
+ right: 0,
2558
+ padding: "16px 6px 4px",
2559
+ background: "linear-gradient(transparent, rgba(0,0,0,0.5))",
2560
+ color: "#fff",
2561
+ fontSize: 11,
2562
+ whiteSpace: "nowrap",
2563
+ overflow: "hidden",
2564
+ textOverflow: "ellipsis",
2565
+ pointerEvents: "none"
2566
+ },
2567
+ children: img.title
2568
+ }
2569
+ ),
2570
+ showActions && isActive && /* @__PURE__ */ jsxRuntime.jsxs(
2571
+ "div",
2572
+ {
2573
+ style: {
2574
+ position: "absolute",
2575
+ inset: 0,
2576
+ display: "flex",
2577
+ alignItems: "flex-end",
2578
+ justifyContent: "center",
2579
+ gap: 6,
2580
+ padding: "8px 8px 24px",
2581
+ background: "linear-gradient(transparent 30%, rgba(0,0,0,0.55))"
2582
+ },
2583
+ children: [
2584
+ /* @__PURE__ */ jsxRuntime.jsx(ActionBtn, { label: "Info", onClick: () => setDetailImage(img) }),
2585
+ /* @__PURE__ */ jsxRuntime.jsx(ActionBtn, { label: "Edit", onClick: () => openEdit(img) }),
2586
+ /* @__PURE__ */ jsxRuntime.jsx(ActionBtn, { label: "Del", onClick: () => openDelete(img), color: "#ff8a80" })
2587
+ ]
2588
+ }
2589
+ )
2590
+ ]
2591
+ },
2592
+ img.imageId
2593
+ );
2594
+ })
2595
+ }
2596
+ ),
2597
+ pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsxs(
2598
+ "div",
2599
+ {
2600
+ style: {
2601
+ display: "flex",
2602
+ justifyContent: "center",
2603
+ alignItems: "center",
2604
+ gap: 12,
2605
+ fontSize: 14
2606
+ },
2607
+ children: [
2608
+ /* @__PURE__ */ jsxRuntime.jsx(
2609
+ "button",
2610
+ {
2611
+ disabled: page <= 0,
2612
+ onClick: () => setPage(page - 1),
2613
+ style: {
2614
+ padding: "4px 12px",
2615
+ borderRadius: 4,
2616
+ border: "1px solid #ddd",
2617
+ background: page <= 0 ? "#f5f5f5" : "#fff",
2618
+ cursor: page <= 0 ? "default" : "pointer",
2619
+ fontFamily: FONT3,
2620
+ fontSize: 13,
2621
+ color: page <= 0 ? "#bbb" : "#333"
2622
+ },
2623
+ children: "Prev"
2624
+ }
2625
+ ),
2626
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { color: "#666", fontSize: 13 }, children: [
2627
+ "Page ",
2628
+ page + 1,
2629
+ " of ",
2630
+ pageCount,
2631
+ " \xB7 ",
2632
+ total,
2633
+ " image",
2634
+ total !== 1 ? "s" : ""
2635
+ ] }),
2636
+ /* @__PURE__ */ jsxRuntime.jsx(
2637
+ "button",
2638
+ {
2639
+ disabled: page >= pageCount - 1,
2640
+ onClick: () => setPage(page + 1),
2641
+ style: {
2642
+ padding: "4px 12px",
2643
+ borderRadius: 4,
2644
+ border: "1px solid #ddd",
2645
+ background: page >= pageCount - 1 ? "#f5f5f5" : "#fff",
2646
+ cursor: page >= pageCount - 1 ? "default" : "pointer",
2647
+ fontFamily: FONT3,
2648
+ fontSize: 13,
2649
+ color: page >= pageCount - 1 ? "#bbb" : "#333"
2650
+ },
2651
+ children: "Next"
2652
+ }
2653
+ )
2654
+ ]
2655
+ }
2656
+ ),
2657
+ detailImage && /* @__PURE__ */ jsxRuntime.jsx(
2658
+ ImageDetails,
2659
+ {
2660
+ image: detailImage,
2661
+ onClose: () => setDetailImage(null),
2662
+ onEdit: showActions ? openEdit : void 0,
2663
+ onDelete: showActions ? openDelete : void 0
2664
+ }
2665
+ ),
2666
+ editImage && /* @__PURE__ */ jsxRuntime.jsx(
2667
+ "div",
2668
+ {
2669
+ style: {
2670
+ position: "fixed",
2671
+ inset: 0,
2672
+ zIndex: 1e4,
2673
+ display: "flex",
2674
+ alignItems: "center",
2675
+ justifyContent: "center",
2676
+ backgroundColor: "rgba(0,0,0,0.4)"
2677
+ },
2678
+ onClick: (e) => {
2679
+ if (e.target === e.currentTarget && !editSaving) setEditImage(null);
2680
+ },
2681
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
2682
+ "div",
2683
+ {
2684
+ style: {
2685
+ backgroundColor: "#fff",
2686
+ borderRadius: 10,
2687
+ padding: 20,
2688
+ maxWidth: 400,
2689
+ width: "90vw",
2690
+ fontFamily: FONT3
2691
+ },
2692
+ children: [
2693
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontWeight: 600, fontSize: 15, marginBottom: 12 }, children: "Edit Image" }),
2694
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
2695
+ /* @__PURE__ */ jsxRuntime.jsx(
2696
+ "input",
2697
+ {
2698
+ type: "text",
2699
+ placeholder: "Title",
2700
+ value: editTitle,
2701
+ onChange: (e) => setEditTitle(e.target.value),
2702
+ disabled: editSaving,
2703
+ maxLength: 250,
2704
+ style: {
2705
+ padding: "8px 12px",
2706
+ borderRadius: 6,
2707
+ border: "1px solid #ddd",
2708
+ fontSize: 14,
2709
+ fontFamily: FONT3,
2710
+ outline: "none"
2711
+ }
2712
+ }
2713
+ ),
2714
+ /* @__PURE__ */ jsxRuntime.jsx(
2715
+ "input",
2716
+ {
2717
+ type: "text",
2718
+ placeholder: "Description (optional)",
2719
+ value: editDesc,
2720
+ onChange: (e) => setEditDesc(e.target.value),
2721
+ disabled: editSaving,
2722
+ maxLength: 500,
2723
+ style: {
2724
+ padding: "8px 12px",
2725
+ borderRadius: 6,
2726
+ border: "1px solid #ddd",
2727
+ fontSize: 14,
2728
+ fontFamily: FONT3,
2729
+ outline: "none"
2730
+ }
2731
+ }
2732
+ )
2733
+ ] }),
2734
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 16 }, children: [
2735
+ /* @__PURE__ */ jsxRuntime.jsx(
2736
+ "button",
2737
+ {
2738
+ onClick: () => setEditImage(null),
2739
+ disabled: editSaving,
2740
+ style: {
2741
+ padding: "6px 14px",
2742
+ borderRadius: 6,
2743
+ border: "1px solid #ddd",
2744
+ backgroundColor: "#fff",
2745
+ fontSize: 13,
2746
+ fontFamily: FONT3,
2747
+ cursor: "pointer",
2748
+ color: "#333"
2749
+ },
2750
+ children: "Cancel"
2751
+ }
2752
+ ),
2753
+ /* @__PURE__ */ jsxRuntime.jsx(
2754
+ "button",
2755
+ {
2756
+ onClick: handleEditSave,
2757
+ disabled: editSaving || !editTitle.trim(),
2758
+ style: {
2759
+ padding: "6px 14px",
2760
+ borderRadius: 6,
2761
+ border: "none",
2762
+ backgroundColor: editSaving || !editTitle.trim() ? "#bbb" : "#1976d2",
2763
+ color: "#fff",
2764
+ fontSize: 13,
2765
+ fontFamily: FONT3,
2766
+ cursor: editSaving ? "default" : "pointer"
2767
+ },
2768
+ children: editSaving ? "Saving..." : "Save"
2769
+ }
2770
+ )
2771
+ ] })
2772
+ ]
2773
+ }
2774
+ )
2775
+ }
2776
+ ),
2777
+ deleteConfirm && /* @__PURE__ */ jsxRuntime.jsx(
2778
+ "div",
2779
+ {
2780
+ style: {
2781
+ position: "fixed",
2782
+ inset: 0,
2783
+ zIndex: 1e4,
2784
+ display: "flex",
2785
+ alignItems: "center",
2786
+ justifyContent: "center",
2787
+ backgroundColor: "rgba(0,0,0,0.4)"
2788
+ },
2789
+ onClick: (e) => {
2790
+ if (e.target === e.currentTarget && !deleting) setDeleteConfirm(null);
2791
+ },
2792
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
2793
+ "div",
2794
+ {
2795
+ style: {
2796
+ backgroundColor: "#fff",
2797
+ borderRadius: 10,
2798
+ padding: 20,
2799
+ maxWidth: 360,
2800
+ width: "90vw",
2801
+ fontFamily: FONT3
2802
+ },
2803
+ children: [
2804
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontWeight: 600, fontSize: 15, marginBottom: 8 }, children: "Delete Image" }),
2805
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { fontSize: 14, color: "#555", marginBottom: 16 }, children: [
2806
+ "Delete \u201C",
2807
+ deleteConfirm.title,
2808
+ "\u201D? This removes all variants and cannot be undone."
2809
+ ] }),
2810
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8 }, children: [
2811
+ /* @__PURE__ */ jsxRuntime.jsx(
2812
+ "button",
2813
+ {
2814
+ onClick: () => setDeleteConfirm(null),
2815
+ disabled: deleting,
2816
+ style: {
2817
+ padding: "6px 14px",
2818
+ borderRadius: 6,
2819
+ border: "1px solid #ddd",
2820
+ backgroundColor: "#fff",
2821
+ fontSize: 13,
2822
+ fontFamily: FONT3,
2823
+ cursor: "pointer",
2824
+ color: "#333"
2825
+ },
2826
+ children: "Cancel"
2827
+ }
2828
+ ),
2829
+ /* @__PURE__ */ jsxRuntime.jsx(
2830
+ "button",
2831
+ {
2832
+ onClick: () => handleDelete(deleteConfirm),
2833
+ disabled: deleting,
2834
+ style: {
2835
+ padding: "6px 14px",
2836
+ borderRadius: 6,
2837
+ border: "none",
2838
+ backgroundColor: deleting ? "#bbb" : "#d32f2f",
2839
+ color: "#fff",
2840
+ fontSize: 13,
2841
+ fontFamily: FONT3,
2842
+ cursor: deleting ? "default" : "pointer"
2843
+ },
2844
+ children: deleting ? "Deleting..." : "Delete"
2845
+ }
2846
+ )
2847
+ ] })
2848
+ ]
2849
+ }
2850
+ )
2851
+ }
2852
+ )
2853
+ ]
2854
+ }
2855
+ );
2856
+ if (mode === "modal") {
2857
+ return /* @__PURE__ */ jsxRuntime.jsx(
2858
+ "div",
2859
+ {
2860
+ style: {
2861
+ position: "fixed",
2862
+ inset: 0,
2863
+ zIndex: 9999,
2864
+ display: "flex",
2865
+ alignItems: "center",
2866
+ justifyContent: "center",
2867
+ backgroundColor: "rgba(0,0,0,0.4)"
2868
+ },
2869
+ onClick: (e) => {
2870
+ if (e.target === e.currentTarget) onClose?.();
2871
+ },
2872
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2873
+ "div",
2874
+ {
2875
+ style: {
2876
+ backgroundColor: "#fff",
2877
+ borderRadius: 12,
2878
+ padding: 20,
2879
+ maxWidth: 640,
2880
+ width: "90vw",
2881
+ maxHeight: "80vh",
2882
+ overflow: "auto",
2883
+ boxShadow: "0 8px 32px rgba(0,0,0,0.2)",
2884
+ ...style
2885
+ },
2886
+ children: content
2887
+ }
2888
+ )
2889
+ }
2890
+ );
2891
+ }
2892
+ return content;
2893
+ }
2894
+ function ActionBtn({
2895
+ label,
2896
+ onClick,
2897
+ color
2898
+ }) {
2899
+ return /* @__PURE__ */ jsxRuntime.jsx(
2900
+ "button",
2901
+ {
2902
+ onClick: (e) => {
2903
+ e.stopPropagation();
2904
+ onClick();
2905
+ },
2906
+ style: {
2907
+ padding: "6px 12px",
2908
+ borderRadius: 4,
2909
+ border: "none",
2910
+ backgroundColor: "rgba(255,255,255,0.2)",
2911
+ color: color || "#fff",
2912
+ fontSize: 12,
2913
+ fontWeight: 500,
2914
+ cursor: "pointer",
2915
+ fontFamily: 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
2916
+ minHeight: 32,
2917
+ backdropFilter: "blur(4px)"
2918
+ },
2919
+ children: label
2920
+ }
2921
+ );
2922
+ }
2923
+ function useFilesConfig() {
2924
+ const { snapshot } = useAppKit();
2925
+ const [filesBaseURL, setFilesBaseURL] = React.useState(null);
2926
+ const [loading, setLoading] = React.useState(true);
2927
+ const [error, setError] = React.useState(null);
2928
+ const fetchedRef = React.useRef("");
2929
+ const retryCountRef = React.useRef(0);
2930
+ const maxRetries = 3;
2931
+ const retryDelayMs = 5e3;
2932
+ const workspaceBaseURL = snapshot?.workspaceBaseURL;
2933
+ const appId = snapshot?.appId;
2934
+ React.useEffect(() => {
2935
+ const cacheKey = workspaceBaseURL && appId ? `${workspaceBaseURL}|${appId}` : "";
2936
+ if (fetchedRef.current !== cacheKey) {
2937
+ fetchedRef.current = "";
2938
+ retryCountRef.current = 0;
2939
+ }
2940
+ }, [workspaceBaseURL, appId]);
2941
+ React.useEffect(() => {
2942
+ if (!workspaceBaseURL || !appId) {
2943
+ setLoading(false);
2944
+ return;
2945
+ }
2946
+ const cacheKey = `${workspaceBaseURL}|${appId}`;
2947
+ if (fetchedRef.current === cacheKey) return;
2948
+ let cancelled = false;
2949
+ let retryTimer = null;
2950
+ setLoading(true);
2951
+ setError(null);
2952
+ const doFetch = () => {
2953
+ fetchAppResource(workspaceBaseURL, appId).then((data) => {
2954
+ if (cancelled) return;
2955
+ setFilesBaseURL(data.filesBaseURL);
2956
+ setLoading(false);
2957
+ fetchedRef.current = cacheKey;
2958
+ retryCountRef.current = 0;
2959
+ }).catch((err) => {
2960
+ if (cancelled) return;
2961
+ retryCountRef.current += 1;
2962
+ if (retryCountRef.current < maxRetries) {
2963
+ retryTimer = setTimeout(doFetch, retryDelayMs);
2964
+ } else {
2965
+ setError(err.message);
2966
+ setLoading(false);
2967
+ }
2968
+ });
2969
+ };
2970
+ doFetch();
2971
+ return () => {
2972
+ cancelled = true;
2973
+ if (retryTimer != null) clearTimeout(retryTimer);
2974
+ };
2975
+ }, [workspaceBaseURL, appId]);
2976
+ return { filesBaseURL, loading, error };
2977
+ }
2978
+
2979
+ // src/files/api.ts
2980
+ function headers2(jwt) {
2981
+ return { Authorization: `Bearer ${jwt}` };
2982
+ }
2983
+ function apiBase2(p) {
2984
+ return `${p.filesBaseURL}/api/${encodeURIComponent(p.appId)}`;
2985
+ }
2986
+ async function listFiles(p, opts) {
2987
+ const url = new URL(`${apiBase2(p)}/files`);
2988
+ if (opts?.page != null) url.searchParams.set("page", String(opts.page));
2989
+ if (opts?.pageSize != null)
2990
+ url.searchParams.set("pageSize", String(opts.pageSize));
2991
+ if (opts?.q) url.searchParams.set("q", opts.q);
2992
+ if (opts?.refType) url.searchParams.set("refType", opts.refType);
2993
+ if (opts?.refId) url.searchParams.set("refId", opts.refId);
2994
+ const res = await fetch(url.toString(), { headers: headers2(p.jwtToken) });
2995
+ if (!res.ok) throw new Error(`List files failed: HTTP ${res.status}`);
2996
+ return res.json();
2997
+ }
2998
+ async function getFile(p, fileId) {
2999
+ const res = await fetch(`${apiBase2(p)}/files/${encodeURIComponent(fileId)}`, {
3000
+ headers: headers2(p.jwtToken)
3001
+ });
3002
+ if (!res.ok) throw new Error(`Get file failed: HTTP ${res.status}`);
3003
+ return res.json();
3004
+ }
3005
+ function uploadFile(p, opts, onProgress, signal) {
3006
+ return new Promise((resolve, reject) => {
3007
+ const xhr = new XMLHttpRequest();
3008
+ xhr.open("POST", `${apiBase2(p)}/files/upload`);
3009
+ xhr.setRequestHeader("Authorization", `Bearer ${p.jwtToken}`);
3010
+ if (signal) {
3011
+ signal.addEventListener("abort", () => xhr.abort(), { once: true });
3012
+ }
3013
+ xhr.upload.onprogress = (e) => {
3014
+ if (e.lengthComputable && onProgress) {
3015
+ const pct = Math.round(e.loaded / e.total * 100);
3016
+ onProgress(pct, e.loaded, e.total);
3017
+ }
3018
+ };
3019
+ xhr.onload = () => {
3020
+ let body = null;
3021
+ try {
3022
+ body = JSON.parse(xhr.responseText);
3023
+ } catch {
3024
+ }
3025
+ resolve({ status: xhr.status, body });
3026
+ };
3027
+ xhr.onerror = () => reject(new Error("Upload network error"));
3028
+ xhr.onabort = () => reject(new Error("Upload aborted"));
3029
+ const fd = new FormData();
3030
+ fd.append("file", opts.file);
3031
+ fd.append("title", opts.title);
3032
+ if (opts.description) fd.append("description", opts.description);
3033
+ if (opts.refType) fd.append("refType", opts.refType);
3034
+ if (opts.refId) fd.append("refId", opts.refId);
3035
+ xhr.send(fd);
3036
+ });
3037
+ }
3038
+ async function updateFile(p, fileId, opts) {
3039
+ const res = await fetch(`${apiBase2(p)}/files/${encodeURIComponent(fileId)}`, {
3040
+ method: "POST",
3041
+ headers: { ...headers2(p.jwtToken), "Content-Type": "application/json" },
3042
+ body: JSON.stringify(opts)
3043
+ });
3044
+ if (!res.ok) throw new Error(`Update file failed: HTTP ${res.status}`);
3045
+ }
3046
+ async function deleteFile(p, fileId) {
3047
+ const res = await fetch(`${apiBase2(p)}/files/${encodeURIComponent(fileId)}`, {
3048
+ method: "DELETE",
3049
+ headers: headers2(p.jwtToken)
3050
+ });
3051
+ if (!res.ok) throw new Error(`Delete file failed: HTTP ${res.status}`);
3052
+ }
3053
+
3054
+ // src/files/useFiles.ts
3055
+ function useFiles(opts) {
3056
+ const { snapshot } = useAppKit();
3057
+ const { filesBaseURL, loading: configLoading } = useFilesConfig();
3058
+ const [files, setFiles] = React.useState([]);
3059
+ const [page, setPageState] = React.useState(opts?.page ?? 0);
3060
+ const [pageSize] = React.useState(opts?.pageSize ?? 50);
3061
+ const [total, setTotal] = React.useState(0);
3062
+ const [pageCount, setPageCount] = React.useState(0);
3063
+ const [query, setQueryState] = React.useState(opts?.q ?? "");
3064
+ const [loading, setLoading] = React.useState(false);
3065
+ const [error, setError] = React.useState(null);
3066
+ const fetchIdRef = React.useRef(0);
3067
+ const enabled = opts?.enabled !== false;
3068
+ const jwtToken = snapshot?.jwtToken;
3069
+ const appId = snapshot?.appId;
3070
+ const available = !!filesBaseURL;
3071
+ const refType = opts?.refType;
3072
+ const refId = opts?.refId;
3073
+ const doFetch = React.useCallback(() => {
3074
+ if (!filesBaseURL || !appId || !jwtToken || !enabled) return;
3075
+ const id = ++fetchIdRef.current;
3076
+ setLoading(true);
3077
+ setError(null);
3078
+ listFiles(
3079
+ { filesBaseURL, appId, jwtToken },
3080
+ { page, pageSize, q: query || void 0, refType, refId }
3081
+ ).then((res) => {
3082
+ if (id !== fetchIdRef.current) return;
3083
+ setFiles(res.files ?? []);
3084
+ setTotal(res.total);
3085
+ setPageCount(res.pageCount);
3086
+ setLoading(false);
3087
+ }).catch((err) => {
3088
+ if (id !== fetchIdRef.current) return;
3089
+ setError(err.message);
3090
+ setLoading(false);
3091
+ });
3092
+ }, [filesBaseURL, appId, jwtToken, page, pageSize, query, refType, refId, enabled]);
3093
+ React.useEffect(() => {
3094
+ doFetch();
3095
+ return () => {
3096
+ fetchIdRef.current++;
3097
+ };
3098
+ }, [doFetch]);
3099
+ const setPage = React.useCallback((p) => setPageState(p), []);
3100
+ const setQuery = React.useCallback((q) => {
3101
+ setQueryState(q);
3102
+ setPageState(0);
3103
+ }, []);
3104
+ const removeFile = React.useCallback(
3105
+ async (fileId) => {
3106
+ if (!filesBaseURL || !appId || !jwtToken) return;
3107
+ await deleteFile({ filesBaseURL, appId, jwtToken }, fileId);
3108
+ doFetch();
3109
+ },
3110
+ [filesBaseURL, appId, jwtToken, doFetch]
3111
+ );
3112
+ const doUpdateFile = React.useCallback(
3113
+ async (fileId, updateOpts) => {
3114
+ if (!filesBaseURL || !appId || !jwtToken) return;
3115
+ await updateFile(
3116
+ { filesBaseURL, appId, jwtToken },
3117
+ fileId,
3118
+ updateOpts
3119
+ );
3120
+ doFetch();
3121
+ },
3122
+ [filesBaseURL, appId, jwtToken, doFetch]
3123
+ );
3124
+ return {
3125
+ files,
3126
+ page,
3127
+ pageSize,
3128
+ total,
3129
+ pageCount,
3130
+ loading: loading || configLoading,
3131
+ error,
3132
+ refetch: doFetch,
3133
+ setPage,
3134
+ setQuery,
3135
+ available,
3136
+ removeFile,
3137
+ updateFile: doUpdateFile
3138
+ };
3139
+ }
3140
+ function useFile(fileId, opts) {
3141
+ const { snapshot } = useAppKit();
3142
+ const { filesBaseURL, loading: configLoading } = useFilesConfig();
3143
+ const [file, setFile] = React.useState(null);
3144
+ const [loading, setLoading] = React.useState(false);
3145
+ const [error, setError] = React.useState(null);
3146
+ const fetchIdRef = React.useRef(0);
3147
+ const enabled = opts?.enabled !== false;
3148
+ const jwtToken = snapshot?.jwtToken;
3149
+ const appId = snapshot?.appId;
3150
+ const available = !!filesBaseURL;
3151
+ const doFetch = React.useCallback(() => {
3152
+ if (!filesBaseURL || !appId || !jwtToken || !fileId || !enabled) return;
3153
+ const id = ++fetchIdRef.current;
3154
+ setLoading(true);
3155
+ setError(null);
3156
+ getFile({ filesBaseURL, appId, jwtToken }, fileId).then((res) => {
3157
+ if (id !== fetchIdRef.current) return;
3158
+ setFile(res);
3159
+ setLoading(false);
3160
+ }).catch((err) => {
3161
+ if (id !== fetchIdRef.current) return;
3162
+ setError(err.message);
3163
+ setLoading(false);
3164
+ });
3165
+ }, [filesBaseURL, appId, jwtToken, fileId, enabled]);
3166
+ React.useEffect(() => {
3167
+ doFetch();
3168
+ return () => {
3169
+ fetchIdRef.current++;
3170
+ };
3171
+ }, [doFetch]);
3172
+ return {
3173
+ file,
3174
+ loading: loading || configLoading,
3175
+ error,
3176
+ refetch: doFetch,
3177
+ available
3178
+ };
3179
+ }
3180
+ var IDLE2 = {
3181
+ status: "idle",
3182
+ progress: 0,
3183
+ bytesUploaded: 0,
3184
+ bytesTotal: 0,
3185
+ error: null
3186
+ };
3187
+ function useFileUpload() {
3188
+ const { snapshot } = useAppKit();
3189
+ const { filesBaseURL } = useFilesConfig();
3190
+ const [progress, setProgress] = React.useState(IDLE2);
3191
+ const abortRef = React.useRef(null);
3192
+ const jwtToken = snapshot?.jwtToken;
3193
+ const appId = snapshot?.appId;
3194
+ const available = !!filesBaseURL;
3195
+ React.useEffect(() => {
3196
+ if (!jwtToken && progress.status === "uploading") {
3197
+ abortRef.current?.abort();
3198
+ }
3199
+ }, [jwtToken, progress.status]);
3200
+ const upload = React.useCallback(
3201
+ async (opts) => {
3202
+ if (!filesBaseURL || !appId || !jwtToken) {
3203
+ setProgress({
3204
+ ...IDLE2,
3205
+ status: "error",
3206
+ error: "Files service not available"
3207
+ });
3208
+ return;
3209
+ }
3210
+ abortRef.current?.abort();
3211
+ const controller = new AbortController();
3212
+ abortRef.current = controller;
3213
+ setProgress({
3214
+ status: "uploading",
3215
+ progress: 0,
3216
+ bytesUploaded: 0,
3217
+ bytesTotal: opts.file.size,
3218
+ error: null
3219
+ });
3220
+ try {
3221
+ const result = await uploadFile(
3222
+ { filesBaseURL, appId, jwtToken },
3223
+ opts,
3224
+ (pct, loaded, total) => {
3225
+ setProgress((prev) => ({
3226
+ ...prev,
3227
+ progress: pct,
3228
+ bytesUploaded: loaded,
3229
+ bytesTotal: total
3230
+ }));
3231
+ },
3232
+ controller.signal
3233
+ );
3234
+ if (result.status === 204) {
3235
+ setProgress({
3236
+ status: "success",
3237
+ progress: 100,
3238
+ bytesUploaded: opts.file.size,
3239
+ bytesTotal: opts.file.size,
3240
+ error: null
3241
+ });
3242
+ } else if (result.status === 409) {
3243
+ setProgress({
3244
+ status: "conflict",
3245
+ progress: 100,
3246
+ bytesUploaded: opts.file.size,
3247
+ bytesTotal: opts.file.size,
3248
+ error: result.body?.message || "A file with identical content already exists",
3249
+ existingFileId: result.body?.fileId
3250
+ });
3251
+ } else if (result.status === 413) {
3252
+ throw new Error("File exceeds 10MB limit");
3253
+ } else if (result.status === 415) {
3254
+ throw new Error(
3255
+ "Unsupported file type. Allowed: PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX, CSV, TXT, ZIP, GZ, JSON, XML"
3256
+ );
3257
+ } else if (result.status === 429) {
3258
+ throw new Error(result.body?.message || "Storage limit reached");
3259
+ } else {
3260
+ throw new Error(`Upload failed: HTTP ${result.status}`);
3261
+ }
3262
+ } catch (err) {
3263
+ if (err.message === "Upload aborted") return;
3264
+ setProgress({
3265
+ status: "error",
3266
+ progress: 0,
3267
+ bytesUploaded: 0,
3268
+ bytesTotal: opts.file.size,
3269
+ error: err.message || "Upload failed"
3270
+ });
3271
+ }
3272
+ },
3273
+ [filesBaseURL, appId, jwtToken]
3274
+ );
3275
+ const cancel = React.useCallback(() => {
3276
+ abortRef.current?.abort();
3277
+ setProgress(IDLE2);
3278
+ }, []);
3279
+ const reset = React.useCallback(() => {
3280
+ setProgress(IDLE2);
3281
+ }, []);
3282
+ return { upload, cancel, progress, reset, available };
3283
+ }
3284
+ var FONT4 = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3285
+ var ACCEPT_DEFAULT2 = "application/pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.csv,text/plain,.zip,.gz,application/json,.xml";
3286
+ var MAX_SIZE_DEFAULT2 = 10 * 1024 * 1024;
3287
+ function formatSize2(bytes) {
3288
+ if (bytes < 1024) return `${bytes} B`;
3289
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
3290
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
3291
+ }
3292
+ function FileUploader({
3293
+ onUpload,
3294
+ onError,
3295
+ defaultTitle,
3296
+ defaultDescription = "",
3297
+ showFields = true,
3298
+ accept = ACCEPT_DEFAULT2,
3299
+ maxSize = MAX_SIZE_DEFAULT2,
3300
+ refType,
3301
+ refId,
3302
+ className,
3303
+ style,
3304
+ label = "Drop a file here or click to select",
3305
+ disabled = false
3306
+ }) {
3307
+ const { upload, cancel, progress, reset, available } = useFileUpload();
3308
+ const fileInputRef = React.useRef(null);
3309
+ const [dragOver, setDragOver] = React.useState(false);
3310
+ const [title, setTitle] = React.useState(defaultTitle ?? "");
3311
+ const [description, setDescription] = React.useState(defaultDescription);
3312
+ const [selectedFile, setSelectedFile] = React.useState(null);
3313
+ const [validationError, setValidationError] = React.useState(null);
3314
+ const [showSuccess, setShowSuccess] = React.useState(false);
3315
+ const handleFile = React.useCallback(
3316
+ (file) => {
3317
+ setValidationError(null);
3318
+ if (file.size > maxSize) {
3319
+ setValidationError(
3320
+ `File too large (${(file.size / 1024 / 1024).toFixed(1)}MB). Max ${(maxSize / 1024 / 1024).toFixed(0)}MB.`
3321
+ );
3322
+ return;
3323
+ }
3324
+ setSelectedFile(file);
3325
+ if (!title) {
3326
+ setTitle(file.name.replace(/\.[^.]+$/, ""));
3327
+ }
3328
+ },
3329
+ [maxSize, title]
3330
+ );
3331
+ const handleDrop = React.useCallback(
3332
+ (e) => {
3333
+ e.preventDefault();
3334
+ setDragOver(false);
3335
+ const file = e.dataTransfer.files[0];
3336
+ if (file) handleFile(file);
3337
+ },
3338
+ [handleFile]
3339
+ );
3340
+ const handleSubmit = React.useCallback(async () => {
3341
+ if (!selectedFile || !title.trim()) return;
3342
+ reset();
3343
+ const opts = {
3344
+ file: selectedFile,
3345
+ title: title.trim(),
3346
+ description: description.trim() || void 0,
3347
+ refType,
3348
+ refId
3349
+ };
3350
+ await upload(opts);
3351
+ }, [selectedFile, title, description, refType, refId, upload, reset]);
3352
+ const prevStatus = React.useRef(progress.status);
3353
+ React.useEffect(() => {
3354
+ if (prevStatus.current === progress.status) return;
3355
+ prevStatus.current = progress.status;
3356
+ if (progress.status === "success") {
3357
+ setSelectedFile(null);
3358
+ setTitle(defaultTitle ?? "");
3359
+ setDescription(defaultDescription);
3360
+ setShowSuccess(true);
3361
+ onUpload?.();
3362
+ }
3363
+ if (progress.status === "conflict") {
3364
+ setSelectedFile(null);
3365
+ setTitle(defaultTitle ?? "");
3366
+ setDescription(defaultDescription);
3367
+ }
3368
+ if (progress.status === "error" && progress.error) {
3369
+ onError?.(progress.error);
3370
+ }
3371
+ }, [progress.status, progress.error, onUpload, onError, defaultTitle, defaultDescription]);
3372
+ React.useEffect(() => {
3373
+ if (!showSuccess) return;
3374
+ const t = setTimeout(() => setShowSuccess(false), 4e3);
3375
+ return () => clearTimeout(t);
3376
+ }, [showSuccess]);
3377
+ if (!available) return null;
3378
+ const isUploading = progress.status === "uploading";
3379
+ const isDisabled = disabled || isUploading;
3380
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: { fontFamily: FONT4, ...style }, children: [
3381
+ /* @__PURE__ */ jsxRuntime.jsxs(
3382
+ "div",
3383
+ {
3384
+ onDragEnter: (e) => {
3385
+ e.preventDefault();
3386
+ setDragOver(true);
3387
+ },
3388
+ onDragOver: (e) => {
3389
+ e.preventDefault();
3390
+ setDragOver(true);
3391
+ },
3392
+ onDragLeave: () => setDragOver(false),
3393
+ onDrop: handleDrop,
3394
+ onClick: () => !isDisabled && fileInputRef.current?.click(),
3395
+ style: {
3396
+ border: `2px dashed ${dragOver ? "#1976d2" : "#ccc"}`,
3397
+ borderRadius: 8,
3398
+ padding: "24px 16px",
3399
+ textAlign: "center",
3400
+ cursor: isDisabled ? "default" : "pointer",
3401
+ backgroundColor: dragOver ? "#e3f2fd" : "#fafafa",
3402
+ transition: "all 0.15s ease",
3403
+ opacity: isDisabled ? 0.6 : 1
3404
+ },
3405
+ children: [
3406
+ /* @__PURE__ */ jsxRuntime.jsx(
3407
+ "input",
3408
+ {
3409
+ ref: fileInputRef,
3410
+ type: "file",
3411
+ accept,
3412
+ style: { display: "none" },
3413
+ onChange: (e) => {
3414
+ const file = e.target.files?.[0];
3415
+ if (file) handleFile(file);
3416
+ e.target.value = "";
3417
+ }
3418
+ }
3419
+ ),
3420
+ !selectedFile && /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#999", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", style: { marginBottom: 6 }, children: [
3421
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
3422
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "17 8 12 3 7 8" }),
3423
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "3", x2: "12", y2: "15" })
3424
+ ] }),
3425
+ selectedFile && /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#666", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", style: { marginBottom: 6 }, children: [
3426
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
3427
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "14 2 14 8 20 8" })
3428
+ ] }),
3429
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 14, color: "#666" }, children: selectedFile ? selectedFile.name : label }),
3430
+ selectedFile && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 12, color: "#999", marginTop: 4 }, children: formatSize2(selectedFile.size) })
3431
+ ]
3432
+ }
3433
+ ),
3434
+ validationError && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "#d32f2f", fontSize: 13, marginTop: 8 }, children: validationError }),
3435
+ showFields && selectedFile && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 12, display: "flex", flexDirection: "column", gap: 8 }, children: [
3436
+ /* @__PURE__ */ jsxRuntime.jsx(
3437
+ "input",
3438
+ {
3439
+ type: "text",
3440
+ placeholder: "Title",
3441
+ value: title,
3442
+ onChange: (e) => setTitle(e.target.value),
3443
+ disabled: isDisabled,
3444
+ maxLength: 250,
3445
+ style: {
3446
+ padding: "8px 12px",
3447
+ borderRadius: 6,
3448
+ border: "1px solid #ddd",
3449
+ fontSize: 14,
3450
+ fontFamily: FONT4,
3451
+ outline: "none"
3452
+ }
3453
+ }
3454
+ ),
3455
+ /* @__PURE__ */ jsxRuntime.jsx(
3456
+ "input",
3457
+ {
3458
+ type: "text",
3459
+ placeholder: "Description (optional)",
3460
+ value: description,
3461
+ onChange: (e) => setDescription(e.target.value),
3462
+ disabled: isDisabled,
3463
+ maxLength: 500,
3464
+ style: {
3465
+ padding: "8px 12px",
3466
+ borderRadius: 6,
3467
+ border: "1px solid #ddd",
3468
+ fontSize: 14,
3469
+ fontFamily: FONT4,
3470
+ outline: "none"
3471
+ }
3472
+ }
3473
+ )
3474
+ ] }),
3475
+ isUploading && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 12 }, children: [
3476
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { fontSize: 12, color: "#666", marginBottom: 4 }, children: [
3477
+ "Uploading... \u2014 ",
3478
+ Math.round(progress.progress),
3479
+ "%",
3480
+ progress.bytesTotal > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3481
+ " \xB7 ",
3482
+ formatSize2(progress.bytesUploaded),
3483
+ " / ",
3484
+ formatSize2(progress.bytesTotal)
3485
+ ] })
3486
+ ] }),
3487
+ /* @__PURE__ */ jsxRuntime.jsx(
3488
+ "div",
3489
+ {
3490
+ style: {
3491
+ height: 6,
3492
+ borderRadius: 3,
3493
+ backgroundColor: "#e0e0e0",
3494
+ overflow: "hidden"
3495
+ },
3496
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3497
+ "div",
3498
+ {
3499
+ style: {
3500
+ width: `${progress.progress}%`,
3501
+ height: "100%",
3502
+ backgroundColor: "#1976d2",
3503
+ transition: "width 0.2s ease"
3504
+ }
3505
+ }
3506
+ )
3507
+ }
3508
+ )
3509
+ ] }),
3510
+ selectedFile && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 12, display: "flex", gap: 8 }, children: [
3511
+ /* @__PURE__ */ jsxRuntime.jsx(
3512
+ "button",
3513
+ {
3514
+ onClick: handleSubmit,
3515
+ disabled: isDisabled || !title.trim(),
3516
+ style: {
3517
+ padding: "8px 20px",
3518
+ borderRadius: 6,
3519
+ border: "none",
3520
+ backgroundColor: isDisabled ? "#bbb" : "#1976d2",
3521
+ color: "#fff",
3522
+ fontSize: 14,
3523
+ fontFamily: FONT4,
3524
+ cursor: isDisabled ? "default" : "pointer",
3525
+ fontWeight: 500
3526
+ },
3527
+ children: "Upload"
3528
+ }
3529
+ ),
3530
+ isUploading && /* @__PURE__ */ jsxRuntime.jsx(
3531
+ "button",
3532
+ {
3533
+ onClick: cancel,
3534
+ style: {
3535
+ padding: "8px 16px",
3536
+ borderRadius: 6,
3537
+ border: "1px solid #ddd",
3538
+ backgroundColor: "#fff",
3539
+ fontSize: 14,
3540
+ fontFamily: FONT4,
3541
+ cursor: "pointer",
3542
+ color: "#666"
3543
+ },
3544
+ children: "Cancel"
3545
+ }
3546
+ )
3547
+ ] }),
3548
+ progress.status === "error" && progress.error && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "#d32f2f", fontSize: 13, marginTop: 8 }, children: progress.error }),
3549
+ progress.status === "conflict" && /* @__PURE__ */ jsxRuntime.jsxs(
3550
+ "div",
3551
+ {
3552
+ style: {
3553
+ marginTop: 12,
3554
+ padding: "10px 14px",
3555
+ borderRadius: 8,
3556
+ backgroundColor: "#fff3e0",
3557
+ border: "1px solid #ffcc80",
3558
+ color: "#e65100",
3559
+ fontSize: 13,
3560
+ fontFamily: FONT4,
3561
+ display: "flex",
3562
+ alignItems: "center",
3563
+ justifyContent: "space-between"
3564
+ },
3565
+ children: [
3566
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: progress.error || "A file with identical content already exists" }),
3567
+ /* @__PURE__ */ jsxRuntime.jsx(
3568
+ "button",
3569
+ {
3570
+ onClick: () => reset(),
3571
+ style: {
3572
+ background: "none",
3573
+ border: "none",
3574
+ color: "#e65100",
3575
+ cursor: "pointer",
3576
+ fontSize: 16,
3577
+ lineHeight: 1,
3578
+ padding: "0 2px"
3579
+ },
3580
+ children: "\xD7"
3581
+ }
3582
+ )
3583
+ ]
3584
+ }
3585
+ ),
3586
+ showSuccess && /* @__PURE__ */ jsxRuntime.jsxs(
3587
+ "div",
3588
+ {
3589
+ style: {
3590
+ marginTop: 12,
3591
+ padding: "10px 14px",
3592
+ borderRadius: 8,
3593
+ backgroundColor: "#e8f5e9",
3594
+ border: "1px solid #a5d6a7",
3595
+ color: "#2e7d32",
3596
+ fontSize: 13,
3597
+ fontFamily: FONT4,
3598
+ display: "flex",
3599
+ alignItems: "center",
3600
+ justifyContent: "space-between"
3601
+ },
3602
+ children: [
3603
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "File uploaded successfully" }),
3604
+ /* @__PURE__ */ jsxRuntime.jsx(
3605
+ "button",
3606
+ {
3607
+ onClick: () => setShowSuccess(false),
3608
+ style: {
3609
+ background: "none",
3610
+ border: "none",
3611
+ color: "#2e7d32",
3612
+ cursor: "pointer",
3613
+ fontSize: 16,
3614
+ lineHeight: 1,
3615
+ padding: "0 2px"
3616
+ },
3617
+ children: "\xD7"
3618
+ }
3619
+ )
3620
+ ]
3621
+ }
3622
+ )
3623
+ ] });
3624
+ }
3625
+ var FONT5 = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3626
+ function formatBytes2(bytes) {
3627
+ if (bytes < 1024) return `${bytes} B`;
3628
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
3629
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
3630
+ }
3631
+ var FORMAT_LABELS = {
3632
+ pdf: "PDF",
3633
+ doc: "Word",
3634
+ docx: "Word",
3635
+ xls: "Excel",
3636
+ xlsx: "Excel",
3637
+ ppt: "PowerPoint",
3638
+ pptx: "PowerPoint",
3639
+ csv: "CSV",
3640
+ txt: "Text",
3641
+ zip: "ZIP",
3642
+ gz: "GZip",
3643
+ json: "JSON",
3644
+ xml: "XML"
3645
+ };
3646
+ function formatLabel(format) {
3647
+ return FORMAT_LABELS[format.toLowerCase()] || format.toUpperCase();
3648
+ }
3649
+ function isSafeURL2(url) {
3650
+ return url.startsWith("https://") || url.startsWith("http://localhost");
3651
+ }
3652
+ function sanitizeFilename2(name) {
3653
+ return name.replace(/[/\\:*?"<>|\x00-\x1f]/g, "_");
3654
+ }
3655
+ function FileDetails({
3656
+ file,
3657
+ onClose,
3658
+ onEdit,
3659
+ onDelete,
3660
+ className,
3661
+ style
3662
+ }) {
3663
+ const [copiedField, setCopiedField] = React.useState(null);
3664
+ const copyText = React.useCallback((text, field) => {
3665
+ navigator.clipboard?.writeText(text).then(() => {
3666
+ setCopiedField(field);
3667
+ setTimeout(() => setCopiedField(null), 1200);
3668
+ }).catch(() => {
3669
+ });
3670
+ }, []);
3671
+ const handleDownload = React.useCallback(async () => {
3672
+ if (!file.objectURL) return;
3673
+ try {
3674
+ const res = await fetch(file.objectURL);
3675
+ const blob = await res.blob();
3676
+ const url = URL.createObjectURL(blob);
3677
+ const a = document.createElement("a");
3678
+ a.href = url;
3679
+ a.download = sanitizeFilename2(file.originalName || `${file.title}.${file.ext}`);
3680
+ document.body.appendChild(a);
3681
+ a.click();
3682
+ document.body.removeChild(a);
3683
+ URL.revokeObjectURL(url);
3684
+ } catch {
3685
+ if (isSafeURL2(file.objectURL)) {
3686
+ window.open(file.objectURL, "_blank");
3687
+ }
3688
+ }
3689
+ }, [file]);
3690
+ return /* @__PURE__ */ jsxRuntime.jsx(
3691
+ "div",
3692
+ {
3693
+ style: {
3694
+ position: "fixed",
3695
+ inset: 0,
3696
+ zIndex: 9999,
3697
+ display: "flex",
3698
+ alignItems: "center",
3699
+ justifyContent: "center",
3700
+ backgroundColor: "rgba(0,0,0,0.5)"
3701
+ },
3702
+ onClick: (e) => {
3703
+ if (e.target === e.currentTarget) onClose();
3704
+ },
3705
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
3706
+ "div",
3707
+ {
3708
+ className,
3709
+ style: {
3710
+ backgroundColor: "#fff",
3711
+ borderRadius: 12,
3712
+ maxWidth: 600,
3713
+ width: "95vw",
3714
+ maxHeight: "90vh",
3715
+ overflow: "auto",
3716
+ boxShadow: "0 8px 32px rgba(0,0,0,0.2)",
3717
+ fontFamily: FONT5,
3718
+ ...style
3719
+ },
3720
+ children: [
3721
+ /* @__PURE__ */ jsxRuntime.jsxs(
3722
+ "div",
3723
+ {
3724
+ style: {
3725
+ display: "flex",
3726
+ alignItems: "center",
3727
+ justifyContent: "space-between",
3728
+ padding: "16px 20px 12px",
3729
+ borderBottom: "1px solid #eee"
3730
+ },
3731
+ children: [
3732
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 10 }, children: [
3733
+ /* @__PURE__ */ jsxRuntime.jsx(FileIcon, { format: file.format }),
3734
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontWeight: 600, fontSize: 16 }, children: file.title })
3735
+ ] }),
3736
+ /* @__PURE__ */ jsxRuntime.jsx(
3737
+ "button",
3738
+ {
3739
+ onClick: onClose,
3740
+ style: {
3741
+ background: "none",
3742
+ border: "none",
3743
+ fontSize: 22,
3744
+ cursor: "pointer",
3745
+ color: "#666",
3746
+ lineHeight: 1,
3747
+ padding: "4px 8px"
3748
+ },
3749
+ children: "\xD7"
3750
+ }
3751
+ )
3752
+ ]
3753
+ }
3754
+ ),
3755
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "16px 20px" }, children: [
3756
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: 8, fontSize: 13 }, children: [
3757
+ /* @__PURE__ */ jsxRuntime.jsx(MetaItem2, { label: "Format", value: formatLabel(file.format) }),
3758
+ /* @__PURE__ */ jsxRuntime.jsx(MetaItem2, { label: "Size", value: formatBytes2(file.sizeBytes) }),
3759
+ /* @__PURE__ */ jsxRuntime.jsx(MetaItem2, { label: "Type", value: file.contentType }),
3760
+ /* @__PURE__ */ jsxRuntime.jsx(
3761
+ MetaItem2,
3762
+ {
3763
+ label: "ID",
3764
+ value: file.fileId.slice(0, 12) + "...",
3765
+ onClick: () => copyText(file.fileId, "id"),
3766
+ actionLabel: copiedField === "id" ? "Copied" : "Copy"
3767
+ }
3768
+ ),
3769
+ file.objectURL && /* @__PURE__ */ jsxRuntime.jsx(
3770
+ MetaItem2,
3771
+ {
3772
+ label: "URL",
3773
+ value: new URL(file.objectURL).pathname.split("/").pop() || "...",
3774
+ onClick: () => copyText(file.objectURL, "url"),
3775
+ actionLabel: copiedField === "url" ? "Copied" : "Copy"
3776
+ }
3777
+ )
3778
+ ] }),
3779
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 12, fontSize: 13, color: "#555" }, children: [
3780
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontWeight: 500, color: "#888" }, children: "File:" }),
3781
+ " ",
3782
+ file.originalName
3783
+ ] }),
3784
+ file.description && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginTop: 8, fontSize: 13, color: "#666" }, children: file.description }),
3785
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 6, fontSize: 12, color: "#999" }, children: [
3786
+ "Uploaded ",
3787
+ new Date(file.uploadedAt).toLocaleDateString()
3788
+ ] })
3789
+ ] }),
3790
+ /* @__PURE__ */ jsxRuntime.jsxs(
3791
+ "div",
3792
+ {
3793
+ style: {
3794
+ display: "flex",
3795
+ justifyContent: "space-between",
3796
+ padding: "12px 20px 16px",
3797
+ borderTop: "1px solid #eee",
3798
+ gap: 8
3799
+ },
3800
+ children: [
3801
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 8 }, children: [
3802
+ onEdit && /* @__PURE__ */ jsxRuntime.jsx(
3803
+ "button",
3804
+ {
3805
+ onClick: () => onEdit(file),
3806
+ style: {
3807
+ padding: "6px 14px",
3808
+ borderRadius: 6,
3809
+ border: "1px solid #ddd",
3810
+ backgroundColor: "#fff",
3811
+ fontSize: 13,
3812
+ fontFamily: FONT5,
3813
+ cursor: "pointer",
3814
+ color: "#333"
3815
+ },
3816
+ children: "Edit"
3817
+ }
3818
+ ),
3819
+ onDelete && /* @__PURE__ */ jsxRuntime.jsx(
3820
+ "button",
3821
+ {
3822
+ onClick: () => onDelete(file),
3823
+ style: {
3824
+ padding: "6px 14px",
3825
+ borderRadius: 6,
3826
+ border: "1px solid #ffcdd2",
3827
+ backgroundColor: "#fff",
3828
+ fontSize: 13,
3829
+ fontFamily: FONT5,
3830
+ cursor: "pointer",
3831
+ color: "#d32f2f"
3832
+ },
3833
+ children: "Delete"
3834
+ }
3835
+ )
3836
+ ] }),
3837
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 8 }, children: [
3838
+ file.objectURL && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3839
+ isSafeURL2(file.objectURL) && /* @__PURE__ */ jsxRuntime.jsx(
3840
+ "a",
3841
+ {
3842
+ href: file.objectURL,
3843
+ target: "_blank",
3844
+ rel: "noopener noreferrer",
3845
+ style: {
3846
+ padding: "6px 14px",
3847
+ borderRadius: 6,
3848
+ border: "1px solid #ddd",
3849
+ backgroundColor: "#fff",
3850
+ fontSize: 13,
3851
+ fontFamily: FONT5,
3852
+ cursor: "pointer",
3853
+ color: "#333",
3854
+ textDecoration: "none",
3855
+ display: "inline-flex",
3856
+ alignItems: "center"
3857
+ },
3858
+ children: "Open"
3859
+ }
3860
+ ),
3861
+ /* @__PURE__ */ jsxRuntime.jsx(
3862
+ "button",
3863
+ {
3864
+ onClick: handleDownload,
3865
+ style: {
3866
+ padding: "6px 14px",
3867
+ borderRadius: 6,
3868
+ border: "1px solid #ddd",
3869
+ backgroundColor: "#fff",
3870
+ fontSize: 13,
3871
+ fontFamily: FONT5,
3872
+ cursor: "pointer",
3873
+ color: "#333"
3874
+ },
3875
+ children: "Download"
3876
+ }
3877
+ )
3878
+ ] }),
3879
+ /* @__PURE__ */ jsxRuntime.jsx(
3880
+ "button",
3881
+ {
3882
+ onClick: onClose,
3883
+ style: {
3884
+ padding: "6px 14px",
3885
+ borderRadius: 6,
3886
+ border: "1px solid #ddd",
3887
+ backgroundColor: "#fafafa",
3888
+ fontSize: 13,
3889
+ fontFamily: FONT5,
3890
+ cursor: "pointer",
3891
+ color: "#333"
3892
+ },
3893
+ children: "Close"
3894
+ }
3895
+ )
3896
+ ] })
3897
+ ]
3898
+ }
3899
+ )
3900
+ ]
3901
+ }
3902
+ )
3903
+ }
3904
+ );
3905
+ }
3906
+ function FileIcon({ format }) {
3907
+ const colors = {
3908
+ pdf: "#d32f2f",
3909
+ doc: "#1565c0",
3910
+ docx: "#1565c0",
3911
+ xls: "#2e7d32",
3912
+ xlsx: "#2e7d32",
3913
+ ppt: "#e65100",
3914
+ pptx: "#e65100",
3915
+ csv: "#2e7d32",
3916
+ json: "#6a1b9a",
3917
+ xml: "#6a1b9a",
3918
+ txt: "#616161",
3919
+ zip: "#795548",
3920
+ gz: "#795548"
3921
+ };
3922
+ const color = colors[format.toLowerCase()] || "#757575";
3923
+ return /* @__PURE__ */ jsxRuntime.jsx(
3924
+ "div",
3925
+ {
3926
+ style: {
3927
+ width: 32,
3928
+ height: 32,
3929
+ borderRadius: 6,
3930
+ backgroundColor: color,
3931
+ color: "#fff",
3932
+ display: "flex",
3933
+ alignItems: "center",
3934
+ justifyContent: "center",
3935
+ fontSize: 10,
3936
+ fontWeight: 700,
3937
+ fontFamily: FONT5,
3938
+ flexShrink: 0,
3939
+ textTransform: "uppercase"
3940
+ },
3941
+ children: format.slice(0, 4)
3942
+ }
3943
+ );
3944
+ }
3945
+ function MetaItem2({
3946
+ label,
3947
+ value,
3948
+ onClick,
3949
+ actionLabel
3950
+ }) {
3951
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3952
+ "span",
3953
+ {
3954
+ style: {
3955
+ display: "inline-flex",
3956
+ alignItems: "center",
3957
+ gap: 4,
3958
+ padding: "3px 10px",
3959
+ borderRadius: 10,
3960
+ backgroundColor: "#f5f5f5",
3961
+ color: "#555",
3962
+ fontSize: 12
3963
+ },
3964
+ children: [
3965
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 500, color: "#888" }, children: [
3966
+ label,
3967
+ ":"
3968
+ ] }),
3969
+ " ",
3970
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontFamily: "monospace" }, children: value }),
3971
+ onClick && /* @__PURE__ */ jsxRuntime.jsx(
3972
+ "button",
3973
+ {
3974
+ onClick,
3975
+ style: {
3976
+ background: "none",
3977
+ border: "none",
3978
+ color: "#1976d2",
3979
+ cursor: "pointer",
3980
+ fontSize: 11,
3981
+ padding: "0 2px",
3982
+ fontFamily: FONT5
3983
+ },
3984
+ children: actionLabel
3985
+ }
3986
+ )
3987
+ ]
3988
+ }
3989
+ );
3990
+ }
3991
+ var FONT6 = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3992
+ var SKELETON_STYLE_ID2 = "mr-file-skeleton-keyframes";
3993
+ function useSkeletonStyle2() {
3994
+ React.useEffect(() => {
3995
+ if (document.getElementById(SKELETON_STYLE_ID2)) return;
3996
+ const style = document.createElement("style");
3997
+ style.id = SKELETON_STYLE_ID2;
3998
+ style.textContent = "@keyframes mr-skeleton-pulse{0%,100%{opacity:1}50%{opacity:.4}}";
3999
+ document.head.appendChild(style);
4000
+ }, []);
4001
+ }
4002
+ function formatBytes3(bytes) {
4003
+ if (bytes < 1024) return `${bytes} B`;
4004
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
4005
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
4006
+ }
4007
+ var FORMAT_COLORS = {
4008
+ pdf: "#d32f2f",
4009
+ doc: "#1565c0",
4010
+ docx: "#1565c0",
4011
+ xls: "#2e7d32",
4012
+ xlsx: "#2e7d32",
4013
+ ppt: "#e65100",
4014
+ pptx: "#e65100",
4015
+ csv: "#2e7d32",
4016
+ json: "#6a1b9a",
4017
+ xml: "#6a1b9a",
4018
+ txt: "#616161",
4019
+ zip: "#795548",
4020
+ gz: "#795548"
4021
+ };
4022
+ function FilePicker({
4023
+ onSelect,
4024
+ onClose,
4025
+ mode = "inline",
4026
+ pageSize = 20,
4027
+ showSearch = true,
4028
+ searchPlaceholder = "Search files...",
4029
+ selectedFileId,
4030
+ showActions = false,
4031
+ refType,
4032
+ refId,
4033
+ refreshSignal,
4034
+ className,
4035
+ style
4036
+ }) {
4037
+ const {
4038
+ files,
4039
+ page,
4040
+ pageCount,
4041
+ total,
4042
+ loading,
4043
+ error,
4044
+ setPage,
4045
+ setQuery,
4046
+ refetch,
4047
+ available,
4048
+ removeFile,
4049
+ updateFile: updateFile2
4050
+ } = useFiles({ pageSize, refType, refId });
4051
+ useSkeletonStyle2();
4052
+ const [searchInput, setSearchInput] = React.useState("");
4053
+ const [activeRow, setActiveRow] = React.useState(null);
4054
+ const [detailFile, setDetailFile] = React.useState(null);
4055
+ const [editFile, setEditFile] = React.useState(null);
4056
+ const [editTitle, setEditTitle] = React.useState("");
4057
+ const [editDesc, setEditDesc] = React.useState("");
4058
+ const [editSaving, setEditSaving] = React.useState(false);
4059
+ const [deleteConfirm, setDeleteConfirm] = React.useState(null);
4060
+ const [deleting, setDeleting] = React.useState(false);
4061
+ const [actionError, setActionError] = React.useState(null);
4062
+ React.useEffect(() => {
4063
+ const timer = setTimeout(() => setQuery(searchInput), 300);
4064
+ return () => clearTimeout(timer);
4065
+ }, [searchInput, setQuery]);
4066
+ const prevSignal = React.useRef(refreshSignal);
4067
+ React.useEffect(() => {
4068
+ if (refreshSignal != null && refreshSignal !== prevSignal.current) {
4069
+ prevSignal.current = refreshSignal;
4070
+ refetch();
4071
+ }
4072
+ }, [refreshSignal, refetch]);
4073
+ const handleKeyDown = React.useCallback(
4074
+ (e) => {
4075
+ if (e.key === "Escape") onClose?.();
4076
+ },
4077
+ [onClose]
4078
+ );
4079
+ const handleDelete = React.useCallback(
4080
+ async (f) => {
4081
+ setDeleting(true);
4082
+ setActionError(null);
4083
+ try {
4084
+ await removeFile(f.fileId);
4085
+ setDeleteConfirm(null);
4086
+ setDetailFile(null);
4087
+ } catch (err) {
4088
+ setActionError(err.message || "Delete failed");
4089
+ } finally {
4090
+ setDeleting(false);
4091
+ }
4092
+ },
4093
+ [removeFile]
4094
+ );
4095
+ const handleEditSave = React.useCallback(async () => {
4096
+ if (!editFile || !editTitle.trim()) return;
4097
+ setEditSaving(true);
4098
+ setActionError(null);
4099
+ try {
4100
+ await updateFile2(editFile.fileId, {
4101
+ title: editTitle.trim(),
4102
+ description: editDesc.trim() || void 0
4103
+ });
4104
+ setEditFile(null);
4105
+ } catch (err) {
4106
+ setActionError(err.message || "Update failed");
4107
+ } finally {
4108
+ setEditSaving(false);
4109
+ }
4110
+ }, [editFile, editTitle, editDesc, updateFile2]);
4111
+ const openEdit = React.useCallback((f) => {
4112
+ setEditTitle(f.title);
4113
+ setEditDesc(f.description || "");
4114
+ setEditFile(f);
4115
+ setDetailFile(null);
4116
+ setActionError(null);
4117
+ }, []);
4118
+ const openDelete = React.useCallback((f) => {
4119
+ setDeleteConfirm(f);
4120
+ setDetailFile(null);
4121
+ setActionError(null);
4122
+ }, []);
4123
+ if (!available) return null;
4124
+ const skeletonCount = Math.min(pageSize, 6);
4125
+ const content = /* @__PURE__ */ jsxRuntime.jsxs(
4126
+ "div",
4127
+ {
4128
+ className,
4129
+ style: {
4130
+ fontFamily: FONT6,
4131
+ display: "flex",
4132
+ flexDirection: "column",
4133
+ gap: 12,
4134
+ ...!mode || mode === "inline" ? style : {}
4135
+ },
4136
+ onKeyDown: handleKeyDown,
4137
+ children: [
4138
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
4139
+ showSearch && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { flex: 1, position: "relative" }, children: [
4140
+ /* @__PURE__ */ jsxRuntime.jsx(
4141
+ "input",
4142
+ {
4143
+ type: "text",
4144
+ placeholder: searchPlaceholder,
4145
+ value: searchInput,
4146
+ onChange: (e) => setSearchInput(e.target.value),
4147
+ style: {
4148
+ width: "100%",
4149
+ padding: "8px 12px",
4150
+ paddingRight: searchInput ? 32 : 12,
4151
+ borderRadius: 6,
4152
+ border: "1px solid #ddd",
4153
+ fontSize: 14,
4154
+ fontFamily: FONT6,
4155
+ outline: "none",
4156
+ boxSizing: "border-box"
4157
+ }
4158
+ }
4159
+ ),
4160
+ searchInput && /* @__PURE__ */ jsxRuntime.jsx(
4161
+ "button",
4162
+ {
4163
+ onClick: () => setSearchInput(""),
4164
+ style: {
4165
+ position: "absolute",
4166
+ right: 6,
4167
+ top: "50%",
4168
+ transform: "translateY(-50%)",
4169
+ background: "none",
4170
+ border: "none",
4171
+ fontSize: 16,
4172
+ cursor: "pointer",
4173
+ color: "#999",
4174
+ lineHeight: 1,
4175
+ padding: "2px 4px"
4176
+ },
4177
+ children: "\xD7"
4178
+ }
4179
+ )
4180
+ ] }),
4181
+ mode === "modal" && onClose && /* @__PURE__ */ jsxRuntime.jsx(
4182
+ "button",
4183
+ {
4184
+ onClick: onClose,
4185
+ style: {
4186
+ background: "none",
4187
+ border: "none",
4188
+ fontSize: 20,
4189
+ cursor: "pointer",
4190
+ padding: "4px 8px",
4191
+ color: "#666",
4192
+ lineHeight: 1
4193
+ },
4194
+ children: "\xD7"
4195
+ }
4196
+ )
4197
+ ] }),
4198
+ actionError && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "#d32f2f", fontSize: 13, padding: "4px 0" }, children: actionError }),
4199
+ loading && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: 4 }, children: Array.from({ length: skeletonCount }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(
4200
+ "div",
4201
+ {
4202
+ style: {
4203
+ height: 48,
4204
+ borderRadius: 6,
4205
+ backgroundColor: "#f0f0f0",
4206
+ animation: "mr-skeleton-pulse 1.5s ease-in-out infinite"
4207
+ }
4208
+ },
4209
+ i
4210
+ )) }),
4211
+ error && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { textAlign: "center", padding: 20, color: "#d32f2f", fontSize: 14 }, children: error }),
4212
+ !loading && !error && files.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { textAlign: "center", padding: "40px 20px", color: "#999" }, children: [
4213
+ /* @__PURE__ */ jsxRuntime.jsxs(
4214
+ "svg",
4215
+ {
4216
+ width: "36",
4217
+ height: "36",
4218
+ viewBox: "0 0 24 24",
4219
+ fill: "none",
4220
+ stroke: "currentColor",
4221
+ strokeWidth: "1.2",
4222
+ strokeLinecap: "round",
4223
+ strokeLinejoin: "round",
4224
+ style: { opacity: 0.4, marginBottom: 8 },
4225
+ children: [
4226
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
4227
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "14 2 14 8 20 8" })
4228
+ ]
4229
+ }
4230
+ ),
4231
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 14, fontWeight: 500, color: "#666" }, children: "No files yet" }),
4232
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 13, marginTop: 4 }, children: searchInput ? "Try a different search term" : "Upload a file to get started" })
4233
+ ] }),
4234
+ !loading && files.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: 2 }, children: files.map((f) => {
4235
+ const isSelected = f.fileId === selectedFileId;
4236
+ const isActive = activeRow === f.fileId;
4237
+ const fmtColor = FORMAT_COLORS[f.format.toLowerCase()] || "#757575";
4238
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4239
+ "div",
4240
+ {
4241
+ onMouseEnter: () => setActiveRow(f.fileId),
4242
+ onMouseLeave: () => setActiveRow(null),
4243
+ onClick: () => onSelect?.(f),
4244
+ style: {
4245
+ display: "flex",
4246
+ alignItems: "center",
4247
+ gap: 10,
4248
+ padding: "8px 12px",
4249
+ borderRadius: 6,
4250
+ border: isSelected ? "1px solid #1976d2" : "1px solid #eee",
4251
+ backgroundColor: isSelected ? "#e3f2fd" : isActive ? "#fafafa" : "#fff",
4252
+ cursor: "pointer",
4253
+ transition: "all 0.1s ease"
4254
+ },
4255
+ children: [
4256
+ /* @__PURE__ */ jsxRuntime.jsx(
4257
+ "div",
4258
+ {
4259
+ style: {
4260
+ width: 36,
4261
+ height: 36,
4262
+ borderRadius: 6,
4263
+ backgroundColor: fmtColor,
4264
+ color: "#fff",
4265
+ display: "flex",
4266
+ alignItems: "center",
4267
+ justifyContent: "center",
4268
+ fontSize: 10,
4269
+ fontWeight: 700,
4270
+ flexShrink: 0,
4271
+ textTransform: "uppercase"
4272
+ },
4273
+ children: f.format.slice(0, 4)
4274
+ }
4275
+ ),
4276
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [
4277
+ /* @__PURE__ */ jsxRuntime.jsx(
4278
+ "div",
4279
+ {
4280
+ style: {
4281
+ fontSize: 14,
4282
+ fontWeight: 500,
4283
+ color: "#222",
4284
+ whiteSpace: "nowrap",
4285
+ overflow: "hidden",
4286
+ textOverflow: "ellipsis"
4287
+ },
4288
+ children: f.title
4289
+ }
4290
+ ),
4291
+ /* @__PURE__ */ jsxRuntime.jsxs(
4292
+ "div",
4293
+ {
4294
+ style: {
4295
+ fontSize: 12,
4296
+ color: "#888",
4297
+ whiteSpace: "nowrap",
4298
+ overflow: "hidden",
4299
+ textOverflow: "ellipsis"
4300
+ },
4301
+ children: [
4302
+ f.originalName,
4303
+ " \xB7 ",
4304
+ formatBytes3(f.sizeBytes)
4305
+ ]
4306
+ }
4307
+ )
4308
+ ] }),
4309
+ showActions && isActive && /* @__PURE__ */ jsxRuntime.jsxs(
4310
+ "div",
4311
+ {
4312
+ style: { display: "flex", gap: 4, flexShrink: 0 },
4313
+ onClick: (e) => e.stopPropagation(),
4314
+ children: [
4315
+ /* @__PURE__ */ jsxRuntime.jsx(RowActionBtn, { label: "Info", onClick: () => setDetailFile(f) }),
4316
+ /* @__PURE__ */ jsxRuntime.jsx(RowActionBtn, { label: "Edit", onClick: () => openEdit(f) }),
4317
+ /* @__PURE__ */ jsxRuntime.jsx(RowActionBtn, { label: "Del", onClick: () => openDelete(f), color: "#d32f2f" })
4318
+ ]
4319
+ }
4320
+ )
4321
+ ]
4322
+ },
4323
+ f.fileId
4324
+ );
4325
+ }) }),
4326
+ pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsxs(
4327
+ "div",
4328
+ {
4329
+ style: {
4330
+ display: "flex",
4331
+ justifyContent: "center",
4332
+ alignItems: "center",
4333
+ gap: 12,
4334
+ fontSize: 14
4335
+ },
4336
+ children: [
4337
+ /* @__PURE__ */ jsxRuntime.jsx(
4338
+ "button",
4339
+ {
4340
+ disabled: page <= 0,
4341
+ onClick: () => setPage(page - 1),
4342
+ style: {
4343
+ padding: "4px 12px",
4344
+ borderRadius: 4,
4345
+ border: "1px solid #ddd",
4346
+ background: page <= 0 ? "#f5f5f5" : "#fff",
4347
+ cursor: page <= 0 ? "default" : "pointer",
4348
+ fontFamily: FONT6,
4349
+ fontSize: 13,
4350
+ color: page <= 0 ? "#bbb" : "#333"
4351
+ },
4352
+ children: "Prev"
4353
+ }
4354
+ ),
4355
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { color: "#666", fontSize: 13 }, children: [
4356
+ "Page ",
4357
+ page + 1,
4358
+ " of ",
4359
+ pageCount,
4360
+ " \xB7 ",
4361
+ total,
4362
+ " file",
4363
+ total !== 1 ? "s" : ""
4364
+ ] }),
4365
+ /* @__PURE__ */ jsxRuntime.jsx(
4366
+ "button",
4367
+ {
4368
+ disabled: page >= pageCount - 1,
4369
+ onClick: () => setPage(page + 1),
4370
+ style: {
4371
+ padding: "4px 12px",
4372
+ borderRadius: 4,
4373
+ border: "1px solid #ddd",
4374
+ background: page >= pageCount - 1 ? "#f5f5f5" : "#fff",
4375
+ cursor: page >= pageCount - 1 ? "default" : "pointer",
4376
+ fontFamily: FONT6,
4377
+ fontSize: 13,
4378
+ color: page >= pageCount - 1 ? "#bbb" : "#333"
4379
+ },
4380
+ children: "Next"
4381
+ }
4382
+ )
4383
+ ]
4384
+ }
4385
+ ),
4386
+ detailFile && /* @__PURE__ */ jsxRuntime.jsx(
4387
+ FileDetails,
4388
+ {
4389
+ file: detailFile,
4390
+ onClose: () => setDetailFile(null),
4391
+ onEdit: showActions ? openEdit : void 0,
4392
+ onDelete: showActions ? openDelete : void 0
4393
+ }
4394
+ ),
4395
+ editFile && /* @__PURE__ */ jsxRuntime.jsx(
4396
+ "div",
4397
+ {
4398
+ style: {
4399
+ position: "fixed",
4400
+ inset: 0,
4401
+ zIndex: 1e4,
4402
+ display: "flex",
4403
+ alignItems: "center",
4404
+ justifyContent: "center",
4405
+ backgroundColor: "rgba(0,0,0,0.4)"
4406
+ },
4407
+ onClick: (e) => {
4408
+ if (e.target === e.currentTarget && !editSaving) setEditFile(null);
4409
+ },
4410
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
4411
+ "div",
4412
+ {
4413
+ style: {
4414
+ backgroundColor: "#fff",
4415
+ borderRadius: 10,
4416
+ padding: 20,
4417
+ maxWidth: 400,
4418
+ width: "90vw",
4419
+ fontFamily: FONT6
4420
+ },
4421
+ children: [
4422
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontWeight: 600, fontSize: 15, marginBottom: 12 }, children: "Edit File" }),
4423
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
4424
+ /* @__PURE__ */ jsxRuntime.jsx(
4425
+ "input",
4426
+ {
4427
+ type: "text",
4428
+ placeholder: "Title",
4429
+ value: editTitle,
4430
+ onChange: (e) => setEditTitle(e.target.value),
4431
+ disabled: editSaving,
4432
+ maxLength: 250,
4433
+ style: {
4434
+ padding: "8px 12px",
4435
+ borderRadius: 6,
4436
+ border: "1px solid #ddd",
4437
+ fontSize: 14,
4438
+ fontFamily: FONT6,
4439
+ outline: "none"
4440
+ }
4441
+ }
4442
+ ),
4443
+ /* @__PURE__ */ jsxRuntime.jsx(
4444
+ "input",
4445
+ {
4446
+ type: "text",
4447
+ placeholder: "Description (optional)",
4448
+ value: editDesc,
4449
+ onChange: (e) => setEditDesc(e.target.value),
4450
+ disabled: editSaving,
4451
+ maxLength: 500,
4452
+ style: {
4453
+ padding: "8px 12px",
4454
+ borderRadius: 6,
4455
+ border: "1px solid #ddd",
4456
+ fontSize: 14,
4457
+ fontFamily: FONT6,
4458
+ outline: "none"
4459
+ }
4460
+ }
4461
+ )
4462
+ ] }),
4463
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 16 }, children: [
4464
+ /* @__PURE__ */ jsxRuntime.jsx(
4465
+ "button",
4466
+ {
4467
+ onClick: () => setEditFile(null),
4468
+ disabled: editSaving,
4469
+ style: {
4470
+ padding: "6px 14px",
4471
+ borderRadius: 6,
4472
+ border: "1px solid #ddd",
4473
+ backgroundColor: "#fff",
4474
+ fontSize: 13,
4475
+ fontFamily: FONT6,
4476
+ cursor: "pointer",
4477
+ color: "#333"
4478
+ },
4479
+ children: "Cancel"
4480
+ }
4481
+ ),
4482
+ /* @__PURE__ */ jsxRuntime.jsx(
4483
+ "button",
4484
+ {
4485
+ onClick: handleEditSave,
4486
+ disabled: editSaving || !editTitle.trim(),
4487
+ style: {
4488
+ padding: "6px 14px",
4489
+ borderRadius: 6,
4490
+ border: "none",
4491
+ backgroundColor: editSaving || !editTitle.trim() ? "#bbb" : "#1976d2",
4492
+ color: "#fff",
4493
+ fontSize: 13,
4494
+ fontFamily: FONT6,
4495
+ cursor: editSaving ? "default" : "pointer"
4496
+ },
4497
+ children: editSaving ? "Saving..." : "Save"
4498
+ }
4499
+ )
4500
+ ] })
4501
+ ]
4502
+ }
4503
+ )
4504
+ }
4505
+ ),
4506
+ deleteConfirm && /* @__PURE__ */ jsxRuntime.jsx(
4507
+ "div",
4508
+ {
4509
+ style: {
4510
+ position: "fixed",
4511
+ inset: 0,
4512
+ zIndex: 1e4,
4513
+ display: "flex",
4514
+ alignItems: "center",
4515
+ justifyContent: "center",
4516
+ backgroundColor: "rgba(0,0,0,0.4)"
4517
+ },
4518
+ onClick: (e) => {
4519
+ if (e.target === e.currentTarget && !deleting) setDeleteConfirm(null);
4520
+ },
4521
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
4522
+ "div",
4523
+ {
4524
+ style: {
4525
+ backgroundColor: "#fff",
4526
+ borderRadius: 10,
4527
+ padding: 20,
4528
+ maxWidth: 360,
4529
+ width: "90vw",
4530
+ fontFamily: FONT6
4531
+ },
4532
+ children: [
4533
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontWeight: 600, fontSize: 15, marginBottom: 8 }, children: "Delete File" }),
4534
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { fontSize: 14, color: "#555", marginBottom: 16 }, children: [
4535
+ "Delete \u201C",
4536
+ deleteConfirm.title,
4537
+ "\u201D? This cannot be undone."
4538
+ ] }),
4539
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8 }, children: [
4540
+ /* @__PURE__ */ jsxRuntime.jsx(
4541
+ "button",
4542
+ {
4543
+ onClick: () => setDeleteConfirm(null),
4544
+ disabled: deleting,
4545
+ style: {
4546
+ padding: "6px 14px",
4547
+ borderRadius: 6,
4548
+ border: "1px solid #ddd",
4549
+ backgroundColor: "#fff",
4550
+ fontSize: 13,
4551
+ fontFamily: FONT6,
4552
+ cursor: "pointer",
4553
+ color: "#333"
4554
+ },
4555
+ children: "Cancel"
4556
+ }
4557
+ ),
4558
+ /* @__PURE__ */ jsxRuntime.jsx(
4559
+ "button",
4560
+ {
4561
+ onClick: () => handleDelete(deleteConfirm),
4562
+ disabled: deleting,
4563
+ style: {
4564
+ padding: "6px 14px",
4565
+ borderRadius: 6,
4566
+ border: "none",
4567
+ backgroundColor: deleting ? "#bbb" : "#d32f2f",
4568
+ color: "#fff",
4569
+ fontSize: 13,
4570
+ fontFamily: FONT6,
4571
+ cursor: deleting ? "default" : "pointer"
4572
+ },
4573
+ children: deleting ? "Deleting..." : "Delete"
4574
+ }
4575
+ )
4576
+ ] })
4577
+ ]
4578
+ }
4579
+ )
4580
+ }
4581
+ )
4582
+ ]
4583
+ }
4584
+ );
4585
+ if (mode === "modal") {
4586
+ return /* @__PURE__ */ jsxRuntime.jsx(
4587
+ "div",
4588
+ {
4589
+ style: {
4590
+ position: "fixed",
4591
+ inset: 0,
4592
+ zIndex: 9999,
4593
+ display: "flex",
4594
+ alignItems: "center",
4595
+ justifyContent: "center",
4596
+ backgroundColor: "rgba(0,0,0,0.4)"
4597
+ },
4598
+ onClick: (e) => {
4599
+ if (e.target === e.currentTarget) onClose?.();
4600
+ },
4601
+ children: /* @__PURE__ */ jsxRuntime.jsx(
4602
+ "div",
4603
+ {
4604
+ style: {
4605
+ backgroundColor: "#fff",
4606
+ borderRadius: 12,
4607
+ padding: 20,
4608
+ maxWidth: 640,
4609
+ width: "90vw",
4610
+ maxHeight: "80vh",
4611
+ overflow: "auto",
4612
+ boxShadow: "0 8px 32px rgba(0,0,0,0.2)",
4613
+ ...style
4614
+ },
4615
+ children: content
4616
+ }
4617
+ )
4618
+ }
4619
+ );
4620
+ }
4621
+ return content;
4622
+ }
4623
+ function RowActionBtn({
4624
+ label,
4625
+ onClick,
4626
+ color
4627
+ }) {
4628
+ return /* @__PURE__ */ jsxRuntime.jsx(
4629
+ "button",
4630
+ {
4631
+ onClick: (e) => {
4632
+ e.stopPropagation();
4633
+ onClick();
4634
+ },
4635
+ style: {
4636
+ padding: "4px 10px",
4637
+ borderRadius: 4,
4638
+ border: "1px solid #e0e0e0",
4639
+ backgroundColor: "#fff",
4640
+ color: color || "#555",
4641
+ fontSize: 12,
4642
+ fontWeight: 500,
4643
+ cursor: "pointer",
4644
+ fontFamily: FONT6
4645
+ },
4646
+ children: label
4647
+ }
4648
+ );
4649
+ }
4650
+
816
4651
  exports.AppKit = AppKit;
817
4652
  exports.AppKitAuthed = AppKitAuthed;
4653
+ exports.FileDetails = FileDetails;
4654
+ exports.FilePicker = FilePicker;
4655
+ exports.FileUploader = FileUploader;
4656
+ exports.ImageDetails = ImageDetails;
4657
+ exports.ImagePicker = ImagePicker;
4658
+ exports.ImageUploader = ImageUploader;
4659
+ exports.MrImage = MrImage;
818
4660
  exports.UserButton = UserButton;
819
4661
  exports.useAppKit = useAppKit;
4662
+ exports.useConfig = useConfig;
4663
+ exports.useConfigValue = useConfigValue;
4664
+ exports.useFeatureFlag = useFeatureFlag;
4665
+ exports.useFeatureFlags = useFeatureFlags;
4666
+ exports.useFile = useFile;
4667
+ exports.useFileUpload = useFileUpload;
4668
+ exports.useFiles = useFiles;
4669
+ exports.useImage = useImage;
4670
+ exports.useImageUpload = useImageUpload;
4671
+ exports.useImages = useImages;
4672
+ exports.usePermission = usePermission;
4673
+ exports.usePermissions = usePermissions;
4674
+ exports.useProject = useProject;
4675
+ exports.useRole = useRole;
4676
+ exports.useRoles = useRoles;
4677
+ exports.useToken = useToken;
4678
+ exports.useUser = useUser;
820
4679
  //# sourceMappingURL=index.cjs.map
821
4680
  //# sourceMappingURL=index.cjs.map