launchframe 0.1.0 → 0.1.2
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/package.json +1 -1
- package/packages/extract/emit.ts +466 -466
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "launchframe",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Point Launchframe at SaaS sites you admire and get back a drop-in shadcn/ui design system (tokens, Tailwind theme, CSS variables, AI handoff) you can build your own UI on top of.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Evan Gruhlkey",
|
package/packages/extract/emit.ts
CHANGED
|
@@ -1,466 +1,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, with the
|
|
13
|
-
* anti-clone disclaimer
|
|
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
|
-
return `# Design system extraction report
|
|
303
|
-
|
|
304
|
-
**Run:** \`${system.runId}\`
|
|
305
|
-
**Generated:** ${run.finishedAt}
|
|
306
|
-
|
|
307
|
-
## Inspiration sources
|
|
308
|
-
|
|
309
|
-
The design system below was synthesized by analyzing the **rendered
|
|
310
|
-
appearance** of these public pages. No source code, brand assets,
|
|
311
|
-
illustrations, logos, or copywriting was extracted or stored.
|
|
312
|
-
|
|
313
|
-
${okCaptures.map((c) => `- ${c.url} \n screenshot: \`${relativize(c.screenshotPath, run.outputDir)}\``).join("\n")}
|
|
314
|
-
|
|
315
|
-
${
|
|
316
|
-
failedCaptures.length > 0
|
|
317
|
-
? `### Skipped or failed\n\n${failedCaptures
|
|
318
|
-
.map((c) => `- ${c.url} — ${c.status}${c.reason ? `: ${c.reason}` : ""}`)
|
|
319
|
-
.join("\n")}`
|
|
320
|
-
: ""
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
## Synthesis decisions
|
|
324
|
-
|
|
325
|
-
${system.notes.length > 0 ? system.notes.map((n) => `- ${n}`).join("\n") : "_No special notes — defaults applied where signals were weak._"}
|
|
326
|
-
|
|
327
|
-
## Typography
|
|
328
|
-
|
|
329
|
-
- **Sans family:** ${system.typography.fontSans}${system.typography.fontSansSubstituted ? " *(substituted)*" : ""}
|
|
330
|
-
- **Mono family:** ${system.typography.fontMono}${system.typography.fontMonoSubstituted ? " *(substituted)*" : ""}
|
|
331
|
-
- **Body base:** ${system.typography.basePx}px
|
|
332
|
-
- **Scale ratio:** ${system.typography.scaleRatio}
|
|
333
|
-
- **Body line-height:** ${system.typography.bodyLineHeight}
|
|
334
|
-
- **Heading line-height:** ${system.typography.headingLineHeight}
|
|
335
|
-
- **Body letter-spacing:** ${system.typography.bodyLetterSpacingEm}em
|
|
336
|
-
|
|
337
|
-
| step | px |
|
|
338
|
-
| ---- | -- |
|
|
339
|
-
${Object.entries(system.typography.steps).map(([k, v]) => `| ${k} | ${v} |`).join("\n")}
|
|
340
|
-
|
|
341
|
-
## Spacing
|
|
342
|
-
|
|
343
|
-
- **Base unit:** ${system.spacing.basePx}px
|
|
344
|
-
- **Container width:** ${system.spacing.containerPx}px
|
|
345
|
-
- **Steps (px):** ${system.spacing.steps.join(", ")}
|
|
346
|
-
|
|
347
|
-
## Radius
|
|
348
|
-
|
|
349
|
-
- **Base radius:** ${system.radius.basePx}px (used for shadcn's \`--radius\`)
|
|
350
|
-
|
|
351
|
-
## Color tokens
|
|
352
|
-
|
|
353
|
-
### Light theme
|
|
354
|
-
|
|
355
|
-
| token | value |
|
|
356
|
-
| ----- | ----- |
|
|
357
|
-
${rampRows(system.light)}
|
|
358
|
-
|
|
359
|
-
### Dark theme
|
|
360
|
-
|
|
361
|
-
| token | value |
|
|
362
|
-
| ----- | ----- |
|
|
363
|
-
${rampRows(system.dark)}
|
|
364
|
-
|
|
365
|
-
## Shadows
|
|
366
|
-
|
|
367
|
-
- \`shadow-sm\` — \`${system.shadows.sm}\`
|
|
368
|
-
- \`shadow-md\` — \`${system.shadows.md}\`
|
|
369
|
-
- \`shadow-lg\` — \`${system.shadows.lg}\`
|
|
370
|
-
|
|
371
|
-
## Drop-in usage
|
|
372
|
-
|
|
373
|
-
1. Copy \`tailwind.config.ts\` into the root of a Next.js + Tailwind project.
|
|
374
|
-
2. Copy \`globals.css\` into \`app/globals.css\` (replacing the existing file).
|
|
375
|
-
3. Render \`theme-preview.tsx\` somewhere to eyeball the system.
|
|
376
|
-
4. Iterate from there. The tokens are yours; this report is a record of where
|
|
377
|
-
they came from.
|
|
378
|
-
|
|
379
|
-
## What this report is not
|
|
380
|
-
|
|
381
|
-
This is **not** a clone of any source site. It does not reproduce layouts,
|
|
382
|
-
copy, logos, or brand identity. It records aggregate design-token signals
|
|
383
|
-
(colors, type sizes, spacing, radii) and synthesizes an original system
|
|
384
|
-
inspired by the corpus.
|
|
385
|
-
|
|
386
|
-
If a source operator requests removal, delete the screenshot, drop the URL
|
|
387
|
-
from the manifest, and re-run the extraction.
|
|
388
|
-
`;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Copy-paste handoff for Cursor / Claude Code / any agent.
|
|
393
|
-
*/
|
|
394
|
-
function emitForAi(system: DesignSystem, run: ExtractionRun): string {
|
|
395
|
-
const abs = run.outputDir.replace(/\\/g, "/");
|
|
396
|
-
return `# Hand this folder to your AI (Cursor, Claude Code, etc.)
|
|
397
|
-
|
|
398
|
-
**Run:** \`${system.runId}\`
|
|
399
|
-
**Absolute path:** \`${abs}\`
|
|
400
|
-
|
|
401
|
-
## What to attach
|
|
402
|
-
|
|
403
|
-
In **Cursor**: \`@\` this folder or add \`FOR_AI.md\`, \`REPORT.md\`, and \`tokens.json\` to context.
|
|
404
|
-
In **Claude Code**: attach the whole \`output/${system.runId}/\` directory (or copy it into your app repo).
|
|
405
|
-
|
|
406
|
-
## Authority order
|
|
407
|
-
|
|
408
|
-
1. **REPORT.md** — typography scale, spacing, radii, colors, container width, notes.
|
|
409
|
-
2. **tokens.json** — the same system as structured JSON.
|
|
410
|
-
3. **tailwind.config.ts** + **globals.css** — drop these into a Next.js + Tailwind + shadcn-style app.
|
|
411
|
-
|
|
412
|
-
## Instruction block (paste into chat)
|
|
413
|
-
|
|
414
|
-
\`\`\`text
|
|
415
|
-
You must follow the design system in the attached \`output/${system.runId}/\` folder.
|
|
416
|
-
|
|
417
|
-
- Read REPORT.md and tokens.json before writing any UI.
|
|
418
|
-
- Merge tailwind.config.ts and globals.css into my project (preserve my existing content paths unless I say otherwise).
|
|
419
|
-
- Style with semantic tokens only: bg-background, text-foreground, text-muted-foreground, border-border, bg-primary, text-primary-foreground, bg-card, text-card-foreground, etc. No ad-hoc hex colors.
|
|
420
|
-
- Use the font families and type steps from REPORT.md; load web fonts if needed.
|
|
421
|
-
- Build a landing page for MY product (I will describe below). Sections: nav, hero, proof strip, features, pricing or CTA, footer. Original copy only — do not copy text from any reference site.
|
|
422
|
-
|
|
423
|
-
My product: [describe your idea, audience, and primary CTA here]
|
|
424
|
-
\`\`\`
|
|
425
|
-
|
|
426
|
-
## After the agent runs
|
|
427
|
-
|
|
428
|
-
- Compare against **theme-preview.tsx** (optional route) to see if components match the token intent.
|
|
429
|
-
- Iterate in the same chat; keep REPORT.md in context for every change.
|
|
430
|
-
`;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function rampRows(ramp: ColorRamp): string {
|
|
434
|
-
const entries: Array<[string, string]> = [
|
|
435
|
-
["background", ramp.background],
|
|
436
|
-
["foreground", ramp.foreground],
|
|
437
|
-
["card", ramp.card],
|
|
438
|
-
["card-foreground", ramp.cardForeground],
|
|
439
|
-
["primary", ramp.primary],
|
|
440
|
-
["primary-foreground", ramp.primaryForeground],
|
|
441
|
-
["secondary", ramp.secondary],
|
|
442
|
-
["secondary-foreground", ramp.secondaryForeground],
|
|
443
|
-
["muted", ramp.muted],
|
|
444
|
-
["muted-foreground", ramp.mutedForeground],
|
|
445
|
-
["accent", ramp.accent],
|
|
446
|
-
["accent-foreground", ramp.accentForeground],
|
|
447
|
-
["destructive", ramp.destructive],
|
|
448
|
-
["destructive-foreground", ramp.destructiveForeground],
|
|
449
|
-
["border", ramp.border],
|
|
450
|
-
["input", ramp.input],
|
|
451
|
-
["ring", ramp.ring],
|
|
452
|
-
];
|
|
453
|
-
return entries.map(([k, v]) => `| \`--${k}\` | \`${v}\` |`).join("\n");
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/* -------------------------------------------------------------------------- */
|
|
457
|
-
/* Helpers */
|
|
458
|
-
/* -------------------------------------------------------------------------- */
|
|
459
|
-
|
|
460
|
-
function pxToRem(px: number): string {
|
|
461
|
-
return (Math.round((px / 16) * 1000) / 1000).toString();
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function relativize(p: string, base: string): string {
|
|
465
|
-
return p.replace(base + "/", "").replace(base + "\\", "");
|
|
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, with the
|
|
13
|
+
* anti-clone disclaimer
|
|
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
|
+
return `# Design system extraction report
|
|
303
|
+
|
|
304
|
+
**Run:** \`${system.runId}\`
|
|
305
|
+
**Generated:** ${run.finishedAt}
|
|
306
|
+
|
|
307
|
+
## Inspiration sources
|
|
308
|
+
|
|
309
|
+
The design system below was synthesized by analyzing the **rendered
|
|
310
|
+
appearance** of these public pages. No source code, brand assets,
|
|
311
|
+
illustrations, logos, or copywriting was extracted or stored.
|
|
312
|
+
|
|
313
|
+
${okCaptures.map((c) => `- ${c.url} \n screenshot: \`${relativize(c.screenshotPath, run.outputDir)}\``).join("\n")}
|
|
314
|
+
|
|
315
|
+
${
|
|
316
|
+
failedCaptures.length > 0
|
|
317
|
+
? `### Skipped or failed\n\n${failedCaptures
|
|
318
|
+
.map((c) => `- ${c.url} — ${c.status}${c.reason ? `: ${c.reason}` : ""}`)
|
|
319
|
+
.join("\n")}`
|
|
320
|
+
: ""
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
## Synthesis decisions
|
|
324
|
+
|
|
325
|
+
${system.notes.length > 0 ? system.notes.map((n) => `- ${n}`).join("\n") : "_No special notes — defaults applied where signals were weak._"}
|
|
326
|
+
|
|
327
|
+
## Typography
|
|
328
|
+
|
|
329
|
+
- **Sans family:** ${system.typography.fontSans}${system.typography.fontSansSubstituted ? " *(substituted)*" : ""}
|
|
330
|
+
- **Mono family:** ${system.typography.fontMono}${system.typography.fontMonoSubstituted ? " *(substituted)*" : ""}
|
|
331
|
+
- **Body base:** ${system.typography.basePx}px
|
|
332
|
+
- **Scale ratio:** ${system.typography.scaleRatio}
|
|
333
|
+
- **Body line-height:** ${system.typography.bodyLineHeight}
|
|
334
|
+
- **Heading line-height:** ${system.typography.headingLineHeight}
|
|
335
|
+
- **Body letter-spacing:** ${system.typography.bodyLetterSpacingEm}em
|
|
336
|
+
|
|
337
|
+
| step | px |
|
|
338
|
+
| ---- | -- |
|
|
339
|
+
${Object.entries(system.typography.steps).map(([k, v]) => `| ${k} | ${v} |`).join("\n")}
|
|
340
|
+
|
|
341
|
+
## Spacing
|
|
342
|
+
|
|
343
|
+
- **Base unit:** ${system.spacing.basePx}px
|
|
344
|
+
- **Container width:** ${system.spacing.containerPx}px
|
|
345
|
+
- **Steps (px):** ${system.spacing.steps.join(", ")}
|
|
346
|
+
|
|
347
|
+
## Radius
|
|
348
|
+
|
|
349
|
+
- **Base radius:** ${system.radius.basePx}px (used for shadcn's \`--radius\`)
|
|
350
|
+
|
|
351
|
+
## Color tokens
|
|
352
|
+
|
|
353
|
+
### Light theme
|
|
354
|
+
|
|
355
|
+
| token | value |
|
|
356
|
+
| ----- | ----- |
|
|
357
|
+
${rampRows(system.light)}
|
|
358
|
+
|
|
359
|
+
### Dark theme
|
|
360
|
+
|
|
361
|
+
| token | value |
|
|
362
|
+
| ----- | ----- |
|
|
363
|
+
${rampRows(system.dark)}
|
|
364
|
+
|
|
365
|
+
## Shadows
|
|
366
|
+
|
|
367
|
+
- \`shadow-sm\` — \`${system.shadows.sm}\`
|
|
368
|
+
- \`shadow-md\` — \`${system.shadows.md}\`
|
|
369
|
+
- \`shadow-lg\` — \`${system.shadows.lg}\`
|
|
370
|
+
|
|
371
|
+
## Drop-in usage
|
|
372
|
+
|
|
373
|
+
1. Copy \`tailwind.config.ts\` into the root of a Next.js + Tailwind project.
|
|
374
|
+
2. Copy \`globals.css\` into \`app/globals.css\` (replacing the existing file).
|
|
375
|
+
3. Render \`theme-preview.tsx\` somewhere to eyeball the system.
|
|
376
|
+
4. Iterate from there. The tokens are yours; this report is a record of where
|
|
377
|
+
they came from.
|
|
378
|
+
|
|
379
|
+
## What this report is not
|
|
380
|
+
|
|
381
|
+
This is **not** a clone of any source site. It does not reproduce layouts,
|
|
382
|
+
copy, logos, or brand identity. It records aggregate design-token signals
|
|
383
|
+
(colors, type sizes, spacing, radii) and synthesizes an original system
|
|
384
|
+
inspired by the corpus.
|
|
385
|
+
|
|
386
|
+
If a source operator requests removal, delete the screenshot, drop the URL
|
|
387
|
+
from the manifest, and re-run the extraction.
|
|
388
|
+
`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Copy-paste handoff for Cursor / Claude Code / any agent.
|
|
393
|
+
*/
|
|
394
|
+
function emitForAi(system: DesignSystem, run: ExtractionRun): string {
|
|
395
|
+
const abs = run.outputDir.replace(/\\/g, "/");
|
|
396
|
+
return `# Hand this folder to your AI (Cursor, Claude Code, etc.)
|
|
397
|
+
|
|
398
|
+
**Run:** \`${system.runId}\`
|
|
399
|
+
**Absolute path:** \`${abs}\`
|
|
400
|
+
|
|
401
|
+
## What to attach
|
|
402
|
+
|
|
403
|
+
In **Cursor**: \`@\` this folder or add \`FOR_AI.md\`, \`REPORT.md\`, and \`tokens.json\` to context.
|
|
404
|
+
In **Claude Code**: attach the whole \`output/${system.runId}/\` directory (or copy it into your app repo).
|
|
405
|
+
|
|
406
|
+
## Authority order
|
|
407
|
+
|
|
408
|
+
1. **REPORT.md** — typography scale, spacing, radii, colors, container width, notes.
|
|
409
|
+
2. **tokens.json** — the same system as structured JSON.
|
|
410
|
+
3. **tailwind.config.ts** + **globals.css** — drop these into a Next.js + Tailwind + shadcn-style app.
|
|
411
|
+
|
|
412
|
+
## Instruction block (paste into chat)
|
|
413
|
+
|
|
414
|
+
\`\`\`text
|
|
415
|
+
You must follow the design system in the attached \`output/${system.runId}/\` folder.
|
|
416
|
+
|
|
417
|
+
- Read REPORT.md and tokens.json before writing any UI.
|
|
418
|
+
- Merge tailwind.config.ts and globals.css into my project (preserve my existing content paths unless I say otherwise).
|
|
419
|
+
- Style with semantic tokens only: bg-background, text-foreground, text-muted-foreground, border-border, bg-primary, text-primary-foreground, bg-card, text-card-foreground, etc. No ad-hoc hex colors.
|
|
420
|
+
- Use the font families and type steps from REPORT.md; load web fonts if needed.
|
|
421
|
+
- Build a landing page for MY product (I will describe below). Sections: nav, hero, proof strip, features, pricing or CTA, footer. Original copy only — do not copy text from any reference site.
|
|
422
|
+
|
|
423
|
+
My product: [describe your idea, audience, and primary CTA here]
|
|
424
|
+
\`\`\`
|
|
425
|
+
|
|
426
|
+
## After the agent runs
|
|
427
|
+
|
|
428
|
+
- Compare against **theme-preview.tsx** (optional route) to see if components match the token intent.
|
|
429
|
+
- Iterate in the same chat; keep REPORT.md in context for every change.
|
|
430
|
+
`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function rampRows(ramp: ColorRamp): string {
|
|
434
|
+
const entries: Array<[string, string]> = [
|
|
435
|
+
["background", ramp.background],
|
|
436
|
+
["foreground", ramp.foreground],
|
|
437
|
+
["card", ramp.card],
|
|
438
|
+
["card-foreground", ramp.cardForeground],
|
|
439
|
+
["primary", ramp.primary],
|
|
440
|
+
["primary-foreground", ramp.primaryForeground],
|
|
441
|
+
["secondary", ramp.secondary],
|
|
442
|
+
["secondary-foreground", ramp.secondaryForeground],
|
|
443
|
+
["muted", ramp.muted],
|
|
444
|
+
["muted-foreground", ramp.mutedForeground],
|
|
445
|
+
["accent", ramp.accent],
|
|
446
|
+
["accent-foreground", ramp.accentForeground],
|
|
447
|
+
["destructive", ramp.destructive],
|
|
448
|
+
["destructive-foreground", ramp.destructiveForeground],
|
|
449
|
+
["border", ramp.border],
|
|
450
|
+
["input", ramp.input],
|
|
451
|
+
["ring", ramp.ring],
|
|
452
|
+
];
|
|
453
|
+
return entries.map(([k, v]) => `| \`--${k}\` | \`${v}\` |`).join("\n");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/* -------------------------------------------------------------------------- */
|
|
457
|
+
/* Helpers */
|
|
458
|
+
/* -------------------------------------------------------------------------- */
|
|
459
|
+
|
|
460
|
+
function pxToRem(px: number): string {
|
|
461
|
+
return (Math.round((px / 16) * 1000) / 1000).toString();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function relativize(p: string, base: string): string {
|
|
465
|
+
return p.replace(base + "/", "").replace(base + "\\", "");
|
|
466
|
+
}
|