egaki 0.6.0 → 0.7.1

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 (271) hide show
  1. package/CHANGELOG.md +145 -9
  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.js → cli/cli.js} +626 -600
  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/cli/credentials.d.ts.map +1 -0
  22. package/dist/{credentials.js → cli/credentials.js} +20 -0
  23. package/dist/cli/elevenlabs-speech-provider.d.ts +26 -0
  24. package/dist/cli/elevenlabs-speech-provider.d.ts.map +1 -0
  25. package/dist/cli/elevenlabs-speech-provider.js +199 -0
  26. package/dist/cli/generate.d.ts +140 -0
  27. package/dist/cli/generate.d.ts.map +1 -0
  28. package/dist/cli/generate.js +604 -0
  29. package/dist/cli/generate.test.d.ts +2 -0
  30. package/dist/cli/generate.test.d.ts.map +1 -0
  31. package/dist/cli/generate.test.js +131 -0
  32. package/dist/cli/login.d.ts.map +1 -0
  33. package/dist/cli/main.d.ts +3 -0
  34. package/dist/cli/main.d.ts.map +1 -0
  35. package/dist/cli/main.js +7 -0
  36. package/dist/{model-catalog.d.ts → cli/model-catalog.d.ts} +6 -0
  37. package/dist/cli/model-catalog.d.ts.map +1 -0
  38. package/dist/{model-catalog.js → cli/model-catalog.js} +714 -4
  39. package/dist/{models.d.ts → cli/models.d.ts} +12 -5
  40. package/dist/cli/models.d.ts.map +1 -0
  41. package/dist/{models.js → cli/models.js} +58 -57
  42. package/dist/cli/open-browser.d.ts.map +1 -0
  43. package/dist/cli/openai-speech-provider.d.ts +3 -0
  44. package/dist/cli/openai-speech-provider.d.ts.map +1 -0
  45. package/dist/cli/openai-speech-provider.js +28 -0
  46. package/dist/cli/separation-generate.d.ts +34 -0
  47. package/dist/cli/separation-generate.d.ts.map +1 -0
  48. package/dist/cli/separation-generate.js +117 -0
  49. package/dist/cli/speech-catalog.d.ts +35 -0
  50. package/dist/cli/speech-catalog.d.ts.map +1 -0
  51. package/dist/cli/speech-catalog.js +195 -0
  52. package/dist/cli/speech-generate.d.ts +86 -0
  53. package/dist/cli/speech-generate.d.ts.map +1 -0
  54. package/dist/cli/speech-generate.js +156 -0
  55. package/dist/cli/speech-models.d.ts +8 -0
  56. package/dist/cli/speech-models.d.ts.map +1 -0
  57. package/dist/cli/speech-models.js +109 -0
  58. package/dist/cli/speech-provider.test.d.ts +2 -0
  59. package/dist/cli/speech-provider.test.d.ts.map +1 -0
  60. package/dist/cli/speech-provider.test.js +197 -0
  61. package/dist/cli/subscription.d.ts.map +1 -0
  62. package/dist/cli/transcription-catalog.d.ts +35 -0
  63. package/dist/cli/transcription-catalog.d.ts.map +1 -0
  64. package/dist/cli/transcription-catalog.js +192 -0
  65. package/dist/cli/transcription-generate.d.ts +62 -0
  66. package/dist/cli/transcription-generate.d.ts.map +1 -0
  67. package/dist/cli/transcription-generate.js +144 -0
  68. package/dist/cli/transcription-models.d.ts +9 -0
  69. package/dist/cli/transcription-models.d.ts.map +1 -0
  70. package/dist/cli/transcription-models.js +100 -0
  71. package/dist/cli/transcription-providers.d.ts +22 -0
  72. package/dist/cli/transcription-providers.d.ts.map +1 -0
  73. package/dist/cli/transcription-providers.js +290 -0
  74. package/dist/cli/video-model-catalog.d.ts.map +1 -0
  75. package/dist/cli/xai-auth.d.ts.map +1 -0
  76. package/dist/{xai-auth.js → cli/xai-auth.js} +1 -1
  77. package/dist/vite/angled-screen.d.ts +57 -0
  78. package/dist/vite/angled-screen.d.ts.map +1 -0
  79. package/dist/vite/angled-screen.js +115 -0
  80. package/dist/vite/app.d.ts +51 -0
  81. package/dist/vite/app.d.ts.map +1 -0
  82. package/dist/vite/app.js +150 -0
  83. package/dist/vite/bands-shader.d.ts +47 -0
  84. package/dist/vite/bands-shader.d.ts.map +1 -0
  85. package/dist/vite/bands-shader.js +267 -0
  86. package/dist/vite/code-block.d.ts +107 -0
  87. package/dist/vite/code-block.d.ts.map +1 -0
  88. package/dist/vite/code-block.js +641 -0
  89. package/dist/vite/components.d.ts +36 -0
  90. package/dist/vite/components.d.ts.map +1 -0
  91. package/dist/vite/components.js +113 -0
  92. package/dist/vite/dispersion-rings-shader.d.ts +44 -0
  93. package/dist/vite/dispersion-rings-shader.d.ts.map +1 -0
  94. package/dist/vite/dispersion-rings-shader.js +282 -0
  95. package/dist/vite/easing-curves.d.ts +129 -0
  96. package/dist/vite/easing-curves.d.ts.map +1 -0
  97. package/dist/vite/easing-curves.js +584 -0
  98. package/dist/vite/easing-curves.test.d.ts +2 -0
  99. package/dist/vite/easing-curves.test.d.ts.map +1 -0
  100. package/dist/vite/easing-curves.test.js +200 -0
  101. package/dist/vite/generated-media-client.d.ts +35 -0
  102. package/dist/vite/generated-media-client.d.ts.map +1 -0
  103. package/dist/vite/generated-media-client.js +76 -0
  104. package/dist/vite/keyframes.d.ts +129 -0
  105. package/dist/vite/keyframes.d.ts.map +1 -0
  106. package/dist/vite/keyframes.js +194 -0
  107. package/dist/vite/layout-editor.d.ts +45 -0
  108. package/dist/vite/layout-editor.d.ts.map +1 -0
  109. package/dist/vite/layout-editor.js +587 -0
  110. package/dist/vite/layout-transition.d.ts +136 -0
  111. package/dist/vite/layout-transition.d.ts.map +1 -0
  112. package/dist/vite/layout-transition.js +581 -0
  113. package/dist/vite/liquid-gradient-shader.d.ts +62 -0
  114. package/dist/vite/liquid-gradient-shader.d.ts.map +1 -0
  115. package/dist/vite/liquid-gradient-shader.js +413 -0
  116. package/dist/vite/mdx-client.d.ts +15 -0
  117. package/dist/vite/mdx-client.d.ts.map +1 -0
  118. package/dist/vite/mdx-client.js +265 -0
  119. package/dist/vite/mdx-parse.d.ts +102 -0
  120. package/dist/vite/mdx-parse.d.ts.map +1 -0
  121. package/dist/vite/mdx-parse.js +251 -0
  122. package/dist/vite/mdx-provided-components.d.ts +18 -0
  123. package/dist/vite/mdx-provided-components.d.ts.map +1 -0
  124. package/dist/vite/mdx-provided-components.js +14 -0
  125. package/dist/vite/mdx-video.d.ts +327 -0
  126. package/dist/vite/mdx-video.d.ts.map +1 -0
  127. package/dist/vite/mdx-video.js +596 -0
  128. package/dist/vite/mdx-video.test.d.ts +9 -0
  129. package/dist/vite/mdx-video.test.d.ts.map +1 -0
  130. package/dist/vite/mdx-video.test.js +1945 -0
  131. package/dist/vite/media-components.d.ts +45 -0
  132. package/dist/vite/media-components.d.ts.map +1 -0
  133. package/dist/vite/media-components.js +355 -0
  134. package/dist/vite/media-duration-store.d.ts +66 -0
  135. package/dist/vite/media-duration-store.d.ts.map +1 -0
  136. package/dist/vite/media-duration-store.js +204 -0
  137. package/dist/vite/player-page.d.ts +35 -0
  138. package/dist/vite/player-page.d.ts.map +1 -0
  139. package/dist/vite/player-page.js +720 -0
  140. package/dist/vite/render-client.d.ts +18 -0
  141. package/dist/vite/render-client.d.ts.map +1 -0
  142. package/dist/vite/render-client.js +67 -0
  143. package/dist/vite/sdk.d.ts +221 -0
  144. package/dist/vite/sdk.d.ts.map +1 -0
  145. package/dist/vite/sdk.js +355 -0
  146. package/dist/vite/server-components.d.ts +55 -0
  147. package/dist/vite/server-components.d.ts.map +1 -0
  148. package/dist/vite/server-components.js +156 -0
  149. package/dist/vite/server-mdx.d.ts +74 -0
  150. package/dist/vite/server-mdx.d.ts.map +1 -0
  151. package/dist/vite/server-mdx.js +259 -0
  152. package/dist/vite/shader-renderer.d.ts +48 -0
  153. package/dist/vite/shader-renderer.d.ts.map +1 -0
  154. package/dist/vite/shader-renderer.js +411 -0
  155. package/dist/vite/store-hooks.d.ts +12 -0
  156. package/dist/vite/store-hooks.d.ts.map +1 -0
  157. package/dist/vite/store-hooks.js +93 -0
  158. package/dist/vite/store.d.ts +75 -0
  159. package/dist/vite/store.d.ts.map +1 -0
  160. package/dist/vite/store.js +52 -0
  161. package/dist/vite/store.test.d.ts +6 -0
  162. package/dist/vite/store.test.d.ts.map +1 -0
  163. package/dist/vite/store.test.js +172 -0
  164. package/dist/vite/tweakpane-hook.d.ts +94 -0
  165. package/dist/vite/tweakpane-hook.d.ts.map +1 -0
  166. package/dist/vite/tweakpane-hook.js +450 -0
  167. package/dist/vite/vite-plugin.d.ts +28 -0
  168. package/dist/vite/vite-plugin.d.ts.map +1 -0
  169. package/dist/vite/vite-plugin.js +539 -0
  170. package/dist/vite/wave-gradient-shader.d.ts +41 -0
  171. package/dist/vite/wave-gradient-shader.d.ts.map +1 -0
  172. package/dist/vite/wave-gradient-shader.js +207 -0
  173. package/package.json +101 -24
  174. package/skills/egaki/SKILL.md +162 -1
  175. package/src/cli/cache-utils.ts +169 -0
  176. package/src/cli/cached-generate.test.ts +531 -0
  177. package/src/cli/cached-generate.ts +348 -0
  178. package/src/cli/cartesia-provider.ts +249 -0
  179. package/src/{cli.ts → cli/cli.ts} +836 -884
  180. package/src/cli/clone-generate.ts +202 -0
  181. package/src/{credentials.ts → cli/credentials.ts} +20 -0
  182. package/src/cli/elevenlabs-speech-provider.ts +243 -0
  183. package/src/cli/generate.test.ts +166 -0
  184. package/src/cli/generate.ts +912 -0
  185. package/src/cli/main.ts +8 -0
  186. package/src/{model-catalog.ts → cli/model-catalog.ts} +787 -58
  187. package/src/{models.ts → cli/models.ts} +65 -86
  188. package/src/cli/openai-speech-provider.ts +34 -0
  189. package/src/cli/separation-generate.ts +187 -0
  190. package/src/cli/speech-catalog.ts +253 -0
  191. package/src/cli/speech-generate.ts +275 -0
  192. package/src/cli/speech-models.ts +146 -0
  193. package/src/cli/speech-provider.test.ts +226 -0
  194. package/src/cli/transcription-catalog.ts +250 -0
  195. package/src/cli/transcription-generate.ts +214 -0
  196. package/src/cli/transcription-models.ts +136 -0
  197. package/src/cli/transcription-providers.ts +427 -0
  198. package/src/{xai-auth.ts → cli/xai-auth.ts} +1 -1
  199. package/src/vite/angled-screen.tsx +214 -0
  200. package/src/vite/app.tsx +197 -0
  201. package/src/vite/bands-shader.tsx +319 -0
  202. package/src/vite/code-block.tsx +939 -0
  203. package/src/vite/components.tsx +278 -0
  204. package/src/vite/dispersion-rings-shader.tsx +331 -0
  205. package/src/vite/easing-curves.test.ts +232 -0
  206. package/src/vite/easing-curves.ts +743 -0
  207. package/src/vite/generated-media-client.tsx +121 -0
  208. package/src/vite/keyframes.tsx +347 -0
  209. package/src/vite/layout-editor.tsx +709 -0
  210. package/src/vite/layout-transition.tsx +706 -0
  211. package/src/vite/liquid-gradient-shader.tsx +474 -0
  212. package/src/vite/mdx-client.tsx +420 -0
  213. package/src/vite/mdx-parse.ts +343 -0
  214. package/src/vite/mdx-provided-components.ts +19 -0
  215. package/src/vite/mdx-video.test.tsx +2219 -0
  216. package/src/vite/mdx-video.tsx +976 -0
  217. package/src/vite/media-components.tsx +473 -0
  218. package/src/vite/media-duration-store.ts +225 -0
  219. package/src/vite/motion-timing.ts +194 -0
  220. package/src/vite/player-page.tsx +1107 -0
  221. package/src/vite/render-client.ts +84 -0
  222. package/src/vite/sdk.ts +598 -0
  223. package/src/vite/server-components.tsx +241 -0
  224. package/src/vite/server-mdx.ts +285 -0
  225. package/src/vite/shader-renderer.tsx +535 -0
  226. package/src/vite/store-hooks.ts +117 -0
  227. package/src/vite/store.test.ts +202 -0
  228. package/src/vite/store.ts +115 -0
  229. package/src/vite/styles.css +21 -0
  230. package/src/vite/tweakpane-hook.tsx +593 -0
  231. package/src/vite/virtual-modules.d.ts +30 -0
  232. package/src/vite/vite-plugin.ts +585 -0
  233. package/src/vite/wave-gradient-shader.tsx +253 -0
  234. package/README.md +0 -370
  235. package/dist/antigravity-auth.d.ts +0 -15
  236. package/dist/antigravity-auth.d.ts.map +0 -1
  237. package/dist/antigravity-auth.js +0 -544
  238. package/dist/antigravity-image.d.ts +0 -11
  239. package/dist/antigravity-image.d.ts.map +0 -1
  240. package/dist/antigravity-image.js +0 -285
  241. package/dist/chatgpt-auth.d.ts.map +0 -1
  242. package/dist/cli.d.ts +0 -3
  243. package/dist/cli.d.ts.map +0 -1
  244. package/dist/credentials.d.ts.map +0 -1
  245. package/dist/gateway-model-candidates.generated.d.ts +0 -11
  246. package/dist/gateway-model-candidates.generated.d.ts.map +0 -1
  247. package/dist/gateway-model-candidates.generated.js +0 -64
  248. package/dist/login.d.ts.map +0 -1
  249. package/dist/model-catalog.d.ts.map +0 -1
  250. package/dist/models.d.ts.map +0 -1
  251. package/dist/open-browser.d.ts.map +0 -1
  252. package/dist/subscription.d.ts.map +0 -1
  253. package/dist/video-model-catalog.d.ts.map +0 -1
  254. package/dist/xai-auth.d.ts.map +0 -1
  255. /package/dist/{chatgpt-auth.d.ts → cli/chatgpt-auth.d.ts} +0 -0
  256. /package/dist/{chatgpt-auth.js → cli/chatgpt-auth.js} +0 -0
  257. /package/dist/{credentials.d.ts → cli/credentials.d.ts} +0 -0
  258. /package/dist/{login.d.ts → cli/login.d.ts} +0 -0
  259. /package/dist/{login.js → cli/login.js} +0 -0
  260. /package/dist/{open-browser.d.ts → cli/open-browser.d.ts} +0 -0
  261. /package/dist/{open-browser.js → cli/open-browser.js} +0 -0
  262. /package/dist/{subscription.d.ts → cli/subscription.d.ts} +0 -0
  263. /package/dist/{subscription.js → cli/subscription.js} +0 -0
  264. /package/dist/{video-model-catalog.d.ts → cli/video-model-catalog.d.ts} +0 -0
  265. /package/dist/{video-model-catalog.js → cli/video-model-catalog.js} +0 -0
  266. /package/dist/{xai-auth.d.ts → cli/xai-auth.d.ts} +0 -0
  267. /package/src/{chatgpt-auth.ts → cli/chatgpt-auth.ts} +0 -0
  268. /package/src/{login.ts → cli/login.ts} +0 -0
  269. /package/src/{open-browser.ts → cli/open-browser.ts} +0 -0
  270. /package/src/{subscription.ts → cli/subscription.ts} +0 -0
  271. /package/src/{video-model-catalog.ts → cli/video-model-catalog.ts} +0 -0
