lvwerra HF Staff Claude Opus 4.7 (1M context) commited on
Commit
e22bb4b
·
1 Parent(s): 0659e8b

Sandbox: fix line overflow when seq block measured before font load

Browse files

The bases-per-line calc was attaching a probe to document.body with
hand-mirrored font CSS, then dividing the seq block width by the
measured glyph width. That drifted from the real rendering context in
two ways:

1. If JetBrains Mono hadn't finished loading at measure-time, the
fallback monospace was narrower → blockW underestimated → bpl too
large → rendered lines overflowed the container.
2. If the sandbox panel was still display:none when first measured
(landing on #intro and switching tabs later), clientWidth was 0
and the probe-based metrics never got refreshed against the real
layout once the panel became visible.

Three coordinated fixes:
- Move the probe inside the seq block itself so it inherits the real
font / letter-spacing / feature settings.
- tabs.js emits a tab:changed CustomEvent on every tab switch;
sandbox listens and drops charMetrics + re-renders on the next
frame once its panel is actually laid out.
- After each render, compare the first line's scrollWidth to the
available width and proportionally shrink bpl if it overshoots,
then back-solve charMetrics from the rendered line so the next
call converges without retriggering the loop.
- CSS overflow-x: auto on .sb-seq-block as a defense-in-depth net
if all of the above ever misses an edge case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

assets/js/sections/sandbox.js CHANGED
@@ -158,14 +158,21 @@
158
  return DARK_RGB_S;
159
  }
160
  function measureSeqChars() {
 
 
 
 
 
 
 
161
  const probe = document.createElement("div");
162
- probe.style.cssText = "position:absolute;visibility:hidden;top:-9999px;font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:400;letter-spacing:1.5px;white-space:pre";
163
  probe.textContent = " 1 ";
164
- document.body.appendChild(probe);
165
  const prefixW = probe.getBoundingClientRect().width;
166
  probe.textContent = "AAAAAAAAAA ";
167
  const blockW = probe.getBoundingClientRect().width;
168
- document.body.removeChild(probe);
169
  charMetrics = { prefixW, blockW };
170
  }
171
  function basesPerLineSb() {
@@ -264,7 +271,7 @@
264
  if (colorMode === "logprob") recomputeLpRange();
265
  const total = promptBases + genText;
266
  els.copy.disabled = total.length === 0;
267
- const bpl = basesPerLineSb();
268
  const totalLines = total ? Math.ceil(total.length / bpl) : 0;
269
  const renderedLines = els.seq.children.length;
270
  const needFull =
@@ -275,6 +282,43 @@
275
  (colorMode === "logprob" && lpRangeShifted(lastRenderedLpRange, lpRange));
276
  if (needFull) fullRender(bpl);
277
  else incrementalRender(bpl);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  }
279
  let renderQueued = false;
280
  function scheduleRender() {
@@ -466,5 +510,15 @@
466
  if (document.fonts && document.fonts.ready) {
467
  document.fonts.ready.then(() => { charMetrics = null; renderSequence(); });
468
  }
 
 
 
 
 
 
 
 
 
 
469
  })();
470
 
 
158
  return DARK_RGB_S;
159
  }
160
  function measureSeqChars() {
161
+ // Measure inside the actual seq block so the probe inherits the same
162
+ // font, size, weight, letter-spacing, font-feature-settings and any
163
+ // ancestor-driven context. Earlier this lived on document.body with
164
+ // hand-mirrored font styles, but that drifted from the real rendering
165
+ // context (font fallback while the web font was still loading, or any
166
+ // future style change on .sb-seq-block) and the resulting blockW was
167
+ // narrower than reality → bpl overshot → lines overflowed.
168
  const probe = document.createElement("div");
169
+ probe.style.cssText = "position:absolute;visibility:hidden;top:-9999px;left:-9999px;white-space:pre;pointer-events:none";
170
  probe.textContent = " 1 ";
171
+ els.seq.appendChild(probe);
172
  const prefixW = probe.getBoundingClientRect().width;
173
  probe.textContent = "AAAAAAAAAA ";
174
  const blockW = probe.getBoundingClientRect().width;
175
+ els.seq.removeChild(probe);
176
  charMetrics = { prefixW, blockW };
177
  }
