motionrender 0.0.0-stage → 0.2.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 (203) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -2
  3. package/blocks/badge.json +68 -0
  4. package/blocks/big-stat.json +240 -0
  5. package/blocks/cta-end.json +283 -0
  6. package/blocks/feature-list.json +286 -0
  7. package/blocks/footage.json +43 -0
  8. package/blocks/fx-background-swap.json +170 -0
  9. package/blocks/fx-before-after.json +212 -0
  10. package/blocks/fx-captions.json +34 -0
  11. package/blocks/fx-chat.json +368 -0
  12. package/blocks/fx-checklist.json +609 -0
  13. package/blocks/fx-clone-wall.json +181 -0
  14. package/blocks/fx-color-pop.json +92 -0
  15. package/blocks/fx-command-bridge.json +358 -0
  16. package/blocks/fx-counter-hero.json +206 -0
  17. package/blocks/fx-cta-card.json +337 -0
  18. package/blocks/fx-day-to-night.json +174 -0
  19. package/blocks/fx-duo-title.json +245 -0
  20. package/blocks/fx-duotone.json +95 -0
  21. package/blocks/fx-extruded-title.json +309 -0
  22. package/blocks/fx-gallery.json +239 -0
  23. package/blocks/fx-glass-cards.json +921 -0
  24. package/blocks/fx-glitch-title.json +373 -0
  25. package/blocks/fx-grade-look.json +91 -0
  26. package/blocks/fx-highlighter.json +53 -0
  27. package/blocks/fx-icon-orbit.json +2694 -0
  28. package/blocks/fx-lens-flare.json +325 -0
  29. package/blocks/fx-letterbox.json +105 -0
  30. package/blocks/fx-light-leak.json +140 -0
  31. package/blocks/fx-map-journey.json +443 -0
  32. package/blocks/fx-neon-outline.json +743 -0
  33. package/blocks/fx-notifications.json +456 -0
  34. package/blocks/fx-progress-ring.json +143 -0
  35. package/blocks/fx-pull-out.json +68 -0
  36. package/blocks/fx-punch-in.json +81 -0
  37. package/blocks/fx-rgb-flash.json +166 -0
  38. package/blocks/fx-shake-hit.json +120 -0
  39. package/blocks/fx-split-reveal.json +110 -0
  40. package/blocks/fx-spotlight.json +1383 -0
  41. package/blocks/fx-stats-split.json +500 -0
  42. package/blocks/fx-step-tracker.json +528 -0
  43. package/blocks/fx-sticker-freeze.json +584 -0
  44. package/blocks/fx-strike-rewrite.json +79 -0
  45. package/blocks/fx-ticker.json +179 -0
  46. package/blocks/fx-timeline.json +1287 -0
  47. package/blocks/fx-tracked-label.json +167 -0
  48. package/blocks/fx-typewriter-quote.json +164 -0
  49. package/blocks/handle.json +43 -0
  50. package/blocks/icon-row.json +404 -0
  51. package/blocks/image-caption.json +248 -0
  52. package/blocks/lower-third.json +106 -0
  53. package/blocks/phone-mockup.json +459 -0
  54. package/blocks/progress-bar.json +51 -0
  55. package/blocks/quote.json +347 -0
  56. package/blocks/section-title.json +240 -0
  57. package/blocks/split-screen.json +283 -0
  58. package/blocks/stat-grid.json +315 -0
  59. package/blocks/steps.json +266 -0
  60. package/blocks/title-hero.json +287 -0
  61. package/dist/builder.d.ts +1 -0
  62. package/dist/builder.js +169 -0
  63. package/dist/cli.js +9301 -0
  64. package/dist/fonts/LICENSE-Inter.txt +93 -0
  65. package/dist/fonts/inter-cyrillic-ext-opsz-italic.woff2 +0 -0
  66. package/dist/fonts/inter-cyrillic-ext-opsz-normal.woff2 +0 -0
  67. package/dist/fonts/inter-cyrillic-opsz-italic.woff2 +0 -0
  68. package/dist/fonts/inter-cyrillic-opsz-normal.woff2 +0 -0
  69. package/dist/fonts/inter-greek-ext-opsz-italic.woff2 +0 -0
  70. package/dist/fonts/inter-greek-ext-opsz-normal.woff2 +0 -0
  71. package/dist/fonts/inter-greek-opsz-italic.woff2 +0 -0
  72. package/dist/fonts/inter-greek-opsz-normal.woff2 +0 -0
  73. package/dist/fonts/inter-latin-ext-opsz-italic.woff2 +0 -0
  74. package/dist/fonts/inter-latin-ext-opsz-normal.woff2 +0 -0
  75. package/dist/fonts/inter-latin-opsz-italic.woff2 +0 -0
  76. package/dist/fonts/inter-latin-opsz-normal.woff2 +0 -0
  77. package/dist/fonts/inter-vietnamese-opsz-italic.woff2 +0 -0
  78. package/dist/fonts/inter-vietnamese-opsz-normal.woff2 +0 -0
  79. package/dist/index.d.ts +2 -0
  80. package/dist/index.js +2330 -0
  81. package/dist/player.d.ts +1 -0
  82. package/dist/player.js +80 -0
  83. package/dist/react.d.ts +1 -0
  84. package/dist/react.js +140 -0
  85. package/dist/runtime/canvas-runtime.js +261 -0
  86. package/dist/runtime/lottie_light_canvas.min.js +1 -0
  87. package/dist/runtime/text-layout.js +50 -0
  88. package/dist/sfx/click.wav +0 -0
  89. package/dist/sfx/ding.wav +0 -0
  90. package/dist/sfx/pop.wav +0 -0
  91. package/dist/sfx/riser.wav +0 -0
  92. package/dist/sfx/thump.wav +0 -0
  93. package/dist/sfx/tick.wav +0 -0
  94. package/dist/sfx/whoosh.wav +0 -0
  95. package/dist/types/packages/ai/src/assets.d.ts +26 -0
  96. package/dist/types/packages/ai/src/batch.d.ts +19 -0
  97. package/dist/types/packages/ai/src/compose.d.ts +34 -0
  98. package/dist/types/packages/ai/src/history.d.ts +37 -0
  99. package/dist/types/packages/ai/src/image.d.ts +19 -0
  100. package/dist/types/packages/ai/src/index.d.ts +13 -0
  101. package/dist/types/packages/ai/src/json.d.ts +2 -0
  102. package/dist/types/packages/ai/src/narrate.d.ts +63 -0
  103. package/dist/types/packages/ai/src/patch.d.ts +15 -0
  104. package/dist/types/packages/ai/src/project.d.ts +27 -0
  105. package/dist/types/packages/ai/src/prompt.d.ts +30 -0
  106. package/dist/types/packages/ai/src/providers.d.ts +109 -0
  107. package/dist/types/packages/ai/src/template.d.ts +80 -0
  108. package/dist/types/packages/ai/src/voiceover.d.ts +49 -0
  109. package/dist/types/packages/animation/src/index.d.ts +27 -0
  110. package/dist/types/packages/assets/src/cache.d.ts +26 -0
  111. package/dist/types/packages/assets/src/index.d.ts +8 -0
  112. package/dist/types/packages/builder/src/index.d.ts +270 -0
  113. package/dist/types/packages/cli/src/ai-studio-page.d.ts +1 -0
  114. package/dist/types/packages/cli/src/ai-studio.d.ts +35 -0
  115. package/dist/types/packages/cli/src/ai.d.ts +3 -0
  116. package/dist/types/packages/cli/src/args.d.ts +9 -0
  117. package/dist/types/packages/cli/src/blocks-cli.d.ts +3 -0
  118. package/dist/types/packages/cli/src/blocks.d.ts +50 -0
  119. package/dist/types/packages/cli/src/build.d.ts +1 -0
  120. package/dist/types/packages/cli/src/dev-page.d.ts +1 -0
  121. package/dist/types/packages/cli/src/dev.d.ts +13 -0
  122. package/dist/types/packages/cli/src/index.d.ts +2 -0
  123. package/dist/types/packages/cli/src/init.d.ts +10 -0
  124. package/dist/types/packages/cli/src/serve.d.ts +2 -0
  125. package/dist/types/packages/cli/src/studio.d.ts +36 -0
  126. package/dist/types/packages/cli/src/templates.d.ts +63 -0
  127. package/dist/types/packages/cli/src/thumbnail.d.ts +20 -0
  128. package/dist/types/packages/cli/src/track.d.ts +35 -0
  129. package/dist/types/packages/cli/src/worker.d.ts +13 -0
  130. package/dist/types/packages/contracts/src/index.d.ts +377 -0
  131. package/dist/types/packages/core/src/audio.d.ts +14 -0
  132. package/dist/types/packages/core/src/distributed.d.ts +50 -0
  133. package/dist/types/packages/core/src/index.d.ts +63 -0
  134. package/dist/types/packages/core/src/media.d.ts +11 -0
  135. package/dist/types/packages/core/src/review.d.ts +29 -0
  136. package/dist/types/packages/ffmpeg/src/index.d.ts +68 -0
  137. package/dist/types/packages/graphics/src/fonts.d.ts +3 -0
  138. package/dist/types/packages/graphics/src/index.d.ts +121 -0
  139. package/dist/types/packages/graphics/src/runtime.d.ts +10 -0
  140. package/dist/types/packages/jobs/src/env.d.ts +78 -0
  141. package/dist/types/packages/jobs/src/handlers.d.ts +17 -0
  142. package/dist/types/packages/jobs/src/index.d.ts +5 -0
  143. package/dist/types/packages/jobs/src/input.d.ts +18 -0
  144. package/dist/types/packages/jobs/src/queue.d.ts +70 -0
  145. package/dist/types/packages/jobs/src/redis.d.ts +42 -0
  146. package/dist/types/packages/player/src/bundle.d.ts +4 -0
  147. package/dist/types/packages/player/src/canvas-runtime.d.ts +11 -0
  148. package/dist/types/packages/player/src/index.d.ts +29 -0
  149. package/dist/types/packages/react/src/index.d.ts +76 -0
  150. package/dist/types/packages/renderer/src/index.d.ts +35 -0
  151. package/dist/types/packages/renderer/src/pool.d.ts +7 -0
  152. package/dist/types/packages/renderer/src/video.d.ts +9 -0
  153. package/dist/types/packages/schema/src/captions.d.ts +24 -0
  154. package/dist/types/packages/schema/src/expression.d.ts +1 -0
  155. package/dist/types/packages/schema/src/index.d.ts +1213 -0
  156. package/dist/types/packages/schema/src/variables.d.ts +115 -0
  157. package/dist/types/packages/storage/src/index.d.ts +78 -0
  158. package/dist/types/packages/timeline/src/index.d.ts +104 -0
  159. package/package.json +78 -4
  160. package/skills/ove/SKILL.md +356 -0
  161. package/skills/ove-ai-compose/SKILL.md +36 -0
  162. package/skills/ove-batch-videos/SKILL.md +23 -0
  163. package/skills/ove-code-video/SKILL.md +68 -0
  164. package/skills/ove-effects/SKILL.md +65 -0
  165. package/skills/ove-make-reel/SKILL.md +30 -0
  166. package/skills/ove-voiceover-captions/SKILL.md +31 -0
  167. package/studio/assets/index-CzOvWv9w.css +1 -0
  168. package/studio/assets/index-J_fvKgng.js +11 -0
  169. package/studio/index.html +16 -0
  170. package/templates/case-study/composition.json +358 -0
  171. package/templates/case-study/template.json +8 -0
  172. package/templates/event-invite/composition.json +245 -0
  173. package/templates/event-invite/template.json +8 -0
  174. package/templates/explainer-square/composition.json +1335 -0
  175. package/templates/explainer-square/template.json +13 -0
  176. package/templates/keynote-wide/composition.json +1458 -0
  177. package/templates/keynote-wide/template.json +13 -0
  178. package/templates/launch-wide/composition.json +1306 -0
  179. package/templates/launch-wide/template.json +13 -0
  180. package/templates/product-card/composition.json +196 -0
  181. package/templates/product-card/data.example.json +5 -0
  182. package/templates/product-card/template.json +8 -0
  183. package/templates/product-wide/composition.json +1609 -0
  184. package/templates/product-wide/template.json +14 -0
  185. package/templates/quote-card/composition.json +123 -0
  186. package/templates/quote-card/template.json +8 -0
  187. package/templates/sale-promo/composition.json +290 -0
  188. package/templates/sale-promo/template.json +8 -0
  189. package/templates/showcase-tall/composition.json +1646 -0
  190. package/templates/showcase-tall/template.json +14 -0
  191. package/templates/slideshow/composition.json +167 -0
  192. package/templates/slideshow/template.json +8 -0
  193. package/templates/social-reel/composition.json +252 -0
  194. package/templates/social-reel/template.json +8 -0
  195. package/templates/starter/assets/logo.svg +4 -0
  196. package/templates/starter/composition.json +84 -0
  197. package/templates/story-tall/composition.json +1379 -0
  198. package/templates/story-tall/template.json +13 -0
  199. package/themes/espresso.json +15 -0
  200. package/themes/midnight.json +15 -0
  201. package/themes/neon.json +15 -0
  202. package/themes/paper.json +15 -0
  203. package/themes/signal.json +15 -0
