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.
Files changed (280) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/dist/cli/cache-utils.d.ts +33 -0
  3. package/dist/cli/cache-utils.d.ts.map +1 -0
  4. package/dist/cli/cache-utils.js +157 -0
  5. package/dist/cli/cached-generate.d.ts +105 -0
  6. package/dist/cli/cached-generate.d.ts.map +1 -0
  7. package/dist/cli/cached-generate.js +208 -0
  8. package/dist/cli/cached-generate.test.d.ts +11 -0
  9. package/dist/cli/cached-generate.test.d.ts.map +1 -0
  10. package/dist/cli/cached-generate.test.js +431 -0
  11. package/dist/cli/cartesia-provider.d.ts +16 -0
  12. package/dist/cli/cartesia-provider.d.ts.map +1 -0
  13. package/dist/cli/cartesia-provider.js +184 -0
  14. package/dist/cli/chatgpt-auth.d.ts.map +1 -0
  15. package/dist/cli/cli.d.ts +4 -0
  16. package/dist/cli/cli.d.ts.map +1 -0
  17. package/dist/cli/cli.js +1329 -0
  18. package/dist/cli/clone-generate.d.ts +26 -0
  19. package/dist/cli/clone-generate.d.ts.map +1 -0
  20. package/dist/cli/clone-generate.js +148 -0
  21. package/dist/{credentials.d.ts → cli/credentials.d.ts} +11 -2
  22. package/dist/cli/credentials.d.ts.map +1 -0
  23. package/dist/{credentials.js → cli/credentials.js} +88 -5
  24. package/dist/cli/elevenlabs-speech-provider.d.ts +26 -0
  25. package/dist/cli/elevenlabs-speech-provider.d.ts.map +1 -0
  26. package/dist/cli/elevenlabs-speech-provider.js +199 -0
  27. package/dist/cli/generate.d.ts +140 -0
  28. package/dist/cli/generate.d.ts.map +1 -0
  29. package/dist/cli/generate.js +604 -0
  30. package/dist/cli/generate.test.d.ts +2 -0
  31. package/dist/cli/generate.test.d.ts.map +1 -0
  32. package/dist/cli/generate.test.js +131 -0
  33. package/dist/cli/login.d.ts.map +1 -0
  34. package/dist/{login.js → cli/login.js} +45 -4
  35. package/dist/cli/main.d.ts +3 -0
  36. package/dist/cli/main.d.ts.map +1 -0
  37. package/dist/cli/main.js +7 -0
  38. package/dist/cli/model-catalog.d.ts +141 -0
  39. package/dist/cli/model-catalog.d.ts.map +1 -0
  40. package/dist/cli/model-catalog.js +2135 -0
  41. package/dist/{models.d.ts → cli/models.d.ts} +16 -9
  42. package/dist/cli/models.d.ts.map +1 -0
  43. package/dist/cli/models.js +313 -0
  44. package/dist/cli/open-browser.d.ts.map +1 -0
  45. package/dist/cli/openai-speech-provider.d.ts +3 -0
  46. package/dist/cli/openai-speech-provider.d.ts.map +1 -0
  47. package/dist/cli/openai-speech-provider.js +28 -0
  48. package/dist/cli/separation-generate.d.ts +34 -0
  49. package/dist/cli/separation-generate.d.ts.map +1 -0
  50. package/dist/cli/separation-generate.js +117 -0
  51. package/dist/cli/speech-catalog.d.ts +35 -0
  52. package/dist/cli/speech-catalog.d.ts.map +1 -0
  53. package/dist/cli/speech-catalog.js +195 -0
  54. package/dist/cli/speech-generate.d.ts +86 -0
  55. package/dist/cli/speech-generate.d.ts.map +1 -0
  56. package/dist/cli/speech-generate.js +156 -0
  57. package/dist/cli/speech-models.d.ts +8 -0
  58. package/dist/cli/speech-models.d.ts.map +1 -0
  59. package/dist/cli/speech-models.js +109 -0
  60. package/dist/cli/speech-provider.test.d.ts +2 -0
  61. package/dist/cli/speech-provider.test.d.ts.map +1 -0
  62. package/dist/cli/speech-provider.test.js +197 -0
  63. package/dist/cli/subscription.d.ts.map +1 -0
  64. package/dist/cli/transcription-catalog.d.ts +35 -0
  65. package/dist/cli/transcription-catalog.d.ts.map +1 -0
  66. package/dist/cli/transcription-catalog.js +192 -0
  67. package/dist/cli/transcription-generate.d.ts +62 -0
  68. package/dist/cli/transcription-generate.d.ts.map +1 -0
  69. package/dist/cli/transcription-generate.js +144 -0
  70. package/dist/cli/transcription-models.d.ts +9 -0
  71. package/dist/cli/transcription-models.d.ts.map +1 -0
  72. package/dist/cli/transcription-models.js +100 -0
  73. package/dist/cli/transcription-providers.d.ts +22 -0
  74. package/dist/cli/transcription-providers.d.ts.map +1 -0
  75. package/dist/cli/transcription-providers.js +290 -0
  76. package/dist/cli/video-model-catalog.d.ts +2 -0
  77. package/dist/cli/video-model-catalog.d.ts.map +1 -0
  78. package/dist/cli/video-model-catalog.js +5 -0
  79. package/dist/cli/xai-auth.d.ts +29 -0
  80. package/dist/cli/xai-auth.d.ts.map +1 -0
  81. package/dist/cli/xai-auth.js +325 -0
  82. package/dist/vite/angled-screen.d.ts +57 -0
  83. package/dist/vite/angled-screen.d.ts.map +1 -0
  84. package/dist/vite/angled-screen.js +115 -0
  85. package/dist/vite/app.d.ts +51 -0
  86. package/dist/vite/app.d.ts.map +1 -0
  87. package/dist/vite/app.js +150 -0
  88. package/dist/vite/bands-shader.d.ts +47 -0
  89. package/dist/vite/bands-shader.d.ts.map +1 -0
  90. package/dist/vite/bands-shader.js +267 -0
  91. package/dist/vite/code-block.d.ts +107 -0
  92. package/dist/vite/code-block.d.ts.map +1 -0
  93. package/dist/vite/code-block.js +641 -0
  94. package/dist/vite/components.d.ts +36 -0
  95. package/dist/vite/components.d.ts.map +1 -0
  96. package/dist/vite/components.js +113 -0
  97. package/dist/vite/dispersion-rings-shader.d.ts +44 -0
  98. package/dist/vite/dispersion-rings-shader.d.ts.map +1 -0
  99. package/dist/vite/dispersion-rings-shader.js +282 -0
  100. package/dist/vite/easing-curves.d.ts +129 -0
  101. package/dist/vite/easing-curves.d.ts.map +1 -0
  102. package/dist/vite/easing-curves.js +584 -0
  103. package/dist/vite/easing-curves.test.d.ts +2 -0
  104. package/dist/vite/easing-curves.test.d.ts.map +1 -0
  105. package/dist/vite/easing-curves.test.js +200 -0
  106. package/dist/vite/generated-media-client.d.ts +35 -0
  107. package/dist/vite/generated-media-client.d.ts.map +1 -0
  108. package/dist/vite/generated-media-client.js +76 -0
  109. package/dist/vite/keyframes.d.ts +129 -0
  110. package/dist/vite/keyframes.d.ts.map +1 -0
  111. package/dist/vite/keyframes.js +194 -0
  112. package/dist/vite/layout-editor.d.ts +45 -0
  113. package/dist/vite/layout-editor.d.ts.map +1 -0
  114. package/dist/vite/layout-editor.js +587 -0
  115. package/dist/vite/layout-transition.d.ts +136 -0
  116. package/dist/vite/layout-transition.d.ts.map +1 -0
  117. package/dist/vite/layout-transition.js +581 -0
  118. package/dist/vite/liquid-gradient-shader.d.ts +62 -0
  119. package/dist/vite/liquid-gradient-shader.d.ts.map +1 -0
  120. package/dist/vite/liquid-gradient-shader.js +413 -0
  121. package/dist/vite/mdx-client.d.ts +15 -0
  122. package/dist/vite/mdx-client.d.ts.map +1 -0
  123. package/dist/vite/mdx-client.js +265 -0
  124. package/dist/vite/mdx-parse.d.ts +102 -0
  125. package/dist/vite/mdx-parse.d.ts.map +1 -0
  126. package/dist/vite/mdx-parse.js +251 -0
  127. package/dist/vite/mdx-provided-components.d.ts +18 -0
  128. package/dist/vite/mdx-provided-components.d.ts.map +1 -0
  129. package/dist/vite/mdx-provided-components.js +14 -0
  130. package/dist/vite/mdx-video.d.ts +327 -0
  131. package/dist/vite/mdx-video.d.ts.map +1 -0
  132. package/dist/vite/mdx-video.js +596 -0
  133. package/dist/vite/mdx-video.test.d.ts +9 -0
  134. package/dist/vite/mdx-video.test.d.ts.map +1 -0
  135. package/dist/vite/mdx-video.test.js +1945 -0
  136. package/dist/vite/media-components.d.ts +45 -0
  137. package/dist/vite/media-components.d.ts.map +1 -0
  138. package/dist/vite/media-components.js +355 -0
  139. package/dist/vite/media-duration-store.d.ts +66 -0
  140. package/dist/vite/media-duration-store.d.ts.map +1 -0
  141. package/dist/vite/media-duration-store.js +204 -0
  142. package/dist/vite/player-page.d.ts +35 -0
  143. package/dist/vite/player-page.d.ts.map +1 -0
  144. package/dist/vite/player-page.js +720 -0
  145. package/dist/vite/render-client.d.ts +18 -0
  146. package/dist/vite/render-client.d.ts.map +1 -0
  147. package/dist/vite/render-client.js +67 -0
  148. package/dist/vite/sdk.d.ts +221 -0
  149. package/dist/vite/sdk.d.ts.map +1 -0
  150. package/dist/vite/sdk.js +355 -0
  151. package/dist/vite/server-components.d.ts +55 -0
  152. package/dist/vite/server-components.d.ts.map +1 -0
  153. package/dist/vite/server-components.js +156 -0
  154. package/dist/vite/server-mdx.d.ts +74 -0
  155. package/dist/vite/server-mdx.d.ts.map +1 -0
  156. package/dist/vite/server-mdx.js +259 -0
  157. package/dist/vite/shader-renderer.d.ts +48 -0
  158. package/dist/vite/shader-renderer.d.ts.map +1 -0
  159. package/dist/vite/shader-renderer.js +411 -0
  160. package/dist/vite/store-hooks.d.ts +12 -0
  161. package/dist/vite/store-hooks.d.ts.map +1 -0
  162. package/dist/vite/store-hooks.js +93 -0
  163. package/dist/vite/store.d.ts +75 -0
  164. package/dist/vite/store.d.ts.map +1 -0
  165. package/dist/vite/store.js +52 -0
  166. package/dist/vite/store.test.d.ts +6 -0
  167. package/dist/vite/store.test.d.ts.map +1 -0
  168. package/dist/vite/store.test.js +172 -0
  169. package/dist/vite/tweakpane-hook.d.ts +94 -0
  170. package/dist/vite/tweakpane-hook.d.ts.map +1 -0
  171. package/dist/vite/tweakpane-hook.js +450 -0
  172. package/dist/vite/vite-plugin.d.ts +28 -0
  173. package/dist/vite/vite-plugin.d.ts.map +1 -0
  174. package/dist/vite/vite-plugin.js +538 -0
  175. package/dist/vite/wave-gradient-shader.d.ts +41 -0
  176. package/dist/vite/wave-gradient-shader.d.ts.map +1 -0
  177. package/dist/vite/wave-gradient-shader.js +207 -0
  178. package/package.json +103 -25
  179. package/skills/egaki/SKILL.md +162 -1
  180. package/src/cli/cache-utils.ts +169 -0
  181. package/src/cli/cached-generate.test.ts +531 -0
  182. package/src/cli/cached-generate.ts +348 -0
  183. package/src/cli/cartesia-provider.ts +249 -0
  184. package/src/cli/cli.ts +1818 -0
  185. package/src/cli/clone-generate.ts +202 -0
  186. package/src/{credentials.ts → cli/credentials.ts} +90 -7
  187. package/src/cli/elevenlabs-speech-provider.ts +243 -0
  188. package/src/cli/generate.test.ts +166 -0
  189. package/src/cli/generate.ts +912 -0
  190. package/src/{login.ts → cli/login.ts} +48 -3
  191. package/src/cli/main.ts +8 -0
  192. package/src/cli/model-catalog.ts +2336 -0
  193. package/src/cli/models.ts +408 -0
  194. package/src/cli/openai-speech-provider.ts +34 -0
  195. package/src/cli/separation-generate.ts +187 -0
  196. package/src/cli/speech-catalog.ts +253 -0
  197. package/src/cli/speech-generate.ts +275 -0
  198. package/src/cli/speech-models.ts +146 -0
  199. package/src/cli/speech-provider.test.ts +226 -0
  200. package/src/cli/transcription-catalog.ts +250 -0
  201. package/src/cli/transcription-generate.ts +214 -0
  202. package/src/cli/transcription-models.ts +136 -0
  203. package/src/cli/transcription-providers.ts +427 -0
  204. package/src/cli/video-model-catalog.ts +14 -0
  205. package/src/cli/xai-auth.ts +396 -0
  206. package/src/vite/angled-screen.tsx +214 -0
  207. package/src/vite/app.tsx +197 -0
  208. package/src/vite/bands-shader.tsx +319 -0
  209. package/src/vite/code-block.tsx +939 -0
  210. package/src/vite/components.tsx +278 -0
  211. package/src/vite/dispersion-rings-shader.tsx +331 -0
  212. package/src/vite/easing-curves.test.ts +232 -0
  213. package/src/vite/easing-curves.ts +743 -0
  214. package/src/vite/generated-media-client.tsx +121 -0
  215. package/src/vite/keyframes.tsx +347 -0
  216. package/src/vite/layout-editor.tsx +709 -0
  217. package/src/vite/layout-transition.tsx +706 -0
  218. package/src/vite/liquid-gradient-shader.tsx +474 -0
  219. package/src/vite/mdx-client.tsx +420 -0
  220. package/src/vite/mdx-parse.ts +343 -0
  221. package/src/vite/mdx-provided-components.ts +19 -0
  222. package/src/vite/mdx-video.test.tsx +2219 -0
  223. package/src/vite/mdx-video.tsx +976 -0
  224. package/src/vite/media-components.tsx +473 -0
  225. package/src/vite/media-duration-store.ts +225 -0
  226. package/src/vite/motion-timing.ts +194 -0
  227. package/src/vite/player-page.tsx +1107 -0
  228. package/src/vite/render-client.ts +84 -0
  229. package/src/vite/sdk.ts +598 -0
  230. package/src/vite/server-components.tsx +241 -0
  231. package/src/vite/server-mdx.ts +285 -0
  232. package/src/vite/shader-renderer.tsx +535 -0
  233. package/src/vite/store-hooks.ts +117 -0
  234. package/src/vite/store.test.ts +202 -0
  235. package/src/vite/store.ts +115 -0
  236. package/src/vite/styles.css +21 -0
  237. package/src/vite/tweakpane-hook.tsx +593 -0
  238. package/src/vite/virtual-modules.d.ts +30 -0
  239. package/src/vite/vite-plugin.ts +584 -0
  240. package/src/vite/wave-gradient-shader.tsx +253 -0
  241. package/README.md +0 -266
  242. package/dist/antigravity-auth.d.ts +0 -15
  243. package/dist/antigravity-auth.d.ts.map +0 -1
  244. package/dist/antigravity-auth.js +0 -544
  245. package/dist/antigravity-image.d.ts +0 -11
  246. package/dist/antigravity-image.d.ts.map +0 -1
  247. package/dist/antigravity-image.js +0 -285
  248. package/dist/chatgpt-auth.d.ts.map +0 -1
  249. package/dist/cli.d.ts +0 -3
  250. package/dist/cli.d.ts.map +0 -1
  251. package/dist/cli.js +0 -968
  252. package/dist/credentials.d.ts.map +0 -1
  253. package/dist/gateway-model-candidates.generated.d.ts +0 -11
  254. package/dist/gateway-model-candidates.generated.d.ts.map +0 -1
  255. package/dist/gateway-model-candidates.generated.js +0 -64
  256. package/dist/login.d.ts.map +0 -1
  257. package/dist/model-catalog.d.ts +0 -42
  258. package/dist/model-catalog.d.ts.map +0 -1
  259. package/dist/model-catalog.js +0 -1001
  260. package/dist/models.d.ts.map +0 -1
  261. package/dist/models.js +0 -246
  262. package/dist/open-browser.d.ts.map +0 -1
  263. package/dist/subscription.d.ts.map +0 -1
  264. package/dist/video-model-catalog.d.ts +0 -52
  265. package/dist/video-model-catalog.d.ts.map +0 -1
  266. package/dist/video-model-catalog.js +0 -306
  267. package/src/cli.ts +0 -1378
  268. package/src/model-catalog.ts +0 -1077
  269. package/src/models.ts +0 -309
  270. package/src/video-model-catalog.ts +0 -363
  271. /package/dist/{chatgpt-auth.d.ts → cli/chatgpt-auth.d.ts} +0 -0
  272. /package/dist/{chatgpt-auth.js → cli/chatgpt-auth.js} +0 -0
  273. /package/dist/{login.d.ts → cli/login.d.ts} +0 -0
  274. /package/dist/{open-browser.d.ts → cli/open-browser.d.ts} +0 -0
  275. /package/dist/{open-browser.js → cli/open-browser.js} +0 -0
  276. /package/dist/{subscription.d.ts → cli/subscription.d.ts} +0 -0
  277. /package/dist/{subscription.js → cli/subscription.js} +0 -0
  278. /package/src/{chatgpt-auth.ts → cli/chatgpt-auth.ts} +0 -0
  279. /package/src/{open-browser.ts → cli/open-browser.ts} +0 -0
  280. /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
+ }