@adamosuiteservices/ui 2.13.0 → 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.
- package/dist/components/ui/file-upload/file-upload.d.ts +11 -3
- package/dist/components/ui/typography/typography.d.ts +1 -1
- package/dist/file-upload.cjs +6 -18
- package/dist/file-upload.js +264 -134
- package/dist/styles.css +1 -1
- package/dist/typography-Bj8oEDuE.cjs +1 -0
- package/dist/typography-MnY0LQoZ.js +50 -0
- package/dist/typography.cjs +1 -1
- package/dist/typography.js +1 -1
- package/docs/components/ui/file-upload.md +882 -0
- package/docs/components/ui/typography.md +48 -8
- package/package.json +1 -1
- package/dist/typography-9EoV0kcN.js +0 -44
- package/dist/typography-DqQZZpkD.cjs +0 -1
|
@@ -0,0 +1,882 @@
|
|
|
1
|
+
# File Upload Component
|
|
2
|
+
|
|
3
|
+
## Descripción
|
|
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. 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
|
+
|
|
7
|
+
## Características
|
|
8
|
+
|
|
9
|
+
- ✅ Drag & Drop con feedback visual
|
|
10
|
+
- ✅ Modo simple (un archivo) y múltiple (varios archivos)
|
|
11
|
+
- ✅ Validación de extensiones de archivo
|
|
12
|
+
- ✅ Validación de tamaño máximo
|
|
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
|
|
18
|
+
- ✅ Etiquetas personalizables (i18n)
|
|
19
|
+
- ✅ Estado de arrastre visual (borde y fondo cambian)
|
|
20
|
+
- ✅ Integración con formularios React
|
|
21
|
+
- ✅ TypeScript completo con tipos exportados
|
|
22
|
+
|
|
23
|
+
## Importación
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { FileUpload } from "@adamosuiteservices/ui/file-upload";
|
|
27
|
+
import type { FileUploadProps, FileUploadLabels } from "@adamosuiteservices/ui/file-upload";
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Uso Básico
|
|
31
|
+
|
|
32
|
+
### Un Solo Archivo
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { useState } from "react";
|
|
36
|
+
import { FileUpload } from "@adamosuiteservices/ui/file-upload";
|
|
37
|
+
|
|
38
|
+
function MyForm() {
|
|
39
|
+
const [file, setFile] = useState<File | null>(null);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<FileUpload
|
|
43
|
+
selectedFile={file}
|
|
44
|
+
onFileSelect={setFile}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
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
|
+
|
|
69
|
+
**Configuración por defecto**:
|
|
70
|
+
- Extensiones aceptadas: `.xls`, `.xlsx`, `.numbers`
|
|
71
|
+
- Tamaño máximo: 50 MB
|
|
72
|
+
- Máximo de archivos (modo múltiple): 10
|
|
73
|
+
- Posición de archivos: `below`
|
|
74
|
+
|
|
75
|
+
## Props
|
|
76
|
+
|
|
77
|
+
### Modo Simple (Un Archivo)
|
|
78
|
+
|
|
79
|
+
#### selectedFile
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
selectedFile?: File | null
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Estado del archivo actualmente seleccionado. Usa `null` cuando no hay archivo.
|
|
86
|
+
|
|
87
|
+
#### onFileSelect
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
onFileSelect?: (file: File | null) => void
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Props Comunes
|
|
94
|
+
|
|
95
|
+
#### acceptedExtensions
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
acceptedExtensions?: string[]
|
|
99
|
+
```
|
|
100
|
+
|
|
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
|
|
289
|
+
|
|
290
|
+
### acceptedExtensions
|
|
291
|
+
|
|
292
|
+
```tsx
|
|
293
|
+
acceptedExtensions?: string[]
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Array de extensiones de archivo permitidas. Por defecto: `[".xls", ".xlsx", ".numbers"]`
|
|
297
|
+
|
|
298
|
+
**Ejemplo**:
|
|
299
|
+
```tsx
|
|
300
|
+
<FileUpload
|
|
301
|
+
selectedFile={file}
|
|
302
|
+
onFileSelect={setFile}
|
|
303
|
+
acceptedExtensions={[".pdf", ".doc", ".docx"]}
|
|
304
|
+
/>
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### maxSizeInMB
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
maxSizeInMB?: number
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Tamaño máximo del archivo en megabytes. Por defecto: `50`
|
|
314
|
+
|
|
315
|
+
**Ejemplo**:
|
|
316
|
+
```tsx
|
|
317
|
+
<FileUpload
|
|
318
|
+
selectedFile={file}
|
|
319
|
+
onFileSelect={setFile}
|
|
320
|
+
maxSizeInMB={10}
|
|
321
|
+
/>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### labels
|
|
325
|
+
|
|
326
|
+
```tsx
|
|
327
|
+
labels?: FileUploadLabels
|
|
328
|
+
|
|
329
|
+
type FileUploadLabels = {
|
|
330
|
+
dragDrop?: string
|
|
331
|
+
selectFile?: string
|
|
332
|
+
fileRequirements?: string
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Etiquetas personalizables para internacionalización o textos específicos.
|
|
337
|
+
|
|
338
|
+
**Ejemplo**:
|
|
339
|
+
```tsx
|
|
340
|
+
<FileUpload
|
|
341
|
+
selectedFile={file}
|
|
342
|
+
onFileSelect={setFile}
|
|
343
|
+
labels={{
|
|
344
|
+
dragDrop: "Arrastra y suelta tu archivo aquí o",
|
|
345
|
+
selectFile: "Selecciona el archivo",
|
|
346
|
+
fileRequirements: "Archivos permitidos: .xls, .xlsx. Tamaño máximo 50 MB."
|
|
347
|
+
}}
|
|
348
|
+
/>
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## Ejemplos
|
|
352
|
+
|
|
353
|
+
### Archivos de Excel (Por Defecto)
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
const [file, setFile] = useState<File | null>(null);
|
|
357
|
+
|
|
358
|
+
<FileUpload
|
|
359
|
+
selectedFile={file}
|
|
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
|
+
}}
|
|
374
|
+
/>
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Múltiples Imágenes
|
|
378
|
+
|
|
379
|
+
```tsx
|
|
380
|
+
const [images, setImages] = useState<File[]>([]);
|
|
381
|
+
|
|
382
|
+
<FileUpload
|
|
383
|
+
selectedFiles={images}
|
|
384
|
+
onFilesSelect={setImages}
|
|
385
|
+
multiple
|
|
386
|
+
acceptedExtensions={[".jpg", ".jpeg", ".png", ".gif", ".webp"]}
|
|
387
|
+
maxSizeInMB={5}
|
|
388
|
+
maxFiles={10}
|
|
389
|
+
labels={{
|
|
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`
|
|
394
|
+
}}
|
|
395
|
+
/>
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Archivos Arriba del Área de Drop
|
|
399
|
+
|
|
400
|
+
```tsx
|
|
401
|
+
const [files, setFiles] = useState<File[]>([]);
|
|
402
|
+
|
|
403
|
+
<FileUpload
|
|
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}
|
|
464
|
+
onFileSelect={setDocument}
|
|
465
|
+
acceptedExtensions={[".pdf"]}
|
|
466
|
+
maxSizeInMB={20}
|
|
467
|
+
labels={{
|
|
468
|
+
dragDrop: "Drag and drop your PDF here or",
|
|
469
|
+
selectFile: "Select PDF",
|
|
470
|
+
fileRequirements: "Only PDF files. Maximum 20 MB."
|
|
471
|
+
}}
|
|
472
|
+
/>
|
|
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
|
|
480
|
+
|
|
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
|
|
487
|
+
|
|
488
|
+
```tsx
|
|
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
|
+
};
|
|
501
|
+
|
|
502
|
+
<FileUpload
|
|
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}
|
|
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
|
|
540
|
+
```
|
|
541
|
+
|
|
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`
|
|
550
|
+
|
|
551
|
+
```tsx
|
|
552
|
+
const [archive, setArchive] = useState<File | null>(null);
|
|
553
|
+
|
|
554
|
+
<FileUpload
|
|
555
|
+
selectedFile={archive}
|
|
556
|
+
onFileSelect={setArchive}
|
|
557
|
+
acceptedExtensions={[".zip", ".rar", ".7z"]}
|
|
558
|
+
maxSizeInMB={500}
|
|
559
|
+
labels={{
|
|
560
|
+
dragDrop: "Drag and drop your archive here or",
|
|
561
|
+
selectFile: "Select the archive"
|
|
562
|
+
}}
|
|
563
|
+
/>
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### Con Integración en Formulario
|
|
567
|
+
|
|
568
|
+
```tsx
|
|
569
|
+
function UploadForm() {
|
|
570
|
+
const [file, setFile] = useState<File | null>(null);
|
|
571
|
+
|
|
572
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
573
|
+
e.preventDefault();
|
|
574
|
+
|
|
575
|
+
if (!file) return;
|
|
576
|
+
|
|
577
|
+
const formData = new FormData();
|
|
578
|
+
formData.append("file", file);
|
|
579
|
+
|
|
580
|
+
try {
|
|
581
|
+
const response = await fetch("/api/upload", {
|
|
582
|
+
method: "POST",
|
|
583
|
+
body: formData
|
|
584
|
+
});
|
|
585
|
+
|
|
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
|
|
597
|
+
console.error("Upload failed:", error);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
return (
|
|
602
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
603
|
+
<FileUpload
|
|
604
|
+
selectedFile={file}
|
|
605
|
+
onFileSelect={setFile}
|
|
606
|
+
/>
|
|
607
|
+
<Button type="submit" disabled={!file}>
|
|
608
|
+
Upload File
|
|
609
|
+
</Button>
|
|
610
|
+
</form>
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
## Estados Visuales
|
|
616
|
+
|
|
617
|
+
### Sin Archivo Seleccionado
|
|
618
|
+
|
|
619
|
+
El componente muestra:
|
|
620
|
+
- Área de drag & drop con borde punteado
|
|
621
|
+
- Icono de documento en fondo azul claro
|
|
622
|
+
- Texto instructivo y link para seleccionar archivo
|
|
623
|
+
- Requisitos del archivo (extensiones y tamaño)
|
|
624
|
+
|
|
625
|
+
### Durante Drag Over
|
|
626
|
+
|
|
627
|
+
Cuando el usuario arrastra un archivo sobre el área:
|
|
628
|
+
- Borde cambia a `border-primary-500`
|
|
629
|
+
- Fondo cambia a `bg-primary-50`
|
|
630
|
+
- Transición suave con `transition-colors`
|
|
631
|
+
|
|
632
|
+
### Con Archivo Seleccionado
|
|
633
|
+
|
|
634
|
+
El componente muestra:
|
|
635
|
+
- Tarjeta con fondo `bg-neutral-100`
|
|
636
|
+
- Icono de documento en fondo blanco
|
|
637
|
+
- Nombre del archivo (con truncate si es largo)
|
|
638
|
+
- Tamaño del archivo en MB
|
|
639
|
+
- Botón destructivo para remover el archivo
|
|
640
|
+
|
|
641
|
+
## Validación
|
|
642
|
+
|
|
643
|
+
El componente valida automáticamente:
|
|
644
|
+
|
|
645
|
+
1. **Extensión**: Solo acepta archivos con extensiones en `acceptedExtensions`
|
|
646
|
+
2. **Tamaño**: Rechaza archivos mayores a `maxSizeInMB`
|
|
647
|
+
|
|
648
|
+
Si un archivo no cumple estas validaciones, no se selecciona y `onFileSelect` no se invoca.
|
|
649
|
+
|
|
650
|
+
## Estilos Base
|
|
651
|
+
|
|
652
|
+
### Área de Drag & Drop
|
|
653
|
+
|
|
654
|
+
- Padding: `p-6` (24px)
|
|
655
|
+
- Border radius: `rounded-2xl` (16px)
|
|
656
|
+
- Border: `border-2 border-dashed`
|
|
657
|
+
- Gap entre elementos: `gap-6` (24px)
|
|
658
|
+
- Transiciones: `transition-colors`
|
|
659
|
+
|
|
660
|
+
### Estado Normal vs Dragging
|
|
661
|
+
|
|
662
|
+
```tsx
|
|
663
|
+
// Normal
|
|
664
|
+
border-neutral-300 bg-neutral-50
|
|
665
|
+
|
|
666
|
+
// Dragging
|
|
667
|
+
border-primary-500 bg-primary-50
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
### Tarjeta de Archivo Seleccionado
|
|
671
|
+
|
|
672
|
+
- Padding: `p-6` (24px)
|
|
673
|
+
- Border radius: `rounded-2xl` (16px)
|
|
674
|
+
- Background: `bg-neutral-100`
|
|
675
|
+
- Gap horizontal: `gap-4` (16px)
|
|
676
|
+
- Icono en fondo blanco con `rounded-xl` y `p-2.5`
|
|
677
|
+
|
|
678
|
+
### Botón de Remover
|
|
679
|
+
|
|
680
|
+
- Variante: `destructive-medium`
|
|
681
|
+
- Icono: `delete` de Material Symbols
|
|
682
|
+
- Color del icono: `text-destructive-500`
|
|
683
|
+
|
|
684
|
+
## Accesibilidad
|
|
685
|
+
|
|
686
|
+
- Input file oculto con `className="adm:hidden"`
|
|
687
|
+
- Label asociado correctamente con `htmlFor="file-upload"`
|
|
688
|
+
- Botón de link usa `asChild` para comportamiento semántico
|
|
689
|
+
- Typography con color `muted` para textos secundarios
|
|
690
|
+
## Tipos TypeScript
|
|
691
|
+
|
|
692
|
+
```typescript
|
|
693
|
+
export type FileUploadLabels = {
|
|
694
|
+
dragDrop?: string
|
|
695
|
+
selectFile?: string
|
|
696
|
+
fileRequirements?: string
|
|
697
|
+
filesSelected?: (count: number) => string
|
|
698
|
+
};
|
|
699
|
+
|
|
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
|
|
721
|
+
|
|
722
|
+
```tsx
|
|
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
|
+
}));
|
|
730
|
+
|
|
731
|
+
const response = await fetch("/api/upload", {
|
|
732
|
+
method: "POST",
|
|
733
|
+
body: formData
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
return response.json();
|
|
737
|
+
};
|
|
738
|
+
|
|
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
|
+
});
|
|
745
|
+
|
|
746
|
+
const response = await fetch("/api/upload-multiple", {
|
|
747
|
+
method: "POST",
|
|
748
|
+
body: formData
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
return response.json();
|
|
752
|
+
};
|
|
753
|
+
```
|
|
754
|
+
```tsx
|
|
755
|
+
// No validar el archivo antes de hacer upload
|
|
756
|
+
// Siempre verificar que file no sea null
|
|
757
|
+
|
|
758
|
+
// Extensiones demasiado permisivas
|
|
759
|
+
acceptedExtensions={[".*"]} // Inseguro
|
|
760
|
+
|
|
761
|
+
// Tamaño excesivo sin justificación
|
|
762
|
+
maxSizeInMB={10000} // 10GB es excesivo
|
|
763
|
+
|
|
764
|
+
// Labels genéricas cuando el contexto es específico
|
|
765
|
+
labels={{ selectFile: "Select file" }} // Poco descriptivo
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
## Tipos TypeScript
|
|
769
|
+
|
|
770
|
+
```typescript
|
|
771
|
+
export type FileUploadLabels = {
|
|
772
|
+
dragDrop?: string
|
|
773
|
+
selectFile?: string
|
|
774
|
+
fileRequirements?: string
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
export type FileUploadProps = ComponentProps<"div"> & Readonly<{
|
|
778
|
+
selectedFile: File | null
|
|
779
|
+
onFileSelect: (file: File | null) => void
|
|
780
|
+
acceptedExtensions?: string[]
|
|
781
|
+
maxSizeInMB?: number
|
|
782
|
+
labels?: FileUploadLabels
|
|
783
|
+
}>;
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
## Integración con Backend
|
|
787
|
+
|
|
788
|
+
### FormData para Upload
|
|
789
|
+
|
|
790
|
+
```tsx
|
|
791
|
+
const uploadFile = async (file: File) => {
|
|
792
|
+
const formData = new FormData();
|
|
793
|
+
formData.append("file", file);
|
|
794
|
+
formData.append("metadata", JSON.stringify({
|
|
795
|
+
uploadedAt: new Date().toISOString()
|
|
796
|
+
}));
|
|
797
|
+
|
|
798
|
+
const response = await fetch("/api/upload", {
|
|
799
|
+
method: "POST",
|
|
800
|
+
body: formData
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
return response.json();
|
|
804
|
+
};
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
### Con Progress Tracking
|
|
808
|
+
|
|
809
|
+
```tsx
|
|
810
|
+
const [progress, setProgress] = useState(0);
|
|
811
|
+
|
|
812
|
+
const uploadWithProgress = async (file: File) => {
|
|
813
|
+
const formData = new FormData();
|
|
814
|
+
## Comparación con Input type="file"
|
|
815
|
+
|
|
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) |
|
|
829
|
+
|
|
830
|
+
## Notas
|
|
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
|
|
839
|
+
```tsx
|
|
840
|
+
<FileUpload
|
|
841
|
+
selectedFile={file}
|
|
842
|
+
onFileSelect={setFile}
|
|
843
|
+
className="adm:max-w-2xl adm:mx-auto"
|
|
844
|
+
/>
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
### Con Wrapper Adicional
|
|
848
|
+
|
|
849
|
+
```tsx
|
|
850
|
+
<div className="adm:space-y-4">
|
|
851
|
+
<Label>Upload your document</Label>
|
|
852
|
+
<FileUpload
|
|
853
|
+
selectedFile={file}
|
|
854
|
+
onFileSelect={setFile}
|
|
855
|
+
/>
|
|
856
|
+
{file && (
|
|
857
|
+
<Typography color="success">
|
|
858
|
+
✓ File ready to upload
|
|
859
|
+
</Typography>
|
|
860
|
+
)}
|
|
861
|
+
</div>
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
## Comparación con Input type="file"
|
|
865
|
+
|
|
866
|
+
| Característica | Input file nativo | FileUpload |
|
|
867
|
+
|----------------|-------------------|------------|
|
|
868
|
+
| Drag & Drop | ❌ No | ✅ Sí |
|
|
869
|
+
| Validación visual | ❌ No | ✅ Sí |
|
|
870
|
+
| Preview del archivo | ❌ No | ✅ Sí con nombre y tamaño |
|
|
871
|
+
| Extensiones validadas | ⚠️ Solo accept attribute | ✅ Con feedback visual |
|
|
872
|
+
| Tamaño validado | ❌ Solo en backend | ✅ Cliente y servidor |
|
|
873
|
+
| UX consistente | ❌ Varía por browser | ✅ Consistente |
|
|
874
|
+
| Labels customizables | ❌ No | ✅ Sí (i18n friendly) |
|
|
875
|
+
|
|
876
|
+
## Notas
|
|
877
|
+
|
|
878
|
+
- El componente usa `File` API nativa del browser
|
|
879
|
+
- La validación es solo en cliente - siempre validar también en el servidor
|
|
880
|
+
- El drag & drop solo funciona con un archivo a la vez
|
|
881
|
+
- Los archivos no se almacenan automáticamente - manejar el upload con `onFileSelect`
|
|
882
|
+
- El componente es completamente controlado - el padre maneja el estado del archivo
|