From 7189df79574906cebb0ca936a8941e687c50ec63 Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Sun, 24 May 2026 14:53:56 -0400 Subject: [PATCH] docs: add NLE editor React polish plan (phases 1-3) --- ...-24-nle-editor-react-polish-phase-1-2-3.md | 797 ++++++++++++++++++ 1 file changed, 797 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-24-nle-editor-react-polish-phase-1-2-3.md diff --git a/docs/superpowers/plans/2026-05-24-nle-editor-react-polish-phase-1-2-3.md b/docs/superpowers/plans/2026-05-24-nle-editor-react-polish-phase-1-2-3.md new file mode 100644 index 0000000..5d95d38 --- /dev/null +++ b/docs/superpowers/plans/2026-05-24-nle-editor-react-polish-phase-1-2-3.md @@ -0,0 +1,797 @@ +# NLE Editor: React SPA Polish — Phases 1–3 Implementation Plan + +> **Date:** 2026-05-24 +> **Status:** Draft — awaiting user review before execution +> **Reference spec:** `docs/superpowers/specs/2026-05-18-editor-growing-file-features-design.md` +> **Architecture note:** The original NLE editor plan (`2026-05-18-nle-editor.md`) was written for standalone HTML pages. Commit `6322b61` deleted those pages — the React SPA is now the only entry. This plan adapts Phase 1 for the React SPA architecture. Phases 2–3 follow the original design spec. + +--- + +## Architectural Context + +**Backend (complete, no changes needed):** +- `003-editor-sequences.sql` migration applied — `sequences` + `sequence_clips` tables live +- `routes/sequences.js` — full CRUD, clip sync (`PUT /:id/clips`), EDL export — registered in `index.js` + +**Frontend (React SPA):** +- `screens-editor.jsx` — skeleton with all features disabled behind "In Development" overlay +- `shell.jsx` — Editor nav item with `DEV` badge +- `app.jsx` — `case 'editor': content = ` +- `data.jsx` — **no** sequence API helpers exist +- `js/timecode.js` — 59.94 DF timecode (vanilla JS, unused by SPA) +- `js/timeline.js` — DOM timeline engine (vanilla JS, unused by SPA) + +**Other services:** +- `services/editor/` — OpenReel Video (separate WebCodecs/WebGPU NLE, proxied at `/editor/`). Not part of this plan's scope — the React SPA editor is the primary in-app NLE. +- `services/premiere-plugin/` — Premiere Pro CEP panel (external integration, not in scope) + +--- + +## File Map + +### Phase 1 — Core Editor + +| Action | Path | Description | +|--------|------|-------------| +| MODIFY | `services/web-ui/public/data.jsx` | Add `getSequences`, `createSequence`, `getSequence`, `updateSequence`, `deleteSequence`, `syncSequenceClips`, `exportSequenceEDL` | +| MODIFY | `services/web-ui/public/screens-editor.jsx` | Full rewrite: source monitor, media panel, timeline, program monitor, sequence management, auto-save, undo/redo, EDL export | +| MODIFY | `services/web-ui/public/shell.jsx` | Remove `DEV` badge from Editor nav item | +| NO CHANGE | `services/web-ui/public/js/timecode.js` | Imported by screens-editor.jsx via ` +``` + +For the React component, create a thin wrapper: +```js +// Inside screens-editor.jsx, before the Editor component: +function useTimecode() { + // Ensure TC is loaded + if (typeof TC === 'undefined') { + console.warn('timecode.js not loaded — timecode functions unavailable'); + } + return { + framesToTC: (f) => TC ? TC.framesToTC(f) : String(f), + tcToFrames: (tc) => TC ? TC.tcToFrames(tc) : 0, + secondsToFrames: (s) => TC ? TC.secondsToFrames(s) : Math.round(s * 60), + framesToSeconds: (f) => TC ? TC.framesToSeconds(f) : f / 60, + FPS: TC ? TC.FPS : 59.94, + }; +} +``` + +**Verify:** In browser console — `TC.framesToTC(3596)` → `"00:01:00;00"` + +--- + +### Task 1.3: React Timeline Component + +**Files:** MODIFY `services/web-ui/public/screens-editor.jsx` (add components inline, same file pattern as other screens) + +Build a `TimelinePanel` React component that wraps the existing `timeline.js` engine: + +```js +function TimelinePanel({ clips, onClipsChanged, playheadFrames, onPlayheadMoved, fps }) { + const containerRef = React.useRef(null); + const engineRef = React.useRef(null); + + React.useEffect(() => { + if (!containerRef.current || engineRef.current) return; + engineRef.current = window.Timeline; + window.Timeline.init(containerRef.current, { + fps: fps || 59.94, + scale: 100, + onClipsChanged: onClipsChanged, + onPlayheadMoved: onPlayheadMoved, + }); + return () => { /* cleanup */ }; + }, []); + + React.useEffect(() => { + if (engineRef.current && clips) { + engineRef.current.render(clips, { fps: fps || 59.94 }); + } + }, [clips, fps]); + + React.useEffect(() => { + if (engineRef.current) { + engineRef.current.setPlayhead(playheadFrames || 0); + } + }, [playheadFrames]); + + return ( +
+
+ {/* Tool buttons: Select(V), Razor(C), Hand(H) */} + {/* Undo/Redo */} + {/* Save status indicator */} +
+
+
+ ); +} +``` + +The component mounts the timeline once, then pushes clip/playhead updates via `render()` and `setPlayhead()`. No rebuild needed on every frame. + +**Tool toolbar state:** +- Three tool buttons (V/C/H) that call `Timeline.setTool(name)` +- Active tool highlighted — sync with keyboard shortcuts +- Undo/redo buttons call the editor's history stack (not timeline.js's — timeline.js doesn't have an undo stack, the editor page in the original plan managed it in the app state) + +**Verify:** Timeline renders 4 track rows (V1, V2, A1, A2) with ruler, playhead at frame 0, no clips. Tool buttons switch cursor mode. + +--- + +### Task 1.4: Rewrite screens-editor.jsx + +**Files:** MODIFY `services/web-ui/public/screens-editor.jsx` + +Full rewrite preserving the `Editor` component on `window.Editor`. The new structure: + +``` +┌─ SOURCE MONITOR ───────┬─ PROGRAM MONITOR ───────┐ +│ [video preview] │ [video preview] │ +│ TC: 00:00:00;00 │ TC: 00:00:00;00 │ +│ [=====scrub=========] │ [=====scrub=========] │ +│ [In] [Out] [Insert] │ [▶] [⏮] [⏭] │ +├─ MEDIA PANEL ──────────┴─ TIMELINE ─────────────┤ +│ [sequence picker ▼] [+]│ [V] [C] [H] | [↩] [↪] │ +│ [filter by bin ▼] │ ruler: 00:00 — 00:05 … │ +│ [asset list] │ V1 ░░░░[clip]░░░░ │ +│ │ V2 │ +│ │ A1 ░░░░[clip]░░░░ │ +│ │ A2 │ +└────────────────────────┴─────────────────────────┘ +``` + +**App state (React hooks):** +```js +const [projectId, setProjectId] = React.useState(null); +const [sequences, setSequences] = React.useState([]); +const [currentSeq, setCurrentSeq] = React.useState(null); // { id, name, clips: [...] } +const [assets, setAssets] = React.useState([]); // from ZAMPP_DATA.ASSETS +const [sourceAsset, setSourceAsset] = React.useState(null); +const [srcIn, setSrcIn] = React.useState(null); +const [srcOut, setSrcOut] = React.useState(null); +const [playing, setPlaying] = React.useState(false); +const [playheadFrames, setPlayheadFrames] = React.useState(0); +const [saveStatus, setSaveStatus] = React.useState(''); +const [history, setHistory] = React.useState([[]]); +const [historyIdx, setHistoryIdx] = React.useState(0); +``` + +**Source monitor:** +- `