commit 5abdcdf62939b4ac47f5ce56a0af202b9a3f1671
parent 6f593104ba87bf73b30095430a00c09efe7c15a8
Author: Kyle Barlow <kb@kylebarlow.com>
Date: Wed, 19 Aug 2026 08:28:39 -0700
Fix off-screen page content: min-height:0 on #game-container
Every game shell is a non-scrolling flex column, and #game-container was a
flex item with the default min-height:auto — so it refused to shrink below
its canvas's own height. When a sibling appeared below it (the work game's
story-credits footer), the column overflowed the viewport and the footer was
pushed past the fold with overflow:hidden making it unreachable. That is why
story credits never appeared even though the DOM said they were visible.
touch-controls.js already injected this fix via !important, which is why the
on-screen keyboard was unaffected; now the shells carry it themselves.
Adds regression coverage: the story-credit test asserts the footer's rect is
inside the viewport, plus per-game checks that the page never overflows and
the touch bar fits on screen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Esj1tf1Rgwe7odu4jqrXdG
Diffstat:
6 files changed, 66 insertions(+), 5 deletions(-)
diff --git a/games/animal-letter/index.html b/games/animal-letter/index.html
@@ -19,7 +19,10 @@
nav { width: 100%; padding: 0.6rem 1.25rem; flex-shrink: 0; }
nav a { color: #aaa; text-decoration: none; font-size: 1rem; }
nav a:hover { color: #fff; }
- #game-container { flex: 1; width: 100%; }
+ /* min-height:0 lets this flex item shrink below the canvas's own height,
+ so siblings below it (credits footer, touch bar) stay on screen. */
+ #game-container { flex: 1; width: 100%; min-height: 0; }
+ #game-container canvas { display: block; }
#audio-controls {
position: fixed;
diff --git a/games/animal-spell/index.html b/games/animal-spell/index.html
@@ -19,7 +19,10 @@
nav { width: 100%; padding: 0.6rem 1.25rem; flex-shrink: 0; }
nav a { color: #aaa; text-decoration: none; font-size: 1rem; }
nav a:hover { color: #fff; }
- #game-container { flex: 1; width: 100%; }
+ /* min-height:0 lets this flex item shrink below the canvas's own height,
+ so siblings below it (credits footer, touch bar) stay on screen. */
+ #game-container { flex: 1; width: 100%; min-height: 0; }
+ #game-container canvas { display: block; }
#audio-controls {
position: fixed;
diff --git a/games/fire-truck/index.html b/games/fire-truck/index.html
@@ -94,7 +94,10 @@
#settings-panel .setting-row {
margin-top: 0.6rem;
}
- #game-container { flex: 1; width: 100%; height: 100%; }
+ /* min-height:0 lets this flex item shrink below the canvas's own height,
+ so siblings below it (credits footer, touch bar) stay on screen. */
+ #game-container { flex: 1; width: 100%; height: 100%; min-height: 0; }
+ #game-container canvas { display: block; }
</style>
</head>
<body>
diff --git a/games/letter-find/index.html b/games/letter-find/index.html
@@ -19,7 +19,10 @@
nav { width: 100%; padding: 0.6rem 1.25rem; flex-shrink: 0; }
nav a { color: #aaa; text-decoration: none; font-size: 1rem; }
nav a:hover { color: #fff; }
- #game-container { flex: 1; width: 100%; }
+ /* min-height:0 lets this flex item shrink below the canvas's own height,
+ so siblings below it (credits footer, touch bar) stay on screen. */
+ #game-container { flex: 1; width: 100%; min-height: 0; }
+ #game-container canvas { display: block; }
#audio-controls {
position: fixed;
diff --git a/games/work/index.html b/games/work/index.html
@@ -19,7 +19,10 @@
nav { width: 100%; padding: 0.6rem 1.25rem; flex-shrink: 0; }
nav a { color: #667; text-decoration: none; font-size: 1rem; }
nav a:hover { color: #223; }
- #game-container { flex: 1; width: 100%; }
+ /* min-height:0 lets this flex item shrink below the canvas's own height,
+ so siblings below it (credits footer, touch bar) stay on screen. */
+ #game-container { flex: 1; width: 100%; min-height: 0; }
+ #game-container canvas { display: block; }
#audio-controls {
position: fixed;
diff --git a/tests/browser/runner.js b/tests/browser/runner.js
@@ -210,6 +210,16 @@ async function main() {
}
const body = await page.textContent('#story-credits');
if (!body.includes(expected.author)) throw new Error('Credit missing author');
+ // Visible in the DOM is not enough: the page never scrolls, so the footer
+ // must actually sit inside the viewport (regression: #game-container's
+ // default min-height:auto pushed it below the fold).
+ const box = await page.evaluate(() => {
+ const r = document.getElementById('story-credits').getBoundingClientRect();
+ return { top: r.top, bottom: r.bottom, innerH: window.innerHeight };
+ });
+ if (box.bottom > box.innerH + 1 || box.top >= box.innerH) {
+ throw new Error(`Story credit is off-screen: ${JSON.stringify(box)}`);
+ }
// Leaving Stories mode hides it again.
await page.evaluate(() => window.__WORK_SCENE__.enterMode('idle'));
await page.waitForTimeout(200);
@@ -825,6 +835,42 @@ async function main() {
if (after <= 0) throw new Error(`Spray hold did not accumulate quench progress: ${after}`);
});
+ // ── Page layout ───────────────────────────────────────────────────────────
+
+ // Every shell is a fixed-height, non-scrolling flex column. If the canvas
+ // refuses to shrink (flex items default to min-height:auto), whatever sits
+ // below it is pushed past the fold with no way to scroll to it.
+ const SHELLS = ['letter-find', 'fire-truck', 'animal-letter', 'animal-spell', 'work'];
+ for (const slug of SHELLS) {
+ await test(`${slug}: page does not overflow the viewport`, async (page, url) => {
+ await page.setViewportSize({ width: 1000, height: 700 });
+ await page.goto(`${url}/games/${slug}/`, { waitUntil: 'domcontentloaded' });
+ await page.waitForTimeout(1200);
+ const m = await page.evaluate(() => ({
+ scrollH: document.documentElement.scrollHeight,
+ innerH: window.innerHeight,
+ }));
+ if (m.scrollH > m.innerH + 1) {
+ throw new Error(`Page overflows viewport: ${JSON.stringify(m)}`);
+ }
+ });
+
+ await test(`${slug}: touch bar fits on screen`, async (page, url) => {
+ await page.addInitScript(() => { window.__FORCE_TOUCH_CONTROLS__ = true; });
+ await page.setViewportSize({ width: 390, height: 700 });
+ await page.goto(`${url}/games/${slug}/`, { waitUntil: 'domcontentloaded' });
+ await page.waitForSelector('#kg-touch');
+ await page.waitForTimeout(600);
+ const m = await page.evaluate(() => {
+ const r = document.getElementById('kg-touch').getBoundingClientRect();
+ return { top: r.top, bottom: r.bottom, innerH: window.innerHeight };
+ });
+ if (m.bottom > m.innerH + 1 || m.top >= m.innerH) {
+ throw new Error(`Touch bar is off-screen: ${JSON.stringify(m)}`);
+ }
+ });
+ }
+
// ── Summary ───────────────────────────────────────────────────────────────
await browser.close();