178
  function basesPerLineSb() {
 
271
  if (colorMode === "logprob") recomputeLpRange();
272
  const total = promptBases + genText;
273
  els.copy.disabled = total.length === 0;
274
+ let bpl = basesPerLineSb();
275
  const totalLines = total ? Math.ceil(total.length / bpl) : 0;
276
  const renderedLines = els.seq.children.length;
277
  const needFull =
 
282
  (colorMode === "logprob" && lpRangeShifted(lastRenderedLpRange, lpRange));
283
  if (needFull) fullRender(bpl);
284
  else incrementalRender(bpl);
285
+ // Self-correct: the probe-based bpl can overshoot when the web font
286
+ // wasn't loaded yet at measure-time (the fallback monospace is
287
+ // narrower than JetBrains Mono → blockW too small → bpl too large).
288
+ // Measure the actual rendered first line, scale bpl down proportionally
289
+ // until it fits, then back-solve charMetrics so future renders converge
290
+ // on the right value without the loop.
291
+ if (total) {
292
+ const cs = getComputedStyle(els.seq);
293
+ const padL = parseFloat(cs.paddingLeft) || 0;
294
+ const padR = parseFloat(cs.paddingRight) || 0;
295
+ const fit = els.seq.clientWidth - padL - padR;
296
+ let safety = 3;
297
+ while (safety-- > 0 && fit > 0) {
298
+ const first = els.seq.firstElementChild;
299
+ if (!first || first.scrollWidth <= fit + 2) break;
300
+ if (bpl <= 10) break;
301
+ // Proportional shrink so we converge in 1-2 iterations even when
302
+ // the probe was off by 2x (font fallback case).
303
+ const ratio = fit / first.scrollWidth;
304
+ const next = Math.max(10, Math.floor((bpl * ratio) / 10) * 10);
305
+ if (next >= bpl) break;
306
+ bpl = next;
307
+ fullRender(bpl);
308
+ }
309
+ const first = els.seq.firstElementChild;
310
+ if (first && fit > 0 && bpl >= 10) {
311
+ // Back-solve charMetrics from the rendered first line so the next
312
+ // basesPerLineSb call lands at this bpl without retriggering the loop.
313
+ const renderedW = first.getBoundingClientRect().width;
314
+ const usedBlocks = bpl / 10;
315
+ const assumedPrefix = (charMetrics && charMetrics.prefixW) || 65;
316
+ const recoveredBlockW = (renderedW - assumedPrefix) / usedBlocks;
317
+ if (recoveredBlockW > 0 && isFinite(recoveredBlockW)) {
318
+ charMetrics = { prefixW: assumedPrefix, blockW: recoveredBlockW };
319
+ }
320
+ }
321
+ }
322
  }
323
  let renderQueued = false;
324
  function scheduleRender() {
 
510
  if (document.fonts && document.fonts.ready) {
511
  document.fonts.ready.then(() => { charMetrics = null; renderSequence(); });
512
  }
513
+ // First time the sandbox panel is shown (or any time we come back to
514
+ // it), the seq block may have been laid out at display:none, leaving
515
+ // basesPerLineSb with a stale measurement (clientWidth = 0 → bpl
516
+ // fallback, or font-not-yet-loaded probe → blockW too small → bpl too
517
+ // large → overflow). Drop charMetrics and re-render once the panel is
518
+ // actually visible, after the browser has had a frame to paint it.
519
+ window.addEventListener("tab:changed", e => {
520
+ if (e.detail?.name !== "sandbox") return;
521
+ requestAnimationFrame(() => { charMetrics = null; renderSequence(); });
522
+ });
523
  })();
524
 
assets/js/tabs.js CHANGED
@@ -23,6 +23,13 @@
23
  if (opts.anchor) location.hash = opts.anchor;
24
  else if (location.hash.replace("#", "") !== name) location.hash = name;
25
  }
 
 
 
 
 
 
 
26
  }
27
  // Exposed so the §0 intro-guide cards (in sections/intro.js) can jump tabs.
28
  window.setTab = setTab;
 
23
  if (opts.anchor) location.hash = opts.anchor;
24
  else if (location.hash.replace("#", "") !== name) location.hash = name;
25
  }
26
+ // Inform tab modules that own layout-measured DOM (e.g. sandbox's
27
+ // bases-per-line calc, which depends on the seq block's clientWidth)
28
+ // that they should re-measure on the next frame, after the new panel
29
+ // has actually been laid out at display:block. Without this, a panel
30
+ // that was first measured while hidden (clientWidth = 0, or before
31
+ // its web font loaded) keeps stale metrics until the user nudges it.
32
+ window.dispatchEvent(new CustomEvent("tab:changed", { detail: { name } }));
33
  }
34
  // Exposed so the §0 intro-guide cards (in sections/intro.js) can jump tabs.
35
  window.setTab = setTab;
assets/styles/sandbox.css CHANGED
@@ -223,12 +223,16 @@
223
 
224
  #panel-sandbox .sb-seq-block {
225
  font-family: "JetBrains Mono", monospace;
226
- background: #f4f4f4; border: 1px solid #ddd;
227
  padding: 16px 20px;
228
  white-space: pre; font-size: 12px; font-weight: 400;
229
  line-height: 1.85; letter-spacing: 1.5px;
230
  min-height: 80px;
231
  flex: 1 0 auto;
 
 
 
 
232
  }
233
  #panel-sandbox .sb-seq-block.empty {
234
  color: #cfcfcf;
 
223
 
224
  #panel-sandbox .sb-seq-block {
225
  font-family: "JetBrains Mono", monospace;
226
+ background: #fff; border: 1px solid #ddd;
227
  padding: 16px 20px;
228
  white-space: pre; font-size: 12px; font-weight: 400;
229
  line-height: 1.85; letter-spacing: 1.5px;
230
  min-height: 80px;
231
  flex: 1 0 auto;
232
+ /* Defense-in-depth: if the bases-per-line calculation ever overshoots
233
+ (e.g. before the web font finishes loading), keep the overflow inside
234
+ the block as a horizontal scroll instead of bleeding into the page. */
235
+ overflow-x: auto;
236
  }
237
  #panel-sandbox .sb-seq-block.empty {
238
  color: #cfcfcf;