@aicut/react 0.4.0 → 0.4.2

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
@@ -174,6 +174,12 @@ function Relight() {
174
174
  apiRef={apiRef}
175
175
  subjectImageUrl="/frames/subject.jpg"
176
176
  onChange={(cfg: LightingConfig) => console.log(cfg)}
177
+ // Reset / Generate / save-preset / etc. buttons go into the
178
+ // controls column's footer slot — the only host-supplied
179
+ // surface the library reserves space for.
180
+ controlsFooter={
181
+ <button onClick={() => apiRef.current?.reset()}>Reset</button>
182
+ }
177
183
  />
178
184
  <aside>
179
185
  <textarea placeholder="Describe the mood…" />
@@ -184,7 +190,11 @@ function Relight() {
184
190
  }
185
191
  ```
186
192
 
187
- Props: `subjectImageUrl`, `defaultConfig`, `defaultView`, `theme`, `locale`, `onChange`. The library is intentionally focused on the picker — Smart mode UI, Generate buttons, close handling, layout all live in host code.
193
+ Props: `subjectImageUrl`, `defaultConfig`, `defaultView`, `theme`, `locale`, `controlsFooter`, `onChange`.
194
+
195
+ Imperative API (`apiRef.current`): `setConfig`, `getConfig`, `setSubjectImage`, `setView`, `getView`, `reset`.
196
+
197
+ The library is intentionally focused on the picker — Smart mode UI, Generate buttons, close handling, layout all live in host code.
188
198
 
189
199
  ## Standalone `<Timeline>`
190
200
 
package/dist/index.cjs CHANGED
@@ -50,7 +50,12 @@ function VideoEditor(props) {
50
50
  locale: cbRef.current.locale
51
51
  });
52
52
  editorRef.current = editor;
53
- setSlots({ left: editor.toolbarLeft, right: editor.toolbarRight });
53
+ setSlots({
54
+ left: editor.toolbarLeft,
55
+ right: editor.toolbarRight,
56
+ headerLeft: editor.headerLeft,
57
+ headerRight: editor.headerRight
58
+ });
54
59
  const offs = [
55
60
  editor.on("change", ({ project }) => cbRef.current.onChange?.(project)),
56
61
  editor.on("export", ({ project }) => cbRef.current.onExport?.(project)),
@@ -91,7 +96,9 @@ function VideoEditor(props) {
91
96
  "data-aicut-host": "",
92
97
  children: [
93
98
  slots && props.toolbarLeft != null ? (0, import_react_dom.createPortal)(props.toolbarLeft, slots.left) : null,
94
- slots && props.toolbarRight != null ? (0, import_react_dom.createPortal)(props.toolbarRight, slots.right) : null
99
+ slots && props.toolbarRight != null ? (0, import_react_dom.createPortal)(props.toolbarRight, slots.right) : null,
100
+ slots && props.headerLeft != null ? (0, import_react_dom.createPortal)(props.headerLeft, slots.headerLeft) : null,
101
+ slots && props.headerRight != null ? (0, import_react_dom.createPortal)(props.headerRight, slots.headerRight) : null
95
102
  ]
96
103
  }
97
104
  );
@@ -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\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 } | 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({ left: editor.toolbarLeft, right: editor.toolbarRight });\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 </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 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"]}
package/dist/index.d.cts CHANGED
@@ -40,6 +40,15 @@ interface VideoEditorProps {
40
40
  toolbarLeft?: ReactNode;
41
41
  /** Same as `toolbarLeft` but at the very right of the toolbar. */
42
42
  toolbarRight?: ReactNode;
43
+ /**
44
+ * Rendered into the LEFT side of an optional header bar above the
45
+ * preview (project name, file menu, breadcrumbs). The header
46
+ * collapses entirely when both header slots are empty, so the
47
+ * default layout is identical to before this slot existed.
48
+ */
49
+ headerLeft?: ReactNode;
50
+ /** Right side of the editor header — conventionally Share / Export / profile. */
51
+ headerRight?: ReactNode;
43
52
  }
44
53
  /**
45
54
  * Declarative React shell over `@aicut/core` `Editor`. Mounts the
package/dist/index.d.ts CHANGED
@@ -40,6 +40,15 @@ interface VideoEditorProps {
40
40
  toolbarLeft?: ReactNode;
41
41
  /** Same as `toolbarLeft` but at the very right of the toolbar. */
42
42
  toolbarRight?: ReactNode;
43
+ /**
44
+ * Rendered into the LEFT side of an optional header bar above the
45
+ * preview (project name, file menu, breadcrumbs). The header
46
+ * collapses entirely when both header slots are empty, so the
47
+ * default layout is identical to before this slot existed.
48
+ */
49
+ headerLeft?: ReactNode;
50
+ /** Right side of the editor header — conventionally Share / Export / profile. */
51
+ headerRight?: ReactNode;
43
52
  }
44
53
  /**
45
54
  * Declarative React shell over `@aicut/core` `Editor`. Mounts the
package/dist/index.js CHANGED
@@ -26,7 +26,12 @@ function VideoEditor(props) {
26
26
  locale: cbRef.current.locale
27
27
  });
28
28
  editorRef.current = editor;
29
- setSlots({ left: editor.toolbarLeft, right: editor.toolbarRight });
29
+ setSlots({
30
+ left: editor.toolbarLeft,
31
+ right: editor.toolbarRight,
32
+ headerLeft: editor.headerLeft,
33
+ headerRight: editor.headerRight
34
+ });
30
35
  const offs = [
31
36
  editor.on("change", ({ project }) => cbRef.current.onChange?.(project)),
32
37
  editor.on("export", ({ project }) => cbRef.current.onExport?.(project)),
@@ -67,7 +72,9 @@ function VideoEditor(props) {
67
72
  "data-aicut-host": "",
68
73
  children: [
69
74
  slots && props.toolbarLeft != null ? createPortal(props.toolbarLeft, slots.left) : null,
70
- slots && props.toolbarRight != null ? createPortal(props.toolbarRight, slots.right) : null
75
+ slots && props.toolbarRight != null ? createPortal(props.toolbarRight, slots.right) : null,
76
+ slots && props.headerLeft != null ? createPortal(props.headerLeft, slots.headerLeft) : null,
77
+ slots && props.headerRight != null ? createPortal(props.headerRight, slots.headerRight) : null
71
78
  ]
72
79
  }
73
80
  );
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\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 } | 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({ left: editor.toolbarLeft, right: editor.toolbarRight });\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 </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 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicut/react",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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>",
@@ -59,7 +59,7 @@
59
59
  "README.md"
60
60
  ],
61
61
  "dependencies": {
62
- "@aicut/core": "0.4.0"
62
+ "@aicut/core": "0.4.2"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "react": "^18.0.0 || ^19.0.0",