diff --git a/src/web/public/story-mode.js b/src/web/public/story-mode.js index c9496a08..57e4a890 100644 --- a/src/web/public/story-mode.js +++ b/src/web/public/story-mode.js @@ -710,7 +710,10 @@ const StoryArt = (() => { if (t && !t.passed && !t.failed && t.errors) return `${t.errors} error${t.errors === 1 ? '' : 's'}`; return `${total} test${total === 1 ? '' : 's'}`; } - function terminal(ctx, x, y, w, h, lines, f, printed = []) { + /** `clear` ({ x, top, bottom }): a band something is held over (the magnifier); lines in it end left of `x`. */ + function terminal(ctx, x, y, w, h, lines, f, printed = [], clear = null) { + const room = (base) => + clear && base + 2 > clear.top && base - 8 < clear.bottom ? Math.min(w - 18, clear.x - x - 13) : w - 18; rr(ctx, x, y, w, h, 7); inked(ctx, C.line, 2.5); ctx.fillStyle = C.balloonInk; @@ -725,7 +728,7 @@ const StoryArt = (() => { ctx.textBaseline = 'alphabetic'; const rows = Math.max(1, Math.floor((h - 24) / 14)); lines.slice(0, rows).forEach((line, i) => { - const text = fitText(ctx, `› ${line}`, w - 18); + const text = fitText(ctx, `› ${line}`, room(y + 30 + i * 14)); const shown = Math.floor(interpolate(f, [8 + i * 12, 20 + i * 12], [0, text.length])); ctx.fillStyle = i === Math.min(rows, lines.length) - 1 ? C.yellow : C.balloon; ctx.fillText(text.slice(0, shown), x + 9, y + 30 + i * 14); @@ -733,7 +736,7 @@ const StoryArt = (() => { const n = Math.min(rows, lines.length); const out = printed.slice(0, Math.max(0, rows - n)); out.forEach((line, i) => { - const text = fitText(ctx, line, w - 18); + const text = fitText(ctx, line, room(y + 30 + (n + i) * 14)); const at = 8 + (n + i) * 12; const shown = Math.floor(interpolate(f, [at, at + 10], [0, text.length])); ctx.save(); @@ -835,16 +838,12 @@ const StoryArt = (() => { const files = p.panel.files || []; const ax = W - 58; if (!files.length) { - terminal( - ctx, - 14, - 92, - Math.min(W * 0.58, 220), - 130, - p.panel.notes?.length ? p.panel.notes : ['looking around'], - f - ); - hit('list', 14, 92, Math.min(W * 0.58, 220), 130, 'See what Claude looked at'); + const tw = Math.min(W * 0.58, 220); + // On a narrow panel the magnifier is held over the terminal: the lines it passes end short of + // it (its rim's reach over every frame of its sway), so it never hides their words. + const lens = { x: ax - 122, top: 125, bottom: 187 }; + terminal(ctx, 14, 92, tw, 130, p.panel.notes?.length ? p.panel.notes : ['looking around'], f, [], lens); + hit('list', 14, 92, tw, 130, 'See what Claude looked at'); } else { const shown = files.length > 8 ? files.slice(0, 7) : files; const extra = files.length - shown.length; diff --git a/test/story-mode-ui.test.ts b/test/story-mode-ui.test.ts index 675356de..d7a18853 100644 --- a/test/story-mode-ui.test.ts +++ b/test/story-mode-ui.test.ts @@ -1396,3 +1396,33 @@ describe('story-mode.js speech balloons', () => { expect(16 + parseFloat(rt.balloon.style.maxWidth)).toBeLessThanOrEqual(face.x0 - 8); }); }); + +describe('story-mode.js magnifier', () => { + it('terminal lines the magnifier is held over end short of it, the others keep their width', async () => { + const look = (id: string, desc: string) => bash(id, `ls -la ${id}`, desc, ''); + boot( + storyOf([ + prompt('Look around please'), + ...look('b1', 'Inspect project folder, git state and the remotes'), + ...look('b2', 'List src and config contents of the whole repository'), + end(4000), + ]) + ); + app.openStoryMode(SID); + await settle(); + const rt = app._story.panels.find((p: { kind: string }) => p.kind === 'investigate'); + expect(rt.panel.files ?? []).toEqual([]); + // A narrow panel: the stage is 300 / (270 / 260) = 289 units wide, Claude at 231, the lens's rim + // at 231 - 119 at most, the terminal 168 units wide from 14. + Object.assign(rt, { w: 300, h: 270, dpr: 1, bgKey: '', clock: 72 }); // the finished frame (HERO) + drawnTexts.length = 0; + app._storyDrawPanel(rt); + const lines = drawnTexts.filter((t) => t.startsWith('› ')); + expect(lines).toHaveLength(2); + // The fake context measures 6 units a character: the first line uses the terminal's width (above + // the lens), the second ends with an ellipsis before the rim (231 - 122 - 14 - 13 = 82 units). + expect(lines[0].length * 6).toBeGreaterThan(120); + expect(lines[1].endsWith('…')).toBe(true); + expect(lines[1].length * 6).toBeLessThanOrEqual(82); + }); +});