@proveanything/smartlinks-utils-ui 1.13.5 → 1.13.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.
@@ -1,9 +1,9 @@
1
1
  import { assertStylesLoaded } from './chunk-OLYC54YT.js';
2
2
  import { cn } from './chunk-L7FQ52F5.js';
3
- import React7, { useState, useRef, useEffect, useCallback, useMemo, useLayoutEffect } from 'react';
3
+ import React8, { 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, Check, Upload, Link, MicOff, Mic, ChevronDown, ChevronRight, Sparkles, Image as Image$1, 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, AlertTriangle, FileIcon, Film, Music, FileText, AppWindow, MoreVertical, Trash2 } 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, onDelete, position = "absolute" }) => {
353
+ var CardMenu = ({ onRename, onReplace, onEditTags, onEditImage, 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, onDelete, position = "absolut
402
402
  window.removeEventListener("scroll", update, true);
403
403
  };
404
404
  }, [open]);
405
- if (!onRename && !onReplace && !onEditTags && !onDelete) return null;
405
+ if (!onRename && !onReplace && !onEditTags && !onEditImage && !onDelete) return null;
406
406
  return /* @__PURE__ */ jsxs(
407
407
  "div",
408
408
  {
@@ -480,6 +480,23 @@ var CardMenu = ({ onRename, onReplace, onEditTags, onDelete, position = "absolut
480
480
  ]
481
481
  }
482
482
  ),
483
+ onEditImage && /* @__PURE__ */ jsxs(
484
+ "button",
485
+ {
486
+ type: "button",
487
+ role: "menuitem",
488
+ onClick: (e) => {
489
+ e.stopPropagation();
490
+ setOpen(false);
491
+ onEditImage();
492
+ },
493
+ className: "w-full flex items-center gap-2 px-2.5 py-1.5 text-xs hover:bg-accent",
494
+ children: [
495
+ /* @__PURE__ */ jsx(Crop, { className: "w-3 h-3" }),
496
+ " Edit image"
497
+ ]
498
+ }
499
+ ),
483
500
  onEditTags && /* @__PURE__ */ jsxs(
484
501
  "button",
485
502
  {
@@ -523,7 +540,7 @@ var CardMenu = ({ onRename, onReplace, onEditTags, onDelete, position = "absolut
523
540
  }
524
541
  );
525
542
  };
526
- var AssetGridItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelete, onRename, onReplace, onEditTags, allowDelete, currentAppId, getAppName, activeLabels, onToggleLabel }) => {
543
+ var AssetGridItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelete, onRename, onReplace, onEditTags, onEditImage, allowDelete, currentAppId, getAppName, activeLabels, onToggleLabel }) => {
527
544
  const thumb = getThumbnail(asset2);
528
545
  const Icon = getIcon(asset2.mimeType);
529
546
  const ownerAppId = getAssetAppId(asset2);
@@ -604,6 +621,7 @@ var AssetGridItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelet
604
621
  onRename,
605
622
  onReplace,
606
623
  onEditTags,
624
+ onEditImage,
607
625
  onDelete: allowDelete ? onDelete : void 0
608
626
  }
609
627
  )
@@ -611,7 +629,7 @@ var AssetGridItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelet
611
629
  }
612
630
  );
613
631
  };
614
- var AssetListItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelete, onRename, onReplace, onEditTags, allowDelete, currentAppId, getAppName, activeLabels, onToggleLabel }) => {
632
+ var AssetListItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelete, onRename, onReplace, onEditTags, onEditImage, allowDelete, currentAppId, getAppName, activeLabels, onToggleLabel }) => {
615
633
  const thumb = getThumbnail(asset2);
616
634
  const Icon = getIcon(asset2.mimeType);
617
635
  const ownerAppId = getAssetAppId(asset2);
@@ -680,6 +698,7 @@ var AssetListItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelet
680
698
  onRename,
681
699
  onReplace,
682
700
  onEditTags,
701
+ onEditImage,
683
702
  onDelete: allowDelete ? onDelete : void 0,
684
703
  position: "inline"
685
704
  }
