@adamosuiteservices/ui 2.13.1 → 2.13.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.
@@ -2,15 +2,19 @@
2
2
 
3
3
  ## Descripción
4
4
 
5
- Componente de carga de archivos con funcionalidad **drag & drop**, validación de extensiones y tamaño, vista previa del archivo seleccionado y área de arrastre visual. Ideal para formularios que requieren subida de archivos con feedback inmediato al usuario.
5
+ Componente de carga de archivos con funcionalidad **drag & drop**, validación de extensiones y tamaño, vista previa del archivo seleccionado y área de arrastre visual. Soporta tanto carga de un solo archivo como múltiples archivos. Ideal para formularios que requieren subida de archivos con feedback inmediato al usuario.
6
6
 
7
7
  ## Características
8
8
 
9
9
  - ✅ Drag & Drop con feedback visual
10
+ - ✅ Modo simple (un archivo) y múltiple (varios archivos)
10
11
  - ✅ Validación de extensiones de archivo
11
12
  - ✅ Validación de tamaño máximo
12
- - ✅ Vista previa del archivo seleccionado con tamaño
13
- - ✅ Botón para remover archivo seleccionado
13
+ - ✅ Vista previa del archivo/archivos seleccionados con tamaño
14
+ - ✅ Botón para remover archivos individuales
15
+ - ✅ Botón "Clear all" para modo múltiple
16
+ - ✅ Posición configurable de archivos (arriba/abajo)
17
+ - ✅ Límite de cantidad de archivos en modo múltiple
14
18
  - ✅ Etiquetas personalizables (i18n)
15
19
  - ✅ Estado de arrastre visual (borde y fondo cambian)
16
20
  - ✅ Integración con formularios React
@@ -25,6 +29,8 @@ import type { FileUploadProps, FileUploadLabels } from "@adamosuiteservices/ui/f
25
29
 
26
30
  ## Uso Básico
27
31
 
32
+ ### Un Solo Archivo
33
+
28
34
  ```tsx
29
35
  import { useState } from "react";
30
36
  import { FileUpload } from "@adamosuiteservices/ui/file-upload";
@@ -41,27 +47,245 @@ function MyForm() {
41
47
  }
42
48
  ```
43
49
 
50
+ ### Múltiples Archivos
51
+
52
+ ```tsx
53
+ import { useState } from "react";
54
+ import { FileUpload } from "@adamosuiteservices/ui/file-upload";
55
+
56
+ function MyForm() {
57
+ const [files, setFiles] = useState<File[]>([]);
58
+
59
+ return (
60
+ <FileUpload
61
+ selectedFiles={files}
62
+ onFilesSelect={setFiles}
63
+ multiple
64
+ />
65
+ );
66
+ }
67
+ ```
68
+
44
69
  **Configuración por defecto**:
45
70
  - Extensiones aceptadas: `.xls`, `.xlsx`, `.numbers`
46
71
  - Tamaño máximo: 50 MB
72
+ - Máximo de archivos (modo múltiple): 10
73
+ - Posición de archivos: `below`
47
74
 
48
75
  ## Props
49
76
 
50
- ### selectedFile (requerido)
77
+ ### Modo Simple (Un Archivo)
78
+
79
+ #### selectedFile
51
80
 
52
81
  ```tsx
53
- selectedFile: File | null
82
+ selectedFile?: File | null
54
83
  ```
55
84
 
56
85
  Estado del archivo actualmente seleccionado. Usa `null` cuando no hay archivo.
57
86
 
58
- ### onFileSelect (requerido)
87
+ #### onFileSelect
88
+
89
+ ```tsx
90
+ onFileSelect?: (file: File | null) => void
91
+ ```
92
+
93
+ ### Props Comunes
94
+
95
+ #### acceptedExtensions
59
96
 
60
97
  ```tsx
61
- onFileSelect: (file: File | null) => void
98
+ acceptedExtensions?: string[]
62
99
  ```
63
100
 
