@spark-ui/components 11.5.1 → 11.6.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.
@@ -1,17 +1,17 @@
1
1
  import {
2
2
  Progress
3
- } from "../chunk-TKAU6SMC.mjs";
3
+ } from "../chunk-7EWSMIZU.mjs";
4
4
  import {
5
5
  IconButton
6
6
  } from "../chunk-DCXWGQVZ.mjs";
7
+ import {
8
+ Icon
9
+ } from "../chunk-UMUMFMFB.mjs";
7
10
  import {
8
11
  Button,
9
12
  buttonStyles
10
13
  } from "../chunk-2YM6GKWW.mjs";
11
14
  import "../chunk-GAK4SC2F.mjs";
12
- import {
13
- Icon
14
- } from "../chunk-UMUMFMFB.mjs";
15
15
  import "../chunk-KEGAAGJW.mjs";
16
16
  import {
17
17
  Slot
@@ -19,8 +19,39 @@ import {
19
19
 
20
20
  // src/file-upload/FileUpload.tsx
21
21
  import { useFormFieldControl } from "@spark-ui/components/form-field";
22
+ import { createContext, useContext, useId, useRef } from "react";
23
+
24
+ // src/file-upload/useFileUploadState.tsx
22
25
  import { useCombinedState } from "@spark-ui/hooks/use-combined-state";
23
- import { createContext, useContext, useId, useRef, useState } from "react";
26
+ import { useState } from "react";
27
+
28
+ // src/file-upload/constants.ts
29
+ var FILE_UPLOAD_ERRORS = {
30
+ /**
31
+ * Exceeds the maxFiles limit
32
+ */
33
+ TOO_MANY_FILES: "TOO_MANY_FILES",
34
+ /**
35
+ * File type not in the accept list
36
+ */
37
+ FILE_INVALID_TYPE: "FILE_INVALID_TYPE",
38
+ /**
39
+ * File size exceeds maxFileSize
40
+ */
41
+ FILE_TOO_LARGE: "FILE_TOO_LARGE",
42
+ /**
43
+ * File size below minFileSize
44
+ */
45
+ FILE_TOO_SMALL: "FILE_TOO_SMALL",
46
+ /**
47
+ * Generic validation failure
48
+ */
49
+ FILE_INVALID: "FILE_INVALID",
50
+ /**
51
+ * Duplicate file detected
52
+ */
53
+ FILE_EXISTS: "FILE_EXISTS"
54
+ };
24
55
 
25
56
  // src/file-upload/utils.ts
26
57
  import { CvOutline } from "@spark-ui/icons/CvOutline";
@@ -121,13 +152,8 @@ function getFileIcon(file) {
121
152
  return createElement(CvOutline);
122
153
  }
123
154
 
124
- // src/file-upload/FileUpload.tsx
125
- import { jsx, jsxs } from "react/jsx-runtime";
126
- var FileUploadContext = createContext(null);
127
- var ID_PREFIX = ":file-upload";
128
- var FileUpload = ({
129
- asChild: _asChild = false,
130
- children,
155
+ // src/file-upload/useFileUploadState.tsx
156
+ function useFileUploadState({
131
157
  defaultValue = [],
132
158
  value: controlledValue,
133
159
  onFileAccept,
@@ -138,32 +164,12 @@ var FileUpload = ({
138
164
  maxFiles,
139
165
  maxFileSize,
140
166
  minFileSize,
141
- disabled: disabledProp = false,
142
- readOnly: readOnlyProp = false,
167
+ disabled = false,
168
+ readOnly = false,
143
169
  locale
144
- }) => {
145
- const field = useFormFieldControl();
146
- const {
147
- id: fieldId,
148
- name: fieldName,
149
- isInvalid,
150
- isRequired,
151
- description,
152
- disabled: fieldDisabled,
153
- readOnly: fieldReadOnly,
154
- labelId
155
- } = field;
170
+ }) {
156
171
  const defaultLocale = locale || (typeof navigator !== "undefined" && navigator.language ? navigator.language : "en");
157
- const internalId = useId();
158
- const inputId = fieldId || `${ID_PREFIX}-${internalId}`;
159
- const inputName = fieldName;
160
- const inputRef = useRef(null);
161
- const triggerRef = useRef(null);
162
- const dropzoneRef = useRef(null);
163
- const deleteButtonRefs = useRef([]);
164
- const disabled = fieldDisabled ?? disabledProp;
165
- const readOnly = fieldReadOnly ?? readOnlyProp;
166
- const [filesState, setFilesState, ,] = useCombinedState(controlledValue, defaultValue);
172
+ const [filesState, setFilesState] = useCombinedState(controlledValue, defaultValue);
167
173
  const files = filesState ?? [];
168
174
  const setFiles = setFilesState;
169
175
  const [rejectedFiles, setRejectedFiles] = useState([]);
@@ -195,20 +201,17 @@ var FileUpload = ({
195
201
  };
196
202
  setFiles((prev) => {
197
203
  const currentFiles = prev ?? [];
198
- if (maxFiles !== void 0) {
199
- const currentCount = currentFiles.length;
200
- const remainingSlots = maxFiles - currentCount;
201
- if (remainingSlots <= 0) {
202
- newFiles.forEach((file) => {
203
- addRejectedFile(file, "TOO_MANY_FILES");
204
- });
205
- }
204
+ const remainingSlots = maxFiles !== void 0 ? maxFiles - currentFiles.length : void 0;
205
+ if (remainingSlots !== void 0 && remainingSlots <= 0) {
206
+ newFiles.forEach((file) => {
207
+ addRejectedFile(file, FILE_UPLOAD_ERRORS.TOO_MANY_FILES);
208
+ });
206
209
  }
207
210
  let filteredFiles = newFiles;
208
211
  if (accept) {
209
212
  const rejectedByAccept = newFiles.filter((file) => !validateFileAccept(file, accept));
210
213
  rejectedByAccept.forEach((file) => {
211
- addRejectedFile(file, "FILE_INVALID_TYPE");
214
+ addRejectedFile(file, FILE_UPLOAD_ERRORS.FILE_INVALID_TYPE);
212
215
  });
213
216
  filteredFiles = newFiles.filter((file) => validateFileAccept(file, accept));
214
217
  }
@@ -218,11 +221,11 @@ var FileUpload = ({
218
221
  const validation = validateFileSize(file, minFileSize, maxFileSize, defaultLocale);
219
222
  if (!validation.valid) {
220
223
  if (maxFileSize !== void 0 && file.size > maxFileSize) {
221
- addRejectedFile(file, "FILE_TOO_LARGE");
224
+ addRejectedFile(file, FILE_UPLOAD_ERRORS.FILE_TOO_LARGE);
222
225
  } else if (minFileSize !== void 0 && file.size < minFileSize) {
223
- addRejectedFile(file, "FILE_TOO_SMALL");
226
+ addRejectedFile(file, FILE_UPLOAD_ERRORS.FILE_TOO_SMALL);
224
227
  } else {
225
- addRejectedFile(file, "FILE_INVALID");
228
+ addRejectedFile(file, FILE_UPLOAD_ERRORS.FILE_INVALID);
226
229
  }
227
230
  return false;
228
231
  }
@@ -230,35 +233,27 @@ var FileUpload = ({
230
233
  });
231
234
  }
232
235
  const seenFiles = /* @__PURE__ */ new Map();
233
- const duplicateFiles = [];
234
236
  const uniqueFiles = validSizeFiles.filter((file) => {
235
237
  const fileKey = `${file.name}-${file.size}`;
236
238
  const existsInPrev = fileExists(file, currentFiles);
237
239
  if (existsInPrev) {
238
- duplicateFiles.push(file);
239
- addRejectedFile(file, "FILE_EXISTS");
240
+ addRejectedFile(file, FILE_UPLOAD_ERRORS.FILE_EXISTS);
240
241
  return false;
241
242
  }
242
243
  if (seenFiles.has(fileKey)) {
243
- duplicateFiles.push(file);
244
- addRejectedFile(file, "FILE_EXISTS");
244
+ addRejectedFile(file, FILE_UPLOAD_ERRORS.FILE_EXISTS);
245
245
  return false;
246
246
  }
247
247
  seenFiles.set(fileKey, file);
248
248
  return true;
249
249
  });
250
250
  let filesToAdd = multiple ? uniqueFiles : uniqueFiles.slice(0, 1);
251
- if (maxFiles !== void 0) {
252
- const currentCount = currentFiles.length;
253
- const remainingSlots = maxFiles - currentCount;
251
+ if (remainingSlots !== void 0) {
254
252
  if (remainingSlots <= 0) {
255
- filesToAdd.forEach((file) => {
256
- addRejectedFile(file, "TOO_MANY_FILES");
257
- });
258
253
  filesToAdd = [];
259
254
  } else if (filesToAdd.length > remainingSlots) {
260
255
  filesToAdd.forEach((file) => {
261
- addRejectedFile(file, "TOO_MANY_FILES");
256
+ addRejectedFile(file, FILE_UPLOAD_ERRORS.TOO_MANY_FILES);
262
257
  });
263
258
  filesToAdd = [];
264
259
  }
@@ -291,7 +286,7 @@ var FileUpload = ({
291
286
  let updatedRejectedFiles = rejectedFiles;
292
287
  if (maxFiles !== void 0 && updated.length < maxFiles) {
293
288
  updatedRejectedFiles = rejectedFiles.filter(
294
- (rejected) => !rejected.errors.includes("TOO_MANY_FILES")
289
+ (rejected) => !rejected.errors.includes(FILE_UPLOAD_ERRORS.TOO_MANY_FILES)
295
290
  );
296
291
  setRejectedFiles(updatedRejectedFiles);
297
292
  }
@@ -310,7 +305,6 @@ var FileUpload = ({
310
305
  }
311
306
  setFiles([]);
312
307
  setRejectedFiles([]);
313
- deleteButtonRefs.current = [];
314
308
  if (onFileChange) {
315
309
  onFileChange({
316
310
  acceptedFiles: [],
@@ -328,6 +322,77 @@ var FileUpload = ({
328
322
  setRejectedFiles([]);
329
323
  };
330
324
  const maxFilesReached = maxFiles !== void 0 && files.length >= maxFiles;
325
+ return {
326
+ files,
327
+ rejectedFiles,
328
+ addFiles,
329
+ removeFile,
330
+ removeRejectedFile,
331
+ clearFiles,
332
+ clearRejectedFiles,
333
+ maxFilesReached
334
+ };
335
+ }
336
+
337
+ // src/file-upload/FileUpload.tsx
338
+ import { jsx, jsxs } from "react/jsx-runtime";
339
+ var FileUploadContext = createContext(null);
340
+ var ID_PREFIX = ":file-upload";
341
+ var FileUpload = ({
342
+ asChild: _asChild = false,
343
+ children,
344
+ defaultValue = [],
345
+ value: controlledValue,
346
+ onFileAccept,
347
+ onFileReject,
348
+ onFileChange,
349
+ multiple = true,
350
+ accept,
351
+ maxFiles,
352
+ maxFileSize,
353
+ minFileSize,
354
+ disabled: disabledProp = false,
355
+ readOnly: readOnlyProp = false,
356
+ locale
357
+ }) => {
358
+ const field = useFormFieldControl();
359
+ const internalId = useId();
360
+ const inputId = field.id || `${ID_PREFIX}-${internalId}`;
361
+ const inputName = field.name;
362
+ const inputRef = useRef(null);
363
+ const triggerRef = useRef(null);
364
+ const dropzoneRef = useRef(null);
365
+ const deleteButtonRefs = useRef([]);
366
+ const disabled = field.disabled ?? disabledProp;
367
+ const readOnly = field.readOnly ?? readOnlyProp;
368
+ const {
369
+ files,
370
+ rejectedFiles,
371
+ addFiles,
372
+ removeFile,
373
+ removeRejectedFile,
374
+ clearFiles: clearFilesFromHook,
375
+ clearRejectedFiles,
376
+ maxFilesReached
377
+ } = useFileUploadState({
378
+ defaultValue,
379
+ value: controlledValue,
380
+ onFileAccept,
381
+ onFileReject,
382
+ onFileChange,
383
+ multiple,
384
+ accept,
385
+ maxFiles,
386
+ maxFileSize,
387
+ minFileSize,
388
+ disabled,
389
+ readOnly,
390
+ locale
391
+ });
392
+ const clearFiles = () => {
393
+ clearFilesFromHook();
394
+ deleteButtonRefs.current = [];
395
+ };
331
396
  return /* @__PURE__ */ jsx(
332
397
  FileUploadContext.Provider,
333
398
  {
@@ -348,10 +413,10 @@ var FileUpload = ({
348
413
  maxFilesReached,
349
414
  disabled,
350
415
  readOnly,
351
- locale: defaultLocale,
352
- description,
353
- isInvalid,
354
- isRequired
416
+ locale: locale || (typeof navigator !== "undefined" && navigator.language ? navigator.language : "en"),
417
+ description: field.description,
418
+ isInvalid: field.isInvalid,
419
+ isRequired: field.isRequired
355
420
  },
356
421
  children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
357
422
  children,
@@ -367,10 +432,10 @@ var FileUpload = ({
367
432
  accept,
368
433
  disabled,
369
434
  readOnly: readOnly && !disabled,
370
- required: isRequired,
371
- "aria-invalid": isInvalid,
372
- "aria-describedby": description,
373
- "aria-label": !labelId ? "Upload files test" : void 0,
435
+ required: field.isRequired,
436
+ "aria-invalid": field.isInvalid,
437
+ "aria-describedby": field.description,
438
+ "aria-label": !field.labelId ? "Upload files" : void 0,
374
439
  className: "sr-only",
375
440
  onChange: (e) => {
376
441
  if (e.target.files && !disabled && !readOnly) {
@@ -398,6 +463,7 @@ var useFileUploadContext = () => {
398
463
 
399
464
  // src/file-upload/FileUploadAcceptedFile.tsx
400
465
  import { cx as cx2 } from "class-variance-authority";
466
+ import { useCallback, useEffect, useState as useState2 } from "react";
401
467
 
402
468
  // src/file-upload/FileUploadItemDeleteTrigger.tsx
403
469
  import { Close } from "@spark-ui/icons/Close";
@@ -418,7 +484,7 @@ var ItemDeleteTrigger = ({
418
484
  return;
419
485
  }
420
486
  removeFile(fileIndex);
421
- setTimeout(() => {
487
+ requestAnimationFrame(() => {
422
488
  const remainingButtons = deleteButtonRefs.current.filter(Boolean);
423
489
  if (remainingButtons.length > 0) {
424
490
  const targetIndex = Math.min(fileIndex, remainingButtons.length - 1);
@@ -432,7 +498,7 @@ var ItemDeleteTrigger = ({
432
498
  focusTarget.focus();
433
499
  }
434
500
  }
435
- }, 0);
501
+ });
436
502
  onClick?.(e);
437
503
  };
438
504
  const setRef = (node) => {
@@ -469,7 +535,6 @@ ItemDeleteTrigger.displayName = "FileUpload.ItemDeleteTrigger";
469
535
  // src/file-upload/FileUploadAcceptedFile.tsx
470
536
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
471
537
  var AcceptedFile = ({
472
- asChild: _asChild = false,
473
538
  className,
474
539
  file,
475
540
  uploadProgress,
@@ -478,6 +543,17 @@ var AcceptedFile = ({
478
543
  ...props
479
544
  }) => {
480
545
  const { locale } = useFileUploadContext();
546
+ const [showProgress, setShowProgress] = useState2(uploadProgress !== void 0);
547
+ useEffect(() => {
548
+ if (uploadProgress !== void 0) {
549
+ setShowProgress(true);
550
+ } else {
551
+ setShowProgress(false);
552
+ }
553
+ }, [uploadProgress]);
554
+ const handleProgressComplete = useCallback(() => {
555
+ setShowProgress(false);
556
+ }, []);
481
557
  return /* @__PURE__ */ jsxs2(
482
558
  "li",
483
559
  {
@@ -490,13 +566,19 @@ var AcceptedFile = ({
490
566
  ),
491
567
  ...props,
492
568
  children: [
493
- /* @__PURE__ */ jsx3("div", { className: "size-sz-40 bg-support-container flex items-center justify-center rounded-md", children: /* @__PURE__ */ jsx3(Icon, { size: "md", children: getFileIcon(file) }) }),
494
- /* @__PURE__ */ jsxs2("div", { className: "min-w-0 flex-1", children: [
495
- /* @__PURE__ */ jsxs2("div", { className: "gap-md flex flex-row items-center justify-between", children: [
496
- /* @__PURE__ */ jsx3("p", { className: "text-body-2 truncate font-medium", children: file.name }),
497
- /* @__PURE__ */ jsx3("p", { className: "text-caption opacity-dim-1", children: formatFileSize(file.size, locale) })
498
- ] }),
499
- uploadProgress !== void 0 && /* @__PURE__ */ jsx3("div", { className: "mt-md", children: /* @__PURE__ */ jsx3(Progress, { value: uploadProgress, max: 100, "aria-label": progressAriaLabel }) })
569
+ /* @__PURE__ */ jsx3("div", { className: "size-sz-36 bg-support-container flex items-center justify-center rounded-md", children: /* @__PURE__ */ jsx3(Icon, { size: "md", children: getFileIcon(file) }) }),
570
+ /* @__PURE__ */ jsxs2("div", { className: "gap-md relative flex min-w-0 flex-1 flex-row items-center justify-between self-stretch", children: [
571
+ /* @__PURE__ */ jsx3("p", { className: "text-body-2 truncate font-medium", children: file.name }),
572
+ /* @__PURE__ */ jsx3("p", { className: "text-caption opacity-dim-1", children: formatFileSize(file.size, locale) }),
573
+ showProgress && uploadProgress !== void 0 && /* @__PURE__ */ jsx3("div", { className: "absolute bottom-0 left-0 w-full", children: /* @__PURE__ */ jsx3(
574
+ Progress,
575
+ {
576
+ value: uploadProgress,
577
+ max: 100,
578
+ "aria-label": progressAriaLabel,
579
+ onComplete: handleProgressComplete
580
+ }
581
+ ) })
500
582
  ] }),
501
583
  /* @__PURE__ */ jsx3(ItemDeleteTrigger, { "aria-label": deleteButtonAriaLabel, file })
502
584
  ]
@@ -612,20 +694,19 @@ Dropzone.displayName = "FileUploadDropzone";
612
694
 
613
695
  // src/file-upload/FileUploadPreviewImage.tsx
614
696
  import { cx as cx4 } from "class-variance-authority";
615
- import { useEffect, useState as useState2 } from "react";
697
+ import { useEffect as useEffect2, useState as useState3 } from "react";
616
698
  import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
617
699
  var PreviewImage = ({
618
- asChild: _asChild = false,
619
700
  className,
620
701
  file,
621
702
  fallback = "\u{1F4C4}",
622
703
  ...props
623
704
  }) => {
624
- const [imageError, setImageError] = useState2(false);
625
- const [imageLoaded, setImageLoaded] = useState2(false);
705
+ const [imageError, setImageError] = useState3(false);
706
+ const [imageLoaded, setImageLoaded] = useState3(false);
626
707
  const isImage = file.type.startsWith("image/");
627
708
  const imageUrl = isImage ? URL.createObjectURL(file) : null;
628
- useEffect(() => {
709
+ useEffect2(() => {
629
710
  return () => {
630
711
  if (imageUrl) {
631
712
  URL.revokeObjectURL(imageUrl);
@@ -724,7 +805,6 @@ RejectedFileDeleteTrigger.displayName = "FileUpload.RejectedFileDeleteTrigger";
724
805
  // src/file-upload/FileUploadRejectedFile.tsx
725
806
  import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
726
807
  var RejectedFile = ({
727
- asChild: _asChild = false,
728
808
  className,
729
809
  rejectedFile,
730
810
  renderError,
@@ -745,7 +825,7 @@ var RejectedFile = ({
745
825
  ),
746
826
  ...props,
747
827
  children: [
748
- /* @__PURE__ */ jsx8("div", { className: "size-sz-40 bg-error-container flex items-center justify-center rounded-md", children: /* @__PURE__ */ jsx8(Icon, { size: "md", className: "text-error", children: /* @__PURE__ */ jsx8(WarningOutline, {}) }) }),
828
+ /* @__PURE__ */ jsx8("div", { className: "size-sz-36 bg-error-container flex items-center justify-center rounded-md", children: /* @__PURE__ */ jsx8(Icon, { size: "md", className: "text-error", children: /* @__PURE__ */ jsx8(WarningOutline, {}) }) }),
749
829
  /* @__PURE__ */ jsx8("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsxs4("div", { className: "gap-md flex flex-col", children: [
750
830
  /* @__PURE__ */ jsxs4("div", { className: "gap-md flex flex-row items-center justify-between", children: [
751
831
  /* @__PURE__ */ jsx8("p", { className: "text-body-2 truncate font-medium", children: rejectedFile.file.name }),
@@ -863,6 +943,7 @@ AcceptedFile.displayName = "FileUpload.AcceptedFile";
863
943
  RejectedFile.displayName = "FileUpload.RejectedFile";
864
944
  RejectedFileDeleteTrigger.displayName = "FileUpload.RejectedFileDeleteTrigger";
865
945
  export {
946
+ FILE_UPLOAD_ERRORS,
866
947
  FileUpload2 as FileUpload
867
948
  };
868
949
  //# sourceMappingURL=index.mjs.map