@proveanything/smartlinks-utils-ui 1.13.8 → 1.13.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +6 -6
  2. package/dist/ErrorBoundary-J9iKgF_H.d.ts +40 -0
  3. package/dist/{chunk-OTJV62XV.js → chunk-5ZQT2GGU.js} +5 -5
  4. package/dist/chunk-5ZQT2GGU.js.map +1 -0
  5. package/dist/{chunk-4LHF5JB7.js → chunk-DH5HG5DW.js} +15 -6
  6. package/dist/chunk-DH5HG5DW.js.map +1 -0
  7. package/dist/{chunk-E3GQ6LNZ.js → chunk-I3T36FSI.js} +528 -49
  8. package/dist/chunk-I3T36FSI.js.map +1 -0
  9. package/dist/{chunk-7UBXTFZQ.js → chunk-JNCRSL2H.js} +13 -12
  10. package/dist/chunk-JNCRSL2H.js.map +1 -0
  11. package/dist/{chunk-JMCV6FOW.js → chunk-WVCNIX7N.js} +3 -3
  12. package/dist/{chunk-JMCV6FOW.js.map → chunk-WVCNIX7N.js.map} +1 -1
  13. package/dist/{chunk-3RRHM4LP.js → chunk-XASZS7EA.js} +131 -4
  14. package/dist/chunk-XASZS7EA.js.map +1 -0
  15. package/dist/components/AssetPicker/index.css +34 -0
  16. package/dist/components/AssetPicker/index.css.map +1 -1
  17. package/dist/components/AssetPicker/index.js +1 -1
  18. package/dist/components/ConditionsEditor/index.css +34 -0
  19. package/dist/components/ConditionsEditor/index.css.map +1 -1
  20. package/dist/components/ConditionsEditor/index.d.ts +2 -2
  21. package/dist/components/ConditionsEditor/index.js +2 -2
  22. package/dist/components/FacetRuleEditor/index.d.ts +1 -1
  23. package/dist/components/FacetRuleEditor/index.js +2 -2
  24. package/dist/components/FontPicker/index.css +34 -0
  25. package/dist/components/FontPicker/index.css.map +1 -1
  26. package/dist/components/FontPicker/index.js +1 -1
  27. package/dist/components/IconPicker/index.css +34 -0
  28. package/dist/components/IconPicker/index.css.map +1 -1
  29. package/dist/components/LinkPicker/index.css +34 -0
  30. package/dist/components/LinkPicker/index.css.map +1 -1
  31. package/dist/components/RecordsAdmin/index.css +34 -0
  32. package/dist/components/RecordsAdmin/index.css.map +1 -1
  33. package/dist/components/RecordsAdmin/index.d.ts +1 -0
  34. package/dist/components/RecordsAdmin/index.js +15 -11
  35. package/dist/components/RecordsAdmin/index.js.map +1 -1
  36. package/dist/index.css +34 -0
  37. package/dist/index.css.map +1 -1
  38. package/dist/index.d.ts +2 -1
  39. package/dist/index.js +7 -7
  40. package/dist/index.js.map +1 -1
  41. package/dist/{types-a2DdgZ2H.d.ts → types-BLqki3Zy.d.ts} +11 -0
  42. package/package.json +3 -3
  43. package/dist/chunk-3RRHM4LP.js.map +0 -1
  44. package/dist/chunk-4LHF5JB7.js.map +0 -1
  45. package/dist/chunk-7UBXTFZQ.js.map +0 -1
  46. package/dist/chunk-E3GQ6LNZ.js.map +0 -1
  47. package/dist/chunk-OTJV62XV.js.map +0 -1
@@ -1,9 +1,9 @@
1
1
  import { assertStylesLoaded } from './chunk-OLYC54YT.js';
2
2
  import { cn } from './chunk-L7FQ52F5.js';
3
- import React8, { useState, useRef, useEffect, useCallback, useMemo, useLayoutEffect } from 'react';
3
+ import React9, { useState, useRef, useEffect, useCallback, useMemo, useLayoutEffect } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
5
  import * as SL from '@proveanything/smartlinks';
6
- import { Filter, Search, LayoutGrid, List, Loader2, AlertCircle, Tag, X, ImageOff, Wand2, Maximize2, Clipboard, Pencil, Crop, Check, Upload, Link, MicOff, Mic, ChevronDown, ChevronRight, Sparkles, Image as Image$1, RotateCcw, RotateCw, FlipHorizontal, FlipVertical, RefreshCw, Plus, AlertTriangle, FileIcon, Film, Music, FileText, AppWindow, MoreVertical, Trash2 } from 'lucide-react';
6
+ import { Filter, Search, LayoutGrid, List, Loader2, AlertCircle, Tag, X, ImageOff, Wand2, Maximize2, Clipboard, Pencil, Crop, Check, Upload, Link, MicOff, Mic, ChevronDown, ChevronRight, Sparkles, Image as Image$1, RotateCcw, RotateCw, FlipHorizontal, FlipVertical, RefreshCw, Plus, Copy, ExternalLink, AlertTriangle, Wrench, CheckCircle2, Trash2, FileIcon, Film, Music, FileText, AppWindow, MoreVertical, Info } from 'lucide-react';
7
7
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
8
8
 
9
9
  // src/components/AssetPicker/types.ts
