--- phase: 04-timeline-editor plan: 04 type: execute wave: 3 depends_on: [04-02] files_modified: - lightsync/frontend/timeline/inspector.js - lightsync/frontend/timeline/timeline.js - lightsync/frontend/timeline/commands.js - lightsync/frontend/app.js autonomous: true requirements: [TL-04] must_haves: truths: - "Selecting a block shows the inspector strip between timeline and transport bar" - "Inspector displays animation type label, color picker, speed, direction, and length controls" - "Changing color in inspector updates the block params and is visible on canvas" - "Changing speed/direction/length updates block params through command pattern (undoable)" - "Deselecting a block hides the inspector" - "Remove button in inspector deletes the block" artifacts: - path: "lightsync/frontend/timeline/inspector.js" provides: "BlockInspector class rendering the inspector strip" exports: ["BlockInspector"] min_lines: 60 key_links: - from: "lightsync/frontend/timeline/inspector.js" to: "lightsync/frontend/timeline/timeline.js" via: "timeline._onSelectBlock callback triggers inspector show/hide" pattern: "_onSelectBlock" - from: "lightsync/frontend/timeline/inspector.js" to: "lightsync/frontend/timeline/commands.js" via: "param changes create UpdateParamsCommand through history" pattern: "UpdateParamsCommand\|history\\.execute" --- Build the block inspector panel — a compact horizontal strip that shows animation type, color picker, and parameter controls for the selected block. All parameter changes go through the command pattern for undo/redo support. Per D-01/D-02/D-03 from CONTEXT.md. Purpose: Without the inspector, users cannot edit block parameters (color, speed, direction). This is the last piece of the timeline editing UX. Output: A working inspector strip with color picker and parameter controls, fully integrated with undo/redo. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/04-timeline-editor/04-CONTEXT.md @.planning/phases/04-timeline-editor/04-RESEARCH.md @.planning/phases/04-timeline-editor/04-UI-SPEC.md @.planning/phases/04-timeline-editor/04-02-SUMMARY.md @lightsync/frontend/timeline/timeline.js @lightsync/frontend/timeline/commands.js @lightsync/frontend/timeline/history.js @lightsync/frontend/index.html @lightsync/frontend/style.css @lightsync/frontend/app.js class TimelineCanvas { selectedBlock: object | null // {id, timestamp, duration, animation, params, device_id} _onSelectBlock: function | null // callback(block) called on select/deselect history: CommandHistory tracks: [{device_id, device_name, strip_type, cues: []}] } block.params = { color: '#00ffff', // hex string speed: 1.0, // float 0.1-10 direction: 'forward', // 'forward' | 'reverse' length: 0.3 // float 0.05-1.0 } .block-inspector { height: 40px; display: flex; ... } .inspector-label { color: var(--text-accent); font-size: 14px; font-weight: 600; } .inspector-field { display: flex; align-items: center; gap: 4px; font-size: 10px; } .inspector-remove { margin-left: auto; } Task 1: UpdateParamsCommand + BlockInspector module lightsync/frontend/timeline/commands.js, lightsync/frontend/timeline/inspector.js lightsync/frontend/timeline/commands.js, lightsync/frontend/timeline/history.js, lightsync/frontend/index.html, lightsync/frontend/style.css **1. Add UpdateParamsCommand to commands.js:** Add a new command class and export it alongside the existing 4 commands: ```javascript export class UpdateParamsCommand { constructor(tracks, block, oldParams, newParams) { this.tracks = tracks; this.block = block; this.oldParams = { ...oldParams }; // shallow copy this.newParams = { ...newParams }; // shallow copy } execute() { Object.assign(this.block.params, this.newParams); syncBlock(this._showId, this.block.device_id, this.block, 'PATCH'); } undo() { Object.assign(this.block.params, this.oldParams); syncBlock(this._showId, this.block.device_id, this.block, 'PATCH'); } } ``` Add `UpdateParamsCommand` to the export list. **2. Create `lightsync/frontend/timeline/inspector.js` — BlockInspector class:** ```javascript import { UpdateParamsCommand } from './commands.js'; export class BlockInspector { constructor(containerEl, history) { this.el = containerEl; // #block-inspector div this.history = history; // CommandHistory reference this.tracks = null; // set by timeline this.currentBlock = null; this._colorInput = null; // hidden } show(block) { this.currentBlock = block; if (!block) { this.el.style.display = 'none'; return; } this.el.style.display = 'flex'; this._render(block); } hide() { this.currentBlock = null; this.el.style.display = 'none'; } _render(block) { const animName = (block.animation || 'BLOCK').toUpperCase().replace(/_/g, ' '); const color = block.params.color || '#00ffff'; const speed = block.params.speed ?? 1.0; const direction = block.params.direction || 'forward'; const length = block.params.length ?? 0.3; this.el.innerHTML = ` ${animName}
`; this._wireEvents(block); } _wireEvents(block) { // Color swatch → open hidden color input const swatch = this.el.querySelector('#color-swatch'); const colorInput = this.el.querySelector('#inspector-color'); swatch?.addEventListener('click', () => colorInput?.click()); colorInput?.addEventListener('input', (e) => { swatch.style.background = e.target.value; }); colorInput?.addEventListener('change', (e) => { const oldParams = { ...block.params }; const newParams = { ...block.params, color: e.target.value }; const cmd = new UpdateParamsCommand(this.tracks, block, oldParams, newParams); this.history.execute(cmd); }); // Speed this.el.querySelector('#inspector-speed')?.addEventListener('change', (e) => { const val = parseFloat(e.target.value); if (isNaN(val) || val < 0.1 || val > 10) return; const oldParams = { ...block.params }; const newParams = { ...block.params, speed: val }; const cmd = new UpdateParamsCommand(this.tracks, block, oldParams, newParams); this.history.execute(cmd); }); // Direction this.el.querySelector('#inspector-direction')?.addEventListener('change', (e) => { const oldParams = { ...block.params }; const newParams = { ...block.params, direction: e.target.value }; const cmd = new UpdateParamsCommand(this.tracks, block, oldParams, newParams); this.history.execute(cmd); }); // Length this.el.querySelector('#inspector-length')?.addEventListener('change', (e) => { const val = parseFloat(e.target.value); if (isNaN(val) || val < 0.05 || val > 1) return; const oldParams = { ...block.params }; const newParams = { ...block.params, length: val }; const cmd = new UpdateParamsCommand(this.tracks, block, oldParams, newParams); this.history.execute(cmd); }); // Remove button this.el.querySelector('.inspector-remove')?.addEventListener('click', () => { if (this._onDelete) this._onDelete(block); }); } } ``` The `_onDelete` callback will be set by app.js to fire a DeleteBlockCommand through the timeline's history.
cd /home/claude/led2 && test -f lightsync/frontend/timeline/inspector.js && grep -q "export class BlockInspector" lightsync/frontend/timeline/inspector.js && grep -q "inspector-color" lightsync/frontend/timeline/inspector.js && grep -q "inspector-speed" lightsync/frontend/timeline/inspector.js && grep -q "inspector-direction" lightsync/frontend/timeline/inspector.js && grep -q "inspector-length" lightsync/frontend/timeline/inspector.js && grep -q "inspector-remove" lightsync/frontend/timeline/inspector.js && grep -q "UpdateParamsCommand" lightsync/frontend/timeline/commands.js && grep -q "UpdateParamsCommand" lightsync/frontend/timeline/inspector.js && echo "ALL PASS" - lightsync/frontend/timeline/commands.js exports UpdateParamsCommand with execute()/undo() storing old/new params - lightsync/frontend/timeline/inspector.js exports BlockInspector class - BlockInspector.show(block) renders inspector HTML with animation label, color swatch, speed, direction, length inputs - BlockInspector.show(null) or hide() sets display:none - Color swatch click opens hidden `` - Speed input fires UpdateParamsCommand on change - Direction select fires UpdateParamsCommand on change - Length input fires UpdateParamsCommand on change - Remove button calls _onDelete callback - All inspector inputs use font-size 12px, monospace font, var(--bg-panel) background - Color input change event (not input) creates command (avoids flood during drag) UpdateParamsCommand added to command suite. BlockInspector renders compact strip with all parameter controls. All changes are undoable.
Task 2: Wire inspector into timeline and app.js lightsync/frontend/timeline/timeline.js, lightsync/frontend/app.js lightsync/frontend/timeline/timeline.js, lightsync/frontend/timeline/inspector.js, lightsync/frontend/app.js, lightsync/frontend/index.html **1. Update app.js initTimeline() to create and wire BlockInspector:** Add import at top of app.js: ```javascript import { BlockInspector } from './timeline/inspector.js'; import { DeleteBlockCommand } from './timeline/commands.js'; ``` In initTimeline(), after creating the TimelineCanvas instance, add: ```javascript // Inspector const inspectorEl = document.getElementById('block-inspector'); const inspector = new BlockInspector(inspectorEl, timeline.history); inspector.tracks = timeline.tracks; // Wire timeline selection → inspector timeline._onSelectBlock = (block) => { inspector.show(block); }; // Wire inspector delete → timeline inspector._onDelete = (block) => { const cmd = new DeleteBlockCommand(timeline.tracks, block); timeline.history.execute(cmd); timeline.selectedBlock = null; inspector.hide(); }; ``` Also, after `timeline.loadTracks(devices)` resolves, update inspector.tracks: ```javascript fetch('/api/devices').then(r => r.json()).then(devices => { timeline.loadTracks(devices); inspector.tracks = timeline.tracks; // keep reference in sync }).catch(e => console.warn('[timeline]', e)); ``` **2. Update timeline.js render() to use block color for block fill:** In the block rendering layer (layer 5 of render()), when drawing block fill, use the block's `params.color` if available to add a color accent to the block. Instead of flat `#0a2a2a`, use a dim version of the block's color: ```javascript // In _drawBlocks or block rendering section: for (const track of this.tracks) { const trackIdx = this.tracks.indexOf(track); for (const cue of track.cues) { const x = this.timeToX(cue.timestamp); const w = cue.duration * this.pixelsPerSecond; const y = this.trackIndexToY(trackIdx); // Skip if off-screen if (x + w < HEADER_WIDTH || x > width) continue; const isSelected = this.selectedBlock && this.selectedBlock.id === cue.id; // Block fill — use block color at low opacity for visual distinction const blockColor = cue.params?.color || '#00ffff'; if (isSelected) { ctx.fillStyle = '#0d3a3a'; } else { // Dim the block color: parse hex, set low alpha ctx.fillStyle = blockColor + '1a'; // ~10% opacity hex suffix } ctx.fillRect(x, y + 1, w, TRACK_HEIGHT - 2); // Border ctx.strokeStyle = isSelected ? '#00ffff' : '#1a4a4a'; ctx.lineWidth = 1; ctx.strokeRect(x, y + 1, w, TRACK_HEIGHT - 2); // Label ctx.fillStyle = '#00ffff'; ctx.font = '10px monospace'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; const label = (cue.animation || '').toUpperCase().replace(/_/g, ' '); // Clip label to block width ctx.save(); ctx.beginPath(); ctx.rect(x + 4, y, w - 8, TRACK_HEIGHT); ctx.clip(); ctx.fillText(label, x + 6, y + TRACK_HEIGHT / 2); ctx.restore(); } } ``` This makes color changes immediately visible on the canvas since the block fill incorporates the selected color. **3. Ensure undo of param change re-renders inspector:** After undo/redo in the keydown handler (already in timeline.js from Plan 02), if `this.selectedBlock` is still set, re-trigger `_onSelectBlock` to refresh the inspector display: ```javascript // In the undo keydown handler: if (e.ctrlKey && !e.shiftKey && e.key === 'z') { e.preventDefault(); this.history.undo(); if (this._onSelectBlock) this._onSelectBlock(this.selectedBlock); } // Same for redo: if ((e.ctrlKey && e.shiftKey && e.key === 'Z') || (e.ctrlKey && e.key === 'y')) { e.preventDefault(); this.history.redo(); if (this._onSelectBlock) this._onSelectBlock(this.selectedBlock); } ``` This ensures the inspector values refresh when undo/redo changes the selected block's params. cd /home/claude/led2 && grep -q "import.*BlockInspector" lightsync/frontend/app.js && grep -q "_onSelectBlock" lightsync/frontend/app.js && grep -q "inspector\\.show\|inspector\\.hide" lightsync/frontend/app.js && grep -q "_onDelete" lightsync/frontend/app.js && grep -q "params.*color\|blockColor" lightsync/frontend/timeline/timeline.js && grep -q "_onSelectBlock.*selectedBlock" lightsync/frontend/timeline/timeline.js && echo "ALL PASS" - app.js imports BlockInspector and DeleteBlockCommand - app.js creates BlockInspector with #block-inspector element and timeline.history - app.js sets timeline._onSelectBlock callback to call inspector.show(block) - app.js sets inspector._onDelete callback to execute DeleteBlockCommand and hide inspector - inspector.tracks is set to timeline.tracks reference - timeline.js block rendering uses block's params.color for fill accent - timeline.js undo/redo handlers re-trigger _onSelectBlock to refresh inspector - Clicking a block shows inspector strip with correct animation name, color, speed, direction, length - Clicking canvas background hides inspector - Changing color in inspector updates block on canvas - Remove button in inspector deletes block and hides inspector Inspector fully wired: shows on block select, hides on deselect, all param changes undoable, remove button works, block color visible on canvas, undo/redo refreshes inspector values.
1. Docker rebuild and restart 2. Place a block on timeline — click it — inspector appears between timeline and transport bar (per D-03) 3. Inspector shows: animation label in cyan uppercase, color swatch, SPEED input, DIR select, LEN input, remove button (per D-02) 4. Click color swatch — native color picker opens 5. Change color — block on canvas updates to show new color accent 6. Change speed to 2.0 — press Ctrl+Z — speed reverts to 1.0 (undo works for params) 7. Click remove button — block deleted, inspector hides 8. Ctrl+Z — block reappears, click it — inspector shows with original params 9. Click empty canvas area — inspector hides (per D-01: hidden when nothing selected) - Inspector panel shows/hides based on block selection (D-01) - Inspector displays label, color, speed, direction, length in horizontal strip (D-02) - Layout is TIMELINE > INSPECTOR > TRANSPORT (D-03) - All parameter changes go through command pattern (undoable) - Block color is visually reflected on canvas - Remove button deletes block via command pattern After completion, create `.planning/phases/04-timeline-editor/04-04-SUMMARY.md`