From b739e8e59a8a19cd2ed48f6972b15c565a805901 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Sun, 18 Oct 2026 13:55:01 +0200 Subject: [PATCH] fix(story-mode): balloons clamp again, and the loop's stays above its board MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The speaker words added for screen readers became the balloon's first child, and the clamp took that element instead of the text: long balloons were no longer cut to fit and ran over the art (the loop's covered the test count). The clamp now finds the text by its class. The loop has no Claude to keep clear of on the left, so its balloon may run wide and is clamped down to one line rather than over the board it points at; on a 400 px panel "All 19 tests pass…" now ends above "19 tests". Co-Authored-By: Claude Opus 5.5 (1M context) --- src/web/public/story-mode.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/web/public/story-mode.js b/src/web/public/story-mode.js index 236bba7f..922bda41 100644 --- a/src/web/public/story-mode.js +++ b/src/web/public/story-mode.js @@ -2598,7 +2598,9 @@ Object.assign(CodemanApp.prototype, { const capRight = cap ? cap.offsetLeft + capW : 0; const capBottom = cap ? cap.offsetTop + cap.offsetHeight : 0; const right = Math.max(10, Math.min(rt.w - 160, rt.w - hx - 38)); - let maxW = Math.min(rt.w * 0.5, rt.w - right - 14); + // The loop has no Claude to keep clear of on the left: its balloon may run wide, so it stays + // short and ends above the board it points at. + let maxW = Math.min(rt.w * (rt.kind === 'loop' ? 0.9 : 0.5), rt.w - right - 14); let top = 10; const besideCap = rt.w - right - capRight - 10; if (besideCap >= 150) maxW = Math.min(maxW, besideCap); @@ -2614,14 +2616,19 @@ Object.assign(CodemanApp.prototype, { for (const prop of ['left', 'right', 'top', 'max-width']) b.style.removeProperty(prop); } // Clamp a long text: inside the panel always, and off Claude's head when three lines allow. - const text = b.firstElementChild; + const text = b.querySelector('.story-balloon-text'); if (!text) return; text.style.removeProperty('--story-lines'); const lh = parseFloat(getComputedStyle(text).lineHeight) || 19; const pad = b.offsetHeight - text.offsetHeight; - const limit = placed ? Math.max(b.offsetTop + pad + 3 * lh, head.top * s - 22) : rt.h - 12; + // The loop's "head" is its board: down to one line rather than over the test count. + const least = rt.kind === 'loop' ? 1 : placed ? 3 : 2; + const limit = placed ? Math.max(b.offsetTop + pad + least * lh, head.top * s - 22) : rt.h - 12; if (b.offsetHeight && b.offsetTop + b.offsetHeight > limit) - text.style.setProperty('--story-lines', String(Math.max(2, Math.floor((limit - b.offsetTop - pad) / lh)))); + text.style.setProperty( + '--story-lines', + String(Math.max(Math.min(least, 2), Math.floor((limit - b.offsetTop - pad) / lh))) + ); }, /** Canvas click regions become real buttons: focusable, labelled, keyboard-reachable. */