@meistrari/minuta-editor
v1.4.0
Published
Editor de minuta do defesa.ai sem framework: folha A4 paginada, edição rica, placeholders de variável e export DOCX fiel ao preview — monta em qualquer app (React, Vue, vanilla)
Maintainers
Keywords
Readme
@meistrari/minuta-editor
O editor de minuta do defesa.ai sem framework: a folha A4 paginada de verdade, edição rica, cabeçalho/rodapé, placeholders de variável e export DOCX que reflui no Word como na folha (só a quebra manual do template é forçada; título leva keep-with-next). Monta em qualquer app — React, Vue, Svelte, vanilla — com uma chamada:
import { createMinutaEditor } from '@meistrari/minuta-editor'
import '@meistrari/minuta-editor/style.css'
const editor = createMinutaEditor(document.getElementById('minuta')!, {
html: documento.html,
onChange: () => salvar(editor.getHTML()),
})Zero I/O. Nada de fetch, autosave, histórico ou auth aqui — isso é do
app que monta. O que entra é HTML; o que sai é HTML e um Blob de .docx.
É o mesmo motor que o @meistrari/minuta-nuxt usa por dentro (o módulo Nuxt
é uma casca fina sobre este pacote), então o que se edita aqui sai idêntico
ao que o defesa.ai produz.
API
const editor = createMinutaEditor(el, {
html, // documento inicial
editable: true,
margins: { top: 95, right: 113, bottom: 95, left: 113 }, // px @96dpi (2,5cm / 3cm)
header: { logoUrl: null, position: 'left' },
chrome: { header: '', footer: '', showPageNumber: true },
variables: [...], // catálogo → liga o modo template
uploadImage: async file => ({ src, vaultSrc }) | null,
imageFallback: 'dataUri', // 'none' = upload falhou → não insere, só onImageUploadError
onImageUploadError: (error, file) => {},
toolbar: true, // false = o host monta a barra dele
onReady: tiptap => {},
onChange: () => {},
onEditChrome: part => {}, // clique no cabeçalho/rodapé
onVariableClick: ({ variableId, parentBlockId, text, kind }) => {}, // kind: 'text' | 'image' | 'missingImage'
onBlockClick: blockId => {}, // read-only (ou com blockClickWhileEditing)
})
editor.getHTML() // → string
editor.exportDocx() // → Promise<Blob> (carrega a lib `docx` sob demanda)
editor.usedVariables() // → string[]
editor.setEditable(bool)
editor.setChrome({ header, footer, headerScope, footerScope, showPageNumber })
editor.setHeader({ logoUrl, position })
editor.setVariables(catalogo)
editor.setBlockClickWhileEditing(bool)
// variáveis do case marcadas no documento (<span|img data-variable-id>, aviso de imagem faltando)
editor.scrollToVariable(id) // → boolean (rola e dá um flash)
editor.setVariableValue(id, texto) // → nº de ocorrências (o aviso de "faltando" vira chip)
editor.setVariableImage(id, src, vaultSrc?) // → nº de ocorrências (o aviso de imagem vira <img>; vaultSrc → data-vault-src)
editor.setImageFallback('dataUri' | 'none')
editor.variablesInDocument() // → string[]
editor.refreshVaultImages(vaultUrl => Promise<string>) // renova o src das imagens do vault (data-vault-src ou src="vault://"); sem undo/onChange
editor.schedulePaginate() // repagina (após mudar algo que afeta altura por fora)
editor.totalPages()
editor.destroy()
editor.tiptap // a instância do Tiptap — escotilha de fuga
editor.root / .container / .sheet // os elementos, para ancorar overlaysDecisões que vale conhecer
exportDocx devolve Blob, não baixa. Quem decide o nome, se manda pro
backend ou se abre em outra aba é o host. Um pacote que dispara download
sozinho toma uma decisão que não é dele.
toolbar: false + editor.tiptap. Um app com design system próprio
(antd, Material…) monta a barra dele e fala com o Tiptap direto —
editor.tiptap.chain().focus().toggleBold().run(), isActive('bold'). É a API
do Tiptap, documentada, sem um wrapper de comandos que sempre falta um.
CSS em arquivo, com namespace. Tudo vive sob .poc-editor-root, inclusive
as utilities atômicas que o defesa.ai tira do UnoCSS. Importe
@meistrari/minuta-editor/style.css uma vez.
A lib docx fica fora do entry principal. É o maior pedaço do bundle
(~1 MB) e só serve no clique de exportar: exportDocx() faz import()
dinâmico, e quem precisa da função crua importa de
@meistrari/minuta-editor/docx.
Modo template (placeholders de variável)
Passe o catálogo e o editor vira editor de template: / no texto (ou o botão
{ } da barra) abre o seletor, busca pelo rótulo e insere um placeholder.
createMinutaEditor(el, {
html,
variables: [
{ key: 'PROCESSO', label: 'Nº do Processo', description: 'De info_processos.processo', group: 'Processo' },
{ key: 'NOME_REU', label: 'Nome do Réu', group: 'Réu' },
],
})
editor.usedVariables() // ['PROCESSO', 'NOME_REU']O placeholder é um nó atômico do Tiptap (<span data-minuta-var="PROCESSO">),
não texto: seleciona inteiro, apaga inteiro, formata inteiro. Negritar metade de
um #PROCESSO textual produziria <b>#PRO</b>CESSO e a substituição passaria
reto; com nó, isso não é representável.
Para resolver, resolveTemplateVariables(html, { PROCESSO: '…' }) (exportado
daqui) — o que não tem valor continua placeholder vivo no documento.
React
import { useEffect, useRef } from 'react'
import { createMinutaEditor, type MinutaEditor } from '@meistrari/minuta-editor'
import '@meistrari/minuta-editor/style.css'
export function MinutaEditor({ html, onChange }: { html: string, onChange: (html: string) => void }) {
const host = useRef<HTMLDivElement>(null)
const editor = useRef<MinutaEditor | null>(null)
useEffect(() => {
editor.current = createMinutaEditor(host.current!, {
html,
onChange: () => onChange(editor.current!.getHTML()),
})
return () => editor.current?.destroy()
// o HTML inicial é lido uma vez: o editor é a fonte da verdade enquanto vive
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return <div ref={host} style={{ height: '100vh' }} />
}Vue / Nuxt
Use @meistrari/minuta-nuxt — ele já embrulha este pacote em componentes
(<MinutaEmbed>, <MinutaTemplateEditor>) e traz o proxy de API do defesa.ai.
Desenvolvimento
bun install
bun run build # unbuild → dist/
bun run typecheckPublicado no npm pelo workflow Publish @meistrari/minuta-editor. Como o
minuta-nuxt depende deste pacote pelo npm, publique este primeiro quando
uma mudança atravessar os dois.
