loopwind 0.19.0 → 0.20.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 (52) hide show
  1. package/README.md +83 -0
  2. package/dist/commands/preview.d.ts.map +1 -1
  3. package/dist/commands/preview.js +2 -1
  4. package/dist/commands/preview.js.map +1 -1
  5. package/dist/commands/render.d.ts.map +1 -1
  6. package/dist/commands/render.js +2 -0
  7. package/dist/commands/render.js.map +1 -1
  8. package/dist/default-templates/AGENTS.md +54 -0
  9. package/dist/lib/renderer.d.ts.map +1 -1
  10. package/dist/lib/renderer.js +3 -0
  11. package/dist/lib/renderer.js.map +1 -1
  12. package/dist/lib/tailwind-browser.d.ts.map +1 -1
  13. package/dist/lib/tailwind-browser.js +169 -6
  14. package/dist/lib/tailwind-browser.js.map +1 -1
  15. package/dist/lib/tailwind.d.ts.map +1 -1
  16. package/dist/lib/tailwind.js +178 -7
  17. package/dist/lib/tailwind.js.map +1 -1
  18. package/dist/lib/video-preview.d.ts +1 -1
  19. package/dist/lib/video-preview.d.ts.map +1 -1
  20. package/dist/lib/video-preview.js +266 -249
  21. package/dist/lib/video-preview.js.map +1 -1
  22. package/dist/lib/video-renderer.d.ts +2 -0
  23. package/dist/lib/video-renderer.d.ts.map +1 -1
  24. package/dist/lib/video-renderer.js +4 -4
  25. package/dist/lib/video-renderer.js.map +1 -1
  26. package/dist/sdk/index.d.ts +2 -0
  27. package/dist/sdk/index.d.ts.map +1 -1
  28. package/dist/sdk/index.js +1 -0
  29. package/dist/sdk/index.js.map +1 -1
  30. package/dist/sdk/preview.d.ts +65 -0
  31. package/dist/sdk/preview.d.ts.map +1 -0
  32. package/dist/sdk/preview.js +262 -0
  33. package/dist/sdk/preview.js.map +1 -0
  34. package/examples/nextjs-template-import.ts +2 -2
  35. package/examples/sdk-video-preview.tsx +120 -0
  36. package/package.json +1 -1
  37. package/render-examples-600x400.mjs +161 -0
  38. package/render-spring-variants-fixed.mjs +60 -0
  39. package/render-staggered-text.mjs +56 -0
  40. package/test-sdk-config.mjs +138 -81
  41. package/test-static-debug.tsx +19 -0
  42. package/test-templates/test-sdk.mjs +46 -22
  43. package/test-video-props.json +3 -0
  44. package/website/DEPLOYMENT.md +1 -0
  45. package/website/OG_IMAGES.md +1 -0
  46. package/website/astro.config.mjs +18 -2
  47. package/website/package-lock.json +2866 -7080
  48. package/website/package.json +1 -2
  49. package/website/public/.gitkeep +1 -0
  50. package/website/templates/og-image.tsx +20 -21
  51. package/website/test-playground.mjs +45 -0
  52. package/output/sdk-static.jpg +0 -0