64
- Callback invocado cuando el usuario selecciona o remueve un archivo.
101
+ Array de extensiones de archivo permitidas. Por defecto: `[".xls", ".xlsx", ".numbers"]`
102
+
103
+ **Ejemplo**:
104
+ ```tsx
105
+ <FileUpload
106
+ selectedFile={file}
107
+ onFileSelect={setFile}
108
+ acceptedExtensions={[".pdf", ".doc", ".docx"]}
109
+ />
110
+ #### maxSizeInMB
111
+
112
+ ```tsx
113
+ maxSizeInMB?: number
114
+ ```
115
+
116
+ Tamaño máximo del archivo en megabytes. Por defecto: `50`
117
+
118
+ **Ejemplo**:
119
+ ```tsx
120
+ <FileUpload
121
+ selectedFile={file}
122
+ onFileSelect={setFile}
123
+ maxSizeInMB={10}
124
+ />
125
+ ```
126
+
127
+ #### labels
128
+
129
+ ```tsx
130
+ labels?: FileUploadLabels
131
+
132
+ type FileUploadLabels = {
133
+ dragDrop?: string
134
+ selectFile?: string
135
+ fileRequirements?: string
136
+ filesSelected?: (count: number) => string
137
+ }
138
+ ```
139
+
140
+ Etiquetas personalizables para internacionalización o textos específicos.
141
+
142
+ **Ejemplo**:
143
+ ```tsx
144
+ <FileUpload
145
+ selectedFile={file}
146
+ onFileSelect={setFile}
147
+ labels={{
148
+ dragDrop: "Arrastra y suelta tu archivo aquí o",
149
+ selectFile: "Selecciona el archivo",
150
+ fileRequirements: "Archivos permitidos: .xls, .xlsx. Tamaño máximo 50 MB."
151
+ }}
152
+ />
153
+ ```
154
+
155
+ #### invalid
156
+
157
+ ```tsx
158
+ invalid?: boolean
159
+ ```
160
+
161
+ Marca el componente como inválido, aplicando estilos destructivos. Por defecto: `false`
162
+
163
+ **Ejemplo**:
164
+ ```tsx
165
+ <FileUpload
166
+ selectedFile={file}
167
+ onFileSelect={setFile}
168
+ invalid={!!errorMessage}
169
+ />
170
+ ```
171
+
172
+ #### onInvalidFile
173
+
174
+ ```tsx
175
+ onInvalidFile?: (file: File, reason: "extension" | "size") => void
176
+ ```
177
+
178
+ Callback invocado cuando un archivo no pasa la validación. Recibe el archivo y el motivo del rechazo.
179
+
180
+ **Ejemplo**:
181
+ ```tsx
182
+ const handleInvalidFile = (file: File, reason: "extension" | "size") => {
183
+ if (reason === "extension") {
184
+ setError(`File "${file.name}" has an invalid extension.`);
185
+ } else if (reason === "size") {
186
+ setError(`File "${file.name}" exceeds the maximum size limit.`);
187
+ }
188
+ };
189
+
190
+ <FileUpload
191
+ selectedFile={file}
192
+ onFileSelect={setFile}
193
+ onInvalidFile={handleInvalidFile}
194
+ invalid={!!error}
195
+ />
196
+ ```
197
+
198
+ ### Modo Múltiple
199
+
200
+ #### selectedFiles
201
+
202
+ ```tsx
203
+ selectedFiles?: File[]
204
+ ```
205
+
206
+ Array de archivos seleccionados.
207
+
208
+ #### onFilesSelect
209
+
210
+ ```tsx
211
+ onFilesSelect?: (files: File[]) => void
212
+ ```
213
+
214
+ Callback invocado cuando el usuario selecciona, agrega o remueve archivos.
215
+
216
+ #### multiple
217
+
218
+ ```tsx
219
+ multiple?: boolean
220
+ ```
221
+
222
+ Habilita el modo de múltiples archivos. Por defecto: `false`
223
+
224
+ #### maxFiles
225
+
226
+ ```tsx
227
+ maxFiles?: number
228
+ ```
229
+
230
+ Número máximo de archivos permitidos en modo múltiple. Por defecto: `10`
231
+
232
+ #### filesPosition
233
+
234
+ ```tsx
235
+ filesPosition?: "above" | "below"
236
+ ```
237
+
238
+ Posición donde aparecen los archivos seleccionados en modo múltiple:
239
+ - `"above"` - Archivos arriba del área de drag & drop
240
+ - `"below"` - Archivos debajo del área de drag & drop (por defecto)
241
+
242
+ **Ejemplo**:
243
+ ```tsx
244
+ <FileUpload
245
+ selectedFiles={files}
246
+ onFilesSelect={setFiles}
247
+ multiple
248
+ filesPosition="above"
249
+ />
250
+ ```
251
+
252
+ #### labels
253
+
254
+ ```tsx
255
+ labels?: FileUploadLabels
256
+
257
+ type FileUploadLabels = {
258
+ dragDrop?: string
259
+ selectFile?: string
260
+ fileRequirements?: string
261
+ filesSelected?: (count: number) => string
262
+ }
263
+ ```
264
+
265
+ Etiquetas personalizables para internacionalización o textos específicos.
266
+
267
+ **Ejemplo**:
268
+ ```tsx
269
+ <FileUpload
270
+ selectedFile={file}
271
+ onFileSelect={setFile}
272
+ labels={{
273
+ dragDrop: "Arrastra y suelta tu archivo aquí o",
274
+ selectFile: "Selecciona el archivo",
275
+ fileRequirements: "Archivos permitidos: .xls, .xlsx. Tamaño máximo 50 MB."
276
+ }}
277
+ />
278
+
279
+ // Modo múltiple con contador personalizado
280
+ <FileUpload
281
+ selectedFiles={files}
282
+ onFilesSelect={setFiles}
283
+ multiple
284
+ labels={{
285
+ filesSelected: (count) => `${count} documento${count !== 1 ? "s" : ""} seleccionado${count !== 1 ? "s" : ""}`
286
+ }}
287
+ />
288
+ ```Comunes
65
289
 
66
290
  ### acceptedExtensions
67
291
 
@@ -133,35 +357,110 @@ const [file, setFile] = useState<File | null>(null);
133
357
 
134
358
  <FileUpload
135
359
  selectedFile={file}
136
- onFileSelect={setFile}
360
+ ### Múltiples Archivos
361
+
362
+ ```tsx
363
+ const [files, setFiles] = useState<File[]>([]);
364
+
365
+ <FileUpload
366
+ selectedFiles={files}
367
+ onFilesSelect={setFiles}
368
+ multiple
369
+ maxFiles={5}
370
+ labels={{
371
+ dragDrop: "Drag and drop your files here or",
372
+ selectFile: "Select files"
373
+ }}
137
374
  />
