@proveanything/smartlinks-utils-ui 1.0.0 → 1.13.0

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,8 +1,8 @@
1
1
  import { assertStylesLoaded } from './chunk-OLYC54YT.js';
2
2
  import { cn } from './chunk-L7FQ52F5.js';
3
- import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
3
+ import React7, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
4
4
  import * as SL from '@proveanything/smartlinks';
5
- import { Filter, Search, LayoutGrid, List, X, Loader2, AlertCircle, ImageOff, Clipboard, Pencil, Check, Upload, Link, MicOff, Mic, ChevronDown, ChevronRight, Sparkles, Image, Trash2, FileIcon, Film, Music, FileText, AppWindow } from 'lucide-react';
5
+ import { Filter, Search, LayoutGrid, List, X, Loader2, AlertCircle, Tag, ImageOff, Wand2, Maximize2, Clipboard, Pencil, Check, Upload, Link, MicOff, Mic, ChevronDown, ChevronRight, Sparkles, Image as Image$1, Plus, FileIcon, Film, Music, FileText, AppWindow, MoreVertical, Trash2 } from 'lucide-react';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
7
 
8
8
  // src/components/AssetPicker/types.ts
@@ -130,8 +130,13 @@ function useAssets({ scope, accept, pageSize, appId, listAppId }) {
130
130
  }
131
131
  }, [scope, appId]);
132
132
  const remove = useCallback(async (assetId) => {
133
+ const collectionId = scope.collectionId;
133
134
  try {
134
- await SL.asset.remove({ assetId, scope });
135
+ if (collectionId && SL.asset.deleteAdmin) {
136
+ await SL.asset.deleteAdmin({ collectionId, assetId });
137
+ } else {
138
+ await SL.asset.remove({ assetId, scope });
139
+ }
135
140
  if (mountedRef.current) {
136
141
  setAssets((prev) => prev.filter((a) => a.id !== assetId));
137
142
  }
@@ -141,6 +146,75 @@ function useAssets({ scope, accept, pageSize, appId, listAppId }) {
141
146
  return false;
142
147
  }
143
148
  }, [scope]);
149
+ const restore = useCallback(async (assetId) => {
150
+ const collectionId = scope.collectionId;
151
+ if (!collectionId) {
152
+ if (mountedRef.current) setError("Restore requires a collection scope");
153
+ return null;
154
+ }
155
+ try {
156
+ const result = await SL.asset.restoreAdmin(collectionId, assetId);
157
+ if (mountedRef.current) {
158
+ setAssets((prev) => {
159
+ const exists = prev.some((a) => a.id === assetId);
160
+ return exists ? prev.map((a) => a.id === assetId ? result : a) : [result, ...prev];
161
+ });
162
+ }
163
+ return result;
164
+ } catch (err) {
165
+ if (mountedRef.current) setError(err?.message || "Restore failed");
166
+ return null;
167
+ }
168
+ }, [scope]);
169
+ const replaceFile = useCallback(async (assetId, file, onProgress) => {
170
+ const collectionId = scope.collectionId;
171
+ if (!collectionId) {
172
+ if (mountedRef.current) setError("Replace requires a collection scope");
173
+ return null;
174
+ }
175
+ setUploading(true);
176
+ setUploadProgress(0);
177
+ try {
178
+ const result = await SL.asset.replaceFile({
179
+ collectionId,
180
+ assetId,
181
+ file,
182
+ onProgress: (pct) => {
183
+ setUploadProgress(pct);
184
+ onProgress?.(pct);
185
+ }
186
+ });
187
+ if (mountedRef.current) {
188
+ setAssets((prev) => prev.map((a) => a.id === assetId ? result : a));
189
+ }
190
+ return result;
191
+ } catch (err) {
192
+ if (mountedRef.current) setError(err?.message || "Replace failed");
193
+ return null;
194
+ } finally {
195
+ if (mountedRef.current) {
196
+ setUploading(false);
197
+ setUploadProgress(0);
198
+ }
199
+ }
200
+ }, [scope]);
201
+ const updateAsset = useCallback(async (assetId, patch) => {
202
+ const collectionId = scope.collectionId;
203
+ if (!collectionId) {
204
+ if (mountedRef.current) setError("Update requires a collection scope");
205
+ return null;
206
+ }
207
+ try {
208
+ const result = await SL.asset.updateAdmin({ collectionId, assetId, ...patch });
209
+ if (mountedRef.current) {
210
+ setAssets((prev) => prev.map((a) => a.id === assetId ? result : a));
211
+ }
212
+ return result;
213
+ } catch (err) {
214
+ if (mountedRef.current) setError(err?.message || "Update failed");
215
+ return null;
216
+ }
217
+ }, [scope]);
144
218
  return {
145
219
  assets,
146
220
  loading,
@@ -150,6 +224,9 @@ function useAssets({ scope, accept, pageSize, appId, listAppId }) {
150
224
  uploadFromUrl,
151
225
  uploadFromRemoteUrl,
152
226
  remove,
227
+ restore,
228
+ replaceFile,
229
+ updateAsset,
153
230
  uploading,
154
231
  uploadProgress
155
232
  };
@@ -232,7 +309,7 @@ function useAppRegistry(collectionId) {
232
309
  }
233
310
  function getIcon(mimeType) {
234
311
  if (!mimeType) return FileIcon;
235
- if (mimeType.startsWith("image/")) return Image;
312
+ if (mimeType.startsWith("image/")) return Image$1;
236
313
  if (mimeType.startsWith("video/")) return Film;
237
314
  if (mimeType.startsWith("audio/")) return Music;
238
315
  return FileText;
@@ -244,6 +321,7 @@ function formatSize(bytes) {
244
321
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
245
322
  }
246
323
  function getThumbnail(asset2) {
324
+ if (asset2.thumbnail) return asset2.thumbnail;
247
325
  if (asset2.thumbnails?.x200) return asset2.thumbnails.x200;
248
326
  if (asset2.thumbnails?.x100) return asset2.thumbnails.x100;
249
327
  if (asset2.thumbnails?.x512) return asset2.thumbnails.x512;
@@ -251,7 +329,7 @@ function getThumbnail(asset2) {
251
329
  return null;
252
330
  }
253
331
  function getAssetAppId(asset2) {
254
- return asset2.appId || asset2.metadata?.appId || asset2.metadata?.app || void 0;
332
+ return asset2.app || asset2.appId || asset2.metadata?.appId || asset2.metadata?.app || void 0;
255
333
  }
256
334
  var AppBadge = ({ appId, appName, size = "sm" }) => {
257
335
  const label = appName && appName !== appId ? appName : appId;
@@ -271,11 +349,134 @@ var AppBadge = ({ appId, appName, size = "sm" }) => {
271
349
  }
272
350
  );
273
351
  };
274
- var AssetGridItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelete, allowDelete, currentAppId, getAppName }) => {
352
+ var CardMenu = ({ onRename, onReplace, onEditTags, onDelete, position = "absolute" }) => {
353
+ const [open, setOpen] = useState(false);
354
+ const ref = useRef(null);
355
+ useEffect(() => {
356
+ if (!open) return;
357
+ const handler = (e) => {
358
+ if (!ref.current?.contains(e.target)) setOpen(false);
359
+ };
360
+ document.addEventListener("mousedown", handler);
361
+ return () => document.removeEventListener("mousedown", handler);
362
+ }, [open]);
363
+ if (!onRename && !onReplace && !onEditTags && !onDelete) return null;
364
+ return /* @__PURE__ */ jsxs(
365
+ "div",
366
+ {
367
+ ref,
368
+ className: cn(
369
+ position === "absolute" ? "absolute bottom-1.5 right-1.5" : "relative flex-shrink-0"
370
+ ),
371
+ onClick: (e) => e.stopPropagation(),
372
+ onDoubleClick: (e) => e.stopPropagation(),
373
+ children: [
374
+ /* @__PURE__ */ jsx(
375
+ "button",
376
+ {
377
+ type: "button",
378
+ onClick: (e) => {
379
+ e.stopPropagation();
380
+ setOpen((o) => !o);
381
+ },
382
+ className: cn(
383
+ "w-6 h-6 rounded-full flex items-center justify-center transition-all",
384
+ "bg-background/90 border border-border text-foreground hover:bg-background shadow-sm",
385
+ position === "absolute" && "opacity-0 group-hover:opacity-100",
386
+ open && "opacity-100"
387
+ ),
388
+ title: "Asset actions",
389
+ "aria-label": "Asset actions",
390
+ children: /* @__PURE__ */ jsx(MoreVertical, { className: "w-3 h-3" })
391
+ }
392
+ ),
393
+ open && /* @__PURE__ */ jsxs(
394
+ "div",
395
+ {
396
+ className: "absolute bottom-full right-0 mb-1 z-20 min-w-[140px] rounded-md border border-border bg-popover text-popover-foreground shadow-md py-1",
397
+ role: "menu",
398
+ children: [
399
+ onRename && /* @__PURE__ */ jsxs(
400
+ "button",
401
+ {
402
+ type: "button",
403
+ role: "menuitem",
404
+ onClick: (e) => {
405
+ e.stopPropagation();
406
+ setOpen(false);
407
+ onRename();
408
+ },
409
+ className: "w-full flex items-center gap-2 px-2.5 py-1.5 text-xs hover:bg-accent",
410
+ children: [
411
+ /* @__PURE__ */ jsx(Pencil, { className: "w-3 h-3" }),
412
+ " Rename"
413
+ ]
414
+ }
415
+ ),
416
+ onReplace && /* @__PURE__ */ jsxs(
417
+ "button",
418
+ {
419
+ type: "button",
420
+ role: "menuitem",
421
+ onClick: (e) => {
422
+ e.stopPropagation();
423
+ setOpen(false);
424
+ onReplace();
425
+ },
426
+ className: "w-full flex items-center gap-2 px-2.5 py-1.5 text-xs hover:bg-accent",
427
+ children: [
428
+ /* @__PURE__ */ jsx(Upload, { className: "w-3 h-3" }),
429
+ " Replace file"
430
+ ]
431
+ }
432
+ ),
433
+ onEditTags && /* @__PURE__ */ jsxs(
434
+ "button",
435
+ {
436
+ type: "button",
437
+ role: "menuitem",
438
+ onClick: (e) => {
439
+ e.stopPropagation();
440
+ setOpen(false);
441
+ onEditTags();
442
+ },
443
+ className: "w-full flex items-center gap-2 px-2.5 py-1.5 text-xs hover:bg-accent",
444
+ children: [
445
+ /* @__PURE__ */ jsx(Tag, { className: "w-3 h-3" }),
446
+ " Edit tags"
447
+ ]
448
+ }
449
+ ),
450
+ onDelete && /* @__PURE__ */ jsxs(
451
+ "button",
452
+ {
453
+ type: "button",
454
+ role: "menuitem",
455
+ onClick: (e) => {
456
+ e.stopPropagation();
457
+ setOpen(false);
458
+ onDelete();
459
+ },
460
+ className: "w-full flex items-center gap-2 px-2.5 py-1.5 text-xs text-destructive hover:bg-destructive/10",
461
+ children: [
462
+ /* @__PURE__ */ jsx(Trash2, { className: "w-3 h-3" }),
463
+ " Delete"
464
+ ]
465
+ }
466
+ )
467
+ ]
468
+ }
469
+ )
470
+ ]
471
+ }
472
+ );
473
+ };
474
+ var AssetGridItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelete, onRename, onReplace, onEditTags, allowDelete, currentAppId, getAppName, activeLabels, onToggleLabel }) => {
275
475
  const thumb = getThumbnail(asset2);
276
476
  const Icon = getIcon(asset2.mimeType);
277
477
  const ownerAppId = getAssetAppId(asset2);
278
478
  const showAppBadge = !!currentAppId && !!ownerAppId && ownerAppId !== currentAppId;
479
+ const labels = asset2.labels || [];
279
480
  return /* @__PURE__ */ jsxs(
280
481
  "div",
281
482
  {
@@ -313,31 +514,57 @@ var AssetGridItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelet
313
514
  formatSize(asset2.size),
314
515
  asset2.mimeType && ` \u2022 ${asset2.mimeType.split("/")[1]?.toUpperCase() || asset2.mimeType}`
315
516
  ] }),
