CODE_GUIDE

JS Code Style Guide

This document describes the conventions and patterns used throughout static/js/.


Module System

Native ES modules (import/export) are used throughout. All files use named exports — no default exports.

// good
export class Workspace { ... }
export class Server { ... }
export function formatTime(s) { ... }

// avoid
export default class Workspace { ... }

Import paths are relative and include the .js extension.


File & Directory Structure

static/js/
├── main.js                 # App entry point — instantiates App class
├── server.js               # Server class — connection state, auth, and all server operations
├── project.js              # Project and Transcript classes
├── workspace.js            # Workspace class (the main editor view)
├── start_page.js           # Start page logic
├── components/             # Self-contained UI components
│   ├── confirm_dialog.js
│   ├── export_panel.js
│   ├── hue_picker.js
│   ├── info_widget.js
│   ├── project_context_menu.js
│   ├── segment_context_menu.js
│   ├── server_panel.js
│   ├── split_popup.js
│   └── transcribe_dialog.js
├── workspace_panels/       # Major editor sub-panels
│   ├── transcript_panel.js
│   ├── speakers_panel.js
│   └── waveform_panel.js
└── utilities/
    ├── constants.js              # Exported named constants (SCREAMING_SNAKE_CASE)
    ├── tools.js                  # Pure utility functions
    ├── audio.js                  # Audio utility functions
    ├── export.js                 # Export formatting helpers (PDF, DOCX, etc.)
    ├── theme.js                  # Theme (dark/light/auto) management
    ├── transcription_pricing.js  # Cloud transcription cost estimation
    └── server_access.js          # Low-level fetch functions (raw API calls, no state)

Naming Conventions

KindConventionExample
ClassesPascalCaseTranscriptPanel, ConfirmDialog
FunctionscamelCaseformatTime, parseCSV
ConstantsSCREAMING_SNAKE_CASEMAX_ZOOM, GAP_THRESHOLD
Private class fields/methods#name (ES2022)#buildDialog(), #getElements()
Private-by-convention properties_name_onConfirm, _onDismiss
Classes used as stateful singletonsPascalCaseServer, App, Workspace

_prefixed properties are used for callback references stored on this (to distinguish them from public interface methods). True implementation details use #private fields.


Class Structure

Every class follows this constructor order:

  1. Class-level field declarations (public first, then private)
  2. constructor — receives a parent/context reference and a callbacks options object; delegates to #getElements() and #setupListeners()
  3. #getElements() — queries and assigns DOM element references to this
  4. #setupListeners() — wires event handlers to pre-rendered DOM elements
  5. Public methods, grouped by concern with section comments
  6. Private helper methods (#name)
export class ExamplePanel {
    activeItem = null;

    constructor(workspace, { onSelect, onClose }) {
        this.workspace = workspace;
        this.onSelect = onSelect ?? (() => {});
        this.onClose  = onClose  ?? (() => {});

        this.#getElements();
        this.#setupListeners();
    }

    #getElements() {
        this.container = document.querySelector('#examplePanel');
        this.closeBtn  = this.container.querySelector('.close-btn');
    }

    #setupListeners() {
        this.closeBtn.addEventListener('click', () => this.close());
    }

    // ----- PUBLIC ----- //

    close() {
        this.container.style.display = 'none';
        this.onClose();
    }

    // ----- PRIVATE ----- //

    #buildItem(data) { ... }
}

Callbacks Pattern

Components accept a single callbacks options object as their last constructor argument. All callbacks default to a no-op so callers only provide what they need.

// Definition
constructor(parent, { onConfirm, onDismiss }) {
    this._onConfirm = onConfirm ?? (() => {});
    this._onDismiss = onDismiss ?? (() => {});
}

// Call site
new ConfirmDialog("Delete?", {
    onConfirm: () => this.deleteProject(id),
    onDismiss: () => {},
});

Optional callbacks that should be disabled entirely (rather than no-op'd) are passed as null and checked before calling:

// null signals "this action is not available"
onUpload: project.isDirty() ? () => { project.syncToServer(); } : null,

State Management

Application state is split across two layers — there is no global state object.

LayerLocationContents
Server/connectionServer instance (server.js)Base URL, auth token, connection status, isConnected flag
Per-projectProject instances (project.js)Transcript, speakers, waveform data, dirty flags

Server is owned by App and passed down via callbacks. Project instances are created per-project and stored in App.serverProjects / App.localProjects.


Server and server_access.js

Server communication is split into two layers:

  • Server class (server.js) — stateful class. Owns baseUrl, token, and isConnected. Exposes all server operations as methods. Fires onStatusChanged, onConnect, onDisconnect when connection state changes.
  • server_access.js (utilities) — stateless module of raw _fetch wrappers, each prefixed with _. Called by Server with the current baseUrl and token. All functions are async and throw on non-2xx responses.
// Adding a new low-level function in server_access.js
export async function _deleteTranscript(baseUrl, token, id) {
    await _fetch(baseUrl, `/api/projects/${id}/transcript`, { method: 'DELETE' }, token);
}

// Exposing it on Server in server.js
async deleteTranscript(id) {
    await _deleteTranscript(this.baseUrl, this.token, id);
}

DOM Construction

UI elements are built imperatively with document.createElement. No inner HTML for structural elements (use textContent or appendChild). innerHTML is acceptable only for simple static markup like icon strings or spans with known-safe content.

// good
const btn = document.createElement('button');
btn.className = 'sample-btn';
btn.textContent = 'Delete';
parent.appendChild(btn);

// avoid for structure
parent.innerHTML = '<button class="sample-btn">Delete</button>';

Inline style.cssText is used in component code for one-off styles that don't belong in a stylesheet. Prefer CSS classes for anything reusable.


Async / Await

All async operations use async/await. Errors are caught at the call site with try/catch, not by chaining .catch().

async refreshServerProjects() {
    try {
        const list = await this.server.listProjects();
        // ...
    } catch (e) {
        this.serverPanel.setServerStatus('error', 'Failed to load projects');
        console.error(e);
    }
}

JSDoc

All exported functions, classes, and public methods get JSDoc. Private helpers use @ignore to suppress them from the generated docs. @param types use JSDoc syntax ({string}, {number}, {HTMLElement}, {function}, etc.).

/**
 * Formats a time in seconds as "M:SS".
 * @param {number} s - seconds
 * @returns {string} e.g. "1:05"
 */
export function formatTime(s) { ... }

/**
 * Builds the internal DOM structure.
 * @ignore
 */
#buildDialog() { ... }

Constants in utilities/constants.js each get a single-line JSDoc comment describing their meaning and units.


Comments

  • Use // ----- SECTION NAME ----- // section headers inside large classes to group related methods.
  • Inline comments explain why, not what. Skip obvious comments.
  • // TODO: marks known gaps or unfinished work. Keep them specific.
  • Commented-out code blocks (e.g. unimplemented features) are acceptable when accompanied by a // TODO: explaining the intent.

Constants & Magic Values

All named constants live in utilities/constants.js and are imported by name. Do not use magic numbers or color literals inline in logic code.

// good
import { MAX_ZOOM, GAP_THRESHOLD } from './utilities/constants.js';

// avoid
if (zoom > 200) { ... }

Formatting

  • Indentation: 4 spaces.
  • Quotes: single quotes for strings; template literals for interpolation.
  • Semicolons: always.
  • Trailing commas: used in multi-line object/array literals.
  • Short related assignments may be column-aligned for readability:
this.isConnected = false;
this.baseUrl     = null;
this.token       = null;