# 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:**
- `