138
375
  ```
139
376
 
140
- ### Imágenes
377
+ ### Múltiples Imágenes
141
378
 
142
379
  ```tsx
143
- const [image, setImage] = useState<File | null>(null);
380
+ const [images, setImages] = useState<File[]>([]);
144
381
 
145
382
  <FileUpload
146
- selectedFile={image}
147
- onFileSelect={setImage}
383
+ selectedFiles={images}
384
+ onFilesSelect={setImages}
385
+ multiple
148
386
  acceptedExtensions={[".jpg", ".jpeg", ".png", ".gif", ".webp"]}
149
387
  maxSizeInMB={5}
388
+ maxFiles={10}
150
389
  labels={{
151
- dragDrop: "Drag and drop your image here or",
152
- selectFile: "Select an image",
153
- fileRequirements: "Supported formats: JPG, PNG, GIF, WebP. Max 5MB."
390
+ dragDrop: "Drag and drop your images here or",
391
+ selectFile: "Select images",
392
+ fileRequirements: "Supported formats: JPG, PNG, GIF, WebP. Max 5MB each. Up to 10 images.",
393
+ filesSelected: (count) => `${count} image${count !== 1 ? "s" : ""} selected`
154
394
  }}
155
395
  />
156
396
  ```
157
397
 
158
- ### Documentos PDF
398
+ ### Archivos Arriba del Área de Drop
159
399
 
160
400
  ```tsx
161
- const [document, setDocument] = useState<File | null>(null);
401
+ const [files, setFiles] = useState<File[]>([]);
162
402
 
163
403
  <FileUpload
