Guidance for AI agents and contributors working on Typeable. Read this before editing — it documents the architecture contract that keeps changes localized.
A client-side React + TypeScript (Vite) app that ranks candidate names (brand names, words, domains — any string) by how fast/comfortable they are to type on a chosen keyboard layout, and shows a per-term keyboard heatmap. See README.md for the product overview and the typing-effort model details.
Everything hangs off src/model/types.ts and the barrel
src/model/index.ts. The UI imports the model only
through the barrel (import { analyzeTerms, getLayout, LAYOUTS } from '../model').
Stable exports — keep these signatures:
model/layouts.ts:LAYOUTS,getLayout(id),resolveChar(layout, char)model/effort.ts:MODEL(constants),analyzeTerm(term, layout): TermAnalysis,analyzeTerms(terms, layout, sortBy?): RankedTerm[](sorted,rank: 1= best under the sort;sortByis aSortMode—'ease'(default) = descending WPM,'time'= ascending total estimated ms)model/keys.ts:PHYSICAL_KEYS,KEY_BY_ID,FINGER_HOME,FINGER_WEIGHT,keyDistance,travelFromHomecomponents/Keyboard.tsx: named exportKeyboard(props: KeyboardProps)
Key idea: the physical keyboard is layout-independent. keys.ts defines key
positions (in key units, with row stagger), finger/hand assignments, and home
keys, keyed by the QWERTY base character at each position. A layout is just a
set of legends mapping each physical key id → the character it produces. Only
legends change between QWERTY / Dvorak / Colemak.
| Area | Files | Notes |
|---|---|---|
| Contract / data | model/types.ts, model/keys.ts, model/index.ts, index.css |
Foundation. Changing these ripples everywhere — do it deliberately. |
| Model | model/layouts.ts, model/effort.ts, model/effort.sanity.ts |
Pure TS, no React. Must keep TermAnalysis shape and the heat array (one KeyHeat per used key). |
| Keyboard viz | components/Keyboard.tsx, components/Keyboard.css |
SVG board from PHYSICAL_KEYS; colors by KeyHeat[] + HeatMode. |
| App / results | App.tsx, App.css, components/{TermInput,LayoutPicker,ResultsTable,ComparisonChart,Methodology}.tsx |
Owns all app state; consumes the model barrel + <Keyboard/>. |
- Styling: dark theme via CSS custom properties in
index.css(--bg,--surface*,--border*,--text*,--accent,--good/--warn/--bad, the--heat-0..5ramp,--finger-*, radii, shadows, fonts). Use tokens — do not hardcode colors. - No UI framework / no CSS framework. Charts and the keyboard are hand-rolled (SVG / divs). Keep dependencies minimal.
- TypeScript is strict;
verbatimModuleSyntaxis on, so useimport typefor types andimport type { JSX } from 'react'if you needJSX.Element. - The model is an estimate — keep it documented and tunable from
MODEL.
- Add a keyboard layout: add a
LayoutIdintypes.ts, add a legend map + entry inLAYOUTS(layouts.ts), and add it to theLayoutPickeroptions. Nothing else needs to change —resolveChar, the model, and the keyboard all read it generically. - Tune the model: edit the
MODELconstants ineffort.ts; re-run the sanity script to confirm orderings still hold. - Add a metric: extend
TermMetricsintypes.ts, populate it inanalyzeTerm, then render it in the App detail pane.
npm run build # tsc -b + vite build must pass
npx tsx src/model/effort.sanity.ts # model sanity assertions — must print PASS
npm run dev # then eyeball it in the browserWhen changing the model or layouts, always re-run the sanity script. When changing the UI, run the dev server and visually confirm the ranking, the heatmap (used keys color across the ramp), the frequency/effort toggle, and layout switching.