Files
led2/.planning/phases/05-live-show-execution/05-UI-SPEC.md
Claude f739c8fe19 docs(05): UI design contract
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 09:02:33 +00:00

13 KiB
Raw Blame History

phase, slug, status, shadcn_initialized, preset, created
phase slug status shadcn_initialized preset created
5 live-show-execution draft false none 2026-04-07

Phase 5 — UI Design Contract

Visual and interaction contract for Phase 5: Live Show Execution. Generated by gsd-ui-researcher, verified by gsd-ui-checker.


Design System

Property Value
Tool none — vanilla CSS with custom properties
Preset not applicable
Component library none (hand-rolled terminal-aesthetic components)
Icon library none — ASCII/Unicode symbols inline (▶, ◼, ▶▶, ▼, ▶)
Font JetBrains Mono, Fira Code, Consolas, monospace (system fallback chain)

Source: lightsync/frontend/style.css :root block — no external design system; all tokens are CSS custom properties.


Spacing Scale

Declared values (multiples of 4 only):

Token Value Usage
xs 4px Inner icon gaps, strip padding (padding: 0 4px)
sm 8px Element gaps inside device strips, panel content padding top/bottom
md 12px Header/transport bar horizontal padding
lg 16px Timeline toolbar gap between field groups
xl 24px
2xl 32px Timeline toolbar fixed height
transport 52px Transport bar grid row height (existing, do not change)

Exceptions:

  • Device strip row height: 18px (compact single-line strip — matches existing .block-inspector at 40px; preview is intentionally denser)
  • Gap between device strips: 1px (separator hairline, not a spacing token)
  • Strip color swatch: 40px wide × 10px tall (fixed decoration element)
  • Live preview panel max-height: 60px (D-06 from CONTEXT.md, overflows to scroll if many devices)

Source: CONTEXT.md D-06 "Compact — 40-60px total" + RESEARCH.md Pattern 2 CSS.


Typography

All text uses --font-mono exclusively. No serif or sans-serif anywhere in this application.

Role Size Weight Line Height
Body / default 13px 400 1.4
Small label (panel headers, toolbar labels, strip labels) 10px 400 1.2
Compact UI text (device rows, inspector fields, transport) 12px 400 1.4
Active label / header title 14px 600 1.2

Letter-spacing rules (terminal convention — do not deviate):

  • Panel headers, section headers, toolbar labels, strip labels: letter-spacing: 0.100.14em
  • Transport time display: font-variant-numeric: tabular-nums (prevents width jumping)
  • Animation name in strip: letter-spacing: 0.05em, font-size: 10px (same as .strip-anim pattern)

Source: style.css — measured from .panel-header (10px/600 LS 0.14em), .device-row-info (12px), .header-title (14px/600), .transport-time (12px tabular-nums).


Color

All values use existing CSS custom properties. No new color tokens for Phase 5.

Role CSS Variable Hex Usage
Dominant (60%) --bg-primary #0a0a0a App background, main area, canvas background
Secondary (30%) --bg-panel / --bg-panel-dark #0f0f0f / #080808 Panels, transport bar, toolbar, preview strip rows
Accent (10%) --accent #00ffff See accent reservation below
Destructive hardcoded #ff3333 #ff3333 Remove/delete actions only (existing pattern)
Status positive hardcoded #00ff88 #00ff88 WebSocket connected indicator dot only
Beat flash hardcoded #ffcc00 #ffcc00 Beat indicator dot flash only
Border dim --border-dim #1a4a4a All panel borders, strip separators

Accent reserved for (explicit list — not for general interactive elements):

  1. Panel header text (color: var(--text-accent))
  2. Active animation tile highlight
  3. Active snap button state (background fill + border)
  4. Transport button hover state (background fill)
  5. Timeline cursor (playback position line)
  6. Selected block outline on timeline canvas
  7. Header title LIGHTSYNC
  8. Beat marks on timeline canvas

Live preview color fills: The preview device strip color swatch uses the cue's params.color value (RGB array from the animation block) as background-color: rgb(r, g, b). This is data-driven, not a design token. Inactive state uses --text-dim (#555555) as fill.