164
- selectedFile={document}
404
+ selectedFiles={files}
405
+ onFilesSelect={setFiles}
406
+ multiple
407
+ filesPosition="above"
408
+ acceptedExtensions={[".pdf"]}
409
+ maxSizeInMB={10}
410
+ />
411
+ ```
412
+ />
413
+ ```
414
+
415
+ ### Imágenes
416
+
417
+ ```tsx
418
+ const [image, setImage] = useState<File | null>(null);
419
+
420
+ ## Estados Visuales
421
+
422
+ ### Modo Simple
423
+
424
+ #### Sin Archivo Seleccionado
425
+
426
+ El componente muestra:
427
+ - Área de drag & drop con borde punteado
428
+ - Icono de documento en fondo azul claro
429
+ - Texto instructivo y link para seleccionar archivo
430
+ - Requisitos del archivo (extensiones y tamaño)
431
+
432
+ #### Con Archivo Seleccionado
433
+
434
+ El área de drag & drop se oculta y muestra:
435
+ - Tarjeta con fondo `bg-muted` y borde
436
+ - Icono de documento en fondo `bg-primary-50`
437
+ - Nombre del archivo (con truncate si es largo)
438
+ - Tamaño del archivo en MB
439
+ - Botón destructivo para remover el archivo
440
+
441
+ ### Modo Múltiple
442
+
443
+ #### Sin Archivos
444
+
445
+ Solo se muestra el área de drag & drop.
446
+
447
+ ### Durante Drag Over
448
+
449
+ Cuando el usuario arrastra archivos sobre el área:
450
+ - Borde cambia a `border-primary`
451
+ - Transición suave con `transition-colors`
452
+
453
+ ### Estado Inválido
454
+
455
+ Cuando `invalid={true}`:
456
+ - Borde: `border-destructive`
457
+ - Fondo: `bg-destructive/5`
458
+ - Icono de documento: `text-destructive` con fondo `bg-destructive/10`
459
+ - Texto de requisitos: `text-destructive`
460
+ - Contador de archivos: `text-destructive`
461
+ - Tarjetas de archivos: borde y fondo con colores destructivos
462
+
463
+ ## Validacióne={document}
165
464
  onFileSelect={setDocument}
166
465
  acceptedExtensions={[".pdf"]}
167
466
  maxSizeInMB={20}
@@ -171,22 +470,83 @@ const [document, setDocument] = useState<File | null>(null);
171
470
  fileRequirements: "Only PDF files. Maximum 20 MB."
172
471
  }}
173
472
  />
174
- ```
473
+ ## Validación
474
+
475
+ El componente valida automáticamente:
476
+
477
+ 1. **Extensión**: Solo acepta archivos con extensiones en `acceptedExtensions`
478
+ 2. **Tamaño**: Rechaza archivos mayores a `maxSizeInMB`
479
+ 3. **Cantidad** (modo múltiple): Limita a `maxFiles` cantidad de archivos
175
480
 
176
- ### CSV/Excel para Importación
481
+ Si un archivo no cumple estas validaciones:
482
+ - No se selecciona
483
+ - Los callbacks `onFileSelect`/`onFilesSelect` no se invocan
484
+ - El callback `onInvalidFile` se invoca (si está definido) con el archivo y el motivo
485
+
486
+ ### Ejemplo con Validación Completa
177
487
 
