@igorvaryvoda/sirv-upload-widget 0.1.0 → 0.1.2

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,1759 @@
1
+ import { useState, useRef, useCallback, useEffect } from 'react';
2
+ import clsx2 from 'clsx';
3
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
+
5
+ // src/components/SirvUploader.tsx
6
+
7
+ // src/utils/image-utils.ts
8
+ var HEIC_TYPES = ["image/heic", "image/heif"];
9
+ var IMAGE_EXTENSIONS = /\.(jpe?g|png|gif|webp|heic|heif|bmp|tiff?|avif)$/i;
10
+ var ACCEPTED_IMAGE_FORMATS = "image/jpeg,image/png,image/gif,image/webp,image/bmp,image/tiff,image/heic,image/heif,image/avif,.jpg,.jpeg,.png,.gif,.webp,.bmp,.tif,.tiff,.heic,.heif,.avif";
11
+ var DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
12
+ function isImageFile(file) {
13
+ if (file.type.startsWith("image/")) return true;
14
+ if (IMAGE_EXTENSIONS.test(file.name)) return true;
15
+ return false;
16
+ }
17
+ function isHeifFile(file) {
18
+ if (HEIC_TYPES.includes(file.type.toLowerCase())) return true;
19
+ if (/\.(heic|heif)$/i.test(file.name)) return true;
20
+ return false;
21
+ }
22
+ async function convertHeicToJpeg(file) {
23
+ const heic2any = (await import('heic2any')).default;
24
+ const blob = await heic2any({
25
+ blob: file,
26
+ toType: "image/jpeg",
27
+ quality: 0.92
28
+ });
29
+ const resultBlob = Array.isArray(blob) ? blob[0] : blob;
30
+ if (!resultBlob || resultBlob.size === 0) {
31
+ throw new Error("HEIC conversion produced empty result");
32
+ }
33
+ const newName = file.name.replace(/\.(heic|heif)$/i, ".jpg") || "converted.jpg";
34
+ return new File([resultBlob], newName.endsWith(".jpg") ? newName : `${newName}.jpg`, {
35
+ type: "image/jpeg"
36
+ });
37
+ }
38
+ async function convertHeicWithFallback(file, serverEndpoint) {
39
+ try {
40
+ return await convertHeicToJpeg(file);
41
+ } catch (primaryError) {
42
+ console.warn("Primary HEIC conversion (heic2any) failed:", primaryError);
43
+ try {
44
+ const img = new Image();
45
+ const canvas = document.createElement("canvas");
46
+ const ctx = canvas.getContext("2d");
47
+ if (!ctx) throw new Error("Canvas not supported");
48
+ const objectUrl = URL.createObjectURL(file);
49
+ await new Promise((resolve, reject) => {
50
+ img.onload = () => resolve();
51
+ img.onerror = () => reject(new Error("Browser cannot decode HEIC natively"));
52
+ img.src = objectUrl;
53
+ });
54
+ canvas.width = img.naturalWidth;
55
+ canvas.height = img.naturalHeight;
56
+ ctx.drawImage(img, 0, 0);
57
+ URL.revokeObjectURL(objectUrl);
58
+ const blob = await new Promise((resolve) => {
59
+ canvas.toBlob(resolve, "image/jpeg", 0.92);
60
+ });
61
+ if (!blob || blob.size === 0) {
62
+ throw new Error("Canvas conversion produced empty result");
63
+ }
64
+ const newName = file.name.replace(/\.(heic|heif)$/i, ".jpg") || "converted.jpg";
65
+ return new File([blob], newName.endsWith(".jpg") ? newName : `${newName}.jpg`, {
66
+ type: "image/jpeg"
67
+ });
68
+ } catch (canvasError) {
69
+ console.warn("Canvas fallback failed:", canvasError);
70
+ if (serverEndpoint) {
71
+ try {
72
+ const formData = new FormData();
73
+ formData.append("file", file);
74
+ const response = await fetch(serverEndpoint, {
75
+ method: "POST",
76
+ body: formData
77
+ });
78
+ if (!response.ok) {
79
+ throw new Error(`Server returned ${response.status}`);
80
+ }
81
+ const { dataUrl, filename } = await response.json();
82
+ const base64Data = dataUrl.split(",")[1];
83
+ const binaryString = atob(base64Data);
84
+ const bytes = new Uint8Array(binaryString.length);
85
+ for (let i = 0; i < binaryString.length; i++) {
86
+ bytes[i] = binaryString.charCodeAt(i);
87
+ }
88
+ const blob = new Blob([bytes], { type: "image/jpeg" });
89
+ return new File([blob], filename, { type: "image/jpeg" });
90
+ } catch (serverError) {
91
+ console.warn("Server-side HEIC conversion failed:", serverError);
92
+ }
93
+ }
94
+ const primaryMsg = primaryError instanceof Error ? primaryError.message : String(primaryError);
95
+ throw new Error(
96
+ `Unable to convert HEIC image (${primaryMsg}). Please export as JPEG from your Photos app.`
97
+ );
98
+ }
99
+ }
100
+ }
101
+ function generateId() {
102
+ const array = new Uint8Array(8);
103
+ crypto.getRandomValues(array);
104
+ const randomPart = Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
105
+ return `${Date.now()}-${randomPart}`;
106
+ }
107
+ function validateFileSize(file, maxSize = DEFAULT_MAX_FILE_SIZE) {
108
+ if (file.size > maxSize) {
109
+ return {
110
+ valid: false,
111
+ error: `File too large. Maximum size is ${Math.round(maxSize / 1024 / 1024)}MB, got ${(file.size / 1024 / 1024).toFixed(1)}MB`
112
+ };
113
+ }
114
+ return { valid: true };
115
+ }
116
+ async function getImageDimensions(file) {
117
+ return new Promise((resolve) => {
118
+ const img = new Image();
119
+ const url = URL.createObjectURL(file);
120
+ img.onload = () => {
121
+ URL.revokeObjectURL(url);
122
+ resolve({ width: img.naturalWidth, height: img.naturalHeight });
123
+ };
124
+ img.onerror = () => {
125
+ URL.revokeObjectURL(url);
126
+ resolve(null);
127
+ };
128
+ img.src = url;
129
+ });
130
+ }
131
+ function formatFileSize(bytes) {
132
+ if (bytes < 1024) return `${bytes} B`;
133
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
134
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
135
+ }
136
+ function getFileExtension(filename) {
137
+ const match = filename.match(/\.([^.]+)$/);
138
+ return match ? match[1].toLowerCase() : "";
139
+ }
140
+ function getMimeType(file) {
141
+ if (file.type) return file.type;
142
+ const ext = getFileExtension(file.name);
143
+ const mimeTypes = {
144
+ jpg: "image/jpeg",
145
+ jpeg: "image/jpeg",
146
+ png: "image/png",
147
+ gif: "image/gif",
148
+ webp: "image/webp",
149
+ avif: "image/avif",
150
+ bmp: "image/bmp",
151
+ tif: "image/tiff",
152
+ tiff: "image/tiff",
153
+ heic: "image/heic",
154
+ heif: "image/heif"
155
+ };
156
+ return mimeTypes[ext] || "application/octet-stream";
157
+ }
158
+
159
+ // src/utils/csv-parser.ts
160
+ var DELIMITERS = [",", " ", ";", "|"];
161
+ function detectDelimiter(csvContent) {
162
+ const lines = csvContent.trim().split(/\r?\n/).slice(0, 5);
163
+ if (lines.length === 0) return ",";
164
+ const delimiterCounts = {
165
+ ",": [],
166
+ " ": [],
167
+ ";": [],
168
+ "|": []
169
+ };
170
+ for (const line of lines) {
171
+ let inQuotes = false;
172
+ const counts = { ",": 0, " ": 0, ";": 0, "|": 0 };
173
+ for (const char of line) {
174
+ if (char === '"') {
175
+ inQuotes = !inQuotes;
176
+ } else if (!inQuotes) {
177
+ if (char in counts) {
178
+ counts[char]++;
179
+ }
180
+ }
181
+ }
182
+ for (const delim of DELIMITERS) {
183
+ delimiterCounts[delim].push(counts[delim]);
184
+ }
185
+ }
186
+ let bestDelimiter = ",";
187
+ let bestScore = -1;
188
+ for (const delim of DELIMITERS) {
189
+ const counts = delimiterCounts[delim];
190
+ if (counts.length === 0) continue;
191
+ const nonZero = counts.filter((c) => c > 0);
192
+ if (nonZero.length === 0) continue;
193
+ const allSame = nonZero.every((c) => c === nonZero[0]);
194
+ const avgCount = nonZero.reduce((a, b) => a + b, 0) / nonZero.length;
195
+ const coverage = nonZero.length / counts.length;
196
+ const score = (allSame ? 100 : 0) + avgCount * coverage;
197
+ if (score > bestScore && avgCount > 0) {
198
+ bestScore = score;
199
+ bestDelimiter = delim;
200
+ }
201
+ }
202
+ return bestDelimiter;
203
+ }
204
+ function parseCsvRow(line, delimiter = ",") {
205
+ const result = [];
206
+ let current = "";
207
+ let inQuotes = false;
208
+ for (let i = 0; i < line.length; i++) {
209
+ const char = line[i];
210
+ if (char === '"') {
211
+ inQuotes = !inQuotes;
212
+ } else if (char === delimiter && !inQuotes) {
213
+ result.push(current.trim().replace(/^"|"$/g, ""));
214
+ current = "";
215
+ } else {
216
+ current += char;
217
+ }
218
+ }
219
+ result.push(current.trim().replace(/^"|"$/g, ""));
220
+ return result;
221
+ }
222
+ function splitMultipleUrls(cellValue) {
223
+ if (!cellValue) return [];
224
+ return cellValue.split(",").map((url) => url.trim()).filter((url) => url.length > 0 && url.startsWith("http"));
225
+ }
226
+ function extractPathFromUrl(url) {
227
+ try {
228
+ return new URL(url).pathname;
229
+ } catch {
230
+ return url;
231
+ }
232
+ }
233
+ function cellToString(cell) {
234
+ if (cell == null) return "";
235
+ if (typeof cell === "object" && "text" in cell) return cell.text;
236
+ if (typeof cell === "object" && "result" in cell) return String(cell.result ?? "");
237
+ return String(cell);
238
+ }
239
+ function getCsvHeaders(csvContent, delimiter) {
240
+ const lines = csvContent.trim().split(/\r?\n/);
241
+ if (lines.length === 0) return [];
242
+ const delim = delimiter ?? detectDelimiter(csvContent);
243
+ return parseCsvRow(lines[0], delim);
244
+ }
245
+ function findColumnIndex(headers, column) {
246
+ const headersLower = headers.map((h) => h.toLowerCase());
247
+ if (column) {
248
+ let columnIndex = headersLower.indexOf(column.toLowerCase());
249
+ if (columnIndex !== -1) return columnIndex;
250
+ columnIndex = headers.indexOf(column);
251
+ if (columnIndex !== -1) return columnIndex;
252
+ }
253
+ return headersLower.findIndex((h) => h === "url");
254
+ }
255
+ var defaultUrlValidator = (url) => {
256
+ try {
257
+ const parsed = new URL(url);
258
+ if (!["http:", "https:"].includes(parsed.protocol)) {
259
+ return { valid: false, error: "URL must use http or https protocol" };
260
+ }
261
+ return { valid: true };
262
+ } catch {
263
+ return { valid: false, error: "Invalid URL format" };
264
+ }
265
+ };
266
+ var sirvUrlValidator = (url) => {
267
+ const sirvMatch = url.match(/^https?:\/\/([^.]+)\.sirv\.com(\/[^?#]+)/);
268
+ if (sirvMatch) {
269
+ return { valid: true };
270
+ }
271
+ const imageMatch = url.match(
272
+ /^https?:\/\/[^/]+(\/[^?#]+\.(jpe?g|png|gif|webp|avif|bmp|tiff?))$/i
273
+ );
274
+ if (imageMatch) {
275
+ return { valid: true };
276
+ }
277
+ return { valid: false, error: "Not a valid Sirv or image URL" };
278
+ };
279
+ async function parseExcelArrayBuffer(arrayBuffer) {
280
+ const ExcelJS = await import('exceljs');
281
+ const workbook = new ExcelJS.default.Workbook();
282
+ await workbook.xlsx.load(arrayBuffer);
283
+ const worksheet = workbook.worksheets[0];
284
+ if (!worksheet) return [];
285
+ const rows = [];
286
+ worksheet.eachRow((row) => {
287
+ const values = row.values;
288
+ rows.push(values.slice(1));
289
+ });
290
+ return rows;
291
+ }
292
+ function getExcelHeaders(rows) {
293
+ if (rows.length === 0) return [];
294
+ return rows[0].map((cell, i) => cellToString(cell) || `Column ${i + 1}`);
295
+ }
296
+ function getExcelSampleRows(rows, urlColumnIndex) {
297
+ if (rows.length < 2) return [];
298
+ const sampleRows = [];
299
+ for (let i = 1; i < rows.length && sampleRows.length < 3; i++) {
300
+ const urlCell = cellToString(rows[i][urlColumnIndex]).trim();
301
+ if (urlCell && urlCell.length > 0) {
302
+ sampleRows.push(rows[i].map((cell) => cellToString(cell)));
303
+ }
304
+ }
305
+ if (sampleRows.length === 0) {
306
+ return rows.slice(1, 4).map((row) => row.map((cell) => cellToString(cell)));
307
+ }
308
+ return sampleRows;
309
+ }
310
+ function getCsvSampleRows(lines, urlColumnIndex, delimiter = ",") {
311
+ if (lines.length < 2) return [];
312
+ const sampleRows = [];
313
+ for (let i = 1; i < lines.length && sampleRows.length < 3; i++) {
314
+ const row = parseCsvRow(lines[i], delimiter);
315
+ const urlCell = row[urlColumnIndex]?.trim();
316
+ if (urlCell && urlCell.length > 0) {
317
+ sampleRows.push(row);
318
+ }
319
+ }
320
+ if (sampleRows.length === 0) {
321
+ for (let i = 1; i <= Math.min(3, lines.length - 1); i++) {
322
+ sampleRows.push(parseCsvRow(lines[i], delimiter));
323
+ }
324
+ }
325
+ return sampleRows;
326
+ }
327
+ function estimateExcelImageCount(rows, columnIndex) {
328
+ if (rows.length < 2 || columnIndex < 0) return rows.length - 1;
329
+ let count = 0;
330
+ for (let i = 1; i < rows.length; i++) {
331
+ const cellValue = cellToString(rows[i][columnIndex]).trim();
332
+ if (cellValue) {
333
+ count += splitMultipleUrls(cellValue).length;
334
+ }
335
+ }
336
+ return count || rows.length - 1;
337
+ }
338
+ function estimateCsvImageCount(lines, columnIndex, delimiter = ",") {
339
+ if (lines.length < 2 || columnIndex < 0) return lines.length - 1;
340
+ let count = 0;
341
+ for (let i = 1; i < lines.length; i++) {
342
+ const values = parseCsvRow(lines[i], delimiter);
343
+ const cellValue = values[columnIndex]?.trim();
344
+ if (cellValue) {
345
+ count += splitMultipleUrls(cellValue).length;
346
+ }
347
+ }
348
+ return count || lines.length - 1;
349
+ }
350
+ function parseCsvClient(csvContent, options = {}) {
351
+ const { validator = defaultUrlValidator } = options;
352
+ const lines = csvContent.trim().split(/\r?\n/);
353
+ const delimiter = detectDelimiter(csvContent);
354
+ const headers = getCsvHeaders(csvContent, delimiter);
355
+ const rowCount = lines.length - 1;
356
+ const headersLower = headers.map((h) => h.toLowerCase());
357
+ let urlColumnIndex = headersLower.findIndex(
358
+ (h) => h === "url" || h === "image" || h === "images" || h === "image_url"
359
+ );
360
+ if (urlColumnIndex === -1) {
361
+ for (let col = 0; col < headers.length && urlColumnIndex === -1; col++) {
362
+ for (let row = 1; row < Math.min(100, lines.length); row++) {
363
+ const values = parseCsvRow(lines[row], delimiter);
364
+ const cell = values[col]?.trim();
365
+ if (cell && cell.startsWith("http")) {
366
+ urlColumnIndex = col;
367
+ break;
368
+ }
369
+ }
370
+ }
371
+ }
372
+ if (urlColumnIndex === -1) urlColumnIndex = 0;
373
+ const sampleRows = getCsvSampleRows(lines, urlColumnIndex, delimiter);
374
+ const estimatedImageCounts = headers.map(
375
+ (_, colIndex) => estimateCsvImageCount(lines, colIndex, delimiter)
376
+ );
377
+ if (options.previewOnly) {
378
+ return {
379
+ headers,
380
+ sampleRows,
381
+ rowCount,
382
+ estimatedImageCounts,
383
+ urls: [],
384
+ validCount: 0,
385
+ invalidCount: 0,
386
+ totalCount: 0
387
+ };
388
+ }
389
+ const columnIndex = findColumnIndex(headers, options.column);
390
+ if (columnIndex === -1) {
391
+ throw new Error("Column not found in CSV");
392
+ }
393
+ const urls = [];
394
+ for (let i = 1; i < lines.length; i++) {
395
+ const values = parseCsvRow(lines[i], delimiter);
396
+ const cellValue = values[columnIndex]?.trim();
397
+ if (cellValue) {
398
+ for (const url of splitMultipleUrls(cellValue)) {
399
+ const validation = validator(url);
400
+ urls.push({
401
+ url,
402
+ path: extractPathFromUrl(url),
403
+ valid: validation.valid,
404
+ error: validation.error
405
+ });
406
+ }
407
+ }
408
+ }
409
+ const validCount = urls.filter((u) => u.valid).length;
410
+ return {
411
+ headers,
412
+ sampleRows,
413
+ rowCount,
414
+ estimatedImageCounts,
415
+ urls,
416
+ validCount,
417
+ invalidCount: urls.length - validCount,
418
+ totalCount: urls.length
419
+ };
420
+ }
421
+ async function parseExcelClient(arrayBuffer, options = {}) {
422
+ const { validator = defaultUrlValidator } = options;
423
+ const rows = await parseExcelArrayBuffer(arrayBuffer);
424
+ const headers = getExcelHeaders(rows);
425
+ const rowCount = rows.length - 1;
426
+ const headersLower = headers.map((h) => h.toLowerCase());
427
+ let urlColumnIndex = headersLower.findIndex(
428
+ (h) => h === "url" || h === "image" || h === "images" || h === "image_url"
429
+ );
430
+ if (urlColumnIndex === -1) {
431
+ for (let col = 0; col < headers.length && urlColumnIndex === -1; col++) {
432
+ for (let row = 1; row < Math.min(100, rows.length); row++) {
433
+ const cell = cellToString(rows[row][col]).trim();
434
+ if (cell && cell.startsWith("http")) {
435
+ urlColumnIndex = col;
436
+ break;
437
+ }
438
+ }
439
+ }
440
+ }
441
+ if (urlColumnIndex === -1) urlColumnIndex = 0;
442
+ const sampleRows = getExcelSampleRows(rows, urlColumnIndex);
443
+ const estimatedImageCounts = headers.map(
444
+ (_, colIndex) => estimateExcelImageCount(rows, colIndex)
445
+ );
446
+ if (options.previewOnly) {
447
+ return {
448
+ headers,
449
+ sampleRows,
450
+ rowCount,
451
+ estimatedImageCounts,
452
+ urls: [],
453
+ validCount: 0,
454
+ invalidCount: 0,
455
+ totalCount: 0
456
+ };
457
+ }
458
+ const columnIndex = findColumnIndex(headers, options.column);
459
+ if (columnIndex === -1) {
460
+ throw new Error("Column not found in Excel file");
461
+ }
462
+ const urls = [];
463
+ for (let i = 1; i < rows.length; i++) {
464
+ const cellValue = cellToString(rows[i][columnIndex]).trim();
465
+ if (cellValue) {
466
+ for (const url of splitMultipleUrls(cellValue)) {
467
+ const validation = validator(url);
468
+ urls.push({
469
+ url,
470
+ path: extractPathFromUrl(url),
471
+ valid: validation.valid,
472
+ error: validation.error
473
+ });
474
+ }
475
+ }
476
+ }
477
+ const validCount = urls.filter((u) => u.valid).length;
478
+ return {
479
+ headers,
480
+ sampleRows,
481
+ rowCount,
482
+ estimatedImageCounts,
483
+ urls,
484
+ validCount,
485
+ invalidCount: urls.length - validCount,
486
+ totalCount: urls.length
487
+ };
488
+ }
489
+ function isSpreadsheetFile(file) {
490
+ const ext = file.name.toLowerCase();
491
+ return ext.endsWith(".csv") || ext.endsWith(".xlsx") || ext.endsWith(".xls") || ext.endsWith(".txt") || file.type === "text/csv" || file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" || file.type === "application/vnd.ms-excel";
492
+ }
493
+ function DropZone({
494
+ onFiles,
495
+ onSpreadsheet,
496
+ accept = ["image/*"],
497
+ maxFiles = 50,
498
+ maxFileSize = 10 * 1024 * 1024,
499
+ disabled = false,
500
+ compact = false,
501
+ className,
502
+ labels = {},
503
+ children
504
+ }) {
505
+ const [isDragOver, setIsDragOver] = useState(false);
506
+ const [isConverting, setIsConverting] = useState(false);
507
+ const [convertingCount, setConvertingCount] = useState(0);
508
+ const inputRef = useRef(null);
509
+ const processFiles = useCallback(
510
+ async (fileList) => {
511
+ const files = Array.from(fileList).slice(0, maxFiles);
512
+ const spreadsheetFile = files.find(isSpreadsheetFile);
513
+ if (spreadsheetFile && onSpreadsheet) {
514
+ onSpreadsheet(spreadsheetFile);
515
+ return;
516
+ }
517
+ const imageFiles = files.filter(isImageFile);
518
+ if (imageFiles.length === 0) return;
519
+ const heifFiles = imageFiles.filter(isHeifFile);
520
+ const regularFiles = imageFiles.filter((f) => !isHeifFile(f));
521
+ setIsConverting(heifFiles.length > 0);
522
+ setConvertingCount(heifFiles.length);
523
+ const processedFiles = [];
524
+ for (const file of regularFiles) {
525
+ const sizeValidation = validateFileSize(file, maxFileSize);
526
+ if (!sizeValidation.valid) {
527
+ processedFiles.push({
528
+ id: generateId(),
529
+ file,
530
+ filename: file.name,
531
+ previewUrl: "",
532
+ status: "error",
533
+ progress: 0,
534
+ error: sizeValidation.error
535
+ });
536
+ continue;
537
+ }
538
+ const dimensions = await getImageDimensions(file);
539
+ processedFiles.push({
540
+ id: generateId(),
541
+ file,
542
+ filename: file.name,
543
+ previewUrl: URL.createObjectURL(file),
544
+ dimensions: dimensions || void 0,
545
+ size: file.size,
546
+ status: "pending",
547
+ progress: 0
548
+ });
549
+ }
550
+ for (const file of heifFiles) {
551
+ try {
552
+ const converted = await convertHeicWithFallback(file);
553
+ const sizeValidation = validateFileSize(converted, maxFileSize);
554
+ if (!sizeValidation.valid) {
555
+ processedFiles.push({
556
+ id: generateId(),
557
+ file: converted,
558
+ filename: converted.name,
559
+ previewUrl: "",
560
+ status: "error",
561
+ progress: 0,
562
+ error: sizeValidation.error
563
+ });
564
+ continue;
565
+ }
566
+ const dimensions = await getImageDimensions(converted);
567
+ processedFiles.push({
568
+ id: generateId(),
569
+ file: converted,
570
+ filename: converted.name,
571
+ previewUrl: URL.createObjectURL(converted),
572
+ dimensions: dimensions || void 0,
573
+ size: converted.size,
574
+ status: "pending",
575
+ progress: 0
576
+ });
577
+ } catch (err) {
578
+ processedFiles.push({
579
+ id: generateId(),
580
+ file,
581
+ filename: file.name,
582
+ previewUrl: "",
583
+ status: "error",
584
+ progress: 0,
585
+ error: err instanceof Error ? err.message : "Failed to convert HEIC file"
586
+ });
587
+ }
588
+ setConvertingCount((c) => c - 1);
589
+ }
590
+ setIsConverting(false);
591
+ setConvertingCount(0);
592
+ if (processedFiles.length > 0) {
593
+ onFiles(processedFiles);
594
+ }
595
+ },
596
+ [maxFiles, maxFileSize, onFiles, onSpreadsheet]
597
+ );
598
+ const handleDragOver = useCallback(
599
+ (e) => {
600
+ e.preventDefault();
601
+ e.stopPropagation();
602
+ if (!disabled) {
603
+ setIsDragOver(true);
604
+ }
605
+ },
606
+ [disabled]
607
+ );
608
+ const handleDragLeave = useCallback((e) => {
609
+ e.preventDefault();
610
+ e.stopPropagation();
611
+ setIsDragOver(false);
612
+ }, []);
613
+ const handleDrop = useCallback(
614
+ async (e) => {
615
+ e.preventDefault();
616
+ e.stopPropagation();
617
+ setIsDragOver(false);
618
+ if (disabled) return;
619
+ const { files } = e.dataTransfer;
620
+ if (files.length > 0) {
621
+ await processFiles(files);
622
+ }
623
+ },
624
+ [disabled, processFiles]
625
+ );
626
+ const handleChange = useCallback(
627
+ async (e) => {
628
+ const { files } = e.target;
629
+ if (files && files.length > 0) {
630
+ await processFiles(files);
631
+ }
632
+ e.target.value = "";
633
+ },
634
+ [processFiles]
635
+ );
636
+ const handleClick = useCallback(() => {
637
+ if (!disabled) {
638
+ inputRef.current?.click();
639
+ }
640
+ }, [disabled]);
641
+ const handleKeyDown = useCallback(
642
+ (e) => {
643
+ if ((e.key === "Enter" || e.key === " ") && !disabled) {
644
+ e.preventDefault();
645
+ inputRef.current?.click();
646
+ }
647
+ },
648
+ [disabled]
649
+ );
650
+ const acceptString = accept.join(",") || ACCEPTED_IMAGE_FORMATS;
651
+ return /* @__PURE__ */ jsxs(
652
+ "div",
653
+ {
654
+ className: clsx2(
655
+ "sirv-dropzone",
656
+ isDragOver && "sirv-dropzone--drag-over",
657
+ disabled && "sirv-dropzone--disabled",
658
+ compact && "sirv-dropzone--compact",
659
+ isConverting && "sirv-dropzone--converting",
660
+ className
661
+ ),
662
+ onDragOver: handleDragOver,
663
+ onDragLeave: handleDragLeave,
664
+ onDrop: handleDrop,
665
+ onClick: handleClick,
666
+ onKeyDown: handleKeyDown,
667
+ role: "button",
668
+ tabIndex: disabled ? -1 : 0,
669
+ "aria-disabled": disabled,
670
+ "aria-label": labels.dropzone || "Drop files here or click to browse",
671
+ children: [
672
+ /* @__PURE__ */ jsx(
673
+ "input",
674
+ {
675
+ ref: inputRef,
676
+ type: "file",
677
+ accept: acceptString,
678
+ multiple: maxFiles > 1,
679
+ onChange: handleChange,
680
+ disabled,
681
+ className: "sirv-dropzone__input",
682
+ "aria-hidden": "true"
683
+ }
684
+ ),
685
+ children || /* @__PURE__ */ jsx("div", { className: "sirv-dropzone__content", children: isConverting ? /* @__PURE__ */ jsxs(Fragment, { children: [
686
+ /* @__PURE__ */ jsx("div", { className: "sirv-dropzone__spinner" }),
687
+ /* @__PURE__ */ jsxs("p", { className: "sirv-dropzone__text", children: [
688
+ "Converting ",
689
+ convertingCount,
690
+ " HEIC file",
691
+ convertingCount !== 1 ? "s" : "",
692
+ "..."
693
+ ] })
694
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
695
+ /* @__PURE__ */ jsxs(
696
+ "svg",
697
+ {
698
+ className: "sirv-dropzone__icon",
699
+ viewBox: "0 0 24 24",
700
+ fill: "none",
701
+ stroke: "currentColor",
702
+ strokeWidth: "2",
703
+ strokeLinecap: "round",
704
+ strokeLinejoin: "round",
705
+ children: [
706
+ /* @__PURE__ */ jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
707
+ /* @__PURE__ */ jsx("polyline", { points: "17 8 12 3 7 8" }),
708
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "3", x2: "12", y2: "15" })
709
+ ]
710
+ }
711
+ ),
712
+ /* @__PURE__ */ jsx("p", { className: "sirv-dropzone__text", children: labels.dropzone || "Drop files here or click to browse" }),
713
+ !compact && /* @__PURE__ */ jsx("p", { className: "sirv-dropzone__hint", children: labels.dropzoneHint || "Supports JPG, PNG, WebP, GIF, HEIC up to 10MB" })
714
+ ] }) })
715
+ ]
716
+ }
717
+ );
718
+ }
719
+ function FileList({
720
+ files,
721
+ onRemove,
722
+ onRetry,
723
+ showThumbnails = true,
724
+ className,
725
+ labels = {}
726
+ }) {
727
+ if (files.length === 0) return null;
728
+ return /* @__PURE__ */ jsx("div", { className: clsx2("sirv-filelist", className), children: files.map((file) => /* @__PURE__ */ jsx(
729
+ FileItem,
730
+ {
731
+ file,
732
+ onRemove,
733
+ onRetry,
734
+ showThumbnail: showThumbnails,
735
+ labels
736
+ },
737
+ file.id
738
+ )) });
739
+ }
740
+ function FileItem({ file, onRemove, onRetry, showThumbnail, labels = {} }) {
741
+ const statusText = {
742
+ pending: "",
743
+ uploading: labels.uploading || "Uploading...",
744
+ processing: labels.processing || "Processing...",
745
+ success: labels.success || "Uploaded",
746
+ error: labels.error || "Failed",
747
+ conflict: "Conflict"
748
+ };
749
+ return /* @__PURE__ */ jsxs(
750
+ "div",
751
+ {
752
+ className: clsx2(
753
+ "sirv-filelist__item",
754
+ `sirv-filelist__item--${file.status}`,
755
+ file.error && "sirv-filelist__item--has-error"
756
+ ),
757
+ children: [
758
+ showThumbnail && file.previewUrl && /* @__PURE__ */ jsx("div", { className: "sirv-filelist__thumbnail", children: /* @__PURE__ */ jsx("img", { src: file.previewUrl, alt: "" }) }),
759
+ /* @__PURE__ */ jsxs("div", { className: "sirv-filelist__info", children: [
760
+ /* @__PURE__ */ jsx("div", { className: "sirv-filelist__name", title: file.filename, children: file.filename }),
761
+ /* @__PURE__ */ jsxs("div", { className: "sirv-filelist__meta", children: [
762
+ file.size && /* @__PURE__ */ jsx("span", { className: "sirv-filelist__size", children: formatFileSize(file.size) }),
763
+ file.dimensions && /* @__PURE__ */ jsxs("span", { className: "sirv-filelist__dimensions", children: [
764
+ file.dimensions.width,
765
+ " \xD7 ",
766
+ file.dimensions.height
767
+ ] }),
768
+ file.status !== "pending" && /* @__PURE__ */ jsx("span", { className: `sirv-filelist__status sirv-filelist__status--${file.status}`, children: statusText[file.status] })
769
+ ] }),
770
+ file.error && /* @__PURE__ */ jsx("div", { className: "sirv-filelist__error", children: file.error })
771
+ ] }),
772
+ (file.status === "uploading" || file.status === "processing") && /* @__PURE__ */ jsx("div", { className: "sirv-filelist__progress", children: /* @__PURE__ */ jsx(
773
+ "div",
774
+ {
775
+ className: "sirv-filelist__progress-bar",
776
+ style: { width: `${file.progress}%` }
777
+ }
778
+ ) }),
779
+ /* @__PURE__ */ jsxs("div", { className: "sirv-filelist__actions", children: [
780
+ file.status === "error" && onRetry && /* @__PURE__ */ jsx(
781
+ "button",
782
+ {
783
+ type: "button",
784
+ className: "sirv-filelist__action sirv-filelist__action--retry",
785
+ onClick: () => onRetry(file.id),
786
+ "aria-label": labels.retry || "Retry upload",
787
+ title: labels.retry || "Retry",
788
+ children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
789
+ /* @__PURE__ */ jsx("path", { d: "M1 4v6h6" }),
790
+ /* @__PURE__ */ jsx("path", { d: "M3.51 15a9 9 0 1 0 2.13-9.36L1 10" })
791
+ ] })
792
+ }
793
+ ),
794
+ onRemove && file.status !== "uploading" && /* @__PURE__ */ jsx(
795
+ "button",
796
+ {
797
+ type: "button",
798
+ className: "sirv-filelist__action sirv-filelist__action--remove",
799
+ onClick: () => onRemove(file.id),
800
+ "aria-label": labels.remove || "Remove file",
801
+ title: labels.remove || "Remove",
802
+ children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
803
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
804
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
805
+ ] })
806
+ }
807
+ ),
808
+ file.status === "success" && /* @__PURE__ */ jsx("span", { className: "sirv-filelist__check", children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" }) }) })
809
+ ] })
810
+ ]
811
+ }
812
+ );
813
+ }
814
+ function FileListSummary({ files, className }) {
815
+ const pending = files.filter((f) => f.status === "pending").length;
816
+ const uploading = files.filter((f) => f.status === "uploading" || f.status === "processing").length;
817
+ const success = files.filter((f) => f.status === "success").length;
818
+ const error = files.filter((f) => f.status === "error").length;
819
+ if (files.length === 0) return null;
820
+ return /* @__PURE__ */ jsxs("div", { className: clsx2("sirv-filelist-summary", className), children: [
821
+ /* @__PURE__ */ jsxs("span", { className: "sirv-filelist-summary__total", children: [
822
+ files.length,
823
+ " files"
824
+ ] }),
825
+ pending > 0 && /* @__PURE__ */ jsxs("span", { className: "sirv-filelist-summary__pending", children: [
826
+ pending,
827
+ " pending"
828
+ ] }),
829
+ uploading > 0 && /* @__PURE__ */ jsxs("span", { className: "sirv-filelist-summary__uploading", children: [
830
+ uploading,
831
+ " uploading"
832
+ ] }),
833
+ success > 0 && /* @__PURE__ */ jsxs("span", { className: "sirv-filelist-summary__success", children: [
834
+ success,
835
+ " uploaded"
836
+ ] }),
837
+ error > 0 && /* @__PURE__ */ jsxs("span", { className: "sirv-filelist-summary__error", children: [
838
+ error,
839
+ " failed"
840
+ ] })
841
+ ] });
842
+ }
843
+ function FilePicker({
844
+ endpoint,
845
+ isOpen,
846
+ onClose,
847
+ onSelect,
848
+ fileType = "image",
849
+ multiple = false,
850
+ initialPath = "/",
851
+ className,
852
+ labels = {}
853
+ }) {
854
+ const [currentPath, setCurrentPath] = useState(initialPath);
855
+ const [items, setItems] = useState([]);
856
+ const [selectedItems, setSelectedItems] = useState([]);
857
+ const [isLoading, setIsLoading] = useState(false);
858
+ const [error, setError] = useState(null);
859
+ const [searchQuery, setSearchQuery] = useState("");
860
+ const searchTimeoutRef = useRef(null);
861
+ const fetchItems = useCallback(
862
+ async (path, search) => {
863
+ setIsLoading(true);
864
+ setError(null);
865
+ try {
866
+ const params = new URLSearchParams({ path });
867
+ if (fileType !== "all") params.set("type", fileType);
868
+ if (search) params.set("search", search);
869
+ const response = await fetch(`${endpoint}/browse?${params}`);
870
+ if (!response.ok) {
871
+ throw new Error(`Failed to load files: ${response.status}`);
872
+ }
873
+ const data = await response.json();
874
+ if (!data.success) {
875
+ throw new Error(data.error || "Failed to load files");
876
+ }
877
+ setItems(data.items || []);
878
+ setCurrentPath(data.path);
879
+ } catch (err) {
880
+ setError(err instanceof Error ? err.message : "Failed to load files");
881
+ setItems([]);
882
+ } finally {
883
+ setIsLoading(false);
884
+ }
885
+ },
886
+ [endpoint, fileType]
887
+ );
888
+ useEffect(() => {
889
+ if (isOpen) {
890
+ setSelectedItems([]);
891
+ setSearchQuery("");
892
+ fetchItems(initialPath);
893
+ }
894
+ }, [isOpen, initialPath, fetchItems]);
895
+ useEffect(() => {
896
+ if (!isOpen) return;
897
+ if (searchTimeoutRef.current) {
898
+ clearTimeout(searchTimeoutRef.current);
899
+ }
900
+ searchTimeoutRef.current = setTimeout(() => {
901
+ fetchItems(currentPath, searchQuery || void 0);
902
+ }, 300);
903
+ return () => {
904
+ if (searchTimeoutRef.current) {
905
+ clearTimeout(searchTimeoutRef.current);
906
+ }
907
+ };
908
+ }, [searchQuery, currentPath, isOpen, fetchItems]);
909
+ const handleNavigate = useCallback((path) => {
910
+ setSearchQuery("");
911
+ setCurrentPath(path);
912
+ }, []);
913
+ const handleGoUp = useCallback(() => {
914
+ const parentPath = currentPath.split("/").slice(0, -1).join("/") || "/";
915
+ handleNavigate(parentPath);
916
+ }, [currentPath, handleNavigate]);
917
+ const handleItemClick = useCallback(
918
+ (item) => {
919
+ if (item.type === "folder") {
920
+ handleNavigate(item.path);
921
+ return;
922
+ }
923
+ if (multiple) {
924
+ setSelectedItems((prev) => {
925
+ const isSelected = prev.some((i) => i.path === item.path);
926
+ if (isSelected) {
927
+ return prev.filter((i) => i.path !== item.path);
928
+ }
929
+ return [...prev, item];
930
+ });
931
+ } else {
932
+ setSelectedItems([item]);
933
+ }
934
+ },
935
+ [multiple, handleNavigate]
936
+ );
937
+ const handleSelect = useCallback(() => {
938
+ if (selectedItems.length > 0) {
939
+ onSelect(selectedItems);
940
+ onClose();
941
+ }
942
+ }, [selectedItems, onSelect, onClose]);
943
+ const handleKeyDown = useCallback(
944
+ (e) => {
945
+ if (e.key === "Escape") {
946
+ onClose();
947
+ }
948
+ },
949
+ [onClose]
950
+ );
951
+ const breadcrumbs = currentPath.split("/").filter(Boolean);
952
+ if (!isOpen) return null;
953
+ return /* @__PURE__ */ jsx(
954
+ "div",
955
+ {
956
+ className: clsx2("sirv-filepicker-overlay", className),
957
+ onClick: onClose,
958
+ onKeyDown: handleKeyDown,
959
+ role: "dialog",
960
+ "aria-modal": "true",
961
+ "aria-label": labels.title || "Select files from Sirv",
962
+ children: /* @__PURE__ */ jsxs("div", { className: "sirv-filepicker", onClick: (e) => e.stopPropagation(), children: [
963
+ /* @__PURE__ */ jsxs("div", { className: "sirv-filepicker__header", children: [
964
+ /* @__PURE__ */ jsx("h2", { className: "sirv-filepicker__title", children: labels.title || "Select from Sirv" }),
965
+ /* @__PURE__ */ jsx(
966
+ "button",
967
+ {
968
+ type: "button",
969
+ className: "sirv-filepicker__close",
970
+ onClick: onClose,
971
+ "aria-label": "Close",
972
+ children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
973
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
974
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
975
+ ] })
976
+ }
977
+ )
978
+ ] }),
979
+ /* @__PURE__ */ jsxs("div", { className: "sirv-filepicker__toolbar", children: [
980
+ /* @__PURE__ */ jsxs("div", { className: "sirv-filepicker__breadcrumbs", children: [
981
+ /* @__PURE__ */ jsx(
982
+ "button",
983
+ {
984
+ type: "button",
985
+ className: "sirv-filepicker__breadcrumb",
986
+ onClick: () => handleNavigate("/"),
987
+ children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("path", { d: "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }) })
988
+ }
989
+ ),
990
+ breadcrumbs.map((part, index) => /* @__PURE__ */ jsxs("span", { children: [
991
+ /* @__PURE__ */ jsx("span", { className: "sirv-filepicker__breadcrumb-separator", children: "/" }),
992
+ /* @__PURE__ */ jsx(
993
+ "button",
994
+ {
995
+ type: "button",
996
+ className: "sirv-filepicker__breadcrumb",
997
+ onClick: () => handleNavigate("/" + breadcrumbs.slice(0, index + 1).join("/")),
998
+ children: part
999
+ }
1000
+ )
1001
+ ] }, index))
1002
+ ] }),
1003
+ /* @__PURE__ */ jsx("div", { className: "sirv-filepicker__search", children: /* @__PURE__ */ jsx(
1004
+ "input",
1005
+ {
1006
+ type: "text",
1007
+ value: searchQuery,
1008
+ onChange: (e) => setSearchQuery(e.target.value),
1009
+ placeholder: labels.search || "Search...",
1010
+ className: "sirv-filepicker__search-input"
1011
+ }
1012
+ ) })
1013
+ ] }),
1014
+ /* @__PURE__ */ jsx("div", { className: "sirv-filepicker__content", children: isLoading ? /* @__PURE__ */ jsxs("div", { className: "sirv-filepicker__loading", children: [
1015
+ /* @__PURE__ */ jsx("div", { className: "sirv-filepicker__spinner" }),
1016
+ /* @__PURE__ */ jsx("p", { children: labels.loading || "Loading..." })
1017
+ ] }) : error ? /* @__PURE__ */ jsxs("div", { className: "sirv-filepicker__error", children: [
1018
+ /* @__PURE__ */ jsx("p", { children: error }),
1019
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => fetchItems(currentPath), children: "Retry" })
1020
+ ] }) : items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "sirv-filepicker__empty", children: /* @__PURE__ */ jsx("p", { children: labels.empty || "No files found" }) }) : /* @__PURE__ */ jsxs("div", { className: "sirv-filepicker__grid", children: [
1021
+ currentPath !== "/" && /* @__PURE__ */ jsxs(
1022
+ "button",
1023
+ {
1024
+ type: "button",
1025
+ className: "sirv-filepicker__item sirv-filepicker__item--folder",
1026
+ onClick: handleGoUp,
1027
+ children: [
1028
+ /* @__PURE__ */ jsx("div", { className: "sirv-filepicker__item-icon", children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("path", { d: "M15 18l-6-6 6-6" }) }) }),
1029
+ /* @__PURE__ */ jsx("div", { className: "sirv-filepicker__item-name", children: ".." })
1030
+ ]
1031
+ }
1032
+ ),
1033
+ items.map((item) => {
1034
+ const isSelected = selectedItems.some((i) => i.path === item.path);
1035
+ return /* @__PURE__ */ jsxs(
1036
+ "button",
1037
+ {
1038
+ type: "button",
1039
+ className: clsx2(
1040
+ "sirv-filepicker__item",
1041
+ `sirv-filepicker__item--${item.type}`,
1042
+ isSelected && "sirv-filepicker__item--selected"
1043
+ ),
1044
+ onClick: () => handleItemClick(item),
1045
+ children: [
1046
+ item.type === "folder" ? /* @__PURE__ */ jsx("div", { className: "sirv-filepicker__item-icon", children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", 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" }) }) }) : item.thumbnail ? /* @__PURE__ */ jsx("div", { className: "sirv-filepicker__item-thumbnail", children: /* @__PURE__ */ jsx("img", { src: item.thumbnail, alt: "" }) }) : /* @__PURE__ */ jsx("div", { className: "sirv-filepicker__item-icon", children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
1047
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
1048
+ /* @__PURE__ */ jsx("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
1049
+ /* @__PURE__ */ jsx("polyline", { points: "21 15 16 10 5 21" })
1050
+ ] }) }),
1051
+ /* @__PURE__ */ jsx("div", { className: "sirv-filepicker__item-name", title: item.name, children: item.name }),
1052
+ item.size && /* @__PURE__ */ jsx("div", { className: "sirv-filepicker__item-size", children: formatFileSize(item.size) }),
1053
+ isSelected && /* @__PURE__ */ jsx("div", { className: "sirv-filepicker__item-check", children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3", children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" }) }) })
1054
+ ]
1055
+ },
1056
+ item.path
1057
+ );
1058
+ })
1059
+ ] }) }),
1060
+ /* @__PURE__ */ jsxs("div", { className: "sirv-filepicker__footer", children: [
1061
+ /* @__PURE__ */ jsx("span", { className: "sirv-filepicker__selection-count", children: selectedItems.length > 0 ? `${selectedItems.length} file${selectedItems.length !== 1 ? "s" : ""} selected` : "No files selected" }),
1062
+ /* @__PURE__ */ jsxs("div", { className: "sirv-filepicker__actions", children: [
1063
+ /* @__PURE__ */ jsx("button", { type: "button", className: "sirv-filepicker__btn", onClick: onClose, children: labels.cancel || "Cancel" }),
1064
+ /* @__PURE__ */ jsx(
1065
+ "button",
1066
+ {
1067
+ type: "button",
1068
+ className: "sirv-filepicker__btn sirv-filepicker__btn--primary",
1069
+ onClick: handleSelect,
1070
+ disabled: selectedItems.length === 0,
1071
+ children: labels.select || "Select"
1072
+ }
1073
+ )
1074
+ ] })
1075
+ ] })
1076
+ ] })
1077
+ }
1078
+ );
1079
+ }
1080
+ function SpreadsheetImport({
1081
+ onUrls,
1082
+ className,
1083
+ labels = {}
1084
+ }) {
1085
+ const [isDragOver, setIsDragOver] = useState(false);
1086
+ const [isLoading, setIsLoading] = useState(false);
1087
+ const [result, setResult] = useState(null);
1088
+ const [error, setError] = useState(null);
1089
+ const [selectedColumn, setSelectedColumn] = useState("");
1090
+ const inputRef = useRef(null);
1091
+ const processFile = useCallback(async (file) => {
1092
+ setIsLoading(true);
1093
+ setError(null);
1094
+ setResult(null);
1095
+ try {
1096
+ let parseResult;
1097
+ if (file.name.endsWith(".csv") || file.name.endsWith(".txt")) {
1098
+ const text = await file.text();
1099
+ parseResult = parseCsvClient(text, { previewOnly: true });
1100
+ } else if (file.name.endsWith(".xlsx") || file.name.endsWith(".xls")) {
1101
+ const buffer = await file.arrayBuffer();
1102
+ parseResult = await parseExcelClient(buffer, { previewOnly: true });
1103
+ } else {
1104
+ throw new Error("Unsupported file type. Please use CSV, XLSX, or TXT.");
1105
+ }
1106
+ setResult(parseResult);
1107
+ const maxIndex = parseResult.estimatedImageCounts.indexOf(
1108
+ Math.max(...parseResult.estimatedImageCounts)
1109
+ );
1110
+ if (maxIndex >= 0 && parseResult.headers[maxIndex]) {
1111
+ setSelectedColumn(parseResult.headers[maxIndex]);
1112
+ }
1113
+ } catch (err) {
1114
+ setError(err instanceof Error ? err.message : "Failed to parse file");
1115
+ } finally {
1116
+ setIsLoading(false);
1117
+ }
1118
+ }, []);
1119
+ const handleDragOver = useCallback((e) => {
1120
+ e.preventDefault();
1121
+ setIsDragOver(true);
1122
+ }, []);
1123
+ const handleDragLeave = useCallback((e) => {
1124
+ e.preventDefault();
1125
+ setIsDragOver(false);
1126
+ }, []);
1127
+ const handleDrop = useCallback(
1128
+ async (e) => {
1129
+ e.preventDefault();
1130
+ setIsDragOver(false);
1131
+ const file = e.dataTransfer.files[0];
1132
+ if (file && isSpreadsheetFile(file)) {
1133
+ await processFile(file);
1134
+ } else {
1135
+ setError("Please drop a CSV or Excel file");
1136
+ }
1137
+ },
1138
+ [processFile]
1139
+ );
1140
+ const handleChange = useCallback(
1141
+ async (e) => {
1142
+ const file = e.target.files?.[0];
1143
+ if (file) {
1144
+ await processFile(file);
1145
+ }
1146
+ e.target.value = "";
1147
+ },
1148
+ [processFile]
1149
+ );
1150
+ const handleImport = useCallback(async () => {
1151
+ if (!result || !selectedColumn) return;
1152
+ setIsLoading(true);
1153
+ try {
1154
+ const validUrls = result.urls.filter((u) => u.valid).map((u) => u.url);
1155
+ onUrls(validUrls);
1156
+ setResult(null);
1157
+ setSelectedColumn("");
1158
+ } catch (err) {
1159
+ setError(err instanceof Error ? err.message : "Failed to import URLs");
1160
+ } finally {
1161
+ setIsLoading(false);
1162
+ }
1163
+ }, [result, selectedColumn, onUrls]);
1164
+ const handleClear = useCallback(() => {
1165
+ setResult(null);
1166
+ setSelectedColumn("");
1167
+ setError(null);
1168
+ }, []);
1169
+ return /* @__PURE__ */ jsx("div", { className: clsx2("sirv-spreadsheet", className), children: !result ? /* @__PURE__ */ jsxs(Fragment, { children: [
1170
+ /* @__PURE__ */ jsxs(
1171
+ "div",
1172
+ {
1173
+ className: clsx2(
1174
+ "sirv-spreadsheet__drop",
1175
+ isDragOver && "sirv-spreadsheet__drop--active"
1176
+ ),
1177
+ onDragOver: handleDragOver,
1178
+ onDragLeave: handleDragLeave,
1179
+ onDrop: handleDrop,
1180
+ onClick: () => inputRef.current?.click(),
1181
+ role: "button",
1182
+ tabIndex: 0,
1183
+ onKeyDown: (e) => {
1184
+ if (e.key === "Enter" || e.key === " ") {
1185
+ inputRef.current?.click();
1186
+ }
1187
+ },
1188
+ children: [
1189
+ /* @__PURE__ */ jsx(
1190
+ "input",
1191
+ {
1192
+ ref: inputRef,
1193
+ type: "file",
1194
+ accept: ".csv,.xlsx,.xls,.txt",
1195
+ onChange: handleChange,
1196
+ style: { display: "none" }
1197
+ }
1198
+ ),
1199
+ isLoading ? /* @__PURE__ */ jsx("div", { className: "sirv-dropzone__spinner" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1200
+ /* @__PURE__ */ jsxs(
1201
+ "svg",
1202
+ {
1203
+ className: "sirv-spreadsheet__icon",
1204
+ viewBox: "0 0 24 24",
1205
+ fill: "none",
1206
+ stroke: "currentColor",
1207
+ strokeWidth: "2",
1208
+ children: [
1209
+ /* @__PURE__ */ jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
1210
+ /* @__PURE__ */ jsx("polyline", { points: "14 2 14 8 20 8" }),
1211
+ /* @__PURE__ */ jsx("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
1212
+ /* @__PURE__ */ jsx("line", { x1: "16", y1: "17", x2: "8", y2: "17" }),
1213
+ /* @__PURE__ */ jsx("polyline", { points: "10 9 9 9 8 9" })
1214
+ ]
1215
+ }
1216
+ ),
1217
+ /* @__PURE__ */ jsx("p", { className: "sirv-spreadsheet__text", children: labels.drop || "Drop CSV or Excel file here" }),
1218
+ /* @__PURE__ */ jsx("p", { className: "sirv-spreadsheet__hint", children: labels.hint || "File should contain a column with image URLs" })
1219
+ ] })
1220
+ ]
1221
+ }
1222
+ ),
1223
+ error && /* @__PURE__ */ jsx("div", { className: "sirv-filelist__error", style: { padding: "8px 16px" }, children: error })
1224
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "sirv-spreadsheet__preview", children: [
1225
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: "12px" }, children: [
1226
+ /* @__PURE__ */ jsx("label", { style: { display: "block", marginBottom: "4px", fontWeight: 500 }, children: "Select URL column:" }),
1227
+ /* @__PURE__ */ jsxs(
1228
+ "select",
1229
+ {
1230
+ value: selectedColumn,
1231
+ onChange: (e) => setSelectedColumn(e.target.value),
1232
+ style: {
1233
+ width: "100%",
1234
+ padding: "8px",
1235
+ borderRadius: "4px",
1236
+ border: "1px solid var(--sirv-border)"
1237
+ },
1238
+ children: [
1239
+ /* @__PURE__ */ jsx("option", { value: "", children: "Select a column" }),
1240
+ result.headers.map((header, i) => /* @__PURE__ */ jsxs("option", { value: header, children: [
1241
+ header,
1242
+ " (",
1243
+ result.estimatedImageCounts[i],
1244
+ " URLs)"
1245
+ ] }, i))
1246
+ ]
1247
+ }
1248
+ )
1249
+ ] }),
1250
+ /* @__PURE__ */ jsxs("table", { className: "sirv-spreadsheet__table", children: [
1251
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: result.headers.map((header, i) => /* @__PURE__ */ jsx(
1252
+ "th",
1253
+ {
1254
+ style: {
1255
+ background: header === selectedColumn ? "var(--sirv-primary-light)" : void 0
1256
+ },
1257
+ children: header
1258
+ },
1259
+ i
1260
+ )) }) }),
1261
+ /* @__PURE__ */ jsx("tbody", { children: result.sampleRows.slice(0, 3).map((row, i) => /* @__PURE__ */ jsx("tr", { children: row.map((cell, j) => /* @__PURE__ */ jsx(
1262
+ "td",
1263
+ {
1264
+ style: {
1265
+ background: result.headers[j] === selectedColumn ? "var(--sirv-primary-light)" : void 0,
1266
+ maxWidth: "200px",
1267
+ overflow: "hidden",
1268
+ textOverflow: "ellipsis",
1269
+ whiteSpace: "nowrap"
1270
+ },
1271
+ children: cell
1272
+ },
1273
+ j
1274
+ )) }, i)) })
1275
+ ] }),
1276
+ /* @__PURE__ */ jsxs("div", { className: "sirv-spreadsheet__stats", children: [
1277
+ /* @__PURE__ */ jsxs("span", { children: [
1278
+ "Total rows: ",
1279
+ result.rowCount
1280
+ ] }),
1281
+ selectedColumn && /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("span", { className: "sirv-spreadsheet__stat--valid", children: [
1282
+ labels.validUrls || "Valid URLs",
1283
+ ":",
1284
+ " ",
1285
+ result.estimatedImageCounts[result.headers.indexOf(selectedColumn)] || 0
1286
+ ] }) })
1287
+ ] }),
1288
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px", marginTop: "12px" }, children: [
1289
+ /* @__PURE__ */ jsx("button", { type: "button", className: "sirv-btn", onClick: handleClear, children: labels.clear || "Clear" }),
1290
+ /* @__PURE__ */ jsx(
1291
+ "button",
1292
+ {
1293
+ type: "button",
1294
+ className: "sirv-btn sirv-btn--primary",
1295
+ onClick: handleImport,
1296
+ disabled: !selectedColumn || isLoading,
1297
+ children: isLoading ? "Importing..." : labels.import || "Import URLs"
1298
+ }
1299
+ )
1300
+ ] })
1301
+ ] }) });
1302
+ }
1303
+ function useSirvUpload(options) {
1304
+ const {
1305
+ presignEndpoint,
1306
+ proxyEndpoint,
1307
+ folder,
1308
+ onConflict,
1309
+ concurrency,
1310
+ autoUpload,
1311
+ onUpload,
1312
+ onError
1313
+ } = options;
1314
+ const [files, setFiles] = useState([]);
1315
+ const abortControllers = useRef(/* @__PURE__ */ new Map());
1316
+ const uploadQueue = useRef([]);
1317
+ const activeUploads = useRef(0);
1318
+ const updateFile = useCallback((id, updates) => {
1319
+ setFiles(
1320
+ (prev) => prev.map((f) => f.id === id ? { ...f, ...updates } : f)
1321
+ );
1322
+ }, []);
1323
+ const uploadWithPresign = useCallback(
1324
+ async (file, signal) => {
1325
+ if (!presignEndpoint) throw new Error("No presign endpoint configured");
1326
+ if (!file.file) throw new Error("No file data");
1327
+ const presignRes = await fetch(presignEndpoint, {
1328
+ method: "POST",
1329
+ headers: { "Content-Type": "application/json" },
1330
+ body: JSON.stringify({
1331
+ filename: file.filename,
1332
+ contentType: getMimeType(file.file),
1333
+ folder,
1334
+ size: file.file.size
1335
+ }),
1336
+ signal
1337
+ });
1338
+ if (!presignRes.ok) {
1339
+ const err = await presignRes.json().catch(() => ({}));
1340
+ throw new Error(err.error || `Failed to get upload URL: ${presignRes.status}`);
1341
+ }
1342
+ const { uploadUrl, publicUrl, path, error } = await presignRes.json();
1343
+ if (error) throw new Error(error);
1344
+ if (!uploadUrl) throw new Error("No upload URL returned");
1345
+ updateFile(file.id, { status: "uploading", progress: 10 });
1346
+ const uploadRes = await fetch(uploadUrl, {
1347
+ method: "PUT",
1348
+ body: file.file,
1349
+ headers: {
1350
+ "Content-Type": getMimeType(file.file)
1351
+ },
1352
+ signal
1353
+ });
1354
+ if (!uploadRes.ok) {
1355
+ throw new Error(`Upload failed: ${uploadRes.status}`);
1356
+ }
1357
+ updateFile(file.id, {
1358
+ status: "success",
1359
+ progress: 100,
1360
+ sirvUrl: publicUrl,
1361
+ sirvPath: path
1362
+ });
1363
+ },
1364
+ [presignEndpoint, folder, updateFile]
1365
+ );
1366
+ const uploadWithProxy = useCallback(
1367
+ async (file, signal) => {
1368
+ if (!proxyEndpoint) throw new Error("No proxy endpoint configured");
1369
+ if (!file.file) throw new Error("No file data");
1370
+ updateFile(file.id, { status: "uploading", progress: 10 });
1371
+ const arrayBuffer = await file.file.arrayBuffer();
1372
+ const base64 = btoa(
1373
+ new Uint8Array(arrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), "")
1374
+ );
1375
+ updateFile(file.id, { progress: 30 });
1376
+ const res = await fetch(`${proxyEndpoint}/upload`, {
1377
+ method: "POST",
1378
+ headers: { "Content-Type": "application/json" },
1379
+ body: JSON.stringify({
1380
+ data: base64,
1381
+ filename: file.filename,
1382
+ folder,
1383
+ contentType: getMimeType(file.file),
1384
+ onConflict: onConflict === "ask" ? "rename" : onConflict
1385
+ }),
1386
+ signal
1387
+ });
1388
+ updateFile(file.id, { progress: 80 });
1389
+ if (!res.ok) {
1390
+ const err = await res.json().catch(() => ({}));
1391
+ throw new Error(err.error || `Upload failed: ${res.status}`);
1392
+ }
1393
+ const result = await res.json();
1394
+ if (!result.success) {
1395
+ throw new Error(result.error || "Upload failed");
1396
+ }
1397
+ updateFile(file.id, {
1398
+ status: "success",
1399
+ progress: 100,
1400
+ sirvUrl: result.url,
1401
+ sirvPath: result.path
1402
+ });
1403
+ },
1404
+ [proxyEndpoint, folder, onConflict, updateFile]
1405
+ );
1406
+ const uploadFile = useCallback(
1407
+ async (id) => {
1408
+ const file = files.find((f) => f.id === id);
1409
+ if (!file || file.status === "uploading" || file.status === "success") return;
1410
+ const controller = new AbortController();
1411
+ abortControllers.current.set(id, controller);
1412
+ try {
1413
+ updateFile(id, { status: "uploading", progress: 0, error: void 0 });
1414
+ if (presignEndpoint) {
1415
+ await uploadWithPresign(file, controller.signal);
1416
+ } else if (proxyEndpoint) {
1417
+ await uploadWithProxy(file, controller.signal);
1418
+ } else {
1419
+ throw new Error("No upload endpoint configured");
1420
+ }
1421
+ const updatedFile = files.find((f) => f.id === id);
1422
+ if (updatedFile && onUpload) {
1423
+ onUpload([{ ...updatedFile, status: "success" }]);
1424
+ }
1425
+ } catch (err) {
1426
+ if (err instanceof Error && err.name === "AbortError") {
1427
+ updateFile(id, { status: "pending", progress: 0 });
1428
+ return;
1429
+ }
1430
+ const errorMsg = err instanceof Error ? err.message : "Upload failed";
1431
+ updateFile(id, { status: "error", progress: 0, error: errorMsg });
1432
+ onError?.(errorMsg, file);
1433
+ } finally {
1434
+ abortControllers.current.delete(id);
1435
+ activeUploads.current--;
1436
+ processQueue();
1437
+ }
1438
+ },
1439
+ [files, presignEndpoint, proxyEndpoint, uploadWithPresign, uploadWithProxy, updateFile, onUpload, onError]
1440
+ );
1441
+ const processQueue = useCallback(() => {
1442
+ while (activeUploads.current < concurrency && uploadQueue.current.length > 0) {
1443
+ const id = uploadQueue.current.shift();
1444
+ if (id) {
1445
+ activeUploads.current++;
1446
+ uploadFile(id);
1447
+ }
1448
+ }
1449
+ }, [concurrency, uploadFile]);
1450
+ const uploadAll = useCallback(async () => {
1451
+ const pendingFiles = files.filter((f) => f.status === "pending" || f.status === "error");
1452
+ uploadQueue.current = pendingFiles.map((f) => f.id);
1453
+ processQueue();
1454
+ }, [files, processQueue]);
1455
+ const addFiles = useCallback(
1456
+ (newFiles) => {
1457
+ setFiles((prev) => [...prev, ...newFiles]);
1458
+ if (autoUpload) {
1459
+ uploadQueue.current.push(...newFiles.map((f) => f.id));
1460
+ processQueue();
1461
+ }
1462
+ },
1463
+ [autoUpload, processQueue]
1464
+ );
1465
+ const addUrls = useCallback(
1466
+ (urls) => {
1467
+ const newFiles = urls.map((url) => {
1468
+ const filename = url.split("/").pop() || "image.jpg";
1469
+ return {
1470
+ id: generateId(),
1471
+ filename,
1472
+ previewUrl: url,
1473
+ sirvUrl: url,
1474
+ status: "success",
1475
+ progress: 100
1476
+ };
1477
+ });
1478
+ setFiles((prev) => [...prev, ...newFiles]);
1479
+ },
1480
+ []
1481
+ );
1482
+ const removeFile = useCallback((id) => {
1483
+ const controller = abortControllers.current.get(id);
1484
+ if (controller) {
1485
+ controller.abort();
1486
+ }
1487
+ uploadQueue.current = uploadQueue.current.filter((qid) => qid !== id);
1488
+ setFiles((prev) => prev.filter((f) => f.id !== id));
1489
+ }, []);
1490
+ const clearFiles = useCallback(() => {
1491
+ abortControllers.current.forEach((controller) => controller.abort());
1492
+ abortControllers.current.clear();
1493
+ uploadQueue.current = [];
1494
+ activeUploads.current = 0;
1495
+ setFiles([]);
1496
+ }, []);
1497
+ const retryFile = useCallback(
1498
+ async (id) => {
1499
+ uploadQueue.current.push(id);
1500
+ processQueue();
1501
+ },
1502
+ [processQueue]
1503
+ );
1504
+ const cancelUpload = useCallback((id) => {
1505
+ const controller = abortControllers.current.get(id);
1506
+ if (controller) {
1507
+ controller.abort();
1508
+ }
1509
+ }, []);
1510
+ const progress = files.length > 0 ? Math.round(files.reduce((sum, f) => sum + f.progress, 0) / files.length) : 0;
1511
+ const isUploading = files.some((f) => f.status === "uploading" || f.status === "processing");
1512
+ const isComplete = files.length > 0 && files.every((f) => f.status === "success");
1513
+ return {
1514
+ files,
1515
+ addFiles,
1516
+ addUrls,
1517
+ removeFile,
1518
+ clearFiles,
1519
+ uploadAll,
1520
+ uploadFile,
1521
+ retryFile,
1522
+ cancelUpload,
1523
+ progress,
1524
+ isUploading,
1525
+ isComplete
1526
+ };
1527
+ }
1528
+ var DEFAULT_LABELS = {
1529
+ dropzone: "Drop files here or click to browse",
1530
+ dropzoneHint: "Supports JPG, PNG, WebP, GIF, HEIC up to 10MB",
1531
+ browse: "Browse",
1532
+ uploadFiles: "Upload Files",
1533
+ importUrls: "Import URLs",
1534
+ selectFromSirv: "Select from Sirv",
1535
+ uploading: "Uploading...",
1536
+ processing: "Processing...",
1537
+ success: "Uploaded",
1538
+ error: "Failed",
1539
+ retry: "Retry",
1540
+ remove: "Remove",
1541
+ cancel: "Cancel",
1542
+ overwrite: "Overwrite",
1543
+ rename: "Rename",
1544
+ skip: "Skip",
1545
+ conflictTitle: "File exists",
1546
+ conflictMessage: "A file with this name already exists."
1547
+ };
1548
+ function SirvUploader({
1549
+ presignEndpoint,
1550
+ proxyEndpoint,
1551
+ sirvAccount,
1552
+ folder = "/",
1553
+ onUpload,
1554
+ onError,
1555
+ onSelect,
1556
+ onRemove,
1557
+ features = {},
1558
+ maxFiles = 50,
1559
+ maxFileSize = 10 * 1024 * 1024,
1560
+ accept = ["image/*"],
1561
+ onConflict = "rename",
1562
+ autoUpload = true,
1563
+ concurrency = 3,
1564
+ className,
1565
+ disabled = false,
1566
+ compact = false,
1567
+ labels: customLabels = {},
1568
+ children
1569
+ }) {
1570
+ const labels = { ...DEFAULT_LABELS, ...customLabels };
1571
+ const {
1572
+ batch = true,
1573
+ csvImport = true,
1574
+ filePicker = true,
1575
+ dragDrop = true
1576
+ } = features;
1577
+ const [activeTab, setActiveTab] = useState("upload");
1578
+ const [isPickerOpen, setIsPickerOpen] = useState(false);
1579
+ if (!presignEndpoint && !proxyEndpoint) {
1580
+ console.warn("SirvUploader: Either presignEndpoint or proxyEndpoint must be provided");
1581
+ }
1582
+ const upload = useSirvUpload({
1583
+ presignEndpoint,
1584
+ proxyEndpoint,
1585
+ folder,
1586
+ onConflict,
1587
+ concurrency,
1588
+ autoUpload,
1589
+ onUpload,
1590
+ onError
1591
+ });
1592
+ const handleFiles = useCallback(
1593
+ (files) => {
1594
+ upload.addFiles(files);
1595
+ onSelect?.(files);
1596
+ },
1597
+ [upload, onSelect]
1598
+ );
1599
+ const handleSpreadsheet = useCallback(() => {
1600
+ setActiveTab("urls");
1601
+ }, []);
1602
+ const handleUrls = useCallback(
1603
+ (urls) => {
1604
+ upload.addUrls(urls);
1605
+ },
1606
+ [upload]
1607
+ );
1608
+ const handlePickerSelect = useCallback(
1609
+ (items) => {
1610
+ const files = items.map((item) => ({
1611
+ id: generateId(),
1612
+ filename: item.name,
1613
+ previewUrl: item.thumbnail || "",
1614
+ sirvUrl: `https://${sirvAccount}.sirv.com${item.path}`,
1615
+ sirvPath: item.path,
1616
+ size: item.size,
1617
+ status: "success",
1618
+ progress: 100
1619
+ }));
1620
+ upload.addFiles(files);
1621
+ onSelect?.(files);
1622
+ },
1623
+ [sirvAccount, upload, onSelect]
1624
+ );
1625
+ const handleRemove = useCallback(
1626
+ (id) => {
1627
+ const file = upload.files.find((f) => f.id === id);
1628
+ upload.removeFile(id);
1629
+ if (file) onRemove?.(file);
1630
+ },
1631
+ [upload, onRemove]
1632
+ );
1633
+ const handleUploadAll = useCallback(() => {
1634
+ upload.uploadAll();
1635
+ }, [upload]);
1636
+ const handleClearAll = useCallback(() => {
1637
+ upload.clearFiles();
1638
+ }, [upload]);
1639
+ const hasFiles = upload.files.length > 0;
1640
+ const hasPendingFiles = upload.files.some((f) => f.status === "pending" || f.status === "error");
1641
+ const showTabs = csvImport && batch;
1642
+ const browseEndpoint = proxyEndpoint || (presignEndpoint ? presignEndpoint.replace(/\/presign$/, "") : "");
1643
+ return /* @__PURE__ */ jsxs("div", { className: clsx2("sirv-uploader", className), children: [
1644
+ showTabs && /* @__PURE__ */ jsxs("div", { className: "sirv-tabs", children: [
1645
+ /* @__PURE__ */ jsx(
1646
+ "button",
1647
+ {
1648
+ type: "button",
1649
+ className: clsx2("sirv-tabs__tab", activeTab === "upload" && "sirv-tabs__tab--active"),
1650
+ onClick: () => setActiveTab("upload"),
1651
+ children: labels.uploadFiles
1652
+ }
1653
+ ),
1654
+ /* @__PURE__ */ jsx(
1655
+ "button",
1656
+ {
1657
+ type: "button",
1658
+ className: clsx2("sirv-tabs__tab", activeTab === "urls" && "sirv-tabs__tab--active"),
1659
+ onClick: () => setActiveTab("urls"),
1660
+ children: labels.importUrls
1661
+ }
1662
+ )
1663
+ ] }),
1664
+ activeTab === "upload" && /* @__PURE__ */ jsxs(Fragment, { children: [
1665
+ dragDrop && /* @__PURE__ */ jsx(
1666
+ DropZone,
1667
+ {
1668
+ onFiles: handleFiles,
1669
+ onSpreadsheet: csvImport ? handleSpreadsheet : void 0,
1670
+ accept,
1671
+ maxFiles: batch ? maxFiles : 1,
1672
+ maxFileSize,
1673
+ disabled,
1674
+ compact,
1675
+ labels: {
1676
+ dropzone: labels.dropzone,
1677
+ dropzoneHint: labels.dropzoneHint,
1678
+ browse: labels.browse
1679
+ },
1680
+ children
1681
+ }
1682
+ ),
1683
+ hasFiles && /* @__PURE__ */ jsx(
1684
+ FileList,
1685
+ {
1686
+ files: upload.files,
1687
+ onRemove: handleRemove,
1688
+ onRetry: upload.retryFile,
1689
+ labels: {
1690
+ retry: labels.retry,
1691
+ remove: labels.remove,
1692
+ uploading: labels.uploading,
1693
+ processing: labels.processing,
1694
+ success: labels.success,
1695
+ error: labels.error
1696
+ }
1697
+ }
1698
+ )
1699
+ ] }),
1700
+ activeTab === "urls" && csvImport && /* @__PURE__ */ jsx(SpreadsheetImport, { onUrls: handleUrls }),
1701
+ (hasFiles || filePicker) && /* @__PURE__ */ jsxs("div", { className: "sirv-uploader__toolbar", children: [
1702
+ /* @__PURE__ */ jsx("div", { className: "sirv-uploader__toolbar-left", children: filePicker && browseEndpoint && /* @__PURE__ */ jsxs(
1703
+ "button",
1704
+ {
1705
+ type: "button",
1706
+ className: "sirv-btn",
1707
+ onClick: () => setIsPickerOpen(true),
1708
+ disabled,
1709
+ children: [
1710
+ /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", 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" }) }),
1711
+ labels.selectFromSirv
1712
+ ]
1713
+ }
1714
+ ) }),
1715
+ /* @__PURE__ */ jsxs("div", { className: "sirv-uploader__toolbar-right", children: [
1716
+ hasFiles && /* @__PURE__ */ jsx(
1717
+ "button",
1718
+ {
1719
+ type: "button",
1720
+ className: "sirv-btn",
1721
+ onClick: handleClearAll,
1722
+ disabled: disabled || upload.isUploading,
1723
+ children: "Clear All"
1724
+ }
1725
+ ),
1726
+ hasPendingFiles && !autoUpload && /* @__PURE__ */ jsx(
1727
+ "button",
1728
+ {
1729
+ type: "button",
1730
+ className: "sirv-btn sirv-btn--primary",
1731
+ onClick: handleUploadAll,
1732
+ disabled: disabled || upload.isUploading,
1733
+ children: upload.isUploading ? labels.uploading : "Upload All"
1734
+ }
1735
+ )
1736
+ ] })
1737
+ ] }),
1738
+ hasFiles && /* @__PURE__ */ jsx(FileListSummary, { files: upload.files }),
1739
+ filePicker && browseEndpoint && /* @__PURE__ */ jsx(
1740
+ FilePicker,
1741
+ {
1742
+ endpoint: browseEndpoint,
1743
+ isOpen: isPickerOpen,
1744
+ onClose: () => setIsPickerOpen(false),
1745
+ onSelect: handlePickerSelect,
1746
+ multiple: batch,
1747
+ initialPath: folder,
1748
+ labels: {
1749
+ title: labels.selectFromSirv,
1750
+ cancel: labels.cancel
1751
+ }
1752
+ }
1753
+ )
1754
+ ] });
1755
+ }
1756
+
1757
+ export { ACCEPTED_IMAGE_FORMATS, DEFAULT_MAX_FILE_SIZE, DropZone, FileList, FileListSummary, FilePicker, SirvUploader, SpreadsheetImport, convertHeicWithFallback, defaultUrlValidator, detectDelimiter, formatFileSize, generateId, getImageDimensions, getMimeType, isHeifFile, isImageFile, isSpreadsheetFile, parseCsvClient, parseExcelClient, sirvUrlValidator, useSirvUpload, validateFileSize };
1758
+ //# sourceMappingURL=index.mjs.map
1759
+ //# sourceMappingURL=index.mjs.map