@@ -32,17 +32,33 @@ function useAssets({ scope, accept, pageSize, appId, listAppId }) {
32
32
  setLoading(true);
33
33
  setError(null);
34
34
  try {
35
- const result = await SL.asset.list({
36
- scope,
37
- mimeTypePrefix: accept,
38
- limit: pageSize,
39
- // Only filter by appId when explicitly requested. When listAppId is undefined,
40
- // we list every asset in the scope — even if uploads are still being stamped
41
- // with `appId`. This is what powers the "All in collection" pill.
42
- ...listAppId ? { appId: listAppId } : {}
43
- });
35
+ const collectionId = scope.collectionId;
36
+ const productId = scope.productId;
37
+ const proofId = scope.proofId;
38
+ const assetType = accept?.startsWith("image/") ? "Image" : accept?.startsWith("video/") ? "Video" : accept?.startsWith("audio/") ? "Audio" : void 0;
39
+ const adminList = SL.asset.listAdmin;
40
+ let items = [];
41
+ if (adminList) {
42
+ const res = await adminList({
43
+ collectionId,
44
+ ...productId ? { productId } : {},
45
+ ...proofId ? { proofId } : {},
46
+ ...listAppId ? { appId: listAppId } : {},
47
+ ...assetType ? { assetType } : {},
48
+ ...pageSize ? { limit: pageSize } : {}
49
+ });
50
+ items = Array.isArray(res) ? res : res?.data ?? [];
51
+ } else {
52
+ const res = await SL.asset.list({
53
+ scope,
54
+ mimeTypePrefix: accept,
55
+ limit: pageSize,
56
+ ...listAppId ? { appId: listAppId } : {}
57
+ });
58
+ items = res;
59
+ }
44
60
  if (mountedRef.current) {
45
- setAssets(result);
61
+ setAssets(items);
46
62
  }
47
63
  } catch (err) {
48
64
  if (mountedRef.current) {
@@ -350,7 +366,7 @@ var AppBadge = ({ appId, appName, size = "sm" }) => {
350
366
  }
351
367
  );
352
368
  };
353
- var CardMenu = ({ onRename, onReplace, onEditTags, onEditImage, onDelete, position = "absolute" }) => {
369
+ var CardMenu = ({ onRename, onReplace, onEditTags, onEditImage, onShowDetails, onDelete, position = "absolute" }) => {
354
370
  const [open, setOpen] = useState(false);
355
371
  const ref = useRef(null);
356
372
  const btnRef = useRef(null);
@@ -402,7 +418,7 @@ var CardMenu = ({ onRename, onReplace, onEditTags, onEditImage, onDelete, positi
402
418
  window.removeEventListener("scroll", update, true);
403
419
  };
404
420
  }, [open]);
405
- if (!onRename && !onReplace && !onEditTags && !onEditImage && !onDelete) return null;
421
+ if (!onRename && !onReplace && !onEditTags && !onEditImage && !onShowDetails && !onDelete) return null;
406
422
  return /* @__PURE__ */ jsxs(
407
423
  "div",
408
424
  {
@@ -446,6 +462,23 @@ var CardMenu = ({ onRename, onReplace, onEditTags, onEditImage, onDelete, positi
446
462
  onClick: (e) => e.stopPropagation(),
447
463
  onMouseDown: (e) => e.stopPropagation(),
448
464
  children: [
465
+ onShowDetails && /* @__PURE__ */ jsxs(
466
+ "button",
467
+ {
468
+ type: "button",
469
+ role: "menuitem",
470
+ onClick: (e) => {
471
+ e.stopPropagation();
472
+ setOpen(false);
473
+ onShowDetails();
474
+ },
475
+ className: "w-full flex items-center gap-2 px-2.5 py-1.5 text-xs hover:bg-accent",
476
+ children: [
477
+ /* @__PURE__ */ jsx(Info, { className: "w-3 h-3" }),
478
+ " Details"
479
+ ]
480
+ }
481
+ ),
449
482
  onRename && /* @__PURE__ */ jsxs(
450
483
  "button",
451
484
  {
@@ -540,7 +573,7 @@ var CardMenu = ({ onRename, onReplace, onEditTags, onEditImage, onDelete, positi
540
573
  }
541
574
  );
542
575
  };
543
- var AssetGridItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelete, onRename, onReplace, onEditTags, onEditImage, allowDelete, currentAppId, getAppName, activeLabels, onToggleLabel }) => {
576
+ var AssetGridItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelete, onRename, onReplace, onEditTags, onEditImage, onShowDetails, allowDelete, currentAppId, getAppName, activeLabels, onToggleLabel }) => {
544
577
  const thumb = getThumbnail(asset2);
545
578
  const Icon = getIcon(asset2.mimeType);
546
579
  const ownerAppId = getAssetAppId(asset2);
@@ -622,6 +655,7 @@ var AssetGridItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelet
622
655
  onReplace,
623
656
  onEditTags,
624
657
  onEditImage,
658
+ onShowDetails,
625
659
  onDelete: allowDelete ? onDelete : void 0
626
660
  }
627
661
  )
@@ -629,7 +663,7 @@ var AssetGridItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelet
629
663
  }
630
664
  );
631
665
  };
632
- var AssetListItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelete, onRename, onReplace, onEditTags, onEditImage, allowDelete, currentAppId, getAppName, activeLabels, onToggleLabel }) => {
666
+ var AssetListItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelete, onRename, onReplace, onEditTags, onEditImage, onShowDetails, allowDelete, currentAppId, getAppName, activeLabels, onToggleLabel }) => {
633
667
  const thumb = getThumbnail(asset2);
634
668
  const Icon = getIcon(asset2.mimeType);
635
669
  const ownerAppId = getAssetAppId(asset2);
@@ -699,6 +733,7 @@ var AssetListItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelet
699
733
  onReplace,
700
734
  onEditTags,
701
735
  onEditImage,
736
+ onShowDetails,
702
737
  onDelete: allowDelete ? onDelete : void 0,
703
738
  position: "inline"
704
739
  }