316
- showAppBadge && /* @__PURE__ */ jsx("div", { className: "mt-1", children: /* @__PURE__ */ jsx(AppBadge, { appId: ownerAppId, appName: getAppName?.(ownerAppId), size: "xs" }) })
517
+ showAppBadge && /* @__PURE__ */ jsx("div", { className: "mt-1", children: /* @__PURE__ */ jsx(AppBadge, { appId: ownerAppId, appName: getAppName?.(ownerAppId), size: "xs" }) }),
518
+ labels.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-1 flex flex-wrap gap-0.5", children: [
519
+ labels.slice(0, 3).map((lbl) => {
520
+ const active = !!activeLabels?.has(lbl);
521
+ return /* @__PURE__ */ jsxs(
522
+ "button",
523
+ {
524
+ type: "button",
525
+ onClick: (e) => {
526
+ e.stopPropagation();
527
+ onToggleLabel?.(lbl);
528
+ },
529
+ className: cn(
530
+ "inline-flex items-center gap-0.5 rounded-full border px-1 py-0 text-[9px] max-w-full truncate transition-colors",
531
+ active ? "bg-primary text-primary-foreground border-primary" : "bg-muted text-muted-foreground border-border hover:bg-muted/70"
532
+ ),
533
+ title: `Filter by label: ${lbl}`,
534
+ children: [
535
+ /* @__PURE__ */ jsx(Tag, { className: "w-2 h-2 flex-shrink-0" }),
536
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: lbl })
537
+ ]
538
+ },
539
+ lbl
540
+ );
541
+ }),
542
+ labels.length > 3 && /* @__PURE__ */ jsxs("span", { className: "text-[9px] text-muted-foreground", children: [
543
+ "+",
544
+ labels.length - 3
545
+ ] })
546
+ ] })
317
547
  ] }),
318
548
  selected && /* @__PURE__ */ jsx("div", { className: "absolute top-2 right-2 w-5 h-5 rounded-full bg-primary flex items-center justify-center", children: /* @__PURE__ */ jsx(Check, { className: "w-3 h-3 text-primary-foreground" }) }),
319
- allowDelete && onDelete && /* @__PURE__ */ jsx(
320
- "button",
549
+ /* @__PURE__ */ jsx(
550
+ CardMenu,
321
551
  {
322
- type: "button",
323
- className: "absolute top-2 left-2 w-6 h-6 rounded-full bg-destructive/80 hover:bg-destructive text-destructive-foreground flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity",
324
- onClick: (e) => {
325
- e.stopPropagation();
326
- onDelete();
327
- },
328
- title: "Delete asset",
329
- children: /* @__PURE__ */ jsx(Trash2, { className: "w-3 h-3" })
552
+ onRename,
553
+ onReplace,
554
+ onEditTags,
555
+ onDelete: allowDelete ? onDelete : void 0
330
556
  }
331
557
  )
332
558
  ]
333
559
  }
334
560
  );
335
561
  };
336
- var AssetListItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelete, allowDelete, currentAppId, getAppName }) => {
562
+ var AssetListItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelete, onRename, onReplace, onEditTags, allowDelete, currentAppId, getAppName, activeLabels, onToggleLabel }) => {
337
563
  const thumb = getThumbnail(asset2);
338
564
  const Icon = getIcon(asset2.mimeType);
339
565
  const ownerAppId = getAssetAppId(asset2);
340
566
  const showAppBadge = !!currentAppId && !!ownerAppId && ownerAppId !== currentAppId;
567
+ const labels = asset2.labels || [];
341
568
  return /* @__PURE__ */ jsxs(
342
569
  "div",
343
570
  {
@@ -369,20 +596,40 @@ var AssetListItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelet
369
596
  /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
370
597
  formatSize(asset2.size),
371
598
  asset2.mimeType && ` \u2022 ${asset2.mimeType}`
372
- ] })
599
+ ] }),
600
+ labels.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-1 flex flex-wrap gap-1", children: labels.map((lbl) => {
601
+ const active = !!activeLabels?.has(lbl);
602
+ return /* @__PURE__ */ jsxs(
603
+ "button",
604
+ {
605
+ type: "button",
606
+ onClick: (e) => {
607
+ e.stopPropagation();
608
+ onToggleLabel?.(lbl);
609
+ },
610
+ className: cn(
611
+ "inline-flex items-center gap-0.5 rounded-full border px-1.5 py-0 text-[10px] transition-colors",
612
+ active ? "bg-primary text-primary-foreground border-primary" : "bg-muted text-muted-foreground border-border hover:bg-muted/70"
613
+ ),
614
+ title: `Filter by label: ${lbl}`,
615
+ children: [
616
+ /* @__PURE__ */ jsx(Tag, { className: "w-2.5 h-2.5 flex-shrink-0" }),
617
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: lbl })
618
+ ]
619
+ },
620
+ lbl
621
+ );
622
+ }) })
373
623
  ] }),