@@ -0,0 +1,720 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /**
4
+ * Client component: Remotion Player wrapper + MP4 export UI.
5
+ *
6
+ * Receives pre-rendered JSX sections from the server (via RSC flight).
7
+ * All MDX processing (parsing, module resolution, safe-mdx rendering)
8
+ * is done server-side in app.tsx. This component only handles:
9
+ * - Wrapping sections in Remotion's Series/Sequence composition
10
+ * - Rendering the Player
11
+ * - MP4 export via WebCodecs
12
+ */
13
+ import './styles.css';
14
+ import { Player } from '@remotion/player';
15
+ import React, { Suspense, useCallback, useContext, useEffect, useLayoutEffect, useRef, useSyncExternalStore, useState } from 'react';
16
+ import { AbsoluteFill, Freeze, Sequence, Series, useCurrentFrame, useDelayRender, useVideoConfig, } from 'remotion';
17
+ import { renderInBrowser } from "./render-client.js";
18
+ import { createSpiceflowFetch } from 'spiceflow/client';
19
+ import { egakiStore } from "./store.js";
20
+ import { useGenerationStatus, useGenerationErrors } from "./store-hooks.js";
21
+ import { egakiSDK } from "./sdk.js";
22
+ import { LayoutEditor } from "./layout-editor.js";
23
+ import { TweakpaneRoot, SIDEBAR_WIDTH } from "./tweakpane-hook.js";
24
+ import { Fill, LayoutAnimationLayer, LayoutGhost, LayoutTransitionProvider, MotionTimingSync, ServerSlotsContext, } from "./mdx-video.js";
25
+ import { SectionIndexContext } from "./media-duration-store.js";
26
+ // ---------------------------------------------------------------------------
27
+ // HMR scene-changed: store pending seek at module scope so it survives
28
+ // component remounts caused by rsc:update (spiceflow payload swap).
29
+ // The vite plugin sends egaki:scene-changed with { sectionIndex } when
30
+ // the user edits a specific section of the entry MDX.
31
+ // ---------------------------------------------------------------------------
32
+ let pendingSceneSeek = null;
33
+ /** Cancels the auto-pause listener from a previous scene-seek, so a new
34
+ * HMR during playback doesn't leave stale listeners that pause the wrong scene. */
35
+ let cancelPreviousAutoPlay = null;
36
+ /** Monotonic counter to invalidate stale requestAnimationFrame callbacks
37
+ * when rapid successive HMR edits arrive before the first rAF fires. */
38
+ let sceneSeekVersion = 0;
39
+ // ---------------------------------------------------------------------------
40
+ // Generation progress client — fetches /api/generation-progress on page
41
+ // load and on every rsc:update using the typed Spiceflow fetch client.
42
+ // The route is an async generator that yields progress events as SSE;
43
+ // createSpiceflowFetch returns an AsyncGenerator we consume with for-await.
44
+ // Max one connection at a time; new rsc:update aborts the previous one.
45
+ // ---------------------------------------------------------------------------
46
+ let progressAbortController = null;
47
+ function connectToProgress() {
48
+ if (typeof window === 'undefined')
49
+ return;
50
+ progressAbortController?.abort();
51
+ const controller = new AbortController();
52
+ progressAbortController = controller;
53
+ // Lazy-init: createSpiceflowFetch needs window.location.origin which
54
+ // is only available in the browser, not during SSR.
55
+ const safeFetch = createSpiceflowFetch(window.location.origin);
56
+ (async () => {
57
+ const result = await safeFetch('/api/generation-progress', {
58
+ signal: controller.signal,
59
+ });
60
+ if (result instanceof Error)
61
+ return;
62
+ // result is an AsyncGenerator<GenerationProgressEvent> from the streaming route
63
+ try {
64
+ for await (const event of result) {
65
+ // Guard: if a newer connection replaced us, stop writing state
66
+ if (progressAbortController !== controller)
67
+ return;
68
+ // Log generation errors to the console and show them in the toolbar
69
+ if (event.errors?.length) {
70
+ for (const err of event.errors) {
71
+ console.error(`[egaki] ${err.namespace} generation failed (${Math.round(err.durationMs / 1000)}s): ${err.error}`);
72
+ }
73
+ // Append new errors with unique IDs and auto-clear them after 8 seconds
74
+ const errorIds = new Set();
75
+ const tagged = event.errors.map((e) => {
76
+ const id = `${e.key}:${Date.now()}:${Math.random()}`;
77
+ errorIds.add(id);
78
+ return { ...e, _id: id };
79
+ });
80
+ const currentErrors = egakiStore.getState().serverGenerationErrors;
81
+ egakiStore.setState({ serverGenerationErrors: [...currentErrors, ...tagged] });
82
+ setTimeout(() => {
83
+ egakiStore.setState({
84
+ serverGenerationErrors: egakiStore.getState().serverGenerationErrors.filter((e) => !errorIds.has(e._id)),
85
+ });
86
+ }, 8000);
87
+ }
88
+ const summary = event.summary;
89
+ egakiStore.setState({
90
+ serverGenerationStatus: summary.total > 0 ? summary : null,
91
+ serverGenerationEntries: event.generations,
92
+ });
93
+ if (event.done)
94
+ break;
95
+ }
96
+ }
97
+ catch {
98
+ // Aborted or network error — non-fatal
99
+ }
100
+ // Guard: only clear state if we're still the active connection
101
+ if (progressAbortController === controller) {
102
+ egakiStore.setState({ serverGenerationStatus: null, serverGenerationEntries: [] });
103
+ }
104
+ })();
105
+ }
106
+ // Connect on initial page load (guarded for SSR)
107
+ if (typeof window !== 'undefined')
108
+ connectToProgress();
109
+ if (import.meta.hot) {
110
+ import.meta.hot.on('egaki:scene-changed', (data) => {
111
+ pendingSceneSeek = data;
112
+ });
113
+ // Reconnect on rsc:update — new generations may have started from
114
+ // MDX edits that trigger server-side rendering of <Server> blocks.
115
+ import.meta.hot.on('rsc:update', () => {
116
+ // Small delay to let the server start processing before we connect,
117
+ // so the first SSE event already includes the new generations.
118
+ setTimeout(connectToProgress, 100);
119
+ });
120
+ }
121
+ // Module-level stable callbacks for useSyncExternalStore (never re-subscribes)
122
+ const subscribeNoop = () => () => { };
123
+ const getClientMounted = () => true;
124
+ const getServerMounted = () => false;
125
+ const PLAYBACK_RATES = [0.5, 1, 1.5, 2, 4, 8];
126
+ function ToolbarSeparator() {
127
+ return _jsx("div", { className: 'w-px h-4 bg-white/15' });
128
+ }
129
+ function DownloadIcon() {
130
+ return (_jsxs("svg", { width: '14', height: '14', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round', children: [_jsx("path", { d: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4' }), _jsx("polyline", { points: '7 10 12 15 17 10' }), _jsx("line", { x1: '12', y1: '15', x2: '12', y2: '3' })] }));
131
+ }
132
+ function XIcon() {
133
+ return (_jsxs("svg", { width: '14', height: '14', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round', children: [_jsx("line", { x1: '18', y1: '6', x2: '6', y2: '18' }), _jsx("line", { x1: '6', y1: '6', x2: '18', y2: '18' })] }));
134
+ }
135
+ function ChevronUpIcon() {
136
+ return (_jsx("svg", { width: '14', height: '14', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round', children: _jsx("polyline", { points: '18 15 12 9 6 15' }) }));
137
+ }
138
+ function ChevronDownIcon() {
139
+ return (_jsx("svg", { width: '14', height: '14', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round', children: _jsx("polyline", { points: '6 9 12 15 18 9' }) }));
140
+ }
141
+ function SpinnerIcon() {
142
+ return (_jsx("svg", { width: '14', height: '14', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round', className: 'animate-spin', children: _jsx("path", { d: 'M21 12a9 9 0 1 1-6.219-8.56' }) }));
143
+ }
144
+ function formatGenerationStatus(status) {
145
+ const parts = [];
146
+ for (const [namespace, count] of Object.entries(status.counts)) {
147
+ if (count <= 0)
148
+ continue;
149
+ // Pluralize: add 's' for most, 'es' for speech
150
+ const plural = count > 1 ? (namespace === 'speech' ? 'es' : 's') : '';
151
+ parts.push(`${count} ${namespace}${plural}`);
152
+ }
153
+ return `Generating ${parts.join(', ')}`;
154
+ }
155
+ /**
156
+ * Remotion-aware Suspense fallback. When a section suspends (throws a promise),
157
+ * this component calls delayRender() to prevent Remotion from taking a
158
+ * screenshot of the incomplete frame. When the suspended component resolves
159
+ * and this fallback unmounts, continueRender() fires and rendering proceeds.
160
+ *
161
+ * This is the same pattern Remotion uses internally in <Composition> (see
162
+ * packages/core/src/Composition.tsx in the Remotion source).
163
+ */
164
+ function SuspenseFallback() {
165
+ const { delayRender, continueRender } = useDelayRender();
166
+ // useLayoutEffect registers the delay handle before paint, preventing
167
+ // the renderer from capturing a frame before the handle exists.
168
+ useLayoutEffect(() => {
169
+ const handle = delayRender('Waiting for section to unsuspend', {
170
+ timeoutInMilliseconds: 10 * 60 * 1000,
171
+ });
172
+ return () => continueRender(handle);
173
+ }, [delayRender, continueRender]);
174
+ // delayRender is a no-op in the Player, so this loading UI is only
175
+ // visible during preview. During export, rendering pauses until the
176
+ // suspended component resolves, then this fallback unmounts before
177
+ // the screenshot is taken.
178
+ return (_jsx(AbsoluteFill, { style: {
179
+ display: 'flex',
180
+ alignItems: 'center',
181
+ justifyContent: 'center',
182
+ background: '#050505',
183
+ }, children: _jsx("span", { style: {
184
+ fontSize: 48,
185
+ fontWeight: 500,
186
+ color: '#52525b',
187
+ fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", sans-serif',
188
+ letterSpacing: '-0.01em',
189
+ }, children: "Loading\u2026" }) }));
190
+ }
191
+ // Shared by the visible section content and the hidden ghost copy of the
192
+ // previous section — element positions only match across the two containers
193
+ // if both use the exact same layout styles.
194
+ const SECTION_CONTENT_STYLE = {
195
+ zIndex: 1,
196
+ gap: 'clamp(1rem, 2vw, 2.5rem)',
197
+ // Force Chrome GPU compositing for subpixel text rendering.
198
+ // Without this, Chrome snaps text positions to whole pixels
199
+ // causing visible stutter on slow translate/scale animations.
200
+ // Not supported by the web-renderer canvas export, but the
201
+ // canvas renderer doesn't have Chrome's pixel snapping issue.
202
+ // See: https://remotion.dev/docs/troubleshooting/subpixel-rendering
203
+ perspective: '1000px',
204
+ willChange: 'transform',
205
+ // Grayscale antialiasing instead of subpixel. Subpixel rendering adds
206
+ // RGB color fringing at text edges which creates shimmering artifacts
207
+ // when text moves or scales in video. Grayscale is also consistent
208
+ // across platforms (subpixel varies by OS and display).
209
+ WebkitFontSmoothing: 'antialiased',
210
+ MozOsxFontSmoothing: 'grayscale',
211
+ };
212
+ // How long (in seconds) the hidden ghost copy of the previous section stays
213
+ // mounted at the start of a section. LayoutTransition springs (default 20
214
+ // frames) settle well within this window. A constant is used because
215
+ // per-element durations are only known after children render — the ghost
216
+ // mount decision happens before that.
217
+ const GHOST_WINDOW_SECONDS = 3;
218
+ // How many seconds before a section cut to premount the next section.
219
+ // Remotion's premountFor renders the next sequence early (hidden) so
220
+ // <Video> elements start loading before the cut, preventing stutter.
221
+ const PREMOUNT_SECONDS = 1;
222
+ /**
223
+ * Section content wrapper enabling <LayoutTransition> FLIP animations
224
+ * across section boundaries.
225
+ *
226
+ * Renders the PREVIOUS section in a hidden ghost container, pinned at its
227
+ * last frame via <Freeze>, so LayoutAnimationLayer can measure where each
228
+ * LayoutTransition element was when the viewer last saw it. Seek-safe: the
229
+ * ghost is derived purely from the current frame, never from playback order.
230
+ *
231
+ * Known limitation: the ghost's useVideoConfig().durationInFrames is clamped
232
+ * by the CURRENT section's Series.Sequence (Remotion Sequences take
233
+ * min(parent, own) duration). Exit animations in the previous section that
234
+ * depend on durationInFrames measure slightly off when the current section
235
+ * is shorter than the previous one. Position measurement is unaffected for
236
+ * static layouts.
237
+ */
238
+ function SectionWithLayoutTransition({ jsx, prevJsx, prevDurationInFrames, }) {
239
+ const frame = useCurrentFrame();
240
+ const { fps } = useVideoConfig();
241
+ const showGhost = prevJsx != null && frame < fps * GHOST_WINDOW_SECONDS;
242
+ return (_jsx(LayoutTransitionProvider, { children: _jsxs(AbsoluteFill, { style: { background: 'transparent' }, children: [showGhost ? (
243
+ // Ghost: hidden but laid out (visibility:hidden keeps geometry).
244
+ // opacity:0 is added for web-renderer safety, and the ghost comes
245
+ // FIRST in DOM order so the visible section paints over it even
246
+ // if a renderer ignores visibility (no z-index in web-renderer).
247
+ _jsx(AbsoluteFill, { style: { visibility: 'hidden', opacity: 0, pointerEvents: 'none' }, "aria-hidden": true, children: _jsx(LayoutGhost, { children: _jsx(Freeze, { frame: prevDurationInFrames - 1, children: _jsx(Sequence, { from: 0, durationInFrames: prevDurationInFrames, layout: "none", showInTimeline: false, children: _jsx(Fill, { style: SECTION_CONTENT_STYLE, children: prevJsx }) }) }) }) })) : null, _jsx(MotionTimingSync, { children: _jsx(Fill, { style: SECTION_CONTENT_STYLE, children: jsx }) }), _jsx(LayoutAnimationLayer, {})] }) }));
248
+ }
249
+ function VideoComposition({ sections, totalDuration, preamble, }) {
250
+ const { fps } = useVideoConfig();
251
+ const premountFrames = Math.round(fps * PREMOUNT_SECONDS);
252
+ return (_jsxs(AbsoluteFill, { style: { background: '#050505', fontSize: 60 }, children: [_jsx(MotionTimingSync, { children: preamble }), _jsx(Series, { children: sections.map((section, i) => (_jsx(Series.Sequence, { durationInFrames: section.durationInFrames,
253
+ // @ts-ignore — name prop exists on Series.Sequence
254
+ name: section.heading || `Section ${i}`,
255
+ // Premount the next section N seconds early so <Video> elements
256
+ // start loading before the cut, preventing playback stutter.
257
+ // Remotion renders the premounted section hidden; it receives
258
+ // its own correct frame context (starting at frame 0).
259
+ premountFor: premountFrames, children: _jsx(SectionIndexContext.Provider, { value: i, children: _jsx(Suspense, { fallback: _jsx(SuspenseFallback, {}), children: _jsx(SectionWithLayoutTransition, { jsx: section.jsx, prevJsx: i > 0 ? sections[i - 1].jsx : null, prevDurationInFrames: i > 0 ? sections[i - 1].durationInFrames : 0 }) }) }) }, i))) })] }));
260
+ }
261
+ export function PlayerPage({ sections, totalDuration, preamble, entryPath, hasUnresolvedDurations = false, frontmatter, availableEntries = [], currentRoute = '', }) {
262
+ const { fps, width, height, scale } = frontmatter;
263
+ const serverSlots = useContext(ServerSlotsContext);
264
+ // Stable component function that reads latest props from a ref.
265
+ // Created once so its identity never changes between renders.
266
+ // Remotion Player doesn't remount when component identity is stable.
267
+ const propsRef = useRef({ sections, totalDuration, preamble, serverSlots });
268
+ propsRef.current = { sections, totalDuration, preamble, serverSlots };
269
+ // Wrap VideoComposition with ServerSlotsContext so that <Server> slots
270
+ // are available during SDK export/screenshot (renderStillOnWeb /
271
+ // renderMediaOnWeb create a fresh React tree from this component).
272
+ const [Component] = useState(() => () => (_jsx(ServerSlotsContext.Provider, { value: propsRef.current.serverSlots, children: _jsx(VideoComposition, { ...propsRef.current }) })));
273
+ const playerRef = useRef(null);
274
+ const playerContainerRef = useRef(null);
275
+ // Register the composition with the SDK so agents can call
276
+ // window.egakiSDK.seekTo() / .screenshot() / .export() via Playwriter.
277
+ useEffect(() => {
278
+ egakiSDK.register({
279
+ component: Component,
280
+ totalDuration,
281
+ fps,
282
+ width,
283
+ height,
284
+ scale,
285
+ sectionCount: sections.length,
286
+ sections: sections.map((s) => ({
287
+ heading: s.heading,
288
+ durationInFrames: s.durationInFrames,
289
+ })),
290
+ playerRef,
291
+ playerContainerRef,
292
+ });
293
+ }, [Component, totalDuration, sections, fps, width, height, scale]);
294
+ // Consume pending scene seek after the RSC refetch delivers new sections.
295
+ // The module-level pendingSceneSeek is set by import.meta.hot.on before
296
+ // React re-renders; this effect fires after the commit with fresh data.
297
+ // Seeks to the changed section's start frame, plays it, and pauses at
298
+ // the section's end so the user sees exactly the content they edited.
299
+ useEffect(() => {
300
+ if (pendingSceneSeek == null)
301
+ return;
302
+ // Ignore scene changes from a different entry MDX file.
303
+ if (pendingSceneSeek.file !== entryPath) {
304
+ pendingSceneSeek = null;
305
+ return;
306
+ }
307
+ const idx = pendingSceneSeek.sectionIndex;
308
+ pendingSceneSeek = null;
309
+ // Wait one frame for the Remotion Player to initialize after remount.
310
+ // Version token ensures stale rAF callbacks from rapid edits are ignored.
311
+ const version = ++sceneSeekVersion;
312
+ requestAnimationFrame(() => {
313
+ if (version !== sceneSeekVersion)
314
+ return;
315
+ const player = playerRef.current;
316
+ if (!player || idx < 0 || idx >= sections.length)
317
+ return;
318
+ // Compute section boundaries from current sections
319
+ let startFrame = 0;
320
+ for (let i = 0; i < idx; i++) {
321
+ startFrame += sections[i].durationInFrames;
322
+ }
323
+ const endFrame = startFrame + sections[idx].durationInFrames - 1;
324
+ player.seekTo(startFrame);
325
+ // Cancel any auto-pause from a previous HMR scene-seek so stale
326
+ // listeners don't pause playback in the wrong section.
327
+ cancelPreviousAutoPlay?.();
328
+ cancelPreviousAutoPlay = null;
329
+ // Only auto-play if currently paused (after rsc:update remount
330
+ // the player always starts paused at frame 0)
331
+ if (!player.isPlaying()) {
332
+ player.play();
333
+ // Pause at section end so only the edited section plays.
334
+ // If the user manually seeks (clicks timeline, arrow keys, etc.)
335
+ // cancel the auto-pause so we don't randomly stop playback later.
336
+ let cancelled = false;
337
+ const cleanup = () => {
338
+ if (cancelled)
339
+ return;
340
+ cancelled = true;
341
+ cancelPreviousAutoPlay = null;
342
+ player.removeEventListener('frameupdate', onFrame);
343
+ player.removeEventListener('seeked', onSeek);
344
+ };
345
+ cancelPreviousAutoPlay = cleanup;
346
+ const onFrame = () => {
347
+ if (cancelled)
348
+ return;
349
+ if (player.getCurrentFrame() >= endFrame) {
350
+ player.pause();
351
+ cleanup();
352
+ }
353
+ };
354
+ const onSeek = (e) => {
355
+ // User seeked away — cancel auto-pause. Ignore seeks within
356
+ // the target section range (could be our own seekTo settling).
357
+ const frame = e?.detail?.frame ?? player.getCurrentFrame();
358
+ if (frame < startFrame || frame > endFrame) {
359
+ cleanup();
360
+ }
361
+ };
362
+ player.addEventListener('frameupdate', onFrame);
363
+ player.addEventListener('seeked', onSeek);
364
+ }
365
+ });
366
+ }, [sections]);
367
+ // Defer Player mount to client only. Remotion's Player and all composition
368
+ // components (Series, AbsoluteFill, Audio, Video) use React context provided
369
+ // by Player at runtime. During SSR there's no Remotion context, so hooks like
370
+ // useCurrentFrame/useVideoConfig crash with "Cannot read properties of null
371
+ // (reading 'useContext')". useSyncExternalStore returns false on the server
372
+ // and true on the client synchronously during hydration (no useEffect tick).
373
+ const mounted = useSyncExternalStore(subscribeNoop, getClientMounted, getServerMounted);
374
+ const generationStatus = useGenerationStatus();
375
+ const generationErrors = useGenerationErrors();
376
+ const [editing, setEditing] = useState(false);
377
+ const [resetKey, setResetKey] = useState(0);
378
+ const [rendering, setRendering] = useState(false);
379
+ const [progress, setProgress] = useState(0);
380
+ const abortRef = useRef(null);
381
+ const [playbackRate, setPlaybackRate] = useState(1);
382
+ const [currentFrame, setCurrentFrame] = useState(0);
383
+ // Track current frame for scene navigation button states
384
+ useEffect(() => {
385
+ const player = playerRef.current;
386
+ if (!player)
387
+ return;
388
+ const onFrame = () => setCurrentFrame(player.getCurrentFrame());
389
+ // Remotion Player fires 'frameupdate' on seek and during playback
390
+ player.addEventListener('frameupdate', onFrame);
391
+ return () => player.removeEventListener('frameupdate', onFrame);
392
+ }, [mounted]);
393
+ // Precompute section start frames for navigation
394
+ const sectionStarts = [];
395
+ {
396
+ let acc = 0;
397
+ for (const s of sections) {
398
+ sectionStarts.push(acc);
399
+ acc += s.durationInFrames;
400
+ }
401
+ }
402
+ // Determine which section the current frame is in
403
+ let currentSectionIdx = 0;
404
+ for (let i = sectionStarts.length - 1; i >= 0; i--) {
405
+ if (currentFrame >= sectionStarts[i]) {
406
+ currentSectionIdx = i;
407
+ break;
408
+ }
409
+ }
410
+ const hasPrevScene = currentSectionIdx > 0 || currentFrame > 0;
411
+ const hasNextScene = currentSectionIdx < sections.length - 1;
412
+ // Single tap: seek to start of current section.
413
+ // Double tap (within 400ms): jump to previous section start. Each
414
+ // additional rapid tap keeps going one section further back.
415
+ const lastPrevTapRef = useRef({ time: 0, targetFrame: -1 });
416
+ const goToPrevScene = useCallback(() => {
417
+ const player = playerRef.current;
418
+ if (!player)
419
+ return;
420
+ const wasPlaying = player.isPlaying();
421
+ if (wasPlaying)
422
+ player.pause();
423
+ const frame = player.getCurrentFrame();
424
+ // Find the start of the current section (largest sectionStart <= frame)
425
+ let currentStart = 0;
426
+ for (let i = sectionStarts.length - 1; i >= 0; i--) {
427
+ if (sectionStarts[i] <= frame) {
428
+ currentStart = sectionStarts[i];
429
+ break;
430
+ }
431
+ }
432
+ const now = Date.now();
433
+ const last = lastPrevTapRef.current;
434
+ // Double-tap: if pressed again within 400ms, go to the section before
435
+ // the last target (so repeated taps keep stepping backwards)
436
+ if (now - last.time < 400) {
437
+ let prevTarget = 0;
438
+ for (let i = sectionStarts.length - 1; i >= 0; i--) {
439
+ if (sectionStarts[i] < last.targetFrame) {
440
+ prevTarget = sectionStarts[i];
441
+ break;
442
+ }
443
+ }
444
+ lastPrevTapRef.current = { time: now, targetFrame: prevTarget };
445
+ player.seekTo(prevTarget);
446
+ }
447
+ else {
448
+ // Single tap: go to start of current section
449
+ lastPrevTapRef.current = { time: now, targetFrame: currentStart };
450
+ player.seekTo(currentStart);
451
+ }
452
+ if (wasPlaying)
453
+ player.play();
454
+ }, [sectionStarts]);
455
+ const goToNextScene = useCallback(() => {
456
+ const player = playerRef.current;
457
+ if (!player)
458
+ return;
459
+ const wasPlaying = player.isPlaying();
460
+ if (wasPlaying)
461
+ player.pause();
462
+ const frame = player.getCurrentFrame();
463
+ let target = totalDuration - 1;
464
+ for (const start of sectionStarts) {
465
+ if (start > frame) {
466
+ target = start;
467
+ break;
468
+ }
469
+ }
470
+ player.seekTo(target);
471
+ if (wasPlaying)
472
+ player.play();
473
+ }, [sectionStarts, totalDuration]);
474
+ // Sync playbackRate state when user changes it via the gear menu
475
+ useEffect(() => {
476
+ const player = playerRef.current;
477
+ if (!player)
478
+ return;
479
+ const onRateChange = (e) => {
480
+ setPlaybackRate(e.detail?.playbackRate ?? 1);
481
+ };
482
+ player.addEventListener('ratechange', onRateChange);
483
+ return () => player.removeEventListener('ratechange', onRateChange);
484
+ }, [mounted]);
485
+ // Keyboard shortcuts — modeled after After Effects / Premiere / DaVinci.
486
+ // Disabled when the layout editor is active (it may use arrow keys).
487
+ //
488
+ // Space play/pause
489
+ // Left/Right ±1 frame
490
+ // Shift+Left/Right ±10 frames
491
+ // Down/Up section start (×2 = prev) / next section
492
+ // , / . ±1 frame (Premiere convention)
493
+ // Home / End first / last frame
494
+ // J / K / L rewind 2s / pause / play (L×2=2x, ×3=4x, ×4=8x)
495
+ // 0-9 jump to 0%-90% of timeline
496
+ useEffect(() => {
497
+ if (editing)
498
+ return;
499
+ const clamp = (frame) => Math.max(0, Math.min(totalDuration - 1, frame));
500
+ const onKeyDown = (e) => {
501
+ const player = playerRef.current;
502
+ if (!player)
503
+ return;
504
+ // Ignore when focus is on an input/textarea/contenteditable
505
+ const tag = e.target?.tagName;
506
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || e.target?.isContentEditable)
507
+ return;
508
+ const frame = player.getCurrentFrame();
509
+ // --- Frame stepping ---
510
+ if (e.key === 'ArrowLeft' && !e.shiftKey) {
511
+ e.preventDefault();
512
+ if (player.isPlaying())
513
+ player.pause();
514
+ player.seekTo(clamp(frame - 1));
515
+ return;
516
+ }
517
+ if (e.key === 'ArrowRight' && !e.shiftKey) {
518
+ e.preventDefault();
519
+ if (player.isPlaying())
520
+ player.pause();
521
+ player.seekTo(clamp(frame + 1));
522
+ return;
523
+ }
524
+ if (e.key === ',') {
525
+ e.preventDefault();
526
+ if (player.isPlaying())
527
+ player.pause();
528
+ player.seekTo(clamp(frame - 1));
529
+ return;
530
+ }
531
+ if (e.key === '.') {
532
+ e.preventDefault();
533
+ if (player.isPlaying())
534
+ player.pause();
535
+ player.seekTo(clamp(frame + 1));
536
+ return;
537
+ }
538
+ // --- 10-frame jump (Shift+Arrow) ---
539
+ if (e.key === 'ArrowLeft' && e.shiftKey) {
540
+ e.preventDefault();
541
+ if (player.isPlaying())
542
+ player.pause();
543
+ player.seekTo(clamp(frame - 10));
544
+ return;
545
+ }
546
+ if (e.key === 'ArrowRight' && e.shiftKey) {
547
+ e.preventDefault();
548
+ if (player.isPlaying())
549
+ player.pause();
550
+ player.seekTo(clamp(frame + 10));
551
+ return;
552
+ }
553
+ // --- Section navigation (Down/Up) — delegates to goToPrev/NextScene
554
+ // which preserve playback state and support double-tap prev ---
555
+ if (e.key === 'ArrowDown') {
556
+ e.preventDefault();
557
+ goToPrevScene();
558
+ return;
559
+ }
560
+ if (e.key === 'ArrowUp') {
561
+ e.preventDefault();
562
+ goToNextScene();
563
+ return;
564
+ }
565
+ // --- Space: play/pause ---
566
+ if (e.key === ' ') {
567
+ e.preventDefault();
568
+ if (player.isPlaying())
569
+ player.pause();
570
+ else
571
+ player.play();
572
+ return;
573
+ }
574
+ // --- Home / End ---
575
+ if (e.key === 'Home') {
576
+ e.preventDefault();
577
+ if (player.isPlaying())
578
+ player.pause();
579
+ player.seekTo(0);
580
+ return;
581
+ }
582
+ if (e.key === 'End') {
583
+ e.preventDefault();
584
+ if (player.isPlaying())
585
+ player.pause();
586
+ player.seekTo(totalDuration - 1);
587
+ return;
588
+ }
589
+ // --- J / K / L (NLE transport) ---
590
+ // L = play, tap again to double speed (1→2→4→8 cap)
591
+ // K = pause, reset rate to 1
592
+ // J = rewind 2 seconds (reverse playback is unreliable in browsers)
593
+ if (e.key === 'l' || e.key === 'L') {
594
+ e.preventDefault();
595
+ if (!player.isPlaying()) {
596
+ setPlaybackRate(1);
597
+ player.play();
598
+ }
599
+ else {
600
+ // Cycle to next rate in the list
601
+ setPlaybackRate((prev) => {
602
+ const idx = PLAYBACK_RATES.indexOf(prev);
603
+ return PLAYBACK_RATES[Math.min((idx === -1 ? 1 : idx) + 1, PLAYBACK_RATES.length - 1)];
604
+ });
605
+ }
606
+ return;
607
+ }
608
+ if (e.key === 'k' || e.key === 'K') {
609
+ e.preventDefault();
610
+ player.pause();
611
+ setPlaybackRate(1);
612
+ return;
613
+ }
614
+ if (e.key === 'j' || e.key === 'J') {
615
+ e.preventDefault();
616
+ // Seek backward 2 seconds
617
+ if (player.isPlaying())
618
+ player.pause();
619
+ player.seekTo(clamp(frame - Math.round(fps * 2)));
620
+ return;
621
+ }
622
+ // --- 0-9: jump to percentage of timeline ---
623
+ if (e.key >= '0' && e.key <= '9' && !e.metaKey && !e.ctrlKey && !e.altKey) {
624
+ e.preventDefault();
625
+ const pct = parseInt(e.key, 10) / 10;
626
+ player.seekTo(Math.floor(totalDuration * pct));
627
+ return;
628
+ }
629
+ };
630
+ window.addEventListener('keydown', onKeyDown);
631
+ return () => window.removeEventListener('keydown', onKeyDown);
632
+ }, [editing, totalDuration, sections, goToPrevScene, goToNextScene, fps]);
633
+ const handleExport = useCallback(async () => {
634
+ setRendering(true);
635
+ setProgress(0);
636
+ const controller = new AbortController();
637
+ abortRef.current = controller;
638
+ try {
639
+ const blob = await renderInBrowser({
640
+ component: Component,
641
+ durationInFrames: totalDuration,
642
+ fps,
643
+ width,
644
+ height,
645
+ scale,
646
+ onProgress: (p) => setProgress(p),
647
+ signal: controller.signal,
648
+ });
649
+ const url = URL.createObjectURL(blob);
650
+ const a = document.createElement('a');
651
+ a.href = url;
652
+ a.download = 'video.mp4';
653
+ a.click();
654
+ URL.revokeObjectURL(url);
655
+ }
656
+ catch (err) {
657
+ if (err.message?.includes('cancelled')) {
658
+ console.log('Export cancelled');
659
+ }
660
+ else {
661
+ console.error('Export failed:', err);
662
+ }
663
+ }
664
+ finally {
665
+ setRendering(false);
666
+ abortRef.current = null;
667
+ }
668
+ }, [Component, totalDuration, fps, width, height, scale]);
669
+ const handleCancel = useCallback(() => {
670
+ abortRef.current?.abort();
671
+ }, []);
672
+ return (_jsxs("div", { className: 'flex gap-3 min-h-screen bg-black', children: [_jsx("div", { className: 'flex flex-col items-center justify-center flex-1 min-w-0', style: { paddingBottom: 96, paddingTop: 24 }, children: _jsx("div", { ref: playerContainerRef, style: { maxWidth: `calc((100vh - 120px) * ${width / height})` }, className: 'w-full overflow-hidden', children: mounted ? (_jsx(Player, { ref: playerRef, component: Component, durationInFrames: totalDuration, fps: fps, compositionWidth: width, compositionHeight: height, loop: true, controls: true, clickToPlay: !editing, spaceKeyToPlayOrPause: true, playbackRate: playbackRate, style: { width: '100%' }, errorFallback: ({ error }) => (_jsx(AbsoluteFill, { style: {
673
+ display: 'flex',
674
+ alignItems: 'center',
675
+ justifyContent: 'center',
676
+ background: '#050505',
677
+ padding: '5% 8%',
678
+ }, children: _jsxs("div", { style: {
679
+ display: 'flex',
680
+ flexDirection: 'column',
681
+ alignItems: 'center',
682
+ gap: '1rem',
683
+ maxWidth: '80%',
684
+ textAlign: 'center',
685
+ }, children: [_jsx("span", { style: {
686
+ fontSize: 48,
687
+ fontWeight: 600,
688
+ color: '#ef4444',
689
+ fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", sans-serif',
690
+ letterSpacing: '-0.02em',
691
+ }, children: "Render Error" }), _jsx("pre", { style: {
692
+ fontSize: 14,
693
+ fontWeight: 400,
694
+ color: '#a1a1aa',
695
+ fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
696
+ whiteSpace: 'pre-wrap',
697
+ wordBreak: 'break-word',
698
+ textAlign: 'left',
699
+ maxHeight: '50%',
700
+ overflow: 'auto',
701
+ width: '100%',
702
+ margin: 0,
703
+ lineHeight: 1.5,
704
+ }, children: error.stack || error.message })] }) })) }, resetKey)) : (_jsx("div", { className: 'aspect-video bg-[#050505]' })) }) }), _jsx(TweakpaneRoot, { playerRef: playerRef, fps: fps, sections: sections, entryPath: entryPath }), _jsxs("div", { className: 'fixed bottom-6 flex items-center gap-1.5 rounded-full bg-[#1c1c1c] border border-white/10 px-2 py-1.5 shadow-2xl', style: {
705
+ left: `calc((100% - ${SIDEBAR_WIDTH}px - 0.75rem) / 2)`,
706
+ transform: 'translateX(-50%)',
707
+ }, children: [availableEntries.length > 1 && (_jsxs(_Fragment, { children: [_jsx("select", { value: currentRoute, onChange: (e) => {
708
+ const route = e.target.value;
709
+ const base = import.meta.env.BASE_URL ?? '/';
710
+ window.location.href = `${base}${route}`;
711
+ }, className: 'appearance-none rounded-full px-3 py-1.5 text-[13px] font-medium text-zinc-300 bg-transparent hover:bg-white/5 transition-colors cursor-pointer outline-none', style: { maxWidth: 150 }, children: availableEntries.map((route) => (_jsx("option", { value: route, style: { background: '#1c1c1c', color: '#d4d4d8' }, children: route || '(default)' }, route))) }), _jsx(ToolbarSeparator, {})] })), rendering ? (_jsxs(_Fragment, { children: [_jsxs("button", { onClick: handleCancel, className: 'flex items-center gap-1.5 rounded-full px-3 py-1.5 text-[13px] font-medium text-red-400 hover:bg-red-500/10 transition-colors cursor-pointer', children: [_jsx(XIcon, {}), "Cancel"] }), _jsx(ToolbarSeparator, {}), _jsxs("div", { className: 'flex items-center gap-2.5 px-2', children: [_jsx("div", { className: 'w-24 h-1 rounded-full bg-white/10 overflow-hidden', children: _jsx("div", { className: 'h-full rounded-full bg-sky-400 transition-[width] duration-300', style: { width: `${Math.round(progress * 100)}%` } }) }), _jsxs("span", { className: 'text-[13px] text-zinc-500 tabular-nums', children: [Math.round(progress * 100), "%"] })] })] })) : (_jsxs("button", { onClick: handleExport, className: 'flex items-center gap-1.5 rounded-full px-3 py-1.5 text-[13px] font-medium text-sky-950 bg-sky-200 hover:bg-sky-100 transition-colors cursor-pointer', children: [_jsx(DownloadIcon, {}), "Export MP4"] })), _jsx(ToolbarSeparator, {}), _jsx("button", { onClick: () => {
712
+ setPlaybackRate((prev) => {
713
+ const idx = PLAYBACK_RATES.indexOf(prev);
714
+ const next = PLAYBACK_RATES[(idx + 1) % PLAYBACK_RATES.length];
715
+ return next;
716
+ });
717
+ }, className: 'flex items-center rounded-full px-2.5 py-1.5 text-[13px] font-medium tabular-nums text-zinc-400 hover:text-zinc-200 hover:bg-white/5 transition-colors cursor-pointer min-w-[3rem] justify-center', title: 'Playback speed (click to cycle)', children: playbackRate === 1 ? '1x' : `${playbackRate}x` }), _jsx(ToolbarSeparator, {}), _jsxs("div", { className: 'flex items-center', children: [_jsx("button", { onClick: goToPrevScene, disabled: !hasPrevScene, className: 'flex items-center justify-center rounded-full w-7 h-7 text-zinc-400 hover:text-zinc-200 hover:bg-white/5 transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-default disabled:hover:bg-transparent disabled:hover:text-zinc-400', title: 'Previous scene (\u2193)', children: _jsx(ChevronDownIcon, {}) }), _jsx("button", { onClick: goToNextScene, disabled: !hasNextScene, className: 'flex items-center justify-center rounded-full w-7 h-7 text-zinc-400 hover:text-zinc-200 hover:bg-white/5 transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-default disabled:hover:bg-transparent disabled:hover:text-zinc-400', title: 'Next scene (\u2191)', children: _jsx(ChevronUpIcon, {}) })] }), _jsx(ToolbarSeparator, {}), _jsx(LayoutEditor, { playerContainerRef: playerContainerRef, playerRef: playerRef, editing: editing, onEditingChange: setEditing, onReset: () => setResetKey((k) => k + 1), sections: sections, fps: fps, entryPath: entryPath }), generationStatus && (_jsxs(_Fragment, { children: [_jsx(ToolbarSeparator, {}), _jsxs("div", { className: 'flex items-center gap-1.5 px-2.5 py-1.5 text-[13px] font-medium text-amber-400/90', children: [_jsx(SpinnerIcon, {}), _jsx("span", { children: formatGenerationStatus(generationStatus) })] })] })), generationErrors.length > 0 && (_jsxs(_Fragment, { children: [_jsx(ToolbarSeparator, {}), _jsxs("div", { className: 'flex items-center gap-1.5 px-2.5 py-1.5 text-[13px] font-medium text-red-400/90', children: [_jsx("span", { style: { fontSize: 14, lineHeight: 1 }, children: "\u2715" }), _jsx("span", { children: generationErrors.length === 1
718
+ ? `${generationErrors[0].namespace} failed: ${generationErrors[0].error.slice(0, 60)}`
719
+ : `${generationErrors.length} generations failed` })] })] }))] })] }));
720
+ }