egaki 0.6.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 (271) hide show
  1. package/CHANGELOG.md +140 -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 +538 -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 +584 -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,420 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Client-side MDX application.
5
+ *
6
+ * All MDX processing happens in the browser: parsing, section splitting,
7
+ * user module resolution, and safe-mdx rendering. Because rendering runs on
8
+ * the client there is no RSC serialization boundary between MDX content and
9
+ * the components — expression props can be functions (easing={x => x}),
10
+ * imported values can be anything, and user components don't need a
11
+ * 'use client' directive.
12
+ *
13
+ * The server (app.tsx) only passes the raw MDX source string through the
14
+ * RSC flight payload. Entry MDX edits flow server → client via rsc:update;
15
+ * user .tsx/.ts/.mdx edits flow through the client module graph: this
16
+ * module accepts HMR updates of virtual:egaki-modules directly via
17
+ * import.meta.hot.accept(dep, cb) and pushes the fresh map into React
18
+ * through useSyncExternalStore.
19
+ */
20
+
21
+ import { createContext, useContext, useEffect, useMemo } from 'react'
22
+ import type { ReactNode } from 'react'
23
+ import { SafeMdxRenderer } from 'safe-mdx'
24
+ import { mdxParse, extractImports, resolveModulePath } from 'safe-mdx/parse'
25
+ import type { EagerModules } from 'safe-mdx/parse'
26
+ import { eagerModules as initialModules } from 'virtual:egaki-modules'
27
+ import { splitIntoSections, calculateTotalDuration, resolveAutoDurations, parseFrontmatter, buildMdxScope } from './mdx-parse.ts'
28
+ import { filterImportNodesToModules } from './server-mdx.ts'
29
+ import { PlayerPage } from './player-page.tsx'
30
+ import { MDX_BUILTIN_COMPONENTS, Img, ServerSlotsContext, type ServerSlots } from './mdx-video.tsx'
31
+ import { MdxCodeBlockWrapper } from './code-block.tsx'
32
+ import { egakiStore } from './store.ts'
33
+ import { useModules, useMediaDurations } from './store-hooks.ts'
34
+ import { resetSectionDurations } from './media-duration-store.ts'
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // MDX components map
38
+ // ---------------------------------------------------------------------------
39
+
40
+ const FONT_SANS =
41
+ '-apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", sans-serif'
42
+ const FONT_MONO =
43
+ '"SF Mono", ui-monospace, SFMono-Regular, "Cascadia Code", monospace'
44
+
45
+ function buildVideoMdxComponents(): Record<string, any> {
46
+ return {
47
+ ...MDX_BUILTIN_COMPONENTS,
48
+
49
+ // Reserved: server component slot marker. The server renders the
50
+ // original children; this client component splices the slot in by
51
+ // matching its data-markdown-line against the slot keys.
52
+ Server,
53
+
54
+ // Standard element overrides
55
+ p: ({ children }: { children: ReactNode }) => (
56
+ <div style={{
57
+ fontSize: 'clamp(1.5rem, 2.5vw, 3rem)', fontWeight: 400,
58
+ color: '#a1a1aa', fontFamily: FONT_SANS, textAlign: 'center',
59
+ letterSpacing: '-0.02em', lineHeight: 1.4, maxWidth: '80%',
60
+ }}>{children}</div>
61
+ ),
62
+ strong: ({ children }: { children: ReactNode }) => (
63
+ <span style={{ color: '#fafafa', fontWeight: 600 }}>{children}</span>
64
+ ),
65
+ em: ({ children }: { children: ReactNode }) => (
66
+ <span style={{ fontStyle: 'italic' }}>{children}</span>
67
+ ),
68
+ a: ({ children }: { children: ReactNode; href?: string }) => (
69
+ <span style={{ color: '#818cf8', textDecoration: 'underline' }}>{children}</span>
70
+ ),
71
+ h1: () => null, h2: () => null, h3: () => null,
72
+ h4: () => null, h5: () => null, h6: () => null,
73
+ blockquote: () => null,
74
+ pre: MdxCodeBlockWrapper,
75
+ inlineCode: ({ children }: { children: ReactNode }) => (
76
+ <span style={{
77
+ fontFamily: FONT_MONO, fontSize: '0.875em', color: '#e4e4e7',
78
+ background: 'rgba(255, 255, 255, 0.06)', borderRadius: '0.25em',
79
+ padding: '0.1em 0.4em',
80
+ }}>{children}</span>
81
+ ),
82
+ ul: ({ children }: { children: ReactNode }) => (
83
+ <div style={{
84
+ fontSize: 'clamp(1.25rem, 2vw, 2rem)', color: '#a1a1aa',
85
+ fontFamily: FONT_SANS, textAlign: 'left', display: 'flex',
86
+ flexDirection: 'column', gap: '0.4em',
87
+ }}>{children}</div>
88
+ ),
89
+ ol: ({ children }: { children: ReactNode }) => (
90
+ <div style={{
91
+ fontSize: 'clamp(1.25rem, 2vw, 2rem)', color: '#a1a1aa',
92
+ fontFamily: FONT_SANS, textAlign: 'left', display: 'flex',
93
+ flexDirection: 'column', gap: '0.4em',
94
+ }}>{children}</div>
95
+ ),
96
+ li: ({ children }: { children: ReactNode }) => (
97
+ <div style={{ display: 'flex', gap: '0.5em' }}>
98
+ <span style={{ color: '#52525b' }}>•</span>
99
+ <span>{children}</span>
100
+ </div>
101
+ ),
102
+ img: ({ src, alt }: { src?: string; alt?: string }) => (
103
+ <Img src={src} alt={alt || ''} style={{
104
+ maxWidth: '80%', maxHeight: '70%', objectFit: 'contain', borderRadius: '0.5em',
105
+ }} />
106
+ ),
107
+ hr: () => (
108
+ <div style={{ width: '40%', height: 1, background: 'rgba(255, 255, 255, 0.1)' }} />
109
+ ),
110
+ table: ({ children }: { children: ReactNode }) => (
111
+ <div style={{ fontSize: 'clamp(0.875rem, 1.2vw, 1.125rem)', fontFamily: FONT_SANS, color: '#a1a1aa' }}>
112
+ {children}
113
+ </div>
114
+ ),
115
+ thead: ({ children }: { children: ReactNode }) => <div>{children}</div>,
116
+ tbody: ({ children }: { children: ReactNode }) => <div>{children}</div>,
117
+ tr: ({ children }: { children: ReactNode }) => (
118
+ <div style={{ display: 'flex', gap: '1em', borderBottom: '1px solid rgba(255,255,255,0.06)', padding: '0.5em 0' }}>
119
+ {children}
120
+ </div>
121
+ ),
122
+ td: ({ children }: { children: ReactNode }) => <div style={{ flex: 1 }}>{children}</div>,
123
+ th: ({ children }: { children: ReactNode }) => (
124
+ <div style={{ flex: 1, fontWeight: 600, color: '#e4e4e7' }}>{children}</div>
125
+ ),
126
+ }
127
+ }
128
+
129
+ // Built once — the map is static.
130
+ const mdxComponents = buildVideoMdxComponents()
131
+
132
+ // Enable function expressions in MDX attribute props. safe-mdx evaluates
133
+ // them with a safe AST interpreter (no eval), so `easing={x => x}` works.
134
+ const evaluateOptions = { functions: true }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // User modules — initial value from static import, HMR updates via store
138
+ // ---------------------------------------------------------------------------
139
+
140
+ egakiStore.setState({ modules: initialModules as EagerModules })
141
+
142
+ if (import.meta.hot) {
143
+ import.meta.hot.accept('virtual:egaki-modules', (next) => {
144
+ if (next?.eagerModules) {
145
+ egakiStore.setState({ modules: next.eagerModules as EagerModules })
146
+ }
147
+ })
148
+ }
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // Composition building (parse → sections → JSX)
152
+ // ---------------------------------------------------------------------------
153
+
154
+ // Slots travel via React context (provided by MdxClientApp around
155
+ // PlayerPage) rather than a safe-mdx renderNode hook: safe-mdx only calls
156
+ // renderNode in its top-level mdast traversal, while JSX elements nested
157
+ // inside other JSX elements go through jsxTransformer which resolves the
158
+ // components map directly. A real `Server` component in the map works at
159
+ // any nesting depth; it matches its slot via the data-markdown-line prop
160
+ // that safe-mdx injects (line numbers are identical on server and client
161
+ // because blankServerContents preserves line positions).
162
+
163
+ function Server(props: { 'data-markdown-line'?: number }) {
164
+ const slots = useContext(ServerSlotsContext)
165
+ const key = String(props['data-markdown-line'])
166
+ if (key in slots) return slots[key]
167
+ // No slot: <Server> inside an imported .mdx file (not scanned, v1
168
+ // limitation) or a stale flight payload.
169
+ console.warn(`[egaki] <Server> at line ${key} has no server-rendered slot; rendering nothing`)
170
+ return null
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Parse error recovery
175
+ //
176
+ // On MDX syntax errors, buildComposition returns the last successful result
177
+ // instead of crashing React. An error overlay section replaces the content
178
+ // so the user sees what went wrong. When the syntax is fixed, the next HMR
179
+ // cycle parses successfully and the real content reappears seamlessly.
180
+ // ---------------------------------------------------------------------------
181
+
182
+ type CompositionResult = {
183
+ sections: { heading: string | null; durationInFrames: number | null; jsx: ReactNode }[]
184
+ preamble: ReactNode | undefined
185
+ frontmatter: ReturnType<typeof parseFrontmatter>
186
+ parseError?: string
187
+ }
188
+
189
+ let lastGoodComposition: CompositionResult | null = null
190
+
191
+ function makeErrorComposition(error: Error): CompositionResult {
192
+ // If we have a cached good result, reuse its frontmatter and show the
193
+ // error as a single-section overlay so the player stays alive.
194
+ const frontmatter = lastGoodComposition?.frontmatter ?? { fps: 30, bpm: 120, width: 1920, height: 1080, scale: 1 }
195
+ return {
196
+ sections: [{
197
+ heading: 'Parse Error',
198
+ durationInFrames: frontmatter.fps * 5,
199
+ jsx: (
200
+ <div style={{
201
+ display: 'flex', flexDirection: 'column', alignItems: 'center',
202
+ justifyContent: 'center', width: '100%', height: '100%',
203
+ padding: '2rem', fontFamily: 'ui-monospace, SFMono-Regular, "Cascadia Code", monospace',
204
+ }}>
205
+ <div style={{ color: '#ef4444', fontSize: 'clamp(1rem, 2vw, 1.5rem)', fontWeight: 600, marginBottom: '1rem' }}>
206
+ MDX Parse Error
207
+ </div>
208
+ <div style={{
209
+ color: '#fca5a5', fontSize: 'clamp(0.75rem, 1.5vw, 1rem)',
210
+ background: 'rgba(239, 68, 68, 0.1)', borderRadius: '0.5rem',
211
+ padding: '1rem 1.5rem', maxWidth: '80%', whiteSpace: 'pre-wrap',
212
+ lineHeight: 1.6, border: '1px solid rgba(239, 68, 68, 0.2)',
213
+ }}>
214
+ {error.message}
215
+ </div>
216
+ <div style={{ color: '#71717a', fontSize: 'clamp(0.625rem, 1vw, 0.875rem)', marginTop: '1rem' }}>
217
+ Fix the syntax error and save. The video will recover automatically.
218
+ </div>
219
+ </div>
220
+ ),
221
+ }],
222
+ preamble: lastGoodComposition?.preamble,
223
+ frontmatter,
224
+ parseError: error.message,
225
+ }
226
+ }
227
+
228
+ function buildComposition(mdxSource: string, modules: EagerModules): CompositionResult {
229
+ let ast: ReturnType<typeof mdxParse>
230
+ try {
231
+ ast = mdxParse(mdxSource)
232
+ } catch (e) {
233
+ console.warn('[egaki] MDX parse error:', (e as Error).message)
234
+ return makeErrorComposition(e as Error)
235
+ }
236
+
237
+ // Compute FPS and BEAT from frontmatter early so they're available as
238
+ // scope variables for all SafeMdxRenderer calls (including imported MDX).
239
+ const { fps, bpm } = parseFrontmatter(ast)
240
+ const mdxScope = buildMdxScope(fps, bpm)
241
+
242
+ // Render imported .mdx/.md files into React components so safe-mdx can
243
+ // resolve `import Intro from './intro.mdx'` and render `<Intro />` via
244
+ // React composition. Each imported MDX gets its own SafeMdxRenderer pass
245
+ // with the same components map.
246
+ const moduleKeys = Object.keys(modules)
247
+ const mergedModules: EagerModules = { ...modules }
248
+ const imports = extractImports(ast)
249
+ for (const imp of imports) {
250
+ if (!/\.mdx?$/.test(imp.source)) continue
251
+ const key = resolveModulePath(imp.source, './', moduleKeys)
252
+ if (!key || !mergedModules[key]) continue
253
+ const rawContent = mergedModules[key].default
254
+ if (typeof rawContent !== 'string') continue
255
+ let importedAst
256
+ try {
257
+ importedAst = mdxParse(rawContent)
258
+ } catch (e) {
259
+ // Imported MDX has a syntax error. Skip it instead of crashing the
260
+ // whole composition; the main file's content still renders.
261
+ console.warn(`[egaki] parse error in imported MDX (${imp.source}):`, (e as Error).message)
262
+ continue
263
+ }
264
+ const renderedJsx = (
265
+ <SafeMdxRenderer
266
+ markdown={rawContent}
267
+ mdast={importedAst}
268
+ components={mdxComponents}
269
+ modules={mergedModules}
270
+ baseUrl="./"
271
+ scope={mdxScope}
272
+ evaluateOptions={evaluateOptions}
273
+ onError={(e) => console.warn('[egaki] imported MDX:', e.message)}
274
+ />
275
+ )
276
+ // Replace the raw string module with a component that returns the
277
+ // pre-rendered JSX. safe-mdx reads mod.default for default imports.
278
+ mergedModules[key] = { default: () => renderedJsx }
279
+ }
280
+
281
+ const result = splitIntoSections(ast)
282
+
283
+ // Extract import nodes (mdxjsEsm) from the full mdast. Section splitting
284
+ // drops them, but SafeMdxRenderer needs them to resolve imported components
285
+ // from the modules map. Prepend to every section's nodes.
286
+ //
287
+ // Imports not resolvable in the client modules map are stripped: those
288
+ // are server-only files (used exclusively inside <Server> blocks, which
289
+ // the server already rendered into slots) excluded from the client map
290
+ // by the vite plugin's inference.
291
+ const importNodes = filterImportNodesToModules(
292
+ ast.children.filter((node: any) => node.type === 'mdxjsEsm'),
293
+ Object.keys(mergedModules),
294
+ )
295
+
296
+ const renderNodes = (nodes: any[]) => (
297
+ <SafeMdxRenderer
298
+ markdown={mdxSource}
299
+ mdast={{ type: 'root', children: [...importNodes, ...nodes] } as any}
300
+ components={mdxComponents}
301
+ modules={mergedModules}
302
+ baseUrl="./"
303
+ scope={mdxScope}
304
+ addMarkdownLineNumbers
305
+ evaluateOptions={evaluateOptions}
306
+ onError={(e) => console.warn('[egaki] MDX:', e.message)}
307
+ />
308
+ )
309
+
310
+ const sections = result.sections.map((section) => ({
311
+ heading: section.heading,
312
+ durationInFrames: section.durationInFrames,
313
+ jsx: renderNodes(section.nodes),
314
+ }))
315
+
316
+ // Preamble: content before the first heading, rendered at composition
317
+ // level (outside Series) so it spans the full video duration.
318
+ const preamble = result.preamble.length > 0
319
+ ? renderNodes(result.preamble)
320
+ : undefined
321
+
322
+ const composition: CompositionResult = { sections, preamble, frontmatter: result.frontmatter }
323
+
324
+ // Cache successful result for parse error recovery
325
+ lastGoodComposition = composition
326
+
327
+ return composition
328
+ }
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // App component
332
+ // ---------------------------------------------------------------------------
333
+
334
+ const EMPTY_SLOTS: ServerSlots = {}
335
+
336
+ export function MdxClientApp({
337
+ mdx,
338
+ serverSlots = EMPTY_SLOTS,
339
+ entryPath,
340
+ availableEntries = [],
341
+ currentRoute = '',
342
+ }: {
343
+ mdx: string
344
+ /** Server-rendered <Server> subtrees keyed by node start line, produced
345
+ * in app.tsx and delivered via RSC flight. Consumed by the Server
346
+ * component through ServerSlotsContext. */
347
+ serverSlots?: ServerSlots
348
+ /** Absolute path of the MDX entry file, for copy prompts. */
349
+ entryPath: string
350
+ /** All available MDX entry route paths for navigation. */
351
+ availableEntries?: string[]
352
+ /** Current route path ('' for default entry). */
353
+ currentRoute?: string
354
+ }) {
355
+ const modules = useModules()
356
+
357
+ // Subscribe to media duration reports. When Audio/Video components report
358
+ // their durations (via mediabunny metadata fetch), this re-renders and
359
+ // sections without explicit duration= get auto-sized to their media content.
360
+ // The map is keyed by section index (as string) → max media duration in
361
+ // seconds, derived from the src-keyed persistent cache.
362
+ const sectionDurations = useMediaDurations()
363
+
364
+ // Split into two memos to avoid an infinite loop:
365
+ // 1. buildComposition creates JSX (depends on mdx + modules only).
366
+ // If this re-ran on sectionDurations changes, it would create new JSX
367
+ // identity → React unmounts Audio/Video → clearSectionDuration fires →
368
+ // snapshot changes → sectionDurations changes → infinite loop.
369
+ // 2. resolveAutoDurations fills in null durations from the section store.
370
+ // This memo is cheap and can re-run on every sectionDurations change
371
+ // without touching JSX identity.
372
+ const composed = useMemo(() => buildComposition(mdx, modules), [mdx, modules])
373
+
374
+ // Reset section duration reports when the composition changes (HMR,
375
+ // MDX edit, module update). Media components will re-report on mount.
376
+ // Does NOT clear the raw src cache so cached durations resolve instantly.
377
+ useEffect(() => {
378
+ resetSectionDurations()
379
+ }, [composed])
380
+
381
+ const { sections, totalDuration, preamble, hasUnresolvedDurations } = useMemo(() => {
382
+ const { fps, bpm } = composed.frontmatter
383
+
384
+ // MDX files without headings (e.g. imported partials rendered standalone)
385
+ // have 0 sections. Synthesize one from the preamble with null duration
386
+ // BEFORE resolving auto-durations, so media auto-sizing still works.
387
+ let sourceSections = composed.sections
388
+ let finalPreamble = composed.preamble
389
+ if (sourceSections.length === 0 && finalPreamble) {
390
+ sourceSections = [{ heading: null, durationInFrames: null, jsx: finalPreamble }]
391
+ finalPreamble = undefined
392
+ }
393
+
394
+ const resolved = resolveAutoDurations(sourceSections, fps, bpm, sectionDurations)
395
+ const hasUnresolved = sourceSections.some((s, i) =>
396
+ s.durationInFrames === null && sectionDurations[String(i)] === undefined,
397
+ )
398
+ return {
399
+ sections: resolved,
400
+ totalDuration: Math.max(1, calculateTotalDuration(resolved)),
401
+ preamble: finalPreamble,
402
+ hasUnresolvedDurations: hasUnresolved,
403
+ }
404
+ }, [composed, sectionDurations])
405
+
406
+ return (
407
+ <ServerSlotsContext.Provider value={serverSlots}>
408
+ <PlayerPage
409
+ sections={sections}
410
+ totalDuration={totalDuration}
411
+ preamble={preamble}
412
+ entryPath={entryPath}
413
+ hasUnresolvedDurations={hasUnresolvedDurations}
414
+ frontmatter={composed.frontmatter}
415
+ availableEntries={availableEntries}
416
+ currentRoute={currentRoute}
417
+ />
418
+ </ServerSlotsContext.Provider>
419
+ )
420
+ }