374
624
  selected && /* @__PURE__ */ jsx("div", { className: "w-5 h-5 rounded-full bg-primary flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ jsx(Check, { className: "w-3 h-3 text-primary-foreground" }) }),
375
- allowDelete && onDelete && /* @__PURE__ */ jsx(
376
- "button",
625
+ /* @__PURE__ */ jsx(
626
+ CardMenu,
377
627
  {
378
- type: "button",
379
- className: "w-6 h-6 rounded-full bg-destructive/80 hover:bg-destructive text-destructive-foreground flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0",
380
- onClick: (e) => {
381
- e.stopPropagation();
382
- onDelete();
383
- },
384
- title: "Delete",
385
- children: /* @__PURE__ */ jsx(Trash2, { className: "w-3 h-3" })
628
+ onRename,
629
+ onReplace,
630
+ onEditTags,
631
+ onDelete: allowDelete ? onDelete : void 0,
632
+ position: "inline"
386
633
  }
387
634
  )
388
635
  ]
@@ -396,9 +643,14 @@ var AssetGrid = ({
396
643
  onToggleSelect,
397
644
  onDoubleClickSelect,
398
645
  onDelete,
646
+ onRename,
647
+ onReplace,
648
+ onEditTags,
399
649
  allowDelete,
400
650
  currentAppId,
401
- getAppName
651
+ getAppName,
652
+ activeLabels,
653
+ onToggleLabel
402
654
  }) => {
403
655
  if (assets.length === 0) return null;
404
656
  if (viewMode === "list") {
@@ -410,9 +662,14 @@ var AssetGrid = ({
410
662
  onToggle: () => onToggleSelect(asset2),
411
663
  onDoubleClick: onDoubleClickSelect ? () => onDoubleClickSelect(asset2) : void 0,
412
664
  onDelete: allowDelete && onDelete ? () => onDelete(asset2.id) : void 0,
665
+ onRename: onRename ? () => onRename(asset2) : void 0,
666
+ onReplace: onReplace ? () => onReplace(asset2) : void 0,
667
+ onEditTags: onEditTags ? () => onEditTags(asset2) : void 0,
413
668
  allowDelete,
414
669
  currentAppId,
415
- getAppName
670
+ getAppName,
671
+ activeLabels,
672
+ onToggleLabel
416
673
  },
417
674
  asset2.id
418
675
  )) });
@@ -425,25 +682,156 @@ var AssetGrid = ({
425
682
  onToggle: () => onToggleSelect(asset2),
426
683
  onDoubleClick: onDoubleClickSelect ? () => onDoubleClickSelect(asset2) : void 0,
427
684
  onDelete: allowDelete && onDelete ? () => onDelete(asset2.id) : void 0,
685
+ onRename: onRename ? () => onRename(asset2) : void 0,
686
+ onReplace: onReplace ? () => onReplace(asset2) : void 0,
687
+ onEditTags: onEditTags ? () => onEditTags(asset2) : void 0,
428
688
  allowDelete,
429
689
  currentAppId,
430
- getAppName
690
+ getAppName,
691
+ activeLabels,
692
+ onToggleLabel
431
693
  },
432
694
  asset2.id
433
695
  )) });
434
696
  };
