Conceitos centrais
O modelo de dados
type Schema = TextSchema | TableSchema | ImageSchema | SectionSchema | ChartSchema | KpiSchema;
type Template = {
version?: TemplateVersion; // versão do formato — ausente = 1, ver "Versionamento de template"
page: PageSize; // { width, height } em mm
headerHeight?: number; // faixas estáticas (mm) repetidas em toda página gerada
footerHeight?: number;
marginLeft?: number;
marginRight?: number;
backgroundImage?: string; // PNG data URI, fundo tipo letterhead
schemas: Schema[];
// Opcional — ver "Templates com várias páginas" abaixo. Quando
// presente e não-vazio, é a fonte da verdade (os campos flat acima
// são ignorados); ausente/vazio cai nos campos flat como única
// página implícita, então todo Template de antes desse campo existir
// continua funcionando sem mudança.
pages?: TemplatePage[];
};
// Mesmo formato do Template, menos o próprio `pages` — um design de
// página independente (tamanho/cabeçalho/rodapé/fundo/schemas próprios).
type TemplatePage = {
id: string;
name?: string;
page: PageSize;
headerHeight?: number;
footerHeight?: number;
marginLeft?: number;
marginRight?: number;
backgroundImage?: string;
schemas: Schema[];
};
type Binding =
| { schemaName: string; type: "scalar"; path: string }
| { schemaName: string; type: "array"; path: string; columns: TableColumn[] }
| { schemaName: string; type: "keyvalue"; paths: string[] }
| { schemaName: string; type: "template"; template: string }
| { schemaName: string; type: "section"; path: string }
| { schemaName: string; type: "chart"; path: string; labelColumn: string; valueColumn: string; filters?: ChartFilterGroup[] };
Todo Schema compartilha BaseSchema (id, name, x/y/width/
height em mm, locked?, sectionId?) mais os campos específicos de
cada tipo — veja a Referência da API pública pra forma
completa e atual.
Unidade de medida: mm em todo o modelo de dados — fácil de
raciocinar, uma folha A4 é simplesmente 210×297mm. Convertido pra
px só pra renderizar no canvas do editor, e pra pt só na hora
de desenhar o PDF de verdade via pdf-lib.
Campos do editor
Texto, tabela, imagem, seção repetida (mestre-detalhe), gráfico (pizza/barra) e cartão indicador (KPI) — todos arrastam/ redimensionam livre (react-rnd), com grade de 5mm travando posição/tamanho por padrão — segure Shift durante o arrasto pra soltar da grade.
- Duplo clique liga edição inline (texto/tabela viram input/textarea direto em cima do campo; imagem abre o seletor de arquivo).
Delete/Backspaceremove todos os campos selecionados;Ctrl/Cmd+C/Vcopia/cola (desativado enquanto digita num input, pra nunca comer digitação normal).- Seleção múltipla:
Ctrl/Cmd+clique, ou arraste uma caixa de seleção sobre o canvas vazio. Arrastar qualquer campo selecionado move o grupo inteiro.
O painel lateral
Uma única fileira de abas, sem aninhamento — Campos (todo campo colocado, clique seleciona, travar/enviar-pra-trás/trazer-pra-frente/ remover), Página (tamanho, orientação, cabeçalho/rodapé/margem, fundo), sempre disponíveis; mais Dados/Estilo/Filtro — só presentes enquanto um campo está selecionado, e só as que fazem sentido pro tipo dele (sem Estilo pra imagem/seção; Filtro só pra gráfico).
As abas são reordenáveis por arraste e fixáveis (esconde com "×",
reaparece com "+") — ordem e abas escondidas persistem no
localStorage, uma preferência de UI separada do Template/
Binding[] salvo.
Edição em bloco na seleção múltipla — selecionar vários campos do MESMO tipo juntos (todos texto, todos KPI, todos gráfico) libera editar em bloco as configurações de Estilo compartilhadas (cor, tamanho de fonte, paleta…) em todos de uma vez, a partir de um painel só; o conteúdo de Dados de cada item fica travado pra evitar sobrescrever o conteúdo próprio de campos diferentes por acidente, exceto pelo punhado de configurações que são genuinamente compartilhadas (ex: o formato de número de um KPI, ou o separador de milhar/"agrupar em Outros" de um gráfico).
Avisos de configuração incompleta
Uma seção/gráfico sem vínculo com o JSON, ou um filtro de gráfico com coluna escolhida mas sem valor preenchido, ganha um ícone de alerta ⚠ amarelo na lista de Campos (e na aba certa) — apontando direto pro que precisa ser corrigido.
Tamanho de página, orientação e fundo
O <Designer> mostra um seletor de tamanho (A4/A3/A5/Carta/Ofício) e
orientação (retrato/paisagem) na aba "Página" —
applyOrientation/orientationOf/matchPreset/PAGE_SIZE_PRESETS
(todos exportados) fazem a mesma conta pra um seletor próprio.
Template.backgroundImage coloca uma imagem (um PNG data URI) atrás de
tudo, tanto no editor quanto no PDF final. O upload do <Designer> aceita
PNG/JPEG; um papel timbrado que só existe em PDF precisa ser exportado pra
PNG antes.
Templates com várias páginas
Template.pages (opcional) deixa UM Template guardar vários designs de
página diferentes — tamanho, cabeçalho/rodapé, fundo, schemas
próprios — que o generatePdf desenha num PDF só, em sequência, com
{pageNumber}/{pageCount} continuando de um design pro outro (se o
design 1 termina na página física 2, o design 2 começa na página 3, não
reinicia na 1). data/Binding[] são compartilhados entre TODAS as
entradas de pages — nome de schema precisa ficar único no template
inteiro, não só dentro de uma página. O exemplo report-builder constrói
uma UI de abas de página em cima disso: cada aba edita uma
TemplatePage, enquanto as fontes de dados JSON continuam
compartilhadas/globais entre todas as abas.
Fontes customizadas
Passe fontBytes (um TTF/OTF de verdade) pra generatePdf(..., { fontBytes }) pra acentuação/Unicode completos via fontkit. Sem
isso, cai no Helvetica padrão do pdf-lib (cobre a maioria dos
acentos latinos, não tudo). .woff/.woff2 também são aceitos —
normalizeFontBytes(bytes) detecta e descomprime pro TTF/OTF de
verdade que o pdf-lib precisa, automaticamente.
Descomprimir .woff2 precisa do wawoff2, uma dependência peer
opcional — não vem instalada sozinha (a maioria dos projetos nunca
embute fonte customizada, e é um binário WASM grande). Rode npm install wawoff2 só se você realmente for passar um arquivo .woff2;
sem ele instalado, passar um .woff2 lança um erro claro pedindo pra
instalar ou converter a fonte pra .ttf/.otf offline antes.