@spark-ui/components 11.2.4 → 11.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,828 @@
1
+ import {
2
+ Progress
3
+ } from "../chunk-TKAU6SMC.mjs";
4
+ import {
5
+ IconButton
6
+ } from "../chunk-XYK6V3JF.mjs";
7
+ import {
8
+ Icon
9
+ } from "../chunk-UMUMFMFB.mjs";
10
+ import {
11
+ Button
12
+ } from "../chunk-HEKSVWYW.mjs";
13
+ import "../chunk-GAK4SC2F.mjs";
14
+ import "../chunk-KEGAAGJW.mjs";
15
+ import {
16
+ Slot
17
+ } from "../chunk-6QCEPQ3U.mjs";
18
+
19
+ // src/file-upload/FileUpload.tsx
20
+ import { useCombinedState } from "@spark-ui/hooks/use-combined-state";
21
+ import { createContext, useContext, useRef, useState } from "react";
22
+
23
+ // src/file-upload/utils.ts
24
+ function validateFileAccept(file, accept) {
25
+ if (!accept) {
26
+ return true;
27
+ }
28
+ const patterns = accept.split(",").map((pattern) => pattern.trim());
29
+ return patterns.some((pattern) => {
30
+ if (pattern.includes("/")) {
31
+ if (pattern.endsWith("/*")) {
32
+ const baseType = pattern.slice(0, -2);
33
+ return file.type.startsWith(baseType + "/");
34
+ }
35
+ return file.type === pattern;
36
+ }
37
+ if (pattern.startsWith(".")) {
38
+ const extension2 = pattern.toLowerCase();
39
+ const fileName2 = file.name.toLowerCase();
40
+ return fileName2.endsWith(extension2);
41
+ }
42
+ const extension = "." + pattern.toLowerCase();
43
+ const fileName = file.name.toLowerCase();
44
+ return fileName.endsWith(extension);
45
+ });
46
+ }
47
+ function validateFileSize(file, minFileSize, maxFileSize, locale) {
48
+ const defaultLocale = locale || getDefaultLocale();
49
+ if (minFileSize !== void 0 && file.size < minFileSize) {
50
+ const errorMessage = `File "${file.name}" is too small. Minimum size is ${formatFileSize(minFileSize, defaultLocale)}.`;
51
+ return {
52
+ valid: false,
53
+ error: errorMessage
54
+ };
55
+ }
56
+ if (maxFileSize !== void 0 && file.size > maxFileSize) {
57
+ const errorMessage = `File "${file.name}" is too large. Maximum size is ${formatFileSize(maxFileSize, defaultLocale)}.`;
58
+ return {
59
+ valid: false,
60
+ error: errorMessage
61
+ };
62
+ }
63
+ return { valid: true };
64
+ }
65
+ function getDefaultLocale() {
66
+ if (typeof navigator !== "undefined" && navigator.language) {
67
+ return navigator.language;
68
+ }
69
+ return "en";
70
+ }
71
+ function formatFileSize(bytes, locale) {
72
+ const defaultLocale = locale || getDefaultLocale();
73
+ let normalizedLocale = defaultLocale;
74
+ if (defaultLocale.length === 2) {
75
+ normalizedLocale = defaultLocale === "fr" ? "fr-FR" : "en-US";
76
+ }
77
+ if (bytes === 0) {
78
+ const formatter2 = new Intl.NumberFormat(normalizedLocale, {
79
+ style: "unit",
80
+ unit: "byte",
81
+ unitDisplay: "long",
82
+ minimumFractionDigits: 0,
83
+ maximumFractionDigits: 0
84
+ });
85
+ return formatter2.format(0);
86
+ }
87
+ const k = 1024;
88
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
89
+ const units = ["byte", "kilobyte", "megabyte", "gigabyte"];
90
+ const unit = units[i] || "byte";
91
+ const size = bytes / Math.pow(k, i);
92
+ const unitDisplay = i === 0 ? "long" : "short";
93
+ const formatter = new Intl.NumberFormat(normalizedLocale, {
94
+ style: "unit",
95
+ unit,
96
+ unitDisplay,
97
+ minimumFractionDigits: 0,
98
+ maximumFractionDigits: 2
99
+ });
100
+ return formatter.format(size);
101
+ }
102
+
103
+ // src/file-upload/FileUpload.tsx
104
+ import { jsx, jsxs } from "react/jsx-runtime";
105
+ var FileUploadContext = createContext(null);
106
+ var FileUpload = ({
107
+ asChild: _asChild = false,
108
+ children,
109
+ defaultValue = [],
110
+ value: controlledValue,
111
+ onFilesChange,
112
+ multiple = true,
113
+ accept,
114
+ maxFiles,
115
+ onMaxFilesReached,
116
+ maxFileSize,
117
+ minFileSize,
118
+ onFileSizeError,
119
+ disabled = false,
120
+ readOnly = false,
121
+ locale
122
+ }) => {
123
+ const defaultLocale = locale || (typeof navigator !== "undefined" && navigator.language ? navigator.language : "en");
124
+ const inputRef = useRef(null);
125
+ const triggerRef = useRef(null);
126
+ const dropzoneRef = useRef(null);
127
+ const deleteButtonRefs = useRef([]);
128
+ const [filesState, setFilesState, ,] = useCombinedState(
129
+ controlledValue,
130
+ defaultValue,
131
+ onFilesChange
132
+ );
133
+ const files = filesState ?? [];
134
+ const setFiles = setFilesState;
135
+ const [rejectedFiles, setRejectedFiles] = useState([]);
136
+ const addFiles = (newFiles) => {
137
+ if (disabled || readOnly) {
138
+ return;
139
+ }
140
+ setRejectedFiles([]);
141
+ const newRejectedFiles = [];
142
+ const fileExists = (file, existingFiles) => {
143
+ return existingFiles.some(
144
+ (existingFile) => existingFile.name === file.name && existingFile.size === file.size
145
+ );
146
+ };
147
+ const addRejectedFile = (file, error) => {
148
+ const existingRejection = newRejectedFiles.find(
149
+ (rejected) => rejected.file.name === file.name && rejected.file.size === file.size
150
+ );
151
+ if (existingRejection) {
152
+ if (!existingRejection.errors.includes(error)) {
153
+ existingRejection.errors.push(error);
154
+ }
155
+ } else {
156
+ newRejectedFiles.push({
157
+ file,
158
+ errors: [error]
159
+ });
160
+ }
161
+ if (onFileSizeError) {
162
+ onFileSizeError(file, error);
163
+ }
164
+ };
165
+ setFiles((prev) => {
166
+ const currentFiles = prev ?? [];
167
+ if (maxFiles !== void 0) {
168
+ const currentCount = currentFiles.length;
169
+ const remainingSlots = maxFiles - currentCount;
170
+ if (remainingSlots <= 0) {
171
+ newFiles.forEach((file) => {
172
+ addRejectedFile(file, "TOO_MANY_FILES");
173
+ });
174
+ }
175
+ }
176
+ let filteredFiles = newFiles;
177
+ if (accept) {
178
+ const rejectedByAccept = newFiles.filter((file) => !validateFileAccept(file, accept));
179
+ rejectedByAccept.forEach((file) => {
180
+ addRejectedFile(file, "FILE_INVALID_TYPE");
181
+ });
182
+ filteredFiles = newFiles.filter((file) => validateFileAccept(file, accept));
183
+ }
184
+ let validSizeFiles = filteredFiles;
185
+ if (minFileSize !== void 0 || maxFileSize !== void 0) {
186
+ validSizeFiles = filteredFiles.filter((file) => {
187
+ const validation = validateFileSize(file, minFileSize, maxFileSize, defaultLocale);
188
+ if (!validation.valid) {
189
+ if (maxFileSize !== void 0 && file.size > maxFileSize) {
190
+ addRejectedFile(file, "FILE_TOO_LARGE");
191
+ } else if (minFileSize !== void 0 && file.size < minFileSize) {
192
+ addRejectedFile(file, "FILE_TOO_SMALL");
193
+ } else {
194
+ addRejectedFile(file, "FILE_INVALID");
195
+ }
196
+ return false;
197
+ }
198
+ return true;
199
+ });
200
+ }
201
+ const seenFiles = /* @__PURE__ */ new Map();
202
+ const duplicateFiles = [];
203
+ const uniqueFiles = validSizeFiles.filter((file) => {
204
+ const fileKey = `${file.name}-${file.size}`;
205
+ const existsInPrev = fileExists(file, currentFiles);
206
+ if (existsInPrev) {
207
+ duplicateFiles.push(file);
208
+ addRejectedFile(file, "FILE_EXISTS");
209
+ return false;
210
+ }
211
+ if (seenFiles.has(fileKey)) {
212
+ duplicateFiles.push(file);
213
+ addRejectedFile(file, "FILE_EXISTS");
214
+ return false;
215
+ }
216
+ seenFiles.set(fileKey, file);
217
+ return true;
218
+ });
219
+ let filesToAdd = multiple ? uniqueFiles : uniqueFiles.slice(0, 1);
220
+ if (maxFiles !== void 0) {
221
+ const currentCount = currentFiles.length;
222
+ const remainingSlots = maxFiles - currentCount;
223
+ if (remainingSlots <= 0) {
224
+ filesToAdd.forEach((file) => {
225
+ addRejectedFile(file, "TOO_MANY_FILES");
226
+ });
227
+ onMaxFilesReached?.(maxFiles, filesToAdd.length);
228
+ filesToAdd = [];
229
+ } else if (filesToAdd.length > remainingSlots) {
230
+ filesToAdd.forEach((file) => {
231
+ addRejectedFile(file, "TOO_MANY_FILES");
232
+ });
233
+ onMaxFilesReached?.(maxFiles, filesToAdd.length);
234
+ filesToAdd = [];
235
+ }
236
+ }
237
+ const updated = multiple ? [...currentFiles, ...filesToAdd] : filesToAdd;
238
+ const rejectedFilesToAdd = [...newRejectedFiles];
239
+ setRejectedFiles(rejectedFilesToAdd);
240
+ return updated;
241
+ });
242
+ };
243
+ const removeFile = (index) => {
244
+ if (disabled || readOnly) {
245
+ return;
246
+ }
247
+ setFiles((prev) => {
248
+ const currentFiles = prev ?? [];
249
+ const updated = currentFiles.filter((_, i) => i !== index);
250
+ if (maxFiles !== void 0 && updated.length < maxFiles) {
251
+ setRejectedFiles(
252
+ (prevRejected) => prevRejected.filter((rejected) => !rejected.errors.includes("TOO_MANY_FILES"))
253
+ );
254
+ }
255
+ return updated;
256
+ });
257
+ };
258
+ const clearFiles = () => {
259
+ if (disabled || readOnly) {
260
+ return;
261
+ }
262
+ setFiles([]);
263
+ setRejectedFiles([]);
264
+ deleteButtonRefs.current = [];
265
+ };
266
+ const removeRejectedFile = (index) => {
267
+ if (disabled || readOnly) {
268
+ return;
269
+ }
270
+ setRejectedFiles((prev) => prev.filter((_, i) => i !== index));
271
+ };
272
+ const clearRejectedFiles = () => {
273
+ setRejectedFiles([]);
274
+ };
275
+ const maxFilesReached = maxFiles !== void 0 && files.length >= maxFiles;
276
+ return /* @__PURE__ */ jsx(
277
+ FileUploadContext.Provider,
278
+ {
279
+ value: {
280
+ inputRef,
281
+ files,
282
+ rejectedFiles,
283
+ addFiles,
284
+ removeFile,
285
+ removeRejectedFile,
286
+ clearFiles,
287
+ clearRejectedFiles,
288
+ triggerRef,
289
+ dropzoneRef,
290
+ deleteButtonRefs,
291
+ multiple,
292
+ maxFiles,
293
+ maxFilesReached,
294
+ disabled,
295
+ readOnly,
296
+ locale: defaultLocale
297
+ },
298
+ children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
299
+ children,
300
+ /* @__PURE__ */ jsx(
301
+ "input",
302
+ {
303
+ ref: inputRef,
304
+ type: "file",
305
+ tabIndex: -1,
306
+ id: "image_uploads",
307
+ multiple,
308
+ name: "image_uploads",
309
+ accept,
310
+ disabled,
311
+ readOnly: readOnly && !disabled,
312
+ className: "sr-only",
313
+ onChange: (e) => {
314
+ if (e.target.files && !disabled && !readOnly) {
315
+ addFiles(Array.from(e.target.files));
316
+ try {
317
+ e.target.value = "";
318
+ } catch {
319
+ }
320
+ }
321
+ }
322
+ }
323
+ )
324
+ ] })
325
+ }
326
+ );
327
+ };
328
+ FileUpload.displayName = "FileUpload";
329
+ var useFileUploadContext = () => {
330
+ const context = useContext(FileUploadContext);
331
+ if (!context) {
332
+ throw Error("useFileUploadContext must be used within a FileUpload provider");
333
+ }
334
+ return context;
335
+ };
336
+
337
+ // src/file-upload/FileUploadAcceptedFile.tsx
338
+ import { CvOutline } from "@spark-ui/icons/CvOutline";
339
+
340
+ // src/file-upload/FileUploadItem.tsx
341
+ import { cx } from "class-variance-authority";
342
+ import { jsx as jsx2 } from "react/jsx-runtime";
343
+ var Item = ({
344
+ asChild: _asChild = false,
345
+ className,
346
+ children,
347
+ ...props
348
+ }) => {
349
+ return /* @__PURE__ */ jsx2(
350
+ "li",
351
+ {
352
+ "data-spark-component": "file-upload-item",
353
+ className: cx(
354
+ "relative",
355
+ "default:bg-surface default:border-sm default:border-outline default:p-md default:rounded-md",
356
+ "gap-md flex items-center justify-between default:w-full",
357
+ className
358
+ ),
359
+ ...props,
360
+ children
361
+ }
362
+ );
363
+ };
364
+ Item.displayName = "FileUpload.Item";
365
+
366
+ // src/file-upload/FileUploadItemDeleteTrigger.tsx
367
+ import { Close } from "@spark-ui/icons/Close";
368
+ import { cx as cx2 } from "class-variance-authority";
369
+ import { useRef as useRef2 } from "react";
370
+ import { jsx as jsx3 } from "react/jsx-runtime";
371
+ var ItemDeleteTrigger = ({
372
+ className,
373
+ fileIndex,
374
+ onClick,
375
+ ...props
376
+ }) => {
377
+ const { removeFile, triggerRef, dropzoneRef, deleteButtonRefs, disabled, readOnly } = useFileUploadContext();
378
+ const buttonRef = useRef2(null);
379
+ const handleClick = (e) => {
380
+ if (disabled || readOnly) {
381
+ return;
382
+ }
383
+ removeFile(fileIndex);
384
+ setTimeout(() => {
385
+ const remainingButtons = deleteButtonRefs.current.filter(Boolean);
386
+ if (remainingButtons.length > 0) {
387
+ const targetIndex = Math.min(fileIndex, remainingButtons.length - 1);
388
+ const nextButton = remainingButtons[targetIndex];
389
+ if (nextButton) {
390
+ nextButton.focus();
391
+ }
392
+ } else {
393
+ const focusTarget = triggerRef.current || dropzoneRef.current;
394
+ if (focusTarget) {
395
+ focusTarget.focus();
396
+ }
397
+ }
398
+ }, 0);
399
+ onClick?.(e);
400
+ };
401
+ const setRef = (node) => {
402
+ buttonRef.current = node;
403
+ if (node) {
404
+ while (deleteButtonRefs.current.length <= fileIndex) {
405
+ deleteButtonRefs.current.push(null);
406
+ }
407
+ deleteButtonRefs.current[fileIndex] = node;
408
+ } else {
409
+ if (deleteButtonRefs.current[fileIndex]) {
410
+ deleteButtonRefs.current[fileIndex] = null;
411
+ }
412
+ }
413
+ };
414
+ return /* @__PURE__ */ jsx3(
415
+ IconButton,
416
+ {
417
+ ref: setRef,
418
+ "data-spark-component": "file-upload-item-delete-trigger",
419
+ className: cx2(className),
420
+ onClick: handleClick,
421
+ disabled: disabled || readOnly,
422
+ size: "sm",
423
+ design: "contrast",
424
+ intent: "surface",
425
+ ...props,
426
+ children: /* @__PURE__ */ jsx3(Icon, { size: "sm", children: /* @__PURE__ */ jsx3(Close, {}) })
427
+ }
428
+ );
429
+ };
430
+ ItemDeleteTrigger.displayName = "FileUpload.ItemDeleteTrigger";
431
+
432
+ // src/file-upload/FileUploadItemFileName.tsx
433
+ import { cx as cx3 } from "class-variance-authority";
434
+ import { jsx as jsx4 } from "react/jsx-runtime";
435
+ var ItemFileName = ({
436
+ asChild: _asChild = false,
437
+ className,
438
+ children,
439
+ ...props
440
+ }) => {
441
+ return /* @__PURE__ */ jsx4(
442
+ "p",
443
+ {
444
+ "data-spark-component": "file-upload-item-file-name",
445
+ className: cx3("text-body-2 truncate font-medium", className),
446
+ ...props,
447
+ children
448
+ }
449
+ );
450
+ };
451
+ ItemFileName.displayName = "FileUpload.ItemFileName";
452
+
453
+ // src/file-upload/FileUploadItemSizeText.tsx
454
+ import { cx as cx4 } from "class-variance-authority";
455
+ import { jsx as jsx5 } from "react/jsx-runtime";
456
+ var ItemSizeText = ({
457
+ asChild: _asChild = false,
458
+ className,
459
+ children,
460
+ ...props
461
+ }) => {
462
+ return /* @__PURE__ */ jsx5(
463
+ "p",
464
+ {
465
+ "data-spark-component": "file-upload-item-size-text",
466
+ className: cx4("text-caption", className),
467
+ ...props,
468
+ children
469
+ }
470
+ );
471
+ };
472
+ ItemSizeText.displayName = "FileUpload.ItemSizeText";
473
+
474
+ // src/file-upload/FileUploadAcceptedFile.tsx
475
+ import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
476
+ var AcceptedFile = ({
477
+ asChild: _asChild = false,
478
+ className,
479
+ file,
480
+ fileIndex,
481
+ uploadProgress,
482
+ ...props
483
+ }) => {
484
+ const { locale } = useFileUploadContext();
485
+ return /* @__PURE__ */ jsxs2(Item, { className, ...props, children: [
486
+ /* @__PURE__ */ jsx6("div", { className: "size-sz-40 bg-support-container flex items-center justify-center rounded-md", children: /* @__PURE__ */ jsx6(Icon, { size: "md", children: /* @__PURE__ */ jsx6(CvOutline, {}) }) }),
487
+ /* @__PURE__ */ jsxs2("div", { className: "min-w-0 flex-1", children: [
488
+ /* @__PURE__ */ jsxs2("div", { className: "gap-md flex flex-row items-center justify-between", children: [
489
+ /* @__PURE__ */ jsx6(ItemFileName, { children: file.name }),
490
+ /* @__PURE__ */ jsx6(ItemSizeText, { className: "opacity-dim-1", children: formatFileSize(file.size, locale) })
491
+ ] }),
492
+ uploadProgress !== void 0 && /* @__PURE__ */ jsx6("div", { className: "mt-md", children: /* @__PURE__ */ jsx6(
493
+ Progress,
494
+ {
495
+ value: uploadProgress,
496
+ max: 100,
497
+ "aria-label": `Upload progress: ${uploadProgress}%`
498
+ }
499
+ ) })
500
+ ] }),
501
+ /* @__PURE__ */ jsx6(ItemDeleteTrigger, { "aria-label": "Delete file", fileIndex })
502
+ ] });
503
+ };
504
+ AcceptedFile.displayName = "FileUpload.AcceptedFile";
505
+
506
+ // src/file-upload/FileUploadContext.tsx
507
+ import { Fragment, jsx as jsx7 } from "react/jsx-runtime";
508
+ var Context = ({ children }) => {
509
+ const { files = [], rejectedFiles = [], locale } = useFileUploadContext();
510
+ return /* @__PURE__ */ jsx7(Fragment, { children: children({
511
+ acceptedFiles: files,
512
+ rejectedFiles,
513
+ formatFileSize,
514
+ locale
515
+ }) });
516
+ };
517
+ Context.displayName = "FileUpload.Context";
518
+
519
+ // src/file-upload/FileUploadDropzone.tsx
520
+ import { cx as cx5 } from "class-variance-authority";
521
+ import { useRef as useRef3 } from "react";
522
+ import { jsx as jsx8 } from "react/jsx-runtime";
523
+ function Dropzone({
524
+ children,
525
+ onFiles,
526
+ className,
527
+ unstyled = false
528
+ }) {
529
+ const ctx = useFileUploadContext();
530
+ const dropzoneRef = useRef3(null);
531
+ if (!ctx) throw new Error("FileUploadDropzone must be used inside <FileUpload>");
532
+ const handleDrop = (e) => {
533
+ e.preventDefault();
534
+ e.stopPropagation();
535
+ e.currentTarget.setAttribute("data-drag-over", "false");
536
+ if (ctx.disabled || ctx.readOnly) {
537
+ return;
538
+ }
539
+ const files = e.dataTransfer.files;
540
+ onFiles?.(files);
541
+ let filesArray = [];
542
+ if (files) {
543
+ filesArray = Array.isArray(files) ? [...files] : Array.from(files);
544
+ }
545
+ if (filesArray.length > 0) {
546
+ ctx.addFiles(filesArray);
547
+ }
548
+ };
549
+ const handleClick = () => {
550
+ if (!ctx.disabled && !ctx.readOnly) {
551
+ ctx.inputRef.current?.click();
552
+ }
553
+ };
554
+ const handleKeyDown = (e) => {
555
+ if (e.key === "Enter" || e.key === " ") {
556
+ e.preventDefault();
557
+ if (!ctx.disabled && !ctx.readOnly) {
558
+ ctx.inputRef.current?.click();
559
+ }
560
+ }
561
+ };
562
+ const isDisabled = ctx.disabled || ctx.readOnly;
563
+ return /* @__PURE__ */ jsx8(
564
+ "div",
565
+ {
566
+ ref: (node) => {
567
+ dropzoneRef.current = node;
568
+ if (ctx.dropzoneRef) {
569
+ ctx.dropzoneRef.current = node;
570
+ }
571
+ },
572
+ role: "button",
573
+ tabIndex: isDisabled ? -1 : 0,
574
+ "aria-disabled": ctx.disabled ? true : void 0,
575
+ onClick: handleClick,
576
+ onKeyDown: handleKeyDown,
577
+ onDrop: handleDrop,
578
+ onDragOver: (e) => {
579
+ e.preventDefault();
580
+ },
581
+ className: unstyled ? className : cx5(
582
+ "default:bg-surface default:border-sm default:border-outline default:rounded-lg default:border-dashed",
583
+ "gap-lg flex flex-col items-center justify-center text-center",
584
+ "default:p-xl",
585
+ "transition-colors duration-200",
586
+ !isDisabled && "hover:bg-surface-hovered",
587
+ "data-[drag-over=true]:border-outline-high data-[drag-over=true]:bg-surface-hovered data-[drag-over=true]:border-solid",
588
+ // Disabled: more visually disabled (opacity + cursor)
589
+ ctx.disabled && "cursor-not-allowed opacity-50",
590
+ // ReadOnly: less visually disabled (just cursor, no opacity)
591
+ ctx.readOnly && !ctx.disabled && "cursor-default",
592
+ className
593
+ ),
594
+ onDragEnter: (e) => {
595
+ if (!isDisabled) {
596
+ e.currentTarget.setAttribute("data-drag-over", "true");
597
+ }
598
+ },
599
+ onDragLeave: (e) => {
600
+ e.currentTarget.setAttribute("data-drag-over", "false");
601
+ },
602
+ children
603
+ }
604
+ );
605
+ }
606
+ Dropzone.displayName = "FileUploadDropzone";
607
+
608
+ // src/file-upload/FileUploadPreviewImage.tsx
609
+ import { cx as cx6 } from "class-variance-authority";
610
+ import { useEffect, useState as useState2 } from "react";
611
+ import { jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime";
612
+ var PreviewImage = ({
613
+ asChild: _asChild = false,
614
+ className,
615
+ file,
616
+ fallback = "\u{1F4C4}",
617
+ ...props
618
+ }) => {
619
+ const [imageError, setImageError] = useState2(false);
620
+ const [imageLoaded, setImageLoaded] = useState2(false);
621
+ const isImage = file.type.startsWith("image/");
622
+ const imageUrl = isImage ? URL.createObjectURL(file) : null;
623
+ useEffect(() => {
624
+ return () => {
625
+ if (imageUrl) {
626
+ URL.revokeObjectURL(imageUrl);
627
+ }
628
+ };
629
+ }, [imageUrl]);
630
+ if (!isImage || imageError) {
631
+ return /* @__PURE__ */ jsx9(
632
+ "div",
633
+ {
634
+ "data-spark-component": "file-upload-preview-image",
635
+ className: cx6(
636
+ "bg-neutral-container flex items-center justify-center rounded-md",
637
+ className
638
+ ),
639
+ ...props,
640
+ children: fallback
641
+ }
642
+ );
643
+ }
644
+ return /* @__PURE__ */ jsxs3(
645
+ "div",
646
+ {
647
+ "data-spark-component": "file-upload-preview-image",
648
+ className: cx6("bg-neutral-container overflow-hidden", className),
649
+ ...props,
650
+ children: [
651
+ /* @__PURE__ */ jsx9(
652
+ "img",
653
+ {
654
+ src: imageUrl,
655
+ alt: file.name,
656
+ className: cx6("size-full object-cover", !imageLoaded && "opacity-0"),
657
+ onLoad: () => setImageLoaded(true),
658
+ onError: () => setImageError(true)
659
+ }
660
+ ),
661
+ !imageLoaded && /* @__PURE__ */ jsx9("div", { className: "absolute inset-0 flex items-center justify-center", children: fallback })
662
+ ]
663
+ }
664
+ );
665
+ };
666
+ PreviewImage.displayName = "FileUpload.PreviewImage";
667
+
668
+ // src/file-upload/FileUploadRejectedFile.tsx
669
+ import { WarningOutline } from "@spark-ui/icons/WarningOutline";
670
+ import { cx as cx8 } from "class-variance-authority";
671
+
672
+ // src/file-upload/FileUploadRejectedFileDeleteTrigger.tsx
673
+ import { Close as Close2 } from "@spark-ui/icons/Close";
674
+ import { cx as cx7 } from "class-variance-authority";
675
+ import { useRef as useRef4 } from "react";
676
+ import { jsx as jsx10 } from "react/jsx-runtime";
677
+ var RejectedFileDeleteTrigger = ({
678
+ className,
679
+ rejectedFileIndex,
680
+ onClick,
681
+ ...props
682
+ }) => {
683
+ const { removeRejectedFile, triggerRef, dropzoneRef, disabled, readOnly } = useFileUploadContext();
684
+ const buttonRef = useRef4(null);
685
+ const handleClick = (e) => {
686
+ if (disabled || readOnly) {
687
+ return;
688
+ }
689
+ removeRejectedFile(rejectedFileIndex);
690
+ setTimeout(() => {
691
+ const focusTarget = triggerRef.current || dropzoneRef.current;
692
+ if (focusTarget) {
693
+ focusTarget.focus();
694
+ }
695
+ }, 0);
696
+ onClick?.(e);
697
+ };
698
+ return /* @__PURE__ */ jsx10(
699
+ IconButton,
700
+ {
701
+ ref: buttonRef,
702
+ "data-spark-component": "file-upload-rejected-file-delete-trigger",
703
+ className: cx7(className),
704
+ onClick: handleClick,
705
+ disabled: disabled || readOnly,
706
+ size: "sm",
707
+ design: "contrast",
708
+ intent: "surface",
709
+ ...props,
710
+ children: /* @__PURE__ */ jsx10(Icon, { size: "sm", children: /* @__PURE__ */ jsx10(Close2, {}) })
711
+ }
712
+ );
713
+ };
714
+ RejectedFileDeleteTrigger.displayName = "FileUpload.RejectedFileDeleteTrigger";
715
+
716
+ // src/file-upload/FileUploadRejectedFile.tsx
717
+ import { jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
718
+ var RejectedFile = ({
719
+ asChild: _asChild = false,
720
+ className,
721
+ rejectedFile,
722
+ rejectedFileIndex,
723
+ renderError,
724
+ ...props
725
+ }) => {
726
+ const { locale } = useFileUploadContext();
727
+ return /* @__PURE__ */ jsxs4(Item, { className: cx8("border-error border-md", className), ...props, children: [
728
+ /* @__PURE__ */ jsx11("div", { className: "size-sz-40 bg-error-container flex items-center justify-center rounded-md", children: /* @__PURE__ */ jsx11(Icon, { size: "md", className: "text-error", children: /* @__PURE__ */ jsx11(WarningOutline, {}) }) }),
729
+ /* @__PURE__ */ jsx11("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsxs4("div", { className: "gap-md flex flex-col", children: [
730
+ /* @__PURE__ */ jsxs4("div", { className: "gap-md flex flex-row items-center justify-between", children: [
731
+ /* @__PURE__ */ jsx11(ItemFileName, { children: rejectedFile.file.name }),
732
+ /* @__PURE__ */ jsx11(ItemSizeText, { className: "opacity-dim-1", children: formatFileSize(rejectedFile.file.size, locale) })
733
+ ] }),
734
+ /* @__PURE__ */ jsx11("div", { className: "gap-xs flex flex-col", children: rejectedFile.errors.map((error, errorIndex) => /* @__PURE__ */ jsx11("div", { className: "text-caption text-error", "data-error-code": error, children: renderError(error) }, errorIndex)) })
735
+ ] }) }),
736
+ /* @__PURE__ */ jsx11(
737
+ RejectedFileDeleteTrigger,
738
+ {
739
+ "aria-label": `Remove ${rejectedFile.file.name} error`,
740
+ rejectedFileIndex
741
+ }
742
+ )
743
+ ] });
744
+ };
745
+ RejectedFile.displayName = "FileUpload.RejectedFile";
746
+
747
+ // src/file-upload/FileUploadTrigger.tsx
748
+ import { cx as cx9 } from "class-variance-authority";
749
+ import { jsx as jsx12 } from "react/jsx-runtime";
750
+ var Trigger = ({
751
+ className,
752
+ children,
753
+ asChild = false,
754
+ unstyled = false,
755
+ design = "filled",
756
+ intent = "basic",
757
+ ref,
758
+ ...props
759
+ }) => {
760
+ const { inputRef, triggerRef, disabled, readOnly } = useFileUploadContext();
761
+ const handleClick = (e) => {
762
+ e.stopPropagation();
763
+ e.preventDefault();
764
+ if (!disabled && !readOnly) {
765
+ inputRef.current?.click();
766
+ }
767
+ };
768
+ const buttonComponent = unstyled ? "button" : Button;
769
+ const Comp = asChild ? Slot : buttonComponent;
770
+ return /* @__PURE__ */ jsx12(
771
+ Comp,
772
+ {
773
+ type: "button",
774
+ ref: (node) => {
775
+ if (triggerRef) {
776
+ triggerRef.current = node;
777
+ }
778
+ if (ref) {
779
+ if (typeof ref === "function") {
780
+ ref(node);
781
+ } else {
782
+ ref.current = node;
783
+ }
784
+ }
785
+ },
786
+ design,
787
+ intent,
788
+ "data-spark-component": "file-upload-trigger",
789
+ className: cx9(className),
790
+ disabled: disabled || readOnly,
791
+ onClick: handleClick,
792
+ ...props,
793
+ children
794
+ }
795
+ );
796
+ };
797
+ Trigger.displayName = "FileUpload.Trigger";
798
+
799
+ // src/file-upload/index.ts
800
+ var FileUpload2 = Object.assign(FileUpload, {
801
+ Trigger,
802
+ Dropzone,
803
+ Context,
804
+ Item,
805
+ ItemFileName,
806
+ ItemSizeText,
807
+ ItemDeleteTrigger,
808
+ PreviewImage,
809
+ AcceptedFile,
810
+ RejectedFile,
811
+ RejectedFileDeleteTrigger
812
+ });
813
+ FileUpload2.displayName = "FileUpload";
814
+ Trigger.displayName = "FileUpload.Trigger";
815
+ Dropzone.displayName = "FileUpload.Dropzone";
816
+ Context.displayName = "FileUpload.Context";
817
+ Item.displayName = "FileUpload.Item";
818
+ ItemFileName.displayName = "FileUpload.ItemFileName";
819
+ ItemSizeText.displayName = "FileUpload.ItemSizeText";
820
+ ItemDeleteTrigger.displayName = "FileUpload.ItemDeleteTrigger";
821
+ PreviewImage.displayName = "FileUpload.PreviewImage";
822
+ AcceptedFile.displayName = "FileUpload.AcceptedFile";
823
+ RejectedFile.displayName = "FileUpload.RejectedFile";
824
+ RejectedFileDeleteTrigger.displayName = "FileUpload.RejectedFileDeleteTrigger";
825
+ export {
826
+ FileUpload2 as FileUpload
827
+ };
828
+ //# sourceMappingURL=index.mjs.map