@adamosuiteservices/ui 2.13.1 → 2.13.3
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/slider/slider.d.ts +5 -2
- 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/slider.cjs +6 -7
- package/dist/slider.js +191 -177
- 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/AI-GUIDE.md +321 -321
- package/docs/components/layout/sidebar.md +399 -399
- package/docs/components/layout/toaster.md +436 -436
- package/docs/components/ui/accordion-rounded.md +584 -584
- package/docs/components/ui/accordion.md +269 -269
- package/docs/components/ui/calendar.md +1159 -1159
- package/docs/components/ui/card.md +1455 -1455
- package/docs/components/ui/checkbox.md +292 -292
- package/docs/components/ui/collapsible.md +323 -323
- package/docs/components/ui/dialog.md +628 -628
- package/docs/components/ui/field.md +706 -706
- package/docs/components/ui/file-upload.md +475 -66
- package/docs/components/ui/hover-card.md +446 -446
- package/docs/components/ui/kbd.md +434 -434
- package/docs/components/ui/label.md +359 -359
- package/docs/components/ui/pagination.md +650 -650
- package/docs/components/ui/popover.md +536 -536
- package/docs/components/ui/progress.md +182 -182
- package/docs/components/ui/radio-group.md +311 -311
- package/docs/components/ui/separator.md +214 -214
- package/docs/components/ui/sheet.md +174 -174
- package/docs/components/ui/skeleton.md +140 -140
- package/docs/components/ui/slider.md +460 -341
- package/docs/components/ui/spinner.md +170 -170
- package/docs/components/ui/switch.md +408 -408
- package/docs/components/ui/tabs-underline.md +106 -106
- package/docs/components/ui/tabs.md +122 -122
- package/docs/components/ui/textarea.md +243 -243
- package/docs/components/ui/toggle.md +237 -237
- package/docs/components/ui/tooltip.md +317 -317
- package/docs/components/ui/typography.md +320 -280
- package/package.json +1 -1
- package/dist/typography-9EoV0kcN.js +0 -44
- package/dist/typography-DqQZZpkD.cjs +0 -1
|
@@ -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
|
|
13
|
-
- ✅ Botón para remover
|
|
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
|
-
###
|
|
77
|
+
### Modo Simple (Un Archivo)
|
|
78
|
+
|
|
79
|
+
#### selectedFile
|
|
51
80
|
|
|
52
81
|
```tsx
|
|
53
|
-
selectedFile
|
|
82
|
+
selectedFile?: File | null
|
|
54
83
|
```
|
|
55
84
|
|
|
56
85
|
Estado del archivo actualmente seleccionado. Usa `null` cuando no hay archivo.
|
|
57
86
|
|
|
58
|
-
|
|
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
|
-
|
|
98
|
+
acceptedExtensions?: string[]
|
|
62
99
|
```
|
|
63
100
|
|
|
64
|
-
|
|
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
|
-
|
|
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 [
|
|
380
|
+
const [images, setImages] = useState<File[]>([]);
|
|
144
381
|
|
|
145
382
|
<FileUpload
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
152
|
-
selectFile: "Select
|
|
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
|
-
###
|
|
398
|
+
### Archivos Arriba del Área de Drop
|
|
159
399
|
|
|
160
400
|
```tsx
|
|
161
|
-
const [
|
|
401
|
+
const [files, setFiles] = useState<File[]>([]);
|
|
162
402
|
|
|
163
403
|
<FileUpload
|
|
164
|
-
|
|
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
|
-
|
|
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 [
|
|
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={
|
|
183
|
-
onFileSelect={
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
692
|
+
```typescript
|
|
693
|
+
export type FileUploadLabels = {
|
|
694
|
+
dragDrop?: string
|
|
695
|
+
selectFile?: string
|
|
696
|
+
fileRequirements?: string
|
|
697
|
+
filesSelected?: (count: number) => string
|
|
698
|
+
};
|
|
326
699
|
|
|
327
|
-
|
|
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
|
-
//
|
|
331
|
-
const
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
731
|
+
const response = await fetch("/api/upload", {
|
|
732
|
+
method: "POST",
|
|
733
|
+
body: formData
|
|
734
|
+
});
|
|
337
735
|
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
//
|
|
346
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|