launchframe 0.1.7 → 0.1.9
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/README.md +238 -229
- package/package.json +2 -1
- package/packages/extract/dom-crawler.ts +521 -521
- package/packages/extract/emit.ts +534 -466
- package/packages/extract/extract.ts +443 -441
- package/packages/extract/mirror-emit.ts +617 -617
- package/packages/extract/reference-dump.ts +431 -230
- package/packages/extract/types.ts +311 -311
package/packages/extract/emit.ts
CHANGED
|
@@ -1,466 +1,534 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Output emitters.
|
|
3
|
-
*
|
|
4
|
-
* Given a synthesized DesignSystem, produce drop-in files the user can
|
|
5
|
-
* copy into a fresh shadcn/ui project:
|
|
6
|
-
*
|
|
7
|
-
* tokens.json — every value, machine-readable
|
|
8
|
-
* tailwind.config.ts — Tailwind theme extension
|
|
9
|
-
* globals.css — shadcn-compatible CSS variables (light + dark)
|
|
10
|
-
* theme-preview.tsx — a self-contained React component that renders
|
|
11
|
-
* every token so you can eyeball the system
|
|
12
|
-
* REPORT.md — what was extracted, from where, and how the
|
|
13
|
-
* output is meant to be used
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
17
|
-
import { join } from "node:path";
|
|
18
|
-
|
|
19
|
-
import type { ColorRamp, DesignSystem, ExtractionRun } from "./types.js";
|
|
20
|
-
|
|
21
|
-
export function emitAll(system: DesignSystem, run: ExtractionRun): string[] {
|
|
22
|
-
mkdirSync(run.outputDir, { recursive: true });
|
|
23
|
-
const written: string[] = [];
|
|
24
|
-
|
|
25
|
-
written.push(write(run.outputDir, "tokens.json", JSON.stringify(system, null, 2) + "\n"));
|
|
26
|
-
written.push(write(run.outputDir, "tailwind.config.ts", emitTailwindConfig(system)));
|
|
27
|
-
written.push(write(run.outputDir, "globals.css", emitGlobalsCss(system)));
|
|
28
|
-
written.push(write(run.outputDir, "theme-preview.tsx", emitThemePreview(system)));
|
|
29
|
-
written.push(write(run.outputDir, "REPORT.md", emitReport(system, run)));
|
|
30
|
-
written.push(write(run.outputDir, "FOR_AI.md", emitForAi(system, run)));
|
|
31
|
-
|
|
32
|
-
return written;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function write(dir: string, file: string, contents: string): string {
|
|
36
|
-
const path = join(dir, file);
|
|
37
|
-
writeFileSync(path, contents);
|
|
38
|
-
return path;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/* -------------------------------------------------------------------------- */
|
|
42
|
-
/* tailwind.config.ts */
|
|
43
|
-
/* -------------------------------------------------------------------------- */
|
|
44
|
-
|
|
45
|
-
function emitTailwindConfig(system: DesignSystem): string {
|
|
46
|
-
return `/**
|
|
47
|
-
* Tailwind theme extracted by launchframe.
|
|
48
|
-
* Run id: ${system.runId}
|
|
49
|
-
*
|
|
50
|
-
* Sources (inspirational only, no source code or assets reused):
|
|
51
|
-
${system.sources.map((s) => ` * - ${s.url}`).join("\n")}
|
|
52
|
-
*/
|
|
53
|
-
|
|
54
|
-
import type { Config } from "tailwindcss";
|
|
55
|
-
|
|
56
|
-
const config: Config = {
|
|
57
|
-
darkMode: ["class"],
|
|
58
|
-
content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}"],
|
|
59
|
-
theme: {
|
|
60
|
-
container: {
|
|
61
|
-
center: true,
|
|
62
|
-
padding: "1.5rem",
|
|
63
|
-
screens: { "2xl": "${pxToRem(system.spacing.containerPx)}rem" },
|
|
64
|
-
},
|
|
65
|
-
extend: {
|
|
66
|
-
colors: {
|
|
67
|
-
background: "hsl(var(--background))",
|
|
68
|
-
foreground: "hsl(var(--foreground))",
|
|
69
|
-
card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" },
|
|
70
|
-
popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))" },
|
|
71
|
-
primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },
|
|
72
|
-
secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" },
|
|
73
|
-
muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" },
|
|
74
|
-
accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" },
|
|
75
|
-
destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" },
|
|
76
|
-
border: "hsl(var(--border))",
|
|
77
|
-
input: "hsl(var(--input))",
|
|
78
|
-
ring: "hsl(var(--ring))",
|
|
79
|
-
},
|
|
80
|
-
fontFamily: {
|
|
81
|
-
sans: ["${system.typography.fontSans}", "ui-sans-serif", "system-ui", "sans-serif"],
|
|
82
|
-
mono: ["${system.typography.fontMono}", "ui-monospace", "SFMono-Regular", "monospace"],
|
|
83
|
-
},
|
|
84
|
-
fontSize: {
|
|
85
|
-
${Object.entries(system.typography.steps)
|
|
86
|
-
.map(([k, v]) => ` "${k}": ["${pxToRem(v)}rem", { lineHeight: "${heightFor(k, system)}" }],`)
|
|
87
|
-
.join("\n")}
|
|
88
|
-
},
|
|
89
|
-
letterSpacing: {
|
|
90
|
-
body: "${system.typography.bodyLetterSpacingEm}em",
|
|
91
|
-
},
|
|
92
|
-
borderRadius: {
|
|
93
|
-
lg: "var(--radius)",
|
|
94
|
-
md: "calc(var(--radius) - 2px)",
|
|
95
|
-
sm: "calc(var(--radius) - 4px)",
|
|
96
|
-
},
|
|
97
|
-
spacing: {
|
|
98
|
-
${system.spacing.steps
|
|
99
|
-
.map((px, i) => ` "${i + 1}p": "${pxToRem(px)}rem",`)
|
|
100
|
-
.join("\n")}
|
|
101
|
-
},
|
|
102
|
-
boxShadow: {
|
|
103
|
-
sm: "${system.shadows.sm}",
|
|
104
|
-
md: "${system.shadows.md}",
|
|
105
|
-
lg: "${system.shadows.lg}",
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
plugins: [],
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
export default config;
|
|
113
|
-
`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function heightFor(step: string, system: DesignSystem): string {
|
|
117
|
-
const headingSteps = new Set(["3xl", "4xl", "5xl", "6xl"]);
|
|
118
|
-
return headingSteps.has(step)
|
|
119
|
-
? String(system.typography.headingLineHeight)
|
|
120
|
-
: String(system.typography.bodyLineHeight);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/* -------------------------------------------------------------------------- */
|
|
124
|
-
/* globals.css */
|
|
125
|
-
/* -------------------------------------------------------------------------- */
|
|
126
|
-
|
|
127
|
-
function emitGlobalsCss(system: DesignSystem): string {
|
|
128
|
-
return `/**
|
|
129
|
-
* Drop-in CSS variables produced by launchframe.
|
|
130
|
-
* Compatible with shadcn/ui's --background / --foreground / etc. tokens.
|
|
131
|
-
*/
|
|
132
|
-
|
|
133
|
-
@tailwind base;
|
|
134
|
-
@tailwind components;
|
|
135
|
-
@tailwind utilities;
|
|
136
|
-
|
|
137
|
-
@layer base {
|
|
138
|
-
:root {
|
|
139
|
-
${cssVarsBlock(system.light)}
|
|
140
|
-
--radius: ${pxToRem(system.radius.basePx)}rem;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
.dark {
|
|
144
|
-
${cssVarsBlock(system.dark)}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
@layer base {
|
|
149
|
-
* { @apply border-border; }
|
|
150
|
-
body {
|
|
151
|
-
@apply bg-background text-foreground antialiased;
|
|
152
|
-
font-feature-settings: "rlig" 1, "calt" 1;
|
|
153
|
-
text-rendering: optimizeLegibility;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
`;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function cssVarsBlock(ramp: ColorRamp): string {
|
|
160
|
-
const map: Array<[string, string]> = [
|
|
161
|
-
["--background", ramp.background],
|
|
162
|
-
["--foreground", ramp.foreground],
|
|
163
|
-
["--card", ramp.card],
|
|
164
|
-
["--card-foreground", ramp.cardForeground],
|
|
165
|
-
["--popover", ramp.popover],
|
|
166
|
-
["--popover-foreground", ramp.popoverForeground],
|
|
167
|
-
["--primary", ramp.primary],
|
|
168
|
-
["--primary-foreground", ramp.primaryForeground],
|
|
169
|
-
["--secondary", ramp.secondary],
|
|
170
|
-
["--secondary-foreground", ramp.secondaryForeground],
|
|
171
|
-
["--muted", ramp.muted],
|
|
172
|
-
["--muted-foreground", ramp.mutedForeground],
|
|
173
|
-
["--accent", ramp.accent],
|
|
174
|
-
["--accent-foreground", ramp.accentForeground],
|
|
175
|
-
["--destructive", ramp.destructive],
|
|
176
|
-
["--destructive-foreground", ramp.destructiveForeground],
|
|
177
|
-
["--border", ramp.border],
|
|
178
|
-
["--input", ramp.input],
|
|
179
|
-
["--ring", ramp.ring],
|
|
180
|
-
];
|
|
181
|
-
return map.map(([k, v]) => ` ${k}: ${hexToHslTokens(v)};`).join("\n");
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function hexToHslTokens(hex: string): string {
|
|
185
|
-
const m = hex.match(/^#([0-9a-fA-F]{6})$/);
|
|
186
|
-
if (!m) return "0 0% 0%";
|
|
187
|
-
const num = parseInt(m[1]!, 16);
|
|
188
|
-
const r = ((num >> 16) & 0xff) / 255;
|
|
189
|
-
const g = ((num >> 8) & 0xff) / 255;
|
|
190
|
-
const b = (num & 0xff) / 255;
|
|
191
|
-
const max = Math.max(r, g, b);
|
|
192
|
-
const min = Math.min(r, g, b);
|
|
193
|
-
const l = (max + min) / 2;
|
|
194
|
-
let h = 0;
|
|
195
|
-
let s = 0;
|
|
196
|
-
if (max !== min) {
|
|
197
|
-
const d = max - min;
|
|
198
|
-
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
199
|
-
switch (max) {
|
|
200
|
-
case r:
|
|
201
|
-
h = ((g - b) / d + (g < b ? 6 : 0)) * 60;
|
|
202
|
-
break;
|
|
203
|
-
case g:
|
|
204
|
-
h = ((b - r) / d + 2) * 60;
|
|
205
|
-
break;
|
|
206
|
-
default:
|
|
207
|
-
h = ((r - g) / d + 4) * 60;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return `${Math.round(h)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/* -------------------------------------------------------------------------- */
|
|
214
|
-
/* theme-preview.tsx */
|
|
215
|
-
/* -------------------------------------------------------------------------- */
|
|
216
|
-
|
|
217
|
-
function emitThemePreview(system: DesignSystem): string {
|
|
218
|
-
return `/**
|
|
219
|
-
* Theme preview for the design system extracted at ${system.runId}.
|
|
220
|
-
*
|
|
221
|
-
* Drop this into your app at \`components/theme-preview.tsx\` and render
|
|
222
|
-
* it on a route to eyeball every token in light and dark mode.
|
|
223
|
-
*/
|
|
224
|
-
|
|
225
|
-
export default function ThemePreview() {
|
|
226
|
-
return (
|
|
227
|
-
<div className="space-y-12 p-8">
|
|
228
|
-
<header className="space-y-2">
|
|
229
|
-
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
|
230
|
-
Design system preview
|
|
231
|
-
</p>
|
|
232
|
-
<h1 className="text-5xl font-semibold tracking-tight">
|
|
233
|
-
Headline at the 5xl step
|
|
234
|
-
</h1>
|
|
235
|
-
<p className="max-w-prose text-lg text-muted-foreground">
|
|
236
|
-
Body copy at the lg step demonstrating the chosen line-height and
|
|
237
|
-
letter-spacing across a representative paragraph length.
|
|
238
|
-
</p>
|
|
239
|
-
</header>
|
|
240
|
-
|
|
241
|
-
<section className="space-y-3">
|
|
242
|
-
<h2 className="text-2xl font-semibold">Type scale</h2>
|
|
243
|
-
<div className="space-y-2 font-mono text-sm">
|
|
244
|
-
${(["xs", "sm", "base", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl"] as const)
|
|
245
|
-
.map(
|
|
246
|
-
(step) =>
|
|
247
|
-
` <p className="text-${step}">${step} — The quick brown fox jumps over the lazy dog</p>`,
|
|
248
|
-
)
|
|
249
|
-
.join("\n")}
|
|
250
|
-
</div>
|
|
251
|
-
</section>
|
|
252
|
-
|
|
253
|
-
<section className="space-y-3">
|
|
254
|
-
<h2 className="text-2xl font-semibold">Color tokens</h2>
|
|
255
|
-
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
|
256
|
-
${[
|
|
257
|
-
"background",
|
|
258
|
-
"foreground",
|
|
259
|
-
"primary",
|
|
260
|
-
"primary-foreground",
|
|
261
|
-
"secondary",
|
|
262
|
-
"secondary-foreground",
|
|
263
|
-
"muted",
|
|
264
|
-
"muted-foreground",
|
|
265
|
-
"accent",
|
|
266
|
-
"accent-foreground",
|
|
267
|
-
"border",
|
|
268
|
-
"ring",
|
|
269
|
-
]
|
|
270
|
-
.map(
|
|
271
|
-
(token) =>
|
|
272
|
-
` <div className="rounded-md border border-border bg-${token.includes("foreground") ? "background" : token} p-4">
|
|
273
|
-
<div className="font-mono text-xs">${token}</div>
|
|
274
|
-
</div>`,
|
|
275
|
-
)
|
|
276
|
-
.join("\n")}
|
|
277
|
-
</div>
|
|
278
|
-
</section>
|
|
279
|
-
|
|
280
|
-
<section className="space-y-3">
|
|
281
|
-
<h2 className="text-2xl font-semibold">Radius and shadow</h2>
|
|
282
|
-
<div className="grid grid-cols-3 gap-3">
|
|
283
|
-
<div className="rounded-lg border border-border bg-card p-6 text-sm shadow-sm">shadow-sm + rounded-lg</div>
|
|
284
|
-
<div className="rounded-lg border border-border bg-card p-6 text-sm shadow-md">shadow-md + rounded-lg</div>
|
|
285
|
-
<div className="rounded-lg border border-border bg-card p-6 text-sm shadow-lg">shadow-lg + rounded-lg</div>
|
|
286
|
-
</div>
|
|
287
|
-
</section>
|
|
288
|
-
</div>
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
`;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/* -------------------------------------------------------------------------- */
|
|
295
|
-
/* REPORT.md */
|
|
296
|
-
/* -------------------------------------------------------------------------- */
|
|
297
|
-
|
|
298
|
-
function emitReport(system: DesignSystem, run: ExtractionRun): string {
|
|
299
|
-
const okCaptures = run.captures.filter((c) => c.status === "ok");
|
|
300
|
-
const failedCaptures = run.captures.filter((c) => c.status !== "ok");
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Output emitters.
|
|
3
|
+
*
|
|
4
|
+
* Given a synthesized DesignSystem, produce drop-in files the user can
|
|
5
|
+
* copy into a fresh shadcn/ui project:
|
|
6
|
+
*
|
|
7
|
+
* tokens.json — every value, machine-readable
|
|
8
|
+
* tailwind.config.ts — Tailwind theme extension
|
|
9
|
+
* globals.css — shadcn-compatible CSS variables (light + dark)
|
|
10
|
+
* theme-preview.tsx — a self-contained React component that renders
|
|
11
|
+
* every token so you can eyeball the system
|
|
12
|
+
* REPORT.md — what was extracted, from where, and how the
|
|
13
|
+
* output is meant to be used
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
19
|
+
import type { ColorRamp, DesignSystem, ExtractionRun } from "./types.js";
|
|
20
|
+
|
|
21
|
+
export function emitAll(system: DesignSystem, run: ExtractionRun): string[] {
|
|
22
|
+
mkdirSync(run.outputDir, { recursive: true });
|
|
23
|
+
const written: string[] = [];
|
|
24
|
+
|
|
25
|
+
written.push(write(run.outputDir, "tokens.json", JSON.stringify(system, null, 2) + "\n"));
|
|
26
|
+
written.push(write(run.outputDir, "tailwind.config.ts", emitTailwindConfig(system)));
|
|
27
|
+
written.push(write(run.outputDir, "globals.css", emitGlobalsCss(system)));
|
|
28
|
+
written.push(write(run.outputDir, "theme-preview.tsx", emitThemePreview(system)));
|
|
29
|
+
written.push(write(run.outputDir, "REPORT.md", emitReport(system, run)));
|
|
30
|
+
written.push(write(run.outputDir, "FOR_AI.md", emitForAi(system, run)));
|
|
31
|
+
|
|
32
|
+
return written;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function write(dir: string, file: string, contents: string): string {
|
|
36
|
+
const path = join(dir, file);
|
|
37
|
+
writeFileSync(path, contents);
|
|
38
|
+
return path;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* -------------------------------------------------------------------------- */
|
|
42
|
+
/* tailwind.config.ts */
|
|
43
|
+
/* -------------------------------------------------------------------------- */
|
|
44
|
+
|
|
45
|
+
function emitTailwindConfig(system: DesignSystem): string {
|
|
46
|
+
return `/**
|
|
47
|
+
* Tailwind theme extracted by launchframe.
|
|
48
|
+
* Run id: ${system.runId}
|
|
49
|
+
*
|
|
50
|
+
* Sources (inspirational only, no source code or assets reused):
|
|
51
|
+
${system.sources.map((s) => ` * - ${s.url}`).join("\n")}
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import type { Config } from "tailwindcss";
|
|
55
|
+
|
|
56
|
+
const config: Config = {
|
|
57
|
+
darkMode: ["class"],
|
|
58
|
+
content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}"],
|
|
59
|
+
theme: {
|
|
60
|
+
container: {
|
|
61
|
+
center: true,
|
|
62
|
+
padding: "1.5rem",
|
|
63
|
+
screens: { "2xl": "${pxToRem(system.spacing.containerPx)}rem" },
|
|
64
|
+
},
|
|
65
|
+
extend: {
|
|
66
|
+
colors: {
|
|
67
|
+
background: "hsl(var(--background))",
|
|
68
|
+
foreground: "hsl(var(--foreground))",
|
|
69
|
+
card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" },
|
|
70
|
+
popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))" },
|
|
71
|
+
primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },
|
|
72
|
+
secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" },
|
|
73
|
+
muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" },
|
|
74
|
+
accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" },
|
|
75
|
+
destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" },
|
|
76
|
+
border: "hsl(var(--border))",
|
|
77
|
+
input: "hsl(var(--input))",
|
|
78
|
+
ring: "hsl(var(--ring))",
|
|
79
|
+
},
|
|
80
|
+
fontFamily: {
|
|
81
|
+
sans: ["${system.typography.fontSans}", "ui-sans-serif", "system-ui", "sans-serif"],
|
|
82
|
+
mono: ["${system.typography.fontMono}", "ui-monospace", "SFMono-Regular", "monospace"],
|
|
83
|
+
},
|
|
84
|
+
fontSize: {
|
|
85
|
+
${Object.entries(system.typography.steps)
|
|
86
|
+
.map(([k, v]) => ` "${k}": ["${pxToRem(v)}rem", { lineHeight: "${heightFor(k, system)}" }],`)
|
|
87
|
+
.join("\n")}
|
|
88
|
+
},
|
|
89
|
+
letterSpacing: {
|
|
90
|
+
body: "${system.typography.bodyLetterSpacingEm}em",
|
|
91
|
+
},
|
|
92
|
+
borderRadius: {
|
|
93
|
+
lg: "var(--radius)",
|
|
94
|
+
md: "calc(var(--radius) - 2px)",
|
|
95
|
+
sm: "calc(var(--radius) - 4px)",
|
|
96
|
+
},
|
|
97
|
+
spacing: {
|
|
98
|
+
${system.spacing.steps
|
|
99
|
+
.map((px, i) => ` "${i + 1}p": "${pxToRem(px)}rem",`)
|
|
100
|
+
.join("\n")}
|
|
101
|
+
},
|
|
102
|
+
boxShadow: {
|
|
103
|
+
sm: "${system.shadows.sm}",
|
|
104
|
+
md: "${system.shadows.md}",
|
|
105
|
+
lg: "${system.shadows.lg}",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
plugins: [],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export default config;
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function heightFor(step: string, system: DesignSystem): string {
|
|
117
|
+
const headingSteps = new Set(["3xl", "4xl", "5xl", "6xl"]);
|
|
118
|
+
return headingSteps.has(step)
|
|
119
|
+
? String(system.typography.headingLineHeight)
|
|
120
|
+
: String(system.typography.bodyLineHeight);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* -------------------------------------------------------------------------- */
|
|
124
|
+
/* globals.css */
|
|
125
|
+
/* -------------------------------------------------------------------------- */
|
|
126
|
+
|
|
127
|
+
function emitGlobalsCss(system: DesignSystem): string {
|
|
128
|
+
return `/**
|
|
129
|
+
* Drop-in CSS variables produced by launchframe.
|
|
130
|
+
* Compatible with shadcn/ui's --background / --foreground / etc. tokens.
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
@tailwind base;
|
|
134
|
+
@tailwind components;
|
|
135
|
+
@tailwind utilities;
|
|
136
|
+
|
|
137
|
+
@layer base {
|
|
138
|
+
:root {
|
|
139
|
+
${cssVarsBlock(system.light)}
|
|
140
|
+
--radius: ${pxToRem(system.radius.basePx)}rem;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.dark {
|
|
144
|
+
${cssVarsBlock(system.dark)}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@layer base {
|
|
149
|
+
* { @apply border-border; }
|
|
150
|
+
body {
|
|
151
|
+
@apply bg-background text-foreground antialiased;
|
|
152
|
+
font-feature-settings: "rlig" 1, "calt" 1;
|
|
153
|
+
text-rendering: optimizeLegibility;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function cssVarsBlock(ramp: ColorRamp): string {
|
|
160
|
+
const map: Array<[string, string]> = [
|
|
161
|
+
["--background", ramp.background],
|
|
162
|
+
["--foreground", ramp.foreground],
|
|
163
|
+
["--card", ramp.card],
|
|
164
|
+
["--card-foreground", ramp.cardForeground],
|
|
165
|
+
["--popover", ramp.popover],
|
|
166
|
+
["--popover-foreground", ramp.popoverForeground],
|
|
167
|
+
["--primary", ramp.primary],
|
|
168
|
+
["--primary-foreground", ramp.primaryForeground],
|
|
169
|
+
["--secondary", ramp.secondary],
|
|
170
|
+
["--secondary-foreground", ramp.secondaryForeground],
|
|
171
|
+
["--muted", ramp.muted],
|
|
172
|
+
["--muted-foreground", ramp.mutedForeground],
|
|
173
|
+
["--accent", ramp.accent],
|
|
174
|
+
["--accent-foreground", ramp.accentForeground],
|
|
175
|
+
["--destructive", ramp.destructive],
|
|
176
|
+
["--destructive-foreground", ramp.destructiveForeground],
|
|
177
|
+
["--border", ramp.border],
|
|
178
|
+
["--input", ramp.input],
|
|
179
|
+
["--ring", ramp.ring],
|
|
180
|
+
];
|
|
181
|
+
return map.map(([k, v]) => ` ${k}: ${hexToHslTokens(v)};`).join("\n");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function hexToHslTokens(hex: string): string {
|
|
185
|
+
const m = hex.match(/^#([0-9a-fA-F]{6})$/);
|
|
186
|
+
if (!m) return "0 0% 0%";
|
|
187
|
+
const num = parseInt(m[1]!, 16);
|
|
188
|
+
const r = ((num >> 16) & 0xff) / 255;
|
|
189
|
+
const g = ((num >> 8) & 0xff) / 255;
|
|
190
|
+
const b = (num & 0xff) / 255;
|
|
191
|
+
const max = Math.max(r, g, b);
|
|
192
|
+
const min = Math.min(r, g, b);
|
|
193
|
+
const l = (max + min) / 2;
|
|
194
|
+
let h = 0;
|
|
195
|
+
let s = 0;
|
|
196
|
+
if (max !== min) {
|
|
197
|
+
const d = max - min;
|
|
198
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
199
|
+
switch (max) {
|
|
200
|
+
case r:
|
|
201
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) * 60;
|
|
202
|
+
break;
|
|
203
|
+
case g:
|
|
204
|
+
h = ((b - r) / d + 2) * 60;
|
|
205
|
+
break;
|
|
206
|
+
default:
|
|
207
|
+
h = ((r - g) / d + 4) * 60;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return `${Math.round(h)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* -------------------------------------------------------------------------- */
|
|
214
|
+
/* theme-preview.tsx */
|
|
215
|
+
/* -------------------------------------------------------------------------- */
|
|
216
|
+
|
|
217
|
+
function emitThemePreview(system: DesignSystem): string {
|
|
218
|
+
return `/**
|
|
219
|
+
* Theme preview for the design system extracted at ${system.runId}.
|
|
220
|
+
*
|
|
221
|
+
* Drop this into your app at \`components/theme-preview.tsx\` and render
|
|
222
|
+
* it on a route to eyeball every token in light and dark mode.
|
|
223
|
+
*/
|
|
224
|
+
|
|
225
|
+
export default function ThemePreview() {
|
|
226
|
+
return (
|
|
227
|
+
<div className="space-y-12 p-8">
|
|
228
|
+
<header className="space-y-2">
|
|
229
|
+
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
|
230
|
+
Design system preview
|
|
231
|
+
</p>
|
|
232
|
+
<h1 className="text-5xl font-semibold tracking-tight">
|
|
233
|
+
Headline at the 5xl step
|
|
234
|
+
</h1>
|
|
235
|
+
<p className="max-w-prose text-lg text-muted-foreground">
|
|
236
|
+
Body copy at the lg step demonstrating the chosen line-height and
|
|
237
|
+
letter-spacing across a representative paragraph length.
|
|
238
|
+
</p>
|
|
239
|
+
</header>
|
|
240
|
+
|
|
241
|
+
<section className="space-y-3">
|
|
242
|
+
<h2 className="text-2xl font-semibold">Type scale</h2>
|
|
243
|
+
<div className="space-y-2 font-mono text-sm">
|
|
244
|
+
${(["xs", "sm", "base", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl"] as const)
|
|
245
|
+
.map(
|
|
246
|
+
(step) =>
|
|
247
|
+
` <p className="text-${step}">${step} — The quick brown fox jumps over the lazy dog</p>`,
|
|
248
|
+
)
|
|
249
|
+
.join("\n")}
|
|
250
|
+
</div>
|
|
251
|
+
</section>
|
|
252
|
+
|
|
253
|
+
<section className="space-y-3">
|
|
254
|
+
<h2 className="text-2xl font-semibold">Color tokens</h2>
|
|
255
|
+
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
|
256
|
+
${[
|
|
257
|
+
"background",
|
|
258
|
+
"foreground",
|
|
259
|
+
"primary",
|
|
260
|
+
"primary-foreground",
|
|
261
|
+
"secondary",
|
|
262
|
+
"secondary-foreground",
|
|
263
|
+
"muted",
|
|
264
|
+
"muted-foreground",
|
|
265
|
+
"accent",
|
|
266
|
+
"accent-foreground",
|
|
267
|
+
"border",
|
|
268
|
+
"ring",
|
|
269
|
+
]
|
|
270
|
+
.map(
|
|
271
|
+
(token) =>
|
|
272
|
+
` <div className="rounded-md border border-border bg-${token.includes("foreground") ? "background" : token} p-4">
|
|
273
|
+
<div className="font-mono text-xs">${token}</div>
|
|
274
|
+
</div>`,
|
|
275
|
+
)
|
|
276
|
+
.join("\n")}
|
|
277
|
+
</div>
|
|
278
|
+
</section>
|
|
279
|
+
|
|
280
|
+
<section className="space-y-3">
|
|
281
|
+
<h2 className="text-2xl font-semibold">Radius and shadow</h2>
|
|
282
|
+
<div className="grid grid-cols-3 gap-3">
|
|
283
|
+
<div className="rounded-lg border border-border bg-card p-6 text-sm shadow-sm">shadow-sm + rounded-lg</div>
|
|
284
|
+
<div className="rounded-lg border border-border bg-card p-6 text-sm shadow-md">shadow-md + rounded-lg</div>
|
|
285
|
+
<div className="rounded-lg border border-border bg-card p-6 text-sm shadow-lg">shadow-lg + rounded-lg</div>
|
|
286
|
+
</div>
|
|
287
|
+
</section>
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* -------------------------------------------------------------------------- */
|
|
295
|
+
/* REPORT.md */
|
|
296
|
+
/* -------------------------------------------------------------------------- */
|
|
297
|
+
|
|
298
|
+
function emitReport(system: DesignSystem, run: ExtractionRun): string {
|
|
299
|
+
const okCaptures = run.captures.filter((c) => c.status === "ok");
|
|
300
|
+
const failedCaptures = run.captures.filter((c) => c.status !== "ok");
|
|
301
|
+
|
|
302
|
+
const captureLines = okCaptures
|
|
303
|
+
.map((c) => {
|
|
304
|
+
const lines = [`- ${c.url}`, ` - screenshot: \`${relativize(c.screenshotPath, run.outputDir)}\``];
|
|
305
|
+
if (c.referenceDir) {
|
|
306
|
+
lines.push(
|
|
307
|
+
` - reference: \`${relativize(c.referenceDir, run.outputDir)}/\` — \`page.html\`, \`dom-structure.json\`, \`structure-outline.txt\`, \`visible-text.*\`, \`media.json\`, \`FOR_AI_REFERENCE.md\``,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (c.mirrorDir) {
|
|
311
|
+
lines.push(
|
|
312
|
+
` - mirror: \`${relativize(c.mirrorDir, run.outputDir)}/\` — \`page.tsx\`, \`MIRROR_NOTES.md\``,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
return lines.join("\n");
|
|
316
|
+
})
|
|
317
|
+
.join("\n");
|
|
318
|
+
|
|
319
|
+
return `# Design system extraction report
|
|
320
|
+
|
|
321
|
+
**Run:** \`${system.runId}\`
|
|
322
|
+
**Generated:** ${run.finishedAt}
|
|
323
|
+
|
|
324
|
+
## Inspiration sources
|
|
325
|
+
|
|
326
|
+
Tokens in this report were synthesized from the **rendered appearance**
|
|
327
|
+
(screenshots + computed styles) of these public pages.
|
|
328
|
+
|
|
329
|
+
When present, each capture also ships a **reference bundle** (\`reference/<host>/\`)
|
|
330
|
+
and a **layout mirror** (\`mirror/<host>/\`): serialized DOM, visible copy, a
|
|
331
|
+
media index, and a React page that reconstructs section structure. Use those
|
|
332
|
+
for structural fidelity alongside this token report — see **FOR_AI.md**.
|
|
333
|
+
|
|
334
|
+
${captureLines}
|
|
335
|
+
|
|
336
|
+
${
|
|
337
|
+
failedCaptures.length > 0
|
|
338
|
+
? `### Skipped or failed\n\n${failedCaptures
|
|
339
|
+
.map((c) => `- ${c.url} — ${c.status}${c.reason ? `: ${c.reason}` : ""}`)
|
|
340
|
+
.join("\n")}`
|
|
341
|
+
: ""
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
## Synthesis decisions
|
|
345
|
+
|
|
346
|
+
${system.notes.length > 0 ? system.notes.map((n) => `- ${n}`).join("\n") : "_No special notes — defaults applied where signals were weak._"}
|
|
347
|
+
|
|
348
|
+
## Typography
|
|
349
|
+
|
|
350
|
+
- **Sans family:** ${system.typography.fontSans}${system.typography.fontSansSubstituted ? " *(substituted)*" : ""}
|
|
351
|
+
- **Mono family:** ${system.typography.fontMono}${system.typography.fontMonoSubstituted ? " *(substituted)*" : ""}
|
|
352
|
+
- **Body base:** ${system.typography.basePx}px
|
|
353
|
+
- **Scale ratio:** ${system.typography.scaleRatio}
|
|
354
|
+
- **Body line-height:** ${system.typography.bodyLineHeight}
|
|
355
|
+
- **Heading line-height:** ${system.typography.headingLineHeight}
|
|
356
|
+
- **Body letter-spacing:** ${system.typography.bodyLetterSpacingEm}em
|
|
357
|
+
|
|
358
|
+
| step | px |
|
|
359
|
+
| ---- | -- |
|
|
360
|
+
${Object.entries(system.typography.steps).map(([k, v]) => `| ${k} | ${v} |`).join("\n")}
|
|
361
|
+
|
|
362
|
+
## Spacing
|
|
363
|
+
|
|
364
|
+
- **Base unit:** ${system.spacing.basePx}px
|
|
365
|
+
- **Container width:** ${system.spacing.containerPx}px
|
|
366
|
+
- **Steps (px):** ${system.spacing.steps.join(", ")}
|
|
367
|
+
|
|
368
|
+
## Radius
|
|
369
|
+
|
|
370
|
+
- **Base radius:** ${system.radius.basePx}px (used for shadcn's \`--radius\`)
|
|
371
|
+
|
|
372
|
+
## Color tokens
|
|
373
|
+
|
|
374
|
+
### Light theme
|
|
375
|
+
|
|
376
|
+
| token | value |
|
|
377
|
+
| ----- | ----- |
|
|
378
|
+
${rampRows(system.light)}
|
|
379
|
+
|
|
380
|
+
### Dark theme
|
|
381
|
+
|
|
382
|
+
| token | value |
|
|
383
|
+
| ----- | ----- |
|
|
384
|
+
${rampRows(system.dark)}
|
|
385
|
+
|
|
386
|
+
## Shadows
|
|
387
|
+
|
|
388
|
+
- \`shadow-sm\` — \`${system.shadows.sm}\`
|
|
389
|
+
- \`shadow-md\` — \`${system.shadows.md}\`
|
|
390
|
+
- \`shadow-lg\` — \`${system.shadows.lg}\`
|
|
391
|
+
|
|
392
|
+
## Drop-in usage
|
|
393
|
+
|
|
394
|
+
1. Copy \`tailwind.config.ts\` into the root of a Next.js + Tailwind project.
|
|
395
|
+
2. Copy \`globals.css\` into \`app/globals.css\` (replacing the existing file).
|
|
396
|
+
3. Render \`theme-preview.tsx\` somewhere to eyeball the system.
|
|
397
|
+
4. Iterate from there. The tokens are yours; this report is a record of where
|
|
398
|
+
they came from.
|
|
399
|
+
|
|
400
|
+
## What this report is (and is not)
|
|
401
|
+
|
|
402
|
+
- **This REPORT** is **token synthesis** (colors, type scale, spacing, radii,
|
|
403
|
+
shadows). It does not, by itself, define every layout detail of a source page.
|
|
404
|
+
- **Per-site \`reference/\` and \`mirror/\` folders** (when emitted) are the
|
|
405
|
+
recon inputs for **structure and copy**: map landmarks and section order from
|
|
406
|
+
\`dom-structure.json\` (canonical nesting) + \`page.html\`, pull strings from \`visible-text.json\` or \`visible-text.txt\`,
|
|
407
|
+
align media via \`media.json\`, and implement or refine UI starting from
|
|
408
|
+
\`mirror/<host>/page.tsx\` (\`data-mirror-section\` markers match the crawl).
|
|
409
|
+
- **Compliance:** Do not impersonate another company, ship their trademarks or
|
|
410
|
+
logos as your own, or use third-party media without permission. Use captured
|
|
411
|
+
copy when mirroring **your own** property or when you have rights; otherwise
|
|
412
|
+
replace with original product copy while keeping layout fidelity.
|
|
413
|
+
|
|
414
|
+
If a source operator requests removal, delete captured artifacts for that URL,
|
|
415
|
+
drop it from your manifest, and re-run the extraction.
|
|
416
|
+
`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Copy-paste handoff for Cursor / Claude Code / any agent.
|
|
421
|
+
*/
|
|
422
|
+
function emitForAi(system: DesignSystem, run: ExtractionRun): string {
|
|
423
|
+
const abs = run.outputDir.replace(/\\/g, "/");
|
|
424
|
+
const ok = run.captures.filter((c) => c.status === "ok");
|
|
425
|
+
const hasReference = ok.some((c) => c.referenceDir);
|
|
426
|
+
const hasMirror = ok.some((c) => c.mirrorDir);
|
|
427
|
+
const perHost = ok
|
|
428
|
+
.filter((c) => c.referenceDir || c.mirrorDir)
|
|
429
|
+
.map((c) => {
|
|
430
|
+
const lines = [`### ${c.url}`];
|
|
431
|
+
if (c.referenceDir) {
|
|
432
|
+
lines.push(`- **Reference:** \`${relativize(c.referenceDir, run.outputDir)}/\` — start with \`FOR_AI_REFERENCE.md\`, then \`dom-structure.json\` (exact tree), \`structure-outline.txt\`, \`page.html\`, \`visible-text.txt\` (or \`.json\`), \`media.json\`.`);
|
|
433
|
+
}
|
|
434
|
+
if (c.mirrorDir) {
|
|
435
|
+
lines.push(`- **Mirror:** \`${relativize(c.mirrorDir, run.outputDir)}/page.tsx\` — section scaffold + \`data-mirror-section\`; read \`MIRROR_NOTES.md\`.`);
|
|
436
|
+
}
|
|
437
|
+
return lines.join("\n");
|
|
438
|
+
})
|
|
439
|
+
.join("\n\n");
|
|
440
|
+
|
|
441
|
+
const structureSection =
|
|
442
|
+
hasReference || hasMirror
|
|
443
|
+
? `
|
|
444
|
+
|
|
445
|
+
## Per-site recon inputs (use for layout + copy)
|
|
446
|
+
|
|
447
|
+
${perHost || "_No reference/mirror paths on this run — token-only._"}
|
|
448
|
+
|
|
449
|
+
Workflow (similar in spirit to **recon → specs → build** pipelines):
|
|
450
|
+
|
|
451
|
+
1. **Recon:** Use \`dom-structure.json\` for **exact nesting and sibling order**; use \`structure-outline.txt\` or \`page.html\` for skimming. Cross-check with \`mirror/.../page.tsx\` (\`data-mirror-section\`).
|
|
452
|
+
2. **Wire copy + media:** Map headings, buttons, and blocks from \`visible-text.*\` into the matching mirror sections (or your components). Use \`media.json\` for asset URLs; replace with licensed or original assets when shipping.
|
|
453
|
+
3. **Build:** Prefer editing **mirror \`page.tsx\`** inside the user's app (or port its structure into their file tree) rather than inventing a new section order from scratch. Apply **REPORT.md** / **tokens** for colors, type, spacing, radii — mirror CSS variables under \`.mirror-root\` should converge to the same semantic palette where possible.
|
|
454
|
+
|
|
455
|
+
**Compliance:** Do not pass off another brand as the user's product. Omit or replace logos, trademarked names used as if they were the user's, and any copy the user says is off-limits. When the user **wants** fidelity to the crawled page (e.g. their own URL), use \`visible-text\` verbatim where appropriate.
|
|
456
|
+
`
|
|
457
|
+
: "";
|
|
458
|
+
|
|
459
|
+
return `# Hand this folder to your AI (Cursor, Claude Code, etc.)
|
|
460
|
+
|
|
461
|
+
**Run:** \`${system.runId}\`
|
|
462
|
+
**Absolute path:** \`${abs}\`
|
|
463
|
+
|
|
464
|
+
## What to attach
|
|
465
|
+
|
|
466
|
+
Attach this entire \`output/${system.runId}/\` folder (or copy it into the app repo).
|
|
467
|
+
|
|
468
|
+
**Minimum:** \`FOR_AI.md\`, \`REPORT.md\`, \`tokens.json\`, \`tailwind.config.ts\`, \`globals.css\`.
|
|
469
|
+
|
|
470
|
+
**When rebuilding from a crawled URL, also attach (per host):**
|
|
471
|
+
\`reference/<host>/\` **and** \`mirror/<host>/\` — the model must see both **verbatim DOM/copy** and the **typed React scaffold**.
|
|
472
|
+
|
|
473
|
+
In **Cursor**, \`@\` those paths explicitly.
|
|
474
|
+
|
|
475
|
+
## Authority order
|
|
476
|
+
|
|
477
|
+
1. **Structural fidelity:** \`reference/<host>/dom-structure.json\` + \`page.html\` + \`visible-text.*\` + \`mirror/<host>/page.tsx\` — exact DOM tree shape, then copy and typed scaffold.
|
|
478
|
+
2. **Design tokens:** **REPORT.md** and **tokens.json** — typography scale, spacing, radii, colors, container width, notes.
|
|
479
|
+
3. **Integration:** **tailwind.config.ts** + **globals.css** — merge into a Next.js + Tailwind + shadcn-style app.
|
|
480
|
+
${structureSection}
|
|
481
|
+
## Instruction block (paste into chat)
|
|
482
|
+
|
|
483
|
+
\`\`\`text
|
|
484
|
+
You must use the attached \`output/${system.runId}/\` folder.
|
|
485
|
+
|
|
486
|
+
- Read REPORT.md and tokens.json before writing UI. Merge tailwind.config.ts and globals.css into my project (preserve my paths unless I say otherwise).
|
|
487
|
+
- Style with semantic tokens: bg-background, text-foreground, text-muted-foreground, border-border, bg-primary, text-primary-foreground, bg-card, text-card-foreground, etc. Prefer these over ad-hoc hex; mirror pages may use --mirror-* variables until merged.
|
|
488
|
+
- If reference/ and mirror/ exist for my source URL: treat them as mandatory context. Preserve **exact DOM nesting and sibling order** from `dom-structure.json` (and cross-check `page.html`). Align components to `data-mirror-section` and the mirror scaffold. Wire copy from visible-text.* and media from media.json unless I say to rewrite for a different product.
|
|
489
|
+
- If I am building a NEW product unrelated to the crawl: keep layout inspiration from mirror/reference but REPLACE product names, claims, and sensitive copy with my copy. Never impersonate another brand.
|
|
490
|
+
|
|
491
|
+
My product / intent: [describe goal — faithful mirror of URL vs new product in same layout; tone and CTA]
|
|
492
|
+
\`\`\`
|
|
493
|
+
|
|
494
|
+
## After the agent runs
|
|
495
|
+
|
|
496
|
+
- Compare against **theme-preview.tsx** to verify token usage.
|
|
497
|
+
- Iterate with REPORT.md + reference/ + mirror/ in context.
|
|
498
|
+
`;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function rampRows(ramp: ColorRamp): string {
|
|
502
|
+
const entries: Array<[string, string]> = [
|
|
503
|
+
["background", ramp.background],
|
|
504
|
+
["foreground", ramp.foreground],
|
|
505
|
+
["card", ramp.card],
|
|
506
|
+
["card-foreground", ramp.cardForeground],
|
|
507
|
+
["primary", ramp.primary],
|
|
508
|
+
["primary-foreground", ramp.primaryForeground],
|
|
509
|
+
["secondary", ramp.secondary],
|
|
510
|
+
["secondary-foreground", ramp.secondaryForeground],
|
|
511
|
+
["muted", ramp.muted],
|
|
512
|
+
["muted-foreground", ramp.mutedForeground],
|
|
513
|
+
["accent", ramp.accent],
|
|
514
|
+
["accent-foreground", ramp.accentForeground],
|
|
515
|
+
["destructive", ramp.destructive],
|
|
516
|
+
["destructive-foreground", ramp.destructiveForeground],
|
|
517
|
+
["border", ramp.border],
|
|
518
|
+
["input", ramp.input],
|
|
519
|
+
["ring", ramp.ring],
|
|
520
|
+
];
|
|
521
|
+
return entries.map(([k, v]) => `| \`--${k}\` | \`${v}\` |`).join("\n");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/* -------------------------------------------------------------------------- */
|
|
525
|
+
/* Helpers */
|
|
526
|
+
/* -------------------------------------------------------------------------- */
|
|
527
|
+
|
|
528
|
+
function pxToRem(px: number): string {
|
|
529
|
+
return (Math.round((px / 16) * 1000) / 1000).toString();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function relativize(p: string, base: string): string {
|
|
533
|
+
return p.replace(base + "/", "").replace(base + "\\", "");
|
|
534
|
+
}
|