@innleaders/documents
v0.7.0
Published
Gerador de documentos Office no padrão InnLeaders — .docx, .pptx e .xlsx tematizados pela marca, sem dependência de runtime
Readme
@innleaders/documents
Gera .docx, .pptx e .xlsx no padrão InnLeaders a partir de um spec de conteúdo — ou a partir de um arquivo cru que alguém mandou. Irmão nativo do caminho HTML→PDF (
templates/documents/+scripts/render_pdf.py): mesma anatomia, mesmos tokens, mesma marca.
As regras de forma estão em brand/documents.md. Este pacote é a implementação delas para os formatos Office — quando a política e o código divergirem, a política vence e o código é corrigido.
Por que existe
Um documento da família é reconhecível: capa com faixa na cor da marca, seções 01 —, tabela com cabeçalho colorido, rodapé com © e paginação. Reproduzir isso à mão em cada proposta é retrabalho — e todo mundo erra um detalhe diferente. Aqui quem escreve descreve o conteúdo; a forma é decisão de marca e vem pronta.
Trocar capa.marca re-tematiza o arquivo inteiro — é o data-brand do lado Office.
Instalação
pnpm add @innleaders/documentsZero dependências de runtime. Os três formatos são OOXML — ZIP de XMLs — escritos direto com node:zlib. Ganhamos ausência de cadeia de suprimentos e controle total da fidelidade à marca; abrimos mão da conveniência de uma biblioteca pronta (o custo aparece quando o formato precisar de um recurso novo, como gráfico nativo ou imagem).
Uso — API
import { gerar, type DocumentoSpec } from '@innleaders/documents';
import { writeFileSync } from 'node:fs';
const spec: DocumentoSpec = {
formato: 'documento',
capa: { marca: 'innmarketing', tipo: 'Proposta comercial', titulo: 'Título da proposta', cliente: 'Cliente Ltda.' },
sumario: true,
blocos: [
{ tipo: 'secao', titulo: 'Contexto e objetivo' },
{ tipo: 'paragrafo', texto: 'Texto com **ênfase** onde importa.' },
{ tipo: 'investimento', valor: 'R$ 8.940', condicoes: 'parcelável em 3× sem juros' },
],
};
writeFileSync('proposta.docx', gerar(spec));Uso — CLI
# spec de referência de cada formato (é o melhor jeito de aprender o vocabulário)
npx innleaders-doc exemplo documento > proposta.json
# validar contra a política de estrutura (o gerar roda isto sozinho)
npx innleaders-doc validar proposta.json
# gerar — spec fora da política NÃO vira arquivo (--force para ignorar, com os avisos)
npx innleaders-doc gerar proposta.json proposta.docx
# reformatar um arquivo cru no padrão da marca
npx innleaders-doc formatar entrada.xlsx saida.xlsx --marca inndata --titulo "Base de contratos"O ciclo completo para IA: document_scaffold (MCP) entrega o esqueleto conforme → preencher → document_validate/validar reprova o que ficou fora da política ou com «placeholder» → gerar escreve o binário. A API expõe validar(spec) para o mesmo gate em código.
Imagens: foto/retrato (deck) e figura (documento) aceitam imagem — caminho de arquivo PNG/JPEG ou data URI base64 — com recorte de cobertura automático (nunca esticada). Sem imagem, sai o espaço reservado padronizado. Gráficos desenhados carregam a série completa como alt-text (descr): leitor de tela e máquina leem os dados.
Iconografia: icone: 'trending-up' nos blocos icones e kpis desenha o ícone da biblioteca oficial (assets/icons/, a mesma que o PDF resolve em {{ICON:nome}}) como vetor — custGeom no PowerPoint, VML no Word. Vetor e não PNG porque o ícone herda a cor da marca: rasterizar exigiria ~1.500 arquivos (9 marcas × 2 tintas × 83 ícones). pnpm icones regera a partir dos SVG, e o gate de CI (scripts/conferir-icones.mjs) quebra o build se um SVG mudar sem regerar.
Logotipo: a capa usa o logo oficial da marca quando o asset existe, e o lockup textual sancionado quando não — agente nunca desenha logo. Os PNG em logos/ são derivados dos SVG oficiais (assets/logos/) por pnpm logos, com o sha256 da origem no manifesto e gate de CI que quebra o build se o SVG mudar sem regenerar. $INNLEADERS_ASSETS/logos/<marca>/<variante>.png sobrepõe o embutido — quem tem o repositório usa sempre o logo mais recente. Arquitetura completa (banco de imagens, cache, geração por IA) no ADR 0006.
O que cada formato entrega
| Formato | Página | O que é nativo (não é imitação) |
|---|---|---|
| .docx | A4 retrato | numeração de seção 01 — (decimalZero), sumário nativo com número de página (campo TOC com resultado em cache e âncoras PAGEREF; abre sem o diálogo de campos externos — INNLEADERS_DOC_TOC_DIRTY=1 troca isso por recálculo automático ao abrir), paginação no rodapé (PAGE/NUMPAGES), marca-d'água de rascunho, propriedades do arquivo preenchidas |
| .pptx | 16:9 (12192000×6858000 EMU) | masters da família como shapes posicionados, tabela OOXML de verdade |
| .xlsx | — | formatos numéricos reais (moeda/percentual/data), painel congelado, autofiltro, SUBTOTAL na linha de total |
Os três formatos carregam o theme1.xml da marca (cores + Montserrat): o seletor de cores/fontes do Word, PowerPoint e Excel abre na paleta do design system — o que o cliente criar depois de receber o arquivo continua na marca.
O documento sai editável: o cliente abre no Word e continua trabalhando sem quebrar o padrão.
Vocabulário do spec
Definido em src/spec.ts — os blocos espelham os componentes do print.css, então o mesmo conteúdo rende nos dois caminhos:
- Documento:
secao·subtitulo·paragrafo·lista·tabela·destaque(marca/info/atenção/risco/sucesso) ·sumarioExecutivo·kpis·investimento·passos·fases·escopo·planos·riscos·citacao·faq·glossario·assinaturas·aprovacoes·versoes·nota·legal·cronograma(barras) ·contato(card do especialista) ·selos·raci·antesDepois·duasColunas·figura·anexo(numeraçãoAnexo A —própria) - Apresentação:
capa·capaClara·agenda·divisor·conteudo(comescuro) ·kpis·dashboard·bignum·grafico·linha·funil·waterfall·stack·donut·duascol·timeline·tabela·processo·matriz·piramide·arquitetura·icones·planos·risco·equipe·citacao·retrato·case·logos·foto·checklist·cta·fim - Planilha: abas com
colunastipadas (texto·numero·moeda·percentual·data·inteiro),kpis,totais,congelar
Ênfase inline: **negrito** no texto de qualquer bloco. O catálogo de masters do PDF tem paridade total no .pptx.
Estrutura de conteúdo por tipo de documento (o que é obrigatório numa proposta, num relatório, num parecer): policies/documents/exemplo-estrutura-proposta.v1.md. A tool document_scaffold do @innleaders/mcp devolve um spec inicial já conforme.
Slides de foto/retrato sem asset — ou com asset que não resolveu — saem com o espaço reservado padronizado (listras diagonais + etiqueta); binário de imagem vive no bucket, nunca no pacote (ver Imagens do acervo, abaixo). Os gráficos do deck (funil, donut, linha, cascata) são desenhados com geometria própria, ponto a ponto: os presets do formato somem ou deformam fora do PowerPoint.
Imagens do acervo (banco://, ilustracao://, ia://)
O spec carrega identificador lógico, nunca bytes e nunca URL de storage (ADR 0006 §8). Um deck de 30 fotos em base64 seria um JSON de ~120 MB; e apontar para o bucket amarraria todo o sistema ao caminho físico.
{ "tipo": "foto", "imagem": "banco://convencao-2026/abertura" }
{ "tipo": "figura", "imagem": "ilustracao://especialidades/gestao-de-leads" }
{ "tipo": "foto", "imagem": "ia://cidade-sao-paulo-aerea" }Resolver é passo assíncrono (tem rede no meio); gerar() continua síncrono. Rode a hidratação antes:
import { gerar, hidratarAcervo } from '@innleaders/documents';
const { spec: pronto, relatorio } = await hidratarAcervo(spec);
writeFileSync('deck.pptx', gerar(pronto));
if (relatorio.reservados) console.warn(`${relatorio.reservados} imagem(ns) caíram no espaço reservado`);A cadeia, na ordem: cache local por conteúdo (<cache>/<sha256>, o que torna o build offline e reprodutível) → $INNLEADERS_ASSETS/acervo/… → catálogo → bucket → espaço reservado. Sem rede, sem cache e sem asset, o documento sai igual, com o espaço reservado padronizado no lugar da imagem: documento nunca quebra por falta de imagem (decisão 5).
| variável | para quê |
|---|---|
| INNLEADERS_ACERVO_URL | endereço do catálogo (ex.: https://mcp.innleaders.com/api/acervo) |
| INNLEADERS_ACERVO_TOKEN | Bearer de serviço (ilsvc_…) com escopo de leitura |
| INNLEADERS_ACERVO_CACHE | onde fica o cache (default ~/.cache/innleaders/acervo) |
Sem as duas primeiras, tudo resolve para espaço reservado — que é o comportamento correto do CI e de quem acabou de clonar o repo.
A imagem chega já reduzida ao tamanho do slot (decisão 4). A largura sai do tipo do bloco — foto/capa/divisor 1920 px · figura/anexo 1600 · retrato/equipe 800 · contato/logos 600, e 1920 para tipo desconhecido. Quem redimensiona é o storage, na entrega: o pacote continua sem rasterizador. Só encolhe — imagem menor que o alvo vem como está. Para forçar, passe larguraAlvo (0 traz o original). E a mesma imagem usada N vezes entra uma vez no arquivo, endereçada pelo sha256.
ia:// é pedido de criação, não referência: ele primeiro busca no acervo (§3, "buscar antes de criar") e só aciona a esteira de geração no vazio, via o gancho opcoes.gerar. Enquanto a esteira (R3·S9) não estiver ligada, ia:// que não encontra nada no acervo cai no espaço reservado.
logo:// e icone:// não passam por aqui: conjunto fechado e crítico, vivem no Git → pacote → CDN (§6/§7). Publicar no acervo: pnpm acervo:publicar (ver scripts/acervo-publicar.mjs).
Reformatar arquivo cru
documentoDeArquivo() e planilhaDeArquivo() leem o arquivo, extraem a estrutura (títulos, listas, tabelas, tipos de coluna) e descartam a formatação de origem — de propósito: o resultado tem que sair com a forma da marca, não com a de quem mandou.
Limites conhecidos: não interpretamos imagens, campos, notas de rodapé, células mescladas nem fórmulas do arquivo de origem; .pptx de terceiro não é reformatado (monte o spec e use gerar).
Tipografia
Montserrat (fonte pública sancionada — Mont e Gotham são comerciais e nunca entram neste pipeline). A fonte vai embutida no .docx (Regular, Bold e Light como .odttf) e no .pptx (.fntdata): o documento renderiza igual em qualquer máquina, mesmo sem a Montserrat instalada. Custo: ~0,6 MB por arquivo; fontes: false no spec gera a versão leve (usa a substituta do sistema onde a fonte faltar). Os TTF vendorizados vivem em fontes/ com a licença OFL junto — ela permite embutir e redistribuir. O .xlsx não tem mecanismo de embed no formato.
Verificação
pnpm --filter @innleaders/documents build
pnpm --filter @innleaders/documents test:smokeO smoke gera os três exemplos e confere: ZIP legível, XML bem formado em todas as partes, partes declaradas no [Content_Types].xml, tema da marca em todo pacote, as 9 marcas × 3 formatos, gate de contraste WCAG (os pares oficiais do ADR 0003, os estados semânticos e as rampas tingidas, nas 9 marcas), lint de layout (todo shape do deck dentro do palco; sangria só por allowlist), idempotência (mesmo spec → mesmos bytes, graças à data fixa do ZIP), guarda de hex local (cor literal em src/*.ts fora de tema.ts reprova — o gate global do repo não cobre OOXML, que escreve cor sem #), casos de borda (spec vazio, tabela sem linhas, texto hostil escapado, nomes de aba duplicados/longos), imagem embutida (mídia + rel + content type), o validador da política (exemplos passam; proposta sem escopo, slide com 6 bullets e «placeholder» reprovam), ida e volta do caminho "arquivo cru → padrão" com fidelidade numérica e — no macOS — que o parser OOXML do próprio sistema abre os arquivos (qlmanage). Tudo isso roda no CI a cada push.
