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