--- phase: 5 slug: live-show-execution status: draft shadcn_initialized: false preset: none created: 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`), live-preview top/bottom padding | | sm | 8px | Element gaps inside device strips, panel content padding top/bottom | | lg | 16px | Header/transport bar horizontal padding | | xl | 24px | — | | 2xl | 32px | Timeline toolbar fixed height | 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) Fixed dimensions (not spacing tokens — declared here for executor reference): - Transport bar grid row height: 52px (existing fixed dimension, unchanged from Phase 4) 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.10–0.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. **Focal point:** The active `.device-strip` color swatch is the primary visual signal during playback. The color fill (data-driven RGB from the animation cue) is the focal point of the live preview panel — all other elements in the strip are secondary context for which device and animation is active. 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):** ```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: 4px 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: 12px; 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:** ``` ``` **Label:** "LOAD" — consistent with existing "LOAD" button for audio. **Empty state:** `` 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. Fixed dimensions (not spacing tokens): - Transport bar grid row height: 52px (existing, do not change) --- ## 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)*