697
+
698
+ // src/components/AssetPicker/imageProcessing.ts
699
+ var WEBP_CANDIDATES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/jpg", "image/bmp"]);
700
+ function isWebpCandidate(file) {
701
+ return WEBP_CANDIDATES.has(file.type.toLowerCase());
702
+ }
703
+ function isProcessableImage(file) {
704
+ if (!file.type.startsWith("image/")) return false;
705
+ if (file.type === "image/svg+xml" || file.type === "image/gif") return false;
706
+ return true;
707
+ }
708
+ async function loadImage(file) {
709
+ const url = URL.createObjectURL(file);
710
+ const img = new Image();
711
+ img.decoding = "async";
712
+ await new Promise((resolve, reject) => {
713
+ img.onload = () => resolve();
714
+ img.onerror = () => reject(new Error("Failed to decode image"));
715
+ img.src = url;
716
+ });
717
+ return { img, url };
718
+ }
719
+ async function getImageDimensions(file) {
720
+ if (!isProcessableImage(file)) return null;
721
+ try {
722
+ const { img, url } = await loadImage(file);
723
+ const out = { width: img.naturalWidth, height: img.naturalHeight };
724
+ URL.revokeObjectURL(url);
725
+ return out;
726
+ } catch {
727
+ return null;
728
+ }
729
+ }
730
+ async function processImage(file, opts = {}) {
731
+ const toWebp = opts.toWebp ?? isWebpCandidate(file);
732
+ const maxDim = opts.maxDimension ?? 2048;
733
+ const quality = opts.quality ?? 0.85;
734
+ const fallback = {
735
+ file,
736
+ changed: false,
737
+ originalSize: file.size,
738
+ newSize: file.size,
739
+ width: 0,
740
+ height: 0
741
+ };
742
+ if (!isProcessableImage(file)) return fallback;
743
+ let img;
744
+ let url;
745
+ try {
746
+ ({ img, url } = await loadImage(file));
747
+ } catch {
748
+ return fallback;
749
+ }
750
+ try {
751
+ const w = img.naturalWidth;
752
+ const h = img.naturalHeight;
753
+ const longest = Math.max(w, h);
754
+ const needsResize = maxDim > 0 && longest > maxDim;
755
+ const needsReencode = toWebp && file.type !== "image/webp";
756
+ if (!needsResize && !needsReencode) {
757
+ return { ...fallback, width: w, height: h };
758
+ }
759
+ const scale = needsResize ? maxDim / longest : 1;
760
+ const targetW = Math.round(w * scale);
761
+ const targetH = Math.round(h * scale);
762
+ const canvas = document.createElement("canvas");
763
+ canvas.width = targetW;
764
+ canvas.height = targetH;
765
+ const ctx = canvas.getContext("2d");
766
+ if (!ctx) return { ...fallback, width: w, height: h };
767
+ ctx.drawImage(img, 0, 0, targetW, targetH);
768
+ const outType = needsReencode ? "image/webp" : file.type;
769
+ const blob = await new Promise(
770
+ (resolve) => canvas.toBlob((b) => resolve(b), outType, quality)
771
+ );
772
+ if (!blob) return { ...fallback, width: w, height: h };
773
+ const base = file.name.replace(/\.[^.]+$/, "") || "image";
774
+ const ext = needsReencode ? "webp" : file.name.match(/\.([^.]+)$/)?.[1] || "png";
775
+ const newName = `${base}.${ext}`;
776
+ const newFile = new File([blob], newName, { type: outType });
777
+ return {
778
+ file: newFile,
779
+ changed: true,
780
+ originalSize: file.size,
781
+ newSize: newFile.size,
782
+ width: targetW,
783
+ height: targetH
784
+ };
785
+ } finally {
786
+ URL.revokeObjectURL(url);
787
+ }
788
+ }
435
789
  var UploadZone = ({
436
790
  onFiles,
437
791
  accept,
438
792
  multiple,
439
793
  uploading,
440
794
  uploadProgress = 0,
441
- className
795
+ className,
796
+ imageOptimization
442
797
  }) => {
798
+ const optConfig = useMemo(() => {
799
+ if (imageOptimization === false) return { forced: "off" };
800
+ if (imageOptimization === true) return { forced: "on", maxDimension: 2048, quality: 0.85 };
801
+ if (typeof imageOptimization === "object" && imageOptimization) {
802
+ return {
803
+ forced: imageOptimization.userToggleable === false ? imageOptimization.defaultEnabled === false ? "off" : "on" : null,
804
+ defaultEnabled: imageOptimization.defaultEnabled ?? true,
805
+ maxDimension: imageOptimization.maxDimension ?? 2048,
806
+ quality: imageOptimization.quality ?? 0.85
807
+ };
808
+ }
809
+ return { forced: null, defaultEnabled: true, maxDimension: 2048, quality: 0.85 };
810
+ }, [imageOptimization]);
443
811
  const [dragOver, setDragOver] = useState(false);
444
812
  const [pastedFile, setPastedFile] = useState(null);
445
813
  const [editingName, setEditingName] = useState(false);
446
814
  const [fileName, setFileName] = useState("");
815
+ const [lightboxOpen, setLightboxOpen] = useState(false);
816
+ const [autoOptimizeUser, setAutoOptimizeUser] = useState(() => {
817
+ if (optConfig.forced) return optConfig.forced === "on";
818
+ try {
819
+ const v = localStorage.getItem("smartlinks.assetPicker.autoOptimize");
820
+ if (v !== null) return v === "1";
821
+ } catch {
822
+ }
823
+ return optConfig.defaultEnabled ?? true;
824
+ });
825
+ const autoOptimize = optConfig.forced ? optConfig.forced === "on" : autoOptimizeUser;
826
+ const showOptimizeToggle = optConfig.forced === null;
827
+ const setAutoOptimizePersist = useCallback((v) => {
828
+ setAutoOptimizeUser(v);
829
+ try {
830
+ localStorage.setItem("smartlinks.assetPicker.autoOptimize", v ? "1" : "0");
831
+ } catch {
832
+ }
833
+ }, []);
834
+ const [optimizing, setOptimizing] = useState(false);
447
835
  const inputRef = useRef(null);
448
836
  const nameInputRef = useRef(null);
449
837
  const zoneRef = useRef(null);
@@ -468,9 +856,12 @@ var UploadZone = ({
468
856
  e.preventDefault();
469
857
  const previewUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : "";
470
858
  const defaultName = file.name === "image.png" ? `pasted-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/[T:]/g, "-")}` : file.name.replace(/\.[^.]+$/, "");
471
- setPastedFile({ file, previewUrl, name: defaultName });
859
+ setPastedFile({ file, previewUrl, name: defaultName, origSize: file.size });
472
860
  setFileName(defaultName);
473
861
  setEditingName(false);
862
+ getImageDimensions(file).then((dims) => {
863
+ if (dims) setPastedFile((prev) => prev && prev.file === file ? { ...prev, origDims: dims } : prev);
864
+ });
474
865
  return;
475
866
  }
476
867
  }
@@ -478,15 +869,28 @@ var UploadZone = ({
478
869
  document.addEventListener("paste", handlePaste);
479
870
  return () => document.removeEventListener("paste", handlePaste);
480
871
  }, [uploading, accept]);
481
- const handleConfirmPaste = useCallback(() => {
872
+ const handleConfirmPaste = useCallback(async () => {
482
873
  if (!pastedFile) return;
483
- const ext = pastedFile.file.name.includes(".") ? pastedFile.file.name.split(".").pop() : "png";
874
+ let working = pastedFile.file;
875
+ if (autoOptimize && isProcessableImage(working)) {
876
+ setOptimizing(true);
877
+ try {
878
+ const result = await processImage(working, {
879
+ maxDimension: optConfig.maxDimension,
880
+ quality: optConfig.quality
881
+ });
882
+ if (result.changed) working = result.file;
883
+ } finally {
884
+ setOptimizing(false);
885
+ }
886
+ }
887
+ const ext = working.name.includes(".") ? working.name.split(".").pop() : "png";
484
888
  const finalName = `${fileName.trim() || "pasted-image"}.${ext}`;
485
- const renamedFile = new File([pastedFile.file], finalName, { type: pastedFile.file.type });
889
+ const renamedFile = new File([working], finalName, { type: working.type });
486
890
  onFiles([renamedFile]);
487
891
  if (pastedFile.previewUrl) URL.revokeObjectURL(pastedFile.previewUrl);
488
892
  setPastedFile(null);
489
- }, [pastedFile, fileName, onFiles]);
893
+ }, [pastedFile, fileName, onFiles, autoOptimize, optConfig]);
490
894
  const handleCancelPaste = useCallback(() => {
491
895
  if (pastedFile?.previewUrl) URL.revokeObjectURL(pastedFile.previewUrl);
492
896
  setPastedFile(null);
@@ -509,9 +913,12 @@ var UploadZone = ({
509
913
  const presentForRename = useCallback((file) => {
510
914
  const previewUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : "";
511
915
  const defaultName = file.name.replace(/\.[^.]+$/, "") || "file";
512
- setPastedFile({ file, previewUrl, name: defaultName });
916
+ setPastedFile({ file, previewUrl, name: defaultName, origSize: file.size });
513
917
  setFileName(defaultName);
514
918
  setEditingName(false);
919
+ getImageDimensions(file).then((dims) => {
920
+ if (dims) setPastedFile((prev) => prev && prev.file === file ? { ...prev, origDims: dims } : prev);
921
+ });
515
922
  }, []);
516
923
  const handleDrop = useCallback((e) => {
517
924
  e.preventDefault();
@@ -521,7 +928,7 @@ var UploadZone = ({
521
928
  if (files.length === 1 && !multiple) {
522
929
  presentForRename(files[0]);
523
930
  } else if (files.length > 0) {
524
- onFiles(multiple ? files : [files[0]]);
931
+ void handleBatchFiles(multiple ? files : [files[0]]);
525
932
  }
526
933
  }, [onFiles, multiple, presentForRename]);
527
934
  const handleInputChange = useCallback((e) => {
@@ -529,87 +936,189 @@ var UploadZone = ({
529
936
  if (files.length === 1 && !multiple) {
530
937
  presentForRename(files[0]);
531
938
  } else if (files.length > 0) {
532
- onFiles(multiple ? files : [files[0]]);
939
+ void handleBatchFiles(multiple ? files : [files[0]]);
533
940
  }
534
941
  e.target.value = "";
535
942
  }, [onFiles, multiple, presentForRename]);
943
+ const handleBatchFiles = useCallback(async (files) => {
944
+ if (!autoOptimize) {
945
+ onFiles(files);
946
+ return;
947
+ }
948
+ setOptimizing(true);
949
+ try {
950
+ const out = [];
951
+ for (const f of files) {
952
+ if (isProcessableImage(f)) {
953
+ const r = await processImage(f, {
954
+ maxDimension: optConfig.maxDimension,
955
+ quality: optConfig.quality
956
+ });
957
+ out.push(r.changed ? r.file : f);
958
+ } else {
959
+ out.push(f);
960
+ }
961
+ }
962
+ onFiles(out);
963
+ } finally {
964
+ setOptimizing(false);
965
+ }
966
+ }, [autoOptimize, onFiles, optConfig]);
967
+ const fmtKB = (n) => n >= 1024 * 1024 ? `${(n / 1024 / 1024).toFixed(1)} MB` : `${(n / 1024).toFixed(1)} KB`;
968
+ const optimizeToggle = showOptimizeToggle ? /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1.5 text-[10px] text-muted-foreground cursor-pointer select-none", children: [
969
+ /* @__PURE__ */ jsx(
970
+ "input",
971
+ {
972
+ type: "checkbox",
973
+ checked: autoOptimize,
974
+ onChange: (e) => setAutoOptimizePersist(e.target.checked),
975
+ className: "cursor-pointer"
976
+ }
977
+ ),
978
+ /* @__PURE__ */ jsx(Wand2, { className: "w-3 h-3" }),
979
+ /* @__PURE__ */ jsxs("span", { children: [
980
+ "Optimize for web (WebP, max ",
981
+ optConfig.maxDimension ?? 2048,
982
+ "px)"
983
+ ] })
984
+ ] }) : null;
536
985
  if (pastedFile) {
537
- return /* @__PURE__ */ jsx("div", { className: cn(
986
+ const willOptimize = autoOptimize && isProcessableImage(pastedFile.file);
987
+ return /* @__PURE__ */ jsxs("div", { className: cn(
538
988
  "border-2 border-solid border-primary rounded-lg p-4 transition-colors",
539
989
  className
540
- ), children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-3", children: [
541
- pastedFile.previewUrl ? /* @__PURE__ */ jsx(
542
- "img",
543
- {
544
- src: pastedFile.previewUrl,
545
- alt: "Pasted content",
546
- className: "max-h-32 max-w-full rounded-md object-contain border border-border"
547
- }
548
- ) : /* @__PURE__ */ jsx("div", { className: "w-16 h-16 rounded-md bg-muted flex items-center justify-center", children: /* @__PURE__ */ jsx(Clipboard, { className: "w-6 h-6 text-muted-foreground" }) }),
549
- /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1.5 w-full max-w-xs", children: editingName ? /* @__PURE__ */ jsx(
550
- "input",
551
- {
552
- ref: nameInputRef,
553
- type: "text",
554
- value: fileName,
555
- onChange: (e) => setFileName(e.target.value),
556
- onKeyDown: (e) => {
557
- if (e.key === "Enter") {
558
- setEditingName(false);
559
- handleConfirmPaste();
560
- }
561
- if (e.key === "Escape") setEditingName(false);
562
- },
563
- onBlur: () => setEditingName(false),
564
- className: "flex-1 px-2 py-1 text-sm rounded border border-border bg-transparent focus:outline-none focus:ring-1 focus:ring-ring text-center",
565
- placeholder: "File name"
566
- }
567
- ) : /* @__PURE__ */ jsxs(
568
- "button",
569
- {
570
- type: "button",
571
- onClick: () => setEditingName(true),
572
- className: "flex items-center gap-1 mx-auto px-2 py-1 text-sm text-muted-foreground hover:text-foreground rounded hover:bg-accent transition-colors",
573
- title: "Rename",
574
- children: [
575
- /* @__PURE__ */ jsx("span", { className: "truncate max-w-[200px]", children: fileName }),
576
- /* @__PURE__ */ jsx(Pencil, { className: "w-3 h-3 flex-shrink-0 opacity-50" })
577
- ]
578
- }
579
- ) }),
580
- /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground", children: [
581
- pastedFile.file.type,
582
- " \xB7 ",
583
- (pastedFile.file.size / 1024).toFixed(1),
584
- " KB"
585
- ] }),
586
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
587
- /* @__PURE__ */ jsxs(
990
+ ), children: [
991
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-3", children: [
992
+ pastedFile.previewUrl ? /* @__PURE__ */ jsxs(
588
993
  "button",
589
994
  {
590
995
  type: "button",
591
- onClick: handleCancelPaste,
592
- 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",
996
+ onClick: () => setLightboxOpen(true),
997
+ className: "group relative w-24 h-24 rounded-md border border-border bg-muted overflow-hidden flex items-center justify-center",
998
+ title: "Click to preview full size",
593
999
  children: [
594
- /* @__PURE__ */ jsx(X, { className: "w-3 h-3" }),
595
- " Cancel"
1000
+ /* @__PURE__ */ jsx(
1001
+ "img",
1002
+ {
1003
+ src: pastedFile.previewUrl,
1004
+ alt: "Pasted content",
1005
+ className: "w-full h-full object-cover",
1006
+ loading: "lazy",
1007
+ decoding: "async"
1008
+ }
1009
+ ),
1010
+ /* @__PURE__ */ jsx("span", { className: "absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors flex items-center justify-center opacity-0 group-hover:opacity-100", children: /* @__PURE__ */ jsx(Maximize2, { className: "w-4 h-4 text-white" }) })
596
1011
  ]
597
1012
  }
598
- ),
599
- /* @__PURE__ */ jsxs(
1013
+ ) : /* @__PURE__ */ jsx("div", { className: "w-16 h-16 rounded-md bg-muted flex items-center justify-center", children: /* @__PURE__ */ jsx(Clipboard, { className: "w-6 h-6 text-muted-foreground" }) }),
1014
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1.5 w-full max-w-xs", children: editingName ? /* @__PURE__ */ jsx(
1015
+ "input",
1016
+ {
1017
+ ref: nameInputRef,
1018
+ type: "text",
1019
+ value: fileName,
1020
+ onChange: (e) => setFileName(e.target.value),
1021
+ onKeyDown: (e) => {
1022
+ if (e.key === "Enter") {
1023
+ setEditingName(false);
1024
+ handleConfirmPaste();
1025
+ }
1026
+ if (e.key === "Escape") setEditingName(false);
1027
+ },
1028
+ onBlur: () => setEditingName(false),
1029
+ className: "flex-1 px-2 py-1 text-sm rounded border border-border bg-transparent focus:outline-none focus:ring-1 focus:ring-ring text-center",
1030
+ placeholder: "File name"
1031
+ }
1032
+ ) : /* @__PURE__ */ jsxs(
600
1033
  "button",
601
1034
  {
602
1035
  type: "button",
603
- onClick: handleConfirmPaste,
604
- className: "px-3 py-1.5 text-xs font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors flex items-center gap-1",
1036
+ onClick: () => setEditingName(true),
1037
+ className: "flex items-center gap-1 mx-auto px-2 py-1 text-sm text-muted-foreground hover:text-foreground rounded hover:bg-accent transition-colors",
1038
+ title: "Rename",
605
1039
  children: [
606
- /* @__PURE__ */ jsx(Check, { className: "w-3 h-3" }),
607
- " Upload"
1040
+ /* @__PURE__ */ jsx("span", { className: "truncate max-w-[200px]", children: fileName }),
1041
+ /* @__PURE__ */ jsx(Pencil, { className: "w-3 h-3 flex-shrink-0 opacity-50" })
608
1042
  ]
609
1043
  }
610
- )
611
- ] })
612
- ] }) });
1044
+ ) }),
1045
+ /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground text-center", children: [
1046
+ pastedFile.file.type,
1047
+ " \xB7 ",
1048
+ fmtKB(pastedFile.origSize),
1049
+ pastedFile.origDims && ` \xB7 ${pastedFile.origDims.width}\xD7${pastedFile.origDims.height}`,
1050
+ willOptimize && pastedFile.origDims && (pastedFile.origDims.width > 2048 || pastedFile.origDims.height > 2048 || pastedFile.file.type !== "image/webp") && /* @__PURE__ */ jsxs("span", { className: "block text-primary mt-0.5", children: [
1051
+ /* @__PURE__ */ jsx(Wand2, { className: "w-3 h-3 inline -mt-0.5 mr-1" }),
1052
+ "Will be optimized to WebP",
1053
+ pastedFile.origDims.width > 2048 || pastedFile.origDims.height > 2048 ? ", max 2048px" : ""
1054
+ ] })
1055
+ ] }),
1056
+ isProcessableImage(pastedFile.file) && optimizeToggle,
1057
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1058
+ /* @__PURE__ */ jsxs(
1059
+ "button",
1060
+ {
1061
+ type: "button",
1062
+ onClick: handleCancelPaste,
1063
+ 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",
1064
+ disabled: optimizing,
1065
+ children: [
1066
+ /* @__PURE__ */ jsx(X, { className: "w-3 h-3" }),
1067
+ " Cancel"
1068
+ ]
1069
+ }
1070
+ ),
1071
+ /* @__PURE__ */ jsxs(
1072
+ "button",
1073
+ {
1074
+ type: "button",
1075
+ onClick: handleConfirmPaste,
1076
+ className: "px-3 py-1.5 text-xs font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors flex items-center gap-1",
1077
+ disabled: optimizing,
1078
+ children: [
1079
+ optimizing ? /* @__PURE__ */ jsx(Loader2, { className: "w-3 h-3 animate-spin" }) : /* @__PURE__ */ jsx(Check, { className: "w-3 h-3" }),
1080
+ optimizing ? "Optimizing\u2026" : "Upload"
1081
+ ]
1082
+ }
1083
+ )
1084
+ ] })
1085
+ ] }),
1086
+ lightboxOpen && pastedFile.previewUrl && /* @__PURE__ */ jsxs(
1087
+ "div",
1088
+ {
1089
+ className: "fixed inset-0 z-[100] bg-black/80 flex items-center justify-center p-6",
1090
+ onClick: () => setLightboxOpen(false),
1091
+ onMouseDown: (e) => e.stopPropagation(),
1092
+ onTouchStart: (e) => e.stopPropagation(),
1093
+ role: "dialog",
1094
+ "aria-label": "Image preview",
1095
+ children: [
1096
+ /* @__PURE__ */ jsx(
1097
+ "button",
1098
+ {
1099
+ type: "button",
1100
+ onClick: (e) => {
1101
+ e.stopPropagation();
1102
+ setLightboxOpen(false);
1103
+ },
1104
+ className: "absolute top-4 right-4 p-2 rounded-md bg-white/10 hover:bg-white/20 text-white",
1105
+ "aria-label": "Close preview",
1106
+ children: /* @__PURE__ */ jsx(X, { className: "w-5 h-5" })
1107
+ }
1108
+ ),
1109
+ /* @__PURE__ */ jsx(
1110
+ "img",
1111
+ {
1112
+ src: pastedFile.previewUrl,
1113
+ alt: "Full preview",
1114
+ className: "max-w-full max-h-full object-contain",
1115
+ onClick: (e) => e.stopPropagation()
1116
+ }
1117
+ )
1118
+ ]
1119
+ }
1120
+ )
1121
+ ] });
613
1122
  }
