docs(05): UI design contract
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
340
.planning/phases/05-live-show-execution/05-UI-SPEC.md
Normal file
340
.planning/phases/05-live-show-execution/05-UI-SPEC.md
Normal file
@@ -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:**
|
||||||
|
```
|
||||||
|
<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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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)*
|
||||||
Reference in New Issue
Block a user