@adamosuiteservices/ui 2.13.0 → 2.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,473 @@
|
|
|
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. 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
|
+
- ✅ Validación de extensiones de archivo
|
|
11
|
+
- ✅ Validación de tamaño máximo
|
|
12
|
+
- ✅ Vista previa del archivo seleccionado con tamaño
|
|
13
|
+
- ✅ Botón para remover archivo seleccionado
|
|
14
|
+
- ✅ Etiquetas personalizables (i18n)
|
|
15
|
+
- ✅ Estado de arrastre visual (borde y fondo cambian)
|
|
16
|
+
- ✅ Integración con formularios React
|
|
17
|
+
- ✅ TypeScript completo con tipos exportados
|
|
18
|
+
|
|
19
|
+
## Importación
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { FileUpload } from "@adamosuiteservices/ui/file-upload";
|
|
23
|
+
import type { FileUploadProps, FileUploadLabels } from "@adamosuiteservices/ui/file-upload";
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Uso Básico
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { useState } from "react";
|
|
30
|
+
import { FileUpload } from "@adamosuiteservices/ui/file-upload";
|
|
31
|
+
|
|
32
|
+
function MyForm() {
|
|
33
|
+
const [file, setFile] = useState<File | null>(null);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<FileUpload
|
|
37
|
+
selectedFile={file}
|
|
38
|
+
onFileSelect={setFile}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Configuración por defecto**:
|
|
45
|
+
- Extensiones aceptadas: `.xls`, `.xlsx`, `.numbers`
|
|
46
|
+
- Tamaño máximo: 50 MB
|
|
47
|
+
|
|
48
|
+
## Props
|
|
49
|
+
|
|
50
|
+
### selectedFile (requerido)
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
selectedFile: File | null
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Estado del archivo actualmente seleccionado. Usa `null` cuando no hay archivo.
|
|
57
|
+
|
|
58
|
+
### onFileSelect (requerido)
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
onFileSelect: (file: File | null) => void
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Callback invocado cuando el usuario selecciona o remueve un archivo.
|
|
65
|
+
|
|
66
|
+
### acceptedExtensions
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
acceptedExtensions?: string[]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Array de extensiones de archivo permitidas. Por defecto: `[".xls", ".xlsx", ".numbers"]`
|
|
73
|
+
|
|
74
|
+
**Ejemplo**:
|
|
75
|
+
```tsx
|
|
76
|
+
<FileUpload
|
|
77
|
+
selectedFile={file}
|
|
78
|
+
onFileSelect={setFile}
|
|
79
|
+
acceptedExtensions={[".pdf", ".doc", ".docx"]}
|
|
80
|
+
/>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### maxSizeInMB
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
maxSizeInMB?: number
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Tamaño máximo del archivo en megabytes. Por defecto: `50`
|
|
90
|
+
|
|
91
|
+
**Ejemplo**:
|
|
92
|
+
```tsx
|
|
93
|
+
<FileUpload
|
|
94
|
+
selectedFile={file}
|
|
95
|
+
onFileSelect={setFile}
|
|
96
|
+
maxSizeInMB={10}
|
|
97
|
+
/>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### labels
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
labels?: FileUploadLabels
|
|
104
|
+
|
|
105
|
+
type FileUploadLabels = {
|
|
106
|
+
dragDrop?: string
|
|
107
|
+
selectFile?: string
|
|
108
|
+
fileRequirements?: string
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Etiquetas personalizables para internacionalización o textos específicos.
|
|
113
|
+
|
|
114
|
+
**Ejemplo**:
|
|
115
|
+
```tsx
|
|
116
|
+
<FileUpload
|
|
117
|
+
selectedFile={file}
|
|
118
|
+
onFileSelect={setFile}
|
|
119
|
+
labels={{
|
|
120
|
+
dragDrop: "Arrastra y suelta tu archivo aquí o",
|
|
121
|
+
selectFile: "Selecciona el archivo",
|
|
122
|
+
fileRequirements: "Archivos permitidos: .xls, .xlsx. Tamaño máximo 50 MB."
|
|
123
|
+
}}
|
|
124
|
+
/>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Ejemplos
|
|
128
|
+
|
|
129
|
+
### Archivos de Excel (Por Defecto)
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
const [file, setFile] = useState<File | null>(null);
|
|
133
|
+
|
|
134
|
+
<FileUpload
|
|
135
|
+
selectedFile={file}
|
|
136
|
+
onFileSelect={setFile}
|
|
137
|
+
/>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Imágenes
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
const [image, setImage] = useState<File | null>(null);
|
|
144
|
+
|
|
145
|
+
<FileUpload
|
|
146
|
+
selectedFile={image}
|
|
147
|
+
onFileSelect={setImage}
|
|
148
|
+
acceptedExtensions={[".jpg", ".jpeg", ".png", ".gif", ".webp"]}
|
|
149
|
+
maxSizeInMB={5}
|
|
150
|
+
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."
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Documentos PDF
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
const [document, setDocument] = useState<File | null>(null);
|
|
162
|
+
|
|
163
|
+
<FileUpload
|
|
164
|
+
selectedFile={document}
|
|
165
|
+
onFileSelect={setDocument}
|
|
166
|
+
acceptedExtensions={[".pdf"]}
|
|
167
|
+
maxSizeInMB={20}
|
|
168
|
+
labels={{
|
|
169
|
+
dragDrop: "Drag and drop your PDF here or",
|
|
170
|
+
selectFile: "Select PDF",
|
|
171
|
+
fileRequirements: "Only PDF files. Maximum 20 MB."
|
|
172
|
+
}}
|
|
173
|
+
/>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### CSV/Excel para Importación
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
const [dataFile, setDataFile] = useState<File | null>(null);
|
|
180
|
+
|
|
181
|
+
<FileUpload
|
|
182
|
+
selectedFile={dataFile}
|
|
183
|
+
onFileSelect={setDataFile}
|
|
184
|
+
acceptedExtensions={[".csv", ".xls", ".xlsx"]}
|
|
185
|
+
maxSizeInMB={30}
|
|
186
|
+
/>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Archivos Comprimidos
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
const [archive, setArchive] = useState<File | null>(null);
|
|
193
|
+
|
|
194
|
+
<FileUpload
|
|
195
|
+
selectedFile={archive}
|
|
196
|
+
onFileSelect={setArchive}
|
|
197
|
+
acceptedExtensions={[".zip", ".rar", ".7z"]}
|
|
198
|
+
maxSizeInMB={500}
|
|
199
|
+
labels={{
|
|
200
|
+
dragDrop: "Drag and drop your archive here or",
|
|
201
|
+
selectFile: "Select the archive"
|
|
202
|
+
}}
|
|
203
|
+
/>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Con Integración en Formulario
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
function UploadForm() {
|
|
210
|
+
const [file, setFile] = useState<File | null>(null);
|
|
211
|
+
|
|
212
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
|
|
215
|
+
if (!file) return;
|
|
216
|
+
|
|
217
|
+
const formData = new FormData();
|
|
218
|
+
formData.append("file", file);
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const response = await fetch("/api/upload", {
|
|
222
|
+
method: "POST",
|
|
223
|
+
body: formData
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (response.ok) {
|
|
227
|
+
alert("File uploaded successfully!");
|
|
228
|
+
setFile(null);
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error("Upload failed:", error);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
237
|
+
<FileUpload
|
|
238
|
+
selectedFile={file}
|
|
239
|
+
onFileSelect={setFile}
|
|
240
|
+
/>
|
|
241
|
+
<Button type="submit" disabled={!file}>
|
|
242
|
+
Upload File
|
|
243
|
+
</Button>
|
|
244
|
+
</form>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Estados Visuales
|
|
250
|
+
|
|
251
|
+
### Sin Archivo Seleccionado
|
|
252
|
+
|
|
253
|
+
El componente muestra:
|
|
254
|
+
- Área de drag & drop con borde punteado
|
|
255
|
+
- Icono de documento en fondo azul claro
|
|
256
|
+
- Texto instructivo y link para seleccionar archivo
|
|
257
|
+
- Requisitos del archivo (extensiones y tamaño)
|
|
258
|
+
|
|
259
|
+
### Durante Drag Over
|
|
260
|
+
|
|
261
|
+
Cuando el usuario arrastra un archivo sobre el área:
|
|
262
|
+
- Borde cambia a `border-primary-500`
|
|
263
|
+
- Fondo cambia a `bg-primary-50`
|
|
264
|
+
- Transición suave con `transition-colors`
|
|
265
|
+
|
|
266
|
+
### Con Archivo Seleccionado
|
|
267
|
+
|
|
268
|
+
El componente muestra:
|
|
269
|
+
- Tarjeta con fondo `bg-neutral-100`
|
|
270
|
+
- Icono de documento en fondo blanco
|
|
271
|
+
- Nombre del archivo (con truncate si es largo)
|
|
272
|
+
- Tamaño del archivo en MB
|
|
273
|
+
- Botón destructivo para remover el archivo
|
|
274
|
+
|
|
275
|
+
## Validación
|
|
276
|
+
|
|
277
|
+
El componente valida automáticamente:
|
|
278
|
+
|
|
279
|
+
1. **Extensión**: Solo acepta archivos con extensiones en `acceptedExtensions`
|
|
280
|
+
2. **Tamaño**: Rechaza archivos mayores a `maxSizeInMB`
|
|
281
|
+
|
|
282
|
+
Si un archivo no cumple estas validaciones, no se selecciona y `onFileSelect` no se invoca.
|
|
283
|
+
|
|
284
|
+
## Estilos Base
|
|
285
|
+
|
|
286
|
+
### Área de Drag & Drop
|
|
287
|
+
|
|
288
|
+
- Padding: `p-6` (24px)
|
|
289
|
+
- Border radius: `rounded-2xl` (16px)
|
|
290
|
+
- Border: `border-2 border-dashed`
|
|
291
|
+
- Gap entre elementos: `gap-6` (24px)
|
|
292
|
+
- Transiciones: `transition-colors`
|
|
293
|
+
|
|
294
|
+
### Estado Normal vs Dragging
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
// Normal
|
|
298
|
+
border-neutral-300 bg-neutral-50
|
|
299
|
+
|
|
300
|
+
// Dragging
|
|
301
|
+
border-primary-500 bg-primary-50
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Tarjeta de Archivo Seleccionado
|
|
305
|
+
|
|
306
|
+
- Padding: `p-6` (24px)
|
|
307
|
+
- Border radius: `rounded-2xl` (16px)
|
|
308
|
+
- Background: `bg-neutral-100`
|
|
309
|
+
- Gap horizontal: `gap-4` (16px)
|
|
310
|
+
- Icono en fondo blanco con `rounded-xl` y `p-2.5`
|
|
311
|
+
|
|
312
|
+
### Botón de Remover
|
|
313
|
+
|
|
314
|
+
- Variante: `destructive-medium`
|
|
315
|
+
- Icono: `delete` de Material Symbols
|
|
316
|
+
- Color del icono: `text-destructive-500`
|
|
317
|
+
|
|
318
|
+
## Accesibilidad
|
|
319
|
+
|
|
320
|
+
- Input file oculto con `className="adm:hidden"`
|
|
321
|
+
- Label asociado correctamente con `htmlFor="file-upload"`
|
|
322
|
+
- Botón de link usa `asChild` para comportamiento semántico
|
|
323
|
+
- Typography con color `muted` para textos secundarios
|
|
324
|
+
|
|
325
|
+
## Buenas Prácticas
|
|
326
|
+
|
|
327
|
+
### ✅ Recomendado
|
|
328
|
+
|
|
329
|
+
```tsx
|
|
330
|
+
// Estado controlado con useState
|
|
331
|
+
const [file, setFile] = useState<File | null>(null);
|
|
332
|
+
|
|
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)
|
|
337
|
+
|
|
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
|
+
}}
|
|
344
|
+
|
|
345
|
+
// Reset después de upload exitoso
|
|
346
|
+
onSuccess={() => setFile(null)}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### ❌ Evitar
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
// No validar el archivo antes de hacer upload
|
|
353
|
+
// Siempre verificar que file no sea null
|
|
354
|
+
|
|
355
|
+
// Extensiones demasiado permisivas
|
|
356
|
+
acceptedExtensions={[".*"]} // Inseguro
|
|
357
|
+
|
|
358
|
+
// Tamaño excesivo sin justificación
|
|
359
|
+
maxSizeInMB={10000} // 10GB es excesivo
|
|
360
|
+
|
|
361
|
+
// Labels genéricas cuando el contexto es específico
|
|
362
|
+
labels={{ selectFile: "Select file" }} // Poco descriptivo
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## Tipos TypeScript
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
export type FileUploadLabels = {
|
|
369
|
+
dragDrop?: string
|
|
370
|
+
selectFile?: string
|
|
371
|
+
fileRequirements?: string
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
export type FileUploadProps = ComponentProps<"div"> & Readonly<{
|
|
375
|
+
selectedFile: File | null
|
|
376
|
+
onFileSelect: (file: File | null) => void
|
|
377
|
+
acceptedExtensions?: string[]
|
|
378
|
+
maxSizeInMB?: number
|
|
379
|
+
labels?: FileUploadLabels
|
|
380
|
+
}>;
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Integración con Backend
|
|
384
|
+
|
|
385
|
+
### FormData para Upload
|
|
386
|
+
|
|
387
|
+
```tsx
|
|
388
|
+
const uploadFile = async (file: File) => {
|
|
389
|
+
const formData = new FormData();
|
|
390
|
+
formData.append("file", file);
|
|
391
|
+
formData.append("metadata", JSON.stringify({
|
|
392
|
+
uploadedAt: new Date().toISOString()
|
|
393
|
+
}));
|
|
394
|
+
|
|
395
|
+
const response = await fetch("/api/upload", {
|
|
396
|
+
method: "POST",
|
|
397
|
+
body: formData
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
return response.json();
|
|
401
|
+
};
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Con Progress Tracking
|
|
405
|
+
|
|
406
|
+
```tsx
|
|
407
|
+
const [progress, setProgress] = useState(0);
|
|
408
|
+
|
|
409
|
+
const uploadWithProgress = async (file: File) => {
|
|
410
|
+
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
|
+
```
|
|
425
|
+
|
|
426
|
+
## Personalización
|
|
427
|
+
|
|
428
|
+
### Custom Styling
|
|
429
|
+
|
|
430
|
+
```tsx
|
|
431
|
+
<FileUpload
|
|
432
|
+
selectedFile={file}
|
|
433
|
+
onFileSelect={setFile}
|
|
434
|
+
className="adm:max-w-2xl adm:mx-auto"
|
|
435
|
+
/>
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Con Wrapper Adicional
|
|
439
|
+
|
|
440
|
+
```tsx
|
|
441
|
+
<div className="adm:space-y-4">
|
|
442
|
+
<Label>Upload your document</Label>
|
|
443
|
+
<FileUpload
|
|
444
|
+
selectedFile={file}
|
|
445
|
+
onFileSelect={setFile}
|
|
446
|
+
/>
|
|
447
|
+
{file && (
|
|
448
|
+
<Typography color="success">
|
|
449
|
+
✓ File ready to upload
|
|
450
|
+
</Typography>
|
|
451
|
+
)}
|
|
452
|
+
</div>
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
## Comparación con Input type="file"
|
|
456
|
+
|
|
457
|
+
| Característica | Input file nativo | FileUpload |
|
|
458
|
+
|----------------|-------------------|------------|
|
|
459
|
+
| Drag & Drop | ❌ No | ✅ Sí |
|
|
460
|
+
| Validación visual | ❌ No | ✅ Sí |
|
|
461
|
+
| Preview del archivo | ❌ No | ✅ Sí con nombre y tamaño |
|
|
462
|
+
| Extensiones validadas | ⚠️ Solo accept attribute | ✅ Con feedback visual |
|
|
463
|
+
| Tamaño validado | ❌ Solo en backend | ✅ Cliente y servidor |
|
|
464
|
+
| UX consistente | ❌ Varía por browser | ✅ Consistente |
|
|
465
|
+
| Labels customizables | ❌ No | ✅ Sí (i18n friendly) |
|
|
466
|
+
|
|
467
|
+
## Notas
|
|
468
|
+
|
|
469
|
+
- El componente usa `File` API nativa del browser
|
|
470
|
+
- La validación es solo en cliente - siempre validar también en el servidor
|
|
471
|
+
- El drag & drop solo funciona con un archivo a la vez
|
|
472
|
+
- Los archivos no se almacenan automáticamente - manejar el upload con `onFileSelect`
|
|
473
|
+
- El componente es completamente controlado - el padre maneja el estado del archivo
|