extraktor 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,502 @@
1
+ // src/genome/design-md-generator.ts
2
+ import Anthropic from "@anthropic-ai/sdk";
3
+ import fs from "fs-extra";
4
+ var DesignMdGenerator = class {
5
+ client;
6
+ model;
7
+ logger;
8
+ constructor(options) {
9
+ const apiKey = options.apiKey || process.env.ANTHROPIC_API_KEY;
10
+ this.client = apiKey ? new Anthropic({ apiKey }) : null;
11
+ this.model = options.model || "claude-sonnet-4-6-20250514";
12
+ this.logger = options.logger;
13
+ }
14
+ async generate(result, outputPath, sourceUrl) {
15
+ this.logger.info("Generating DESIGN.md...");
16
+ const siteName = this.getSiteName(sourceUrl);
17
+ const layoutArch = result.layoutArchitecture;
18
+ const sections = [
19
+ this.section1_VisualTheme(result, siteName, sourceUrl),
20
+ this.section2_ColorPalette(result),
21
+ this.section3_Typography(result),
22
+ this.section4_Components(result),
23
+ this.section5_Layout(result, layoutArch),
24
+ this.section6_Depth(result),
25
+ this.section7_DosAndDonts(result),
26
+ this.section8_Responsive(result, layoutArch),
27
+ this.section9_AgentPrompt(result, siteName)
28
+ ];
29
+ let md = `# Design System: ${siteName}
30
+
31
+ `;
32
+ md += sections.join("\n\n");
33
+ if (this.client) {
34
+ md = await this.enhanceWithAI(md, result, sourceUrl);
35
+ }
36
+ await fs.writeFile(outputPath, md, "utf-8");
37
+ this.logger.info(`DESIGN.md written to ${outputPath}`);
38
+ return md;
39
+ }
40
+ // ─── Section 1: Visual Theme & Atmosphere ──────────────────
41
+ section1_VisualTheme(result, siteName, url) {
42
+ const fonts = result.typography.fontFamilies;
43
+ const primaryFont = fonts[0]?.name || "system-ui";
44
+ const monoFont = fonts.find((f) => f.category === "mono")?.name;
45
+ const palette = result.colors.palette;
46
+ const bgColors = palette.filter((c) => c.usage?.includes("background"));
47
+ const textColors = palette.filter((c) => c.usage?.includes("text"));
48
+ const isDark = bgColors.some((c) => {
49
+ const hex = c.value.replace("#", "");
50
+ const r = parseInt(hex.slice(0, 2), 16);
51
+ const g = parseInt(hex.slice(2, 4), 16);
52
+ const b = parseInt(hex.slice(4, 6), 16);
53
+ return (r + g + b) / 3 < 80;
54
+ });
55
+ let md = `## 1. Visual Theme & Atmosphere
56
+
57
+ `;
58
+ md += `**Site:** ${url}
59
+
60
+ `;
61
+ md += `**Key Characteristics:**
62
+ `;
63
+ md += `- ${isDark ? "Dark-mode-first" : "Light-mode-first"} design
64
+ `;
65
+ md += `- Primary typeface: ${primaryFont}${fonts[0]?.fallbacks?.length ? ` with fallbacks: ${fonts[0].fallbacks.join(", ")}` : ""}
66
+ `;
67
+ if (monoFont) md += `- Monospace: ${monoFont}
68
+ `;
69
+ md += `- ${palette.length} colors in palette, ${result.colors.gradients.length} gradients
70
+ `;
71
+ md += `- ${result.animations.keyframes.length} keyframe animations, ${result.animations.transitions.length} transitions
72
+ `;
73
+ const accentColors = palette.filter(
74
+ (c) => c.usage?.includes("accent") || c.usage?.includes("border")
75
+ );
76
+ if (accentColors.length > 0) {
77
+ md += `- Accent colors: ${accentColors.slice(0, 3).map((c) => `\`${c.value}\``).join(", ")}
78
+ `;
79
+ }
80
+ return md;
81
+ }
82
+ // ─── Section 2: Color Palette & Roles ──────────────────────
83
+ section2_ColorPalette(result) {
84
+ let md = `## 2. Color Palette & Roles
85
+
86
+ `;
87
+ const categories = {};
88
+ for (const color of result.colors.palette) {
89
+ const category = this.categorizeColor(color);
90
+ if (!categories[category]) categories[category] = [];
91
+ categories[category].push(color);
92
+ }
93
+ for (const [category, colors] of Object.entries(categories)) {
94
+ md += `### ${category}
95
+ `;
96
+ for (const c of colors.slice(0, 8)) {
97
+ md += `- **${c.name}** (\`${c.value}\`): Used ${c.frequency}x${c.usage?.length ? ` - ${c.usage.join(", ")}` : ""}
98
+ `;
99
+ }
100
+ md += "\n";
101
+ }
102
+ if (result.colors.gradients.length > 0) {
103
+ md += `### Gradients
104
+ `;
105
+ for (const g of result.colors.gradients.slice(0, 5)) {
106
+ md += `- \`${g.value.slice(0, 80)}${g.value.length > 80 ? "..." : ""}\`
107
+ `;
108
+ }
109
+ md += "\n";
110
+ }
111
+ return md;
112
+ }
113
+ // ─── Section 3: Typography Rules ──────────────────────────
114
+ section3_Typography(result) {
115
+ let md = `## 3. Typography Rules
116
+
117
+ `;
118
+ md += `### Font Families
119
+ `;
120
+ for (const font of result.typography.fontFamilies) {
121
+ const fallbacks = font.fallbacks?.length ? `: \`${font.value}\`, fallbacks: ${font.fallbacks.join(", ")}` : "";
122
+ md += `- **${font.category || "Primary"}**: ${font.name}${fallbacks}
123
+ `;
124
+ }
125
+ md += "\n";
126
+ if (result.typography.fontSizes.length > 0) {
127
+ md += `### Type Scale
128
+
129
+ `;
130
+ md += `| Role | Size | Weight | Line Height |
131
+ `;
132
+ md += `|------|------|--------|-------------|
133
+ `;
134
+ for (const size of result.typography.fontSizes.slice(0, 15)) {
135
+ const weight = result.typography.fontWeights.find(
136
+ (w) => w.frequency > 5
137
+ )?.value || "400";
138
+ md += `| ${size.name || "-"} | ${size.value} | ${weight} | - |
139
+ `;
140
+ }
141
+ md += "\n";
142
+ }
143
+ if (result.typography.fontWeights.length > 0) {
144
+ md += `### Font Weights
145
+ `;
146
+ for (const w of result.typography.fontWeights) {
147
+ md += `- **${w.name || w.value}** (${w.value}): Used ${w.frequency}x
148
+ `;
149
+ }
150
+ md += "\n";
151
+ }
152
+ return md;
153
+ }
154
+ // ─── Section 4: Component Stylings ─────────────────────────
155
+ section4_Components(result) {
156
+ let md = `## 4. Component Stylings
157
+
158
+ `;
159
+ md += `### Buttons
160
+ `;
161
+ const radii = result.effects.borderRadii;
162
+ const buttonRadius = radii.find((r) => parseFloat(r.value) >= 4 && parseFloat(r.value) <= 8);
163
+ const pillRadius = radii.find((r) => parseFloat(r.value) >= 9e3);
164
+ if (buttonRadius) {
165
+ md += `- Standard radius: \`${buttonRadius.value}\`
166
+ `;
167
+ }
168
+ if (pillRadius) {
169
+ md += `- Pill radius: \`${pillRadius.value}\`
170
+ `;
171
+ }
172
+ const btnTransition = result.animations.transitions.find(
173
+ (t) => t.property === "all" || t.property === "background"
174
+ );
175
+ if (btnTransition) {
176
+ md += `- Transition: \`${btnTransition.value || `${btnTransition.duration} ${btnTransition.timingFunction}`}\`
177
+ `;
178
+ }
179
+ md += "\n";
180
+ md += `### Cards & Containers
181
+ `;
182
+ const cardRadius = radii.find((r) => parseFloat(r.value) >= 8 && parseFloat(r.value) <= 16);
183
+ if (cardRadius) md += `- Radius: \`${cardRadius.value}\`
184
+ `;
185
+ if (result.effects.shadows.length > 0) {
186
+ md += `- Shadow: \`${result.effects.shadows[0].value}\`
187
+ `;
188
+ }
189
+ md += "\n";
190
+ md += `### Navigation
191
+ `;
192
+ md += `- Font: ${result.typography.fontFamilies[0]?.name || "system"}
193
+ `;
194
+ const navSize = result.typography.fontSizes.find(
195
+ (s) => parseFloat(s.value) >= 13 && parseFloat(s.value) <= 15
196
+ );
197
+ if (navSize) md += `- Size: \`${navSize.value}\`
198
+ `;
199
+ md += "\n";
200
+ return md;
201
+ }
202
+ // ─── Section 5: Layout Principles ──────────────────────────
203
+ section5_Layout(result, arch) {
204
+ let md = `## 5. Layout Principles
205
+
206
+ `;
207
+ md += `### Spacing System
208
+ `;
209
+ const spacingValues = result.spacing.scale.slice(0, 12).map((s) => s.value);
210
+ if (spacingValues.length > 0) {
211
+ md += `- Scale: ${spacingValues.join(", ")}
212
+ `;
213
+ const pxValues = result.spacing.scale.map((s) => s.pxValue).filter((v) => v > 0).sort((a, b) => a - b);
214
+ if (pxValues.length >= 2) {
215
+ const diffs = pxValues.slice(1).map((v, i) => v - pxValues[i]);
216
+ const gcd = diffs.reduce((a, b) => this.gcd(a, b));
217
+ if (gcd >= 4) md += `- Base unit: ${gcd}px
218
+ `;
219
+ }
220
+ }
221
+ md += "\n";
222
+ md += `### Grid & Container
223
+ `;
224
+ if (result.layout.containers.length > 0) {
225
+ const container = result.layout.containers[0];
226
+ if (container.maxWidth) md += `- Max width: \`${container.maxWidth}\`
227
+ `;
228
+ }
229
+ if (result.layout.gridPatterns.length > 0) {
230
+ for (const grid of result.layout.gridPatterns.slice(0, 3)) {
231
+ md += `- Grid: ${grid.columns} columns, gap \`${grid.gap}\`
232
+ `;
233
+ }
234
+ }
235
+ md += "\n";
236
+ if (arch?.layoutType) {
237
+ md += `### Layout Type
238
+ `;
239
+ md += `- Pattern: \`${arch.layoutType}\`
240
+ `;
241
+ md += `- Sections: ${arch.sections?.length || 0}
242
+ `;
243
+ }
244
+ if (result.effects.borderRadii.length > 0) {
245
+ md += `
246
+ ### Border Radius Scale
247
+ `;
248
+ for (const r of result.effects.borderRadii.slice(0, 8)) {
249
+ md += `- ${r.name || "-"}: \`${r.value}\`
250
+ `;
251
+ }
252
+ }
253
+ md += "\n";
254
+ return md;
255
+ }
256
+ // ─── Section 6: Depth & Elevation ──────────────────────────
257
+ section6_Depth(result) {
258
+ let md = `## 6. Depth & Elevation
259
+
260
+ `;
261
+ if (result.effects.shadows.length > 0) {
262
+ md += `| Level | Shadow | Usage |
263
+ `;
264
+ md += `|-------|--------|-------|
265
+ `;
266
+ for (let i = 0; i < Math.min(result.effects.shadows.length, 6); i++) {
267
+ const s = result.effects.shadows[i];
268
+ const value = s.value.length > 60 ? s.value.slice(0, 60) + "..." : s.value;
269
+ md += `| Level ${i + 1} | \`${value}\` | ${s.name || "-"} |
270
+ `;
271
+ }
272
+ md += "\n";
273
+ }
274
+ if (result.effects.opacity.length > 0) {
275
+ md += `### Opacity Values
276
+ `;
277
+ for (const o of result.effects.opacity.slice(0, 5)) {
278
+ md += `- ${o.name || "-"}: \`${o.value}\`
279
+ `;
280
+ }
281
+ md += "\n";
282
+ }
283
+ return md;
284
+ }
285
+ // ─── Section 7: Do's and Don'ts ────────────────────────────
286
+ section7_DosAndDonts(result) {
287
+ let md = `## 7. Do's and Don'ts
288
+
289
+ `;
290
+ const primaryFont = result.typography.fontFamilies[0]?.name || "the primary font";
291
+ const primaryColor = result.colors.palette[0]?.value || "#000";
292
+ const accentColor = result.colors.palette.find(
293
+ (c) => c.usage?.includes("accent") || c.name?.includes("accent") || c.name?.includes("primary")
294
+ )?.value;
295
+ md += `### Do
296
+ `;
297
+ md += `- Use \`${primaryFont}\` as the primary typeface with proper fallbacks
298
+ `;
299
+ if (result.typography.fontFamilies[0]?.fallbacks?.length) {
300
+ md += `- Include fallback stack: ${result.typography.fontFamilies[0].fallbacks.join(", ")}
301
+ `;
302
+ }
303
+ md += `- Follow the spacing scale: ${result.spacing.scale.slice(0, 5).map((s) => s.value).join(", ")}...
304
+ `;
305
+ if (accentColor) {
306
+ md += `- Use accent color (\`${accentColor}\`) for interactive elements and CTAs only
307
+ `;
308
+ }
309
+ if (result.effects.borderRadii.length > 0) {
310
+ md += `- Use consistent border radius: \`${result.effects.borderRadii[0].value}\` for standard elements
311
+ `;
312
+ }
313
+ md += `- Match the transition timing: ${result.animations.transitions[0]?.value || "150ms ease"}
314
+ `;
315
+ md += "\n";
316
+ md += `### Don't
317
+ `;
318
+ md += `- Don't mix font families - stick to the extracted type system
319
+ `;
320
+ md += `- Don't use arbitrary spacing values outside the scale
321
+ `;
322
+ if (accentColor) {
323
+ md += `- Don't use accent color (\`${accentColor}\`) decoratively - reserve for interactive elements
324
+ `;
325
+ }
326
+ md += `- Don't ignore the shadow/elevation system - use the extracted levels
327
+ `;
328
+ md += `- Don't add border-radius values outside the established scale
329
+ `;
330
+ md += "\n";
331
+ return md;
332
+ }
333
+ // ─── Section 8: Responsive Behavior ────────────────────────
334
+ section8_Responsive(result, arch) {
335
+ let md = `## 8. Responsive Behavior
336
+
337
+ `;
338
+ if (result.layout.breakpoints.length > 0) {
339
+ md += `### Breakpoints
340
+
341
+ `;
342
+ md += `| Name | Width |
343
+ `;
344
+ md += `|------|-------|
345
+ `;
346
+ for (const bp of result.layout.breakpoints) {
347
+ md += `| ${bp.name || "-"} | ${bp.minWidth || bp.maxWidth || "-"}px |
348
+ `;
349
+ }
350
+ md += "\n";
351
+ } else {
352
+ md += `### Breakpoints (Standard)
353
+
354
+ `;
355
+ md += `| Name | Width |
356
+ `;
357
+ md += `|------|-------|
358
+ `;
359
+ md += `| Mobile | <640px |
360
+ `;
361
+ md += `| Tablet | 640-1024px |
362
+ `;
363
+ md += `| Desktop | >1024px |
364
+
365
+ `;
366
+ }
367
+ md += `### Collapsing Strategy
368
+ `;
369
+ if (result.layout.gridPatterns.length > 0) {
370
+ const grid = result.layout.gridPatterns[0];
371
+ md += `- Grid: ${grid.columns}-column -> single column on mobile
372
+ `;
373
+ }
374
+ md += `- Navigation: horizontal -> hamburger menu
375
+ `;
376
+ md += `- Section spacing: reduce by ~40% on mobile
377
+ `;
378
+ md += "\n";
379
+ return md;
380
+ }
381
+ // ─── Section 9: Agent Prompt Guide ──────────────────────────
382
+ section9_AgentPrompt(result, siteName) {
383
+ let md = `## 9. Agent Prompt Guide
384
+
385
+ `;
386
+ md += `### Quick Color Reference
387
+ `;
388
+ for (const c of result.colors.palette.slice(0, 10)) {
389
+ md += `- ${c.name}: \`${c.value}\`
390
+ `;
391
+ }
392
+ md += "\n";
393
+ md += `### Quick Typography Reference
394
+ `;
395
+ for (const f of result.typography.fontFamilies) {
396
+ md += `- ${f.category || "Primary"}: \`${f.name}\`${f.fallbacks?.length ? `, fallbacks: ${f.fallbacks.join(", ")}` : ""}
397
+ `;
398
+ }
399
+ md += "\n";
400
+ const bg = result.colors.palette.find((c) => c.usage?.includes("background"))?.value || "#ffffff";
401
+ const text = result.colors.palette.find((c) => c.usage?.includes("text"))?.value || "#000000";
402
+ const accent = result.colors.palette.find(
403
+ (c) => c.usage?.includes("accent") || c.name?.includes("primary") || c.name?.includes("blue")
404
+ )?.value || "#3366ff";
405
+ const font = result.typography.fontFamilies[0]?.name || "system-ui";
406
+ const radius = result.effects.borderRadii[0]?.value || "8px";
407
+ md += `### Example Component Prompts
408
+
409
+ `;
410
+ md += `- "Create a hero section on \`${bg}\` background. Headline using ${font}, color \`${text}\`. `;
411
+ md += `CTA button with \`${accent}\` background, \`${radius}\` radius."
412
+
413
+ `;
414
+ md += `- "Design a card: \`${bg}\` background, border \`1px solid rgba(0,0,0,0.1)\`, `;
415
+ md += `\`${radius}\` radius. Title in ${font} bold, color \`${text}\`."
416
+
417
+ `;
418
+ md += `- "Build navigation: ${font} at 14px for links, color \`${text}\`. `;
419
+ md += `Accent CTA \`${accent}\` right-aligned."
420
+
421
+ `;
422
+ md += `### Iteration Guide
423
+ `;
424
+ md += `1. Always use \`${font}\` with the specified fallback stack
425
+ `;
426
+ md += `2. Follow the spacing scale: ${result.spacing.scale.slice(0, 5).map((s) => s.value).join(", ")}...
427
+ `;
428
+ md += `3. Accent color (\`${accent}\`) is for interactive elements only
429
+ `;
430
+ md += `4. Use the shadow/elevation system for depth
431
+ `;
432
+ md += `5. Match transition timing from the extracted values
433
+ `;
434
+ return md;
435
+ }
436
+ // ─── AI Enhancement ────────────────────────────────────────
437
+ async enhanceWithAI(md, result, sourceUrl) {
438
+ if (!this.client) return md;
439
+ try {
440
+ this.logger.info("Enhancing DESIGN.md with AI narrative...");
441
+ const response = await this.client.messages.create({
442
+ model: this.model,
443
+ max_tokens: 1024,
444
+ messages: [{
445
+ role: "user",
446
+ content: `I have a DESIGN.md for ${sourceUrl}. The Section 1 "Visual Theme & Atmosphere" needs a rich, descriptive opening paragraph (3-4 sentences) that captures the design personality. Here's what I know:
447
+
448
+ Colors: ${result.colors.palette.slice(0, 8).map((c) => `${c.name}:${c.value}`).join(", ")}
449
+ Fonts: ${result.typography.fontFamilies.map((f) => f.name).join(", ")}
450
+ Animations: ${result.animations.keyframes.length} keyframes
451
+ Gradients: ${result.colors.gradients.length}
452
+
453
+ Write ONLY the opening paragraph. No heading, no bullets. Describe the visual feel, the mood, the design philosophy. Be specific about colors and typography choices.`
454
+ }]
455
+ });
456
+ const narrative = response.content[0]?.type === "text" ? response.content[0].text : "";
457
+ if (narrative) {
458
+ md = md.replace(
459
+ /## 1\. Visual Theme & Atmosphere\n\n/,
460
+ `## 1. Visual Theme & Atmosphere
461
+
462
+ ${narrative}
463
+
464
+ `
465
+ );
466
+ }
467
+ } catch (error) {
468
+ this.logger.warn("AI enhancement failed, using raw data only");
469
+ }
470
+ return md;
471
+ }
472
+ // ─── Helpers ───────────────────────────────────────────────
473
+ getSiteName(url) {
474
+ try {
475
+ const hostname = new URL(url).hostname;
476
+ return hostname.replace("www.", "").split(".")[0];
477
+ } catch {
478
+ return "Unknown Site";
479
+ }
480
+ }
481
+ categorizeColor(color) {
482
+ if (color.usage?.includes("background")) return "Background Surfaces";
483
+ if (color.usage?.includes("text")) return "Text & Content";
484
+ if (color.usage?.includes("border")) return "Border & Divider";
485
+ if (color.usage?.includes("accent")) return "Brand & Accent";
486
+ if (color.usage?.includes("shadow")) return "Shadows";
487
+ if (color.name?.includes("gray") || color.name?.includes("grey")) return "Neutrals";
488
+ if (color.name?.includes("blue") || color.name?.includes("red") || color.name?.includes("green")) return "Brand & Accent";
489
+ return "Other";
490
+ }
491
+ gcd(a, b) {
492
+ a = Math.abs(Math.round(a));
493
+ b = Math.abs(Math.round(b));
494
+ while (b) {
495
+ [a, b] = [b, a % b];
496
+ }
497
+ return a;
498
+ }
499
+ };
500
+ export {
501
+ DesignMdGenerator
502
+ };