loopwind 0.15.0 ā 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/REGISTRY_SETUP.md +1 -55
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +0 -3
- package/dist/commands/add.js.map +1 -1
- package/dist/default-templates/AGENTS.md +63 -2
- package/dist/lib/installer.d.ts +0 -8
- package/dist/lib/installer.d.ts.map +1 -1
- package/dist/lib/installer.js +1 -48
- package/dist/lib/installer.js.map +1 -1
- package/dist/lib/renderer.d.ts +6 -1
- package/dist/lib/renderer.d.ts.map +1 -1
- package/dist/lib/renderer.js +9 -9
- package/dist/lib/renderer.js.map +1 -1
- package/dist/lib/tailwind.d.ts.map +1 -1
- package/dist/lib/tailwind.js +55 -8
- package/dist/lib/tailwind.js.map +1 -1
- package/dist/lib/video-renderer.d.ts +3 -0
- package/dist/lib/video-renderer.d.ts.map +1 -1
- package/dist/lib/video-renderer.js +7 -7
- package/dist/lib/video-renderer.js.map +1 -1
- package/dist/sdk/index.d.ts +4 -2
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +29 -8
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/template.d.ts +3 -0
- package/dist/sdk/template.d.ts.map +1 -1
- package/dist/sdk/template.js.map +1 -1
- package/package.json +1 -1
- package/test-font-files.mjs +72 -0
- package/test-sdk-config.mjs +397 -0
- package/website/README.md +39 -19
- package/website/astro.config.mjs +3 -0
- package/website/package.json +6 -1
- package/website/public/.gitkeep +1 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test for font files in SDK StyleConfig
|
|
3
|
+
* Run with: node test-font-files.mjs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { defineTemplate, renderImage } from './dist/sdk/index.js';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
|
|
9
|
+
const { createElement: h } = React;
|
|
10
|
+
|
|
11
|
+
console.log('š Testing font files in StyleConfig...\n');
|
|
12
|
+
|
|
13
|
+
// Test with external URL (Google Fonts mirror)
|
|
14
|
+
const config = {
|
|
15
|
+
colors: {
|
|
16
|
+
text: '#000000',
|
|
17
|
+
},
|
|
18
|
+
fonts: {
|
|
19
|
+
sans: {
|
|
20
|
+
family: ['Inter', 'sans-serif'],
|
|
21
|
+
files: [
|
|
22
|
+
{
|
|
23
|
+
path: 'https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-400-normal.woff',
|
|
24
|
+
weight: 400,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
path: 'https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-700-normal.woff',
|
|
28
|
+
weight: 700,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const template = defineTemplate({
|
|
36
|
+
name: 'test-font-files',
|
|
37
|
+
size: { width: 600, height: 400 },
|
|
38
|
+
config,
|
|
39
|
+
render: ({ tw }) =>
|
|
40
|
+
h('div', { style: tw('flex flex-col items-center justify-center w-full h-full bg-white p-8') },
|
|
41
|
+
h('h1', { style: tw('font-sans text-6xl font-bold text-text') }, 'Bold Text'),
|
|
42
|
+
h('p', { style: tw('font-sans text-3xl font-normal text-text') }, 'Normal Text')
|
|
43
|
+
),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
console.log('ā³ Rendering with external font URLs...');
|
|
48
|
+
const svg = await renderImage(template, {}, { format: 'svg' });
|
|
49
|
+
|
|
50
|
+
console.log('ā
SUCCESS: Font files loaded from URLs');
|
|
51
|
+
console.log(`ā
Generated SVG: ${svg.length} bytes`);
|
|
52
|
+
|
|
53
|
+
// Also test PNG to ensure fonts render correctly
|
|
54
|
+
console.log('\nā³ Testing PNG rendering with fonts...');
|
|
55
|
+
const png = await renderImage(template, {}, { format: 'png' });
|
|
56
|
+
|
|
57
|
+
if (png[0] === 0x89 && png[1] === 0x50 && png[2] === 0x4e && png[3] === 0x47) {
|
|
58
|
+
console.log('ā
SUCCESS: PNG generated with fonts');
|
|
59
|
+
console.log(`ā
PNG size: ${png.length} bytes`);
|
|
60
|
+
} else {
|
|
61
|
+
console.error('ā FAIL: Invalid PNG signature');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log('\nš All font file tests passed!');
|
|
66
|
+
console.log('\n⨠Font files with URLs work correctly in SDK StyleConfig');
|
|
67
|
+
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('ā ERROR:', error.message);
|
|
70
|
+
console.error(error.stack);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for SDK StyleConfig
|
|
3
|
+
* Run with: node test-sdk-config.mjs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { defineTemplate, renderImage, renderVideo } from './dist/sdk/index.js';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const { createElement: h } = React;
|
|
14
|
+
|
|
15
|
+
// Test utilities
|
|
16
|
+
let testsPassed = 0;
|
|
17
|
+
let testsFailed = 0;
|
|
18
|
+
|
|
19
|
+
function assert(condition, message) {
|
|
20
|
+
if (!condition) {
|
|
21
|
+
console.error(`ā FAIL: ${message}`);
|
|
22
|
+
testsFailed++;
|
|
23
|
+
throw new Error(message);
|
|
24
|
+
} else {
|
|
25
|
+
console.log(`ā
PASS: ${message}`);
|
|
26
|
+
testsPassed++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function test(name, fn) {
|
|
31
|
+
console.log(`\nš Test: ${name}`);
|
|
32
|
+
try {
|
|
33
|
+
await fn();
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error(`Error in test "${name}":`, error.message);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Helper to check if SVG contains specific color
|
|
40
|
+
function svgContainsColor(svg, color) {
|
|
41
|
+
// Normalize color format (remove # if present)
|
|
42
|
+
const normalizedColor = color.replace('#', '');
|
|
43
|
+
return svg.includes(normalizedColor) || svg.includes(color);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Test 1: Config in defineTemplate (image)
|
|
47
|
+
await test('Config in defineTemplate - Image', async () => {
|
|
48
|
+
const config = {
|
|
49
|
+
colors: {
|
|
50
|
+
primary: '#ff0000',
|
|
51
|
+
background: '#00ff00',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
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
|
+
});
|
|
64
|
+
|
|
65
|
+
assert(template.config, 'Template should have config');
|
|
66
|
+
assert(template.config.colors.primary === '#ff0000', 'Config colors should match');
|
|
67
|
+
|
|
68
|
+
const svg = await renderImage(template, { title: 'Test' }, { format: 'svg' });
|
|
69
|
+
const svgString = svg.toString();
|
|
70
|
+
|
|
71
|
+
assert(svgContainsColor(svgString, '#ff0000'), 'SVG should contain primary color');
|
|
72
|
+
assert(svgContainsColor(svgString, '#00ff00'), 'SVG should contain background color');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Test 2: Config in renderImage options
|
|
76
|
+
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 }) =>
|
|
81
|
+
h('div', { style: tw('flex items-center justify-center w-full h-full bg-secondary') },
|
|
82
|
+
h('h1', { style: tw('text-4xl font-bold text-accent') }, title)
|
|
83
|
+
),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const svg = await renderImage(
|
|
87
|
+
template,
|
|
88
|
+
{ title: 'Test' },
|
|
89
|
+
{
|
|
90
|
+
format: 'svg',
|
|
91
|
+
config: {
|
|
92
|
+
colors: {
|
|
93
|
+
accent: '#0000ff',
|
|
94
|
+
secondary: '#ffff00',
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const svgString = svg.toString();
|
|
101
|
+
assert(svgContainsColor(svgString, '#0000ff'), 'SVG should contain accent color from options');
|
|
102
|
+
assert(svgContainsColor(svgString, '#ffff00'), 'SVG should contain secondary color from options');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Test 3: Config override (options override template)
|
|
106
|
+
await test('Config override - Options override template', async () => {
|
|
107
|
+
const template = defineTemplate({
|
|
108
|
+
name: 'test-override',
|
|
109
|
+
size: { width: 400, height: 400 },
|
|
110
|
+
config: {
|
|
111
|
+
colors: {
|
|
112
|
+
brand: '#111111',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
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
|
+
});
|
|
120
|
+
|
|
121
|
+
const svg = await renderImage(
|
|
122
|
+
template,
|
|
123
|
+
{},
|
|
124
|
+
{
|
|
125
|
+
format: 'svg',
|
|
126
|
+
config: {
|
|
127
|
+
colors: {
|
|
128
|
+
brand: '#222222',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const svgString = svg.toString();
|
|
135
|
+
assert(svgContainsColor(svgString, '#222222'), 'SVG should contain overridden brand color');
|
|
136
|
+
assert(!svgContainsColor(svgString, '#111111'), 'SVG should NOT contain original template brand color');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Test 4: PNG format with config
|
|
140
|
+
await test('PNG format with config', async () => {
|
|
141
|
+
const template = defineTemplate({
|
|
142
|
+
name: 'test-png',
|
|
143
|
+
size: { width: 200, height: 200 },
|
|
144
|
+
render: ({ tw }) =>
|
|
145
|
+
h('div', { style: tw('flex items-center justify-center w-full h-full bg-primary') },
|
|
146
|
+
h('h1', { style: tw('text-2xl font-bold text-white') }, 'PNG')
|
|
147
|
+
),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const png = await renderImage(
|
|
151
|
+
template,
|
|
152
|
+
{},
|
|
153
|
+
{
|
|
154
|
+
format: 'png',
|
|
155
|
+
config: {
|
|
156
|
+
colors: {
|
|
157
|
+
primary: '#ff00ff',
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
assert(Buffer.isBuffer(png), 'PNG should be a Buffer');
|
|
164
|
+
assert(png.length > 0, 'PNG buffer should not be empty');
|
|
165
|
+
// PNG signature check
|
|
166
|
+
assert(png[0] === 0x89 && png[1] === 0x50 && png[2] === 0x4e && png[3] === 0x47, 'Should be valid PNG');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Test 5: Video with config
|
|
170
|
+
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
|
+
},
|
|
180
|
+
},
|
|
181
|
+
render: ({ tw, progress }) => {
|
|
182
|
+
const opacity = progress;
|
|
183
|
+
return h('div', { style: tw('flex items-center justify-center w-full h-full bg-white') },
|
|
184
|
+
h('h1', { style: { ...tw('text-4xl font-bold text-primary'), opacity } }, 'Video')
|
|
185
|
+
);
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const mp4 = await renderVideo(template, {});
|
|
190
|
+
|
|
191
|
+
assert(Buffer.isBuffer(mp4), 'MP4 should be a Buffer');
|
|
192
|
+
assert(mp4.length > 0, 'MP4 buffer should not be empty');
|
|
193
|
+
// MP4 signature check (ftyp)
|
|
194
|
+
const signature = mp4.toString('ascii', 4, 8);
|
|
195
|
+
assert(signature === 'ftyp', 'Should be valid MP4');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Test 6: Video with config override
|
|
199
|
+
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 },
|
|
205
|
+
config: {
|
|
206
|
+
colors: {
|
|
207
|
+
brand: '#333333',
|
|
208
|
+
},
|
|
209
|
+
},
|
|
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
|
+
});
|
|
215
|
+
|
|
216
|
+
const mp4 = await renderVideo(template, {}, {
|
|
217
|
+
config: {
|
|
218
|
+
colors: {
|
|
219
|
+
brand: '#444444',
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
assert(Buffer.isBuffer(mp4), 'MP4 should be a Buffer');
|
|
225
|
+
assert(mp4.length > 0, 'MP4 buffer should not be empty');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Test 7: Multiple custom colors
|
|
229
|
+
await test('Multiple custom colors', async () => {
|
|
230
|
+
const config = {
|
|
231
|
+
colors: {
|
|
232
|
+
brand: '#8b5cf6',
|
|
233
|
+
'brand-light': '#a78bfa',
|
|
234
|
+
'brand-dark': '#7c3aed',
|
|
235
|
+
accent: '#f59e0b',
|
|
236
|
+
success: '#10b981',
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const template = defineTemplate({
|
|
241
|
+
name: 'test-multi-colors',
|
|
242
|
+
size: { width: 600, height: 400 },
|
|
243
|
+
config,
|
|
244
|
+
render: ({ tw }) =>
|
|
245
|
+
h('div', { style: tw('flex flex-col items-center justify-center w-full h-full bg-brand-light p-8') },
|
|
246
|
+
h('h1', { style: tw('text-4xl font-bold text-brand') }, 'Brand'),
|
|
247
|
+
h('p', { style: tw('text-2xl text-brand-dark') }, 'Brand Dark'),
|
|
248
|
+
h('p', { style: tw('text-xl text-accent') }, 'Accent'),
|
|
249
|
+
h('p', { style: tw('text-lg text-success') }, 'Success')
|
|
250
|
+
),
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const svg = await renderImage(template, {}, { format: 'svg' });
|
|
254
|
+
const svgString = svg.toString();
|
|
255
|
+
|
|
256
|
+
assert(svgContainsColor(svgString, '#8b5cf6'), 'SVG should contain brand color');
|
|
257
|
+
assert(svgContainsColor(svgString, '#a78bfa'), 'SVG should contain brand-light color');
|
|
258
|
+
assert(svgContainsColor(svgString, '#7c3aed'), 'SVG should contain brand-dark color');
|
|
259
|
+
assert(svgContainsColor(svgString, '#f59e0b'), 'SVG should contain accent color');
|
|
260
|
+
assert(svgContainsColor(svgString, '#10b981'), 'SVG should contain success color');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Test 8: Fonts in config
|
|
264
|
+
await test('Fonts in config', async () => {
|
|
265
|
+
const config = {
|
|
266
|
+
colors: {
|
|
267
|
+
text: '#000000',
|
|
268
|
+
},
|
|
269
|
+
fonts: {
|
|
270
|
+
sans: ['Inter', 'system-ui', 'sans-serif'],
|
|
271
|
+
mono: ['JetBrains Mono', 'monospace'],
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const template = defineTemplate({
|
|
276
|
+
name: 'test-fonts',
|
|
277
|
+
size: { width: 400, height: 400 },
|
|
278
|
+
config,
|
|
279
|
+
render: ({ tw }) =>
|
|
280
|
+
h('div', { style: tw('flex flex-col items-center justify-center w-full h-full bg-white') },
|
|
281
|
+
h('h1', { style: tw('font-sans text-4xl text-text') }, 'Sans Font'),
|
|
282
|
+
h('p', { style: tw('font-mono text-xl text-text') }, 'Mono Font')
|
|
283
|
+
),
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const svg = await renderImage(template, {}, { format: 'svg' });
|
|
287
|
+
const svgString = svg.toString();
|
|
288
|
+
|
|
289
|
+
assert(svg.length > 0, 'SVG should be generated with fonts');
|
|
290
|
+
// Fonts are embedded in the SVG rendering, no need to check for specific strings
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Test 9: Empty config (should work with defaults)
|
|
294
|
+
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 }) =>
|
|
299
|
+
h('div', { style: tw('flex items-center justify-center w-full h-full bg-white') },
|
|
300
|
+
h('h1', { style: tw('text-4xl font-bold text-black') }, 'No Config')
|
|
301
|
+
),
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const svg = await renderImage(template, {}, { format: 'svg' });
|
|
305
|
+
assert(svg.length > 0, 'SVG should be generated without config');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Test 10: Partial config merge
|
|
309
|
+
await test('Partial config merge', async () => {
|
|
310
|
+
const template = defineTemplate({
|
|
311
|
+
name: 'test-partial-merge',
|
|
312
|
+
size: { width: 400, height: 400 },
|
|
313
|
+
config: {
|
|
314
|
+
colors: {
|
|
315
|
+
primary: '#111111',
|
|
316
|
+
secondary: '#222222',
|
|
317
|
+
},
|
|
318
|
+
},
|
|
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
|
+
});
|
|
324
|
+
|
|
325
|
+
const svg = await renderImage(
|
|
326
|
+
template,
|
|
327
|
+
{},
|
|
328
|
+
{
|
|
329
|
+
format: 'svg',
|
|
330
|
+
config: {
|
|
331
|
+
colors: {
|
|
332
|
+
primary: '#999999', // Override only primary
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
}
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const svgString = svg.toString();
|
|
339
|
+
assert(svgContainsColor(svgString, '#999999'), 'SVG should contain overridden primary color');
|
|
340
|
+
assert(svgContainsColor(svgString, '#222222'), 'SVG should retain original secondary color');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Test 11: Font files from URLs
|
|
344
|
+
await test('Font files from URLs', async () => {
|
|
345
|
+
const config = {
|
|
346
|
+
colors: {
|
|
347
|
+
text: '#000000',
|
|
348
|
+
},
|
|
349
|
+
fonts: {
|
|
350
|
+
sans: {
|
|
351
|
+
family: ['Inter', 'sans-serif'],
|
|
352
|
+
files: [
|
|
353
|
+
{
|
|
354
|
+
path: 'https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-400-normal.woff',
|
|
355
|
+
weight: 400,
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
path: 'https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-700-normal.woff',
|
|
359
|
+
weight: 700,
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const template = defineTemplate({
|
|
367
|
+
name: 'test-font-files',
|
|
368
|
+
size: { width: 600, height: 400 },
|
|
369
|
+
config,
|
|
370
|
+
render: ({ tw }) =>
|
|
371
|
+
h('div', { style: tw('flex flex-col items-center justify-center w-full h-full bg-white p-8') },
|
|
372
|
+
h('h1', { style: tw('font-sans text-6xl font-bold text-text') }, 'Bold Text'),
|
|
373
|
+
h('p', { style: tw('font-sans text-3xl font-normal text-text') }, 'Normal Text')
|
|
374
|
+
),
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const svg = await renderImage(template, {}, { format: 'svg' });
|
|
378
|
+
assert(svg.length > 0, 'SVG should be generated with font files from URLs');
|
|
379
|
+
|
|
380
|
+
const png = await renderImage(template, {}, { format: 'png' });
|
|
381
|
+
assert(Buffer.isBuffer(png), 'PNG should be a Buffer');
|
|
382
|
+
assert(png.length > 0, 'PNG buffer should not be empty');
|
|
383
|
+
assert(png[0] === 0x89 && png[1] === 0x50 && png[2] === 0x4e && png[3] === 0x47, 'Should be valid PNG with custom fonts');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Summary
|
|
387
|
+
console.log('\n' + '='.repeat(50));
|
|
388
|
+
console.log(`ā
Tests passed: ${testsPassed}`);
|
|
389
|
+
console.log(`ā Tests failed: ${testsFailed}`);
|
|
390
|
+
console.log('='.repeat(50));
|
|
391
|
+
|
|
392
|
+
if (testsFailed > 0) {
|
|
393
|
+
process.exit(1);
|
|
394
|
+
} else {
|
|
395
|
+
console.log('\nš All tests passed!');
|
|
396
|
+
process.exit(0);
|
|
397
|
+
}
|
package/website/README.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
This is the documentation website for loopwind, built with [Astro](https://astro.build/) and styled with [Tailwind CSS 4](https://tailwindcss.com/) using shadcn/ui design tokens.
|
|
4
4
|
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Dynamic OG Images**: Each page generates its own Open Graph image on-the-fly using the loopwind SDK
|
|
8
|
+
- **Dark Theme**: Beautiful dark mode optimized for readability
|
|
9
|
+
- **Responsive Design**: Works perfectly on desktop, tablet, and mobile
|
|
10
|
+
- **AI-Friendly**: Copy raw markdown for LLM context
|
|
11
|
+
|
|
5
12
|
## Development
|
|
6
13
|
|
|
7
14
|
Install dependencies:
|
|
@@ -33,29 +40,19 @@ Preview the build:
|
|
|
33
40
|
npm run preview
|
|
34
41
|
```
|
|
35
42
|
|
|
36
|
-
##
|
|
43
|
+
## Dynamic OG Images
|
|
37
44
|
|
|
38
|
-
|
|
45
|
+
Each documentation page automatically gets a custom OG image generated using the loopwind SDK. The images are:
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
```
|
|
47
|
+
- Generated at build time (pre-rendered)
|
|
48
|
+
- Cached indefinitely
|
|
49
|
+
- Customized per page with title and description
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
cd website
|
|
49
|
-
loopwind init
|
|
50
|
-
loopwind add video-intro # or create a custom template
|
|
51
|
-
loopwind render video-intro '{"title":"loopwind","subtitle":"Generate videos with React + Tailwind"}' --out public/demo-intro.mp4
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
The `CodeVideoDemo` component expects videos in `/public/demo-intro.mp4`.
|
|
51
|
+
The OG image API route is at `src/pages/api/og/[...slug].ts`.
|
|
55
52
|
|
|
56
53
|
## Deployment
|
|
57
54
|
|
|
58
|
-
### Netlify
|
|
55
|
+
### Netlify (Current)
|
|
59
56
|
|
|
60
57
|
This site is configured to deploy on Netlify. The `netlify.toml` file in the root directory handles the configuration.
|
|
61
58
|
|
|
@@ -70,7 +67,7 @@ Just connect your GitHub repo to Netlify and it will automatically deploy!
|
|
|
70
67
|
|
|
71
68
|
You can also deploy to:
|
|
72
69
|
- **Vercel**: Connect repo, set build settings
|
|
73
|
-
- **Cloudflare Pages**:
|
|
70
|
+
- **Cloudflare Pages**: Install `@astrojs/cloudflare` adapter
|
|
74
71
|
- **GitHub Pages**: May need additional configuration
|
|
75
72
|
|
|
76
73
|
## Adding Documentation
|
|
@@ -89,6 +86,7 @@ description: Page description
|
|
|
89
86
|
---
|
|
90
87
|
```
|
|
91
88
|
3. Add to navigation in `src/layouts/DocsLayout.astro`
|
|
89
|
+
4. Add metadata to `src/pages/api/og/[...slug].ts` for OG images
|
|
92
90
|
|
|
93
91
|
## Styling
|
|
94
92
|
|
|
@@ -108,12 +106,14 @@ website/
|
|
|
108
106
|
āāā src/
|
|
109
107
|
ā āāā components/
|
|
110
108
|
ā ā āāā Hero.astro # Home page hero
|
|
109
|
+
ā ā āāā Logo.astro # loopwind logo
|
|
111
110
|
ā ā āāā CodeVideoDemo.astro # Code + video side-by-side
|
|
112
111
|
ā āāā layouts/
|
|
113
112
|
ā ā āāā BaseLayout.astro # Base HTML with meta tags
|
|
114
113
|
ā ā āāā DocsLayout.astro # Docs with sidebar
|
|
115
114
|
ā āāā pages/
|
|
116
115
|
ā ā āāā index.mdx # Getting Started
|
|
116
|
+
ā ā āāā getting-started.mdx # Installation guide
|
|
117
117
|
ā ā āāā templates.mdx # Installing Templates
|
|
118
118
|
ā ā āāā images.mdx # Image Rendering
|
|
119
119
|
ā ā āāā video.mdx # Video Rendering
|
|
@@ -121,9 +121,14 @@ website/
|
|
|
121
121
|
ā ā āāā helpers.mdx # QR & Composition
|
|
122
122
|
ā ā āāā styling.mdx # Tailwind & shadcn/ui
|
|
123
123
|
ā ā āāā fonts.mdx # Font Handling
|
|
124
|
+
ā ā āāā config.mdx # loopwind.json reference
|
|
124
125
|
ā ā āāā agents.mdx # AI Agents & Cursor
|
|
125
126
|
ā ā āāā sdk.mdx # Programmatic SDK
|
|
126
|
-
ā ā
|
|
127
|
+
ā ā āāā preview.mdx # Preview Mode
|
|
128
|
+
ā ā āāā llm.txt.ts # Combined docs endpoint
|
|
129
|
+
ā ā āāā api/
|
|
130
|
+
ā ā āāā og/[...slug].ts # Dynamic OG image generation
|
|
131
|
+
ā ā āāā raw-markdown/[...path].ts # Raw markdown API
|
|
127
132
|
ā āāā styles/
|
|
128
133
|
ā āāā global.css # Tailwind 4 + design tokens
|
|
129
134
|
āāā public/
|
|
@@ -136,3 +141,18 @@ website/
|
|
|
136
141
|
āāā package.json
|
|
137
142
|
āāā tsconfig.json
|
|
138
143
|
```
|
|
144
|
+
|
|
145
|
+
## Technologies
|
|
146
|
+
|
|
147
|
+
- **[Astro](https://astro.build/)** - Static site generator
|
|
148
|
+
- **[Tailwind CSS 4](https://tailwindcss.com/)** - Utility-first CSS
|
|
149
|
+
- **[MDX](https://mdxjs.com/)** - Markdown with JSX
|
|
150
|
+
- **[loopwind SDK](https://loopwind.dev/sdk)** - Dynamic OG image generation
|
|
151
|
+
- **[shadcn/ui](https://ui.shadcn.com/)** - Design system tokens
|
|
152
|
+
|
|
153
|
+
## Performance
|
|
154
|
+
|
|
155
|
+
- Fast page loads with Astro's static generation
|
|
156
|
+
- Pre-rendered OG images for instant social previews
|
|
157
|
+
- Optimized fonts and assets
|
|
158
|
+
- Minimal JavaScript bundle
|
package/website/astro.config.mjs
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { defineConfig } from 'astro/config';
|
|
2
2
|
import mdx from '@astrojs/mdx';
|
|
3
3
|
import tailwindcss from '@tailwindcss/vite';
|
|
4
|
+
import cloudflare from '@astrojs/cloudflare';
|
|
4
5
|
|
|
5
6
|
// https://astro.build/config
|
|
6
7
|
export default defineConfig({
|
|
7
8
|
site: 'https://loopwind.dev',
|
|
9
|
+
output: 'hybrid',
|
|
10
|
+
adapter: cloudflare(),
|
|
8
11
|
integrations: [mdx()],
|
|
9
12
|
vite: {
|
|
10
13
|
plugins: [tailwindcss()],
|
package/website/package.json
CHANGED
|
@@ -7,17 +7,22 @@
|
|
|
7
7
|
"start": "astro dev",
|
|
8
8
|
"build": "astro check && astro build",
|
|
9
9
|
"preview": "astro preview",
|
|
10
|
+
"deploy": "astro build && wrangler deploy",
|
|
11
|
+
"cf:deploy": "wrangler deploy",
|
|
10
12
|
"astro": "astro"
|
|
11
13
|
},
|
|
12
14
|
"dependencies": {
|
|
13
15
|
"@astrojs/check": "^0.9.3",
|
|
16
|
+
"@astrojs/cloudflare": "^11.1.0",
|
|
14
17
|
"@astrojs/mdx": "^3.1.7",
|
|
15
18
|
"astro": "^4.15.11",
|
|
19
|
+
"loopwind": "latest",
|
|
16
20
|
"typescript": "^5.6.2"
|
|
17
21
|
},
|
|
18
22
|
"devDependencies": {
|
|
19
23
|
"@tailwindcss/vite": "^4.0.0",
|
|
20
|
-
"tailwindcss": "^4.0.0"
|
|
24
|
+
"tailwindcss": "^4.0.0",
|
|
25
|
+
"wrangler": "^3.86.1"
|
|
21
26
|
}
|
|
22
27
|
}
|
|
23
28
|
|
package/website/public/.gitkeep
CHANGED