@@ -0,0 +1,161 @@
1
+ import { defineTemplate, renderVideo } from './dist/sdk/index.js';
2
+ import fs from 'fs/promises';
3
+ import React from 'react';
4
+
5
+ const { createElement: h } = React;
6
+
7
+ function createTemplateModule(meta, render) {
8
+ return { meta, default: render };
9
+ }
10
+
11
+ const templates = [
12
+ // 1. Bouncing ball with spring
13
+ createTemplateModule(
14
+ {
15
+ name: 'bouncing-ball',
16
+ description: 'Bouncing ball with spring easing animation',
17
+ type: 'video',
18
+ size: { width: 600, height: 400 },
19
+ video: { fps: 60, duration: 4 },
20
+ },
21
+ ({ tw }) =>
22
+ h('div', { style: tw('flex items-center justify-center w-full h-full bg-gradient-to-br from-blue-900 to-purple-900') },
23
+ h('div', { style: tw('w-48 h-48 rounded-full bg-gradient-to-br from-yellow-400 to-orange-500 shadow-2xl spring loop-translate-y-16/1000') })
24
+ )
25
+ ),
26
+
27
+ // 2. Linear easing
28
+ createTemplateModule(
29
+ {
30
+ name: 'easing-linear',
31
+ description: 'Bouncing ball with linear easing (constant speed)',
32
+ type: 'video',
33
+ size: { width: 600, height: 400 },
34
+ video: { fps: 60, duration: 4 },
35
+ },
36
+ ({ tw }) =>
37
+ h('div', { style: tw('flex flex-col items-center justify-center gap-6 w-full h-full bg-gradient-to-br from-slate-900 to-slate-800') },
38
+ h('div', { style: tw('w-32 h-32 rounded-full bg-gradient-to-br from-cyan-400 to-blue-500 shadow-2xl linear loop-translate-y-12/1000') }),
39
+ h('div', { style: tw('text-white text-2xl font-bold') }, 'Linear Easing'),
40
+ h('div', { style: tw('text-white/60 text-sm text-center max-w-md px-4') }, 'Constant speed')
41
+ )
42
+ ),
43
+
44
+ // 3. Ease-in
45
+ createTemplateModule(
46
+ {
47
+ name: 'easing-ease-in',
48
+ description: 'Bouncing ball with ease-in (accelerating)',
49
+ type: 'video',
50
+ size: { width: 600, height: 400 },
51
+ video: { fps: 60, duration: 4 },
52
+ },
53
+ ({ tw }) =>
54
+ h('div', { style: tw('flex flex-col items-center justify-center gap-6 w-full h-full bg-gradient-to-br from-emerald-900 to-teal-800') },
55
+ h('div', { style: tw('w-32 h-32 rounded-full bg-gradient-to-br from-emerald-400 to-green-500 shadow-2xl ease-in loop-translate-y-12/1000') }),
56
+ h('div', { style: tw('text-white text-2xl font-bold') }, 'Ease In'),
57
+ h('div', { style: tw('text-white/60 text-sm text-center max-w-md px-4') }, 'Slow start, fast end')
58
+ )
59
+ ),
60
+
61
+ // 4. Ease-out
62
+ createTemplateModule(
63
+ {
64
+ name: 'easing-ease-out',
65
+ description: 'Bouncing ball with ease-out (decelerating)',
66
+ type: 'video',
67
+ size: { width: 600, height: 400 },
68
+ video: { fps: 60, duration: 4 },
69
+ },
70
+ ({ tw }) =>
71
+ h('div', { style: tw('flex flex-col items-center justify-center gap-6 w-full h-full bg-gradient-to-br from-rose-900 to-pink-800') },
72
+ h('div', { style: tw('w-32 h-32 rounded-full bg-gradient-to-br from-rose-400 to-pink-500 shadow-2xl ease-out loop-translate-y-12/1000') }),
73
+ h('div', { style: tw('text-white text-2xl font-bold') }, 'Ease Out'),
74
+ h('div', { style: tw('text-white/60 text-sm text-center max-w-md px-4') }, 'Fast start, slow end')
75
+ )
76
+ ),
77
+
78
+ // 5. Easing comparison (3 balls)
79
+ createTemplateModule(
80
+ {
81
+ name: 'easing-comparison',
82
+ description: 'Three balls with different easings side-by-side',
83
+ type: 'video',
84
+ size: { width: 600, height: 400 },
85
+ video: { fps: 60, duration: 4 },
86
+ },
87
+ ({ tw }) =>
88
+ h('div', { style: tw('flex items-center justify-center gap-8 w-full h-full bg-gradient-to-br from-slate-900 to-gray-900 px-8') },
89
+ h('div', { style: tw('flex flex-col items-center gap-4') },
90
+ h('div', { style: tw('w-24 h-24 rounded-full bg-gradient-to-br from-cyan-400 to-blue-500 shadow-2xl linear loop-translate-y-10/800') }),
91
+ h('div', { style: tw('text-white text-sm font-bold') }, 'Linear'),
92
+ h('div', { style: tw('text-white/50 text-xs') }, '800ms')
93
+ ),
94
+ h('div', { style: tw('flex flex-col items-center gap-4') },
95
+ h('div', { style: tw('w-24 h-24 rounded-full bg-gradient-to-br from-pink-400 to-rose-500 shadow-2xl ease-out loop-translate-y-10/1000') }),
96
+ h('div', { style: tw('text-white text-sm font-bold') }, 'Ease Out'),
97
+ h('div', { style: tw('text-white/50 text-xs') }, '1000ms')
98
+ ),
99
+ h('div', { style: tw('flex flex-col items-center gap-4') },
100
+ h('div', { style: tw('w-24 h-24 rounded-full bg-gradient-to-br from-yellow-400 to-orange-500 shadow-2xl spring loop-translate-y-10/1200') }),
101
+ h('div', { style: tw('text-white text-sm font-bold') }, 'Spring'),
102
+ h('div', { style: tw('text-white/50 text-xs') }, '1200ms')
103
+ )
104
+ )
105
+ ),
106
+
107
+ // 6. Spring variants
108
+ createTemplateModule(
109
+ {
110
+ name: 'spring-variants',
111
+ description: 'Different spring configurations with varying bounce',
112
+ type: 'video',
113
+ size: { width: 600, height: 400 },
114
+ video: { fps: 60, duration: 4 },
115
+ },
116
+ ({ tw }) =>
117
+ h('div', { style: tw('flex items-center justify-center gap-6 w-full h-full bg-gradient-to-br from-indigo-900 to-purple-900 px-6') },
118
+ h('div', { style: tw('flex flex-col items-center gap-3') },
119
+ h('div', { style: tw('w-24 h-24 rounded-full bg-gradient-to-br from-blue-400 to-cyan-500 shadow-2xl spring/1/100/10 loop-translate-y-10/1000') }),
120
+ h('div', { style: tw('text-white text-xs font-bold') }, 'Gentle'),
121
+ h('div', { style: tw('text-white/50 text-[10px]') }, '1/100/10')
122
+ ),
123
+ h('div', { style: tw('flex flex-col items-center gap-3') },
124
+ h('div', { style: tw('w-24 h-24 rounded-full bg-gradient-to-br from-pink-400 to-rose-500 shadow-2xl spring/1/170/8 loop-translate-y-10/1000') }),
125
+ h('div', { style: tw('text-white text-xs font-bold') }, 'Bouncy'),
126
+ h('div', { style: tw('text-white/50 text-[10px]') }, '1/170/8')
127
+ ),
128
+ h('div', { style: tw('flex flex-col items-center gap-3') },
129
+ h('div', { style: tw('w-24 h-24 rounded-full bg-gradient-to-br from-emerald-400 to-green-500 shadow-2xl spring/1/200/15 loop-translate-y-10/1000') }),
130
+ h('div', { style: tw('text-white text-xs font-bold') }, 'Snappy'),
131
+ h('div', { style: tw('text-white/50 text-[10px]') }, '1/200/15')
132
+ )
133
+ )
134
+ ),
135
+ ];
136
+
137
+ async function renderAll() {
138
+ console.log('Rendering all examples at 600x400...\n');
139
+
140
+ for (let i = 0; i < templates.length; i++) {
141
+ const templateModule = templates[i];
142
+ const template = defineTemplate(templateModule);
143
+ const outputName = `example-${templateModule.meta.name}.mp4`;
144
+ const outputPath = `output/${outputName}`;
145
+
146
+ console.log(`📹 Rendering ${outputName}...`);
147
+
148
+ try {
149
+ const buffer = await renderVideo(template, {}, { quality: 20 });
150
+ await fs.writeFile(outputPath, buffer);
151
+ console.log(` ✔ ${outputName} (${(buffer.length / 1024).toFixed(1)}KB)\n`);
152
+ } catch (err) {
153
+ console.log(` ✖ Failed: ${err.message}\n`);
154
+ console.error(err);
155
+ }
156
+ }
157
+
158
+ console.log('✅ All examples rendered at 600x400!');
159
+ }
160
+
161
+ renderAll().catch(console.error);
@@ -0,0 +1,60 @@
1
+ import { defineTemplate, renderVideo } from './dist/sdk/index.js';
2
+ import fs from 'fs/promises';
3
+ import React from 'react';
4
+
5
+ const { createElement: h } = React;
6
+
7
+ function createTemplateModule(meta, render) {
8
+ return { meta, default: render };
9
+ }
10
+
11
+ // Spring variants with enter animations to show the bounce better
12
+ const springVariantsTemplate = createTemplateModule(
13
+ {
14
+ name: 'spring-variants',
15
+ description: 'Different spring configurations with varying bounce',
16
+ type: 'video',
17
+ size: { width: 600, height: 400 },
18
+ video: { fps: 60, duration: 3 },
19
+ },
20
+ ({ tw }) =>
21
+ h('div', { style: tw('flex items-center justify-center gap-6 w-full h-full bg-gradient-to-br from-indigo-900 to-purple-900 px-6') },
22
+ h('div', { style: tw('flex flex-col items-center gap-3') },
23
+ h('div', { style: tw('w-24 h-24 rounded-full bg-gradient-to-br from-blue-400 to-cyan-500 shadow-2xl opacity-0 enter-ease-spring/1/100/10 enter-fade-in/0/1500 enter--translate-y-32/0/1500') }),
24
+ h('div', { style: tw('text-white text-xs font-bold opacity-0 enter-fade-in/0/800') }, 'Gentle'),
25
+ h('div', { style: tw('text-white/50 text-[10px] opacity-0 enter-fade-in/0/800') }, '1/100/10')
26
+ ),
27
+ h('div', { style: tw('flex flex-col items-center gap-3') },
28
+ h('div', { style: tw('w-24 h-24 rounded-full bg-gradient-to-br from-pink-400 to-rose-500 shadow-2xl opacity-0 enter-ease-spring/1/170/8 enter-fade-in/0/1500 enter--translate-y-32/0/1500') }),
29
+ h('div', { style: tw('text-white text-xs font-bold opacity-0 enter-fade-in/200/800') }, 'Bouncy'),
30
+ h('div', { style: tw('text-white/50 text-[10px] opacity-0 enter-fade-in/200/800') }, '1/170/8')
31
+ ),
32
+ h('div', { style: tw('flex flex-col items-center gap-3') },
33
+ h('div', { style: tw('w-24 h-24 rounded-full bg-gradient-to-br from-emerald-400 to-green-500 shadow-2xl opacity-0 enter-ease-spring/1/200/15 enter-fade-in/0/1500 enter--translate-y-32/0/1500') }),
34
+ h('div', { style: tw('text-white text-xs font-bold opacity-0 enter-fade-in/400/800') }, 'Snappy'),
35
+ h('div', { style: tw('text-white/50 text-[10px] opacity-0 enter-fade-in/400/800') }, '1/200/15')
36
+ )
37
+ )
38
+ );
39
+
40
+ async function render() {
41
+ console.log('Rendering spring-variants with enter animations...\n');
42
+
43
+ const template = defineTemplate(springVariantsTemplate);
44
+ const outputPath = 'output/example-spring-variants.mp4';
45
+
46
+ try {
47
+ const buffer = await renderVideo(template, {}, { quality: 20 });
48
+ await fs.writeFile(outputPath, buffer);
49
+ console.log(`✔ example-spring-variants.mp4 (${(buffer.length / 1024).toFixed(1)}KB)\n`);
50
+ console.log('✅ Now you can see the spring differences!');
51
+ console.log(' - Gentle: smooth drop with slight overshoot bounce');
52
+ console.log(' - Bouncy: exaggerated drop with lots of bounce');
53
+ console.log(' - Snappy: fast drop with minimal/no bounce');
54
+ } catch (err) {
55
+ console.log(`✖ Failed: ${err.message}\n`);
56
+ console.error(err);
57
+ }
58
+ }
59
+
60
+ render().catch(console.error);
@@ -0,0 +1,56 @@
1
+ import { defineTemplate, renderVideo } from './dist/sdk/index.js';
2
+ import fs from 'fs/promises';
3
+ import React from 'react';
4
+
5
+ const { createElement: h } = React;
6
+
7
+ function createTemplateModule(meta, render) {
8
+ return { meta, default: render };
9
+ }
10
+
11
+ const staggeredTextTemplate = createTemplateModule(
12
+ {
13
+ name: 'staggered-text',
14
+ description: 'Staggered text animation with spring easing',
15
+ type: 'video',
16
+ size: { width: 600, height: 400 },
17
+ video: { fps: 60, duration: 3 },
18
+ },
19
+ ({ tw }) => {
20
+ const text = 'loopwind';
21
+ const letters = text.split('');
22
+ const staggerDelay = 80; // ms between each letter
23
+
24
+ return h('div', { style: tw('flex items-center justify-center w-full h-full bg-gradient-to-br from-violet-900 via-purple-900 to-fuchsia-900') },
25
+ h('div', { style: tw('flex items-center justify-center gap-1') },
26
+ ...letters.map((letter, i) =>
27
+ h('span', {
28
+ key: i,
29
+ style: tw(`text-8xl font-black bg-clip-text text-transparent bg-gradient-to-br from-cyan-400 via-blue-400 to-purple-400 opacity-0 enter-ease-spring enter-fade-in/${i * staggerDelay}/600 enter--translate-y-12/${i * staggerDelay}/600`)
30
+ }, letter)
31
+ )
32
+ )
33
+ );
34
+ }
35
+ );
36
+
37
+ async function render() {
38
+ console.log('Rendering staggered-text animation...\n');
39
+
40
+ const template = defineTemplate(staggeredTextTemplate);
41
+ const outputPath = 'output/example-staggered-text.mp4';
42
+
43
+ try {
44
+ const buffer = await renderVideo(template, {}, { quality: 20 });
45
+ await fs.writeFile(outputPath, buffer);
46
+ console.log(`✔ example-staggered-text.mp4 (${(buffer.length / 1024).toFixed(1)}KB)\n`);
47
+ console.log('✅ Staggered text animation complete!');
48
+ console.log(' Each letter animates in with spring easing');
49
+ console.log(' 80ms delay between each letter');
50
+ } catch (err) {
51
+ console.log(`✖ Failed: ${err.message}\n`);
52
+ console.error(err);
53
+ }
54
+ }
55
+
56
+ render().catch(console.error);
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { defineTemplate, renderImage, renderVideo } from './dist/sdk/index.js';
7
+ import * as configTestTemplate from './test-templates/config-test.mjs';
7
8
  import React from 'react';
