@adamosuiteservices/ui 2.18.0 → 2.18.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/index.d.ts +0 -1
- package/dist/components/ui/{file-upload → file-upload-v2}/file-upload-v2.d.ts +9 -0
- package/dist/components/ui/file-upload-v2/index.d.ts +1 -0
- package/dist/file-upload-v2.cjs +6 -0
- package/dist/file-upload-v2.js +351 -0
- package/dist/file-upload.cjs +3 -12
- package/dist/file-upload.js +198 -531
- package/dist/styles.css +1 -1
- package/docs/components/ui/file-upload-v2.md +421 -68
- package/docs/components/ui/file-upload.md +157 -395
- package/llm.txt +4 -4
- package/package.json +5 -1
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
File upload component with **drag & drop** functionality, extension and size validation, preview of selected files, and visual drag area. Supports both single file and multiple file upload. Ideal for forms that require file uploads with immediate user feedback.
|
|
6
6
|
|
|
7
|
+
> **Looking for server integration?** Check out [FileUploadV2](file-upload-v2.md) which includes automatic ID generation and metadata support for server-side file tracking.
|
|
8
|
+
|
|
7
9
|
## Features
|
|
8
10
|
|
|
9
11
|
- ✅ Drag & drop with visual feedback
|
|
@@ -437,50 +439,13 @@ Position where selected files appear in multiple mode:
|
|
|
437
439
|
/>
|
|
438
440
|
```
|
|
439
441
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
```tsx
|
|
443
|
-
labels?: FileUploadLabels
|
|
444
|
-
|
|
445
|
-
type FileUploadLabels = {
|
|
446
|
-
dragDrop?: string
|
|
447
|
-
selectFile?: string
|
|
448
|
-
fileRequirements?: string
|
|
449
|
-
filesSelected?: (count: number) => string
|
|
450
|
-
}
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
Customizable labels for internationalization or specific texts.
|
|
454
|
-
|
|
455
|
-
**Example**:
|
|
456
|
-
|
|
457
|
-
````tsx
|
|
458
|
-
<FileUpload
|
|
459
|
-
selectedFile={file}
|
|
460
|
-
onFileSelect={setFile}
|
|
461
|
-
labels={{
|
|
462
|
-
dragDrop: "Drag and drop your file here or",
|
|
463
|
-
selectFile: "Select the file",
|
|
464
|
-
fileRequirements: "Allowed files: .xls, .xlsx. Maximum size 50 MB."
|
|
465
|
-
}}
|
|
466
|
-
/>
|
|
467
|
-
|
|
468
|
-
// Multiple mode with custom counter
|
|
469
|
-
<FileUpload
|
|
470
|
-
selectedFiles={files}
|
|
471
|
-
onFilesSelect={setFiles}
|
|
472
|
-
multiple
|
|
473
|
-
labels={{
|
|
474
|
-
filesSelected: (count) => `${count} document${count !== 1 ? "s" : ""} selected`
|
|
475
|
-
}}
|
|
476
|
-
/>
|
|
477
|
-
```Comunes
|
|
442
|
+
## Examples
|
|
478
443
|
|
|
479
|
-
###
|
|
444
|
+
### With Field components
|
|
480
445
|
|
|
481
446
|
```tsx
|
|
482
447
|
acceptedExtensions?: string[]
|
|
483
|
-
|
|
448
|
+
```
|
|
484
449
|
|
|
485
450
|
Array of allowed file extensions. Default: `[".xls", ".xlsx", ".numbers"]`
|
|
486
451
|
|
|
@@ -508,87 +473,44 @@ Maximum file size in megabytes. Default: `50`
|
|
|
508
473
|
<FileUpload selectedFile={file} onFileSelect={setFile} maxSizeInMB={10} />
|
|
509
474
|
```
|
|
510
475
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
```tsx
|
|
514
|
-
labels?: FileUploadLabels
|
|
476
|
+
## Examples
|
|
515
477
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
478
|
+
const handleInvalidFile = (file: File, reason: "extension" | "size") => {
|
|
479
|
+
if (reason === "extension") {
|
|
480
|
+
setError(`File "${file.name}" has an invalid extension.`);
|
|
481
|
+
} else if (reason === "size") {
|
|
482
|
+
setError(`File "${file.name}" exceeds the maximum size limit.`);
|
|
520
483
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
Customizable labels for internationalization or specific texts.
|
|
524
|
-
|
|
525
|
-
**Example**:
|
|
484
|
+
};
|
|
526
485
|
|
|
527
|
-
|
|
486
|
+
return (
|
|
487
|
+
<FieldGroup className="adm:max-w-xl">
|
|
488
|
+
<Field>
|
|
489
|
+
<FieldLabel>Upload document (PDF or DOCX only, max 2MB)</FieldLabel>
|
|
528
490
|
<FileUpload
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
491
|
+
selectedFile={file}
|
|
492
|
+
onFileSelect={(newFile) => {
|
|
493
|
+
setFile(newFile);
|
|
494
|
+
if (newFile) setError("");
|
|
495
|
+
}}
|
|
496
|
+
onInvalidFile={handleInvalidFile}
|
|
497
|
+
aria-invalid={!!error}
|
|
498
|
+
acceptedExtensions={[".pdf", ".docx"]}
|
|
499
|
+
maxSizeInMB={2}
|
|
536
500
|
/>
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
FieldDescription,
|
|
548
|
-
FieldError,
|
|
549
|
-
FieldGroup,
|
|
550
|
-
} from "@adamosuiteservices/ui/field";
|
|
551
|
-
import { FileUpload } from "@adamosuiteservices/ui/file-upload";
|
|
552
|
-
|
|
553
|
-
function FileUploadForm() {
|
|
554
|
-
const [file, setFile] = useState<File | null>(null);
|
|
555
|
-
const [error, setError] = useState<string>("");
|
|
556
|
-
|
|
557
|
-
const handleInvalidFile = (file: File, reason: "extension" | "size") => {
|
|
558
|
-
if (reason === "extension") {
|
|
559
|
-
setError(`File "${file.name}" has an invalid extension.`);
|
|
560
|
-
} else if (reason === "size") {
|
|
561
|
-
setError(`File "${file.name}" exceeds the maximum size limit.`);
|
|
562
|
-
}
|
|
563
|
-
};
|
|
564
|
-
|
|
565
|
-
return (
|
|
566
|
-
<FieldGroup className="adm:max-w-xl">
|
|
567
|
-
<Field>
|
|
568
|
-
<FieldLabel>Upload document (PDF or DOCX only, max 2MB)</FieldLabel>
|
|
569
|
-
<FileUpload
|
|
570
|
-
selectedFile={file}
|
|
571
|
-
onFileSelect={(newFile) => {
|
|
572
|
-
setFile(newFile);
|
|
573
|
-
if (newFile) setError("");
|
|
574
|
-
}}
|
|
575
|
-
onInvalidFile={handleInvalidFile}
|
|
576
|
-
aria-invalid={!!error}
|
|
577
|
-
acceptedExtensions={[".pdf", ".docx"]}
|
|
578
|
-
maxSizeInMB={2}
|
|
579
|
-
/>
|
|
580
|
-
{error ? (
|
|
581
|
-
<FieldError>{error}</FieldError>
|
|
582
|
-
) : (
|
|
583
|
-
<FieldDescription>
|
|
584
|
-
Supported formats: PDF, DOCX. Maximum 2MB.
|
|
585
|
-
</FieldDescription>
|
|
586
|
-
)}
|
|
587
|
-
</Field>
|
|
588
|
-
</FieldGroup>
|
|
589
|
-
);
|
|
501
|
+
{error ? (
|
|
502
|
+
<FieldError>{error}</FieldError>
|
|
503
|
+
) : (
|
|
504
|
+
<FieldDescription>
|
|
505
|
+
Supported formats: PDF, DOCX. Maximum 2MB.
|
|
506
|
+
</FieldDescription>
|
|
507
|
+
)}
|
|
508
|
+
</Field>
|
|
509
|
+
</FieldGroup>
|
|
510
|
+
);
|
|
590
511
|
}
|
|
591
|
-
|
|
512
|
+
|
|
513
|
+
````
|
|
592
514
|
|
|
593
515
|
### Estado Deshabilitado
|
|
594
516
|
|
|
@@ -633,7 +555,7 @@ function DisabledWithFileExample() {
|
|
|
633
555
|
</FieldGroup>
|
|
634
556
|
);
|
|
635
557
|
}
|
|
636
|
-
|
|
558
|
+
````
|
|
637
559
|
|
|
638
560
|
### Inside fieldset disabled
|
|
639
561
|
|
|
@@ -992,269 +914,97 @@ const handleInvalidFile = (file: File, reason: "extension" | "size") => {
|
|
|
992
914
|
- Dragging a new file replaces the previous one
|
|
993
915
|
- Using the input also replaces the file
|
|
994
916
|
|
|
995
|
-
###
|
|
917
|
+
### Multiple mode (`selectedFiles` + `onFilesSelect`)
|
|
996
918
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
919
|
+
- Allows multiple files up to `maxFiles` limit
|
|
920
|
+
- Files can be displayed above or below the drop area (`filesPosition`)
|
|
921
|
+
- Each file has its own remove button
|
|
922
|
+
- A "Clear all" button appears when there are 2+ files
|
|
1000
923
|
|
|
1001
|
-
|
|
1002
|
-
border-primary
|
|
1003
|
-
```
|
|
924
|
+
### Visual styling details
|
|
1004
925
|
|
|
1005
|
-
|
|
926
|
+
**Drag & drop area:**
|
|
927
|
+
|
|
928
|
+
- Padding: `p-6` (24px)
|
|
929
|
+
- Border radius: `rounded-2xl` (16px)
|
|
930
|
+
- Border: `border-2 border-dashed`
|
|
931
|
+
- Normal state: `border-input bg-background`
|
|
932
|
+
- Dragging: `border-primary`
|
|
933
|
+
- Transitions: `transition-colors`
|
|
934
|
+
|
|
935
|
+
**Selected file card:**
|
|
1006
936
|
|
|
1007
937
|
- Padding: `p-6` (24px)
|
|
1008
938
|
- Border radius: `rounded-2xl` (16px)
|
|
1009
939
|
- Background: `bg-muted`
|
|
1010
940
|
- Border: `border-input`
|
|
1011
941
|
- Horizontal gap: `gap-4` (16px)
|
|
1012
|
-
- Icon
|
|
1013
|
-
|
|
1014
|
-
```tsx
|
|
1015
|
-
const [archive, setArchive] = useState<File | null>(null);
|
|
1016
|
-
|
|
1017
|
-
<FileUpload
|
|
1018
|
-
selectedFile={archive}
|
|
1019
|
-
onFileSelect={setArchive}
|
|
1020
|
-
acceptedExtensions={[".zip", ".rar", ".7z"]}
|
|
1021
|
-
maxSizeInMB={500}
|
|
1022
|
-
labels={{
|
|
1023
|
-
dragDrop: "Drag and drop your archive here or",
|
|
1024
|
-
selectFile: "Select the archive",
|
|
1025
|
-
}}
|
|
1026
|
-
/>;
|
|
1027
|
-
```
|
|
1028
|
-
|
|
1029
|
-
### With form integration
|
|
1030
|
-
|
|
1031
|
-
```tsx
|
|
1032
|
-
function UploadForm() {
|
|
1033
|
-
const [file, setFile] = useState<File | null>(null);
|
|
1034
|
-
|
|
1035
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
1036
|
-
e.preventDefault();
|
|
1037
|
-
|
|
1038
|
-
if (!file) return;
|
|
1039
|
-
|
|
1040
|
-
const formData = new FormData();
|
|
1041
|
-
formData.append("file", file);
|
|
942
|
+
- Icon: `text-primary` on `bg-primary/15` background
|
|
1042
943
|
|
|
1043
|
-
|
|
1044
|
-
const response = await fetch("/api/upload", {
|
|
1045
|
-
method: "POST",
|
|
1046
|
-
body: formData
|
|
1047
|
-
});
|
|
1048
|
-
|
|
1049
|
-
### Remove button
|
|
944
|
+
**Remove button:**
|
|
1050
945
|
|
|
1051
946
|
- Variant: `destructive-medium`
|
|
1052
947
|
- Icon: `delete` from Material Symbols
|
|
1053
948
|
- Icon color: `text-destructive`
|
|
1054
949
|
|
|
1055
|
-
|
|
950
|
+
**Clear all button (multiple mode):**
|
|
1056
951
|
|
|
1057
952
|
- Variant: `ghost`
|
|
1058
953
|
- Size: `sm`
|
|
1059
954
|
- Appears only when there are 2+ files
|
|
1060
|
-
console.error("Upload failed:", error);
|
|
1061
|
-
}
|
|
1062
|
-
};
|
|
1063
|
-
|
|
1064
|
-
return (
|
|
1065
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
1066
|
-
<FileUpload
|
|
1067
|
-
selectedFile={file}
|
|
1068
|
-
onFileSelect={setFile}
|
|
1069
|
-
/>
|
|
1070
|
-
<Button type="submit" disabled={!file}>
|
|
1071
|
-
Upload File
|
|
1072
|
-
</Button>
|
|
1073
|
-
</form>
|
|
1074
|
-
);
|
|
1075
|
-
}
|
|
1076
|
-
```
|
|
1077
|
-
|
|
1078
|
-
## Visual states
|
|
1079
|
-
|
|
1080
|
-
### Without selected file
|
|
1081
|
-
|
|
1082
|
-
The component shows:
|
|
1083
|
-
|
|
1084
|
-
- Drag & drop area with dashed border
|
|
1085
|
-
- Document icon on light blue background
|
|
1086
|
-
- Instructional text and link to select file
|
|
1087
|
-
- File requirements (extensions and size)
|
|
1088
|
-
|
|
1089
|
-
### During drag over
|
|
1090
|
-
|
|
1091
|
-
When the user drags a file over the area:
|
|
1092
|
-
|
|
1093
|
-
- Border changes to `border-primary-500`
|
|
1094
|
-
- Background changes to `bg-primary-50`
|
|
1095
|
-
- Smooth transition with `transition-colors`
|
|
1096
|
-
|
|
1097
|
-
### With selected file
|
|
1098
|
-
|
|
1099
|
-
The component shows:
|
|
1100
|
-
|
|
1101
|
-
- Card with `bg-neutral-100` background
|
|
1102
|
-
- Document icon on white background
|
|
1103
|
-
- File name (with truncate if long)
|
|
1104
|
-
- File size in MB
|
|
1105
|
-
- Destructive button to remove the file
|
|
1106
|
-
|
|
1107
|
-
## Validation
|
|
1108
|
-
|
|
1109
|
-
The component automatically validates:
|
|
1110
|
-
|
|
1111
|
-
1. **Extension**: Only accepts files with extensions in `acceptedExtensions`
|
|
1112
|
-
2. **Size**: Rejects files larger than `maxSizeInMB`
|
|
1113
|
-
|
|
1114
|
-
If a file does not meet these validations, it is not selected and `onFileSelect` is not invoked.
|
|
1115
|
-
|
|
1116
|
-
## Base styles
|
|
1117
|
-
|
|
1118
|
-
### Drag & drop area
|
|
1119
|
-
|
|
1120
|
-
- Padding: `p-6` (24px)
|
|
1121
|
-
- Border radius: `rounded-2xl` (16px)
|
|
1122
|
-
- Border: `border-2 border-dashed`
|
|
1123
|
-
- Gap between elements: `gap-6` (24px)
|
|
1124
|
-
- Transitions: `transition-colors`
|
|
1125
|
-
|
|
1126
|
-
### Normal state vs dragging
|
|
1127
|
-
|
|
1128
|
-
```tsx
|
|
1129
|
-
// Normal
|
|
1130
|
-
border-neutral-300 bg-neutral-50
|
|
1131
|
-
|
|
1132
|
-
// Dragging
|
|
1133
|
-
border-primary-500 bg-primary-50
|
|
1134
|
-
```
|
|
1135
|
-
|
|
1136
|
-
### Selected file card
|
|
1137
|
-
|
|
1138
|
-
- Padding: `p-6` (24px)
|
|
1139
|
-
- Border radius: `rounded-2xl` (16px)
|
|
1140
|
-
- Background: `bg-neutral-100`
|
|
1141
|
-
- Horizontal gap: `gap-4` (16px)
|
|
1142
|
-
- Icon on white background with `rounded-xl` and `p-2.5`
|
|
1143
|
-
|
|
1144
|
-
### Remove button
|
|
1145
|
-
|
|
1146
|
-
- Variant: `destructive-medium`
|
|
1147
|
-
- Icon: `delete` from Material Symbols
|
|
1148
|
-
- Icon color: `text-destructive-500`
|
|
1149
955
|
|
|
1150
956
|
## Accessibility
|
|
1151
957
|
|
|
1152
958
|
- Hidden file input with `className="adm:hidden"`
|
|
1153
|
-
- Label properly associated
|
|
959
|
+
- Label properly associated via auto-generated ID or custom `input.id`
|
|
1154
960
|
- Link button uses `asChild` for semantic behavior
|
|
1155
961
|
- Typography with `muted` color for secondary text
|
|
962
|
+
- Supports `aria-invalid` for form validation feedback
|
|
963
|
+
- Supports `aria-disabled` and `disabled` for accessibility
|
|
964
|
+
- Automatically detects and respects `<fieldset disabled>`
|
|
1156
965
|
|
|
1157
966
|
## TypeScript types
|
|
1158
967
|
|
|
1159
|
-
````typescript
|
|
1160
|
-
export type FileUploadLabels = {
|
|
1161
|
-
dragDrop?: string
|
|
1162
|
-
selectFile?: string
|
|
1163
|
-
fileRequirements?: string
|
|
1164
|
-
filesSelected?: (count: number) => string
|
|
1165
|
-
};
|
|
1166
|
-
|
|
1167
|
-
export type FileUploadProps = ComponentProps<"div"> & Readonly<{
|
|
1168
|
-
// Simple mode
|
|
1169
|
-
selectedFile?: File | null
|
|
1170
|
-
onFileSelect?: (file: File | null) => void
|
|
1171
|
-
|
|
1172
|
-
// Multiple mode
|
|
1173
|
-
selectedFiles?: File[]
|
|
1174
|
-
onFilesSelect?: (files: File[]) => void
|
|
1175
|
-
multiple?: boolean
|
|
1176
|
-
maxFiles?: number
|
|
1177
|
-
filesPosition?: "above" | "below"
|
|
1178
|
-
|
|
1179
|
-
// Validation
|
|
1180
|
-
onInvalidFile?: (file: File, reason: "extension" | "size") => void
|
|
1181
|
-
invalid?: boolean
|
|
1182
|
-
"aria-invalid"?: boolean
|
|
1183
|
-
|
|
1184
|
-
// States
|
|
1185
|
-
disabled?: boolean
|
|
1186
|
-
|
|
1187
|
-
// Common
|
|
1188
|
-
acceptedExtensions?: string[]
|
|
1189
|
-
maxSizeInMB?: number
|
|
1190
|
-
labels?: FileUploadLabels
|
|
1191
|
-
input?: ComponentProps<"input">
|
|
1192
|
-
}>
|
|
1193
|
-
### FormData for upload
|
|
1194
|
-
|
|
1195
|
-
```tsx
|
|
1196
|
-
// Un archivo
|
|
1197
|
-
const uploadFile = async (file: File) => {
|
|
1198
|
-
const formData = new FormData();
|
|
1199
|
-
formData.append("file", file);
|
|
1200
|
-
formData.append("metadata", JSON.stringify({
|
|
1201
|
-
uploadedAt: new Date().toISOString()
|
|
1202
|
-
}));
|
|
1203
|
-
|
|
1204
|
-
const response = await fetch("/api/upload", {
|
|
1205
|
-
method: "POST",
|
|
1206
|
-
body: formData
|
|
1207
|
-
});
|
|
1208
|
-
|
|
1209
|
-
return response.json();
|
|
1210
|
-
};
|
|
1211
|
-
|
|
1212
|
-
// Múltiples archivos
|
|
1213
|
-
const uploadFiles = async (files: File[]) => {
|
|
1214
|
-
const formData = new FormData();
|
|
1215
|
-
files.forEach((file, index) => {
|
|
1216
|
-
formData.append(`file${index}`, file);
|
|
1217
|
-
});
|
|
1218
|
-
|
|
1219
|
-
const response = await fetch("/api/upload-multiple", {
|
|
1220
|
-
method: "POST",
|
|
1221
|
-
body: formData
|
|
1222
|
-
});
|
|
1223
|
-
|
|
1224
|
-
return response.json();
|
|
1225
|
-
};
|
|
1226
|
-
````
|
|
1227
|
-
|
|
1228
|
-
```tsx
|
|
1229
|
-
// No validar el archivo antes de hacer upload
|
|
1230
|
-
// Siempre verificar que file no sea null
|
|
1231
|
-
|
|
1232
|
-
// Extensiones demasiado permisivas
|
|
1233
|
-
acceptedExtensions={[".*"]} // Inseguro
|
|
1234
|
-
|
|
1235
|
-
// Tamaño excesivo sin justificación
|
|
1236
|
-
maxSizeInMB={10000} // 10GB es excesivo
|
|
1237
|
-
|
|
1238
|
-
// Labels genéricas cuando el contexto es específico
|
|
1239
|
-
labels={{ selectFile: "Select file" }} // Poco descriptivo
|
|
1240
|
-
```
|
|
1241
|
-
|
|
1242
|
-
## TypeScript types (duplicate)
|
|
1243
|
-
|
|
1244
968
|
```typescript
|
|
1245
969
|
export type FileUploadLabels = {
|
|
1246
970
|
dragDrop?: string;
|
|
1247
971
|
selectFile?: string;
|
|
1248
972
|
fileRequirements?: string;
|
|
973
|
+
filesSelected?: (count: number) => string;
|
|
1249
974
|
};
|
|
1250
975
|
|
|
1251
976
|
export type FileUploadProps = ComponentProps<"div"> &
|
|
1252
977
|
Readonly<{
|
|
1253
|
-
|
|
1254
|
-
|
|
978
|
+
// Simple mode
|
|
979
|
+
selectedFile?: File | null;
|
|
980
|
+
onFileSelect?: (file: File | null) => void;
|
|
981
|
+
|
|
982
|
+
// Multiple mode
|
|
983
|
+
selectedFiles?: File[];
|
|
984
|
+
onFilesSelect?: (files: File[]) => void;
|
|
985
|
+
multiple?: boolean;
|
|
986
|
+
maxFiles?: number;
|
|
987
|
+
filesPosition?: "above" | "below";
|
|
988
|
+
|
|
989
|
+
// Callbacks
|
|
990
|
+
onFileAdd?: (file: File) => void;
|
|
991
|
+
onFileRemove?: (file: File) => void;
|
|
992
|
+
onFilesAdd?: (files: File[]) => void;
|
|
993
|
+
onFilesRemove?: (files: File[]) => void;
|
|
994
|
+
|
|
995
|
+
// Validation
|
|
996
|
+
onInvalidFile?: (file: File, reason: "extension" | "size") => void;
|
|
997
|
+
invalid?: boolean;
|
|
998
|
+
"aria-invalid"?: boolean;
|
|
999
|
+
|
|
1000
|
+
// States
|
|
1001
|
+
disabled?: boolean;
|
|
1002
|
+
|
|
1003
|
+
// Common
|
|
1255
1004
|
acceptedExtensions?: string[];
|
|
1256
1005
|
maxSizeInMB?: number;
|
|
1257
1006
|
labels?: FileUploadLabels;
|
|
1007
|
+
input?: ComponentProps<"input">;
|
|
1258
1008
|
}>;
|
|
1259
1009
|
```
|
|
1260
1010
|
|
|
@@ -1263,6 +1013,7 @@ export type FileUploadProps = ComponentProps<"div"> &
|
|
|
1263
1013
|
### FormData for upload
|
|
1264
1014
|
|
|
1265
1015
|
```tsx
|
|
1016
|
+
// Single file
|
|
1266
1017
|
const uploadFile = async (file: File) => {
|
|
1267
1018
|
const formData = new FormData();
|
|
1268
1019
|
formData.append("file", file);
|
|
@@ -1280,6 +1031,21 @@ const uploadFile = async (file: File) => {
|
|
|
1280
1031
|
|
|
1281
1032
|
return response.json();
|
|
1282
1033
|
};
|
|
1034
|
+
|
|
1035
|
+
// Multiple files
|
|
1036
|
+
const uploadFiles = async (files: File[]) => {
|
|
1037
|
+
const formData = new FormData();
|
|
1038
|
+
files.forEach((file, index) => {
|
|
1039
|
+
formData.append(`file${index}`, file);
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
const response = await fetch("/api/upload-multiple", {
|
|
1043
|
+
method: "POST",
|
|
1044
|
+
body: formData,
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
return response.json();
|
|
1048
|
+
};
|
|
1283
1049
|
```
|
|
1284
1050
|
|
|
1285
1051
|
### With progress tracking
|
|
@@ -1310,73 +1076,69 @@ const uploadWithProgress = async (file: File) => {
|
|
|
1310
1076
|
};
|
|
1311
1077
|
```
|
|
1312
1078
|
|
|
1313
|
-
##
|
|
1314
|
-
|
|
1315
|
-
| Característica | Input file nativo | FileUpload |
|
|
1316
|
-
| --------------------- | ------------------------ | ------------------------- |
|
|
1317
|
-
| Drag & Drop | ❌ No | ✅ Sí |
|
|
1318
|
-
| Modo múltiple | ⚠️ Básico | ✅ Completo con UI |
|
|
1319
|
-
| Validación visual | ❌ No | ✅ Sí |
|
|
1320
|
-
| Preview de archivos | ❌ No | ✅ Sí con nombre y tamaño |
|
|
1321
|
-
| Extensiones validadas | ⚠️ Solo accept attribute | ✅ Con feedback visual |
|
|
1322
|
-
| Tamaño validado | ❌ Solo en backend | ✅ Cliente y servidor |
|
|
1323
|
-
| Límite de archivos | ❌ No | ✅ Sí (maxFiles) |
|
|
1324
|
-
| Remover archivos | ❌ No | ✅ Individual y Clear all |
|
|
1325
|
-
| Posición de preview | ❌ No configurable | ✅ Arriba o abajo |
|
|
1326
|
-
| UX consistente | ❌ Varía por browser | ✅ Consistente |
|
|
1327
|
-
| Labels customizables | ❌ No | ✅ Sí (i18n friendly) |
|
|
1328
|
-
| aria-invalid | ⚠️ Manual | ✅ Integrado |
|
|
1329
|
-
| Fieldset disabled | ⚠️ Native but no styles | ✅ Automatically detected |
|
|
1330
|
-
| Disabled files | ❌ Not supported | ✅ Full visual feedback |
|
|
1079
|
+
## Best practices
|
|
1331
1080
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
- The component uses native browser `File` API
|
|
1335
|
-
- Validation is client-side only - always validate on the server as well
|
|
1336
|
-
- In simple mode, drag & drop accepts one file at a time
|
|
1337
|
-
- In multiple mode, multiple files can be dragged simultaneously
|
|
1338
|
-
- Files are not stored automatically - handle upload with callbacks
|
|
1339
|
-
- The component is fully controlled - the parent manages file state
|
|
1340
|
-
- The file input is reusable: after selecting, it resets to allow selecting the same file again
|
|
1341
|
-
- **Automatic ID generation**: If no `id` is provided in the `input` prop, one is automatically generated with format `file-upload-{random}` to ensure accessibility
|
|
1342
|
-
- **Automatic fieldset detection**: The component detects when it is inside a `<fieldset disabled>` using MutationObserver and disables itself automatically
|
|
1343
|
-
- **Selected files and disabled**: When the component is disabled, selected files are visually shown as disabled and cannot be removed
|
|
1344
|
-
- Use with Field components (`Field`, `FieldLabel`, `FieldError`, etc.) for a better form experience
|
|
1081
|
+
### ✅ Do
|
|
1345
1082
|
|
|
1346
1083
|
```tsx
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1084
|
+
// Validate on server-side as well
|
|
1085
|
+
// Always check that file is not null before upload
|
|
1086
|
+
|
|
1087
|
+
// Use specific extensions for security
|
|
1088
|
+
acceptedExtensions={[".pdf", ".docx", ".xlsx"]}
|
|
1089
|
+
|
|
1090
|
+
// Set reasonable size limits
|
|
1091
|
+
maxSizeInMB={10}
|
|
1092
|
+
|
|
1093
|
+
// Provide descriptive labels for context
|
|
1094
|
+
labels={{
|
|
1095
|
+
dragDrop: "Drag and drop your invoice here or",
|
|
1096
|
+
selectFile: "Select invoice"
|
|
1097
|
+
}}
|
|
1352
1098
|
```
|
|
1353
1099
|
|
|
1354
|
-
###
|
|
1100
|
+
### ❌ Don't
|
|
1355
1101
|
|
|
1356
1102
|
```tsx
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1103
|
+
// Don't skip server-side validation
|
|
1104
|
+
// Always verify file is not null
|
|
1105
|
+
|
|
1106
|
+
// Don't use overly permissive extensions (security risk)
|
|
1107
|
+
acceptedExtensions={[".*"]}
|
|
1108
|
+
|
|
1109
|
+
// Don't set excessive size limits without justification
|
|
1110
|
+
maxSizeInMB={10000} // 10GB is excessive
|
|
1111
|
+
|
|
1112
|
+
// Don't use generic labels when context is specific
|
|
1113
|
+
labels={{ selectFile: "Select file" }} // Not descriptive enough
|
|
1362
1114
|
```
|
|
1363
1115
|
|
|
1364
|
-
## Comparison with
|
|
1116
|
+
## Comparison with FileUploadV2
|
|
1117
|
+
|
|
1118
|
+
| Feature | FileUpload | FileUploadV2 |
|
|
1119
|
+
| ------------------- | ------------------------------------ | -------------------------------------------------- |
|
|
1120
|
+
| File type | `File` (plain browser File object) | `FileWithMetadata` (with required ID) |
|
|
1121
|
+
| Status tracking | ❌ No | ✅ Yes (idle, uploading, success, error, deleting) |
|
|
1122
|
+
| Unique IDs | ❌ No | ✅ Yes (auto-generated) |
|
|
1123
|
+
| Custom ID generator | ❌ No | ✅ Yes (generateId prop) |
|
|
1124
|
+
| Metadata support | ❌ No | ✅ Yes (metadata property) |
|
|
1125
|
+
| Server integration | ⚠️ Manual only | ✅ Built-in with IDs and status |
|
|
1126
|
+
| Use case | Simple forms without status tracking | Server integration with file tracking |
|
|
1127
|
+
| Complexity | ⚡ Simple | 🔧 Advanced |
|
|
1365
1128
|
|
|
1366
|
-
|
|
1367
|
-
| -------------------- | ------------------------ | ------------------------- |
|
|
1368
|
-
| Drag & Drop | ❌ No | ✅ Yes |
|
|
1369
|
-
| Visual validation | ❌ No | ✅ Yes |
|
|
1370
|
-
| File preview | ❌ No | ✅ Yes with name and size |
|
|
1371
|
-
| Validated extensions | ⚠️ Only accept attribute | ✅ With visual feedback |
|
|
1372
|
-
| Validated size | ❌ Only on backend | ✅ Client and server |
|
|
1373
|
-
| Consistent UX | ❌ Varies by browser | ✅ Consistent |
|
|
1374
|
-
| Customizable labels | ❌ No | ✅ Yes (i18n friendly) |
|
|
1129
|
+
> **Need status tracking and metadata?** Use [FileUploadV2](file-upload-v2.md) for server-side file tracking with built-in unique identifiers and status management.
|
|
1375
1130
|
|
|
1376
1131
|
## Notes
|
|
1377
1132
|
|
|
1378
|
-
- The component uses native browser `File` API
|
|
1379
|
-
- Validation is client-side only - always validate on the server as well
|
|
1380
|
-
-
|
|
1381
|
-
-
|
|
1133
|
+
- The component uses native browser `File` API (plain File objects without modifications)
|
|
1134
|
+
- Validation is client-side only - **always validate on the server as well**
|
|
1135
|
+
- In simple mode, drag & drop accepts one file at a time
|
|
1136
|
+
- In multiple mode, multiple files can be dragged simultaneously
|
|
1137
|
+
- Files are not stored automatically - handle upload with callbacks
|
|
1382
1138
|
- The component is fully controlled - the parent manages file state
|
|
1139
|
+
- The file input is reusable: after selecting, it resets to allow selecting the same file again
|
|
1140
|
+
- **No status tracking**: FileUpload works with plain File objects - use [FileUploadV2](file-upload-v2.md) if you need status tracking
|
|
1141
|
+
- **Automatic ID generation**: If no `id` is provided in the `input` prop, one is automatically generated with format `file-upload-{random}` to ensure accessibility
|
|
1142
|
+
- **Automatic fieldset detection**: The component detects when it is inside a `<fieldset disabled>` using MutationObserver and disables itself automatically
|
|
1143
|
+
- **Selected files and disabled**: When the component is disabled, selected files are visually shown as disabled and cannot be removed
|
|
1144
|
+
- Use with Field components (`Field`, `FieldLabel`, `FieldError`, etc.) for a better form experience
|
package/llm.txt
CHANGED
|
@@ -32,7 +32,7 @@ Before creating ANY component, verify it doesn't exist here. For implementation
|
|
|
32
32
|
- **Label** [`docs/components/ui/label.md`] - `@adamosuiteservices/ui/label`
|
|
33
33
|
- **Input Group** [`docs/components/ui/input-group.md`] - `@adamosuiteservices/ui/input-group`
|
|
34
34
|
- **File Upload** [`docs/components/ui/file-upload.md`] - `@adamosuiteservices/ui/file-upload` ⚠️ Simple file upload
|
|
35
|
-
- **File Upload V2** [`docs/components/ui/file-upload-v2.md`] - `@adamosuiteservices/ui/file-upload` ⚠️ With IDs for server integration
|
|
35
|
+
- **File Upload V2** [`docs/components/ui/file-upload-v2.md`] - `@adamosuiteservices/ui/file-upload-v2` ⚠️ With unique IDs and metadata for server integration
|
|
36
36
|
|
|
37
37
|
### Overlay Components (8)
|
|
38
38
|
- **Dialog** [`docs/components/ui/dialog.md`] - `@adamosuiteservices/ui/dialog` (modal)
|
|
@@ -90,9 +90,9 @@ Before creating ANY component, verify it doesn't exist here. For implementation
|
|
|
90
90
|
1. ❌ Creating components that already exist (check list above first!)
|
|
91
91
|
2. ❌ Using barrel imports: `import { X } from "@adamosuiteservices/ui"`
|
|
92
92
|
3. ❌ Using `adm:` prefix in user code
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
4. ❌ Using FileUploadV2 when you don't need unique IDs or metadata (use FileUpload instead)
|
|
94
|
+
5. ❌ Using FileUpload without status when you need server-side tracking (use FileUploadV2 with IDs)
|
|
95
|
+
6. ❌ Not consulting component documentation before implementation
|
|
96
96
|
|
|
97
97
|
## 📚 Documentation Structure
|
|
98
98
|
|