feat(mui): add FileUpload kit primitive (hidden input + selected-file chips)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-07 00:02:22 +02:00
parent ff527ca577
commit acc5434aca
2 changed files with 118 additions and 0 deletions

117
src/admin/ui/FileUpload.tsx Normal file
View File

@@ -0,0 +1,117 @@
import { useRef, type ChangeEvent } from "react";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
import Button from "./Button";
function formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} kB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
export interface FileUploadProps {
/** Selected files. Single-select mode keeps 01 items. */
files: File[];
onFilesChange: (files: File[]) => void;
/** e.g. "application/pdf" */
accept?: string;
/** Allow multiple files (appends on pick). Default false = single (replaces). */
multiple?: boolean;
/** Picker button label. */
buttonLabel?: string;
disabled?: boolean;
}
/**
* File picker: a button wrapping a hidden `<input type=file>`, with selected-file
* chips (name + size + remove). Single mode (default) replaces the selection;
* `multiple` appends. Value is always `File[]` so callers handle both uniformly.
* Validation (size/count/type beyond `accept`) stays with the caller.
*/
export default function FileUpload({
files,
onFilesChange,
accept,
multiple = false,
buttonLabel = "Vybrat soubor",
disabled,
}: FileUploadProps) {
const inputRef = useRef<HTMLInputElement>(null);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const picked = Array.from(e.target.files ?? []);
if (picked.length === 0) return;
onFilesChange(multiple ? [...files, ...picked] : picked.slice(0, 1));
// Reset so picking the same file again (after removal) still fires onChange.
if (inputRef.current) inputRef.current.value = "";
};
const remove = (idx: number) =>
onFilesChange(files.filter((_, i) => i !== idx));
return (
<Box>
<Button
component="label"
variant="outlined"
color="inherit"
size="small"
disabled={disabled}
>
{buttonLabel}
<input
ref={inputRef}
type="file"
hidden
accept={accept}
multiple={multiple}
onChange={handleChange}
/>
</Button>
{files.length > 0 && (
<Box sx={{ mt: 1, display: "flex", flexWrap: "wrap", gap: 1 }}>
{files.map((f, i) => (
<Box
key={`${f.name}-${f.size}-${i}`}
sx={{
display: "flex",
alignItems: "center",
gap: 0.5,
pl: 1,
pr: 0.25,
py: 0.25,
borderRadius: 1,
bgcolor: "action.hover",
}}
>
<Typography
variant="caption"
sx={{
maxWidth: 240,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{f.name}
</Typography>
<Typography variant="caption" color="text.secondary">
({formatSize(f.size)})
</Typography>
<IconButton
size="small"
onClick={() => remove(i)}
aria-label="Odebrat soubor"
disabled={disabled}
sx={{ p: 0.25, fontSize: "1rem", lineHeight: 1 }}
>
&times;
</IconButton>
</Box>
))}
</Box>
)}
</Box>
);
}

View File

@@ -23,3 +23,4 @@ export { default as LoadingState } from "./LoadingState";
export { default as FilterBar } from "./FilterBar";
export { default as StatCard } from "./StatCard";
export type { StatCardColor } from "./StatCard";
export { default as FileUpload } from "./FileUpload";