Source: style.css entire file; CONTEXT.md D-11/D-12/D-15; Phase 1 CONTEXT.md D-11 through D-15.


Component Inventory

New in Phase 5

1. Live Preview Panel (#live-preview)

Purpose: Shows active animation per device, updated by preview_update WebSocket messages.

Placement: Between #timeline-canvas and #block-inspector in .main-area flex column. (TIMELINE → LIVE PREVIEW → BLOCK INSPECTOR → TRANSPORT)

Structure:

#live-preview.live-preview
  └── [one per device] div.device-strip[data-device-id="{id}"]
        ├── span.strip-label   — device name, UPPERCASE, min-width 80px
        ├── div.strip-color    — 40×10px swatch, background = active animation color
        └── span.strip-anim    — animation name, UPPERCASE, dim when inactive

Visual states:

State strip-color background strip-label color strip-anim text
Active (block firing) rgb(r, g, b) from cue params --text-primary animation name in uppercase
Inactive (no block) --text-dim (#555555) --text-dim empty or "—"
Panel hidden display: none

Panel visibility: display: flex when a show is loaded, display: none before first show load.

Collapsing: If device count > 3, panel height exceeds 60px and scrolls via overflow-y: auto. No explicit collapse toggle in v1 — scrolling is sufficient.

CSS contract (extend style.css):

.live-preview {
    border-top: 1px solid var(--border-dim);
    border-bottom: 1px solid var(--border-dim);
    background: var(--bg-panel-dark);
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: 2px 0;
    max-height: 60px;
    overflow-y: auto;
    flex-shrink: 0;
}

.device-strip {
    display: flex;
    align-items: center;
    height: 18px;
    padding: 0 4px;
    background: var(--bg-panel);
    gap: 8px;
    font-size: 11px;
    font-family: var(--font-mono);
    letter-spacing: 0.05em;
    transition: background-color 0.1s;
}

.strip-label {
    color: var(--text-dim);
    text-transform: uppercase;
    min-width: 80px;
    flex-shrink: 0;
    font-size: 10px;
    letter-spacing: 0.12em;
}

.strip-color {
    width: 40px;
    height: 10px;
    background: var(--text-dim);
    flex-shrink: 0;
    transition: background-color 0.15s;
}

.strip-anim {
    color: var(--text-dim);
    font-size: 10px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
}

.device-strip.active .strip-label {
    color: var(--text-primary);
}

.device-strip.active .strip-anim {
    color: var(--text-primary);
}

Transition: Color changes use transition: background-color 0.15s — fast enough to track 10Hz ticks without visual lag.

2. Show Selector UI

Purpose: Load a saved show into the timeline. Replicates the existing audio file select pattern.

Placement: In the transport bar, after the existing transport-sep, before or after the audio file controls. Use the existing transport-file-select + transport-btn pattern — no new CSS classes needed.

Structure:

<select id="show-select" class="transport-file-select">
  <option value="">— select show —</option>
  <option value="{id}">{show name}</option>
</select>
<button id="btn-load-show" class="transport-btn">LOAD</button>

Label: "LOAD" — consistent with existing "LOAD" button for audio.

Empty state: <option value="">— no shows saved —</option> when GET /api/shows returns empty list.

Loading state: Button text changes to "..." while show loads, reverts to "LOAD" on complete.

3. Keyboard Shortcut Bindings

No visual component — pure JS keydown handler. The executor must add wireKeyboard() to app.js.

Canonical bindings (locked — from CONTEXT.md Claude's Discretion):

Key Combination Action Guard
Space Play / Pause toggle Skip if activeElement is input/select/textarea/button
ArrowLeft Seek 5 seconds Skip if activeElement is input/select/textarea
ArrowRight Seek +5 seconds Skip if activeElement is input/select/textarea
Ctrl+Z Undo Skip if activeElement is input/select/textarea
Ctrl+Shift+Z Redo Skip if activeElement is input/select/textarea
Ctrl+Y Redo (alternate) Skip if activeElement is input/select/textarea
Ctrl+S Explicit save (triggers existing save endpoint) Always prevent default; no-op if no show loaded

All handlers call e.preventDefault() before acting to suppress browser defaults (especially Space and Ctrl+S).


Copywriting Contract

Element Copy
Primary CTA — show load "LOAD" (button label, uppercase — matches existing transport button convention)
Primary CTA — save No button label needed; Ctrl+S only. Status feedback: "SAVED" flash on the show-select area (optional)
Show selector placeholder "— select show —"
Show selector empty state "— no shows —"
Preview panel section header None — the panel has no header. The .strip-label per device identifies context sufficiently
Inactive strip animation text "—" (em dash, single character — consistent with terminal dim convention)
Keyboard hint (transport bar) Optional dim label: [SPACE] PLAY [←→] SEEK [^Z] UNDO [^S] SAVE at far right of transport bar, font-size: 10px, color: var(--text-dim)
Error: show load fails Browser console [show] failed to load {id} — no modal or toast in v1 (terminal aesthetic = no popups)
Error: cue fire fails (unknown animation) Server log only: [ws] unknown animation {name} — not surfaced to UI
Destructive action — none in Phase 5 Phase 5 has no destructive actions. No confirmation dialogs needed.

Source: CONTEXT.md "Show save/load UX" + existing UI pattern (all error handling is console-only in terminal aesthetic).


Interaction Contracts

Cue Scheduler Feedback Loop

The UI must visually close the loop between audio position and cue firing:

  1. Audio plays in browser → timeupdate sends tick to server at 10Hz
  2. Server fires UDP cue → broadcasts preview_update message
  3. Browser receives preview_update → updates .device-strip color and animation label within one event loop tick
  4. Color transition duration: 0.15s (fast enough for 10Hz, prevents flicker)

No loading spinner, no progress indicator — the live preview IS the execution feedback.

Seek Behavior

When user drags the seek slider or presses ArrowLeft/ArrowRight:

  1. audio.currentTime updates
  2. Browser sends {"type": "seek", "position": N} to server
  3. Server rebuilds fired_ids (all cues before position 60ms marked fired)
  4. All device strips reset to inactive state (dark) — browser clears .device-strip.active class on seek
  5. Show resumes scheduling from new position on next tick

Visual contract for seek: device strips dim immediately on seek, re-light when next cue fires.

Show Load Sequence

  1. User selects show from dropdown → clicks "LOAD"
  2. Button shows "..." while GET /api/shows/{id} resolves
  3. Timeline tracks populate with saved blocks
  4. setCurrentShowId(id) called in commands.js
  5. WebSocket load message sent: {"type": "load", "show_id": "{id}", "path": "{audio_path}"}
  6. Server acquires cue list per-connection
  7. Preview panel becomes visible (display: flex)
  8. Button reverts to "LOAD"

Layout Contract

The .main-area flex column order for Phase 5:

.main-area (flex-direction: column)
  1. .timeline-toolbar          (height: 32px, flex-shrink: 0)
  2. #timeline-canvas            (flex: 1, min-height: 0)
  3. #live-preview               (max-height: 60px, flex-shrink: 0) ← NEW
  4. #block-inspector            (height: 40px, flex-shrink: 0, display:none when empty)

Transport bar remains in .app-shell grid transport area — it is NOT inside .main-area.

The show selector controls are added to the existing .transport-bar flex row.


Registry Safety

Registry Blocks Used Safety Gate
shadcn official none not applicable — no shadcn in this project
third-party none not applicable

No component registries are used. All UI is vanilla HTML/CSS/JS.


Checker Sign-Off

  • Dimension 1 Copywriting: PASS
  • Dimension 2 Visuals: PASS
  • Dimension 3 Color: PASS
  • Dimension 4 Typography: PASS
  • Dimension 5 Spacing: PASS
  • Dimension 6 Registry Safety: PASS

Approval: pending


Phase: 05-live-show-execution UI-SPEC generated: 2026-04-07 Pre-populated from: CONTEXT.md (7 decisions), RESEARCH.md (5 patterns), style.css (all tokens)