DEC-091 - 2026-07-20 - accepted
Renderização de Variáveis/Widgets no Editor HTML Clássico e Preview, igual à Editbar (req-093 / BATCH-093 — Fase base). Decisões desta rodada (escopo confirmado com o Chefe: DUPLICAR a infra no clássico mantendo a Editbar intacta; itens 1+2, executados tecnicamente faseados):
-
Backend cria as caixas → frontend só reverte (simplificação-chave): diferente da Editbar (que mapeia DOM vivo × cru via
mapTree), o Editor Clássico monta iframessrcdoca partir do HTML do CodeMirror. Como o BACKEND pode renderizar as variáveis já em caixas com o marcador embutido (data-c2f-marker), o frontend NÃO precisa portar omapTreeinteiro — basta reverter as caixas no save. Novas funções emhtml-editor.php(duplicadas/adaptadas das da Editbar):html_editor_boxes_variaveis/_boxes_widgets/resolver_var/resolver_variaveis/var_box/render_widget_signature+ rotahtml-editor-render-vars({boxes, values}). Regex TOLERANTE (@?[[...]]@?): o clássico usa[[var]](o template-load remove o cerco), então o marcador guarda o texto EXATO de entrada e a reversão restitui o mesmo — round-trip perfeito seja[[var]]ou@[[var]]@. -
Resolução só das GLOBAIS; LOCAIS preservadas:
html_editor_resolver_varresolvepagina#url-raiz/url-full-http/gestor#versao/pagina#menu+gestor_variaveis_globais; retornanullpara variáveis desconhecidas (locais/simulação de módulo), que ficam literais para o frontend resolver (harmonização compublisherVariablesOrSimulationfica para a próxima rodada). Widgets inline são pulados na resolução de variáveis (tratados por_boxes_widgets). -
Integração ADITIVA e segura no frontend (
html-editor-interface.js):htmlEditorRenderVars(html,cb)(POST à rota; falha →cb(null)→ HTML cru, fluxo antigo intacto);htmlEditorReconstructVars(html)(reverte[data-c2f-marker]ao marcador; early-return sem caixas = no-op).editorHtmlVisualpara páginas/componentes passou a renderizar as caixas (async) antes de abrir o editor visual; o save (previsualizarConfirmar) aplica a reversão embodyContentantes de gravar no CodeMirror → o banco recebe a variável, não o valor. Layouts seguem síncronos (sem caixas por ora). Reversão testada com a função REAL extraída do arquivo (round-trip, no-op, cerco exato, múltiplas caixas).
Cache-bust: biblioteca-html-editor 1.4.10→1.4.11. Validação: php -l OK + smoke das funções de caixas; node --check OK; vitest reversão 4/4; suíte 29/29 + composer test 110/110 sem regressão. Nenhum git commit/git push executado.
Adendo DEC-091 (Fase 2 — preview + publisher-pages, mesma rodada)
-
htmlEditorRenderVarsgeneralizado (cb({boxes, values})): o editor visual consomeboxes(globais em caixas), o preview consomevalues(globais resolvidas para valor).previewHtmlpassou a assíncrono: aplica a simulação LOCAL (como antes) e resolve as GLOBAIS viavalues, com fallback ao HTML local (o preview nunca quebra). -
publisher-pages coberto pela infra genérica (sem tocar
publisher-pages.php): a rota resolve as GLOBAIS para qualquer módulo (roteada porhtml_editor_ajax_interface) e preserva as LOCAIS. No preview,publisherVariablesOrSimulation/OrValues(locais, simulação replicada) + backend (globais) coexistem. No editor visual, as GLOBAIS viram caixas e as LOCAIS ficam como marcadores ([[item#X]]), preservadas no save. Decisão: as locais NÃO recebem a simulação REPLICADA no editor visual — replicar o blocoitemN vezes quebraria a reversão 1:1 (gravaria N marcadores onde havia 1); o usuário edita o TEMPLATE, não a replicação. A simulação replicada permanece exclusiva do preview. -
Refinamento futuro (opcional): envolver as variáveis LOCAIS em caixas protegidas 1:1 no editor visual (anotação/proteção sem replicar) — melhoria de UX; o comportamento atual (locais como marcadores, preservadas no save) já é seguro. Status do batch:
complete(itens 1+2 atendidos).
DEC-092 - 2026-07-29 - accepted
Mapeamento visual de mídia/documento embutido, proteção de eventos, suporte híbrido a PDF e modal estruturado (req-096 / BATCH-096). Decisões desta rodada:
-
Invólucro é UI de RUNTIME, nunca persistência:
object/iframe/embed/video/audio(e o contêiner.conn2flow-pdfjs) recebem em runtime o invólucro.conn2flow-embed-wrapper(badge de tipo + escudo.c2f-embed-shield+ 4 alças de resize +.conn2flow-embed-innercom o conteúdo inerte a ponteiro). A remoção acontece no ponto ÚNICO de saída de HTML do motor —extractUserHtml— que roda tanto no save (getCleanHtml) quanto nos snapshots de undo;init/applyState/afterDomMutationreconstroem o invólucro de forma idempotente a partir da tag limpa. Consequência prática: o banco nunca vê o invólucro, oreconstructOriginalda Editbar segue vendo apenas variáveis e widgets, e o histórico compara sempre HTML limpo (sem oscilação de snapshot). Alternativa descartada: remover o invólucro só no save — deixaria o wrapper vazar para os snapshots e o undo aninharia invólucros. -
Escudo transparente em vez de
pointer-events:noneno embed:<iframe>/<object>entregam o ponteiro ao documento interno antes da página, matando hover/seleção/arraste do editor. A solução é um escudo absoluto sobre o bloco (que recebe hover, clique e o duplo clique de edição) +pointer-events:noneno conteúdo embutido.resolveEditablefaz escudo, alça e o próprio embed resolverem para o invólucro, tornando o conjunto UM bloco atômico (mesmo padrão dos widgets). -
Embeds dentro de blocos dinâmicos NÃO recebem invólucro: widget renderizado (
.conn2flow-widget-wrapper/[data-c2f-widget-id]) e caixa de variável (.c2f-dyn-box) voltam ao marcador no save — permitir editar um<iframe>interno criaria uma alteração descartada silenciosamente. Também ficam de fora a UI do editor e os iframes de sistema do Live Editor (#c2f-site-toolbar,#c2f-device-preview). -
Dimensões no
style, atributos legados removidos: largura/altura (com unidadepx/%/vhno modal e px no drag-resize) são gravadas emstyle.width/heighte os atributoswidth/heightsão removidos, evitando tamanho inconsistente entre navegadores quando os dois competem. -
3 motores de PDF como GERADORES DE MARKUP puros (
buildPdfNativeMarkup/buildPdfJsMarkup/buildPdfGoogleMarkup, roteados porbuildEmbedMarkup): funções que recebem a configuração e devolvem string HTML — testáveis sem DOM de editor e reaproveitáveis. Motor A =<object type="application/pdf">com fallback amigável interno; motor B =<div class="conn2flow-pdfjs" data-pdf-src|zoom|toolbar|page|scroll>; motor C =<iframe src="[REDACTED]>com a URL absolutizada (o Google precisa de URL pública absoluta). -
Fallback do motor A com estilos INLINE: esse HTML é persistido e renderizado no site publicado, onde o CSS do editor não existe. Os estilos vão inline (classes preservadas para customização do projeto), garantindo a "mensagem + botão estilizado" pedida pelo intake em qualquer página — sem obrigar o projeto a incluir CSS novo.
-
*PDF.js: contrato por atributos `data-
+ runtime de core auto-suficiente**: o novo assetgestor/assets/interface/pdf-viewer.jsvarre.conn2flow-pdfjs, injeta seu próprio CSS, monta barra de ferramentas (páginas/zoom), renderiza em canvas nos modosvertical(todas as páginas) epage, e **carrega a lib do PDF.js da CDN sob demanda** quandowindow.pdfjsLibnão existe. Expõewindow.conn2flowPdfViewerInit()(idempotente viadata-c2f-pdfjs-ready), chamado pelo motor visual após aplicar/desfazer alterações. Versão fixada em 3.11.174 (build UMD/legada, compatível com Safari/iOS antigo) — a build ESM.mjsexigiriatype=modulenosrcdoc` e no site. -
Injeção de assets condicionada à página, com helper PURO no core:
gestor_pdf_viewer_detectar($html)compara TOKEN por TOKEN do atributoclass(uma checagem por substring, ou com\b, daria falso positivo emconn2flow-pdfjs-legacy) egestor_pdf_viewer_assets($urlRaiz,$versao)monta as tags; ambas na bibliotecagestor(carregada pelo bootstrap do PHPUnit → testáveis sem banco). O pipelinegestor_pagina_pdf_viewer()(emgestor/gestor.php) roda DEPOIS degestor_pagina_widgets()(o contêiner pode vir de um widget) e ANTES degestor_pagina_extra_head_e_javascript()(que serializa as inclusões). No editor clássico, osrcdocé montado por JS e não passa pelo pipeline PHP — daímontarPdfViewerHead()nohtml-editor-interface.js(preview e editor visual, página e layout).
Cache-bust: biblioteca-html-editor 1.4.12→1.5.0, dashboard.json 1.0.11→1.0.12, motor no Live Editor ?v=c2f10→?v=c2f11. Validação: php -l 4/4, node --check 4/4, npx vitest run 50/50 (novo html-editor-embed.test.js 19/19), composer test 158/158 (novo PdfViewerAssetsTest 5/5). Homologação runtime (deploy Update => Core, 3 motores em desktop e mobile) pendente com o operador. Nenhum git commit/git push executado.
DEC-093 - 2026-07-30 - accepted
Correções de homologação do BATCH-096, separação da edição avançada e embeds no painel "+" (req-097 / BATCH-097). Decisões desta rodada:
-
A duplicação era o embed indo para o LAYOUT, não uma cópia extra: a evidência (
temp/html-output.html) mostra um<object>dentro de#c2f-page-contente outro como último filho de#c2f-layout-root. No Live Editor a raiz editável é o layout (a edição cobre layout + conteúdo) e operformSavesepara pelo#c2f-page-content; o fallback de inserção usavacontentRoot.appendChild(), então o elemento nascia fora do conteúdo e era gravado emlayouts.html. Como o layout é compartilhado por todas as páginas e pela páginadashboard-site-toolbarrenderizada dentro do iframe da barra, o mesmo embed aparecia 2–3 vezes e "vazava" para dentro da Editbar. Correção:insertionRoot()— havendo#c2f-page-contentna raiz editável, ele é o destino padrão de qualquer inserção sem alvo (código customizado e painel "+"); o fallback do modo de inserção deixou de cair emdocument.body(o que colocava o elemento fora do que é salvo). Inserir no layout continua possível de forma explícita: basta selecionar um elemento do layout e inserir relativo a ele. -
Isolamento dos elementos de sistema centralizado em
isEditorOwned:#c2f-site-toolbar,#c2f-device-preview(e descendentes) e#c2f-save-loaderpassaram a ser reconhecidos ali, o que blinda de uma só vezwrapEmbeds,resolveEditable,findEditableFromPointeisUserContentNode/extractUserHtml— em vez de repetir a exceção em cada varredura. A extração também remove esses ids do clone, como defesa em profundidade contra um HTML já salvo com o iframe da barra dentro. -
Extração estritamente idempotente: além do invólucro,
extractUserHtmlagora (a) limpa o contêiner do PDF.js (o runtime desenha toolbar + canvas dentro dele e marcadata-c2f-pdfjs-ready; o contrato salvo é o<div>VAZIO com osdata-pdf-*) e (b) remove resíduos órfãos de invólucro (.c2f-embed-shield,.c2f-embed-handle,.conn2flow-embed-label) que tenham sido persistidos por uma versão anterior — o save "cura" o HTML sujo em vez de propagá-lo. Complementarmente, opdf-viewer.jsdeixou de escreverdisplay/flex-directioninline no contêiner (foi para CSS via[data-c2f-pdfjs-ready]), para ostylepersistido não ganhar sujeira de runtime. -
Escudo e
pointer-eventssão exclusivos do modo de edição:disable()desembrulha os embeds no DOM VIVO eenable()reconstrói. Assim, sair da edição, entrar no preview de dispositivo ou salvar devolve imediatamente a interatividade (rolar/ampliar o PDF, tocar o vídeo), sem depender de reload. O invólucro continua sendo removido na extração — as duas garantias são independentes. -
Embeds nascem posicionados (
position:relative;z-index:1): a causa da "imagem estática travada" no site publicado não é só o escudo — templates Tailwind usam camadas decorativas absolutas (absolute inset-0 z-0) dentro da seção, e um embed com posição estática fica ABAIXO delas e não recebe o ponteiro. O markup gerado passa a nascer com contexto de empilhamento próprio; se o usuário definirposition/z-indexnos estilos extras do modal, os dele vencem (a detecção é por regex nostyleExtra). -
Z-index: seletor de arquivos acima do modal:
#c2f-he-embed-modalem1000000e#c2f-he-imagepick-overlayem1000060(antes 1000004 × 1000002, com o gerenciador abrindo atrás da caixa). Faixa reservada: painéis do editor ≤ 1000003, modais ≤ 1000001, seletor de arquivos no topo. -
PDF.js sob demanda no editor: o runtime só é injetado no page load quando a página JÁ tem um leitor (
gestor_pagina_pdf_viewer). Ao escolher o Motor B durante a edição,ensurePdfViewer()carregainterface/pdf-viewer.js(que por sua vez busca a lib na CDN) erefreshPdfJsViewers()disparaconn2flowPdfViewerInit()— noapplyEmbedModal, nowrapEmbedse noapplyState. Antes, o usuário via um bloco cinza estático. -
Dois links de edição avançada, em vez de um link que troca de módulo: "Editar Avançado" aponta SEMPRE para
admin-paginas/editar/?id=…(a página crua existe para qualquer página) e o novo "Editar Publicação Avançado" aponta parapublisher-pages/editar/?id=…&publisher_id=…, exibido apenas quando há publicação vinculada. O bloco extra vive nos templates (<!-- dropdown-page-publisher < --> … > -->, pt-br e en) e é removido pormodelo_tag_delquando$ehPublisheré falso — mesmo padrão já usado no<!-- dropdown-page -->. -
Os 5 tipos de embed no painel "+":
object,iframe,embed,videoeaudioentram nas listas de elementos do Live Editor (dashboard.toolbar.js) e do editor clássico (html-editor-visual-controls.js);buildElementcria a tag com dimensões padrão e SEM fonte, e oonInsertClickseleciona o invólucro recém-criado e abre o modal na aba "Atributos" — a fonte é o primeiro dado que falta.
Cache-bust: biblioteca-html-editor 1.5.0→1.5.1, dashboard.json 1.0.12→1.0.13, motor no Live Editor ?v=c2f11→?v=c2f12. Validação: php -l OK, node --check 4/4, npx vitest run 58/58 (html-editor-embed.test.js 19→27 casos), composer test 158/158 sem regressão. Homologação runtime pendente com o operador. Nenhum git commit/git push executado.
DEC-094 - 2026-07-30 - accepted
Área de transferência persistente do Editor Visual (demanda direta do Engenheiro Chefe na sessão de 2026-07-30, complementando o req-097 / BATCH-098). Decisões desta rodada:
-
localStoragecomo meio de persistência, com HTML como formato: a cópia deixa de ser um nó em memória (clipboardElement) e passa a ser uma STRING de HTML guardada emlocalStorage['c2f-he-clipboard']({html, ts, origem}). OlocalStorageé por origem, então a mesma cópia atravessa páginas do site, o Live Editor e o editor clássico (osrcdocherda a origem) e sobrevive ao reload que acontece ao sair do modo de edição — que era exatamente o ponto onde a cópia se perdia. Guardar HTML (e não o nó) evita depender do DOM da página de origem e torna a colagem idêntica na mesma sessão ou em outra. -
Sanitização obrigatória no COPIAR, não no COLAR:
buildClipboardMarkup()remove o invólucro de embed e o leitor PDF.js renderizado (UI de runtime) e descartadata-c2f-variable/contenteditable, que só fazem sentido junto dovarMapda página de ORIGEM — mantê-los faria a Editbar reconstruir a variável errada na página de destino. Osdata-c2f-marker(base64 do marcador) são PRESERVADOS porque são autossuficientes: é o que faz um widget colado continuar sendo um widget onde quer que seja colado. Sanear na cópia mantém o que está guardado sempre válido. -
Renumeração dos ids ao colar:
remapPastedIds()trocadata-c2f-widget-idpor ids novos e reregistra wrappers clássicos nowidgetsMapda sessão. Sem isso, colar na MESMA página do original criaria dois grupos com o mesmo id e oreconstructOriginalda Editbar — que agrupa por esse id e remove os "irmãos" do grupo — descartaria parte do conteúdo no salvamento. -
Colar sem seleção passa a ser válido: o caso de uso é "abri outra página e quero colar", quando não há nada selecionado. Nesse caso o bloco vai para o fim do conteúdo editável via
insertionRoot()(BATCH-097) — nunca na raiz do layout, que reapareceria em todas as páginas. O Ctrl+V acompanhou (hasClipboard()no lugar declipboardElement). -
Falha de storage é silenciosa: modo privado, storage negado ou cota estourada apenas mantêm a cópia em memória (try/catch em leitura e escrita) — o editor nunca quebra por causa da área de transferência.
Cache-bust: biblioteca-html-editor 1.5.1→1.5.2, motor no Live Editor ?v=c2f12→?v=c2f13. Validação: node --check OK, npx vitest run 64/64 (6 casos novos), composer test 158/158. Homologação runtime pendente com o operador. Nenhum git commit/git push executado.
DEC-095 - 2026-07-30 - accepted
Upload e gestão de pastas liberados no modo picker do admin-arquivos (demanda direta do Engenheiro Chefe na sessão de 2026-07-30 / BATCH-099). Decisões desta rodada:
-
O picker é o mesmo gerenciador, não uma versão reduzida: todos os pontos de seleção de arquivo do sistema (campo de imagem, seletor de fonte do embed, image-picker do styler) abrem
admin-arquivos/?paginaIframe=sim. Esconder ali as ferramentas de gestão (<!-- normal-tools -->: Adicionar, Nova pasta, Selecionar todos) obrigava o usuário a abrir o módulo em outra aba só para subir o arquivo que ele estava tentando escolher. A ocultação foi removida. -
A ocultação contradizia o resto do código: o próprio template já emitia o href do botão "Adicionar" com o placeholder
#paginaIframe#(resolvido para?paginaIframe=sim) e o JS já mantinha esse href atualizado com a pasta corrente acrescentando&paginaIframe=sim(atualizarAddHref()). O fluxo de upload dentro do iframe estava implementado — apenas inacessível. Nenhuma outra funcionalidade dependia da ausência desses controles (o JS não condiciona "Nova pasta", renomear, excluir nem a barra de seleção ao modo picker). -
O botão voltar precisa existir no picker: com o upload liberado,
admin-arquivos-adicionarpassou a ser alcançável dentro do iframe; o botão voltar (destino já preparado:admin-arquivos/?paginaIframe=sim) é o caminho de retorno à listagem para escolher o arquivo recém-enviado. Antes ele era removido no picker justamente porque esse fluxo não existia — mantê-lo removido deixaria o usuário preso na tela de envio. -
O que continua diferente no picker (de propósito): a célula de arquivo mostra "Selecionar" em vez de "Copiar URL" (a ação do picker é devolver o arquivo ao campo que o chamou), a galeria segue a mesma regra e a listagem não usa o cache de última pasta (
localStorage), começando na pasta pedida por quem abriu o seletor.
Validação: php -l OK, composer test 158/158, npx vitest run 64/64. A mudança é de renderização condicional de template (sem cobertura unitária viável sem banco), então a verificação é manual/runtime, registrada no checklist. Homologação pendente com o operador. Nenhum git commit/git push executado.