@@ -698,6 +717,7 @@ var AssetGrid = ({
698
717
  onRename,
699
718
  onReplace,
700
719
  onEditTags,
720
+ onEditImage,
701
721
  allowDelete,
702
722
  currentAppId,
703
723
  getAppName,
@@ -717,6 +737,7 @@ var AssetGrid = ({
717
737
  onRename: onRename ? () => onRename(asset2) : void 0,
718
738
  onReplace: onReplace ? () => onReplace(asset2) : void 0,
719
739
  onEditTags: onEditTags ? () => onEditTags(asset2) : void 0,
740
+ onEditImage: onEditImage ? () => onEditImage(asset2) : void 0,
720
741
  allowDelete,
721
742
  currentAppId,
722
743
  getAppName,
@@ -737,6 +758,7 @@ var AssetGrid = ({
737
758
  onRename: onRename ? () => onRename(asset2) : void 0,
738
759
  onReplace: onReplace ? () => onReplace(asset2) : void 0,
739
760
  onEditTags: onEditTags ? () => onEditTags(asset2) : void 0,
761
+ onEditImage: onEditImage ? () => onEditImage(asset2) : void 0,
740
762
  allowDelete,
741
763
  currentAppId,
742
764
  getAppName,
@@ -838,6 +860,431 @@ async function processImage(file, opts = {}) {
838
860
  URL.revokeObjectURL(url);
839
861
  }
840
862
  }
863
+ var ASPECTS = [
864
+ { key: "free", label: "Free", ratio: null },
865
+ { key: "1:1", label: "1:1", ratio: 1 },
866
+ { key: "4:3", label: "4:3", ratio: 4 / 3 },
867
+ { key: "3:4", label: "3:4", ratio: 3 / 4 },
868
+ { key: "16:9", label: "16:9", ratio: 16 / 9 },
869
+ { key: "9:16", label: "9:16", ratio: 9 / 16 }
870
+ ];
871
+ function loadImage2(file) {
872
+ const url = URL.createObjectURL(file);
873
+ const img = new Image();
874
+ return new Promise((resolve, reject) => {
875
+ img.onload = () => resolve({ img, url });
876
+ img.onerror = () => {
877
+ URL.revokeObjectURL(url);
878
+ reject(new Error("Failed to decode"));
879
+ };
880
+ img.src = url;
881
+ });
882
+ }
883
+ var ImageEditor = ({
884
+ file,
885
+ initialName,
886
+ maxDimension = 2048,
887
+ quality = 0.85,
888
+ toWebp,
889
+ onCancel,
890
+ onConfirm
891
+ }) => {
892
+ const [img, setImg] = useState(null);
893
+ const [imgUrl, setImgUrl] = useState(null);
894
+ const [rotation, setRotation] = useState(0);
895
+ const [flipH, setFlipH] = useState(false);
896
+ const [flipV, setFlipV] = useState(false);
897
+ const [aspect, setAspect] = useState("free");
898
+ const [crop, setCrop] = useState(null);
899
+ const [resizeMax, setResizeMax] = useState(maxDimension || 0);
900
+ const [name, setName] = useState(initialName || file.name.replace(/\.[^.]+$/, ""));
901
+ const [busy, setBusy] = useState(false);
902
+ const stageRef = useRef(null);
903
+ const dragStateRef = useRef(null);
904
+ useEffect(() => {
905
+ let cancelled = false;
906
+ loadImage2(file).then(({ img: img2, url }) => {
907
+ if (cancelled) {
908
+ URL.revokeObjectURL(url);
909
+ return;
910
+ }
911
+ setImg(img2);
912
+ setImgUrl(url);
913
+ }).catch(() => {
914
+ });
915
+ return () => {
916
+ cancelled = true;
917
+ };
918
+ }, [file]);
919
+ useEffect(() => () => {
920
+ if (imgUrl) URL.revokeObjectURL(imgUrl);
921
+ }, [imgUrl]);
922
+ const rotated = rotation === 90 || rotation === 270;
923
+ const effW = img ? rotated ? img.naturalHeight : img.naturalWidth : 0;
924
+ const effH = img ? rotated ? img.naturalWidth : img.naturalHeight : 0;
925
+ useEffect(() => {
926
+ if (!img) return;
927
+ setCrop(null);
928
+ }, [img, rotation]);
929
+ const ratio = ASPECTS.find((a) => a.key === aspect)?.ratio || null;
930
+ const beginCrop = useCallback(() => {
931
+ if (!effW || !effH) return;
932
+ if (ratio) {
933
+ let w = effW, h = effW / ratio;
934
+ if (h > effH) {
935
+ h = effH;
936
+ w = effH * ratio;
937
+ }
938
+ setCrop({ x: (effW - w) / 2, y: (effH - h) / 2, w, h });
939
+ } else {
940
+ const w = effW * 0.8;
941
+ const h = effH * 0.8;
942
+ setCrop({ x: (effW - w) / 2, y: (effH - h) / 2, w, h });
943
+ }
944
+ }, [effW, effH, ratio]);
945
+ useEffect(() => {
946
+ if (!crop || !ratio) return;
947
+ let { x, y, w, h } = crop;
948
+ h = w / ratio;
949
+ if (y + h > effH) h = effH - y;
950
+ w = h * ratio;
951
+ if (x + w > effW) {
952
+ w = effW - x;
953
+ h = w / ratio;
954
+ }
955
+ setCrop({ x, y, w, h });
956
+ }, [aspect]);
957
+ const onPointerDown = useCallback((mode) => (e) => {
958
+ if (!crop || !stageRef.current) return;
959
+ e.preventDefault();
960
+ e.stopPropagation();
961
+ e.target.setPointerCapture(e.pointerId);
962
+ const rect = stageRef.current.getBoundingClientRect();
963
+ dragStateRef.current = {
964
+ mode,
965
+ startX: e.clientX,
966
+ startY: e.clientY,
967
+ startCrop: { ...crop },
968
+ stageW: rect.width,
969
+ stageH: rect.height
970
+ };
971
+ }, [crop]);
972
+ const onPointerMove = useCallback((e) => {
973
+ const ds = dragStateRef.current;
974
+ if (!ds || !crop) return;
975
+ const scaleX = effW / ds.stageW;
976
+ const scaleY = effH / ds.stageH;
977
+ const dx = (e.clientX - ds.startX) * scaleX;
978
+ const dy = (e.clientY - ds.startY) * scaleY;
979
+ let { x, y, w, h } = ds.startCrop;
980
+ const clampX = (v) => Math.max(0, Math.min(effW, v));
981
+ const clampY = (v) => Math.max(0, Math.min(effH, v));
982
+ if (ds.mode === "move") {
983
+ x = clampX(x + dx);
984
+ y = clampY(y + dy);
985
+ if (x + w > effW) x = effW - w;
986
+ if (y + h > effH) y = effH - h;
987
+ } else {
988
+ let x2 = x + w, y2 = y + h;
989
+ if (ds.mode?.includes("w")) x = clampX(x + dx);
990
+ if (ds.mode?.includes("e")) x2 = clampX(x2 + dx);
991
+ if (ds.mode?.includes("n")) y = clampY(y + dy);
992
+ if (ds.mode?.includes("s")) y2 = clampY(y2 + dy);
993
+ w = Math.max(8, x2 - x);
994
+ h = Math.max(8, y2 - y);
995
+ if (ratio) {
996
+ const sign = ds.mode === "nw" || ds.mode === "ne" || ds.mode === "n" ? -1 : 1;
997
+ const newH = w / ratio;
998
+ if (sign < 0) {
999
+ y = y + h - newH;
1000
+ if (y < 0) {
1001
+ y = 0;
1002
+ h = ds.startCrop.y + ds.startCrop.h;
1003
+ w = h * ratio;
1004
+ } else h = newH;
1005
+ } else {
1006
+ h = newH;
1007
+ if (y + h > effH) {
1008
+ h = effH - y;
1009
+ w = h * ratio;
1010
+ }
1011
+ }
1012
+ }
1013
+ }
1014
+ setCrop({ x, y, w, h });
1015
+ }, [crop, effW, effH, ratio]);
1016
+ const onPointerUp = useCallback((e) => {
1017
+ dragStateRef.current = null;
1018
+ try {
1019
+ e.target.releasePointerCapture(e.pointerId);
1020
+ } catch {
1021
+ }
1022
+ }, []);
1023
+ const handleApply = useCallback(async () => {
1024
+ if (!img) return;
1025
+ setBusy(true);
1026
+ try {
1027
+ const baseW = effW;
1028
+ const baseH = effH;
1029
+ const work = document.createElement("canvas");
1030
+ work.width = baseW;
1031
+ work.height = baseH;
1032
+ const wctx = work.getContext("2d");
1033
+ if (!wctx) throw new Error("canvas unsupported");
1034
+ wctx.save();
1035
+ wctx.translate(baseW / 2, baseH / 2);
1036
+ wctx.rotate(rotation * Math.PI / 180);
1037
+ wctx.scale(flipH ? -1 : 1, flipV ? -1 : 1);
1038
+ wctx.drawImage(img, -img.naturalWidth / 2, -img.naturalHeight / 2);
1039
+ wctx.restore();
1040
+ const c = crop || { x: 0, y: 0, w: baseW, h: baseH };
1041
+ let outW = Math.round(c.w);
1042
+ let outH = Math.round(c.h);
1043
+ if (resizeMax > 0) {
1044
+ const longest = Math.max(outW, outH);
1045
+ if (longest > resizeMax) {
1046
+ const s = resizeMax / longest;
1047
+ outW = Math.round(outW * s);
1048
+ outH = Math.round(outH * s);
1049
+ }
1050
+ }
1051
+ const out = document.createElement("canvas");
1052
+ out.width = outW;
1053
+ out.height = outH;
1054
+ const octx = out.getContext("2d");
1055
+ if (!octx) throw new Error("canvas unsupported");
1056
+ octx.drawImage(work, c.x, c.y, c.w, c.h, 0, 0, outW, outH);
1057
+ const useWebp = toWebp ?? file.type !== "image/webp";
1058
+ const outType = useWebp ? "image/webp" : file.type || "image/png";
1059
+ const blob = await new Promise((r) => out.toBlob((b) => r(b), outType, quality));
1060
+ if (!blob) throw new Error("encode failed");
1061
+ const ext = useWebp ? "webp" : file.name.match(/\.([^.]+)$/)?.[1] || "png";
1062
+ const trimmed = (name.trim() || "image").replace(/\.[^.]+$/, "");
1063
+ const newFile = new File([blob], `${trimmed}.${ext}`, { type: outType });
1064
+ onConfirm(newFile, trimmed);
1065
+ } finally {
1066
+ setBusy(false);
1067
+ }
1068
+ }, [img, crop, effW, effH, rotation, flipH, flipV, resizeMax, file, toWebp, quality, name, onConfirm]);
1069
+ const reset = useCallback(() => {
1070
+ setRotation(0);
1071
+ setFlipH(false);
1072
+ setFlipV(false);
1073
+ setCrop(null);
1074
+ setAspect("free");
1075
+ setResizeMax(maxDimension || 0);
1076
+ }, [maxDimension]);
1077
+ const cropStyle = useMemo(() => {
1078
+ if (!crop || !effW || !effH) return null;
1079
+ return {
1080
+ left: `${crop.x / effW * 100}%`,
1081
+ top: `${crop.y / effH * 100}%`,
1082
+ width: `${crop.w / effW * 100}%`,
1083
+ height: `${crop.h / effH * 100}%`
1084
+ };
1085
+ }, [crop, effW, effH]);
1086
+ if (typeof document === "undefined") return null;
1087
+ return createPortal(
1088
+ /* @__PURE__ */ jsx(
1089
+ "div",
1090
+ {
1091
+ className: "fixed inset-0 z-[2147483646] flex items-center justify-center p-4 bg-black/60",
1092
+ role: "dialog",
1093
+ "aria-modal": "true",
1094
+ "aria-label": "Edit image",
1095
+ onMouseDown: (e) => e.stopPropagation(),
1096
+ onTouchStart: (e) => e.stopPropagation(),
1097
+ onClick: (e) => e.stopPropagation(),
1098
+ children: /* @__PURE__ */ jsxs("div", { className: "relative w-full max-w-3xl max-h-[90vh] flex flex-col rounded-lg border border-border bg-popover text-popover-foreground shadow-xl overflow-hidden", children: [
1099
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-2 border-b border-border", children: [
1100
+ /* @__PURE__ */ jsxs("h3", { className: "text-sm font-semibold flex items-center gap-1.5", children: [
1101
+ /* @__PURE__ */ jsx(Crop, { className: "w-4 h-4" }),
1102
+ " Edit image"
1103
+ ] }),
1104
+ /* @__PURE__ */ jsx(
1105
+ "button",
1106
+ {
1107
+ type: "button",
1108
+ onClick: onCancel,
1109
+ className: "p-1 rounded hover:bg-accent",
1110
+ "aria-label": "Close",
1111
+ children: /* @__PURE__ */ jsx(X, { className: "w-4 h-4" })
1112
+ }
1113
+ )
1114
+ ] }),
1115
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0 flex items-center justify-center bg-muted/30 p-3 overflow-auto", children: !img ? /* @__PURE__ */ jsx(Loader2, { className: "w-6 h-6 animate-spin text-muted-foreground" }) : /* @__PURE__ */ jsxs(
1116
+ "div",
1117
+ {
1118
+ ref: stageRef,
1119
+ className: "relative select-none touch-none",
1120
+ style: {
1121
+ aspectRatio: `${effW} / ${effH}`,
1122
+ maxWidth: "100%",
1123
+ maxHeight: "60vh",
1124
+ width: "min(100%, 60vh * (var(--ar)))",
1125
+ // CSS var fallback — explicit width/height managed by aspect-ratio + maxes
1126
+ ["--ar"]: `${effW / effH}`
1127
+ },
1128
+ children: [
1129
+ /* @__PURE__ */ jsx(
1130
+ "img",
1131
+ {
1132
+ src: imgUrl,
1133
+ alt: "",
1134
+ draggable: false,
1135
+ className: "block w-full h-full object-contain pointer-events-none",
1136
+ style: {
1137
+ transform: `rotate(${rotation}deg) scale(${flipH ? -1 : 1}, ${flipV ? -1 : 1})`,
1138
+ transformOrigin: "center"
1139
+ }
1140
+ }
1141
+ ),
1142
+ cropStyle && /* @__PURE__ */ jsxs(Fragment, { children: [
1143
+ /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 pointer-events-none", children: [
1144
+ /* @__PURE__ */ jsx("div", { className: "absolute bg-black/50", style: { left: 0, top: 0, right: 0, height: cropStyle.top } }),
1145
+ /* @__PURE__ */ jsx("div", { className: "absolute bg-black/50", style: { left: 0, bottom: 0, right: 0, top: `calc(${cropStyle.top} + ${cropStyle.height})` } }),
1146
+ /* @__PURE__ */ jsx("div", { className: "absolute bg-black/50", style: { left: 0, top: cropStyle.top, width: cropStyle.left, height: cropStyle.height } }),
1147
+ /* @__PURE__ */ jsx("div", { className: "absolute bg-black/50", style: { right: 0, top: cropStyle.top, left: `calc(${cropStyle.left} + ${cropStyle.width})`, height: cropStyle.height } })
1148
+ ] }),
1149
+ /* @__PURE__ */ jsx(
1150
+ "div",
1151
+ {
1152
+ className: "absolute border-2 border-primary cursor-move",
1153
+ style: cropStyle,
1154
+ onPointerDown: onPointerDown("move"),
1155
+ onPointerMove,
1156
+ onPointerUp,
1157
+ children: ["nw", "ne", "sw", "se"].map((h) => /* @__PURE__ */ jsx(
1158
+ "div",
1159
+ {
1160
+ className: cn(
1161
+ "absolute w-3 h-3 bg-primary border border-background rounded-sm",
1162
+ h === "nw" && "-top-1.5 -left-1.5 cursor-nwse-resize",
1163
+ h === "ne" && "-top-1.5 -right-1.5 cursor-nesw-resize",
1164
+ h === "sw" && "-bottom-1.5 -left-1.5 cursor-nesw-resize",
1165
+ h === "se" && "-bottom-1.5 -right-1.5 cursor-nwse-resize"
1166
+ ),
1167
+ onPointerDown: onPointerDown(h),
1168
+ onPointerMove,
1169
+ onPointerUp
1170
+ },
1171
+ h
1172
+ ))
1173
+ }
1174
+ )
1175
+ ] })
1176
+ ]
1177
+ }
1178
+ ) }),
1179
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 px-4 py-2 border-t border-border", children: [
1180
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
1181
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
1182
+ /* @__PURE__ */ jsx("button", { type: "button", title: "Rotate left", onClick: () => setRotation((r) => (r + 270) % 360), className: "p-1.5 rounded border border-border hover:bg-accent", children: /* @__PURE__ */ jsx(RotateCcw, { className: "w-3.5 h-3.5" }) }),
1183
+ /* @__PURE__ */ jsx("button", { type: "button", title: "Rotate right", onClick: () => setRotation((r) => (r + 90) % 360), className: "p-1.5 rounded border border-border hover:bg-accent", children: /* @__PURE__ */ jsx(RotateCw, { className: "w-3.5 h-3.5" }) }),
1184
+ /* @__PURE__ */ jsx("button", { type: "button", title: "Flip horizontal", onClick: () => setFlipH((v) => !v), className: cn("p-1.5 rounded border border-border hover:bg-accent", flipH && "bg-accent"), children: /* @__PURE__ */ jsx(FlipHorizontal, { className: "w-3.5 h-3.5" }) }),
1185
+ /* @__PURE__ */ jsx("button", { type: "button", title: "Flip vertical", onClick: () => setFlipV((v) => !v), className: cn("p-1.5 rounded border border-border hover:bg-accent", flipV && "bg-accent"), children: /* @__PURE__ */ jsx(FlipVertical, { className: "w-3.5 h-3.5" }) })
1186
+ ] }),
1187
+ /* @__PURE__ */ jsx("span", { className: "mx-1 h-5 w-px bg-border" }),
1188
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 flex-wrap", children: [
1189
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: "Crop:" }),
1190
+ ASPECTS.map((a) => /* @__PURE__ */ jsx(
1191
+ "button",
1192
+ {
1193
+ type: "button",
1194
+ onClick: () => {
1195
+ setAspect(a.key);
1196
+ if (!crop) beginCrop();
1197
+ },
1198
+ className: cn(
1199
+ "px-2 py-0.5 text-[11px] rounded border transition-colors",
1200
+ aspect === a.key && crop ? "border-primary bg-primary text-primary-foreground" : "border-border text-muted-foreground hover:bg-accent"
1201
+ ),
1202
+ children: a.label
1203
+ },
1204
+ a.key
1205
+ )),
1206
+ !crop && /* @__PURE__ */ jsx("button", { type: "button", onClick: beginCrop, className: "px-2 py-0.5 text-[11px] rounded border border-border text-muted-foreground hover:bg-accent", children: "Start crop" }),
1207
+ crop && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setCrop(null), className: "px-2 py-0.5 text-[11px] rounded border border-border text-muted-foreground hover:bg-accent", children: "Clear crop" })
1208
+ ] }),
1209
+ /* @__PURE__ */ jsx("span", { className: "mx-1 h-5 w-px bg-border" }),
1210
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
1211
+ "Max edge:",
1212
+ /* @__PURE__ */ jsx(
1213
+ "input",
1214
+ {
1215
+ type: "number",
1216
+ min: 0,
1217
+ step: 64,
1218
+ value: resizeMax,
1219
+ onChange: (e) => setResizeMax(Math.max(0, parseInt(e.target.value || "0", 10))),
1220
+ className: "w-20 px-1.5 py-0.5 text-xs rounded border border-border bg-transparent focus:outline-none focus:ring-1 focus:ring-ring"
1221
+ }
1222
+ ),
1223
+ /* @__PURE__ */ jsxs("span", { children: [
1224
+ "px ",
1225
+ resizeMax === 0 && "(off)"
1226
+ ] })
1227
+ ] }),
1228
+ /* @__PURE__ */ jsxs("button", { type: "button", onClick: reset, className: "ml-auto inline-flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground", children: [
1229
+ /* @__PURE__ */ jsx(RefreshCw, { className: "w-3 h-3" }),
1230
+ " Reset"
1231
+ ] })
1232
+ ] }),
1233
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1234
+ /* @__PURE__ */ jsx(
1235
+ "input",
1236
+ {
1237
+ type: "text",
1238
+ value: name,
1239
+ onChange: (e) => setName(e.target.value),
1240
+ placeholder: "File name",
1241
+ className: "flex-1 px-2 py-1 text-xs rounded border border-border bg-transparent focus:outline-none focus:ring-1 focus:ring-ring"
1242
+ }
1243
+ ),
1244
+ /* @__PURE__ */ jsx(
1245
+ "button",
1246
+ {
1247
+ type: "button",
1248
+ onClick: onCancel,
1249
+ disabled: busy,
1250
+ className: "px-3 py-1 text-xs rounded border border-border text-muted-foreground hover:bg-accent",
1251
+ children: "Cancel"
1252
+ }
1253
+ ),
1254
+ /* @__PURE__ */ jsxs(
1255
+ "button",
1256
+ {
1257
+ type: "button",
1258
+ onClick: handleApply,
1259
+ disabled: busy || !img,
1260
+ className: "px-3 py-1 text-xs rounded bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 inline-flex items-center gap-1",
1261
+ children: [
1262
+ busy ? /* @__PURE__ */ jsx(Loader2, { className: "w-3 h-3 animate-spin" }) : /* @__PURE__ */ jsx(Check, { className: "w-3 h-3" }),
1263
+ "Apply"
1264
+ ]
1265
+ }
1266
+ )
1267
+ ] })
1268
+ ] })
1269
+ ] })
1270
+ }
1271
+ ),
1272
+ document.body
1273
+ );
1274
+ };
1275
+ async function urlToFile(url, name) {
1276
+ const res = await fetch(url, { mode: "cors" });
1277
+ const blob = await res.blob();
1278
+ const ext = (name.match(/\.([^.]+)$/)?.[1] || (blob.type.split("/")[1] || "png")).toLowerCase();
1279
+ const finalName = /\.[^.]+$/.test(name) ? name : `${name}.${ext}`;
1280
+ return new File([blob], finalName, { type: blob.type || "image/png" });
1281
+ }
1282
+ function isEditableImage(mimeType) {
1283
+ if (!mimeType) return false;
1284
+ if (!mimeType.startsWith("image/")) return false;
1285
+ if (mimeType === "image/svg+xml" || mimeType === "image/gif") return false;
1286
+ return true;
1287
+ }
841
1288
  var UploadZone = ({
842
1289
  onFiles,
843
1290
  accept,
@@ -884,6 +1331,7 @@ var UploadZone = ({
884
1331
  }
885
1332
  }, []);
886
1333
  const [optimizing, setOptimizing] = useState(false);
1334
+ const [editing, setEditing] = useState(false);
887
1335
  const inputRef = useRef(null);
888
1336
  const nameInputRef = useRef(null);
889
1337
  const zoneRef = useRef(null);
@@ -1107,6 +1555,19 @@ var UploadZone = ({
1107
1555
  ] }),
