kgames

KGames — free keyboard games for kids
git clone https://www.keyboard.games/code/kgames.git
Log | Files | Refs | README | LICENSE

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:
Mgames/animal-letter/index.html | 5++++-
Mgames/animal-spell/index.html | 5++++-
Mgames/fire-truck/index.html | 5++++-
Mgames/letter-find/index.html | 5++++-
Mgames/work/index.html | 5++++-
Mtests/browser/runner.js | 46++++++++++++++++++++++++++++++++++++++++++++++
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();