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,593 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Tweakpane integration for the egaki video player.
5
+ *
6
+ * Provides useTweakpane() — a React hook that registers a folder of
7
+ * parameters in a shared tweakpane Pane singleton. Each component gets
8
+ * its own folder; when the component unmounts the folder is disposed and
9
+ * removed from the UI. This means only currently-visible components show
10
+ * their parameters.
11
+ *
12
+ * The Pane is mounted into a sidebar container managed by <TweakpaneRoot />,
13
+ * rendered once in player-page.tsx. The sidebar is always visible on the
14
+ * right side of the page and takes its own space in the layout (no floating
15
+ * overlay). A single "Copy changes" button at the top of the pane serializes
16
+ * all active component parameters as a structured prompt for AI agents.
17
+ *
18
+ * Tweakpane is pure DOM (no React dependency), so there are no dual-React
19
+ * or SSR issues. During SSR (typeof window === 'undefined') the hook
20
+ * returns the raw default values and skips all DOM work.
21
+ */
22
+
23
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore, createContext, useContext } from 'react'
24
+ import type { PlayerRef } from '@remotion/player'
25
+ import { LayoutContainerContext, useIsPremounting } from './mdx-video.tsx'
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Types
29
+ // ---------------------------------------------------------------------------
30
+
31
+ /** Slider config: value + optional min/max/step. */
32
+ interface SliderParam {
33
+ value: number
34
+ min?: number
35
+ max?: number
36
+ step?: number
37
+ }
38
+
39
+ /** Select/dropdown config: value + options list. */
40
+ interface SelectParam {
41
+ value: string
42
+ options: string[]
43
+ }
44
+
45
+ /** Cubic bezier config: four control points [x1, y1, x2, y2]. */
46
+ export interface CubicBezierParam {
47
+ type: 'cubicBezier'
48
+ value: [number, number, number, number]
49
+ }
50
+
51
+ /** Range config: min/max pair displayed as a single range slider (plugin-essentials). */
52
+ interface RangeParam {
53
+ type: 'range'
54
+ value: { min: number; max: number }
55
+ min: number
56
+ max: number
57
+ step?: number
58
+ }
59
+
60
+ /**
61
+ * A param value is either:
62
+ * - a bare primitive (number, boolean, string) — auto-inferred control
63
+ * - a SliderParam object with explicit range
64
+ * - a SelectParam object with a dropdown of string options
65
+ * - a CubicBezierParam for a visual bezier curve editor
66
+ * - a RangeParam for a min/max range slider (plugin-essentials)
67
+ */
68
+ type ParamValue = number | boolean | string | SliderParam | SelectParam | CubicBezierParam | RangeParam
69
+
70
+ type ParamSchema = Record<string, ParamValue>
71
+
72
+ /** Resolved values: SliderParam becomes number, SelectParam becomes string, CubicBezierParam becomes tuple, RangeParam becomes {min, max}, everything else stays. */
73
+ type ResolvedValues<T extends ParamSchema> = {
74
+ [K in keyof T]: T[K] extends SliderParam ? number
75
+ : T[K] extends SelectParam ? string
76
+ : T[K] extends CubicBezierParam ? [number, number, number, number]
77
+ : T[K] extends RangeParam ? { min: number; max: number }
78
+ : T[K] extends number ? number
79
+ : T[K] extends boolean ? boolean
80
+ : T[K] extends string ? string
81
+ : T[K]
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Pane context — player metadata for the copy prompt
86
+ // ---------------------------------------------------------------------------
87
+
88
+ export interface TweakpaneContextValue {
89
+ playerRef: React.RefObject<PlayerRef | null>
90
+ fps: number
91
+ sections: { heading: string | null; durationInFrames: number }[]
92
+ entryPath: string
93
+ }
94
+
95
+ export const TweakpaneContext = createContext<TweakpaneContextValue | null>(null)
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Global disable switch
99
+ // ---------------------------------------------------------------------------
100
+
101
+ /**
102
+ * Set to `true` to completely disable tweakpane across all components.
103
+ * useTweakpane() will return raw default values without mounting any UI.
104
+ */
105
+ export let TWEAKPANE_DISABLED = false
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Singleton pane state (module-level, not React state)
109
+ // ---------------------------------------------------------------------------
110
+
111
+ /** Lazily imported tweakpane Pane class and essentials plugin module. */
112
+ let PaneClass: typeof import('tweakpane').Pane | null = null
113
+ let EssentialsPlugin: any = null
114
+ let paneInstance: import('tweakpane').Pane | null = null
115
+
116
+ /** External container element set by TweakpaneRoot. The Pane mounts into
117
+ * this DOM node instead of creating its own floating div. */
118
+ let externalContainer: HTMLElement | null = null
119
+
120
+ /** Persisted expanded state for the root pane across destroy/recreate cycles. */
121
+ let paneExpandedState = true
122
+
123
+ /** Registry of active folders for the copy prompt. */
124
+ interface FolderEntry {
125
+ label: string
126
+ /** The mutable params object bound to tweakpane. */
127
+ params: Record<string, unknown>
128
+ /** The original default values for diffing. */
129
+ defaults: Record<string, unknown>
130
+ }
131
+ const activeFolders = new Map<string, FolderEntry>()
132
+
133
+ /** Listeners notified when activeFolders changes (for copy button visibility). */
134
+ const folderListeners = new Set<() => void>()
135
+ function notifyFolderListeners() {
136
+ folderListeners.forEach((fn) => fn())
137
+ }
138
+
139
+ /** Shallow-clone arrays and plain objects so tweakpane's in-place mutations
140
+ * don't leak between params and defaults references. */
141
+ function cloneValue<T>(value: T): T {
142
+ if (Array.isArray(value)) return [...value] as T
143
+ if (typeof value === 'object' && value !== null) return { ...value } as T
144
+ return value
145
+ }
146
+
147
+ function resolveDefault(v: ParamValue): number | boolean | string | [number, number, number, number] | { min: number; max: number } {
148
+ if (isCubicBezierParam(v)) return [...v.value]
149
+ if (isRangeParam(v)) return { ...v.value }
150
+ if (typeof v === 'object' && v !== null && 'value' in v) return v.value
151
+ return v
152
+ }
153
+
154
+ function isSliderParam(v: ParamValue): v is SliderParam {
155
+ return typeof v === 'object' && v !== null && 'value' in v && !('options' in v) && !('type' in v)
156
+ }
157
+
158
+ function isSelectParam(v: ParamValue): v is SelectParam {
159
+ return typeof v === 'object' && v !== null && 'options' in v
160
+ }
161
+
162
+ function isCubicBezierParam(v: ParamValue): v is CubicBezierParam {
163
+ return typeof v === 'object' && v !== null && 'type' in v && (v as any).type === 'cubicBezier'
164
+ }
165
+
166
+ function isRangeParam(v: ParamValue): v is RangeParam {
167
+ return typeof v === 'object' && v !== null && 'type' in v && (v as any).type === 'range'
168
+ }
169
+
170
+ /** Guard against concurrent ensurePane() calls during the async import. */
171
+ let panePromise: Promise<import('tweakpane').Pane | null> | null = null
172
+
173
+ async function ensurePane(): Promise<import('tweakpane').Pane | null> {
174
+ if (paneInstance) return paneInstance
175
+ if (panePromise) return panePromise
176
+
177
+ panePromise = (async () => {
178
+ if (!PaneClass) {
179
+ // Load tweakpane and the essentials plugin (cubic bezier blade) together.
180
+ // Both are cached at module level so subsequent pane re-creations (after
181
+ // disposePane on scene change) register the plugin synchronously, avoiding
182
+ // a race where concurrent ensurePane() callers get a pane without the
183
+ // plugin registered.
184
+ const [mod, essentials] = await Promise.all([
185
+ import('tweakpane'),
186
+ import('@tweakpane/plugin-essentials').catch(() => null),
187
+ ])
188
+ PaneClass = mod.Pane
189
+ EssentialsPlugin = essentials
190
+ }
191
+ if (!externalContainer) {
192
+ console.warn('[egaki] TweakpaneRoot not mounted — tweakpane has no container')
193
+ return null
194
+ }
195
+ paneInstance = new PaneClass!({ container: externalContainer, title: 'Parameters', expanded: paneExpandedState })
196
+ if (EssentialsPlugin) paneInstance.registerPlugin(EssentialsPlugin)
197
+ paneInstance.on('fold', (ev) => {
198
+ paneExpandedState = ev.expanded
199
+ })
200
+ return paneInstance
201
+ })()
202
+
203
+ const pane = await panePromise
204
+ panePromise = null
205
+ return pane
206
+ }
207
+
208
+ function disposePane() {
209
+ if (paneInstance) {
210
+ paneInstance.dispose()
211
+ paneInstance = null
212
+ }
213
+ // Don't remove the container — it's owned by TweakpaneRoot (React).
214
+ // The Pane's dispose() already removes its own element from the container.
215
+ }
216
+
217
+ // ---------------------------------------------------------------------------
218
+ // Copy prompt serialization
219
+ // ---------------------------------------------------------------------------
220
+
221
+ function getCurrentSection(
222
+ frame: number,
223
+ sections: { heading: string | null; durationInFrames: number }[],
224
+ ): { heading: string | null; durationInFrames: number } | null {
225
+ let acc = 0
226
+ for (const s of sections) {
227
+ if (frame < acc + s.durationInFrames) return s
228
+ acc += s.durationInFrames
229
+ }
230
+ return sections.length > 0 ? sections[sections.length - 1]! : null
231
+ }
232
+
233
+ function formatValue(v: unknown): string {
234
+ if (Array.isArray(v) && v.length === 4 && v.every((x) => typeof x === 'number')) {
235
+ // Cubic bezier control points — format as cubicBezier() call so agents
236
+ // can paste it directly as an easing prop value.
237
+ return `cubicBezier(${v.join(', ')})`
238
+ }
239
+ if (typeof v === 'object' && v !== null && 'min' in v && 'max' in v) {
240
+ const r = v as { min: number; max: number }
241
+ const fmtNum = (n: number) => Number.isInteger(n) ? String(n) : n.toFixed(4).replace(/0+$/, '').replace(/\.$/, '')
242
+ return `{ min: ${fmtNum(r.min)}, max: ${fmtNum(r.max)} }`
243
+ }
244
+ if (typeof v === 'number') {
245
+ return Number.isInteger(v) ? String(v) : v.toFixed(4).replace(/0+$/, '').replace(/\.$/, '')
246
+ }
247
+ return JSON.stringify(v)
248
+ }
249
+
250
+ function serializeTweakpanePrompt(
251
+ ctx: TweakpaneContextValue | null,
252
+ ): string {
253
+ if (activeFolders.size === 0) return ''
254
+
255
+ const parts: string[] = ['## Parameter changes\n']
256
+
257
+ if (ctx?.entryPath) {
258
+ parts.push(`File: ${ctx.entryPath}`)
259
+ }
260
+
261
+ // Frame + section context
262
+ if (ctx) {
263
+ const currentFrame = ctx.playerRef.current?.getCurrentFrame() ?? null
264
+ if (currentFrame !== null) {
265
+ const seconds = (currentFrame / ctx.fps).toFixed(1)
266
+ parts.push(`Current frame: ${currentFrame} (${seconds}s at ${ctx.fps}fps)`)
267
+ const section = getCurrentSection(currentFrame, ctx.sections)
268
+ if (section?.heading) {
269
+ parts.push(`Section: "${section.heading}"`)
270
+ }
271
+ parts.push('')
272
+ }
273
+ }
274
+
275
+ let hasAnyChanges = false
276
+ for (const [id, entry] of activeFolders) {
277
+ const changed: string[] = []
278
+ for (const [key, current] of Object.entries(entry.params)) {
279
+ const def = entry.defaults[key]
280
+ if (current !== def) {
281
+ changed.push(` ${key}: ${formatValue(current)}`)
282
+ }
283
+ }
284
+ if (changed.length > 0) {
285
+ hasAnyChanges = true
286
+ parts.push(`**${entry.label}**`)
287
+ parts.push(changed.join('\n'))
288
+ parts.push('')
289
+ }
290
+ }
291
+
292
+ // If nothing changed from defaults, still show all current values
293
+ if (!hasAnyChanges) {
294
+ parts.push('No parameters changed from defaults.\n')
295
+ for (const [id, entry] of activeFolders) {
296
+ parts.push(`**${entry.label}** (current values)`)
297
+ for (const [key, current] of Object.entries(entry.params)) {
298
+ parts.push(` ${key}: ${formatValue(current)}`)
299
+ }
300
+ parts.push('')
301
+ }
302
+ }
303
+
304
+ parts.push('Apply these values as the new props in the component code.')
305
+ return parts.join('\n')
306
+ }
307
+
308
+ // Module-level ref for the context value (set by TweakpaneRoot, read by the
309
+ // copy button handler). Avoids threading React context into the imperative
310
+ // tweakpane button click handler.
311
+ let contextRef: TweakpaneContextValue | null = null
312
+
313
+ // Stable module-level callbacks for useSyncExternalStore in CopyChangesButton.
314
+ function subscribeFolders(cb: () => void) {
315
+ folderListeners.add(cb)
316
+ return () => { folderListeners.delete(cb) }
317
+ }
318
+ const getHasActiveFolders = () => activeFolders.size > 0
319
+ const getServerHasActiveFolders = () => false
320
+
321
+ /** React component: copy button rendered above the tweakpane pane. */
322
+ function CopyChangesButton() {
323
+ const [copied, setCopied] = useState(false)
324
+ const hasActiveFolders = useSyncExternalStore(
325
+ subscribeFolders,
326
+ getHasActiveFolders,
327
+ getServerHasActiveFolders,
328
+ )
329
+
330
+ if (!hasActiveFolders) return null
331
+
332
+ return (
333
+ <button
334
+ onClick={() => {
335
+ const text = serializeTweakpanePrompt(contextRef)
336
+ if (!text) return
337
+ navigator.clipboard.writeText(text).catch(() => {})
338
+ setCopied(true)
339
+ setTimeout(() => setCopied(false), 1500)
340
+ }}
341
+ style={{
342
+ display: 'flex',
343
+ alignItems: 'center',
344
+ justifyContent: 'center',
345
+ gap: 6,
346
+ margin: '8px 8px 4px',
347
+ width: 'calc(100% - 16px)',
348
+ padding: '7px 12px',
349
+ background: copied ? 'rgba(34,197,94,0.15)' : 'rgba(255,255,255,0.06)',
350
+ border: '1px solid rgba(255,255,255,0.08)',
351
+ borderRadius: 6,
352
+ color: copied ? '#4ade80' : '#a1a1aa',
353
+ fontSize: 12,
354
+ fontWeight: 500,
355
+ fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif',
356
+ cursor: 'pointer',
357
+ transition: 'background 0.15s, color 0.15s',
358
+ }}
359
+ >
360
+ {copied ? '✓ Copied prompt' : '📋 Copy changes'}
361
+ </button>
362
+ )
363
+ }
364
+
365
+ // ---------------------------------------------------------------------------
366
+ // useTweakpane hook
367
+ // ---------------------------------------------------------------------------
368
+
369
+ /**
370
+ * Register a folder of tweakable parameters for the current component.
371
+ *
372
+ * @param label Folder title shown in the pane UI. Also set as
373
+ * `data-tweakpane="label"` on a wrapper div for agent inspection.
374
+ * @param schema Parameter definitions. Each key maps to either:
375
+ * - a bare `number`, `boolean`, or `string` (auto-inferred control)
376
+ * - a `{ value, min?, max?, step? }` object for explicit slider range
377
+ *
378
+ * Returns a reactive object with current values. Updating a control in the
379
+ * pane immediately triggers a React re-render with fresh values.
380
+ *
381
+ * On unmount the folder is disposed and removed from the pane.
382
+ */
383
+ export function useTweakpane<T extends ParamSchema>(
384
+ label: string,
385
+ schema: T,
386
+ ): ResolvedValues<T> {
387
+ // Skip registration inside layout-transition ghost renders (the previous
388
+ // section is re-rendered hidden for FLIP measurement; we don't want its
389
+ // components polluting the tweakpane UI) and premounted sequences
390
+ // (premountFor renders the next section early but invisible for preloading;
391
+ // Remotion freezes useCurrentFrame() at 0 and sets opacity: 0).
392
+ const container = useContext(LayoutContainerContext)
393
+ const isGhost = container === 'ghost'
394
+ const isPremounting = useIsPremounting()
395
+ const skip = isGhost || isPremounting
396
+
397
+ const [values, setValues] = useState<Record<string, unknown>>(() => {
398
+ const v: Record<string, unknown> = {}
399
+ for (const [key, param] of Object.entries(schema)) {
400
+ v[key] = resolveDefault(param)
401
+ }
402
+ return v
403
+ })
404
+
405
+ // Track the schema for updates
406
+ const schemaRef = useRef(schema)
407
+ schemaRef.current = schema
408
+
409
+ // Stable ID for this hook instance
410
+ const idRef = useRef(`${label}-${Math.random().toString(36).slice(2, 8)}`)
411
+
412
+ useEffect(() => {
413
+ // SSR guard, ghost/premount render, or globally disabled — skip all pane work.
414
+ // `skip` is in the dep array so the effect re-runs when premounting ends and
415
+ // the section becomes active (registers tweakpane) or vice versa (tears down).
416
+ if (typeof window === 'undefined' || skip || TWEAKPANE_DISABLED) return
417
+
418
+ const id = idRef.current
419
+ // Mutable params object that tweakpane binds to
420
+ const params: Record<string, unknown> = {}
421
+ const defaults: Record<string, unknown> = {}
422
+ for (const [key, param] of Object.entries(schemaRef.current)) {
423
+ const def = resolveDefault(param)
424
+ // Clone object/array values so tweakpane's in-place mutations on params
425
+ // don't corrupt the defaults (used by "Copy changes" diffing).
426
+ params[key] = cloneValue(def)
427
+ defaults[key] = cloneValue(def)
428
+ }
429
+
430
+ let folder: import('tweakpane').FolderApi | null = null
431
+ let disposed = false
432
+
433
+ void (async () => {
434
+ const pane = await ensurePane()
435
+ if (disposed || !pane) return
436
+
437
+ folder = pane.addFolder({ title: label, expanded: true })
438
+
439
+ for (const [key, param] of Object.entries(schemaRef.current)) {
440
+ if (isCubicBezierParam(param)) {
441
+ // Cubic bezier blade — visual curve editor
442
+ const blade = folder.addBlade({
443
+ view: 'cubicbezier',
444
+ value: param.value,
445
+ expanded: true,
446
+ label: key,
447
+ picker: 'inline',
448
+ } as any)
449
+ ;(blade as any).on('change', (ev: any) => {
450
+ const val = ev.value
451
+ const tuple: [number, number, number, number] = [
452
+ Math.round(val.x1 * 1000) / 1000,
453
+ Math.round(val.y1 * 1000) / 1000,
454
+ Math.round(val.x2 * 1000) / 1000,
455
+ Math.round(val.y2 * 1000) / 1000,
456
+ ]
457
+ params[key] = tuple
458
+ setValues((prev) => ({ ...prev, [key]: tuple }))
459
+ })
460
+ continue
461
+ }
462
+ if (isRangeParam(param)) {
463
+ // Range slider from plugin-essentials — min/max pair as one control
464
+ folder.addBinding(params, key, {
465
+ min: param.min,
466
+ max: param.max,
467
+ step: param.step,
468
+ }).on('change', (ev) => {
469
+ const val = ev.value as { min: number; max: number }
470
+ setValues((prev) => ({ ...prev, [key]: { min: val.min, max: val.max } }))
471
+ })
472
+ continue
473
+ }
474
+ const opts: Record<string, unknown> = {}
475
+ if (isSelectParam(param)) {
476
+ // Build { Label: value } map for tweakpane list binding
477
+ const optionsMap: Record<string, string> = {}
478
+ for (const opt of param.options) optionsMap[opt] = opt
479
+ opts.options = optionsMap
480
+ } else if (isSliderParam(param)) {
481
+ if (param.min !== undefined) opts.min = param.min
482
+ if (param.max !== undefined) opts.max = param.max
483
+ if (param.step !== undefined) opts.step = param.step
484
+ } else if (typeof param === 'number') {
485
+ // Auto-infer range for bare numbers
486
+ if (param >= 0 && param <= 1) {
487
+ opts.min = 0; opts.max = 1; opts.step = 0.01
488
+ } else if (param >= 0 && param <= 10) {
489
+ opts.min = 0; opts.max = param * 3 || 10; opts.step = 0.1
490
+ } else if (param >= 0 && param <= 100) {
491
+ opts.min = 0; opts.max = param * 3 || 100; opts.step = 1
492
+ } else if (param >= 0) {
493
+ opts.min = 0; opts.max = param * 3 || 1000; opts.step = 10
494
+ }
495
+ }
496
+
497
+ folder.addBinding(params, key, opts).on('change', (ev) => {
498
+ setValues((prev) => ({ ...prev, [key]: ev.value }))
499
+ })
500
+ }
501
+
502
+ // Register for copy prompt
503
+ activeFolders.set(id, { label, params, defaults })
504
+ notifyFolderListeners()
505
+ })()
506
+
507
+ return () => {
508
+ disposed = true
509
+ if (folder) {
510
+ folder.dispose()
511
+ folder = null
512
+ }
513
+ activeFolders.delete(id)
514
+ notifyFolderListeners()
515
+
516
+ // If no more folders, tear down the pane entirely
517
+ if (activeFolders.size === 0) {
518
+ disposePane()
519
+ }
520
+ }
521
+ }, [label, skip])
522
+
523
+ return values as ResolvedValues<T>
524
+ }
525
+
526
+ // ---------------------------------------------------------------------------
527
+ // TweakpaneRoot — sidebar container for the tweakpane pane
528
+ // ---------------------------------------------------------------------------
529
+
530
+ /** Sidebar width constant shared with player-page layout. */
531
+ export const SIDEBAR_WIDTH = 280
532
+
533
+ /**
534
+ * Right sidebar that hosts the tweakpane Pane. Always visible in the page
535
+ * layout; the Pane mounts into the scrollable inner div. Also provides
536
+ * player metadata (frame, fps, sections) to the copy prompt button.
537
+ */
538
+ export function TweakpaneRoot({
539
+ playerRef,
540
+ fps,
541
+ sections,
542
+ entryPath,
543
+ }: {
544
+ playerRef: React.RefObject<PlayerRef | null>
545
+ fps: number
546
+ sections: { heading: string | null; durationInFrames: number }[]
547
+ entryPath: string
548
+ }) {
549
+ // Ref callback sets the container synchronously when the DOM node mounts,
550
+ // before any child useEffect (where useTweakpane calls ensurePane). On
551
+ // unmount the callback receives null, so we clean up the pane.
552
+ const containerRefCallback = useCallback((node: HTMLDivElement | null) => {
553
+ if (node) {
554
+ externalContainer = node
555
+ } else {
556
+ disposePane()
557
+ externalContainer = null
558
+ }
559
+ }, [])
560
+
561
+ // Keep module-level ref in sync for the copy button handler
562
+ useEffect(() => {
563
+ contextRef = { playerRef, fps, sections, entryPath }
564
+ return () => {
565
+ contextRef = null
566
+ }
567
+ }, [playerRef, fps, sections, entryPath])
568
+
569
+ return (
570
+ <div
571
+ className='egaki-sidebar'
572
+ style={{
573
+ width: SIDEBAR_WIDTH,
574
+ minWidth: SIDEBAR_WIDTH,
575
+ height: '100vh',
576
+ position: 'sticky',
577
+ top: 0,
578
+ overflowY: 'auto',
579
+ overflowX: 'hidden',
580
+ background: '#111',
581
+ borderLeft: '1px solid rgba(255,255,255,0.08)',
582
+ flexShrink: 0,
583
+ }}
584
+ >
585
+ <CopyChangesButton />
586
+ <div
587
+ ref={containerRefCallback}
588
+ id='egaki-tweakpane'
589
+ style={{ width: '100%' }}
590
+ />
591
+ </div>
592
+ )
593
+ }
@@ -0,0 +1,30 @@
1
+ /** Type declarations for virtual modules generated by the video vite plugin */
2
+
3
+ /// <reference types="vite/client" />
4
+
5
+ declare module 'virtual:egaki-mdx' {
6
+ /** Default export: raw MDX source of the default entry (backward compat). */
7
+ const mdx: string
8
+ export default mdx
9
+ /** Map of all discovered MDX entries: routePath → raw MDX source string.
10
+ * The default entry's key matches `defaultRoute`. */
11
+ export const entries: Record<string, string>
12
+ /** Map of all discovered MDX entries: routePath → absolute file path. */
13
+ export const entryPaths: Record<string, string>
14
+ /** Route path of the default entry (e.g. 'video' or 'index'). */
15
+ export const defaultRoute: string
16
+ /** Absolute path of the vite project root, for resolving relative MDX
17
+ * import sources in <Server> slot rendering (app.tsx). */
18
+ export const projectRoot: string
19
+ /** Absolute path of the default MDX entry file (backward compat). */
20
+ export const entryPath: string
21
+ /** Composition dimensions from frontmatter (default 1920×1080). */
22
+ export const compositionWidth: number
23
+ export const compositionHeight: number
24
+ /** Basename of the project folder, used for the page title. */
25
+ export const folderName: string
26
+ }
27
+
28
+ declare module 'virtual:egaki-modules' {
29
+ export const eagerModules: Record<string, Record<string, any>>
30
+ }