Coding standards and best practices for the portfolio codebase.
- No
any. Useunknownif the type is truly unknown, then narrow with type guards. - Strict null checks. Always handle
nullandundefinedexplicitly. - Prefer
constandlet. Never usevar. - Explicit return types on public functions. Private/internal functions can infer.
// Prefer interfaces for object shapes
interface WindowOptions {
onClose: () => void
onMinimize: () => void
}
// Use type aliases for unions, complex types
type EditMode = 'normal' | 'insert' | 'cmd'
type PendingOp = 'd' | 'c' | 'y'
// Export types that cross module boundaries
export type { WindowOptions, EditMode }export class Example {
// Public properties (no underscore)
readonly el: HTMLElement
// Private properties (no underscore, rely on TypeScript)
private mode: EditMode = 'normal'
private buffer = ''
// Static constants
private static readonly MAX_HISTORY = 50
constructor(opts: WindowOptions) {
// Initialize inline where possible
}
// Public methods
focus(): void {
this.syncFocus()
}
// Private methods — verb-first naming
private syncFocus(): void {
// implementation
}
}// Verb-first naming for actions
function mountComponent(): void
function renderOutput(): string[]
function playNotificationSound(): void
// Utility functions — descriptive names
function escapeHtml(input: string): string
function debounce<T extends (...args: any[]) => void>(fn: T, ms: number): T
// Async functions — explicit Promise return
async function loadConfig(): Promise<Config>
// Avoid boolean trap — prefer object parameter
function setEnabled(opts: { on: boolean; persist?: boolean }): void
// NOT: function setEnabled(on: boolean, persist?: boolean)| Pattern | Example | Use Case |
|---|---|---|
kebab-case.ts |
browser-window.ts |
Module files |
*.test.ts |
storage.test.ts |
Test files (co-located) |
*.d.ts |
— | Type declarations (if needed) |
// 1. External dependencies first
import { Terminal } from '@xterm/xterm'
// 2. Absolute project imports (with @/ alias where configured)
import { storageGet, storageSet } from './storage'
import type { WindowSpec } from './appwindow'
// 3. Relative imports within same directory
import { buildResumeSkills } from './resume-copy'
// Group related imports
import {
vfsPwd,
vfsLs,
vfsCat,
type VfsNode,
} from './os-fs'Use the existing token system. Don't hardcode values.
/* ✅ Correct */
.my-element {
padding: var(--ui-space-3);
border-radius: var(--ui-radius-sm);
background: rgba(var(--th-overlay-rgb), 0.42);
}
/* ❌ Incorrect */
.my-element {
padding: 12px;
border-radius: 6px;
background: rgba(30, 30, 46, 0.42);
}Spacing: --ui-space-1 through --ui-space-6
Border radius: --ui-radius-sm, --ui-radius-md, --ui-radius-lg
Duration: --ui-duration-fast, --ui-duration-normal, --ui-duration-slow
Easing: --ui-easing-out, --ui-easing-out-back
/* Component prefix */
.browser-toolbar { }
.fe-icon-btn { } /* fe = file-explorer */
.snake-hud { }
/* State suffixes */
.btn--primary { } /* modifier */
.win-line--hang { } /* variant */
.is-active { } /* JS toggle */
.has-error { } /* JS toggle */Use container queries for component-level responsive behavior:
.my-component {
container-type: inline-size;
container-name: myComponent;
}
@container myComponent (max-width: 400px) {
.my-component > .child {
flex-direction: column;
}
}Always use the storage.ts wrapper:
import { storageGet, storageSet, storageGetJson } from './storage'
// Returns null on failure
const value = storageGet('key')
// Returns fallback on failure
const config = storageGetJson<Config>('config', {})Use non-null assertion (!) only when element is guaranteed by HTML structure:
// ✅ OK — element exists in static HTML
const terminal = document.getElementById('terminal')!
// ❌ Dangerous — check or handle null
const maybe = document.getElementById('maybe')
if (maybe) {
maybe.classList.add('active')
}Keep error handling minimal. Most errors are silently ignored (e.g., localStorage in private mode):
function save(): void {
storageSet('key', value) // Already handles errors internally
}Co-locate with source: module.ts → module.test.ts
import { describe, expect, it, beforeEach, vi } from 'vitest'
import { VimInput } from './vim'
describe('VimInput', () => {
let vim: VimInput
beforeEach(() => {
vim = new VimInput(() => {})
})
describe('mode transitions', () => {
it('switches to normal mode on Escape', () => {
vim.handleKey(keyEvent('Escape'))
expect(vim.mode).toBe('normal')
})
})
describe('operators', () => {
it('deletes word with dw', () => {
vim.setBuffer('hello world')
vim.handleKey(keyEvent('Escape'))
vim.handleKey(keyEvent('d'))
vim.handleKey(keyEvent('w'))
expect(vim.getValue()).not.toContain('hello')
})
})
})Mock DOM globals for Node environment:
// @ts-expect-error mock for tests
;(globalThis as unknown as { localStorage: Storage }).localStorage = new MockStorage()Use JSDoc for public APIs:
/**
* Safe localStorage wrapper with error handling.
* Returns null on any error or if key not found.
*/
export function storageGet(key: string): string | null
/**
* Create standard window chrome with titlebar and traffic light buttons.
* @param opts - Window configuration and callbacks
* @returns Container element and references to interactive parts
*/
export function createWindowChrome(opts: WindowChromeOptions): WindowChromeElementsExplain the why, not the what:
// ✅ Good — explains rationale
// Debounce saves to reduce localStorage writes during rapid operations
const SAVE_DEBOUNCE_MS = 150
// ❌ Redundant — code is self-explanatory
// Set the timeout to 150 milliseconds- Debounce writes (see
os-fs.ts: 150ms debounce) - Read once at module init, cache in memory
- Use
storage.tswrapper (handles errors, private mode)
- Pause on
document.hidden(seematrix-bg.ts) - Check
shouldRun()before each frame - Cancel animation frames on cleanup
- Use
AbortControlleror manual cleanup indestroy()methods - Debounce resize handlers
- Use
ResizeObserverfor element-level changes (not window resize)
const ro = new ResizeObserver(() => this.layout())
ro.observe(this.el)
// Cleanup
destroy(): void {
ro.disconnect()
}- All interactive elements must be keyboard accessible
- Document shortcuts in YASB settings panel
- Use
:focus-visiblefor focus rings (not:focus)
- Use
aria-labelfor icon-only buttons - Use
aria-hidden="true"for decorative icons - Provide
rolefor custom widgets
const btn = document.createElement('button')
btn.setAttribute('aria-label', 'Close window')
btn.innerHTML = '<span class="dot dot-close" aria-hidden="true"></span>'Respect prefers-reduced-motion:
@media (prefers-reduced-motion: reduce) {
.animated-element {
animation: none;
transition: none;
}
}Subject line (50 chars max)
Body explaining what and why (wrap at 72 chars)
- Bullet points for details
- Another point if needed
Fix resume narrow layout: photo on top, narrative below
When resume window shrinks to narrow width (<679px), the layout now
stacks vertically instead of side-by-side:
- Photo: full-width centered on top
- Narrative: full-width below photo
- Skills: full-width at bottom
This eliminates the gap that previously appeared when the photo stayed
in a side column while skills fell below.
Add storage.ts: centralized localStorage wrapper
- Replaces try/catch boilerplate across 8 files
- Provides typed JSON helpers (storageGetJson, storageSetJson)
- Handles private mode gracefully (returns fallback)
- Adds comprehensive test suite (20 tests)
Keep docs in docs/ updated when making architectural changes:
ARCHITECTURE.md— module structure, entry points, patternsTHEMING.md— custom properties, adding themesSTYLE_GUIDE.md— this file (coding standards)
When adding new modules, update the relevant tables in ARCHITECTURE.md.
// Don't use any
function process(data: any): any
// Don't ignore errors silently without comment
try {
riskyOperation()
} catch {
// empty
}
// Don't hardcode magic numbers
if (width < 679) { }
// Don't use innerHTML with dynamic content (XSS risk)
el.innerHTML = userInput// Use unknown + type guard
function process(data: unknown): Output {
if (isValidInput(data)) { /* ... */ }
}
// Document why error is safe to ignore
try {
localStorage.setItem(key, value)
} catch {
/* private mode — persistence not possible */
}
// Use named constants
const NARROW_BREAKPOINT = 679
if (width < NARROW_BREAKPOINT) { }
// Use textContent or escape HTML
el.textContent = userInput
el.innerHTML = escapeHtml(userInput)