614
1123
  return /* @__PURE__ */ jsxs(
615
1124
  "div",
@@ -644,13 +1153,10 @@ var UploadZone = ({
644
1153
  className: "hidden"
645
1154
  }
646
1155
  ),
647
- uploading ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-2 py-2", children: [
1156
+ uploading || optimizing ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-2 py-2", children: [
648
1157
  /* @__PURE__ */ jsx(Loader2, { className: "w-6 h-6 text-primary animate-spin" }),
649
- /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
650
- "Uploading\u2026 ",
651
- uploadProgress > 0 ? `${Math.round(uploadProgress)}%` : ""
652
- ] }),
653
- uploadProgress > 0 && /* @__PURE__ */ jsx("div", { className: "w-full max-w-xs h-1.5 bg-muted rounded-full overflow-hidden", children: /* @__PURE__ */ jsx(
1158
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: optimizing ? "Optimizing\u2026" : `Uploading\u2026 ${uploadProgress > 0 ? `${Math.round(uploadProgress)}%` : ""}` }),
1159
+ !optimizing && uploadProgress > 0 && /* @__PURE__ */ jsx("div", { className: "w-full max-w-xs h-1.5 bg-muted rounded-full overflow-hidden", children: /* @__PURE__ */ jsx(
654
1160
  "div",
655
1161
  {
656
1162
  className: "h-full bg-primary rounded-full transition-all duration-300",
@@ -671,7 +1177,15 @@ var UploadZone = ({
671
1177
  accept && /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground", children: [
672
1178
  "Accepts: ",
673
1179
  accept
674
- ] })
1180
+ ] }),
1181
+ /* @__PURE__ */ jsx(
1182
+ "div",
1183
+ {
1184
+ className: "mt-2 pt-2 border-t border-border/50 w-full flex justify-center",
1185
+ onClick: (e) => e.stopPropagation(),
1186
+ children: optimizeToggle
1187
+ }
1188
+ )
675
1189
  ] })
