Files
led2/.planning/phases/04-timeline-editor/04-UI-SPEC.md
2026-04-06 23:09:42 +00:00

319 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: 4
slug: timeline-editor
status: draft
shadcn_initialized: false
preset: none
created: 2026-04-06
---
# Phase 4 — UI Design Contract
> Visual and interaction contract for the Timeline Editor phase. Generated by gsd-ui-researcher.
> Stack is plain HTML5/CSS/JS served by FastAPI — no React, no shadcn, no build step.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | none — hand-rolled CSS variables |
| Preset | not applicable |
| Component library | none — plain DOM elements |
| Icon library | Unicode glyphs inline (▶, ■, ▼, ✕, >) — no icon font |
| Font | JetBrains Mono, Fira Code, Consolas, monospace (system fallback chain) |
Source: `lightsync/frontend/style.css` — CSS variable declarations at `:root`.
---
## Spacing Scale
Declared values (must be multiples of 4):
| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Gap between inline inspector fields, scrollbar width |
| sm | 8px | Panel content padding, element gaps |
| md | 16px | Default spacing between grouped elements |
| lg | 24px | Section-level padding |
| xl | 32px | Layout gaps |
| 2xl | 48px | TRACK_HEIGHT — per-device track row height on canvas |
| 3xl | 64px | Reserved; not used in this phase |
Exceptions:
- TIME_AXIS_HEIGHT: 24px — time ruler height above tracks
- HEADER_WIDTH: 140px — track label column width (device names)
- Timeline toolbar height: 32px — compact single row above canvas
- Block inspector strip height: 40px — compact horizontal strip (D-02)
- Resize handle hit zone: 8px on left/right edge of each canvas block
- Snap threshold: 100ms (not a pixel dimension, but a time threshold for beat snap)
Source: RESEARCH.md Pattern 1 (coordinate system constants); CONTEXT.md D-02.
---
## Typography
| Role | Size | Weight | Line Height | Usage |
|------|------|--------|-------------|-------|
| Body | 13px | 400 | 1.4 | Default text, form inputs, device names |
| Label | 10px | 400 | 1.2 | Panel headers (DEVICES, ANIMATIONS, TIMELINE), toolbar labels, canvas track names |
| Compact | 12px | 400 | 1.3 | Transport controls, animation names, inspector field values, beat tick labels |
| Heading | 14px | 600 | 1.2 | Header title (LIGHTSYNC), block animation type label in inspector |
All text: font-family `var(--font-mono)` — monospace throughout.
Text transform: uppercase for all panel headers, labels, and inspector field names.
Letter spacing: 0.14em on panel headers; 0.06em on animation names; 0.15em on header title.
Source: `style.css``.panel-header` (10px/0.14em), `.header-title` (14px/600), `.transport-time` (12px), `:root` (13px base).
---
## Color
| Role | Value | Usage |
|------|-------|-------|
| Dominant (60%) | `#0a0a0a` | Body background, main-area background, canvas background |
| Secondary (30%) | `#0f0f0f` (`--bg-panel`) / `#080808` (`--bg-panel-dark`) | Sidebar, header, transport bar, block inspector strip, timeline toolbar |
| Accent (10%) | `#00ffff` | See reserved list below |
| Destructive | `#ff3333` | Delete block hover state on remove controls only |
| Dim border | `#1a4a4a` (`--border-dim`) | All panel borders, track separator lines, time axis tick marks |
| Waveform background | `#1a3a3a` | Waveform peaks drawn behind blocks (very dim cyan — not accent) |
| Beat marks | `#00ffff` at 40% opacity | Thin vertical beat tick marks on timeline canvas |
| Playback cursor | `#00ffff` | 1px vertical line spanning full canvas height |
| Selected block | `#00ffff` border at 1px | Selected block outline |
| Block fill (default) | `#0a2a2a` | Unselected animation block fill |
| Block fill (selected) | `#0d3a3a` | Selected animation block fill |
| Ghost block (drag) | `#00ffff` at 20% opacity | Block preview during drag-and-place |
Accent (`#00ffff`) reserved for:
- Playback cursor line
- Selected block border
- Active animation type label in inspector (`.inspector-label`)
- Panel header text (`--text-accent`)
- Transport button hover state (accent fills button bg, primary bg for text)
- Beat indicator flash in header
- Timeline toolbar labels in focus/active state
- `>` cursor glyph in active animation palette row
Accent is NOT used for:
- Block fills (blocks use `#0a2a2a` / `#0d3a3a`)
- Waveform strokes (use `#1a3a3a`)
- Time axis background ticks (use `--border-dim` at reduced opacity)
Source: `style.css` CSS variables; CONTEXT.md "terminal aesthetic carries forward"; RESEARCH.md Pattern 6 (waveform color `#1a3a3a`).
---
## Component Inventory
### New in Phase 4
| Component | Type | Description |
|-----------|------|-------------|
| `#timeline-toolbar` | HTML div (flex row) | 32px strip above canvas: zoom control, beat offset input, snap toggle |
| `#timeline-canvas` | HTML5 Canvas | Fills remaining flex space in main-area; custom hit-testing, virtual scroll |
| `#block-inspector` | HTML div (flex row) | 40px horizontal strip between canvas and transport-bar; hidden when no block selected |
| Animation palette rows | HTML buttons in sidebar | Compact single-line rows below DEVICES panel; drag source for placing blocks |
### Modified in Phase 4
| Component | Change |
|-----------|--------|
| `.main-area` | Replaces animation tile grid with: toolbar + canvas + inspector (flex column) |
| `.sidebar` | Adds ANIMATIONS panel below DEVICES; items are compact drag-source rows |
### Retained Unchanged
Transport bar, header, device panel, all CSS variables, all form element styles.
---
## Canvas Rendering Contract
All canvas rendering uses HTML5 Canvas 2D API. No third-party canvas library.
### Coordinate System
```
HEADER_WIDTH = 140px // track label column
TIME_AXIS_HEIGHT = 24px
TRACK_HEIGHT = 48px
PIXELS_PER_SECOND = 100 // default zoom; adjustable via toolbar
```
### Layer Draw Order (back to front)
1. Background fill `#0a0a0a`
2. Track row alternating banding: even `#0a0a0a`, odd `#0c0c0c` (2px brightness difference only)
3. Waveform peaks: `#1a3a3a` vertical bars behind blocks
4. Beat marks: `#00ffff` at 40% opacity, 1px wide vertical lines at calibrated beat times
5. Animation blocks: filled rects `#0a2a2a`, 1px border `#1a4a4a`; selected: `#0d3a3a` fill, `#00ffff` 1px border
6. Block labels: animation name in `#00ffff`, 10px, uppercase, clipped to block width
7. Drag ghost: 1px border `#00ffff`, `rgba(0,255,255,0.15)` fill
8. Track headers: `#0f0f0f` fill, device name in `--text-primary` 12px
9. Time axis: `#080808` fill, time labels `--text-dim` 10px, tick marks `--border-dim`
10. Playback cursor: `#00ffff` 1px line, full height
### devicePixelRatio Scaling
Canvas must call `resizeCanvas()` on init and on `window resize`. Scale ctx by `window.devicePixelRatio` to prevent blur on HiDPI.
---
## Interaction Contract
### Block Placement (TL-02)
1. User drags from animation palette row in sidebar.
2. Ghost block follows mouse across canvas at target track Y position.
3. On `mouseup` over a track: snap to nearest beat if within 100ms threshold; place block at snapped time.
4. On `mouseup` outside canvas: cancel drag, remove ghost.
5. Attach `mousemove` and `mouseup` to `document` during drag; remove on `mouseup`. (Pitfall 2 prevention.)
### Block Move (TL-03)
1. `mousedown` on block center region (not within 8px edge): enter move mode.
2. `mousemove`: update block start time live on canvas (visual only, no API call during drag).
3. `mouseup`: snap to beat; fire `MoveBlockCommand.execute()` → updates local state → async PATCH to backend.
### Block Resize (TL-03)
1. `mousedown` within 8px of left or right block edge: enter resize mode; cursor changes to `ew-resize`.
2. `mousemove`: update duration or start time live on canvas.
3. `mouseup`: snap to beat; fire `ResizeBlockCommand.execute()`.
### Block Delete (TL-03)
- `Delete` key when block selected: fire `DeleteBlockCommand.execute()` immediately. No confirmation dialog for individual block delete.
- Block remove button in inspector (`✕`): same behavior as Delete key.
### Block Selection
- `mousedown` on block body (not dragging): select block, show inspector strip.
- `mousedown` on canvas background: deselect, hide inspector strip.
- Only one block selected at a time (no multi-select in Phase 4).
### Undo/Redo (TL-06)
- `Ctrl+Z`: `CommandHistory.undo()`
- `Ctrl+Shift+Z` or `Ctrl+Y`: `CommandHistory.redo()`
- Minimum 50 undo steps (CommandHistory `maxSteps = 50`).
- All mutations (place, move, resize, delete, param change) must go through `CommandHistory.execute()`.
### Beat Snap (TL-08)
- Snap active by default; toggle via "SNAP" button in toolbar (lit accent when on, dim when off).
- Snap threshold: 100ms.
- Apply calibration offset before snap: `displayBeat = rawBeat - calibrationOffset`.
### Timeline Scroll
- Horizontal scroll via mouse wheel on canvas: updates `scrollX` offset.
- No native CSS overflow scrollbar on canvas — virtual scroll only.
- Auto-scroll: when playback cursor would exit the visible window, scroll to keep cursor at 20% from left edge.
---
## Inspector Strip Contract (D-01, D-02, D-03)
Height: 40px. Display: flex row. Background: `--bg-panel-dark`. Border-top: `1px solid var(--border-dim)`.
Hidden (`display: none`) when no block selected. Shown (`display: flex`) when block selected.
Field order (left to right):
1. `LABEL` — animation type name, `--text-accent`, 14px, 600 weight, uppercase, `flex-shrink: 0`, min-width 120px
2. `COLOR` — label + `<input type="color">` hidden, visible swatch 16×16px with 1px `--border-dim` border; click swatch triggers color input
3. `SPEED` — label + `<input type="number" step="0.1" min="0.1" max="10">`, width 60px
4. `DIR` — label + `<select>` with FWD / REV options
5. `LEN` — label + `<input type="number" step="0.05" min="0.05" max="1">`, width 60px
6. Spacer (`flex: 1`)
7. `✕` — remove button, `--text-dim`, hover: `#ff3333`
All field labels: 10px, uppercase, `--text-dim`, letter-spacing 0.1em.
All inputs: `var(--font-mono)`, 12px, `--bg-panel`, `--border-dim` border, no border-radius.
---
## Timeline Toolbar Contract
Height: 32px. Background: `--bg-panel-dark`. Border-bottom: `1px solid var(--border-dim)`.
Display: flex row, align-center, gap 16px, padding 0 12px.
Fields (left to right):
1. Zoom: `ZOOM` label + `<input type="range" min="20" max="400" step="10" value="100">` — updates `pixelsPerSecond`
2. Separator: 1px `--border-dim` vertical line, 20px tall
3. Beat offset: `BEAT OFFSET` label + `<input type="number" step="0.005" min="-0.2" max="0.2" value="0.030">` width 70px + `s` unit label
4. Separator
5. Snap toggle: `SNAP` button — `.transport-btn` style, lit (accent bg, primary color text) when active, dim when inactive
All labels: 10px, uppercase, `--text-dim`, letter-spacing 0.1em.
---
## Animation Palette Panel (Sidebar)
Placed below DEVICES panel in sidebar. Panel header: `ANIMATIONS` — same `.panel-header` style.
Items: one row per animation type (7 total). Each row is a draggable element styled as `.anim-tile`.
Compact layout: `>` glyph + animation name + brief description, same as existing `.anim-tile` pattern.
Drag cursor: `grab` on hover, `grabbing` during drag.
---
## Copywriting Contract
| Element | Copy |
|---------|------|
| Primary CTA (place block) | PLACE BLOCK |
| Animation palette panel header | ANIMATIONS |
| Timeline toolbar label | TIMELINE |
| Empty timeline state (no devices) | NO DEVICES — add a device in the sidebar to create tracks |
| Empty timeline state (devices, no audio) | LOAD AUDIO — use the transport bar to load a file |
| Empty track state (device exists, no blocks) | (no label — empty track row is visually self-evident) |
| Beat offset field label | BEAT OFFSET |
| Snap toggle | SNAP |
| Beat marks loading state | DETECTING BEATS... (shown as dim text in time axis area) |
| Beat marks load error | BEAT DETECTION FAILED — check audio file |
| Block delete (no confirmation dialog needed — undo covers it) | (immediate, no confirm) |
| Inspector: block removed | (inspector hides; no toast) |
| Undo available indicator | (Ctrl+Z visible in toolbar tooltip only — no persistent UI) |
No modal confirmations for any Phase 4 action. All destructive actions are undoable via Ctrl+Z.
---
## Error States
| Scenario | Visual Treatment | Copy |
|----------|-----------------|------|
| Beat detection API call fails | Dim status text in time axis | BEAT DETECTION FAILED |
| Block PATCH/DELETE API call fails | Console warning only (silent in UI — client is source of truth) | — |
| Block placed over existing block on same track | Placement rejected; ghost turns `#ff3333` briefly | OVERLAP — cannot place here |
| Audio not loaded when timeline renders | Dim placeholder text in canvas center | LOAD AUDIO — transport bar |
| No devices registered | Dim placeholder text in canvas center | NO DEVICES — add a device |
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none | not applicable — no shadcn |
| third-party | none | not applicable |
No third-party component registries. All UI is hand-rolled 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