1108
1556
  isProcessableImage(pastedFile.file) && optimizeToggle,
1109
1557
  /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1558
+ isEditableImage(pastedFile.file.type) && /* @__PURE__ */ jsxs(
1559
+ "button",
1560
+ {
1561
+ type: "button",
1562
+ onClick: () => setEditing(true),
1563
+ className: "px-3 py-1.5 text-xs font-medium rounded-md border border-border text-muted-foreground hover:bg-accent transition-colors flex items-center gap-1",
1564
+ disabled: optimizing,
1565
+ children: [
1566
+ /* @__PURE__ */ jsx(Crop, { className: "w-3 h-3" }),
1567
+ " Edit"
1568
+ ]
1569
+ }
1570
+ ),
1110
1571
  /* @__PURE__ */ jsxs(
1111
1572
  "button",
1112
1573
  {
@@ -1135,6 +1596,26 @@ var UploadZone = ({
1135
1596
  )
1136
1597
  ] })
1137
1598
  ] }),
1599
+ editing && /* @__PURE__ */ jsx(
1600
+ ImageEditor,
1601
+ {
1602
+ file: pastedFile.file,
1603
+ initialName: fileName,
1604
+ maxDimension: optConfig.maxDimension ?? 2048,
1605
+ quality: optConfig.quality ?? 0.85,
1606
+ onCancel: () => setEditing(false),
1607
+ onConfirm: (newFile, newName) => {
1608
+ if (pastedFile.previewUrl) URL.revokeObjectURL(pastedFile.previewUrl);
1609
+ const previewUrl = URL.createObjectURL(newFile);
1610
+ setPastedFile({ file: newFile, previewUrl, name: newName, origSize: newFile.size });
1611
+ setFileName(newName);
1612
+ getImageDimensions(newFile).then((dims) => {
1613
+ if (dims) setPastedFile((prev) => prev && prev.file === newFile ? { ...prev, origDims: dims } : prev);
1614
+ });
1615
+ setEditing(false);
1616
+ }
1617
+ }
1618
+ ),
1138
1619
  lightboxOpen && pastedFile.previewUrl && /* @__PURE__ */ jsxs(
1139
1620
  "div",
1140
1621
  {
@@ -1364,6 +1845,7 @@ function extractImageUrl(response) {
1364
1845
  var AIImageGenerate = ({
1365
1846
  collectionId,
1366
1847
  onSave,
1848
+ onSaveFile,
1367
1849
  saving,
1368
1850
  className
1369
1851
  }) => {
@@ -1379,6 +1861,13 @@ var AIImageGenerate = ({
1379
1861
  const [previewUrl, setPreviewUrl] = useState(null);
1380
1862
  const [saved, setSaved] = useState(false);
1381
1863
  const [customName, setCustomName] = useState("");
1864
+ const [editing, setEditing] = useState(false);
1865
+ const [editedFile, setEditedFile] = useState(null);
1866
+ const [editedPreview, setEditedPreview] = useState(null);
1867
+ const [preparingEdit, setPreparingEdit] = useState(false);
1868
+ useEffect(() => () => {
1869
+ if (editedPreview) URL.revokeObjectURL(editedPreview);
1870
+ }, [editedPreview]);
1382
1871
  const SpeechRecognitionCtor = typeof window !== "undefined" ? window.SpeechRecognition || window.webkitSpeechRecognition : null;
1383
1872
  const voiceSupported = !!SpeechRecognitionCtor;
1384
1873
  const recognitionRef = useRef(null);
@@ -1483,16 +1972,40 @@ var AIImageGenerate = ({
1483
1972
  if (!previewUrl) return;
1484
1973
  const custom = customName.trim();
1485
1974
  const fileName = custom ? /\.[a-z0-9]{2,5}$/i.test(custom) ? custom : `${custom}.png` : `ai-${prompt.trim().slice(0, 60).replace(/\s+/g, "-") || "ai-image"}.png`;
1486
- const result = await onSave(previewUrl, fileName);
1975
+ let result;
1976
+ if (editedFile && onSaveFile) {
1977
+ result = await onSaveFile(editedFile, fileName);
1978
+ } else {
1979
+ result = await onSave(previewUrl, fileName);
1980
+ }
1487
1981
  if (result) {
1488
1982
  setSaved(true);
1489
1983
  setTimeout(() => {
1490
1984
  setPreviewUrl(null);
1491
1985
  setSaved(false);
1492
1986
  setCustomName("");
1987
+ if (editedPreview) URL.revokeObjectURL(editedPreview);
1988
+ setEditedFile(null);
1989
+ setEditedPreview(null);
1493
1990
  }, 1500);
1494
1991
  }
1495
- }, [previewUrl, prompt, customName, onSave]);
1992
+ }, [previewUrl, prompt, customName, onSave, onSaveFile, editedFile, editedPreview]);
1993
+ const startEdit = useCallback(async () => {
1994
+ if (!previewUrl) return;
1995
+ setPreparingEdit(true);
1996
+ try {
1997
+ const baseName = customName.trim() || `ai-${prompt.trim().slice(0, 40).replace(/\s+/g, "-") || "image"}`;
1998
+ const file = editedFile || await urlToFile(previewUrl, `${baseName}.png`);
1999
+ if (!editedFile) {
2000
+ setEditedFile(file);
2001
+ }
2002
+ setEditing(true);
2003
+ } catch {
2004
+ setError("Could not load image for editing.");
2005
+ } finally {
2006
+ setPreparingEdit(false);
2007
+ }
2008
+ }, [previewUrl, customName, prompt, editedFile]);
1496
2009
  if (!collectionId) {
1497
2010
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 p-3 rounded-md bg-muted text-muted-foreground text-sm", children: [
1498
2011
  /* @__PURE__ */ jsx(AlertCircle, { className: "w-4 h-4 flex-shrink-0" }),
@@ -1669,14 +2182,17 @@ var AIImageGenerate = ({
1669
2182
  error
1670
2183
  ] }),
1671
2184
  previewUrl && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 border border-border rounded-md p-3 bg-muted/30", children: [
1672
- /* @__PURE__ */ jsx("div", { className: "relative rounded overflow-hidden bg-background flex items-center justify-center", style: { minHeight: "12rem" }, children: /* @__PURE__ */ jsx(
1673
- "img",
1674
- {
1675
- src: previewUrl,
1676
- alt: prompt,
1677
- className: "max-w-full max-h-80 object-contain"
1678
- }
1679
- ) }),
2185
+ /* @__PURE__ */ jsxs("div", { className: "relative rounded overflow-hidden bg-background flex items-center justify-center", style: { minHeight: "12rem" }, children: [
2186
+ /* @__PURE__ */ jsx(
2187
+ "img",
2188
+ {
2189
+ src: editedPreview || previewUrl,
2190
+ alt: prompt,
2191
+ className: "max-w-full max-h-80 object-contain"
2192
+ }
2193
+ ),
2194
+ editedPreview && /* @__PURE__ */ jsx("span", { className: "absolute top-1 left-1 px-1.5 py-0.5 text-[10px] rounded bg-primary text-primary-foreground", children: "Edited" })
2195
+ ] }),
1680
2196
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
1681
2197
  /* @__PURE__ */ jsx("label", { className: "text-[11px] font-medium text-foreground", children: "Name (optional)" }),
1682
2198
  /* @__PURE__ */ jsx(
@@ -1692,6 +2208,19 @@ var AIImageGenerate = ({
1692
2208
  )
1693
2209
  ] }),
1694
2210
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2", children: [
2211
+ /* @__PURE__ */ jsxs(
2212
+ "button",
2213
+ {
2214
+ type: "button",
2215
+ onClick: startEdit,
2216
+ disabled: saving || saved || preparingEdit,
2217
+ className: "px-3 py-1.5 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors inline-flex items-center gap-1",
2218
+ children: [
2219
+ preparingEdit ? /* @__PURE__ */ jsx(Loader2, { className: "w-3 h-3 animate-spin" }) : /* @__PURE__ */ jsx(Crop, { className: "w-3 h-3" }),
2220
+ "Edit"
2221
+ ]
2222
+ }
2223
+ ),
1695
2224
  /* @__PURE__ */ jsx(
1696
2225
  "button",
1697
2226
  {
@@ -1699,6 +2228,9 @@ var AIImageGenerate = ({
1699
2228
  onClick: () => {
1700
2229
  setPreviewUrl(null);
1701
2230
  setSaved(false);
2231
+ if (editedPreview) URL.revokeObjectURL(editedPreview);
2232
+ setEditedFile(null);
2233
+ setEditedPreview(null);
1702
2234
  },
1703
2235
  disabled: saving,
1704
2236
  className: "px-3 py-1.5 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors",
@@ -1722,7 +2254,22 @@ var AIImageGenerate = ({
1722
2254
  ]
1723
2255
  }
1724
2256
  )
1725
- ] })
2257
+ ] }),
2258
+ editing && editedFile && /* @__PURE__ */ jsx(
2259
+ ImageEditor,
2260
+ {
2261
+ file: editedFile,
2262
+ initialName: customName.trim() || `ai-${prompt.trim().slice(0, 40).replace(/\s+/g, "-") || "image"}`,
2263
+ onCancel: () => setEditing(false),
2264
+ onConfirm: (file, name) => {
2265
+ if (editedPreview) URL.revokeObjectURL(editedPreview);
2266
+ setEditedFile(file);
2267
+ setEditedPreview(URL.createObjectURL(file));
2268
+ setCustomName(name);
2269
+ setEditing(false);
2270
+ }
2271
+ }
2272
+ )
1726
2273
  ] })
