@elah/editor 0.4.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,179 +1,241 @@
1
- # @elah/editor
2
-
3
- The full Elah video editor SDK for React. Combines the core engine, timeline UI, WebGL2 renderer, media library, and export pipeline into a single package.
4
-
5
- Ships `EditorProvider`, `Preview` (WebGL2 canvas + interactive transform overlays), `Timeline`, `AssetPanel`, `SourcePanel`, and `ElementsPanel`, and re-exports the public `@elah/core`, `@elah/react`, and `@elah/timeline` API — so most apps only ever import from `@elah/editor`. (Renderer and debug internals are the exception; import those from `@elah/core` directly.) Supports video, image, text, **shape**, and **freehand** clips, **multi-track audio**, and MP4 export.
6
-
7
- [![npm](https://img.shields.io/npm/v/@elah/editor)](https://www.npmjs.com/package/@elah/editor)
8
- [![gzip size](https://img.shields.io/badge/gzip-~63%20KiB%20full%20SDK-brightgreen)](../../BUNDLE_STRATEGY.md)
9
- [![license](https://img.shields.io/badge/license-Apache--2.0-blue)](https://github.com/elahlabs/elah/blob/main/LICENSE)
10
-
11
- ---
12
-
13
- ## Install
14
-
15
- ```bash
16
- npm install @elah/editor
17
- ```
18
-
19
- Peer dependencies: `react`, `react-dom` >= 18, `lucide-react` >= 0.400.0 (used by the
20
- bundled `@elah/timeline` UI for clip icons — install it alongside `react`/`react-dom`
21
- even though nothing in your own code imports it).
22
-
23
- **Bundle size:** ~10 KiB gzipped for the editor layer (51 KiB raw); ~63 KiB gzipped for the full SDK graph (`core` + `timeline` + `editor`, 330 KiB raw). `mediabunny` is injected, never bundled — see [BUNDLE_STRATEGY.md](../../BUNDLE_STRATEGY.md).
24
-
25
- ---
26
-
27
- ## Quick start
28
-
29
- ```tsx
30
- import { EditorProvider, Timeline } from '@elah/editor'
31
-
32
- function App() {
33
- return (
34
- <EditorProvider fps={30}>
35
- <Timeline style={{ height: 300 }} />
36
- </EditorProvider>
37
- )
38
- }
39
- ```
40
-
41
- ---
42
-
43
- ## Styling
44
-
45
- Import the compiled stylesheets once. They are plain CSS — your app does **not**
46
- need Tailwind, and no utility class names leak into your global scope (preflight
47
- is disabled, so nothing resets your elements):
48
-
49
- ```ts
50
- import '@elah/timeline/styles.css'
51
- import '@elah/editor/styles.css'
52
- import '@elah/editor/styles/tokens.css' // --elah-* dark defaults (standalone)
53
- ```
54
-
55
- When embedding inside an app that already defines `.elah-root` (mapping `--elah-*`
56
- onto its own design system), skip `tokens.css`. Re-theme or white-label by
57
- overriding `--elah-*` variables in your own `.elah-root` scope — see
58
- [design-tokens.md](https://github.com/elahlabs/elah/blob/main/docs/design-tokens.md).
59
-
60
- ---
61
-
62
- ## With preview and asset panel
63
-
64
- ```tsx
65
- import { EditorProvider, Timeline, Preview, AssetPanel, createDefaultDemuxerFactory } from '@elah/editor'
66
-
67
- const demuxerFactory = createDefaultDemuxerFactory()
68
-
69
- function App() {
70
- return (
71
- <EditorProvider fps={30}>
72
- <div style={{ display: 'flex', height: '100vh' }}>
73
- <AssetPanel style={{ width: 220 }} />
74
- <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
75
- <Preview demuxerFactory={demuxerFactory} style={{ flex: 1 }} />
76
- <Timeline style={{ height: 300 }} />
77
- </div>
78
- </div>
79
- </EditorProvider>
80
- )
81
- }
82
- ```
83
-
84
- ---
85
-
86
- ## Import media
87
-
88
- ```ts
89
- import { importFiles, importUrl, importBlob, useMediaLibrary } from '@elah/editor'
90
-
91
- await importFiles(Array.from(fileList)) // local files
92
- await importUrl('https://example.com/clip.mp4') // remote URL
93
- await importBlob(recordedBlob, { name: 'take-1.webm' })
94
-
95
- // Subscribe in React — useMediaLibrary() takes no arguments and returns
96
- // { assets, getAsset, removeAsset, updateAsset, importFiles, importUrl, importBlob }
97
- // with assets in insertion order. `useAssets` is an alias for the same hook.
98
- const { assets, importFiles: addFiles } = useMediaLibrary()
99
- ```
100
-
101
- ### Programmatic insertion (no drag)
102
-
103
- ```ts
104
- import { insertMediaAsset } from '@elah/editor'
105
-
106
- // Place an imported asset onto the timeline — powers tap-to-add on touch.
107
- // Returns a typed InsertAssetResult ({ ok, kind, trackId, clipIds } | { ok:false, reason }).
108
- const result = await insertMediaAsset(engine, assetId, { desiredStartFrame: 0 })
109
- ```
110
-
111
- ---
112
-
113
- ## Export to MP4
114
-
115
- ```ts
116
- import { exportVideo } from '@elah/editor'
117
-
118
- const blob = await exportVideo(engine.getProject(), {
119
- videoBitrate: 8_000_000,
120
- onProgress: ({ frame, totalFrames }) => {
121
- console.log(`${Math.round((frame / totalFrames) * 100)}%`)
122
- },
123
- })
124
- ```
125
-
126
- Runs in a web worker. Reuses `resolveTimeline` + the GPU renderer's placement math.
127
-
128
- ---
129
-
130
- ## Keyboard shortcuts
131
-
132
- | Key | Action |
133
- |---|---|
134
- | `Space` | Play / pause |
135
- | `S` | Split clip at playhead |
136
- | `Delete` / `Backspace` | Delete selected clip(s) |
137
- | `Ctrl/Cmd + C` | Copy |
138
- | `Ctrl/Cmd + V` | Paste at playhead |
139
- | `Ctrl/Cmd + Z` | Undo |
140
- | `Ctrl/Cmd + Shift + Z` / `Ctrl/Cmd + Y` | Redo |
141
- | `Ctrl/Cmd + scroll` | Zoom |
142
- | `← / →` | Step one frame |
143
-
144
- ---
145
-
146
- ## Package layers
147
-
148
- ```
149
- @elah/core — engine, playback, resolver, vanilla stores, media, export (framework-agnostic)
150
- @elah/react — React bindings: EditorContext, store hooks, audio hooks
151
- @elah/timeline — React timeline UI components and hooks (consumes @elah/core + @elah/react)
152
- @elah/editor — EditorProvider, Preview, AssetPanel + re-exports @elah/core, @elah/react, @elah/timeline
153
- @elah/cli — headless split/trim/build/export/serve on top of @elah/core
154
- ```
155
-
156
- Use `@elah/editor` for the full in-browser experience. Use `@elah/core` directly
157
- for headless or custom rendering pipelines, or pair it with `@elah/react` if you
158
- want the hooks without the timeline UI. Use `@elah/cli` for automation,
159
- AI-generation pipelines, and server-side rendering.
160
-
161
- **One active project per page.** `@elah/core`'s stores (`tracksStore`,
162
- `playbackStore`, `selectionStore`, `transitionsStore`, `mediaLibraryStore`) are
163
- module-level singletons, and `<EditorProvider>` wires the `TimelineEngine` /
164
- `PlaybackEngine` it creates into those same shared stores. Mounting a second
165
- `<EditorProvider>` on the same page (two independent projects at once) will have
166
- both instances overwrite each other's state — there is currently no per-instance
167
- scoping. If you need multiple concurrent editors, isolate each in its own
168
- tab/window/iframe.
169
-
170
- ---
171
-
172
- ## Links
173
-
174
- - [Website](https://www.elah.dev)
175
- - [GitHub](https://github.com/elahlabs/elah)
176
- - [Discord](https://discord.gg/8CeZ2XbPy)
177
- - [Headless CLI — @elah/cli](https://www.npmjs.com/package/@elah/cli)
178
- - [License](https://github.com/elahlabs/elah/blob/main/LICENSE)
179
- - [Commercial licensing](mailto:paul@elah.dev)
1
+ # @elah/editor
2
+
3
+ The full Elah video editor SDK for React. Combines the core engine, timeline UI, WebGL2 renderer, media library, and export pipeline into a single package.
4
+
5
+ Ships `EditorProvider`, `Preview` (WebGL2 canvas + interactive transform overlays), `Timeline`, `AssetPanel`, `SourcePanel`, and `ElementsPanel`, and re-exports the public `@elah/core`, `@elah/react`, and `@elah/timeline` API — so most apps only ever import from `@elah/editor`. (Renderer and debug internals, and the 1.0.0 programmatic layer, are the exception; import those from `@elah/core` directly.) Supports video, image, text, **shape**, and **freehand** clips, **multiple video tracks**, **multi-track audio**, and export to MP4, WebM, GIF or a PNG sequence. Projects that use the newer **adjustment**, **generator**, **compound** and **sequence** clips, and the nine transitions, open, play, save and export; `@elah/editor` has no editing UI for those clip types yet. A selected clip does show its **keyframes** as diamonds on its bottom edge: click to seek and select, drag to retime with snapping as one undo step, Shift+click for multi-select (`<KeyframeLane>` from `@elah/timeline`, drawn automatically; `useKeySelectionStore` is re-exported here).
6
+
7
+ **1.0.0:** the exports of this package follow semver. The 1.0.0 programmatic layer (effects, generators, motion helpers, VideoSpec, HTML scenes, editor ops, `packageFontsBaseUrl`) is **not** re-exported here: import it from `@elah/core`, which this package already depends on (list it in your own `package.json`). VideoSpec, HTML scenes and the op vocabulary are live in 1.0 and sit on their own entries, not the `@elah/core` root: `@elah/core/spec`, `@elah/core/html`, `@elah/core/ops` and `@elah/core/icons`; see [the changelog](../../CHANGELOG.md).
8
+
9
+ [![npm](https://img.shields.io/npm/v/@elah/editor)](https://www.npmjs.com/package/@elah/editor)
10
+ [![license](https://img.shields.io/badge/license-Apache--2.0-blue)](https://github.com/elahlabs/elah/blob/main/LICENSE)
11
+
12
+ ---
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install @elah/editor
18
+ ```
19
+
20
+ Peer dependencies: `react`, `react-dom` >= 18, `lucide-react` >= 0.400.0 (used by the
21
+ bundled `@elah/timeline` UI for clip icons — install it alongside `react`/`react-dom`
22
+ even though nothing in your own code imports it).
23
+
24
+ **Bundle size:** an editor app (`EditorProvider`, `Preview`, `Timeline`) is about **112 KiB gz** — esbuild, minified, gzipped, with `react`, `lucide-react` and `mediabunny` external — up from 68 KiB in 0.6.0, because effects, generators, sequence clips, roto masks and audio Enhance became chunks that load on first use (`prepareFeatures(project)` loads a project's features up front). [BUNDLE_STRATEGY.md](../../BUNDLE_STRATEGY.md) explains how the figures are measured (`node scripts/measure-bundle.mjs`). `@elah/core` grew substantially in 1.0.0 (it now carries the motion layer, effects and the model-facing layers); import `@elah/core/engine` directly if you need only the engine. `createDefaultDemuxerFactory()` imports `mediabunny` statically, so it pulls the codec into the startup graph; every other path loads it lazily.
25
+
26
+ ---
27
+
28
+ ## Quick start
29
+
30
+ ```tsx
31
+ import { EditorProvider, Timeline } from '@elah/editor'
32
+
33
+ function App() {
34
+ return (
35
+ <EditorProvider fps={30}>
36
+ <Timeline style={{ height: 300 }} />
37
+ </EditorProvider>
38
+ )
39
+ }
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Styling
45
+
46
+ Import the compiled stylesheets once. They are plain CSS — your app does **not**
47
+ need Tailwind, and no utility class names leak into your global scope (preflight
48
+ is disabled, so nothing resets your elements):
49
+
50
+ ```ts
51
+ import '@elah/timeline/styles.css'
52
+ import '@elah/editor/styles.css'
53
+ import '@elah/editor/styles/tokens.css' // --elah-* dark defaults (standalone)
54
+ ```
55
+
56
+ When embedding inside an app that already defines `.elah-root` (mapping `--elah-*`
57
+ onto its own design system), skip `tokens.css`. Re-theme or white-label by
58
+ overriding `--elah-*` variables in your own `.elah-root` scope — see
59
+ [design-tokens.md](https://github.com/elahlabs/elah/blob/main/docs/design-tokens.md).
60
+
61
+ ---
62
+
63
+ ## With preview and asset panel
64
+
65
+ ```tsx
66
+ import { EditorProvider, Timeline, Preview, AssetPanel, createDefaultDemuxerFactory } from '@elah/editor'
67
+
68
+ const demuxerFactory = createDefaultDemuxerFactory()
69
+
70
+ function App() {
71
+ return (
72
+ <EditorProvider fps={30}>
73
+ <div style={{ display: 'flex', height: '100vh' }}>
74
+ <AssetPanel style={{ width: 220 }} />
75
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
76
+ <Preview demuxerFactory={demuxerFactory} style={{ flex: 1 }} />
77
+ <Timeline style={{ height: 300 }} />
78
+ </div>
79
+ </div>
80
+ </EditorProvider>
81
+ )
82
+ }
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Import media
88
+
89
+ ```ts
90
+ import { importFiles, importUrl, importBlob, beginImportUrl, useMediaLibrary } from '@elah/editor'
91
+
92
+ await importFiles(Array.from(fileList)) // local files
93
+ await importUrl('https://example.com/clip.mp4') // remote URL (waits for metadata)
94
+ const pending = await beginImportUrl('https://example.com/clip.mp4') // returns at once, status: 'pending'
95
+ await importBlob(recordedBlob, { name: 'take-1.webm' })
96
+
97
+ // Subscribe in React — useMediaLibrary() takes no arguments and returns
98
+ // { assets, getAsset, removeAsset, updateAsset, importFiles, importUrl, importBlob }
99
+ // with assets in insertion order. `useAssets` is an alias for the same hook.
100
+ const { assets, importFiles: addFiles } = useMediaLibrary()
101
+ ```
102
+
103
+ `beginImportUrl` is for "add to timeline" from a remote gallery: place a clip right away
104
+ and let it resize itself once the real duration arrives. Pair it with
105
+ `growClipToAssetDuration` (see [`@elah/timeline`](https://www.npmjs.com/package/@elah/timeline)).
106
+ `determineAssetHasAudio` answers whether a video has an audio track.
107
+
108
+ ### Programmatic insertion (no drag)
109
+
110
+ ```ts
111
+ import { insertMediaAsset } from '@elah/editor'
112
+
113
+ // Place an imported asset onto the timeline — powers tap-to-add on touch.
114
+ // Returns a typed InsertAssetResult ({ ok, kind, trackId, clipIds } | { ok:false, reason }).
115
+ const result = await insertMediaAsset(engine, assetId, { desiredStartFrame: 0 })
116
+ ```
117
+
118
+ ---
119
+
120
+ ## Multiple video tracks
121
+
122
+ `engine.addTrack('video')` always adds a lane. Video tracks composite in track order
123
+ (topmost lane draws on top) and a new one lands directly below the last video track.
124
+
125
+ ---
126
+
127
+ ## Preview overlays, crop and speed
128
+
129
+ `<Preview>` paints the interactive overlays for you. **Text** clips: drag, resize, a rotate knob
130
+ (Shift snaps to 15°) and inline edit. **Video and image** clips (`MediaTransformOverlay`): drag,
131
+ 8 resize handles with non-uniform scale (Shift keeps the aspect ratio), and a Resize/Crop toggle
132
+ that edits `Clip.crop`. There is no rotate handle for video or image clips; the box tilts with
133
+ `transform.rotation`, which you set through the engine. Clip speed (`engine.setClipSpeed`,
134
+ 0.25–4) and `Clip.cornerRadius` are engine-level fields.
135
+
136
+ ---
137
+
138
+ ## Text templates and frame sequences
139
+
140
+ ```ts
141
+ import { BUILT_IN_TEXT_TEMPLATES, applyTextTemplate, createFrameSequence, frameSequenceToProject } from '@elah/editor'
142
+
143
+ engine.updateClip(clip.id, clip.trackId, applyTextTemplate(BUILT_IN_TEXT_TEMPLATES[0], clip)) // one undo entry
144
+ ```
145
+
146
+ 14 built-in looks with layered entry/exit motion (`MotionSpec`). A template's `stagger` and
147
+ `tracking` are data that `applyTextTemplate` ignores, and rotation motion has no effect on shape
148
+ clips. Transitions are `fade`, `slide` and `wipe` (`slide` goes left only for `direction: 'left'`,
149
+ `wipe` ignores direction). Frame sequences, `snapshotMediaLibrary` / `hydrateMediaLibrary` and the
150
+ rest are documented in [`@elah/core`](https://www.npmjs.com/package/@elah/core); everything is
151
+ re-exported from this package.
152
+
153
+ ---
154
+
155
+ ## Save and restore
156
+
157
+ ```ts
158
+ import { serializeProject, deserializeProject } from '@elah/editor'
159
+
160
+ localStorage.setItem('project', serializeProject(engine))
161
+ deserializeProject(engine, localStorage.getItem('project')!) // throws, engine untouched, if unreadable
162
+ ```
163
+
164
+ For control over the refusal, read first, then load:
165
+ `engine.loadProject(readProjectDocument(JSON.parse(json)))`. `readProjectDocument` throws
166
+ `ProjectDocumentError` (`'unreadable'` or `'unsupported-version'`). Files imported from
167
+ disk are `blob:` URLs that do not survive a reload; after your media library is rebuilt,
168
+ `relinkProjectMedia(project, assets)` re-points clips at it and lists what is `missing`
169
+ (never dropped). Load its result with `{ transport: 'keep', history: 'keep' }` so the
170
+ playhead and undo stack stay put. See [`@elah/core`](https://www.npmjs.com/package/@elah/core).
171
+
172
+ ---
173
+
174
+ ## Export to MP4
175
+
176
+ ```ts
177
+ import { exportVideo } from '@elah/editor'
178
+
179
+ const blob = await exportVideo(engine.getProject(), {
180
+ videoBitrate: 8_000_000,
181
+ outputHeight: 1080, // the stage's short edge: 1920×1080 landscape or 1080×1920 portrait
182
+ onProgress: ({ frame, totalFrames }) => {
183
+ console.log(`${Math.round((frame / totalFrames) * 100)}%`)
184
+ },
185
+ })
186
+ ```
187
+
188
+ Runs in a web worker. Reuses `resolveTimeline` + the GPU renderer's placement math.
189
+
190
+ ---
191
+
192
+ ## Keyboard shortcuts
193
+
194
+ | Key | Action |
195
+ |---|---|
196
+ | `Space` | Play / pause |
197
+ | `S` | Split clip at playhead |
198
+ | `Delete` / `Backspace` | Delete selected clip(s) |
199
+ | `Ctrl/Cmd + C` | Copy |
200
+ | `Ctrl/Cmd + V` | Paste at playhead |
201
+ | `Ctrl/Cmd + Z` | Undo |
202
+ | `Ctrl/Cmd + Shift + Z` / `Ctrl/Cmd + Y` | Redo |
203
+ | `Ctrl/Cmd + scroll` | Zoom |
204
+ | `← / →` | Step one frame |
205
+
206
+ ---
207
+
208
+ ## Package layers
209
+
210
+ ```
211
+ @elah/core — engine, playback, resolver, vanilla stores, media, export (framework-agnostic)
212
+ @elah/react — React bindings: EditorContext, store hooks, audio hooks
213
+ @elah/timeline — React timeline UI components and hooks (consumes @elah/core + @elah/react)
214
+ @elah/editor — EditorProvider, Preview, AssetPanel + re-exports @elah/core, @elah/react, @elah/timeline
215
+ @elah/cli — headless split/trim/build/export/serve on top of @elah/core
216
+ ```
217
+
218
+ Use `@elah/editor` for the full in-browser experience. Use `@elah/core` directly
219
+ for headless or custom rendering pipelines, or pair it with `@elah/react` if you
220
+ want the hooks without the timeline UI. Use `@elah/cli` for automation,
221
+ AI-generation pipelines, and server-side rendering.
222
+
223
+ **One active project per page.** `@elah/core`'s stores (`tracksStore`,
224
+ `playbackStore`, `selectionStore`, `transitionsStore`, `mediaLibraryStore`, `textStylePresetsStore`, `clipLoadStore`) are
225
+ module-level singletons, and `<EditorProvider>` wires the `TimelineEngine` /
226
+ `PlaybackEngine` it creates into those same shared stores. Mounting a second
227
+ `<EditorProvider>` on the same page (two independent projects at once) will have
228
+ both instances overwrite each other's state — there is currently no per-instance
229
+ scoping. If you need multiple concurrent editors, isolate each in its own
230
+ tab/window/iframe.
231
+
232
+ ---
233
+
234
+ ## Links
235
+
236
+ - [Website](https://www.elah.dev)
237
+ - [GitHub](https://github.com/elahlabs/elah)
238
+ - [Discord](https://discord.gg/qKJXvc4Pwu)
239
+ - [Headless CLI — @elah/cli](https://www.npmjs.com/package/@elah/cli)
240
+ - [License](https://github.com/elahlabs/elah/blob/main/LICENSE)
241
+ - [Commercial licensing](mailto:paul@elah.dev)
@@ -239,6 +239,6 @@ export function AssetPanel({ style, className, activateOnTap, onAssetActivate, }
239
239
  color: toast.tone === 'warn' ? 'var(--elah-danger-text, #f5d0a9)' : 'var(--elah-info-text, #c8d8f0)',
240
240
  background: toast.tone === 'warn' ? 'var(--elah-danger-bg, #3a2418)' : 'var(--elah-info-bg, #1a2433)',
241
241
  border: `1px solid ${toast.tone === 'warn' ? 'var(--elah-danger-border, #7a4a2a)' : 'var(--elah-info-border, #355070)'}`,
242
- boxShadow: '0 4px 12px rgba(0, 0, 0, 0.35)',
242
+ boxShadow: 'var(--elah-toast-shadow, 0 4px 12px rgba(0, 0, 0, 0.35))',
243
243
  }, children: toast.message })), assets.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center justify-center min-h-[120px] p-4 text-center text-ed-text-muted text-[11px] border border-dashed border-ed-border rounded-md", children: [_jsx("span", { className: "mb-2 text-2xl opacity-50", children: "\u2193" }), "Drop files here", _jsx("br", {}), "or click Add"] })) : (_jsx("div", { className: "flex flex-col gap-[6px]", children: assets.map((asset) => (_jsx(AssetThumbnail, { asset: asset, onDelete: handleDeleteAsset, onActivate: activationEnabled ? handleAssetActivate : undefined }, asset.id))) }))] })] }));
244
244
  }
@@ -1 +1 @@
1
- export declare function MediaTransformOverlay(): import("react/jsx-runtime").JSX.Element;
1
+ export declare const MediaTransformOverlay: import("react").NamedExoticComponent<object>;