Files
led2/.planning/phases/05-live-show-execution/05-UI-SPEC.md

347 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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.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.
**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:**
```
<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.
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)*