676
1190
  ]
677
1191
  }
@@ -1323,7 +1837,7 @@ var StockPhotoSearch = ({
1323
1837
  /* @__PURE__ */ jsx(Check, { className: "w-3.5 h-3.5" }),
1324
1838
  " Saved"
1325
1839
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1326
- /* @__PURE__ */ jsx(Image, { className: "w-3.5 h-3.5" }),
1840
+ /* @__PURE__ */ jsx(Image$1, { className: "w-3.5 h-3.5" }),
1327
1841
  " Save"
1328
1842
  ] })
1329
1843
  }
@@ -1358,6 +1872,144 @@ var StockPhotoSearch = ({
1358
1872
  }) })
1359
1873
  ] });
1360
1874
  };
1875
+ var TagEditor = ({ initial, suggestions, assetName, onCancel, onSave }) => {
1876
+ const [labels, setLabels] = useState(() => Array.from(new Set(initial.map((l) => l.trim()).filter(Boolean))));
1877
+ const [input, setInput] = useState("");
1878
+ const [saving, setSaving] = useState(false);
1879
+ const inputRef = useRef(null);
1880
+ useEffect(() => {
1881
+ inputRef.current?.focus();
1882
+ }, []);
1883
+ const filteredSuggestions = useMemo(() => {
1884
+ const q = input.trim().toLowerCase();
1885
+ const have = new Set(labels.map((l) => l.toLowerCase()));
1886
+ return suggestions.filter((s) => !have.has(s.toLowerCase())).filter((s) => !q || s.toLowerCase().includes(q)).slice(0, 8);
1887
+ }, [suggestions, input, labels]);
1888
+ const addLabel = (raw) => {
1889
+ const v = raw.trim();
1890
+ if (!v) return;
1891
+ setLabels((prev) => prev.some((p) => p.toLowerCase() === v.toLowerCase()) ? prev : [...prev, v]);
1892
+ setInput("");
1893
+ };
1894
+ const removeLabel = (label) => {
1895
+ setLabels((prev) => prev.filter((l) => l !== label));
1896
+ };
1897
+ const handleKey = (e) => {
1898
+ if (e.key === "Enter" || e.key === ",") {
1899
+ e.preventDefault();
1900
+ addLabel(input);
1901
+ } else if (e.key === "Backspace" && !input && labels.length > 0) {
1902
+ setLabels((prev) => prev.slice(0, -1));
1903
+ }
1904
+ };
1905
+ const handleSave = async () => {
1906
+ setSaving(true);
1907
+ try {
1908
+ await onSave(labels);
1909
+ } finally {
1910
+ setSaving(false);
1911
+ }
1912
+ };
1913
+ return /* @__PURE__ */ jsx(
1914
+ "div",
1915
+ {
1916
+ className: "fixed inset-0 z-[100] flex items-center justify-center bg-black/60",
1917
+ onClick: onCancel,
1918
+ onMouseDown: (e) => e.stopPropagation(),
1919
+ onTouchStart: (e) => e.stopPropagation(),
1920
+ children: /* @__PURE__ */ jsxs(
1921
+ "div",
1922
+ {
1923
+ className: "w-[min(28rem,92vw)] rounded-lg bg-background border border-border shadow-xl p-4 flex flex-col gap-3",
1924
+ onClick: (e) => e.stopPropagation(),
1925
+ children: [
1926
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1927
+ /* @__PURE__ */ jsx(Tag, { className: "w-4 h-4 text-muted-foreground" }),
1928
+ /* @__PURE__ */ jsxs("h3", { className: "text-sm font-semibold text-foreground truncate", children: [
1929
+ "Edit tags",
1930
+ assetName ? ` \u2014 ${assetName}` : ""
1931
+ ] }),
1932
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onCancel, className: "ml-auto p-1 rounded hover:bg-muted", "aria-label": "Close", children: /* @__PURE__ */ jsx(X, { className: "w-4 h-4 text-muted-foreground" }) })
1933
+ ] }),
1934
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1 p-2 rounded-md border border-border min-h-[2.5rem]", children: [
1935
+ labels.map((label) => /* @__PURE__ */ jsxs(
1936
+ "span",
1937
+ {
1938
+ className: "inline-flex items-center gap-1 rounded-full bg-primary/10 text-primary border border-primary/30 px-2 py-0.5 text-xs",
1939
+ children: [
1940
+ label,
1941
+ /* @__PURE__ */ jsx(
1942
+ "button",
1943
+ {
1944
+ type: "button",
1945
+ onClick: () => removeLabel(label),
1946
+ className: "opacity-70 hover:opacity-100",
1947
+ "aria-label": `Remove ${label}`,
1948
+ children: /* @__PURE__ */ jsx(X, { className: "w-3 h-3" })
1949
+ }
1950
+ )
1951
+ ]
1952
+ },
1953
+ label
1954
+ )),
1955
+ /* @__PURE__ */ jsx(
1956
+ "input",
1957
+ {
1958
+ ref: inputRef,
1959
+ type: "text",
1960
+ value: input,
1961
+ onChange: (e) => setInput(e.target.value),
1962
+ onKeyDown: handleKey,
1963
+ placeholder: labels.length === 0 ? "Add a tag and press Enter\u2026" : "Add another\u2026",
1964
+ className: "flex-1 min-w-[8rem] bg-transparent text-sm focus:outline-none text-foreground"
1965
+ }
1966
+ )
1967
+ ] }),
1968
+ filteredSuggestions.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-1", children: [
1969
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground self-center mr-1", children: "Suggestions:" }),
1970
+ filteredSuggestions.map((s) => /* @__PURE__ */ jsxs(
1971
+ "button",
1972
+ {
1973
+ type: "button",
1974
+ onClick: () => addLabel(s),
1975
+ className: cn(
1976
+ "inline-flex items-center gap-0.5 rounded-full border border-border bg-muted text-muted-foreground hover:bg-muted/70 px-2 py-0.5 text-[11px]"
1977
+ ),
1978
+ children: [
1979
+ /* @__PURE__ */ jsx(Plus, { className: "w-2.5 h-2.5" }),
1980
+ s
1981
+ ]
1982
+ },
1983
+ s
1984
+ ))
1985
+ ] }),
1986
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2 pt-1", children: [
1987
+ /* @__PURE__ */ jsx(
1988
+ "button",
1989
+ {
1990
+ type: "button",
1991
+ onClick: onCancel,
1992
+ className: "px-3 py-1.5 text-xs rounded-md border border-border text-foreground hover:bg-muted",
1993
+ children: "Cancel"
1994
+ }
1995
+ ),
1996
+ /* @__PURE__ */ jsx(
1997
+ "button",
1998
+ {
1999
+ type: "button",
2000
+ onClick: handleSave,
2001
+ disabled: saving,
2002
+ className: "px-3 py-1.5 text-xs rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50",
2003
+ children: saving ? "Saving\u2026" : "Save tags"
2004
+ }
2005
+ )
2006
+ ] })
2007
+ ]
2008
+ }
2009
+ )
2010
+ }
2011
+ );
2012
+ };
1361
2013
  var GlobalUploadToggle = ({ checked, onChange, appName }) => /* @__PURE__ */ jsxs("label", { className: "flex items-start gap-2 text-xs text-muted-foreground cursor-pointer select-none p-2 rounded-md border border-border bg-muted/30", children: [
1362
2014
  /* @__PURE__ */ jsx(
1363
2015
  "input",
@@ -1374,14 +2026,78 @@ var GlobalUploadToggle = ({ checked, onChange, appName }) => /* @__PURE__ */ jsx
1374
2026
  ] })
1375
2027
  ] });
