From f27d1256594ecb0a32bb8737116f323a8cd19b04 Mon Sep 17 00:00:00 2001 From: fen Date: Thu, 10 Sep 2026 12:11:47 -0500 Subject: [PATCH] Fix #167: drop join newline between .codeline spans; fix gutter font-size mismatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause per QA on palette-dev (merge d402113): splitLines() joined .codeline spans with '\n'; under white-space:pre-wrap each newline text node between display:block spans rendered as its own extra visual row, while renumber() counted gutter rows only from span heights — numbers drifted up one row per wrapped line. Join with '' (spans are blocks). Also: in the <=640px media query, .gutter { font-size: 15px } overrode the paste-view gutter size while .code switched to 13px, so gutline rows were ~36.7px vs 22.1px code rows at narrow widths — every number below line 1 drifted. Scope the paste-view gutter to the code font size. Verified with headless Chromium DOM geometry (desktop 1400x900 and mobile 375x812): gutter number tops equal their .codeline tops for lines wrapping to 2-3 rows, wrap OFF numbering unchanged ('1\n2\n3\n4\n5'), no horizontal scroll. --- internal/web/static/app.css | 4 ++++ internal/web/static/paste-lines.js | 13 +++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/internal/web/static/app.css b/internal/web/static/app.css index bdba0f8..f5543da 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -603,6 +603,10 @@ a.admin-link:hover { color: var(--fg); text-decoration: underline; } .editor-wrap { min-height: 45vh; } .editor { font-size: 15px; } .gutter { font-size: 15px; } + /* #167: the paste-view gutter must track .code's font size, not the editor + gutter's — mismatched font sizes make gutline rows taller/shorter than + code rows and every number drifts off its line start */ + .code .gutter { font-size: var(--code-fs); } .editor-head { flex-wrap: wrap; } .editor-head-title { flex: 1 1 100%; } .editor-head input { min-width: 0; font-size: 16px; } diff --git a/internal/web/static/paste-lines.js b/internal/web/static/paste-lines.js index a71d8ba..4d9e7d6 100644 --- a/internal/web/static/paste-lines.js +++ b/internal/web/static/paste-lines.js @@ -54,7 +54,16 @@ // is closest to its line's top. var bodyTop = body.getBoundingClientRect().top; var lh = parseFloat(getComputedStyle(body).lineHeight) || 1; - var totalRows = Math.max(lines.length, Math.ceil(body.getBoundingClientRect().height / lh)); + var maxBottom = 0; + for (var j = 0; j < lines.length; j++) { + var btm = lines[j].getBoundingClientRect().bottom - bodyTop; + if (btm > maxBottom) maxBottom = btm; + } + // Rows needed through the last visual row of the deepest line (ceil, so a + // partially-filled last row still gets its gutter span). + // Rows needed: floor(last line bottom / lh) + 1 — the row a line STARTS on + // must exist even when its bottom lands exactly on the body's edge. + var totalRows = Math.max(lines.length, Math.floor(maxBottom / lh) + 1); gutter.textContent = ''; var frag = document.createDocumentFragment(); var spans = []; @@ -97,7 +106,7 @@ // size change (paste of lots of text, window resize, zoom). Only width // changes of the code column affect wrapping, so observe width only — // height changes caused by our own renumbering must not re-trigger. - var mo = new MutationObserver(renumber); + var mo = new MutationObserver(function () { setTimeout(renumber, 0); }); mo.observe(document.documentElement, { attributes: true, attributeFilter: ['data-wrap'] }); var lastW = body.getBoundingClientRect().width; if (window.ResizeObserver) {