Repository navigation
Keep the dictionary card on screen, and stop keyboard input under overlays - #88
Conversation
…rlays The dictionary card was only clamped horizontally: `top` was hardcoded to `anchor.bottom + 8`. Anchored on the last grid row with a long definition (WordNet's GAS, 311 chars, level 335) the card ran off the bottom of the viewport — and it is `position: fixed`, so it could be neither scrolled to nor tapped. Only way out was restarting the level. - `fitVertically(anchorRect, height, viewportHeight)` in dictionary-card.js: flips the card above the anchor when it does not fit below, and when neither side fits picks the larger one and returns a `maxHeight` so the card scrolls itself. Split out as a pure function so the arithmetic is unit-testable (design doc §12); measuring and style writing stay in `show()`. - `.dict-card` gets `overflow-y: auto` + `overscroll-behavior: none`, and joins the `touchmove` allowlist in main.js — without it a clamped card cannot be scrolled on iOS. - dictionary-card.js reads `globalThis` instead of `window` at module level so the module imports under `bun test`, which has no DOM. - wheel.js's dev-only keyboard input now also bails inside `.overlay`: the clear/settings cards focus themselves, so Enter used to submit a word the player cannot see and letter keys mutated the covered preview and `#status` announcement. Reachable with a Bluetooth keyboard on a tablet. - fetch-data.mjs comment: wordfreq is Apache-2.0, not MIT (design doc §14). Docs updated: design doc §4/§12/§17, UI doc §4-E, CLAUDE.md testing split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR Summary by QodoClamp dictionary card vertically; block wheel keyboard input under overlays
AI Description
Diagram
High-Level Assessment
Files changed (9)
|
Code Review by Qodo
1. Overlay guard focus-bypass
|
| ### Testing split (design doc §12) | ||
|
|
||
| Only pure logic is auto-tested (`tests/*.test.mjs`): game rules, wheel hit/selection math, save normalization, redemption-code verification, `bridge.share`'s tri-state return (the one non-pure exception — the test swaps `navigator`/`document` on `globalThis`; its decision table is documented in UI doc §4-D and only misbehaves on a real device), plus a validator pass over `data/levels/` and consistency checks on the two machine-checkable PWA shell lists (`sw.js`'s `SHELL`, `index.html`'s `modulepreload` — see Architecture for why `main.js`'s imports are not one of them) and on `deploy.yml`'s root-file `cp` allowlist, plus a grep of the tracked tree for `.local.*` citations (see the Doc sync map for why). Unit tests use inline fixtures — never assert on `data/levels/` contents, which change on regeneration. UI, animation, and touch feel are manually tested against the §17 acceptance checklist on real devices. Desktop keyboard input in the wheel (letter keys / Backspace / Enter) exists for dev iteration, not for players. | ||
| Only pure logic is auto-tested (`tests/*.test.mjs`): game rules, wheel hit/selection math, save normalization, redemption-code verification, the dictionary card's vertical placement (`fitVertically` — the card is `position: fixed`, so an unclamped `top` puts it off-screen where it can be neither scrolled to nor dismissed; the arithmetic is split out from the measuring exactly so it can be pinned here), `bridge.share`'s tri-state return (the one non-pure exception — the test swaps `navigator`/`document` on `globalThis`; its decision table is documented in UI doc §4-D and only misbehaves on a real device), plus a validator pass over `data/levels/` and consistency checks on the two machine-checkable PWA shell lists (`sw.js`'s `SHELL`, `index.html`'s `modulepreload` — see Architecture for why `main.js`'s imports are not one of them) and on `deploy.yml`'s root-file `cp` allowlist, plus a grep of the tracked tree for `.local.*` citations (see the Doc sync map for why). Unit tests use inline fixtures — never assert on `data/levels/` contents, which change on regeneration. UI, animation, and touch feel are manually tested against the §17 acceptance checklist on real devices. Desktop keyboard input in the wheel (letter keys / Backspace / Enter) exists for dev iteration, not for players. |
There was a problem hiding this comment.
1. claude.md includes .local.* reference 📘 Rule violation ⚙ Maintainability
The modified documentation line in CLAUDE.md contains the substring .local.*, which is explicitly disallowed in comments/docs. This can mislead readers toward local-only artifacts that are not available in other clones/environments.
Agent Prompt
## Issue description
A modified documentation line includes the substring `.local.*`, which violates the rule prohibiting references to `.local.*` files/patterns in comments and documentation.
## Issue Context
The compliance rule forbids mentioning any `.local.` substring in docs/comments, regardless of intent.
## Fix Focus Areas
- CLAUDE.md[61-61]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // main.js 會把焦點放進卡片本身,接鍵盤等於在遮罩底下拼字:Enter 會送出一個玩家看不見的 | ||
| // 字(過關卡上按 Enter 就直接判定成功/失敗),字母鍵還會改動被蓋住的 #preview 與 #status | ||
| // 播報。沒有焦點時 target 是 body,遊戲畫面的開發用鍵盤輸入不受影響。 | ||
| if (ev.target.closest?.('input, textarea, .overlay')) return; |
There was a problem hiding this comment.
2. Overlay guard focus-bypass 🐞 Bug ≡ Correctness
createWheel()'s dev keyboard handler only bails when the keydown event target is inside .overlay, so if focus escapes the overlay (e.g., via Tab) the handler can still submit/mutate the hidden game UI while the overlay mask is visible. This contradicts the intended “stop keyboard input under overlays” behavior and can still trigger invisible submissions/preview changes.
Agent Prompt
## Issue description
`src/wheel.js` blocks keyboard input only when `ev.target.closest('.overlay')` is truthy. Overlays intentionally do **not** implement a JS focus trap, so focus can escape via Tab; once focus is outside the overlay, keyboard input can again drive the wheel while an overlay remains visible.
## Issue Context
This PR’s goal includes preventing keyboard-driven submissions under overlays. Relying on focus containment is weaker than checking whether an overlay is currently shown.
## Fix Focus Areas
- src/wheel.js[213-237]
## Suggested fix
In `onKey`, add an early return based on overlay visibility, not focus ancestry, e.g.:
```js
if (document.querySelector('.overlay:not([hidden])')) return;
```
Optionally keep the existing `input, textarea` check as well.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Why
The dictionary card was clamped horizontally but not vertically —
topwas hardcoded toanchor.bottom + 8. Anchored on the last grid row with a long definition (WordNet'sGAS, 311 chars — the longest in the whole dataset; level 335 places it on the bottom row) the card ran off the bottom of the viewport. It isposition: fixed, so it could be neither scrolled to nor tapped: the only way out was restarting the level. Desktop windows are tall enough that this almost never reproduces there.What changed
fitVertically(anchorRect, height, viewportHeight)(src/dictionary-card.js) — flips the card above the anchor when it does not fit below; when neither side fits (browser zoom, large system font) it takes the larger side and returns amaxHeightso the card scrolls itself. Split out as a pure function so the arithmetic is unit-testable (design doc §12); the measuring and style writing stay inshow()..dict-cardgetsoverflow-y: auto+overscroll-behavior: none, and joins thetouchmoveallowlist inmain.js— without that a clamped card cannot be scrolled on iOS. It is now the thirdoverflow-y: autoin the stylesheet.globalThisinstead ofwindowatdictionary-card.jsmodule level, so the module imports underbun test(no DOM there — readingwindowwouldReferenceErrorat import time).wheel.jsdev-only keyboard input now bails inside.overlay. The clear and settings cards focus themselves, so Enter used to submit a word the player cannot see (scoring a hit or a miss under the mask) and letter keys mutated the covered#previewand the#statusannouncement. Reachable with a Bluetooth keyboard on a tablet, not just on a dev machine.tools/fetch-data.mjscomment: wordfreq is Apache-2.0, not MIT (design doc §14 already had it right).Tests
Two new cases pin
fitVertically: the card never leaves the viewport across a grid of anchor positions and card heights (including one taller than the screen), and it opens below the anchor when there is room, flipping up only when there is not.bun run checkpasses (48 tests, lint clean).Manual verification stays on the §17 checklist — a new line covers level 335's
GASon a real device.Docs
Design doc §4 (
touchmoveallowlist), §12 (testing split + the keyboard rule), §17 (acceptance line); UI doc §4-E (four-side clamping); CLAUDE.md testing split.🤖 Generated with Claude Code