@proveanything/smartlinks-utils-ui 1.0.0 → 1.0.1

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.
@@ -2,7 +2,7 @@ import { assertStylesLoaded } from './chunk-OLYC54YT.js';
2
2
  import { cn } from './chunk-L7FQ52F5.js';
3
3
  import { 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, Clipboard, Pencil, Check, Upload, Link, MicOff, Mic, ChevronDown, ChevronRight, Sparkles, Image, Trash2, FileIcon, Film, Music, FileText, AppWindow } 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
  };
@@ -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,12 @@ 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 AssetGridItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelete, allowDelete, currentAppId, getAppName, activeLabels, onToggleLabel }) => {
275
353
  const thumb = getThumbnail(asset2);
276
354
  const Icon = getIcon(asset2.mimeType);
277
355
  const ownerAppId = getAssetAppId(asset2);
278
356
  const showAppBadge = !!currentAppId && !!ownerAppId && ownerAppId !== currentAppId;
357
+ const labels = asset2.labels || [];
279
358
  return /* @__PURE__ */ jsxs(
280
359
  "div",
281
360
  {
@@ -313,7 +392,36 @@ var AssetGridItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelet
313
392
  formatSize(asset2.size),
314
393
  asset2.mimeType && ` \u2022 ${asset2.mimeType.split("/")[1]?.toUpperCase() || asset2.mimeType}`
315
394
  ] }),
316
- showAppBadge && /* @__PURE__ */ jsx("div", { className: "mt-1", children: /* @__PURE__ */ jsx(AppBadge, { appId: ownerAppId, appName: getAppName?.(ownerAppId), size: "xs" }) })
395
+ showAppBadge && /* @__PURE__ */ jsx("div", { className: "mt-1", children: /* @__PURE__ */ jsx(AppBadge, { appId: ownerAppId, appName: getAppName?.(ownerAppId), size: "xs" }) }),
396
+ labels.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-1 flex flex-wrap gap-0.5", children: [
397
+ labels.slice(0, 3).map((lbl) => {
398
+ const active = !!activeLabels?.has(lbl);
399
+ return /* @__PURE__ */ jsxs(
400
+ "button",
401
+ {
402
+ type: "button",
403
+ onClick: (e) => {
404
+ e.stopPropagation();
405
+ onToggleLabel?.(lbl);
406
+ },
407
+ className: cn(
408
+ "inline-flex items-center gap-0.5 rounded-full border px-1 py-0 text-[9px] max-w-full truncate transition-colors",
409
+ active ? "bg-primary text-primary-foreground border-primary" : "bg-muted text-muted-foreground border-border hover:bg-muted/70"
410
+ ),
411
+ title: `Filter by label: ${lbl}`,
412
+ children: [
413
+ /* @__PURE__ */ jsx(Tag, { className: "w-2 h-2 flex-shrink-0" }),
414
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: lbl })
415
+ ]
416
+ },
417
+ lbl
418
+ );
419
+ }),
420
+ labels.length > 3 && /* @__PURE__ */ jsxs("span", { className: "text-[9px] text-muted-foreground", children: [
421
+ "+",
422
+ labels.length - 3
423
+ ] })
424
+ ] })
317
425
  ] }),
318
426
  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
427
  allowDelete && onDelete && /* @__PURE__ */ jsx(
@@ -333,11 +441,12 @@ var AssetGridItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelet
333
441
  }
334
442
  );
335
443
  };
336
- var AssetListItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelete, allowDelete, currentAppId, getAppName }) => {
444
+ var AssetListItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelete, allowDelete, currentAppId, getAppName, activeLabels, onToggleLabel }) => {
337
445
  const thumb = getThumbnail(asset2);
338
446
  const Icon = getIcon(asset2.mimeType);
339
447
  const ownerAppId = getAssetAppId(asset2);
340
448
  const showAppBadge = !!currentAppId && !!ownerAppId && ownerAppId !== currentAppId;
449
+ const labels = asset2.labels || [];
341
450
  return /* @__PURE__ */ jsxs(
342
451
  "div",
343
452
  {
@@ -369,7 +478,30 @@ var AssetListItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelet
369
478
  /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
370
479
  formatSize(asset2.size),
371
480
  asset2.mimeType && ` \u2022 ${asset2.mimeType}`
372
- ] })
481
+ ] }),
482
+ labels.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-1 flex flex-wrap gap-1", children: labels.map((lbl) => {
483
+ const active = !!activeLabels?.has(lbl);
484
+ return /* @__PURE__ */ jsxs(
485
+ "button",
486
+ {
487
+ type: "button",
488
+ onClick: (e) => {
489
+ e.stopPropagation();
490
+ onToggleLabel?.(lbl);
491
+ },
492
+ className: cn(
493
+ "inline-flex items-center gap-0.5 rounded-full border px-1.5 py-0 text-[10px] transition-colors",
494
+ active ? "bg-primary text-primary-foreground border-primary" : "bg-muted text-muted-foreground border-border hover:bg-muted/70"
495
+ ),
496
+ title: `Filter by label: ${lbl}`,
497
+ children: [
498
+ /* @__PURE__ */ jsx(Tag, { className: "w-2.5 h-2.5 flex-shrink-0" }),
499
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: lbl })
500
+ ]
501
+ },
502
+ lbl
503
+ );
504
+ }) })
373
505
  ] }),