1376
2028
  var ScopedAssetBrowser = ({ scope, accept, pageSize, viewMode, search, selectedIds, onToggleSelect, onDoubleClickSelect, onDelete, allowDelete, emptyText, listAppId, currentAppId, currentAppName, getAppName }) => {
1377
- const { assets, loading, error, refresh } = useAssets({ scope, accept, pageSize, listAppId });
2029
+ const { assets, loading, error, refresh, remove, updateAsset, replaceFile } = useAssets({ scope, accept, pageSize, listAppId });
2030
+ const replaceInputRef = React7.useRef(null);
2031
+ const replaceTargetRef = React7.useRef(null);
2032
+ const handleRename = useCallback(async (asset2) => {
2033
+ const current = asset2.cleanName || asset2.name || "";
2034
+ const next = window.prompt("Rename asset", current);
2035
+ if (next === null) return;
2036
+ const trimmed = next.trim();
2037
+ if (!trimmed || trimmed === current) return;
2038
+ await updateAsset(asset2.id, { name: trimmed });
2039
+ }, [updateAsset]);
2040
+ const handleReplace = useCallback((asset2) => {
2041
+ replaceTargetRef.current = asset2.id;
2042
+ replaceInputRef.current?.click();
2043
+ }, []);
2044
+ const handleReplaceFiles = useCallback(async (e) => {
2045
+ const file = e.target.files?.[0];
2046
+ const assetId = replaceTargetRef.current;
2047
+ e.target.value = "";
2048
+ replaceTargetRef.current = null;
2049
+ if (!file || !assetId) return;
2050
+ await replaceFile(assetId, file);
2051
+ }, [replaceFile]);
2052
+ const handleDeleteWithConfirm = useCallback(async (assetId) => {
2053
+ if (!window.confirm("Delete this asset? It can be restored from the asset manager within 30 days.")) return;
2054
+ const ok = await remove(assetId);
2055
+ if (ok) onDelete?.(assetId);
2056
+ }, [remove, onDelete]);
2057
+ const [tagEditorAsset, setTagEditorAsset] = useState(null);
2058
+ const handleEditTags = useCallback((asset2) => {
2059
+ setTagEditorAsset(asset2);
2060
+ }, []);
2061
+ const handleSaveTags = useCallback(async (next) => {
2062
+ if (!tagEditorAsset) return;
2063
+ await updateAsset(tagEditorAsset.id, { labels: next });
2064
+ setTagEditorAsset(null);
2065
+ }, [tagEditorAsset, updateAsset]);
2066
+ const [activeLabels, setActiveLabels] = useState(/* @__PURE__ */ new Set());
2067
+ const toggleLabel = useCallback((label) => {
2068
+ setActiveLabels((prev) => {
2069
+ const next = new Set(prev);
2070
+ if (next.has(label)) next.delete(label);
2071
+ else next.add(label);
2072
+ return next;
2073
+ });
2074
+ }, []);
2075
+ const clearLabels = useCallback(() => setActiveLabels(/* @__PURE__ */ new Set()), []);
2076
+ const allLabels = useMemo(() => {
2077
+ const counts = /* @__PURE__ */ new Map();
2078
+ for (const a of assets) {
2079
+ for (const lbl of a.labels || []) {
2080
+ counts.set(lbl, (counts.get(lbl) || 0) + 1);
2081
+ }
2082
+ }
2083
+ return Array.from(counts.entries()).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([label, count]) => ({ label, count }));
2084
+ }, [assets]);
1378
2085
  const filteredAssets = useMemo(() => {
1379
- if (!search.trim()) return assets;
1380
- const q = search.toLowerCase();
1381
- return assets.filter(
1382
- (a) => (a.name || "").toLowerCase().includes(q) || (a.cleanName || "").toLowerCase().includes(q) || (a.mimeType || "").toLowerCase().includes(q)
1383
- );
1384
- }, [assets, search]);
2086
+ const q = search.trim().toLowerCase();
2087
+ return assets.filter((a) => {
2088
+ if (q) {
2089
+ const hit = (a.name || "").toLowerCase().includes(q) || (a.cleanName || "").toLowerCase().includes(q) || (a.mimeType || "").toLowerCase().includes(q) || (a.labels || []).some((l) => l.toLowerCase().includes(q));
2090
+ if (!hit) return false;
2091
+ }
2092
+ if (activeLabels.size > 0) {
2093
+ const labels = new Set(a.labels || []);
2094
+ for (const need of activeLabels) {
2095
+ if (!labels.has(need)) return false;
2096
+ }
2097
+ }
2098
+ return true;
2099
+ });
2100
+ }, [assets, search, activeLabels]);
1385
2101
  if (loading && assets.length === 0) {
1386
2102
  return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-12", children: /* @__PURE__ */ jsx(Loader2, { className: "w-6 h-6 text-muted-foreground animate-spin" }) });
1387
2103
  }
@@ -1392,28 +2108,87 @@ var ScopedAssetBrowser = ({ scope, accept, pageSize, viewMode, search, selectedI
1392
2108
  /* @__PURE__ */ jsx("button", { type: "button", onClick: refresh, className: "ml-auto underline text-xs", children: "Retry" })
1393
2109
  ] });
1394
2110
  }
