diff --git a/.planning/phases/05-live-show-execution/05-UI-SPEC.md b/.planning/phases/05-live-show-execution/05-UI-SPEC.md new file mode 100644 index 0000000..9e5562f --- /dev/null +++ b/.planning/phases/05-live-show-execution/05-UI-SPEC.md @@ -0,0 +1,340 @@ +--- +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`) | +| 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.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. + +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: 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:** +``` + + +``` + +**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. + +--- + +## 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)*