diff --git a/.gitignore b/.gitignore index 2cd91ff..a08f07d 100644 --- a/.gitignore +++ b/.gitignore @@ -36,5 +36,5 @@ docs/superpowers/ # Local editor and tooling metadata .cursor/ .claude/ -AGENTS.md +/AGENTS.md .cursorrules diff --git a/README.md b/README.md index 6f73261..e0c2ba6 100644 --- a/README.md +++ b/README.md @@ -1,96 +1,111 @@ # namefailed.github.io -**Live:** https://mrgrey.site -**Alt:** https://namefailed.github.io/ +**Live:** [mrgrey.site](https://mrgrey.site) · **Alt:** [namefailed.github.io](https://namefailed.github.io/) -A personal portfolio built as an in-browser window manager — tiling layout, xterm.js terminal, a toy filesystem, seven colour themes, and optional CRT/matrix effects. +A personal portfolio built as an **in-browser desktop OS** — tiling window manager, xterm.js terminal, virtual filesystem, vim-style editor, interactive demos, and seven runtime colour themes. The same content also ships as a polished brochure at `/static/` (mobile default). -A second entry (`/static/`) serves the same portfolio content as a polished brochure page. Mobile visitors are redirected there automatically. +--- -## Stack +## At a glance -- TypeScript, Vite 8 (two HTML entry points) -- [@xterm/xterm](https://github.com/xtermjs/xterm.js) + fit + web-links addons -- Three.js (Rubik's cube — lazy-loaded in its own chunk) -- Web Audio API (sound effects) -- No framework — vanilla DOM throughout +| | | +|---|---| +| **Stack** | TypeScript · Vite 8 · vanilla DOM (no framework) | +| **Terminal** | [@xterm/xterm](https://github.com/xtermjs/xterm.js) + vim input layer | +| **3D** | Three.js (Rubik cube — lazy chunk only) | +| **Tests** | **563** unit · **3** e2e smoke · CI on every `main` push | +| **Deploy** | GitHub Actions → GitHub Pages (`dist/`) | + +--- + +## Why this exists + +Most portfolios list skills. This one **runs** them: modular TypeScript, lazy code splitting, keyboard-driven UX, tested pure helpers, and client-side persistence — packaged as something memorable to explore. + +**For reviewers:** start with [docs/OVERVIEW.md](docs/OVERVIEW.md) — architecture diagram, technical highlights, skills map. + +**For contributors & AI agents:** [docs/README.md](docs/README.md) — full documentation index. + +--- + +## Quick start + +```bash +npm install +npm run dev # desktop → http://localhost:5173/ +npm test # 563 unit tests +npm run build && npm run test:e2e +``` + +Open the terminal (`Ctrl+T`) and type `help`. + +--- ## Features ### Desktop shell -- Tiling window manager with floating dock and launcher overlay -- xterm.js terminal with vim-mode editing (insert / normal / visual) -- Seven switchable colour themes — Catppuccin Mocha, Dracula, Nord, Gruvbox Dark, Tokyo Night, Solarized Dark, One Dark -- Matrix rain backdrop and CRT scanline/vignette overlay (both toggleable and persistent) -- Virtual filesystem (VFS v8) backed by `localStorage` — `cat`, `ls`, `mkdir`, `touch`, `rm`, `mv`, `cp`, `edit`, `wc`, and more - -### Shell commands -- `resume` — full résumé with inline skills matrix in ANSI colour -- `projects` — portfolio project listing with links -- `contact`, `about`, `help`, `whoami`, `motd`, `fortune` -- `theme [id|list|random]` — switch colour packs at runtime -- `browse ` — embedded browser tile -- `edit [file]` — in-shell text editor backed by the VFS; `F5` / `:run` plays a `.js` file in the p5 viewer -- `p5` — p5.js sketch viewer; 8+ built-in sketches; `Open…` loads from VFS -- `cube` — interactive Rubik's cube (Three.js); drag to spin, U/D/L/R/F/B keys, animated scramble/solve, algorithm picker -- `snake`, `pong` — playable games -- `paint` — pixel canvas -- `ssh`, `apt`, `cowsay`, `neofetch`, `wc`, `matrix` — easter eggs and flavour commands - -### p5.js sketches -Pre-loaded in `~/sketches/` (VFS): -- Fractal Tree, Game of Life, Lorenz Attractor, Spirograph, Noise Terrain, Mandelbrot, Bouncing Balls, Sine Waves -- Create your own: `edit ~/sketches/myscript.js` → `F5` to run it live - -### Static brochure (`/static/`) -- Scroll progress bar, floating section-nav dots with tooltips -- Typewriter headline effect, animated stat counters -- Scroll-triggered fade-in animations (respects `prefers-reduced-motion`) -- Experience cards with role-type colour strips and a "Featured" badge -- Auto-redirect from mobile: viewport ≤ 768px lands on `/static/` by default - -### Keybinds +- BSP tiling window manager with drag splitters, floating dock, Applications launcher +- Lazy-loaded tiles: résumé, projects, editor, file explorer, browser, p5 viewer, games, Rubik cube +- xterm.js shell with 50+ commands and vim-style prompt editing +- VFS v8 in `localStorage` — real `edit` / `ls` / `mkdir` workflow +- Seven themes, CRT overlay, matrix rain, wallpaper, Web Audio UI sounds + +### Brochure (`/static/`) +- Scroll progress, section nav, typewriter hero, animated counters +- Auto-redirect on viewport ≤768px + +--- + +## Keyboard shortcuts + | Chord | Action | |-------|--------| | `Ctrl+T` | Open / focus terminal | -| `Ctrl+D` | Desktop / launcher | -| `Ctrl+H` | Focus terminal (← left) | -| `Ctrl+L` | Enter right pane (→) | -| `Ctrl+K` | Previous window (↑) | -| `Ctrl+J` | Next window (↓) | -| `Ctrl+Q` | Close focused window | -| `Ctrl+M` | Minimise focused window | -| `Ctrl+F` | Maximise / restore | -| `Ctrl+1–9` | Focus Nth open window | +| `Ctrl+D` | Applications launcher | +| `Ctrl+H/L/K/J` | Focus window ← → ↑ ↓ | +| `Ctrl+Q/M/F` | Close / minimize / maximize | +| `Ctrl+1–9` | Focus dock slot | + +Full list: `keybinds` in terminal or [docs/USER_GUIDE.md](docs/USER_GUIDE.md). + +--- + +## Documentation + +| Document | Audience | +|----------|----------| +| [docs/README.md](docs/README.md) | **Documentation hub** | +| [docs/OVERVIEW.md](docs/OVERVIEW.md) | Employers & technical reviewers | +| [docs/USER_GUIDE.md](docs/USER_GUIDE.md) | Site users | +| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Module layout & data flow | +| [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) | Local dev & change recipes | +| [docs/AGENTS.md](docs/AGENTS.md) | AI agents & automation | +| [docs/API.md](docs/API.md) | Types & extension APIs | +| [docs/THEMING.md](docs/THEMING.md) | Colour packs & CSS tokens | +| [docs/STYLE_GUIDE.md](docs/STYLE_GUIDE.md) | Coding standards | + +--- ## Scripts | Command | Description | |---------|-------------| -| `npm run dev` | Vite dev server — desktop at `/`, brochure at `/static/` | -| `npm run build` | `tsc` then Vite build → `dist/` and `dist/static/` | -| `npm run preview` | Preview `dist/` locally | -| `npm test` | Vitest — `*.test.ts` files | -| `npm run test:coverage` | Vitest with v8 coverage report | -| `npm run lint` | ESLint (TypeScript + e2e) | -| `npm run test:e2e` | Playwright smoke tests against `dist/` preview | +| `npm run dev` | Vite dev — `/` desktop, `/static/` brochure | +| `npm run build` | `tsc` + Vite → `dist/` | +| `npm run preview` | Preview production build | +| `npm test` | Vitest unit suite | +| `npm run test:coverage` | Coverage report | +| `npm run lint` | ESLint | +| `npm run test:e2e` | Playwright smoke tests | -## GitHub Pages +--- -The deploy publishes `dist/`, not the repo root. `index.html` at the root only works under `vite dev`. - -**One-time setup:** repo → Settings → Pages → Build and deployment → Source → **GitHub Actions**. Pushes to `main` build and deploy via `.github/workflows/deploy-pages.yml`. +## GitHub Pages -## Docs +Deploy publishes **`dist/`**, not the repo root. One-time: repo → Settings → Pages → Source → **GitHub Actions**. Pushes to `main` run lint, tests, build, e2e, then deploy (`.github/workflows/deploy-pages.yml`). -- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — module layout, bootstrap order, chunk strategy, testing -- [docs/THEMING.md](docs/THEMING.md) — ThemePack interface, custom property reference, how to add a pack -- [docs/STYLE_GUIDE.md](docs/STYLE_GUIDE.md) — TypeScript standards, CSS conventions, testing guidelines -- [docs/API.md](docs/API.md) — Window system, VFS, storage utilities, theming API +--- -## Testing +## License -428 unit tests across 27 test files (plus Playwright smoke e2e). -- `npm test` — run Vitest suite -- Tests co-located with source: `module.ts` → `module.test.ts` -- Coverage: VFS, vim input, storage, ANSI, CLI tools, window chrome, matrix rain, boot splash, rubik model, p5 sketches, launcher catalog, desktop tiles, desktop WM (focus/keyboard), static motion, intro toasts, hint bubbles, wallpaper, first-visit flags, BSP layout, theme control +Personal portfolio — source available for review; contact author for reuse. diff --git a/docs/AGENTS.md b/docs/AGENTS.md new file mode 100644 index 0000000..90230f1 --- /dev/null +++ b/docs/AGENTS.md @@ -0,0 +1,205 @@ +# Agent Guide + +Machine-oriented reference for AI coding agents, automation, and contributors who need **deterministic navigation** of this repository. + +--- + +## Mission + +Build and maintain an in-browser fake desktop portfolio. Primary constraints: + +1. **No UI framework** — vanilla TypeScript + DOM. +2. **Lazy tiles** — dynamic `import()` for every `*-window.ts` except `appwindow.ts`. +3. **Never move iframe-backed DOM nodes** after mount (p5, browser tiles reload if reparented). +4. **Persist via `storage.ts`** — handle private-mode failure gracefully. +5. **Test pure logic** in Node; stub DOM when testing layout/WM. + +--- + +## Entry-point graph + +``` +index.html + └─ src/main.ts + └─ bootstrap-shell.ts + ├─ boot-splash.ts + ├─ theme-control.ts (initThemeFromStorage) + ├─ retro-fx.ts, os-sound.ts, os-systray.ts + ├─ matrix-bg.ts (idle) + └─ new Desktop(#desktop) + ├─ bsp-layout.ts (#right-pane) + ├─ desktop-open-window.ts (lazy tiles) + └─ terminal.ts (lazy tile via openWindow) + +static/index.html + └─ src/static/main.ts (brochure only — no desktop imports) +``` + +--- + +## File → responsibility map + +| Path pattern | Role | +|--------------|------| +| `desktop.ts` | WM orchestrator — do not re-bloat; extract to `desktop-*.ts` | +| `desktop-wm-*.ts` | WM subsystems (lifecycle, maximize, sync, focus, …) | +| `desktop-open-window.ts` | **Single dispatch** for opening tiles | +| `desktop-window-spec.ts` | Build `WindowSpec` from command strings | +| `launcher-catalog.ts` | Dock pins, launcher grid, prefetch map | +| `*-window.ts` | Tile UI class (`el`, `command`, WM callbacks) | +| `terminal.ts` | xterm + command execution + `openWindow` bridge | +| `commands/index.ts` | Shell command registry merge point | +| `commands/app-commands.ts` | Tile stubs returning `[]` | +| `os-fs.ts` | VFS — key `portfolio-vfs-v8-namefailed-home` | +| `os-registry.ts` | Breaks circular import: terminal → desktop ref | +| `editor-vim-ops.ts` | Pure editor motions — **preferred home for new vim text helpers** | +| `editor-vim-keys.ts` | Pure editor key-chord helpers | +| `vim.ts` | Terminal one-line vim widget (separate from editor tile) | +| `bsp-layout.ts` | Two-column BSP + splitters | +| `splitter.ts` | Pointer drag resize | +| `theme-packs.ts` | All `--th-*` values per theme | +| `content/copy/*.ts` | Portfolio text sources | +| `portfolio.ts` | ANSI line arrays for content tiles | + +--- + +## Critical invariants + +| Invariant | Why | +|-----------|-----| +| `app-commands` handlers return `[]` | Desktop opens tiles; terminal must not print fake output | +| `setDesktopRef(this)` in Desktop ctor | Terminal `openWindow` routing | +| `TERMINAL_TILE_SENTINEL = '__terminal__'` | Dock/focus id for terminal tile | +| `focusedId === null` | No right-pane tile focused (not “legacy terminal focused”) | +| `#panes` contains only `#right-pane` | Terminal is a tile, not static HTML column | +| BSP `maxVisible = 4` | Fifth tile bumps oldest to dock | +| Editor buffer mutations | Prefer `editor-vim-ops.ts` + tests before touching `editor-window.ts` | + +--- + +## Change recipes + +### New shell command (no tile) + +``` +commands/-commands.ts → add Command entry +commands/-commands.test.ts → add test +commands/index.ts → already spreads submodule +``` + +### New tile command `myapp` + +``` +1. src/myapp-window.ts — tile class +2. desktop-open-window.ts — case in dispatchOpenWindow + dynamic import +3. commands/app-commands.ts — myapp: { run: () => [], loadMs: N } +4. launcher-catalog.ts — TILED_WINDOW_COMMANDS, LAUNCHER_ICON_ROWS, prefetch +5. desktop-window-spec.ts — if portfolio spec needed +6. tests for pure helpers only +``` + +### WM behaviour change + +Prefer editing the extracted module, not `desktop.ts`: + +| Concern | Module | +|---------|--------| +| Close/minimize animation | `desktop-wm-lifecycle.ts` | +| Maximize | `desktop-wm-maximize.ts` | +| Ctrl+chords | `desktop-keyboard-handler.ts` + `desktop-keyboard-chords.ts` | +| H/J/K/L focus | `desktop-spatial-focus.ts` | +| Shell CSS dataset | `desktop-wm-sync.ts` | +| Host bindings | `desktop-wm-hosts.ts` | + +### Editor vim motion + +``` +1. Add pure function to editor-vim-ops.ts +2. Test in editor-vim-ops.test.ts +3. Wire one-liner in editor-window.ts +``` + +--- + +## Test commands + +```bash +npm test # all unit tests +npm test -- src/foo.test.ts # single file +npm run lint +npm run build +npm run test:e2e # needs dist + playwright chromium +``` + +Vitest config: `vite.config.ts` → `test.environment: 'node'`. + +WM tests often use `FakeEl` trees — copy pattern from `src/desktop.test.ts` or `src/bsp-layout.test.ts`. + +--- + +## localStorage keys (authoritative) + +| Key | Module | Purpose | +|-----|--------|---------| +| `portfolio-vfs-v8-namefailed-home` | `os-fs.ts` | VFS JSON state | +| `mrgrey-theme` | `theme-control.ts` | Active theme id | +| `mrgrey-os-sound` | `os-sound.ts` | Sound enabled | +| `mrgrey-os-volume` | `os-sound.ts` | Volume 0–1 | +| `mrgrey-retro-fx` | `retro-fx.ts` | CRT overlay | +| `mrgrey-matrix-bg` | `matrix-bg.ts` | Matrix rain on/off | +| `mrgrey-wallpaper` | `wallpaper.ts` | Wallpaper URL | +| `mrgrey-desktop-tile-positions-v6` | `desktop-tiles.ts` | Folder tile drag layout | +| `portfolio-fe-prefs-v1` | `file-explorer-window.ts` | Explorer prefs | +| `mrgrey-browser-iframe-tip-dismiss` | `browser-window.ts` | Browser tip permanent dismiss | +| `mrgrey-boot-seen` | `boot-splash.ts` | Skip boot animation | +| `mrgrey-guide-seen` | `welcome-guide.ts` | Onboarding guide | +| `mrgrey-toasts-seen` | `intro-toasts.ts` | Intro toast sequence | +| `mrgrey-hint-` | `hint-bubbles.ts` | Per-tile hint dismissal | +| `mrgrey-p5-tip-seen` | `p5-window.ts` | p5 viewer tip | +| `mrgrey-pkgs-v1` | `os-packages.ts` | Installed joke packages | +| `mrgrey-apt-cowsay` | `os-apt.ts` | cowsay install flag | + +Always read/write through `storage.ts` helpers. + +--- + +## Custom events + +| Event | When | +|-------|------| +| `mrgrey-theme-change` | Theme applied | +| `mrgrey-wallpaper-change` | Wallpaper set/cleared | +| `mrgrey-first-window` | First tile opened (onboarding) | +| `mrgrey-terminal-cmd` | First terminal command run | + +--- + +## Docs to update when you change… + +| Change | Update | +|--------|--------| +| New module / layer | `docs/ARCHITECTURE.md` table | +| New storage key | `docs/ARCHITECTURE.md` + this file | +| New command / keybind | `docs/USER_GUIDE.md` + `help-output.ts` | +| New theme | `docs/THEMING.md` pack table | +| Public API / types | `docs/API.md` | +| Test count shift | `docs/README.md`, root `README.md` | + +--- + +## Anti-patterns + +- Adding static `#terminal-window` HTML — terminal is **lazy tile only** +- Importing `desktop.ts` from tiles — use callbacks + `os-registry.ts` +- Large new features entirely inside `desktop.ts` — extract module + tests +- Raw `localStorage.setItem` — use `storage.ts` +- Hard-coded colours — use `--th-*` from theme packs +- DOM-heavy tests without stubs — will fail in Node Vitest + +--- + +## See also + +- [DEVELOPMENT.md](./DEVELOPMENT.md) — human setup guide +- [ARCHITECTURE.md](./ARCHITECTURE.md) — full module catalogue +- [API.md](./API.md) — type definitions diff --git a/docs/API.md b/docs/API.md index 07f5056..e08f17f 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,61 +1,47 @@ # API Reference -Key interfaces and types for extending the portfolio OS. +Interfaces and extension points for the portfolio OS. For step-by-step recipes see [DEVELOPMENT.md](./DEVELOPMENT.md) and [AGENTS.md](./AGENTS.md). --- -## Window System +## Window system ### WindowSpec -The contract between terminal commands and the desktop tile system. +Contract between shell commands and the desktop tile system (`appwindow.ts`). ```typescript interface WindowSpec { - /** Unique tile id — the same CLI again focuses or closes this window */ + /** Unique tile id — same CLI again focuses or toggles this window */ command: string title: string content: string[] - - /** Virtual path for `edit` / `vim` (in-browser FS) */ - editorPath?: string - - /** Starting directory for `explorer` (vfs path) */ - explorerPath?: string - - /** Initial URL for embedded `browse` */ - browserUrl?: string - - /** When `command === 'resume'`, ANSI lines plus optional skills aside */ - resumeSkills?: string[] - - /** Résumé header lines (name/contact) — paired with resumeBody */ + + editorPath?: string // edit / vim / editor + explorerPath?: string // explorer starting directory + browserUrl?: string // browse initial URL + resumeSkills?: string[] // resume tile aside resumeLead?: string[] - - /** PROFILE … certifications — full-width under header row */ resumeBody?: string[] - - /** Thumbnail/metadata list for `projects` tile layout */ projectCards?: readonly PortfolioProjectEntry[] + p5SketchPath?: string // p5 viewer VFS path } ``` -### AppWindowOptions +Build specs from command names: `windowSpecForCommand()` in `desktop-window-spec.ts`. -Used when creating a new window tile. +### Opening tiles -```typescript -interface AppWindowOptions extends WindowSpec { - onClose: () => void - onMinimize: () => void - onMaximize: () => void - onFocus: () => void -} -``` +Flow: -### WindowChromeOptions +1. Terminal runs command → `app-commands` returns `[]` +2. `terminal.ts` calls `getDesktopRef().openWindow(spec)` +3. `desktop.ts` → `dispatchOpenWindow()` in `desktop-open-window.ts` +4. Dynamic `import('./foo-window')` → mount via `BspLayout` + +Do **not** instantiate tiles from command handlers directly. -Factory options for `createWindowChrome()`. +### WindowChromeOptions ```typescript interface WindowChromeOptions { @@ -64,82 +50,81 @@ interface WindowChromeOptions { onMinimize: () => void onMaximize: () => void onFocus?: () => void - /** When true, focus callback is also attached to titlebar mousedown. Default true. */ - focusOnTitlebar?: boolean + focusOnTitlebar?: boolean // default true } interface WindowChromeElements { - el: HTMLElement // Container (.app-window) - titlebar: HTMLElement // Title bar element - titleEl: HTMLElement // Title text span - btnClose: HTMLElement // Close button - btnMin: HTMLElement // Minimize button - btnMax: HTMLElement // Maximize button + el: HTMLElement + titlebar: HTMLElement + titleEl: HTMLElement + btnClose: HTMLElement + btnMin: HTMLElement + btnMax: HTMLElement } ``` +Factory: `createWindowChrome()` in `window-chrome.ts`. + +### TiledWin + +Union of all tile classes (`desktop-open-window.ts`): + +```typescript +type TiledWin = + | AppWindow | EditorWindow | FileExplorerWindow | BrowserWindow + | PaintWindow | SnakeWindow | PongWindow | RubikWindow | P5Window + | TerminalWindow +``` + +Each tile exposes: `el`, `command`, `onFocus`, `setActive()`, `setMinimized()`, `isMaximized()`, etc. + --- ## Commands -### Command Interface +### Command interface ```typescript interface Command { - /** One-line description shown in help */ description: string - - /** Handler function — receives arguments, returns lines to print */ run: (args: string[]) => string[] - - /** Hidden from `help` listing (e.g., deprecated aliases) */ hidden?: boolean - - /** Fake load time for window-spawning commands (milliseconds) */ - loadMs?: number + loadMs?: number // fake delay for tile commands } ``` -### Registering a Command +Registry: `commands/index.ts` merges `vfsCommands`, `systemCommands`, `appCommands`. -Add to `commands/index.ts`: +### Register a text command ```typescript -export const commands: Record = { - mycommand: { - description: 'Does something useful', - run: (args) => { - return ['Output line 1', 'Output line 2'] - }, +// commands/system-commands.ts +export const systemCommands = { + hello: { + description: 'Says hello', + run: args => [`Hello, ${args[0] ?? 'world'}!`], }, } ``` -For window-spawning commands, return empty array and handle via `desktop.ts`: +### Register a tile command ```typescript +// commands/app-commands.ts myapp: { - description: 'Opens my custom app', + description: 'Opens my app tile', loadMs: 400, - run: () => [], -} + run: () => [], // required — desktop opens the tile +}, ``` -Then in `desktop.ts` `openWindow()`: - -```typescript -case 'myapp': { - const win = new MyAppWindow({ ... }) - // ... attach to DOM - return -} -``` +Then add dispatch in `desktop-open-window.ts` and launcher entries in `launcher-catalog.ts`. --- -## Virtual Filesystem +## Virtual filesystem -### Node Types +### Node types ```typescript type FsDir = { t: 'd'; c: Record } @@ -147,181 +132,103 @@ type FsFile = { t: 'f'; body: string } type FsNode = FsDir | FsFile ``` -### Core Operations - -```typescript -// Path operations -export function vfsNormalize(input: string): string -export function vfsPwd(): string -export function vfsCd(path: string): string | null // error or null - -// Directory operations -export function vfsLs(path?: string): string[] -export function vfsLsLong(path?: string): VfsLongEntry[] -export function vfsMkdir(path: string): string | null // error or null - -// File operations -export function vfsCat(path: string): string | null // contents or error -export function vfsTouch(path: string): string | null // error or null -export function vfsWrite(path: string, body: string): string | null -export function vfsReadRaw(path: string): { ok: true; body: string } | { ok: false; err: string } - -// Utility -export function vfsFormatPath(abs: string): string // /home/namefailed → ~ -``` +Storage key: **`portfolio-vfs-v8-namefailed-home`** (`os-fs.ts`). -### Usage Example +### Core operations ```typescript -import { vfsCat, vfsWrite, vfsNormalize, FS_HOME } from './os-fs' - -// Read file -const content = vfsCat('/home/namefailed/notes.txt') -if (content !== null) { - console.log(content) -} - -// Write file -const err = vfsWrite('notes.txt', 'New content') -if (err) { - console.error('Write failed:', err) -} - -// Normalize path -const abs = vfsNormalize('~/Documents') // → /home/namefailed/Documents +vfsNormalize(input: string): string +vfsPwd(): string +vfsCd(path: string): string | null +vfsLs(path?: string): string[] +vfsLsLong(path?: string): VfsLongEntry[] +vfsCat(path: string): string | null +vfsTouch(path: string): string | null +vfsWrite(path: string, body: string): string | null +vfsReadRaw(path: string): { ok: true; body: string } | { ok: false; err: string } +vfsFormatPath(abs: string): string // /home/namefailed → ~ +vfsMkdir(path: string): string | null ``` +Debounced save: **150 ms** after mutations. + --- ## Storage -### Functions +Always use `storage.ts` — never raw `localStorage` in new code. ```typescript -/** Get item from localStorage; returns null on any error or if not found */ -export function storageGet(key: string): string | null - -/** Set item in localStorage; returns true on success, false on error */ -export function storageSet(key: string, value: string): boolean - -/** Remove item from localStorage; returns true on success, false on error */ -export function storageRemove(key: string): boolean - -/** Get and parse JSON from localStorage; returns fallback on error or if not found */ -export function storageGetJson(key: string, fallback: T): T - -/** Stringify and store JSON in localStorage; returns true on success */ -export function storageSetJson(key: string, value: T): boolean - -/** Get numeric value from localStorage with bounds checking */ -export function storageGetNumber(key: string, fallback: number, min?: number, max?: number): number - -/** Get boolean from localStorage (parses '1'/'0') */ -export function storageGetBool(key: string, fallback: boolean): boolean - -/** Store boolean as '1'/'0' */ -export function storageSetBool(key: string, value: boolean): boolean +storageGet(key: string): string | null +storageSet(key: string, value: string): boolean +storageRemove(key: string): boolean +storageGetJson(key: string, fallback: T): T +storageSetJson(key: string, value: T): boolean +storageGetNumber(key: string, fallback: number, min?: number, max?: number): number +storageGetBool(key: string, fallback: boolean): boolean +storageSetBool(key: string, value: boolean): boolean ``` -### Usage - -```typescript -import { storageSet, storageGetJson, storageGetBool } from './storage' - -// Simple values -storageSet('my-key', 'value') - -// JSON objects -interface Config { - theme: string - volume: number -} -const config = storageGetJson('my-config', { theme: 'mocha', volume: 0.72 }) - -// Booleans -const enabled = storageGetBool('feature-flag', true) -storageSetBool('feature-flag', false) -``` +### Keys reference + +| Key | Module | +|-----|--------| +| `portfolio-vfs-v8-namefailed-home` | `os-fs.ts` | +| `mrgrey-theme` | `theme-control.ts` | +| `mrgrey-os-sound` / `mrgrey-os-volume` | `os-sound.ts` | +| `mrgrey-retro-fx` | `retro-fx.ts` | +| `mrgrey-matrix-bg` | `matrix-bg.ts` | +| `mrgrey-wallpaper` | `wallpaper.ts` | +| `mrgrey-desktop-tile-positions-v6` | `desktop-tiles.ts` | +| `portfolio-fe-prefs-v1` | `file-explorer-window.ts` | +| `mrgrey-browser-iframe-tip-dismiss` | `browser-window.ts` | +| `mrgrey-boot-seen` | `boot-splash.ts` | +| `mrgrey-guide-seen` | `welcome-guide.ts` | +| `mrgrey-toasts-seen` | `intro-toasts.ts` | +| `mrgrey-hint-` | `hint-bubbles.ts` | +| `mrgrey-p5-tip-seen` | `p5-window.ts` | +| `mrgrey-pkgs-v1` | `os-packages.ts` | + +Full table: [AGENTS.md](./AGENTS.md#localstorage-keys-authoritative). --- ## Themes -### ThemePack Interface +### ThemePack ```typescript -export interface ThemePack { - id: string // slug used in localStorage and the `theme` command - label: string // display name shown in the picker - terminal: ITheme // xterm.js colour palette - matrixRain: string[] // 8 hex colours for matrix rain glyph tints - css: Record // maps every --th-* custom property to a value +interface ThemePack { + id: string + label: string + terminal: ITheme // xterm.js palette + matrixRain: string[] // 8 hex colours + css: Record // all --th-* properties } ``` -### Theme Control Functions +### Control functions ```typescript -/** Apply named theme; returns false if id is unknown */ -export function applyTheme(id: string): boolean - -/** Restore the saved pack at boot; falls back to Mocha */ -export function initThemeFromStorage(): void - -/** Returns the currently applied ThemePack */ -export function getActivePack(): ThemePack - -/** Returns the current pack id string */ -export function getThemeId(): string - -/** Returns { id, label }[] for the picker UI */ -export function listThemeSummaries(): ReadonlyArray<{ id: string; label: string }> -``` - -### Adding a Theme - -1. Define in `src/theme-packs.ts`: - -```typescript -const myThemeCss: Record = { - ...mochaCss, - '--th-desktop-bg': '#0d1117', - '--th-accent': '#ff6b6b', - // ... overrides -} - -const myTheme: ThemePack = { - id: 'my-theme', - label: 'My Theme', - terminal: { - background: '#0d1117', - foreground: '#c9d1d9', - // ... xterm palette - }, - matrixRain: ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7', '#dfe6e9', '#74b9ff', '#a29bfe'], - css: myThemeCss, -} +applyTheme(id: string): boolean +initThemeFromStorage(): void +getActivePack(): ThemePack +getThemeId(): string +listThemeSummaries(): ReadonlyArray<{ id: string; label: string }> ``` -2. Add to `THEME_PACKS` array: - -```typescript -export const THEME_PACKS: ThemePack[] = [ - // ... existing themes - myTheme, -] -``` +Adding a pack: [THEMING.md](./THEMING.md). --- -## Vim Input +## Vim input (terminal) -### VimInput Class +`vim.ts` — one-line shell editor (separate from editor tile). ```typescript -export type VimMode = 'insert' | 'normal' | 'visual' +type VimMode = 'insert' | 'normal' | 'visual' -export type InputAction = +type InputAction = | { type: 'none' } | { type: 'rendered' } | { type: 'submit'; value: string } @@ -330,163 +237,112 @@ export type InputAction = | { type: 'interrupt' } | { type: 'clear' } -export class VimInput { +class VimInput { mode: VimMode - - constructor(onModeChange: (mode: VimMode) => void) - + handleKey(ev: KeyboardEvent): InputAction getValue(): string setBuffer(text: string): void - setBufferInsert(text: string): void - clear(): void - render(): string - cursorBack(): number - handleKey(ev: KeyboardEvent): InputAction -} -``` - -### Usage - -```typescript -import { VimInput } from './vim' - -const vim = new VimInput((mode) => { - console.log('Mode changed to:', mode) -}) - -// In a keydown handler -const action = vim.handleKey(event) -switch (action.type) { - case 'submit': - console.log('Submit:', action.value) - break - case 'history': - console.log('Navigate history:', action.dir) - break - case 'complete': - triggerAutocomplete() - break + // ... } ``` --- -## ANSI Helpers +## Editor vim ops (tile) -### ansi.ts +Pure functions in `editor-vim-ops.ts` — safe to unit test without DOM: ```typescript -/** Convert ANSI escape sequences to HTML */ -export function ansiToHtml(input: string): string - -/** Convert ANSI to HTML with clickable link detection */ -export function ansiToHtmlWithLinks(input: string): string - -/** Strip all ANSI escape codes */ -export function stripAnsi(input: string): string - -/** Theme color helpers */ -export const c = { - pink: '\x1b[35m', - blue: '\x1b[34m', - green: '\x1b[32m', - yellow: '\x1b[33m', - red: '\x1b[31m', - cyan: '\x1b[36m', - white: '\x1b[37m', - dim: '\x1b[2m', - bold: '\x1b[1m', - reset: '\x1b[0m', -} +lineCountTotal(text: string): number +getLineCol(text: string, pos: number): { line: number; col: number } +moveVertPos(text: string, pos: number, delta: -1 | 1): number +moveHorizPos(text: string, pos: number, delta: -1 | 1, steps?: number): number +wordForwardPos / wordBackPos / wordEndForwardPos +findNextOnLine(text, kind, ch, fromPos): number | null +deleteLineBlockText / yankLineBlockText / joinLinesText / pasteYankText +// ... ``` -### Usage +Key chords: `editor-vim-keys.ts` — `insertModeKeyAction`, `tryAppendCountDigit`. -```typescript -import { ansiToHtmlWithLinks, c } from './ansi' +Ex commands: `parseEditorExCommand()` in `editor-ex-commands.ts`. + +--- -const output = [ - `${c.pink}Welcome${c.reset} to the terminal`, - `${c.dim}Type ${c.blue}help${c.reset}${c.dim} to get started${c.reset}`, -] +## ANSI -const html = output.map(line => ansiToHtmlWithLinks(line)).join('
') +```typescript +ansiToHtml(input: string): string +ansiToHtmlWithLinks(input: string): string +stripAnsi(input: string): string + +const c = { pink, blue, green, yellow, red, cyan, white, dim, bold, reset } ``` --- -## Event System +## Events & sound -### Custom Events +### Custom events -```typescript -// Theme change -window.dispatchEvent(new CustomEvent('mrgrey-theme-change')) - -// Matrix rain toggle -document.documentElement.dataset.matrixBg = 'on' | 'off' -``` +| Event | Detail | +|-------|--------| +| `mrgrey-theme-change` | none | +| `mrgrey-wallpaper-change` | `string \| null` URL | +| `mrgrey-first-window` | none | +| `mrgrey-terminal-cmd` | none | ### Sound ```typescript -import { playOsSound } from './os-sound' - -type OsSoundKind = 'focus' | 'click' | 'notify' | 'boot' - -playOsSound('focus') // Window focus -playOsSound('click') // Button click -playOsSound('notify') // Toast notification -playOsSound('boot') // System boot +playOsSound('focus' | 'click' | 'notify' | 'boot') ``` -### Toast Notifications +### Toasts ```typescript -import { pushToast } from './os-systray' - -pushToast('Message text') // Default timeout -pushToast('Message text', 5000) // 5 second timeout -pushToast('Message text', 0) // Persistent (manual dismiss) +pushToast(message: string, timeoutMs?: number) // 0 = persistent ``` --- -## Keyboard Shortcuts +## WM keyboard API -| Chord | Action | -|-------|--------| -| `Ctrl+H` | Focus terminal (← left) | -| `Ctrl+L` | Enter right pane (→) | -| `Ctrl+K` | Previous window (↑) | -| `Ctrl+J` | Next window (↓) | -| `Ctrl+Q` | Close focused window | -| `Ctrl+M` | Minimize focused window | -| `Ctrl+F` | Maximize / restore | -| `Ctrl+1–9` | Activate dock slot N | -| `Ctrl+T` | Focus terminal | -| `Ctrl+D` | Desktop / launcher | +Host interface for `desktop-keyboard-handler.ts`: + +```typescript +interface DesktopKeyboardHost { + openTerminal(): void + focusTaskbarIndex(index: number): void + focusSpatial(dir: 'h' | 'j' | 'k' | 'l'): void + closeFocusedOrTerminal(): void + minimizeFocusedOrTerminal(): void + toggleMaximizeFocused(): void + toggleShowDesktop(): void +} +``` + +Chord table: [USER_GUIDE.md](./USER_GUIDE.md). --- -## Storage Keys +## OS registry + +Breaks circular import between terminal and desktop: + +```typescript +// os-registry.ts +setDesktopRef(desktop: Desktop): void +getDesktopRef(): Desktop | null +``` -| Key | Purpose | Module | -|-----|---------|--------| -| `portfolio-vfs-v3-namefailed-home` | VFS tree and cwd | os-fs | -| `mrgrey-theme` | Selected theme id | theme-control | -| `mrgrey-os-sound` | Sound enabled flag | os-sound | -| `mrgrey-os-volume` | Volume level (0–1) | os-sound | -| `mrgrey-retro-fx` | CRT toggle | retro-fx | -| `mrgrey-matrix-bg` | Matrix rain toggle | matrix-bg | -| `mrgrey-browser-iframe-tip-dismiss` | Browser tip dismissed | browser-window | -| `portfolio-fe-prefs-v1` | File explorer prefs | file-explorer | -| `mrgrey-pkgs-v1` | Installed packages | os-packages | +Called once from `Desktop` constructor. --- -## See Also +## See also -- [ARCHITECTURE.md](./ARCHITECTURE.md) — Module structure and entry points -- [THEMING.md](./THEMING.md) — Detailed theme customization -- [STYLE_GUIDE.md](./STYLE_GUIDE.md) — Coding standards +- [ARCHITECTURE.md](./ARCHITECTURE.md) — module map +- [DEVELOPMENT.md](./DEVELOPMENT.md) — setup & workflows +- [AGENTS.md](./AGENTS.md) — machine-oriented guide +- [THEMING.md](./THEMING.md) — CSS token catalogue diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c068c13..d69cf27 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,264 +1,309 @@ # Architecture -Personal portfolio site built as an in-browser fake desktop environment. TypeScript throughout, Vite for bundling, no framework. All state persists to `localStorage`. +Technical map of the portfolio desktop OS. For a narrative introduction see [OVERVIEW.md](./OVERVIEW.md). For change recipes see [DEVELOPMENT.md](./DEVELOPMENT.md) and [AGENTS.md](./AGENTS.md). + +--- + +## System diagram + +```mermaid +sequenceDiagram + participant HTML as index.html + participant Main as main.ts + participant Boot as bootstrap-shell + participant Desk as Desktop + participant Term as terminal + participant Tile as *-window.ts + + HTML->>Main: load + Main->>Boot: bootstrapShellUi() + Boot->>Boot: theme, splash, sound, systray + Boot->>Desk: new Desktop(#desktop) + Note over Desk: BSP layout, dock, keyboard chords + Term->>Desk: openWindow(spec) via os-registry + Desk->>Tile: dynamic import() on first open +``` + +--- -## Entry Points +## Entry points | File | Purpose | |------|---------| -| `index.html` | Desktop shell — CRT monitor frame, YASB status bar, launcher overlay, tiling panes, floating dock | -| `static/index.html` | Brochure page — same portfolio content, no OS chrome. Mobile auto-redirects here (viewport ≤ 768px) | -| `src/main.ts` | Vite entry: imports CSS, calls `bootstrapShellUi()` | -| `src/bootstrap-shell.ts` | Startup sequence: boot splash → theme → retro-fx → sound → Desktop → matrix rain. Terminal is a lazy tile, not pre-mounted. | -| `src/static/main.ts` | Brochure entry: mounts banner, hero, sections, scroll-spy, animations | +| `index.html` | Desktop shell — monitor frame, YASB bar, launcher, `#panes` → `#right-pane` | +| `static/index.html` | Brochure — no OS chrome; mobile redirect target | +| `src/main.ts` | Imports CSS, calls `bootstrapShellUi()` | +| `src/bootstrap-shell.ts` | Boot splash → theme → wallpaper → retro FX → sound → systray → `Desktop` → matrix rain (idle) | +| `src/static/main.ts` | Brochure: hero, sections, scroll-spy, motion | + +**Terminal is not in static HTML.** It opens as a lazy tile (`Ctrl+T`, dock, or `terminal` command). + +--- + +## Bootstrap order + +1. `runBootSplash()` — skippable after first visit (`mrgrey-boot-seen`) +2. `initThemeFromStorage()` — apply `--th-*` tokens + xterm palette +3. `loadSavedWallpaper()` +4. `initRetroFxFromStorage()` +5. `initOsSound()` + `initSystray()` +6. `new Desktop(desktopEl)` — WM, dock, folder tiles, keyboard listener +7. `initMatrixBg()` — deferred via `requestIdleCallback` + +--- + +## Module layers + +### Window manager (`desktop*.ts`) -## Module Layers +`desktop.ts` (~340 lines) orchestrates extracted subsystems via `desktop-wm-hosts.ts`: -### Core Shell | Module | Responsibility | |--------|----------------| -| `desktop.ts` | WM orchestrator (~430 lines) — constructor, `wm()` delegate, public API | -| `desktop-wm-hosts.ts` | Host/context bindings into extracted WM modules | -| `desktop-open-window.ts` | Lazy tile open dispatch (editor, games, portfolio, terminal tile) | -| `desktop-taskbar.ts` | Dock rendering, YASB title, auto-hide hover zone | -| `desktop-wm-lifecycle.ts` | Tiled close / minimize / restore animations | -| `desktop-wm-maximize.ts` | Right-pane content maximize only | -| `desktop-wm-terminal.ts` | YASB launcher chrome (terminal is a lazy tile) | -| `desktop-keyboard-handler.ts` | Ctrl+chord dispatch | -| `desktop-spatial-focus.ts` | Ctrl+H/J/K/L geometry | -| `desktop-launcher-overlay.ts` | Show-desktop / Applications overlay flags + DOM sync | -| `desktop-launcher-grid.ts` | Applications overlay icon grid | -| `desktop-window-spec.ts` | `WindowSpec` builders for tiles and launcher | -| `desktop-wm-animations.ts` | Mount/unmount tile animations | -| `desktop-wm-tile-limit.ts` | Cap visible tiles (bump oldest to dock) | -| `desktop-wm-sync.ts` | Shell `dataset.*` mirrors for CSS | -| `desktop-ps-snapshot.ts` | Simulated `ps` rows for terminal MOTD | -| `editor-ex-commands.ts` | Modal editor `:w` / `:q` / `:e` ex-mode parsing | -| `editor-vim-ops.ts` | Pure caret/line/motion helpers for vim motions (`j`/`k`, `:N`, counts, dd/yy) | -| `editor-vim-keys.ts` | INSERT/NORMAL key-chord helpers (Esc, count prefix) | -| `editor-window-meta.ts` | Editor path compare + title string helpers | -| `terminal.ts` | xterm.js façade, scripted boot lines, Vim-style prompt, command dispatch | -| `bootstrap-shell.ts` | Boot sequence orchestration | -| `launcher-catalog.ts` | Launcher grid definitions, dock membership, lazy-chunk prefetch triggers | - -### Window Tiles (`*-window.ts`) -Each tile is self-contained with close/minimize/maximize/focus callbacks. All lazy-loaded except `appwindow.ts`. `desktop.ts` uses dynamic `import()` so each module ships in its own chunk. - -| Module | Type | Notes | -|--------|------|-------| -| `appwindow.ts` | Content | Read-only portfolio tiles (resume, projects, contact, about) | -| `editor-window.ts` | Editor | Vim-mode text editor over VFS; `F5` / `:run` plays a `.js` file in the p5 viewer | -| `file-explorer-window.ts` | Explorer | File browser with rename/cut/copy/paste/delete; double-click `.js` → p5 viewer | -| `browser-window.ts` | Browser | Iframe embed with URL bar, bookmarks | -| `paint-window.ts` | Canvas | Pixel canvas with brush, eraser, fill | -| `p5-window.ts` | Viewer | Sandboxed p5.js sketch viewer; loads built-in sketches + VFS files | -| `rubik-window.ts` | Game | Interactive 3D Rubik's cube (Three.js); drag to spin, animated scramble/solve, algorithm picker | -| `pong-window.ts` | Game | Pong vs CPU or P2 | -| `snake-window.ts` | Game | Snake with WASD/arrow controls | - -Three.js ships exclusively in the `rubik-window` lazy chunk (~139 kB gzipped). It is never in the main bundle. - -### Shared Utilities -| Module | Purpose | -|--------|---------| -| `storage.ts` | Safe `localStorage` wrapper with JSON helpers, error handling for private mode | -| `window-chrome.ts` | Shared window titlebar factory — eliminates duplication across tiles | -| `splitter.ts` | Drag-to-resize handle for horizontal/vertical splits | -| `theme.ts` / `theme-control.ts` / `theme-packs.ts` | Theme system — CSS custom properties, xterm palettes, matrix rain colors | -| `ansi.ts` | ANSI-to-HTML conversion for rendering terminal output | -| `ascii.ts` | ASCII art strings used in boot splash and neofetch | -| `boot-splash.ts` | Scripted boot animation lines | -| `hint-bubbles.ts` | Dismissable first-visit hints on desktop folder tiles | -| `intro-toasts.ts` | Auto-dismiss toast notifications on first visit | -| `matrix-bg.ts` | Canvas matrix rain animation with visibility pause optimization | -| `random-pick.ts` | Seeded random / weighted pick utilities | -| `retro-fx.ts` | CRT scanlines and vignette toggle | -| `vim.ts` | Vim-style line editor — insert/normal/visual modes, operators, motions | - -### Rubik Model (`rubik-*.ts`) -| Module | Purpose | -|--------|---------| -| `rubik-model.ts` | Pure cube state: face arrays, move functions, scramble generator, canonical algorithms | -| `rubik-stickers-layout.ts` | Three.js geometry helpers: sticker center, animation axis/angle, face outward vector | +| `desktop.ts` | Public API, `openWindow`, global key listener, sync | +| `desktop-wm-hosts.ts` | Context objects wired into WM modules | +| `desktop-open-window.ts` | Lazy tile dispatch (`dispatchOpenWindow`) | +| `desktop-wm-lifecycle.ts` | Mount / close / minimize / restore + animations | +| `desktop-wm-maximize.ts` | Content-window maximize (`max-content` on `#panes`) | +| `desktop-wm-focus.ts` | Focus tile / terminal tile | +| `desktop-wm-sync.ts` | `#desktop` `dataset.*` for CSS (counts, maximized, terminal closed) | +| `desktop-wm-tile-limit.ts` | Cap visible tiles; bump oldest to dock | +| `desktop-wm-animations.ts` | Mount/unmount animation classes | +| `desktop-wm-terminal.ts` | YASB Applications button chrome | +| `desktop-keyboard-handler.ts` | Ctrl+chord routing | +| `desktop-keyboard-chords.ts` | Allowed WM key set | +| `desktop-spatial-focus.ts` | Ctrl+H/J/K/L bounding-rect focus | +| `desktop-taskbar.ts` | Dock render, YASB title, auto-hide | +| `desktop-launcher-overlay.ts` | Show-desktop + launcher overlay state | +| `desktop-launcher-grid.ts` | Applications icon grid | +| `desktop-window-spec.ts` | `WindowSpec` builders from command names | +| `desktop-ps-snapshot.ts` | Fake `ps` rows for MOTD | +| `desktop-tiles.ts` | Draggable folder icons on workspace | +| `launcher-catalog.ts` | Dock pins, launcher rows, chunk prefetch | + +### Layout -### p5.js Sketches -| Module | Purpose | -|--------|---------| -| `p5-sketches.ts` | 8+ built-in p5.js sketch definitions (code as strings); `sketchFilename()` helper | -| `p5-window.ts` | Sandboxed iframe viewer; loads sketches from built-in list or VFS | +| Module | Responsibility | +|--------|----------------| +| `bsp-layout.ts` | Two-column BSP; shorter column first; max 4 visible | +| `splitter.ts` | Pointer drag resize (column width / row height) | +| `window-layout.ts` | `WindowLayout` interface | -Sketches are seeded into `~/sketches/` in the VFS (`os-fs.ts`) at first visit. +### Window tiles (`*-window.ts`) + +Self-contained classes with `el`, `command`, WM callbacks. Lazy-loaded except `appwindow.ts`. + +| Module | Type | +|--------|------| +| `appwindow.ts` | Portfolio content (resume, projects, contact, about) | +| `terminal.ts` | xterm.js shell + command dispatch | +| `editor-window.ts` | Modal vim editor over VFS | +| `file-explorer-window.ts` | VFS browser | +| `browser-window.ts` | iframe + URL bar | +| `paint-window.ts` | Pixel canvas | +| `p5-window.ts` | Sandboxed p5 viewer | +| `rubik-window.ts` | Three.js Rubik cube | +| `pong-window.ts` / `snake-window.ts` | Arcade games | + +Three.js ships **only** in the `rubik-window` lazy chunk. + +### Editor vim stack + +| Module | Responsibility | +|--------|----------------| +| `editor-window.ts` | DOM, modes, key routing, VFS I/O | +| `editor-vim-ops.ts` | Pure text/cursor/motion helpers (unit tested) | +| `editor-vim-keys.ts` | Pure INSERT/NORMAL key-chord helpers | +| `editor-ex-commands.ts` | `:w` / `:q` / `:e` ex-mode parsing | +| `editor-window-meta.ts` | Path compare, title strings | +| `vim.ts` | **Separate** — terminal one-line vim widget | + +### Fake OS (`os-*.ts`) -### Fake OS Layer (`os-*.ts`) | Module | Purpose | |--------|---------| -| `os-fs.ts` | Virtual filesystem backed by `localStorage` key `portfolio-vfs-v8-namefailed-home`. Debounced saves (150ms). Seeds `~/p5.js/` with all p5 examples on first visit | -| `os-sound.ts` | Web Audio API for UI sound effects | -| `os-systray.ts` | Toast notifications, settings panel | -| `os-registry.ts` | Shared ref for shell commands → Desktop (prevents circular imports) | -| `os-apt.ts` | `apt install` joke command | -| `os-packages.ts` | `cowsay` package simulation | +| `os-fs.ts` | VFS v8 — `portfolio-vfs-v8-namefailed-home` | +| `os-registry.ts` | `Desktop` ref for terminal → `openWindow` | +| `os-sound.ts` | Web Audio UI sounds | +| `os-systray.ts` | Toasts + settings panel | +| `os-apt.ts` / `os-packages.ts` | Joke package manager | ### Commands (`commands/`) + | File | Purpose | |------|---------| -| `index.ts` | Keyword-to-handler registry (thin merger of sub-registries) | -| `app-commands.ts` | Tile launcher stubs — commands that return `[]` and are intercepted by the desktop layer | -| `vfs-commands.ts` | VFS shell commands: `ls`, `cat`, `cd`, `mkdir`, `rm`, `mv`, `cp`, `touch`, `pwd`, `wc`, `tree` | -| `system-commands.ts` | System-flavour commands: `echo`, `env`, `date`, `whoami`, `neofetch`, `cowsay`, `ssh`, `apt` | -| `help-output.ts` | Help screen rendering | -| `cli-text-utils.ts` | Text utilities: `cal`, `wc`, human-readable bytes | -| `cli-fortunes.ts` | Echo flavor text | -| `types.ts` | `Command` interface | - -### Content (`content/`) -| File | Purpose | -|------|---------| -| `copy/resume-copy.ts` | Resume text, skill matrix data | -| `copy/about-copy.ts` | `whoami` output, neofetch column | -| `copy/contact-copy.ts` | Contact tile content | -| `copy/projects-catalog.ts` | Project listings | -| `portfolio.ts` | Assembled ANSI line arrays for each portfolio tile | +| `index.ts` | Registry merge | +| `app-commands.ts` | Tile stubs — `run: () => []`, desktop intercepts | +| `vfs-commands.ts` | `ls`, `cat`, `cd`, `mkdir`, … | +| `system-commands.ts` | `theme`, `neofetch`, `help`, … | +| `help-output.ts` | Help screens + `keybinds` legend | +| `cli-text-utils.ts` | `cal`, `wc`, bytes formatting | -### Static Site (`src/static/`) -| File | Purpose | +### Content + +| Path | Purpose | |------|---------| -| `main.ts` | Brochure page mount — progress bar, scroll-spy, typewriter, animated counters | -| `static-data.ts` | Single source of truth for profile, contact, skills, experience | -| `static.css` | Standalone stylesheet using `--plain-*` tokens (not `--th-*`) | +| `content/copy/*.ts` | Résumé, projects, about, contact copy | +| `portfolio.ts` | ANSI arrays for content tiles | +| `static/static-data.ts` | Brochure single source of truth | -## Testing +### Shared utilities + +| Module | Purpose | +|--------|---------| +| `storage.ts` | Safe localStorage wrapper | +| `window-chrome.ts` | Titlebar + traffic-light factory | +| `theme-packs.ts` / `theme-control.ts` | Runtime themes | +| `ansi.ts` | ANSI → HTML | +| `matrix-bg.ts` / `retro-fx.ts` / `wallpaper.ts` | Visual effects | +| `boot-splash.ts` / `welcome-guide.ts` / `hint-bubbles.ts` | Onboarding | + +### Rubik model + +| Module | Purpose | +|--------|---------| +| `rubik-model.ts` | Pure cube state — moves, scramble, algorithms | +| `rubik-stickers-layout.ts` | Three.js geometry helpers | + +--- + +## Data flow: command → tile + +```mermaid +flowchart LR + A[User types command] --> B[terminal.ts] + B --> C{commands registry} + C -->|app command returns []| D[os-registry Desktop ref] + D --> E[desktop.openWindow] + E --> F[dispatchOpenWindow] + F --> G[dynamic import *-window] + G --> H[bsp-layout.mount] +``` + +--- + +## Build & chunks + +Vite entry points (`vite.config.ts`): -563 tests across 46 test files (plus Playwright smoke e2e). Tests co-located with source: `module.ts` → `module.test.ts`. - -| Test File | Coverage | -|-----------|----------| -| `ansi.test.ts` | ANSI-to-HTML conversion | -| `boot-splash.test.ts` | Boot animation line arrays | -| `browser-url.test.ts` | URL normalization | -| `bsp-layout.test.ts` | BSP column routing, splitter placement | -| `commands/cli-text-utils.test.ts` | `cal`, `wc`, human-readable bytes | -| `commands/system-commands.test.ts` | System-flavour shell commands | -| `commands/vfs-commands.test.ts` | VFS shell commands | -| `content/portfolio.test.ts` | Portfolio content assembly | -| `desktop-tiles.test.ts` | Tile layout, drag snap, persistence | -| `desktop.test.ts` | Window manager: focus, keyboard chords, tile limit | -| `desktop-window-spec.test.ts` | Portfolio/tool WindowSpec builders | -| `desktop-spatial-focus.test.ts` | Ctrl+H/J/K/L spatial focus geometry | -| `desktop-launcher-overlay.test.ts` | Launcher overlay flag state machine | -| `folder-popup-layout.test.ts` | Folder popup placement vs viewport edges | -| `live-site-screenshot.test.ts` | mShots preview URL builder | -| `prefers-reduced-motion.test.ts` | Reduced-motion media query probe | -| `static/static-motion.test.ts` | Brochure typewriter/counter + reduced motion | -| `static-portfolio-href.test.ts` | Classic portfolio path resolution | -| `first-visit-flags.test.ts` | First-visit onboarding flags | -| `hint-bubbles.test.ts` | Hint bubble show/dismiss logic | -| `intro-toasts.test.ts` | Toast sequencing | -| `launcher-catalog.test.ts` | Launcher grid, TILED_WINDOW_COMMANDS, prefetch | -| `matrix-bg.test.ts` | Matrix rain canvas logic | -| `os-fs.test.ts` | Virtual filesystem operations | -| `p5-sketches.test.ts` | Sketch definitions, `sketchFilename()` | -| `random-pick.test.ts` | Random/weighted pick | -| `retro-fx.test.ts` | CRT toggle | -| `rubik-model.test.ts` | Cube model: all moves, inverses, scramble, algorithms | -| `rubik-stickers-layout.test.ts` | 3D sticker layout ↔ model move lock | -| `storage.test.ts` | localStorage wrapper, JSON serialization | -| `terminal-motd.test.ts` | MOTD rendering | -| `theme-control.test.ts` | Theme pack apply/persist | -| `vim.test.ts` | Vim input: modes, motions, operators, undo | -| `wallpaper.test.ts` | Wallpaper apply/clear events | -| `window-chrome.test.ts` | Window chrome factory | - -Run tests: `npm test` · coverage: `npm run test:coverage` · lint: `npm run lint` · e2e: `npm run test:e2e` +- `index.html` → main bundle + lazy chunks per tile +- `static/index.html` → separate brochure bundle + +`prefetchLazyWindowModule()` in `launcher-catalog.ts` warms chunks on launcher hover. + +--- ## Stylesheets -Desktop shell CSS lives under `src/styles/` (`section.css` plus `section-2.css` … `section-22.css`). `src/style.css` is an `@import` hub only — regenerate with `node scripts/split-style-css.mjs` after editing the monolith backup if needed. +| Area | Location | +|------|----------| +| Desktop | `src/styles/section.css` + `section-2.css` … `section-22.css` via `src/style.css` | +| Brochure | `src/static/static.css` (`--plain-*` tokens) | -Brochure styles remain in `src/static/static.css` (`--plain-*` tokens). +Regenerate split CSS: `node scripts/split-style-css.mjs` -## Naming Conventions +--- -- **Storage/Init**: `initXFromStorage` — reads and applies persisted state once at boot -- **Window Contract**: `openWindow` / `WindowSpec` — contract between terminal and desktop tiles -- **Tile Suffix**: `*-window.ts` — self-contained tile. If it doesn't end in `-window.ts`, it isn't a tile. -- **OS Prefix**: `os-*.ts` — fake OS layer modules -- **Verb-First**: `runShellHelp`, `renderKeybindsLegend`, `playOsSound`, `pushToast` -- **Private Methods**: `handleKey()`, `syncDom()` — private methods don't use `_` prefix (class-based privacy) +## State persistence -## Key Patterns +All client-side via `storage.ts`: -### Window Chrome (window-chrome.ts) -```typescript -import { createWindowChrome } from './window-chrome' - -const { el, titlebar, titleEl, btnClose, btnMin, btnMax } = createWindowChrome({ - title: 'Window Title', - onClose: () => {}, - onMinimize: () => {}, - onMaximize: () => {}, - onFocus: () => {}, -}) -``` +| Key | Contents | +|-----|----------| +| `portfolio-vfs-v8-namefailed-home` | VFS tree + cwd | +| `mrgrey-theme` | Theme id | +| `mrgrey-os-sound` / `mrgrey-os-volume` | Audio prefs | +| `mrgrey-retro-fx` | CRT toggle | +| `mrgrey-matrix-bg` | Matrix rain | +| `mrgrey-wallpaper` | Wallpaper URL | +| `mrgrey-desktop-tile-positions-v6` | Folder tile positions | +| `portfolio-fe-prefs-v1` | File explorer prefs | +| `mrgrey-boot-seen` / `mrgrey-guide-seen` / `mrgrey-toasts-seen` | Onboarding flags | -### Storage Utility (storage.ts) -```typescript -import { storageGet, storageSet, storageGetJson, storageSetJson, storageGetBool } from './storage' +Full list: [AGENTS.md](./AGENTS.md#localstorage-keys-authoritative). -storageSet('key', 'value') // Returns boolean success -storageGetJson('key', {}) // Returns fallback on error -storageGetBool('key', true) // Parses '1'/'0' strings -``` +Bump VFS key version in `os-fs.ts` to reset visitor filesystems. + +--- + +## Testing -### Lazy Window Dispatch -`desktop.ts` uses `await import('./module-name')` to open each tile. This keeps all window code out of the main bundle. The `prefetchLazyWindowModule()` function in `launcher-catalog.ts` fires the same import on hover/focus to warm the chunk before the user clicks. +**563 tests** · **46 files** · Vitest in Node · Playwright e2e smoke. -### Theme System -Themes are self-contained `ThemePack` objects in `theme-packs.ts`. Adding a theme requires no other file changes — the picker and `theme` command auto-detect via array iteration. +### By domain -## Build & Deploy +| Domain | Example test files | +|--------|-------------------| +| WM / desktop | `desktop.test.ts`, `desktop-wm-*.test.ts`, `desktop-keyboard-handler.test.ts`, `bsp-layout.test.ts` | +| Editor vim | `editor-vim-ops.test.ts`, `editor-vim-keys.test.ts`, `editor-ex-commands.test.ts` | +| Terminal / vim | `vim.test.ts`, `terminal-motd.test.ts` | +| VFS / commands | `os-fs.test.ts`, `commands/*.test.ts` | +| Tiles / launcher | `launcher-catalog.test.ts`, `desktop-open-window.test.ts`, `window-chrome.test.ts` | +| Rubik | `rubik-model.test.ts`, `rubik-stickers-layout.test.ts` | +| Theme / FX | `theme-control.test.ts`, `matrix-bg.test.ts`, `retro-fx.test.ts`, `wallpaper.test.ts` | +| Brochure | `static/static-motion.test.ts`, `static-portfolio-href.test.ts` | +| Content | `content/portfolio.test.ts`, `p5-sketches.test.ts` | ```bash -npm install -npm run dev # Vite dev server — desktop at /, brochure at /static/ -npm run build # tsc && vite build → dist/ and dist/static/ -npm test # Vitest -npm run preview # Preview dist/ locally +npm test +npm run test:coverage +npm run test:e2e ``` -Deploy publishes `dist/` to GitHub Pages via Actions (`.github/workflows/deploy-pages.yml`). +--- -## State Persistence +## Naming conventions -All state in `localStorage`: +| Pattern | Meaning | +|---------|---------| +| `*-window.ts` | Lazy-loaded tile | +| `desktop-wm-*` | Window manager subsystem | +| `os-*` | Fake OS layer | +| `initXFromStorage` | Boot-time restore from localStorage | +| `editor-vim-*` | Pure editor helpers | -| Key | Contents | -|-----|----------| -| `portfolio-vfs-v8-namefailed-home` | VFS tree and cwd (bump version to force fresh defaults) | -| `mrgrey-theme` | Selected theme id | -| `mrgrey-os-sound` / `mrgrey-os-volume` | Sound on/off and volume | -| `mrgrey-retro-fx` | CRT overlay toggle | -| `mrgrey-matrix-bg` | Matrix rain toggle | -| `mrgrey-browser-iframe-tip-dismiss` | Browser tile tip dismissal | -| `mrgrey-desktop-tile-positions` | Desktop tile drag positions | -| `portfolio-fe-prefs-v1` | File explorer preferences | +Details: [STYLE_GUIDE.md](./STYLE_GUIDE.md). + +--- + +## Key patterns -No backend required. +### Window chrome + +```typescript +const { el, titleEl } = createWindowChrome({ + title: 'My Tile', + onClose, onMinimize, onMaximize, onFocus, +}) +``` + +### Lazy open + +```typescript +// desktop-open-window.ts +case 'myapp': { + const { MyAppWindow } = await import('./myapp-window') + // mount via layout + push to windows[] +} +``` -## Layers (narrative summary) +### Theme pack -**`content/`** — Portfolio copy. Long text lives under `content/copy/`. `portfolio.ts` assembles ANSI line arrays for each tile (resume, whoami, projects, links). +Self-contained `ThemePack` in `theme-packs.ts` — picker auto-detects new entries. -**`commands/`** — `index.ts` is the keyword-to-handler map. Split into `app-commands.ts` (tile stubs), `vfs-commands.ts` (filesystem), `system-commands.ts` (OS-flavour text commands). +--- -**`desktop.ts` + `launcher-catalog.ts`** — `desktop.ts` owns tiling layout, dock, focus, minimize/maximize, and all Ctrl-chord keyboard handling. `launcher-catalog.ts` holds grid definitions and dock membership. +## CI / deploy -**`*-window.ts`** — One file per tile. Each takes close/minimize/maximize/focus callbacks. All lazy-loaded on first open via `await import()`. Three.js is inside `rubik-window` only. +`.github/workflows/deploy-pages.yml`: -**`rubik-model.ts`** — Pure cube state with no DOM dependency. All move functions, inverse sequences, scramble generator, and named algorithms live here and are fully unit-tested. +`lint` → `npm test` → `npm run build` → `npm run test:e2e` → upload `dist/` → GitHub Pages -**`p5-sketches.ts`** — Sketch definitions (code as strings). Seeded into the VFS by `os-fs.ts` on first visit. `p5-window.ts` renders them in a sandboxed iframe. +--- -**`os-*.ts`** — Fake OS layer. `os-fs.ts` is VFS v8 backed by localStorage. Bumping the version number forces fresh default trees for returning visitors. +## See also -**`theme-control.ts` + `theme-packs.ts`** — Theme packs define CSS custom properties (`--th-*`), xterm palette, and matrix rain colors. `theme-control.ts` applies them and persists the selection. +- [OVERVIEW.md](./OVERVIEW.md) — project narrative for reviewers +- [API.md](./API.md) — type reference +- [AGENTS.md](./AGENTS.md) — agent change recipes +- [THEMING.md](./THEMING.md) — `--th-*` catalogue diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 0000000..8493d30 --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,159 @@ +# Development Guide + +Local setup, verification, and common change workflows. + +--- + +## Prerequisites + +- **Node.js 22+** (matches CI) +- npm (comes with Node) + +--- + +## Quick start + +```bash +git clone https://github.com/namefailed/namefailed.github.io.git +cd namefailed.github.io +npm install +npm run dev +``` + +| URL | Entry | +|-----|-------| +| http://localhost:5173/ | Desktop shell | +| http://localhost:5173/static/ | Brochure | + +Production build: + +```bash +npm run build # tsc + vite → dist/ +npm run preview # serve dist/ locally +``` + +--- + +## Verification checklist + +Run before opening a PR: + +```bash +npm run lint # ESLint +npm test # Vitest — 563 unit tests +npm run build # TypeScript + Vite +npm run test:e2e # Playwright (requires preview build) +``` + +CI (`.github/workflows/deploy-pages.yml`) runs the same sequence on every push to `main`. + +Coverage report (optional): + +```bash +npm run test:coverage # output in coverage/ (gitignored) +``` + +Watch mode during development: + +```bash +npm run test:watch +``` + +--- + +## Project conventions + +| Topic | Rule | +|-------|------| +| Tests | Co-located: `foo.ts` → `foo.test.ts` | +| Test env | Node (no real DOM) — stub `document` / `localStorage` when needed | +| Tiles | Filename ends in `-window.ts` | +| OS layer | Prefix `os-` | +| WM helpers | Prefix `desktop-wm-` or `desktop-` | +| Storage | Always use `storage.ts` wrapper, never raw `localStorage` in new code | +| CSS tokens | Use `--th-*` / `--ui-*` vars — see [THEMING.md](./THEMING.md) | + +Full coding standards: [STYLE_GUIDE.md](./STYLE_GUIDE.md). + +--- + +## Common tasks + +### Add a shell command (text only) + +1. Add handler in `src/commands/system-commands.ts` or `vfs-commands.ts`. +2. Export spreads into `src/commands/index.ts` automatically via submodule import. +3. Add tests in matching `*.test.ts`. +4. Update help groups in `help-output.ts` if the command should appear in a section. + +### Add a window tile + +1. Create `src/my-app-window.ts` implementing close/minimize/maximize/focus callbacks. +2. Add lazy import branch in `src/desktop-open-window.ts` → `dispatchOpenWindow`. +3. Register command in `src/commands/app-commands.ts` (returns `[]` — desktop intercepts). +4. Add to `TILED_WINDOW_COMMANDS` in `launcher-catalog.ts` if it should appear in launcher. +5. Add `prefetchLazyWindowModule` case for hover warmup. +6. Add tests for any pure logic; WM integration may use `desktop.test.ts` patterns. + +See [API.md](./API.md) for `WindowSpec` and `createWindowChrome`. + +### Add a colour theme + +1. Define pack in `src/theme-packs.ts` (spread `mochaCss`, override deltas). +2. Append to `THEME_PACKS` array — picker and `theme` command auto-detect. + +Details: [THEMING.md](./THEMING.md). + +### Bump VFS schema + +1. Change `STORAGE_KEY` version in `src/os-fs.ts` (e.g. `v8` → `v9`). +2. Update default seed tree if needed. +3. Update docs: [ARCHITECTURE.md](./ARCHITECTURE.md) persistence table. + +Returning visitors get a fresh tree; old data is orphaned under the previous key. + +### Edit desktop CSS + +Desktop styles live in `src/styles/section*.css`, imported via `src/style.css`. + +Regenerate split files from monolith backup (if used): + +```bash +node scripts/split-style-css.mjs +``` + +Brochure CSS is separate: `src/static/static.css` (`--plain-*` tokens). + +--- + +## Debugging tips + +| Issue | Check | +|-------|-------| +| Tile doesn't open | Browser console; `dispatchOpenWindow` switch; command in `app-commands.ts` | +| Terminal command no-op | App commands return `[]` — desktop must handle via `openWindow` | +| Theme not applying | `theme-control.ts`; `--th-*` keys all present in pack | +| Test fails in Node | Missing DOM stub — see `bsp-layout.test.ts` or `desktop.test.ts` FakeEl patterns | +| iframe reloads on layout | BSP never moves iframe elements after mount — see `bsp-layout.ts` comment | + +--- + +## Deploy + +GitHub Pages serves **`dist/`**, not the repo root. + +1. Repo → Settings → Pages → Source → **GitHub Actions** +2. Push to `main` triggers build + deploy + +Custom domain: configured in repo settings (currently `mrgrey.site`). + +--- + +## Further reading + +| Doc | When | +|-----|------| +| [ARCHITECTURE.md](./ARCHITECTURE.md) | Module map and data flow | +| [AGENTS.md](./AGENTS.md) | Machine-oriented repo guide | +| [API.md](./API.md) | Type reference | +| [USER_GUIDE.md](./USER_GUIDE.md) | End-user features | diff --git a/docs/OVERVIEW.md b/docs/OVERVIEW.md new file mode 100644 index 0000000..8e43938 --- /dev/null +++ b/docs/OVERVIEW.md @@ -0,0 +1,147 @@ +# Project Overview + +**Live:** [mrgrey.site](https://mrgrey.site) · **Source:** [github.com/namefailed/namefailed.github.io](https://github.com/namefailed/namefailed.github.io) + +This repository is a **personal portfolio implemented as a fake desktop operating system** in the browser. Instead of scrolling through a PDF résumé, visitors open windows, run shell commands, edit files in a vim-style editor, and explore interactive demos — while the same content is also available as a conventional brochure at `/static/`. + +--- + +## What problem it solves + +Traditional portfolio sites show information. This one **demonstrates how the author builds software**: structured TypeScript, tested modules, lazy-loaded bundles, keyboard-driven UX, and persistent client-side state — all without a UI framework. + +The desktop metaphor is not decorative. It is the architecture: + +- **Terminal commands** map to **window tiles** (résumé, projects, editor, games). +- A **virtual filesystem** backs real read/write operations in `localStorage`. +- A **tiling window manager** handles focus, minimize, maximize, and spatial navigation. +- **Seven colour themes** swap the entire UI and terminal palette at runtime. + +--- + +## High-level architecture + +```mermaid +flowchart TB + subgraph entries [Entry points] + IDX[index.html] + STA[static/index.html] + end + + subgraph desktop [Desktop shell] + BOOT[bootstrap-shell.ts] + DESK[desktop.ts] + TERM[terminal.ts] + BSP[bsp-layout.ts] + end + + subgraph tiles [Lazy window tiles] + APP[appwindow.ts] + EDIT[editor-window.ts] + GAMES[games / rubik / p5 …] + end + + subgraph os [Fake OS layer] + VFS[os-fs.ts] + CMD[commands/] + THEME[theme-packs.ts] + end + + IDX --> BOOT --> DESK + BOOT --> TERM + DESK --> BSP + DESK -->|openWindow| tiles + TERM --> CMD + CMD --> VFS + DESK --> VFS + BOOT --> THEME + STA --> BRO[static/main.ts] +``` + +--- + +## Technical highlights + +### No framework — deliberate choice + +The UI is built with **vanilla TypeScript and DOM APIs**. State lives in class instances and `localStorage`, not in a global store. This keeps bundle size predictable and makes every interaction traceable in source. + +### Code splitting by feature + +Each window tile (`editor-window.ts`, `rubik-window.ts`, etc.) is loaded with **dynamic `import()`** on first open. Three.js (~140 kB gzipped) never touches the main bundle — it ships only inside the Rubik cube chunk. + +### Window manager extracted into modules + +`desktop.ts` (~340 lines) orchestrates subsystems that were split for testability: + +| Module | Responsibility | +|--------|----------------| +| `desktop-open-window.ts` | Dispatch which tile to open | +| `desktop-wm-lifecycle.ts` | Mount / close / minimize animations | +| `desktop-wm-maximize.ts` | Full-pane maximize | +| `desktop-spatial-focus.ts` | Ctrl+H/J/K/L geometry | +| `desktop-taskbar.ts` | Dock and YASB status bar | +| `bsp-layout.ts` | Two-column tiling with drag splitters | + +### Two vim implementations + +| Layer | Module | Purpose | +|-------|--------|---------| +| Terminal one-liner | `vim.ts` | Shell prompt — insert/normal/visual, history, completion | +| Modal editor tile | `editor-window.ts` + `editor-vim-ops.ts` | Full buffer editor over the VFS — motions, operators, ex commands | + +Pure caret helpers in `editor-vim-ops.ts` are **unit-tested without a DOM**, which keeps editor logic maintainable. + +### Virtual filesystem (VFS v8) + +Files persist in `localStorage` under `portfolio-vfs-v8-namefailed-home`. First visit seeds `~/sketches/` with p5.js examples and `~/p5.js/` with reference material. Shell commands (`ls`, `cat`, `edit`, `mkdir`, …) operate on this tree. + +### Test coverage and CI + +| Stage | Tool | +|-------|------| +| Unit | Vitest — **563 tests**, **46 files**, Node environment with DOM stubs where needed | +| Lint | ESLint 9 + TypeScript-eslint | +| E2e | Playwright — production build smoke (desktop shell, brochure, nav) | +| Deploy | GitHub Actions → `dist/` → GitHub Pages | + +Every push to `main` runs the full pipeline before deploy. + +### Accessibility and motion + +- `prefers-reduced-motion` respected in brochure animations and WM transitions +- Keyboard-first navigation documented in `keybinds` shell command +- Skip links and ARIA labels on window chrome + +--- + +## Dual entry: desktop vs brochure + +| Route | Audience | Behaviour | +|-------|----------|-----------| +| `/` | Desktop experience | Full OS chrome, terminal, tiling | +| `/static/` | Mobile + print-friendly | Scroll-based résumé; viewport ≤768px auto-redirects here | + +Both entries share portfolio **content** (`src/content/`, `src/static/static-data.ts`) but use separate CSS token systems (`--th-*` vs `--plain-*`). + +--- + +## Skills this project demonstrates + +- **TypeScript** — strict typing, module boundaries, no `any` +- **Browser APIs** — Canvas, Web Audio, `localStorage`, dynamic imports, `ResizeObserver` +- **UX engineering** — keyboard chords, spatial focus, lazy prefetch on launcher hover +- **Graphics** — xterm.js integration, Three.js Rubik cube, p5.js sandboxed viewer +- **Testing** — co-located unit tests, fake DOM stubs, CI-gated e2e +- **Documentation** — architecture docs, API reference, agent-oriented guides + +--- + +## Where to go next + +| Goal | Document | +|------|----------| +| Try the site | [USER_GUIDE.md](./USER_GUIDE.md) | +| Understand modules in depth | [ARCHITECTURE.md](./ARCHITECTURE.md) | +| Set up locally | [DEVELOPMENT.md](./DEVELOPMENT.md) | +| Extend commands or tiles | [API.md](./API.md) + [AGENTS.md](./AGENTS.md) | diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..cedab9a --- /dev/null +++ b/docs/README.md @@ -0,0 +1,61 @@ +# Documentation + +Personal portfolio site ([mrgrey.site](https://mrgrey.site)) built as an in-browser desktop OS — tiling window manager, xterm.js shell, virtual filesystem, games, and a separate mobile brochure. + +--- + +## Start here + +| If you are… | Read this | +|-------------|-----------| +| **Hiring / reviewing the project** | [OVERVIEW.md](./OVERVIEW.md) — what it is, why it was built, technical highlights | +| **Using the site** | [USER_GUIDE.md](./USER_GUIDE.md) — keyboard shortcuts, shell commands, tiles | +| **Contributing or extending** | [DEVELOPMENT.md](./DEVELOPMENT.md) — setup, tests, common change recipes | +| **An AI agent or automation** | [AGENTS.md](./AGENTS.md) — repo map, invariants, where to edit what | +| **Changing visuals / themes** | [THEMING.md](./THEMING.md) — colour packs and `--th-*` tokens | +| **Looking up types and APIs** | [API.md](./API.md) — interfaces, storage, commands, events | + +--- + +## Reference + +| Document | Contents | +|----------|----------| +| [ARCHITECTURE.md](./ARCHITECTURE.md) | Module layers, bootstrap order, build chunks, persistence, test map | +| [STYLE_GUIDE.md](./STYLE_GUIDE.md) | TypeScript, CSS, testing, and commit conventions | +| [THEMING.md](./THEMING.md) | `ThemePack` structure and custom property catalogue | +| [API.md](./API.md) | `WindowSpec`, VFS, storage, vim input, events | + +--- + +## Project health (main branch) + +| Metric | Value | +|--------|-------| +| Unit tests | **563** across **46** files (`npm test`) | +| E2e smoke | **3** Playwright specs (`npm run test:e2e`) | +| CI | lint → unit tests → build → e2e → GitHub Pages deploy | +| Stack | TypeScript, Vite 8, vanilla DOM — no React/Vue/Svelte | +| Bundle strategy | Lazy `import()` per window tile; Three.js isolated to Rubik chunk | + +--- + +## Repository layout + +``` +namefailed.github.io/ +├── index.html # Desktop shell entry +├── static/index.html # Brochure entry (mobile default) +├── src/ +│ ├── main.ts # Desktop bootstrap +│ ├── desktop.ts # Window manager orchestrator (~340 lines) +│ ├── desktop-*.ts # WM subsystems (focus, lifecycle, taskbar, …) +│ ├── *-window.ts # Lazy-loaded tiles +│ ├── commands/ # Shell command registry +│ ├── content/ # Portfolio copy +│ ├── os-*.ts # Fake OS (VFS, sound, systray) +│ └── static/ # Brochure-only code + CSS +├── docs/ # ← you are here +├── e2e/ # Playwright smoke tests +└── .github/workflows/ # CI + Pages deploy +``` diff --git a/docs/STYLE_GUIDE.md b/docs/STYLE_GUIDE.md index a278fff..c43a289 100644 --- a/docs/STYLE_GUIDE.md +++ b/docs/STYLE_GUIDE.md @@ -1,6 +1,6 @@ # Style Guide -Coding standards and best practices for the portfolio codebase. +Coding standards for the portfolio codebase. For AI agent workflows see [AGENTS.md](./AGENTS.md). For setup see [DEVELOPMENT.md](./DEVELOPMENT.md). --- @@ -417,11 +417,16 @@ Add storage.ts: centralized localStorage wrapper Keep docs in `docs/` updated when making architectural changes: -- `ARCHITECTURE.md` — module structure, entry points, patterns -- `THEMING.md` — custom properties, adding themes -- `STYLE_GUIDE.md` — this file (coding standards) +| Doc | Update when… | +|-----|----------------| +| [ARCHITECTURE.md](./ARCHITECTURE.md) | New modules, bootstrap changes, persistence keys | +| [API.md](./API.md) | Public types, extension points | +| [AGENTS.md](./AGENTS.md) | New invariants, storage keys, file-map changes | +| [USER_GUIDE.md](./USER_GUIDE.md) | New commands or keybinds | +| [THEMING.md](./THEMING.md) | New themes or `--th-*` tokens | +| [README.md](../README.md) | Test counts, stack versions, deploy notes | -When adding new modules, update the relevant tables in ARCHITECTURE.md. +When adding modules, update the relevant tables — do not leave docs stale. --- @@ -474,6 +479,7 @@ el.innerHTML = escapeHtml(userInput) ## Tooling - `npm run lint` — ESLint (TypeScript, e2e specs, maintainer scripts) +- `npm test` — **563** Vitest unit tests (46 files) - `npm run test:coverage` — Vitest v8 coverage (`coverage/` gitignored) - `npm run test:e2e` — Playwright smoke against `vite preview` - Desktop CSS: `src/styles/*.css` via `src/style.css` hub; regenerate with `node scripts/split-style-css.mjs` diff --git a/docs/THEMING.md b/docs/THEMING.md index e480293..1e67918 100644 --- a/docs/THEMING.md +++ b/docs/THEMING.md @@ -1,6 +1,8 @@ # Theming -The site ships with seven colour packs selectable at runtime via the `theme` shell command or the settings panel. Each pack is a self-contained object—no separate CSS files, no build step. +Seven colour packs selectable at runtime via the `theme` shell command or the settings panel. Each pack is a self-contained object — no separate CSS files, no build step. + +**See also:** [API.md](./API.md#themes) · [ARCHITECTURE.md](./ARCHITECTURE.md) · [USER_GUIDE.md](./USER_GUIDE.md#themes-and-effects) ## ThemePack interface @@ -112,12 +114,14 @@ const myPackCss: Record = { | `--th-matrix-g2` | Gradient stop 2 | | `--th-matrix-g3` | Gradient stop 3 (darkest, matches desktop) | -### Splitters +### Splitters (BSP tiling) | Property | Used by | |----------|---------| -| `--th-splitter-idle` | Drag handle at rest | -| `--th-splitter-hover` | Drag handle on hover / active | +| `--th-splitter-idle` | Column / row drag handle at rest | +| `--th-splitter-hover` | Handle on hover / active drag | + +Note: legacy horizontal terminal splitter CSS was removed; splitters are BSP-only (`.splitter-v`, `.splitter-bsp-h`). ### Dock and icons diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md new file mode 100644 index 0000000..07e46fc --- /dev/null +++ b/docs/USER_GUIDE.md @@ -0,0 +1,128 @@ +# User Guide + +How to use the desktop portfolio at [mrgrey.site](https://mrgrey.site). + +--- + +## First visit + +1. **Boot splash** plays once (skipped on return visits). +2. **Desktop tiles** on the workspace open portfolio windows when clicked. +3. Press **`Ctrl+T`** or click **Terminal** in the dock to open the shell. +4. Type **`help`** for commands or **`keybinds`** for the full shortcut list. + +Mobile visitors (viewport ≤768px) land on the **[brochure](/static/)** automatically — same content, no OS chrome. + +--- + +## Window manager shortcuts + +Global chords work while any tile is focused (Ctrl, no Alt/Meta): + +| Chord | Action | +|-------|--------| +| `Ctrl+T` | Open or focus **terminal** tile | +| `Ctrl+D` | Toggle **Applications** launcher / show desktop | +| `Ctrl+H` | Focus window **←** (spatial); opens terminal if nothing focused | +| `Ctrl+L` | Focus window **→** | +| `Ctrl+K` | Focus window **↑** | +| `Ctrl+J` | Focus window **↓** | +| `Ctrl+Q` | Close focused window | +| `Ctrl+M` | Minimize focused window | +| `Ctrl+F` | Maximize / restore focused window | +| `Ctrl+1` … `Ctrl+9` | Focus Nth dock slot | +| `Escape` | Close launcher overlay | + +Spatial focus picks the nearest window in the chosen direction using on-screen geometry (BSP columns). + +--- + +## Terminal + +The terminal uses **xterm.js** with a vim-style input layer (`vim.ts`): + +| Key | Action | +|-----|--------| +| `i` | Insert mode | +| `Esc` | Normal mode | +| `v` | Visual mode | +| `Tab` | Command completion | +| `↑` / `↓` | Command history | +| `Ctrl+C` | Cancel / interrupt | +| `Ctrl+U` | Clear to line start | + +### Essential commands + +| Command | Opens / does | +|---------|----------------| +| `help` | Command summary | +| `help -v` | Full command glossary | +| `keybinds` | Keyboard legend | +| `resume` | Résumé tile | +| `projects` | Projects tile | +| `whoami` | About tile | +| `links` | Contact tile | +| `edit [path]` | Vim-style editor (`~/notes.txt` default) | +| `explorer [path]` | File browser | +| `browse [url]` | Embedded browser | +| `p5` | p5.js sketch viewer | +| `cube` | Rubik's cube (Three.js) | +| `paint` / `snake` / `pong` | Mini-apps | +| `theme [id\|list\|random]` | Switch colour pack | +| `retro on\|off` | CRT scanline overlay | +| `matrix on\|off` | Matrix rain backdrop | +| `ls`, `cat`, `cd`, `mkdir`, `touch`, `rm`, `mv`, `cp` | Virtual filesystem | + +Type a command name in the terminal to open its tile, or use the **Applications** launcher (`Ctrl+D`). + +--- + +## Editor tile + +Opened via `edit`, `vim`, `editor`, or the launcher. Modal vim over the VFS: + +| Mode | Keys | +|------|------| +| Normal | `hjkl`, `w`/`b`/`e`, `dd`/`yy`/`p`, `f`/`F`/`t`/`T`, `>>`/`<<`, `u`, counts | +| Insert | type text; `Esc` or `Ctrl+[` → Normal | +| Ex | `:` then `:w`, `:q`, `:wq`, `:e path`, `:run` | +| Any | `F5` — save and open buffer in **p5 viewer** | + +--- + +## p5.js sketches + +Built-in sketches live in `~/sketches/` after first visit. Workflow: + +```text +edit ~/sketches/my-sketch.js +# write code, then F5 or :run in editor +``` + +--- + +## Themes and effects + +| Command | Effect | +|---------|--------| +| `theme list` | Show all seven packs | +| `theme nord` | Apply by id | +| `theme random` | Random pack | +| `retro on` | CRT filter | +| `matrix on` | Matrix rain (also in settings) | + +Settings panel (YASB bar) exposes sound, wallpaper, theme, and effect toggles. + +--- + +## Static brochure + +Visit **`/static/`** for a scroll-based layout: hero, experience cards, skills, contact — no terminal required. Ideal for mobile and quick scanning. + +--- + +## See also + +- [OVERVIEW.md](./OVERVIEW.md) — project architecture for reviewers +- [API.md](./API.md) — extending commands and tiles +- In-app: `help keybinds` and `help theme`