@proveanything/smartlinks-utils-ui 0.1.1 → 0.1.4

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.
@@ -0,0 +1,920 @@
1
+ import { cn } from './chunk-L7FQ52F5.js';
2
+ import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
3
+ import * as SL from '@proveanything/smartlinks';
4
+ import { Filter, Search, LayoutGrid, List, X, Loader2, AlertCircle, ImageOff, Clipboard, Pencil, Check, Upload, Link, Trash2, FileIcon, Image, Film, Music, FileText } from 'lucide-react';
5
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
+
7
+ // src/components/AssetPicker/types.ts
8
+ var ASSET_MIME_FILTERS = [
9
+ { value: "all", label: "All files" },
10
+ { value: "image", label: "Images", prefix: "image/" },
11
+ { value: "video", label: "Videos", prefix: "video/" },
12
+ { value: "audio", label: "Audio", prefix: "audio/" },
13
+ { value: "document", label: "Documents", prefix: "application/" },
14
+ { value: "pdf", label: "PDFs", prefix: "application/pdf" }
15
+ ];
16
+ function useAssets({ scope, accept, admin, pageSize }) {
17
+ const [assets, setAssets] = useState([]);
18
+ const [loading, setLoading] = useState(true);
19
+ const [error, setError] = useState(null);
20
+ const [uploading, setUploading] = useState(false);
21
+ const [uploadProgress, setUploadProgress] = useState(0);
22
+ const mountedRef = useRef(true);
23
+ useEffect(() => {
24
+ mountedRef.current = true;
25
+ return () => {
26
+ mountedRef.current = false;
27
+ };
28
+ }, []);
29
+ const fetchAssets = useCallback(async () => {
30
+ setLoading(true);
31
+ setError(null);
32
+ try {
33
+ const result = await SL.asset.list({
34
+ scope,
35
+ mimeTypePrefix: accept,
36
+ limit: pageSize
37
+ });
38
+ if (mountedRef.current) {
39
+ setAssets(result);
40
+ }
41
+ } catch (err) {
42
+ if (mountedRef.current) {
43
+ setError(err?.message || "Failed to load assets");
44
+ setAssets([]);
45
+ }
46
+ } finally {
47
+ if (mountedRef.current) setLoading(false);
48
+ }
49
+ }, [scope.type, scope.collectionId, scope.productId, scope.proofId, accept, pageSize]);
50
+ useEffect(() => {
51
+ fetchAssets();
52
+ }, [fetchAssets]);
53
+ const upload = useCallback(async (file, onProgress) => {
54
+ setUploading(true);
55
+ setUploadProgress(0);
56
+ try {
57
+ const result = await SL.asset.upload({
58
+ file,
59
+ scope,
60
+ name: file.name,
61
+ admin,
62
+ onProgress: (pct) => {
63
+ setUploadProgress(pct);
64
+ onProgress?.(pct);
65
+ }
66
+ });
67
+ if (mountedRef.current) {
68
+ setAssets((prev) => [result, ...prev]);
69
+ }
70
+ return result;
71
+ } catch (err) {
72
+ if (mountedRef.current) setError(err?.message || "Upload failed");
73
+ return null;
74
+ } finally {
75
+ if (mountedRef.current) {
76
+ setUploading(false);
77
+ setUploadProgress(0);
78
+ }
79
+ }
80
+ }, [scope, admin]);
81
+ const uploadFromUrl = useCallback(async (url, name) => {
82
+ setUploading(true);
83
+ setUploadProgress(0);
84
+ try {
85
+ const response = await fetch(url);
86
+ if (!response.ok) throw new Error(`Failed to fetch: ${response.statusText}`);
87
+ const blob = await response.blob();
88
+ const fileName = name || url.split("/").pop()?.split("?")[0] || "imported-file";
89
+ const file = new File([blob], fileName, { type: blob.type });
90
+ return await upload(file);
91
+ } catch (err) {
92
+ if (mountedRef.current) setError(err?.message || "URL import failed");
93
+ return null;
94
+ } finally {
95
+ if (mountedRef.current) {
96
+ setUploading(false);
97
+ setUploadProgress(0);
98
+ }
99
+ }
100
+ }, [upload]);
101
+ const remove = useCallback(async (assetId) => {
102
+ try {
103
+ await SL.asset.remove({ assetId, scope });
104
+ if (mountedRef.current) {
105
+ setAssets((prev) => prev.filter((a) => a.id !== assetId));
106
+ }
107
+ return true;
108
+ } catch (err) {
109
+ if (mountedRef.current) setError(err?.message || "Delete failed");
110
+ return false;
111
+ }
112
+ }, [scope]);
113
+ return {
114
+ assets,
115
+ loading,
116
+ error,
117
+ refresh: fetchAssets,
118
+ upload,
119
+ uploadFromUrl,
120
+ remove,
121
+ uploading,
122
+ uploadProgress
123
+ };
124
+ }
125
+ function getIcon(mimeType) {
126
+ if (!mimeType) return FileIcon;
127
+ if (mimeType.startsWith("image/")) return Image;
128
+ if (mimeType.startsWith("video/")) return Film;
129
+ if (mimeType.startsWith("audio/")) return Music;
130
+ return FileText;
131
+ }
132
+ function formatSize(bytes) {
133
+ if (!bytes) return "";
134
+ if (bytes < 1024) return `${bytes} B`;
135
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
136
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
137
+ }
138
+ function getThumbnail(asset2) {
139
+ if (asset2.thumbnails?.x200) return asset2.thumbnails.x200;
140
+ if (asset2.thumbnails?.x100) return asset2.thumbnails.x100;
141
+ if (asset2.thumbnails?.x512) return asset2.thumbnails.x512;
142
+ if (asset2.mimeType?.startsWith("image/")) return asset2.url;
143
+ return null;
144
+ }
145
+ var AssetGridItem = ({ asset: asset2, selected, onToggle, onDelete, allowDelete }) => {
146
+ const thumb = getThumbnail(asset2);
147
+ const Icon = getIcon(asset2.mimeType);
148
+ return /* @__PURE__ */ jsxs(
149
+ "div",
150
+ {
151
+ className: cn(
152
+ "group relative rounded-lg border-2 cursor-pointer transition-all overflow-hidden",
153
+ "hover:shadow-md",
154
+ selected ? "border-primary bg-primary/10" : "border-border hover:border-ring"
155
+ ),
156
+ onClick: onToggle,
157
+ role: "button",
158
+ tabIndex: 0,
159
+ onKeyDown: (e) => {
160
+ if (e.key === "Enter" || e.key === " ") {
161
+ e.preventDefault();
162
+ onToggle();
163
+ }
164
+ },
165
+ children: [
166
+ /* @__PURE__ */ jsx("div", { className: "aspect-square bg-muted flex items-center justify-center overflow-hidden", children: thumb ? /* @__PURE__ */ jsx(
167
+ "img",
168
+ {
169
+ src: thumb,
170
+ alt: asset2.name || asset2.id,
171
+ className: "w-full h-full object-cover",
172
+ loading: "lazy"
173
+ }
174
+ ) : /* @__PURE__ */ jsx(Icon, { className: "w-8 h-8 text-muted-foreground" }) }),
175
+ /* @__PURE__ */ jsxs("div", { className: "p-2", children: [
176
+ /* @__PURE__ */ jsx("p", { className: "text-xs font-medium truncate text-foreground", title: asset2.name, children: asset2.cleanName || asset2.name || asset2.id }),
177
+ /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground mt-0.5", children: [
178
+ formatSize(asset2.size),
179
+ asset2.mimeType && ` \u2022 ${asset2.mimeType.split("/")[1]?.toUpperCase() || asset2.mimeType}`
180
+ ] })
181
+ ] }),
182
+ 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" }) }),
183
+ allowDelete && onDelete && /* @__PURE__ */ jsx(
184
+ "button",
185
+ {
186
+ 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",
187
+ onClick: (e) => {
188
+ e.stopPropagation();
189
+ onDelete();
190
+ },
191
+ title: "Delete asset",
192
+ children: /* @__PURE__ */ jsx(Trash2, { className: "w-3 h-3" })
193
+ }
194
+ )
195
+ ]
196
+ }
197
+ );
198
+ };
199
+ var AssetListItem = ({ asset: asset2, selected, onToggle, onDelete, allowDelete }) => {
200
+ const thumb = getThumbnail(asset2);
201
+ const Icon = getIcon(asset2.mimeType);
202
+ return /* @__PURE__ */ jsxs(
203
+ "div",
204
+ {
205
+ className: cn(
206
+ "group flex items-center gap-3 p-2 rounded-lg border cursor-pointer transition-all",
207
+ "hover:shadow-sm",
208
+ selected ? "border-primary bg-primary/10" : "border-border hover:border-ring"
209
+ ),
210
+ onClick: onToggle,
211
+ role: "button",
212
+ tabIndex: 0,
213
+ onKeyDown: (e) => {
214
+ if (e.key === "Enter" || e.key === " ") {
215
+ e.preventDefault();
216
+ onToggle();
217
+ }
218
+ },
219
+ children: [
220
+ /* @__PURE__ */ jsx("div", { className: "w-10 h-10 rounded bg-muted flex items-center justify-center overflow-hidden flex-shrink-0", children: thumb ? /* @__PURE__ */ jsx("img", { src: thumb, alt: asset2.name, className: "w-full h-full object-cover", loading: "lazy" }) : /* @__PURE__ */ jsx(Icon, { className: "w-4 h-4 text-muted-foreground" }) }),
221
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
222
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium truncate text-foreground", children: asset2.cleanName || asset2.name || asset2.id }),
223
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
224
+ formatSize(asset2.size),
225
+ asset2.mimeType && ` \u2022 ${asset2.mimeType}`
226
+ ] })
227
+ ] }),
228
+ 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" }) }),
229
+ allowDelete && onDelete && /* @__PURE__ */ jsx(
230
+ "button",
231
+ {
232
+ 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",
233
+ onClick: (e) => {
234
+ e.stopPropagation();
235
+ onDelete();
236
+ },
237
+ title: "Delete",
238
+ children: /* @__PURE__ */ jsx(Trash2, { className: "w-3 h-3" })
239
+ }
240
+ )
241
+ ]
242
+ }
243
+ );
244
+ };
245
+ var AssetGrid = ({
246
+ assets,
247
+ viewMode,
248
+ selectedIds,
249
+ onToggleSelect,
250
+ onDelete,
251
+ allowDelete
252
+ }) => {
253
+ if (assets.length === 0) return null;
254
+ if (viewMode === "list") {
255
+ return /* @__PURE__ */ jsx("div", { className: "space-y-1", children: assets.map((asset2) => /* @__PURE__ */ jsx(
256
+ AssetListItem,
257
+ {
258
+ asset: asset2,
259
+ selected: selectedIds.has(asset2.id),
260
+ onToggle: () => onToggleSelect(asset2),
261
+ onDelete: allowDelete && onDelete ? () => onDelete(asset2.id) : void 0,
262
+ allowDelete
263
+ },
264
+ asset2.id
265
+ )) });
266
+ }
267
+ return /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 gap-2", children: assets.map((asset2) => /* @__PURE__ */ jsx(
268
+ AssetGridItem,
269
+ {
270
+ asset: asset2,
271
+ selected: selectedIds.has(asset2.id),
272
+ onToggle: () => onToggleSelect(asset2),
273
+ onDelete: allowDelete && onDelete ? () => onDelete(asset2.id) : void 0,
274
+ allowDelete
275
+ },
276
+ asset2.id
277
+ )) });
278
+ };
279
+ var UploadZone = ({
280
+ onFiles,
281
+ accept,
282
+ multiple,
283
+ uploading,
284
+ uploadProgress = 0,
285
+ className
286
+ }) => {
287
+ const [dragOver, setDragOver] = useState(false);
288
+ const [pastedFile, setPastedFile] = useState(null);
289
+ const [editingName, setEditingName] = useState(false);
290
+ const [fileName, setFileName] = useState("");
291
+ const inputRef = useRef(null);
292
+ const nameInputRef = useRef(null);
293
+ const zoneRef = useRef(null);
294
+ useEffect(() => {
295
+ return () => {
296
+ if (pastedFile?.previewUrl) URL.revokeObjectURL(pastedFile.previewUrl);
297
+ };
298
+ }, [pastedFile]);
299
+ useEffect(() => {
300
+ if (editingName) nameInputRef.current?.select();
301
+ }, [editingName]);
302
+ useEffect(() => {
303
+ const handlePaste = (e) => {
304
+ if (uploading) return;
305
+ const items = e.clipboardData?.items;
306
+ if (!items) return;
307
+ for (const item of Array.from(items)) {
308
+ if (item.kind === "file") {
309
+ const file = item.getAsFile();
310
+ if (!file) continue;
311
+ if (accept && !file.type.startsWith(accept.replace("*", ""))) continue;
312
+ e.preventDefault();
313
+ const previewUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : "";
314
+ const defaultName = file.name === "image.png" ? `pasted-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/[T:]/g, "-")}` : file.name.replace(/\.[^.]+$/, "");
315
+ setPastedFile({ file, previewUrl, name: defaultName });
316
+ setFileName(defaultName);
317
+ setEditingName(false);
318
+ return;
319
+ }
320
+ }
321
+ };
322
+ document.addEventListener("paste", handlePaste);
323
+ return () => document.removeEventListener("paste", handlePaste);
324
+ }, [uploading, accept]);
325
+ const handleConfirmPaste = useCallback(() => {
326
+ if (!pastedFile) return;
327
+ const ext = pastedFile.file.name.includes(".") ? pastedFile.file.name.split(".").pop() : "png";
328
+ const finalName = `${fileName.trim() || "pasted-image"}.${ext}`;
329
+ const renamedFile = new File([pastedFile.file], finalName, { type: pastedFile.file.type });
330
+ onFiles([renamedFile]);
331
+ if (pastedFile.previewUrl) URL.revokeObjectURL(pastedFile.previewUrl);
332
+ setPastedFile(null);
333
+ }, [pastedFile, fileName, onFiles]);
334
+ const handleCancelPaste = useCallback(() => {
335
+ if (pastedFile?.previewUrl) URL.revokeObjectURL(pastedFile.previewUrl);
336
+ setPastedFile(null);
337
+ setEditingName(false);
338
+ }, [pastedFile]);
339
+ const handleDrag = useCallback((e) => {
340
+ e.preventDefault();
341
+ e.stopPropagation();
342
+ }, []);
343
+ const handleDragIn = useCallback((e) => {
344
+ e.preventDefault();
345
+ e.stopPropagation();
346
+ setDragOver(true);
347
+ }, []);
348
+ const handleDragOut = useCallback((e) => {
349
+ e.preventDefault();
350
+ e.stopPropagation();
351
+ setDragOver(false);
352
+ }, []);
353
+ const handleDrop = useCallback((e) => {
354
+ e.preventDefault();
355
+ e.stopPropagation();
356
+ setDragOver(false);
357
+ const files = Array.from(e.dataTransfer.files);
358
+ if (files.length > 0) {
359
+ onFiles(multiple ? files : [files[0]]);
360
+ }
361
+ }, [onFiles, multiple]);
362
+ const handleInputChange = useCallback((e) => {
363
+ const files = Array.from(e.target.files || []);
364
+ if (files.length > 0) {
365
+ onFiles(multiple ? files : [files[0]]);
366
+ }
367
+ e.target.value = "";
368
+ }, [onFiles, multiple]);
369
+ if (pastedFile) {
370
+ return /* @__PURE__ */ jsx("div", { className: cn(
371
+ "border-2 border-solid border-primary rounded-lg p-4 transition-colors",
372
+ className
373
+ ), children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-3", children: [
374
+ pastedFile.previewUrl ? /* @__PURE__ */ jsx(
375
+ "img",
376
+ {
377
+ src: pastedFile.previewUrl,
378
+ alt: "Pasted content",
379
+ className: "max-h-32 max-w-full rounded-md object-contain border border-border"
380
+ }
381
+ ) : /* @__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" }) }),
382
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1.5 w-full max-w-xs", children: editingName ? /* @__PURE__ */ jsx(
383
+ "input",
384
+ {
385
+ ref: nameInputRef,
386
+ type: "text",
387
+ value: fileName,
388
+ onChange: (e) => setFileName(e.target.value),
389
+ onKeyDown: (e) => {
390
+ if (e.key === "Enter") {
391
+ setEditingName(false);
392
+ handleConfirmPaste();
393
+ }
394
+ if (e.key === "Escape") setEditingName(false);
395
+ },
396
+ onBlur: () => setEditingName(false),
397
+ 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",
398
+ placeholder: "File name"
399
+ }
400
+ ) : /* @__PURE__ */ jsxs(
401
+ "button",
402
+ {
403
+ onClick: () => setEditingName(true),
404
+ 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",
405
+ title: "Rename",
406
+ children: [
407
+ /* @__PURE__ */ jsx("span", { className: "truncate max-w-[200px]", children: fileName }),
408
+ /* @__PURE__ */ jsx(Pencil, { className: "w-3 h-3 flex-shrink-0 opacity-50" })
409
+ ]
410
+ }
411
+ ) }),
412
+ /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground", children: [
413
+ pastedFile.file.type,
414
+ " \xB7 ",
415
+ (pastedFile.file.size / 1024).toFixed(1),
416
+ " KB"
417
+ ] }),
418
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
419
+ /* @__PURE__ */ jsxs(
420
+ "button",
421
+ {
422
+ onClick: handleCancelPaste,
423
+ 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",
424
+ children: [
425
+ /* @__PURE__ */ jsx(X, { className: "w-3 h-3" }),
426
+ " Cancel"
427
+ ]
428
+ }
429
+ ),
430
+ /* @__PURE__ */ jsxs(
431
+ "button",
432
+ {
433
+ onClick: handleConfirmPaste,
434
+ 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",
435
+ children: [
436
+ /* @__PURE__ */ jsx(Check, { className: "w-3 h-3" }),
437
+ " Upload"
438
+ ]
439
+ }
440
+ )
441
+ ] })
442
+ ] }) });
443
+ }
444
+ return /* @__PURE__ */ jsxs(
445
+ "div",
446
+ {
447
+ ref: zoneRef,
448
+ className: cn(
449
+ "relative border-2 border-dashed rounded-lg p-4 text-center cursor-pointer transition-colors",
450
+ dragOver ? "border-primary bg-primary/5" : "border-border hover:border-ring",
451
+ uploading && "pointer-events-none opacity-70",
452
+ className
453
+ ),
454
+ onDrag: handleDrag,
455
+ onDragOver: handleDragIn,
456
+ onDragEnter: handleDragIn,
457
+ onDragLeave: handleDragOut,
458
+ onDrop: handleDrop,
459
+ onClick: () => inputRef.current?.click(),
460
+ role: "button",
461
+ tabIndex: 0,
462
+ onKeyDown: (e) => {
463
+ if (e.key === "Enter" || e.key === " ") inputRef.current?.click();
464
+ },
465
+ children: [
466
+ /* @__PURE__ */ jsx(
467
+ "input",
468
+ {
469
+ ref: inputRef,
470
+ type: "file",
471
+ accept,
472
+ multiple,
473
+ onChange: handleInputChange,
474
+ className: "hidden"
475
+ }
476
+ ),
477
+ uploading ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-2 py-2", children: [
478
+ /* @__PURE__ */ jsx(Loader2, { className: "w-6 h-6 text-primary animate-spin" }),
479
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
480
+ "Uploading\u2026 ",
481
+ uploadProgress > 0 ? `${Math.round(uploadProgress)}%` : ""
482
+ ] }),
483
+ uploadProgress > 0 && /* @__PURE__ */ jsx("div", { className: "w-full max-w-xs h-1.5 bg-muted rounded-full overflow-hidden", children: /* @__PURE__ */ jsx(
484
+ "div",
485
+ {
486
+ className: "h-full bg-primary rounded-full transition-all duration-300",
487
+ style: { width: `${uploadProgress}%` }
488
+ }
489
+ ) })
490
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1 py-2", children: [
491
+ /* @__PURE__ */ jsx(Upload, { className: "w-6 h-6 text-muted-foreground" }),
492
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
493
+ "Drop files here, ",
494
+ /* @__PURE__ */ jsx("span", { className: "text-primary underline", children: "browse" }),
495
+ ", or paste from clipboard"
496
+ ] }),
497
+ /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground flex items-center gap-1", children: [
498
+ /* @__PURE__ */ jsx(Clipboard, { className: "w-3 h-3" }),
499
+ " Ctrl+V / \u2318V to paste"
500
+ ] }),
501
+ accept && /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground", children: [
502
+ "Accepts: ",
503
+ accept
504
+ ] })
505
+ ] })
506
+ ]
507
+ }
508
+ );
509
+ };
510
+ var UrlImport = ({
511
+ onImport,
512
+ importing,
513
+ className
514
+ }) => {
515
+ const [url, setUrl] = useState("");
516
+ const [error, setError] = useState("");
517
+ const handleSubmit = async (e) => {
518
+ e.preventDefault();
519
+ if (!url.trim()) return;
520
+ try {
521
+ new URL(url.trim());
522
+ } catch {
523
+ setError("Please enter a valid URL");
524
+ return;
525
+ }
526
+ setError("");
527
+ const result = await onImport(url.trim());
528
+ if (result) setUrl("");
529
+ };
530
+ return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: cn("flex gap-2", className), children: [
531
+ /* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
532
+ /* @__PURE__ */ jsx(Link, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" }),
533
+ /* @__PURE__ */ jsx(
534
+ "input",
535
+ {
536
+ type: "text",
537
+ value: url,
538
+ onChange: (e) => {
539
+ setUrl(e.target.value);
540
+ setError("");
541
+ },
542
+ placeholder: "https://example.com/image.png",
543
+ disabled: importing,
544
+ className: cn(
545
+ "w-full pl-8 pr-3 py-2 text-sm rounded-md border bg-transparent",
546
+ "placeholder:text-muted-foreground",
547
+ "focus:outline-none focus:ring-2 focus:ring-ring",
548
+ error ? "border-destructive" : "border-border"
549
+ )
550
+ }
551
+ )
552
+ ] }),
553
+ /* @__PURE__ */ jsxs(
554
+ "button",
555
+ {
556
+ type: "submit",
557
+ disabled: !url.trim() || importing,
558
+ className: cn(
559
+ "px-3 py-2 text-sm font-medium rounded-md transition-colors",
560
+ "bg-primary text-primary-foreground hover:bg-primary/90",
561
+ "disabled:opacity-50 disabled:cursor-not-allowed",
562
+ "flex items-center gap-1.5"
563
+ ),
564
+ children: [
565
+ importing ? /* @__PURE__ */ jsx(Loader2, { className: "w-3.5 h-3.5 animate-spin" }) : null,
566
+ "Import"
567
+ ]
568
+ }
569
+ ),
570
+ error && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive mt-1 absolute", children: error })
571
+ ] });
572
+ };
573
+ var ScopedAssetBrowser = ({ scope, accept, admin, pageSize, viewMode, search, selectedIds, onToggleSelect, onDelete, allowDelete, emptyText }) => {
574
+ const { assets, loading, error, refresh } = useAssets({ scope, accept, admin, pageSize });
575
+ const filteredAssets = useMemo(() => {
576
+ if (!search.trim()) return assets;
577
+ const q = search.toLowerCase();
578
+ return assets.filter(
579
+ (a) => (a.name || "").toLowerCase().includes(q) || (a.cleanName || "").toLowerCase().includes(q) || (a.mimeType || "").toLowerCase().includes(q)
580
+ );
581
+ }, [assets, search]);
582
+ if (loading && assets.length === 0) {
583
+ 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" }) });
584
+ }
585
+ if (error) {
586
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 p-3 rounded-md bg-destructive/10 text-destructive text-sm", children: [
587
+ /* @__PURE__ */ jsx(AlertCircle, { className: "w-4 h-4 flex-shrink-0" }),
588
+ error,
589
+ /* @__PURE__ */ jsx("button", { onClick: refresh, className: "ml-auto underline text-xs", children: "Retry" })
590
+ ] });
591
+ }
592
+ if (filteredAssets.length === 0) {
593
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-muted-foreground", children: [
594
+ /* @__PURE__ */ jsx(ImageOff, { className: "w-8 h-8 mb-2" }),
595
+ /* @__PURE__ */ jsx("p", { className: "text-sm", children: emptyText || "No assets found" }),
596
+ search && /* @__PURE__ */ jsx("p", { className: "text-xs mt-1", children: "Try adjusting your search" })
597
+ ] });
598
+ }
599
+ return /* @__PURE__ */ jsx(
600
+ AssetGrid,
601
+ {
602
+ assets: filteredAssets,
603
+ viewMode,
604
+ selectedIds,
605
+ onToggleSelect,
606
+ onDelete: admin && allowDelete ? onDelete : void 0,
607
+ allowDelete: admin && allowDelete
608
+ }
609
+ );
610
+ };
611
+ var AssetPickerContent = ({
612
+ scope,
613
+ productScope,
614
+ allowUpload = true,
615
+ allowUrlImport = true,
616
+ multiple = false,
617
+ accept: acceptProp,
618
+ showTypeFilter,
619
+ value,
620
+ onSelect,
621
+ admin = false,
622
+ allowDelete = false,
623
+ defaultView = "grid",
624
+ emptyText,
625
+ pageSize = 50,
626
+ onConfirm
627
+ }) => {
628
+ const { assets, upload, uploadFromUrl, uploading, uploadProgress } = useAssets({
629
+ scope,
630
+ accept: acceptProp,
631
+ admin,
632
+ pageSize
633
+ });
634
+ const [tab, setTab] = useState("browse");
635
+ const [viewMode, setViewMode] = useState(defaultView);
636
+ const [search, setSearch] = useState("");
637
+ const [mimeFilter, setMimeFilter] = useState("all");
638
+ const [selectedIds, setSelectedIds] = useState(() => {
639
+ if (!value) return /* @__PURE__ */ new Set();
640
+ return new Set(Array.isArray(value) ? value : [value]);
641
+ });
642
+ const hasProductScope = !!productScope;
643
+ const [scopeTab, setScopeTab] = useState("collection");
644
+ const effectiveAccept = useMemo(() => {
645
+ if (acceptProp) return acceptProp;
646
+ const entry = ASSET_MIME_FILTERS.find((f) => f.value === mimeFilter);
647
+ return entry?.prefix;
648
+ }, [acceptProp, mimeFilter]);
649
+ const shouldShowFilter = showTypeFilter ?? !acceptProp;
650
+ const activeScope = useMemo(() => {
651
+ if (hasProductScope && scopeTab === "product") {
652
+ return { type: "product", collectionId: productScope.collectionId, productId: productScope.productId };
653
+ }
654
+ return scope;
655
+ }, [scope, productScope, scopeTab, hasProductScope]);
656
+ const toSelection = useCallback((asset2) => ({
657
+ id: asset2.id,
658
+ url: asset2.url,
659
+ name: asset2.name,
660
+ mimeType: asset2.mimeType,
661
+ size: asset2.size,
662
+ metadata: asset2.metadata,
663
+ thumbnails: asset2.thumbnails
664
+ }), []);
665
+ const handleToggleSelect = useCallback((asset2) => {
666
+ setSelectedIds((prev) => {
667
+ const next = new Set(prev);
668
+ if (next.has(asset2.id)) {
669
+ next.delete(asset2.id);
670
+ } else {
671
+ if (!multiple) next.clear();
672
+ next.add(asset2.id);
673
+ }
674
+ if (!onConfirm) {
675
+ const sel = toSelection(asset2);
676
+ if (multiple) {
677
+ if (next.has(asset2.id)) {
678
+ onSelect?.([sel]);
679
+ }
680
+ } else {
681
+ onSelect?.(next.has(asset2.id) ? sel : { id: "", url: "", name: "" });
682
+ }
683
+ }
684
+ return next;
685
+ });
686
+ }, [multiple, onSelect, onConfirm, toSelection]);
687
+ const handleUploadFiles = useCallback(async (files) => {
688
+ for (const file of files) {
689
+ const result = await upload(file);
690
+ if (result && !multiple) {
691
+ setSelectedIds(/* @__PURE__ */ new Set([result.id]));
692
+ onSelect?.(toSelection(result));
693
+ }
694
+ }
695
+ setTab("browse");
696
+ }, [upload, multiple, onSelect, toSelection]);
697
+ const handleUrlImport = useCallback(async (url) => {
698
+ const result = await uploadFromUrl(url);
699
+ if (result) {
700
+ setTab("browse");
701
+ if (!multiple) {
702
+ setSelectedIds(/* @__PURE__ */ new Set([result.id]));
703
+ onSelect?.(toSelection(result));
704
+ }
705
+ }
706
+ return result;
707
+ }, [uploadFromUrl, multiple, onSelect, toSelection]);
708
+ const handleDelete = useCallback(async (assetId) => {
709
+ setSelectedIds((prev) => {
710
+ const next = new Set(prev);
711
+ next.delete(assetId);
712
+ return next;
713
+ });
714
+ }, []);
715
+ const handleConfirm = useCallback(() => {
716
+ const selectedAssets = assets.filter((a) => selectedIds.has(a.id)).map(toSelection);
717
+ onConfirm?.(selectedAssets);
718
+ }, [assets, selectedIds, onConfirm, toSelection]);
719
+ const tabs = [
720
+ { key: "browse", label: "Browse", show: true },
721
+ { key: "upload", label: "Upload", show: allowUpload },
722
+ { key: "url", label: "URL", show: allowUrlImport }
723
+ ];
724
+ return /* @__PURE__ */ jsxs("div", { className: "smartlinks-ui-asset-picker-content flex flex-col gap-3", children: [
725
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [
726
+ /* @__PURE__ */ jsx("div", { className: "flex gap-1 bg-muted rounded-md p-0.5", children: tabs.filter((t) => t.show).map((t) => /* @__PURE__ */ jsx(
727
+ "button",
728
+ {
729
+ onClick: () => setTab(t.key),
730
+ className: cn(
731
+ "px-2.5 py-1 text-xs font-medium rounded transition-colors flex items-center gap-1",
732
+ tab === t.key ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
733
+ ),
734
+ children: t.label
735
+ },
736
+ t.key
737
+ )) }),
738
+ /* @__PURE__ */ jsx("div", { className: "flex-1" }),
739
+ tab === "browse" && shouldShowFilter && /* @__PURE__ */ jsxs("div", { className: "relative flex items-center gap-1", children: [
740
+ /* @__PURE__ */ jsx(Filter, { className: "w-3.5 h-3.5 text-muted-foreground" }),
741
+ /* @__PURE__ */ jsx(
742
+ "select",
743
+ {
744
+ value: mimeFilter,
745
+ onChange: (e) => setMimeFilter(e.target.value),
746
+ className: "text-xs py-1 pl-1 pr-5 rounded-md border border-border bg-transparent text-foreground focus:outline-none focus:ring-1 focus:ring-ring appearance-none cursor-pointer",
747
+ children: ASSET_MIME_FILTERS.map((f) => /* @__PURE__ */ jsx("option", { value: f.value, children: f.label }, f.value))
748
+ }
749
+ )
750
+ ] }),
751
+ tab === "browse" && /* @__PURE__ */ jsxs("div", { className: "relative", children: [
752
+ /* @__PURE__ */ jsx(Search, { className: "absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground" }),
753
+ /* @__PURE__ */ jsx(
754
+ "input",
755
+ {
756
+ type: "text",
757
+ value: search,
758
+ onChange: (e) => setSearch(e.target.value),
759
+ placeholder: "Search\u2026",
760
+ className: "pl-7 pr-2 py-1 text-xs rounded-md border border-border bg-transparent focus:outline-none focus:ring-1 focus:ring-ring w-36"
761
+ }
762
+ )
763
+ ] }),
764
+ tab === "browse" && /* @__PURE__ */ jsxs("div", { className: "flex gap-0.5 bg-muted rounded p-0.5", children: [
765
+ /* @__PURE__ */ jsx(
766
+ "button",
767
+ {
768
+ onClick: () => setViewMode("grid"),
769
+ className: cn("p-1 rounded", viewMode === "grid" ? "bg-background shadow-sm" : ""),
770
+ title: "Grid view",
771
+ children: /* @__PURE__ */ jsx(LayoutGrid, { className: "w-3.5 h-3.5 text-muted-foreground" })
772
+ }
773
+ ),
774
+ /* @__PURE__ */ jsx(
775
+ "button",
776
+ {
777
+ onClick: () => setViewMode("list"),
778
+ className: cn("p-1 rounded", viewMode === "list" ? "bg-background shadow-sm" : ""),
779
+ title: "List view",
780
+ children: /* @__PURE__ */ jsx(List, { className: "w-3.5 h-3.5 text-muted-foreground" })
781
+ }
782
+ )
783
+ ] })
784
+ ] }),
785
+ tab === "browse" && hasProductScope && /* @__PURE__ */ jsxs("div", { className: "flex gap-1 border-b border-border", children: [
786
+ /* @__PURE__ */ jsx(
787
+ "button",
788
+ {
789
+ onClick: () => setScopeTab("collection"),
790
+ className: cn(
791
+ "px-3 py-1.5 text-xs font-medium border-b-2 transition-colors -mb-px",
792
+ scopeTab === "collection" ? "border-primary text-primary" : "border-transparent text-muted-foreground hover:text-foreground"
793
+ ),
794
+ children: "Collection Assets"
795
+ }
796
+ ),
797
+ /* @__PURE__ */ jsx(
798
+ "button",
799
+ {
800
+ onClick: () => setScopeTab("product"),
801
+ className: cn(
802
+ "px-3 py-1.5 text-xs font-medium border-b-2 transition-colors -mb-px",
803
+ scopeTab === "product" ? "border-primary text-primary" : "border-transparent text-muted-foreground hover:text-foreground"
804
+ ),
805
+ children: "Product Assets"
806
+ }
807
+ )
808
+ ] }),
809
+ tab === "browse" && /* @__PURE__ */ jsx(
810
+ ScopedAssetBrowser,
811
+ {
812
+ scope: activeScope,
813
+ accept: effectiveAccept,
814
+ admin,
815
+ pageSize,
816
+ viewMode,
817
+ search,
818
+ selectedIds,
819
+ onToggleSelect: handleToggleSelect,
820
+ onDelete: handleDelete,
821
+ allowDelete,
822
+ emptyText
823
+ },
824
+ `${activeScope.type}-${activeScope.productId || ""}-${effectiveAccept || "all"}`
825
+ ),
826
+ tab === "upload" && /* @__PURE__ */ jsx(
827
+ UploadZone,
828
+ {
829
+ onFiles: handleUploadFiles,
830
+ accept: acceptProp,
831
+ multiple,
832
+ uploading,
833
+ uploadProgress
834
+ }
835
+ ),
836
+ tab === "url" && /* @__PURE__ */ jsx(
837
+ UrlImport,
838
+ {
839
+ onImport: handleUrlImport,
840
+ importing: uploading
841
+ }
842
+ ),
843
+ onConfirm && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between pt-2 border-t border-border", children: [
844
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
845
+ selectedIds.size,
846
+ " selected"
847
+ ] }),
848
+ /* @__PURE__ */ jsx(
849
+ "button",
850
+ {
851
+ onClick: handleConfirm,
852
+ disabled: selectedIds.size === 0,
853
+ className: cn(
854
+ "px-4 py-1.5 text-sm font-medium rounded-md transition-colors",
855
+ "bg-primary text-primary-foreground hover:bg-primary/90",
856
+ "disabled:opacity-50 disabled:cursor-not-allowed"
857
+ ),
858
+ children: "Confirm"
859
+ }
860
+ )
861
+ ] })
862
+ ] });
863
+ };
864
+ var PickerDialog = ({ open, onClose, children }) => {
865
+ if (!open) return null;
866
+ return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
867
+ /* @__PURE__ */ jsx(
868
+ "div",
869
+ {
870
+ className: "absolute inset-0 bg-black/50 backdrop-blur-sm",
871
+ onClick: onClose
872
+ }
873
+ ),
874
+ /* @__PURE__ */ jsxs("div", { className: "relative z-10 w-full max-w-2xl max-h-[80vh] bg-background rounded-xl shadow-2xl border border-border flex flex-col overflow-hidden mx-4", children: [
875
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3 border-b border-border", children: [
876
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Select Asset" }),
877
+ /* @__PURE__ */ jsx(
878
+ "button",
879
+ {
880
+ onClick: onClose,
881
+ className: "p-1 rounded hover:bg-accent transition-colors",
882
+ children: /* @__PURE__ */ jsx(X, { className: "w-4 h-4 text-muted-foreground" })
883
+ }
884
+ )
885
+ ] }),
886
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto p-4", children })
887
+ ] })
888
+ ] });
889
+ };
890
+ var AssetPicker = (props) => {
891
+ const { mode = "inline", open: controlledOpen, onClose, trigger, onSelect, multiple, className } = props;
892
+ const [internalOpen, setInternalOpen] = useState(false);
893
+ const isOpen = controlledOpen ?? internalOpen;
894
+ const handleOpen = useCallback(() => {
895
+ setInternalOpen(true);
896
+ }, []);
897
+ const handleClose = useCallback(() => {
898
+ setInternalOpen(false);
899
+ onClose?.();
900
+ }, [onClose]);
901
+ const handleConfirm = useCallback((selections) => {
902
+ if (multiple) {
903
+ onSelect?.(selections);
904
+ } else {
905
+ onSelect?.(selections[0] || { id: "", url: "", name: "" });
906
+ }
907
+ handleClose();
908
+ }, [onSelect, multiple, handleClose]);
909
+ if (mode === "inline") {
910
+ return /* @__PURE__ */ jsx("div", { className: cn("smartlinks-ui-asset-picker", className), children: /* @__PURE__ */ jsx(AssetPickerContent, { ...props }) });
911
+ }
912
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
913
+ trigger && /* @__PURE__ */ jsx("span", { onClick: handleOpen, className: "cursor-pointer", children: trigger }),
914
+ /* @__PURE__ */ jsx(PickerDialog, { open: isOpen, onClose: handleClose, children: /* @__PURE__ */ jsx(AssetPickerContent, { ...props, onConfirm: handleConfirm }) })
915
+ ] });
916
+ };
917
+
918
+ export { ASSET_MIME_FILTERS, AssetPicker, useAssets };
919
+ //# sourceMappingURL=chunk-WOCLZGRB.js.map
920
+ //# sourceMappingURL=chunk-WOCLZGRB.js.map