@aicut/react 0.4.3 → 0.6.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
@@ -75,6 +75,17 @@ interface VideoEditorProps {
75
75
  toolbarLeft?: ReactNode; // host controls — left bookend
76
76
  toolbarRight?: ReactNode; // right bookend
77
77
 
78
+ playbackEngine?: PlaybackEngineFactory; // pluggable playback; default
79
+ // HtmlVideoEngine. Bound at
80
+ // mount — change + remount
81
+ // via `key` to re-apply.
82
+ timelineHeight?: number; // outer height of the bottom
83
+ // timeline area (default 240).
84
+ // Reactive; no remount needed.
85
+ trackHeight?: number; // per-row height (default 56).
86
+ // Initial-only; process-wide.
87
+ rulerHeight?: number; // time-label strip (default 24).
88
+
78
89
  apiRef?: Ref<VideoEditorApi | null>;
79
90
 
80
91
  onReady?: (api: VideoEditorApi) => void;
@@ -163,6 +174,110 @@ import { VideoEditor, localeZh } from "@aicut/react";
163
174
 
164
175
  `locale` is reactive too — runtime swap re-titles the toolbar and re-paints canvas labels in place.
165
176
 
177
+ ## Compact viewports
178
+
179
+ Default chrome is sized for desktop. For laptop side panels or embedded editors, shrink the bottom area to reclaim preview height:
180
+
181
+ ```tsx
182
+ const [timelineHeight, setTimelineHeight] = useState(160);
183
+
184
+ <VideoEditor
185
+ defaultProject={project}
186
+ timelineHeight={timelineHeight} // reactive — drag a slider, the
187
+ // editor recompacts in place
188
+ trackHeight={40} // initial-only; needs remount to
189
+ // change (key={trackHeight})
190
+ />
191
+ ```
192
+
193
+ Range guidance: `timelineHeight` ∈ [120, 480], `trackHeight` ∈ [28, 96], `rulerHeight` ∈ [18, 36]. The internal canvas scrolls vertically when tracks overflow.
194
+
195
+ ## Custom playback engine
196
+
197
+ The editor talks to playback through a single interface. The default is
198
+ `HtmlVideoEngine` (one hidden `<video>` per source, swap on clip
199
+ boundaries). To plug in a different one — WebCodecs, WebGL compositor,
200
+ desktop-wrapper IPC bridge — pass a factory:
201
+
202
+ ```tsx
203
+ import {
204
+ VideoEditor,
205
+ type PlaybackEngineFactory,
206
+ } from "@aicut/react";
207
+
208
+ const myEngine: PlaybackEngineFactory = ({ host, project }) =>
209
+ new MyCustomEngine(host, project); // implements PlaybackEngine
210
+
211
+ <VideoEditor
212
+ defaultProject={project}
213
+ playbackEngine={myEngine} // initial-only — bound at mount
214
+ /* … */
215
+ />
216
+ ```
217
+
218
+ `PlaybackEngine`, `PlaybackEngineFactory`, `PlaybackEngineOptions`, and
219
+ the built-in `HtmlVideoEngine` are re-exported from `@aicut/react` so
220
+ you don't need a separate `@aicut/core` import to write one.
221
+
222
+ See [@aicut/core's playback section](https://www.npmjs.com/package/@aicut/core#playback-engine)
223
+ for the full interface contract.
224
+
225
+ ### WebCodecs engine (opt-in sub-entry)
226
+
227
+ For frame-accurate playback via the browser's `VideoDecoder` API, import from the sub-entry so mp4box.js (~200 KB) only loads when you ask for it:
228
+
229
+ ```tsx
230
+ import {
231
+ WebCodecsEngine,
232
+ isWebCodecsSupported,
233
+ } from "@aicut/react/webcodecs";
234
+
235
+ const factory = isWebCodecsSupported()
236
+ ? (opts) => new WebCodecsEngine({ ...opts, debug: true })
237
+ : undefined; // VideoEditor falls back to HtmlVideoEngine when undefined
238
+
239
+ <VideoEditor playbackEngine={factory} /* … */ />;
240
+ ```
241
+
242
+ `WebCodecsEngine` v1 covers single-track MP4/MOV playback (H.264 / HEVC / VP9 / AV1 — whatever the browser's `VideoDecoder` supports). Multi-track compositing, audio, transitions land in follow-up releases.
243
+
244
+ ## Keyframes (panX / panY / scale animation)
245
+
246
+ Off by default. Flip the `keyframes` prop and **all three** playback engines (HTML5, Canvas, WebCodecs) start interpolating per-clip transforms between adjacent keyframes. Diamond markers appear on the timeline; drag them, edit values via the floating panel, snap them to each other.
247
+
248
+ ```tsx
249
+ const [kfEnabled, setKfEnabled] = useState(true);
250
+ const [edgeNav, setEdgeNav] = useState(true);
251
+
252
+ <VideoEditor
253
+ defaultProject={project}
254
+ keyframes={{ enabled: kfEnabled }}
255
+ clipEdgeNav={{ enabled: edgeNav }} // adds the |◀ ▶| buttons + I/O shortcuts
256
+ onKeyframeSelectionChange={(target) => console.log(target)}
257
+ /* … */
258
+ />
259
+
260
+ // Per-property mutators (panX / panY / scale animate independently).
261
+ apiRef.current?.addKeyframe("clip-1", "scale", { time: 0, value: 1 });
262
+ apiRef.current?.addKeyframe("clip-1", "scale", {
263
+ time: 2000,
264
+ value: 2.5,
265
+ easing: "easeInOut",
266
+ });
267
+ apiRef.current?.setKeyframeValue("clip-1", kfId, 1.8);
268
+ apiRef.current?.setKeyframeEasing("clip-1", kfId, "easeOut");
269
+
270
+ // Toolbar-style "K at playhead" drops all 3 props at once.
271
+ apiRef.current?.setSelection("clip-1");
272
+ apiRef.current?.toggleKeyframeAtPlayhead();
273
+ ```
274
+
275
+ `Keyframe`, `KeyframeProp`, `EasingKind`, `EffectiveTransform`, `getEffectiveTransform`, `getTransformAtTimelineTime`, `IDENTITY_TRANSFORM`, `isIdentityTransform` are all re-exported from `@aicut/react` for thumbnail / preview rendering outside the editor.
276
+
277
+ **Backend export:** both `@aicut/backend-ts` and `@aicut/backend-go` compile keyframes to ffmpeg `t`-expressions (`scale=…:eval=frame` + `overlay=…:eval=frame`). Pass `output: { width, height, fps }` in the export request — required for the keyframe filter graph to apply.
278
+
279
+ See [@aicut/core's keyframes section](https://www.npmjs.com/package/@aicut/core#keyframes-per-clip-panx--pany--scale-animation) for the full API surface.
280
+
166
281
  ## `<LightingEditor>` (opt-in sub-entry)
167
282
 
168
283
  A 3D lighting director for AI relighting flows — separate component that doesn't pull three.js into the rest of your bundle.
package/dist/index.cjs CHANGED
@@ -20,12 +20,24 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ CanvasCompositorEngine: () => import_core3.CanvasCompositorEngine,
24
+ HEADER_WIDTH: () => import_core3.HEADER_WIDTH,
25
+ HtmlVideoEngine: () => import_core3.HtmlVideoEngine,
26
+ IDENTITY_TRANSFORM: () => import_core3.IDENTITY_TRANSFORM,
27
+ RULER_HEIGHT: () => import_core3.RULER_HEIGHT,
28
+ TRACK_HEIGHT: () => import_core3.TRACK_HEIGHT,
23
29
  Timeline: () => Timeline,
24
30
  VideoEditor: () => VideoEditor,
31
+ canvasCompositorEngineFactory: () => import_core3.canvasCompositorEngineFactory,
25
32
  createEmptyProject: () => import_core3.createEmptyProject,
26
33
  createId: () => import_core3.createId,
34
+ getEffectiveTransform: () => import_core3.getEffectiveTransform,
35
+ getTransformAtTimelineTime: () => import_core3.getTransformAtTimelineTime,
36
+ htmlVideoEngineFactory: () => import_core3.htmlVideoEngineFactory,
37
+ isIdentityTransform: () => import_core3.isIdentityTransform,
27
38
  localeEn: () => import_core3.localeEn,
28
- localeZh: () => import_core3.localeZh
39
+ localeZh: () => import_core3.localeZh,
40
+ setTimelineMetrics: () => import_core3.setTimelineMetrics
29
41
  });
30
42
  module.exports = __toCommonJS(index_exports);
31
43
 
@@ -47,7 +59,13 @@ function VideoEditor(props) {
47
59
  container: host,
48
60
  project: cbRef.current.defaultProject,
49
61
  theme: cbRef.current.theme,
50
- locale: cbRef.current.locale
62
+ locale: cbRef.current.locale,
63
+ playbackEngine: cbRef.current.playbackEngine,
64
+ ...cbRef.current.trackHeight != null ? { trackHeight: cbRef.current.trackHeight } : {},
65
+ ...cbRef.current.rulerHeight != null ? { rulerHeight: cbRef.current.rulerHeight } : {},
66
+ ...cbRef.current.timelineHeight != null ? { timelineHeight: cbRef.current.timelineHeight } : {},
67
+ ...cbRef.current.keyframes != null ? { keyframes: cbRef.current.keyframes } : {},
68
+ ...cbRef.current.clipEdgeNav != null ? { clipEdgeNav: cbRef.current.clipEdgeNav } : {}
51
69
  });
52
70
  editorRef.current = editor;
53
71
  setSlots({
@@ -66,6 +84,10 @@ function VideoEditor(props) {
66
84
  "selectionChange",
67
85
  ({ clipId }) => cbRef.current.onSelectionChange?.(clipId)
68
86
  ),
87
+ editor.on(
88
+ "keyframeSelectionChange",
89
+ ({ target }) => cbRef.current.onKeyframeSelectionChange?.(target)
90
+ ),
69
91
  editor.on("error", ({ error }) => cbRef.current.onError?.(error))
70
92
  ];
71
93
  cbRef.current.onReady?.(editor);
@@ -82,6 +104,34 @@ function VideoEditor(props) {
82
104
  (0, import_react.useEffect)(() => {
83
105
  if (props.locale) editorRef.current?.setLocale(props.locale);
84
106
  }, [props.locale]);
107
+ (0, import_react.useEffect)(() => {
108
+ const editor = editorRef.current;
109
+ if (!editor) return;
110
+ const desired = props.keyframes?.enabled === true;
111
+ if (editor.isKeyframesEnabled() !== desired) {
112
+ editor.setKeyframesEnabled(desired);
113
+ }
114
+ }, [props.keyframes?.enabled]);
115
+ (0, import_react.useEffect)(() => {
116
+ const editor = editorRef.current;
117
+ if (!editor) return;
118
+ const desired = props.clipEdgeNav?.enabled === true;
119
+ if (editor.isClipEdgeNavEnabled() !== desired) {
120
+ editor.setClipEdgeNavEnabled(desired);
121
+ }
122
+ }, [props.clipEdgeNav?.enabled]);
123
+ (0, import_react.useEffect)(() => {
124
+ const host = hostRef.current;
125
+ if (!host) return;
126
+ if (props.timelineHeight != null && props.timelineHeight > 0) {
127
+ host.style.setProperty(
128
+ "--aicut-timeline-height",
129
+ `${Math.round(props.timelineHeight)}px`
130
+ );
131
+ } else {
132
+ host.style.removeProperty("--aicut-timeline-height");
133
+ }
134
+ }, [props.timelineHeight]);
85
135
  (0, import_react.useImperativeHandle)(
86
136
  props.apiRef,
87
137
  () => editorRef.current,
@@ -195,11 +245,23 @@ function Timeline(props) {
195
245
  var import_core3 = require("@aicut/core");
196
246
  // Annotate the CommonJS export names for ESM import in node:
197
247
  0 && (module.exports = {
248
+ CanvasCompositorEngine,
249
+ HEADER_WIDTH,
250
+ HtmlVideoEngine,
251
+ IDENTITY_TRANSFORM,
252
+ RULER_HEIGHT,
253
+ TRACK_HEIGHT,
198
254
  Timeline,
199
255
  VideoEditor,
256
+ canvasCompositorEngineFactory,
200
257
  createEmptyProject,
201
258
  createId,
259
+ getEffectiveTransform,
260
+ getTransformAtTimelineTime,
261
+ htmlVideoEngineFactory,
262
+ isIdentityTransform,
202
263
  localeEn,
203
- localeZh
264
+ localeZh,
265
+ setTimelineMetrics
204
266
  });
205
267
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/VideoEditor.tsx","../src/Timeline.tsx"],"sourcesContent":["export { VideoEditor } from \"./VideoEditor.js\";\nexport type { VideoEditorProps, VideoEditorApi } from \"./VideoEditor.js\";\nexport { Timeline } from \"./Timeline.js\";\nexport type { TimelineProps, TimelineApi } from \"./Timeline.js\";\nexport type {\n Project,\n MediaSource,\n Track,\n Clip,\n Ms,\n Theme,\n EditorApi,\n Locale,\n} from \"@aicut/core\";\nexport { createEmptyProject, createId, localeEn, localeZh } from \"@aicut/core\";\n","import {\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n type Ref,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n Editor,\n type EditorApi,\n type Locale,\n type Ms,\n type Project,\n type Theme,\n} from \"@aicut/core\";\n\nexport type VideoEditorApi = EditorApi;\n\nexport interface VideoEditorProps {\n /**\n * Initial project. Read once on mount — to swap projects after mount,\n * call `apiRef.current.setProject(...)` so React doesn't reinstantiate\n * the editor and lose playback state.\n */\n defaultProject?: Project;\n /** CSS variable overrides applied on mount and whenever this ref changes. */\n theme?: Theme;\n /**\n * UI string overrides (English default). Mirror prop — switching the\n * value calls `editor.setLocale` and the toolbar / canvas labels\n * update in place. Use `localeZh` from `@aicut/core` for Chinese.\n */\n locale?: Partial<Locale>;\n\n className?: string;\n style?: CSSProperties;\n\n /** Imperative handle for cut/seek/getProject/setProject/etc. */\n apiRef?: Ref<VideoEditorApi | null>;\n\n onReady?: (api: VideoEditorApi) => void;\n onChange?: (project: Project) => void;\n onExport?: (project: Project) => void;\n onTimeUpdate?: (timeMs: Ms) => void;\n onPlay?: () => void;\n onPause?: () => void;\n onSelectionChange?: (clipId: string | null) => void;\n onError?: (error: Error) => void;\n\n /**\n * Rendered into the very left of the editor's top toolbar — host\n * adds anything here (size dropdown, branding, status badge). The\n * library reserves no space for it; if you pass nothing, no\n * separator appears.\n */\n toolbarLeft?: ReactNode;\n /** Same as `toolbarLeft` but at the very right of the toolbar. */\n toolbarRight?: ReactNode;\n /**\n * Rendered into the LEFT side of an optional header bar above the\n * preview (project name, file menu, breadcrumbs). The header\n * collapses entirely when both header slots are empty, so the\n * default layout is identical to before this slot existed.\n */\n headerLeft?: ReactNode;\n /** Right side of the editor header — conventionally Share / Export / profile. */\n headerRight?: ReactNode;\n}\n\n/**\n * Declarative React shell over `@aicut/core` `Editor`. Mounts the\n * editor instance once, mirrors prop changes (`theme`) into it, and\n * forwards events as React-style callbacks.\n *\n * Intentionally uncontrolled for project state — the editor owns the\n * current project. Use `onChange` to persist and `apiRef.setProject`\n * to restore.\n */\nexport function VideoEditor(props: VideoEditorProps) {\n const hostRef = useRef<HTMLDivElement | null>(null);\n const editorRef = useRef<Editor | null>(null);\n // Toolbar slot DOM nodes don't exist until the editor mounts; we\n // hold them in state so React re-runs the render after mount and\n // the portals attach. Tracked separately for left + right because\n // each is independently controlled by host props.\n const [slots, setSlots] = useState<{\n left: HTMLElement;\n right: HTMLElement;\n headerLeft: HTMLElement;\n headerRight: HTMLElement;\n } | null>(null);\n\n // Latest-callback refs so the effect that creates the editor doesn't\n // re-run on every parent render just because props.onChange is a new\n // identity — the editor would otherwise be torn down constantly.\n const cbRef = useRef(props);\n cbRef.current = props;\n\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n const editor = Editor.create({\n container: host,\n project: cbRef.current.defaultProject,\n theme: cbRef.current.theme,\n locale: cbRef.current.locale,\n });\n editorRef.current = editor;\n setSlots({\n left: editor.toolbarLeft,\n right: editor.toolbarRight,\n headerLeft: editor.headerLeft,\n headerRight: editor.headerRight,\n });\n\n const offs = [\n editor.on(\"change\", ({ project }) => cbRef.current.onChange?.(project)),\n editor.on(\"export\", ({ project }) => cbRef.current.onExport?.(project)),\n editor.on(\"time\", ({ timeMs }) => cbRef.current.onTimeUpdate?.(timeMs)),\n editor.on(\"play\", () => cbRef.current.onPlay?.()),\n editor.on(\"pause\", () => cbRef.current.onPause?.()),\n editor.on(\"selectionChange\", ({ clipId }) =>\n cbRef.current.onSelectionChange?.(clipId),\n ),\n editor.on(\"error\", ({ error }) => cbRef.current.onError?.(error)),\n ];\n\n cbRef.current.onReady?.(editor);\n\n return () => {\n for (const off of offs) off();\n editor.destroy();\n editorRef.current = null;\n setSlots(null);\n };\n // Editor lifecycle is tied to mount; we deliberately don't list\n // any reactive deps. `theme` changes are pushed through the\n // separate effect below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (props.theme) editorRef.current?.setTheme(props.theme);\n }, [props.theme]);\n\n useEffect(() => {\n if (props.locale) editorRef.current?.setLocale(props.locale);\n }, [props.locale]);\n\n // Deps must include `slots`. Without it, the factory ran once during\n // the first commit — BEFORE the useEffect above had a chance to\n // create the editor — so `apiRef.current` was permanently locked to\n // null. `slots` flips from null to a real value the same instant\n // the editor is created, so it's the cleanest re-run trigger.\n useImperativeHandle<VideoEditorApi | null, VideoEditorApi | null>(\n props.apiRef,\n () => editorRef.current,\n [slots],\n );\n\n return (\n <div\n ref={hostRef}\n className={props.className}\n style={props.style}\n data-aicut-host=\"\"\n >\n {slots && props.toolbarLeft != null\n ? createPortal(props.toolbarLeft, slots.left)\n : null}\n {slots && props.toolbarRight != null\n ? createPortal(props.toolbarRight, slots.right)\n : null}\n {slots && props.headerLeft != null\n ? createPortal(props.headerLeft, slots.headerLeft)\n : null}\n {slots && props.headerRight != null\n ? createPortal(props.headerRight, slots.headerRight)\n : null}\n </div>\n );\n}\n","import {\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n type Ref,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n Timeline as CoreTimeline,\n type Clip,\n type Locale,\n type Ms,\n type Project,\n type TimelineOptions,\n} from \"@aicut/core\";\n\n/** Imperative handle exposed via `apiRef`. */\nexport interface TimelineApi {\n setProject(p: Project): void;\n getProject(): Project;\n setTime(t: Ms): void;\n getTime(): Ms;\n setScale(pxPerSec: number): void;\n getScale(): number;\n setSelection(id: string | null): void;\n getSelection(): string | null;\n setSnap(snap: boolean): void;\n fitToWindow(): void;\n getDebugInfo(): ReturnType<CoreTimeline[\"getDebugInfo\"]>;\n}\n\nexport interface TimelineProps {\n /** Initial project. Use `apiRef.current.setProject(...)` to swap. */\n defaultProject: Project;\n /** Initial scale (px/sec). Defaults to 80; auto-fits on first render. */\n defaultScale?: number;\n /** Initial playhead position. */\n defaultTime?: Ms;\n /** Initial selection. */\n defaultSelectedClipId?: string | null;\n\n /** Hide the left header column (compact / frame-picker mode). */\n showHeader?: boolean;\n /** Disable all editing interactions. */\n readOnly?: boolean;\n /** Snap to clip edges + playhead when dragging. Default true. */\n snap?: boolean;\n /** Apply fit-to-window on mount once duration is known. Default true. */\n autoFit?: boolean;\n /** UI string overrides (English default). */\n locale?: Partial<Locale>;\n /**\n * Render a 36px top toolbar strip with empty left/right flex slots\n * for host-supplied controls. Default false. Pair with `toolbarLeft`\n * / `toolbarRight` to inject content.\n */\n toolbar?: boolean;\n /** Rendered into the left slot of the timeline toolbar (toolbar must be true). */\n toolbarLeft?: ReactNode;\n /** Rendered into the right slot of the timeline toolbar. */\n toolbarRight?: ReactNode;\n\n className?: string;\n style?: CSSProperties;\n\n apiRef?: Ref<TimelineApi | null>;\n\n onSeek?: (timeMs: Ms) => void;\n onSelectClip?: (clipId: string | null) => void;\n onScaleChange?: (pxPerSec: number) => void;\n onMoveClip?: TimelineOptions[\"onMoveClip\"];\n onResizeClip?: TimelineOptions[\"onResizeClip\"];\n onChange?: (project: Project) => void;\n}\n\n/**\n * Standalone, framework-agnostic canvas Timeline wrapped for React.\n * Mount it without an `Editor` for use cases like a video frame-picker:\n *\n * ```tsx\n * <Timeline\n * defaultProject={{ version: 1, sources: [video], tracks: [{ id, kind: \"video\", clips: [{...}] }] }}\n * showHeader={false}\n * readOnly\n * onSeek={(ms) => setCurrentMs(ms)}\n * />\n * ```\n *\n * Uncontrolled for `project` and `pxPerSec` — the underlying Timeline\n * owns them and reports changes via callbacks. Call methods on\n * `apiRef.current` to drive it imperatively (mirroring ag-Grid /\n * VideoEditor patterns).\n */\nexport function Timeline(props: TimelineProps) {\n const hostRef = useRef<HTMLDivElement | null>(null);\n const tlRef = useRef<CoreTimeline | null>(null);\n const [slots, setSlots] = useState<{\n left: HTMLElement;\n right: HTMLElement;\n } | null>(null);\n\n // Latest-callback ref so the create-once effect doesn't tear the\n // timeline down on every render just because callback identities\n // change.\n const cbRef = useRef(props);\n cbRef.current = props;\n\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n const tl = CoreTimeline.create({\n container: host,\n project: cbRef.current.defaultProject,\n pxPerSec: cbRef.current.defaultScale,\n time: cbRef.current.defaultTime,\n selectedClipId: cbRef.current.defaultSelectedClipId ?? null,\n showHeader: cbRef.current.showHeader,\n readOnly: cbRef.current.readOnly,\n snap: cbRef.current.snap,\n autoFit: cbRef.current.autoFit,\n locale: cbRef.current.locale,\n toolbar: cbRef.current.toolbar,\n onSeek: (t) => cbRef.current.onSeek?.(t),\n onSelectClip: (id) => cbRef.current.onSelectClip?.(id),\n onScaleChange: (s) => cbRef.current.onScaleChange?.(s),\n onMoveClip: (id, opts) => cbRef.current.onMoveClip?.(id, opts),\n onResizeClip: (id, e) => cbRef.current.onResizeClip?.(id, e),\n onChange: (p) => cbRef.current.onChange?.(p),\n });\n tlRef.current = tl;\n if (tl.toolbarLeft && tl.toolbarRight) {\n setSlots({ left: tl.toolbarLeft, right: tl.toolbarRight });\n }\n return () => {\n tl.destroy();\n tlRef.current = null;\n setSlots(null);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (props.locale) tlRef.current?.setLocale(props.locale);\n }, [props.locale]);\n\n useImperativeHandle<TimelineApi | null, TimelineApi | null>(\n props.apiRef,\n () => {\n const tl = tlRef.current;\n if (!tl) return null;\n return {\n setProject: (p) => tl.setProject(p),\n getProject: () => tl.getProject(),\n setTime: (t) => tl.setTime(t),\n getTime: () => tl.getTime(),\n setScale: (s) => tl.setScale(s),\n getScale: () => tl.getScale(),\n setSelection: (id) => tl.setSelection(id),\n getSelection: () => tl.getSelection(),\n setSnap: (s) => tl.setSnap(s),\n fitToWindow: () => tl.fitToWindow(),\n getDebugInfo: () => tl.getDebugInfo(),\n };\n },\n // Same caveat as VideoEditor.tsx — factory must re-run once the\n // timeline is created in useEffect, otherwise apiRef.current is\n // null forever. `slots` flips from null to a real value the\n // instant the timeline is ready, so it's the cleanest trigger.\n [slots],\n );\n\n return (\n <div\n ref={hostRef}\n className={props.className}\n style={{ width: \"100%\", height: 240, ...props.style }}\n data-aicut-timeline-host=\"\"\n >\n {slots && props.toolbarLeft != null\n ? createPortal(props.toolbarLeft, slots.left)\n : null}\n {slots && props.toolbarRight != null\n ? createPortal(props.toolbarRight, slots.right)\n : null}\n </div>\n );\n\n // Type-only re-export used to keep React/Vue prop typings in lockstep\n // with the core. Reference here so the symbol isn't tree-shaken.\n void ({} as Clip);\n}\n"],"mappings":"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","names":["import_react","import_react_dom","import_core","import_jsx_runtime","CoreTimeline","import_core"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/VideoEditor.tsx","../src/Timeline.tsx"],"sourcesContent":["export { VideoEditor } from \"./VideoEditor.js\";\nexport type { VideoEditorProps, VideoEditorApi } from \"./VideoEditor.js\";\nexport { Timeline } from \"./Timeline.js\";\nexport type { TimelineProps, TimelineApi } from \"./Timeline.js\";\nexport type {\n Project,\n MediaSource,\n Track,\n Clip,\n Keyframe,\n Ms,\n Theme,\n EditorApi,\n Locale,\n PlaybackEngine,\n PlaybackEngineFactory,\n PlaybackEngineOptions,\n CanvasCompositorEngineOptions,\n EffectiveTransform,\n} from \"@aicut/core\";\nexport {\n createEmptyProject,\n createId,\n localeEn,\n localeZh,\n HtmlVideoEngine,\n htmlVideoEngineFactory,\n CanvasCompositorEngine,\n canvasCompositorEngineFactory,\n // Live bindings — re-reading them after `setTimelineMetrics` (which\n // EditorOptions.trackHeight / .rulerHeight calls under the hood)\n // returns the updated values.\n TRACK_HEIGHT,\n RULER_HEIGHT,\n HEADER_WIDTH,\n setTimelineMetrics,\n // Pure-math keyframe helpers — hosts can read effective transforms\n // for previews / thumbnails without touching the playback engine.\n IDENTITY_TRANSFORM,\n isIdentityTransform,\n getEffectiveTransform,\n getTransformAtTimelineTime,\n} from \"@aicut/core\";\n","import {\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n type Ref,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n Editor,\n type EditorApi,\n type Locale,\n type Ms,\n type PlaybackEngineFactory,\n type Project,\n type Theme,\n} from \"@aicut/core\";\n\nexport type VideoEditorApi = EditorApi;\n\nexport interface VideoEditorProps {\n /**\n * Initial project. Read once on mount — to swap projects after mount,\n * call `apiRef.current.setProject(...)` so React doesn't reinstantiate\n * the editor and lose playback state.\n */\n defaultProject?: Project;\n /** CSS variable overrides applied on mount and whenever this ref changes. */\n theme?: Theme;\n /**\n * UI string overrides (English default). Mirror prop — switching the\n * value calls `editor.setLocale` and the toolbar / canvas labels\n * update in place. Use `localeZh` from `@aicut/core` for Chinese.\n */\n locale?: Partial<Locale>;\n\n className?: string;\n style?: CSSProperties;\n\n /** Imperative handle for cut/seek/getProject/setProject/etc. */\n apiRef?: Ref<VideoEditorApi | null>;\n\n onReady?: (api: VideoEditorApi) => void;\n onChange?: (project: Project) => void;\n onExport?: (project: Project) => void;\n onTimeUpdate?: (timeMs: Ms) => void;\n onPlay?: () => void;\n onPause?: () => void;\n onSelectionChange?: (clipId: string | null) => void;\n onError?: (error: Error) => void;\n\n /**\n * Rendered into the very left of the editor's top toolbar — host\n * adds anything here (size dropdown, branding, status badge). The\n * library reserves no space for it; if you pass nothing, no\n * separator appears.\n */\n toolbarLeft?: ReactNode;\n /** Same as `toolbarLeft` but at the very right of the toolbar. */\n toolbarRight?: ReactNode;\n /**\n * Rendered into the LEFT side of an optional header bar above the\n * preview (project name, file menu, breadcrumbs). The header\n * collapses entirely when both header slots are empty, so the\n * default layout is identical to before this slot existed.\n */\n headerLeft?: ReactNode;\n /** Right side of the editor header — conventionally Share / Export / profile. */\n headerRight?: ReactNode;\n\n /**\n * Initial-only — picks the playback engine used by the underlying\n * core Editor. Defaults to the built-in `HtmlVideoEngine`. Pass a\n * factory to plug in a custom engine (WebCodecs, WebGL compositor,\n * IPC bridge to a native player, …). Swapping this prop after mount\n * has no effect — the editor binds its engine at construction.\n */\n playbackEngine?: PlaybackEngineFactory;\n /**\n * Initial-only — pixel height of each track row (default 56). Lower\n * values (~32–40) shrink the timeline for small viewports where the\n * default crowds out the preview. Applied process-wide; to re-apply\n * change this prop AND remount the component (e.g. via `key`).\n */\n trackHeight?: number;\n /** Initial-only — pixel height of the timeline ruler (default 24). */\n rulerHeight?: number;\n /**\n * Pixel height of the whole bottom timeline area (default 240).\n * Reactive — set anytime to change. The canvas inside fills 100%\n * and shows an internal scrollbar when track count overflows.\n * Useful range: [120, 480] depending on viewport.\n */\n timelineHeight?: number;\n /**\n * Per-clip keyframe animation (X / Y / Scale). Reactive — set\n * `{ enabled: true }` to surface keyframe diamonds on the timeline\n * and route the canvas-based engines through the transform pipeline.\n * Disabling hides the editing UI but preserves the data in\n * `Project.tracks[].clips[].keyframes`.\n *\n * `HtmlVideoEngine` cannot animate frames; swap to\n * `CanvasCompositorEngine` or `WebCodecsEngine` for live preview.\n */\n keyframes?: { enabled?: boolean };\n /** Fires when the user selects or deselects a keyframe diamond. */\n onKeyframeSelectionChange?: (\n target: { clipId: string; keyframeId: string } | null,\n ) => void;\n /**\n * Jump-to-clip-edge toolbar cluster (|◀ ▶|) + I/O keyboard shortcuts.\n * Reactive — set `{ enabled: true }` to surface the buttons next to\n * the keyframe diamond and bind the shortcuts. Off hides the buttons\n * entirely (display: none, no toolbar space cost) and lets I/O\n * fall through to the page.\n */\n clipEdgeNav?: { enabled?: boolean };\n}\n\n/**\n * Declarative React shell over `@aicut/core` `Editor`. Mounts the\n * editor instance once, mirrors prop changes (`theme`) into it, and\n * forwards events as React-style callbacks.\n *\n * Intentionally uncontrolled for project state — the editor owns the\n * current project. Use `onChange` to persist and `apiRef.setProject`\n * to restore.\n */\nexport function VideoEditor(props: VideoEditorProps) {\n const hostRef = useRef<HTMLDivElement | null>(null);\n const editorRef = useRef<Editor | null>(null);\n // Toolbar slot DOM nodes don't exist until the editor mounts; we\n // hold them in state so React re-runs the render after mount and\n // the portals attach. Tracked separately for left + right because\n // each is independently controlled by host props.\n const [slots, setSlots] = useState<{\n left: HTMLElement;\n right: HTMLElement;\n headerLeft: HTMLElement;\n headerRight: HTMLElement;\n } | null>(null);\n\n // Latest-callback refs so the effect that creates the editor doesn't\n // re-run on every parent render just because props.onChange is a new\n // identity — the editor would otherwise be torn down constantly.\n const cbRef = useRef(props);\n cbRef.current = props;\n\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n const editor = Editor.create({\n container: host,\n project: cbRef.current.defaultProject,\n theme: cbRef.current.theme,\n locale: cbRef.current.locale,\n playbackEngine: cbRef.current.playbackEngine,\n ...(cbRef.current.trackHeight != null\n ? { trackHeight: cbRef.current.trackHeight }\n : {}),\n ...(cbRef.current.rulerHeight != null\n ? { rulerHeight: cbRef.current.rulerHeight }\n : {}),\n ...(cbRef.current.timelineHeight != null\n ? { timelineHeight: cbRef.current.timelineHeight }\n : {}),\n ...(cbRef.current.keyframes != null\n ? { keyframes: cbRef.current.keyframes }\n : {}),\n ...(cbRef.current.clipEdgeNav != null\n ? { clipEdgeNav: cbRef.current.clipEdgeNav }\n : {}),\n });\n editorRef.current = editor;\n setSlots({\n left: editor.toolbarLeft,\n right: editor.toolbarRight,\n headerLeft: editor.headerLeft,\n headerRight: editor.headerRight,\n });\n\n const offs = [\n editor.on(\"change\", ({ project }) => cbRef.current.onChange?.(project)),\n editor.on(\"export\", ({ project }) => cbRef.current.onExport?.(project)),\n editor.on(\"time\", ({ timeMs }) => cbRef.current.onTimeUpdate?.(timeMs)),\n editor.on(\"play\", () => cbRef.current.onPlay?.()),\n editor.on(\"pause\", () => cbRef.current.onPause?.()),\n editor.on(\"selectionChange\", ({ clipId }) =>\n cbRef.current.onSelectionChange?.(clipId),\n ),\n editor.on(\"keyframeSelectionChange\", ({ target }) =>\n cbRef.current.onKeyframeSelectionChange?.(target),\n ),\n editor.on(\"error\", ({ error }) => cbRef.current.onError?.(error)),\n ];\n\n cbRef.current.onReady?.(editor);\n\n return () => {\n for (const off of offs) off();\n editor.destroy();\n editorRef.current = null;\n setSlots(null);\n };\n // Editor lifecycle is tied to mount; we deliberately don't list\n // any reactive deps. `theme` changes are pushed through the\n // separate effect below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (props.theme) editorRef.current?.setTheme(props.theme);\n }, [props.theme]);\n\n useEffect(() => {\n if (props.locale) editorRef.current?.setLocale(props.locale);\n }, [props.locale]);\n\n // Reactive — flipping `keyframes.enabled` instantly toggles diamond\n // visibility on the timeline and routes the canvas / WebCodecs\n // engines through the transform pipeline (or not). Data is\n // preserved either way.\n useEffect(() => {\n const editor = editorRef.current;\n if (!editor) return;\n const desired = props.keyframes?.enabled === true;\n if (editor.isKeyframesEnabled() !== desired) {\n editor.setKeyframesEnabled(desired);\n }\n }, [props.keyframes?.enabled]);\n\n useEffect(() => {\n const editor = editorRef.current;\n if (!editor) return;\n const desired = props.clipEdgeNav?.enabled === true;\n if (editor.isClipEdgeNavEnabled() !== desired) {\n editor.setClipEdgeNavEnabled(desired);\n }\n }, [props.clipEdgeNav?.enabled]);\n\n // Reactive — the underlying CSS custom property can be updated on\n // the container any time; the timeline picks up the new height\n // immediately via CSS. No remount required.\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n if (props.timelineHeight != null && props.timelineHeight > 0) {\n host.style.setProperty(\n \"--aicut-timeline-height\",\n `${Math.round(props.timelineHeight)}px`,\n );\n } else {\n host.style.removeProperty(\"--aicut-timeline-height\");\n }\n }, [props.timelineHeight]);\n\n // Deps must include `slots`. Without it, the factory ran once during\n // the first commit — BEFORE the useEffect above had a chance to\n // create the editor — so `apiRef.current` was permanently locked to\n // null. `slots` flips from null to a real value the same instant\n // the editor is created, so it's the cleanest re-run trigger.\n useImperativeHandle<VideoEditorApi | null, VideoEditorApi | null>(\n props.apiRef,\n () => editorRef.current,\n [slots],\n );\n\n return (\n <div\n ref={hostRef}\n className={props.className}\n style={props.style}\n data-aicut-host=\"\"\n >\n {slots && props.toolbarLeft != null\n ? createPortal(props.toolbarLeft, slots.left)\n : null}\n {slots && props.toolbarRight != null\n ? createPortal(props.toolbarRight, slots.right)\n : null}\n {slots && props.headerLeft != null\n ? createPortal(props.headerLeft, slots.headerLeft)\n : null}\n {slots && props.headerRight != null\n ? createPortal(props.headerRight, slots.headerRight)\n : null}\n </div>\n );\n}\n","import {\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n type Ref,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n Timeline as CoreTimeline,\n type Clip,\n type Locale,\n type Ms,\n type Project,\n type TimelineOptions,\n} from \"@aicut/core\";\n\n/** Imperative handle exposed via `apiRef`. */\nexport interface TimelineApi {\n setProject(p: Project): void;\n getProject(): Project;\n setTime(t: Ms): void;\n getTime(): Ms;\n setScale(pxPerSec: number): void;\n getScale(): number;\n setSelection(id: string | null): void;\n getSelection(): string | null;\n setSnap(snap: boolean): void;\n fitToWindow(): void;\n getDebugInfo(): ReturnType<CoreTimeline[\"getDebugInfo\"]>;\n}\n\nexport interface TimelineProps {\n /** Initial project. Use `apiRef.current.setProject(...)` to swap. */\n defaultProject: Project;\n /** Initial scale (px/sec). Defaults to 80; auto-fits on first render. */\n defaultScale?: number;\n /** Initial playhead position. */\n defaultTime?: Ms;\n /** Initial selection. */\n defaultSelectedClipId?: string | null;\n\n /** Hide the left header column (compact / frame-picker mode). */\n showHeader?: boolean;\n /** Disable all editing interactions. */\n readOnly?: boolean;\n /** Snap to clip edges + playhead when dragging. Default true. */\n snap?: boolean;\n /** Apply fit-to-window on mount once duration is known. Default true. */\n autoFit?: boolean;\n /** UI string overrides (English default). */\n locale?: Partial<Locale>;\n /**\n * Render a 36px top toolbar strip with empty left/right flex slots\n * for host-supplied controls. Default false. Pair with `toolbarLeft`\n * / `toolbarRight` to inject content.\n */\n toolbar?: boolean;\n /** Rendered into the left slot of the timeline toolbar (toolbar must be true). */\n toolbarLeft?: ReactNode;\n /** Rendered into the right slot of the timeline toolbar. */\n toolbarRight?: ReactNode;\n\n className?: string;\n style?: CSSProperties;\n\n apiRef?: Ref<TimelineApi | null>;\n\n onSeek?: (timeMs: Ms) => void;\n onSelectClip?: (clipId: string | null) => void;\n onScaleChange?: (pxPerSec: number) => void;\n onMoveClip?: TimelineOptions[\"onMoveClip\"];\n onResizeClip?: TimelineOptions[\"onResizeClip\"];\n onChange?: (project: Project) => void;\n}\n\n/**\n * Standalone, framework-agnostic canvas Timeline wrapped for React.\n * Mount it without an `Editor` for use cases like a video frame-picker:\n *\n * ```tsx\n * <Timeline\n * defaultProject={{ version: 1, sources: [video], tracks: [{ id, kind: \"video\", clips: [{...}] }] }}\n * showHeader={false}\n * readOnly\n * onSeek={(ms) => setCurrentMs(ms)}\n * />\n * ```\n *\n * Uncontrolled for `project` and `pxPerSec` — the underlying Timeline\n * owns them and reports changes via callbacks. Call methods on\n * `apiRef.current` to drive it imperatively (mirroring ag-Grid /\n * VideoEditor patterns).\n */\nexport function Timeline(props: TimelineProps) {\n const hostRef = useRef<HTMLDivElement | null>(null);\n const tlRef = useRef<CoreTimeline | null>(null);\n const [slots, setSlots] = useState<{\n left: HTMLElement;\n right: HTMLElement;\n } | null>(null);\n\n // Latest-callback ref so the create-once effect doesn't tear the\n // timeline down on every render just because callback identities\n // change.\n const cbRef = useRef(props);\n cbRef.current = props;\n\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n const tl = CoreTimeline.create({\n container: host,\n project: cbRef.current.defaultProject,\n pxPerSec: cbRef.current.defaultScale,\n time: cbRef.current.defaultTime,\n selectedClipId: cbRef.current.defaultSelectedClipId ?? null,\n showHeader: cbRef.current.showHeader,\n readOnly: cbRef.current.readOnly,\n snap: cbRef.current.snap,\n autoFit: cbRef.current.autoFit,\n locale: cbRef.current.locale,\n toolbar: cbRef.current.toolbar,\n onSeek: (t) => cbRef.current.onSeek?.(t),\n onSelectClip: (id) => cbRef.current.onSelectClip?.(id),\n onScaleChange: (s) => cbRef.current.onScaleChange?.(s),\n onMoveClip: (id, opts) => cbRef.current.onMoveClip?.(id, opts),\n onResizeClip: (id, e) => cbRef.current.onResizeClip?.(id, e),\n onChange: (p) => cbRef.current.onChange?.(p),\n });\n tlRef.current = tl;\n if (tl.toolbarLeft && tl.toolbarRight) {\n setSlots({ left: tl.toolbarLeft, right: tl.toolbarRight });\n }\n return () => {\n tl.destroy();\n tlRef.current = null;\n setSlots(null);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (props.locale) tlRef.current?.setLocale(props.locale);\n }, [props.locale]);\n\n useImperativeHandle<TimelineApi | null, TimelineApi | null>(\n props.apiRef,\n () => {\n const tl = tlRef.current;\n if (!tl) return null;\n return {\n setProject: (p) => tl.setProject(p),\n getProject: () => tl.getProject(),\n setTime: (t) => tl.setTime(t),\n getTime: () => tl.getTime(),\n setScale: (s) => tl.setScale(s),\n getScale: () => tl.getScale(),\n setSelection: (id) => tl.setSelection(id),\n getSelection: () => tl.getSelection(),\n setSnap: (s) => tl.setSnap(s),\n fitToWindow: () => tl.fitToWindow(),\n getDebugInfo: () => tl.getDebugInfo(),\n };\n },\n // Same caveat as VideoEditor.tsx — factory must re-run once the\n // timeline is created in useEffect, otherwise apiRef.current is\n // null forever. `slots` flips from null to a real value the\n // instant the timeline is ready, so it's the cleanest trigger.\n [slots],\n );\n\n return (\n <div\n ref={hostRef}\n className={props.className}\n style={{ width: \"100%\", height: 240, ...props.style }}\n data-aicut-timeline-host=\"\"\n >\n {slots && props.toolbarLeft != null\n ? createPortal(props.toolbarLeft, slots.left)\n : null}\n {slots && props.toolbarRight != null\n ? createPortal(props.toolbarRight, slots.right)\n : null}\n </div>\n );\n\n // Type-only re-export used to keep React/Vue prop typings in lockstep\n // with the core. Reference here so the symbol isn't tree-shaken.\n void ({} as Clip);\n}\n"],"mappings":"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","names":["import_react","import_react_dom","import_core","import_jsx_runtime","CoreTimeline","import_core"]}
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { CSSProperties, Ref, ReactNode } from 'react';
3
- import { Project, Theme, Locale, EditorApi, Ms, Timeline as Timeline$1, TimelineOptions } from '@aicut/core';
4
- export { Clip, EditorApi, Locale, MediaSource, Ms, Project, Theme, Track, createEmptyProject, createId, localeEn, localeZh } from '@aicut/core';
3
+ import { Project, Theme, Locale, EditorApi, Ms, PlaybackEngineFactory, Timeline as Timeline$1, TimelineOptions } from '@aicut/core';
4
+ export { CanvasCompositorEngine, CanvasCompositorEngineOptions, Clip, EditorApi, EffectiveTransform, HEADER_WIDTH, HtmlVideoEngine, IDENTITY_TRANSFORM, Keyframe, Locale, MediaSource, Ms, PlaybackEngine, PlaybackEngineFactory, PlaybackEngineOptions, Project, RULER_HEIGHT, TRACK_HEIGHT, Theme, Track, canvasCompositorEngineFactory, createEmptyProject, createId, getEffectiveTransform, getTransformAtTimelineTime, htmlVideoEngineFactory, isIdentityTransform, localeEn, localeZh, setTimelineMetrics } from '@aicut/core';
5
5
 
6
6
  type VideoEditorApi = EditorApi;
7
7
  interface VideoEditorProps {
@@ -49,6 +49,58 @@ interface VideoEditorProps {
49
49
  headerLeft?: ReactNode;
50
50
  /** Right side of the editor header — conventionally Share / Export / profile. */
51
51
  headerRight?: ReactNode;
52
+ /**
53
+ * Initial-only — picks the playback engine used by the underlying
54
+ * core Editor. Defaults to the built-in `HtmlVideoEngine`. Pass a
55
+ * factory to plug in a custom engine (WebCodecs, WebGL compositor,
56
+ * IPC bridge to a native player, …). Swapping this prop after mount
57
+ * has no effect — the editor binds its engine at construction.
58
+ */
59
+ playbackEngine?: PlaybackEngineFactory;
60
+ /**
61
+ * Initial-only — pixel height of each track row (default 56). Lower
62
+ * values (~32–40) shrink the timeline for small viewports where the
63
+ * default crowds out the preview. Applied process-wide; to re-apply
64
+ * change this prop AND remount the component (e.g. via `key`).
65
+ */
66
+ trackHeight?: number;
67
+ /** Initial-only — pixel height of the timeline ruler (default 24). */
68
+ rulerHeight?: number;
69
+ /**
70
+ * Pixel height of the whole bottom timeline area (default 240).
71
+ * Reactive — set anytime to change. The canvas inside fills 100%
72
+ * and shows an internal scrollbar when track count overflows.
73
+ * Useful range: [120, 480] depending on viewport.
74
+ */
75
+ timelineHeight?: number;
76
+ /**
77
+ * Per-clip keyframe animation (X / Y / Scale). Reactive — set
78
+ * `{ enabled: true }` to surface keyframe diamonds on the timeline
79
+ * and route the canvas-based engines through the transform pipeline.
80
+ * Disabling hides the editing UI but preserves the data in
81
+ * `Project.tracks[].clips[].keyframes`.
82
+ *
83
+ * `HtmlVideoEngine` cannot animate frames; swap to
84
+ * `CanvasCompositorEngine` or `WebCodecsEngine` for live preview.
85
+ */
86
+ keyframes?: {
87
+ enabled?: boolean;
88
+ };
89
+ /** Fires when the user selects or deselects a keyframe diamond. */
90
+ onKeyframeSelectionChange?: (target: {
91
+ clipId: string;
92
+ keyframeId: string;
93
+ } | null) => void;
94
+ /**
95
+ * Jump-to-clip-edge toolbar cluster (|◀ ▶|) + I/O keyboard shortcuts.
96
+ * Reactive — set `{ enabled: true }` to surface the buttons next to
97
+ * the keyframe diamond and bind the shortcuts. Off hides the buttons
98
+ * entirely (display: none, no toolbar space cost) and lets I/O
99
+ * fall through to the page.
100
+ */
101
+ clipEdgeNav?: {
102
+ enabled?: boolean;
103
+ };
52
104
  }
53
105
  /**
54
106
  * Declarative React shell over `@aicut/core` `Editor`. Mounts the
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { CSSProperties, Ref, ReactNode } from 'react';
3
- import { Project, Theme, Locale, EditorApi, Ms, Timeline as Timeline$1, TimelineOptions } from '@aicut/core';
4
- export { Clip, EditorApi, Locale, MediaSource, Ms, Project, Theme, Track, createEmptyProject, createId, localeEn, localeZh } from '@aicut/core';
3
+ import { Project, Theme, Locale, EditorApi, Ms, PlaybackEngineFactory, Timeline as Timeline$1, TimelineOptions } from '@aicut/core';
4
+ export { CanvasCompositorEngine, CanvasCompositorEngineOptions, Clip, EditorApi, EffectiveTransform, HEADER_WIDTH, HtmlVideoEngine, IDENTITY_TRANSFORM, Keyframe, Locale, MediaSource, Ms, PlaybackEngine, PlaybackEngineFactory, PlaybackEngineOptions, Project, RULER_HEIGHT, TRACK_HEIGHT, Theme, Track, canvasCompositorEngineFactory, createEmptyProject, createId, getEffectiveTransform, getTransformAtTimelineTime, htmlVideoEngineFactory, isIdentityTransform, localeEn, localeZh, setTimelineMetrics } from '@aicut/core';
5
5
 
6
6
  type VideoEditorApi = EditorApi;
7
7
  interface VideoEditorProps {
@@ -49,6 +49,58 @@ interface VideoEditorProps {
49
49
  headerLeft?: ReactNode;
50
50
  /** Right side of the editor header — conventionally Share / Export / profile. */
51
51
  headerRight?: ReactNode;
52
+ /**
53
+ * Initial-only — picks the playback engine used by the underlying
54
+ * core Editor. Defaults to the built-in `HtmlVideoEngine`. Pass a
55
+ * factory to plug in a custom engine (WebCodecs, WebGL compositor,
56
+ * IPC bridge to a native player, …). Swapping this prop after mount
57
+ * has no effect — the editor binds its engine at construction.
58
+ */
59
+ playbackEngine?: PlaybackEngineFactory;
60
+ /**
61
+ * Initial-only — pixel height of each track row (default 56). Lower
62
+ * values (~32–40) shrink the timeline for small viewports where the
63
+ * default crowds out the preview. Applied process-wide; to re-apply
64
+ * change this prop AND remount the component (e.g. via `key`).
65
+ */
66
+ trackHeight?: number;
67
+ /** Initial-only — pixel height of the timeline ruler (default 24). */
68
+ rulerHeight?: number;
69
+ /**
70
+ * Pixel height of the whole bottom timeline area (default 240).
71
+ * Reactive — set anytime to change. The canvas inside fills 100%
72
+ * and shows an internal scrollbar when track count overflows.
73
+ * Useful range: [120, 480] depending on viewport.
74
+ */
75
+ timelineHeight?: number;
76
+ /**
77
+ * Per-clip keyframe animation (X / Y / Scale). Reactive — set
78
+ * `{ enabled: true }` to surface keyframe diamonds on the timeline
79
+ * and route the canvas-based engines through the transform pipeline.
80
+ * Disabling hides the editing UI but preserves the data in
81
+ * `Project.tracks[].clips[].keyframes`.
82
+ *
83
+ * `HtmlVideoEngine` cannot animate frames; swap to
84
+ * `CanvasCompositorEngine` or `WebCodecsEngine` for live preview.
85
+ */
86
+ keyframes?: {
87
+ enabled?: boolean;
88
+ };
89
+ /** Fires when the user selects or deselects a keyframe diamond. */
90
+ onKeyframeSelectionChange?: (target: {
91
+ clipId: string;
92
+ keyframeId: string;
93
+ } | null) => void;
94
+ /**
95
+ * Jump-to-clip-edge toolbar cluster (|◀ ▶|) + I/O keyboard shortcuts.
96
+ * Reactive — set `{ enabled: true }` to surface the buttons next to
97
+ * the keyframe diamond and bind the shortcuts. Off hides the buttons
98
+ * entirely (display: none, no toolbar space cost) and lets I/O
99
+ * fall through to the page.
100
+ */
101
+ clipEdgeNav?: {
102
+ enabled?: boolean;
103
+ };
52
104
  }
53
105
  /**
54
106
  * Declarative React shell over `@aicut/core` `Editor`. Mounts the
package/dist/index.js CHANGED
@@ -23,7 +23,13 @@ function VideoEditor(props) {
23
23
  container: host,
24
24
  project: cbRef.current.defaultProject,
25
25
  theme: cbRef.current.theme,
26
- locale: cbRef.current.locale
26
+ locale: cbRef.current.locale,
27
+ playbackEngine: cbRef.current.playbackEngine,
28
+ ...cbRef.current.trackHeight != null ? { trackHeight: cbRef.current.trackHeight } : {},
29
+ ...cbRef.current.rulerHeight != null ? { rulerHeight: cbRef.current.rulerHeight } : {},
30
+ ...cbRef.current.timelineHeight != null ? { timelineHeight: cbRef.current.timelineHeight } : {},
31
+ ...cbRef.current.keyframes != null ? { keyframes: cbRef.current.keyframes } : {},
32
+ ...cbRef.current.clipEdgeNav != null ? { clipEdgeNav: cbRef.current.clipEdgeNav } : {}
27
33
  });
28
34
  editorRef.current = editor;
29
35
  setSlots({
@@ -42,6 +48,10 @@ function VideoEditor(props) {
42
48
  "selectionChange",
43
49
  ({ clipId }) => cbRef.current.onSelectionChange?.(clipId)
44
50
  ),
51
+ editor.on(
52
+ "keyframeSelectionChange",
53
+ ({ target }) => cbRef.current.onKeyframeSelectionChange?.(target)
54
+ ),
45
55
  editor.on("error", ({ error }) => cbRef.current.onError?.(error))
46
56
  ];
47
57
  cbRef.current.onReady?.(editor);
@@ -58,6 +68,34 @@ function VideoEditor(props) {
58
68
  useEffect(() => {
59
69
  if (props.locale) editorRef.current?.setLocale(props.locale);
60
70
  }, [props.locale]);
71
+ useEffect(() => {
72
+ const editor = editorRef.current;
73
+ if (!editor) return;
74
+ const desired = props.keyframes?.enabled === true;
75
+ if (editor.isKeyframesEnabled() !== desired) {
76
+ editor.setKeyframesEnabled(desired);
77
+ }
78
+ }, [props.keyframes?.enabled]);
79
+ useEffect(() => {
80
+ const editor = editorRef.current;
81
+ if (!editor) return;
82
+ const desired = props.clipEdgeNav?.enabled === true;
83
+ if (editor.isClipEdgeNavEnabled() !== desired) {
84
+ editor.setClipEdgeNavEnabled(desired);
85
+ }
86
+ }, [props.clipEdgeNav?.enabled]);
87
+ useEffect(() => {
88
+ const host = hostRef.current;
89
+ if (!host) return;
90
+ if (props.timelineHeight != null && props.timelineHeight > 0) {
91
+ host.style.setProperty(
92
+ "--aicut-timeline-height",
93
+ `${Math.round(props.timelineHeight)}px`
94
+ );
95
+ } else {
96
+ host.style.removeProperty("--aicut-timeline-height");
97
+ }
98
+ }, [props.timelineHeight]);
61
99
  useImperativeHandle(
62
100
  props.apiRef,
63
101
  () => editorRef.current,
@@ -175,13 +213,42 @@ function Timeline(props) {
175
213
  }
176
214
 
177
215
  // src/index.ts
178
- import { createEmptyProject, createId, localeEn, localeZh } from "@aicut/core";
216
+ import {
217
+ createEmptyProject,
218
+ createId,
219
+ localeEn,
220
+ localeZh,
221
+ HtmlVideoEngine,
222
+ htmlVideoEngineFactory,
223
+ CanvasCompositorEngine,
224
+ canvasCompositorEngineFactory,
225
+ TRACK_HEIGHT,
226
+ RULER_HEIGHT,
227
+ HEADER_WIDTH,
228
+ setTimelineMetrics,
229
+ IDENTITY_TRANSFORM,
230
+ isIdentityTransform,
231
+ getEffectiveTransform,
232
+ getTransformAtTimelineTime
233
+ } from "@aicut/core";
179
234
  export {
235
+ CanvasCompositorEngine,
236
+ HEADER_WIDTH,
237
+ HtmlVideoEngine,
238
+ IDENTITY_TRANSFORM,
239
+ RULER_HEIGHT,
240
+ TRACK_HEIGHT,
180
241
  Timeline,
181
242
  VideoEditor,
243
+ canvasCompositorEngineFactory,
182
244
  createEmptyProject,
183
245
  createId,
246
+ getEffectiveTransform,
247
+ getTransformAtTimelineTime,
248
+ htmlVideoEngineFactory,
249
+ isIdentityTransform,
184
250
  localeEn,
185
- localeZh
251
+ localeZh,
252
+ setTimelineMetrics
186
253
  };
187
254
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/VideoEditor.tsx","../src/Timeline.tsx","../src/index.ts"],"sourcesContent":["import {\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n type Ref,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n Editor,\n type EditorApi,\n type Locale,\n type Ms,\n type Project,\n type Theme,\n} from \"@aicut/core\";\n\nexport type VideoEditorApi = EditorApi;\n\nexport interface VideoEditorProps {\n /**\n * Initial project. Read once on mount — to swap projects after mount,\n * call `apiRef.current.setProject(...)` so React doesn't reinstantiate\n * the editor and lose playback state.\n */\n defaultProject?: Project;\n /** CSS variable overrides applied on mount and whenever this ref changes. */\n theme?: Theme;\n /**\n * UI string overrides (English default). Mirror prop — switching the\n * value calls `editor.setLocale` and the toolbar / canvas labels\n * update in place. Use `localeZh` from `@aicut/core` for Chinese.\n */\n locale?: Partial<Locale>;\n\n className?: string;\n style?: CSSProperties;\n\n /** Imperative handle for cut/seek/getProject/setProject/etc. */\n apiRef?: Ref<VideoEditorApi | null>;\n\n onReady?: (api: VideoEditorApi) => void;\n onChange?: (project: Project) => void;\n onExport?: (project: Project) => void;\n onTimeUpdate?: (timeMs: Ms) => void;\n onPlay?: () => void;\n onPause?: () => void;\n onSelectionChange?: (clipId: string | null) => void;\n onError?: (error: Error) => void;\n\n /**\n * Rendered into the very left of the editor's top toolbar — host\n * adds anything here (size dropdown, branding, status badge). The\n * library reserves no space for it; if you pass nothing, no\n * separator appears.\n */\n toolbarLeft?: ReactNode;\n /** Same as `toolbarLeft` but at the very right of the toolbar. */\n toolbarRight?: ReactNode;\n /**\n * Rendered into the LEFT side of an optional header bar above the\n * preview (project name, file menu, breadcrumbs). The header\n * collapses entirely when both header slots are empty, so the\n * default layout is identical to before this slot existed.\n */\n headerLeft?: ReactNode;\n /** Right side of the editor header — conventionally Share / Export / profile. */\n headerRight?: ReactNode;\n}\n\n/**\n * Declarative React shell over `@aicut/core` `Editor`. Mounts the\n * editor instance once, mirrors prop changes (`theme`) into it, and\n * forwards events as React-style callbacks.\n *\n * Intentionally uncontrolled for project state — the editor owns the\n * current project. Use `onChange` to persist and `apiRef.setProject`\n * to restore.\n */\nexport function VideoEditor(props: VideoEditorProps) {\n const hostRef = useRef<HTMLDivElement | null>(null);\n const editorRef = useRef<Editor | null>(null);\n // Toolbar slot DOM nodes don't exist until the editor mounts; we\n // hold them in state so React re-runs the render after mount and\n // the portals attach. Tracked separately for left + right because\n // each is independently controlled by host props.\n const [slots, setSlots] = useState<{\n left: HTMLElement;\n right: HTMLElement;\n headerLeft: HTMLElement;\n headerRight: HTMLElement;\n } | null>(null);\n\n // Latest-callback refs so the effect that creates the editor doesn't\n // re-run on every parent render just because props.onChange is a new\n // identity — the editor would otherwise be torn down constantly.\n const cbRef = useRef(props);\n cbRef.current = props;\n\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n const editor = Editor.create({\n container: host,\n project: cbRef.current.defaultProject,\n theme: cbRef.current.theme,\n locale: cbRef.current.locale,\n });\n editorRef.current = editor;\n setSlots({\n left: editor.toolbarLeft,\n right: editor.toolbarRight,\n headerLeft: editor.headerLeft,\n headerRight: editor.headerRight,\n });\n\n const offs = [\n editor.on(\"change\", ({ project }) => cbRef.current.onChange?.(project)),\n editor.on(\"export\", ({ project }) => cbRef.current.onExport?.(project)),\n editor.on(\"time\", ({ timeMs }) => cbRef.current.onTimeUpdate?.(timeMs)),\n editor.on(\"play\", () => cbRef.current.onPlay?.()),\n editor.on(\"pause\", () => cbRef.current.onPause?.()),\n editor.on(\"selectionChange\", ({ clipId }) =>\n cbRef.current.onSelectionChange?.(clipId),\n ),\n editor.on(\"error\", ({ error }) => cbRef.current.onError?.(error)),\n ];\n\n cbRef.current.onReady?.(editor);\n\n return () => {\n for (const off of offs) off();\n editor.destroy();\n editorRef.current = null;\n setSlots(null);\n };\n // Editor lifecycle is tied to mount; we deliberately don't list\n // any reactive deps. `theme` changes are pushed through the\n // separate effect below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (props.theme) editorRef.current?.setTheme(props.theme);\n }, [props.theme]);\n\n useEffect(() => {\n if (props.locale) editorRef.current?.setLocale(props.locale);\n }, [props.locale]);\n\n // Deps must include `slots`. Without it, the factory ran once during\n // the first commit — BEFORE the useEffect above had a chance to\n // create the editor — so `apiRef.current` was permanently locked to\n // null. `slots` flips from null to a real value the same instant\n // the editor is created, so it's the cleanest re-run trigger.\n useImperativeHandle<VideoEditorApi | null, VideoEditorApi | null>(\n props.apiRef,\n () => editorRef.current,\n [slots],\n );\n\n return (\n <div\n ref={hostRef}\n className={props.className}\n style={props.style}\n data-aicut-host=\"\"\n >\n {slots && props.toolbarLeft != null\n ? createPortal(props.toolbarLeft, slots.left)\n : null}\n {slots && props.toolbarRight != null\n ? createPortal(props.toolbarRight, slots.right)\n : null}\n {slots && props.headerLeft != null\n ? createPortal(props.headerLeft, slots.headerLeft)\n : null}\n {slots && props.headerRight != null\n ? createPortal(props.headerRight, slots.headerRight)\n : null}\n </div>\n );\n}\n","import {\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n type Ref,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n Timeline as CoreTimeline,\n type Clip,\n type Locale,\n type Ms,\n type Project,\n type TimelineOptions,\n} from \"@aicut/core\";\n\n/** Imperative handle exposed via `apiRef`. */\nexport interface TimelineApi {\n setProject(p: Project): void;\n getProject(): Project;\n setTime(t: Ms): void;\n getTime(): Ms;\n setScale(pxPerSec: number): void;\n getScale(): number;\n setSelection(id: string | null): void;\n getSelection(): string | null;\n setSnap(snap: boolean): void;\n fitToWindow(): void;\n getDebugInfo(): ReturnType<CoreTimeline[\"getDebugInfo\"]>;\n}\n\nexport interface TimelineProps {\n /** Initial project. Use `apiRef.current.setProject(...)` to swap. */\n defaultProject: Project;\n /** Initial scale (px/sec). Defaults to 80; auto-fits on first render. */\n defaultScale?: number;\n /** Initial playhead position. */\n defaultTime?: Ms;\n /** Initial selection. */\n defaultSelectedClipId?: string | null;\n\n /** Hide the left header column (compact / frame-picker mode). */\n showHeader?: boolean;\n /** Disable all editing interactions. */\n readOnly?: boolean;\n /** Snap to clip edges + playhead when dragging. Default true. */\n snap?: boolean;\n /** Apply fit-to-window on mount once duration is known. Default true. */\n autoFit?: boolean;\n /** UI string overrides (English default). */\n locale?: Partial<Locale>;\n /**\n * Render a 36px top toolbar strip with empty left/right flex slots\n * for host-supplied controls. Default false. Pair with `toolbarLeft`\n * / `toolbarRight` to inject content.\n */\n toolbar?: boolean;\n /** Rendered into the left slot of the timeline toolbar (toolbar must be true). */\n toolbarLeft?: ReactNode;\n /** Rendered into the right slot of the timeline toolbar. */\n toolbarRight?: ReactNode;\n\n className?: string;\n style?: CSSProperties;\n\n apiRef?: Ref<TimelineApi | null>;\n\n onSeek?: (timeMs: Ms) => void;\n onSelectClip?: (clipId: string | null) => void;\n onScaleChange?: (pxPerSec: number) => void;\n onMoveClip?: TimelineOptions[\"onMoveClip\"];\n onResizeClip?: TimelineOptions[\"onResizeClip\"];\n onChange?: (project: Project) => void;\n}\n\n/**\n * Standalone, framework-agnostic canvas Timeline wrapped for React.\n * Mount it without an `Editor` for use cases like a video frame-picker:\n *\n * ```tsx\n * <Timeline\n * defaultProject={{ version: 1, sources: [video], tracks: [{ id, kind: \"video\", clips: [{...}] }] }}\n * showHeader={false}\n * readOnly\n * onSeek={(ms) => setCurrentMs(ms)}\n * />\n * ```\n *\n * Uncontrolled for `project` and `pxPerSec` — the underlying Timeline\n * owns them and reports changes via callbacks. Call methods on\n * `apiRef.current` to drive it imperatively (mirroring ag-Grid /\n * VideoEditor patterns).\n */\nexport function Timeline(props: TimelineProps) {\n const hostRef = useRef<HTMLDivElement | null>(null);\n const tlRef = useRef<CoreTimeline | null>(null);\n const [slots, setSlots] = useState<{\n left: HTMLElement;\n right: HTMLElement;\n } | null>(null);\n\n // Latest-callback ref so the create-once effect doesn't tear the\n // timeline down on every render just because callback identities\n // change.\n const cbRef = useRef(props);\n cbRef.current = props;\n\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n const tl = CoreTimeline.create({\n container: host,\n project: cbRef.current.defaultProject,\n pxPerSec: cbRef.current.defaultScale,\n time: cbRef.current.defaultTime,\n selectedClipId: cbRef.current.defaultSelectedClipId ?? null,\n showHeader: cbRef.current.showHeader,\n readOnly: cbRef.current.readOnly,\n snap: cbRef.current.snap,\n autoFit: cbRef.current.autoFit,\n locale: cbRef.current.locale,\n toolbar: cbRef.current.toolbar,\n onSeek: (t) => cbRef.current.onSeek?.(t),\n onSelectClip: (id) => cbRef.current.onSelectClip?.(id),\n onScaleChange: (s) => cbRef.current.onScaleChange?.(s),\n onMoveClip: (id, opts) => cbRef.current.onMoveClip?.(id, opts),\n onResizeClip: (id, e) => cbRef.current.onResizeClip?.(id, e),\n onChange: (p) => cbRef.current.onChange?.(p),\n });\n tlRef.current = tl;\n if (tl.toolbarLeft && tl.toolbarRight) {\n setSlots({ left: tl.toolbarLeft, right: tl.toolbarRight });\n }\n return () => {\n tl.destroy();\n tlRef.current = null;\n setSlots(null);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (props.locale) tlRef.current?.setLocale(props.locale);\n }, [props.locale]);\n\n useImperativeHandle<TimelineApi | null, TimelineApi | null>(\n props.apiRef,\n () => {\n const tl = tlRef.current;\n if (!tl) return null;\n return {\n setProject: (p) => tl.setProject(p),\n getProject: () => tl.getProject(),\n setTime: (t) => tl.setTime(t),\n getTime: () => tl.getTime(),\n setScale: (s) => tl.setScale(s),\n getScale: () => tl.getScale(),\n setSelection: (id) => tl.setSelection(id),\n getSelection: () => tl.getSelection(),\n setSnap: (s) => tl.setSnap(s),\n fitToWindow: () => tl.fitToWindow(),\n getDebugInfo: () => tl.getDebugInfo(),\n };\n },\n // Same caveat as VideoEditor.tsx — factory must re-run once the\n // timeline is created in useEffect, otherwise apiRef.current is\n // null forever. `slots` flips from null to a real value the\n // instant the timeline is ready, so it's the cleanest trigger.\n [slots],\n );\n\n return (\n <div\n ref={hostRef}\n className={props.className}\n style={{ width: \"100%\", height: 240, ...props.style }}\n data-aicut-timeline-host=\"\"\n >\n {slots && props.toolbarLeft != null\n ? createPortal(props.toolbarLeft, slots.left)\n : null}\n {slots && props.toolbarRight != null\n ? createPortal(props.toolbarRight, slots.right)\n : null}\n </div>\n );\n\n // Type-only re-export used to keep React/Vue prop typings in lockstep\n // with the core. Reference here so the symbol isn't tree-shaken.\n void ({} as Clip);\n}\n","export { VideoEditor } from \"./VideoEditor.js\";\nexport type { VideoEditorProps, VideoEditorApi } from \"./VideoEditor.js\";\nexport { Timeline } from \"./Timeline.js\";\nexport type { TimelineProps, TimelineApi } from \"./Timeline.js\";\nexport type {\n Project,\n MediaSource,\n Track,\n Clip,\n Ms,\n Theme,\n EditorApi,\n Locale,\n} from \"@aicut/core\";\nexport { createEmptyProject, createId, localeEn, localeZh } from \"@aicut/core\";\n"],"mappings":"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","names":["useEffect","useImperativeHandle","useRef","useState","createPortal","jsxs"]}
1
+ {"version":3,"sources":["../src/VideoEditor.tsx","../src/Timeline.tsx","../src/index.ts"],"sourcesContent":["import {\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n type Ref,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n Editor,\n type EditorApi,\n type Locale,\n type Ms,\n type PlaybackEngineFactory,\n type Project,\n type Theme,\n} from \"@aicut/core\";\n\nexport type VideoEditorApi = EditorApi;\n\nexport interface VideoEditorProps {\n /**\n * Initial project. Read once on mount — to swap projects after mount,\n * call `apiRef.current.setProject(...)` so React doesn't reinstantiate\n * the editor and lose playback state.\n */\n defaultProject?: Project;\n /** CSS variable overrides applied on mount and whenever this ref changes. */\n theme?: Theme;\n /**\n * UI string overrides (English default). Mirror prop — switching the\n * value calls `editor.setLocale` and the toolbar / canvas labels\n * update in place. Use `localeZh` from `@aicut/core` for Chinese.\n */\n locale?: Partial<Locale>;\n\n className?: string;\n style?: CSSProperties;\n\n /** Imperative handle for cut/seek/getProject/setProject/etc. */\n apiRef?: Ref<VideoEditorApi | null>;\n\n onReady?: (api: VideoEditorApi) => void;\n onChange?: (project: Project) => void;\n onExport?: (project: Project) => void;\n onTimeUpdate?: (timeMs: Ms) => void;\n onPlay?: () => void;\n onPause?: () => void;\n onSelectionChange?: (clipId: string | null) => void;\n onError?: (error: Error) => void;\n\n /**\n * Rendered into the very left of the editor's top toolbar — host\n * adds anything here (size dropdown, branding, status badge). The\n * library reserves no space for it; if you pass nothing, no\n * separator appears.\n */\n toolbarLeft?: ReactNode;\n /** Same as `toolbarLeft` but at the very right of the toolbar. */\n toolbarRight?: ReactNode;\n /**\n * Rendered into the LEFT side of an optional header bar above the\n * preview (project name, file menu, breadcrumbs). The header\n * collapses entirely when both header slots are empty, so the\n * default layout is identical to before this slot existed.\n */\n headerLeft?: ReactNode;\n /** Right side of the editor header — conventionally Share / Export / profile. */\n headerRight?: ReactNode;\n\n /**\n * Initial-only — picks the playback engine used by the underlying\n * core Editor. Defaults to the built-in `HtmlVideoEngine`. Pass a\n * factory to plug in a custom engine (WebCodecs, WebGL compositor,\n * IPC bridge to a native player, …). Swapping this prop after mount\n * has no effect — the editor binds its engine at construction.\n */\n playbackEngine?: PlaybackEngineFactory;\n /**\n * Initial-only — pixel height of each track row (default 56). Lower\n * values (~32–40) shrink the timeline for small viewports where the\n * default crowds out the preview. Applied process-wide; to re-apply\n * change this prop AND remount the component (e.g. via `key`).\n */\n trackHeight?: number;\n /** Initial-only — pixel height of the timeline ruler (default 24). */\n rulerHeight?: number;\n /**\n * Pixel height of the whole bottom timeline area (default 240).\n * Reactive — set anytime to change. The canvas inside fills 100%\n * and shows an internal scrollbar when track count overflows.\n * Useful range: [120, 480] depending on viewport.\n */\n timelineHeight?: number;\n /**\n * Per-clip keyframe animation (X / Y / Scale). Reactive — set\n * `{ enabled: true }` to surface keyframe diamonds on the timeline\n * and route the canvas-based engines through the transform pipeline.\n * Disabling hides the editing UI but preserves the data in\n * `Project.tracks[].clips[].keyframes`.\n *\n * `HtmlVideoEngine` cannot animate frames; swap to\n * `CanvasCompositorEngine` or `WebCodecsEngine` for live preview.\n */\n keyframes?: { enabled?: boolean };\n /** Fires when the user selects or deselects a keyframe diamond. */\n onKeyframeSelectionChange?: (\n target: { clipId: string; keyframeId: string } | null,\n ) => void;\n /**\n * Jump-to-clip-edge toolbar cluster (|◀ ▶|) + I/O keyboard shortcuts.\n * Reactive — set `{ enabled: true }` to surface the buttons next to\n * the keyframe diamond and bind the shortcuts. Off hides the buttons\n * entirely (display: none, no toolbar space cost) and lets I/O\n * fall through to the page.\n */\n clipEdgeNav?: { enabled?: boolean };\n}\n\n/**\n * Declarative React shell over `@aicut/core` `Editor`. Mounts the\n * editor instance once, mirrors prop changes (`theme`) into it, and\n * forwards events as React-style callbacks.\n *\n * Intentionally uncontrolled for project state — the editor owns the\n * current project. Use `onChange` to persist and `apiRef.setProject`\n * to restore.\n */\nexport function VideoEditor(props: VideoEditorProps) {\n const hostRef = useRef<HTMLDivElement | null>(null);\n const editorRef = useRef<Editor | null>(null);\n // Toolbar slot DOM nodes don't exist until the editor mounts; we\n // hold them in state so React re-runs the render after mount and\n // the portals attach. Tracked separately for left + right because\n // each is independently controlled by host props.\n const [slots, setSlots] = useState<{\n left: HTMLElement;\n right: HTMLElement;\n headerLeft: HTMLElement;\n headerRight: HTMLElement;\n } | null>(null);\n\n // Latest-callback refs so the effect that creates the editor doesn't\n // re-run on every parent render just because props.onChange is a new\n // identity — the editor would otherwise be torn down constantly.\n const cbRef = useRef(props);\n cbRef.current = props;\n\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n const editor = Editor.create({\n container: host,\n project: cbRef.current.defaultProject,\n theme: cbRef.current.theme,\n locale: cbRef.current.locale,\n playbackEngine: cbRef.current.playbackEngine,\n ...(cbRef.current.trackHeight != null\n ? { trackHeight: cbRef.current.trackHeight }\n : {}),\n ...(cbRef.current.rulerHeight != null\n ? { rulerHeight: cbRef.current.rulerHeight }\n : {}),\n ...(cbRef.current.timelineHeight != null\n ? { timelineHeight: cbRef.current.timelineHeight }\n : {}),\n ...(cbRef.current.keyframes != null\n ? { keyframes: cbRef.current.keyframes }\n : {}),\n ...(cbRef.current.clipEdgeNav != null\n ? { clipEdgeNav: cbRef.current.clipEdgeNav }\n : {}),\n });\n editorRef.current = editor;\n setSlots({\n left: editor.toolbarLeft,\n right: editor.toolbarRight,\n headerLeft: editor.headerLeft,\n headerRight: editor.headerRight,\n });\n\n const offs = [\n editor.on(\"change\", ({ project }) => cbRef.current.onChange?.(project)),\n editor.on(\"export\", ({ project }) => cbRef.current.onExport?.(project)),\n editor.on(\"time\", ({ timeMs }) => cbRef.current.onTimeUpdate?.(timeMs)),\n editor.on(\"play\", () => cbRef.current.onPlay?.()),\n editor.on(\"pause\", () => cbRef.current.onPause?.()),\n editor.on(\"selectionChange\", ({ clipId }) =>\n cbRef.current.onSelectionChange?.(clipId),\n ),\n editor.on(\"keyframeSelectionChange\", ({ target }) =>\n cbRef.current.onKeyframeSelectionChange?.(target),\n ),\n editor.on(\"error\", ({ error }) => cbRef.current.onError?.(error)),\n ];\n\n cbRef.current.onReady?.(editor);\n\n return () => {\n for (const off of offs) off();\n editor.destroy();\n editorRef.current = null;\n setSlots(null);\n };\n // Editor lifecycle is tied to mount; we deliberately don't list\n // any reactive deps. `theme` changes are pushed through the\n // separate effect below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (props.theme) editorRef.current?.setTheme(props.theme);\n }, [props.theme]);\n\n useEffect(() => {\n if (props.locale) editorRef.current?.setLocale(props.locale);\n }, [props.locale]);\n\n // Reactive — flipping `keyframes.enabled` instantly toggles diamond\n // visibility on the timeline and routes the canvas / WebCodecs\n // engines through the transform pipeline (or not). Data is\n // preserved either way.\n useEffect(() => {\n const editor = editorRef.current;\n if (!editor) return;\n const desired = props.keyframes?.enabled === true;\n if (editor.isKeyframesEnabled() !== desired) {\n editor.setKeyframesEnabled(desired);\n }\n }, [props.keyframes?.enabled]);\n\n useEffect(() => {\n const editor = editorRef.current;\n if (!editor) return;\n const desired = props.clipEdgeNav?.enabled === true;\n if (editor.isClipEdgeNavEnabled() !== desired) {\n editor.setClipEdgeNavEnabled(desired);\n }\n }, [props.clipEdgeNav?.enabled]);\n\n // Reactive — the underlying CSS custom property can be updated on\n // the container any time; the timeline picks up the new height\n // immediately via CSS. No remount required.\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n if (props.timelineHeight != null && props.timelineHeight > 0) {\n host.style.setProperty(\n \"--aicut-timeline-height\",\n `${Math.round(props.timelineHeight)}px`,\n );\n } else {\n host.style.removeProperty(\"--aicut-timeline-height\");\n }\n }, [props.timelineHeight]);\n\n // Deps must include `slots`. Without it, the factory ran once during\n // the first commit — BEFORE the useEffect above had a chance to\n // create the editor — so `apiRef.current` was permanently locked to\n // null. `slots` flips from null to a real value the same instant\n // the editor is created, so it's the cleanest re-run trigger.\n useImperativeHandle<VideoEditorApi | null, VideoEditorApi | null>(\n props.apiRef,\n () => editorRef.current,\n [slots],\n );\n\n return (\n <div\n ref={hostRef}\n className={props.className}\n style={props.style}\n data-aicut-host=\"\"\n >\n {slots && props.toolbarLeft != null\n ? createPortal(props.toolbarLeft, slots.left)\n : null}\n {slots && props.toolbarRight != null\n ? createPortal(props.toolbarRight, slots.right)\n : null}\n {slots && props.headerLeft != null\n ? createPortal(props.headerLeft, slots.headerLeft)\n : null}\n {slots && props.headerRight != null\n ? createPortal(props.headerRight, slots.headerRight)\n : null}\n </div>\n );\n}\n","import {\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n type Ref,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n Timeline as CoreTimeline,\n type Clip,\n type Locale,\n type Ms,\n type Project,\n type TimelineOptions,\n} from \"@aicut/core\";\n\n/** Imperative handle exposed via `apiRef`. */\nexport interface TimelineApi {\n setProject(p: Project): void;\n getProject(): Project;\n setTime(t: Ms): void;\n getTime(): Ms;\n setScale(pxPerSec: number): void;\n getScale(): number;\n setSelection(id: string | null): void;\n getSelection(): string | null;\n setSnap(snap: boolean): void;\n fitToWindow(): void;\n getDebugInfo(): ReturnType<CoreTimeline[\"getDebugInfo\"]>;\n}\n\nexport interface TimelineProps {\n /** Initial project. Use `apiRef.current.setProject(...)` to swap. */\n defaultProject: Project;\n /** Initial scale (px/sec). Defaults to 80; auto-fits on first render. */\n defaultScale?: number;\n /** Initial playhead position. */\n defaultTime?: Ms;\n /** Initial selection. */\n defaultSelectedClipId?: string | null;\n\n /** Hide the left header column (compact / frame-picker mode). */\n showHeader?: boolean;\n /** Disable all editing interactions. */\n readOnly?: boolean;\n /** Snap to clip edges + playhead when dragging. Default true. */\n snap?: boolean;\n /** Apply fit-to-window on mount once duration is known. Default true. */\n autoFit?: boolean;\n /** UI string overrides (English default). */\n locale?: Partial<Locale>;\n /**\n * Render a 36px top toolbar strip with empty left/right flex slots\n * for host-supplied controls. Default false. Pair with `toolbarLeft`\n * / `toolbarRight` to inject content.\n */\n toolbar?: boolean;\n /** Rendered into the left slot of the timeline toolbar (toolbar must be true). */\n toolbarLeft?: ReactNode;\n /** Rendered into the right slot of the timeline toolbar. */\n toolbarRight?: ReactNode;\n\n className?: string;\n style?: CSSProperties;\n\n apiRef?: Ref<TimelineApi | null>;\n\n onSeek?: (timeMs: Ms) => void;\n onSelectClip?: (clipId: string | null) => void;\n onScaleChange?: (pxPerSec: number) => void;\n onMoveClip?: TimelineOptions[\"onMoveClip\"];\n onResizeClip?: TimelineOptions[\"onResizeClip\"];\n onChange?: (project: Project) => void;\n}\n\n/**\n * Standalone, framework-agnostic canvas Timeline wrapped for React.\n * Mount it without an `Editor` for use cases like a video frame-picker:\n *\n * ```tsx\n * <Timeline\n * defaultProject={{ version: 1, sources: [video], tracks: [{ id, kind: \"video\", clips: [{...}] }] }}\n * showHeader={false}\n * readOnly\n * onSeek={(ms) => setCurrentMs(ms)}\n * />\n * ```\n *\n * Uncontrolled for `project` and `pxPerSec` — the underlying Timeline\n * owns them and reports changes via callbacks. Call methods on\n * `apiRef.current` to drive it imperatively (mirroring ag-Grid /\n * VideoEditor patterns).\n */\nexport function Timeline(props: TimelineProps) {\n const hostRef = useRef<HTMLDivElement | null>(null);\n const tlRef = useRef<CoreTimeline | null>(null);\n const [slots, setSlots] = useState<{\n left: HTMLElement;\n right: HTMLElement;\n } | null>(null);\n\n // Latest-callback ref so the create-once effect doesn't tear the\n // timeline down on every render just because callback identities\n // change.\n const cbRef = useRef(props);\n cbRef.current = props;\n\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n const tl = CoreTimeline.create({\n container: host,\n project: cbRef.current.defaultProject,\n pxPerSec: cbRef.current.defaultScale,\n time: cbRef.current.defaultTime,\n selectedClipId: cbRef.current.defaultSelectedClipId ?? null,\n showHeader: cbRef.current.showHeader,\n readOnly: cbRef.current.readOnly,\n snap: cbRef.current.snap,\n autoFit: cbRef.current.autoFit,\n locale: cbRef.current.locale,\n toolbar: cbRef.current.toolbar,\n onSeek: (t) => cbRef.current.onSeek?.(t),\n onSelectClip: (id) => cbRef.current.onSelectClip?.(id),\n onScaleChange: (s) => cbRef.current.onScaleChange?.(s),\n onMoveClip: (id, opts) => cbRef.current.onMoveClip?.(id, opts),\n onResizeClip: (id, e) => cbRef.current.onResizeClip?.(id, e),\n onChange: (p) => cbRef.current.onChange?.(p),\n });\n tlRef.current = tl;\n if (tl.toolbarLeft && tl.toolbarRight) {\n setSlots({ left: tl.toolbarLeft, right: tl.toolbarRight });\n }\n return () => {\n tl.destroy();\n tlRef.current = null;\n setSlots(null);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (props.locale) tlRef.current?.setLocale(props.locale);\n }, [props.locale]);\n\n useImperativeHandle<TimelineApi | null, TimelineApi | null>(\n props.apiRef,\n () => {\n const tl = tlRef.current;\n if (!tl) return null;\n return {\n setProject: (p) => tl.setProject(p),\n getProject: () => tl.getProject(),\n setTime: (t) => tl.setTime(t),\n getTime: () => tl.getTime(),\n setScale: (s) => tl.setScale(s),\n getScale: () => tl.getScale(),\n setSelection: (id) => tl.setSelection(id),\n getSelection: () => tl.getSelection(),\n setSnap: (s) => tl.setSnap(s),\n fitToWindow: () => tl.fitToWindow(),\n getDebugInfo: () => tl.getDebugInfo(),\n };\n },\n // Same caveat as VideoEditor.tsx — factory must re-run once the\n // timeline is created in useEffect, otherwise apiRef.current is\n // null forever. `slots` flips from null to a real value the\n // instant the timeline is ready, so it's the cleanest trigger.\n [slots],\n );\n\n return (\n <div\n ref={hostRef}\n className={props.className}\n style={{ width: \"100%\", height: 240, ...props.style }}\n data-aicut-timeline-host=\"\"\n >\n {slots && props.toolbarLeft != null\n ? createPortal(props.toolbarLeft, slots.left)\n : null}\n {slots && props.toolbarRight != null\n ? createPortal(props.toolbarRight, slots.right)\n : null}\n </div>\n );\n\n // Type-only re-export used to keep React/Vue prop typings in lockstep\n // with the core. Reference here so the symbol isn't tree-shaken.\n void ({} as Clip);\n}\n","export { VideoEditor } from \"./VideoEditor.js\";\nexport type { VideoEditorProps, VideoEditorApi } from \"./VideoEditor.js\";\nexport { Timeline } from \"./Timeline.js\";\nexport type { TimelineProps, TimelineApi } from \"./Timeline.js\";\nexport type {\n Project,\n MediaSource,\n Track,\n Clip,\n Keyframe,\n Ms,\n Theme,\n EditorApi,\n Locale,\n PlaybackEngine,\n PlaybackEngineFactory,\n PlaybackEngineOptions,\n CanvasCompositorEngineOptions,\n EffectiveTransform,\n} from \"@aicut/core\";\nexport {\n createEmptyProject,\n createId,\n localeEn,\n localeZh,\n HtmlVideoEngine,\n htmlVideoEngineFactory,\n CanvasCompositorEngine,\n canvasCompositorEngineFactory,\n // Live bindings — re-reading them after `setTimelineMetrics` (which\n // EditorOptions.trackHeight / .rulerHeight calls under the hood)\n // returns the updated values.\n TRACK_HEIGHT,\n RULER_HEIGHT,\n HEADER_WIDTH,\n setTimelineMetrics,\n // Pure-math keyframe helpers — hosts can read effective transforms\n // for previews / thumbnails without touching the playback engine.\n IDENTITY_TRANSFORM,\n isIdentityTransform,\n getEffectiveTransform,\n getTransformAtTimelineTime,\n} from \"@aicut/core\";\n"],"mappings":"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","names":["useEffect","useImperativeHandle","useRef","useState","createPortal","jsxs"]}
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/webcodecs.ts
21
+ var webcodecs_exports = {};
22
+ __export(webcodecs_exports, {
23
+ WebCodecsEngine: () => import_webcodecs.WebCodecsEngine,
24
+ isWebCodecsSupported: () => import_webcodecs.isWebCodecsSupported,
25
+ webCodecsEngineFactory: () => import_webcodecs.webCodecsEngineFactory
26
+ });
27
+ module.exports = __toCommonJS(webcodecs_exports);
28
+ var import_webcodecs = require("@aicut/core/webcodecs");
29
+ // Annotate the CommonJS export names for ESM import in node:
30
+ 0 && (module.exports = {
31
+ WebCodecsEngine,
32
+ isWebCodecsSupported,
33
+ webCodecsEngineFactory
34
+ });
35
+ //# sourceMappingURL=webcodecs.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/webcodecs.ts"],"sourcesContent":["/**\n * @aicut/react/webcodecs — separate entry that pulls mp4box.js for\n * frame-accurate WebCodecs playback. Users who never import this\n * path don't pay the mp4box bundle cost.\n *\n * Pass the factory directly to `<VideoEditor playbackEngine={…} />`,\n * or wrap it in a closure to flip `debug: true`.\n */\nexport {\n WebCodecsEngine,\n webCodecsEngineFactory,\n isWebCodecsSupported,\n type WebCodecsEngineOptions,\n type DemuxedTrack,\n} from \"@aicut/core/webcodecs\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,uBAMO;","names":[]}
@@ -0,0 +1 @@
1
+ export { DemuxedTrack, WebCodecsEngine, WebCodecsEngineOptions, isWebCodecsSupported, webCodecsEngineFactory } from '@aicut/core/webcodecs';
@@ -0,0 +1 @@
1
+ export { DemuxedTrack, WebCodecsEngine, WebCodecsEngineOptions, isWebCodecsSupported, webCodecsEngineFactory } from '@aicut/core/webcodecs';
@@ -0,0 +1,12 @@
1
+ // src/webcodecs.ts
2
+ import {
3
+ WebCodecsEngine,
4
+ webCodecsEngineFactory,
5
+ isWebCodecsSupported
6
+ } from "@aicut/core/webcodecs";
7
+ export {
8
+ WebCodecsEngine,
9
+ isWebCodecsSupported,
10
+ webCodecsEngineFactory
11
+ };
12
+ //# sourceMappingURL=webcodecs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/webcodecs.ts"],"sourcesContent":["/**\n * @aicut/react/webcodecs — separate entry that pulls mp4box.js for\n * frame-accurate WebCodecs playback. Users who never import this\n * path don't pay the mp4box bundle cost.\n *\n * Pass the factory directly to `<VideoEditor playbackEngine={…} />`,\n * or wrap it in a closure to flip `debug: true`.\n */\nexport {\n WebCodecsEngine,\n webCodecsEngineFactory,\n isWebCodecsSupported,\n type WebCodecsEngineOptions,\n type DemuxedTrack,\n} from \"@aicut/core/webcodecs\";\n"],"mappings":";AAQA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicut/react",
3
- "version": "0.4.3",
3
+ "version": "0.6.0",
4
4
  "description": "React wrapper for the AiCut video editor + lighting picker — thin declarative shells over @aicut/core.",
5
5
  "license": "MIT",
6
6
  "author": "ziqiang <ziqiangytu@gmail.com>",
@@ -52,6 +52,11 @@
52
52
  "types": "./dist/lighting.d.ts",
53
53
  "import": "./dist/lighting.js",
54
54
  "require": "./dist/lighting.cjs"
55
+ },
56
+ "./webcodecs": {
57
+ "types": "./dist/webcodecs.d.ts",
58
+ "import": "./dist/webcodecs.js",
59
+ "require": "./dist/webcodecs.cjs"
55
60
  }
56
61
  },
57
62
  "files": [
@@ -59,7 +64,7 @@
59
64
  "README.md"
60
65
  ],
61
66
  "dependencies": {
62
- "@aicut/core": "0.4.3"
67
+ "@aicut/core": "0.6.0"
63
68
  },
64
69
  "peerDependencies": {
65
70
  "react": "^18.0.0 || ^19.0.0",