UNPKG

@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
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" : ""] }))] })); }