@proveanything/smartlinks-utils-ui 1.13.8 → 1.13.11

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 (40) hide show
  1. package/README.md +6 -6
  2. package/dist/{chunk-OTJV62XV.js → chunk-5ZQT2GGU.js} +5 -5
  3. package/dist/chunk-5ZQT2GGU.js.map +1 -0
  4. package/dist/{chunk-E3GQ6LNZ.js → chunk-7RWLFKHC.js} +502 -39
  5. package/dist/chunk-7RWLFKHC.js.map +1 -0
  6. package/dist/{chunk-7UBXTFZQ.js → chunk-A4YZYKWT.js} +9 -8
  7. package/dist/chunk-A4YZYKWT.js.map +1 -0
  8. package/dist/{chunk-4LHF5JB7.js → chunk-DH5HG5DW.js} +15 -6
  9. package/dist/chunk-DH5HG5DW.js.map +1 -0
  10. package/dist/{chunk-JMCV6FOW.js → chunk-WVCNIX7N.js} +3 -3
  11. package/dist/{chunk-JMCV6FOW.js.map → chunk-WVCNIX7N.js.map} +1 -1
  12. package/dist/components/AssetPicker/index.css +34 -0
  13. package/dist/components/AssetPicker/index.css.map +1 -1
  14. package/dist/components/AssetPicker/index.js +1 -1
  15. package/dist/components/ConditionsEditor/index.css +34 -0
  16. package/dist/components/ConditionsEditor/index.css.map +1 -1
  17. package/dist/components/ConditionsEditor/index.d.ts +2 -2
  18. package/dist/components/ConditionsEditor/index.js +2 -2
  19. package/dist/components/FacetRuleEditor/index.d.ts +1 -1
  20. package/dist/components/FacetRuleEditor/index.js +2 -2
  21. package/dist/components/FontPicker/index.css +34 -0
  22. package/dist/components/FontPicker/index.css.map +1 -1
  23. package/dist/components/FontPicker/index.js +1 -1
  24. package/dist/components/IconPicker/index.css +34 -0
  25. package/dist/components/IconPicker/index.css.map +1 -1
  26. package/dist/components/LinkPicker/index.css +34 -0
  27. package/dist/components/LinkPicker/index.css.map +1 -1
  28. package/dist/components/RecordsAdmin/index.css +34 -0
  29. package/dist/components/RecordsAdmin/index.css.map +1 -1
  30. package/dist/components/RecordsAdmin/index.js +2 -2
  31. package/dist/index.css +34 -0
  32. package/dist/index.css.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +5 -5
  35. package/dist/{types-a2DdgZ2H.d.ts → types-BLqki3Zy.d.ts} +11 -0
  36. package/package.json +3 -3
  37. package/dist/chunk-4LHF5JB7.js.map +0 -1
  38. package/dist/chunk-7UBXTFZQ.js.map +0 -1
  39. package/dist/chunk-E3GQ6LNZ.js.map +0 -1
  40. 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
@@ -350,7 +350,7 @@ var AppBadge = ({ appId, appName, size = "sm" }) => {
350
350
  }
351
351
  );
352
352
  };
353
- var CardMenu = ({ onRename, onReplace, onEditTags, onEditImage, onDelete, position = "absolute" }) => {
353
+ var CardMenu = ({ onRename, onReplace, onEditTags, onEditImage, onShowDetails, onDelete, position = "absolute" }) => {
354
354
  const [open, setOpen] = useState(false);
355
355
  const ref = useRef(null);
356
356
  const btnRef = useRef(null);
@@ -402,7 +402,7 @@ var CardMenu = ({ onRename, onReplace, onEditTags, onEditImage, onDelete, positi
402
402
  window.removeEventListener("scroll", update, true);
403
403
  };
404
404
  }, [open]);
405
- if (!onRename && !onReplace && !onEditTags && !onEditImage && !onDelete) return null;
405
+ if (!onRename && !onReplace && !onEditTags && !onEditImage && !onShowDetails && !onDelete) return null;
406
406
  return /* @__PURE__ */ jsxs(
407
407
  "div",
408
408
  {
@@ -446,6 +446,23 @@ var CardMenu = ({ onRename, onReplace, onEditTags, onEditImage, onDelete, positi
446
446
  onClick: (e) => e.stopPropagation(),
447
447
  onMouseDown: (e) => e.stopPropagation(),
448
448
  children: [
449
+ onShowDetails && /* @__PURE__ */ jsxs(
450
+ "button",
451
+ {
452
+ type: "button",
453
+ role: "menuitem",
454
+ onClick: (e) => {
455
+ e.stopPropagation();
456
+ setOpen(false);
457
+ onShowDetails();
458
+ },
459
+ className: "w-full flex items-center gap-2 px-2.5 py-1.5 text-xs hover:bg-accent",
460
+ children: [
461
+ /* @__PURE__ */ jsx(Info, { className: "w-3 h-3" }),
462
+ " Details"
463
+ ]
464
+ }
465
+ ),
449
466
  onRename && /* @__PURE__ */ jsxs(
450
467
  "button",
451
468
  {
@@ -540,7 +557,7 @@ var CardMenu = ({ onRename, onReplace, onEditTags, onEditImage, onDelete, positi
540
557
  }
541
558
  );
542
559
  };
543
- var AssetGridItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelete, onRename, onReplace, onEditTags, onEditImage, allowDelete, currentAppId, getAppName, activeLabels, onToggleLabel }) => {
560
+ var AssetGridItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelete, onRename, onReplace, onEditTags, onEditImage, onShowDetails, allowDelete, currentAppId, getAppName, activeLabels, onToggleLabel }) => {
544
561
  const thumb = getThumbnail(asset2);
545
562
  const Icon = getIcon(asset2.mimeType);
546
563
  const ownerAppId = getAssetAppId(asset2);
@@ -622,6 +639,7 @@ var AssetGridItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelet
622
639
  onReplace,
623
640
  onEditTags,
624
641
  onEditImage,
642
+ onShowDetails,
625
643
  onDelete: allowDelete ? onDelete : void 0
626
644
  }
627
645
  )
@@ -629,7 +647,7 @@ var AssetGridItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelet
629
647
  }
630
648
  );
631
649
  };