1727
2274
  ] });
1728
2275
  };
@@ -1741,6 +2288,7 @@ function getFullUrl(p) {
1741
2288
  var StockPhotoSearch = ({
1742
2289
  collectionId,
1743
2290
  onSave,
2291
+ onSaveFile,
1744
2292
  saving,
1745
2293
  className
1746
2294
  }) => {
@@ -1749,15 +2297,19 @@ var StockPhotoSearch = ({
1749
2297
  const [searching, setSearching] = useState(false);
1750
2298
  const [error, setError] = useState(null);
1751
2299
  const [results, setResults] = useState([]);
1752
- const [savedUrl, setSavedUrl] = useState(null);
1753
- const [pendingUrl, setPendingUrl] = useState(null);
1754
- const [nameOverrides, setNameOverrides] = useState({});
2300
+ const [staged, setStaged] = useState(null);
2301
+ const [preparingEdit, setPreparingEdit] = useState(false);
2302
+ const [editing, setEditing] = useState(false);
2303
+ const [savedFlash, setSavedFlash] = useState(false);
2304
+ useEffect(() => () => {
2305
+ if (staged?.previewUrl) URL.revokeObjectURL(staged.previewUrl);
2306
+ }, [staged]);
1755
2307
  const handleSearch = useCallback(async () => {
1756
2308
  if (!query.trim() || !collectionId) return;
1757
2309
  setSearching(true);
1758
2310
  setError(null);
1759
2311
  setResults([]);
1760
- setSavedUrl(null);
2312
+ setSavedFlash(false);
1761
2313
  try {
1762
2314
  const params = {
1763
2315
  query: query.trim(),
@@ -1776,18 +2328,42 @@ var StockPhotoSearch = ({
1776
2328
  setSearching(false);
1777
2329
  }
1778
2330
  }, [query, orientation, collectionId]);
1779
- const handleSave = useCallback(async (photo) => {
1780
- const fullUrl = getFullUrl(photo);
1781
- setPendingUrl(fullUrl);
1782
- const override = (nameOverrides[fullUrl] || "").trim();
1783
- const fileName = override ? /\.[a-z0-9]{2,5}$/i.test(override) ? override : `${override}.jpg` : `stock-${(photo.alt || query.trim() || "stock-photo").slice(0, 60).replace(/\s+/g, "-")}.jpg`;
1784
- const result = await onSave(fullUrl, fileName);
1785
- setPendingUrl(null);
2331
+ const handlePick = useCallback((photo) => {
2332
+ const baseName = `stock-${(photo.alt || query.trim() || "photo").slice(0, 60).replace(/\s+/g, "-")}`;
2333
+ setStaged({ photo, name: baseName, file: null, previewUrl: null });
2334
+ }, [query]);
2335
+ const handleConfirmSave = useCallback(async () => {
2336
+ if (!staged) return;
2337
+ const fullUrl = getFullUrl(staged.photo);
2338
+ const trimmed = staged.name.trim() || "stock-photo";
2339
+ const fileName = /\.[a-z0-9]{2,5}$/i.test(trimmed) ? trimmed : `${trimmed}.${staged.file ? "webp" : "jpg"}`;
2340
+ let result;
2341
+ if (staged.file && onSaveFile) {
2342
+ result = await onSaveFile(staged.file, fileName);
2343
+ } else {
2344
+ result = await onSave(fullUrl, fileName);
2345
+ }
1786
2346
  if (result) {
1787
- setSavedUrl(fullUrl);
1788
- setTimeout(() => setSavedUrl(null), 1500);
2347
+ setSavedFlash(true);
2348
+ if (staged.previewUrl) URL.revokeObjectURL(staged.previewUrl);
2349
+ setStaged(null);
2350
+ setTimeout(() => setSavedFlash(false), 1500);
2351
+ }
2352
+ }, [staged, onSave, onSaveFile]);
2353
+ const handleStartEdit = useCallback(async () => {
2354
+ if (!staged) return;
2355
+ setPreparingEdit(true);
2356
+ try {
2357
+ const fullUrl = getFullUrl(staged.photo);
2358
+ const file = staged.file || await urlToFile(fullUrl, `${staged.name || "stock-photo"}.jpg`);
2359
+ setStaged((prev) => prev ? { ...prev, file } : prev);
2360
+ setEditing(true);
2361
+ } catch {
2362
+ setError("Could not load image for editing.");
2363
+ } finally {
2364
+ setPreparingEdit(false);
1789
2365
  }
1790
- }, [onSave, query, nameOverrides]);
2366
+ }, [staged]);
1791
2367
  if (!collectionId) {
1792
2368
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 p-3 rounded-md bg-muted text-muted-foreground text-sm", children: [
1793
2369
  /* @__PURE__ */ jsx(AlertCircle, { className: "w-4 h-4 flex-shrink-0" }),
@@ -1850,74 +2426,151 @@ var StockPhotoSearch = ({
1850
2426
  /* @__PURE__ */ jsx(AlertCircle, { className: "w-4 h-4 flex-shrink-0" }),
1851
2427
  error
1852
2428
  ] }),
2429
+ savedFlash && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 p-2 rounded-md bg-primary/10 text-primary text-xs", children: [
2430
+ /* @__PURE__ */ jsx(Check, { className: "w-3.5 h-3.5" }),
2431
+ " Saved to library."
2432
+ ] }),
2433
+ staged && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 border border-border rounded-md p-3 bg-muted/30", children: [
2434
+ /* @__PURE__ */ jsxs("div", { className: "relative rounded overflow-hidden bg-background flex items-center justify-center", style: { minHeight: "12rem" }, children: [
2435
+ /* @__PURE__ */ jsx(
2436
+ "img",
2437
+ {
2438
+ src: staged.previewUrl || getThumb(staged.photo),
2439
+ alt: staged.photo.alt || "",
2440
+ className: "max-w-full max-h-80 object-contain"
2441
+ }
2442
+ ),
2443
+ staged.previewUrl && /* @__PURE__ */ jsx("span", { className: "absolute top-1 left-1 px-1.5 py-0.5 text-[10px] rounded bg-primary text-primary-foreground", children: "Edited" })
2444
+ ] }),
2445
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
2446
+ /* @__PURE__ */ jsx("label", { className: "text-[11px] font-medium text-foreground", children: "Name" }),
2447
+ /* @__PURE__ */ jsx(
2448
+ "input",
2449
+ {
2450
+ type: "text",
2451
+ value: staged.name,
2452
+ onChange: (e) => setStaged((prev) => prev ? { ...prev, name: e.target.value } : prev),
2453
+ disabled: !!saving,
2454
+ className: "w-full px-2 py-1.5 text-xs rounded-md border border-border bg-transparent placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
2455
+ }
2456
+ )
2457
+ ] }),
2458
+ staged.photo.photographer && !staged.previewUrl && /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground", children: [
2459
+ "Photo by ",
2460
+ staged.photo.photographerUrl ? /* @__PURE__ */ jsx("a", { href: staged.photo.photographerUrl, target: "_blank", rel: "noreferrer noopener", className: "underline", children: staged.photo.photographer }) : staged.photo.photographer
2461
+ ] }),
2462
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2", children: [
2463
+ /* @__PURE__ */ jsxs(
2464
+ "button",
2465
+ {
2466
+ type: "button",
2467
+ onClick: handleStartEdit,
2468
+ disabled: !!saving || preparingEdit,
2469
+ className: "px-3 py-1.5 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors inline-flex items-center gap-1",
2470
+ children: [
2471
+ preparingEdit ? /* @__PURE__ */ jsx(Loader2, { className: "w-3 h-3 animate-spin" }) : /* @__PURE__ */ jsx(Crop, { className: "w-3 h-3" }),
2472
+ "Edit"
2473
+ ]
2474
+ }
2475
+ ),
2476
+ /* @__PURE__ */ jsxs(
2477
+ "button",
2478
+ {
2479
+ type: "button",
2480
+ onClick: () => {
2481
+ if (staged.previewUrl) URL.revokeObjectURL(staged.previewUrl);
2482
+ setStaged(null);
2483
+ },
2484
+ disabled: !!saving,
2485
+ className: "px-3 py-1.5 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors inline-flex items-center gap-1",
2486
+ children: [
2487
+ /* @__PURE__ */ jsx(X, { className: "w-3 h-3" }),
2488
+ " Discard"
2489
+ ]
2490
+ }
2491
+ ),
2492
+ /* @__PURE__ */ jsxs(
2493
+ "button",
2494
+ {
2495
+ type: "button",
2496
+ onClick: handleConfirmSave,
2497
+ disabled: !!saving,
2498
+ className: "px-3 py-1.5 text-xs font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-70 inline-flex items-center gap-1.5",
2499
+ children: [
2500
+ saving ? /* @__PURE__ */ jsx(Loader2, { className: "w-3 h-3 animate-spin" }) : /* @__PURE__ */ jsx(Check, { className: "w-3 h-3" }),
2501
+ saving ? "Saving\u2026" : "Save to library"
2502
+ ]
2503
+ }
2504
+ )
2505
+ ] }),
2506
+ editing && staged.file && /* @__PURE__ */ jsx(
2507
+ ImageEditor,
2508
+ {
2509
+ file: staged.file,
2510
+ initialName: staged.name,
2511
+ onCancel: () => setEditing(false),
2512
+ onConfirm: (file, name) => {
2513
+ setStaged((prev) => {
2514
+ if (!prev) return prev;
2515
+ if (prev.previewUrl) URL.revokeObjectURL(prev.previewUrl);
2516
+ return { ...prev, file, previewUrl: URL.createObjectURL(file), name };
2517
+ });
2518
+ setEditing(false);
2519
+ }
2520
+ }
2521
+ )
2522
+ ] }),
1853
2523
  results.length > 0 && /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2", children: results.map((photo, i) => {