374
506
  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
507
  allowDelete && onDelete && /* @__PURE__ */ jsx(
@@ -398,7 +530,9 @@ var AssetGrid = ({
398
530
  onDelete,
399
531
  allowDelete,
400
532
  currentAppId,
401
- getAppName
533
+ getAppName,
534
+ activeLabels,
535
+ onToggleLabel
402
536
  }) => {
403
537
  if (assets.length === 0) return null;
404
538
  if (viewMode === "list") {
@@ -412,7 +546,9 @@ var AssetGrid = ({
412
546
  onDelete: allowDelete && onDelete ? () => onDelete(asset2.id) : void 0,
413
547
  allowDelete,
414
548
  currentAppId,
415
- getAppName
549
+ getAppName,
550
+ activeLabels,
551
+ onToggleLabel
416
552
  },
417
553
  asset2.id
418
554
  )) });
@@ -427,7 +563,9 @@ var AssetGrid = ({
427
563
  onDelete: allowDelete && onDelete ? () => onDelete(asset2.id) : void 0,
428
564
  allowDelete,
429
565
  currentAppId,
430
- getAppName
566
+ getAppName,
567
+ activeLabels,
568
+ onToggleLabel
431
569
  },
432
570
  asset2.id
433
571
  )) });
@@ -1375,13 +1513,41 @@ var GlobalUploadToggle = ({ checked, onChange, appName }) => /* @__PURE__ */ jsx
1375
1513
  ] });
1376
1514
  var ScopedAssetBrowser = ({ scope, accept, pageSize, viewMode, search, selectedIds, onToggleSelect, onDoubleClickSelect, onDelete, allowDelete, emptyText, listAppId, currentAppId, currentAppName, getAppName }) => {
1377
1515
  const { assets, loading, error, refresh } = useAssets({ scope, accept, pageSize, listAppId });
1516
+ const [activeLabels, setActiveLabels] = useState(/* @__PURE__ */ new Set());
1517
+ const toggleLabel = useCallback((label) => {
1518
+ setActiveLabels((prev) => {
1519
+ const next = new Set(prev);
1520
+ if (next.has(label)) next.delete(label);
1521
+ else next.add(label);
1522
+ return next;
1523
+ });
1524
+ }, []);
1525
+ const clearLabels = useCallback(() => setActiveLabels(/* @__PURE__ */ new Set()), []);
1526
+ const allLabels = useMemo(() => {
1527
+ const counts = /* @__PURE__ */ new Map();
1528
+ for (const a of assets) {
1529
+ for (const lbl of a.labels || []) {
1530
+ counts.set(lbl, (counts.get(lbl) || 0) + 1);
1531
+ }
1532
+ }
1533
+ return Array.from(counts.entries()).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([label, count]) => ({ label, count }));
1534
+ }, [assets]);
1378
1535
  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]);
1536
+ const q = search.trim().toLowerCase();
1537
+ return assets.filter((a) => {
1538
+ if (q) {
1539
+ 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));
1540
+ if (!hit) return false;
1541
+ }
1542
+ if (activeLabels.size > 0) {
1543
+ const labels = new Set(a.labels || []);
1544
+ for (const need of activeLabels) {
1545
+ if (!labels.has(need)) return false;
1546
+ }
1547
+ }
1548
+ return true;
1549
+ });
1550
+ }, [assets, search, activeLabels]);
1385
1551
  if (loading && assets.length === 0) {
1386
1552
  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
1553
  }
