@manyrows/appkit-react 0.1.5 → 0.1.7

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