npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-react

O 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 idempotente

Todo 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 sem getBrandOnboarding, completeBrandOnboarding e updateBrandProfile abrem 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 de process.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.