diff --git a/docs/design-system.md b/docs/design-system.md new file mode 100644 index 0000000..9da90db --- /dev/null +++ b/docs/design-system.md @@ -0,0 +1,380 @@ +# code-sinth — design system + +The studio hardware emulation language. Anything we add to the right-pane +control surface should follow this. The goal is consistency: a new component +should look like it belongs on the same physical rack as everything else. + +The reference implementation lives in [web/sandbox/studio.html](../web/sandbox/studio.html). + +## Vision + +> Vintage studio rack module. Dark anodised aluminium plates mounted on a +> darker frame, lit from above by a softbox. Metal parts catch the light. +> Engraved labels on the metal. Amber LEDs and amber readouts where +> something is "active". + +References that pull this together: Moog Voyager, Roland TR-808, Korg MS-20, +Universal Audio 1176. Skeuomorphic but **disciplined** — every effect +exists to model a physical material under a specific lighting setup, not +for decoration. + +## Lighting model + +**One single rule, never broken**: light comes from **above**, slightly +in front. Every reflective surface lights its top and shadows its bottom. +This applies regardless of where the surface sits within the rack. + +Practical consequences: +- Vertical gradients on metal (lighter top, darker bottom). +- Specular bands on the upper portion of any large flat surface. +- Bevels: bright on top edge, dark on bottom edge. +- Top-edge gloss crescent on circular pieces (knob disc, indicator dot). +- Subtle warm rim light at the very bottom — bounce light from the surface + the rack sits on (typical of softbox-lit photos of studio gear). + +When in doubt, ask: "if this were aluminium under a softbox lamp, where +would the highlight go?" + +## Color tokens + +All colors live as CSS custom properties on `:root`. Semantic naming, not +literal — adding a new theme later means changing the values, not the +selectors. + +```css +/* surfaces (warm-neutral dark, faint warm tint to feel like metal not plastic) */ +--bg: #131210; /* page bg, behind the rack */ +--hw-bg: #1a1916; /* outer rack base */ +--hw-bg-hi: #232220; +--hw-bg-lo: #0e0d0b; +--hw-edge: #050505; /* sharp dark edges between surfaces */ +--hw-screen: #161412; /* code-area glass — slightly darker than the rack */ + +/* text */ +--hw-fg: #a8a39a; /* general body text */ +--hw-fg-hi: #d8d0c0; /* numeric values, labels at attention */ +--hw-fg-dim: #6c6660; /* secondary status text */ +--hw-engrave: #8a847a; /* engraved labels on metal */ + +/* accent — amber. ONLY for active indicators: arcs, LEDs, running label */ +--hw-amber: #e8a050; +--hw-amber-hi: #f4c890; +--hw-amber-mut: rgba(232, 160, 80, 0.18); +--hw-amber-glow: rgba(232, 160, 80, 0.45); +--hw-amber-off: #28201a; + +/* status LEDs */ +--hw-led-red: #c84838; /* stop / alert */ +--hw-led-green: #6aca8a; /* play / running */ + +/* code syntax — warm desaturated palette, never bright primary colors */ +--hw-syn-com: #5a554f; +--hw-syn-kw: #d68868; +--hw-syn-num: #d6a268; +--hw-syn-fn: #c8a878; +--hw-syn-id: #b8b0a0; +--hw-syn-op: #6c6660; +``` + +**The amber accent is precious**. Used for: knob value arcs, fader scale +indication, step LEDs when on, the running label, numeric readouts on +amber LCDs (we don't currently have those). Never for plain text, plain +borders, or decorative accents — overuse kills its meaning. + +## The "metal" language (3 reusable layers) + +Any frame that should read as a brushed-aluminium plate composes three +shared CSS variables: + +```css +/* (1) horizontal specular band on the upper portion */ +--metal-specular: linear-gradient(180deg, + rgba(255, 248, 230, 0.08) 0%, + rgba(255, 248, 230, 0.03) 28%, + rgba(255, 248, 230, 0) 45%, + rgba(0, 0, 0, 0) 70%, + rgba(0, 0, 0, 0.10) 100%); + +/* (2) very fine repeating horizontal stripes (brushed steel feel) */ +--metal-brush: repeating-linear-gradient(0deg, + rgba(255, 250, 235, 0.018) 0px, + rgba(255, 250, 235, 0.018) 1px, + rgba(0, 0, 0, 0.030) 1px, + rgba(0, 0, 0, 0.030) 2px); + +/* (3) 4-edge bevel via inset shadows */ +--metal-bevel: + inset 0 1px 0 rgba(255, 248, 230, 0.18), /* top edge highlight */ + inset 0 -1px 0 rgba(0, 0, 0, 0.70), /* bottom edge shadow */ + inset 1px 0 0 rgba(255, 248, 230, 0.06), /* left edge faint highlight */ + inset -1px 0 0 rgba(0, 0, 0, 0.45); /* right edge dark */ +``` + +A frame plugs them in and adds its own base color and drop shadow: + +```css +.some-panel { + background: + var(--metal-specular), + var(--metal-brush), + linear-gradient(180deg, /* base color top → bottom */ ); + box-shadow: + var(--metal-bevel), + /* element-specific drop shadow */; +} +``` + +This is the single most important rule: **every metal surface uses these +three layers**. Don't reinvent them per component. + +## Surface hierarchy + +Three depth layers, each visually brighter than the one beneath so they +read as plates physically mounted on top of each other: + +| Layer | Element | Base gradient | Drop shadow | +|------------------|-------------------------------|-------------------------------------------|----------------------------------------------| +| L0 (deepest) | `.hardware` (outer rack) | `#181513` → `#080604` | `0 8px 32px rgba(0,0,0,0.65)` | +| L1 (mid) | `.top-bar`, `.bottom-bar` | `#25211d` → `#16130f` | `0 2px 4px rgba(0,0,0,0.4)` | +| L2 (brightest) | `.knobs`, `.faders`, `.seq` | `#2c2823` → `#1a1612` → `#110d09` | `0 2px 4px + 0 6px 14px` (two-layer) | + +The progressively brighter base + progressively bolder drop shadow makes +the L2 plates feel "lifted" off the rack, while the bars feel mounted +flush. The code "screen" is **outside** this hierarchy — it's a recess, +not a surface (see below). + +## Components + +### Outer rack frame (`.hardware`) + +- L0 of the surface hierarchy. Largest border-radius (10px) — the overall + unit feels like a single piece. +- Has 4 corner screws, **only on the outermost frame** — sub-panels do not + carry screws. +- Padding 14px. Grid: editor column on the left, fixed-width control + column (220px) on the right. + +### Top / bottom bars (`.top-bar`, `.bottom-bar`) + +- L1. Twin styling — they read as a matched pair anchoring the screen. +- Height 30px. Border-radius 4px (lower than rack). +- Top bar: transport button group + status text. Bottom bar: running + label with pulsing amber dot. Same height/style/border, different + contents — visual symmetry is the point. + +### Code "screen" (`.screen`) + +- Not a panel — a **recess**. No raised surface, no bevel highlights. +- Achieved with deep inset shadows on a slightly darker background: + ```css + background: var(--hw-screen); + box-shadow: + inset 0 2px 4px rgba(0,0,0,0.7), + inset 0 0 0 1px var(--hw-edge), + inset 0 6px 14px rgba(0,0,0,0.5); + ``` +- Reads as a hole cut into the rack with a glass screen behind it. +- Sits between top-bar and bottom-bar to complete the "header / screen / + footer" feel of a single rack module. + +### Sub-panels (`.knobs`, `.faders`, `.seq`) + +- L2 of the hierarchy. Border-radius 6px. +- Use `.knobs, .seq, .faders` shared selector for the metal language; + each adds its own internal layout (grid for knobs, flex row for faders, + flex column for seq). + +### Knobs + +The most elaborate component. Eight layers in canvas, in this order: + +1. **Drop shadow** — black disc fill at slight Y-offset with `shadowBlur`. + Sits the knob "on" the panel. +2. **Amber value arc** — outside the knob's outer edge. Faint full-track + stroke + bright glowing active arc (length proportional to value). +3. **Dark rim** — thin (~2.5px) outer ring of the knob body. Vertical + gradient `#2a2826 → #0a0908 → #040404`. +4. **Big metallic disc** — fills most of the knob's area. This is the + centerpiece. Six sub-layers stacked: + - **Base** radial gradient (matte foundation, slightly darker at the edge). + - **Concentric brushing** — concentric circles every 0.55px with + pseudo-random alpha alternating bright/dark. Imitates lathe-turned + aluminium. Deterministic seed so it doesn't shimmer between frames. + - **Vertical lighting** overlay (white wash 32% on top, black wash 45% + on bottom). + - **Horizontal specular band** on the upper third (white-warm 42% peak + with soft falloff). + - **Bottom rim light** — warm cream wash 14% at the very bottom. + - **Bevel** — bright crescent on the top edge (55% white-warm) + + dark crescent on the bottom edge (50% black). +5. **Inset shadow** between disc and rim, gives the disc a "sunk-in" feel. +6. **Indicator notch** — small dark triangle on the disc, base near the + center, tip toward the rim, rotated to the value angle. Engraved + appearance: dark fill + thin bright lower-edge highlight catching + light on the inside of the groove. + +Fixed pixel sizes: `totalR = 26`, `rimW = 2.5`, `discR = 23.5`, notch +half-base = 1.5. Canvas is `72×72` — the extra ~10px margin lets the +amber arc and drop shadow render without clipping. + +Indicator angle range: `Math.PI * 0.78` (lower-left) to `Math.PI * 2.22` +(lower-right) — a 7:30 → 4:30 sweep, matching how real synth knobs end +their throw. + +### Faders + +Vertical sliders, sit in their own sub-panel arranged in a row. + +- **Track** (`.fd-track`) — 5px wide × 78px tall. Deep slot cut into the + panel. Heavy `inset 0 1px 3px rgba(0,0,0,0.85)` shadow + thin warm + highlight at the bottom edge for chamfer feel. +- **Tick marks** (`.fd-ticks`) — 16px wide pseudo-element on each side + of the track. `repeating-linear-gradient` 1px on / 6px off. Engraved + scale alongside the slot. +- **Cap** (`.fd-cap`) — 26×14px rounded-corners metallic block. Same + metal language as the knob disc, condensed for a small element: + - Brushed stripes via `repeating-linear-gradient` (4% / 5% alpha). + - Vertical light gradient `#b6b0a4 → #807a6e → #3a352e → #1a1612`. + - Horizontal grip line — dark band at 46-54% Y for the "thumb groove". + - 4-edge inset bevel (top bright, bottom dark, left subtle highlight, + right dark). + - Drop shadow `0 2px 3px rgba(0,0,0,0.6)`. + +Interaction: click anywhere on the track jumps the cap there + starts a +drag; drag follows pointer; pointerCapture is on the track itself so the +cap follows even past the bounds. + +### Step LEDs (`.led`) + +- Off: `linear-gradient(180deg, #100c08 → #28201a)` — looks unlit but + not flat black (so the off LEDs have a subtle "physical" feel). +- On: `radial-gradient(circle at 50% 25%, #f4c890 → #e8a050 → #a06820)` + + `0 0 5px var(--hw-amber-glow)` outer glow. +- Hover: brighter dim variant. Click toggles. +- 16-cell row. Beat dividers every 4 cells via a pseudo-element on the + left edge of cells where `i % 4 === 0`. +- Playhead: `outline: 1px solid rgba(232, 160, 80, 0.6)` + 1px offset. + +### Buttons (`.hw-btn`) + +- Vertical gradient `#2c261f → #14110d`. +- Border `1px solid var(--hw-edge)`. +- Inset highlight on top edge + dark on bottom edge. +- Letter-spacing 0.12em, uppercase, font-size 10px. +- Hover: text changes to amber. Active: `transform: translateY(1px)` + + inset darkening (the "press" feel). +- **Button groups** in the top bar (STOP+RUN): adjacent, no gap, share + borders. First child has left-rounded corners only, last child has + right-rounded only. The middle border merges. Gives the feel of a + single rocker switch. + +### Screws (`.screw`) + +- 8px diameter. **Only on the outermost rack** — sub-panels don't carry + screws (real rack panels are usually pop-mounted, not bolted). +- `radial-gradient(circle at 30% 25%, #8a7d6a → #3a3025 → #100c08)` for + the metal head. +- `::after` adds a slot mark with a 45° linear-gradient hard-edge. +- Inset shadow + small drop shadow for "sunk into the metal". + +### Engraved labels + +The `.label` class on knob/fader cells, the engraved text inside bars. + +- `font-size: 9-10px`, `letter-spacing: 0.16-0.18em`, `text-transform: + uppercase`. +- Color `var(--hw-engrave)` — the desaturated warm gray. +- Double text-shadow for the engraved feel: + ```css + text-shadow: + 0 1px 0 rgba(0, 0, 0, 0.7), /* deep shadow below */ + 0 -1px 0 rgba(255, 220, 180, 0.04); /* faint highlight above */ + ``` +- The shadow below + highlight above creates the optical illusion of + text incised into metal lit from above. + +### Numeric value displays + +- Below knobs and faders. Cream color (`var(--hw-fg-hi)`), tabular nums. +- **No frame, no inset background** — flat text on the panel. We + experimented with LCD-style amber inset readouts and it felt too busy. + The amber is reserved for active indicators; values are passive. +- Width matches the knob (~46px min) so the layout doesn't shift when + the digit count changes. + +## Typography + +Single font stack: `'JetBrains Mono', 'Cascadia Code', Consolas, monospace`. +Everything monospace — keeps the "instrument firmware" feel. + +Sizes: +- Body / labels: 10-11px +- Code: 12-13px +- Headings (when needed): 12-14px + +Letter-spacing: `0.06em` for normal text, `0.16-0.18em` for engraved +labels and button text. Wider tracking gives the "carved into metal" +feel. + +Tabular numerals (`font-variant-numeric: tabular-nums`) for any value +display so they don't wobble when digits change. + +## Interaction + +- **Drag**: knobs and faders use `pointerdown` + `setPointerCapture`. + Always change `cursor` to `grabbing` on body during the drag (so the + cursor doesn't flicker if the pointer leaves the element bounds). +- **Click track to jump**: faders accept a click anywhere on the track + to jump the cap there, then a drag continues from that point. +- **Double-click to recenter**: knobs reset to their default value. +- **Shift-drag for fine adjust**: knobs accept `ev.shiftKey` for ¼-speed + adjustment. +- **`pointercancel`** is always handled alongside `pointerup` — covers + edge cases where the browser cancels a drag mid-gesture. +- Always remove all listeners and reset cursors on the up/cancel handler. + +## Spacing & proportions + +- Outer rack padding: 14px. +- Outer rack gap (editor ↔ control column): 12px. +- Right-stack inner gap (knobs / faders / seq): 8px. +- Sub-panel padding: 8-14px depending on density. +- Element gap inside sub-panels: 4-12px. + +The unifying scale is multiples of 2 (2/4/8/12/14). Avoid 5px, 7px, +13px — they look like accidents. + +## What is intentionally NOT here + +To keep the language disciplined, these were considered and rejected: + +- **Skeuomorphic photo textures** (real wood, real brushed metal photos). + Pure CSS gradients keep the file small and stay sharp at any DPI. +- **3D rotation / perspective transforms**. The lighting model already + conveys depth; transforms add visual noise. +- **Animated reflections / moving highlights**. Static metal looks + premium; moving highlights look like a screensaver. +- **Multiple accent colors**. Amber is the only "active" hue. A second + accent would dilute the meaning. +- **Decorative dots / flourishes / brand marks**. The rack is + minimalist — every visible element is functional. +- **Bright primaries in syntax highlighting**. The code area is + "behind glass" with warm light passing through; saturated greens + and blues break the illusion. + +## How to add a new component + +1. Decide its surface hierarchy layer (L0 / L1 / L2). Almost everything + new is L2 (a sub-panel). +2. Apply the metal language: `var(--metal-specular)`, `var(--metal-brush)`, + `var(--metal-bevel)` plus a base color and drop shadow appropriate + to the layer. +3. Use `--hw-engrave` for any label text, with the double-text-shadow + formula. +4. Use `--hw-amber` only if the new component has an "active" indicator + to show. +5. Drag interactions: pointerdown + setPointerCapture, body cursor = + `grabbing`, listen for pointermove/up/cancel, clean up on up/cancel. +6. Numbers: tabular-nums, `--hw-fg-hi` color, no inset frame. +7. If it's reflective (metal disc, cap, sphere), light it from above. + No exceptions.