@@ -718,6 +753,7 @@ var AssetGrid = ({
718
753
  onReplace,
719
754
  onEditTags,
720
755
  onEditImage,
756
+ onShowDetails,
721
757
  allowDelete,
722
758
  currentAppId,
723
759
  getAppName,
@@ -738,6 +774,7 @@ var AssetGrid = ({
738
774
  onReplace: onReplace ? () => onReplace(asset2) : void 0,
739
775
  onEditTags: onEditTags ? () => onEditTags(asset2) : void 0,
740
776
  onEditImage: onEditImage ? () => onEditImage(asset2) : void 0,
777
+ onShowDetails: onShowDetails ? () => onShowDetails(asset2) : void 0,
741
778
  allowDelete,
742
779
  currentAppId,
743
780
  getAppName,
@@ -759,6 +796,7 @@ var AssetGrid = ({
759
796
  onReplace: onReplace ? () => onReplace(asset2) : void 0,
760
797
  onEditTags: onEditTags ? () => onEditTags(asset2) : void 0,
761
798
  onEditImage: onEditImage ? () => onEditImage(asset2) : void 0,
799
+ onShowDetails: onShowDetails ? () => onShowDetails(asset2) : void 0,
762
800
  allowDelete,
763
801
  currentAppId,
764
802
  getAppName,
@@ -779,6 +817,51 @@ function isProcessableImage(file) {
779
817
  if (file.type === "image/svg+xml" || file.type === "image/gif") return false;
780
818
  return true;
781
819
  }
820
+ async function sniffImageMime(file) {
821
+ try {
822
+ const head = new Uint8Array(await file.slice(0, 16).arrayBuffer());
823
+ if (head.length < 4) return null;
824
+ if (head[0] === 137 && head[1] === 80 && head[2] === 78 && head[3] === 71) {
825
+ return { mime: "image/png", ext: "png" };
826
+ }
827
+ if (head[0] === 255 && head[1] === 216 && head[2] === 255) {
828
+ return { mime: "image/jpeg", ext: "jpg" };
829
+ }
830
+ if (head[0] === 71 && head[1] === 73 && head[2] === 70 && head[3] === 56) {
831
+ return { mime: "image/gif", ext: "gif" };
832
+ }
833
+ if (head[0] === 82 && head[1] === 73 && head[2] === 70 && head[3] === 70 && head[8] === 87 && head[9] === 69 && head[10] === 66 && head[11] === 80) {
834
+ return { mime: "image/webp", ext: "webp" };
835
+ }
836
+ if (head[0] === 66 && head[1] === 77) {
837
+ return { mime: "image/bmp", ext: "bmp" };
838
+ }
839
+ if (head[4] === 102 && head[5] === 116 && head[6] === 121 && head[7] === 112) {
840
+ const brand = String.fromCharCode(head[8], head[9], head[10], head[11]);
841
+ if (brand === "avif" || brand === "avis") return { mime: "image/avif", ext: "avif" };
842
+ if (brand === "heic" || brand === "heix" || brand === "mif1" || brand === "msf1") {
843
+ return { mime: "image/heic", ext: "heic" };
844
+ }
845
+ }
846
+ if (head[0] === 60) {
847
+ const text = new TextDecoder().decode(head);
848
+ if (/^<\?xml|^<svg/i.test(text)) return { mime: "image/svg+xml", ext: "svg" };
849
+ }
850
+ return null;
851
+ } catch {
852
+ return null;
853
+ }
854
+ }
855
+ async function normalizeFileType(file, fallbackBaseName) {
856
+ const declaredType = (file.type || "").toLowerCase();
857
+ const hasExt = /\.[a-z0-9]+$/i.test(file.name);
858
+ if (declaredType.startsWith("image/") && hasExt) return file;
859
+ const sniffed = await sniffImageMime(file);
860
+ if (!sniffed) return file;
861
+ const base = (fallbackBaseName || file.name.replace(/\.[^.]+$/, "") || "image").trim() || "image";
862
+ const newName = `${base}.${sniffed.ext}`;
863
+ return new File([file], newName, { type: sniffed.mime, lastModified: file.lastModified });
864
+ }
782
865
  async function loadImage(file) {
783
866
  const url = URL.createObjectURL(file);
784
867
  const img = new Image();
@@ -1350,18 +1433,22 @@ var UploadZone = ({
1350
1433
  if (!items) return;
1351
1434
  for (const item of Array.from(items)) {
1352
1435
  if (item.kind === "file") {
1353
- const file = item.getAsFile();
1354
- if (!file) continue;
1355
- if (accept && !file.type.startsWith(accept.replace("*", ""))) continue;
1436
+ const raw = item.getAsFile();
1437
+ if (!raw) continue;
1356
1438
  e.preventDefault();
1357
- const previewUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : "";
1358
- const defaultName = file.name === "image.png" ? `pasted-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/[T:]/g, "-")}` : file.name.replace(/\.[^.]+$/, "");
1359
- setPastedFile({ file, previewUrl, name: defaultName, origSize: file.size });
1360
- setFileName(defaultName);
1361
- setEditingName(false);
1362
- getImageDimensions(file).then((dims) => {
1363
- if (dims) setPastedFile((prev) => prev && prev.file === file ? { ...prev, origDims: dims } : prev);
1364
- });
1439
+ const defaultBase = !raw.name || raw.name === "image.png" || raw.name === "image" ? `pasted-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/[T:]/g, "-")}` : raw.name.replace(/\.[^.]+$/, "");
1440
+ void (async () => {
1441
+ const file = await normalizeFileType(raw, defaultBase);
1442
+ if (accept && file.type && !file.type.startsWith(accept.replace("*", ""))) return;
1443
+ const previewUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : "";
1444
+ const nameForDisplay = file.name.replace(/\.[^.]+$/, "") || defaultBase;
1445
+ setPastedFile({ file, previewUrl, name: nameForDisplay, origSize: file.size });
1446
+ setFileName(nameForDisplay);
1447
+ setEditingName(false);
1448
+ getImageDimensions(file).then((dims) => {
1449
+ if (dims) setPastedFile((prev) => prev && prev.file === file ? { ...prev, origDims: dims } : prev);
1450
+ });
1451
+ })();
1365
1452
  return;
1366
1453
  }
1367
1454
  }
@@ -1410,7 +1497,8 @@ var UploadZone = ({
1410
1497
  e.stopPropagation();
1411
1498
  setDragOver(false);
1412
1499
  }, []);
1413
- const presentForRename = useCallback((file) => {
1500
+ const presentForRename = useCallback(async (raw) => {
1501
+ const file = await normalizeFileType(raw);
1414
1502
  const previewUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : "";
1415
1503
  const defaultName = file.name.replace(/\.[^.]+$/, "") || "file";
1416
1504
  setPastedFile({ file, previewUrl, name: defaultName, origSize: file.size });
@@ -1426,7 +1514,7 @@ var UploadZone = ({
1426
1514
  setDragOver(false);
1427
1515
  const files = Array.from(e.dataTransfer.files);
1428
1516
  if (files.length === 1 && !multiple) {
1429
- presentForRename(files[0]);
1517
+ void presentForRename(files[0]);
1430
1518
  } else if (files.length > 0) {
1431
1519
  void handleBatchFiles(multiple ? files : [files[0]]);
1432
1520
  }
@@ -1434,21 +1522,22 @@ var UploadZone = ({
1434
1522
  const handleInputChange = useCallback((e) => {
1435
1523
  const files = Array.from(e.target.files || []);
1436
1524
  if (files.length === 1 && !multiple) {
1437
- presentForRename(files[0]);
1525
+ void presentForRename(files[0]);
1438
1526
  } else if (files.length > 0) {
1439
1527
  void handleBatchFiles(multiple ? files : [files[0]]);
1440
1528
  }
1441
1529
  e.target.value = "";
1442
1530
  }, [onFiles, multiple, presentForRename]);
1443
1531
  const handleBatchFiles = useCallback(async (files) => {
1532
+ const normalized = await Promise.all(files.map((f) => normalizeFileType(f)));
1444
1533
  if (!autoOptimize) {
1445
- onFiles(files);
1534
+ onFiles(normalized);
1446
1535
  return;
1447
1536
  }
1448
1537
  setOptimizing(true);
1449
1538
  try {
1450
1539
  const out = [];
1451
- for (const f of files) {
1540
+ for (const f of normalized) {
1452
1541
  if (isProcessableImage(f)) {
1453
1542
  const r = await processImage(f, {
1454
1543
  maxDimension: optConfig.maxDimension,
@@ -1668,12 +1757,8 @@ var UploadZone = ({
1668
1757
  onDragEnter: handleDragIn,
1669
1758
  onDragLeave: handleDragOut,
1670
1759
  onDrop: handleDrop,
1671
- onClick: () => inputRef.current?.click(),
1672
- role: "button",
1760
+ onClick: () => zoneRef.current?.focus(),
1673
1761
  tabIndex: 0,
1674
- onKeyDown: (e) => {
1675
- if (e.key === "Enter" || e.key === " ") inputRef.current?.click();
1676
- },
1677
1762
  children: [
1678
1763
  /* @__PURE__ */ jsx(
1679
1764
  "input",
@@ -1699,8 +1784,20 @@ var UploadZone = ({
1699
1784
  ] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1 py-2", children: [
1700
1785
  /* @__PURE__ */ jsx(Upload, { className: "w-6 h-6 text-muted-foreground" }),
1701
1786
  /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
1702
- "Drop files here, ",
1703
- /* @__PURE__ */ jsx("span", { className: "text-primary underline", children: "browse" }),
1787
+ "Drop files here,",
1788
+ " ",
1789
+ /* @__PURE__ */ jsx(
1790
+ "button",
1791
+ {
1792
+ type: "button",
1793
+ onClick: (e) => {
1794
+ e.stopPropagation();
1795
+ inputRef.current?.click();
1796
+ },
1797
+ className: "text-primary underline hover:no-underline focus:outline-none focus:ring-1 focus:ring-ring rounded",
1798
+ children: "browse"
1799
+ }
1800
+ ),
1704
1801
  ", or paste from clipboard"
1705
1802
  ] }),
1706
1803
  /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground flex items-center gap-1", children: [
@@ -2329,6 +2426,10 @@ var StockPhotoSearch = ({
2329
2426
  }
2330
2427
  }, [query, orientation, collectionId]);
2331
2428
  const handlePick = useCallback((photo) => {
2429
+ setStaged((prev) => {
2430
+ if (prev?.previewUrl) URL.revokeObjectURL(prev.previewUrl);
2431
+ return null;
2432
+ });
2332
2433
  const baseName = `stock-${(photo.alt || query.trim() || "photo").slice(0, 60).replace(/\s+/g, "-")}`;
2333
2434
  setStaged({ photo, name: baseName, file: null, previewUrl: null });
2334
2435
  }, [query]);
@@ -2543,7 +2644,7 @@ var StockPhotoSearch = ({
2543
2644
  {
2544
2645
  type: "button",
2545
2646
  onClick: () => handlePick(photo),
2546
- disabled: !!staged || saving,
2647
+ disabled: isStaged || saving,
2547
2648
  className: cn(
2548
2649
  "absolute inset-0 flex items-center justify-center text-xs font-medium",
2549
2650
  "bg-background/80 opacity-0 group-hover:opacity-100 transition-opacity",
@@ -2717,8 +2818,359 @@ var TagEditor = ({ initial, suggestions, assetName, onCancel, onSave }) => {
2717
2818
  }
2718
2819
  );
2719
2820
  };
2821
+ function getIcon2(mimeType) {
2822
+ if (!mimeType) return FileIcon;
2823
+ if (mimeType.startsWith("image/")) return Image$1;
2824
+ if (mimeType.startsWith("video/")) return Film;
2825
+ if (mimeType.startsWith("audio/")) return Music;
2826
+ return FileText;
2827
+ }
2828
+ function formatSize2(bytes) {
2829
+ if (bytes == null) return "\u2014";
2830
+ if (bytes < 1024) return `${bytes} B`;
2831
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
2832
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
2833
+ }
2834
+ function formatAge(iso) {
2835
+ if (!iso) return "\u2014";
2836
+ const t = Date.parse(iso);
2837
+ if (Number.isNaN(t)) return iso;
2838
+ const diff = Date.now() - t;
2839
+ const day = 24 * 60 * 60 * 1e3;
2840
+ if (diff < 6e4) return "just now";
2841
+ if (diff < 36e5) return `${Math.floor(diff / 6e4)} min ago`;
2842
+ if (diff < day) return `${Math.floor(diff / 36e5)} h ago`;
2843
+ if (diff < 30 * day) return `${Math.floor(diff / day)} d ago`;
2844
+ if (diff < 365 * day) return `${Math.floor(diff / (30 * day))} mo ago`;
2845
+ return `${Math.floor(diff / (365 * day))} y ago`;
2846
+ }
2847
+ function isUnknownMime(asset2) {
2848
+ const m = (asset2.mimeType || "").toLowerCase();
2849
+ if (!m || m === "application/octet-stream" || m === "unknown" || m.endsWith("/unknown")) return true;
2850
+ const name = (asset2.name || asset2.cleanName || "").toLowerCase();
2851
+ if (/\.unknown(\?|$)/.test(name)) return true;
2852
+ return false;
2853
+ }
2854
+ function getThumbnailUrl(asset2) {
2855
+ if (asset2.thumbnail) return asset2.thumbnail;
2856
+ if (asset2.thumbnails?.x200) return asset2.thumbnails.x200;
2857
+ if (asset2.thumbnails?.x100) return asset2.thumbnails.x100;
2858
+ if (asset2.thumbnails?.x512) return asset2.thumbnails.x512;
2859
+ return null;
2860
+ }
2861
+ async function probeSize(url) {
2862
+ try {
2863
+ const res = await fetch(url, { method: "HEAD" });
2864
+ const len = res.headers.get("content-length");
2865
+ return len ? Number(len) : null;
2866
+ } catch {
2867
+ return null;
2868
+ }
2869
+ }
2870
+ function probeImageDimensions(url) {
2871
+ return new Promise((resolve) => {
2872
+ const img = new Image();
2873
+ img.crossOrigin = "anonymous";
2874
+ img.onload = () => resolve({ w: img.naturalWidth, h: img.naturalHeight });
2875
+ img.onerror = () => resolve(null);
2876
+ img.src = url;
2877
+ });
2878
+ }
2879
+ var Row = ({ label, value, mono }) => /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-3 py-1", children: [
2880
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: label }),
2881
+ /* @__PURE__ */ jsx("span", { className: cn("text-xs text-foreground text-right truncate", mono && "font-mono"), children: value })
2882
+ ] });
2883
+ var AssetDetails = ({
2884
+ asset: asset2,
2885
+ onClose,
2886
+ onRename,
2887
+ onReplace,
2888
+ onEditTags,
2889
+ onEditImage,
2890
+ onDelete,
2891
+ onFix
2892
+ }) => {
2893
+ const Icon = getIcon2(asset2.mimeType);
2894
+ const thumbUrl = getThumbnailUrl(asset2);
2895
+ const isImage = (asset2.mimeType || "").startsWith("image/");
2896
+ const unknownMime = isUnknownMime(asset2);
2897
+ const missingThumbnail = !thumbUrl && isImage;
2898
+ const [dims, setDims] = useState(null);
2899
+ const [thumbDims, setThumbDims] = useState(null);
2900
+ const [thumbBytes, setThumbBytes] = useState(null);
2901
+ const [sourceMimeProbe, setSourceMimeProbe] = useState(null);
2902
+ const [fixing, setFixing] = useState(false);
2903
+ useEffect(() => {
2904
+ let cancelled = false;
2905
+ (async () => {
2906
+ if (isImage) {
2907
+ const d = await probeImageDimensions(asset2.url);
2908
+ if (!cancelled) setDims(d);
2909
+ }
2910
+ if (thumbUrl) {
2911
+ const [d, b] = await Promise.all([probeImageDimensions(thumbUrl), probeSize(thumbUrl)]);
2912
+ if (cancelled) return;
2913
+ setThumbDims(d);
2914
+ setThumbBytes(b);
2915
+ }
2916
+ if (unknownMime) {
2917
+ try {
2918
+ const res = await fetch(asset2.url);
2919
+ if (!res.ok) return;
2920
+ const blob = await res.blob();
2921
+ const file = new File([blob], asset2.name || "file", { type: blob.type });
2922
+ const sniffed = await sniffImageMime(file);
2923
+ if (!cancelled) setSourceMimeProbe(sniffed);
2924
+ } catch {
2925
+ }
2926
+ }
2927
+ })();
2928
+ return () => {
2929
+ cancelled = true;
2930
+ };
2931
+ }, [asset2.id, asset2.url, thumbUrl, isImage, unknownMime]);
2932
+ useEffect(() => {
2933
+ const onKey = (e) => {
2934
+ if (e.key === "Escape") {
2935
+ e.stopPropagation();
2936
+ onClose();
2937
+ }
2938
+ };
2939
+ window.addEventListener("keydown", onKey, true);
2940
+ return () => window.removeEventListener("keydown", onKey, true);
2941
+ }, [onClose]);
2942
+ const copyId = () => {
2943
+ try {
2944
+ navigator.clipboard?.writeText(asset2.id);
2945
+ } catch {
2946
+ }
2947
+ };
2948
+ const handleFix = async () => {
2949
+ if (!onFix || fixing) return;
2950
+ setFixing(true);
2951
+ try {
2952
+ await onFix(asset2);
2953
+ } finally {
2954
+ setFixing(false);
2955
+ }
2956
+ };
2957
+ if (typeof document === "undefined") return null;
2958
+ const warnings = [];
2959
+ if (unknownMime) {
2960
+ const suggestion = sourceMimeProbe ? ` Detected as ${sourceMimeProbe.mime}.` : "";
2961
+ warnings.push({ kind: "mime", text: `File type is missing or unknown.${suggestion}` });
2962
+ }
2963
+ if (missingThumbnail) warnings.push({ kind: "thumb", text: "Thumbnail has not been generated." });
2964
+ return createPortal(
2965
+ /* @__PURE__ */ jsxs(
2966
+ "div",
2967
+ {
2968
+ className: "fixed inset-0 z-[2147483646] flex items-center justify-center p-4",
2969
+ onMouseDown: (e) => e.stopPropagation(),
2970
+ onClick: (e) => e.stopPropagation(),
2971
+ children: [
2972
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/50", onClick: onClose }),
2973
+ /* @__PURE__ */ jsxs(
2974
+ "div",
2975
+ {
2976
+ role: "dialog",
2977
+ "aria-modal": "true",
2978
+ "aria-label": "Asset details",
2979
+ className: "relative w-full max-w-lg max-h-[90vh] overflow-auto rounded-lg border border-border bg-popover text-popover-foreground shadow-xl",
2980
+ children: [
2981
+ /* @__PURE__ */ jsxs("div", { className: "sticky top-0 z-10 flex items-center justify-between gap-2 px-4 py-3 border-b border-border bg-popover", children: [
2982
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold truncate", children: asset2.cleanName || asset2.name || "Asset details" }),
2983
+ /* @__PURE__ */ jsx(
2984
+ "button",
2985
+ {
2986
+ type: "button",
2987
+ onClick: onClose,
2988
+ className: "p-1 rounded hover:bg-accent text-muted-foreground",
2989
+ "aria-label": "Close",
2990
+ children: /* @__PURE__ */ jsx(X, { className: "w-4 h-4" })
2991
+ }
2992
+ )
2993
+ ] }),
2994
+ /* @__PURE__ */ jsxs("div", { className: "p-4 space-y-4", children: [
2995
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-3", children: [
2996
+ /* @__PURE__ */ jsx("div", { className: "w-28 h-28 rounded-md bg-muted flex items-center justify-center overflow-hidden flex-shrink-0 border border-border", children: thumbUrl ? /* @__PURE__ */ jsx("img", { src: thumbUrl, alt: "", className: "w-full h-full object-cover" }) : isImage ? /* @__PURE__ */ jsx("img", { src: asset2.url, alt: "", className: "w-full h-full object-cover", onError: () => {
2997
+ } }) : /* @__PURE__ */ jsx(Icon, { className: "w-10 h-10 text-muted-foreground" }) }),
2998
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
2999
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium truncate", title: asset2.name, children: asset2.cleanName || asset2.name || asset2.id }),
3000
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground truncate", title: asset2.id, children: /* @__PURE__ */ jsxs("button", { type: "button", onClick: copyId, className: "inline-flex items-center gap-1 hover:text-foreground", children: [
3001
+ /* @__PURE__ */ jsx(Copy, { className: "w-3 h-3" }),
3002
+ " ",
3003
+ asset2.id
3004
+ ] }) }),
3005
+ /* @__PURE__ */ jsxs(
3006
+ "a",
3007
+ {
3008
+ href: asset2.url,
3009
+ target: "_blank",
3010
+ rel: "noreferrer",
3011
+ className: "mt-1 inline-flex items-center gap-1 text-[11px] text-primary hover:underline",
3012
+ children: [
3013
+ /* @__PURE__ */ jsx(ExternalLink, { className: "w-3 h-3" }),
3014
+ " Open original"
3015
+ ]
3016
+ }
3017
+ )
3018
+ ] })
3019
+ ] }),
3020
+ warnings.length > 0 && /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-amber-500/40 bg-amber-500/10 p-3 space-y-2", children: [
3021
+ warnings.map((w, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 text-xs text-foreground", children: [
3022
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "w-3.5 h-3.5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" }),
3023
+ /* @__PURE__ */ jsx("span", { children: w.text })
3024
+ ] }, i)),
3025
+ onFix && /* @__PURE__ */ jsxs(
3026
+ "button",
3027
+ {
3028
+ type: "button",
3029
+ onClick: handleFix,
3030
+ disabled: fixing,
3031
+ className: cn(
3032
+ "mt-1 inline-flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-md",
3033
+ "bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
3034
+ ),
3035
+ title: "Re-saves the file with the correct type so the backend regenerates the thumbnail.",
3036
+ children: [
3037
+ fixing ? /* @__PURE__ */ jsx(Loader2, { className: "w-3 h-3 animate-spin" }) : /* @__PURE__ */ jsx(Wrench, { className: "w-3 h-3" }),
3038
+ fixing ? "Fixing\u2026" : "Fix file type & thumbnail"
3039
+ ]
3040
+ }
3041
+ )
3042
+ ] }),
3043
+ warnings.length === 0 && isImage && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-emerald-700 dark:text-emerald-400", children: [
3044
+ /* @__PURE__ */ jsx(CheckCircle2, { className: "w-3.5 h-3.5" }),
3045
+ " File type and thumbnail look healthy."
3046
+ ] }),
3047
+ /* @__PURE__ */ jsxs("section", { children: [
3048
+ /* @__PURE__ */ jsx("h4", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground mb-1", children: "File" }),
3049
+ /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-border divide-y divide-border px-3", children: [
3050
+ /* @__PURE__ */ jsx(Row, { label: "Type", value: asset2.mimeType || /* @__PURE__ */ jsx("span", { className: "text-amber-600 dark:text-amber-400", children: "unknown" }), mono: true }),
3051
+ /* @__PURE__ */ jsx(Row, { label: "Size", value: formatSize2(asset2.size) }),
3052
+ /* @__PURE__ */ jsx(Row, { label: "Dimensions", value: dims ? `${dims.w} \xD7 ${dims.h}` : isImage ? "\u2014" : "n/a" }),
3053
+ /* @__PURE__ */ jsx(Row, { label: "Uploaded", value: formatAge(asset2.createdAt) }),
3054
+ asset2.app && /* @__PURE__ */ jsx(Row, { label: "App", value: asset2.app, mono: true }),
3055
+ asset2.labels && asset2.labels.length > 0 && /* @__PURE__ */ jsx(Row, { label: "Labels", value: asset2.labels.join(", ") })
3056
+ ] })
3057
+ ] }),
3058
+ isImage && /* @__PURE__ */ jsxs("section", { children: [
3059
+ /* @__PURE__ */ jsx("h4", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground mb-1", children: "Thumbnail" }),
3060
+ /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-border divide-y divide-border px-3", children: [
3061
+ /* @__PURE__ */ jsx(
3062
+ Row,
3063
+ {
3064
+ label: "Status",
3065
+ value: thumbUrl ? /* @__PURE__ */ jsx("span", { className: "text-emerald-700 dark:text-emerald-400", children: "Generated" }) : /* @__PURE__ */ jsx("span", { className: "text-amber-600 dark:text-amber-400", children: "Missing" })
3066
+ }
3067
+ ),
3068
+ thumbUrl && /* @__PURE__ */ jsx(Row, { label: "Dimensions", value: thumbDims ? `${thumbDims.w} \xD7 ${thumbDims.h}` : "\u2014" }),
3069
+ thumbUrl && /* @__PURE__ */ jsx(Row, { label: "Size", value: formatSize2(thumbBytes) })
3070
+ ] })
3071
+ ] }),
3072
+ /* @__PURE__ */ jsxs("section", { className: "flex flex-wrap gap-2 pt-1 border-t border-border", children: [
3073
+ onEditImage && (asset2.mimeType || "").startsWith("image/") && /* @__PURE__ */ jsxs(
3074
+ "button",
3075
+ {
3076
+ type: "button",
3077
+ onClick: () => {
3078
+ onClose();
3079
+ onEditImage(asset2);
3080
+ },
3081
+ className: "inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-md border border-border bg-background hover:bg-accent",
3082
+ children: [
3083
+ /* @__PURE__ */ jsx(Crop, { className: "w-3 h-3" }),
3084
+ " Edit image"
3085
+ ]
3086
+ }
3087
+ ),
3088
+ onRename && /* @__PURE__ */ jsxs(
3089
+ "button",
3090
+ {
3091
+ type: "button",
3092
+ onClick: () => {
3093
+ onClose();
3094
+ onRename(asset2);
3095
+ },
3096
+ className: "inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-md border border-border bg-background hover:bg-accent",
3097
+ children: [
3098
+ /* @__PURE__ */ jsx(Pencil, { className: "w-3 h-3" }),
3099
+ " Rename"
3100
+ ]
3101
+ }
3102
+ ),
3103
+ onReplace && /* @__PURE__ */ jsxs(
3104
+ "button",
3105
+ {
3106
+ type: "button",
3107
+ onClick: () => {
3108
+ onClose();
3109
+ onReplace(asset2);
3110
+ },
3111
+ className: "inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-md border border-border bg-background hover:bg-accent",
3112
+ children: [
3113
+ /* @__PURE__ */ jsx(Upload, { className: "w-3 h-3" }),
3114
+ " Replace file"
3115
+ ]
3116
+ }
3117
+ ),
3118
+ onEditTags && /* @__PURE__ */ jsxs(
3119
+ "button",
3120
+ {
3121
+ type: "button",
3122
+ onClick: () => {
3123
+ onClose();
3124
+ onEditTags(asset2);
3125
+ },
3126
+ className: "inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-md border border-border bg-background hover:bg-accent",
3127
+ children: [
3128
+ /* @__PURE__ */ jsx(Tag, { className: "w-3 h-3" }),
3129
+ " Edit tags"
3130
+ ]
3131
+ }
3132
+ ),
3133
+ onDelete && /* @__PURE__ */ jsxs(
3134
+ "button",
3135
+ {
3136
+ type: "button",
3137
+ onClick: () => {
3138
+ onClose();
3139
+ onDelete(asset2);
3140
+ },
3141
+ className: "ml-auto inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-md border border-destructive/40 text-destructive bg-background hover:bg-destructive/10",
3142
+ children: [
3143
+ /* @__PURE__ */ jsx(Trash2, { className: "w-3 h-3" }),
3144
+ " Delete"
3145
+ ]
3146
+ }
3147
+ )
3148
+ ] })
3149
+ ] })
3150
+ ]
3151
+ }
3152
+ )
3153
+ ]
3154
+ }
3155
+ ),
3156
+ document.body
3157
+ );
3158
+ };
3159
+ async function buildFixFile(asset2) {
3160
+ try {
3161
+ const res = await fetch(asset2.url);
3162
+ if (!res.ok) return null;
3163
+ const blob = await res.blob();
3164
+ const baseName = (asset2.cleanName || asset2.name || "file").replace(/\.[^.]+$/, "") || "file";
3165
+ const raw = new File([blob], asset2.name || baseName, { type: blob.type || "" });
3166
+ const normalised = await normalizeFileType(raw, baseName);
3167
+ return normalised;
3168
+ } catch {
3169
+ return null;
3170
+ }
3171
+ }
2720
3172
  var InlineConfirm = ({ open, title, body, confirmLabel = "Confirm", cancelLabel = "Cancel", destructive, onConfirm, onCancel }) => {
2721
- React8.useEffect(() => {
3173
+ React9.useEffect(() => {
2722
3174
  if (!open) return;
2723
3175
  const onKey = (e) => {
2724
3176
  if (e.key === "Escape") {
@@ -2823,8 +3275,8 @@ var AttachToContextToggle = ({ checked, onChange, contextLabel }) => /* @__PURE_
2823
3275
  ] })
2824
3276
  ] });
2825
3277
  var ScopedAssetBrowser = ({ scope: _scope, accept: _accept, pageSize: _pageSize, viewMode, search, selectedIds, onToggleSelect, onDoubleClickSelect, onDelete, allowDelete, emptyText, listAppId: _listAppId, currentAppId, currentAppName, getAppName, assets, loading, error, refresh, remove, updateAsset, replaceFile }) => {
2826
- const replaceInputRef = React8.useRef(null);
2827
- const replaceTargetRef = React8.useRef(null);
3278
+ const replaceInputRef = React9.useRef(null);
3279
+ const replaceTargetRef = React9.useRef(null);
2828
3280
  const handleRename = useCallback(async (asset2) => {
2829
3281
  const current = asset2.cleanName || asset2.name || "";
2830
3282
  const next = window.prompt("Rename asset", current);
@@ -2880,6 +3332,16 @@ var ScopedAssetBrowser = ({ scope: _scope, accept: _accept, pageSize: _pageSize,
2880
3332
  await updateAsset(tagEditorAsset.id, { labels: next });
2881
3333
  setTagEditorAsset(null);
2882
3334
  }, [tagEditorAsset, updateAsset]);
3335
+ const [detailsAsset, setDetailsAsset] = useState(null);
3336
+ const handleShowDetails = useCallback((asset2) => {
3337
+ setDetailsAsset(asset2);
3338
+ }, []);
3339
+ const handleFixAsset = useCallback(async (asset2) => {
3340
+ const fixed = await buildFixFile(asset2);
3341
+ if (!fixed) return;
3342
+ const result = await replaceFile(asset2.id, fixed);
3343
+ if (result) setDetailsAsset(result);
3344
+ }, [replaceFile]);
2883
3345
  const [activeLabels, setActiveLabels] = useState(/* @__PURE__ */ new Set());
2884
3346
  const toggleLabel = useCallback((label) => {
2885
3347
  setActiveLabels((prev) => {
@@ -2979,6 +3441,7 @@ var ScopedAssetBrowser = ({ scope: _scope, accept: _accept, pageSize: _pageSize,
2979
3441
  onReplace: handleReplace,
2980
3442
  onEditTags: handleEditTags,
2981
3443
  onEditImage: handleEditImage,
3444
+ onShowDetails: handleShowDetails,
2982
3445
  allowDelete,
2983
3446
  currentAppId,
2984
3447
  currentAppName,
@@ -3024,6 +3487,19 @@ var ScopedAssetBrowser = ({ scope: _scope, accept: _accept, pageSize: _pageSize,
3024
3487
  onSave: handleSaveTags
3025
3488
  }
3026
3489
  ),
3490
+ detailsAsset && /* @__PURE__ */ jsx(
3491
+ AssetDetails,
3492
+ {
3493
+ asset: detailsAsset,
3494
+ onClose: () => setDetailsAsset(null),
3495
+ onRename: handleRename,
3496
+ onReplace: handleReplace,
3497
+ onEditTags: handleEditTags,
3498
+ onEditImage: handleEditImage,
3499
+ onDelete: allowDelete ? (a) => handleDeleteWithConfirm(a.id) : void 0,
3500
+ onFix: handleFixAsset
3501
+ }
3502
+ ),
3027
3503
  /* @__PURE__ */ jsx(
3028
3504
  InlineConfirm,
3029
3505
  {
@@ -3228,11 +3704,14 @@ var AssetPickerContent = ({
3228
3704
  name,
3229
3705
  ...sameAsActive ? {} : { scopeOverride: targetScope }
3230
3706
  });
3231
- if (result && !multiple) {
3232
- setSelectedIds(/* @__PURE__ */ new Set([result.id]));
3233
- onSelect?.(toSelection(result));
3707
+ if (result) {
3708
+ setTab("browse");
3709
+ if (!multiple) {
3710
+ setSelectedIds(/* @__PURE__ */ new Set([result.id]));
3711
+ onSelect?.(toSelection(result));
3712
+ }
3713
+ await reconcileAfterUpload(targetScope);
3234
3714
  }
3235
- if (result) await reconcileAfterUpload(targetScope);
3236
3715
  return result;
3237
3716
  }, [uploadFromRemoteUrl, multiple, onSelect, toSelection, uploadScope, activeScope, reconcileAfterUpload]);
3238
3717
  const handleDelete = useCallback(async (assetId) => {
@@ -3634,5 +4113,5 @@ var AssetPicker = (props) => {
3634
4113
  assertStylesLoaded();
3635
4114
 
3636
4115
  export { ASSET_MIME_FILTERS, AssetPicker, useAppRegistry, useAssets };
3637
- //# sourceMappingURL=chunk-E3GQ6LNZ.js.map
3638
- //# sourceMappingURL=chunk-E3GQ6LNZ.js.map
4116
+ //# sourceMappingURL=chunk-I3T36FSI.js.map
4117
+ //# sourceMappingURL=chunk-I3T36FSI.js.map