launchframe 0.2.0 → 0.2.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.
Files changed (73) hide show
  1. package/README.md +144 -183
  2. package/bin/launchframe.mjs +261 -28
  3. package/package.json +52 -67
  4. package/template/.aider.conf.yml +3 -0
  5. package/template/.amazonq/cli-agents/clone-website.json +9 -0
  6. package/template/.amazonq/rules/project.md +161 -0
  7. package/template/.augment/commands/clone-website.md +518 -0
  8. package/template/.claude/skills/clone-website/SKILL.md +517 -0
  9. package/template/.clinerules +161 -0
  10. package/template/.codex/skills/clone-website/SKILL.md +517 -0
  11. package/template/.continue/commands/clone-website.md +519 -0
  12. package/template/.continue/rules/project.md +165 -0
  13. package/template/.cursor/commands/clone-website.md +514 -0
  14. package/template/.cursor/rules/project.mdc +20 -0
  15. package/template/.dockerignore +60 -0
  16. package/template/.gemini/commands/clone-website.toml +520 -0
  17. package/template/.gitattributes +9 -0
  18. package/template/.github/ISSUE_TEMPLATE/bug_report.yml +86 -0
  19. package/template/.github/ISSUE_TEMPLATE/config.yml +5 -0
  20. package/template/.github/ISSUE_TEMPLATE/feature_request.yml +50 -0
  21. package/template/.github/PULL_REQUEST_TEMPLATE.md +19 -0
  22. package/template/.github/copilot-instructions.md +161 -0
  23. package/template/.github/copilot-setup-steps.yml +3 -0
  24. package/template/.github/skills/clone-website/SKILL.md +517 -0
  25. package/template/.github/workflows/ci.yml +36 -0
  26. package/template/.nvmrc +1 -0
  27. package/template/.opencode/commands/clone-website.md +517 -0
  28. package/template/.windsurf/workflows/clone-website.md +514 -0
  29. package/template/.windsurfrules +2 -0
  30. package/template/AGENTS.md +79 -0
  31. package/template/CHANGELOG.md +80 -0
  32. package/template/CLAUDE.md +1 -0
  33. package/template/Dockerfile +114 -0
  34. package/template/Dockerfile.dev +15 -0
  35. package/template/GEMINI.md +1 -0
  36. package/template/README.md +118 -0
  37. package/template/START_HERE.md +15 -0
  38. package/template/components.json +25 -0
  39. package/template/docker-compose.yml +53 -0
  40. package/template/docs/design-references/.gitkeep +0 -0
  41. package/template/docs/design-references/comparison.png +0 -0
  42. package/template/docs/research/INSPECTION_GUIDE.md +80 -0
  43. package/template/eslint.config.mjs +18 -0
  44. package/template/next.config.ts +8 -0
  45. package/template/package.json +59 -0
  46. package/template/postcss.config.mjs +7 -0
  47. package/template/public/images/.gitkeep +0 -0
  48. package/template/public/seo/.gitkeep +0 -0
  49. package/template/public/videos/.gitkeep +0 -0
  50. package/template/scripts/.gitkeep +0 -0
  51. package/template/scripts/sync-agent-rules.sh +88 -0
  52. package/template/scripts/sync-skills.mjs +111 -0
  53. package/template/src/app/favicon.ico +0 -0
  54. package/template/src/app/globals.css +130 -0
  55. package/template/src/app/layout.tsx +33 -0
  56. package/template/src/app/page.tsx +9 -0
  57. package/template/src/components/ui/button.tsx +60 -0
  58. package/template/src/hooks/.gitkeep +0 -0
  59. package/template/src/lib/utils.ts +6 -0
  60. package/template/src/types/.gitkeep +0 -0
  61. package/template/tsconfig.json +34 -0
  62. package/packages/extract/automated-clone-pass.ts +0 -353
  63. package/packages/extract/browser-extract.ts +0 -237
  64. package/packages/extract/cloner-research-emit.ts +0 -270
  65. package/packages/extract/dom-crawler.ts +0 -521
  66. package/packages/extract/emit.ts +0 -553
  67. package/packages/extract/extract.ts +0 -548
  68. package/packages/extract/host-slug.ts +0 -5
  69. package/packages/extract/mirror-emit.ts +0 -620
  70. package/packages/extract/package.json +0 -13
  71. package/packages/extract/reference-dump.ts +0 -431
  72. package/packages/extract/synthesize.ts +0 -551
  73. package/packages/extract/types.ts +0 -316
