@joshualevy029/tiptap-editor
v0.3.3
Published
Rich-text editor component built with Tiptap and Material UI.
Readme
@joshualevy029/tiptap-editor
Editor rich-text em React construído sobre Tiptap v3, com UI em Material UI e ícones Iconify. Pensado para conteúdo didático: fórmulas matemáticas (MathLive + KaTeX), fórmulas químicas (mhchem), tabelas com balloon estilo CKEditor, imagens com resizer, colunas estilo jornal e variantes com visual de folha (documento paginado e página de apostila).
Sumário
- Compatibilidade
- Instalação
- Uso rápido
- Props do
Editor - Formato do conteúdo: HTML ou Markdown
- Modo controlado e não controlado
- Ref imperativa
- Features: ativar, desativar e configurar
- Customização de UI (prop
ui) - Variantes: DocumentEditor e ApostilaEditor
- Campos compactos:
inputetextarea - Ocultar a toolbar (
showToolbar) - Tema, aparência e dark mode
- Uso com Next.js
- Exibindo o conteúdo salvo
- Desenvolvimento
Compatibilidade
| Ambiente | Suporte | |---|---| | React | 18 ou 19 | | MUI (peer) | v9 | | Next.js | App Router e Pages Router (componente client-side — ver Uso com Next.js) | | Node no projeto consumidor | Qualquer (runtime é browser). Instalação via registro: funciona em Node 18+. Instalação via git: exige Node ≥ 20.19 (o build roda no install) |
Instalação
# via registro (recomendado — dist pré-buildado, funciona em Node 18)
npm install @joshualevy029/tiptap-editor
# ou via git (o build roda no install; exige Node >= 20.19)
npm install git+ssh://[email protected]/JoshuaLevy029/tiptap-editor.gitPeer dependencies (o projeto host precisa tê-las instaladas):
npm install react react-dom @mui/material @emotion/react @emotion/styledTiptap, KaTeX, MathLive e Iconify são dependências internas — você não precisa instalá-las nem conhecê-las.
Uso rápido
import { useState } from "react";
import { Editor } from "@joshualevy029/tiptap-editor";
export function MinhaTela() {
const [content, setContent] = useState("");
return <Editor defaultValue={content} onChange={setContent} />;
}<Editor /> sem nenhuma prop já funciona com todas as features ativas.
O valor emitido pelo onChange é uma string pronta para persistir no banco.
Props do Editor
Referência de todas as props. As detalhadas têm seção própria (link na coluna).
| Prop | Tipo | Descrição |
| --- | --- | --- |
| defaultValue | string | Conteúdo inicial (modo não controlado), no formato de saveAs. |
| value | string | Conteúdo controlado — o documento segue esta prop. |
| onChange | (value: string) => void | Chamado a cada alteração, com o conteúdo serializado (saveAs). |
| saveAs | "html" \| "markdown" | Formato de onChange/defaultValue/value. Default: "html". Ver Formato do conteúdo. |
| placeholder | string | Texto livre exibido enquanto o editor está vazio. Vale para todas as variantes. |
| readOnly | boolean | Somente leitura (oculta a toolbar). Default: false. |
| features | EditorFeatures | Ativa/desativa/configura cada feature. Ver Features. |
| ui | EditorUiOverrides | Customização de UI por item da toolbar. Ver Customização de UI. |
| variant | "standard" \| "document" \| "apostila" \| "input" \| "textarea" | Layout do editor. Default: "standard". Ver Variantes e Campos compactos. |
| showToolbar | boolean | Exibe/oculta a toolbar. Default: true em bloco, false em compacto. Ver Ocultar a toolbar. |
| appearance | EditorAppearance | Modo (light/dark) e cores sem ThemeProvider. Ver Aparência. |
| onSubmit | (value: string) => void | variant="input": chamado no Enter, com o conteúdo serializado. |
| minRows / maxRows | number | variant="textarea": altura mínima/máxima em linhas. Default: 2 / 8. |
| size | "small" \| "medium" | Densidade das variantes compactas. Default: "medium". |
| sx | SxProps<Theme> | sx do MUI aplicado à raiz, mesclado com o estilo interno. |
| className, style, id, aria-*, data-*, onClick, tabIndex… | atributos de <div> | Qualquer prop padrão de um <div> é encaminhada ao elemento raiz do editor. |
| ref | Ref<EditorHandle> | Acesso imperativo (getHTML, focus, clear…). Ver Ref imperativa. |
As únicas props não encaminhadas são as consumidas pelo editor (
value,defaultValue,onChange,saveAs,features,variant, …) e as que quebrariam o conteúdo (children,dangerouslySetInnerHTML).onChangeeonSubmittêm assinatura própria do editor (recebem a string, não um evento).
// O placeholder é texto livre, por instância, em qualquer variante:
<Editor placeholder="Digite o enunciado…" />
<Editor variant="input" placeholder="Comente e mencione com @…" />Formato do conteúdo: HTML ou Markdown
A prop saveAs define o formato do onChange e de defaultValue/value:
<Editor defaultValue="<p>Olá <strong>mundo</strong></p>" onChange={setHtml} />
<Editor defaultValue={"# Olá\n\n**mundo**"} onChange={setMd} saveAs="markdown" />- Default:
"html". - Markdown usa o serializador oficial
@tiptap/markdown; fórmulas saem como$...$/$$...$$. - Preservados no carregamento (mesmo sem botão na toolbar, pois fazem parte
da integridade do conteúdo): títulos, negrito, itálico, tachado, código inline
e em bloco (com linguagem), listas (aninhadas), link, blockquote,
task list (
- [ ]), linha horizontal (---) e tabela GFM. - Lossy em Markdown (sem sintaxe equivalente — só sobrevivem em HTML): cor do texto/fundo, marca-texto, fonte, tamanho, alinhamento e recuo. Para o catálogo completo, persista HTML.
Modo controlado e não controlado
// Não controlado: defaultValue só na montagem; o editor é dono do estado
<Editor defaultValue={inicial} onChange={setContent} />
// Controlado: o documento segue a prop value
<Editor value={content} onChange={setContent} />No modo controlado, o eco do próprio onChange é ignorado (não reseta cursor);
apenas mudanças externas de value substituem o documento. Para carregar
conteúdo vindo de um fetch no modo não controlado, monte o editor quando o
dado chegar: {data && <Editor defaultValue={data} />}.
Ref imperativa
const ref = useRef<EditorHandle>(null);
<Editor ref={ref} />;
ref.current?.getHTML(); // string HTML
ref.current?.getMarkdown(); // string Markdown
ref.current?.getJSON(); // JSON do Tiptap (ProseMirror doc)
ref.current?.focus();
ref.current?.clear();
ref.current?.getEditor(); // instância Tiptap (escape hatch)Features: ativar, desativar e configurar
Todas as features vêm ativas por padrão (exceto mention, que precisa de
uma fonte de dados — ver abaixo). Cada chave da prop features aceita:
true/ omitida → ativa com os defaults;false→ desativada (some da toolbar e a extensão nem carrega);- um objeto de configuração → ativa com ajustes.
// Desligar só o que você não quer (o resto continua ativo)
<Editor features={{ table: false, chemistry: false }} />
// Ou o oposto: começar de tudo desligado e ligar só o essencial
<Editor
features={{
// desative o resto explicitamente…
textType: false, image: false, table: false, math: false, /* … */
// …e ative o que precisa
bold: true,
italic: true,
bulletList: true,
}}
/>
// Configurar features específicas
<Editor
features={{
textType: { levels: [1, 2, 3] },
image: { onUpload: async (file) => minhaApi.upload(file) },
}}
/>Dica de tipagem: use
satisfies EditorFeaturesno objeto de config para o TypeScript validar as chaves e pegar erros de digitação.
Chaves disponíveis
As 26 chaves (todas em features.<chave>): textType, bold, italic,
bulletList, orderedList, indent, columns, image, table, undoRedo,
textAlign, lineHeight, backgroundColor, textColor, fontFamily,
fontSize, highlight, specialCharacters, strike, underline,
subscript, superscript, sourceCode, math, chemistry, mention.
Catálogo
| Chave | Feature | Configuração |
|---|---|---|
| textType | Parágrafo/Título 1–6 | levels: (1\|2\|...\|6)[] |
| bold / italic / strike / underline | Formatação básica | — |
| subscript / superscript | Sub/sobrescrito | — |
| bulletList | Lista com marcadores | styles: ("disc"\|"dash")[] |
| orderedList | Lista numerada | types: ("1"\|"a"\|"A"\|"i"\|"I")[] |
| indent | Recuo | maxLevel, step |
| columns | Colunas estilo jornal | counts: number[] (2–4) |
| image | Imagem (upload/URL, resizer, alt, legenda, alinhamento) | ver abaixo |
| table | Tabela (balloon com linhas/colunas/mesclar/propriedades) | resizable: boolean |
| undoRedo | Desfazer/refazer | — |
| textAlign | Alinhamento | alignments |
| lineHeight | Entrelinha | options: {label, value}[] |
| textColor / backgroundColor | Cores do texto/fundo | colors: string[] |
| highlight | Marca-texto | colors: string[] |
| fontFamily | Fonte | fonts: {label, value}[] |
| fontSize | Tamanho | sizes: {label, value}[] |
| specialCharacters | Caracteres especiais (busca + categorias) | sets |
| sourceCode | Código-fonte | mode: "html" \| "codeBlock" |
| math | Fórmulas matemáticas (MathLive + KaTeX) | templates, keyboardLayouts |
| chemistry | Fórmulas químicas (mhchem) | groups, templates |
| mention | Menção @ com dropdown de sugestões | items (obrigatório), char |
Imagem: upload
O editor entrega o File e usa a string retornada como src — pode ser a URL
devolvida pela sua API ou um data-URL base64:
image: {
onUpload: async (file) => {
const { url } = await api.upload(file); // sua API
return url;
},
accept: ["image/png", "image/jpeg"],
maxSizeBytes: 5 * 1024 * 1024,
onUploadError: (error, file) => toast.error(error.message),
}Sem onUpload, o editor converte para base64 (default zero-config). No
documento, a imagem selecionada ganha resizer nos cantos e toolbar flutuante
com alinhamento, texto alternativo (alt) e legenda.
Matemática e química
math: {
templates: ["\\hat{#0}", "\\frac{#0}{#?}"], // menu "Modelos personalizados"
keyboardLayouts: ["numeric", "greek", meuLayout], // abas do teclado virtual
},
chemistry: {
groups: [{ label: "Setas", items: [{ label: "→", value: "->" }] }],
templates: [{ label: "Combustão", value: "CH4 + 2O2 -> CO2 + 2H2O" }],
}O dialog de matemática tem editor visual (MathLive), modo LaTeX bruto sempre disponível e preservação byte a byte do código (fórmulas nunca são corrompidas por round-trip). O de química usa mhchem com paleta lateral por categorias.
Caracteres especiais
specialCharacters: {
sets: [{ label: "Meus símbolos", characters: ["©", { label: "seta", value: "→" }] }],
}Menção (@)
Ao digitar @, abre um dropdown de sugestões. A menção fica ativa apenas
quando você fornece items — é você quem decide o que sugerir (usuários,
termos, tópicos…), síncrono ou assíncrono:
<Editor
features={{
mention: {
// recebe o texto após o "@" e devolve os itens a sugerir
items: async (query) => {
const users = await api.buscarUsuarios(query);
return users.map((u) => ({ id: u.id, label: u.nome }));
},
char: "@", // gatilho (default "@"); use "#" para tópicos, por exemplo
},
}}
/>- Cada item é
{ id: string; label: string }. Olabelaparece no dropdown e no chip inserido; oidfica salvo no HTML (data-id) para você linkar depois. - Navegação por teclado (↑ ↓ Enter) e clique já vêm prontos.
- No HTML, a menção sai como
<span data-type="mention" data-id="42" data-label="Fulano" class="mention">@Fulano</span>. - Sem
items, a feature simplesmente não é carregada (não há o que sugerir).
Customização de UI (prop ui)
Ajuste a aparência de qualquer item da toolbar sem mexer no comportamento:
<Editor
ui={{
bold: { icon: "mdi:format-bold", tooltip: "Negrito (Ctrl+B)" },
textType: { icon: <MeuIcone /> }, // ReactNode também
italic: { hidden: true }, // some da toolbar (feature segue ativa)
orderedList: { hideOptions: ["I", "i"] }, // oculta opções do menu
}}
/>| Campo | Efeito |
|---|---|
| icon | Nome Iconify (string) ou ReactNode |
| tooltip | Substitui tooltip e nome acessível |
| hidden | Remove o botão da toolbar; atalhos/parse continuam funcionando |
| hideOptions | Oculta opções de menus pelo value |
Variantes: DocumentEditor e ApostilaEditor
import { DocumentEditor, ApostilaEditor } from "@joshualevy029/tiptap-editor";DocumentEditor— folhas paginadas estilo Word/Docs: página A4 (ou Carta/Ofício) sobre fundo cinza, zoom 50–150%, painel de configuração (formato, orientação, margens em cm, cor da folha) e paginação visual: blocos que estouram a altura útil pulam para a folha seguinte.ApostilaEditor— folha única no formato de página de apostila (736px, padding 40/24, tipografia 16px/1.6). Interpreta HTML legado comdiv.colscomo bloco de colunas editável.
Ambos aceitam todas as props do Editor (features, saveAs, ui…).
Campos compactos: input e textarea
Além das variantes de bloco, há duas variantes compactas — ideais para uma
caixa de comentário com menção @. A toolbar fica oculta por padrão nelas.
// Uma linha: Enter dispara onSubmit e nunca quebra a linha.
<Editor
variant="input"
features={{ mention: { items } }}
value={comment}
onChange={setComment}
onSubmit={(value) => {
enviar(value); // value no formato de saveAs (HTML por padrão)
setComment(""); // o editor não limpa sozinho — você decide
}}
placeholder="Comente e mencione com @…"
/>
// Multilinha que cresce entre minRows e maxRows.
<Editor
variant="textarea"
minRows={3}
maxRows={8}
features={{ mention: { items } }}
/>| Prop | Aplica a | Descrição |
| --- | --- | --- |
| onSubmit(value) | input | Chamado no Enter, com o conteúdo serializado (saveAs). |
| minRows | textarea | Altura mínima em linhas (default 2). |
| maxRows | textarea | Altura máxima antes de rolar (default 8). |
| size | input / textarea | Densidade estilo MUI: "small" | "medium" (default "medium"). Ajusta fonte e padding. |
Ocultar a toolbar (showToolbar)
showToolbar controla a exibição da toolbar. O default depende da variante:
true nas de bloco (standard/document/apostila) e false nas compactas
(input/textarea).
<Editor showToolbar={false} /> {/* bloco sem toolbar */}
<Editor variant="textarea" showToolbar /> {/* compacto com toolbar */}Tema, aparência e dark mode
O componente herda o ThemeProvider MUI do host. Há duas formas de
customizar cores e modo, combináveis:
1. Prop appearance (rápida, sem ThemeProvider)
Sobrescreve modo e cores pontuais sem precisar montar um tema. Omitida, o
editor herda o tema do host. Os campos de paleta propagam via um
ThemeProvider aninhado; os específicos da toolbar são aplicados direto.
<Editor
appearance={{
mode: "dark", // "light" | "dark" — omita para herdar do host
accent: "#7c3aed", // chip de menção, seleção, itens ativos
background: "#1e1e2e", // fundo da área de edição
text: "#e4e4e7", // cor do texto padrão
border: "#3f3f52", // bordas e divisores (tabela, toolbar)
toolbarBackground: "#2a2a3c",
iconColor: "#a1a1aa", // ícones das features (itens inativos)
buttonBackground: "#facc15", // fundo dos botões ativos
activeIconColor: "#000000", // ícone do botão ativo (contraste com o fundo)
iconSize: 22, // tamanho dos ícones (px ou string CSS)
buttonSize: 40, // tamanho (quadrado, mínimo) dos botões
}}
/>iconSize e buttonSize aceitam número (px) ou string CSS. buttonSize é o
lado mínimo do botão; botões com mais de um elemento (menus, seletor de cor)
crescem além disso na largura. Use activeIconColor quando buttonBackground
for uma cor forte, para o ícone ativo não sumir sobre o fundo.
Todos os campos são independentes: você pode, por exemplo, usar
mode: "dark" e sobrescrever apenas buttonBackground — o resto das cores
segue os defaults do modo escuro.
O dark/light é controlado pelo host (via appearance.mode ou tema) — não há
botão de alternância embutido.
2. createEditorTheme (controle total via MUI)
Para hosts que já usam MUI e querem um tema completo. Faz deep-merge sobre a
base grafite, então sobrescritas parciais (ex.: só palette.mode) preservam o
resto:
import { createEditorTheme } from "@joshualevy029/tiptap-editor";
<ThemeProvider theme={createEditorTheme({ palette: { mode: "dark" } })}>
<Editor />
</ThemeProvider>;createEditorTheme(options) aceita qualquer ThemeOptions do MUI.
Uso com Next.js
O editor é um componente client-side (usa DOM/contenteditable).
App Router — marque o arquivo que o usa:
"use client";
import { Editor } from "@joshualevy029/tiptap-editor";Pages Router (ou para evitar SSR por completo):
import dynamic from "next/dynamic";
const Editor = dynamic(
() => import("@joshualevy029/tiptap-editor").then((m) => m.Editor),
{ ssr: false },
);O componente já monta com immediatelyRender: false (seguro para hidratação).
Os CSS do KaTeX são importados pela própria lib.
Exibindo o conteúdo salvo
- Com o próprio pacote:
<Editor defaultValue={html} readOnly />— render fiel, fórmulas via KaTeX embutido. - Com MathJax no host: as fórmulas saem no HTML como
<span data-type="inline-math" data-latex="...">/<div data-type="block-math" data-latex="...">. Convertadata-latexem delimitadores (\(...\)/$$...$$) antes do typeset e carregue a extensãomhchempara química.
Desenvolvimento
npm install
npm run dev # playground (Vite) — inclui a aba "Modificador"
npm test # vitest (93 testes)
npm run typecheck # lib + playground + tests
npm run build # dist/ (ESM + CJS + d.ts)O playground abre na aba "Modificador": um painel interativo para ajustar variante, toolbar, aparência (cores e tamanhos) e features ao vivo e ver o editor reagir — útil para explorar toda a API sem escrever código.
A especificação completa (decisões, invariantes P1–P3 das fórmulas, roadmap headless) vive em SPEC.md.
Apoie o projeto
Se este editor te ajudou, considere apoiar o desenvolvimento:
