Initial build: typo, a typing test built from your own code

No backend, no build step -- static HTML/CSS/JS, themed identically to
flit/wisp (same CSS vars, same "> " title prefix, same dark/mono/green
aesthetic). Snippets are real, shipped code pulled from six repos in
this family (flit, wisp, keep, stash, delve-term, goonk) rather than
generic pangrams.

Stats: WPM = (correct chars / 5) / minutes elapsed, timing from first
keystroke not page load. Accuracy = correct keystrokes / total
keystrokes tracked at the keydown level (comparing the pressed key
against the exact next expected character before it's inserted), so
backspaced-over mistakes still count against it rather than only
diffing the final text. Keys/sec updates live via a 200ms tick.
Tabs in source are rendered as two spaces so players never need to
press Tab -- sidesteps a browser default-Tab-moves-focus fight
entirely rather than fighting it with preventDefault.

Verified via simulated typing sessions (puppeteer), not just static
review -- both an artificially fast pass (confirms the math is
internally consistent: 308 correct/310 total keystrokes -> 99%,
matches displayed) and a realistic ~60wpm-paced pass (confirms no
unit-conversion bug that only a slow, deliberate typist would hit
tests-at-lightspeed wouldn't surface). That process caught two real
bugs before they'd have hit a real player:

1. A stray `tabindex="0"` on the code display let the browser's
   native click-to-focus behavior steal focus away from the hidden
   textarea before our own focus() call ran -- typing silently did
   nothing. Fixed by removing the tabindex and switching to a
   mousedown+preventDefault handler instead of click.
2. The help modal's CSS set `display: flex` unconditionally, which
   (author CSS always beats the UA stylesheet) overrode the `hidden`
   attribute's default `display: none` -- the modal was invisibly
   covering the entire viewport and intercepting every click, at all
   times, whether or not it had been opened. Fixed with an explicit
   `.modal-overlay:not([hidden])` rule.

Also verified the actual Docker image builds and serves correctly
(nginx, no build stage needed since there's nothing to compile).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-07-15 14:12:44 +02:00
commit 4192a05a78
10 changed files with 677 additions and 0 deletions

54
README.md Normal file
View File

@@ -0,0 +1,54 @@
# typo
A typing test built from your own code. Every snippet is real, shipped
code pulled from real repos — flit, wisp, keep, stash, delve-term, goonk
itself — not generic pangrams or Lorem Ipsum.
No backend, no build step — plain HTML/CSS/JS served statically, with
snippets baked into `snippets.json`.
## Stats
- **WPM** — (correct characters / 5) / minutes elapsed, the standard
typing-test formula. Timing starts on the first keystroke, not page load.
- **Accuracy** — correct keystrokes / total keystrokes, including ones you
backspaced over. Tracked at the keydown level (comparing the pressed key
against the exact next expected character), not just diffed after the
fact.
- **Keys/sec** — total keystrokes / elapsed seconds, live.
Mistakes are highlighted in place rather than blocking progress — you can
backspace to fix them or keep going, same as most real typing tests.
Tabs in the original source are shown as two spaces so you never need to
press Tab.
## Adding snippets
Edit `snippets.json` — each entry is `{ repo, file, lang, code }`. Keep
snippets short (10-20 lines), self-contained, and free of exotic
Unicode in comments (em dashes etc. are typeable but annoying) if you
want them pleasant to type.
## Local development
Any static file server works, e.g.:
```bash
python3 -m http.server 8850
```
## Status
Built and verified via simulated typing sessions (not just static
review): focus handling, live character-by-character highlighting,
newline handling for multi-line snippets, and the WPM/accuracy/KPS math
all confirmed against both artificially fast and realistic human-speed
input. Caught and fixed two real bugs in the process — a focus-stealing
issue caused by a stray `tabindex` on the code display, and a modal
overlay that was invisibly covering the whole page and intercepting
every click because its CSS unconditionally overrode the `hidden`
attribute.
## License
MIT.