@@ -1,553 +0,0 @@
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 landingfram.
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 landingfram.
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 ideaSection = run.saasIdea?.trim()
428
- ? `
429
-
430
- ## Operator SaaS idea (from CLI)
431
-
432
- ${run.saasIdea.trim()}
433
-
434
- Use this narrative when replacing mirror placeholders and reference copy: headings, subheads, and CTAs should describe **your** product. Do not present another brand's trademarks or product name as yours.
435
- `
436
- : "";
437
- const intentPaste =
438
- run.saasIdea?.trim() ??
439
- "[describe goal — faithful mirror of URL vs new product in same layout; tone and CTA]";
440
- const perHost = ok
441
- .filter((c) => c.referenceDir || c.mirrorDir)
442
- .map((c) => {
443
- const lines = [`### ${c.url}`];
444
- if (c.referenceDir) {
445
- 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\`.`);
446
- }
447
- if (c.mirrorDir) {
448
- lines.push(`- **Mirror:** \`${relativize(c.mirrorDir, run.outputDir)}/page.tsx\` — section scaffold + \`data-mirror-section\`; read \`MIRROR_NOTES.md\`.`);
449
- }
450
- return lines.join("\n");
451
- })
452
- .join("\n\n");
453
-
454
- const structureSection =
455
- hasReference || hasMirror
456
- ? `
457
-
458
- ## Per-site recon inputs (use for layout + copy)
459
-
460
- ${perHost || "_No reference/mirror paths on this run — token-only._"}
461
-
462
- Workflow (similar in spirit to **recon → specs → build** pipelines):
463
-
464
- 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\`).
465
- 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.
466
- 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.
467
-
468
- **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.
469
- `
470
- : "";
471
-
472
- return `# Hand this folder to your AI (Cursor, Claude Code, etc.)
473
-
474
- **Run:** \`${system.runId}\`
475
- **Absolute path:** \`${abs}\`
476
-
477
- ## What to attach
478
-
479
- Attach this entire \`output/${system.runId}/\` folder (or copy it into the app repo).
480
-
481
- **Minimum:** \`FOR_AI.md\`, \`REPORT.md\`, \`tokens.json\`, \`tailwind.config.ts\`, \`globals.css\`.
482
-
483
- **When rebuilding from a crawled URL, also attach (per host):**
484
- \`reference/<host>/\` **and** \`mirror/<host>/\` — the model must see both **verbatim DOM/copy** and the **typed React scaffold**.
485
-
486
- **For clone-style parallel builders:** attach \`docs/research/\` (per-host topology, behaviors checklist, component specs).
487
-
488
- **Automated recon (design references):** \`docs/design-references/<host-slug>/\` — desktop/tablet/mobile captures, scroll sweep frames, header scroll probe, interaction hints — plus optional \`downloaded_assets/<host-slug>/manifest.json\`.
489
-
490
- In **Cursor**, \`@\` those paths explicitly.
491
-
492
- ## Authority order
493
-
494
- 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.
495
- 2. **Design tokens:** **REPORT.md** and **tokens.json** — typography scale, spacing, radii, colors, container width, notes.
496
- 3. **Integration:** **tailwind.config.ts** + **globals.css** — merge into a Next.js + Tailwind + shadcn-style app.
497
- 4. **Builder specs:** **docs/research/** — per-host \`PAGE_TOPOLOGY.md\`, \`BEHAVIORS.md\`, \`components/*.spec.md\`.
498
- 5. **Visual parity:** **docs/design-references/** — screenshots + automated probes for scroll/chrome behavior (supplement with manual MCP for tab/modal states).
499
- ${ideaSection}${structureSection}
500
- ## Instruction block (paste into chat)
501
-
502
- \`\`\`text
503
- You must use the attached \`output/${system.runId}/\` folder.
504
-
505
- - 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).
506
- - 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.
507
- - 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.
508
- - 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.
509
-
510
- My product / intent: ${intentPaste}
511
- \`\`\`
512
-
513
- ## After the agent runs
514
-
515
- - Compare against **theme-preview.tsx** to verify token usage.
516
- - Iterate with REPORT.md + reference/ + mirror/ + **docs/research/** + **docs/design-references/** in context.
517
- `;
518
- }
519
-
520
- function rampRows(ramp: ColorRamp): string {
521
- const entries: Array<[string, string]> = [
522
- ["background", ramp.background],
523
- ["foreground", ramp.foreground],
524
- ["card", ramp.card],
525
- ["card-foreground", ramp.cardForeground],
526
- ["primary", ramp.primary],
527
- ["primary-foreground", ramp.primaryForeground],
528
- ["secondary", ramp.secondary],
529
- ["secondary-foreground", ramp.secondaryForeground],
530
- ["muted", ramp.muted],
531
- ["muted-foreground", ramp.mutedForeground],
532
- ["accent", ramp.accent],
533
- ["accent-foreground", ramp.accentForeground],
534
- ["destructive", ramp.destructive],
535
- ["destructive-foreground", ramp.destructiveForeground],
536
- ["border", ramp.border],
537
- ["input", ramp.input],
538
- ["ring", ramp.ring],
539
- ];
540
- return entries.map(([k, v]) => `| \`--${k}\` | \`${v}\` |`).join("\n");
541
- }
542
-
543
- /* -------------------------------------------------------------------------- */
544
- /* Helpers */
545
- /* -------------------------------------------------------------------------- */
546
-
547
- function pxToRem(px: number): string {
548
- return (Math.round((px / 16) * 1000) / 1000).toString();
549
- }
550
-
551
- function relativize(p: string, base: string): string {
552
- return p.replace(base + "/", "").replace(base + "\\", "");
553
- }