From ce31a451245d2ba6d6ec6494add85fce2f33092a Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Sun, 24 May 2026 16:20:38 -0400 Subject: [PATCH] feat(editor): Phase 1 core NLE editor React SPA rewrite --- ...-24-nle-editor-react-polish-phase-1-2-3.md | 846 ++---------------- services/web-ui/public/data.jsx | 56 +- services/web-ui/public/index.html | 2 + services/web-ui/public/screens-editor.jsx | 565 +++++++++--- services/web-ui/public/shell.jsx | 2 +- 5 files changed, 548 insertions(+), 923 deletions(-) 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 index 5d95d38..4e05d9b 100644 --- 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 @@ -1,797 +1,73 @@ # 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. +> **Status:** Phase 1 IN PROGRESS +> **Progress:** Tasks 1.1–1.6 code-complete, pending test/deploy --- -## Architectural Context +## Phase 1 — Core Editor (IN PROGRESS) -**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` +### Task 1.1: Sequence API helpers in data.jsx ✅ +- Added `getSequences`, `createSequence`, `getSequence`, `updateSequence`, `deleteSequence`, `syncSequenceClips`, `exportSequenceEDL` to `data.jsx` +- All exported on `window.ZAMPP_API` -**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) +### Task 1.2: Timecode.js wired into SPA ✅ +- Added ` -``` - -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:** -- `