@codeworker.br/govbr-tw-react
Version:
Biblioteca de componentes React usando Tailwind CSS que implementa o Padrão Digital de Governo.
129 lines (128 loc) • 10.9 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useCallback, useId, useMemo, useRef, useState } from "react";
import { cva } from "class-variance-authority";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
// Toolbar usando seus componentes
import { ButtonGroup } from "../ButtonGroup";
import { Button } from "../Button";
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, } from "../DropdownMenu";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBold, faItalic, faHeading, faLink, faListUl, faListOl, faCode, faQuoteRight, faTable, faEye, faPen, } from "@fortawesome/free-solid-svg-icons";
/**
* TextEditor – Markdown textarea com toolbar e preview
* Agora com i18n: labels configuráveis via prop `labels`.
*/
const editorVariants = cva("rounded-md w-full placeholder:italic placeholder:text-base border box-border disabled:opacity-60 disabled:cursor-not-allowed read-only:cursor-default read-only:outline-none font-sans", {
variants: {
variant: {
default: "bg-govbr-pure-0 border-govbr-gray-20 outline-govbr-blue-warm-vivid-70 text-govbr-pure-100 hover:bg-govbr-gray-10 disabled:hover:bg-govbr-pure-0 read-only:hover:bg-govbr-pure-0",
danger: "border-govbr-red-vivid-50 outline-govbr-red-vivid-50 bg-govbr-red-vivid-10 placeholder:text-govbr-red-vivid-50 text-govbr-red-vivid-50",
success: "border-govbr-green-cool-vivid-50 outline-govbr-green-cool-vivid-50 bg-govbr-green-cool-vivid-5 placeholder:text-govbr-green-cool-vivid-50 text-govbr-green-cool-vivid-50",
warning: "border-govbr-yellow-vivid-20 outline-govbr-yellow-vivid-20 bg-govbr-yellow-vivid-5 placeholder:text-govbr-pure-100 text-govbr-pure-100",
dark: "border-govbr-blue-warm-20 outline-govbr-blue-warm-20 bg-transparent hover:bg-govbr-blue-warm-20/20 placeholder:text-govbr-blue-warm-20 text-govbr-pure-0 disabled:bg-govbr-blue-warm-20/20 read-only:hover:bg-transparent",
featured: "border-govbr-gray-2 outline-govbr-blue-warm-vivid-70 text-govbr-pure-100 bg-govbr-gray-2 hover:bg-govbr-gray-10 disabled:hover:bg-govbr-gray-2 read-only:hover:bg-govbr-gray-2 text-govbr-gray-80",
},
density: {
lowest: "min-h-56 p-4 text-[1.05rem] leading-6",
low: "min-h-48 p-4 text-base leading-6",
default: "min-h-40 p-3 text-base leading-6",
high: "min-h-32 p-2 text-sm leading-5",
},
},
defaultVariants: {
variant: "default",
density: "default",
},
});
export const DEFAULT_LABELS_PT_BR = {
bold: "Negrito",
italic: "Itálico",
headings: "Cabeçalhos (H1–H6)",
headingsMenuTitle: "Cabeçalhos",
link: "Link",
unorderedList: "Lista não ordenada",
orderedList: "Lista numerada",
code: "Código inline",
quote: "Citação",
table: "Inserir tabela (linhas/colunas)",
preview: "Visualizar",
edit: "Voltar para edição",
columns: "Colunas",
rows: "Linhas",
insert: "Inserir",
cancel: "Cancelar",
selectedPlaceholder: "texto",
tableHeaderBase: "Cabeçalho",
nothingToPreview: "_Nada para pré-visualizar._",
};
function cx(...cls) {
return cls.filter(Boolean).join(" ");
}
export default function TextEditor({ value, onChange, placeholder, label, hint, previewInitially = false, className, disabled, readOnly, variant, density, labels, }) {
const L = Object.assign(Object.assign({}, DEFAULT_LABELS_PT_BR), (labels || {}));
const [showPreview, setShowPreview] = useState(previewInitially);
const [tableFormOpen, setTableFormOpen] = useState(false);
const [cols, setCols] = useState(3);
const [rows, setRows] = useState(3);
const areaRef = useRef(null);
const id = useId();
const insertAtCursor = useCallback((text) => {
var _a, _b;
if (!areaRef.current)
return;
const ta = areaRef.current;
const start = (_a = ta.selectionStart) !== null && _a !== void 0 ? _a : 0;
const end = (_b = ta.selectionEnd) !== null && _b !== void 0 ? _b : 0;
const before = value.slice(0, start);
const after = value.slice(end);
const next = `${before}${text}${after}`;
onChange(next);
requestAnimationFrame(() => {
const pos = start + text.length;
ta.focus();
ta.setSelectionRange(pos, pos);
});
}, [value, onChange]);
const apply = useCallback((wrap) => {
var _a, _b;
if (!areaRef.current)
return;
const ta = areaRef.current;
const start = (_a = ta.selectionStart) !== null && _a !== void 0 ? _a : 0;
const end = (_b = ta.selectionEnd) !== null && _b !== void 0 ? _b : 0;
const before = value.slice(0, start);
const selected = value.slice(start, end);
const after = value.slice(end);
let next = value;
if (typeof wrap === "function") {
next = before + wrap(selected) + after;
}
else {
const { prefix = "", suffix = "" } = wrap;
next = before + prefix + (selected || L.selectedPlaceholder) + suffix + after;
}
onChange(next);
requestAnimationFrame(() => {
const pos = start + 2;
ta.focus();
ta.setSelectionRange(pos, pos);
});
}, [value, onChange, L.selectedPlaceholder]);
const insertHeading = useCallback((level) => apply((sel) => `${"#".repeat(level)} ${sel || `${L.tableHeaderBase} ${level}`}`), [apply, L.tableHeaderBase]);
const buildMarkdownTable = useCallback((c, r) => {
const safeC = Math.max(1, Math.min(20, Math.floor(c)));
const safeR = Math.max(1, Math.min(50, Math.floor(r)));
const header = Array.from({ length: safeC }, (_, i) => `${L.tableHeaderBase} ${i + 1}`).join(" | ");
const sep = Array.from({ length: safeC }, () => "---").join(" | ");
const row = Array.from({ length: safeC }, () => " ").join(" | ");
const body = Array.from({ length: safeR }, () => `| ${row} |`).join("\n");
return `\n| ${header} |\n| ${sep} |\n${body}\n`;
}, [L.tableHeaderBase]);
const handleInsertTable = useCallback(() => {
insertAtCursor(buildMarkdownTable(cols, rows));
setTableFormOpen(false);
}, [insertAtCursor, buildMarkdownTable, cols, rows]);
const toolbar = useMemo(() => (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsxs("div", { className: "flex gap-0", children: [_jsx(Button, { size: "icon", onClick: () => apply({ prefix: "**", suffix: "**" }), "aria-label": L.bold, variant: "ghost", children: _jsx(FontAwesomeIcon, { icon: faBold }) }), _jsx(Button, { size: "icon", onClick: () => apply({ prefix: "*", suffix: "*" }), "aria-label": L.italic, variant: "ghost", children: _jsx(FontAwesomeIcon, { icon: faItalic }) }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { size: "icon", "aria-label": L.headings, variant: "ghost", children: _jsx(FontAwesomeIcon, { icon: faHeading }) }) }), _jsxs(DropdownMenuContent, { side: "bottom", className: "min-w-36", children: [_jsx(DropdownMenuLabel, { children: L.headingsMenuTitle }), [1, 2, 3, 4, 5, 6].map((n) => (_jsx(DropdownMenuItem, { onClick: () => insertHeading(n), children: `H${n}` }, n)))] })] }), _jsx(Button, { size: "icon", onClick: () => apply({ prefix: "[", suffix: "](https://)" }), "aria-label": L.link, variant: "ghost", children: _jsx(FontAwesomeIcon, { icon: faLink }) }), _jsx(Button, { size: "icon", onClick: () => apply((sel) => `- ${sel || "item"}`), "aria-label": L.unorderedList, variant: "ghost", children: _jsx(FontAwesomeIcon, { icon: faListUl }) }), _jsx(Button, { size: "icon", onClick: () => apply((sel) => `1. ${sel || "item"}`), "aria-label": L.orderedList, variant: "ghost", children: _jsx(FontAwesomeIcon, { icon: faListOl }) }), _jsx(Button, { size: "icon", onClick: () => apply({ prefix: "`", suffix: "`" }), "aria-label": L.code, variant: "ghost", children: _jsx(FontAwesomeIcon, { icon: faCode }) }), _jsx(Button, { size: "icon", onClick: () => apply((sel) => `> ${sel || "citação"}`), "aria-label": L.quote, variant: "ghost", children: _jsx(FontAwesomeIcon, { icon: faQuoteRight }) }), _jsx(Button, { size: "icon", onClick: () => setTableFormOpen((s) => !s), "aria-label": L.table, variant: "ghost", children: _jsx(FontAwesomeIcon, { icon: faTable }) })] }), _jsx(ButtonGroup, { className: "ml-auto", children: _jsx(Button, { size: "icon", variant: showPreview ? "default-success" : "default", onClick: () => setShowPreview((s) => !s), "aria-label": showPreview ? L.edit : L.preview, children: _jsx(FontAwesomeIcon, { icon: showPreview ? faPen : faEye }) }) })] }), tableFormOpen && (_jsxs("div", { className: "flex items-end gap-3 rounded-md border bg-govbr-gray-10 p-3", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-xs", children: L.columns }), _jsx("input", { type: "number", min: 1, max: 20, value: cols, onChange: (e) => setCols(parseInt(e.target.value || "1", 10)), className: "w-24 rounded border border-govbr-gray-30 bg-govbr-pure-100/5 p-2 text-sm" })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-xs", children: L.rows }), _jsx("input", { type: "number", min: 1, max: 50, value: rows, onChange: (e) => setRows(parseInt(e.target.value || "1", 10)), className: "w-24 rounded border border-govbr-gray-30 bg-govbr-pure-100/5 p-2 text-sm" })] }), _jsxs("div", { className: "ml-auto flex gap-2", children: [_jsx(Button, { onClick: handleInsertTable, children: L.insert }), _jsx(Button, { variant: "outline", onClick: () => setTableFormOpen(false), children: L.cancel })] })] }))] })), [L, apply, insertHeading, showPreview, tableFormOpen, cols, rows, handleInsertTable]);
return (_jsxs("div", { className: cx("w-full flex flex-col gap-2", className), children: [label && (_jsx("label", { htmlFor: id, className: "text-sm font-medium text-govbr-gray-80", children: label })), toolbar, _jsx("div", { className: "relative", children: !showPreview ? (_jsx("textarea", { id: id, ref: areaRef, placeholder: placeholder, className: editorVariants({ variant, density }), spellCheck: true, disabled: disabled, readOnly: readOnly, value: value, onChange: (e) => onChange(e.target.value) })) : (_jsx("div", { className: cx(editorVariants({ variant, density }), "prose prose-sm max-w-none overflow-auto bg-transparent"), children: _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], children: value || L.nothingToPreview }) })) }), hint && _jsx("span", { className: "text-xs text-govbr-gray-70", children: hint })] }));
}