1395
- if (filteredAssets.length === 0) {
1396
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-muted-foreground", children: [
2111
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2112
+ allLabels.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 flex-wrap", children: [
2113
+ /* @__PURE__ */ jsx(Tag, { className: "w-3.5 h-3.5 text-muted-foreground flex-shrink-0" }),
2114
+ allLabels.slice(0, 12).map(({ label, count }) => {
2115
+ const active = activeLabels.has(label);
2116
+ return /* @__PURE__ */ jsxs(
2117
+ "button",
2118
+ {
2119
+ type: "button",
2120
+ onClick: () => toggleLabel(label),
2121
+ className: cn(
2122
+ "inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] transition-colors",
2123
+ active ? "bg-primary text-primary-foreground border-primary" : "bg-muted text-muted-foreground border-border hover:bg-muted/70"
2124
+ ),
2125
+ title: active ? `Remove "${label}" from filter` : `Filter by "${label}"`,
2126
+ children: [
2127
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: label }),
2128
+ /* @__PURE__ */ jsx("span", { className: "opacity-70", children: count })
2129
+ ]
2130
+ },
2131
+ label
2132
+ );
2133
+ }),
2134
+ activeLabels.size > 0 && /* @__PURE__ */ jsxs(
2135
+ "button",
2136
+ {
2137
+ type: "button",
2138
+ onClick: clearLabels,
2139
+ className: "inline-flex items-center gap-0.5 text-[11px] text-muted-foreground hover:text-foreground",
2140
+ title: "Clear label filter",
2141
+ children: [
2142
+ /* @__PURE__ */ jsx(X, { className: "w-3 h-3" }),
2143
+ " clear"
2144
+ ]
2145
+ }
2146
+ )
2147
+ ] }),
2148
+ filteredAssets.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-muted-foreground", children: [
1397
2149
  /* @__PURE__ */ jsx(ImageOff, { className: "w-8 h-8 mb-2" }),
1398
2150
  /* @__PURE__ */ jsx("p", { className: "text-sm", children: emptyText || "No assets found" }),
1399
- search && /* @__PURE__ */ jsx("p", { className: "text-xs mt-1", children: "Try adjusting your search" })
1400
- ] });
1401
- }
1402
- return /* @__PURE__ */ jsx(
1403
- AssetGrid,
1404
- {
1405
- assets: filteredAssets,
1406
- viewMode,
1407
- selectedIds,
1408
- onToggleSelect,
1409
- onDoubleClickSelect,
1410
- onDelete: allowDelete ? onDelete : void 0,
1411
- allowDelete,
1412
- currentAppId,
1413
- currentAppName,
1414
- getAppName
1415
- }
1416
- );
2151
+ (search || activeLabels.size > 0) && /* @__PURE__ */ jsx("p", { className: "text-xs mt-1", children: "Try adjusting your filters" })
2152
+ ] }) : /* @__PURE__ */ jsx(
2153
+ AssetGrid,
2154
+ {
2155
+ assets: filteredAssets,
2156
+ viewMode,
2157
+ selectedIds,
2158
+ onToggleSelect,
2159
+ onDoubleClickSelect,
2160
+ onDelete: allowDelete ? handleDeleteWithConfirm : void 0,
2161
+ onRename: allowDelete ? handleRename : void 0,
2162
+ onReplace: allowDelete ? handleReplace : void 0,
2163
+ onEditTags: allowDelete ? handleEditTags : void 0,
2164
+ allowDelete,
2165
+ currentAppId,
2166
+ currentAppName,
2167
+ getAppName,
2168
+ activeLabels,
2169
+ onToggleLabel: toggleLabel
2170
+ }
2171
+ ),
2172
+ /* @__PURE__ */ jsx(
2173
+ "input",
2174
+ {
2175
+ ref: replaceInputRef,
2176
+ type: "file",
2177
+ className: "hidden",
2178
+ onChange: handleReplaceFiles
2179
+ }
2180
+ ),
2181
+ tagEditorAsset && /* @__PURE__ */ jsx(
2182
+ TagEditor,
2183
+ {
2184
+ initial: tagEditorAsset.labels || [],
2185
+ suggestions: allLabels.map((l) => l.label),
2186
+ assetName: tagEditorAsset.cleanName || tagEditorAsset.name,
2187
+ onCancel: () => setTagEditorAsset(null),
2188
+ onSave: handleSaveTags
2189
+ }
2190
+ )
2191
+ ] });
1417
2192
  };
1418
2193
  var AssetPickerContent = ({
1419
2194
  scope,
@@ -1434,7 +2209,8 @@ var AssetPickerContent = ({
1434
2209
  defaultView = "grid",
1435
2210
  emptyText,
1436
2211
  pageSize = 50,
1437
- onConfirm
2212
+ onConfirm,
2213
+ imageOptimization
1438
2214
  }) => {
1439
2215
  const [appFilter, setAppFilter] = useState(defaultAppFilter);
1440
2216
  const hasAppFilter = !!appId;
@@ -1444,13 +2220,6 @@ var AssetPickerContent = ({
1444
2220
  const { apps: registryApps, getAppName } = useAppRegistry(collectionIdForRegistry);
1445
2221
  const resolvedAppName = appName || (appId ? getAppName(appId) : void 0);
1446
2222
  const listAppId = hasAppFilter ? appFilter === "app" ? appId : otherAppFilter || void 0 : void 0;
1447
- const { assets, upload, uploadFromUrl, uploadFromRemoteUrl, uploading, uploadProgress } = useAssets({
1448
- scope,
1449
- accept: acceptProp,
1450
- pageSize,
1451
- appId: uploadGlobal ? void 0 : appId,
1452
- listAppId
1453
- });
1454
2223
  const [tab, setTab] = useState("browse");
1455
2224
  const [viewMode, setViewMode] = useState(defaultView);
1456
2225
  const [search, setSearch] = useState("");
@@ -1460,7 +2229,7 @@ var AssetPickerContent = ({
1460
2229
  return new Set(Array.isArray(value) ? value : [value]);
1461
2230
  });
1462
2231
  const hasProductScope = !!productScope;
1463
- const [scopeTab, setScopeTab] = useState("collection");
2232
+ const [scopeTab, setScopeTab] = useState(hasProductScope ? "product" : "collection");
1464
2233
  const effectiveAccept = useMemo(() => {
1465
2234
  if (acceptProp) return acceptProp;
1466
2235
  const entry = ASSET_MIME_FILTERS.find((f) => f.value === mimeFilter);
@@ -1477,6 +2246,13 @@ var AssetPickerContent = ({
1477
2246
  }
1478
2247
  return scope;
1479
2248
  }, [scope, productScope, scopeTab, hasProductScope]);
2249
+ const { assets, upload, uploadFromUrl, uploadFromRemoteUrl, uploading, uploadProgress } = useAssets({
2250
+ scope: activeScope,
2251
+ accept: acceptProp,
2252
+ pageSize,
2253
+ appId: uploadGlobal ? void 0 : appId,
2254
+ listAppId
2255
+ });
1480
2256
  const toSelection = useCallback((asset2) => ({
1481
2257
  id: asset2.id,
1482
2258
  url: asset2.url,
@@ -1484,6 +2260,10 @@ var AssetPickerContent = ({
1484
2260
  mimeType: asset2.mimeType,
1485
2261
  size: asset2.size,
1486
2262
  metadata: asset2.metadata,
2263
+ thumbnail: asset2.thumbnail ?? null,
2264
+ app: asset2.app ?? null,
2265
+ labels: asset2.labels,
2266
+ // Deprecated — kept for backward-compat with hosts still reading `thumbnails`.
1487
2267
  thumbnails: asset2.thumbnails
1488
2268
  }), []);
1489
2269
  const handleToggleSelect = useCallback((asset2) => {
@@ -1743,7 +2523,8 @@ var AssetPickerContent = ({
1743
2523
  accept: acceptProp,
1744
2524
  multiple,
1745
2525
  uploading,
1746
- uploadProgress
2526
+ uploadProgress,
2527
+ imageOptimization
1747
2528
  }
1748
2529
  )
1749
2530
  ] }),
@@ -1892,5 +2673,5 @@ var AssetPicker = (props) => {
1892
2673
  assertStylesLoaded();
1893
2674
 
1894
2675
  export { ASSET_MIME_FILTERS, AssetPicker, useAppRegistry, useAssets };
1895
- //# sourceMappingURL=chunk-ZTUZPAHD.js.map
1896
- //# sourceMappingURL=chunk-ZTUZPAHD.js.map
2676
+ //# sourceMappingURL=chunk-PSVYUVZC.js.map
2677
+ //# sourceMappingURL=chunk-PSVYUVZC.js.map