---
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)*