@proveanything/smartlinks-utils-ui 0.1.0 → 0.1.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.
@@ -1,920 +0,0 @@
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-blue-500 bg-blue-50 dark:bg-blue-950/30" : "border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600"
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-gray-100 dark:bg-gray-800 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-gray-400 dark:text-gray-500" }) }),
175
- /* @__PURE__ */ jsxs("div", { className: "p-2", children: [
176
- /* @__PURE__ */ jsx("p", { className: "text-xs font-medium truncate text-gray-700 dark:text-gray-300", title: asset2.name, children: asset2.cleanName || asset2.name || asset2.id }),
177
- /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-gray-400 dark:text-gray-500 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-blue-500 flex items-center justify-center", children: /* @__PURE__ */ jsx(Check, { className: "w-3 h-3 text-white" }) }),
183
- allowDelete && onDelete && /* @__PURE__ */ jsx(
184
- "button",
185
- {
186
- className: "absolute top-2 left-2 w-6 h-6 rounded-full bg-red-500/80 hover:bg-red-600 text-white 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-blue-500 bg-blue-50 dark:bg-blue-950/30" : "border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600"
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-gray-100 dark:bg-gray-800 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-gray-400" }) }),
221
- /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
222
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium truncate text-gray-700 dark:text-gray-300", children: asset2.cleanName || asset2.name || asset2.id }),
223
- /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-400 dark:text-gray-500", 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-blue-500 flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ jsx(Check, { className: "w-3 h-3 text-white" }) }),
229
- allowDelete && onDelete && /* @__PURE__ */ jsx(
230
- "button",
231
- {
232
- className: "w-6 h-6 rounded-full bg-red-500/80 hover:bg-red-600 text-white 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-blue-400 dark:border-blue-500 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-gray-200 dark:border-gray-700"
380
- }
381
- ) : /* @__PURE__ */ jsx("div", { className: "w-16 h-16 rounded-md bg-gray-100 dark:bg-gray-800 flex items-center justify-center", children: /* @__PURE__ */ jsx(Clipboard, { className: "w-6 h-6 text-gray-400" }) }),
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-gray-300 dark:border-gray-600 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-500 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-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 rounded hover:bg-gray-100 dark:hover:bg-gray-800 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-gray-400 dark:text-gray-500", 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-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 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-blue-500 text-white hover:bg-blue-600 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-blue-400 bg-blue-50 dark:bg-blue-950/30" : "border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500",
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-blue-500 animate-spin" }),
479
- /* @__PURE__ */ jsxs("p", { className: "text-sm text-gray-500 dark:text-gray-400", 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-gray-200 dark:bg-gray-700 rounded-full overflow-hidden", children: /* @__PURE__ */ jsx(
484
- "div",
485
- {
486
- className: "h-full bg-blue-500 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-gray-400 dark:text-gray-500" }),
492
- /* @__PURE__ */ jsxs("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: [
493
- "Drop files here, ",
494
- /* @__PURE__ */ jsx("span", { className: "text-blue-500 underline", children: "browse" }),
495
- ", or paste from clipboard"
496
- ] }),
497
- /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-gray-400 dark:text-gray-500 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-gray-400 dark:text-gray-500", 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-gray-400" }),
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-gray-400 dark:placeholder:text-gray-500",
547
- "focus:outline-none focus:ring-2 focus:ring-blue-500",
548
- error ? "border-red-400 dark:border-red-500" : "border-gray-300 dark:border-gray-600"
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-blue-500 text-white hover:bg-blue-600",
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-red-500 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-gray-400 animate-spin" }) });
584
- }
585
- if (error) {
586
- return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 p-3 rounded-md bg-red-50 dark:bg-red-950/30 text-red-600 dark:text-red-400 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-gray-400 dark:text-gray-500", 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-gray-100 dark:bg-gray-800 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-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm" : "text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300"
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-gray-400" }),
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-gray-300 dark:border-gray-600 bg-transparent text-gray-700 dark:text-gray-300 focus:outline-none focus:ring-1 focus:ring-blue-500 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-gray-400" }),
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-gray-300 dark:border-gray-600 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-500 w-36"
761
- }
762
- )
763
- ] }),
764
- tab === "browse" && /* @__PURE__ */ jsxs("div", { className: "flex gap-0.5 bg-gray-100 dark:bg-gray-800 rounded p-0.5", children: [
765
- /* @__PURE__ */ jsx(
766
- "button",
767
- {
768
- onClick: () => setViewMode("grid"),
769
- className: cn("p-1 rounded", viewMode === "grid" ? "bg-white dark:bg-gray-700 shadow-sm" : ""),
770
- title: "Grid view",
771
- children: /* @__PURE__ */ jsx(LayoutGrid, { className: "w-3.5 h-3.5 text-gray-600 dark:text-gray-400" })
772
- }
773
- ),
774
- /* @__PURE__ */ jsx(
775
- "button",
776
- {
777
- onClick: () => setViewMode("list"),
778
- className: cn("p-1 rounded", viewMode === "list" ? "bg-white dark:bg-gray-700 shadow-sm" : ""),
779
- title: "List view",
780
- children: /* @__PURE__ */ jsx(List, { className: "w-3.5 h-3.5 text-gray-600 dark:text-gray-400" })
781
- }
782
- )
783
- ] })
784
- ] }),
785
- tab === "browse" && hasProductScope && /* @__PURE__ */ jsxs("div", { className: "flex gap-1 border-b border-gray-200 dark:border-gray-700", 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-blue-500 text-blue-600 dark:text-blue-400" : "border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300"
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-blue-500 text-blue-600 dark:text-blue-400" : "border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300"
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-gray-200 dark:border-gray-700", children: [
844
- /* @__PURE__ */ jsxs("span", { className: "text-xs text-gray-500 dark:text-gray-400", 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-blue-500 text-white hover:bg-blue-600",
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-white dark:bg-gray-900 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700 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-gray-200 dark:border-gray-700", children: [
876
- /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: "Select Asset" }),
877
- /* @__PURE__ */ jsx(
878
- "button",
879
- {
880
- onClick: onClose,
881
- className: "p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors",
882
- children: /* @__PURE__ */ jsx(X, { className: "w-4 h-4 text-gray-500" })
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-V7JHAER7.js.map
920
- //# sourceMappingURL=chunk-V7JHAER7.js.map