import * as React from "react"; import { cn } from "../../utils/cn"; import { useUiContent } from "../../content"; import { useUiId } from "../../hooks"; export interface FileUploadProps { label?: string; accept?: string; multiple?: boolean; disabled?: boolean; files: File[]; onFilesChange: (files: File[]) => void; /** Max size per file in bytes; oversized files are rejected and omitted. */ maxSizeBytes?: number; } function formatBytes(bytes: number) { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } /** * FileUpload — drag-and-drop zone plus a browse button (always present, so * drag-and-drop is never the *only* way to select files). Files are fully * controlled: the component reports changes via `onFilesChange` rather than * owning selection state itself, so consumers can validate/upload freely. */ export function FileUpload({ label, accept, multiple, disabled, files, onFilesChange, maxSizeBytes }: FileUploadProps) { const content = useUiContent(); const inputId = useUiId("file-upload"); const [isDragging, setIsDragging] = React.useState(false); const inputRef = React.useRef(null); function addFiles(list: FileList | null) { if (!list) return; const incoming = Array.from(list).filter((f) => !maxSizeBytes || f.size <= maxSizeBytes); onFilesChange(multiple ? [...files, ...incoming] : incoming.slice(0, 1)); } function removeFile(index: number) { onFilesChange(files.filter((_, i) => i !== index)); } return (
{label && {label}}
inputRef.current?.click()} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); inputRef.current?.click(); } }} onDragOver={(e) => { e.preventDefault(); if (!disabled) setIsDragging(true); }} onDragLeave={() => setIsDragging(false)} onDrop={(e) => { e.preventDefault(); setIsDragging(false); if (!disabled) addFiles(e.dataTransfer.files); }} className={cn( "ui-flex ui-cursor-pointer ui-flex-col ui-items-center ui-gap-1 ui-rounded-[var(--ui-radius-lg)] ui-border-2 ui-border-dashed ui-border-[var(--ui-border-strong)] ui-p-8 ui-text-center ui-transition-colors", "focus-visible:ui-outline-none focus-visible:ui-shadow-[var(--ui-focus-ring)]", isDragging && "ui-border-[var(--ui-primary)] ui-bg-[var(--ui-bg-subtle)]", disabled && "ui-cursor-not-allowed ui-opacity-50" )} >

{content.fileUpload.dragPrompt}{" "} {content.fileUpload.browse}

addFiles(e.target.files)} className="ui-visually-hidden" />
{files.length > 0 && ( )}
); }