@@ -0,0 +1,270 @@
1
+ import type { Composition } from '../../schema/src/index.js';
2
+ export type Easing = string | [number, number, number, number];
3
+ export type Keyframe = {
4
+ frame: number;
5
+ value: number | string;
6
+ easing?: Easing;
7
+ };
8
+ export type Animation = {
9
+ property: string;
10
+ keyframes: Keyframe[];
11
+ repeat?: number;
12
+ reverse?: boolean;
13
+ };
14
+ export type Value<T = number> = T | string;
15
+ export type Transition = {
16
+ type: 'fade' | 'slide' | 'wipe' | 'zoom' | 'iris' | 'push' | 'dip' | 'blur';
17
+ durationFrames: number;
18
+ direction?: 'left' | 'right' | 'top' | 'bottom';
19
+ easing?: Easing;
20
+ color?: string;
21
+ };
22
+ /** What every layer accepts. Strings may hold {{expressions}}; anything else the schema allows can be passed too. */
23
+ export type LayerOptions = {
24
+ id?: string;
25
+ x?: Value;
26
+ y?: Value;
27
+ opacity?: Value;
28
+ rotation?: Value;
29
+ scale?: Value;
30
+ originX?: Value<number | 'center'>;
31
+ originY?: Value<number | 'center'>;
32
+ startFrame?: Value;
33
+ endFrame?: Value;
34
+ zIndex?: number;
35
+ fixed?: boolean;
36
+ blur?: Value;
37
+ animations?: Animation[];
38
+ if?: string;
39
+ [other: string]: unknown;
40
+ };
41
+ export type Layer = Record<string, unknown> & {
42
+ type?: string;
43
+ id?: string;
44
+ };
45
+ export type TextOptions = LayerOptions & {
46
+ fontSize?: Value;
47
+ fontFamily?: string;
48
+ fontWeight?: number | 'normal' | 'bold';
49
+ color?: string;
50
+ textAlign?: 'left' | 'center' | 'right';
51
+ maxWidth?: Value;
52
+ maxLines?: number;
53
+ minFontSize?: number;
54
+ lineHeight?: number;
55
+ letterSpacing?: number;
56
+ split?: {
57
+ by?: 'words' | 'letters' | 'lines';
58
+ stagger?: number;
59
+ preset?: 'rise' | 'fade' | 'pop' | 'blur' | 'slide' | 'drop' | 'typewriter';
60
+ order?: 'forward' | 'reverse' | 'center' | 'edges' | 'random';
61
+ duration?: number;
62
+ };
63
+ };
64
+ export type ShapeOptions = LayerOptions & {
65
+ width?: Value;
66
+ height?: Value;
67
+ fill?: unknown;
68
+ stroke?: unknown;
69
+ strokeWidth?: number;
70
+ radius?: Value;
71
+ };
72
+ export declare const text: (content: string, options?: TextOptions) => Layer;
73
+ export declare const rect: (options: ShapeOptions) => Layer;
74
+ export declare const circle: (options: ShapeOptions & {
75
+ width: Value;
76
+ }) => Layer;
77
+ export declare const path: (d: string, options?: ShapeOptions) => Layer;
78
+ export declare const image: (src: string, options?: LayerOptions & {
79
+ width?: Value;
80
+ height?: Value;
81
+ objectFit?: "cover" | "contain" | "fill";
82
+ radius?: number;
83
+ }) => Layer;
84
+ export declare const clip: (src: string, options?: LayerOptions & {
85
+ width?: Value;
86
+ height?: Value;
87
+ trimStart?: number;
88
+ volume?: number;
89
+ }) => Layer;
90
+ export declare const audio: (src: string, options?: LayerOptions & {
91
+ volume?: number;
92
+ }) => Layer;
93
+ /** A built-in sound effect: whoosh, click, tick, pop, thump, ding or riser. */
94
+ export declare const sfx: (name: "whoosh" | "click" | "tick" | "pop" | "thump" | "ding" | "riser", options?: LayerOptions & {
95
+ volume?: number;
96
+ }) => Layer;
97
+ export declare const group: (children: Layer[], options?: LayerOptions & {
98
+ width?: Value;
99
+ height?: Value;
100
+ layout?: {
101
+ direction?: "row" | "column";
102
+ gap?: number;
103
+ padding?: number;
104
+ align?: "start" | "center" | "end";
105
+ justify?: "start" | "center" | "end" | "space-between";
106
+ };
107
+ }) => Layer;
108
+ export declare const html: (markup: string, options: LayerOptions & {
109
+ width: number;
110
+ height: number;
111
+ css?: string;
112
+ vars?: Record<string, number | string>;
113
+ }) => Layer;
114
+ export declare const lottie: (src: string, options: LayerOptions & {
115
+ width: number;
116
+ height: number;
117
+ loop?: boolean;
118
+ }) => Layer;
119
+ export declare const shader: (fragment: string, options: LayerOptions & {
120
+ width: number;
121
+ height: number;
122
+ vars?: Record<string, number | string>;
123
+ }) => Layer;
124
+ export declare const captions: (src: string, options?: LayerOptions) => Layer;
125
+ /** A reusable component declared with `video.component(...)`, placed with values for its parameters. */
126
+ export declare const use: (component: string, values?: Record<string, unknown>, options?: LayerOptions) => Layer;
127
+ /** The arguments a code layer's draw function receives. */
128
+ export type CodeArgs = {
129
+ frame: number;
130
+ t: number;
131
+ fps: number;
132
+ width: number;
133
+ height: number;
134
+ props: Record<string, any>;
135
+ api: {
136
+ random(seed: number): () => number;
137
+ noise(x: number, y?: number, z?: number): number;
138
+ interpolate(x: number, input: number[], output: number[], options?: {
139
+ easing?: string;
140
+ clamp?: boolean;
141
+ }): number;
142
+ spring(options: {
143
+ frame: number;
144
+ fps?: number;
145
+ damping?: number;
146
+ stiffness?: number;
147
+ mass?: number;
148
+ from?: number;
149
+ to?: number;
150
+ }): number;
151
+ clamp(x: number, min: number, max: number): number;
152
+ lerp(a: number, b: number, t: number): number;
153
+ };
154
+ };
155
+ /**
156
+ * Drawing code that runs every frame on a canvas. Give a function (it is copied into the composition as text, so it must not use
157
+ * anything from the surrounding file) or the source of `function draw(ctx, args)`. Needs `--allow-code` to render: only for your own files.
158
+ */
159
+ export declare const code: (draw: ((ctx: CanvasRenderingContext2D, args: CodeArgs) => void) | string, options: LayerOptions & {
160
+ width: number;
161
+ height: number;
162
+ props?: Record<string, unknown>;
163
+ resolution?: number;
164
+ }) => Layer;
165
+ /** Keyframes from [frame, value, easing?] triples: `keyframes('y', [[0, 40, 'land'], [18, 0]])`. */
166
+ export declare const keyframes: (property: string, frames: [number, number | string, Easing?][], options?: {
167
+ repeat?: number;
168
+ reverse?: boolean;
169
+ }) => Animation;
170
+ /** One value moving to another: `tween('opacity', 0, 1, { frames: 12, delay: 4 })`. */
171
+ export declare const tween: (property: string, from: number | string, to: number | string, options?: {
172
+ frames?: number;
173
+ delay?: number;
174
+ easing?: Easing;
175
+ }) => Animation;
176
+ export declare const fadeIn: (frames?: number, delay?: number) => Animation;
177
+ export declare const fadeOut: (atFrame: number, frames?: number) => Animation;
178
+ /** Rises into place at `y` (use 0 for a child of a group with a layout): `riseIn(560, 30)` starts 30 px lower and fades in. Returns both animations. */
179
+ export declare const riseIn: (y: number, distance?: number, frames?: number, delay?: number) => Animation[];
180
+ /** Samples any function of the frame into keyframes, so springs, noise and physics become ordinary animations: `sample('x', 0, 60, f => Math.sin(f / 8) * 100)`. */
181
+ export declare const sample: (property: string, from: number, to: number, fn: (frame: number) => number, options?: {
182
+ step?: number;
183
+ }) => Animation;
184
+ /** A damped spring settling from `from` to `to`, as a function of the frame (use inside `sample`). */
185
+ export declare function spring(options: {
186
+ frame: number;
187
+ fps?: number;
188
+ damping?: number;
189
+ stiffness?: number;
190
+ mass?: number;
191
+ from?: number;
192
+ to?: number;
193
+ }): number;
194
+ declare const eases: Record<string, (t: number) => number>;
195
+ /** Maps a value through ranges: `interpolate(frame, [0, 30, 60], [0, 1, 0])`. */
196
+ export declare function interpolate(x: number, input: number[], output: number[], options?: {
197
+ easing?: keyof typeof eases | ((t: number) => number);
198
+ clamp?: boolean;
199
+ }): number;
200
+ export type VideoOptions = {
201
+ id: string;
202
+ name?: string;
203
+ width: number;
204
+ height: number;
205
+ fps?: number;
206
+ background?: unknown;
207
+ motionBlur?: {
208
+ samples?: number;
209
+ shutter?: number;
210
+ } | boolean;
211
+ /** Seconds of the whole video; leave out to follow the scenes. */ durationSeconds?: number;
212
+ tempo?: {
213
+ bpm: number;
214
+ offset?: number;
215
+ beatsPerBar?: number;
216
+ };
217
+ };
218
+ export type SceneOptions = {
219
+ name?: string; /** Frames, or use `seconds`. */
220
+ duration?: number;
221
+ seconds?: number;
222
+ startFrame?: number;
223
+ transitionIn?: Transition;
224
+ transitionOut?: Transition;
225
+ overlap?: number;
226
+ camera?: {
227
+ x?: number;
228
+ y?: number;
229
+ zoom?: number;
230
+ rotation?: number;
231
+ shake?: number;
232
+ animations?: Animation[];
233
+ };
234
+ background?: unknown;
235
+ [other: string]: unknown;
236
+ };
237
+ export type SceneBuilder = {
238
+ readonly layers: Layer[];
239
+ add(...layers: (Layer | Layer[])[]): SceneBuilder;
240
+ };
241
+ export type VideoBuilder = {
242
+ readonly fps: number;
243
+ readonly width: number;
244
+ readonly height: number;
245
+ /** Seconds to frames at this video's rate. */ frames(seconds: number): number;
246
+ scene(id: string, options: SceneOptions, build: ((scene: SceneBuilder) => void) | Layer[]): VideoBuilder;
247
+ variable(name: string, declaration: {
248
+ type: 'string' | 'number' | 'boolean' | 'color' | 'list';
249
+ default?: unknown;
250
+ description?: string;
251
+ items?: Record<string, unknown>;
252
+ }): VideoBuilder;
253
+ component(name: string, definition: {
254
+ params?: Record<string, unknown>;
255
+ layers: Layer[];
256
+ description?: string;
257
+ }): VideoBuilder;
258
+ asset(id: string, asset: {
259
+ type: 'image' | 'video' | 'audio' | 'font' | 'lottie' | 'captions';
260
+ src: string;
261
+ [other: string]: unknown;
262
+ }): VideoBuilder;
263
+ export(settings: Record<string, unknown>): VideoBuilder;
264
+ /** The composition as plain JSON, ready for `ove validate` and `ove render`. */ toJSON(): Record<string, unknown>;
265
+ };
266
+ export declare function createVideo(options: VideoOptions): VideoBuilder;
267
+ /** What `ove build` accepts from a file: a builder, a composition object, or a function (async too) returning either. */
268
+ export type Buildable = VideoBuilder | Composition | Record<string, unknown> | (() => Buildable | Promise<Buildable>);
269
+ export declare function resolveBuildable(value: Buildable): Promise<Record<string, unknown>>;
270
+ export {};
@@ -0,0 +1 @@
1
+ export declare const aiStudioPage = "<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>MotionRender | OVE AI studio</title>\n<style>\n:root{\n --bg:#07090e;\n --bg-gradient:radial-gradient(circle at 50% -10%,rgba(45,212,191,0.08) 0%,transparent 55%),radial-gradient(circle at 95% 15%,rgba(129,140,248,0.06) 0%,transparent 45%),#07090e;\n --panel:rgba(15,22,34,0.78);\n --panel-solid:#0f1622;\n --panel2:rgba(10,15,24,0.88);\n --panel2-solid:#0a0f18;\n --line:rgba(255,255,255,0.08);\n --line-hover:rgba(255,255,255,0.16);\n --text:#f8fafc;\n --muted:#94a3b8;\n --dim:#64748b;\n --accent:#2dd4bf;\n --accent-glow:rgba(45,212,191,0.25);\n --accent2:#818cf8;\n --ok:#34d399;\n --warn:#fbbf24;\n --err:#fb7185;\n --radius:12px;\n --radius-sm:8px;\n}\n*{box-sizing:border-box}html,body{height:100%}\nbody{margin:0;background:var(--bg-gradient);background-attachment:fixed;color:var(--text);font:14px/1.55 Inter,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif;letter-spacing:-.01em;display:flex;flex-direction:column;overflow:hidden;-webkit-font-smoothing:antialiased}\n::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.12);border-radius:999px}::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.24)}\na{color:var(--accent);text-decoration:none;transition:color .15s}a:hover{filter:brightness(1.15)}\nheader.top{display:flex;align-items:center;gap:16px;padding:0 22px;height:58px;border-bottom:1px solid var(--line);background:rgba(9,13,20,0.85);backdrop-filter:blur(20px);flex:none;z-index:10}\n.brand{display:flex;align-items:center;gap:10px;font-weight:700;font-size:15px;color:var(--text);letter-spacing:-.02em}\n.mark{font:800 11px/1 ui-monospace,Consolas,monospace;letter-spacing:.08em;color:#04211e;background:linear-gradient(135deg,#2dd4bf,#06b6d4);padding:6px 8px;border-radius:6px;box-shadow:0 0 12px var(--accent-glow)}\n.vpill{font-size:11px;font-weight:600;color:var(--muted);background:rgba(255,255,255,0.05);padding:2px 8px;border-radius:999px;border:1px solid var(--line)}\nnav.main{display:flex;gap:3px;background:rgba(255,255,255,0.03);padding:3px;border-radius:10px;border:1px solid var(--line)}\nnav.main a{color:var(--muted);padding:5px 13px;border-radius:7px;font-size:13px;font-weight:500;transition:all .15s}\nnav.main a:hover{color:var(--text);background:rgba(255,255,255,0.05)}\nnav.main a.on{color:#fff;background:rgba(255,255,255,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.3)}\n.grow{flex:1}\n.pbadge{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);background:rgba(255,255,255,0.03);border:1px solid var(--line);padding:4px 10px;border-radius:999px;text-decoration:none;transition:all .15s}\n.pbadge:hover{border-color:var(--line-hover);color:var(--text)}\n.pdot{width:7px;height:7px;border-radius:50%;background:var(--ok);box-shadow:0 0 8px var(--ok)}\n.busy{display:none;align-items:center;gap:9px;color:var(--muted);font-size:12.5px;max-width:320px;background:rgba(255,255,255,0.04);border:1px solid var(--line);padding:4px 11px;border-radius:999px}\n.busy.on{display:flex}.busy span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.spin{width:12px;height:12px;border:2px solid rgba(255,255,255,0.15);border-top-color:var(--accent);border-radius:50%;animation:rot .8s linear infinite;flex:none}@keyframes rot{to{transform:rotate(360deg)}}\n#app{flex:1;min-height:0;overflow:auto}\n.page{max-width:1160px;margin:0 auto;padding:32px 24px 64px}\n.hero{margin-bottom:28px;display:flex;align-items:flex-end;justify-content:space-between;gap:16px;flex-wrap:wrap}\nh1{margin:0 0 6px;font-size:26px;font-weight:700;letter-spacing:-.03em;color:#fff}\nh2{margin:0 0 10px;font-size:16px;font-weight:600;letter-spacing:-.01em}\n.lead{color:var(--muted);margin:0;font-size:14px}\nbutton{font:inherit;cursor:pointer}button:disabled{opacity:.45;cursor:not-allowed}\n.btn{background:linear-gradient(135deg,#2dd4bf 0%,#14b8a6 100%);color:#03211c;border:0;border-radius:8px;padding:8px 16px;font-weight:600;font-size:13.5px;box-shadow:0 2px 10px -2px rgba(45,212,191,0.4);transition:all .15s;display:inline-flex;align-items:center;justify-content:center;gap:7px}\n.btn:hover{filter:brightness(1.08);box-shadow:0 4px 16px -2px rgba(45,212,191,0.55);transform:translateY(-1px)}\n.btn:active{transform:translateY(0)}\n.btn.sec{background:rgba(255,255,255,0.05);color:var(--text);border:1px solid var(--line);font-weight:500;box-shadow:none}\n.btn.sec:hover{background:rgba(255,255,255,0.09);border-color:var(--line-hover);filter:none}\n.btn.sm{padding:5px 11px;font-size:12.5px;border-radius:6px}\n.btn.danger{color:#fda4af}.btn.danger:hover{background:rgba(244,63,94,0.15);border-color:rgba(244,63,94,0.3)}\ninput,textarea,select{width:100%;background:var(--panel2);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:9px 12px;font:inherit;transition:border-color .15s,box-shadow .15s}\ntextarea{resize:vertical;min-height:96px;font-size:13.5px}\ninput:focus,textarea:focus,select:focus{outline:0;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow)}\ninput[type=color]{padding:2px;height:36px;width:64px;border-radius:6px}input[type=checkbox]{width:auto}\nlabel.f{display:block;margin:0 0 16px}label.f>span{display:block;color:var(--muted);font-size:12.5px;font-weight:500;margin-bottom:6px}\n.bar{display:flex;align-items:center;gap:10px}\n.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(270px,1fr));gap:20px}\n.card{background:var(--panel);backdrop-filter:blur(16px);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;display:flex;flex-direction:column;transition:all .2s ease;box-shadow:0 4px 20px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.04)}\n.card:hover{border-color:rgba(45,212,191,0.35);transform:translateY(-3px);box-shadow:0 12px 28px -6px rgba(0,0,0,0.6),0 0 0 1px rgba(45,212,191,0.25)}\n.thumb{aspect-ratio:16/10;background:#030508;display:flex;align-items:center;justify-content:center;overflow:hidden;cursor:pointer;position:relative}\n.thumb img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .3s ease}\n.card:hover .thumb img{transform:scale(1.03)}\n.thumb .ph{color:var(--dim);font-size:12px;display:flex;align-items:center;gap:6px}\n.tbadge{position:absolute;bottom:8px;right:8px;background:rgba(0,0,0,0.75);backdrop-filter:blur(6px);color:#fff;font-size:11px;font-weight:600;padding:2px 7px;border-radius:5px;border:1px solid rgba(255,255,255,0.1)}\n.abadge{position:absolute;top:8px;left:8px;background:rgba(0,0,0,0.75);backdrop-filter:blur(6px);color:var(--muted);font-size:10.5px;font-weight:500;padding:2px 7px;border-radius:5px;border:1px solid rgba(255,255,255,0.1)}\n.cardbody{padding:14px 16px 12px;display:flex;flex-direction:column;gap:5px;flex:1}\n.cardbody b{font-size:15px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#fff}\n.meta{color:var(--muted);font-size:12.5px;display:flex;align-items:center;gap:6px}\n.cardfoot{display:flex;gap:6px;padding:0 14px 14px;border-top:1px solid rgba(255,255,255,0.04);padding-top:10px}\n.tag{display:inline-block;font-size:11px;font-weight:500;background:rgba(255,255,255,0.04);border:1px solid var(--line);border-radius:999px;padding:1px 8px;color:var(--muted)}\n.tag.ok{color:var(--ok);border-color:rgba(52,211,153,0.3);background:rgba(52,211,153,0.06)}\n.empty{text-align:center;padding:70px 24px;border:1px dashed var(--line);border-radius:16px;background:rgba(15,22,34,0.4);color:var(--muted)}\n.empty h2{color:var(--text);font-size:20px;margin-bottom:8px}\n.empty-icon{font-size:36px;margin-bottom:12px;display:block}\n.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:18px}\n.tile{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:14px 12px;text-align:left;color:var(--text);transition:all .15s}\n.tile:hover{border-color:var(--line-hover);background:rgba(255,255,255,0.03)}\n.tile b{display:block;font-size:14px;margin-bottom:3px;color:#fff}\n.tile span{color:var(--muted);font-size:12px;line-height:1.4;display:block}\n.tile.on{border-color:var(--accent);background:rgba(45,212,191,0.08);box-shadow:0 0 14px rgba(45,212,191,0.15)}\n.tile.on b{color:var(--accent)}\n.chips{display:flex;gap:7px;flex-wrap:wrap;margin:10px 0 18px}\n.chip{background:rgba(255,255,255,0.03);border:1px solid var(--line);color:var(--muted);border-radius:999px;padding:5px 13px;font-size:12.5px;transition:all .15s}\n.chip:hover{color:var(--text);border-color:rgba(45,212,191,0.4);background:rgba(45,212,191,0.05);transform:translateY(-1px)}\n.drop{border:1px dashed rgba(255,255,255,0.16);border-radius:10px;padding:22px;text-align:center;color:var(--muted);background:var(--panel2);transition:all .15s}\n.drop:hover,.drop.over{border-color:var(--accent);color:var(--text);background:rgba(45,212,191,0.04)}\n.files{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}\n.files span{background:rgba(255,255,255,0.06);border:1px solid var(--line);border-radius:6px;padding:4px 10px;font-size:12.5px;display:inline-flex;align-items:center;gap:6px}\ndetails{margin:8px 0 20px;background:rgba(255,255,255,0.02);border:1px solid var(--line);border-radius:10px;padding:12px 14px}\ndetails summary{cursor:pointer;color:var(--muted);font-weight:500;user-select:none}\ndetails summary:hover{color:var(--text)}\n.two{display:grid;grid-template-columns:1fr 1fr;gap:14px}\n.narrow{max-width:740px}\n.err{color:#fda4af;background:rgba(244,63,94,0.1);border:1px solid rgba(244,63,94,0.3);border-radius:8px;padding:12px 14px;white-space:pre-wrap;margin:12px 0;font-size:13px}\n.note{color:#fef08a;background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.25);border-radius:8px;padding:10px 14px;font-size:13px;margin:12px 0}\n.box-info{color:#a5f3fc;background:rgba(6,182,212,0.08);border:1px solid rgba(6,182,212,0.25);border-radius:8px;padding:10px 14px;font-size:13px;margin:12px 0}\n.log{background:var(--panel2-solid);border:1px solid var(--line);border-radius:10px;padding:12px 14px;font:12.5px/1.6 ui-monospace,Consolas,monospace;color:#93c5fd;max-height:260px;overflow:auto;white-space:pre-wrap}\n.center{max-width:580px;margin:80px auto;text-align:center}\n.center .spin{width:40px;height:40px;border-width:3px;margin:0 auto 24px;border-top-color:var(--accent)}\n/* creation mode switch */\n.mode-switch{display:flex;gap:4px;background:rgba(255,255,255,0.03);border:1px solid var(--line);padding:4px;border-radius:10px;margin-bottom:24px}\n.mode-switch button{flex:1;background:transparent;border:0;color:var(--muted);padding:8px 14px;border-radius:8px;font-size:13.5px;font-weight:600;transition:all .15s}\n.mode-switch button:hover{color:var(--text)}\n.mode-switch button.on{color:#fff;background:rgba(255,255,255,0.1);box-shadow:0 2px 6px rgba(0,0,0,0.3)}\n/* editor */\n.editor{display:grid;grid-template-columns:minmax(0,1fr) 420px;height:100%;min-height:0}\n.stage{display:flex;flex-direction:column;min-width:0;min-height:0;padding:16px 22px;gap:12px;overflow:auto}\n.etop{display:flex;align-items:center;gap:12px;flex-wrap:wrap}\n.back-link{color:var(--muted);font-size:13px;font-weight:500;padding:5px 9px;border-radius:6px;background:rgba(255,255,255,0.03);border:1px solid var(--line)}\n.back-link:hover{color:var(--text);border-color:var(--line-hover)}\n.etop input{font-size:17px;font-weight:700;color:#fff;background:transparent;border:1px solid transparent;width:auto;min-width:220px;padding:4px 8px;border-radius:6px}\n.etop input:hover,.etop input:focus{border-color:var(--line);background:var(--panel2)}\n.saved{color:var(--muted);font-size:12.5px;font-weight:500}\n.screen{background:#000;border-radius:14px;overflow:hidden;display:flex;justify-content:center;border:1px solid var(--line);box-shadow:0 8px 30px rgba(0,0,0,0.6);position:relative}\n#stage-player{width:100%;max-width:880px}\n#stage-player .ove-player{margin:0 auto}\n#stage-player canvas{max-height:56vh;margin:0 auto;width:auto!important;max-width:100%;box-shadow:0 0 30px rgba(0,0,0,0.8)}\n.xport{display:flex;align-items:center;gap:12px;background:rgba(15,22,34,0.6);border:1px solid var(--line);border-radius:10px;padding:8px 14px}\n.xport input[type=range]{flex:1;padding:0;accent-color:var(--accent);height:6px;cursor:pointer}\n.xport .t{color:var(--muted);font-variant-numeric:tabular-nums;font-size:12.5px;min-width:96px;text-align:right;font-family:ui-monospace,Consolas,monospace}\n.scenes{display:flex;gap:8px;overflow:auto;padding-bottom:4px}\n.scenes button{background:rgba(255,255,255,0.03);border:1px solid var(--line);color:var(--text);border-radius:8px;padding:7px 12px;white-space:nowrap;font-size:12.5px;font-weight:500;transition:all .15s}\n.scenes button:hover{border-color:rgba(45,212,191,0.4);background:rgba(45,212,191,0.05);transform:translateY(-1px)}\n.scenes small{color:var(--dim);margin-left:6px;font-family:ui-monospace,Consolas,monospace}\n.side{border-left:1px solid var(--line);background:var(--panel);backdrop-filter:blur(20px);display:flex;flex-direction:column;min-height:0}\n.tabs{display:flex;border-bottom:1px solid var(--line);padding:0 8px;gap:2px}\n.tabs button{background:none;border:0;border-bottom:2px solid transparent;color:var(--muted);padding:14px 12px;font-size:13px;font-weight:600;transition:all .15s}\n.tabs button:hover{color:var(--text)}\n.tabs button.on{color:var(--accent);border-bottom-color:var(--accent)}\n.pane{padding:20px;overflow:auto;min-height:0;flex:1}\n.hist{border-top:1px solid var(--line);padding:12px 0;display:flex;gap:10px;align-items:flex-start;justify-content:space-between}\n.hist b{font-size:13.5px;color:#fff}.hist small{display:block;color:var(--muted);margin-top:2px}\n.themes{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}\n.theme{border:1px solid var(--line);border-radius:10px;padding:10px;background:var(--panel2);color:var(--text);text-align:left;transition:all .15s}\n.theme:hover{border-color:rgba(45,212,191,0.4);transform:translateY(-1px)}\n.sw{display:flex;height:24px;border-radius:6px;overflow:hidden;margin-bottom:8px;box-shadow:0 1px 4px rgba(0,0,0,0.4)}\n.sw i{flex:1}\n.blocklist{display:flex;flex-direction:column;gap:8px}\n.block{display:flex;gap:10px;align-items:center;justify-content:space-between;border:1px solid var(--line);border-radius:10px;padding:10px 12px;background:var(--panel2);transition:all .15s}\n.block:hover{border-color:var(--line-hover)}\n.block b{font-size:13.5px;color:#fff}.block small{display:block;color:var(--muted);margin-top:2px}\n.result video,.result img{width:100%;border-radius:10px;border:1px solid var(--line);background:#000;margin-top:12px;box-shadow:0 4px 20px rgba(0,0,0,0.5)}\n.cat{margin:28px 0 10px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;font-size:11.5px;font-weight:700}\n.toast{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);background:#131c2a;border:1px solid #2a3c54;color:#fff;border-radius:10px;padding:11px 18px;z-index:99;max-width:80vw;box-shadow:0 10px 30px rgba(0,0,0,0.6);font-size:13.5px;font-weight:500;animation:toastIn .2s ease}\n.toast.bad{background:#2a1117;border-color:#5c1d29;color:#fecdd3}\n@keyframes toastIn{from{opacity:0;transform:translate(-50%,10px)}to{opacity:1;transform:translate(-50%,0)}}\n.modal{position:fixed;inset:0;background:rgba(2,4,8,0.75);backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;z-index:80}.modal>div{background:var(--panel-solid);border:1px solid var(--line);border-radius:14px;padding:24px;width:min(460px,92vw);box-shadow:0 20px 40px rgba(0,0,0,0.8)}\n@media (max-width:960px){.editor{grid-template-columns:1fr;height:auto}.side{border-left:0;border-top:1px solid var(--line)}header.top{gap:10px;padding:0 12px}.busy{display:none!important}}\n</style></head><body>\n<header class=\"top\">\n<a class=\"brand\" href=\"#/\"><span class=\"mark\">MR</span> MotionRender <span class=\"vpill\">AI Studio</span></a>\n<nav class=\"main\"><a href=\"#/\" data-nav=\"home\">Projects</a><a href=\"#/library\" data-nav=\"library\">Library</a><a href=\"#/settings\" data-nav=\"settings\">Settings</a></nav>\n<div class=\"grow\"></div>\n<a class=\"pbadge\" id=\"pbadge\" href=\"#/settings\"><span class=\"pdot\"></span><span id=\"pbadgetext\">AI Ready</span></a>\n<div class=\"busy\" id=\"busy\"><div class=\"spin\"></div><span id=\"busytext\"></span></div>\n<a class=\"btn\" href=\"#/new\">+ New Video</a>\n</header>\n<div id=\"app\"></div>\n<script>\nvar $ = function (s, r) { return (r || document).querySelector(s); };\nfunction h(tag, attrs, kids) {\n var el = document.createElement(tag);\n for (var k in (attrs || {})) { var v = attrs[k]; if (v === false || v == null) continue; if (k === 'class') el.className = v; else if (k.slice(0, 2) === 'on') el.addEventListener(k.slice(2), v); else if (k === 'text') el.textContent = v; else el.setAttribute(k, v === true ? '' : v); }\n [].concat(kids == null ? [] : kids).forEach(function (c) { if (c == null || c === false) return; el.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); });\n return el;\n}\nfunction api(path, body) {\n var opt = body === undefined ? {} : { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) };\n return fetch(path, opt).then(function (r) { return r.text().then(function (t) { var j = {}; try { j = JSON.parse(t); } catch (e) { j = { error: t }; } if (!r.ok) throw new Error(j.error || r.statusText); return j; }); });\n}\nfunction toast(msg, bad) { var t = h('div', { class: 'toast' + (bad ? ' bad' : ''), text: msg }); document.body.appendChild(t); setTimeout(function () { t.remove(); }, bad ? 6000 : 2800); }\nfunction ago(ms) { var s = Math.max(1, Math.round((Date.now() - ms) / 1000)); if (s < 60) return 'just now'; if (s < 3600) return Math.round(s / 60) + ' min ago'; if (s < 86400) return Math.round(s / 3600) + ' h ago'; return Math.round(s / 86400) + ' d ago'; }\nfunction dur(sec) { var m = Math.floor(sec / 60), s = Math.round(sec - m * 60); return m ? m + ':' + (s < 10 ? '0' : '') + s : s + 's'; }\nfunction ask(title, value, ok) {\n var input = h('input', { value: value || '' }), m = h('div', { class: 'modal' }, h('div', null, [h('h2', { text: title }), input, h('div', { class: 'bar', style: 'margin:18px 0 0;justify-content:flex-end' }, [\n h('button', { class: 'btn sec', text: 'Cancel', onclick: function () { m.remove(); } }), h('button', { class: 'btn', text: 'OK', onclick: function () { m.remove(); ok(input.value.trim()); } })])]));\n document.body.appendChild(m); input.focus(); input.select(); input.addEventListener('keydown', function (e) { if (e.key === 'Enter') { m.remove(); ok(input.value.trim()); } if (e.key === 'Escape') m.remove(); });\n}\nfunction confirmBox(text, ok) { var m = h('div', { class: 'modal' }, h('div', null, [h('p', { text: text, style: 'font-size:15px;line-height:1.5;margin:0 0 16px' }), h('div', { class: 'bar', style: 'justify-content:flex-end' }, [h('button', { class: 'btn sec', text: 'Cancel', onclick: function () { m.remove(); } }), h('button', { class: 'btn', text: 'Yes, proceed', onclick: function () { m.remove(); ok(); } })])])); document.body.appendChild(m); }\n\n/* provider status polling in top bar */\nfunction updateStatusBadge() {\n api('/api/status').then(function (s) {\n var ptext = $('#pbadgetext'); if (!ptext) return;\n if (s.provider) ptext.textContent = (s.provider.id === 'groq' ? '\u26A1 Groq \u00B7 ' : '\u2728 ' + s.provider.id + ' \u00B7 ') + s.provider.model;\n else ptext.textContent = '\uD83D\uDCCB Paste JSON mode';\n }, function () {});\n}\n\n/* jobs: one event stream, a promise per job */\nvar jobs = {}, jobWaiters = {};\nfunction onJob(job) {\n jobs[job.id] = job; var running = Object.keys(jobs).map(function (k) { return jobs[k]; }).filter(function (j) { return j.status === 'running' || j.status === 'queued'; });\n $('#busy').classList.toggle('on', running.length > 0); var last = running[0]; $('#busytext').textContent = last ? (last.messages.length ? last.messages[last.messages.length - 1] : 'Working on ' + last.kind + '\u2026') : '';\n (jobWaiters[job.id] || []).forEach(function (w) { w.progress(job); if (job.status === 'done') w.ok(job); if (job.status === 'error') w.fail(new Error(job.error || 'Failed')); });\n if (job.status === 'done' || job.status === 'error') delete jobWaiters[job.id];\n}\nfunction connect() {\n var es = new EventSource('/api/events');\n es.addEventListener('snapshot', function (e) { JSON.parse(e.data).jobs.forEach(onJob); });\n es.addEventListener('job', function (e) { onJob(JSON.parse(e.data)); });\n}\nfunction waitJob(job, progress) {\n return new Promise(function (ok, fail) {\n var cur = jobs[job.id] || job; if (cur.status === 'done') return ok(cur); if (cur.status === 'error') return fail(new Error(cur.error));\n (jobWaiters[job.id] = jobWaiters[job.id] || []).push({ ok: ok, fail: fail, progress: progress || function () {} });\n var poll = setInterval(function () { if (!jobWaiters[job.id]) return clearInterval(poll); api('/api/job?id=' + job.id).then(function (r) { onJob(r.job); }, function () {}); }, 3500);\n });\n}\nfunction run(path, body, progress) { return api(path, body).then(function (r) { return waitJob(r.job, progress); }).then(function (j) { return j.result; }); }\n\n/* routes */\nvar app = $('#app'), cleanup = null;\nfunction route() {\n if (cleanup) { cleanup(); cleanup = null; }\n var hash = location.hash.replace('#', '') || '/', parts = hash.split('/').filter(Boolean), view = parts[0] || 'home';\n document.querySelectorAll('[data-nav]').forEach(function (a) { a.classList.toggle('on', a.getAttribute('data-nav') === (view === 'new' || view === 'p' ? 'home' : view)); });\n app.innerHTML = ''; app.scrollTop = 0;\n var go = { home: home, new: create, library: library, settings: settings }[view];\n if (view === 'p' && parts[1]) return editor(decodeURIComponent(parts[1]), parts[2]);\n if (view === 'making' && parts[1]) return making(decodeURIComponent(parts[1]), parts[2]);\n (go || home)();\n}\nwindow.addEventListener('hashchange', route);\nfunction fail(el, e) { el.appendChild(h('div', { class: 'err', text: e && e.message ? e.message : String(e) })); }\n\n/* projects dashboard */\nfunction home() {\n var page = h('div', { class: 'page' }); app.appendChild(page);\n var hero = h('div', { class: 'hero' }, [\n h('div', null, [h('h1', { text: 'Your Videos' }), h('p', { class: 'lead', text: 'All your video projects. Click any project to open the live editor, re-style it, or export MP4.' })]),\n h('div', { class: 'bar' }, [h('a', { class: 'btn', href: '#/new', text: '+ New video' })])\n ]);\n page.appendChild(hero);\n api('/api/projects').then(function (r) {\n if (!r.projects.length) {\n page.appendChild(h('div', { class: 'empty' }, [\n h('span', { class: 'empty-icon', text: '\uD83C\uDFAC' }),\n h('h2', { text: 'No video projects yet' }),\n h('p', { class: 'lead', style: 'margin-bottom:20px', text: 'Describe an idea with AI, or paste composition JSON generated in ChatGPT or Claude.' }),\n h('a', { class: 'btn', href: '#/new', text: 'Create your first video' })\n ]));\n return;\n }\n var grid = h('div', { class: 'grid' }); r.projects.forEach(function (p) { grid.appendChild(card(p)); }); page.appendChild(grid);\n [].reduce.call(grid.querySelectorAll('img'), function (chain, img) { return chain.then(function () { return new Promise(function (ok) { img.onload = img.onerror = ok; img.src = img.getAttribute('data-src'); }); }); }, Promise.resolve());\n }, function (e) { fail(page, e); });\n}\nfunction card(p) {\n var open = function () { location.hash = '#/p/' + encodeURIComponent(p.id); };\n var img = h('img', { 'data-src': '/api/thumb?id=' + encodeURIComponent(p.id), alt: '' });\n img.addEventListener('error', function () { img.replaceWith(h('div', { class: 'ph', text: '\u25B6 Preview' })); });\n var aspectLabel = p.width < p.height ? '9:16 vertical' : p.width === p.height ? '1:1 square' : '16:9 wide';\n return h('div', { class: 'card' }, [\n h('div', { class: 'thumb', onclick: open }, [\n img,\n h('span', { class: 'abadge', text: aspectLabel }),\n h('span', { class: 'tbadge', text: '\u25B6 ' + dur(p.seconds) })\n ]),\n h('div', { class: 'cardbody' }, [\n h('b', { text: p.name, title: p.name }),\n h('div', { class: 'meta', text: p.width + '\u00D7' + p.height + ' \u00B7 ' + p.scenes + ' scene(s)' }),\n h('div', { class: 'meta' }, ['Edited ' + ago(p.modified) + ' ', h('span', { class: 'tag' + (p.mode === 'template' ? ' ok' : ''), text: p.mode === 'template' ? 'editable text' : 'custom' })])\n ]),\n h('div', { class: 'cardfoot' }, [\n h('button', { class: 'btn sm', text: 'Open Studio', onclick: open }),\n h('button', { class: 'btn sec sm', text: 'Duplicate', onclick: function () { api('/api/project/duplicate', { id: p.id }).then(function () { toast('Duplicated project'); route(); }, function (e) { toast(e.message, true); }); } }),\n h('button', { class: 'btn sec sm danger', text: 'Delete', onclick: function () { confirmBox('Delete \"' + p.name + '\"? It moves to .trash folder.', function () { api('/api/project/delete', { id: p.id }).then(function () { toast('Deleted'); route(); }, function (e) { toast(e.message, true); }); }); } })\n ])\n ]);\n}\n\n/* create screen with dual mode */\nvar kinds = [\n ['reel', '\uD83D\uDCF1 Social Reel', 'Vertical 9:16, ~20s. For TikTok, Shorts, Reels'],\n ['product', '\u23F9\uFE0F Product Video', 'Square 1:1, ~18s. For Instagram / LinkedIn feeds'],\n ['promo', '\uD83D\uDCFA Promo Video', 'Wide 16:9, ~30s. For YouTube, landing pages'],\n ['project', '\uD83D\uDCBC Case Study', 'Wide 16:9, ~60s. For case studies, deep dives']\n];\nvar examples = [\n 'Vertical reel explaining panic attacks and how the 4-7-8 breathing technique stops the spiral',\n 'Launch video for a budgeting app called Pocket, highlighting 3 key features and download link',\n 'Product card for a luxury hand-poured soy candle with free shipping offer',\n 'Event invite for a tech and design meetup in Berlin with RSVP call-to-action'\n];\n\nfunction create() {\n var activeMode = 'ai'; // 'ai' or 'paste'\n var kind = 'reel', files = [], page = h('div', { class: 'page narrow' }); app.appendChild(page);\n\n var modeSwitch = h('div', { class: 'mode-switch' });\n var btnAi = h('button', { class: 'on', text: '\u2728 Generate with AI' });\n var btnPaste = h('button', { text: '\uD83D\uDCCB Paste JSON (ChatGPT / Claude)' });\n modeSwitch.append(btnAi, btnPaste);\n\n var contentAi = h('div');\n var contentPaste = h('div', { style: 'display:none' });\n\n btnAi.addEventListener('click', function () {\n activeMode = 'ai'; btnAi.classList.add('on'); btnPaste.classList.remove('on');\n contentAi.style.display = 'block'; contentPaste.style.display = 'none';\n });\n btnPaste.addEventListener('click', function () {\n activeMode = 'paste'; btnPaste.classList.add('on'); btnAi.classList.remove('on');\n contentAi.style.display = 'none'; contentPaste.style.display = 'block';\n });\n\n page.append(h('h1', { text: 'Create New Video' }), h('p', { class: 'lead', style: 'margin-bottom:20px', text: 'Build with your connected AI provider, or paste composition JSON generated in ChatGPT or Claude.' }), modeSwitch, contentAi, contentPaste);\n\n /* Tab 1: AI Generation */\n var brief = h('textarea', { placeholder: 'What is your video about? Who is it for? Key points or message?', style: 'min-height:120px' });\n var name = h('input', { placeholder: 'Optional. Auto-generated from your brief if left empty' });\n var tiles = h('div', { class: 'tiles' }), fileList = h('div', { class: 'files' }), errBox = h('div');\n var drawTiles = function () {\n tiles.innerHTML = '';\n kinds.forEach(function (k) {\n tiles.appendChild(h('button', { class: 'tile' + (kind === k[0] ? ' on' : ''), onclick: function () { kind = k[0]; drawTiles(); } }, [h('b', { text: k[1] }), h('span', { text: k[2] })]));\n });\n };\n drawTiles();\n\n var drawFiles = function () {\n fileList.innerHTML = '';\n files.forEach(function (f, i) {\n fileList.appendChild(h('span', { text: f.name + ' \u00D7', style: 'cursor:pointer', onclick: function () { files.splice(i, 1); drawFiles(); } }));\n });\n };\n var picker = h('input', { type: 'file', multiple: true, accept: 'image/*,video/*,audio/*', style: 'display:none', onchange: function () { [].forEach.call(picker.files, function (f) { files.push(f); }); drawFiles(); picker.value = ''; } });\n var drop = h('div', { class: 'drop' }, ['Drag & drop photos or clips here, or ', h('a', { href: '#', text: 'browse files', onclick: function (e) { e.preventDefault(); picker.click(); } }), picker]);\n drop.addEventListener('dragover', function (e) { e.preventDefault(); drop.classList.add('over'); });\n drop.addEventListener('dragleave', function () { drop.classList.remove('over'); });\n drop.addEventListener('drop', function (e) { e.preventDefault(); drop.classList.remove('over'); [].forEach.call(e.dataTransfer.files, function (f) { files.push(f); }); drawFiles(); });\n\n var duration = h('input', { type: 'number', min: '3', placeholder: 'Default for kind' });\n var aspect = h('select', null, [['', 'Default for format'], ['9:16', 'Vertical 9:16 (Shorts/Reels)'], ['1:1', 'Square 1:1 (Feed)'], ['16:9', 'Wide 16:9 (Landscape)'], ['4:5', 'Portrait 4:5']].map(function (a) { return h('option', { value: a[0], text: a[1] }); }));\n var voice = h('input', { type: 'checkbox' }), review = h('input', { type: 'checkbox' }), go = h('button', { class: 'btn', text: '\u2728 Generate Video' });\n var chips = h('div', { class: 'chips' }, examples.map(function (t) {\n return h('button', { class: 'chip', text: t.length > 56 ? t.slice(0, 54) + '\u2026' : t, title: t, onclick: function () { brief.value = t; } });\n }));\n\n contentAi.append(\n h('label', { class: 'f' }, [h('span', { text: 'Format & Aspect Ratio' }), tiles]),\n h('label', { class: 'f' }, [h('span', { text: 'Video Brief' }), brief]),\n chips,\n h('label', { class: 'f' }, [h('span', { text: 'Media Assets (optional)' }), drop, fileList]),\n h('details', null, [\n h('summary', { text: 'Advanced Settings' }),\n h('div', { style: 'margin-top:14px' }, [\n h('label', { class: 'f' }, [h('span', { text: 'Project Name' }), name]),\n h('div', { class: 'two' }, [h('label', { class: 'f' }, [h('span', { text: 'Duration in seconds' }), duration]), h('label', { class: 'f' }, [h('span', { text: 'Aspect Ratio Override' }), aspect])]),\n h('label', { class: 'f' }, [voice, ' Add voiceover narration and captions']),\n h('label', { class: 'f' }, [review, ' Automatic review and self-correction pass'])\n ])\n ]),\n errBox,\n h('div', { style: 'margin-top:16px' }, go)\n );\n\n go.addEventListener('click', function () {\n errBox.innerHTML = ''; var text = brief.value.trim(); if (!text) { fail(errBox, new Error('Please describe your video in the brief')); return; }\n go.disabled = true; go.textContent = 'Starting\u2026';\n var title = name.value.trim() || text.split(' ').slice(0, 6).join(' ');\n api('/api/project/new', { name: title, kind: kind }).then(function (made) {\n return files.reduce(function (p, f) {\n return p.then(function () {\n return fetch('/api/upload?project=' + encodeURIComponent(made.id) + '&name=' + encodeURIComponent(f.name), { method: 'POST', headers: { 'content-type': 'application/octet-stream' }, body: f }).then(function (r) { if (!r.ok) return r.json().then(function (j) { throw new Error(j.error || 'Upload failed'); }); });\n });\n }, Promise.resolve()).then(function () {\n var body = { brief: text, kind: kind, folder: made.id, narration: voice.checked, review: review.checked };\n if (duration.value) body.duration = duration.value;\n if (aspect.value) body.aspect = aspect.value;\n return api('/api/compose', body);\n }).then(function (r) {\n location.hash = '#/making/' + encodeURIComponent(made.id) + '/' + r.job.id;\n });\n }).catch(function (e) { go.disabled = false; go.textContent = '\u2728 Generate Video'; fail(errBox, e); });\n });\n\n /* Tab 2: Paste JSON Workflow */\n var pasteBrief = h('textarea', { placeholder: 'Brief for AI prompt generator (e.g. 20-second vertical reel on panic attack recovery)...', style: 'min-height:75px' });\n var pasteKind = h('select', null, [['reel', 'Vertical Reel (9:16)'], ['product', 'Square (1:1)'], ['promo', 'Landscape Promo (16:9)'], ['project', 'Case Study (16:9)']].map(function (k) { return h('option', { value: k[0], text: k[1] }); }));\n var pastePromptBtn = h('button', { class: 'btn sec', text: '\uD83D\uDCCB Copy AI Prompt Cheatsheet' });\n var pasteInput = h('textarea', { placeholder: 'Paste valid composition JSON starting with { \"name\": ..., \"scenes\": [...] } here...', style: 'min-height:160px;font:12.5px/1.5 ui-monospace,Consolas,monospace' });\n var pasteFolder = h('input', { placeholder: 'Optional folder name (e.g. panic-attack-reel)' });\n var pasteGo = h('button', { class: 'btn', text: '\u26A1 Import & Launch Studio' });\n var pasteErr = h('div');\n\n pastePromptBtn.addEventListener('click', function () {\n pastePromptBtn.disabled = true; pastePromptBtn.textContent = 'Generating prompt\u2026';\n api('/api/prompt', { brief: pasteBrief.value.trim() || 'A modern promotional video', kind: pasteKind.value, schema: true }).then(function (r) {\n pastePromptBtn.disabled = false; pastePromptBtn.textContent = '\uD83D\uDCCB Copy AI Prompt Cheatsheet';\n if (navigator.clipboard) {\n navigator.clipboard.writeText(r.prompt).then(function () { toast('Copied prompt cheatsheet to clipboard!'); }, function () { toast('Prompt ready'); });\n } else {\n toast('Prompt ready');\n }\n }, function (e) {\n pastePromptBtn.disabled = false; pastePromptBtn.textContent = '\uD83D\uDCCB Copy AI Prompt Cheatsheet';\n fail(pasteErr, e);\n });\n });\n\n pasteGo.addEventListener('click', function () {\n pasteErr.innerHTML = ''; var raw = pasteInput.value.trim();\n if (!raw) { fail(pasteErr, new Error('Please paste your composition JSON first')); return; }\n pasteGo.disabled = true; pasteGo.textContent = 'Importing\u2026';\n api('/api/import', { json: raw, folder: pasteFolder.value.trim() || undefined }).then(function (r) {\n return waitJob(r.job, function (j) {\n if (j.messages.length) pasteGo.textContent = j.messages[j.messages.length - 1];\n });\n }).then(function (done) {\n var projId = (done.result && (done.result.id || done.result.project)) || (done.result && done.result.relative && done.result.relative.split('/')[0]) || pasteFolder.value.trim() || 'imported-video';\n toast('Imported video successfully!');\n location.hash = '#/p/' + encodeURIComponent(projId);\n }).catch(function (e) {\n pasteGo.disabled = false; pasteGo.textContent = '\u26A1 Import & Launch Studio';\n fail(pasteErr, e);\n });\n });\n\n contentPaste.append(\n h('div', { class: 'box-info', text: 'Zero-key workflow: Copy the official schema prompt into ChatGPT, Claude or Groq chat, then paste the returned JSON below.' }),\n h('h2', { style: 'margin-top:16px', text: 'Step 1: Get AI Chat Prompt' }),\n h('div', { class: 'two' }, [\n h('label', { class: 'f' }, [h('span', { text: 'Format' }), pasteKind]),\n h('label', { class: 'f' }, [h('span', { text: 'Video Topic' }), pasteBrief])\n ]),\n pastePromptBtn,\n h('h2', { style: 'margin-top:24px', text: 'Step 2: Paste JSON from AI' }),\n h('label', { class: 'f' }, [h('span', { text: 'Composition JSON' }), pasteInput]),\n h('label', { class: 'f' }, [h('span', { text: 'Project Folder Name (optional)' }), pasteFolder]),\n pasteErr,\n h('div', { style: 'margin-top:16px' }, pasteGo)\n );\n}\n\n/* live progress */\nfunction making(id, jobId) {\n var log = h('div', { class: 'log' }), title = h('h1', { text: 'Building your video\u2026' });\n var box = h('div', { class: 'center' }, [\n h('div', { class: 'spin' }),\n title,\n h('p', { class: 'lead', style: 'margin-bottom:18px', text: 'Writing scenes, positioning animations, and compiling design tokens. Ready in under a minute.' }),\n log\n ]);\n app.appendChild(box);\n waitJob({ id: jobId }, function (j) {\n log.textContent = j.messages.join(String.fromCharCode(10));\n log.scrollTop = log.scrollHeight;\n }).then(function () {\n location.hash = '#/p/' + encodeURIComponent(id);\n }, function (e) {\n var sp = box.querySelector('.spin'); if (sp) sp.remove();\n title.textContent = 'Could not finish video';\n fail(box, e);\n box.appendChild(h('div', { class: 'bar', style: 'justify-content:center;margin-top:20px' }, [\n h('a', { class: 'btn', href: '#/new', text: 'Try again' }),\n h('button', { class: 'btn sec', text: 'Delete empty folder', onclick: function () { api('/api/project/delete', { id: id }).then(function () { location.hash = '#/'; }); } })\n ]));\n });\n}\n\n/* full studio editor */\nfunction editor(id, tabHint) {\n var d, player = null, tab = tabHint || 'edit', box = h('div', { class: 'editor' }), stage = h('div', { class: 'stage' }), side = h('div', { class: 'side' });\n box.append(stage, side); app.appendChild(box); app.style.overflow = 'hidden';\n var destroyed = false;\n cleanup = function () {\n destroyed = true; document.removeEventListener('keydown', keys); app.style.overflow = '';\n if (player) { try { player.destroy(); } catch (e) {} }\n };\n var file = id + '/composition.json';\n var savedNote = h('span', { class: 'saved', text: '\u2713 Ready' });\n var screen = h('div', { class: 'screen' }, h('div', { id: 'stage-player' }));\n var sceneRow = h('div', { class: 'scenes' });\n var nameInput = h('input', { title: 'Click to rename project' });\n var pills = h('span', { class: 'tag', style: 'margin-left:4px' });\n\n var playBtn = h('button', { class: 'btn sm', text: '\u25B6 Play' });\n var scrub = h('input', { type: 'range', min: '0', max: '1', value: '0', step: '1' });\n var clock = h('span', { class: 't' });\n var xport = h('div', { class: 'xport' }, [playBtn, scrub, clock]);\n\n stage.append(\n h('div', { class: 'etop' }, [\n h('a', { class: 'back-link', href: '#/', text: '\u2190 Projects' }),\n nameInput,\n pills,\n h('div', { class: 'grow' }),\n savedNote,\n h('button', { class: 'btn sm sec', text: 'Export MP4', onclick: function () { tab = 'export'; drawTabs(); drawPane(); } })\n ]),\n screen,\n xport,\n sceneRow\n );\n\n function tick(f) {\n scrub.value = String(f);\n clock.textContent = dur(f / d.fps) + ' / ' + dur(d.durationFrames / d.fps) + ' \u00B7 f' + f;\n }\n playBtn.addEventListener('click', function () {\n if (!player) return;\n if (player.playing) player.pause(); else player.play();\n });\n scrub.addEventListener('input', function () {\n if (player) {\n if (player.playing) player.pause();\n player.seek(Number(scrub.value));\n tick(Number(scrub.value));\n }\n });\n document.addEventListener('keydown', keys);\n function keys(e) {\n if (e.key === ' ' && player && e.target === document.body) { e.preventDefault(); playBtn.click(); }\n }\n\n var tabs = h('div', { class: 'tabs' }), pane = h('div', { class: 'pane' }); side.append(tabs, pane);\n var names = [['edit', '\u270D\uFE0F Edit'], ['ai', '\u2728 Ask AI'], ['look', '\uD83C\uDFA8 Look'], ['history', '\uD83D\uDD52 History'], ['export', '\uD83D\uDE80 Export']];\n function drawTabs() {\n tabs.innerHTML = '';\n names.forEach(function (n) {\n tabs.appendChild(h('button', { class: tab === n[0] ? 'on' : '', text: n[1], onclick: function () { tab = n[0]; drawTabs(); drawPane(); } }));\n });\n }\n\n function playerStart(frame) {\n if (player) { try { player.destroy(); } catch (e) {} player = null; }\n $('#stage-player').innerHTML = '';\n return import('/player.js').then(function (m) {\n return m.createPlayer($('#stage-player'), {\n composition: d.source.raw,\n media: d.source.media,\n assetBase: location.origin + d.source.base,\n allowCode: false,\n controls: false\n });\n }).then(function (p) {\n if (destroyed) { p.destroy(); return; }\n player = p; scrub.max = String(Math.max(1, d.durationFrames - 1));\n tick(frame || 0); playBtn.textContent = '\u25B6 Play';\n p.on('frame', function (f) { tick(typeof f === 'number' ? f : p.frame); });\n p.on('play', function () { playBtn.textContent = '\u23F8 Pause'; });\n p.on('pause', function () { playBtn.textContent = '\u25B6 Play'; });\n p.on('ended', function () { playBtn.textContent = '\u25B6 Play'; });\n p.on('error', function (e) { savedNote.textContent = 'Preview error: ' + (e && e.message ? e.message : e); });\n if (frame) p.seek(frame);\n }).catch(function (e) {\n $('#stage-player').appendChild(h('div', { class: 'err', text: 'Preview player error: ' + e.message }));\n });\n }\n\n function load(keepFrame) {\n var frame = keepFrame && player ? player.frame : 0;\n return api('/api/project?id=' + encodeURIComponent(id)).then(function (r) {\n d = r; nameInput.value = d.name;\n pills.textContent = d.width + '\u00D7' + d.height + ' \u00B7 ' + d.fps + 'fps';\n drawScenes();\n return playerStart(frame);\n }).then(function () { drawPane(); }, function (e) { pane.innerHTML = ''; fail(pane, e); });\n }\n\n function drawScenes() {\n sceneRow.innerHTML = '';\n d.scenes.forEach(function (s, idx) {\n var btn = h('button', { onclick: function () { if (player) { player.seek(s.startFrame); tick(s.startFrame); } } }, [\n (idx + 1) + '. ' + s.name,\n h('small', { text: dur(s.durationFrames / d.fps) })\n ]);\n sceneRow.appendChild(btn);\n });\n }\n\n nameInput.addEventListener('change', function () {\n var v = nameInput.value.trim(); if (!v) { nameInput.value = d.name; return; }\n api('/api/project/rename', { id: id, name: v }).then(function () { d.name = v; savedNote.textContent = '\u2713 Renamed'; }, function (e) { toast(e.message, true); });\n });\n\n function save(path, body, note) {\n savedNote.textContent = 'Saving\u2026';\n return api(path, body).then(function () {\n savedNote.textContent = '\u2713 ' + (note || 'Saved');\n return load(true);\n }, function (e) { savedNote.textContent = 'Error'; toast(e.message, true); throw e; });\n }\n\n function drawPane() {\n pane.innerHTML = ''; if (!d) return;\n ({ edit: paneEdit, ai: paneAi, look: paneLook, history: paneHistory, export: paneExport }[tab])();\n }\n\n var themeNames = ['bg', 'surface', 'text', 'muted', 'accent', 'accent2', 'radius', 'headWeight'];\n\n function paneEdit() {\n var fields = [], apply = h('button', { class: 'btn', text: 'Save changes' }), box2 = h('div');\n if (d.mode === 'template') {\n d.variables.filter(function (v) { return themeNames.indexOf(v.name) < 0; }).forEach(function (v) {\n var input, label = v.description ? v.description : v.name.replace(/([A-Z])/g, ' $1').replace(/[-_]/g, ' ');\n if (v.type === 'boolean') { input = h('input', { type: 'checkbox' }); input.checked = !!v.value; }\n else if (v.type === 'color') input = h('input', { type: 'color', value: /^#[0-9a-fA-F]{6}$/.test(v.value) ? v.value : '#000000' });\n else if (v.type === 'number') input = h('input', { type: 'number', value: String(v.value) });\n else if (v.type === 'list') { input = h('textarea', { style: 'min-height:120px;font:12px ui-monospace,Consolas,monospace' }); input.value = JSON.stringify(v.value, null, 2); }\n else { input = h('textarea', { style: 'min-height:' + (String(v.value).length > 60 ? 90 : 42) + 'px' }); input.value = v.value == null ? '' : String(v.value); }\n fields.push({ v: v, input: input });\n box2.appendChild(h('label', { class: 'f' }, [h('span', { text: label }), input]));\n });\n if (!fields.length) box2.appendChild(h('p', { class: 'lead', text: 'This video has no exposed variable fields. Use Ask AI to adjust scene text.' }));\n apply.addEventListener('click', function () {\n var values = {};\n try {\n fields.forEach(function (f) {\n var v = f.v, x = f.input;\n values[v.name] = v.type === 'boolean' ? x.checked : v.type === 'number' ? Number(x.value) : v.type === 'list' ? JSON.parse(x.value) : x.value;\n });\n } catch (e) { toast('A list is not valid JSON: ' + e.message, true); return; }\n save('/api/variables', { id: id, values: values }, 'Saved');\n });\n } else {\n d.texts.forEach(function (t) {\n var input = h('textarea', { style: 'min-height:' + (t.content.length > 60 ? 80 : 40) + 'px' });\n input.value = t.content;\n fields.push({ t: t, input: input });\n box2.appendChild(h('label', { class: 'f' }, [h('span', { text: t.scene + ' \u00B7 ' + t.id }), input]));\n });\n if (!fields.length) box2.appendChild(h('p', { class: 'lead', text: 'No text layers found in this video.' }));\n apply.addEventListener('click', function () {\n var changed = fields.filter(function (f) { return f.input.value.trim() && f.input.value !== f.t.content; });\n changed.reduce(function (p, f) {\n return p.then(function () { return api('/api/text', { id: id, path: f.t.path, content: f.input.value }); });\n }, Promise.resolve()).then(function () {\n savedNote.textContent = changed.length ? '\u2713 Saved' : 'No changes';\n return load(true);\n }, function (e) { toast(e.message, true); });\n });\n }\n pane.append(\n h('h2', { text: 'Content & Text' }),\n h('p', { class: 'lead', style: 'font-size:13px;margin-bottom:16px', text: d.mode === 'template' ? 'Update headlines, copy, and variables. The visual layout adjusts dynamically.' : 'Every text line in the video in appearance order.' }),\n box2,\n fields.length ? apply : null\n );\n }\n\n function paneAi() {\n var text = h('textarea', { placeholder: 'e.g. Make it snappier, change the call to action, or add more energy', style: 'min-height:110px' });\n var go = h('button', { class: 'btn', text: '\u2728 Apply AI Change' }), errBox = h('div');\n var chips = h('div', { class: 'chips' }, [\n 'Make pace faster and punchier',\n 'Shorten all captions',\n 'Add a stronger call to action at the end',\n 'Calmer and more minimal',\n 'Use bolder, larger typography'\n ].map(function (t) { return h('button', { class: 'chip', text: t, onclick: function () { text.value = t; } }); }));\n\n pane.append(\n h('h2', { text: 'Ask AI to Refine Video' }),\n h('p', { class: 'lead', style: 'font-size:13px', text: 'Describe what you want to change in plain English. Every edit is saved to History so you can rollback anytime.' }),\n text,\n chips,\n d.mode === 'template' ? h('div', { class: 'note', text: 'Note: AI refinements unlock custom scene-by-scene editing.' }) : null,\n errBox,\n go\n );\n go.addEventListener('click', function () {\n errBox.innerHTML = ''; if (!text.value.trim()) return;\n go.disabled = true; go.textContent = 'Refining\u2026';\n run('/api/refine', { file: file, instruction: text.value.trim() }, function (j) {\n savedNote.textContent = j.messages.length ? j.messages[j.messages.length - 1] : 'Working\u2026';\n }).then(function (r) {\n savedNote.textContent = '\u2713 Updated';\n toast('Video refined successfully' + (r.warnings && r.warnings.length ? ' (with notes)' : ''));\n return load(false);\n }, function (e) {\n go.disabled = false; go.textContent = '\u2728 Apply AI Change';\n fail(errBox, e);\n });\n });\n }\n\n function paneLook() {\n pane.append(h('h2', { text: 'Look & Style' }));\n var themes = h('div', { class: 'themes' }), blocks = h('div', { class: 'blocklist' }), cat = h('select', { style: 'margin-bottom:12px' }), all = [];\n pane.append(\n h('p', { class: 'lead', style: 'font-size:13px', text: 'Pick a color palette to re-style every scene instantly.' }),\n themes,\n h('h2', { style: 'margin-top:28px', text: 'Add Scene Block' }),\n cat,\n blocks\n );\n api('/api/library').then(function (lib) {\n var canTheme = Object.keys(d.theme).length > 0;\n lib.themes.forEach(function (t) {\n var v = t.variables;\n var b = h('button', { class: 'theme', disabled: !canTheme, title: canTheme ? t.description : 'This video does not use theme tokens', onclick: function () { save('/api/theme', { id: id, theme: t.id }, 'Theme updated'); } }, [\n h('div', { class: 'sw' }, [v.bg, v.surface, v.accent, v.accent2].map(function (c) { return h('i', { style: 'background:' + c }); })),\n h('b', { text: t.name })\n ]);\n themes.appendChild(b);\n });\n all = lib.blocks;\n var cats = ['all'].concat(all.map(function (b) { return b.category; }).filter(function (c, i, a) { return a.indexOf(c) === i; }));\n cat.innerHTML = '';\n cats.forEach(function (c) { cat.appendChild(h('option', { value: c, text: c === 'all' ? 'All categories' : c })); });\n var draw = function () {\n blocks.innerHTML = '';\n all.filter(function (b) { return cat.value === 'all' || b.category === cat.value; }).forEach(function (b) {\n blocks.appendChild(h('div', { class: 'block' }, [\n h('div', null, [h('b', { text: b.name }), h('small', { text: b.description })]),\n h('button', { class: 'btn sec sm', text: '+ Add', onclick: function () { save('/api/add-block', { id: id, block: b.id }, 'Added ' + b.name).then(function () { toast('Added ' + b.name); }, function () {}); } })\n ]));\n });\n };\n cat.addEventListener('change', draw); draw();\n }, function (e) { fail(pane, e); });\n }\n\n function paneHistory() {\n pane.append(h('h2', { text: 'Version History' }), h('p', { class: 'lead', style: 'font-size:13px', text: 'Every change is versioned. Roll back anytime.' }));\n var list = h('div'); pane.appendChild(list);\n api('/api/history?file=' + encodeURIComponent(file)).then(function (r) {\n if (!r.revisions.length) { list.appendChild(h('p', { class: 'lead', text: 'No recorded versions yet.' })); return; }\n r.revisions.slice().reverse().forEach(function (rev, i) {\n list.appendChild(h('div', { class: 'hist' }, [\n h('div', null, [\n h('b', { text: '#' + rev.n + ' \u00B7 ' + rev.kind }),\n h('small', { text: rev.label }),\n h('small', { text: new Date(rev.at).toLocaleString() })\n ]),\n i === 0 ? h('span', { class: 'tag ok', text: 'current' }) : h('button', { class: 'btn sec sm', text: 'Rollback', onclick: function () {\n api('/api/restore', { file: file, n: rev.n }).then(function () { savedNote.textContent = 'Restored #' + rev.n; return load(false); }, function (e) { toast(e.message, true); });\n } })\n ]));\n });\n }, function (e) { fail(list, e); });\n }\n\n function paneExport() {\n var quality = h('select', null, [['1', 'Full HD (100% scale)'], ['0.75', 'Standard (75% scale)'], ['0.5', 'Fast Draft (50% scale)']].map(function (o) { return h('option', { value: o[0], text: o[1] }); }));\n var go = h('button', { class: 'btn', text: '\uD83C\uDFAC Render MP4 Video' }), out = h('div', { class: 'result' });\n var checkBtn = h('button', { class: 'btn sec', text: '\uD83D\uDD0D Check for issues' }), thumbBtn = h('button', { class: 'btn sec', text: '\uD83D\uDDBC\uFE0F Make Thumbnail' });\n var errBox = h('div');\n\n pane.append(\n h('h2', { text: 'Export & Render' }),\n h('p', { class: 'lead', style: 'font-size:13px', text: d.width + '\u00D7' + d.height + ' \u00B7 ' + dur(d.durationFrames / d.fps) + ' \u00B7 MP4' }),\n h('label', { class: 'f' }, [h('span', { text: 'Render Quality' }), quality]),\n go,\n h('div', { class: 'bar', style: 'margin:14px 0 0' }, [checkBtn, thumbBtn]),\n errBox,\n out\n );\n\n var guard = function (btn, label, fn) {\n btn.addEventListener('click', function () {\n errBox.innerHTML = ''; btn.disabled = true; btn.textContent = 'Working\u2026';\n fn().then(function () { btn.disabled = false; btn.textContent = label; }, function (e) { btn.disabled = false; btn.textContent = label; fail(errBox, e); });\n });\n };\n guard(go, '\uD83C\uDFAC Render MP4 Video', function () {\n return run('/api/render', { file: file, output: 'out.mp4', scale: Number(quality.value) }, function (j) {\n go.textContent = j.messages.length ? j.messages[j.messages.length - 1] : 'Rendering\u2026';\n }).then(function (r) {\n out.innerHTML = '';\n out.append(\n h('b', { text: '\uD83C\uDF89 Your video is ready!' }),\n h('video', { src: r.url, controls: true }),\n h('div', { class: 'bar', style: 'margin:10px 0 0' }, [\n h('a', { class: 'btn sm', href: r.url, download: id + '.mp4', text: 'Download MP4' }),\n h('span', { class: 'saved', text: r.relative })\n ])\n );\n });\n });\n guard(checkBtn, '\uD83D\uDD0D Check for issues', function () {\n return run('/api/review', { file: file }).then(function (r) {\n out.innerHTML = '';\n out.append(\n h('b', { text: r.issues.length ? r.issues.length + ' item(s) to review' : '\u2713 No issues found' }),\n h('ul', null, r.issues.slice(0, 12).map(function (i) { return h('li', { text: (i.message || String(i)) }); })),\n h('img', { src: r.url + '?t=' + Date.now() })\n );\n });\n });\n guard(thumbBtn, '\uD83D\uDDBC\uFE0F Make Thumbnail', function () {\n return run('/api/thumbnail', { file: file }).then(function (r) {\n out.innerHTML = '';\n out.append(\n h('b', { text: 'Generated Thumbnail' }),\n h('img', { src: r.url + '?t=' + Date.now() }),\n h('div', { class: 'bar', style: 'margin:10px 0 0' }, [\n h('a', { class: 'btn sm', href: r.url, download: id + '.png', text: 'Download PNG' })\n ])\n );\n });\n });\n }\n\n drawTabs(); load(false);\n}\n\n/* library screen */\nfunction library() {\n var page = h('div', { class: 'page' }); app.appendChild(page);\n page.append(\n h('div', { class: 'hero' }, [\n h('div', null, [h('h1', { text: 'Design Library' }), h('p', { class: 'lead', text: 'Curated color palettes and modular scene blocks ready to inject into any video.' })])\n ])\n );\n api('/api/library').then(function (lib) {\n page.appendChild(h('h2', { text: 'Color Palettes' }));\n var row = h('div', { class: 'grid', style: 'grid-template-columns:repeat(auto-fill,minmax(200px,1fr));margin-bottom:36px' });\n lib.themes.forEach(function (t) {\n var v = t.variables;\n row.appendChild(h('div', { class: 'card', style: 'padding:14px' }, [\n h('div', { class: 'sw' }, [v.bg, v.surface, v.accent, v.accent2].map(function (c) { return h('i', { style: 'background:' + c }); })),\n h('b', { text: t.name, style: 'font-size:14.5px;color:#fff' }),\n h('div', { class: 'meta', style: 'margin-top:4px', text: t.description })\n ]));\n });\n page.appendChild(row);\n var cats = lib.blocks.map(function (b) { return b.category; }).filter(function (c, i, a) { return a.indexOf(c) === i; });\n cats.forEach(function (c) {\n page.appendChild(h('div', { class: 'cat', text: c }));\n var g = h('div', { class: 'grid', style: 'grid-template-columns:repeat(auto-fill,minmax(260px,1fr));margin-bottom:24px' });\n lib.blocks.filter(function (b) { return b.category === c; }).forEach(function (b) {\n g.appendChild(h('div', { class: 'card', style: 'padding:16px;gap:8px' }, [\n h('b', { text: b.name, style: 'font-size:14.5px;color:#fff' }),\n h('div', { class: 'meta', text: b.description }),\n h('div', { class: 'meta', text: (b.kind === 'overlay' ? 'Overlay \u00B7 ' : 'Scene \u00B7 ') + dur(b.durationFrames / 30) }),\n h('button', { class: 'btn sec sm', style: 'align-self:flex-start;margin-top:6px', text: '+ Add to video\u2026', onclick: function () { pick(b); } })\n ]));\n });\n page.appendChild(g);\n });\n }, function (e) { fail(page, e); });\n}\n\nfunction pick(block) {\n api('/api/projects').then(function (r) {\n if (!r.projects.length) { toast('Create a video project first', true); return; }\n var sel = h('select', null, r.projects.map(function (p) { return h('option', { value: p.id, text: p.name }); }));\n var m = h('div', { class: 'modal' }, h('div', null, [\n h('h2', { text: 'Add ' + block.name + ' to project:' }),\n sel,\n h('div', { class: 'bar', style: 'margin:18px 0 0;justify-content:flex-end' }, [\n h('button', { class: 'btn sec', text: 'Cancel', onclick: function () { m.remove(); } }),\n h('button', { class: 'btn', text: 'Add Scene', onclick: function () {\n api('/api/add-block', { id: sel.value, block: block.id }).then(function () {\n m.remove(); location.hash = '#/p/' + encodeURIComponent(sel.value);\n }, function (e) { toast(e.message, true); });\n } })\n ])\n ]));\n document.body.appendChild(m);\n });\n}\n\n/* settings screen */\nfunction settings() {\n var page = h('div', { class: 'page narrow' }); app.appendChild(page);\n page.append(\n h('h1', { text: 'Studio Settings' }),\n h('p', { class: 'lead', style: 'margin-bottom:24px', text: 'Configuration, local storage path, and AI engine status.' })\n );\n api('/api/status').then(function (s) {\n var pinfo = s.provider ? s.provider.id.toUpperCase() + ' (' + s.provider.model + ')' : 'Not Connected';\n var rows = [\n ['Projects Directory', s.outDir],\n ['Active AI Provider', pinfo]\n ];\n rows.forEach(function (r) {\n page.appendChild(h('label', { class: 'f' }, [h('span', { text: r[0] }), h('input', { value: r[1], readonly: true })]));\n });\n if (s.error) {\n page.appendChild(h('div', { class: 'err', text: s.error + String.fromCharCode(10) + 'Tip: Add GROQ_API_KEY=... or GEMINI_API_KEY=... in your .env file, or use the zero-key Paste JSON workflow!' }));\n } else {\n page.appendChild(h('div', { class: 'note', text: 'Connected to ' + pinfo + '. AI generation and refinements will execute directly through this provider.' }));\n }\n page.appendChild(h('div', { class: 'box-info', text: 'Zero-API Key Workflow: You can also use ChatGPT or Claude directly by copying the prompt cheatsheet from \"+ New Video > Paste JSON\" and pasting the returned JSON.' }));\n page.appendChild(h('p', { class: 'meta', style: 'margin-top:20px', text: 'To change output directory, run: ove ai studio --out <custom-folder>' }));\n }, function (e) { fail(page, e); });\n}\n\nupdateStatusBadge();\nconnect();\nroute();\n</script></body></html>";
@@ -0,0 +1,35 @@
1
+ import { type AiProvider } from '../../ai/src/index.js';
2
+ import { type ApiLibrary, type PlayerFiles } from '../../api/src/index.js';
3
+ import { canvasFor, folderName, type JobEnv } from '../../jobs/src/index.js';
4
+ import { FsProjectStore, within, type Principal } from '../../storage/src/index.js';
5
+ export type AiStudioOptions = {
6
+ /** Interface to listen on. Defaults to 127.0.0.1; anything else exposes a server that has no login. */
7
+ host?: string;
8
+ /** The port people use to reach the studio when it differs from the one it listens on (a container's published port). */
9
+ publicPort?: number;
10
+ port?: number;
11
+ outDir?: string;
12
+ provider?: string;
13
+ model?: string;
14
+ baseUrl?: string;
15
+ allowHosts?: string[];
16
+ };
17
+ export type AiStudioServer = {
18
+ url: string;
19
+ close(): Promise<void>;
20
+ };
21
+ export { canvasFor, folderName, within };
22
+ /** What ships with this command and the API shows: blocks, themes, templates. */
23
+ export declare const studioLibrary: ApiLibrary;
24
+ /** The Player and what it loads beside itself (the same files `ove dev` serves). */
25
+ export declare const studioPlayer: PlayerFiles;
26
+ export declare const studioApp: {
27
+ dir: string;
28
+ fallbackPage: string;
29
+ };
30
+ /** The environment every job runs in on this machine: the store's folders, the provider from the environment, and the local-only inputs. */
31
+ export declare function jobEnvFor(store: FsProjectStore, requireProvider: () => AiProvider, providerInfo: () => {
32
+ provider?: string;
33
+ model?: string;
34
+ }, local: boolean): (who: Principal) => JobEnv;
35
+ export declare function startAiStudio(options?: AiStudioOptions): Promise<AiStudioServer>;
@@ -0,0 +1,3 @@
1
+ declare const usage = "Usage:\n ove ai doctor show the provider, model and capabilities\n ove ai history <composition.json> list the kept revisions of an AI-made video\n ove ai restore <composition.json> <n> bring revision n back (nothing is lost: the restore is a revision too)\n ove ai compose <dir> --brief \"...\" [--kind reel|product|promo|project] [--aspect 9:16] [--duration 20] [--fps 30] [--full]\n [--narration] [--review] [--render] [--force] [--dry-run] [--asset id=path] [--fields name[:type],...]\n ove ai refine <composition.json> --instruction \"...\" [--review] [--dry-run]\n ove ai narrate <script.txt> [--composition video.json] [--out dir] [--voice Puck] [--language en] [--tail 0.8]\n ove ai narrate --brief \"...\" [--duration 20] [--composition video.json]\n ove ai transcribe <audio> [--composition video.json] [--out dir] [--language en] [--start 0]\n ove ai image \"<prompt>\" --out assets/bg.png [--size square|portrait|landscape|WxH] [--composition video.json --id bg]\n ove ai batch <rows.json> --brief \"...\" --output \"out/{{id}}.mp4\" [--template video.json] [--out-dir dir] [--limit n]\n [--duration 18] [--fps 30] [render options]\n ove ai studio [--port 3100] [--out ai-videos] open a local page for the AI commands in a browser\n\nProvider options (all commands): --provider groq|mock, --model name, --base-url URL\nEnvironment: OVE_AI_PROVIDER, OVE_AI_MODEL, OVE_AI_API_KEY, and GROQ_API_KEY.\nRender options: --crf, --bitrate, --audio-bitrate, --scale, --loudness, --concurrency, --preset, --codec, --format";
2
+ export declare function runAi(argv: string[]): Promise<number>;
3
+ export { usage as aiUsage };
@@ -0,0 +1,9 @@
1
+ type Spec = {
2
+ values?: string[];
3
+ flags?: string[];
4
+ positionals?: number;
5
+ };
6
+ export declare const commandSpecs: Record<string, Spec>;
7
+ export declare const wantsHelp: (argv: string[]) => boolean;
8
+ export declare function checkArgs(argv: string[]): string[];
9
+ export {};
@@ -0,0 +1,3 @@
1
+ export declare function runBlocks(argv: string[]): Promise<void>;
2
+ export declare function runAdd(argv: string[]): Promise<void>;
3
+ export declare function runTheme(argv: string[]): Promise<void>;
@@ -0,0 +1,50 @@
1
+ import { type VariableDeclaration } from '../../schema/src/index.js';
2
+ export declare const blocksDir: string;
3
+ export declare const themesDir: string;
4
+ export type Block = {
5
+ id: string;
6
+ name: string;
7
+ description: string;
8
+ category: string;
9
+ kind: 'scene' | 'overlay';
10
+ durationFrames: number;
11
+ transition?: {
12
+ type: string;
13
+ durationFrames: number;
14
+ }; /** Scene settings the block brings along, such as a slow camera push. */
15
+ scene?: Record<string, unknown>;
16
+ params: Record<string, VariableDeclaration>;
17
+ layers: unknown[];
18
+ };
19
+ export type Theme = {
20
+ id: string;
21
+ name: string;
22
+ description: string;
23
+ variables: Record<string, string | number>;
24
+ };
25
+ export declare const listBlocks: (root?: string) => Promise<Block[]>;
26
+ export declare const listThemes: (root?: string) => Promise<Theme[]>;
27
+ export declare function readBlock(id: string, root?: string): Promise<Block>;
28
+ export declare function readTheme(id: string, root?: string): Promise<Theme>;
29
+ /** The variable names every block reads. */
30
+ export declare const themeTokens: readonly ["bg", "surface", "text", "muted", "accent", "accent2", "radius", "headWeight"];
31
+ /** Sets the theme's values as the composition's variable defaults (other variables are left alone). */
32
+ export declare function applyTheme(raw: any, theme: Theme, only?: readonly string[]): any;
33
+ /** `frames` sets how long a scene lasts, or how long an overlay stays (instead of the block's own length). */
34
+ export type AddOptions = {
35
+ at?: number;
36
+ frames?: number;
37
+ scene?: string;
38
+ id?: string;
39
+ set?: Record<string, unknown>;
40
+ };
41
+ export type AddResult = {
42
+ composition: any;
43
+ placed: string;
44
+ where: string;
45
+ };
46
+ /** Adds the block to the raw composition (a new scene, or an overlay inside a scene) and returns the checked result. */
47
+ export declare function addBlock(input: any, block: Block, options?: AddOptions): AddResult;
48
+ /** A new, empty composition of the given size for the first block to go into. */
49
+ export declare function emptyComposition(name: string, width: number, height: number, fps?: number): any;
50
+ export declare const themeDefaults: Theme;
@@ -0,0 +1 @@
1
+ export declare function runBuild(argv: string[]): Promise<void>;
@@ -0,0 +1 @@
1
+ export declare const devPage = "<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>OVE preview</title>\n<style>\n:root{--bg:#0b0f17;--panel:#111827;--line:#1f2937;--text:#e5e7eb;--muted:#9ca3af;--accent:#818cf8;--err:#f87171}\n*{box-sizing:border-box}\nbody{margin:0;background:var(--bg);color:var(--text);font:13px/1.4 system-ui,-apple-system,\"Segoe UI\",sans-serif;height:100vh;display:grid;grid-template-columns:minmax(0,1fr) 300px;grid-template-rows:auto minmax(0,1fr) auto}\nheader{grid-column:1/3;display:flex;gap:12px;align-items:baseline;padding:10px 16px;border-bottom:1px solid var(--line)}\nheader b{font-size:14px}#file,#status{color:var(--muted)}#status{margin-left:auto}\n#stage{position:relative;display:flex;align-items:center;justify-content:center;min-height:0;padding:16px}\n#frame{width:100%;height:100%;object-fit:contain}\n#layers{max-height:190px;overflow:auto;margin-top:6px;position:relative}\n.lrow{position:relative;height:22px}\n.lbar{position:absolute;top:3px;height:16px;background:#334155;border-radius:3px;font-size:11px;line-height:16px;padding:0 6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#cbd5e1;cursor:grab;user-select:none;touch-action:none;box-sizing:border-box}\n.lbar.sel{background:#4f46e5;color:#fff}.lbar.ro{background:repeating-linear-gradient(45deg,#1e293b 0 6px,#273449 6px 12px);color:var(--muted);cursor:pointer}\n.lbar .grip{position:absolute;right:0;top:0;bottom:0;width:7px;cursor:ew-resize;background:rgba(255,255,255,.12)}.lbar.ro .grip{display:none}\n#props .field{display:flex;gap:8px;align-items:center;margin:6px 0}#props .field span{width:92px;flex:none;color:var(--muted);font-size:12px;overflow:hidden;text-overflow:ellipsis}\n#props .field input:not([type=checkbox]):not([type=color]),#props .field textarea{flex:1;min-width:0}#props .field input[type=color]{width:34px;height:28px;padding:1px;flex:none}#props h3{margin:0 0 2px;font-size:14px}#props .note{color:var(--muted);font-size:12px;margin:4px 0 8px}#props .err{color:#fca5a5;font-size:12px;white-space:pre-wrap}\n#live{position:absolute;inset:16px;display:none;align-items:center;justify-content:center}\n#live .ove-player{width:auto!important;height:100%;display:flex;align-items:center;justify-content:center}\n#live canvas{width:auto!important;height:auto!important;max-width:100%;max-height:100%}\n#error{position:absolute;top:16px;left:16px;right:16px;background:#2a1215;border:1px solid var(--err);color:#fecaca;padding:10px 12px;border-radius:6px;white-space:pre-wrap;font:12px/1.5 ui-monospace,Consolas,monospace;max-height:60%;overflow:auto;display:none}\naside{grid-row:2/4;grid-column:2;border-left:1px solid var(--line);background:var(--panel);padding:14px;overflow:auto}\naside h2{font-size:12px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin:0 0 4px}\nfooter{padding:10px 16px 14px;border-top:1px solid var(--line)}\n.controls{display:flex;gap:10px;align-items:center;margin-bottom:8px}\nbutton{background:var(--line);color:var(--text);border:0;border-radius:6px;padding:6px 12px;cursor:pointer;font:inherit}button:hover{background:#2b3648}\n#time{font-variant-numeric:tabular-nums;color:var(--muted)}.keys{margin-left:auto;color:var(--muted);font-size:12px}\n#timeline{position:relative;height:40px;cursor:pointer;user-select:none;touch-action:none}\n.scene{position:absolute;top:0;height:18px;background:#1e293b;border:1px solid #334155;border-radius:3px;font-size:11px;line-height:16px;padding:0 5px;overflow:hidden;white-space:nowrap;color:var(--muted)}\n#track{position:absolute;top:26px;left:0;right:0;height:6px;background:var(--line);border-radius:3px}\n.scene.ed{cursor:grab}.sgrip{position:absolute;top:0;bottom:0;width:7px;cursor:ew-resize;background:rgba(148,163,184,.25)}.sgrip.l{left:0}.sgrip.r{right:0}\n.lbar .lgrip{left:0;right:auto}.krow{height:12px;position:relative}.kf{position:absolute;top:1px;width:9px;height:9px;margin-left:-4px;background:#fbbf24;transform:rotate(45deg);cursor:ew-resize;border:1px solid #92400e}\n#playhead{position:absolute;top:0;bottom:0;width:2px;margin-left:-1px;background:var(--accent);pointer-events:none}\nlabel{display:block;margin:12px 0 4px;color:var(--text)}label small{color:var(--muted);display:block;font-size:12px}\ninput,textarea{width:100%;background:var(--bg);color:var(--text);border:1px solid var(--line);border-radius:5px;padding:6px 7px;font:inherit}textarea{resize:vertical;min-height:54px}\ninput:focus,textarea:focus{outline:2px solid var(--accent);outline-offset:-1px}\n.row{display:flex;gap:6px;align-items:center}.row input[type=color]{width:42px;height:32px;padding:2px;flex:none}.row input[type=checkbox]{width:auto}\n.hint{color:var(--muted);font-size:12px;margin-top:6px}\n</style></head>\n<body>\n<header><b id=\"name\">OVE preview</b><span id=\"file\"></span><span id=\"status\">connecting\u2026</span></header>\n<main id=\"stage\"><img id=\"frame\" alt=\"Current frame\"><div id=\"live\"></div><div id=\"error\" role=\"alert\"></div></main>\n<aside><div id=\"props\"><h2>Layer</h2><p class=\"note\">Click a layer bar below the timeline to edit it. Drag a bar to move it in time, drag its right edge to change when it ends.</p></div><h2 style=\"margin-top:18px\">Variables</h2><div id=\"vars\"></div><p class=\"hint\" id=\"varhint\"></p></aside>\n<footer>\n <div class=\"controls\"><button id=\"play\" title=\"Play or pause (space)\">Play</button><button id=\"undo\" title=\"Undo the last edit made here (ctrl+z)\" disabled>Undo</button><span id=\"time\"></span><span class=\"keys\">space play \u00B7 \u2190 \u2192 frame \u00B7 shift \u00B1 10 \u00B7 home/end</span></div>\n <div id=\"timeline\"><div id=\"scenes\"></div><div id=\"track\"></div><div id=\"playhead\"></div></div>\n <div id=\"layers\"></div>\n</footer>\n<script>\nvar $ = function (s) { return document.querySelector(s); };\nvar state = null, frame = 0, playing = false, shown = '', cache = new Map(), inflight = new Set(), varTimer = 0;\nfunction comp() { return state && state.composition; }\nfunction key(f) { return state.version + ':' + f; }\nfunction clamp(f) { return Math.max(0, Math.min(comp().durationFrames - 1, f)); }\nfunction request(f) {\n if (!comp() || f < 0 || f >= comp().durationFrames) return;\n var k = key(f), v = state.version; if (cache.has(k) || inflight.has(k)) return; inflight.add(k);\n fetch('/api/frame?f=' + f + '&v=' + v).then(function (r) { if (!r.ok) throw new Error(); return r.blob(); })\n .then(function (b) { if (v !== state.version) return; cache.set(k, URL.createObjectURL(b)); while (cache.size > 400) { var first = cache.keys().next().value; URL.revokeObjectURL(cache.get(first)); cache.delete(first); } draw(); })\n .catch(function () {}).then(function () { inflight.delete(k); prefetch(); });\n}\nfunction prefetch() { if (!comp()) return; var ahead = playing ? 30 : 3; for (var i = 1; i <= ahead && inflight.size < 6; i++) request((frame + i) % comp().durationFrames); }\nfunction draw() {\n var c = comp(); if (!c) return; var url = cache.get(key(frame));\n if (!playing && url && shown !== url) { $('#frame').src = url; shown = url; }\n $('#status').textContent = playing ? 'playing live with sound' : url ? 'exact frame' : 'rendering frame ' + frame + '\u2026';\n $('#time').textContent = 'frame ' + frame + ' / ' + (c.durationFrames - 1) + ' \u00B7 ' + (frame / c.fps).toFixed(2) + 's of ' + (c.durationFrames / c.fps).toFixed(2) + 's';\n $('#playhead').style.left = (c.durationFrames > 1 ? frame / (c.durationFrames - 1) * 100 : 0) + '%';\n}\nfunction seek(f) { if (!comp()) return; frame = clamp(f); if (playing && player) player.seek(frame); request(frame); prefetch(); draw(); }\n// Playing hands over to the live Player (drawn in this browser, with sound); pausing returns to the exact frame rendered by the server.\nvar player = null, playerVersion = -1, starting = false;\nfunction dropPlayer() { if (player) { player.destroy(); player = null; } playerVersion = -1; }\nfunction ensurePlayer() {\n if (player && playerVersion === state.version) return Promise.resolve(player); dropPlayer();\n return fetch('/api/source').then(function (r) { return r.json(); }).then(function (source) { return import('/player.js').then(function (m) {\n return m.createPlayer($('#live'), { composition: source.raw, values: source.values, media: source.media, assetBase: location.origin + '/media/', allowCode: source.allowCode === true, controls: false }); }); })\n .then(function (p) { player = p; playerVersion = state.version;\n p.on('frame', function (f) { if (playing) { frame = f; draw(); } }); p.on('ended', function () { setPlaying(false); });\n p.on('error', function (e) { $('#status').textContent = 'live playback: ' + (e && e.message ? e.message : e); }); return p; });\n}\nfunction setPlaying(p) {\n if (p && comp() && !playing && !starting) { starting = true; $('#status').textContent = 'starting live playback\u2026';\n ensurePlayer().then(function (pl) { return pl.seek(frame).then(function () { playing = true; $('#live').style.display = 'flex'; $('#frame').style.visibility = 'hidden'; $('#play').textContent = 'Pause'; return pl.play(); }); })\n .catch(function (e) { $('#status').textContent = 'live playback failed: ' + e.message; }).then(function () { starting = false; draw(); });\n return; }\n if (!p && playing) { if (player) { player.pause(); frame = player.frame; } playing = false; $('#live').style.display = 'none'; $('#frame').style.visibility = 'visible'; $('#play').textContent = 'Play'; shown = ''; request(frame); prefetch(); draw(); }\n}\n// Studio: one bar per layer on the same time scale as the timeline. Editable bars can be dragged (move in time) or resized from the\n// right edge (end frame); clicking one opens its properties. Edits go to the server, which validates and writes composition.json.\nvar selected = null, selectedRaw = null;\nfunction el(tag, props, children) { var e = document.createElement(tag); Object.keys(props || {}).forEach(function (k) { if (k === 'style') e.style.cssText = props[k]; else if (k === 'text') e.textContent = props[k]; else e[k] = props[k]; }); (children || []).forEach(function (c) { e.appendChild(c); }); return e; }\nfunction pct(f) { return (f / comp().durationFrames * 100) + '%'; }\nfunction renderLayers() {\n var box = $('#layers'); box.innerHTML = ''; if (!comp()) return;\n comp().layers.forEach(function (l) {\n var bar = el('div', { className: 'lbar' + (l.editable ? '' : ' ro') + (l.id === selected ? ' sel' : ''), title: l.id + ' (' + l.type + ') \u00B7 frames ' + l.start + '\u2013' + (l.end - 1) + (l.editable ? '' : ' \u00B7 read-only here'), text: (l.depth ? '\\u2514 ' : '') + l.id + ' \u00B7 ' + l.type });\n bar.style.left = pct(l.start); bar.style.width = 'calc(' + pct(Math.max(1, l.end - l.start)) + ' - 1px)';\n if (l.editable) { var lgrip = el('div', { className: 'grip lgrip' }); bar.appendChild(lgrip); lgrip.addEventListener('pointerdown', function (e) { e.stopPropagation(); startDrag(e, l, bar, 'start'); }); var grip = el('div', { className: 'grip' }); bar.appendChild(grip); grip.addEventListener('pointerdown', function (e) { e.stopPropagation(); startDrag(e, l, bar, 'end'); }); bar.addEventListener('pointerdown', function (e) { startDrag(e, l, bar, 'move'); }); }\n else bar.addEventListener('click', function () { selectLayer(l.id); });\n box.appendChild(el('div', { className: 'lrow' }, [bar])); var keys = keyframeRow(l); if (keys) box.appendChild(keys);\n });\n}\nfunction startDrag(e, layer, bar, mode) {\n selectLayer(layer.id); var x0 = e.clientX, width = $('#layers').getBoundingClientRect().width, delta = 0, left0 = parseFloat(bar.style.left), total = comp().durationFrames; bar.setPointerCapture(e.pointerId); bar.style.cursor = 'grabbing';\n function move(ev) { delta = Math.round((ev.clientX - x0) / width * total); if (mode !== 'end' && Math.abs(layer.start + delta - frame) <= 2) delta = frame - layer.start; if (mode === 'move') bar.style.left = 'calc(' + left0 + '% + ' + (delta / total * width) + 'px)'; else if (mode === 'start') { bar.style.left = 'calc(' + left0 + '% + ' + (delta / total * width) + 'px)'; bar.style.width = 'calc(' + pct(Math.max(1, layer.end - layer.start - delta)) + ' - 1px)'; } else bar.style.width = 'calc(' + pct(Math.max(1, layer.end - layer.start + delta)) + ' - 1px)'; $('#status').textContent = (mode === 'move' ? 'move by ' : mode === 'start' ? 'start ' : 'end ') + (delta >= 0 ? '+' : '') + delta + ' frames'; }\n function up() { bar.removeEventListener('pointermove', move); bar.removeEventListener('pointerup', up); bar.style.cursor = ''; if (!delta) return;\n waitForRaw().then(function (raw) { var start = typeof raw.startFrame === 'number' ? raw.startFrame : 0, parentOffset = layer.start - start, set = {};\n if (typeof raw.startFrame === 'string' || typeof raw.endFrame === 'string') return showPropsError('Its timing comes from a variable, so it can only be changed in the file.');\n if (mode === 'move') { set.startFrame = Math.max(0, start + delta); if (typeof raw.endFrame === 'number') set.endFrame = Math.max(set.startFrame + 1, raw.endFrame + delta); }\n else if (mode === 'start') set.startFrame = Math.max(0, start + delta); else set.endFrame = Math.max(start + 1, layer.end - parentOffset + delta);\n editLayer(set, []); }); }\n bar.addEventListener('pointermove', move); bar.addEventListener('pointerup', up);\n}\nvar rawRequest = null;\nfunction waitForRaw() { return rawRequest || Promise.resolve(selectedRaw); }\nfunction selectLayer(id) { selected = id; document.querySelectorAll('.lbar').forEach(function (b) { b.classList.toggle('sel', b.title.indexOf(id + ' (') === 0); });\n rawRequest = fetch('/api/layer?id=' + encodeURIComponent(id)).then(function (r) { return r.json(); }).then(function (info) { selectedRaw = info.layer; renderProps(info); renderLayers(); rawRequest = null; return info.layer; }); return rawRequest; }\nvar common = ['x', 'y', 'width', 'height', 'opacity', 'rotation', 'scale', 'startFrame', 'endFrame'];\nfunction renderProps(info) {\n var box = $('#props'), row = (comp().layers.filter(function (l) { return l.id === info.id; })[0]) || {}; box.innerHTML = '';\n box.appendChild(el('h2', { text: 'Layer' })); box.appendChild(el('h3', { text: info.id + ' \u00B7 ' + (row.type || '') }));\n box.appendChild(el('p', { className: 'note', text: 'Frames ' + row.start + '\u2013' + (row.end - 1) + (info.editable ? '. Changes are saved to composition.json.' : '. ' + info.reason) }));\n box.appendChild(el('div', { className: 'err', id: 'propsError' }));\n if (!info.editable || !info.layer) return;\n var layer = info.layer, keys = Object.keys(layer).filter(function (k) { return ['id', 'type'].indexOf(k) < 0 && ['number', 'string', 'boolean'].indexOf(typeof layer[k]) >= 0; });\n common.forEach(function (k) { if (keys.indexOf(k) < 0 && !(row.type === 'audio' && ['x', 'y', 'width', 'height', 'opacity', 'rotation', 'scale'].indexOf(k) >= 0)) keys.push(k); });\n keys.forEach(function (k) { var v = layer[k], templated = typeof v === 'string' && v.indexOf('{{') >= 0, input;\n if (typeof v === 'boolean') { input = el('input', { type: 'checkbox', checked: v }); }\n else if (k === 'content') { input = el('textarea', { value: v || '', rows: 2 }); }\n else if (typeof v === 'number' || (v === undefined && common.indexOf(k) >= 0)) { input = el('input', { type: 'number', step: 'any', value: v === undefined ? '' : v, placeholder: 'default' }); }\n else { input = el('input', { value: v === undefined ? '' : v }); }\n if (templated) { input.disabled = true; input.title = 'From a variable; change it in the file or the variables panel'; }\n input.addEventListener('change', function () { var set = {}, unset = [];\n if (input.type === 'checkbox') set[k] = input.checked; else if (input.type === 'number') { if (input.value === '') unset.push(k); else set[k] = Number(input.value); } else set[k] = input.value;\n editLayer(set, unset); });\n var field = el('label', { className: 'field' }, [el('span', { text: k, title: k }), input]);\n if (typeof v === 'string' && /^(#[0-9a-f]{6})$/i.test(v)) { var picker = el('input', { type: 'color', value: v }); picker.addEventListener('change', function () { input.value = picker.value; var set = {}; set[k] = picker.value; editLayer(set, []); }); field.insertBefore(picker, input); }\n box.appendChild(field); });\n}\nfunction showPropsError(message) { var e = document.querySelector('#propsError'); if (e) e.textContent = message || ''; }\nfunction editLayer(set, unset) { showPropsError(''); $('#status').textContent = 'saving\u2026';\n fetch('/api/layer', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: selected, set: set, unset: unset }) }).then(function (r) { return r.json(); })\n .then(function (result) { if (!result.ok) { showPropsError(result.error); renderLayers(); $('#status').textContent = 'not saved'; } }); }\n// Scenes: drag a scene bar to move it, or an edge to change when it starts or how long it lasts. Edits go to the file like layer edits.\nfunction startSceneDrag(e, s, bar, mode) {\n e.stopPropagation(); var width = $('#timeline').getBoundingClientRect().width, total = comp().durationFrames, x0 = e.clientX, delta = 0; bar.setPointerCapture(e.pointerId);\n function shown() { var start = s.startFrame, dur = s.durationFrames; if (mode === 'move') start = Math.max(0, s.startFrame + delta); else if (mode === 'end') dur = Math.max(1, s.durationFrames + delta); else { start = Math.max(0, Math.min(s.startFrame + delta, s.startFrame + s.durationFrames - 1)); dur = s.durationFrames - (start - s.startFrame); } return { start: start, dur: dur }; }\n function move(ev) { delta = Math.round((ev.clientX - x0) / width * total); var v = shown(); bar.style.left = (v.start / total * 100) + '%'; bar.style.width = (v.dur / total * 100) + '%'; $('#status').textContent = 'scene ' + s.id + ': start ' + v.start + ', ' + v.dur + ' frames'; }\n function up() { bar.removeEventListener('pointermove', move); bar.removeEventListener('pointerup', up); if (!delta) return; var v = shown(), set = {}; if (v.start !== s.startFrame) set.startFrame = v.start; if (v.dur !== s.durationFrames) set.durationFrames = v.dur; editScene(s.id, set); }\n bar.addEventListener('pointermove', move); bar.addEventListener('pointerup', up);\n}\nfunction editScene(id, set) { $('#status').textContent = 'saving\u2026'; fetch('/api/scene', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: id, set: set }) }).then(function (r) { return r.json(); })\n .then(function (result) { if (!result.ok) { $('#status').textContent = result.error; renderTimeline(); } }); }\n// Keyframes of the selected layer as diamonds under its bar: drag one to retime it (frames count from the layer's start).\nfunction keyframeRow(l) {\n if (l.id !== selected || !selectedRaw || !Array.isArray(selectedRaw.animations) || !l.editable) return null; var row = el('div', { className: 'lrow krow' });\n selectedRaw.animations.forEach(function (a, ai) { (a.keyframes || []).forEach(function (k, ki) { if (typeof k.frame !== 'number') return;\n var d = el('div', { className: 'kf', title: a.property + ' at frame ' + k.frame + ' = ' + k.value + ' (drag to retime)' }); d.style.left = pct(Math.min(comp().durationFrames - 1, l.start + k.frame));\n d.addEventListener('pointerdown', function (e) { startKeyframeDrag(e, l, ai, ki, k, d); }); row.appendChild(d); }); });\n return row;\n}\nfunction startKeyframeDrag(e, l, ai, ki, k, d) {\n e.stopPropagation(); var width = $('#layers').getBoundingClientRect().width, total = comp().durationFrames, x0 = e.clientX, delta = 0, left0 = parseFloat(d.style.left); d.setPointerCapture(e.pointerId);\n function move(ev) { delta = Math.round((ev.clientX - x0) / width * total); if (l.start + k.frame + delta < 0) delta = -(l.start + k.frame); d.style.left = (left0 + delta / total * 100) + '%'; $('#status').textContent = 'keyframe at ' + (k.frame + delta); }\n function up() { d.removeEventListener('pointermove', move); d.removeEventListener('pointerup', up); if (!delta) return; $('#status').textContent = 'saving\u2026';\n fetch('/api/keyframe', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: l.id, animation: ai, keyframe: ki, frame: Math.max(0, k.frame + delta) }) }).then(function (r) { return r.json(); })\n .then(function (result) { if (!result.ok) { $('#status').textContent = result.error; renderLayers(); } }); }\n d.addEventListener('pointermove', move); d.addEventListener('pointerup', up);\n}\n// Alt with the arrow keys moves the selected layer by one frame (ten with Shift).\ndocument.addEventListener('keydown', function (e) { if (!e.altKey || !selected || !comp() || /^(INPUT|TEXTAREA)$/.test(e.target.tagName) || (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft')) return; e.preventDefault();\n var step = (e.key === 'ArrowRight' ? 1 : -1) * (e.shiftKey ? 10 : 1); waitForRaw().then(function (raw) { if (!raw || typeof raw.startFrame === 'string' || typeof raw.endFrame === 'string') return; var start = typeof raw.startFrame === 'number' ? raw.startFrame : 0, set = { startFrame: Math.max(0, start + step) };\n if (typeof raw.endFrame === 'number') set.endFrame = Math.max(set.startFrame + 1, raw.endFrame + step); editLayer(set, []); }); });\nfunction undo() { fetch('/api/undo', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' }).then(function (r) { return r.json(); }).then(function (result) { if (!result.ok) $('#status').textContent = result.error; }); }\nfunction renderTimeline() {\n var c = comp(), box = $('#scenes'); box.innerHTML = ''; if (!c) return;\n c.scenes.forEach(function (s) { var el = document.createElement('div'); el.className = 'scene ed'; el.textContent = s.name; el.title = s.name + ' \u00B7 frames ' + s.startFrame + '\u2013' + (s.startFrame + s.durationFrames - 1) + ' \u00B7 drag to move, drag an edge to resize';\n el.style.left = (s.startFrame / c.durationFrames * 100) + '%'; el.style.width = (s.durationFrames / c.durationFrames * 100) + '%';\n [['l', 'start'], ['r', 'end']].forEach(function (g) { var grip = document.createElement('div'); grip.className = 'sgrip ' + g[0]; grip.addEventListener('pointerdown', function (e) { startSceneDrag(e, s, el, g[1]); }); el.appendChild(grip); });\n el.addEventListener('pointerdown', function (e) { startSceneDrag(e, s, el, 'move'); }); box.appendChild(el); });\n}\nfunction renderVars() {\n var box = $('#vars'), names = Object.keys(state.variables || {}); box.innerHTML = '';\n $('#varhint').textContent = names.length ? 'Changes re-render the preview. Values here are not saved to the file.' : 'This composition declares no variables.';\n names.forEach(function (name) {\n var d = state.variables[name], value = state.values[name], label = document.createElement('label'), input;\n label.textContent = name; if (d.description) { var small = document.createElement('small'); small.textContent = d.description; label.appendChild(small); }\n if (d.type === 'boolean') { input = document.createElement('input'); input.type = 'checkbox'; input.checked = value === true; }\n else if (d.type === 'number') { input = document.createElement('input'); input.type = 'number'; input.step = 'any'; input.value = value === undefined ? '' : value; }\n else if (d.type === 'string') { input = document.createElement('textarea'); input.value = value === undefined ? '' : value; input.rows = String(value || '').split('\\n').length; }\n else if (d.type === 'list') { input = document.createElement('textarea'); input.value = JSON.stringify(value || [], null, 2); input.rows = Math.min(14, input.value.split('\\n').length); input.spellcheck = false; input.style.fontFamily = 'ui-monospace,Consolas,monospace'; input.style.fontSize = '12px'; }\n else { input = document.createElement('input'); input.value = value === undefined ? '' : value; }\n input.dataset.name = name; input.dataset.type = d.type; input.addEventListener('input', onVarInput);\n if (d.type === 'color') { var row = document.createElement('div'), picker = document.createElement('input'); row.className = 'row'; picker.type = 'color';\n if (/^#[0-9a-f]{6}$/i.test(input.value)) picker.value = input.value; else picker.disabled = true;\n picker.addEventListener('input', function () { input.value = picker.value; onVarInput(); }); input.addEventListener('input', function () { if (/^#[0-9a-f]{6}$/i.test(input.value)) { picker.disabled = false; picker.value = input.value; } });\n row.appendChild(picker); row.appendChild(input); label.appendChild(row); } else label.appendChild(input);\n box.appendChild(label);\n });\n}\nfunction onVarInput() {\n clearTimeout(varTimer); varTimer = setTimeout(function () { var values = {};\n document.querySelectorAll('#vars [data-name]').forEach(function (el) { values[el.dataset.name] = el.dataset.type === 'boolean' ? el.checked : el.value; });\n $('#status').textContent = 'applying variables\u2026'; fetch('/api/variables', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(values) }); }, 350);\n}\nfunction showError(message) { var el = $('#error'); el.style.display = message ? 'block' : 'none'; el.textContent = message ? (comp() ? 'Not reloaded \u2014 showing the last version that worked.' : 'Could not load the composition. Fix the file and save; the preview reloads by itself.') + '\\n\\n' + message : ''; }\nfunction loadState() {\n return fetch('/api/state').then(function (r) { return r.json(); }).then(function (s) {\n var versionChanged = !state || s.version !== state.version, focused = document.activeElement && document.activeElement.dataset && document.activeElement.dataset.name;\n state = s; $('#file').textContent = s.file; showError(s.error);\n if (!comp()) { $('#status').textContent = 'fix the error to load the composition'; return; }\n $('#name').textContent = comp().name; document.title = comp().name + ' \u00B7 OVE preview';\n if (versionChanged) { cache.forEach(function (u) { URL.revokeObjectURL(u); }); cache.clear(); frame = clamp(frame); renderTimeline(); renderLayers(); if (selected) selectLayer(selected); }\n $('#undo').disabled = !s.canUndo;\n if (!focused) renderVars();\n request(frame); prefetch(); draw();\n });\n}\nvar dragging = false;\nfunction scrub(e) { var r = $('#timeline').getBoundingClientRect(); seek(Math.round((e.clientX - r.left) / r.width * (comp().durationFrames - 1))); }\n$('#timeline').addEventListener('pointerdown', function (e) { if (!comp()) return; dragging = true; $('#timeline').setPointerCapture(e.pointerId); setPlaying(false); scrub(e); });\n$('#timeline').addEventListener('pointermove', function (e) { if (dragging) scrub(e); });\n$('#timeline').addEventListener('pointerup', function () { dragging = false; });\n$('#play').addEventListener('click', function () { setPlaying(!playing); });\n$('#undo').addEventListener('click', undo);\ndocument.addEventListener('keydown', function (e) { if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !/^(INPUT|TEXTAREA)$/.test(e.target.tagName)) { e.preventDefault(); undo(); } });\ndocument.addEventListener('keydown', function (e) { if (!comp() || /^(INPUT|TEXTAREA)$/.test(e.target.tagName)) return;\n if (e.key === ' ') { e.preventDefault(); setPlaying(!playing); } else if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') { e.preventDefault(); setPlaying(false); seek(frame + (e.key === 'ArrowRight' ? 1 : -1) * (e.shiftKey ? 10 : 1)); }\n else if (e.key === 'Home') seek(0); else if (e.key === 'End') seek(comp().durationFrames - 1); });\nnew EventSource('/api/events').addEventListener('change', function () { setPlaying(false); dropPlayer(); loadState(); });\nloadState();\n</script>\n</body></html>";
@@ -0,0 +1,13 @@
1
+ export type DevOptions = {
2
+ port?: number;
3
+ values?: Record<string, unknown>;
4
+ concurrency?: number;
5
+ allowHosts?: string[]; /** Run code layers (JavaScript from the composition) in the preview: only for compositions you trust. */
6
+ allowCode?: boolean;
7
+ };
8
+ export declare const playerScript: () => Promise<string>;
9
+ export type DevServer = {
10
+ url: string;
11
+ close(): Promise<void>;
12
+ };
13
+ export declare function startDevServer(file: string, options?: DevOptions): Promise<DevServer>;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};