sanity-plugin-smart-asset-manager 1.0.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,1336 @@
1
+ // src/index.ts
2
+ import { definePlugin } from "sanity";
3
+
4
+ // src/components/SmartAssetManagerTool.tsx
5
+ import { useState as useState4, useMemo, useEffect as useEffect3 } from "react";
6
+ import { Box as Box6, Grid as Grid3, Text as Text5, Stack as Stack5, Flex as Flex6, Spinner as Spinner2, Card as Card6, Button as Button6, useToast as useToast2 } from "@sanity/ui";
7
+ import { useClient } from "sanity";
8
+
9
+ // src/hooks/useAssets.ts
10
+ import { useState, useEffect } from "react";
11
+ function useAssets(sanityClient, searchQuery, sortBy, type, offset, limit) {
12
+ const [assets, setAssets] = useState([]);
13
+ const [loading, setLoading] = useState(true);
14
+ const [total, setTotal] = useState(0);
15
+ const [refreshSeed, setRefreshSeed] = useState(0);
16
+ const refreshAssets = () => setRefreshSeed((s) => s + 1);
17
+ useEffect(() => {
18
+ async function fetchAssets() {
19
+ setLoading(true);
20
+ try {
21
+ let filterParts = ['_type in ["sanity.imageAsset", "sanity.fileAsset"]'];
22
+ if (type === "image") {
23
+ filterParts.push('_type == "sanity.imageAsset"');
24
+ } else if (type === "file") {
25
+ filterParts.push(
26
+ '_type == "sanity.fileAsset" && !(mimeType match "video/*") && !(mimeType match "audio/*")'
27
+ );
28
+ } else if (type === "video") {
29
+ filterParts.push('_type == "sanity.fileAsset" && mimeType match "video/*"');
30
+ } else if (type === "audio") {
31
+ filterParts.push('_type == "sanity.fileAsset" && mimeType match "audio/*"');
32
+ }
33
+ if (searchQuery) {
34
+ filterParts.push("(originalFilename match $searchQuery || _id match $searchQuery)");
35
+ }
36
+ const filter = filterParts.join(" && ");
37
+ const countQuery = `count(*[${filter}])`;
38
+ const query = `*[${filter}] | order(${sortBy} desc) [${offset}...${offset + limit}] {
39
+ _id,
40
+ _type,
41
+ url,
42
+ extension,
43
+ size,
44
+ mimeType,
45
+ originalFilename,
46
+ metadata {
47
+ dimensions
48
+ }
49
+ }`;
50
+ const [totalCount, results] = await Promise.all([
51
+ sanityClient.fetch(countQuery, { searchQuery: `*${searchQuery}*` }),
52
+ sanityClient.fetch(query, { searchQuery: `*${searchQuery}*` })
53
+ ]);
54
+ setTotal(totalCount);
55
+ setAssets(results);
56
+ } catch (error) {
57
+ console.error("Error fetching assets:", error);
58
+ } finally {
59
+ setLoading(false);
60
+ }
61
+ }
62
+ fetchAssets();
63
+ }, [sanityClient, searchQuery, sortBy, type, refreshSeed, offset, limit]);
64
+ return { assets, loading, total, refreshAssets };
65
+ }
66
+
67
+ // src/utils/assetQueries.ts
68
+ async function getAssetUsage(sanityClient, assetId) {
69
+ const query = `*[references($assetId)] {
70
+ _id,
71
+ _type,
72
+ "title": coalesce(title, name, label, _id)
73
+ }`;
74
+ return await sanityClient.fetch(query, { assetId });
75
+ }
76
+ async function findUnusedAssets(sanityClient) {
77
+ const query = `*[
78
+ _type in ["sanity.imageAsset", "sanity.fileAsset"] &&
79
+ count(*[references(^._id)]) == 0
80
+ ] {
81
+ _id,
82
+ _type,
83
+ url,
84
+ size,
85
+ extension,
86
+ mimeType,
87
+ originalFilename,
88
+ metadata { dimensions }
89
+ }`;
90
+ return await sanityClient.fetch(query);
91
+ }
92
+ async function deleteAssets(sanityClient, assetIds) {
93
+ const transaction = sanityClient.transaction();
94
+ assetIds.forEach((id) => transaction.delete(id));
95
+ return await transaction.commit();
96
+ }
97
+ async function updateAssetFilename(sanityClient, assetId, filename) {
98
+ return await sanityClient.patch(assetId).set({ originalFilename: filename }).commit();
99
+ }
100
+
101
+ // src/components/AssetCard.tsx
102
+ import { Box, Flex, Stack, Text, Badge, Button, Card, Checkbox } from "@sanity/ui";
103
+
104
+ // src/utils/formatBytes.ts
105
+ var formatBytes = (bytes) => {
106
+ if (bytes === 0) return "0 B";
107
+ const k = 1024;
108
+ const sizes = ["B", "KB", "MB", "GB"];
109
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
110
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
111
+ };
112
+
113
+ // src/components/common/Icons.tsx
114
+ import { jsx, jsxs } from "react/jsx-runtime";
115
+ var SearchIcon = (props) => /* @__PURE__ */ jsxs(
116
+ "svg",
117
+ {
118
+ width: "1em",
119
+ height: "1em",
120
+ viewBox: "0 0 24 24",
121
+ fill: "none",
122
+ stroke: "currentColor",
123
+ strokeWidth: "2",
124
+ strokeLinecap: "round",
125
+ strokeLinejoin: "round",
126
+ ...props,
127
+ children: [
128
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "8" }),
129
+ /* @__PURE__ */ jsx("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" })
130
+ ]
131
+ }
132
+ );
133
+ var TrashIcon = (props) => /* @__PURE__ */ jsxs(
134
+ "svg",
135
+ {
136
+ width: "1em",
137
+ height: "1em",
138
+ viewBox: "0 0 24 24",
139
+ fill: "none",
140
+ stroke: "currentColor",
141
+ strokeWidth: "2",
142
+ strokeLinecap: "round",
143
+ strokeLinejoin: "round",
144
+ ...props,
145
+ children: [
146
+ /* @__PURE__ */ jsx("polyline", { points: "3 6 5 6 21 6" }),
147
+ /* @__PURE__ */ jsx("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" }),
148
+ /* @__PURE__ */ jsx("line", { x1: "10", y1: "11", x2: "10", y2: "17" }),
149
+ /* @__PURE__ */ jsx("line", { x1: "14", y1: "11", x2: "14", y2: "17" })
150
+ ]
151
+ }
152
+ );
153
+ var ResetIcon = (props) => /* @__PURE__ */ jsxs(
154
+ "svg",
155
+ {
156
+ width: "1em",
157
+ height: "1em",
158
+ viewBox: "0 0 24 24",
159
+ fill: "none",
160
+ stroke: "currentColor",
161
+ strokeWidth: "2",
162
+ strokeLinecap: "round",
163
+ strokeLinejoin: "round",
164
+ ...props,
165
+ children: [
166
+ /* @__PURE__ */ jsx("path", { d: "M1 4v6h6" }),
167
+ /* @__PURE__ */ jsx("path", { d: "M3.51 15a9 9 0 1 0 2.13-9.36L1 10" })
168
+ ]
169
+ }
170
+ );
171
+ var DocumentsIcon = (props) => /* @__PURE__ */ jsxs(
172
+ "svg",
173
+ {
174
+ width: "1em",
175
+ height: "1em",
176
+ viewBox: "0 0 24 24",
177
+ fill: "none",
178
+ stroke: "currentColor",
179
+ strokeWidth: "2",
180
+ strokeLinecap: "round",
181
+ strokeLinejoin: "round",
182
+ ...props,
183
+ children: [
184
+ /* @__PURE__ */ jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
185
+ /* @__PURE__ */ jsx("polyline", { points: "14 2 14 8 20 8" }),
186
+ /* @__PURE__ */ jsx("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
187
+ /* @__PURE__ */ jsx("line", { x1: "16", y1: "17", x2: "8", y2: "17" }),
188
+ /* @__PURE__ */ jsx("polyline", { points: "10 9 9 9 8 9" })
189
+ ]
190
+ }
191
+ );
192
+ var DownloadIcon = (props) => /* @__PURE__ */ jsxs(
193
+ "svg",
194
+ {
195
+ width: "1em",
196
+ height: "1em",
197
+ viewBox: "0 0 24 24",
198
+ fill: "none",
199
+ stroke: "currentColor",
200
+ strokeWidth: "2",
201
+ strokeLinecap: "round",
202
+ strokeLinejoin: "round",
203
+ ...props,
204
+ children: [
205
+ /* @__PURE__ */ jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
206
+ /* @__PURE__ */ jsx("polyline", { points: "7 10 12 15 17 10" }),
207
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "15", x2: "12", y2: "3" })
208
+ ]
209
+ }
210
+ );
211
+ var FolderIcon = (props) => /* @__PURE__ */ jsx(
212
+ "svg",
213
+ {
214
+ width: "1em",
215
+ height: "1em",
216
+ viewBox: "0 0 24 24",
217
+ fill: "none",
218
+ stroke: "currentColor",
219
+ strokeWidth: "2",
220
+ strokeLinecap: "round",
221
+ strokeLinejoin: "round",
222
+ ...props,
223
+ children: /* @__PURE__ */ jsx("path", { d: "M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" })
224
+ }
225
+ );
226
+ var UploadIcon = (props) => /* @__PURE__ */ jsxs(
227
+ "svg",
228
+ {
229
+ width: "1em",
230
+ height: "1em",
231
+ viewBox: "0 0 24 24",
232
+ fill: "none",
233
+ stroke: "currentColor",
234
+ strokeWidth: "2",
235
+ strokeLinecap: "round",
236
+ strokeLinejoin: "round",
237
+ ...props,
238
+ children: [
239
+ /* @__PURE__ */ jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
240
+ /* @__PURE__ */ jsx("polyline", { points: "17 8 12 3 7 8" }),
241
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "3", x2: "12", y2: "15" })
242
+ ]
243
+ }
244
+ );
245
+ var PdfIcon = (props) => /* @__PURE__ */ jsxs(
246
+ "svg",
247
+ {
248
+ width: "1em",
249
+ height: "1em",
250
+ viewBox: "0 0 24 24",
251
+ fill: "none",
252
+ stroke: "currentColor",
253
+ strokeWidth: "2",
254
+ strokeLinecap: "round",
255
+ strokeLinejoin: "round",
256
+ ...props,
257
+ children: [
258
+ /* @__PURE__ */ jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
259
+ /* @__PURE__ */ jsx("polyline", { points: "14 2 14 8 20 8" }),
260
+ /* @__PURE__ */ jsx("path", { d: "M9 15h3a2 2 0 0 0 2-2V13a2 2 0 0 0-2-2H9v4z" }),
261
+ /* @__PURE__ */ jsx("line", { x1: "9", y1: "11", x2: "9", y2: "17" })
262
+ ]
263
+ }
264
+ );
265
+ var AudioIcon = (props) => /* @__PURE__ */ jsxs(
266
+ "svg",
267
+ {
268
+ width: "1em",
269
+ height: "1em",
270
+ viewBox: "0 0 24 24",
271
+ fill: "none",
272
+ stroke: "currentColor",
273
+ strokeWidth: "2",
274
+ strokeLinecap: "round",
275
+ strokeLinejoin: "round",
276
+ ...props,
277
+ children: [
278
+ /* @__PURE__ */ jsx("path", { d: "M9 18V5l12-2v13" }),
279
+ /* @__PURE__ */ jsx("circle", { cx: "6", cy: "18", r: "3" }),
280
+ /* @__PURE__ */ jsx("circle", { cx: "18", cy: "16", r: "3" })
281
+ ]
282
+ }
283
+ );
284
+ var SortIcon = (props) => /* @__PURE__ */ jsxs(
285
+ "svg",
286
+ {
287
+ width: "1em",
288
+ height: "1em",
289
+ viewBox: "0 0 24 24",
290
+ fill: "none",
291
+ stroke: "currentColor",
292
+ strokeWidth: "2",
293
+ strokeLinecap: "round",
294
+ strokeLinejoin: "round",
295
+ ...props,
296
+ children: [
297
+ /* @__PURE__ */ jsx("line", { x1: "11", y1: "5", x2: "21", y2: "5" }),
298
+ /* @__PURE__ */ jsx("line", { x1: "11", y1: "9", x2: "18", y2: "9" }),
299
+ /* @__PURE__ */ jsx("line", { x1: "11", y1: "13", x2: "15", y2: "13" }),
300
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "17", x2: "21", y2: "17" }),
301
+ /* @__PURE__ */ jsx("polyline", { points: "7 5 3 9 7 13" }),
302
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "9", x2: "3", y2: "1" })
303
+ ]
304
+ }
305
+ );
306
+
307
+ // src/components/AssetCard.tsx
308
+ import styled from "styled-components";
309
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
310
+ var StyledCard = styled(Card)`
311
+ overflow: hidden;
312
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
313
+ cursor: pointer;
314
+ position: relative;
315
+ border: 1px solid
316
+ ${(props) => props.selected ? "var(--card-focus-ring-color)" : "var(--card-border-color)"};
317
+ background-color: ${(props) => props.selected ? "var(--card-accent-bg-color)" : "inherit"};
318
+
319
+ &:hover {
320
+ transform: translateY(-4px);
321
+ box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08);
322
+ border-color: var(--card-focus-ring-color);
323
+ }
324
+ `;
325
+ var ImageContainer = styled(Box)`
326
+ width: 100%;
327
+ aspect-ratio: 1.25;
328
+ background-color: transparent;
329
+ position: relative;
330
+ display: flex;
331
+ align-items: center;
332
+ justify-content: center;
333
+ border-bottom: 1px solid var(--card-border-color);
334
+ overflow: hidden;
335
+ `;
336
+ var SelectOverlay = styled(Box)`
337
+ position: absolute;
338
+ top: 8px;
339
+ left: 8px;
340
+ z-index: 5;
341
+ `;
342
+ var AssetPreview = styled.img`
343
+ width: 100%;
344
+ height: 100%;
345
+ object-fit: cover;
346
+ transition: transform 0.3s ease;
347
+
348
+ ${StyledCard}:hover & {
349
+ transform: scale(1.05);
350
+ }
351
+ `;
352
+ var VideoPreview = styled.video`
353
+ width: 100%;
354
+ height: 100%;
355
+ object-fit: cover;
356
+ background: #000;
357
+ `;
358
+ var ActionBar = styled(Flex)`
359
+ position: absolute;
360
+ bottom: 8px;
361
+ right: 8px;
362
+ background: var(--card-bg-color);
363
+ opacity: 0.95;
364
+ backdrop-filter: blur(4px);
365
+ border-radius: 8px;
366
+ padding: 2px;
367
+ opacity: 0;
368
+ border: 1px solid var(--card-border-color);
369
+ transition: opacity 0.2s ease;
370
+
371
+ ${StyledCard}:hover & {
372
+ opacity: 1;
373
+ }
374
+ `;
375
+ var AssetCard = ({ asset, onClick, isSelected, onSelect }) => {
376
+ const isImage = asset._type === "sanity.imageAsset";
377
+ const isVideo = asset?.mimeType?.startsWith("video/");
378
+ const isAudio = asset?.mimeType?.startsWith("audio/");
379
+ const isPdf = asset?.extension === "pdf" || asset?.mimeType === "application/pdf";
380
+ const handleCardClick = (e) => {
381
+ if (e.target.closest("button")) return;
382
+ onClick(asset);
383
+ };
384
+ return /* @__PURE__ */ jsxs2(StyledCard, { radius: 3, onClick: handleCardClick, selected: isSelected, children: [
385
+ /* @__PURE__ */ jsxs2(ImageContainer, { children: [
386
+ onSelect && /* @__PURE__ */ jsx2(SelectOverlay, { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx2(
387
+ Checkbox,
388
+ {
389
+ checked: isSelected,
390
+ onChange: (e) => onSelect(asset._id, e.target.checked)
391
+ }
392
+ ) }),
393
+ isImage ? /* @__PURE__ */ jsx2(
394
+ AssetPreview,
395
+ {
396
+ src: `${asset.url}?w=400&h=320&fit=crop`,
397
+ alt: asset.originalFilename || ""
398
+ }
399
+ ) : isVideo ? /* @__PURE__ */ jsx2(VideoPreview, { src: asset.url, muted: true, playsInline: true }) : /* @__PURE__ */ jsxs2(
400
+ Flex,
401
+ {
402
+ direction: "column",
403
+ align: "center",
404
+ justify: "center",
405
+ gap: 3,
406
+ style: { height: "100%", width: "100%" },
407
+ children: [
408
+ isPdf ? /* @__PURE__ */ jsx2(
409
+ PdfIcon,
410
+ {
411
+ style: {
412
+ fontSize: "48px",
413
+ color: "#ef4444"
414
+ }
415
+ }
416
+ ) : isAudio ? /* @__PURE__ */ jsx2(
417
+ AudioIcon,
418
+ {
419
+ style: {
420
+ fontSize: "48px",
421
+ color: "#ef4444"
422
+ }
423
+ }
424
+ ) : /* @__PURE__ */ jsx2(
425
+ DocumentsIcon,
426
+ {
427
+ style: {
428
+ fontSize: "48px",
429
+ color: "#9ca3af"
430
+ }
431
+ }
432
+ ),
433
+ /* @__PURE__ */ jsx2(Text, { size: 1, weight: "bold", muted: true, children: (asset.extension || asset.mimeType?.split("/")[1])?.toUpperCase() || "OTHER" })
434
+ ]
435
+ }
436
+ ),
437
+ /* @__PURE__ */ jsx2(ActionBar, { gap: 1, children: /* @__PURE__ */ jsx2(
438
+ Button,
439
+ {
440
+ icon: DownloadIcon,
441
+ mode: "bleed",
442
+ fontSize: 1,
443
+ padding: 2,
444
+ onClick: (e) => {
445
+ e.stopPropagation();
446
+ window.open(asset.url);
447
+ }
448
+ }
449
+ ) })
450
+ ] }),
451
+ /* @__PURE__ */ jsx2(Box, { padding: 3, children: /* @__PURE__ */ jsx2(Stack, { space: 3, children: /* @__PURE__ */ jsxs2(Flex, { align: "center", direction: "column", gap: 2, children: [
452
+ /* @__PURE__ */ jsx2(Text, { size: 1, weight: "semibold", textOverflow: "ellipsis", style: { flex: 1 }, children: asset.originalFilename || asset._id.substring(0, 10) }),
453
+ /* @__PURE__ */ jsx2(Badge, { tone: asset.size > 1048576 ? "caution" : "default", fontSize: 0, children: formatBytes(asset.size) })
454
+ ] }) }) })
455
+ ] });
456
+ };
457
+
458
+ // src/components/AssetDetailsDialog.tsx
459
+ import { useState as useState2, useEffect as useEffect2 } from "react";
460
+ import {
461
+ Box as Box2,
462
+ Flex as Flex2,
463
+ Stack as Stack2,
464
+ Text as Text2,
465
+ Badge as Badge2,
466
+ Button as Button2,
467
+ Dialog,
468
+ TextInput,
469
+ Label,
470
+ Card as Card2,
471
+ Grid,
472
+ Spinner,
473
+ useToast,
474
+ Tooltip
475
+ } from "@sanity/ui";
476
+ import { useRouter } from "sanity/router";
477
+ import styled2 from "styled-components";
478
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
479
+ var PreviewContainer = styled2(Box2)`
480
+ width: 100%;
481
+ border-radius: 8px;
482
+ overflow: hidden;
483
+ display: flex;
484
+ align-items: center;
485
+ justify-content: center;
486
+ min-height: 200px;
487
+ margin-bottom: 16px;
488
+ border: 1px solid var(--card-border-color);
489
+ `;
490
+ var PreviewImage = styled2.img`
491
+ max-width: 100%;
492
+ max-height: 400px;
493
+ object-fit: contain;
494
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
495
+ `;
496
+ var VideoPreview2 = styled2.video`
497
+ max-width: 100%;
498
+ max-height: 400px;
499
+ object-fit: contain;
500
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
501
+ background: #000;
502
+ `;
503
+ var PdfPreview = styled2.iframe`
504
+ width: 100%;
505
+ height: 400px;
506
+ border: none;
507
+ border-radius: 4px;
508
+ `;
509
+ var AudioPreview = styled2.audio`
510
+ width: 100%;
511
+ margin-top: 16px;
512
+ `;
513
+ var UsageCard = styled2(Card2)`
514
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
515
+ cursor: pointer;
516
+ border: 1px solid var(--card-border-color);
517
+ &:hover {
518
+ background: var(--card-bg-color);
519
+ border-color: var(--card-focus-ring-color);
520
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
521
+ }
522
+ `;
523
+ var ViewText = styled2(Text2)`
524
+ transition: all 0.2s ease;
525
+ color: var(--card-focus-ring-color);
526
+ `;
527
+ var AssetDetailsDialog = ({
528
+ asset,
529
+ onClose,
530
+ sanityClient,
531
+ onUpdate,
532
+ onDelete
533
+ }) => {
534
+ const [newName, setNewName] = useState2(asset.originalFilename || "");
535
+ const [isUpdating, setIsUpdating] = useState2(false);
536
+ const [isDeleting, setIsDeleting] = useState2(false);
537
+ const [confirmDelete, setConfirmDelete] = useState2(false);
538
+ const [usage, setUsage] = useState2([]);
539
+ const [loadingUsage, setLoadingUsage] = useState2(true);
540
+ const toast = useToast();
541
+ const router = useRouter();
542
+ useEffect2(() => {
543
+ async function fetchUsage() {
544
+ setLoadingUsage(true);
545
+ try {
546
+ const results = await getAssetUsage(sanityClient, asset._id);
547
+ setUsage(results);
548
+ } catch (err) {
549
+ console.error(err);
550
+ } finally {
551
+ setLoadingUsage(false);
552
+ }
553
+ }
554
+ fetchUsage();
555
+ }, [asset._id, sanityClient]);
556
+ const handleUpdate = async () => {
557
+ if (!newName.trim()) return;
558
+ setIsUpdating(true);
559
+ try {
560
+ await updateAssetFilename(sanityClient, asset._id, newName);
561
+ onUpdate(asset._id, newName);
562
+ toast.push({
563
+ status: "success",
564
+ title: "Filename updated"
565
+ });
566
+ } catch (err) {
567
+ toast.push({
568
+ status: "error",
569
+ title: "Failed to update filename"
570
+ });
571
+ } finally {
572
+ setIsUpdating(false);
573
+ }
574
+ };
575
+ const handleDelete = async () => {
576
+ setIsDeleting(true);
577
+ try {
578
+ onDelete(asset._id);
579
+ onClose();
580
+ toast.push({
581
+ status: "success",
582
+ title: "Asset deleted"
583
+ });
584
+ } catch (err) {
585
+ toast.push({
586
+ status: "error",
587
+ title: "Failed to delete asset"
588
+ });
589
+ } finally {
590
+ setIsDeleting(false);
591
+ setConfirmDelete(false);
592
+ }
593
+ };
594
+ const handleDocClick = (docId, docType) => {
595
+ if (router && typeof router.navigateIntent === "function") {
596
+ router.navigateIntent("edit", { id: docId, type: docType });
597
+ onClose();
598
+ } else {
599
+ window.location.hash = `intent/edit/id=${docId};type=${docType}`;
600
+ onClose();
601
+ }
602
+ };
603
+ const isImage = asset._type === "sanity.imageAsset";
604
+ const isVideo = asset?.mimeType?.startsWith("video/");
605
+ const isAudio = asset?.mimeType?.startsWith("audio/");
606
+ const isPdf = asset?.extension === "pdf" || asset?.mimeType === "application/pdf";
607
+ return /* @__PURE__ */ jsx3(
608
+ Dialog,
609
+ {
610
+ id: "asset-details-dialog",
611
+ header: "Asset Details",
612
+ onClose,
613
+ width: 2,
614
+ footer: /* @__PURE__ */ jsx3(Box2, { padding: 3, children: /* @__PURE__ */ jsxs3(Flex2, { justify: "space-between", align: "center", children: [
615
+ /* @__PURE__ */ jsx3(
616
+ Tooltip,
617
+ {
618
+ content: /* @__PURE__ */ jsx3(Box2, { padding: 2, style: { maxWidth: "350px" }, children: /* @__PURE__ */ jsx3(Text2, { size: 1, children: usage.length > 0 ? "This asset is being used by documents and cannot be deleted. Remove all references first." : "Delete this asset permanently" }) }),
619
+ placement: "top",
620
+ portal: true,
621
+ children: /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsx3(
622
+ Button2,
623
+ {
624
+ icon: TrashIcon,
625
+ text: "Delete Asset",
626
+ tone: "critical",
627
+ mode: "ghost",
628
+ onClick: () => setConfirmDelete(true),
629
+ disabled: usage.length > 0 || loadingUsage
630
+ }
631
+ ) })
632
+ }
633
+ ),
634
+ /* @__PURE__ */ jsx3(Flex2, { gap: 2, children: /* @__PURE__ */ jsx3(Button2, { text: "Close", mode: "ghost", onClick: onClose }) })
635
+ ] }) }),
636
+ children: /* @__PURE__ */ jsxs3(Box2, { padding: 4, children: [
637
+ confirmDelete && /* @__PURE__ */ jsx3(
638
+ Dialog,
639
+ {
640
+ id: "confirm-asset-delete",
641
+ header: "Delete Asset?",
642
+ onClose: () => setConfirmDelete(false),
643
+ footer: /* @__PURE__ */ jsx3(Box2, { padding: 3, children: /* @__PURE__ */ jsxs3(Flex2, { gap: 2, justify: "flex-end", children: [
644
+ /* @__PURE__ */ jsx3(Button2, { text: "Cancel", mode: "ghost", onClick: () => setConfirmDelete(false) }),
645
+ /* @__PURE__ */ jsx3(
646
+ Button2,
647
+ {
648
+ text: "Delete Permanently",
649
+ tone: "critical",
650
+ onClick: handleDelete,
651
+ loading: isDeleting
652
+ }
653
+ )
654
+ ] }) }),
655
+ children: /* @__PURE__ */ jsx3(Box2, { padding: 4, children: /* @__PURE__ */ jsxs3(Text2, { children: [
656
+ "Are you sure you want to delete",
657
+ " ",
658
+ /* @__PURE__ */ jsx3("strong", { children: asset.originalFilename || "this asset" }),
659
+ "? This action cannot be undone."
660
+ ] }) })
661
+ }
662
+ ),
663
+ /* @__PURE__ */ jsxs3(Grid, { columns: [1, 1, 2], gap: 4, children: [
664
+ /* @__PURE__ */ jsxs3(Stack2, { space: 4, children: [
665
+ /* @__PURE__ */ jsx3(PreviewContainer, { children: isImage ? /* @__PURE__ */ jsx3(PreviewImage, { src: `${asset.url}?w=800`, alt: "" }) : isVideo ? /* @__PURE__ */ jsx3(VideoPreview2, { src: asset.url, controls: true, muted: true, playsInline: true }) : isPdf ? /* @__PURE__ */ jsx3(PdfPreview, { src: asset.url, title: "PDF Preview" }) : isAudio ? /* @__PURE__ */ jsxs3(Flex2, { direction: "column", align: "center", gap: 4, style: { width: "100%" }, children: [
666
+ /* @__PURE__ */ jsx3(AudioIcon, { style: { fontSize: "64px", color: "#ef4444" } }),
667
+ /* @__PURE__ */ jsx3(AudioPreview, { src: asset.url, controls: true })
668
+ ] }) : /* @__PURE__ */ jsx3(
669
+ Flex2,
670
+ {
671
+ direction: "column",
672
+ align: "center",
673
+ justify: "center",
674
+ gap: 3,
675
+ style: { height: "300px", width: "100%" },
676
+ children: isPdf ? /* @__PURE__ */ jsx3(PdfIcon, { style: { fontSize: "80px", color: "#ef4444" } }) : isAudio ? /* @__PURE__ */ jsx3(AudioIcon, { style: { fontSize: "80px", color: "#ef4444" } }) : /* @__PURE__ */ jsx3(
677
+ DocumentsIcon,
678
+ {
679
+ style: {
680
+ fontSize: "80px",
681
+ color: "#9ca3af"
682
+ }
683
+ }
684
+ )
685
+ }
686
+ ) }),
687
+ /* @__PURE__ */ jsxs3(Stack2, { space: 3, children: [
688
+ /* @__PURE__ */ jsxs3(Box2, { children: [
689
+ /* @__PURE__ */ jsx3(Label, { size: 1, children: "Original Filename" }),
690
+ /* @__PURE__ */ jsxs3(Flex2, { gap: 2, marginTop: 2, children: [
691
+ /* @__PURE__ */ jsx3(Box2, { style: { flex: 1 }, children: /* @__PURE__ */ jsx3(
692
+ TextInput,
693
+ {
694
+ value: newName,
695
+ onChange: (e) => setNewName(e.currentTarget.value),
696
+ placeholder: "Enter filename..."
697
+ }
698
+ ) }),
699
+ /* @__PURE__ */ jsx3(
700
+ Button2,
701
+ {
702
+ text: "Save",
703
+ tone: "primary",
704
+ onClick: handleUpdate,
705
+ loading: isUpdating,
706
+ disabled: newName === asset.originalFilename
707
+ }
708
+ )
709
+ ] })
710
+ ] }),
711
+ /* @__PURE__ */ jsx3(Card2, { padding: 3, border: true, radius: 2, children: /* @__PURE__ */ jsxs3(Stack2, { space: 3, children: [
712
+ /* @__PURE__ */ jsx3(Text2, { weight: "bold", size: 1, children: "File Information" }),
713
+ /* @__PURE__ */ jsxs3(Stack2, { space: 3, children: [
714
+ /* @__PURE__ */ jsxs3(Flex2, { align: "flex-start", gap: 3, children: [
715
+ /* @__PURE__ */ jsx3(Box2, { style: { minWidth: "85px" }, children: /* @__PURE__ */ jsx3(Text2, { size: 1, muted: true, children: "ID:" }) }),
716
+ /* @__PURE__ */ jsx3(Box2, { flex: 1, children: /* @__PURE__ */ jsx3(Text2, { size: 1, style: { wordBreak: "break-all" }, children: asset._id }) })
717
+ ] }),
718
+ /* @__PURE__ */ jsxs3(Flex2, { align: "center", gap: 3, children: [
719
+ /* @__PURE__ */ jsx3(Box2, { style: { minWidth: "85px" }, children: /* @__PURE__ */ jsx3(Text2, { size: 1, muted: true, children: "Type:" }) }),
720
+ /* @__PURE__ */ jsx3(Box2, { flex: 1, children: /* @__PURE__ */ jsx3(Badge2, { tone: "primary", mode: "outline", fontSize: 0, children: (asset.extension || asset.mimeType?.split("/")[1])?.toUpperCase() || "OTHER" }) })
721
+ ] }),
722
+ /* @__PURE__ */ jsxs3(Flex2, { align: "center", gap: 3, children: [
723
+ /* @__PURE__ */ jsx3(Box2, { style: { minWidth: "85px" }, children: /* @__PURE__ */ jsx3(Text2, { size: 1, muted: true, children: "Size:" }) }),
724
+ /* @__PURE__ */ jsx3(Box2, { flex: 1, children: /* @__PURE__ */ jsx3(Text2, { size: 1, children: formatBytes(asset.size) }) })
725
+ ] }),
726
+ asset.metadata?.dimensions && /* @__PURE__ */ jsxs3(Flex2, { align: "center", gap: 3, children: [
727
+ /* @__PURE__ */ jsx3(Box2, { style: { minWidth: "85px" }, children: /* @__PURE__ */ jsx3(Text2, { size: 1, muted: true, children: "Dimensions:" }) }),
728
+ /* @__PURE__ */ jsx3(Box2, { flex: 1, children: /* @__PURE__ */ jsxs3(Text2, { size: 1, children: [
729
+ asset.metadata.dimensions.width,
730
+ " \xD7 ",
731
+ asset.metadata.dimensions.height,
732
+ " ",
733
+ "px"
734
+ ] }) })
735
+ ] })
736
+ ] }),
737
+ /* @__PURE__ */ jsx3(
738
+ Button2,
739
+ {
740
+ icon: DownloadIcon,
741
+ text: "Download File",
742
+ mode: "ghost",
743
+ onClick: () => window.open(asset.url)
744
+ }
745
+ )
746
+ ] }) })
747
+ ] })
748
+ ] }),
749
+ /* @__PURE__ */ jsxs3(Stack2, { space: 4, children: [
750
+ /* @__PURE__ */ jsx3(Text2, { weight: "bold", size: 1, children: "Usage in Documents" }),
751
+ loadingUsage ? /* @__PURE__ */ jsx3(Flex2, { align: "center", justify: "center", style: { padding: "40px" }, children: /* @__PURE__ */ jsx3(Spinner, {}) }) : usage?.length > 0 ? /* @__PURE__ */ jsx3(Stack2, { space: 2, style: { maxHeight: "500px", overflowY: "auto" }, children: usage?.map((doc) => /* @__PURE__ */ jsx3(
752
+ UsageCard,
753
+ {
754
+ padding: 3,
755
+ border: true,
756
+ radius: 2,
757
+ onClick: () => handleDocClick(doc._id, doc._type),
758
+ children: /* @__PURE__ */ jsxs3(Flex2, { align: "center", justify: "space-between", gap: 3, children: [
759
+ /* @__PURE__ */ jsxs3(Flex2, { direction: "column", align: "flex-start", gap: 2, children: [
760
+ /* @__PURE__ */ jsx3(Text2, { size: 1, weight: "semibold", children: doc.title }),
761
+ /* @__PURE__ */ jsx3(Badge2, { tone: "caution", fontSize: 0, children: doc._type })
762
+ ] }),
763
+ /* @__PURE__ */ jsx3(Flex2, { align: "center", gap: 3, children: /* @__PURE__ */ jsx3(ViewText, { size: 0, weight: "bold", className: "view-text", children: "VIEW \u2192" }) })
764
+ ] })
765
+ },
766
+ doc._id
767
+ )) }) : /* @__PURE__ */ jsx3(Card2, { padding: 4, border: true, radius: 2, tone: "transparent", style: { textAlign: "center" }, children: /* @__PURE__ */ jsx3(Text2, { size: 1, muted: true, children: "No references found for this asset." }) })
768
+ ] })
769
+ ] })
770
+ ] })
771
+ }
772
+ );
773
+ };
774
+
775
+ // src/components/TopToolbar.tsx
776
+ import React2 from "react";
777
+ import { Flex as Flex3, Box as Box3, TextInput as TextInput2, Select, Button as Button3, Card as Card3 } from "@sanity/ui";
778
+ import styled3 from "styled-components";
779
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
780
+ var StickyCard = styled3(Card3)`
781
+ position: sticky;
782
+ top: 0;
783
+ z-index: 10;
784
+ `;
785
+ var SearchBox = styled3(Box3)`
786
+ flex: 1;
787
+ `;
788
+ var FilterBox = styled3(Box3)`
789
+ width: 140px;
790
+ `;
791
+ var TopToolbar = ({
792
+ searchQuery,
793
+ setSearchQuery,
794
+ sortBy,
795
+ setSortBy,
796
+ assetType,
797
+ setAssetType,
798
+ sizeFilter,
799
+ setSizeFilter,
800
+ onReset,
801
+ onUpload,
802
+ isUploading
803
+ }) => {
804
+ const fileInputRef = React2.useRef(null);
805
+ const handleUploadClick = () => {
806
+ fileInputRef.current?.click();
807
+ };
808
+ const handleFileChange = (e) => {
809
+ if (e.target.files && e.target.files.length > 0) {
810
+ onUpload(e.target.files);
811
+ e.target.value = "";
812
+ }
813
+ };
814
+ return /* @__PURE__ */ jsx4(StickyCard, { padding: 3, borderBottom: true, children: /* @__PURE__ */ jsxs4(Flex3, { align: "center", gap: 3, children: [
815
+ /* @__PURE__ */ jsx4(
816
+ "input",
817
+ {
818
+ type: "file",
819
+ ref: fileInputRef,
820
+ style: { display: "none" },
821
+ onChange: handleFileChange,
822
+ multiple: true,
823
+ accept: "image/*,video/*,audio/*,application/*,text/*"
824
+ }
825
+ ),
826
+ /* @__PURE__ */ jsx4(SearchBox, { children: /* @__PURE__ */ jsx4(
827
+ TextInput2,
828
+ {
829
+ icon: SearchIcon,
830
+ placeholder: "Search name, extension, ID...",
831
+ value: searchQuery,
832
+ onChange: (e) => setSearchQuery(e.currentTarget.value),
833
+ fontSize: 1
834
+ }
835
+ ) }),
836
+ /* @__PURE__ */ jsx4(Box3, { children: /* @__PURE__ */ jsxs4(
837
+ Select,
838
+ {
839
+ fontSize: 1,
840
+ value: sortBy,
841
+ onChange: (e) => setSortBy(e.currentTarget.value),
842
+ children: [
843
+ /* @__PURE__ */ jsx4("option", { value: "_createdAt", children: "Upload Date" }),
844
+ /* @__PURE__ */ jsx4("option", { value: "size", children: "File Size" }),
845
+ /* @__PURE__ */ jsx4("option", { value: "originalFilename", children: "Name A-Z" })
846
+ ]
847
+ }
848
+ ) }),
849
+ /* @__PURE__ */ jsx4(FilterBox, { children: /* @__PURE__ */ jsxs4(
850
+ Select,
851
+ {
852
+ fontSize: 1,
853
+ value: assetType,
854
+ onChange: (e) => setAssetType(e.currentTarget.value),
855
+ children: [
856
+ /* @__PURE__ */ jsx4("option", { value: "all", children: "All Types" }),
857
+ /* @__PURE__ */ jsx4("option", { value: "image", children: "Images" }),
858
+ /* @__PURE__ */ jsx4("option", { value: "video", children: "Videos" }),
859
+ /* @__PURE__ */ jsx4("option", { value: "audio", children: "Audio" }),
860
+ /* @__PURE__ */ jsx4("option", { value: "file", children: "Other Files" })
861
+ ]
862
+ }
863
+ ) }),
864
+ /* @__PURE__ */ jsx4(FilterBox, { children: /* @__PURE__ */ jsxs4(
865
+ Select,
866
+ {
867
+ fontSize: 1,
868
+ value: sizeFilter,
869
+ onChange: (e) => setSizeFilter(e.currentTarget.value),
870
+ children: [
871
+ /* @__PURE__ */ jsx4("option", { value: "all", children: "All Sizes" }),
872
+ /* @__PURE__ */ jsx4("option", { value: "small", children: "< 100 KB" }),
873
+ /* @__PURE__ */ jsx4("option", { value: "medium", children: "100KB - 1MB" }),
874
+ /* @__PURE__ */ jsx4("option", { value: "large", children: "> 1MB" })
875
+ ]
876
+ }
877
+ ) }),
878
+ /* @__PURE__ */ jsx4(Button3, { icon: ResetIcon, mode: "ghost", onClick: onReset, title: "Reset filters" }),
879
+ /* @__PURE__ */ jsx4(
880
+ Button3,
881
+ {
882
+ icon: UploadIcon,
883
+ text: isUploading ? "Uploading..." : "Upload",
884
+ tone: "primary",
885
+ onClick: handleUploadClick,
886
+ loading: isUploading
887
+ }
888
+ )
889
+ ] }) });
890
+ };
891
+
892
+ // src/components/tabs/SizeAnalyzer.tsx
893
+ import React3 from "react";
894
+ import { Stack as Stack3, Heading, Card as Card4, Box as Box4, Flex as Flex4, Text as Text3, Badge as Badge3, Button as Button4 } from "@sanity/ui";
895
+ import styled4 from "styled-components";
896
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
897
+ var TableCard = styled4(Card4)`
898
+ border: 1px solid var(--card-border-color);
899
+ `;
900
+ var TableHeader = styled4(Box4)`
901
+ border-bottom: 1px solid var(--card-border-color);
902
+ `;
903
+ var TableRow = styled4(Flex4)`
904
+ border-bottom: ${(props) => props.$isLast ? "none" : "1px solid var(--card-border-color)"};
905
+ transition: background 0.2s ease;
906
+ &:hover {
907
+ background: var(--card-bg-color);
908
+ filter: brightness(0.95);
909
+ }
910
+ `;
911
+ var Thumbnail = styled4.img`
912
+ width: 40px;
913
+ height: 40px;
914
+ object-fit: cover;
915
+ border-radius: 4px;
916
+ background-color: var(--card-border-color);
917
+ `;
918
+ var SizeAnalyzer = ({ assets }) => {
919
+ const [sortOrder, setSortOrder] = React3.useState("desc");
920
+ const sortedAssets = React3.useMemo(() => {
921
+ return [...assets].sort((a, b) => {
922
+ return sortOrder === "desc" ? b.size - a.size : a.size - b.size;
923
+ });
924
+ }, [assets, sortOrder]);
925
+ const toggleSort = () => {
926
+ setSortOrder((prev) => prev === "desc" ? "asc" : "desc");
927
+ };
928
+ return /* @__PURE__ */ jsxs5(Stack3, { space: 4, children: [
929
+ /* @__PURE__ */ jsx5(Heading, { size: 1, children: "Image Size Analysis" }),
930
+ /* @__PURE__ */ jsxs5(TableCard, { border: true, radius: 2, children: [
931
+ /* @__PURE__ */ jsx5(TableHeader, { padding: 2, children: /* @__PURE__ */ jsxs5(Flex4, { align: "center", gap: 3, children: [
932
+ /* @__PURE__ */ jsx5(Box4, { style: { flex: 1 }, paddingLeft: 3, children: /* @__PURE__ */ jsx5(Text3, { size: 1, weight: "bold", children: "File" }) }),
933
+ /* @__PURE__ */ jsx5(Box4, { style: { width: "120px" }, children: /* @__PURE__ */ jsx5(Text3, { size: 1, weight: "bold", children: "Dimensions" }) }),
934
+ /* @__PURE__ */ jsx5(Box4, { style: { width: "140px" }, children: /* @__PURE__ */ jsxs5(Flex4, { align: "center", gap: 1, children: [
935
+ /* @__PURE__ */ jsx5(Text3, { size: 1, weight: "bold", children: "Weight" }),
936
+ /* @__PURE__ */ jsx5(
937
+ Button4,
938
+ {
939
+ icon: SortIcon,
940
+ mode: "bleed",
941
+ padding: 2,
942
+ onClick: toggleSort,
943
+ fontSize: 1,
944
+ title: `Sort by weight ${sortOrder === "desc" ? "Ascending" : "Descending"}`,
945
+ style: { transform: sortOrder === "asc" ? "rotate(180deg)" : "none" }
946
+ }
947
+ )
948
+ ] }) })
949
+ ] }) }),
950
+ sortedAssets.slice(0, 50).map((asset, i) => /* @__PURE__ */ jsxs5(
951
+ TableRow,
952
+ {
953
+ padding: 3,
954
+ align: "center",
955
+ gap: 3,
956
+ $isLast: i === (sortedAssets.length < 50 ? sortedAssets.length - 1 : 49),
957
+ children: [
958
+ asset._type === "sanity.imageAsset" ? /* @__PURE__ */ jsx5(Thumbnail, { src: `${asset.url}?w=40&h=40&fit=crop`, alt: "" }) : /* @__PURE__ */ jsx5(
959
+ Box4,
960
+ {
961
+ style: {
962
+ width: "40px",
963
+ height: "40px",
964
+ display: "flex",
965
+ alignItems: "center",
966
+ justifyContent: "center",
967
+ background: "var(--card-border-color)",
968
+ borderRadius: "4px"
969
+ },
970
+ children: asset.mimeType?.startsWith("video/") ? /* @__PURE__ */ jsx5(DocumentsIcon, { style: { fontSize: "20px", color: "#3b82f6" } }) : asset.mimeType?.startsWith("audio/") ? /* @__PURE__ */ jsx5(AudioIcon, { style: { fontSize: "20px", color: "#ef4444" } }) : asset.extension === "pdf" || asset.mimeType === "application/pdf" ? /* @__PURE__ */ jsx5(PdfIcon, { style: { fontSize: "20px", color: "#ef4444" } }) : /* @__PURE__ */ jsx5(DocumentsIcon, { style: { fontSize: "20px", color: "#9ca3af" } })
971
+ }
972
+ ),
973
+ /* @__PURE__ */ jsx5(Box4, { style: { flex: 1 }, children: /* @__PURE__ */ jsx5(Text3, { size: 1, weight: "semibold", children: asset.originalFilename || asset._id }) }),
974
+ /* @__PURE__ */ jsx5(Box4, { style: { width: "120px" }, children: /* @__PURE__ */ jsxs5(Text3, { size: 1, children: [
975
+ asset.metadata?.dimensions?.width || "\u2014",
976
+ " x",
977
+ " ",
978
+ asset.metadata?.dimensions?.height || "\u2014"
979
+ ] }) }),
980
+ /* @__PURE__ */ jsx5(Box4, { style: { width: "140px" }, children: /* @__PURE__ */ jsx5(
981
+ Badge3,
982
+ {
983
+ tone: asset.size > 1048576 ? "critical" : asset.size > 5e5 ? "caution" : "positive",
984
+ children: formatBytes(asset.size)
985
+ }
986
+ ) })
987
+ ]
988
+ },
989
+ asset._id
990
+ ))
991
+ ] })
992
+ ] });
993
+ };
994
+
995
+ // src/components/tabs/UnusedAssets.tsx
996
+ import { useState as useState3 } from "react";
997
+ import { Stack as Stack4, Flex as Flex5, Heading as Heading2, Button as Button5, Card as Card5, Text as Text4, Grid as Grid2, Dialog as Dialog2, Box as Box5, Checkbox as Checkbox2 } from "@sanity/ui";
998
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
999
+ var UnusedAssets = ({
1000
+ unusedAssets,
1001
+ onBulkDelete,
1002
+ onAssetClick
1003
+ }) => {
1004
+ const [selectedIds, setSelectedIds] = useState3([]);
1005
+ const [confirmDelete, setConfirmDelete] = useState3({
1006
+ show: false,
1007
+ ids: []
1008
+ });
1009
+ const handleSelect = (id, selected) => {
1010
+ setSelectedIds((prev) => selected ? [...prev, id] : prev.filter((i) => i !== id));
1011
+ };
1012
+ const handleSelectAll = (checked) => {
1013
+ setSelectedIds(checked ? unusedAssets.map((a) => a._id) : []);
1014
+ };
1015
+ const triggerBulkDelete = (ids) => {
1016
+ setConfirmDelete({ show: true, ids });
1017
+ };
1018
+ const handleConfirmDelete = () => {
1019
+ onBulkDelete(confirmDelete.ids);
1020
+ setSelectedIds((prev) => prev.filter((id) => !confirmDelete.ids.includes(id)));
1021
+ setConfirmDelete({ show: false, ids: [] });
1022
+ };
1023
+ return /* @__PURE__ */ jsxs6(Stack4, { space: 4, children: [
1024
+ /* @__PURE__ */ jsxs6(Flex5, { align: "center", justify: "space-between", children: [
1025
+ /* @__PURE__ */ jsxs6(Stack4, { space: 3, children: [
1026
+ /* @__PURE__ */ jsx6(Heading2, { size: 1, children: "Bulk Deletion of Unused Assets" }),
1027
+ unusedAssets.length > 0 && /* @__PURE__ */ jsxs6(Flex5, { align: "center", gap: 3, children: [
1028
+ /* @__PURE__ */ jsx6(
1029
+ Checkbox2,
1030
+ {
1031
+ id: "select-all-unused",
1032
+ checked: selectedIds.length === unusedAssets.length && unusedAssets.length > 0,
1033
+ indeterminate: selectedIds.length > 0 && selectedIds.length < unusedAssets.length,
1034
+ onChange: (e) => handleSelectAll(e.currentTarget.checked)
1035
+ }
1036
+ ),
1037
+ /* @__PURE__ */ jsx6(Text4, { size: 1, weight: "semibold", muted: true, children: selectedIds.length > 0 ? `${selectedIds.length} Selected` : "Select All" })
1038
+ ] })
1039
+ ] }),
1040
+ /* @__PURE__ */ jsxs6(Flex5, { gap: 2, children: [
1041
+ selectedIds.length > 0 && /* @__PURE__ */ jsx6(
1042
+ Button5,
1043
+ {
1044
+ icon: TrashIcon,
1045
+ tone: "critical",
1046
+ text: `Delete Selected (${selectedIds.length})`,
1047
+ onClick: () => triggerBulkDelete(selectedIds)
1048
+ }
1049
+ ),
1050
+ unusedAssets.length > 0 && selectedIds.length !== unusedAssets.length && /* @__PURE__ */ jsx6(
1051
+ Button5,
1052
+ {
1053
+ mode: "ghost",
1054
+ icon: TrashIcon,
1055
+ tone: "critical",
1056
+ text: `Delete All Unused (${unusedAssets.length})`,
1057
+ onClick: () => triggerBulkDelete(unusedAssets.map((a) => a._id))
1058
+ }
1059
+ )
1060
+ ] })
1061
+ ] }),
1062
+ unusedAssets.length === 0 ? /* @__PURE__ */ jsx6(Card5, { padding: 5, border: true, radius: 3, children: /* @__PURE__ */ jsx6(Text4, { align: "center", children: "No unused assets found." }) }) : /* @__PURE__ */ jsx6(Grid2, { columns: [2, 3, 4, 5, 6], gap: 3, children: unusedAssets.map((asset) => /* @__PURE__ */ jsx6(
1063
+ AssetCard,
1064
+ {
1065
+ asset,
1066
+ onClick: onAssetClick,
1067
+ isSelected: selectedIds.includes(asset._id),
1068
+ onSelect: handleSelect
1069
+ },
1070
+ asset._id
1071
+ )) }),
1072
+ confirmDelete.show && /* @__PURE__ */ jsx6(
1073
+ Dialog2,
1074
+ {
1075
+ id: "confirm-bulk-delete",
1076
+ header: `Delete ${confirmDelete.ids.length} Asset(s)?`,
1077
+ onClose: () => setConfirmDelete({ show: false, ids: [] }),
1078
+ footer: /* @__PURE__ */ jsx6(Box5, { padding: 3, children: /* @__PURE__ */ jsxs6(Flex5, { gap: 2, justify: "flex-end", children: [
1079
+ /* @__PURE__ */ jsx6(
1080
+ Button5,
1081
+ {
1082
+ text: "Cancel",
1083
+ mode: "ghost",
1084
+ onClick: () => setConfirmDelete({ show: false, ids: [] })
1085
+ }
1086
+ ),
1087
+ /* @__PURE__ */ jsx6(Button5, { text: "Delete Permanently", tone: "critical", onClick: handleConfirmDelete })
1088
+ ] }) }),
1089
+ children: /* @__PURE__ */ jsx6(Box5, { padding: 4, children: /* @__PURE__ */ jsxs6(Text4, { children: [
1090
+ "Are you sure you want to delete ",
1091
+ /* @__PURE__ */ jsx6("strong", { children: confirmDelete.ids.length }),
1092
+ " asset(s)? This action cannot be undone and will permanently remove them from your project."
1093
+ ] }) })
1094
+ }
1095
+ )
1096
+ ] });
1097
+ };
1098
+
1099
+ // src/components/SmartAssetManagerTool.tsx
1100
+ import styled5 from "styled-components";
1101
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1102
+ var AppContainer = styled5(Card6)`
1103
+ height: 100vh;
1104
+ display: flex;
1105
+ flex-direction: column;
1106
+ `;
1107
+ var ScrollableContent = styled5(Box6)`
1108
+ flex: 1;
1109
+ overflow-y: auto;
1110
+ `;
1111
+ var TabNav = styled5(Card6)`
1112
+ z-index: 10;
1113
+ `;
1114
+ function SmartAssetManagerTool() {
1115
+ const sanityClient = useClient({ apiVersion: "2025-02-07" });
1116
+ const [activeTab, setActiveTab] = useState4("all");
1117
+ const [searchQuery, setSearchQuery] = useState4("");
1118
+ const [sortBy, setSortBy] = useState4("_createdAt");
1119
+ const [assetType, setAssetType] = useState4("all");
1120
+ const [sizeFilter, setSizeFilter] = useState4("all");
1121
+ const limit = 20;
1122
+ const [currentPage, setCurrentPage] = useState4(1);
1123
+ const { assets, loading, total, refreshAssets } = useAssets(
1124
+ sanityClient,
1125
+ searchQuery,
1126
+ sortBy,
1127
+ assetType,
1128
+ (currentPage - 1) * limit,
1129
+ limit
1130
+ );
1131
+ const [scanning, setScanning] = useState4(false);
1132
+ const [isUploading, setIsUploading] = useState4(false);
1133
+ const toast = useToast2();
1134
+ const [unusedAssets, setUnusedAssets] = useState4([]);
1135
+ const [selectedAsset, setSelectedAsset] = useState4(null);
1136
+ const filteredAssets = useMemo(() => {
1137
+ return assets.filter((asset) => {
1138
+ if (sizeFilter === "small") return asset.size < 102400;
1139
+ if (sizeFilter === "medium") return asset.size >= 102400 && asset.size < 1048576;
1140
+ if (sizeFilter === "large") return asset.size >= 1048576;
1141
+ return true;
1142
+ });
1143
+ }, [assets, sizeFilter]);
1144
+ useEffect3(() => {
1145
+ setCurrentPage(1);
1146
+ }, [searchQuery, assetType, sizeFilter, activeTab]);
1147
+ const handleFindUnused = async () => {
1148
+ setScanning(true);
1149
+ const unused = await findUnusedAssets(sanityClient);
1150
+ setUnusedAssets(unused);
1151
+ setScanning(false);
1152
+ setActiveTab("unused");
1153
+ };
1154
+ const handleDeleteAsset = async (id) => {
1155
+ const ids = Array.isArray(id) ? id : [id];
1156
+ await deleteAssets(sanityClient, ids);
1157
+ refreshAssets();
1158
+ setUnusedAssets((prev) => prev.filter((a) => !ids.includes(a._id)));
1159
+ if (activeTab === "unused") handleFindUnused();
1160
+ };
1161
+ const handleResetFilters = () => {
1162
+ setSearchQuery("");
1163
+ setAssetType("all");
1164
+ setSizeFilter("all");
1165
+ setCurrentPage(1);
1166
+ };
1167
+ const handleUpload = async (files) => {
1168
+ if (files.length > 5) {
1169
+ toast.push({
1170
+ status: "error",
1171
+ title: "Upload limit exceeded",
1172
+ description: "You can only upload up to 5 files at a time."
1173
+ });
1174
+ return;
1175
+ }
1176
+ setIsUploading(true);
1177
+ const fileArray = Array.from(files);
1178
+ const filenames = fileArray.map((f) => f.name);
1179
+ try {
1180
+ const existingAssetNames = await sanityClient.fetch(
1181
+ `*[_type in ["sanity.imageAsset", "sanity.fileAsset"] && originalFilename in $filenames].originalFilename`,
1182
+ { filenames }
1183
+ );
1184
+ const filesToUpload = fileArray.filter((f) => !existingAssetNames.includes(f.name));
1185
+ const skippedCount = fileArray.length - filesToUpload.length;
1186
+ if (filesToUpload.length === 0) {
1187
+ setIsUploading(false);
1188
+ if (skippedCount > 0) {
1189
+ toast.push({
1190
+ status: "info",
1191
+ title: "No new files were added",
1192
+ description: `All ${skippedCount} file(s) already exist in your library.`
1193
+ });
1194
+ }
1195
+ return;
1196
+ }
1197
+ const uploadPromises = filesToUpload.map(async (file) => {
1198
+ const type = file.type.startsWith("image/") ? "image" : "file";
1199
+ return sanityClient.assets.upload(type, file, {
1200
+ filename: file.name
1201
+ });
1202
+ });
1203
+ await Promise.all(uploadPromises);
1204
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1205
+ refreshAssets();
1206
+ setIsUploading(false);
1207
+ toast.push({
1208
+ status: "success",
1209
+ title: "Upload successful",
1210
+ description: `Uploaded ${filesToUpload.length} new file(s). ${skippedCount > 0 ? `Skipped ${skippedCount} existing files.` : ""}`
1211
+ });
1212
+ } catch (err) {
1213
+ console.error("Upload failed:", err);
1214
+ setIsUploading(false);
1215
+ toast.push({
1216
+ status: "error",
1217
+ title: "Upload failed",
1218
+ description: "An error occurred while uploading your files."
1219
+ });
1220
+ }
1221
+ };
1222
+ return /* @__PURE__ */ jsxs7(AppContainer, { height: "fill", children: [
1223
+ /* @__PURE__ */ jsx7(TabNav, { borderBottom: true, padding: 2, children: /* @__PURE__ */ jsx7(Flex6, { gap: 1, children: [
1224
+ { id: "all", label: "All Assets" },
1225
+ { id: "analysis", label: "Size Analyzer" },
1226
+ { id: "unused", label: "Unused Assets", onClick: handleFindUnused }
1227
+ ].map((tab) => /* @__PURE__ */ jsx7(
1228
+ Button6,
1229
+ {
1230
+ mode: activeTab === tab.id ? "default" : "bleed",
1231
+ padding: 3,
1232
+ text: tab.label,
1233
+ onClick: () => {
1234
+ setActiveTab(tab.id);
1235
+ if (tab.onClick) tab.onClick();
1236
+ },
1237
+ selected: activeTab === tab.id
1238
+ },
1239
+ tab.id
1240
+ )) }) }),
1241
+ /* @__PURE__ */ jsxs7(ScrollableContent, { children: [
1242
+ activeTab === "all" && /* @__PURE__ */ jsx7(
1243
+ TopToolbar,
1244
+ {
1245
+ searchQuery,
1246
+ setSearchQuery,
1247
+ sortBy,
1248
+ setSortBy,
1249
+ assetType,
1250
+ setAssetType,
1251
+ sizeFilter,
1252
+ setSizeFilter,
1253
+ onReset: handleResetFilters,
1254
+ onUpload: handleUpload,
1255
+ isUploading
1256
+ }
1257
+ ),
1258
+ /* @__PURE__ */ jsx7(Box6, { padding: 4, children: loading || scanning ? /* @__PURE__ */ jsx7(Flex6, { align: "center", justify: "center", style: { minHeight: "400px" }, children: /* @__PURE__ */ jsxs7(Stack5, { space: 4, children: [
1259
+ /* @__PURE__ */ jsx7(Flex6, { align: "center", justify: "center", children: /* @__PURE__ */ jsx7(Spinner2, { muted: true }) }),
1260
+ /* @__PURE__ */ jsx7(Text5, { size: 1, muted: true, children: "Syncing with media library..." })
1261
+ ] }) }) : /* @__PURE__ */ jsxs7(Box6, { children: [
1262
+ activeTab === "all" && /* @__PURE__ */ jsxs7(Box6, { children: [
1263
+ filteredAssets.length === 0 ? /* @__PURE__ */ jsx7(Card6, { padding: 5, border: true, radius: 3, style: { textAlign: "center" }, children: /* @__PURE__ */ jsx7(Text5, { muted: true, children: "No assets found matching your filters." }) }) : /* @__PURE__ */ jsx7(Grid3, { columns: [2, 3, 4, 5, 6], gap: 3, children: filteredAssets.map((asset) => /* @__PURE__ */ jsx7(AssetCard, { asset, onClick: setSelectedAsset }, asset._id)) }),
1264
+ total > limit && /* @__PURE__ */ jsxs7(Flex6, { justify: "center", align: "center", gap: 3, marginTop: 5, marginBottom: 2, children: [
1265
+ /* @__PURE__ */ jsx7(
1266
+ Button6,
1267
+ {
1268
+ padding: 3,
1269
+ mode: "ghost",
1270
+ text: "Previous",
1271
+ disabled: currentPage === 1 || loading,
1272
+ onClick: () => setCurrentPage((p) => Math.max(1, p - 1))
1273
+ }
1274
+ ),
1275
+ /* @__PURE__ */ jsx7(Card6, { padding: 3, radius: 2, border: true, children: /* @__PURE__ */ jsxs7(Text5, { size: 1, weight: "semibold", children: [
1276
+ "Page ",
1277
+ currentPage,
1278
+ " of ",
1279
+ Math.ceil(total / limit)
1280
+ ] }) }),
1281
+ /* @__PURE__ */ jsx7(
1282
+ Button6,
1283
+ {
1284
+ padding: 3,
1285
+ mode: "ghost",
1286
+ text: "Next",
1287
+ disabled: currentPage >= Math.ceil(total / limit) || loading,
1288
+ onClick: () => setCurrentPage((p) => p + 1)
1289
+ }
1290
+ )
1291
+ ] })
1292
+ ] }),
1293
+ activeTab === "analysis" && /* @__PURE__ */ jsx7(SizeAnalyzer, { assets }),
1294
+ activeTab === "unused" && /* @__PURE__ */ jsx7(
1295
+ UnusedAssets,
1296
+ {
1297
+ unusedAssets,
1298
+ onBulkDelete: handleDeleteAsset,
1299
+ onAssetClick: setSelectedAsset
1300
+ }
1301
+ )
1302
+ ] }) })
1303
+ ] }),
1304
+ selectedAsset && /* @__PURE__ */ jsx7(
1305
+ AssetDetailsDialog,
1306
+ {
1307
+ asset: selectedAsset,
1308
+ sanityClient,
1309
+ onClose: () => setSelectedAsset(null),
1310
+ onUpdate: (_id, newName) => {
1311
+ refreshAssets();
1312
+ setSelectedAsset((prev) => prev ? { ...prev, originalFilename: newName } : null);
1313
+ },
1314
+ onDelete: handleDeleteAsset
1315
+ }
1316
+ )
1317
+ ] });
1318
+ }
1319
+
1320
+ // src/index.ts
1321
+ var smartAssetManager = definePlugin(() => {
1322
+ return {
1323
+ name: "sanity-plugin-smart-asset-manager",
1324
+ tools: [
1325
+ {
1326
+ name: "smart-asset-manager",
1327
+ title: "Smart Asset Manager",
1328
+ component: SmartAssetManagerTool,
1329
+ icon: FolderIcon
1330
+ }
1331
+ ]
1332
+ };
1333
+ });
1334
+ export {
1335
+ smartAssetManager
1336
+ };