Skip to content
Projects
Live2026

Cyber Typing

A typing-speed test with WPM, accuracy, consistency and infinite level progression.

Role
Frontend Engineer
Status
Live
Cyber Typing
// Overview

Cyber Typing is a typing-speed test that is part of the pablobanker.dev ecosystem — it reuses the portfolio's design tokens (the same dark background, cyan accent and Geist typography). Across three screens (home, test, results) it measures net and gross WPM, accuracy and consistency, tracks your progress and levels you up as you go. The core principle is that no business logic lives inside Svelte components. A pure, framework-agnostic TypingEngine class takes keystrokes and exposes immutable snapshots; metrics, level progression and the WPM rank are pure modules; and a services layer orchestrates everything without touching the UI. Progression is infinite: each level scales word count, word length, capital letters, punctuation and numbers, and you only advance by hitting the 90% accuracy goal. The rank is derived from the average net WPM of the 10 most recent tests. There is no backend: history, preferences and the current level live in LocalStorage under versioned keys, and every record is re-validated on load with hand-written type guards. Input capture is accent- and IME-safe (dead keys and composition events are handled explicitly), paste is blocked so metrics can't be gamed, and the whole thing is covered by Vitest unit tests plus a Playwright end-to-end flow running against the production build.

Key features

  • Live per-character feedback: correct, incorrect, extra and missed.
  • Net and gross WPM, accuracy and consistency — all pure, guarded formulas.
  • Infinite levels scaling word count, word length, capitals, punctuation and numbers.
  • Advance only by hitting the 90% accuracy goal; otherwise the level repeats.
  • WPM rank derived from the average of the 10 most recent tests.
  • Portuguese and English word lists with deterministic, seed-based generation.
  • History, preferences and level persisted in versioned LocalStorage keys, validated on load.
  • Accent/IME-safe input capture (dead keys, composition events); paste blocked.
  • Keyboard shortcuts and View Transitions between screens.
  • Animations respect prefers-reduced-motion.
// Architecture

Architecture

No business logic inside Svelte components: a pure TypingEngine class holds the test state and exposes immutable snapshots, pure modules compute metrics, progression and rank, and a services layer orchestrates results, stats and persistence — components only render.

  • Flow: home hands off the level → typing screen mirrors engine snapshots into runes state → results build a TestResult → history persists it → home comes back populated.
  • TypingEngine is framework-agnostic and 100% testable in Node, without a DOM.
  • Characters are evaluated by code point, so accented words behave correctly.
  • Metrics (wpm, accuracy, consistency via coefficient of variation) are pure modules guarded against division by zero.
  • Infinite progression: levelConfig(n) scales difficulty smoothly along four axes.
  • SSR-safe persistence: every LocalStorage access is browser-guarded and hydration is lazy, client-only.
  • Persisted data is never trusted — type guards validate each record and drop invalid ones individually.
  • Vitest covers the engine, metrics and services; Playwright runs the real three-screen flow against the production build.
// Challenges & solutions

Challenges & solutions

Accents and IME without losing keystrokes

Problem

Dead keys (ABNT2) and IME composition fire events in different orders across browsers, which can lose or double-count accented characters.

Solution

Input is read from a transparent overlaid input's buffer: during a composition the buffer is not cleared and the commit happens on compositionend, with NFC normalization and code-point iteration.

Keeping the typing logic testable

Problem

Typing logic tangled into components is hard to test and easy to break.

Solution

The engine is a pure class that knows nothing about Svelte: it takes press/space/backspace, holds internal state and exposes an immutable snapshot the UI mirrors — so the core runs and is tested entirely in Node.

Persistence without a backend

Problem

All history lives in LocalStorage, where data can be stale, malformed or from an older version of the app.

Solution

Versioned keys plus hand-written type guards that re-validate every record on load: invalid entries are dropped individually, old results are back-filled instead of discarded, and history is capped at the 100 most recent.

// What I learned
  • How to build reactive UI with Svelte 5 runes on top of a framework-agnostic core.
  • How to design metric formulas (net/gross WPM, accuracy, consistency via CV) as pure, guarded modules.
  • How to handle dead keys, IME composition and Unicode code points in input capture.
  • How to persist safely in LocalStorage with versioned keys and load-time validation.
  • How to cover a whole app with unit tests plus an end-to-end flow against the production build.
// Tech stack
SvelteKitSvelte 5TypeScriptGSAPVitestPlaywrightLocalStorageView Transitions