632
- var AssetListItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelete, onRename, onReplace, onEditTags, onEditImage, allowDelete, currentAppId, getAppName, activeLabels, onToggleLabel }) => {
650
+ var AssetListItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelete, onRename, onReplace, onEditTags, onEditImage, onShowDetails, allowDelete, currentAppId, getAppName, activeLabels, onToggleLabel }) => {
633
651
  const thumb = getThumbnail(asset2);
634
652
  const Icon = getIcon(asset2.mimeType);
635
653
  const ownerAppId = getAssetAppId(asset2);
@@ -699,6 +717,7 @@ var AssetListItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelet
699
717
  onReplace,
700
718
  onEditTags,
701
719
  onEditImage,
720
+ onShowDetails,
702
721
  onDelete: allowDelete ? onDelete : void 0,
703
722
  position: "inline"
704
723
  }
@@ -718,6 +737,7 @@ var AssetGrid = ({
718
737
  onReplace,
719
738
  onEditTags,
720
739
  onEditImage,
740
+ onShowDetails,
721
741
  allowDelete,
722
742
  currentAppId,
723
743
  getAppName,
@@ -738,6 +758,7 @@ var AssetGrid = ({
738
758
  onReplace: onReplace ? () => onReplace(asset2) : void 0,
739
759
  onEditTags: onEditTags ? () => onEditTags(asset2) : void 0,
740
760
  onEditImage: onEditImage ? () => onEditImage(asset2) : void 0,
761
+ onShowDetails: onShowDetails ? () => onShowDetails(asset2) : void 0,
741
762
  allowDelete,
742
763
  currentAppId,
743
764
  getAppName,
@@ -759,6 +780,7 @@ var AssetGrid = ({
759
780
  onReplace: onReplace ? () => onReplace(asset2) : void 0,
760
781
  onEditTags: onEditTags ? () => onEditTags(asset2) : void 0,
761
782
  onEditImage: onEditImage ? () => onEditImage(asset2) : void 0,
783
+ onShowDetails: onShowDetails ? () => onShowDetails(asset2) : void 0,
762
784
  allowDelete,
763
785
  currentAppId,
764
786
  getAppName,
@@ -779,6 +801,51 @@ function isProcessableImage(file) {
779
801
  if (file.type === "image/svg+xml" || file.type === "image/gif") return false;
780
802
  return true;
781
803
  }
804
+ async function sniffImageMime(file) {
805
+ try {
806
+ const head = new Uint8Array(await file.slice(0, 16).arrayBuffer());
807
+ if (head.length < 4) return null;
808
+ if (head[0] === 137 && head[1] === 80 && head[2] === 78 && head[3] === 71) {
809
+ return { mime: "image/png", ext: "png" };
810
+ }
811
+ if (head[0] === 255 && head[1] === 216 && head[2] === 255) {
812
+ return { mime: "image/jpeg", ext: "jpg" };
813
+ }
814
+ if (head[0] === 71 && head[1] === 73 && head[2] === 70 && head[3] === 56) {
815
+ return { mime: "image/gif", ext: "gif" };
816
+ }
817
+ 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) {
818
+ return { mime: "image/webp", ext: "webp" };
819
+ }
820
+ if (head[0] === 66 && head[1] === 77) {
821
+ return { mime: "image/bmp", ext: "bmp" };
822
+ }
823
+ if (head[4] === 102 && head[5] === 116 && head[6] === 121 && head[7] === 112) {
824
+ const brand = String.fromCharCode(head[8], head[9], head[10], head[11]);
825
+ if (brand === "avif" || brand === "avis") return { mime: "image/avif", ext: "avif" };
826
+ if (brand === "heic" || brand === "heix" || brand === "mif1" || brand === "msf1") {
827
+ return { mime: "image/heic", ext: "heic" };
828
+ }
829
+ }
830
+ if (head[0] === 60) {
831
+ const text = new TextDecoder().decode(head);
832
+ if (/^<\?xml|^<svg/i.test(text)) return { mime: "image/svg+xml", ext: "svg" };
833
+ }
834
+ return null;
835
+ } catch {
836
+ return null;
837
+ }
838
+ }
839
+ async function normalizeFileType(file, fallbackBaseName) {
840
+ const declaredType = (file.type || "").toLowerCase();
841
+ const hasExt = /\.[a-z0-9]+$/i.test(file.name);
842
+ if (declaredType.startsWith("image/") && hasExt) return file;
843
+ const sniffed = await sniffImageMime(file);
844
+ if (!sniffed) return file;
845
+ const base = (fallbackBaseName || file.name.replace(/\.[^.]+$/, "") || "image").trim() || "image";
846
+ const newName = `${base}.${sniffed.ext}`;
847
+ return new File([file], newName, { type: sniffed.mime, lastModified: file.lastModified });
848
+ }
782
849
  async function loadImage(file) {
783
850
  const url = URL.createObjectURL(file);
784
851
  const img = new Image();
@@ -1350,18 +1417,22 @@ var UploadZone = ({
1350
1417
  if (!items) return;
1351
1418
  for (const item of Array.from(items)) {
1352
1419
  if (item.kind === "file") {
1353
- const file = item.getAsFile();
1354
- if (!file) continue;
1355
- if (accept && !file.type.startsWith(accept.replace("*", ""))) continue;
1420
+ const raw = item.getAsFile();
1421
+ if (!raw) continue;
1356
1422
  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
- });
1423
+ 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(/\.[^.]+$/, "");
1424
+ void (async () => {
1425
+ const file = await normalizeFileType(raw, defaultBase);
1426
+ if (accept && file.type && !file.type.startsWith(accept.replace("*", ""))) return;
1427
+ const previewUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : "";
1428
+ const nameForDisplay = file.name.replace(/\.[^.]+$/, "") || defaultBase;
1429
+ setPastedFile({ file, previewUrl, name: nameForDisplay, origSize: file.size });
1430
+ setFileName(nameForDisplay);
1431
+ setEditingName(false);
1432
+ getImageDimensions(file).then((dims) => {
1433
+ if (dims) setPastedFile((prev) => prev && prev.file === file ? { ...prev, origDims: dims } : prev);
1434
+ });
1435
+ })();
1365
1436
  return;
1366
1437
  }
1367
1438
  }
@@ -1410,7 +1481,8 @@ var UploadZone = ({
1410
1481
  e.stopPropagation();
1411
1482
  setDragOver(false);
1412
1483
  }, []);
1413
- const presentForRename = useCallback((file) => {
1484
+ const presentForRename = useCallback(async (raw) => {
1485
+ const file = await normalizeFileType(raw);
1414
1486
  const previewUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : "";
1415
1487
  const defaultName = file.name.replace(/\.[^.]+$/, "") || "file";
1416
1488
  setPastedFile({ file, previewUrl, name: defaultName, origSize: file.size });
@@ -1426,7 +1498,7 @@ var UploadZone = ({
1426
1498
  setDragOver(false);
1427
1499
  const files = Array.from(e.dataTransfer.files);
1428
1500
  if (files.length === 1 && !multiple) {
1429
- presentForRename(files[0]);
1501
+ void presentForRename(files[0]);
1430
1502
  } else if (files.length > 0) {
1431
1503
  void handleBatchFiles(multiple ? files : [files[0]]);
1432
1504
  }
@@ -1434,21 +1506,22 @@ var UploadZone = ({
1434
1506
  const handleInputChange = useCallback((e) => {
1435
1507
  const files = Array.from(e.target.files || []);
1436
1508
  if (files.length === 1 && !multiple) {
1437
- presentForRename(files[0]);
1509
+ void presentForRename(files[0]);
1438
1510
  } else if (files.length > 0) {
1439
1511
  void handleBatchFiles(multiple ? files : [files[0]]);
1440
1512
  }
1441
1513
  e.target.value = "";
1442
1514
  }, [onFiles, multiple, presentForRename]);
1443
1515
  const handleBatchFiles = useCallback(async (files) => {
1516
+ const normalized = await Promise.all(files.map((f) => normalizeFileType(f)));
1444
1517
  if (!autoOptimize) {
1445
- onFiles(files);
1518
+ onFiles(normalized);
1446
1519
  return;
1447
1520
  }
1448
1521
  setOptimizing(true);
1449
1522
  try {
1450
1523
  const out = [];
1451
- for (const f of files) {
1524
+ for (const f of normalized) {
1452
1525
  if (isProcessableImage(f)) {
1453
1526
  const r = await processImage(f, {
1454
1527
  maxDimension: optConfig.maxDimension,
@@ -1668,12 +1741,8 @@ var UploadZone = ({
1668
1741
  onDragEnter: handleDragIn,
1669
1742
  onDragLeave: handleDragOut,
1670
1743
  onDrop: handleDrop,
1671
- onClick: () => inputRef.current?.click(),
1672
- role: "button",
1744
+ onClick: () => zoneRef.current?.focus(),
1673
1745
  tabIndex: 0,
1674
- onKeyDown: (e) => {
1675
- if (e.key === "Enter" || e.key === " ") inputRef.current?.click();
1676
- },
1677
1746
  children: [
1678
1747
  /* @__PURE__ */ jsx(
1679
1748
  "input",
@@ -1699,8 +1768,20 @@ var UploadZone = ({
1699
1768
  ] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1 py-2", children: [
1700
1769
  /* @__PURE__ */ jsx(Upload, { className: "w-6 h-6 text-muted-foreground" }),
1701
1770
  /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
1702
- "Drop files here, ",
1703
- /* @__PURE__ */ jsx("span", { className: "text-primary underline", children: "browse" }),
1771
+ "Drop files here,",
1772
+ " ",
1773
+ /* @__PURE__ */ jsx(
1774
+ "button",
1775
+ {
1776
+ type: "button",
1777
+ onClick: (e) => {
1778
+ e.stopPropagation();
1779
+ inputRef.current?.click();
1780
+ },
1781
+ className: "text-primary underline hover:no-underline focus:outline-none focus:ring-1 focus:ring-ring rounded",
1782
+ children: "browse"
1783
+ }
1784
+ ),
1704
1785
  ", or paste from clipboard"
1705
1786
  ] }),
1706
1787
  /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground flex items-center gap-1", children: [
@@ -2329,6 +2410,10 @@ var StockPhotoSearch = ({
2329
2410
  }
2330
2411
  }, [query, orientation, collectionId]);
2331
2412
  const handlePick = useCallback((photo) => {
2413
+ setStaged((prev) => {
2414
+ if (prev?.previewUrl) URL.revokeObjectURL(prev.previewUrl);
2415
+ return null;
2416
+ });
2332
2417
  const baseName = `stock-${(photo.alt || query.trim() || "photo").slice(0, 60).replace(/\s+/g, "-")}`;
2333
2418
  setStaged({ photo, name: baseName, file: null, previewUrl: null });
2334
2419
  }, [query]);
@@ -2543,7 +2628,7 @@ var StockPhotoSearch = ({
2543
2628
  {
2544
2629
  type: "button",
2545
2630
  onClick: () => handlePick(photo),
2546
- disabled: !!staged || saving,
2631
+ disabled: isStaged || saving,
2547
2632
  className: cn(
2548
2633
  "absolute inset-0 flex items-center justify-center text-xs font-medium",
2549
2634
  "bg-background/80 opacity-0 group-hover:opacity-100 transition-opacity",
@@ -2717,8 +2802,359 @@ var TagEditor = ({ initial, suggestions, assetName, onCancel, onSave }) => {
2717
2802
  }
2718
2803
  );
2719
2804
  };
2805
+ function getIcon2(mimeType) {
2806
+ if (!mimeType) return FileIcon;
2807
+ if (mimeType.startsWith("image/")) return Image$1;
2808
+ if (mimeType.startsWith("video/")) return Film;
2809
+ if (mimeType.startsWith("audio/")) return Music;
2810
+ return FileText;
2811
+ }
2812
+ function formatSize2(bytes) {
2813
+ if (bytes == null) return "\u2014";
2814
+ if (bytes < 1024) return `${bytes} B`;
2815
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
2816
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
2817
+ }
2818
+ function formatAge(iso) {
2819
+ if (!iso) return "\u2014";
2820
+ const t = Date.parse(iso);
2821
+ if (Number.isNaN(t)) return iso;
2822
+ const diff = Date.now() - t;
2823
+ const day = 24 * 60 * 60 * 1e3;
2824
+ if (diff < 6e4) return "just now";
2825
+ if (diff < 36e5) return `${Math.floor(diff / 6e4)} min ago`;
2826
+ if (diff < day) return `${Math.floor(diff / 36e5)} h ago`;
2827
+ if (diff < 30 * day) return `${Math.floor(diff / day)} d ago`;
2828
+ if (diff < 365 * day) return `${Math.floor(diff / (30 * day))} mo ago`;
2829
+ return `${Math.floor(diff / (365 * day))} y ago`;
2830
+ }
2831
+ function isUnknownMime(asset2) {
2832
+ const m = (asset2.mimeType || "").toLowerCase();
2833
+ if (!m || m === "application/octet-stream" || m === "unknown" || m.endsWith("/unknown")) return true;
2834
+ const name = (asset2.name || asset2.cleanName || "").toLowerCase();
2835
+ if (/\.unknown(\?|$)/.test(name)) return true;
2836
+ return false;
2837
+ }
2838
+ function getThumbnailUrl(asset2) {
2839
+ if (asset2.thumbnail) return asset2.thumbnail;
2840
+ if (asset2.thumbnails?.x200) return asset2.thumbnails.x200;
2841
+ if (asset2.thumbnails?.x100) return asset2.thumbnails.x100;
2842
+ if (asset2.thumbnails?.x512) return asset2.thumbnails.x512;
2843
+ return null;
2844
+ }
2845
+ async function probeSize(url) {
2846
+ try {
2847
+ const res = await fetch(url, { method: "HEAD" });
2848
+ const len = res.headers.get("content-length");
2849
+ return len ? Number(len) : null;
2850
+ } catch {
2851
+ return null;
2852
+ }
2853
+ }
2854
+ function probeImageDimensions(url) {
2855
+ return new Promise((resolve) => {
2856
+ const img = new Image();
2857
+ img.crossOrigin = "anonymous";
2858
+ img.onload = () => resolve({ w: img.naturalWidth, h: img.naturalHeight });
2859
+ img.onerror = () => resolve(null);
2860
+ img.src = url;
2861
+ });
2862
+ }
2863
+ var Row = ({ label, value, mono }) => /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-3 py-1", children: [
2864
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: label }),
2865
+ /* @__PURE__ */ jsx("span", { className: cn("text-xs text-foreground text-right truncate", mono && "font-mono"), children: value })
2866
+ ] });
2867
+ var AssetDetails = ({
2868
+ asset: asset2,
2869
+ onClose,
2870
+ onRename,
2871
+ onReplace,
2872
+ onEditTags,
2873
+ onEditImage,
2874
+ onDelete,
2875
+ onFix
2876
+ }) => {
2877
+ const Icon = getIcon2(asset2.mimeType);
2878
+ const thumbUrl = getThumbnailUrl(asset2);
2879
+ const isImage = (asset2.mimeType || "").startsWith("image/");
2880
+ const unknownMime = isUnknownMime(asset2);
2881
+ const missingThumbnail = !thumbUrl && isImage;
2882
+ const [dims, setDims] = useState(null);
2883
+ const [thumbDims, setThumbDims] = useState(null);
2884
+ const [thumbBytes, setThumbBytes] = useState(null);
2885
+ const [sourceMimeProbe, setSourceMimeProbe] = useState(null);
2886
+ const [fixing, setFixing] = useState(false);
2887
+ useEffect(() => {
2888
+ let cancelled = false;
2889
+ (async () => {
2890
+ if (isImage) {
2891
+ const d = await probeImageDimensions(asset2.url);
2892
+ if (!cancelled) setDims(d);
2893
+ }
2894
+ if (thumbUrl) {
2895
+ const [d, b] = await Promise.all([probeImageDimensions(thumbUrl), probeSize(thumbUrl)]);
2896
+ if (cancelled) return;
2897
+ setThumbDims(d);
2898
+ setThumbBytes(b);
2899
+ }
2900
+ if (unknownMime) {
2901
+ try {
2902
+ const res = await fetch(asset2.url);
2903
+ if (!res.ok) return;
2904
+ const blob = await res.blob();
2905
+ const file = new File([blob], asset2.name || "file", { type: blob.type });
2906
+ const sniffed = await sniffImageMime(file);
2907
+ if (!cancelled) setSourceMimeProbe(sniffed);
2908
+ } catch {
2909
+ }
2910
+ }
2911
+ })();
2912
+ return () => {
2913
+ cancelled = true;
2914
+ };
2915
+ }, [asset2.id, asset2.url, thumbUrl, isImage, unknownMime]);
2916
+ useEffect(() => {
2917
+ const onKey = (e) => {
2918
+ if (e.key === "Escape") {
2919
+ e.stopPropagation();
2920
+ onClose();
2921
+ }
2922
+ };
2923
+ window.addEventListener("keydown", onKey, true);
2924
+ return () => window.removeEventListener("keydown", onKey, true);
2925
+ }, [onClose]);
2926
+ const copyId = () => {
2927
+ try {
2928
+ navigator.clipboard?.writeText(asset2.id);
2929
+ } catch {
2930
+ }
2931
+ };
2932
+ const handleFix = async () => {
2933
+ if (!onFix || fixing) return;
2934
+ setFixing(true);
2935
+ try {
2936
+ await onFix(asset2);
2937
+ } finally {
2938
+ setFixing(false);
2939
+ }
2940
+ };
2941
+ if (typeof document === "undefined") return null;
2942
+ const warnings = [];
2943
+ if (unknownMime) {
2944
+ const suggestion = sourceMimeProbe ? ` Detected as ${sourceMimeProbe.mime}.` : "";
2945
+ warnings.push({ kind: "mime", text: `File type is missing or unknown.${suggestion}` });
2946
+ }
2947
+ if (missingThumbnail) warnings.push({ kind: "thumb", text: "Thumbnail has not been generated." });
2948
+ return createPortal(
2949
+ /* @__PURE__ */ jsxs(
2950
+ "div",
2951
+ {
2952
+ className: "fixed inset-0 z-[2147483646] flex items-center justify-center p-4",
2953
+ onMouseDown: (e) => e.stopPropagation(),
2954
+ onClick: (e) => e.stopPropagation(),
2955
+ children: [
2956
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/50", onClick: onClose }),
2957
+ /* @__PURE__ */ jsxs(
2958
+ "div",
2959
+ {
2960
+ role: "dialog",
2961
+ "aria-modal": "true",
2962
+ "aria-label": "Asset details",
2963
+ className: "relative w-full max-w-lg max-h-[90vh] overflow-auto rounded-lg border border-border bg-popover text-popover-foreground shadow-xl",
2964
+ children: [
2965
+ /* @__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: [
2966
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold truncate", children: asset2.cleanName || asset2.name || "Asset details" }),
2967
+ /* @__PURE__ */ jsx(
2968
+ "button",
2969
+ {
2970
+ type: "button",
2971
+ onClick: onClose,
2972
+ className: "p-1 rounded hover:bg-accent text-muted-foreground",
2973
+ "aria-label": "Close",
2974
+ children: /* @__PURE__ */ jsx(X, { className: "w-4 h-4" })
2975
+ }
2976
+ )
2977
+ ] }),
2978
+ /* @__PURE__ */ jsxs("div", { className: "p-4 space-y-4", children: [
2979
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-3", children: [
2980
+ /* @__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: () => {
2981
+ } }) : /* @__PURE__ */ jsx(Icon, { className: "w-10 h-10 text-muted-foreground" }) }),
2982
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
2983
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium truncate", title: asset2.name, children: asset2.cleanName || asset2.name || asset2.id }),
2984
+ /* @__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: [
2985
+ /* @__PURE__ */ jsx(Copy, { className: "w-3 h-3" }),
2986
+ " ",
2987
+ asset2.id
2988
+ ] }) }),
2989
+ /* @__PURE__ */ jsxs(
2990
+ "a",
2991
+ {
2992
+ href: asset2.url,
2993
+ target: "_blank",
2994
+ rel: "noreferrer",
2995
+ className: "mt-1 inline-flex items-center gap-1 text-[11px] text-primary hover:underline",
2996
+ children: [
2997
+ /* @__PURE__ */ jsx(ExternalLink, { className: "w-3 h-3" }),
2998
+ " Open original"
2999
+ ]
3000
+ }
3001
+ )
3002
+ ] })
3003
+ ] }),
3004
+ warnings.length > 0 && /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-amber-500/40 bg-amber-500/10 p-3 space-y-2", children: [
3005
+ warnings.map((w, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 text-xs text-foreground", children: [
3006
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "w-3.5 h-3.5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" }),
3007
+ /* @__PURE__ */ jsx("span", { children: w.text })
3008
+ ] }, i)),
3009
+ onFix && /* @__PURE__ */ jsxs(
3010
+ "button",
3011
+ {
3012
+ type: "button",
3013
+ onClick: handleFix,
3014
+ disabled: fixing,
3015
+ className: cn(
3016
+ "mt-1 inline-flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-md",
3017
+ "bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
3018
+ ),
3019
+ title: "Re-saves the file with the correct type so the backend regenerates the thumbnail.",
3020
+ children: [
3021
+ fixing ? /* @__PURE__ */ jsx(Loader2, { className: "w-3 h-3 animate-spin" }) : /* @__PURE__ */ jsx(Wrench, { className: "w-3 h-3" }),
3022
+ fixing ? "Fixing\u2026" : "Fix file type & thumbnail"
3023
+ ]
3024
+ }
3025
+ )
3026
+ ] }),
3027
+ warnings.length === 0 && isImage && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-emerald-700 dark:text-emerald-400", children: [
3028
+ /* @__PURE__ */ jsx(CheckCircle2, { className: "w-3.5 h-3.5" }),
3029
+ " File type and thumbnail look healthy."
3030
+ ] }),
3031
+ /* @__PURE__ */ jsxs("section", { children: [
3032
+ /* @__PURE__ */ jsx("h4", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground mb-1", children: "File" }),
3033
+ /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-border divide-y divide-border px-3", children: [
3034
+ /* @__PURE__ */ jsx(Row, { label: "Type", value: asset2.mimeType || /* @__PURE__ */ jsx("span", { className: "text-amber-600 dark:text-amber-400", children: "unknown" }), mono: true }),
3035
+ /* @__PURE__ */ jsx(Row, { label: "Size", value: formatSize2(asset2.size) }),
3036
+ /* @__PURE__ */ jsx(Row, { label: "Dimensions", value: dims ? `${dims.w} \xD7 ${dims.h}` : isImage ? "\u2014" : "n/a" }),
3037
+ /* @__PURE__ */ jsx(Row, { label: "Uploaded", value: formatAge(asset2.createdAt) }),
3038
+ asset2.app && /* @__PURE__ */ jsx(Row, { label: "App", value: asset2.app, mono: true }),
3039
+ asset2.labels && asset2.labels.length > 0 && /* @__PURE__ */ jsx(Row, { label: "Labels", value: asset2.labels.join(", ") })
3040
+ ] })
3041
+ ] }),
3042
+ isImage && /* @__PURE__ */ jsxs("section", { children: [
3043
+ /* @__PURE__ */ jsx("h4", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground mb-1", children: "Thumbnail" }),
3044
+ /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-border divide-y divide-border px-3", children: [
3045
+ /* @__PURE__ */ jsx(
3046
+ Row,
3047
+ {
3048
+ label: "Status",
3049
+ 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" })
3050
+ }
3051
+ ),
3052
+ thumbUrl && /* @__PURE__ */ jsx(Row, { label: "Dimensions", value: thumbDims ? `${thumbDims.w} \xD7 ${thumbDims.h}` : "\u2014" }),
3053
+ thumbUrl && /* @__PURE__ */ jsx(Row, { label: "Size", value: formatSize2(thumbBytes) })
3054
+ ] })
3055
+ ] }),
3056
+ /* @__PURE__ */ jsxs("section", { className: "flex flex-wrap gap-2 pt-1 border-t border-border", children: [
3057
+ onEditImage && (asset2.mimeType || "").startsWith("image/") && /* @__PURE__ */ jsxs(
3058
+ "button",
3059
+ {
3060
+ type: "button",
3061
+ onClick: () => {
3062
+ onClose();
3063
+ onEditImage(asset2);
3064
+ },
3065
+ 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",
3066
+ children: [
3067
+ /* @__PURE__ */ jsx(Crop, { className: "w-3 h-3" }),
3068
+ " Edit image"
3069
+ ]
3070
+ }
3071
+ ),
3072
+ onRename && /* @__PURE__ */ jsxs(
3073
+ "button",
3074
+ {
3075
+ type: "button",
3076
+ onClick: () => {
3077
+ onClose();
3078
+ onRename(asset2);
3079
+ },
3080
+ 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",
3081
+ children: [
3082
+ /* @__PURE__ */ jsx(Pencil, { className: "w-3 h-3" }),
3083
+ " Rename"
3084
+ ]
3085
+ }
3086
+ ),
3087
+ onReplace && /* @__PURE__ */ jsxs(
3088
+ "button",
3089
+ {
3090
+ type: "button",
3091
+ onClick: () => {
3092
+ onClose();
3093
+ onReplace(asset2);
3094
+ },
3095
+ 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",
3096
+ children: [
3097
+ /* @__PURE__ */ jsx(Upload, { className: "w-3 h-3" }),
3098
+ " Replace file"
3099
+ ]
3100
+ }
3101
+ ),
3102
+ onEditTags && /* @__PURE__ */ jsxs(
3103
+ "button",
3104
+ {
3105
+ type: "button",
3106
+ onClick: () => {
3107
+ onClose();
3108
+ onEditTags(asset2);
3109
+ },
3110
+ 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",
3111
+ children: [
3112
+ /* @__PURE__ */ jsx(Tag, { className: "w-3 h-3" }),
3113
+ " Edit tags"
3114
+ ]
3115
+ }
3116
+ ),
3117
+ onDelete && /* @__PURE__ */ jsxs(
3118
+ "button",
3119
+ {
3120
+ type: "button",
3121
+ onClick: () => {
3122
+ onClose();
3123
+ onDelete(asset2);
3124
+ },
3125
+ 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",
3126
+ children: [
3127
+ /* @__PURE__ */ jsx(Trash2, { className: "w-3 h-3" }),
3128
+ " Delete"
3129
+ ]
3130
+ }
3131
+ )
3132
+ ] })
3133
+ ] })
3134
+ ]
3135
+ }
3136
+ )
3137
+ ]
3138
+ }
3139
+ ),
3140
+ document.body
3141
+ );
3142
+ };
3143
+ async function buildFixFile(asset2) {
3144
+ try {
3145
+ const res = await fetch(asset2.url);
3146
+ if (!res.ok) return null;
3147
+ const blob = await res.blob();
3148
+ const baseName = (asset2.cleanName || asset2.name || "file").replace(/\.[^.]+$/, "") || "file";
3149
+ const raw = new File([blob], asset2.name || baseName, { type: blob.type || "" });
3150
+ const normalised = await normalizeFileType(raw, baseName);
3151
+ return normalised;
3152
+ } catch {
3153
+ return null;
3154
+ }
3155
+ }
2720
3156
  var InlineConfirm = ({ open, title, body, confirmLabel = "Confirm", cancelLabel = "Cancel", destructive, onConfirm, onCancel }) => {
2721
- React8.useEffect(() => {
3157
+ React9.useEffect(() => {
2722
3158
  if (!open) return;
2723
3159
  const onKey = (e) => {
2724
3160
  if (e.key === "Escape") {
@@ -2823,8 +3259,8 @@ var AttachToContextToggle = ({ checked, onChange, contextLabel }) => /* @__PURE_
2823
3259
  ] })
2824
3260
  ] });
2825
3261
  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);
3262
+ const replaceInputRef = React9.useRef(null);
3263
+ const replaceTargetRef = React9.useRef(null);
2828
3264
  const handleRename = useCallback(async (asset2) => {
2829
3265
  const current = asset2.cleanName || asset2.name || "";
2830
3266
  const next = window.prompt("Rename asset", current);
@@ -2880,6 +3316,16 @@ var ScopedAssetBrowser = ({ scope: _scope, accept: _accept, pageSize: _pageSize,
2880
3316
  await updateAsset(tagEditorAsset.id, { labels: next });
2881
3317
  setTagEditorAsset(null);
2882
3318
  }, [tagEditorAsset, updateAsset]);
3319
+ const [detailsAsset, setDetailsAsset] = useState(null);
3320
+ const handleShowDetails = useCallback((asset2) => {
3321
+ setDetailsAsset(asset2);
3322
+ }, []);
3323
+ const handleFixAsset = useCallback(async (asset2) => {
3324
+ const fixed = await buildFixFile(asset2);
3325
+ if (!fixed) return;
3326
+ const result = await replaceFile(asset2.id, fixed);
3327
+ if (result) setDetailsAsset(result);
3328
+ }, [replaceFile]);
2883
3329
  const [activeLabels, setActiveLabels] = useState(/* @__PURE__ */ new Set());
2884
3330
  const toggleLabel = useCallback((label) => {
2885
3331
  setActiveLabels((prev) => {
@@ -2979,6 +3425,7 @@ var ScopedAssetBrowser = ({ scope: _scope, accept: _accept, pageSize: _pageSize,
2979
3425
  onReplace: handleReplace,
2980
3426
  onEditTags: handleEditTags,
2981
3427
  onEditImage: handleEditImage,
3428
+ onShowDetails: handleShowDetails,
2982
3429
  allowDelete,
2983
3430
  currentAppId,
2984
3431
  currentAppName,
@@ -3024,6 +3471,19 @@ var ScopedAssetBrowser = ({ scope: _scope, accept: _accept, pageSize: _pageSize,
3024
3471
  onSave: handleSaveTags
3025
3472
  }
3026
3473
  ),
3474
+ detailsAsset && /* @__PURE__ */ jsx(
3475
+ AssetDetails,
3476
+ {
3477
+ asset: detailsAsset,
3478
+ onClose: () => setDetailsAsset(null),
3479
+ onRename: handleRename,
3480
+ onReplace: handleReplace,
3481
+ onEditTags: handleEditTags,
3482
+ onEditImage: handleEditImage,
3483
+ onDelete: allowDelete ? (a) => handleDeleteWithConfirm(a.id) : void 0,
3484
+ onFix: handleFixAsset
3485
+ }
3486
+ ),
3027
3487
  /* @__PURE__ */ jsx(
3028
3488
  InlineConfirm,
3029
3489
  {
@@ -3228,11 +3688,14 @@ var AssetPickerContent = ({
3228
3688
  name,
3229
3689
  ...sameAsActive ? {} : { scopeOverride: targetScope }
3230
3690
  });
3231
- if (result && !multiple) {
3232
- setSelectedIds(/* @__PURE__ */ new Set([result.id]));
3233
- onSelect?.(toSelection(result));
3691
+ if (result) {
3692
+ setTab("browse");
3693
+ if (!multiple) {
3694
+ setSelectedIds(/* @__PURE__ */ new Set([result.id]));
3695
+ onSelect?.(toSelection(result));
3696
+ }
3697
+ await reconcileAfterUpload(targetScope);
3234
3698
  }
3235
- if (result) await reconcileAfterUpload(targetScope);
3236
3699
  return result;
3237
3700
  }, [uploadFromRemoteUrl, multiple, onSelect, toSelection, uploadScope, activeScope, reconcileAfterUpload]);
3238
3701
  const handleDelete = useCallback(async (assetId) => {
@@ -3634,5 +4097,5 @@ var AssetPicker = (props) => {
3634
4097
  assertStylesLoaded();
3635
4098
 
3636
4099
  export { ASSET_MIME_FILTERS, AssetPicker, useAppRegistry, useAssets };
3637
- //# sourceMappingURL=chunk-E3GQ6LNZ.js.map
3638
- //# sourceMappingURL=chunk-E3GQ6LNZ.js.map
4100
+ //# sourceMappingURL=chunk-7RWLFKHC.js.map
4101
+ //# sourceMappingURL=chunk-7RWLFKHC.js.map