8
9
  import fs from 'fs/promises';
9
10
  import path from 'path';
@@ -12,6 +13,11 @@ import { fileURLToPath } from 'url';
12
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
14
  const { createElement: h } = React;
14
15
 
16
+ // Helper to create inline template modules for testing
17
+ function createTemplateModule(meta, render) {
18
+ return { meta, default: render };
19
+ }
20
+
15
21
  // Test utilities
16
22
  let testsPassed = 0;
17
23
  let testsFailed = 0;
@@ -52,15 +58,7 @@ await test('Config in defineTemplate - Image', async () => {
52
58
  },
53
59
  };
54
60
 
55
- const template = defineTemplate({
56
- name: 'test-config-template',
57
- size: { width: 400, height: 400 },
58
- config,
59
- render: ({ tw, title }) =>
60
- h('div', { style: tw('flex items-center justify-center w-full h-full bg-background') },
61
- h('h1', { style: tw('text-4xl font-bold text-primary') }, title)
62
- ),
63
- });
61
+ const template = defineTemplate(configTestTemplate, { config });
64
62
 
65
63
  assert(template.config, 'Template should have config');
66
64
  assert(template.config.colors.primary === '#ff0000', 'Config colors should match');
@@ -74,14 +72,20 @@ await test('Config in defineTemplate - Image', async () => {
74
72
 
75
73
  // Test 2: Config in renderImage options
76
74
  await test('Config in renderImage options', async () => {
77
- const template = defineTemplate({
78
- name: 'test-render-config',
79
- size: { width: 400, height: 400 },
80
- render: ({ tw, title }) =>
75
+ const templateModule = createTemplateModule(
76
+ {
77
+ name: 'test-render-config',
78
+ type: 'image',
79
+ size: { width: 400, height: 400 },
80
+ props: { title: 'string' },
81
+ },
82
+ ({ tw, title }) =>
81
83
  h('div', { style: tw('flex items-center justify-center w-full h-full bg-secondary') },
82
84
  h('h1', { style: tw('text-4xl font-bold text-accent') }, title)
83
- ),
84
- });
85
+ )
86
+ );
87
+
88
+ const template = defineTemplate(templateModule);
85
89
 
86
90
  const svg = await renderImage(
87
91
  template,
@@ -104,18 +108,25 @@ await test('Config in renderImage options', async () => {
104
108
 
105
109
  // Test 3: Config override (options override template)
106
110
  await test('Config override - Options override template', async () => {
107
- const template = defineTemplate({
108
- name: 'test-override',
109
- size: { width: 400, height: 400 },
111
+ const templateModule = createTemplateModule(
112
+ {
113
+ name: 'test-override',
114
+ type: 'image',
115
+ size: { width: 400, height: 400 },
116
+ props: {},
117
+ },
118
+ ({ tw }) =>
119
+ h('div', { style: tw('flex items-center justify-center w-full h-full bg-brand') },
120
+ h('h1', { style: tw('text-4xl font-bold') }, 'Override Test')
121
+ )
122
+ );
123
+
124
+ const template = defineTemplate(templateModule, {
110
125
  config: {
111
126
  colors: {
112
127
  brand: '#111111',
113
128
  },
114
129
  },
115
- render: ({ tw }) =>
116
- h('div', { style: tw('flex items-center justify-center w-full h-full bg-brand') },
117
- h('h1', { style: tw('text-4xl font-bold') }, 'Override Test')
118
- ),
119
130
  });
120
131
 
121
132
  const svg = await renderImage(
@@ -138,14 +149,20 @@ await test('Config override - Options override template', async () => {
138
149
 
139
150
  // Test 4: PNG format with config
140
151
  await test('PNG format with config', async () => {
141
- const template = defineTemplate({
142
- name: 'test-png',
143
- size: { width: 200, height: 200 },
144
- render: ({ tw }) =>
152
+ const templateModule = createTemplateModule(
153
+ {
154
+ name: 'test-png',
155
+ type: 'image',
156
+ size: { width: 200, height: 200 },
157
+ props: {},
158
+ },
159
+ ({ tw }) =>
145
160
  h('div', { style: tw('flex items-center justify-center w-full h-full bg-primary') },
146
161
  h('h1', { style: tw('text-2xl font-bold text-white') }, 'PNG')
147
- ),
148
- });
162
+ )
163
+ );
164
+
165
+ const template = defineTemplate(templateModule);
149
166
 
150
167
  const png = await renderImage(
151
168
  template,
@@ -168,21 +185,27 @@ await test('PNG format with config', async () => {
168
185
 
169
186
  // Test 5: Video with config
170
187
  await test('Video with config', async () => {
171
- const template = defineTemplate({
172
- name: 'test-video',
173
- type: 'video',
174
- size: { width: 400, height: 400 },
175
- video: { fps: 30, duration: 0.5 },
176
- config: {
177
- colors: {
178
- primary: '#ff6600',
179
- },
188
+ const templateModule = createTemplateModule(
189
+ {
190
+ name: 'test-video',
191
+ type: 'video',
192
+ size: { width: 400, height: 400 },
193
+ video: { fps: 30, duration: 0.5 },
194
+ props: {},
180
195
  },
181
- render: ({ tw, progress }) => {
196
+ ({ tw, progress }) => {
182
197
  const opacity = progress;
183
198
  return h('div', { style: tw('flex items-center justify-center w-full h-full bg-white') },
184
199
  h('h1', { style: { ...tw('text-4xl font-bold text-primary'), opacity } }, 'Video')
185
200
  );
201
+ }
202
+ );
203
+
204
+ const template = defineTemplate(templateModule, {
205
+ config: {
206
+ colors: {
207
+ primary: '#ff6600',
208
+ },
186
209
  },
187
210
  });
188
211
 
@@ -197,20 +220,26 @@ await test('Video with config', async () => {
197
220
 
198
221
  // Test 6: Video with config override
199
222
  await test('Video with config override', async () => {
200
- const template = defineTemplate({
201
- name: 'test-video-override',
202
- type: 'video',
203
- size: { width: 400, height: 400 },
204
- video: { fps: 30, duration: 0.5 },
223
+ const templateModule = createTemplateModule(
224
+ {
225
+ name: 'test-video-override',
226
+ type: 'video',
227
+ size: { width: 400, height: 400 },
228
+ video: { fps: 30, duration: 0.5 },
229
+ props: {},
230
+ },
231
+ ({ tw, progress }) =>
232
+ h('div', { style: tw('flex items-center justify-center w-full h-full bg-brand') },
233
+ h('h1', { style: { ...tw('text-4xl font-bold'), opacity: progress } }, 'Video')
234
+ )
235
+ );
236
+
237
+ const template = defineTemplate(templateModule, {
205
238
  config: {
206
239
  colors: {
207
240
  brand: '#333333',
208
241
  },
209
242
  },
210
- render: ({ tw, progress }) =>
211
- h('div', { style: tw('flex items-center justify-center w-full h-full bg-brand') },
212
- h('h1', { style: { ...tw('text-4xl font-bold'), opacity: progress } }, 'Video')
213
- ),
214
243
  });
215
244
 
216
245
  const mp4 = await renderVideo(template, {}, {
@@ -237,18 +266,23 @@ await test('Multiple custom colors', async () => {
237
266
  },
238
267
  };
239
268
 
240
- const template = defineTemplate({
241
- name: 'test-multi-colors',
242
- size: { width: 600, height: 400 },
243
- config,
244
- render: ({ tw }) =>
269
+ const templateModule = createTemplateModule(
270
+ {
271
+ name: 'test-multi-colors',
272
+ type: 'image',
273
+ size: { width: 600, height: 400 },
274
+ props: {},
275
+ },
276
+ ({ tw }) =>
245
277
  h('div', { style: tw('flex flex-col items-center justify-center w-full h-full bg-brand-light p-8') },
246
278
  h('h1', { style: tw('text-4xl font-bold text-brand') }, 'Brand'),
247
279
  h('p', { style: tw('text-2xl text-brand-dark') }, 'Brand Dark'),
248
280
  h('p', { style: tw('text-xl text-accent') }, 'Accent'),
249
281
  h('p', { style: tw('text-lg text-success') }, 'Success')
250
- ),
251
- });
282
+ )
283
+ );
284
+
285
+ const template = defineTemplate(templateModule, { config });
252
286
 
253
287
  const svg = await renderImage(template, {}, { format: 'svg' });
254
288
  const svgString = svg.toString();
@@ -272,16 +306,21 @@ await test('Fonts in config', async () => {
272
306
  },
273
307
  };
274
308
 
275
- const template = defineTemplate({
276
- name: 'test-fonts',
277
- size: { width: 400, height: 400 },
278
- config,
279
- render: ({ tw }) =>
309
+ const templateModule = createTemplateModule(
310
+ {
311
+ name: 'test-fonts',
312
+ type: 'image',
313
+ size: { width: 400, height: 400 },
314
+ props: {},
315
+ },
316
+ ({ tw }) =>
280
317
  h('div', { style: tw('flex flex-col items-center justify-center w-full h-full bg-white') },
281
318
  h('h1', { style: tw('font-sans text-4xl text-text') }, 'Sans Font'),
282
319
  h('p', { style: tw('font-mono text-xl text-text') }, 'Mono Font')
283
- ),
284
- });
320
+ )
321
+ );
322
+
323
+ const template = defineTemplate(templateModule, { config });
285
324
 
286
325
  const svg = await renderImage(template, {}, { format: 'svg' });
287
326
  const svgString = svg.toString();
@@ -292,14 +331,20 @@ await test('Fonts in config', async () => {
292
331
 
293
332
  // Test 9: Empty config (should work with defaults)
294
333
  await test('Empty config - should work with defaults', async () => {
295
- const template = defineTemplate({
296
- name: 'test-no-config',
297
- size: { width: 400, height: 400 },
298
- render: ({ tw }) =>
334
+ const templateModule = createTemplateModule(
335
+ {
336
+ name: 'test-no-config',
337
+ type: 'image',
338
+ size: { width: 400, height: 400 },
339
+ props: {},
340
+ },
341
+ ({ tw }) =>
299
342
  h('div', { style: tw('flex items-center justify-center w-full h-full bg-white') },
300
343
  h('h1', { style: tw('text-4xl font-bold text-black') }, 'No Config')
301
- ),
302
- });
344
+ )
345
+ );
346
+
347
+ const template = defineTemplate(templateModule);
303
348
 
304
349
  const svg = await renderImage(template, {}, { format: 'svg' });
305
350
  assert(svg.length > 0, 'SVG should be generated without config');
@@ -307,19 +352,26 @@ await test('Empty config - should work with defaults', async () => {
307
352
 
308
353
  // Test 10: Partial config merge
309
354
  await test('Partial config merge', async () => {
310
- const template = defineTemplate({
311
- name: 'test-partial-merge',
312
- size: { width: 400, height: 400 },
355
+ const templateModule = createTemplateModule(
356
+ {
357
+ name: 'test-partial-merge',
358
+ type: 'image',
359
+ size: { width: 400, height: 400 },
360
+ props: {},
361
+ },
362
+ ({ tw }) =>
363
+ h('div', { style: tw('flex items-center justify-center w-full h-full bg-secondary') },
364
+ h('h1', { style: tw('text-4xl font-bold text-primary') }, 'Merge')
365
+ )
366
+ );
367
+
368
+ const template = defineTemplate(templateModule, {
313
369
  config: {
314
370
  colors: {
315
371
  primary: '#111111',
316
372
  secondary: '#222222',
317
373
  },
318
374
  },
319
- render: ({ tw }) =>
320
- h('div', { style: tw('flex items-center justify-center w-full h-full bg-secondary') },
321
- h('h1', { style: tw('text-4xl font-bold text-primary') }, 'Merge')
322
- ),
323
375
  });
324
376
 
325
377
  const svg = await renderImage(
@@ -363,16 +415,21 @@ await test('Font files from URLs', async () => {
363
415
  },
364
416
  };
365
417
 
366
- const template = defineTemplate({
367
- name: 'test-font-files',
368
- size: { width: 600, height: 400 },
369
- config,
370
- render: ({ tw }) =>
418
+ const templateModule = createTemplateModule(
419
+ {
420
+ name: 'test-font-files',
421
+ type: 'image',
422
+ size: { width: 600, height: 400 },
423
+ props: {},
424
+ },
425
+ ({ tw }) =>
371
426
  h('div', { style: tw('flex flex-col items-center justify-center w-full h-full bg-white p-8') },
372
427
  h('h1', { style: tw('font-sans text-6xl font-bold text-text') }, 'Bold Text'),
373
428
  h('p', { style: tw('font-sans text-3xl font-normal text-text') }, 'Normal Text')
374
- ),
375
- });
429
+ )
430
+ );
431
+
432
+ const template = defineTemplate(templateModule, { config });
376
433
 
377
434
  const svg = await renderImage(template, {}, { format: 'svg' });
378
435
  assert(svg.length > 0, 'SVG should be generated with font files from URLs');
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+
3
+ export const meta = {
4
+ name: 'test-static-debug',
5
+ description: 'Simple static image for debug testing',
6
+ type: 'image' as const,
7
+ size: { width: 600, height: 400 },
8
+ };
9
+
10
+ export default function TestStatic({ tw }: { tw: any }) {
11
+ return (
12
+ <div style={tw('flex items-center justify-center w-full h-full bg-gradient-to-br from-blue-500 to-purple-600')}>
13
+ <div style={tw('flex flex-col items-center gap-4 p-8 bg-white rounded-lg')}>
14
+ <h1 style={tw('text-4xl font-bold text-gray-900')}>Debug Test</h1>
15
+ <p style={tw('text-lg text-gray-600')}>Testing Satori debug mode</p>
16
+ </div>
17
+ </div>
18
+ );
19
+ }