From 66ffd51d5b355a82c62d58c60ea735dbfe97742f Mon Sep 17 00:00:00 2001 From: Nathnael Date: Sat, 4 Jul 2026 09:27:38 +0000 Subject: [PATCH] style: update the multi file document ui --- .../src/components/SmartFileInput/index.tsx | 600 ++++++++++++------ 1 file changed, 397 insertions(+), 203 deletions(-) diff --git a/packages/ui-common/src/components/SmartFileInput/index.tsx b/packages/ui-common/src/components/SmartFileInput/index.tsx index 0da3e2f37..1e1eaf223 100644 --- a/packages/ui-common/src/components/SmartFileInput/index.tsx +++ b/packages/ui-common/src/components/SmartFileInput/index.tsx @@ -153,6 +153,76 @@ function ExistingFileLink({ ); } +/** + * A file the user just picked (in memory, not yet persisted). Rendered with a + * subtle "just added" entrance + an emerald accent so a fresh upload reads as + * distinct from the neutral surrounding surface. + */ +function NewFileCard({ + file: fileObj, + onRemove, + disabled, + hasError, + inputName, +}: { + file: File; + onRemove: () => void; + disabled?: boolean; + hasError?: boolean; + inputName: string; +}) { + return ( +
+
+
+ +
+ +
+

+ {fileObj.name} +

+
+ + {formatBytes(fileObj.size)} + + + Ready to upload + +
+
+
+ + + + {/* Hidden input to represent file details in traditional form submissions */} + +
+ ); +} + export function SmartFileInput({ file, value, @@ -407,228 +477,352 @@ export function SmartFileInput({

)} - {/* Selected Files List */} - {currentFiles.length > 0 && ( -
- {currentFiles.map((fileObj, idx) => ( -
-
-
- -
- -
-

- {fileObj.name} -

-
- - {formatBytes(fileObj.size)} - - - Ready - -
-
-
- - - - {/* Hidden inputs to represent file details in traditional form submissions */} - -
- ))} -
- )} - - {/* Dropzone area */} - {!reachedLimit && - (variant === "minimal" ? ( -
- + {/* + Multiple-file fields (default variant) render as ONE integrated + drag-and-drop surface. Uploaded files live INSIDE the dropzone as + lightweight rows — part of the surface, not separate cards — with + the "add more" prompt on the same surface below them. A full-cover + transparent input makes clicking anywhere (outside a file row) + open the picker; the prompt is pointer-transparent so clicks fall + through to it, while file rows and their controls sit above it. + */} + {variant === "default" && field.isMultiple ? ( +
handleDrag(e, field.fileKey, true)} + onDragLeave={(e) => handleDrag(e, field.fileKey, false)} + onDrop={(e) => handleDrop(e, field)} + className={cn( + "relative flex flex-col gap-2.5 rounded-xl border-2 border-dashed p-4 transition-all", + isDragOver + ? "border-primary bg-primary/5 dark:bg-primary/10" + : fieldError + ? "border-destructive/70" + : "border-border bg-card/40 hover:border-primary/40", + disabled && "pointer-events-none opacity-50", + )} + > + {/* Click anywhere on the surface (except a file row) to browse */} + {!reachedLimit && ( { - if (fileInputRefs.current) { - fileInputRefs.current[field.fileKey] = el; - } - }} - multiple={field.isMultiple} + multiple accept={acceptString} disabled={disabled} onChange={(e) => handleFileSelect(e, field)} - className="hidden" - /> - - Accepts:{" "} - {field.allowedExtensions.join(", ").toUpperCase() || - "All"} - - {existingForField.length > 0 && ( -
- {existingForField.map((f, idx) => ( - - ))} -
- )} -
- ) : isUploaded ? ( - // Uploaded state: a solid success panel that still doubles as a - // replace target (click anywhere or drag a new file onto it). -
handleDrag(e, field.fileKey, true)} - onDragLeave={(e) => handleDrag(e, field.fileKey, false)} - onDrop={(e) => handleDrop(e, field)} - className={cn( - "group relative flex items-center gap-4 rounded-lg border p-4 transition-all", - isDragOver - ? "border-2 border-dashed border-primary bg-primary/5 dark:bg-primary/10" - : "border-emerald-300/70 bg-emerald-50/60 dark:border-emerald-500/30 dark:bg-emerald-500/10", - disabled && - "opacity-50 pointer-events-none cursor-not-allowed", - )} - > - handleFileSelect(e, field)} - id={`file-input-${field.fileKey}`} - className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed" - aria-label={`Replace ${field.fileLabel}`} + className="absolute inset-0 z-0 h-full w-full cursor-pointer opacity-0 disabled:cursor-not-allowed" + aria-label={`Add files to ${field.fileLabel}`} /> + )} -
- {isDragOver ? ( - - ) : ( - - )} -
- -
-

- {isDragOver ? "Drop to replace" : "Document uploaded"} -

- {existingForField.length > 0 ? ( -
- {existingForField.map((f, idx) => ( + {(existingForField.length > 0 || currentFiles.length > 0) && ( +
+ {/* Already-saved (server) files — view/download only */} + {existingForField.map((f, idx) => ( +
+ +
- ))} +
+ + Saved +
- ) : ( -

- {isDragOver - ? "Release to replace the document on file." - : "Saved to your application. Drag a new file here or click to replace it."} -

+ ))} + + {/* Just-added (in-memory) files */} + {currentFiles.map((fileObj, idx) => ( +
+ +
+

+ {fileObj.name} +

+

+ {formatBytes(fileObj.size)} +

+
+ + Ready + + + +
+ ))} +
+ )} + + {reachedLimit ? ( +
+ + Maximum of {maxFiles} files reached +
+ ) : ( +
0 || currentFiles.length > 0 + ? "py-1" + : "py-6", )} -
- - - - Replace - -
- ) : ( -
handleDrag(e, field.fileKey, true)} - onDragLeave={(e) => handleDrag(e, field.fileKey, false)} - onDrop={(e) => handleDrop(e, field)} - className={cn( - "relative border-2 border-dashed rounded-lg p-6 flex flex-col items-center justify-center text-center transition-all bg-card/50", - isDragOver - ? "border-primary bg-primary/5 dark:bg-primary/10" - : "border-border hover:border-primary/50 hover:bg-muted/10", - fieldError && - "border-destructive hover:border-destructive/80", - disabled && - "opacity-50 pointer-events-none cursor-not-allowed", - )} - > - handleFileSelect(e, field)} - id={`file-input-${field.fileKey}`} - className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed" - /> - -
- +
0 || currentFiles.length > 0 + ? "p-1.5" + : "p-3", )} - /> + > + 0 || + currentFiles.length > 0 + ? "h-4 w-4" + : "h-6 w-6", + isDragOver && "animate-bounce text-primary", + )} + /> +
+

+ {isDragOver + ? "Drop your files here" + : existingForField.length > 0 || + currentFiles.length > 0 + ? "Add more files, or " + : "Drag & drop your files here, or "} + {!isDragOver && ( + browse + )} +

+

+ {field.allowedExtensions.join(", ").toUpperCase() || + "All formats"} + {" • "} + {currentFiles.length}/{maxFiles} added +

+ )} +
+ ) : ( + <> + {/* Selected Files List */} + {currentFiles.length > 0 && ( +
+ {currentFiles.map((fileObj, idx) => ( + removeFile(field.fileKey, idx)} + /> + ))} +
+ )} -