1854
2524
  const thumb = getThumb(photo);
1855
2525
  const full = getFullUrl(photo);
1856
- const isPending = pendingUrl === full;
1857
- const isSaved = savedUrl === full;
1858
- return /* @__PURE__ */ jsxs(
2526
+ const isStaged = staged?.photo && getFullUrl(staged.photo) === full;
2527
+ return /* @__PURE__ */ jsx(
1859
2528
  "div",
1860
2529
  {
1861
2530
  className: "flex flex-col gap-1",
1862
- children: [
1863
- /* @__PURE__ */ jsxs("div", { className: "group relative aspect-square rounded-md overflow-hidden border border-border bg-muted/30", children: [
1864
- /* @__PURE__ */ jsx(
1865
- "img",
1866
- {
1867
- src: thumb,
1868
- alt: photo.alt || "",
1869
- loading: "lazy",
1870
- className: "w-full h-full object-cover"
1871
- }
1872
- ),
1873
- /* @__PURE__ */ jsx(
1874
- "button",
1875
- {
1876
- type: "button",
1877
- onClick: () => handleSave(photo),
1878
- disabled: !!pendingUrl || saving,
1879
- className: cn(
1880
- "absolute inset-0 flex items-center justify-center text-xs font-medium",
1881
- "bg-background/80 opacity-0 group-hover:opacity-100 transition-opacity",
1882
- "text-foreground gap-1.5",
1883
- (isPending || isSaved) && "opacity-100"
1884
- ),
1885
- children: isPending ? /* @__PURE__ */ jsxs(Fragment, { children: [
1886
- /* @__PURE__ */ jsx(Loader2, { className: "w-3.5 h-3.5 animate-spin" }),
1887
- " Saving\u2026"
1888
- ] }) : isSaved ? /* @__PURE__ */ jsxs(Fragment, { children: [
1889
- /* @__PURE__ */ jsx(Check, { className: "w-3.5 h-3.5" }),
1890
- " Saved"
1891
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1892
- /* @__PURE__ */ jsx(Image$1, { className: "w-3.5 h-3.5" }),
1893
- " Save"
1894
- ] })
1895
- }
1896
- ),
1897
- photo.photographer && /* @__PURE__ */ jsx("div", { className: "absolute bottom-0 inset-x-0 px-1.5 py-0.5 text-[10px] text-background bg-foreground/60 truncate", children: photo.photographerUrl ? /* @__PURE__ */ jsx(
1898
- "a",
1899
- {
1900
- href: photo.photographerUrl,
1901
- target: "_blank",
1902
- rel: "noreferrer noopener",
1903
- onClick: (e) => e.stopPropagation(),
1904
- className: "hover:underline",
1905
- children: photo.photographer
1906
- }
1907
- ) : photo.photographer })
1908
- ] }),
2531
+ children: /* @__PURE__ */ jsxs("div", { className: "group relative aspect-square rounded-md overflow-hidden border border-border bg-muted/30", children: [
1909
2532
  /* @__PURE__ */ jsx(
1910
- "input",
2533
+ "img",
1911
2534
  {
1912
- type: "text",
1913
- value: nameOverrides[full] || "",
1914
- onChange: (e) => setNameOverrides((prev) => ({ ...prev, [full]: e.target.value })),
1915
- placeholder: "Optional name",
1916
- disabled: isPending || saving,
1917
- className: "w-full px-1.5 py-1 text-[11px] rounded-md border border-border bg-transparent placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
2535
+ src: thumb,
2536
+ alt: photo.alt || "",
2537
+ loading: "lazy",
2538
+ className: "w-full h-full object-cover"
1918
2539
  }
1919
- )
1920
- ]
2540
+ ),
2541
+ /* @__PURE__ */ jsx(
2542
+ "button",
2543
+ {
2544
+ type: "button",
2545
+ onClick: () => handlePick(photo),
2546
+ disabled: !!staged || saving,
2547
+ className: cn(
2548
+ "absolute inset-0 flex items-center justify-center text-xs font-medium",
2549
+ "bg-background/80 opacity-0 group-hover:opacity-100 transition-opacity",
2550
+ "text-foreground gap-1.5",
2551
+ isStaged && "opacity-100"
2552
+ ),
2553
+ children: isStaged ? /* @__PURE__ */ jsxs(Fragment, { children: [
2554
+ /* @__PURE__ */ jsx(Check, { className: "w-3.5 h-3.5" }),
2555
+ " Selected"
2556
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2557
+ /* @__PURE__ */ jsx(Image$1, { className: "w-3.5 h-3.5" }),
2558
+ " Pick"
2559
+ ] })
2560
+ }
2561
+ ),
2562
+ photo.photographer && /* @__PURE__ */ jsx("div", { className: "absolute bottom-0 inset-x-0 px-1.5 py-0.5 text-[10px] text-background bg-foreground/60 truncate", children: photo.photographerUrl ? /* @__PURE__ */ jsx(
2563
+ "a",
2564
+ {
2565
+ href: photo.photographerUrl,
2566
+ target: "_blank",
2567
+ rel: "noreferrer noopener",
2568
+ onClick: (e) => e.stopPropagation(),
2569
+ className: "hover:underline",
2570
+ children: photo.photographer
2571
+ }
2572
+ ) : photo.photographer })
2573
+ ] })
1921
2574
  },
1922
2575
  `${full}-${i}`
1923
2576
  );
