@rhizzalab/report-react
v0.47.0
Published
Widget embutível de relatórios/propostas da Report Platform (editor de documentos orientado a blocos).
Readme
@rhizzalab/report-react
Widget embutível de relatórios da Report Platform: um editor
de documentos orientado a blocos que empresas usam para compor,
revisar e exportar documentos a partir dos seus dados estruturados. O host embute o editor como biblioteca — sem router,
login ou navegação próprios — passando apenas um token temporário, o
reportType e os data; o usuário compõe o documento visualmente,
reorganiza blocos, controla campos e colunas, salva modelos reutilizáveis e
exporta o PDF.
A arquitetura é híbrida e declarativa: ProseMirror gerencia a árvore editável (seleção, transações, undo/redo), JSON Schema valida dados e configurações e uma DSL JSON descreve a composição visual dos blocos. Regras sensíveis, autorização, uploads e geração de PDF ficam no backend multiempresa da plataforma. Nenhum JSON vindo de cadastro executa código, e editor, prévia e PDF compartilham a mesma árvore de renderização.
Instalação
npm install @rhizzalab/report-react
# ou
pnpm add @rhizzalab/report-reactO pacote traz duas entradas, com o mesmo contrato de opções, callbacks e capacidades:
| Entrada | Para quem | Runtime |
| ------------------------------------ | ------------------------------------------------ | ------------------------------------------------------------------------------------- |
| @rhizzalab/report-react | aplicações React (18 ou 19) | react e react-dom são peer dependencies do host; ProseMirror vem embutido |
| @rhizzalab/report-react/standalone | qualquer página/host que não é uma app React | React, ReactDOM e ProseMirror vêm embutidos no bundle |
Ambas são distribuídas em ESM (.mjs) e CommonJS (.cjs), com declarações
TypeScript autocontidas (.d.ts/.d.cts) — nenhum declare module é
necessário, em qualquer moduleResolution (bundler, node16/nodenext ou
node). O manifesto declara react/react-dom como peer dependencies do
pacote inteiro; a entrada standalone não os usa em runtime (traz o próprio
React), mas gerenciadores que instalam peers automaticamente (npm 7+) vão
baixá-los mesmo num host sem React — é inofensivo.
Desde a 0.37.0 a entrada React também traz o ProseMirror embutido
(ADR 0117, adendo de 30/09/2026).
Um host que empacota TipTap, ou outro prosemirror-model, levava o gerenciador
de pacotes a instalar uma segunda cópia, e a mistura das duas quebrava o widget
("Can not convert <> to a Fragment"). Os pacotes prosemirror-* continuam em
dependencies porque as declarações .d.ts importam os tipos deles, mas o
bundle não os carrega em runtime. Por isso, nunca passe objetos ProseMirror do
host (nós, estados, transações) para funções do SDK, nem o contrário: são
cópias diferentes.
Pré-requisito: o token vem do SEU backend
O widget nunca conhece o clientSecret da sua empresa. Seu backend troca a
credencial por um JWT temporário (TTL ≤ 1 h, sem refresh token) e o
entrega ao navegador; é esse token que o widget recebe. Em Node, o caminho
curto é a lib irmã @rhizzalab/report-node
(createReportPlatformClient(...).issueEditorToken(...) — ADR 0049). A troca é
servidor-a-servidor — o endpoint não responde a CORS de navegador, por
construção:
curl -sS -X POST "https://api.rhizzalab.com/v1/auth/editor-tokens" \
-H "Authorization: Basic $(printf '%s:%s' "$CLIENT_ID" "$CLIENT_SECRET" | base64)" \
-H "Content-Type: application/json" \
-d '{
"userId": "usuario-42",
"workspaceId": "obra-7",
"reports": [
{ "reportType": "acme.relatorio-mensal",
"scopes": ["editor:read", "templates:read", "templates:create"] }
]
}'Peça o mínimo de escopos que a tela precisa. Lista fechada:
editor:read, templates:read, templates:create, templates:update-own,
templates:set-default, assets:create, export:pdf. O workspaceId é
opcional: o id, no seu sistema, da conta/obra/projeto em que o usuário está
trabalhando — decidido pelo seu backend, usado só para identificação e medição
(auditoria e página "Uso"), nunca para autorização; o widget não o vê e sua API
não muda.
O data do relatório também é derivado no seu backend e validado pela
plataforma contra o dataSchema publicado do reportType. A sessão do editor
guarda apenas o hash do data e o documento de trabalho persistido nunca
contém dados; somente os snapshots de prévia/exportação congelam os dados
validados (para gerar o PDF) e são purgados pela plataforma.
Uso — host sem React (mountReportEditor)
import { mountReportEditor } from "@rhizzalab/report-react/standalone";
const handle = mountReportEditor(document.getElementById("editor"), {
token, // JWT temporário vindo do SEU backend
reportType: "acme.relatorio-mensal",
data, // derivado no SEU backend
documentKey: "orcamento-18", // opcional: a plataforma persiste e restaura o documento
onReady(context) {
/* sessionId, permissões efetivas, modelos disponíveis, avisos… */
},
onAuthRequired() {
renovarToken().then((novo) => handle.setToken(novo));
},
onConflict(conflict) {
// Três variantes, discriminadas por conflict.kind:
// "data-update" → keepLocal() | acceptIncoming()
// "working-document-save" → keepLocal() | acceptRemote()
// "template-save" → informativo (currentVersion, lastAuthor)
// Sem resposta, o documento local é preservado.
},
onError(error) {
console.warn(error.code, error.message); // código estável + mensagem pt-BR
},
});
// mais tarde
const documento = handle.getDocument();
handle.unmount(); // limpo e idempotenteTodo o DOM do widget — inclusive o <style> com escopo e os overlays — é
renderizado dentro do filho que o SDK cria no seu container. Nada é
injetado em document.head; o único toque em document.body é o
<a download> oculto, imediatamente removido, que dispara o download do PDF
exportado.
Uso — aplicação React
import { useMemo, useRef } from "react";
import {
ReportEditor,
ReportSdkProvider,
createReportClient,
type ReportEditorHandle,
type ReportEditorProps,
} from "@rhizzalab/report-react";
type Props = { token: string; data: ReportEditorProps["data"] };
export function ReportScreen({ token, data }: Props) {
const client = useMemo(() => createReportClient(), []); // API de produção por padrão
const handleRef = useRef<ReportEditorHandle | null>(null);
return (
<ReportSdkProvider client={client}>
<ReportEditor
ref={handleRef}
token={token}
reportType="acme.relatorio-mensal"
data={data}
documentKey="orcamento-18"
onReady={(ctx) => console.info("sessão", ctx.sessionId)}
onAuthRequired={() => renovarToken().then((novo) => handleRef.current?.setToken(novo))}
onError={(error) => console.warn(error.code, error.message)}
/>
</ReportSdkProvider>
);
}createReportClient é chamado uma vez por host; o provider carrega
apenas o client — nenhum token, dado ou estado de sessão vive nele.
apiBaseUrl — opcional, produção por padrão
Sem apiBaseUrl, o SDK fala com a API de produção da plataforma:
import { DEFAULT_API_BASE_URL } from "@rhizzalab/report-react"; // também exportado de /standalone
// "https://api.rhizzalab.com"Esse é hoje o domínio de andaime do Railway; quando o domínio definitivo da
plataforma entrar no ar, o default muda em nova versão do SDK e o domínio
anterior continua respondendo durante a janela de suporte. Instalações que
precisam de estabilidade absoluta devem passar apiBaseUrl explicitamente.
Para outro ambiente (desenvolvimento local, homologação, instalação própria), informe a base explicitamente — em qualquer uma das entradas:
createReportClient({ apiBaseUrl: "http://localhost:3001" });
mountReportEditor(el, { apiBaseUrl: "http://localhost:3001", token, reportType, data });Um valor em branco é rejeitado com TypeError (configuração errada falha
alto; nunca há fallback silencioso). O SDK nunca deriva a URL de
window.location.
Opções, callbacks e handle
Opções de mountReportEditor. As props de ReportEditor (React) são as
mesmas, com três diferenças: apiBaseUrl, fetch e validationLimits
pertencem a createReportClient na entrada React; previewDebounceMs
(padrão 2000 ms) e templatesToolbarTarget existem só na entrada React:
| Opção | Tipo | Descrição |
| --------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| token | string | JWT temporário — só em memória, enviado como Authorization: Bearer. Trocá-lo segue o fluxo de setToken |
| reportType | string | Chave estável da definição do relatório (ex.: acme.relatorio-mensal) |
| data | JsonObject | Dados do relatório, validados contra o schema publicado. Trocá-los (por referência) segue o fluxo updateData |
| documentKey | string? | Identidade do documento de negócio (1–200 caracteres). Presente → a plataforma persiste, restaura e faz autosave; mudar a chave abre um contexto novo |
| presentationState | DocumentPresentationState? | Snapshot inicial de tema, variáveis, imagens e sumário, persistido pelo host junto ao documento de negócio. |
| onPresentationStateChange | (state: DocumentPresentationState) => void | Snapshot imutável após mudanças locais; não dispara na restauração nem no eco das props. |
| autosaveDebounceMs | number? | Debounce do autosave quando documentKey está presente (padrão 3000 ms) |
| apiBaseUrl | string? | Base da API (padrão: produção — acima) |
| fetch | FetchLike? | Implementação de fetch injetável (testes, proxies) |
| validationLimits | Partial<ValidationLimits>? | Ajustes dos limites estruturais de validação (bytes/profundidade/nós) |
| logo | { src, alt? } \| null? | Logo quadrada da empresa (40 px, ícone de fallback 22 px, centrada no slot da rail) no canto esquerdo da navbar; null/src inseguro → ícone de prédio; imagem quebrada → ícone + onLogoError |
| title | string? | Linha principal da identidade do documento no lado esquerdo da navbar (ADR 0050). Vazia/whitespace = ausente |
| subtitle | string? | Linha secundária discreta sob o title (válida sem ele), mostrada só sem documentKey — com persistência a segunda linha é o status do autosave. Vazia/whitespace = ausente |
| onClose | (() => void)? | Callback do X da navbar e de X/Escape/Cancelar na abertura. Cancelar a abertura invalida suas operações; desmontar/confirmar dirty continua sendo responsabilidade do host |
| onPreviewOpenChange | ((open) => void)? | O modo Tema (prévia paginada) abriu/fechou — pelo cartão "Alterar tema", por "Voltar ao documento"/Esc, por uma aba da rail ou pelo handle; só em transições, nunca no mount |
| onEditorRevealed | (() => void)? | O editor ficou visível: o quadro de abertura saiu ou, sem abertura, o editor pronto apareceu. Uma vez por abertura (avisos que não podem ficar atrás do quadro) |
| appearance | "light" \| "dark"? | Aparência do chrome do editor (ADR 0063). O papel (página A4) continua branco, como no PDF |
| brandColor | string? | Cor da marca (#rgb/#rrggbb) — vira chrome-primary (superfícies dos CTAs) e o acento legível chrome-accent (texto, bordas, foco, ajustado a ≥ 4,5:1); inválida é ignorada |
| onPanelChange | ((panel) => void)? | O painel esquerdo fixo trocou de seção (catalog | templates | images); só em transições, nunca no mount; abrir/fechar o inspector (aba Propriedades) não conta |
Callbacks (ReportEditorCallbacks): onReady, onChange ({ revision,
dirty }, com throttle — nunca carrega o documento, puxe com
getDocument()), onDirtyChange, onTemplateSaved, onAuthRequired,
onConflict, onWarning, onError, onTelemetry (100% opt-in; sem token,
dado ou conteúdo de documento), onClose (ADR 0050 — o X no canto direito),
onPreviewOpenChange, onEditorRevealed e onLogoError. Com logo/title/subtitle/onClose
o host mostra a identidade do documento e o "sair" na navbar do próprio
widget (a topbar do protótipo: logo, título e status à esquerda; a toolbar
do editor no centro, sobre o documento — ADR 0069;
"Gerar PDF" e o X à direita), sem empilhar
uma top bar própria — tudo opt-in: ausentes, nada muda. Quem esconde a navbar
por CSS (host-chrome) declara --rp-navbar-height: 0px na raiz do widget.
Handle (ReportEditorMountHandle no standalone; ReportEditorHandle via
ref no React): getDocument(), undo(), redo(), setToken(token),
updateData(data) e openPanel(panel, { focus? }) (ADR 0063 — mostra a
seção Inserir/Modelo/Imagens do painel esquerdo fixo — o mesmo seam que a
rail usa; com o inspector aberto, fecha-o sem dismiss; "structure" deixou
de ser painel no ADR 0105 e é ignorado em silêncio; "brand" saiu do tipo
ShellPanel no ADR 0117, mas em tempo de execução abre o modo Tema na aba
Globais — no-op para um cliente sem getBrandProfile ou sem
listDocumentThemes); no standalone ainda
update(options) e
unmount(); no React ainda getTemplateActions(), setPreviewOpen(open) e
getExportController() para hosts que dirigem Prévia/Exportar do próprio
toolbar.
Exportação de PDF
"Gerar PDF" abre um diálogo central que mostra as fases da geração e pode ser
recolhido ("Voltar ao documento", Escape ou o fundo) sem interromper nada: o
botão passa a dizer "Gerando PDF" e o desfecho chega como aviso. O download é
sempre explícito e se chama {title}.pdf quando o editor tem title; sem
título, vale o nome do servidor. Um ExportPdfButton montado pelo host aceita
onExported(result, { suggestedFilename }) (o result.filename continua
sendo o do servidor) e available={false}, que tira o botão da tela sem
perder a geração em andamento. Detalhes no adendo de 04/10/2026 ao ADR 0051.
Persistir a apresentação do documento
O autosave com documentKey salva o documento, enquanto o host pode salvar a
apresentação separadamente usando DocumentPresentationState:
type DocumentPresentationState = {
version: 1;
themeId: string | null;
themeOverrides: ThemeOverridesByTheme;
summaryDisabledThemeIds: readonly string[];
};themeOverrides guarda overrides por tema e campo: { kind: "literal", text },
{ kind: "path", segments } ou { kind: "asset", assetId }. O host carrega esse
snapshot antes de montar ReportEditor/ReportStudio, entrega-o em
presentationState e persiste os snapshots imutáveis recebidos em
onPresentationStateChange. Dados comerciais, credenciais e URLs não pertencem
a esse envelope. O host deve verificar acesso aos IDs novamente ao salvar,
renderizar e exportar.
A restauração é inicial por contexto: mudar documentKey, relatório ou cliente
abre outro contexto. Repassar o snapshot nas props da mesma proposta não desfaz
edições locais nem dispara callback de eco. Para aplicar uma versão externa do
snapshot à mesma proposta, remonte o editor após resolver eventuais conflitos.
Trocar token para outra identidade limpa a apresentação anterior; o host deve
remontar para fornecer o snapshot do novo usuário. Em autoria de modelos, este
seam é ignorado: a persistência continua em authoring.onSave.
O tema salvo é validado no catálogo assíncrono antes da prévia/exportação. Se deixou de estar disponível, o editor exige uma escolha explícita e não grava um snapshot vazio sobre o estado salvo. Restaurar texto, imagem ou sumário também não emite callback. Escolher Sem tema e editar variáveis emite as mudanças para o host; bindings continuam bindings, sem congelar dados comerciais.
Antes de fechar, aguarde await ref.current.flushPendingChanges(): true
significa que não há edição de documento pendente no contexto atual; false
indica falha, conflito, troca de contexto ou edição sem persistência habilitada.
O método aguarda um autosave já em voo e salva edições concorrentes. Aguarde
também a fila de persistência de onPresentationStateChange do próprio host,
pois ela não faz parte do autosave do documento.
Primeiro acesso configurável (ADR 0120 — F03)
A nova jornada é opt-in em ReportStudio, ReportEditor e suas montagens
standalone. Sem experience, os hosts mantêm a abertura existente:
<ReportStudio
{...hostProps}
experience={{
version: 1,
opening: {
mode: "choose",
entries: ["blank", "standard-models", "saved-models"],
},
onboarding: "optional-after-selection",
personalization: { afterSelection: ["blank", "standard-models"] },
themeSelection: "immediate",
}}
/>Na abertura configurável, as entradas aparecem juntas: Começar do zero, Modelos
padrão e Meus modelos. Quando a etapa institucional é necessária, ela reúne
templates visuais, prévia e dados da empresa. Tema e sumário ficam locais até
Criar e passam pela mesma barreira de persistência. As cores dos controles
seguem brandColor; imagens dos modelos continuam sendo uploads do catálogo.
inherit é o default de onboarding; disabled dispensa somente a coleta
opcional. Obrigações do backend prevalecem. Uma falha de leitura da política
mostra recuperação e não libera a nova abertura. authoring é incompatível.
resume, hiddenActions, labels e preview-then-apply ainda são recusados;
o contrato completo proposto na F00 será implementado nas respectivas fases.
As entradas priorizam openingRole cadastrado no catálogo, recebido como
entrySource na listagem. Na ausência dessa classificação, modelos autorizados
SYSTEM/COMPANY aparecem em Modelos padrão e USER/GROUP em Meus modelos.
Sem uma estrutura blank cadastrada, a API oferece blankStart e o SDK usa
bootstrapBlank para gerar o mínimo permitido pelo relatório. Blocos obrigatórios
permanecem; conteúdo e apresentação do modelo padrão não são copiados. APIs e
clientes anteriores sem esse recurso mantêm a opção indisponível. O upload da
imagem continua opcional e fornecido pelo usuário.
experience.editor: { layout: "contextual" } ativa a edição com página
centralizada, inserção entre blocos e botão Ferramentas para abrir os painéis.
Essa tela vem depois da escolha/personalização ou da retomada do documento.
Omitir editor ou usar layout: "panels" preserva a apresentação anterior.
A opção não altera permissões, dados vinculados, histórico nem persistência.
Blocos únicos/maxCount são exibidos indisponíveis no catálogo contextual; um
destino que perdeu validade fecha o catálogo sem inserir em outro lugar.
Ao selecionar uma seção, Propriedades → Documento final oferece
Incluir no documento final. Desativar conserva o conteúdo editável e salvo,
mas o exclui da prévia, do PDF, da numeração e do sumário. A marca no editor
permite restaurar; desfazer/refazer usa o histórico comum. Regras explícitas
allowedBlocks[].requiredInOutput impedem ocultar seções obrigatórias.
Um rascunho sem seções incluídas pode ser salvo; o PDF exige ao menos uma.
Esse contrato usa document schema 7/protocolo 6: atualizar a API antes do SDK.
Veja a ADR 0125.
personalization.afterSelection seleciona quais entradas mostram a tela de tema,
logo, cores e dados institucionais a cada nova escolha, inclusive com marca já
preenchida. A lista pode incluir blank, standard-models e saved-models.
Valores atuais são reaproveitados; heranças não viram overrides ao confirmar sem
edição. A configuração não obriga novos campos nem repete a etapa ao retomar um
documento salvo. Sem workspace autenticado, não há edição da marca do cliente.
Sem essa opção, optional-after-selection continua oferecendo somente a coleta
inicial para marca incompleta e ainda não concluída.
Os textos do catálogo vêm de openingPreset na listagem, configurado para o
relatório no painel administrativo. Campos omitidos usam textos genéricos;
o título padrão é Novo documento, independente de title da instância.
O catálogo exibe a imagem enviada ou um ícone simples, sem gerar miniaturas
de conteúdo ou temas. A tipografia continua seguindo o token font-family.
O formulário mantém um rascunho e o confirma em Criar. A API oferece
createBrandDraft, getBrandDraft, discardBrandDraft e commitBrandDraft
no cliente; métodos são opcionais para preservar clientes legados. A jornada
opt-in informa capacidades ausentes. A prévia abre uma sessão temporária sem
salvar o documento e aplica o candidato institucional no renderer. Repetições
usam o mesmo ID; conflito pede revisão, e resultado desconhecido conserva a
operação para recuperação. Texto rico existente não é achatado.
Use documentKey para guardar o documento e presentationPersistence da F01
para confirmar também a apresentação no host. A criação espera os ACKs
pertinentes antes de revelar o editor; essas gravações não constituem uma
transação distribuída com a marca. No opt-in com documentKey, clientes sem
saveWorkingDocument ou sem workingDocumentRevision numérico no bootstrap
recebem erro visível e não revelam o editor. Sem a chave, documento/apresentação
continuam transitórios; confirmar a marca ainda persiste o perfil institucional.
API/migrações devem estar disponíveis antes
de ativar o opt-in. Guia e revisão F03.
Modelos existentes (ADR 0123 — F04)
Com experience, a abertura mostra até quatro modelos salvos recentes e três
padrões. Ver todos abre a respectiva coleção no mesmo modal, com busca por nome
e ordenação por edição ou A–Z. Não há nova opção obrigatória no host.
lastEditedAt é uma data ISO UTC fornecida pela API; clientes anteriores podem
omiti-la e recebem ordenação por nome. Uso/leitura/default não contam como edição.
A seleção revalida o modelo antes de substituir conteúdo. Documento salvo
retoma sem reaplicar o modelo; a troca pelo painel exige confirmação, inclusive
para reaplicar seu modelo de origem. Recusa antes de aplicar permite atualizar
o catálogo. Uma criação já aplicada sem ACK só pode repetir a mesma operação.
A personalização F03 segue personalization.afterSelection.
Entrega, semântica e limites F04. API nova é necessária para recência real; esta implementação não equivale a release nem instalação no consumidor.
Persistência confirmada no Studio (ADR 0118 — F01)
Na árvore local, ReportStudio e mountReportStudio aceitam a opção
presentationPersistence junto a documentKey. A porta fornece load(context)
e save(request): leitura confirmada, comparação de revisão e confirmação
idempotente de cada operação. O backend do host deve autorizar o objeto e
gravar apresentação/recibo de forma transacional. A plataforma continua
salvando apenas o working document pelo autosave existente.
Nesse modo, o Studio carrega a apresentação antes de montar o editor e retoma
um working document salvo sem o seletor inicial. O X e
await ref.current.requestClose() só chamam onClose quando as duas gravações
terminam no mesmo contexto. flushPendingChanges() também aguarda ambas, sem
fechar. false mantém a tela aberta. O host deve usar essa barreira para suas
próprias rotas/botões; desmontagem forçada ou encerramento do navegador não
aguardam gravações.
getPresentationState() retorna uma cópia imutável da apresentação atual,
incluindo Sem tema explícito, overrides e sumário. Recusa apresentação ainda
não resolvida, tema indisponível e autoria de modelos. O fechamento gerenciado
captura também a apresentação inicial quando o host confirmou ausência.
Não combinar a porta com presentationState, onPresentationStateChange ou
authoring; essa exclusividade é validada em runtime. Preserve a mesma
instância da porta durante a edição e renove-a ao trocar a identidade
autenticada/objeto. Sem a porta, os contratos anteriores continuam iguais.
resumeSavedDocument é também uma opção independente no ReportEditor e
em sua montagem standalone; ausente/falso mantém a abertura anterior.
O prazo padrão do ciclo é 30 s e o debounce de apresentação é 300 ms,
configuráveis por timeoutMs/debounceMs na porta. Timeout não prova rollback:
a próxima tentativa repete a operação sem duplicar commit. Conflito mantém a
edição e oferece escolha explícita da apresentação salva; salva o conteúdo
antes de substituir a apresentação local.
Tipos, protocolo, adaptador genérico e prova em dois domínios estão no guia F01. Esse guia distingue o simulador em memória de um backend durável.
O layout do editor é um shell fixo de três colunas (ADR 0063; ADR
0068): rail permanente de 60 px (role="tablist", ↑/↓/Home/End), painel
esquerdo fixo de 350 px e o canvas com a página A4. A rail tem as abas
Inserir/Modelo/Imagens (seções sempre montadas; a inativa fica hidden)
e, depois de um separador, Propriedades — o inspector do bloco
selecionado, que ocupa o mesmo painel esquerdo (não existe coluna à
direita): abre sozinho ao selecionar um bloco no canvas, fica aria-disabled
sem bloco selecionado, e fecha no X, no Esc, ao deselecionar e ao clicar em
outra aba da rail (inclusive via openPanel). Navegar pelo sumário do
documento, inserir/mover pelo catálogo/DnD e undo/redo não trocam o painel
— a aba só acende. Tudo em px — o chrome não escala com o font-size do
host.
O tema do documento fica num cartão flutuante no canto inferior direito do
canvas (ADR 0117;
[data-rp-theme-current], sem o antigo selo "Tema atual"). Pela largura do
widget, ele é um cartão completo com capa, nome, descrição e Alterar tema
em qualquer largura acima de 700 px e um botão redondo só com ícone até
700 px. A pílula compacta que a 0.35.0 usava entre 701 e 1679 px saiu na
0.37.0. Um cliente sem listDocumentThemes vê Prévia do
documento com o botão Ver prévia. O cartão some no modo Tema, durante o
arrasto de blocos e enquanto o modal de abertura está na tela; clicar nele não
desfaz a seleção do canvas. Alterar tema abre o modo Tema: a escolha de
temas ocupa a coluna do painel esquerdo e a prévia paginada ocupa o canvas.
Durante o modo nenhuma aba da rail fica selecionada. Sai-se por Voltar ao
documento, no topo da escolha de temas, por Esc ou por qualquer aba da
rail.
O sumário do documento (ADR 0105) fica à direita da página, pendurado
sob a toolbar: fechado é um minimapa de linhas (uma por título, mais curta
quanto mais fundo o nível, a da seleção em destaque); ao passar o mouse,
focar ou tocar abre um cartão com os títulos do texto rico e dos blocos em
ordem de documento — blocos sem título entram pelo rótulo do cadastro — e
um clique leva até eles. É só navegação: nada é gravado no documento, no
modelo ou no PDF. Ele existe onde a mesa tem espaço ao lado da página
(container do widget ≥ 1360 px) e substitui a antiga aba "Estrutura". A navbar não tem caixa "Modelo" (ADR 0069): a aba Modelo da rail
é a entrada — e acende um ponto (data-rp-template-dirty) quando o
documento diverge do modelo aplicado. Só um host-chrome que entrega
templatesToolbarTarget recebe o gatilho "Modelo" portalizado, que abre a
mesma seção do painel do widget. A superfície de Modelo inteira (gatilho
portalizado, aba e seção) só existe quando o token traz templates:read;
sem o escopo openPanel("templates") é no-op.
No painel Modelos, os modelos aparecem em Modelos do sistema (sistema e empresa) e Meus modelos (pessoais e compartilhados com o grupo), com miniatura do tema efetivo, descrição opcional e indicadores de padrão e em uso. Os cartões não repetem os rótulos Sistema, Da empresa e Meu; Do grupo continua indicando compartilhamento. As permissões de cada modelo são preservadas. A descrição pode ser informada ao salvar um novo modelo e aceita até 240 caracteres.
Ao abrir o widget, a rota vem antes do modal (ADR 0114, adendo de 05/10/2026). Enquanto o bootstrap e as leituras da abertura não respondem, nada aparece. Se passar de 400 ms, aparece um esqueleto com a geometria do destino: o primeiro acesso, descrito adiante, ou Escolha um modelo. O seletor espera a própria lista e fica com o botão ocupado enquanto o modelo é aplicado. Não há mais etapas de carregamento.
O mesmo contêiner permanece montado desde o token e a marca do Studio até o
editor aparecer. O modal mede min(1000px, 100vw − 32px) × min(640px, 100dvh −
48px), ocupa a tela até 680 px de largura e rola inteiro com até 560 px de
altura. O X e o Escape pedem antes de fechar. O token do Studio e o bootstrap
da abertura têm prazo de 30 s e, depois disso, mostram o erro com Tentar
novamente.
Na escolha, a lista fica à esquerda, agrupada em Seus modelos,
Compartilhados e Modelos prontos, com a descrição em até duas linhas e
a etiqueta do padrão (Seu padrão, Padrão da empresa ou Padrão). A
prévia fica à direita, com a descrição inteira e a imagem enviada para o
catálogo, ajustada sem recorte. Prévia do tema expande as páginas do tema
separadamente: capa, sumário (quando houver) e página interna, pelas
miniaturas de 300 px quando a API as manda. A busca só aparece com mais
de seis modelos, e o botão é
Usar este modelo (adendo de 05/10/2026 à ADR 0114).
Na F02, TemplateListItem.catalogImage é opcional e tem
{ assetId, url, width, height }; a URL é temporária e pode ser null.
Clientes anteriores continuam válidos. Sem imagem indica ausência do
upload; Imagem indisponível indica que a URL está ausente, inválida ou
falhou. Ambos preservam a seleção e o uso do modelo, sem substituir a imagem
por capa, logo, cor ou prévia gerada. Falha de listagem e catálogo vazio
continuam estados próprios. O SDK não grava essa URL no documento nem na
apresentação; a administração da imagem usa o fluxo autenticado do catálogo.
Enquanto o modelo é aplicado, o seletor fica com o botão ocupado, sem tempo mínimo; se os dados, o catálogo ou o tema ainda não estiverem prontos, ele continua ocupado. A barra lateral, a toolbar e a página aparecem depois dessa etapa. O documento permanece montado para preservar o rascunho e o histórico ao continuar. O switch Abrir o modelo padrão direto, na área de modelos, dispensa essa seleção nas próximas aberturas: o editor abre direto, com o esqueleto da abertura se a montagem passar de 400 ms. A preferência começa desativada e é salva no servidor por empresa, usuário e relatório; não altera qual modelo é o padrão. Um rascunho salvo oferece Continuar rascunho e só é substituído depois da ação explícita Substituir rascunho. Renovar o token ou atualizar os dados do documento não repete um fluxo de abertura concluído. O tour aguarda a revelação do editor.
X, Escape e Cancelar, no mesmo modal, encerram a abertura, invalidam operações pendentes e
invocam onClose quando fornecido. Respostas e timers atrasados não aplicam um
modelo nem reabrem o editor após cancelar; o host continua responsável por
desmontar seu contêiner. Para abrir o rascunho, use Continuar rascunho ou a
ação de continuar com o modelo atual. Os carregamentos usam ilustração
transparente embarcada no SDK, sem URL externa. Com prefers-reduced-motion,
a entrada e as trocas de conteúdo viram um fade de até 120 ms, sem deslocamento,
e os laços da ilustração param, mantendo os 1.000 ms mínimos e a espera pelo
trabalho real. Esse fluxo não acrescenta API, permissão, migração ou gravação de dados
comerciais.
Clientes próprios podem implementar getTemplatePreferences e
setTemplatePreferences (GET/PUT /v1/editor/template-preferences/me/:reportType,
escopo templates:read, campo openDefaultDirectly). Sem esses métodos, a
abertura mantém o comportamento anterior. O cliente HTTP embutido já os inclui;
instale a migração 0049 e a API correspondente antes de distribuir este SDK.
Uma falha na preferência permite continuar pela seleção e tentar salvar de novo.
É possível escolher modelos que usam outra versão do mesmo relatório (ADR 0116). Na mesma versão, aplicar continua sendo uma ação reversível pelo undo. Entre versões, o SDK solicita uma nova sessão autenticada, com os dados atuais validados contra o contrato do modelo escolhido, e inicia um novo histórico. O painel pede confirmação; a seleção inicial avisa quando substitui um rascunho. Uma falha mantém o documento atual. A escolha não altera os modelos padrão.
Com documentKey, a troca lê a revisão salva e a substituição só persiste
no autosave normal, com controle de concorrência (expectedRevision/CAS).
Sem a chave, a escolha fica na sessão. Clients customizados podem implementar
bootstrapTemplate(token, { reportType, data, templateId, documentKey? });
sem essa capability opcional, os modelos de outra versão continuam
indisponíveis. O client HTTP embutido já a implementa usando
POST /v1/editor/bootstrap com templateId. Implante a API correspondente
antes de distribuir o SDK.
Com um tema selecionado, a barra direita do modo Tema chama-se Variáveis e
tem duas abas: Do tema (textos, datas e imagens do tema) e Globais
(ADR 0117,
que substitui a aba Marca do ADR 0113). Globais edita os dados da marca do
cliente do token, em três seções: Identidade (Nome da empresa, logos para
fundo claro e escuro, cores primária e secundária), Quem somos (texto rico,
até 1.500 caracteres no texto simples) e Contato (endereço, telefone, WhatsApp,
site e Instagram). Um valor herdado do padrão da empresa aparece como placeholder,
com o selo "Padrão da empresa", e nunca é copiado para o cliente. Salvar
dados globais envia só os campos alterados; as logos são enviadas assim que
escolhidas. A etiqueta Neste tema marca o que o tema selecionado usa
(brandUsage do catálogo), e o selo "Marca · …" de uma variável do tema leva
ao campo em Globais. Depois de gravar, a prévia é refeita, o catálogo de temas
é relido em silêncio e o PDF retido é descartado. A aba fica somente leitura
sem cliente na sessão (o padrão da empresa continua no portal e na API do host)
ou quando o cliente não implementa updateBrandProfile. Ela não aparece na
autoria de modelos nem sem getBrandProfile.
Desde a 0.37.0 o Quem somos é texto rico
(ADR 0117, adendo de 30/09/2026):
negrito, itálico, sublinhado, listas com marcadores ou numeradas (até 3 níveis)
e links http(s) ou mailto. O editor é o componente público
BrandRichTextEditor, feito sobre o ProseMirror do próprio SDK, sem TipTap.
Ele recebe value (um BrandRichTextDoc ou null) e devolve
onChange(doc, change), com readOnly, disabled, placeholder, invalid,
counter e os atributos ARIA. Hosts que o montam fora de .rp-widget usam
BRAND_RICH_TEXT_EDITOR_CSS ou estilizam os ganchos data-rp-brand-rich-*. O
perfil da marca traz aboutRich ao lado do about simples, e o PATCH do
widget envia só aboutRich. Os temas continuam imprimindo a projeção em texto
simples.
Com o texto preenchido, o painel Inserir mostra o item Quem somos na
seção Texto ([data-rp-brand-about]). O clique insere os blocos na
seleção, e o arrasto os insere onde forem soltos. O cursor fica no fim do texto
inserido. É uma cópia: mudar a marca depois não altera o documento. Para
montar esse item, o widget lê o perfil da marca na montagem, uma vez por
identidade, enquanto o catálogo está na tela. O item não aparece na autoria de
modelos.
Os métodos de cliente são opcionais: getBrandProfile (leitura, ADR 0113),
updateBrandProfile (PATCH /v1/editor/brand-profile, validado localmente
antes da rede), putBrandLogo (multipart em
/v1/editor/brand-profile/logos/{light|dark}) e deleteBrandLogo. O cliente
HTTP embutido implementa todos. Instale a API do ADR 0117 (migração 0050 e,
para o Quem somos rico, a 0051) antes de distribuir este SDK: contra uma API
anterior o formulário aparece, mas a gravação falha com um alerta no próprio
formulário.
Se a empresa ligou o primeiro acesso do cliente no portal, a configuração
inicial (data-rp-opening-stage="onboarding") entra no mesmo modal antes de
Escolha um modelo, ou antes do editor na abertura direta (adendo de
05/10/2026 à
ADR 0117).
O que a pessoa vê:
- Introdução: "Vamos personalizar seus documentos".
- Lateral: o nome da área, que vem da prop
areaName(padrão "Área de Documentos e Relatórios"), e as etapas pedidas: Nome da empresa, Logo, Cores, Quem somos e Contato. - Etapas: uma por tela, cada uma gravada ao avançar, com PATCH só do que mudou. "Pular esta etapa" só navega.
- Conferência: quando parte dos dados já existe, termina numa conferência com edição. Com tudo preenchido, abre direto nela.
Regras:
- Campos: a empresa escolhe os campos pedidos e os obrigatórios. Um valor herdado do padrão da empresa conta como preenchido e aparece só como placeholder, nunca copiado para o cliente.
- Pular: Pular configuração aparece quando nenhum obrigatório falta. A conclusão vem de "Concluir cadastro", "Confirmar dados" ou "Pular configuração".
- Por cliente: a etapa vale para o cliente (o workspace do token), não se repete na renovação do token e só volta se um campo obrigatório ficar sem valor ou se o owner resetar o cliente no portal (ADR 0111).
- Leitura do status:
getBrandOnboarding(GET /v1/editor/brand-onboarding) segue o prazo da preferência de abertura e falha aberta. Erro, API anterior ou cliente semgetBrandOnboarding,completeBrandOnboardingeupdateBrandProfileabrem o editor sem a etapa. - Sem a etapa: sem cliente na sessão, ou em sessões sem o modal de abertura, não há primeiro acesso.
A coluna lateral (rail + painel esquerdo, inspector incluído) é position:
sticky; top: 0 com altura 100dvh − --rp-chrome-offset-top. O token só
encurta a altura: ele pressupõe que o scrollport do host é o próprio
contêiner do widget, com 100dvh − offset de altura útil (content-box).
Numa página que rola inteira, deixe o default 0px; num contêiner próprio
com overflow: auto, informe o que sobra acima dele — max-height menos o
padding vertical, não a altura do toolbar (--rp-chrome-offset-top: 182px
para max-height: calc(100vh - 150px) com padding: 16px). A largura do
painel esquerdo é --rp-panel-width, que aliasa o nome antigo
--rp-flyout-width (sobrescrever qualquer um dos dois funciona; o novo
vence).
Erros chegam por onError como ReportEditorError — code estável
(AUTH_REQUIRED, FORBIDDEN_REPORT, INVALID_DATA, INVALID_CONFIG,
INVALID_DOCUMENT, PROTOCOL_INCOMPATIBLE, PRIMITIVES_UNSUPPORTED,
CONFLICT, RATE_LIMITED, INTERNAL), message em pt-BR, issues[] com
JSON Pointer por campo e requestId. AUTH_REQUIRED no bootstrap é roteado
para onAuthRequired.
CSP da página do host
| Diretiva | Valor necessário | Por quê |
| ------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| connect-src | origem da API | bootstrap, modelos, uploads e exportação via fetch/XHR |
| frame-src | origem da API | a prévia paginada é um iframe da rota SSR de impressão |
| img-src | origem da API + origem do storage assinado (S3/MinIO) + data: | imagens da galeria usam URLs assinadas; a ilustração de abertura vem embutida no SDK |
| style-src | 'unsafe-inline' | o widget injeta seu stylesheet com escopo dentro do próprio container |
Nenhum cookie é usado; o SDK não carrega script remoto.
data: é necessário somente para a ilustração decorativa embarcada do
modal de abertura. Isso não muda o contrato de imagens comerciais: documentos
continuam guardando assetId, sem URL externa ou base64, e a leitura depende
da autorização de assets.
Uma URL de impressão na mesma origem do host usa iframe com sandbox="allow-scripts"
e exige prontidão da origem opaca ("null"), da janela atual e do snapshot atual.
Se o host faz proxy de um renderer que omite snapshotId, o gateway deve vincular
o sinal àquele snapshot autorizado, mantendo a decisão de prontidão do renderer.
URLs em outra origem usam allow-scripts allow-same-origin: renderers legados podem
omitir o ID somente quando event.origin e event.source correspondem à API e ao
iframe atual. Um ID presente diferente do snapshot é recusado em ambos os casos.
O modo srcdoc continua isolado em origem opaca e vinculado à janela atual.
Compatibilidade
- Navegadores evergreen (bundle
es2022; o standalone não depende deprocess.env). - React 18 ou 19 (só na entrada React).
- O SDK negocia protocolo, schema de documento e conjunto de primitivas com a
API a cada bootstrap; incompatibilidades chegam tipadas
(
PROTOCOL_INCOMPATIBLE,INVALID_DOCUMENT,PRIMITIVES_UNSUPPORTED) e nunca renderizam um documento pela metade — bloco desconhecido abre em fallback somente leitura com os atributos preservados. - Versionamento SemVer; a superfície pública tipada (
.d.ts) é o contrato.
Licença
UNLICENSED — todos os direitos reservados. Uso mediante credencial emitida
pela Report Platform.
