Architecture of a Terminal-Themed CV Site
This site started as three JavaScript files and a question at 3 AM: "Can you turn my PDF resume into a website?" What came back was 5,620 lines of vanilla JS that worked -- until it didn't. Every new feature tangled with every other feature. Navigation raced with scroll spy. The TOC fought with the headings panel. Copy feedback timers leaked. And there was no way to test any of it without opening a browser.
The fix was not a framework. It was 15 state machines -- pure TypeScript functions with zero DOM dependencies, zero side effects, and zero coupling between them. Each one small enough to read in five minutes. Each one testable in milliseconds. Together, they run every interactive behavior on this site.
This series walks through the extraction, from the simplest toggle to the most complex scroll orchestration, then covers the TypeScript migration that made the interfaces explicit and the testing strategy that keeps them honest.
Before and After
| Metric | Before (vanilla JS) | After (TypeScript state machines) |
|---|---|---|
| Files with behavior logic | 3 (app.js, markdown-renderer.js, theme-switcher.js) | 15 state machines + 3 wiring files |
| Largest file | ~3,500 lines | 163 lines (TocScrollMachine) |
| DOM references in logic | Everywhere | Zero (callback injection) |
| Unit tests | 0 | 27 test files, 200+ test cases |
| Coverage (src/lib/) | N/A | 98% statements, 95% branches |
| Test execution time | Manual only | 600ms (unit), ~30s (E2E) |
Part I: Three Files and Five Thousand Lines
The original architecture and why it broke. State encoded as booleans, race conditions on rapid clicks, DOM coupling that made unit testing impossible. The "before" picture that motivated everything that follows.
Part II: The Pattern -- Factory, Closures, Callbacks
The architecture applied to the four simplest machines: FontSizeManager, AccentPaletteMachine, CopyFeedbackMachine, and SidebarResizeMachine. Factory functions, closure state, callback injection, guard clauses, and pure helpers. The template that every machine follows.
Part III: Navigation and Interaction Machines
SpaNavMachine and PageLoadMachine -- the two machines that orchestrate every page transition. Plus TourStateMachine, ScrollSpyMachine, ZoomPanState, and KeyboardNavState. Navigation classification, the generation counter pattern, composition at the wiring layer, and three pattern variations.
Part IV: The TOC Machine Cluster -- When Simple Isn't Enough
Five machines that power the table of contents. From the 30-line TocTooltipMachine to the 8-state TocScrollMachine -- the most complex machine in the project. Geometry injection, timer injection, user scroll locking, and the wiring diagram that connects them all.
Part V: From Vanilla JS to TypeScript
The migration that made it all explicit. tsconfig choices, esbuild for browser bundles, tsx for scripts, the IIFE output format, and concrete bugs that strict mode caught. Two build pipelines: browser (esbuild) and Node.js (tsx).
Part VI: Testing Pure State Machines
98% statement coverage with zero DOM dependencies. Unit tests with Vitest, property-based tests with fast-check, E2E tests with Playwright, coverage thresholds as quality gates, and the @FeatureTest / @Implements decorator system that links tests to requirements.
How to Read This Series
If you want the architecture: Start with Part I for motivation, then Part II for the pattern. Parts III and IV apply it at increasing complexity.
If you want the migration story: Jump to Part V (TypeScript) and Part VI (testing). They reference earlier parts but stand alone.
If you're here for the TOC complexity: Part IV is the climax. But read Part II first -- it introduces the vocabulary (factory function, callback injection, guard clause, geometry injection) that Part IV uses heavily.
The Machine Catalog
All 15 state machines, sorted by complexity:
| Machine | States | Lines | Part | Pattern |
|---|---|---|---|---|
| TocBreadcrumbMachine | — | 43 | IV | Pure functions |
| FontSizeManager | 5 steps | 47 | II | Factory + closure |
| AccentPaletteMachine | 2 | 91 | II | Factory + closure |
| TocTooltipMachine | 3 | 101 | IV | Factory + closure |
| PageLoadMachine | 6 | 103 | III | Factory + closure |
| KeyboardNavState | — | 105 | III | Pure functions |
| CopyFeedbackMachine | 4 | 105 | II | Factory + closure |
| TourStateMachine | 4 | 110 | III | Factory + closure |
| TocExpandMachine | 2/section | 111 | IV | Factory + closure |
| SidebarResizeMachine | 2 | 114 | II | Factory + closure |
| ScrollSpyMachine | — | 116 | III | Pure functions |
| ZoomPanState | 2 | 78 | III | Immutable reducer |
| HeadingsPanelMachine | 5 | 151 | IV | Factory + closure |
| TocScrollMachine | 8 | 163 | IV | Factory + closure |
| SpaNavMachine | 5 | 192 | III | Factory + closure |
Key Concepts by Part
This table maps each concept to the part where it's introduced:
| Concept | Introduced in | Used throughout |
|---|---|---|
| Factory function pattern | Part II (FontSizeManager) | All machines |
| Closure state | Part II (FontSizeManager) | 11 of 15 machines |
| Callback injection | Part II (AccentPaletteMachine) | 11 of 15 machines |
| Guard clauses | Part II (CopyFeedbackMachine) | All machines |
| Timer injection | Part II (CopyFeedbackMachine) | CopyFeedback, TocTooltip |
| Pure helper functions | Part II (SidebarResizeMachine) | Most machines |
| Navigation classification | Part III (SpaNavMachine) | SPA navigation |
| Generation counter | Part III (PageLoadMachine) | Page load staleness |
| Immutable reducer | Part III (ZoomPanState) | Zoom/pan overlays |
| Pure function pattern | Part III (ScrollSpyMachine) | ScrollSpy, Breadcrumbs, Keyboard |
| Modal priority routing | Part III (KeyboardNavState) | Escape chain |
| Geometry injection | Part IV (HeadingsPanelMachine) | HeadingsPanel, TocScroll |
| CSS animation as state | Part IV (HeadingsPanelMachine) | HeadingsPanel, SpaNav |
| User scroll locking | Part IV (TocScrollMachine) | TOC scroll orchestration |
| Wiring layer composition | Part IV (all 5 TOC machines) | All machines |
| Discriminated unions | Part V (TypeScript) | All state types |
| IIFE bundling | Part V (esbuild) | Browser delivery |
| Coverage thresholds | Part VI (Vitest) | Quality gates |
| Property-based testing | Part VI (fast-check) | Invariant verification |
| Requirements decorators | Part VI (@FeatureTest) | Compliance tracing |
Related
- This Website: From PDF to Terminal-Styled SPA in 72 Hours -- the origin story
- This Website: Going Static -- static generation and SEO
- FiniteStateMachine: Three Tiers, One Engine, Zero Exceptions -- the .NET FSM framework (different project, same philosophy)