@@ -2063,7 +2716,7 @@ var TagEditor = ({ initial, suggestions, assetName, onCancel, onSave }) => {
2063
2716
  );
2064
2717
  };
2065
2718
  var InlineConfirm = ({ open, title, body, confirmLabel = "Confirm", cancelLabel = "Cancel", destructive, onConfirm, onCancel }) => {
2066
- React7.useEffect(() => {
2719
+ React8.useEffect(() => {
2067
2720
  if (!open) return;
2068
2721
  const onKey = (e) => {
2069
2722
  if (e.key === "Escape") {
@@ -2168,8 +2821,8 @@ var AttachToContextToggle = ({ checked, onChange, contextLabel }) => /* @__PURE_
2168
2821
  ] })
2169
2822
  ] });
2170
2823
  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 }) => {
2171
- const replaceInputRef = React7.useRef(null);
2172
- const replaceTargetRef = React7.useRef(null);
2824
+ const replaceInputRef = React8.useRef(null);
2825
+ const replaceTargetRef = React8.useRef(null);
2173
2826
  const handleRename = useCallback(async (asset2) => {
2174
2827
  const current = asset2.cleanName || asset2.name || "";
2175
2828
  const next = window.prompt("Rename asset", current);
@@ -2182,6 +2835,21 @@ var ScopedAssetBrowser = ({ scope: _scope, accept: _accept, pageSize: _pageSize,
2182
2835
  replaceTargetRef.current = asset2.id;
2183
2836
  replaceInputRef.current?.click();
2184
2837
  }, []);
2838
+ const [editAsset, setEditAsset] = useState(null);
2839
+ const [editFile, setEditFile] = useState(null);
2840
+ const [editLoading, setEditLoading] = useState(false);
2841
+ const handleEditImage = useCallback(async (asset2) => {
2842
+ if (!isEditableImage(asset2.mimeType || void 0)) return;
2843
+ setEditLoading(true);
2844
+ try {
2845
+ const file = await urlToFile(asset2.url, asset2.cleanName || asset2.name || "image");
2846
+ setEditFile(file);
2847
+ setEditAsset(asset2);
2848
+ } catch {
2849
+ } finally {
2850
+ setEditLoading(false);
2851
+ }
2852
+ }, []);
2185
2853
  const handleReplaceFiles = useCallback(async (e) => {
2186
2854
  const file = e.target.files?.[0];
2187
2855
  const assetId = replaceTargetRef.current;
@@ -2308,6 +2976,7 @@ var ScopedAssetBrowser = ({ scope: _scope, accept: _accept, pageSize: _pageSize,
2308
2976
  onRename: handleRename,
2309
2977
  onReplace: handleReplace,
2310
2978
  onEditTags: handleEditTags,
2979
+ onEditImage: handleEditImage,
2311
2980
  allowDelete,
2312
2981
  currentAppId,
2313
2982
  currentAppName,
@@ -2325,6 +2994,24 @@ var ScopedAssetBrowser = ({ scope: _scope, accept: _accept, pageSize: _pageSize,
2325
2994
  onChange: handleReplaceFiles
2326
2995
  }
2327
2996
  ),
2997
+ editAsset && editFile && /* @__PURE__ */ jsx(
2998
+ ImageEditor,
2999
+ {
3000
+ file: editFile,
3001
+ initialName: editAsset.cleanName || editAsset.name || "image",
3002
+ onCancel: () => {
3003
+ setEditAsset(null);
3004
+ setEditFile(null);
3005
+ },
3006
+ onConfirm: async (file) => {
3007
+ const target = editAsset;
3008
+ setEditAsset(null);
3009
+ setEditFile(null);
3010
+ if (target) await replaceFile(target.id, file);
3011
+ }
3012
+ }
3013
+ ),
3014
+ editLoading && /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-[2147483645] flex items-center justify-center bg-black/30 pointer-events-none", children: /* @__PURE__ */ jsx(Loader2, { className: "w-6 h-6 animate-spin text-primary" }) }),
2328
3015
  tagEditorAsset && /* @__PURE__ */ jsx(
2329
3016
  TagEditor,
2330
3017
  {
@@ -2503,6 +3190,21 @@ var AssetPickerContent = ({
2503
3190
  setTab("browse");
2504
3191
  await reconcileAfterUpload(targetScope);
2505
3192
  }, [upload, multiple, onSelect, toSelection, uploadScope, activeScope, reconcileAfterUpload]);
3193
+ const handleSaveEditedFile = useCallback(async (file, name) => {
3194
+ const finalFile = name && name.trim() ? new File([file], /\.[^.]+$/.test(name) ? name : `${name}.${file.name.split(".").pop() || "webp"}`, { type: file.type }) : file;
3195
+ const targetScope = uploadScope;
3196
+ const sameAsActive = targetScope === activeScope;
3197
+ const result = await upload(finalFile, void 0, sameAsActive ? void 0 : targetScope);
3198
+ if (result) {
3199
+ setTab("browse");
3200
+ if (!multiple) {
3201
+ setSelectedIds(/* @__PURE__ */ new Set([result.id]));
3202
+ onSelect?.(toSelection(result));
3203
+ }
3204
+ await reconcileAfterUpload(targetScope);
3205
+ }
3206
+ return result;
3207
+ }, [upload, uploadScope, activeScope, multiple, onSelect, toSelection, reconcileAfterUpload]);
2506
3208
  const handleUrlImport = useCallback(async (url, name) => {
2507
3209
  const targetScope = uploadScope;
2508
3210
  const sameAsActive = targetScope === activeScope;
@@ -2587,7 +3289,7 @@ var AssetPickerContent = ({
2587
3289
  )
2588
3290
  ] }),
2589
3291
  tab === "browse" && /* @__PURE__ */ jsxs("div", { className: "relative", children: [
2590
- /* @__PURE__ */ jsx(Search, { className: "absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground" }),
3292
+ /* @__PURE__ */ jsx(Search, { className: "absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground pointer-events-none" }),
2591
3293
  /* @__PURE__ */ jsx(
2592
3294
  "input",
2593
3295
  {
@@ -2595,7 +3297,7 @@ var AssetPickerContent = ({
2595
3297
  value: search,
2596
3298
  onChange: (e) => setSearch(e.target.value),
2597
3299
  placeholder: "Search\u2026",
2598
- className: "pl-7 pr-2 py-1 text-xs rounded-md border border-border bg-transparent focus:outline-none focus:ring-1 focus:ring-ring w-36"
3300
+ className: "pl-8 pr-2 py-1 text-xs rounded-md border border-border bg-transparent focus:outline-none focus:ring-1 focus:ring-ring w-36"
2599
3301
  }
2600
3302
  )
2601
3303
  ] }),
@@ -2804,6 +3506,7 @@ var AssetPickerContent = ({
2804
3506
  {
2805
3507
  collectionId,
2806
3508
  onSave: handleRemoteIngest,
3509
+ onSaveFile: handleSaveEditedFile,
2807
3510
  saving: uploading
2808
3511
  }
2809
3512
  )
@@ -2830,6 +3533,7 @@ var AssetPickerContent = ({
2830
3533
  {
2831
3534
  collectionId,
2832
3535
  onSave: handleRemoteIngest,
3536
+ onSaveFile: handleSaveEditedFile,
2833
3537
  saving: uploading
2834
3538
  }
2835
3539
  )
@@ -2928,5 +3632,5 @@ var AssetPicker = (props) => {
2928
3632
  assertStylesLoaded();
2929
3633
 
2930
3634
  export { ASSET_MIME_FILTERS, AssetPicker, useAppRegistry, useAssets };
2931
- //# sourceMappingURL=chunk-N2FPPTHH.js.map
2932
- //# sourceMappingURL=chunk-N2FPPTHH.js.map
3635
+ //# sourceMappingURL=chunk-WLN4WW7K.js.map
3636
+ //# sourceMappingURL=chunk-WLN4WW7K.js.map