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