From 82417d2e79e7ad2cff927f70e3e27b2f5671a495 Mon Sep 17 00:00:00 2001 From: lgcosta Date: Thu, 5 Nov 2026 12:08:42 -0300 Subject: [PATCH] =?UTF-8?q?fix(editor):=20alinhamento=20sele=C3=A7=C3=A3o/?= =?UTF-8?q?Enter=20=E2=80=94=20paridade=20de=20layout=20textarea=C3=97over?= =?UTF-8?q?lay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dois defeitos de geometria entre o textarea transparente (desenha caret e seleção) e o overlay #hl (desenha o texto visível): 1. Largura: no Windows/Chrome o scrollbar clássico do textarea (~15-17px) reduz sua área de texto; o overlay (overflow:hidden) não — cada linha quebrava em colunas diferentes e a seleção ficava com 'buracos' e deslocamento progressivo. Agora: scrollbar-gutter:stable no textarea (largura constante, sem saltar ao cruzar 1 tela) + --sbw medido por JS espelhado no padding-right do overlay a cada render. 2. Negrito sintetizado: tk-h/tk-strong/tk-mark usavam font-weight 700; sem face bold na fonte monoespaçada, o Chrome alonga os glifos (~1-2px/char) e o overlay acumulava offset por linha (visível no print: seleção parava no fim do trecho negrito). font-synthesis:none no overlay + pseudo-negrito por text-shadow (.35px) — peso visual preservado, avanço idêntico. Também: renderEditorVisual reassincroniza scrollTop/scrollLeft de hl/gutter após replaceChildren (antes o Enter em nota longa zerava o scroll do overlay — 'Enter não atualiza a visualização'). Verificado headless: com scrollbar clássico simulado (15px), largura de texto textarea==overlay (1078==1078), seleção contínua até o fim correto da linha quebrada; Enter com doc rolado mantém ta/hl/gutter em 5000; wysiwyg/preview sem regressão. sw v14. --- web/css/app.css | 10 ++++++---- web/js/app.js | 8 ++++++++ web/sw.js | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/web/css/app.css b/web/css/app.css index 8ea4bec..bb3b851 100644 --- a/web/css/app.css +++ b/web/css/app.css @@ -336,10 +336,11 @@ body.no-gutter #gutter { display: none; } inset: 0; z-index: 1; overflow: hidden; - padding: var(--pad-y) var(--pad-x); + padding: var(--pad-y) calc(var(--pad-x) + var(--sbw, 0px)) var(--pad-y) var(--pad-x); font-family: var(--editor-font); font-size: var(--editor-size); line-height: var(--line-h); + font-synthesis: none; /* negrito sintetizado alonga glifos e desloca a quebra vs textarea */ color: var(--fg); white-space: pre-wrap; overflow-wrap: break-word; @@ -367,6 +368,7 @@ body.no-gutter #gutter { display: none; } border: 0; resize: none; overflow: auto; + scrollbar-gutter: stable; /* reserva a barra clássica sempre: largura de texto constante p/ casar com o overlay */ padding: var(--pad-y) var(--pad-x); font-family: var(--editor-font); font-size: var(--editor-size); @@ -381,8 +383,8 @@ body.no-gutter #gutter { display: none; } #editor::placeholder { color: var(--muted); } /* tokens de colorização */ -.tk-h { color: var(--tk-heading); font-weight: 700; } -.tk-strong { color: var(--tk-strong); font-weight: 700; } +.tk-h { color: var(--tk-heading); font-weight: 700; text-shadow: .35px 0 currentColor; } +.tk-strong { color: var(--tk-strong); font-weight: 700; text-shadow: .35px 0 currentColor; } .tk-em { color: var(--tk-em); font-style: italic; } .tk-strike { color: var(--tk-strike); text-decoration: line-through; } .tk-code { color: var(--tk-code); background: var(--tk-code-bg); } @@ -391,7 +393,7 @@ body.no-gutter #gutter { display: none; } .tk-link { color: var(--tk-link); text-decoration: underline; } .tk-url { color: var(--tk-url); text-decoration: underline; } .tk-tag { color: var(--tk-tag); font-weight: 600; } -.tk-mark { color: var(--tk-mark); font-weight: 700; } +.tk-mark { color: var(--tk-mark); font-weight: 700; text-shadow: .35px 0 currentColor; } .tk-quote { color: var(--tk-quote); font-style: italic; } .tk-html { color: var(--tk-html); } .tk-attr { color: var(--tk-attr); } diff --git a/web/js/app.js b/web/js/app.js index c3d526b..55cd322 100644 --- a/web/js/app.js +++ b/web/js/app.js @@ -1157,6 +1157,10 @@ const frag = document.createDocumentFragment(); for (const r of rowEls) frag.appendChild(r); E.hl.replaceChildren(frag); + // paridade de largura com o textarea: com scrollbar-gutter:stable o valor é + // constante por plataforma (clássico ~15-17px; overlay 0) e o espelha aqui + const sbw = E.editor.offsetWidth - E.editor.clientWidth; + E.hl.style.setProperty('--sbw', sbw > 0 ? sbw + 'px' : '0px'); if (lines.length !== lastGutterCount) { const gf = document.createDocumentFragment(); for (let i = 1; i <= lines.length; i++) { @@ -1172,6 +1176,10 @@ const rows = E.gutterInner.children; for (let i = 0; i < rowEls.length; i++) rows[i].style.height = E.hl.children[i].offsetHeight + 'px'; } + // replaceChildren zera o scroll do overlay/gutter — reassincroniza com o textarea + E.hl.scrollTop = E.editor.scrollTop; + E.hl.scrollLeft = E.editor.scrollLeft; + E.gutter.scrollTop = E.editor.scrollTop; updateMarker(); } diff --git a/web/sw.js b/web/sw.js index c79b12c..c71a53c 100644 --- a/web/sw.js +++ b/web/sw.js @@ -1,7 +1,7 @@ /* Service Worker do Notas — caches versionados. * BUMP de CACHE em todo deploy que mude shell/assets (o activate limpa os antigos). */ -const CACHE = 'notas-shell-v13'; +const CACHE = 'notas-shell-v14'; const SHELL = [ '/', '/css/app.css',