docs(04): UI design contract for timeline editor phase
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
318
.planning/phases/04-timeline-editor/04-UI-SPEC.md
Normal file
318
.planning/phases/04-timeline-editor/04-UI-SPEC.md
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user