@codeworker.br/govbr-tw-react
Version:
Biblioteca de componentes React usando Tailwind CSS que implementa o Padrão Digital de Governo.
192 lines (191 loc) • 13.9 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useEffect, useMemo, useRef, useState } from "react";
import { cva } from "class-variance-authority";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faFolderOpen, faUpload, faImage, faFile, faXmark, faTrashCan, faChevronLeft, faChevronRight, } from "@fortawesome/free-solid-svg-icons";
// Ajuste o caminho conforme seu projeto
import { Button } from "../Button";
const wrapperVariants = cva("w-full", {
variants: {
theme: {
light: "text-govbr-gray-80",
dark: "text-govbr-pure-0",
},
density: {
comfy: "space-y-3",
compact: "space-y-2",
},
},
defaultVariants: {
theme: "light",
density: "comfy",
},
});
const chipVariants = cva("inline-flex items-center gap-2 rounded-md border px-2.5 py-1.5 text-xs", {
variants: {
theme: {
light: "border-govbr-gray-20 bg-white",
dark: "border-govbr-blue-warm-20/50 bg-govbr-blue-warm-10/30",
},
},
defaultVariants: { theme: "light" },
});
function cx(...classes) {
return classes.filter(Boolean).join(" ");
}
function formatBytes(bytes) {
if (!Number.isFinite(bytes))
return "-";
const units = ["B", "KB", "MB", "GB", "TB"];
let i = 0;
let n = bytes;
while (n >= 1024 && i < units.length - 1) {
n /= 1024;
i++;
}
return `${n.toFixed(n < 10 && i > 0 ? 1 : 0)} ${units[i]}`;
}
function getExt(name) {
const idx = name.lastIndexOf(".");
return idx >= 0 ? name.slice(idx + 1).toLowerCase() : "";
}
function isImage(f) {
return f.type.startsWith("image/");
}
function fileKey(f) {
return `${f.name}-${f.size}-${f.lastModified}`;
}
export default function InputFile({ accept, multiple, preview = true, theme, density, className, onUpload, onChange, labels, onlyIcon = false, disabled, value, setValue, buttonDensity = "default", pickVariant = "outline", uploadVariant = "default", itemsPerPage = 8, }) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
const inputRef = useRef(null);
// Estado interno (se não for controlado)
const [internalFiles, setInternalFiles] = useState([]);
const files = (_a = value) !== null && _a !== void 0 ? _a : internalFiles;
// i18n defaults
const i18n = {
pick: (_b = labels === null || labels === void 0 ? void 0 : labels.pick) !== null && _b !== void 0 ? _b : "Selecionar arquivo",
upload: (_c = labels === null || labels === void 0 ? void 0 : labels.upload) !== null && _c !== void 0 ? _c : "Upload",
acceptedHint: (_d = labels === null || labels === void 0 ? void 0 : labels.acceptedHint) !== null && _d !== void 0 ? _d : "Aceita:",
removeAria: (_e = labels === null || labels === void 0 ? void 0 : labels.removeAria) !== null && _e !== void 0 ? _e : "Remover",
prevPageAria: (_f = labels === null || labels === void 0 ? void 0 : labels.prevPageAria) !== null && _f !== void 0 ? _f : "Página anterior",
nextPageAria: (_g = labels === null || labels === void 0 ? void 0 : labels.nextPageAria) !== null && _g !== void 0 ? _g : "Próxima página",
pageXofY: (_h = labels === null || labels === void 0 ? void 0 : labels.pageXofY) !== null && _h !== void 0 ? _h : ((x, y) => `Página ${x} de ${y}`),
clearAllAria: (_j = labels === null || labels === void 0 ? void 0 : labels.clearAllAria) !== null && _j !== void 0 ? _j : "Remover todos",
};
// Previews de imagem via ObjectURL (sem mutar File)
const [previews, setPreviews] = useState(new Map());
useEffect(() => {
// cria URLs que faltam
const next = new Map(previews);
let changed = false;
for (const f of files) {
if (isImage(f)) {
const key = fileKey(f);
if (!next.has(key)) {
next.set(key, URL.createObjectURL(f));
changed = true;
}
}
}
// remove URLs de arquivos que saíram
for (const [key] of next) {
const stillExists = files.some((f) => fileKey(f) === key && isImage(f));
if (!stillExists) {
const url = next.get(key);
URL.revokeObjectURL(url);
next.delete(key);
changed = true;
}
}
if (changed)
setPreviews(next);
// cleanup ao desmontar
return () => {
next.forEach((u) => URL.revokeObjectURL(u));
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [files]);
function updateFiles(next) {
if (setValue)
setValue(next);
else
setInternalFiles(next);
onChange === null || onChange === void 0 ? void 0 : onChange(next);
}
function openPicker() {
var _a;
if (!disabled)
(_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.click();
}
function handleInputChange(e) {
const list = e.target.files;
if (!list)
return;
const picked = Array.from(list);
const next = multiple ? [...files, ...picked] : picked.slice(0, 1);
updateFiles(next);
// permite re-selecionar o mesmo arquivo depois
e.currentTarget.value = "";
}
function removeAt(idx) {
const key = fileKey(files[idx]);
const url = previews.get(key);
if (url) {
URL.revokeObjectURL(url);
const cp = new Map(previews);
cp.delete(key);
setPreviews(cp);
}
const next = files.filter((_, i) => i !== idx);
updateFiles(next);
}
// Limpar todos os arquivos
function clearAll() {
previews.forEach((url) => URL.revokeObjectURL(url));
setPreviews(new Map());
updateFiles([]);
}
const hasFiles = files.length > 0;
const canUpload = hasFiles && !disabled;
function handleUpload() {
return __awaiter(this, void 0, void 0, function* () {
if (!onUpload || !canUpload)
return;
yield onUpload(files);
});
}
// Paginação
const totalPages = Math.max(1, Math.ceil(files.length / Math.max(1, itemsPerPage)));
const [page, setPage] = useState(1);
useEffect(() => {
if (page > totalPages)
setPage(totalPages);
}, [files.length, itemsPerPage, totalPages, page]);
const start = (page - 1) * itemsPerPage;
const end = start + itemsPerPage;
const pageFiles = useMemo(() => files.slice(start, end), [files, start, end]);
const simpleList = useMemo(() => pageFiles.map((f, i) => {
const idx = start + i;
return (_jsxs("div", { className: cx(chipVariants({ theme }), "justify-between w-full md:w-auto"), children: [_jsxs("span", { className: "inline-flex items-center gap-2", children: [_jsx(FontAwesomeIcon, { icon: isImage(f) ? faImage : faFile }), _jsx("span", { className: "font-medium", children: f.name })] }), _jsxs("span", { className: "opacity-70 ml-3", children: [getExt(f.name).toUpperCase(), " \u2022 ", formatBytes(f.size)] }), _jsx(Button, { size: "icon", variant: "ghost-danger", density: buttonDensity, onClick: () => removeAt(idx), "aria-label": `${i18n.removeAria} ${f.name}`, title: i18n.removeAria, children: _jsx(FontAwesomeIcon, { icon: faXmark }) })] }, `${fileKey(f)}-simple`));
}), [pageFiles, theme, buttonDensity, start, i18n.removeAria]);
const showPager = files.length > itemsPerPage;
return (_jsxs("div", { className: cx(wrapperVariants({ theme, density }), className), children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs(Button, Object.assign({ type: "button", onClick: openPicker, variant: pickVariant, density: buttonDensity, disabled: disabled }, (onlyIcon ? { size: "icon" } : {}), { children: [_jsx(FontAwesomeIcon, { icon: faFolderOpen }), !onlyIcon && _jsx("span", { children: i18n.pick })] })), _jsx("input", { ref: inputRef, type: "file", className: "hidden", accept: accept, multiple: multiple, onChange: handleInputChange, "aria-label": i18n.pick }), _jsxs(Button, Object.assign({ type: "button", onClick: handleUpload, variant: uploadVariant, density: buttonDensity, disabled: !canUpload }, (onlyIcon ? { size: "icon" } : {}), { children: [_jsx(FontAwesomeIcon, { icon: faUpload }), !onlyIcon && _jsx("span", { children: i18n.upload })] })), _jsx(Button, { type: "button", onClick: clearAll, variant: "default-danger" // ajuste para a variante vermelha do seu <Button/>, p.ex. "destructive"
, density: buttonDensity, size: "icon", disabled: !hasFiles || disabled, "aria-label": i18n.clearAllAria, title: i18n.clearAllAria, children: _jsx(FontAwesomeIcon, { icon: faTrashCan }) }), showPager && (_jsxs("div", { className: "ml-auto flex items-center gap-1", children: [_jsx(Button, { size: "icon", variant: "ghost", density: buttonDensity, disabled: page <= 1, onClick: () => setPage((p) => Math.max(1, p - 1)), "aria-label": i18n.prevPageAria, title: i18n.prevPageAria, children: _jsx(FontAwesomeIcon, { icon: faChevronLeft }) }), _jsx("span", { className: "text-xs opacity-70 px-1 select-none", children: i18n.pageXofY(page, totalPages) }), _jsx(Button, { size: "icon", variant: "ghost", density: buttonDensity, disabled: page >= totalPages, onClick: () => setPage((p) => Math.min(totalPages, p + 1)), "aria-label": i18n.nextPageAria, title: i18n.nextPageAria, children: _jsx(FontAwesomeIcon, { icon: faChevronRight }) })] }))] }), hasFiles && preview ? (_jsxs(_Fragment, { children: [_jsx("ul", { className: "mt-3 grid grid-cols-1 gap-3 md:grid-cols-2", children: pageFiles.map((f, i) => {
const idx = start + i;
const key = fileKey(f);
const previewUrl = previews.get(key);
const isImg = isImage(f);
return (_jsxs("li", { className: cx("flex items-center gap-3 rounded-lg border p-3", theme === "dark"
? "border-govbr-blue-warm-20/50 bg-govbr-blue-warm-10/30"
: "border-govbr-gray-20 bg-white"), children: [_jsx("div", { className: "w-16 h-16 shrink-0 rounded-md overflow-hidden bg-govbr-gray-10 flex items-center justify-center", children: isImg && previewUrl ? (_jsx("img", { src: previewUrl, alt: f.name, className: "w-full h-full object-cover", draggable: false })) : (_jsx(FontAwesomeIcon, { icon: isImg ? faImage : faFile, className: "text-xl opacity-70" })) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "truncate font-medium", children: f.name }), _jsxs("div", { className: "text-xs opacity-70", children: [getExt(f.name).toUpperCase(), " \u2022 ", formatBytes(f.size)] })] }), _jsx("div", { className: "flex items-center gap-2", children: _jsx(Button, { type: "button", size: "icon", variant: "ghost-danger", density: buttonDensity, onClick: () => removeAt(idx), "aria-label": `${i18n.removeAria} ${f.name}`, title: i18n.removeAria, children: _jsx(FontAwesomeIcon, { icon: faTrashCan }) }) })] }, `${key}-card`));
}) }), showPager && (_jsxs("div", { className: "mt-3 flex items-center justify-end gap-1", children: [_jsx(Button, { size: "icon", variant: "ghost", density: buttonDensity, disabled: page <= 1, onClick: () => setPage((p) => Math.max(1, p - 1)), "aria-label": i18n.prevPageAria, title: i18n.prevPageAria, children: _jsx(FontAwesomeIcon, { icon: faChevronLeft }) }), _jsx("span", { className: "text-xs opacity-70 px-1 select-none", children: i18n.pageXofY(page, totalPages) }), _jsx(Button, { size: "icon", variant: "ghost", density: buttonDensity, disabled: page >= totalPages, onClick: () => setPage((p) => Math.min(totalPages, p + 1)), "aria-label": i18n.nextPageAria, title: i18n.nextPageAria, children: _jsx(FontAwesomeIcon, { icon: faChevronRight }) })] }))] })) : hasFiles ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "mt-3 flex flex-col gap-2", children: simpleList }), showPager && (_jsxs("div", { className: "mt-3 flex items-center justify-end gap-1", children: [_jsx(Button, { size: "icon", variant: "ghost", density: buttonDensity, disabled: page <= 1, onClick: () => setPage((p) => Math.max(1, p - 1)), "aria-label": i18n.prevPageAria, title: i18n.prevPageAria, children: _jsx(FontAwesomeIcon, { icon: faChevronLeft }) }), _jsx("span", { className: "text-xs opacity-70 px-1 select-none", children: i18n.pageXofY(page, totalPages) }), _jsx(Button, { size: "icon", variant: "ghost", density: buttonDensity, disabled: page >= totalPages, onClick: () => setPage((p) => Math.min(totalPages, p + 1)), "aria-label": i18n.nextPageAria, title: i18n.nextPageAria, children: _jsx(FontAwesomeIcon, { icon: faChevronRight }) })] }))] })) : null, accept && (_jsxs("p", { className: "mt-2 text-xs opacity-70", children: [i18n.acceptedHint, " ", _jsx("code", { className: "font-mono", children: accept }), multiple ? " • múltiplos arquivos" : ""] }))] }));
}