- Drag & drop your file here, or{" "} - - browse - -

+ {/* Dropzone area */} + {!reachedLimit && + (variant === "minimal" ? ( +
+ + { + if (fileInputRefs.current) { + fileInputRefs.current[field.fileKey] = el; + } + }} + multiple={field.isMultiple} + accept={acceptString} + disabled={disabled} + onChange={(e) => handleFileSelect(e, field)} + className="hidden" + /> + + Accepts:{" "} + {field.allowedExtensions.join(", ").toUpperCase() || + "All"} + + {existingForField.length > 0 && ( +
+ {existingForField.map((f, idx) => ( + + ))} +
+ )} +
+ ) : isUploaded ? ( + // Uploaded state: a solid success panel that still doubles as a + // replace target (click anywhere or drag a new file onto it). +
handleDrag(e, field.fileKey, true)} + onDragLeave={(e) => handleDrag(e, field.fileKey, false)} + onDrop={(e) => handleDrop(e, field)} + className={cn( + "group relative flex items-center gap-4 rounded-lg border p-4 transition-all", + isDragOver + ? "border-2 border-dashed border-primary bg-primary/5 dark:bg-primary/10" + : "border-emerald-300/70 bg-emerald-50/60 dark:border-emerald-500/30 dark:bg-emerald-500/10", + disabled && + "opacity-50 pointer-events-none cursor-not-allowed", + )} + > + handleFileSelect(e, field)} + id={`file-input-${field.fileKey}`} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed" + aria-label={`Replace ${field.fileLabel}`} + /> -

- Supported formats:{" "} - {field.allowedExtensions.join(", ").toUpperCase() || - "All"} -

-
- ))} +
+ {isDragOver ? ( + + ) : ( + + )} +
+ +
+

+ {isDragOver + ? "Drop to replace" + : "Document uploaded"} +

+ {existingForField.length > 0 ? ( +
+ {existingForField.map((f, idx) => ( + + ))} +
+ ) : ( +

+ {isDragOver + ? "Release to replace the document on file." + : "Saved to your application. Drag a new file here or click to replace it."} +

+ )} +
+ + + + Replace + +
+ ) : ( +
handleDrag(e, field.fileKey, true)} + onDragLeave={(e) => handleDrag(e, field.fileKey, false)} + onDrop={(e) => handleDrop(e, field)} + className={cn( + "relative border-2 border-dashed rounded-lg p-6 flex flex-col items-center justify-center text-center transition-all bg-card/50", + isDragOver + ? "border-primary bg-primary/5 dark:bg-primary/10" + : "border-border hover:border-primary/50 hover:bg-muted/10", + fieldError && + "border-destructive hover:border-destructive/80", + disabled && + "opacity-50 pointer-events-none cursor-not-allowed", + )} + > + handleFileSelect(e, field)} + id={`file-input-${field.fileKey}`} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed" + /> + +
+ +
+ +

+ Drag & drop your file here, or{" "} + + browse + +

+ +

+ Supported formats:{" "} + {field.allowedExtensions.join(", ").toUpperCase() || + "All"} +

+
+ ))} + + )} {/* Validation Error Message */} {fieldError && (