Changelog
Todas as mudanças relevantes deste pacote ficam documentadas aqui. Formato inspirado, sem seguir à risca, em Keep a Changelog.
3.3.0 (2026-09-10)
Acessibilidade, e o CI que teria pegado isso.
Quatro operações do editor existiam só como gesto de mouse — quem navega
por teclado não conseguia fazer nenhuma delas. Uma, renomear coluna de tabela,
era regressão da própria 3.2.0: ela removeu o campo "Colunas (cabeçalho,
vírgula)", que era a única via por tecla, e deixou no lugar um duplo clique
num <span>.
Added
- Botão de renomear (lápis) em todo chip de coluna, ao lado do
ƒx, do estilo e do remover.<button>de verdade, então o Tab alcança e o leitor de tela anuncia — o que o duplo clique nunca foi. O duplo clique continua valendo; agora é o atalho, não o único caminho. - Botão de renomear em toda linha da lista de campos, mesmo motivo e mesmo formato.
IconPencilentra no conjunto de ícones exportados (agora 21).- Os três modais são diálogos de verdade.
role="dialog",aria-modal="true", earia-labelledbyapontando pro título que já está na tela (assim os dois não divergem). Mais o comportamento que faz a marcação significar algo: o foco entra no diálogo ao abrir, o Tab fica preso dentro dele, e o foco volta pro elemento que abriu ao fechar. Compartilhado emcomponents/ui/useDialogA11y.ts, porque oPdfPreviewModalnão passa pela casca doModal. - O
PdfPreviewModalfecha com Escape. Era o único modal do pacote que só fechava com clique.
Fixed
- Selecionar campo era só-mouse. A linha da lista carregava o
onClicke nada na lista era focável além dos botões de ação, então quem usa teclado não conseguia selecionar campo NENHUM — o que trava toda edição, já que o painel de propriedades segue a seleção. O nome do campo agora é um<button>comaria-pressed; a linha segue clicável por conveniência de mouse, marcadarole="presentation"porque não é mais o controle acessível. Mesmo tratamento nas sub-linhas de elemento de KPI. - O × de "esconder aba" era um
<span role="button">DENTRO de um<button>— interativo aninhado, que é HTML inválido, e semtabIndex, então esconder aba também era só-mouse. Agora é um<button>irmão, dentro de um wrapper novo.jpd-tab__slot. O<button className="jpd-tab">em si ficou intacto de propósito: odata-activedele é lido por umquerySelector, pelo CSS e pelo JSX, e esses três andam juntos. Diferença visível: o sublinhado de aba ativa agora termina no rótulo, em vez de passar por baixo do ×. - O botão de fechar do
PdfPreviewModalnão tinha nome acessível — só um glifo×dentro do botão, anunciado como "button". - Renomear não substituía nada. Os dois inputs de rename abrem com o texto
selecionado. Antes o caret caía no fim e digitar anexava (
"PNR"+ o que se digita) — achado no navegador, não em teste. Vale registrar a segunda metade:onFocusnão resolve. OautoFocusdo React foca durante o commit, antes do handler estar resolvível, então ele não dispara (medido:selectionStarteselectionEndos dois no fim). Exige um ref callback ESTÁVEL — e estável importa, porque uma arrow inline re-selecionaria o texto a cada tecla.
Changed
jsx-a11yligado no linter. Ele simplesmente não estava emplugins, e foi assim que 25 achados se acumularam sem ninguém ver. Quatorze regras são erro; quatro estão desligadas com o motivo escrito ao lado no.oxlintrc.json, e o motivo é o ponto:click-events-have-key-events,no-static-element-interactionseno-noninteractive-element-interactionsdisparam em superfície de gesto — a folha do canvas, o puxador de redimensionar coluna, o arrasto de KPI, o duplo clique que troca a imagem, a célula que entra em edição inline. Nenhuma tem equivalente de teclado que faça sentido ("arraste isto 3mm pra direita" não é uma tecla), e todas são alcançáveis de outro jeito: posição, tamanho, largura de coluna, imagem e conteúdo de célula são editáveis pelo painel de propriedades, que é só campo de formulário. Ligar as três produziria 13 avisos que só se calam pondorole="button"e umonKeyDownvazio em<div>que não são botão — mentir pro leitor de tela pra calar o linter.interactive-supports-focusfica como erro, e é a regra que pegou o × da barra de abas.no-autofocusestá desligada porque, num editor inline, o autofocus é o comportamento certo.CardTitlepassachildrenexplícito em vez de pelo spread. Com<h3 {...rest} />a regraheading-has-contentnão vê conteúdo nenhum e acusa todoCardTitlecomo cabeçalho vazio; escrito assim a regra fica ligada e continua pegando um genuinamente vazio.viewportRefpassa a serMutableRefObject, nãoRefObject— ver a seção de CI para o motivo.
CI
O trabalho de acessibilidade acima era invisível pro CI, e outras três coisas que o pacote promete também eram:
peerDependenciesaceitareact ^18 || ^19, e só uma era testada. Um job novoreact-18rebaixa o React e roda typecheck, testes e build. Falhou na hora, em duas incompatibilidades reais de tipo entre as majors, que um consumidor em React 18 buildando do fonte teria batido:Slot<P, E>no registry de UI. No@types/react18 a call signature de um exotic component (o queforwardRefdevolve) retornaReactNode, enquanto oFunctionComponentdentro deComponentTyperetornaReactElement | null— mais estreito, então os nossos próprios doze componentesforwardRefdeixavam de ser atribuíveis aos slots deles (12 × TS2322). O tipo agora é uma união que cobre as duas majors, sem afrouxar nada pro consumidor.viewportRef. O@types/react19 tornou oRefObjectmutável e fazuseRef<T|null>(null)devolverRefObject<T|null>; no 18 oRefObject<T>temcurrentreadonly, e a proprefde um<div>lá recusa essa instanciação.MutableRefObject<T|null>é{ current: T|null }nas duas.
enginesprometenode >=18, e nada testava. Uma matriz de Node no job principal não serviria: o toolchain de dev não roda em 18 (vitest 4 pede^20||^22||>=24, oxlint pede^20.19||>=22.12). Mas o consumidor não roda vitest — ele importa odist. Então o job novoconsumer-nodeempacota com um Node suportado e depois instala e executa o tarball em 18, 20 e 22, que é a promessa que está de fato sendo feita.- O site de docs só era gate depois do merge. O build do Docusaurus rodava
só no
pages.yml, que dispara em push pra master — então MDX quebrado chegava em master e falhava no deploy, não no PR. E MDX quebra fácil aqui: o changelog e a página de vínculos são cheios de{...}e<...>, que o MDX lê como expressão e JSX. O job novodocsbuilda os dois locales, porque tradução ausente ou erro de MDX só no espelho pt-BR não apareceria no builden.
Tests
test/a11y.test.tsx (novo, 14 casos): a marcação de diálogo afirmada sem DOM
(que é exatamente por que o ModalShell existe separado do Modal), guards de
fonte pra cada operação que era só-mouse, e um guard de que o plugin do linter
continue ligado. Os 14 foram mutation-testados — 12 regressões deliberadas,
todas pegas. Duas coisas que passam e vale escrever, porque as duas são modos
de falha de guard que este repo já sofreu:
- A primeira versão casava o próprio comentário. Os comentários daqui
citam o código antigo de propósito (
era um <span role="button">), então os guards de fonte leem através destripComments— que já existia emtest/support/classScan.ts. toContain("useEscapeToClose")passava pela linha de import: apagar a chamada e deixar o import não derrubava o guard. Achado mutando o próprio guard. Agora ele checa a chamada.
3.2.0 (2026-09-04)
Coluna de tabela passou a ter identidade própria. Renomear o título muda o
título e nada mais, o editor ƒx sempre abre com o token que está realmente
em uso, e toda chave de JSON ganhou uma forma que dá pra escrever como
caminho — inclusive as que não tinham nenhuma.
Adicionado
-
Caminho entre brackets —
{[token]}. A leitura permissiva que este formato sempre teve (um operador só é operador com espaço dos dois lados, então{my-key}e{my key}são caminhos) cobria quase toda chave de JSON, mas não todas: chave com ponto literal, ou com(/)/,/", ou com operador cercado de espaço dentro do nome, não tinha forma nenhuma. A forma delimitada dá nome a elas:Escrito Resolve {[id]}a chave id{[cliente].[nome]}caminha cliente→nome{[cliente.nome]}a chave literal "cliente.nome"— ponto dentro do bracket não separa{["token name"]}a chave "token name"— segmento com espaço precisa de aspas{[total] + 1}conta; o operador está fora do bracket {CURRENCY([total], "R$", 2)}caminho bracketado como argumento de função Segmento com espaço tem que vir entre aspas (
"ou', a que o conteúdo permitir), e isso é recusa deliberada, não inconveniência:[a + b]é ambíguo entre a chave"a + b"e uma conta dentro do bracket, e escolher um em silêncio é pior que dizer não.emptySegment,unclosedBracketespaceInSegmentsão os três códigos de erro de expressão novos.O nó
pathda AST passou a carregarsegments: string[]em vez depath: string. Não é cosmético: uma string com pontos não consegue representar[a.b](um segmento que contém ponto), que é exatamente a ambiguidade que os brackets existem pra resolver. -
Renomear coluna com duplo clique na lista "Colunas atuais da tabela", e a ação
renameTableColumn(index, label)por trás. Ela mudahead[index]e, numa coluna calculada, olabeldo vínculo — e não toca em mais nada. -
tokenFor(key)— a regra única de como uma chave vira token, usada pela tabela recém-vinculada, pela normalização e pelos chips doƒx, então as três não têm como discordar. MaissegmentForpro segmento sozinho. -
columnFormulaFor(content, columns, index)— a mesma precedência que o PDF usa, exportada pra quem desenha o próprio painel. -
makeBoundTable(nextY, path, columns)— tabela já vinculada a um caminho de array, com o token de cada coluna preenchido. Isto estava fora do pacote, e os cinco examples deste repo o reimplementavam — errado do mesmo jeito (ver Corrigido). -
normalizeTableColumns(template, bindings)— converte coluna de chave crua em{label, formula}em dado já salvo. Idempotente, e exportada também dejson-pdf-designer/server, porque normalizar acervo é trabalho de script tanto quanto de editor. Ela não é chamada automaticamente: reescrever o template do consumidor na montagem seria efeito colateral invisível.
Mudou
-
{[a]}significava a chave literalmente chamada[a](o lexer não tratava bracket, então ele era absorvido pelo identificador) e agora significa o caminhoa. Chave de JSON com bracket no nome se escreve{["[a]"]}. O resto está intacto:{cliente.nome},{my-key}e{my key}continuam os caminhos que sempre foram.- {[a]} // a chave chamada "[a]"+ {["[a]"]} // a mesma chave, depois da 3.2.0 -
O campo "Colunas (cabeçalho, vírgula)" saiu do painel de tabela. Ele era a causa do bug de renomear, não uma baixa da correção: substituía o
headinteiro a cada tecla. A açãosetTableHeade a proponSetHeadListcontinuam exportadas — consumidor que renderizaPropertyPanelTabledireto não quebra; o painel só não tem mais um controle que reescreva a lista. -
A célula do canvas esconde os brackets.
{[id]}é o que fica gravado e{id}é o que é desenhado — odisplayCelljá fazia isso com{CURRENCY(x, "R$", 2)}. Numa tabela de 68 colunas, 68 pares de brackets é ruído, e a forma gravada continua explícita.
Corrigido
-
O editor
ƒxde uma coluna de tabela abria vazio. O painel o semeava a partir debinding.columns[i], e só quando aquela entrada era objeto, enquanto o PDF lêcontent[0][i]primeiro. Então numa tabela vinculada por fonte de dados — cujocolumns[i]é string crua e cujocontent[0][i]guarda o token — o editor lia o depósito de fallback e o PDF o principal. O token estava lá, funcionava, e a interface nunca o mostrava. O destaque do botãoƒxvinha da mesma leitura errada, então ele também não acendia. Os dois agora passam porcolumnFormulaFor, que é a precedência do próprio resolver. -
Renomear o título de uma coluna perdia a referência de dado. Não existia operação de renomear. O único jeito de mudar um título era o campo separado por vírgula, que chamava
setTableHeadcom a lista inteira a cada tecla; e aquilo re-deriva todo slot casando cada nome novo contra o head antigo — um nome renomeado, por definição, não está no head antigo. Então a coluna caía no fallback e perdiacontent[0][i](o token, que é o que decide o PDF),columnStyles[i]ecolumnWidths[i], enquantobinding.columns[i]era sobrescrito com o título novo virando chave de JSON. Renomear era indistinguível de "apaga a coluna X, insere a coluna Y".Verificado no
report-builder: depois do rename,binding.columns[2]continuafare_amount, a célula de design continua100.00, e o Ctrl+Z do próprio app desfaz o rename — ou seja ele passa pelosetStatedo consumidor como toda outra mutação. -
Tabela arrastada da árvore de campos nascia sem token. Montar tabela vinculada nunca esteve no pacote (
onCanvasDropé passthrough cru), então cada example fazia à mão e escreviacontent: [columns.map((c) => c.toUpperCase())]— um placeholder sem chaves ("DESCRICAO"), que o resolver de propósito não trata como template. É por isso que a tabela arrastada tinha oƒxvazio e a vinculada pelo painel não. Os cinco examples passam portokenForagora, e omakeBoundTablepõe a coisa inteira no pacote. -
O
handleChangeBindingescrevia a forma sem brackets. Vincular uma tabela pelo painel emitia`{${c}}`, que está errado pra qualquer chave com ponto, espaço ou parêntese. Ele usatokenForagora, e as colunas que escreve são{label, formula}em vez de chave crua.
Interno — layout de arquivo
Nenhum nome público mudou: o publicSurface.test.ts mantém um inventário
explícito de todo export de runtime nas duas direções, e ele fica intacto
depois destes movimentos. O mapa exports do package.json é fechado (.,
./server, ./preview, as duas folhas de estilo), então caminho interno
nunca foi alcançável pelo import de um consumidor. O que mudou foram
caminhos de fonte, e isso importa porque ARCHITECTURE.pt-BR.md e
package-structure listam eles.
O formato em que isto chegou: treze pastas dentro de src/, nenhuma com um
arquivo só, e a maior listagem plana é components/ui/ com 21 — que são 21
arquivos de um componente cada, então a contagem é justamente o ponto.
Antes, três pastas tinham um arquivo cada e o próprio src/ tinha dez
arquivos que não são entry point.
-
src/fields/agrupa as três pastas por tipo de campo que estavam na raiz como irmãs depdf/edesigner/:fields/table/,fields/kpi/,fields/chart/. São a mesma coisa — a lógica pura que um tipo de campo tem — e otypes/schema.tsjá deixa claro que tipo de campo é o eixo em que o pacote inteiro gira. Okpi/em si já havia substituído osrc/kpiFormat.ts, cujo nome descrevia UM de treze exports (os outros doze são defaults do cartão, raio de canto, e posição/trava por elemento); agora éfields/kpi/card.tsmaisfields/kpi/format.ts. -
src/page/guardaunits.ts,sizes.ts(erapageSizes.ts) ezones.ts. Isto reverte uma decisão tomada antes nesta mesma seção: eles estavam listados como "deliberadamente não movidos", pelo argumento de que uma pastautil/seria gaveta de tranqueira. O argumento estava certo sobreutil/e errado sobre estes três — eles não são utilitários, são a folha em si: o tamanho dela, como converte entre mm/px/pt, e em que faixa um campo cai.page/é um assunto, não uma gaveta, e a diferença é toda aí. -
src/template.tssubstitui osrc/template/migrate.ts— uma pasta com exatamente um arquivo dentro, um nível de diretório que não carrega informação nenhuma. -
src/i18n/locales/en.ts,locales/pt-BR.ts. Os dois dicionários eram 1115 das 1219 linhas da pasta, então os sete arquivos que têm a lógica de i18n de verdade ficavam soterrados por eles em qualquer listagem. Nada da fronteira sem-React mudou: odictionaries.tscontinua com o mapa locale →Dictsem importar React, que é o que deixa o/serveralcançar um dicionário, e otest/entryBoundaries.test.tscontinua guardando isso. -
src/expressions/engine/guardatokenize.ts,parse.ts,evaluate.ts,functions.tseformatters.ts. Esta foi medida, não olhada: dos doze arquivos deexpressions/, exatamente estes cinco têm zero importador fora da pasta, e os outros sete (resolve.ts,templateText.ts,schemaExpressions.ts,suggest.ts,suspicious.ts,errors.ts,dataAccess.ts) têm de dois a sete cada. A fronteira de pasta agora diz o que o grafo de import já dizia. -
src/components/PropertyPanel/esrc/components/formula/tiram onze arquivos de uma listagem plana de vinte. Os painéis seguem o formato que oFieldBox/já tinha estabelecido — pasta com o nome do componente, o componente noindex.tsx, as variantes por tipo do lado — entãoimport { PropertyPanel } from "../../components/PropertyPanel"resolve sem mudança. Oformula/é minúsculo porque é assunto e não componente: oFormulaModal.tsxé o editor, mas oFormulaButton.tsxé importado sozinho pelos painéis de propriedade, então batizar a pasta com o nome de qualquer um dos dois descreveria ela errado. -
O
test/acompanha osrc/onde tinha ficado atrás: otest/page/guarda os testes de units/sizes/zones, e otest/template/migrate.test.tsviroutest/template.test.ts. Otest/expressions/ficou plano de propósito — obrackets.test.tsexercitatokenize,parseeevaluatejuntos, então ele não tem lado nenhum de um splitengine/pra sentar, e forçar um poria a fronteira de pasta num lugar onde os testes de fato não se dividem.
Quatro comentários em designer/ apontavam pra src/bindingBuilders.ts e
src/canvasGeometry.ts — dois arquivos que já tinham parado de existir em
releases anteriores. Agora eles citam src/bindings/builders.ts e
src/canvas/geometry.ts. Comentário citando caminho que não resolve pra nada
é a mesma podridão de prosa velha, e é mais difícil de notar porque nada
compila ele.
Um guard pegou um movimento sozinho: o teste de "nenhum throw monta a
mensagem com dicionário" lê uma lista fixa de sete arquivos de fonte, e o
template/migrate.ts era um deles. Ele falhou com um ENOENT cru de dentro
do helper de leitura — falha de verdade, mas com uma mensagem que apontava pro
helper em vez de pra lista. Agora ele afirma primeiro que cada alvo existe,
então o próximo rename diz "um alvo deste guard mudou de caminho — atualize a
lista".
Ainda na raiz de src/, e por quê: três dos onze arquivos são os entry points
(index.ts, server.ts, preview.ts). Os outros oito — errors.ts,
errorUtils.ts, fieldWarnings.ts, materialIcons.ts, numberFormat.ts,
schemaFactory.ts, drag.ts, template.ts — têm importador espalhado por
três ou mais pastas cada, que é o que "mora na raiz" quer dizer. A nota
anterior sobre o errorUtils.ts continua valendo e vale repetir, porque o
raciocínio quase se inverteu por contagem de linha: ele foi considerado pra
fundir no errors.ts sob o argumento de ter oito linhas contra 639. O grafo de
importadores diz o contrário — o errorUtils.ts tem dois importadores e os
dois estão do lado React, o errors.ts tem doze e nenhum deles importa React.
Estão em lados opostos da fronteira do /server. Duas preocupações, dois
arquivos.
3.1.0 (2026-09-03)
Adicionado
-
useDesignerZoom()— o zoom do canvas passa a ser legível e gravável de fora do canvas. Relatado contra a 3.0.1: montando o editor comDesignerProvider+ peças soltas, não havia como ler o zoom atual, nem disparar zoom/fit/reset de uma barra própria, nem mover a barra de zoom pra fora do canvas — a.jpd-zoombarpadrão éposition: stickydentro do.jpd-designer__canvas, então CSS só a movia pelos cantos daquela caixa. A única alternativa era reimplementar zoom do zero, com uma segunda cópia do valor pra dessincronizar da folha que o canvas realmente renderiza.const { zoom, min, max, setZoom, zoomIn, zoomOut, reset, fitWidth, fitHeight } = useDesignerZoom();Mais o
viewportRef(o elemento que rola, preenchido peloDesignerCanvasmontado), o tipoDesignerZoomValue, eclampZoom/ZOOM_MIN/ZOOM_MAX/ZOOM_STEPpra um controle montado à mão não discordar do canvas sobre os limites. -
<DesignerCanvas hideZoombar />— tira o controle padrão e nada mais. A folha, as réguas e o zoom em si continuam funcionando. -
<PageCanvas zoom onChangeZoom hideZoombar />— o componente agora é controlado-ou-não no zoom, decidido pela presença da prop. Omitida, ele segue dono do próprio zoom exatamente como antes, que é o que o caminho headless usa.- Por que contexto separado, e não um dos cinco. A decisão original de
manter o zoom interno tinha argumento de verdade: arrastar um slider
re-renderizaria toda peça se o valor morasse no provider. Ela estava
certa sobre o problema e errada sobre a solução — o que cascateia é o
valor morar num contexto que todas as peças leem. O zoom ganhou contexto
próprio, então só quem chama
useDesignerZoom()re-renderiza: a sua barra e o canvas. A lista de campos, o inspetor e os painéis de propriedade ficam parados. É o mesmo argumento que já dera contexto próprio ao registry de primitivos. fitWidth()/fitHeight()medem o viewport DO CANVAS, não a janela, e é por isso que funcionam a partir de um botão que não está perto dele: oDesignerCanvasmontado se registra noviewportRef. Sem canvas montado eles não fazem nada, de propósito — o fallback antigo erawindow.innerWidth, e foi assim que "ajustar largura" já deu 113% com 338px de página fora da tela.- Montando um slider: use
step="any", nãostep={step}. Commin={0.25}estep={0.1}um input range prende na grade 0,25 / 0,35 / … / 1,05, então pedir 1 dá 105% e o slider nunca encosta em 100% exato. OZOOM_STEPé o incremento dos botões +/−; a escala é contínua. - O
examples/composed-layoutagora demonstra isso: a barra dele fica ao lado das abas de página, fora do canvas, com<DesignerCanvas hideZoombar />— slider de verdade, porcentagem ao vivo, e botões de ajustar/resetar.
- Por que contexto separado, e não um dos cinco. A decisão original de
manter o zoom interno tinha argumento de verdade: arrastar um slider
re-renderizaria toda peça se o valor morasse no provider. Ela estava
certa sobre o problema e errada sobre a solução — o que cascateia é o
valor morar num contexto que todas as peças leem. O zoom ganhou contexto
próprio, então só quem chama
Corrigido
clampZoom(NaN)devolviaNaN, e isso chegava emtransform: scale(NaN)— a folha desaparecia sem nada no console e sem nada no DOM parecendo errado.Math.max(0.25, NaN)éNaN, e oNaNsobrevive aoMath.minseguinte. Agora devolve 1 (100%), porqueNaNsignifica "não há valor" e 100% é a resposta menos surpreendente;Infinitycontinua clampando pro máximo, já que ali existe valor. Alcançável de dois jeitos:fitWidth()sobre uma página cujowidthéNaN, e uma barra própria passandoNumber("")de um campo esvaziado.- A guarda do
fitWidth()usavapaginaPx <= 0, eNaN <= 0éfalse, então um tamanho de páginaNaNatravessava a checagem e ia pra divisão. Agora testaNumber.isFinite. - O
test/publicSurface.test.tsguardava um terço da superfície. O inventário dele filtrava por PascalCase, então todo hook (camelCase), helper (camelCase) e constante (SCREAMING_CASE) ficava sem checagem — 104 exports de runtime sem guard nenhum, numa suíte que afirmava cobrir a API pública "nas duas direções".useDesignerZoom,clampZoome os trêsZOOM_*entraram na API pública com a suíte inteira verde, e foi assim que o buraco apareceu. Os 104 estão declarados agora, e o caso dos hooks checa as duas direções.
3.0.0 (2026-09-02)
Tira o Tailwind do pacote e quebra o <Designer> em peças que você mesmo
posiciona. As duas coisas são uma só, não duas: o preset só pôde virar peças
posicionáveis depois que a marcação deixou de carregar strings de utilitária
que dependiam de um build de Tailwind pra existir. No lugar entra um
theme.css escrito à mão sobre classes .jpd-* e custom properties
--jpd-*, um <DesignerProvider> com dez peças e dez hooks em volta, e um
registry que troca o Button/Input/Modal que o editor usa por dentro pelos
seus.
Breaking
-
json-pdf-designer/style.cssnão existe mais — importejson-pdf-designer/theme.css. As chaves./style.csse./dist/style.csssaíram doexports, e não há alias — então o import antigo agora falha ao resolver em tempo de build (ERR_PACKAGE_PATH_NOT_EXPORTED).- import "json-pdf-designer/style.css";+ import "json-pdf-designer/theme.css";Uma linha basta: o
theme.cssjá importa o reset por dentro.- Por quê: o pacote enviava Tailwind compilado, e isso incluía o Preflight — um reset global caindo no seu app, vindo de uma folha cujo único trabalho era pintar um editor.
- Por que não um alias: ele entregaria em silêncio uma folha diferente (sem Preflight, com internals novos), e você descobriria por uma tela renderizando errado. Erro de resolve é a falha melhor, porque acontece no build e aponta pra esta entrada.
- O Tailwind saiu do pacote inteiro:
tailwindcsse@tailwindcss/clifora dodevDependencies, scriptbuild:cssremovido,src/style.cssdeletado, enpm run buildagora é sótsup.theme.cssereset.csssão escritos à mão e não minificados, de propósito — são o contrato público que você lê pra aprender os nomes de classe e de token, e o seu bundler minifica de qualquer jeito.
-
O
theme.cssnão carrega reset global. Cada classe.jpd-*passa a carregar o reset de que o elemento dela dependia — o trabalho que o Preflight fazia. A única regra com*é escopada às raízes do próprio editor e envolta em:where(), o que lhe dá especificidade zero: qualquer regra sua ganha dela.- O que o Preflight lhe dava de graça, e pode agora estar faltando na
sua marcação:
box-sizing: border-box,margin/padding: 0em heading, parágrafo e lista,list-style: none,font/color/backgroundem controle de formulário,svg { display: block },codeem família monoespaçada,table { border-collapse: collapse }eappearance: button. Se o seu app se apoiava na folha do pacote pra isso, devolva o que você precisa. - Novo export
json-pdf-designer/reset.css— o subconjunto sem aparência, pra quem vai estilizar.jpd-*do zero e não quer levar o tema. Importar os dois é redundante, não é erro. - A armadilha que quase todo mundo erra devolvendo reset à mão:
border: 0põeborder-style: none, e aí qualquerborder-widthposterior computa zero. Tem de serborder: 0 solid. O Preflight escrevia assim de propósito; as nossas duas folhas também.
- O que o Preflight lhe dava de graça, e pode agora estar faltando na
sua marcação:
-
Dark mode agora tem um hook documentado:
[data-jpd-theme="dark"]. Ponha no<html>ou em qualquer ancestral. A.darkcontinua funcionando, mantida como alias do que a 2.x usava (@custom-variant dark (&:where(.dark, .dark *))) — verificado no navegador que as duas pintam igual.[data-jpd-theme="light"]força claro.- Sem media query, de propósito: biblioteca não deve virar o seu app
light-only escuro porque o sistema operacional está. Pra seguir o SO,
leia
matchMediae escreva o atributo você mesmo. - Limitação que vale saber: uma ilha de dark escopada — o atributo
num container em vez de no
<html>— não pinta modal portalizado. O<Modal>renderiza porcreatePortal(document.body), que é exatamente por que os tokens moram no:root.
- Sem media query, de propósito: biblioteca não deve virar o seu app
light-only escuro porque o sistema operacional está. Pra seguir o SO,
leia
-
Toda classe dentro do
<Designer>mudou. A marcação agora é.jpd-*maisdata-*, na convençãojpd-block__element--modifiercom UM nível de elemento; estado é atributodata-*, nunca classe (a regra: se o JSX teria de concatenar ou escolher uma string de classe, édata-*). O CSS que você escreveu contra os nomes de classe Tailwind antigos para de casar.- Verificado nos examples deste próprio repo: o
examples/custom-uiestiliza só os containers em volta do editor, sem nenhum seletor descendente alcançando a marcação de dentro — então nenhum consumidor in-repo quebrou. Essa também é a forma que continua funcionando: estilize a sua casca, e mire.jpd-*de propósito quando quiser redefinir o editor.
- Verificado nos examples deste próprio repo: o
-
Modal:maxWidthClass→size. A prop antiga recebia uma string de classe Tailwind, que não sobrevive num pacote sem Tailwind dentro.- <Modal title="Campos" maxWidthClass="max-w-3xl" onClose={fechar}>+ <Modal title="Campos" size="lg" onClose={fechar}>sizeé"sm" | "md" | "lg" | "xl" | "full", com default"lg"= 48rem, que é o que omax-w-3xldava. Largura arbitrária éstyle={{ maxWidth: 900 }}, e ostyleagora chega no painel.- Nenhum call site interno passava
maxWidthClass, e ele não estava documentado em doc nenhum — vivia só no código — então a remoção não invalida nenhuma prosa que a gente escreveu.
- Nenhum call site interno passava
-
classNamedeixa de concatenar e passa a fazer merge. Ocxdeduplica por token exato e devolveundefined(não"") quando não sobra nada, então componentes que emitiamclass="… "com espaço sobrando não emitem mais — oCardera o pior deles, acrescentando um espaço até semclassNamenenhum. Se você faz asserção em cima da marcação, aquele espaço acabou.- A ordem dos tokens dentro do atributo
classnão decide mais nada. Ela nunca decidiu cascata nem com Tailwind, mas sem utilitária nenhuma sobrando não há o que decidir:.jpd-btne a sua classe têm a mesma especificidade, então quem ganha é a ordem na FOLHA DE ESTILO. Quem precisa vencer otheme.csscarrega o CSS depois dele, ou usastyle, que ganha sempre.
- A ordem dos tokens dentro do atributo
-
Os componentes do kit repassam
ref, então não são mais funções simples.Button,Input,Select,Textarea,Modal,Card,TabPanele os outros são componentesforwardRef:Button.namedeixa de ser"Button", e chamarButton(props)direto para de funcionar. ÉforwardRefe não a proprefdo React 19 porque o React 18 continua peer suportado.- Os 20 ícones são a exceção. Continuam funções simples, sem ref, e as
props deles alargaram de
{ className }praIconProps=SVGAttributes. De propósito nãoSVGProps, que estendeClassAttributese aceitaria umrefque não vai a lugar nenhum — o tipo mentiria.
- Os 20 ícones são a exceção. Continuam funções simples, sem ref, e as
props deles alargaram de
-
mononão adiciona maisfont-mono, e nenhum componente emite classe Tailwind. NoInpute noTextarea,monovirou um atributodata-monoque otheme.cssestiliza. OPaletteSwatchese oPalettePickerperderam toda prop cujo valor era classe Tailwind (size = "h-4 w-4",swatchSize,swatchGap,swatchShrink); como a API limpa é a que estreia pública, não há nota de migração pra elas.- Isto aposenta o workflow "Using your own Tailwind installation". Não
há mais utilitária no
distpra escanear, então toda receita de@sourceoucontentapontando pro pacote é agora conselho ativamente prejudicial: silenciosamente não produz nada. Traga o seu Tailwind, sim — só aponte pros seus próprios arquivos e estilize.jpd-*com ele.
- Isto aposenta o workflow "Using your own Tailwind installation". Não
há mais utilitária no
-
A estrutura de DOM dentro do
<Designer>também mudou, não só os nomes de classe: cada peça renderiza o wrapper dela, com uma classejpd-*na raiz. Se você alcançava a marcação do editor com seletor descendente, ou com variante arbitrária do Tailwind tipo[&_.foo]:…num container, isso pode quebrar. Nada neste repo fazia — a busca por variante arbitrária emexamples/na 2.1.1 volta vazia. -
Duplo-clique-pra-colapsar é afordância do
DesignerSidebar, não de cada peça. Dentro do<Designer>é idêntico: duplo clique na aba ativa e o painel colapsa. ODesignerTabBarESCREVE o flag; oDesignerSidebaré quem LÊ e colapsa. Então um<DesignerTabBar>avulso dá um duplo clique sem efeito visível, que é o comportamento certo pra quem não tem o que colapsar.- Por que não por peça: o truque do
TabPanelé um grid1fr→0fr, e isso exige um paiflex columncommin-block-size: 0. Peça avulsa não garante esse pai, então colapso por peça animaria errado, em silêncio, em vez de degradar.
- Por que não por peça: o truque do
-
sidebarTabnão gateia mais um painel a menos que você peça. O gate por aba é opt-in, porwhenTab: semwhenTab, a peça renderiza sempre; comwhenTab="pagina"(ou um array de abas), ela só aparece ali. É assim que oDesignerSidebare o<Designer>reproduzem o comportamento de hoje, então dentro do preset nada mudou.- Por quê: se o gate fosse o default, pôr
DesignerPropertyPaneleDesignerPageSettingslado a lado no seu layout apagaria um dos dois, porque só uma aba pode estar ativa. Seriam peças que parecem decompostas mas só funcionam dentro de uma sidebar com abas, o que anula a feature inteira.
- Por quê: se o gate fosse o default, pôr
-
O
PdfPreviewModalnão herda primitivos de um<Designer>em outro lugar da página. Ele vive no entryjson-pdf-designer/previewe resolve doUiComponentsProvidermais próximo — e o provider do<Designer>embrulha só a subárvore do próprio<Designer>. Se você quer o seu Button dentro do modal de preview, ponha um<UiComponentsProvider>acima do modal também (a mesma constante de módulo serve). -
O
generatePdfagora lança erros tipados, e oerror.messageestá em inglês. É a única quebra desta lista que não tem nada a ver com CSS nem com marcação — ela alcança código que nunca importa folha de estilo, inclusive um backend usando o entryjson-pdf-designer/server.Até a 2.1.1 toda falha era um
Errorcomum, e a mensagem dele era português cravado — pra todo mundo, inclusive pra quem rodava o editor em inglês. O único jeito de distinguir uma falha da outra era casar essa frase:- // 2.1.1 — casando uma frase em português, num app que pode estar em inglês- if (/tamanho inválido/.test(err.message)) mostrarAjudaDeTamanho();- else if (/Paginação travada/.test(err.message)) reportarBug(err);+ // 3.0.0 — casando uma classe+ if (err instanceof InvalidPageSizeError) mostrarAjudaDeTamanho(err.pageId);+ else if (err instanceof PaginationStalledError) reportarBug(err);São 18 classes concretas sob uma base abstrata (
PdfGenerationError, abstrata de propósito — ninguém deve lançar a base). Cada uma carrega umcodeliteral e os campos estruturados que aquela falha realmente sabe, pra nada precisar ser extraído de volta de uma frase:class InvalidPageSizeError extends PdfGenerationError {readonly code = "invalidPageSize";readonly blame = "template";constructor(readonly pageId: string, readonly width: number, readonly height: number) { … }}-
Por que o
messageem inglês: ele é a camada de diagnóstico — o console, seu rastreador de erro, a string que alguém cola num issue. Mensagem que muda de idioma faz o agrupamento de log depender do idioma de quem leu, e deixa stack trace impossível de buscar. -
Isto ACRESCENTA localização, não tira. O texto que uma pessoa lê passa a sair do mesmo dicionário que você já entrega ao editor:
const problem = describePdfError(err, dictFor(locale));// → { code, blame, title, action?, field?, detail } | nulltitleeactionvêm nolocale;detailé a mensagem crua em inglês. Ou seja quem usa em inglês para de ver português, e trocar olocaleretraduz. -
A armadilha, e é a razão inteira de
describePdfErrorser uma chamada separada: nunca guarde o texto descrito em estado. Guarde o erro cru e descreva na renderização. Frase traduzida guardada em estado congela no idioma em que foi criada, então o seletor de idioma deixa aquele pedaço da tela atrás — e nada falha, só renderiza velho. Batemos nisso treze vezes nos examples deste próprio repo enquanto os migrávamos, e é por isso que os cinco agora guardam um motivo e resolvem o texto no render.- catch (err) { setErro(describePdfError(err, dictFor(locale))); }+ catch (err) { setCaixaDeErro({ err }); } // o erro cru+ // …e no corpo do render:+ const problem = caixaDeErro && describePdfError(caixaDeErro.err, dictFor(locale)); -
O
describePdfErrordevolvenullpra erro que não é do pacote, de propósito, em vez de inventar um título pra uma falha que ele não entende. Os seus erros ficam com a sua cópia — vejaexamples/*/src/lib/generationError.tspra forma. -
blameé"data" | "template" | "config" | "package"— a mesma distinção que um backend usa pra escolher entre 4xx e 500, então ela não devia existir em duas versões.blame: "package"quer dizer que o bug é nosso, e não do template de quem chamou. -
Tudo acima é exportado dos dois entries —
json-pdf-designerejson-pdf-designer/server— então um servidor classifica a falha sem puxar o React.
-
A @layer da cascata, e por que não é regressão
O theme.css mora dentro de uma @layer json-pdf-designer. É isso que faz
o className que você passa sempre valer: CSS SEM layer ganha de CSS COM
layer independente de especificidade, então qualquer regra sua vence a nossa
por default, sem briga de especificidade. A ordem de layer é declarada antes
da primeira regra (@layer json-pdf-designer, utilities;), então vale não
importa qual folha você importa primeiro.
O efeito colateral é que um seletor de elemento solto — button { … } no
seu CSS global — também ganha, e alcança o chrome do editor. Isto não é
novidade da 3.0.0. Foi medido: o dist/style.css da 2.x era output do
Tailwind v4, e o Tailwind v4 também emite @layer theme / @layer base /
@layer utilities, então um seletor de elemento sem layer já vencia lá. A
@layer preserva a posição de cascata da 2.1.1 em vez de mudá-la. O
conselho é o mesmo de sempre: escope por classe, e mire .jpd-* de
propósito quando a intenção for redefinir o editor.
Adicionado
<DesignerProvider>— o provider de estado, e o que torna o resto desta lista possível. Props:template,onChangeTemplate,bindings,onChangeBindings,onCanvasDrop,dataSources,gridSizeMm,expandOnSelect,children. ODesigner.tsxfoi de 986 linhas pra 101, porque é só isso que o preset é agora: três providers e duas peças num layout de duas colunas.- Dez peças posicionáveis:
DesignerCanvas,DesignerTabBar,DesignerFieldList,DesignerToolbar,DesignerPageSettings,DesignerPropertyPanel,DesignerFilterPanel,DesignerBindingEditor,DesignerInspectoreDesignerSidebar— a última uma conveniência que compõe as sete peças de conteúdo com o gate de aba do preset.- Toda peça aceita
className(merge, a sua vem depois),style(o seu ganha) ewhenTab. Várias aceitamparts, que endereça os elementos de dentro por papel, mais flags comoheading,hint,headereposition. - O
DesignerPropertyPanelrecebesection="dados" | "estilo"como prop, em vez de ler a aba ativa — então duas instâncias com sections diferentes renderizam juntas. - Nunca posicionáveis, de propósito:
FieldBox/*e os painéisPropertyPanel{Text,Table,…}, que são despachados porschema.type. Um<DesignerTextPanel/>avulso não tem resposta pra "qual schema?" que não seja "o selecionado", e aí ele é só umDesignerPropertyPanelpior.
- Toda peça aceita
- Dez hooks públicos. Acessores
useDesignerData,useDesignerActions,useDesignerSelection,useDesignerUi,useDesignerConfig; seletoresuseDesignerSelectedSchema,useDesignerFieldListSchemas,useDesignerBulkEdit,useDesignerTabWarnings,useDesignerFilterColumns.- Os cinco contextos são divididos por frequência de mudança, então
cada peça lê só o que usa. O
useDesignerActionsnunca troca de identidade — é o load-bearing, o motivo pelo qual uma peça memoizada consegue segurar um mutador sem re-renderizar a cada mudança de template — enquanto ouseDesignerDatamuda a cada edição. - Usados fora do provider, eles lançam, com mensagem nomeando o
provider, em vez de devolver
null. Peça sem estado não tem fallback nenhum, então umnullsilencioso deixaria você olhando um buraco na tela.
- Os cinco contextos são divididos por frequência de mudança, então
cada peça lê só o que usa. O
UiComponentsProvider/useUiComponents/defaultUiComponents— troca os primitivos que o editor usa por dentro. Doze slots:Button,Input,ColorInput,Select,Textarea,Checkbox,Modal,Card,CardHeader,CardTitle,Badge,TabPanel. Todo tipo*Propsé exportado pra que um adapter seja umas cinco linhas, tipado nas duas pontas, e o exemplo escreve comsatisfies UiComponentsOverride. O<Designer components={…}>é açúcar pra montar o provider.undefinednuma chave significa herda do provider de cima, não volta-ao-default. Pra ter o nosso de volta, nomeie:{ Modal: defaultUiComponents.Modal }.- A pegadinha pra ler antes de escrever um adapter: o slot recebe as
props como o chamador as escreveu, e os defaults moram dentro dos
nossos componentes. O
<Button>desestrutura{ variant = "primary", size = "sm" }na assinatura dele, então um chamador que escreve<Button>ok</Button>mandavariant: undefinedpro seu adapter — medido: cinco dos seis botões da Toolbar chegam semvariant. O seu adapter precisa dos próprios defaults. Os do editor são Button primary/sm, Modallg, eTabPanel.collapsedsem default. - Identidade instável é a aresta mais afiada. Objeto inline cria um tipo de componente novo a cada render e o React remonta o que você slotou — o sintoma é o campo perder o foco a cada tecla. Hoiste o mapa pra constante de módulo. Fora de produção o provider avisa no console, uma vez.
components={{ Button: MuiButton }}não type-checka direto (ovariantdo MUI conflita com o nosso), e nenhum truque resolve — o adapter é a resposta.labelé a prop que morde: um slot que descartalabelremove o nome acessível de uns dezesseis controles. OTextFielddo MUI temlabelcompatível; oInputdo Chakra não tem. E oTextareaé o único slot em que ignorar arefquebra algo: o modal de fórmula passa uma ref pra reposicionar o caret depois que você aceita uma sugestão.- Não slotáveis:
PageCanvas/Ruler/FieldBox(são a maquete do PDF — padding de primitivo estranho quebra o WYSIWYG), a barra de abas (a 2.1.1 inteira foi gasta encaixando seis abas em 290px; omin-widthde um Button slotado desfaz isso), o input de rename doFieldList(que depende deautoFocusmais commit noonBlur), o input de arquivo escondido, o glifo "ƒx" e os ícones.
Checkbox— fecha a lacuna de três<input type="checkbox">crus no editor, e é o décimo segundo slot.- Componentes do kit agora públicos:
PalettePicker,PaletteSwatches,MaterialIcon,CollapsibleSection,ClearFieldButton. Todos eles — e tudo que já era público — aceitamclassName,stylee...rest, sob uma regra:className/style/...restvão pro elemento que dá NOME ao componente; todo outro elemento que ele renderiza é endereçado porparts, por papel. EntãoInput.classNamecontinua no<input>(igual à 2.x) e o<label>que embrulha éparts.root;Modal.classNameé o painel e o fundo escurecido éparts.overlay. Opartsaceita sóclassName/style— sem handler, sem ref — com atalho de string (parts={{ label: "minha-classe" }}) e forma de objeto quando você precisa destyle. OmergeStyledá a vitória ao seustyle.- O
BulkLockedfica interno de propósito: ele significa "travado porque você selecionou vários do mesmo tipo", que é um modo do<Designer>. Fora daquele contexto o componente não quer dizer nada.
- O
- Uma superfície de erro tipada:
PdfGenerationError(abstrata) mais 18 classes concretas,PDF_ERROR_CODES(os 18 códigos como tuplaconst),PdfErrorCode,PdfErrorBlame,AnyPdfErroreisPdfError(err), que estreitaunknownpraAnyPdfError. Tudo dos dois entries. O porquê de as mensagens antigas deErrorterem que sair está na seção Breaking. describePdfError(err, dict)→PdfProblem | null— uma chamada que transforma um erro capturado em{ code, blame, title, action?, field?, detail }com a cópia já no idioma do dicionário. Use junto comdictFor(locale), também exportado dos dois entries.- Ela é exaustiva, então "caiu no ramo genérico porque eu não reconheci"
deixou de ser um estado possível do pacote — e um código novo em
PDF_ERROR_CODESpara de compilar até ter cópia nos dois idiomas. - O
PdfProblemCodetem 19 membros enquanto oPDF_ERROR_CODEStem 18, e o extra não é descuido:"expression"cobre oExpressionError, que é uma hierarquia própria em vez de uma das 18 (ele já carrega umlocalize(dict), então descrevê-lo de novo aqui daria duas frases diferentes pra mesma falha). É também o código de problema que você dificilmente verá vindo dogeneratePdf— a geração é tolerante de propósito, e expressão inválida renderiza como campo vazio. Ele aparece quando você chama a API estrita, por exemplo oparsenum backend que valida o template antes de salvar.
- Ela é exaustiva, então "caiu no ramo genérico porque eu não reconheci"
deixou de ser um estado possível do pacote — e um código novo em
json-pdf-designer/reset.csscomo export próprio — ver a entrada de Breaking acima.- Props novas do
<Designer>, todas opcionais:gridSizeMm,expandOnSelect,className,style,components. As sete props que ele sempre teve seguem inalteradas. examples/composed-layout(porta 5177) — o app que só funciona porque o gate de aba é opt-in: toolbar full-width em cima, lista de campos à esquerda, canvas no meio, e uma coluna à direita com cinco painéis empilhados que dentro do<Designer>seriam abas diferentes. Nenhuma peça recebewhenTab. Medido: oito peças no DOM ao mesmo tempo, e zero.jpd-tabs.- O
examples/report-buildermigrou pras peças com um layout idêntico, e o que a migração comprou foi o<SelectedFieldBar>— a casca do próprio app lendo a seleção do editor por hook, o que era impossível na 2.x (o comentário antigo emApp.tsxdizia exatamente isso: o<Designer>era dono da seleção e não havia prop pra dirigi-la de fora). Verificado: "remover" na barra do próprio app leva o template de três campos pra dois, e o Ctrl+Z do app desfaz (dois de volta pra três) — ou seja o mutador do pacote passa pelosetStatedo consumidor, e o undo/redo dele vê. - O
examples/custom-uifica no<Designer>de propósito: é o teste de regressão do caminho de uma linha, e não importa nenhum CSS do pacote, estilizando.jpd-*do zero — esse é o modo SEM FOLHA DE ESTILO do pacote. Oexamples/no-previewé outra coisa: ele importa otheme.cssnormalmente, e é o melhor smoke test da PERDA DO PREFLIGHT, porque não tem pipeline de Tailwind em ponta nenhuma — oreport-buildertem o próprio, então emite Preflight por conta e não reproduz uma perda de reset. Testar nele primeiro dá falso verde.
- O
Corrigido
- O
aria-labeldo botão de fechar doModalera o título do diálogo. Leitor de tela anunciava "Editor de fórmula" como o nome do botão que fecha o editor de fórmula. Agora tem nome próprio, traduzido (t.modal.close). - O
fitTodo canvas nunca usou o viewport do canvas. Ele procurava por[data-scroll-root], [class*="overflow-auto"], [class*="overflow-y-auto"]— mas odata-scroll-rootnão era escrito em lugar nenhum, e o wrapper usavaoverflow-x-auto, que não contém nenhuma das duas strings como substring. Medido na 2.1.1: "ajustar largura" dava 113% com 338px de página fora da viewport. Com o atributo no lugar: 67%, e zero overflow. - A régua não tinha dark mode. O fundo escurecia
(
dark:bg-gray-800), mas o tick e o número eram atributos SVG de apresentação com hex fixo (stroke="#94a3b8",fill="#64748b") — tick escuro sobre fundo escuro. Os três vêm de token agora (--jpd-ruler-bg,--jpd-ruler-tick,--jpd-ruler-label), então andam juntos. - A faixa de abas do modal de fórmula transbordava, só em pt-BR. Medido
no
examples/no-previewnum viewport de 1280px: "Campos do item" (96,7px) mais "Caminhos de dado" (108,8px) dá 205,4px dentro de uma coluna de 176px, e a faixa tinhaoverflow: visible— então os botões pintavam por cima da coluna do editor ao lado. Em inglês as mesmas duas abas ("Item fields" / "Data paths") somam 151px e cabem com folga, e é por isso que o bug só existia traduzido. A faixa agora éjpd-tabs jpd-tabs--scroll(overflow-x: automaismin-inline-size: 0), então ela medescrollWidth207 contraclientWidth176 e rola em vez de cortar.- Só aquela instância recebe o modificador. O
.jpd-tabsdo painel lateral não pode receberoverflow, porque cortaria o popover do.jpd-tabs__more.
- Só aquela instância recebe o modificador. O
- Os ícones perdiam o
display: block. Sem Preflight, todo<svg>inline volta pra inline-baseline e ganha vão de descendente dentro de uma linha flex — uns 23 call sites, incluindo o<IconX/>dentro dos botões do próprio kit. Corrigido no base deicons.tsx, onde ojpd-iconagora entra em todo ícone em vez de em cada call site. - A borda do input de rename não tinha cor de dark. O input inline do
FieldListcarregavaborder-sky-400sem nenhumdark:border-*— sky-400 sobre gray-800. - A seta do seletor de paleta da tabela não tinha cor de dark. Ela era
preservada por uma prop
staticArrowque existia só pra manter vivo um detalhe não intencional. A prop morreu e a seta segue o tema. - O
gridSizeMmnão era honrado nem pelo colar nem pelo nascimento de campo. OPageCanvasjá recebia a prop, mas ocomputeSpawnPosition, onextFreeYe o caminho do colar usavam a constante direto: comgridSizeMm={3}, o arrasto alinhava em 3 enquanto nascer e colar alinhavam em 5, então campo novo já nascia fora da sua grade. Os quatro caminhos recebem o passo agora, e o<Designer gridSizeMm>existe. - O
.jpd-tabs__stripperdeu o::-webkit-scrollbar { display: none }na migração — o.jpd-scroll-xantigo tinha as duas regras e só uma atravessou.scrollbar-widthsozinho não cobre Safari nem Chromium < 121, e numa faixa de 28px de altura a barra come metade da altura. - Template sem tamanho de página estourava no layout em vez de falhar como
problema de template. O
assertFinitePageSizemorava dentro do render, que roda depois dolayoutDocument— e o layout lê o tamanho direto (pageDef.page.height - footerHeight). Então um template cujopagenão existia — JSON editado à mão, arquivo de outra ferramenta, formato que a migração não cobre — lançavaTypeError: Cannot read properties of undefined (reading 'height')de dentro do layout, antes de o guard estar na pilha. O guard estourava justamente na entrada que ele existe pra recusar.- A mensagem feia não era o custo real. Um
TypeErrornão é erro nosso, entãodescribePdfErrordevolvenullpra ele, e o consumidor classifica a falha comoblame: "package"— "não é culpa sua, reporte" — quando o problema era o template dele. É a confusão que a superfície de erro tipada existe pra acabar. - Agora o tamanho de toda página é conferido de antemão, antes do layout,
sobre a lista NORMALIZADA de páginas (
pagesé opcional; ausente, os campos planos são a página implícita, que é o template mais comum que existe). Conferir todas de uma vez também significa que quem carrega um arquivo descobre que a página 7 está errada sem esperar seis páginas boas renderizarem.
- A mensagem feia não era o custo real. Um
- O texto sobre a folha não tinha cor própria, então no dark ele ficava
ilegível. O
.jpd-pageé a maquete do PDF, e a folha é sempre papel branco — o PDF não tem dark mode. Só que ele declarava apenasbackground-color, então todo texto de campo sem cor própria herdava o--jpd-textdo tema: cinza-claro sobre branco. Medido noexamples/no-preview, as células vazias—de uma tabela davam 1.54:1 — texto que existe e não se lê. Agora o.jpd-pagetambém declaracolor: var(--jpd-canvas-ink)(17,75:1), um token que de propósito não tem versão dark. Campo comfontColorno schema continua sobrescrevendo inline, como sempre. - O gatilho do seletor de paleta caía na cor de botão do sistema. Ele é
um
<button>que tem classe, então o reset por elemento (:not([class])) não o alcança — de propósito, pra o reset nunca repintar o botão do próprio consumidor — e ele também não estava na lista por classe doreset.css. Sembackground-colorpróprio ele caía noButtonFacedo sistema, que comcolor-scheme: darké cinza-escuro: o rótulo dava 3,45:1 e a seta 1,25:1. Agora ele declarabackground-color: var(--jpd-surface-field), como todo outro controle. - O
reset.csslia três tokens que só otheme.cssdeclara.--jpd-font-sans,--jpd-font-monoe--jpd-text-placeholder, todos sem fallback — e como oreset.cssé export público por si, usado avulso cadavar()caía em nada e a regra morria calada. O pior dos três era o<code>dowithInlineCode, que sai sem classe e cuja monoespaçada vinha só do Preflight.
Não quebra, e vale dizer em voz alta
- As sete props que o
<Designer>sempre teve são idênticas:template,onChangeTemplate,bindings,onChangeBindings,onCanvasDrop,dataSources,locale. Tudo o que é novo nele é opcional. - As chaves de localStorage de ordem e visibilidade de aba são
preservadas —
json-pdf-designer:tab-orderejson-pdf-designer:hidden-tabs. O layout de abas do usuário sobrevive ao upgrade; verificado no navegador. PageCanvas,Toolbar,FieldList,PropertyPanel,BindingEditor,FilterTabeTemplateInspectormantêm as props de hoje e continuam exportados dos módulos deles, então o caminho headless por props segue funcionando exatamente como funcionava.- O
react-rndcontinua só necessário pro canvas. A peça é dona da geometria da folha (mm→px,transform: scale(zoom)) porque o react-rnd calcula o delta de arrasto contra esse transform — sobrescreva e o campo foge do cursor. Você é dono do viewport que rola, que é o que oclassNameatinge, e da largura da sidebar (os 320px moram no CSS do preset, não na peça). O zoom continua estado interno do<PageCanvas>e nunca subiu pro contexto. - O
server.tsestá intocado —server.d.tsbyte-idêntico no build. O install de backend sem React é exatamente o que era na 2.0.0.
2.1.1 (2026-09-01)
Corrigido
- A barra de abas do painel lateral rola, em vez de cortar uma aba. Com as
seis abas à mostra (Campos/Dados/Estilo/Filtro/Página/Inspetor) a barra
precisava de mais que os 320px do painel — medido: 335px de abas em 290px de
espaço em português, com o "Inspetor" cortado em 45px, e o "×" de fixar (que
só a aba ativa desenha) respondendo pelos últimos 12px. Agora a faixa rola na
horizontal, sem barra de rolagem visível (ela comeria metade de uma faixa de
30px de altura) e com a aba ativa trazida pra vista sempre que muda —
inclusive quando quem muda é outra coisa, como selecionar um campo, que troca
pra "Dados" por conta própria. O "+" que reabre aba escondida fica fora da
faixa que rola, pra não ser a primeira coisa a sair de alcance.
- Alargar o painel conforme a contagem de abas era a outra saída, e é pior: Dados/Estilo/Filtro aparecem e desaparecem conforme o tipo do campo selecionado, então o canvas pularia a cada troca de seleção.
2.1.0 (2026-09-01)
Fase 1 da roadmap pós-2.0.0: a fundação de que o resto depende — versionamento do formato de template, depois AST de expressões, depois uma passada de layout unificada. Em cima dela, o que a AST destravou: visibilidade condicional, uma superfície de falha tipada, e um editor de expressão com lista de campos e autocomplete.
Adicionado
-
Editor de expressão (
ƒx) — o botãoƒxagora abre uma janela com os campos a que o schema está vinculado à esquerda e um editor multilinha no centro, com autocomplete das funções e operadores do formato mais validação ao vivo. O editor guarda o valor do campo em si, com as chaves, aberto já com o que estava lá — a edição é no lugar, então prefixo literal (oFAT-deFAT-{fatura}) fica onde está. As sugestões aparecem só dentro das chaves; fora delas é texto literal.- Chave desbalanceada agora é acusada — nada mais fazia isso. O
resolvedor de template casa
/\{([^{}]+)\}/g, então uma{sem par simplesmente não casa e aquele trecho sai como texto literal no PDF ({CURRENCY(totalimpresso na cara). Não é erro de sintaxe de expressão (o parser nunca vê) nem falha de geração; era um campo saindo errado em silêncio.braceError(template, t?)etokenAtCaret(template, caret)são exportados pra quem monta editor próprio. - Disponível em quatro alvos: fórmula de coluna de tabela, cada célula da linha de totais, título/valor/legenda do KPI e conteúdo de um campo de texto. Antes disso só a fórmula de coluna tinha algo, e era um campo de uma linha numa sidebar de 320px.
- A lista de campos tem dois grupos, porque resolvem em escopos diferentes:
campos de cada item do array vinculado (
total, sem prefixo — o que funciona dentro de uma linha) e os caminhos completos (faturas.total— o que uma agregação precisa). Confundir um com o outro era dos erros mais fáceis de cometer. - O autocomplete oferece as 11 funções com a dica de cada uma mais
AND/OR/NOT, e põe os espaços que um operador precisa em volta — então não consegue produzir o operador de um lado só descrito abaixo. - Erro de sintaxe bloqueia o salvar; operador suspeito só avisa (chave JSON
com
/no nome é uso legítimo). - O
Modalentra na lista de componentes de UI exportados.
- Chave desbalanceada agora é acusada — nada mais fazia isso. O
resolvedor de template casa
-
suspiciousOperator(source)/templateSuspiciousOperators(template)— operador com espaço em exatamente um lado agora é apontado. Não é erro de sintaxe e não pode virar um: operador só é operador cercado de espaço dos dois lados, e é isso que mantém{my-key},{fatura/2}e{a==2}acessíveis como caminho de dado. Mas{fatura /}—/com}do lado direito — virava em silêncio a chave"fatura /", resolvia pra vazio, e nada acusava. Agora acusa.- A checagem roda sobre os tokens
ident, não sobre a string crua, e é isso que a deixa sem falso positivo: operador de verdade já é tokenop, texto entre aspas já é tokenstring, e sinal de negativo (-1depois de vírgula) já é tokennumber— nenhum deles chega na checagem. SchemaExpressionErrorganhouseverity: "error" | "warning". Erro significa que o campo renderiza vazio com certeza; aviso significa que compila mas quase certamente não é o que o autor quis. OfieldWarningreporta o erro primeiro.
- A checagem roda sobre os tokens
-
Visibilidade condicional (
schema.visibleWhen) — uma expressão, sem chaves, avaliada contra o JSON de verdade na hora de gerar; o campo só é desenhado quando ela é verdadeira.{ type: "text", content: "Desconto PJ", visibleWhen: 'cliente.tipo == "empresa"' }{ type: "table", visibleWhen: "NOT cancelado" }- Funciona em todo tipo de campo, inclusive tabela e seção repetida, e
nas faixas repetidas — a condição de um cabeçalho/rodapé pode usar
pageNumber/pageCount, então "só na última página" épageNumber == pageCount. - Fluxo: esconder um item devolve a altura dele e nada mais. O que vem depois sobe exatamente isso, e o espaçamento autorado nos dois lados continua valendo. Esconder um campo de uma linha que tem vizinhos visíveis deixa o buraco, porque a linha continua existindo pros outros; escondendo todos, a linha inteira sai.
- Ausente (ou em branco) = sempre visível, então nenhum template existente é afetado.
- Condição inválida conta como visível, nunca escondida: um erro de digitação não pode fazer um campo desaparecer do relatório em silêncio. O editor avisa (ver abaixo) e o campo continua aparecendo até alguém consertar.
- O
<Designer>tem um campo "Mostrar só quando" pra isso, ao lado de X/Y/largura/altura, com o erro de sintaxe aparecendo ao vivo embaixo.
- Funciona em todo tipo de campo, inclusive tabela e seção repetida, e
nas faixas repetidas — a condição de um cabeçalho/rodapé pode usar
-
AND/OR/NOTnas expressões —{a > 1 AND NOT cancelado}.ANDliga mais forte queOR, parênteses agrupam, e os dois curto-circuitam. Mesma regra lexical de todo operador: só cercado de espaço, então uma chave JSON chamadaANDcontinua acessível como{AND}. Case-insensitive, igual nome de função. -
A validação de expressão virou API pública —
expressionError(source),templateExpressionErrors(template)(nas duas entries) eexpressionErrors(schema, binding)/fieldWarning(schema, binding, t)(entry principal). Um backend pode recusar um template com expressão quebrada antes de salvar; uma UI de editor própria pode apontar do jeito que o<Designer>aponta.- O
tdofieldWarningagora é opcional (inglês por padrão), edictFor(locale)devolve um dicionário de tradução como valor comum. OfieldWarningera exportado mas exigia umDictque só saía douseT(), dentro de um componente React — então o lugar onde ele era mais útil, uma passagem de validação fora do editor, não conseguia chamar. Achado escrevendo essa passagem noexamples/report-builder.
- O
-
Template.versionemigrateTemplate()— umTemplateé um formato de documento, não uma estrutura interna: uma vez que ele vive num banco, sobrevive a qualquer versão deste pacote.versionacompanha o formato JSON (não o pacote npm),TemplateVersioné uma união de um só membro hoje pra que adicionar| 2faça o compilador apontar todo lugar que precisa decidir, emigrateTemplate(input)normaliza um template vindo de banco/arquivo/API pro formato que este build entende.versionausente é tratada como formato 1 — exato pra todo template salvo antes do campo existir, já que nenhuma mudança de forma aconteceu desde então.versionmaior do que este build entende estoura, em vez de gerar um PDF que descarta em silêncio campos que ele não conhece.- O
generatePdfchama internamente, no ponto único por onde todo template passa, então nunca se gera PDF de um template não migrado. - As migrações moram numa cadeia só (
src/template/migrate.ts), um degrau por versão — nuncaif (version === 1) … if (version === 2) …espalhado pelos chamadores. A cadeia está vazia hoje porque existe um formato; ela existe agora porque adicionar depois de haver template em banco de produção custa muito mais. - Exportado por
json-pdf-designere porjson-pdf-designer/server.
Corrigido
-
Editar a célula da tabela no canvas agora atualiza a fórmula da coluna também. A célula é a fórmula da coluna — o
generate.tsresolve a linha a partir deschema.content— e o painelƒxlêbinding.columns[i].formula. Editar peloƒxjá gravava nos dois; editar a célula direto gravava só o content, então o painel seguia mostrando a fórmula antiga: dois valores pra mesma coisa, com o que aparecia sendo o que não chega no PDF. Mesmas regras nos dois sentidos (célula esvaziada volta a ser a coluna crua). -
Fórmula de coluna de tabela não é mais acusada de expressão quebrada. O
bindingExpressionErrorsvalidavacolumn.formulacomo expressão pura, mas o render resolve comrenderTemplate— fórmula é template, então"FAT-{fatura}"está correto e estava sendo apontado. Dois dos templates de exemplo que o próprio pacote entrega acenderam por causa disso. Agora é validado como template, igual ao que roda de verdade. -
Expressão mal-formada não derruba mais o PDF inteiro. A geração voltou a ser tolerante: um
{...}inválido resolve pra vazio, então uma vírgula sobrando deixa aquele campo em branco em vez de fazer ogeneratePdffalhar e não sair documento nenhum. Esse era o raio de alcance antes da AST, e trocar "um campo em branco" por "nenhum relatório" não era melhoria.- O parser em si continua estrito — é ele que alimenta o aviso novo no campo
(
"Expressão inválida em \"content\" — renderiza vazio"), que é onde o problema deve aparecer: no editor, antes de gerar. CONCAT(a,)— vírgula sobrando antes do)— voltou a ser tolerado, como era antes da AST.
- O parser em si continua estrito — é ele que alimenta o aviso novo no campo
(
-
Relatório grande não é mais truncado em silêncio. Dois contadores de iteração (1000 fatias de tabela, 20000 repetições de seção) limitavam volume sem avisar: 60 mil linhas de tabela saíam como 40.998, e 20 mil repetições de seção saíam como 18.667, num PDF que parecia completo. Omitir linha de um relatório sem dizer nada é o pior resultado possível.
- Eles também protegiam de um laço que não pode acontecer: o da tabela sempre
quebra em
capacity <= 0, e o da seção sempre consegue colocar o item na página seguinte (numa página recém-aberta o cursor é o headerHeight, e aíneedsNewPageForItemé falso). Contar iteração era medir a coisa errada. - O limite agora é em página — o recurso escasso de verdade, venha a
página de tabela, de seção ou de várias páginas-design — vale 5000 por
default, é configurável com
generatePdf(..., { maxPages }), e estourá-lo lançaPageLimitErrornomeando o campo que estava sendo paginado e dizendo o que fazer. Volumes que antes eram cortados (60 mil linhas → 1464 páginas) agora saem completos. - Uma passada de paginação que não consome conteúdo nem abre página também estoura agora: isso é bug de aritmética do pacote, e girar até um contador esgotar escondia isso.
- Eles também protegiam de um laço que não pode acontecer: o da tabela sempre
quebra em
-
Os erros de geração são exportados como classes —
PageLimitError,UnsupportedGlyphError,ExpressionSyntaxError/ExpressionDepthError(sobExpressionError) eDEFAULT_MAX_PAGES, nas duas entries. Um backend responde 413 pra um e 400 pra outro sem precisar casar mensagem de erro. -
Caractere de controle no dado não derruba mais o PDF. Um
\nno nome de um cliente — de um textarea, de um endereço com quebra, de um import de CSV — fazia o documento inteiro falhar comWinAnsi cannot encode "\n". Caracteres de controle (C0, DEL, C1) agora viram espaço antes de medir ou desenhar, em todo caminho que chega ao papel: campo de texto, célula de tabela, KPI, rótulo de gráfico e as faixas repetidas. Eles não têm glifo em fonte nenhuma, nem numa Unicode completa passada emfontBytes— então não é perda de conteúdo, é a única renderização possível. Era o crash de maior impacto do pacote: o gatilho é o dado, que quem montou o template não controla. -
Glifo ausente agora diz qual campo e qual caractere. Emoji/CJK sem fonte customizada continua falhando — descartar caractere de um documento que alguém assina seria pior — mas o erro agora é
Campo "cliente_nome": o caractere "🎉" (U+1F389) não existe na fonte usada …em vez doWinAnsi cannot encodecru do pdf-lib, que não nomeava nada. Cobre os cinco caminhos (texto, célula de tabela, KPI, rótulo de gráfico e tabela aninhada em seção repetida), e um emoji fora do BMP é reportado como um caractere, não como duas metades de par surrogate. -
Fundo de página corrompido lança um
Errorde verdade. O pdf-lib/pako lançava uma string crua ali ("The input is not a PNG file!"), então ocatch (e) { e.message }de quem chama recebiaundefined. Mesmo tratamento que o campo de imagem já tinha. -
Vínculo de imagem agora chega ao PDF. O
drawImageFieldliaschema.contente ignorava o valor resolvido, então o editor deixava vincular um campo de imagem ao JSON e o gerador desenhava sempre a imagem de design. Valor vinculado que não é data URI (path errado, URL http, texto solto) deixa o campo vazio em vez de falhar. -
Número degenerado não produz mais um
TypeErroropaco do pdf-lib.fontSizeNaN cai num default (uma medida perdida não pode custar o documento); tamanho de página inválido falha comPágina "…": tamanho inválido (width=NaN, …)— é estrutural, e não há default sensato pra adivinhar. -
Expressão aninhada demais também não derruba mais o PDF. O guarda de profundidade lançava um
Errorcru, e a camada tolerante só pegavaExpressionSyntaxError— então o único caso pra que o guarda existe (template malformado ou malicioso) era justamente o que a tolerância não cobria. Os erros agora são uma hierarquia pequena de classes (ExpressionError→ExpressionSyntaxError/ExpressionDepthError), então a camada tolerante pega os dois por TIPO em vez de casar a mensagem de erro com regex, e o que não é problema de template continua subindo.- A garantia original está intacta e continua testada: aninhamento excessivo
é um erro claro e limitado, não um stack overflow do V8. Ela agora mora na
API estrita (
parse,expressionError), enquanto a geração renderiza o campo vazio e o editor aponta — a mesma troca que os erros de sintaxe fazem.
- A garantia original está intacta e continua testada: aninhamento excessivo
é um erro claro e limitado, não um stack overflow do V8. Ela agora mora na
API estrita (
-
Literal numérico mantém as casas que o autor escreveu.
{2.50}volta a renderizar"2.50"(não"2.5"),{007}renderiza"007". Dentro de uma conta o valor é coagido normalmente, então{2.50 + 0}continua2.5. -
A posição do erro de sintaxe agora é o offset exato. Cada token guarda o próprio início, então a posição está certa mesmo com espaço entre tokens e com token repetido — nem
indexOf(primeira ocorrência) nem soma de tamanhos (ignora o espaço) acertavam isso. -
Precedência de operador nas expressões de template.
{a + b * c}agora avalia comoa + (b * c). O motor anterior dobrava da esquerda pra direita, como calculadora de bolso, então coma=2 b=3 c=4produzia 20 em vez de 14 — número errado num relatório financeiro, sem erro nenhum. -
Agrupamento por parêntese.
{(a + b) * c}agora funciona. O motor anterior não tinha noção de agrupamento: o padrão de chamada de função não casava com um(no início, a passada aritmética não sabia agrupar, e o resultado virava0, em silêncio. -
Texto numa expressão aritmética não estoura mais.
{"x" + 1}devolve vazio, a convenção do formato pra "não deu pra resolver". Antes, a passada aritmética desistia, o fallback reprocessava a mesma string, e a recursão corria até o guarda de aninhamento estourar — com uma mensagem sobre profundidade que não tinha nada a ver com o problema. -
Divisão por zero não estoura mais.
{a / zero}devolve vazio, pelo mesmo motivo. Esse é uma entrada plausível de verdade: um denominador que zera numa linha.
Alterado
- O motor de expressões agora é parse → AST → evaluate
(
src/expressions/), substituindo o reescritor recursivo de strings que reparseava a mesma string em cada nível de aninhamento. As quatro correções acima vêm da estrutura, não de remendo em cima dela.resolveToken(token, data)mantém assinatura e retornostring, então nada a jusante mudou e a suíte de testes que já existia rodou sem alteração como prova de não-regressão.- A regra lexical do formato agora é explícita e testada: um operador só é
operador quando cercado de espaço em branco dos dois lados.
{my-key}e{my key}continuam paths (chave JSON com hífen e com espaço é comum),{a - b}é subtração. O motor anterior tinha isso por acidente, via regex; um tokenizador convencional quebraria{my-key}em três tokens e devolveria 0. - Valores intermediários agora são
string | numberem vez de sempre string. A coerção acontece na fronteira de cada operador — é isso que faz a precedência funcionar. - Erro de sintaxe (aspas não fechadas, parêntese aberto) agora estoura com a
posição, em vez de devolver
0ou vazio em silêncio. - Continua sem
eval/new Function: um template pode vir de fonte não confiável, e o avaliador percorre a AST.
- A paginação é decidida numa travessia só
(
src/pdf/layout/layoutDocument.ts), substituindo as duas que existiam antes: ogenerate.tsdecidia quebra de página e desenhava no mesmo laço, enquanto ocountBodyPagespercorria o corpo inteiro uma segunda vez só pra saber o total, porque{pageCount}precisa do número antes do primeiro traço. Só as decisões atômicas eram compartilhadas (pagination.ts); o avanço de cursor, o laço de fatiar tabela e a repetição de seção estavam escritos duas vezes, e uma mudança em só uma das cópias significaria "o dry-run disse 7 páginas, o desenho fez 8".layoutDocument(template, data, bindings, inputs)devolve umLayoutDocument: páginas, cada uma comPlacements já posicionados e com valor já resolvido. A contagem de páginas épages.length— não pode discordar do desenho porque É o desenho.- O
countBodyPagesdeixou de existir. Ogenerate.tsnão tem mais nenhuma decisão de paginação; orender/*recebe valores resolvidos em vez de resolvê-los. - O cursor depois de uma tabela agora avança por
computeTableSlice().heightMmem vez do Y que odrawTableSlicedevolvia — o layout não precisa mais desenhar pra saber onde continuar. - A medição de seção foi pro
layout/sectionLayout.tse as métricas de tabela propdf/tableMetrics.ts, então olayout/não importa mais dorender/e é de fato livre de pdf-lib. - Conferido contra o build da v2.0.0 em 11 templates (tabelas de 1/35/36/37/
120/600 linhas, linha de totais,
repeatHeader: false, fluxo texto-tabela-texto, seção mestre-detalhe,Template.pages): contagem de páginas idêntica em todos, e content streams byte-a-byte iguais exceto uma coordenada Y que difere em 1,4e-13 mm — ruído de ida e volta de float que o caminho novo tem um passo a menos.
- Comparação solta agora renderiza
"true"/"false"—{a > 1}antes renderizava vazio, porque o motor antigo tratava a coisa inteira como path e não achava nada. É de propósito: a saída antiga era acidental, e uma comparação que avalia pra algo é a base de que ovisibleWhenprecisa. Comparação dentro de{IF(...)}se comporta exatamente como antes.
2.0.0 (2026-09-01)
Entrega o split do pdfjs-dist que estava em "Planejado" — mais a
segunda metade que aquela entrada não considerava: o fundo de página
vindo de PDF também passava pelo pdf.js, direto do <Designer>, então o
novo entry point sozinho não teria liberado o entry principal. Esse
recurso sai nesta release (fundo de página agora é só imagem), o que
deixa o preview como o único lugar em que o pdf.js é usado.
Breaking
-
O
pdfjs-distnão é mais instalado automaticamente — saiu dedependencyobrigatória pra peer dependency opcional (peerDependenciesMeta["pdfjs-dist"].optional: true), o mesmo tratamento que owawoff2recebeu na 1.6.2. Só projetos que renderizam preview do PDF precisam dele; rodenpm install pdfjs-distno seu projeto se for o caso.- Por quê: o pdf.js tem ~35MB instalado, e como
dependencydo entry principal todo consumidor pagava por ele — inclusive apps que só renderizam o<Designer>e nunca dão preview em nada.
- Por quê: o pdf.js tem ~35MB instalado, e como
-
O
react-rndtambém não é mais instalado automaticamente — mesma mudança, e é ela que de fato torna real o install de backend sem React. Ele eradependencynormal, então descia em todo install, e como os peersreact/react-domdele não são opcionais, o npm instalava o stack React inteiro (react,react-dom,react-draggable,re-resizable,scheduler,prop-types, …: ~8,7MB) até num projeto que só importajson-pdf-designer/server. Ooptional: trueque este pacote já tinha nos próprios peers react estava sendo derrotado um nível abaixo.- Se você usa o
<Designer>, instale junto com o react:npm install react react-dom react-rnd. Só oPageCanvas.tsxusa (arrastar/redimensionar), e nada alcançável do/serverusa. - Um install de backend hoje resolve
fontkit,pdf-libetiny-inflatemais as árvores deles, e nada de React.
- Se você usa o
-
PdfPreview,PdfPreviewModaleconfigurePdfWorkerforam pro novo entry pointjson-pdf-designer/preview. Atualize o import:- import { PdfPreviewModal, configurePdfWorker } from "json-pdf-designer";+ import { PdfPreviewModal, configurePdfWorker } from "json-pdf-designer/preview";Todo o resto —
<Designer>,generatePdf,downloadPdf, os componentes de UI, os tipos — continua exatamente onde estava. Umimport()lazy nos arquivos antigos não resolveria: re-exportados do grafo do entry principal, qualquer bundler resolvendo"json-pdf-designer"ainda precisava resolver opdfjs-distem tempo de build. -
Fundo de página não pode mais ser um PDF — só imagem. O upload de fundo aceita PNG/JPEG, e o botão passa a dizer "Imagem de fundo" em vez de "PDF/imagem de fundo". Rasterizar a 1ª página de um PDF enviado era a outra coisa que o pdf.js fazia neste pacote, e o
src/pdf/backgroundImage.tso importava direto enquanto o<Designer>importa ele — então o split de entry acima teria deixado o pdf.js no grafo do entry principal de qualquer jeito. Tirar o recurso é o que de fato libera o entry principal, e deixa renderizar o preview como o único uso de pdf.js no pacote inteiro.- Migração: se você usa um papel timbrado que só existe em PDF, exporte
a página pra PNG uma vez e envie esse arquivo. Templates existentes
não são afetados —
Template.backgroundImagesempre foi um PNG data URI, então um fundo capturado de PDF antes desta release continua funcionando exatamente igual. - O
fileToBackgroundImage(file)(interno) não trata maisapplication/pdf; osrc/pdf/rasterizePdfPage.tsdeixou de existir.
- Migração: se você usa um papel timbrado que só existe em PDF, exporte
a página pra PNG uma vez e envie esse arquivo. Templates existentes
não são afetados —
Adicionado
examples/no-preview— quarto app de exemplo:<Designer>+generatePdf+downloadPdf, direto pro download, sem tela de preview, e sempdfjs-distem nenhum campo de dependência. É o app que prova que o entry principal funciona sem o peer opcional, e ele builda na CI — então um import de pdf.js vazando de volta pro entry principal quebra o build do Pages.test/entryBoundaries.test.ts— percorre o grafo de código a partir desrc/index.ts,src/server.tsesrc/preview.tse falha se opdfjs-distfor alcançável a partir dos dois primeiros, ou sereact/react-dom/react-rndfor alcançável a partir do/server. Dois casos de controle impedem que um walker quebrado faça os outros passarem por vacuidade. A checagem do tarball na CI também garante que opdfjs-distfica ausente donode_modulesdepois de instalar o pacote empacotado.
1.6.4 (2026-08-31)
Resposta a uma auditoria externa de código — conferi as afirmações contra o código de verdade antes de agir em cima delas (ver o plano/auditoria pra veredito completo; várias afirmações estavam desatualizadas ou exageradas, ex: a divergência de versão npm/GitHub reportada não existe mais, e a preocupação de segurança "sem eval/new Function" já estava satisfeita). Esta versão cobre as partes que se confirmaram reais e que valia a pena corrigir agora, mais algumas features de acompanhamento e uma reorganização interna feitas na mesma leva.
Corrigido
{SUM(a) - SUM(b)}agora subtrai de verdade. Antes, o regex de chamada de função casava gulosamente a expressão inteira como UMA chamada só (capturando"a) - SUM(b"como argumento), renderizando silenciosamente"0"em vez da diferença. Duas chamadas de função combinadas por operador no mesmo{...}agora caem corretamente pra aritmética em vez disso.
Segurança
- Limite de profundidade de recursão em
{FUNCAO(...)}aninhadas — antes sem limite nenhum; um template com milhares de parênteses aninhados (ex:{CURRENCY(CURRENCY(CURRENCY(...)))}) podia derrubar o processo com um estouro de call stack não tratado. Agora lança um erro claro e capturável além de ~40 níveis de aninhamento — bem mais do que qualquer template legítimo precisa. - Limites de tamanho/contagem de imagem —
ImageSchema.contenteTemplate.backgroundImageagora respeitam um teto de 15MB decodificado por imagem e 200 imagens distintas por documento (os dois lançam erro claro em vez de aceitar qualquer coisa silenciosamente). O upload de fundo pelo navegador (fileToBackgroundImage) também rejeita arquivo maior que 20MB antes de tentar processar. Importa mais pra quem aceita template de fonte não confiável (multi-tenant).
Adicionado
- Suíte de testes golden/torture
(
test/pdf/generate.torture.test.ts+test/pdf/fixtures/) — roda o pipelinegeneratePdfde verdade (não uma página falsa, um documentopdf-libreal) contra templates propositalmente extremos: tabela vazia, tabela de 600 linhas espalhada por muitas páginas físicas, seção maior que uma página inteira, dado vinculado ausente/null, e a fronteira real de codificação de texto (acentuação pt-BR funciona com a fonte padrão, emoji/CJK corretamente precisam defontBytes— agora travado como teste explícito e intencional, não um crash sem documentação). - CI agora verifica o tarball publicado, não só o código-fonte — um
passo novo roda
npm pack, instala o.tgzresultante num consumidor descartável, e chamageneratePdfdejson-pdf-designer/serverde verdade. Pega regressão deexports/files/.d.tsque só aparece no que é publicado de fato (vertest/pack-consumer/). - Função de template
{IF(condição, "então", "senão")}—condiçãoé uma comparação (status == "paid",total > 100; operadores==,!=,>,>=,<,<=, sempre cercados de espaço, reaproveitando a mesma lógica de eq/gt/lt que os filtros de chart/KPI já usam) ou um path/expressão isolada checado como verdadeiro/falso (string vazia,"0"e"false"contam como falso). Só o lado escolhido é resolvido de verdade, então{IF(temDesconto, valorDesconto, "0")}não quebra mesmo quandovalorDescontonão existe no dado no lado falso. Adicionada ao seletor de função do editor de vínculo e do editor de fórmula de coluna de tabela, junto deSUM/CONCAT/etc. - Inspetor de Template — nova aba opcional/removível na barra
lateral (mesmo padrão de mostrar/esconder/reordenar de "Dados"/
"Estilo"/"Filtro"/"Página") mostrando uma árvore somente-leitura de
todo campo da página atual, agrupada por zona (Header/Body/Footer/
margens, reaproveitando
classifyZone/isRedZonejá existentes emsrc/zones.ts— sem lógica de classificação nova). Cada linha mostra tipo do campo, posição, seção-pai (se membro de seção), um resumo curto do vínculo, e o z-index (mesma ordem que enviar-pra-trás/trazer- pra-frente já usa). Clicar numa linha seleciona o campo no canvas, reaproveitando a seleção já existente — sem mecanismo de seleção paralelo.
Alterado
src/pdf/reorganizado emlayout/erender/— refatoração interna pura, sem mudança de API pública.generate.tsagora é só um orquestrador fino; a matemática de paginação (buildBodyItems,boundsOf/gapAfter,normalizePageDefs,countBodyPages) foi prasrc/pdf/layout/, e o desenho de verdade nopdf-lib(antesdrawTable.ts/drawSection.ts/drawChart.ts/drawKpi.ts, mais dois arquivos novos separados degenerate.tspra texto/imagem) foi prasrc/pdf/render/comorenderTable.ts/renderSection.ts/renderChart.ts/renderKpi.ts/renderText.ts/renderImage.ts, despachados porrender/index.ts. Verificado como movimentação pura: mesma contagem de testes passando antes e depois, e o tamanho do bundle final ficou inalterado. Só importa pra quem importava um desses arquivos internos direto (não fazem parte dos entry points públicos do pacote) — atualize o caminho do import se for o caso.
1.6.3 (2026-08-31)
Removido
- Dependência
lodash.get— o único ponto que usava (resolver o path do array vinculado de uma seção repetida) passou a usar a própria função de path case-insensitive que o pacote já tinha (agora exportada debindings.tscomogetCaseInsensitive), igual todo outro tipo de vínculo já resolvia. Removelodash.gete@types/lodash.getde vez (ambos marcados deprecated pelo próprio autor) e corrige uma inconsistência real: path de seção era case-sensitive enquanto chart/kpi/tabela já não eram.
1.6.2 (2026-08-31)
Quebra de compatibilidade (dependência peer)
wawoff2não vem mais instalado sozinho — saiu dedependencyobrigatória pra dependência peer opcional (peerDependenciesMeta.wawoff2.optional: true). Só quem embute fonte.woff2de verdade (generatePdf(..., { fontBytes })com bytes de um.woff2) precisa dela agora; rodenpm install wawoff2no seu próprio projeto se for o caso. Sem ele instalado, passar um.woff2lança um erro claro (em vez do descompressor simplesmente estar lá por baixo) pedindo pra instalar ou converter a fonte pra.ttf/.otfoffline antes..ttf,.otfe.woff(v1, descomprimido viatiny-inflate, que continua obrigatório) não são afetados.- Motivo: o binding WASM do
wawoff2tem um caminho de código só pra Node (fs/path) que bundlers tipo Vite acusam com um aviso confuso (mas inofensivo) de "externalized for browser compatibility" — pra TODO consumidor, mesmo quem nunca embute fonte customizada nenhuma. Tornar opcional faz esse aviso, e o binário WASM em si, só aparecerem pra quem realmente usa a funcionalidade. src/pdf/fontUtils.tsagora carregawawoff2/build/decompress_binding.jssob demanda, viaimport()dinâmico em vez de estático, só no momento real de descomprimir um.woff2.
- Motivo: o binding WASM do
Adicionado
- Paleta de cores da tabela ganhou uma entrada "Personalizada" explícita e clicável no seletor (antes só era alcançável implicitamente, sem escolher nenhum preset).
- Linhas zebradas da tabela agora é um interruptor à parte de qual paleta/preset está ativo — qualquer preset pode ser aplicado com ou sem listras.
TableSchema.borderColor— a cor da grade da tabela agora é configurável (antes era um cinza fixo tanto no PDF gerado quanto no preview do canvas); presets de cor também aplicam uma cor de borda de verdade.
1.6.1
- Refatoração interna: reorganizou
src/por domínio (table/,chart/,designer/,bindings/builders.tsetc. — ver Arquitetura e Estrutura do pacote pra estrutura atual), extraiu helpers compartilhados pra remover duplicação entre o código de desenho do PDF e do canvas, e adicionou cobertura de teste pra módulos de lógica pura que não tinham nenhuma. Sem mudança de API pública. - Corrigiu o contorno da tabela desenhando um quadrado por cima do
preenchimento arredondado quando
headBorderRadius/bodyBorderRadius/footerBorderRadiusestava definido.
1.6.0 e anteriores
Ver o histórico de commits no GitHub pras mudanças de antes deste changelog existir.