egaki 0.5.0 → 0.7.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/CHANGELOG.md +188 -0
- package/dist/cli/cache-utils.d.ts +33 -0
- package/dist/cli/cache-utils.d.ts.map +1 -0
- package/dist/cli/cache-utils.js +157 -0
- package/dist/cli/cached-generate.d.ts +105 -0
- package/dist/cli/cached-generate.d.ts.map +1 -0
- package/dist/cli/cached-generate.js +208 -0
- package/dist/cli/cached-generate.test.d.ts +11 -0
- package/dist/cli/cached-generate.test.d.ts.map +1 -0
- package/dist/cli/cached-generate.test.js +431 -0
- package/dist/cli/cartesia-provider.d.ts +16 -0
- package/dist/cli/cartesia-provider.d.ts.map +1 -0
- package/dist/cli/cartesia-provider.js +184 -0
- package/dist/cli/chatgpt-auth.d.ts.map +1 -0
- package/dist/cli/cli.d.ts +4 -0
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/cli/cli.js +1329 -0
- package/dist/cli/clone-generate.d.ts +26 -0
- package/dist/cli/clone-generate.d.ts.map +1 -0
- package/dist/cli/clone-generate.js +148 -0
- package/dist/{credentials.d.ts → cli/credentials.d.ts} +11 -2
- package/dist/cli/credentials.d.ts.map +1 -0
- package/dist/{credentials.js → cli/credentials.js} +88 -5
- package/dist/cli/elevenlabs-speech-provider.d.ts +26 -0
- package/dist/cli/elevenlabs-speech-provider.d.ts.map +1 -0
- package/dist/cli/elevenlabs-speech-provider.js +199 -0
- package/dist/cli/generate.d.ts +140 -0
- package/dist/cli/generate.d.ts.map +1 -0
- package/dist/cli/generate.js +604 -0
- package/dist/cli/generate.test.d.ts +2 -0
- package/dist/cli/generate.test.d.ts.map +1 -0
- package/dist/cli/generate.test.js +131 -0
- package/dist/cli/login.d.ts.map +1 -0
- package/dist/{login.js → cli/login.js} +45 -4
- package/dist/cli/main.d.ts +3 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +7 -0
- package/dist/cli/model-catalog.d.ts +141 -0
- package/dist/cli/model-catalog.d.ts.map +1 -0
- package/dist/cli/model-catalog.js +2135 -0
- package/dist/{models.d.ts → cli/models.d.ts} +16 -9
- package/dist/cli/models.d.ts.map +1 -0
- package/dist/cli/models.js +313 -0
- package/dist/cli/open-browser.d.ts.map +1 -0
- package/dist/cli/openai-speech-provider.d.ts +3 -0
- package/dist/cli/openai-speech-provider.d.ts.map +1 -0
- package/dist/cli/openai-speech-provider.js +28 -0
- package/dist/cli/separation-generate.d.ts +34 -0
- package/dist/cli/separation-generate.d.ts.map +1 -0
- package/dist/cli/separation-generate.js +117 -0
- package/dist/cli/speech-catalog.d.ts +35 -0
- package/dist/cli/speech-catalog.d.ts.map +1 -0
- package/dist/cli/speech-catalog.js +195 -0
- package/dist/cli/speech-generate.d.ts +86 -0
- package/dist/cli/speech-generate.d.ts.map +1 -0
- package/dist/cli/speech-generate.js +156 -0
- package/dist/cli/speech-models.d.ts +8 -0
- package/dist/cli/speech-models.d.ts.map +1 -0
- package/dist/cli/speech-models.js +109 -0
- package/dist/cli/speech-provider.test.d.ts +2 -0
- package/dist/cli/speech-provider.test.d.ts.map +1 -0
- package/dist/cli/speech-provider.test.js +197 -0
- package/dist/cli/subscription.d.ts.map +1 -0
- package/dist/cli/transcription-catalog.d.ts +35 -0
- package/dist/cli/transcription-catalog.d.ts.map +1 -0
- package/dist/cli/transcription-catalog.js +192 -0
- package/dist/cli/transcription-generate.d.ts +62 -0
- package/dist/cli/transcription-generate.d.ts.map +1 -0
- package/dist/cli/transcription-generate.js +144 -0
- package/dist/cli/transcription-models.d.ts +9 -0
- package/dist/cli/transcription-models.d.ts.map +1 -0
- package/dist/cli/transcription-models.js +100 -0
- package/dist/cli/transcription-providers.d.ts +22 -0
- package/dist/cli/transcription-providers.d.ts.map +1 -0
- package/dist/cli/transcription-providers.js +290 -0
- package/dist/cli/video-model-catalog.d.ts +2 -0
- package/dist/cli/video-model-catalog.d.ts.map +1 -0
- package/dist/cli/video-model-catalog.js +5 -0
- package/dist/cli/xai-auth.d.ts +29 -0
- package/dist/cli/xai-auth.d.ts.map +1 -0
- package/dist/cli/xai-auth.js +325 -0
- package/dist/vite/angled-screen.d.ts +57 -0
- package/dist/vite/angled-screen.d.ts.map +1 -0
- package/dist/vite/angled-screen.js +115 -0
- package/dist/vite/app.d.ts +51 -0
- package/dist/vite/app.d.ts.map +1 -0
- package/dist/vite/app.js +150 -0
- package/dist/vite/bands-shader.d.ts +47 -0
- package/dist/vite/bands-shader.d.ts.map +1 -0
- package/dist/vite/bands-shader.js +267 -0
- package/dist/vite/code-block.d.ts +107 -0
- package/dist/vite/code-block.d.ts.map +1 -0
- package/dist/vite/code-block.js +641 -0
- package/dist/vite/components.d.ts +36 -0
- package/dist/vite/components.d.ts.map +1 -0
- package/dist/vite/components.js +113 -0
- package/dist/vite/dispersion-rings-shader.d.ts +44 -0
- package/dist/vite/dispersion-rings-shader.d.ts.map +1 -0
- package/dist/vite/dispersion-rings-shader.js +282 -0
- package/dist/vite/easing-curves.d.ts +129 -0
- package/dist/vite/easing-curves.d.ts.map +1 -0
- package/dist/vite/easing-curves.js +584 -0
- package/dist/vite/easing-curves.test.d.ts +2 -0
- package/dist/vite/easing-curves.test.d.ts.map +1 -0
- package/dist/vite/easing-curves.test.js +200 -0
- package/dist/vite/generated-media-client.d.ts +35 -0
- package/dist/vite/generated-media-client.d.ts.map +1 -0
- package/dist/vite/generated-media-client.js +76 -0
- package/dist/vite/keyframes.d.ts +129 -0
- package/dist/vite/keyframes.d.ts.map +1 -0
- package/dist/vite/keyframes.js +194 -0
- package/dist/vite/layout-editor.d.ts +45 -0
- package/dist/vite/layout-editor.d.ts.map +1 -0
- package/dist/vite/layout-editor.js +587 -0
- package/dist/vite/layout-transition.d.ts +136 -0
- package/dist/vite/layout-transition.d.ts.map +1 -0
- package/dist/vite/layout-transition.js +581 -0
- package/dist/vite/liquid-gradient-shader.d.ts +62 -0
- package/dist/vite/liquid-gradient-shader.d.ts.map +1 -0
- package/dist/vite/liquid-gradient-shader.js +413 -0
- package/dist/vite/mdx-client.d.ts +15 -0
- package/dist/vite/mdx-client.d.ts.map +1 -0
- package/dist/vite/mdx-client.js +265 -0
- package/dist/vite/mdx-parse.d.ts +102 -0
- package/dist/vite/mdx-parse.d.ts.map +1 -0
- package/dist/vite/mdx-parse.js +251 -0
- package/dist/vite/mdx-provided-components.d.ts +18 -0
- package/dist/vite/mdx-provided-components.d.ts.map +1 -0
- package/dist/vite/mdx-provided-components.js +14 -0
- package/dist/vite/mdx-video.d.ts +327 -0
- package/dist/vite/mdx-video.d.ts.map +1 -0
- package/dist/vite/mdx-video.js +596 -0
- package/dist/vite/mdx-video.test.d.ts +9 -0
- package/dist/vite/mdx-video.test.d.ts.map +1 -0
- package/dist/vite/mdx-video.test.js +1945 -0
- package/dist/vite/media-components.d.ts +45 -0
- package/dist/vite/media-components.d.ts.map +1 -0
- package/dist/vite/media-components.js +355 -0
- package/dist/vite/media-duration-store.d.ts +66 -0
- package/dist/vite/media-duration-store.d.ts.map +1 -0
- package/dist/vite/media-duration-store.js +204 -0
- package/dist/vite/player-page.d.ts +35 -0
- package/dist/vite/player-page.d.ts.map +1 -0
- package/dist/vite/player-page.js +720 -0
- package/dist/vite/render-client.d.ts +18 -0
- package/dist/vite/render-client.d.ts.map +1 -0
- package/dist/vite/render-client.js +67 -0
- package/dist/vite/sdk.d.ts +221 -0
- package/dist/vite/sdk.d.ts.map +1 -0
- package/dist/vite/sdk.js +355 -0
- package/dist/vite/server-components.d.ts +55 -0
- package/dist/vite/server-components.d.ts.map +1 -0
- package/dist/vite/server-components.js +156 -0
- package/dist/vite/server-mdx.d.ts +74 -0
- package/dist/vite/server-mdx.d.ts.map +1 -0
- package/dist/vite/server-mdx.js +259 -0
- package/dist/vite/shader-renderer.d.ts +48 -0
- package/dist/vite/shader-renderer.d.ts.map +1 -0
- package/dist/vite/shader-renderer.js +411 -0
- package/dist/vite/store-hooks.d.ts +12 -0
- package/dist/vite/store-hooks.d.ts.map +1 -0
- package/dist/vite/store-hooks.js +93 -0
- package/dist/vite/store.d.ts +75 -0
- package/dist/vite/store.d.ts.map +1 -0
- package/dist/vite/store.js +52 -0
- package/dist/vite/store.test.d.ts +6 -0
- package/dist/vite/store.test.d.ts.map +1 -0
- package/dist/vite/store.test.js +172 -0
- package/dist/vite/tweakpane-hook.d.ts +94 -0
- package/dist/vite/tweakpane-hook.d.ts.map +1 -0
- package/dist/vite/tweakpane-hook.js +450 -0
- package/dist/vite/vite-plugin.d.ts +28 -0
- package/dist/vite/vite-plugin.d.ts.map +1 -0
- package/dist/vite/vite-plugin.js +538 -0
- package/dist/vite/wave-gradient-shader.d.ts +41 -0
- package/dist/vite/wave-gradient-shader.d.ts.map +1 -0
- package/dist/vite/wave-gradient-shader.js +207 -0
- package/package.json +103 -25
- package/skills/egaki/SKILL.md +162 -1
- package/src/cli/cache-utils.ts +169 -0
- package/src/cli/cached-generate.test.ts +531 -0
- package/src/cli/cached-generate.ts +348 -0
- package/src/cli/cartesia-provider.ts +249 -0
- package/src/cli/cli.ts +1818 -0
- package/src/cli/clone-generate.ts +202 -0
- package/src/{credentials.ts → cli/credentials.ts} +90 -7
- package/src/cli/elevenlabs-speech-provider.ts +243 -0
- package/src/cli/generate.test.ts +166 -0
- package/src/cli/generate.ts +912 -0
- package/src/{login.ts → cli/login.ts} +48 -3
- package/src/cli/main.ts +8 -0
- package/src/cli/model-catalog.ts +2336 -0
- package/src/cli/models.ts +408 -0
- package/src/cli/openai-speech-provider.ts +34 -0
- package/src/cli/separation-generate.ts +187 -0
- package/src/cli/speech-catalog.ts +253 -0
- package/src/cli/speech-generate.ts +275 -0
- package/src/cli/speech-models.ts +146 -0
- package/src/cli/speech-provider.test.ts +226 -0
- package/src/cli/transcription-catalog.ts +250 -0
- package/src/cli/transcription-generate.ts +214 -0
- package/src/cli/transcription-models.ts +136 -0
- package/src/cli/transcription-providers.ts +427 -0
- package/src/cli/video-model-catalog.ts +14 -0
- package/src/cli/xai-auth.ts +396 -0
- package/src/vite/angled-screen.tsx +214 -0
- package/src/vite/app.tsx +197 -0
- package/src/vite/bands-shader.tsx +319 -0
- package/src/vite/code-block.tsx +939 -0
- package/src/vite/components.tsx +278 -0
- package/src/vite/dispersion-rings-shader.tsx +331 -0
- package/src/vite/easing-curves.test.ts +232 -0
- package/src/vite/easing-curves.ts +743 -0
- package/src/vite/generated-media-client.tsx +121 -0
- package/src/vite/keyframes.tsx +347 -0
- package/src/vite/layout-editor.tsx +709 -0
- package/src/vite/layout-transition.tsx +706 -0
- package/src/vite/liquid-gradient-shader.tsx +474 -0
- package/src/vite/mdx-client.tsx +420 -0
- package/src/vite/mdx-parse.ts +343 -0
- package/src/vite/mdx-provided-components.ts +19 -0
- package/src/vite/mdx-video.test.tsx +2219 -0
- package/src/vite/mdx-video.tsx +976 -0
- package/src/vite/media-components.tsx +473 -0
- package/src/vite/media-duration-store.ts +225 -0
- package/src/vite/motion-timing.ts +194 -0
- package/src/vite/player-page.tsx +1107 -0
- package/src/vite/render-client.ts +84 -0
- package/src/vite/sdk.ts +598 -0
- package/src/vite/server-components.tsx +241 -0
- package/src/vite/server-mdx.ts +285 -0
- package/src/vite/shader-renderer.tsx +535 -0
- package/src/vite/store-hooks.ts +117 -0
- package/src/vite/store.test.ts +202 -0
- package/src/vite/store.ts +115 -0
- package/src/vite/styles.css +21 -0
- package/src/vite/tweakpane-hook.tsx +593 -0
- package/src/vite/virtual-modules.d.ts +30 -0
- package/src/vite/vite-plugin.ts +584 -0
- package/src/vite/wave-gradient-shader.tsx +253 -0
- package/README.md +0 -266
- package/dist/antigravity-auth.d.ts +0 -15
- package/dist/antigravity-auth.d.ts.map +0 -1
- package/dist/antigravity-auth.js +0 -544
- package/dist/antigravity-image.d.ts +0 -11
- package/dist/antigravity-image.d.ts.map +0 -1
- package/dist/antigravity-image.js +0 -285
- package/dist/chatgpt-auth.d.ts.map +0 -1
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -968
- package/dist/credentials.d.ts.map +0 -1
- package/dist/gateway-model-candidates.generated.d.ts +0 -11
- package/dist/gateway-model-candidates.generated.d.ts.map +0 -1
- package/dist/gateway-model-candidates.generated.js +0 -64
- package/dist/login.d.ts.map +0 -1
- package/dist/model-catalog.d.ts +0 -42
- package/dist/model-catalog.d.ts.map +0 -1
- package/dist/model-catalog.js +0 -1001
- package/dist/models.d.ts.map +0 -1
- package/dist/models.js +0 -246
- package/dist/open-browser.d.ts.map +0 -1
- package/dist/subscription.d.ts.map +0 -1
- package/dist/video-model-catalog.d.ts +0 -52
- package/dist/video-model-catalog.d.ts.map +0 -1
- package/dist/video-model-catalog.js +0 -306
- package/src/cli.ts +0 -1378
- package/src/model-catalog.ts +0 -1077
- package/src/models.ts +0 -309
- package/src/video-model-catalog.ts +0 -363
- /package/dist/{chatgpt-auth.d.ts → cli/chatgpt-auth.d.ts} +0 -0
- /package/dist/{chatgpt-auth.js → cli/chatgpt-auth.js} +0 -0
- /package/dist/{login.d.ts → cli/login.d.ts} +0 -0
- /package/dist/{open-browser.d.ts → cli/open-browser.d.ts} +0 -0
- /package/dist/{open-browser.js → cli/open-browser.js} +0 -0
- /package/dist/{subscription.d.ts → cli/subscription.d.ts} +0 -0
- /package/dist/{subscription.js → cli/subscription.js} +0 -0
- /package/src/{chatgpt-auth.ts → cli/chatgpt-auth.ts} +0 -0
- /package/src/{open-browser.ts → cli/open-browser.ts} +0 -0
- /package/src/{subscription.ts → cli/subscription.ts} +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout editor for the video player.
|
|
3
|
+
*
|
|
4
|
+
* Lets users visually drag, scale, and edit text on elements inside the
|
|
5
|
+
* Remotion Player. All spatial changes are applied via CSS transform
|
|
6
|
+
* (translate + scale). Double-click text to edit inline.
|
|
7
|
+
*
|
|
8
|
+
* Changes are tracked per-element using safe-mdx's `data-markdown-line`
|
|
9
|
+
* attribute, then serialized as structured text for an agent to apply.
|
|
10
|
+
*
|
|
11
|
+
* Keyboard: Delete/Backspace = delete selected element, Esc = select parent
|
|
12
|
+
* (deselect at root), Ctrl+Z = undo.
|
|
13
|
+
*
|
|
14
|
+
* Bug-fix notes (from oracle review, commit 1158b916):
|
|
15
|
+
* - Undo stores a snapshot of the previous ElementChange so restoring
|
|
16
|
+
* doesn't delete the change entirely (was: changesRef.current.delete).
|
|
17
|
+
* - attachMoveable uses a generation counter ref to guard against stale
|
|
18
|
+
* async import resolutions (race between disable/new-select and import).
|
|
19
|
+
* - Text edit keydown uses a named handler removed in the blur cleanup.
|
|
20
|
+
* - finishTextEdit() is extracted and called from the !editing cleanup
|
|
21
|
+
* effect so toggling editing off mid-text-edit cleans up properly.
|
|
22
|
+
* - Moveable instance is destroyed on unmount via a dedicated useEffect.
|
|
23
|
+
* - Clipboard "Copied!" timeout is tracked in a ref and cleared on unmount.
|
|
24
|
+
* - Moveable constructor cast cleaned up (kept hand-written interfaces
|
|
25
|
+
* because react-moveable types are not installed as a dependency).
|
|
26
|
+
*/
|
|
27
|
+
export interface SectionMeta {
|
|
28
|
+
heading: string | null;
|
|
29
|
+
durationInFrames: number;
|
|
30
|
+
}
|
|
31
|
+
export declare function LayoutEditor({ playerContainerRef, playerRef, editing, onEditingChange, onReset, sections, fps, entryPath }: {
|
|
32
|
+
playerContainerRef: React.RefObject<HTMLElement | null>;
|
|
33
|
+
playerRef: React.RefObject<{
|
|
34
|
+
getCurrentFrame: () => number;
|
|
35
|
+
addEventListener: (name: any, cb: any) => void;
|
|
36
|
+
removeEventListener: (name: any, cb: any) => void;
|
|
37
|
+
} | null>;
|
|
38
|
+
editing: boolean;
|
|
39
|
+
onEditingChange: (editing: boolean) => void;
|
|
40
|
+
onReset: () => void;
|
|
41
|
+
sections: SectionMeta[];
|
|
42
|
+
fps: number;
|
|
43
|
+
entryPath: string;
|
|
44
|
+
}): import("react").JSX.Element;
|
|
45
|
+
//# sourceMappingURL=layout-editor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout-editor.d.ts","sourceRoot":"","sources":["../../src/vite/layout-editor.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AA0DH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,gBAAgB,EAAE,MAAM,CAAA;CACzB;AAyJD,wBAAgB,YAAY,CAAC,EAAE,kBAAkB,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE;IAC3H,kBAAkB,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IACvD,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC;QAAE,eAAe,EAAE,MAAM,MAAM,CAAC;QAAC,gBAAgB,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,KAAK,IAAI,CAAC;QAAC,mBAAmB,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,KAAK,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IACvK,OAAO,EAAE,OAAO,CAAA;IAChB,eAAe,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IAC3C,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,QAAQ,EAAE,WAAW,EAAE,CAAA;IACvB,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;CAClB,+BA4cA"}
|
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Layout editor for the video player.
|
|
4
|
+
*
|
|
5
|
+
* Lets users visually drag, scale, and edit text on elements inside the
|
|
6
|
+
* Remotion Player. All spatial changes are applied via CSS transform
|
|
7
|
+
* (translate + scale). Double-click text to edit inline.
|
|
8
|
+
*
|
|
9
|
+
* Changes are tracked per-element using safe-mdx's `data-markdown-line`
|
|
10
|
+
* attribute, then serialized as structured text for an agent to apply.
|
|
11
|
+
*
|
|
12
|
+
* Keyboard: Delete/Backspace = delete selected element, Esc = select parent
|
|
13
|
+
* (deselect at root), Ctrl+Z = undo.
|
|
14
|
+
*
|
|
15
|
+
* Bug-fix notes (from oracle review, commit 1158b916):
|
|
16
|
+
* - Undo stores a snapshot of the previous ElementChange so restoring
|
|
17
|
+
* doesn't delete the change entirely (was: changesRef.current.delete).
|
|
18
|
+
* - attachMoveable uses a generation counter ref to guard against stale
|
|
19
|
+
* async import resolutions (race between disable/new-select and import).
|
|
20
|
+
* - Text edit keydown uses a named handler removed in the blur cleanup.
|
|
21
|
+
* - finishTextEdit() is extracted and called from the !editing cleanup
|
|
22
|
+
* effect so toggling editing off mid-text-edit cleans up properly.
|
|
23
|
+
* - Moveable instance is destroyed on unmount via a dedicated useEffect.
|
|
24
|
+
* - Clipboard "Copied!" timeout is tracked in a ref and cleared on unmount.
|
|
25
|
+
* - Moveable constructor cast cleaned up (kept hand-written interfaces
|
|
26
|
+
* because react-moveable types are not installed as a dependency).
|
|
27
|
+
*/
|
|
28
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
29
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
30
|
+
function isTextElement(el) {
|
|
31
|
+
return el.children.length === 0 && (el.textContent?.trim().length ?? 0) > 0;
|
|
32
|
+
}
|
|
33
|
+
function findMdxLine(el, boundary) {
|
|
34
|
+
let cur = el;
|
|
35
|
+
while (cur && boundary.contains(cur)) {
|
|
36
|
+
const v = cur.getAttribute('data-markdown-line');
|
|
37
|
+
if (v)
|
|
38
|
+
return parseInt(v, 10);
|
|
39
|
+
cur = cur.parentElement;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function classifyElement(el) {
|
|
44
|
+
const tag = el.tagName.toLowerCase();
|
|
45
|
+
if (tag === 'img' || tag === 'picture')
|
|
46
|
+
return 'image';
|
|
47
|
+
if (tag === 'video')
|
|
48
|
+
return 'video';
|
|
49
|
+
if (tag === 'audio')
|
|
50
|
+
return 'audio';
|
|
51
|
+
if (tag === 'svg')
|
|
52
|
+
return 'svg';
|
|
53
|
+
if (tag === 'canvas')
|
|
54
|
+
return 'canvas';
|
|
55
|
+
if (tag === 'pre' || tag === 'code')
|
|
56
|
+
return 'code';
|
|
57
|
+
if (el.querySelector('img, picture'))
|
|
58
|
+
return 'image';
|
|
59
|
+
if (el.querySelector('video'))
|
|
60
|
+
return 'video';
|
|
61
|
+
if (el.querySelector('canvas'))
|
|
62
|
+
return 'canvas';
|
|
63
|
+
if (el.querySelector('pre, code'))
|
|
64
|
+
return 'code';
|
|
65
|
+
// Text: has meaningful text content and no non-text media children
|
|
66
|
+
if ((el.textContent?.trim().length ?? 0) > 0 && !el.querySelector('img, video, canvas, svg:not([aria-hidden])')) {
|
|
67
|
+
return 'text';
|
|
68
|
+
}
|
|
69
|
+
return 'element';
|
|
70
|
+
}
|
|
71
|
+
/** Extract src for images and videos, checking the element itself and its children */
|
|
72
|
+
function extractMediaSrc(el) {
|
|
73
|
+
const tag = el.tagName.toLowerCase();
|
|
74
|
+
if (tag === 'img')
|
|
75
|
+
return el.getAttribute('src');
|
|
76
|
+
if (tag === 'video')
|
|
77
|
+
return el.getAttribute('src') ?? el.querySelector('source')?.getAttribute('src') ?? null;
|
|
78
|
+
const img = el.querySelector('img');
|
|
79
|
+
if (img)
|
|
80
|
+
return img.getAttribute('src');
|
|
81
|
+
const video = el.querySelector('video');
|
|
82
|
+
if (video)
|
|
83
|
+
return video.getAttribute('src') ?? video.querySelector('source')?.getAttribute('src') ?? null;
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
function preview(el) {
|
|
87
|
+
const t = (el.textContent ?? '').trim();
|
|
88
|
+
return t.length > 60 ? t.slice(0, 60) + '…' : t;
|
|
89
|
+
}
|
|
90
|
+
function hasChange(c) {
|
|
91
|
+
return c.status !== 'unchanged'
|
|
92
|
+
|| c.translateX !== 0 || c.translateY !== 0
|
|
93
|
+
|| c.scaleX !== 1 || c.scaleY !== 1
|
|
94
|
+
|| c.textContent !== null;
|
|
95
|
+
}
|
|
96
|
+
function cloneChange(c) {
|
|
97
|
+
return { ...c };
|
|
98
|
+
}
|
|
99
|
+
/** Derive which section the current frame falls in */
|
|
100
|
+
function getCurrentSection(frame, sections) {
|
|
101
|
+
let acc = 0;
|
|
102
|
+
for (const s of sections) {
|
|
103
|
+
if (frame < acc + s.durationInFrames)
|
|
104
|
+
return s;
|
|
105
|
+
acc += s.durationInFrames;
|
|
106
|
+
}
|
|
107
|
+
return sections.length > 0 ? sections[sections.length - 1] : null;
|
|
108
|
+
}
|
|
109
|
+
// ── Serialization ──────────────────────────────────────────────────────
|
|
110
|
+
function serializeChanges(changes, currentFrame, fps, sections, entryPath) {
|
|
111
|
+
const entries = Array.from(changes.values()).filter(hasChange);
|
|
112
|
+
if (entries.length === 0)
|
|
113
|
+
return '';
|
|
114
|
+
const parts = ['## Layout changes\n'];
|
|
115
|
+
if (entryPath) {
|
|
116
|
+
parts.push(`File: ${entryPath}`);
|
|
117
|
+
}
|
|
118
|
+
// Frame + section context
|
|
119
|
+
if (currentFrame !== null) {
|
|
120
|
+
const seconds = (currentFrame / fps).toFixed(1);
|
|
121
|
+
parts.push(`Current frame: ${currentFrame} (${seconds}s at ${fps}fps)`);
|
|
122
|
+
const section = getCurrentSection(currentFrame, sections);
|
|
123
|
+
if (section?.heading) {
|
|
124
|
+
parts.push(`Section: "${section.heading}"`);
|
|
125
|
+
}
|
|
126
|
+
parts.push('');
|
|
127
|
+
}
|
|
128
|
+
for (const c of entries) {
|
|
129
|
+
const line = c.mdxLine !== null ? ` (line ${c.mdxLine})` : '';
|
|
130
|
+
const text = c.kind === 'text' && c.textPreview ? `: "${c.textPreview}"` : '';
|
|
131
|
+
const src = c.src && (c.kind === 'image' || c.kind === 'video') ? ` (src: ${c.src})` : '';
|
|
132
|
+
parts.push(`**${c.kind}**${line}${text}${src}`);
|
|
133
|
+
if (c.status === 'deleted') {
|
|
134
|
+
parts.push(`- Deleted`);
|
|
135
|
+
parts.push('');
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (c.translateX !== 0 || c.translateY !== 0) {
|
|
139
|
+
const x = `${c.translateX > 0 ? '+' : ''}${Math.round(c.translateX)}px`;
|
|
140
|
+
const y = `${c.translateY > 0 ? '+' : ''}${Math.round(c.translateY)}px`;
|
|
141
|
+
parts.push(`- Move: x=${x}, y=${y}`);
|
|
142
|
+
}
|
|
143
|
+
if (c.scaleX !== 1 || c.scaleY !== 1) {
|
|
144
|
+
parts.push(`- Scale: ${c.scaleX.toFixed(2)}x, ${c.scaleY.toFixed(2)}y`);
|
|
145
|
+
}
|
|
146
|
+
if (c.textContent !== null && c.originalTextContent !== null) {
|
|
147
|
+
parts.push(`- Text: "${c.originalTextContent}" -> "${c.textContent}"`);
|
|
148
|
+
}
|
|
149
|
+
parts.push('');
|
|
150
|
+
}
|
|
151
|
+
return parts.join('\n');
|
|
152
|
+
}
|
|
153
|
+
// ── Toolbar button ─────────────────────────────────────────────────────
|
|
154
|
+
function ToolbarButton({ onClick, active, children, title }) {
|
|
155
|
+
return (_jsx("button", { type: 'button', onClick: onClick, title: title, className: [
|
|
156
|
+
'flex items-center gap-1.5 rounded-full px-3 py-1.5 text-[13px] font-medium whitespace-nowrap transition-colors cursor-pointer',
|
|
157
|
+
active
|
|
158
|
+
? 'bg-sky-200 text-sky-950'
|
|
159
|
+
: 'text-zinc-300 hover:bg-white/10',
|
|
160
|
+
].join(' '), children: children }));
|
|
161
|
+
}
|
|
162
|
+
// ── Main component ─────────────────────────────────────────────────────
|
|
163
|
+
export function LayoutEditor({ playerContainerRef, playerRef, editing, onEditingChange, onReset, sections, fps, entryPath }) {
|
|
164
|
+
const [selectedEl, setSelectedEl] = useState(null);
|
|
165
|
+
const [changesCount, setChangesCount] = useState(0);
|
|
166
|
+
const [copied, setCopied] = useState(false);
|
|
167
|
+
const moveableRef = useRef(null);
|
|
168
|
+
const changesRef = useRef(new Map());
|
|
169
|
+
const textEditRef = useRef(null);
|
|
170
|
+
const undoStack = useRef([]);
|
|
171
|
+
/** Generation counter to guard against stale async import resolutions */
|
|
172
|
+
const moveableGenRef = useRef(0);
|
|
173
|
+
/** Timeout ID for the "Copied!" indicator reset */
|
|
174
|
+
const copiedTimeoutRef = useRef(null);
|
|
175
|
+
const getPlayer = useCallback(() => {
|
|
176
|
+
return playerContainerRef.current?.querySelector('.__remotion-player');
|
|
177
|
+
}, [playerContainerRef]);
|
|
178
|
+
const isSelectable = useCallback((el, player) => {
|
|
179
|
+
if (el.className?.includes?.('moveable-'))
|
|
180
|
+
return false;
|
|
181
|
+
const d = getComputedStyle(el).display;
|
|
182
|
+
if (!['block', 'flex', 'grid', 'inline-block'].includes(d))
|
|
183
|
+
return false;
|
|
184
|
+
if (el.offsetHeight <= 10 || el.offsetWidth <= 20)
|
|
185
|
+
return false;
|
|
186
|
+
const r = el.getBoundingClientRect();
|
|
187
|
+
const p = player.getBoundingClientRect();
|
|
188
|
+
if (Math.abs(r.width - p.width) < 5 && Math.abs(r.height - p.height) < 5)
|
|
189
|
+
return false;
|
|
190
|
+
return true;
|
|
191
|
+
}, []);
|
|
192
|
+
const findTarget = useCallback((el) => {
|
|
193
|
+
const player = getPlayer();
|
|
194
|
+
if (!player?.contains(el))
|
|
195
|
+
return null;
|
|
196
|
+
let cur = el;
|
|
197
|
+
while (cur && player.contains(cur)) {
|
|
198
|
+
if (isSelectable(cur, player))
|
|
199
|
+
return cur;
|
|
200
|
+
cur = cur.parentElement;
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}, [getPlayer, isSelectable]);
|
|
204
|
+
/** Try all elements at a screen point, skipping transparent overlays like Fill */
|
|
205
|
+
const findTargetAtPoint = useCallback((clientX, clientY) => {
|
|
206
|
+
const elements = document.elementsFromPoint(clientX, clientY);
|
|
207
|
+
for (const el of elements) {
|
|
208
|
+
const hit = findTarget(el);
|
|
209
|
+
if (hit)
|
|
210
|
+
return hit;
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}, [findTarget]);
|
|
214
|
+
function countChanges() {
|
|
215
|
+
let n = 0;
|
|
216
|
+
for (const c of changesRef.current.values())
|
|
217
|
+
if (hasChange(c))
|
|
218
|
+
n++;
|
|
219
|
+
return n;
|
|
220
|
+
}
|
|
221
|
+
// ── Finish text editing (extracted so it can be called from !editing cleanup) ──
|
|
222
|
+
const finishTextEdit = useCallback(() => {
|
|
223
|
+
const target = textEditRef.current;
|
|
224
|
+
if (!target)
|
|
225
|
+
return;
|
|
226
|
+
target.contentEditable = 'false';
|
|
227
|
+
target.style.outline = '';
|
|
228
|
+
target.style.outlineOffset = '';
|
|
229
|
+
const c = changesRef.current.get(target);
|
|
230
|
+
if (c && target.textContent !== c.originalTextContent) {
|
|
231
|
+
c.textContent = target.textContent ?? '';
|
|
232
|
+
}
|
|
233
|
+
textEditRef.current = null;
|
|
234
|
+
setChangesCount(countChanges());
|
|
235
|
+
}, []);
|
|
236
|
+
// ── Attach Moveable (drag + scale, all via transform) ──
|
|
237
|
+
const attachMoveable = useCallback(async (target) => {
|
|
238
|
+
moveableRef.current?.destroy();
|
|
239
|
+
moveableRef.current = null;
|
|
240
|
+
const container = playerContainerRef.current;
|
|
241
|
+
// Increment generation before the async import
|
|
242
|
+
const gen = ++moveableGenRef.current;
|
|
243
|
+
const mod = await import('moveable');
|
|
244
|
+
// Bail if editing was disabled or a newer selection happened during import
|
|
245
|
+
if (moveableGenRef.current !== gen)
|
|
246
|
+
return;
|
|
247
|
+
const Cls = mod.default;
|
|
248
|
+
const m = new Cls(container ?? document.body, {
|
|
249
|
+
target,
|
|
250
|
+
draggable: true,
|
|
251
|
+
scalable: true,
|
|
252
|
+
keepRatio: true,
|
|
253
|
+
origin: false,
|
|
254
|
+
throttleDrag: 1,
|
|
255
|
+
throttleScale: 0.01,
|
|
256
|
+
renderDirections: ['nw', 'ne', 'sw', 'se'],
|
|
257
|
+
edge: false,
|
|
258
|
+
zoom: 1,
|
|
259
|
+
});
|
|
260
|
+
if (getComputedStyle(target).position === 'static') {
|
|
261
|
+
target.style.position = 'relative';
|
|
262
|
+
}
|
|
263
|
+
if (!changesRef.current.has(target)) {
|
|
264
|
+
const player = getPlayer();
|
|
265
|
+
changesRef.current.set(target, {
|
|
266
|
+
mdxLine: findMdxLine(target, player ?? document.body),
|
|
267
|
+
kind: classifyElement(target),
|
|
268
|
+
status: 'unchanged',
|
|
269
|
+
translateX: 0, translateY: 0,
|
|
270
|
+
scaleX: 1, scaleY: 1,
|
|
271
|
+
textContent: null, originalTextContent: null,
|
|
272
|
+
textPreview: preview(target),
|
|
273
|
+
tagName: target.tagName.toLowerCase(),
|
|
274
|
+
src: extractMediaSrc(target),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
m.on('dragStart', ({ target: el }) => {
|
|
278
|
+
const htmlEl = el;
|
|
279
|
+
const existing = changesRef.current.get(htmlEl);
|
|
280
|
+
undoStack.current.push({
|
|
281
|
+
el: htmlEl,
|
|
282
|
+
transform: htmlEl.style.transform,
|
|
283
|
+
position: htmlEl.style.position,
|
|
284
|
+
display: htmlEl.style.display,
|
|
285
|
+
prevChange: existing ? cloneChange(existing) : null,
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
m.on('drag', ({ target: el, transform }) => {
|
|
289
|
+
const htmlEl = el;
|
|
290
|
+
htmlEl.style.transform = transform;
|
|
291
|
+
const c = changesRef.current.get(htmlEl);
|
|
292
|
+
if (c) {
|
|
293
|
+
const m = transform.match(/translate\(([-\d.]+)px,\s*([-\d.]+)px\)/);
|
|
294
|
+
if (m) {
|
|
295
|
+
c.translateX = parseFloat(m[1]);
|
|
296
|
+
c.translateY = parseFloat(m[2]);
|
|
297
|
+
}
|
|
298
|
+
setChangesCount(countChanges());
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
m.on('scaleStart', ({ target: el }) => {
|
|
302
|
+
const htmlEl = el;
|
|
303
|
+
const existing = changesRef.current.get(htmlEl);
|
|
304
|
+
undoStack.current.push({
|
|
305
|
+
el: htmlEl,
|
|
306
|
+
transform: htmlEl.style.transform,
|
|
307
|
+
position: htmlEl.style.position,
|
|
308
|
+
display: htmlEl.style.display,
|
|
309
|
+
prevChange: existing ? cloneChange(existing) : null,
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
m.on('scale', ({ target: el, scale, drag }) => {
|
|
313
|
+
const htmlEl = el;
|
|
314
|
+
htmlEl.style.transform = drag.transform;
|
|
315
|
+
const c = changesRef.current.get(htmlEl);
|
|
316
|
+
if (c) {
|
|
317
|
+
c.scaleX = scale[0];
|
|
318
|
+
c.scaleY = scale[1];
|
|
319
|
+
const mt = drag.transform.match(/translate\(([-\d.]+)px,\s*([-\d.]+)px\)/);
|
|
320
|
+
if (mt) {
|
|
321
|
+
c.translateX = parseFloat(mt[1]);
|
|
322
|
+
c.translateY = parseFloat(mt[2]);
|
|
323
|
+
}
|
|
324
|
+
setChangesCount(countChanges());
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
moveableRef.current = m;
|
|
328
|
+
setSelectedEl(target);
|
|
329
|
+
}, [playerContainerRef, getPlayer]);
|
|
330
|
+
// ── Double-click to edit text ──
|
|
331
|
+
const handleDoubleClick = useCallback((e) => {
|
|
332
|
+
if (!editing)
|
|
333
|
+
return;
|
|
334
|
+
const player = getPlayer();
|
|
335
|
+
if (!player?.contains(e.target))
|
|
336
|
+
return;
|
|
337
|
+
// For text editing, try all elements at this point (skipping transparent
|
|
338
|
+
// overlays like Fill), looking for the innermost text-bearing element.
|
|
339
|
+
let target = null;
|
|
340
|
+
const elements = document.elementsFromPoint(e.clientX, e.clientY);
|
|
341
|
+
for (const candidate of elements) {
|
|
342
|
+
if (!player.contains(candidate))
|
|
343
|
+
continue;
|
|
344
|
+
let cur = candidate;
|
|
345
|
+
while (cur && player.contains(cur)) {
|
|
346
|
+
if (isTextElement(cur)) {
|
|
347
|
+
target = cur;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
cur = cur.parentElement;
|
|
351
|
+
}
|
|
352
|
+
if (target)
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
// Fall back to selectable parent if no leaf text found
|
|
356
|
+
if (!target)
|
|
357
|
+
target = findTargetAtPoint(e.clientX, e.clientY);
|
|
358
|
+
if (!target || !isTextElement(target))
|
|
359
|
+
return;
|
|
360
|
+
e.preventDefault();
|
|
361
|
+
e.stopPropagation();
|
|
362
|
+
// Invalidate any in-flight attachMoveable async imports so they bail
|
|
363
|
+
// after resolving. A double-click fires click→click→dblclick; the second
|
|
364
|
+
// click starts attachMoveable whose import may resolve after we enter
|
|
365
|
+
// text edit mode, causing a __CROACT__ null error in Moveable's renderer.
|
|
366
|
+
moveableGenRef.current++;
|
|
367
|
+
moveableRef.current?.destroy();
|
|
368
|
+
moveableRef.current = null;
|
|
369
|
+
textEditRef.current = target;
|
|
370
|
+
const change = changesRef.current.get(target);
|
|
371
|
+
if (change && change.originalTextContent === null) {
|
|
372
|
+
change.originalTextContent = target.textContent ?? '';
|
|
373
|
+
}
|
|
374
|
+
target.contentEditable = 'true';
|
|
375
|
+
target.focus();
|
|
376
|
+
target.style.outline = '2px solid #7dd3fc';
|
|
377
|
+
target.style.outlineOffset = '2px';
|
|
378
|
+
const range = document.createRange();
|
|
379
|
+
range.selectNodeContents(target);
|
|
380
|
+
window.getSelection()?.removeAllRanges();
|
|
381
|
+
window.getSelection()?.addRange(range);
|
|
382
|
+
// Named handler so we can remove it in cleanup
|
|
383
|
+
const onEscapeKey = (ke) => { if (ke.key === 'Escape')
|
|
384
|
+
target.blur(); };
|
|
385
|
+
const finish = () => {
|
|
386
|
+
target.removeEventListener('keydown', onEscapeKey);
|
|
387
|
+
finishTextEdit();
|
|
388
|
+
};
|
|
389
|
+
target.addEventListener('blur', finish, { once: true });
|
|
390
|
+
target.addEventListener('keydown', onEscapeKey);
|
|
391
|
+
}, [editing, getPlayer, findTargetAtPoint, finishTextEdit]);
|
|
392
|
+
// ── Click to select ──
|
|
393
|
+
const handleClick = useCallback((e) => {
|
|
394
|
+
if (!editing || textEditRef.current)
|
|
395
|
+
return;
|
|
396
|
+
if (e.target.closest('[data-layout-editor-toolbar]'))
|
|
397
|
+
return;
|
|
398
|
+
if (e.target.className?.includes?.('moveable-'))
|
|
399
|
+
return;
|
|
400
|
+
// Click outside player container → deselect
|
|
401
|
+
if (!playerContainerRef.current?.contains(e.target)) {
|
|
402
|
+
if (selectedEl) {
|
|
403
|
+
moveableRef.current?.destroy();
|
|
404
|
+
moveableRef.current = null;
|
|
405
|
+
setSelectedEl(null);
|
|
406
|
+
}
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const target = findTargetAtPoint(e.clientX, e.clientY);
|
|
410
|
+
if (target) {
|
|
411
|
+
e.preventDefault();
|
|
412
|
+
e.stopPropagation();
|
|
413
|
+
void attachMoveable(target);
|
|
414
|
+
}
|
|
415
|
+
else if (selectedEl) {
|
|
416
|
+
// Clicked inside player but not on a selectable element → deselect
|
|
417
|
+
moveableRef.current?.destroy();
|
|
418
|
+
moveableRef.current = null;
|
|
419
|
+
setSelectedEl(null);
|
|
420
|
+
}
|
|
421
|
+
}, [editing, selectedEl, findTargetAtPoint, attachMoveable, playerContainerRef]);
|
|
422
|
+
// ── Keyboard: Esc (select parent / deselect), Ctrl+Z (undo) ──
|
|
423
|
+
useEffect(() => {
|
|
424
|
+
if (!editing)
|
|
425
|
+
return;
|
|
426
|
+
const player = getPlayer();
|
|
427
|
+
const onKey = (e) => {
|
|
428
|
+
if (textEditRef.current)
|
|
429
|
+
return;
|
|
430
|
+
if (e.key === 'z' && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
|
|
431
|
+
e.preventDefault();
|
|
432
|
+
const entry = undoStack.current.pop();
|
|
433
|
+
if (entry) {
|
|
434
|
+
entry.el.style.transform = entry.transform;
|
|
435
|
+
entry.el.style.position = entry.position;
|
|
436
|
+
entry.el.style.display = entry.display;
|
|
437
|
+
// Restore previous change snapshot instead of deleting entirely
|
|
438
|
+
if (entry.prevChange) {
|
|
439
|
+
changesRef.current.set(entry.el, entry.prevChange);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
changesRef.current.delete(entry.el);
|
|
443
|
+
}
|
|
444
|
+
setChangesCount(countChanges());
|
|
445
|
+
if (selectedEl === entry.el)
|
|
446
|
+
void attachMoveable(entry.el);
|
|
447
|
+
}
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if ((e.key === 'Delete' || e.key === 'Backspace') && selectedEl) {
|
|
451
|
+
e.preventDefault();
|
|
452
|
+
// Invalidate any in-flight attachMoveable async imports
|
|
453
|
+
moveableGenRef.current++;
|
|
454
|
+
const player = getPlayer();
|
|
455
|
+
// Ensure a change entry exists for this element
|
|
456
|
+
if (!changesRef.current.has(selectedEl)) {
|
|
457
|
+
changesRef.current.set(selectedEl, {
|
|
458
|
+
mdxLine: findMdxLine(selectedEl, player ?? document.body),
|
|
459
|
+
kind: classifyElement(selectedEl),
|
|
460
|
+
status: 'unchanged',
|
|
461
|
+
translateX: 0, translateY: 0,
|
|
462
|
+
scaleX: 1, scaleY: 1,
|
|
463
|
+
textContent: null, originalTextContent: null,
|
|
464
|
+
textPreview: preview(selectedEl),
|
|
465
|
+
tagName: selectedEl.tagName.toLowerCase(),
|
|
466
|
+
src: extractMediaSrc(selectedEl),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
const existing = changesRef.current.get(selectedEl);
|
|
470
|
+
undoStack.current.push({
|
|
471
|
+
el: selectedEl,
|
|
472
|
+
transform: selectedEl.style.transform,
|
|
473
|
+
position: selectedEl.style.position,
|
|
474
|
+
display: selectedEl.style.display,
|
|
475
|
+
prevChange: cloneChange(existing),
|
|
476
|
+
});
|
|
477
|
+
existing.status = 'deleted';
|
|
478
|
+
selectedEl.style.display = 'none';
|
|
479
|
+
moveableRef.current?.destroy();
|
|
480
|
+
moveableRef.current = null;
|
|
481
|
+
setSelectedEl(null);
|
|
482
|
+
setChangesCount(countChanges());
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (e.key === 'Escape') {
|
|
486
|
+
if (!selectedEl || !player) {
|
|
487
|
+
moveableRef.current?.destroy();
|
|
488
|
+
moveableRef.current = null;
|
|
489
|
+
setSelectedEl(null);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
let parent = selectedEl.parentElement;
|
|
493
|
+
while (parent && player.contains(parent)) {
|
|
494
|
+
if (isSelectable(parent, player)) {
|
|
495
|
+
void attachMoveable(parent);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
parent = parent.parentElement;
|
|
499
|
+
}
|
|
500
|
+
moveableRef.current?.destroy();
|
|
501
|
+
moveableRef.current = null;
|
|
502
|
+
setSelectedEl(null);
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
document.addEventListener('keydown', onKey);
|
|
506
|
+
return () => document.removeEventListener('keydown', onKey);
|
|
507
|
+
}, [editing, selectedEl, getPlayer, isSelectable, attachMoveable]);
|
|
508
|
+
// ── Deselect on player seek ──
|
|
509
|
+
useEffect(() => {
|
|
510
|
+
if (!editing)
|
|
511
|
+
return;
|
|
512
|
+
const player = playerRef.current;
|
|
513
|
+
if (!player)
|
|
514
|
+
return;
|
|
515
|
+
const onSeek = () => {
|
|
516
|
+
moveableRef.current?.destroy();
|
|
517
|
+
moveableRef.current = null;
|
|
518
|
+
setSelectedEl(null);
|
|
519
|
+
};
|
|
520
|
+
player.addEventListener('seeked', onSeek);
|
|
521
|
+
return () => player.removeEventListener('seeked', onSeek);
|
|
522
|
+
}, [editing, playerRef]);
|
|
523
|
+
// ── Listeners lifecycle ──
|
|
524
|
+
useEffect(() => {
|
|
525
|
+
if (!editing)
|
|
526
|
+
return;
|
|
527
|
+
document.addEventListener('click', handleClick, true);
|
|
528
|
+
document.addEventListener('dblclick', handleDoubleClick, true);
|
|
529
|
+
return () => {
|
|
530
|
+
document.removeEventListener('click', handleClick, true);
|
|
531
|
+
document.removeEventListener('dblclick', handleDoubleClick, true);
|
|
532
|
+
};
|
|
533
|
+
}, [editing, handleClick, handleDoubleClick]);
|
|
534
|
+
// ── Cleanup when editing is toggled off ──
|
|
535
|
+
useEffect(() => {
|
|
536
|
+
if (!editing) {
|
|
537
|
+
// Increment generation to invalidate any in-flight attachMoveable
|
|
538
|
+
moveableGenRef.current++;
|
|
539
|
+
moveableRef.current?.destroy();
|
|
540
|
+
moveableRef.current = null;
|
|
541
|
+
setSelectedEl(null);
|
|
542
|
+
// Clean up any active text edit
|
|
543
|
+
finishTextEdit();
|
|
544
|
+
}
|
|
545
|
+
}, [editing, finishTextEdit]);
|
|
546
|
+
// ── Destroy moveable on unmount ──
|
|
547
|
+
useEffect(() => {
|
|
548
|
+
return () => {
|
|
549
|
+
moveableRef.current?.destroy();
|
|
550
|
+
moveableRef.current = null;
|
|
551
|
+
};
|
|
552
|
+
}, []);
|
|
553
|
+
// ── Clear copied timeout on unmount ──
|
|
554
|
+
useEffect(() => {
|
|
555
|
+
return () => {
|
|
556
|
+
if (copiedTimeoutRef.current !== null) {
|
|
557
|
+
clearTimeout(copiedTimeoutRef.current);
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
}, []);
|
|
561
|
+
// ── Actions ──
|
|
562
|
+
const handleCopy = useCallback(async () => {
|
|
563
|
+
const currentFrame = playerRef.current?.getCurrentFrame() ?? null;
|
|
564
|
+
const text = serializeChanges(changesRef.current, currentFrame, fps, sections, entryPath);
|
|
565
|
+
if (!text)
|
|
566
|
+
return;
|
|
567
|
+
await navigator.clipboard.writeText(text);
|
|
568
|
+
setCopied(true);
|
|
569
|
+
if (copiedTimeoutRef.current !== null) {
|
|
570
|
+
clearTimeout(copiedTimeoutRef.current);
|
|
571
|
+
}
|
|
572
|
+
copiedTimeoutRef.current = setTimeout(() => {
|
|
573
|
+
setCopied(false);
|
|
574
|
+
copiedTimeoutRef.current = null;
|
|
575
|
+
}, 2000);
|
|
576
|
+
}, [playerRef, fps, sections]);
|
|
577
|
+
const handleReset = useCallback(() => {
|
|
578
|
+
moveableRef.current?.destroy();
|
|
579
|
+
moveableRef.current = null;
|
|
580
|
+
changesRef.current.clear();
|
|
581
|
+
undoStack.current = [];
|
|
582
|
+
setChangesCount(0);
|
|
583
|
+
setSelectedEl(null);
|
|
584
|
+
onReset();
|
|
585
|
+
}, [onReset]);
|
|
586
|
+
return (_jsxs("div", { "data-layout-editor-toolbar": true, className: 'flex items-center gap-1.5 text-[13px]', children: [_jsx(ToolbarButton, { onClick: () => onEditingChange(!editing), active: editing, title: editing ? 'Exit editing mode' : 'Edit layout — click to select, drag to move, corners to scale, double-click text to edit', children: editing ? '✦ Editing' : '✦ Edit Layout' }), editing && changesCount > 0 && (_jsxs(_Fragment, { children: [_jsx(ToolbarButton, { onClick: handleCopy, title: 'Copy changes for an AI agent', children: copied ? 'Copied!' : `Copy ${changesCount} change${changesCount > 1 ? 's' : ''}` }), _jsx(ToolbarButton, { onClick: handleReset, title: 'Revert all changes', children: "Reset" })] })), editing && !selectedEl && (_jsx("span", { className: 'text-zinc-600 italic', children: "Click an element" }))] }));
|
|
587
|
+
}
|