178
488
  ```tsx
179
- const [dataFile, setDataFile] = useState<File | null>(null);
489
+ const [file, setFile] = useState<File | null>(null);
490
+ const [error, setError] = useState<string>("");
491
+
492
+ const handleInvalidFile = (file: File, reason: "extension" | "size") => {
493
+ if (reason === "extension") {
494
+ setError(`El archivo "${file.name}" tiene una extensión no permitida.`);
495
+ } else {
496
+ setError(`El archivo "${file.name}" excede el tamaño máximo.`);
497
+ }
498
+ // Auto-limpiar error después de 5 segundos
499
+ setTimeout(() => setError(""), 5000);
500
+ };
180
501
 
181
502
  <FileUpload
182
- selectedFile={dataFile}
183
- onFileSelect={setDataFile}
184
- acceptedExtensions={[".csv", ".xls", ".xlsx"]}
185
- maxSizeInMB={30}
503
+ selectedFile={file}
504
+ onFileSelect={(newFile) => {
505
+ setFile(newFile);
506
+ setError(""); // Limpiar error al seleccionar archivo válido
507
+ }}
508
+ onInvalidFile={handleInvalidFile}
509
+ invalid={!!error}
510
+ acceptedExtensions={[".pdf", ".docx"]}
511
+ maxSizeInMB={2}
186
512
  />
513
+
514
+ {error && (
515
+ <div className="adm:rounded-lg adm:border adm:border-destructive adm:bg-destructive/5 adm:p-4">
516
+ <Typography color="destructive" className="adm:text-sm adm:font-medium">
517
+ {error}
518
+ </Typography>
519
+ </div>
520
+ )}
521
+ ```
522
+
523
+ ## Comportamiento por Modo
524
+
525
+ ### Modo Simple (`selectedFile` + `onFileSelect`)
526
+
527
+ - Solo permite un archivo a la vez
528
+ - El área de drag & drop se reemplaza con la vista del archivo seleccionado
529
+ - Arrastrar un nuevo archivo reemplaza el anterior
530
+ - Usar el input también reemplaza el archivo
531
+
532
+ ### Estado Normal vs Dragging
533
+
534
+ ```tsx
535
+ // Normal
536
+ border-input bg-background
537
+
538
+ // Dragging
539
+ border-primary
187
540
  ```
188
541
 
189
- ### Archivos Comprimidos
542
+ ### Tarjeta de Archivo Seleccionado
543
+
544
+ - Padding: `p-6` (24px)
545
+ - Border radius: `rounded-2xl` (16px)
546
+ - Background: `bg-muted`
547
+ - Border: `border-input`
548
+ - Gap horizontal: `gap-4` (16px)
549
+ - Icono en fondo `bg-primary-50` con `rounded-xl` y `p-2.5`
190
550
 
191
551
  ```tsx
192
552
  const [archive, setArchive] = useState<File | null>(null);
@@ -223,11 +583,17 @@ function UploadForm() {
223
583
  body: formData
224
584
  });
225
585
 
226
- if (response.ok) {
227
- alert("File uploaded successfully!");
228
- setFile(null);
229
- }
230
- } catch (error) {
586
+ ### Botón de Remover
587
+
588
+ - Variante: `destructive-medium`
589
+ - Icono: `delete` de Material Symbols
590
+ - Color del icono: `text-destructive`
591
+
592
+ ### Botón Clear All (Modo Múltiple)
593
+
594
+ - Variante: `ghost`
595
+ - Size: `sm`
596
+ - Aparece solo cuando hay 2+ archivos
231
597
  console.error("Upload failed:", error);
232
598
  }
233
599
  };
@@ -321,33 +687,70 @@ border-primary-500 bg-primary-50
321
687
  - Label asociado correctamente con `htmlFor="file-upload"`
322
688
  - Botón de link usa `asChild` para comportamiento semántico
323
689
  - Typography con color `muted` para textos secundarios
690
+ ## Tipos TypeScript
324
691
 
325
- ## Buenas Prácticas
692
+ ```typescript
693
+ export type FileUploadLabels = {
694
+ dragDrop?: string
695
+ selectFile?: string
696
+ fileRequirements?: string
697
+ filesSelected?: (count: number) => string
698
+ };
326
699
 
