UNPKG

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