@@ -1392,28 +1558,65 @@ var ScopedAssetBrowser = ({ scope, accept, pageSize, viewMode, search, selectedI
1392
1558
  /* @__PURE__ */ jsx("button", { type: "button", onClick: refresh, className: "ml-auto underline text-xs", children: "Retry" })
1393
1559
  ] });
1394
1560
  }
1395
- if (filteredAssets.length === 0) {
1396
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-muted-foreground", children: [
1561
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1562
+ allLabels.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 flex-wrap", children: [
1563
+ /* @__PURE__ */ jsx(Tag, { className: "w-3.5 h-3.5 text-muted-foreground flex-shrink-0" }),
1564
+ allLabels.slice(0, 12).map(({ label, count }) => {
1565
+ const active = activeLabels.has(label);
1566
+ return /* @__PURE__ */ jsxs(
1567
+ "button",
1568
+ {
1569
+ type: "button",
1570
+ onClick: () => toggleLabel(label),
1571
+ className: cn(
1572
+ "inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] transition-colors",
1573
+ active ? "bg-primary text-primary-foreground border-primary" : "bg-muted text-muted-foreground border-border hover:bg-muted/70"
1574
+ ),
1575
+ title: active ? `Remove "${label}" from filter` : `Filter by "${label}"`,
1576
+ children: [
1577
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: label }),
1578
+ /* @__PURE__ */ jsx("span", { className: "opacity-70", children: count })
1579
+ ]
1580
+ },
1581
+ label
1582
+ );
1583
+ }),
1584
+ activeLabels.size > 0 && /* @__PURE__ */ jsxs(
1585
+ "button",
1586
+ {
1587
+ type: "button",
1588
+ onClick: clearLabels,
1589
+ className: "inline-flex items-center gap-0.5 text-[11px] text-muted-foreground hover:text-foreground",
1590
+ title: "Clear label filter",
1591
+ children: [
1592
+ /* @__PURE__ */ jsx(X, { className: "w-3 h-3" }),
1593
+ " clear"
1594
+ ]
1595
+ }
1596
+ )
1597
+ ] }),
1598
+ filteredAssets.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-muted-foreground", children: [
1397
1599
  /* @__PURE__ */ jsx(ImageOff, { className: "w-8 h-8 mb-2" }),
1398
1600
  /* @__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
- );
1601
+ (search || activeLabels.size > 0) && /* @__PURE__ */ jsx("p", { className: "text-xs mt-1", children: "Try adjusting your filters" })
1602
+ ] }) : /* @__PURE__ */ jsx(
1603
+ AssetGrid,
1604
+ {
1605
+ assets: filteredAssets,
1606
+ viewMode,
1607
+ selectedIds,
1608
+ onToggleSelect,
1609
+ onDoubleClickSelect,
1610
+ onDelete: allowDelete ? onDelete : void 0,
1611
+ allowDelete,
1612
+ currentAppId,
1613
+ currentAppName,
1614
+ getAppName,
1615
+ activeLabels,
1616
+ onToggleLabel: toggleLabel
1617
+ }
1618
+ )
1619
+ ] });
1417
1620
  };
1418
1621
  var AssetPickerContent = ({
1419
1622
  scope,
@@ -1484,6 +1687,10 @@ var AssetPickerContent = ({
1484
1687
  mimeType: asset2.mimeType,
1485
1688
  size: asset2.size,
1486
1689
  metadata: asset2.metadata,
1690
+ thumbnail: asset2.thumbnail ?? null,
1691
+ app: asset2.app ?? null,
1692
+ labels: asset2.labels,
1693
+ // Deprecated — kept for backward-compat with hosts still reading `thumbnails`.
1487
1694
  thumbnails: asset2.thumbnails
1488
1695
  }), []);
1489
1696
  const handleToggleSelect = useCallback((asset2) => {
@@ -1892,5 +2099,5 @@ var AssetPicker = (props) => {
1892
2099
  assertStylesLoaded();
1893
2100
 
1894
2101
  export { ASSET_MIME_FILTERS, AssetPicker, useAppRegistry, useAssets };
1895
- //# sourceMappingURL=chunk-ZTUZPAHD.js.map
1896
- //# sourceMappingURL=chunk-ZTUZPAHD.js.map
2102
+ //# sourceMappingURL=chunk-PQD2B6DA.js.map
2103
+ //# sourceMappingURL=chunk-PQD2B6DA.js.map