327
- ### Recomendado
700
+ export type FileUploadProps = ComponentProps<"div"> & Readonly<{
701
+ // Modo simple
702
+ selectedFile?: File | null
703
+ onFileSelect?: (file: File | null) => void
704
+
705
+ // Modo múltiple
706
+ selectedFiles?: File[]
707
+ onFilesSelect?: (files: File[]) => void
708
+ multiple?: boolean
709
+ maxFiles?: number
710
+ filesPosition?: "above" | "below"
711
+
712
+ // Validación
713
+ onInvalidFile?: (file: File, reason: "extension" | "size") => void
714
+ invalid?: boolean
715
+
716
+ // Común
717
+ acceptedExtensions?: string[]
718
+ maxSizeInMB?: number
719
+ labels?: FileUploadLabels
720
+ ### FormData para Upload
328
721
 
329
722
  ```tsx
330
- // Estado controlado con useState
331
- const [file, setFile] = useState<File | null>(null);
723
+ // Un archivo
724
+ const uploadFile = async (file: File) => {
725
+ const formData = new FormData();
726
+ formData.append("file", file);
727
+ formData.append("metadata", JSON.stringify({
728
+ uploadedAt: new Date().toISOString()
729
+ }));
332
730
 
333
- // Validación de tamaño apropiada
334
- maxSizeInMB={10} // Para documentos comunes
335
- maxSizeInMB={5} // Para imágenes
336
- maxSizeInMB={500} // Para archivos grandes (zip, videos)
731
+ const response = await fetch("/api/upload", {
732
+ method: "POST",
733
+ body: formData
734
+ });
337
735
 
338
- // Labels específicas y claras
339
- labels={{
340
- dragDrop: "Drag and drop your invoice here or",
341
- selectFile: "Select invoice",
342
- fileRequirements: "PDF only. Max 10MB."
343
- }}
736
+ return response.json();
737
+ };
344
738
 
345
- // Reset después de upload exitoso
346
- onSuccess={() => setFile(null)}
347
- ```
739
+ // Múltiples archivos
740
+ const uploadFiles = async (files: File[]) => {
741
+ const formData = new FormData();
742
+ files.forEach((file, index) => {
743
+ formData.append(`file${index}`, file);
744
+ });
348
745
 
349
- ### Evitar
746
+ const response = await fetch("/api/upload-multiple", {
747
+ method: "POST",
748
+ body: formData
749
+ });
350
750
 
751
+ return response.json();
752
+ };
753
+ ```
351
754
  ```tsx
352
755
  // No validar el archivo antes de hacer upload
353
756
  // Siempre verificar que file no sea null
@@ -408,25 +811,31 @@ const [progress, setProgress] = useState(0);
408
811
 
409
812
  const uploadWithProgress = async (file: File) => {
410
813
  const formData = new FormData();
411
- formData.append("file", file);
412
-
413
- const xhr = new XMLHttpRequest();
414
-
415
- xhr.upload.addEventListener("progress", (e) => {
416
- if (e.lengthComputable) {
417
- setProgress((e.loaded / e.total) * 100);
418
- }
419
- });
420
-
421
- xhr.open("POST", "/api/upload");
422
- xhr.send(formData);
423
- };
424
- ```
814
+ ## Comparación con Input type="file"
425
815
 
426
- ## Personalización
816
+ | Característica | Input file nativo | FileUpload |
817
+ |----------------|-------------------|------------|
818
+ | Drag & Drop | ❌ No | ✅ Sí |
819
+ | Modo múltiple | ⚠️ Básico | ✅ Completo con UI |
820
+ | Validación visual | ❌ No | ✅ Sí |
821
+ | Preview de archivos | ❌ No | ✅ Sí con nombre y tamaño |
822
+ | Extensiones validadas | ⚠️ Solo accept attribute | ✅ Con feedback visual |
823
+ | Tamaño validado | ❌ Solo en backend | ✅ Cliente y servidor |
824
+ | Límite de archivos | ❌ No | ✅ Sí (maxFiles) |
825
+ | Remover archivos | ❌ No | ✅ Individual y Clear all |
826
+ | Posición de preview | ❌ No configurable | ✅ Arriba o abajo |
827
+ | UX consistente | ❌ Varía por browser | ✅ Consistente |
828
+ | Labels customizables | ❌ No | ✅ Sí (i18n friendly) |
427
829
 
428
- ### Custom Styling
830
+ ## Notas
429
831
 
832
+ - El componente usa `File` API nativa del browser
833
+ - La validación es solo en cliente - siempre validar también en el servidor
834
+ - En modo simple, el drag & drop acepta un archivo a la vez
835
+ - En modo múltiple, se pueden arrastrar múltiples archivos simultáneamente
836
+ - Los archivos no se almacenan automáticamente - manejar el upload con los callbacks
837
+ - El componente es completamente controlado - el padre maneja el estado de los archivos
838
+ - El input file es reutilizable: después de seleccionar, se resetea para permitir seleccionar el mismo archivo de nuevo
430
839
  ```tsx
431
840
  <FileUpload
432
841
  selectedFile={file}