13 KiB
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), 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-inspectorat 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-animpattern)
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):
- Panel header text (
color: var(--text-accent)) - Active animation tile highlight
- Active snap button state (background fill + border)
- Transport button hover state (background fill)
- Timeline cursor (playback position line)
- Selected block outline on timeline canvas
- Header title
LIGHTSYNC - 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):
.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:
- Audio plays in browser →
timeupdatesendstickto server at 10Hz - Server fires UDP cue → broadcasts
preview_updatemessage - Browser receives
preview_update→ updates.device-stripcolor and animation label within one event loop tick - 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:
audio.currentTimeupdates- Browser sends
{"type": "seek", "position": N}to server - Server rebuilds
fired_ids(all cues beforeposition − 60msmarked fired) - All device strips reset to inactive state (dark) — browser clears
.device-strip.activeclass on seek - 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
- User selects show from dropdown → clicks "LOAD"
- Button shows "..." while
GET /api/shows/{id}resolves - Timeline tracks populate with saved blocks
setCurrentShowId(id)called in commands.js- WebSocket
loadmessage sent:{"type": "load", "show_id": "{id}", "path": "{audio_path}"} - Server acquires cue list per-connection
- Preview panel becomes visible (
display: flex) - 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)