designlang 1.0.0 → 3.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,128 @@
1
+ // Interaction state extraction — hover, focus, active styles
2
+
3
+ import { chromium } from 'playwright';
4
+
5
+ export async function captureInteractions(url, options = {}) {
6
+ const { width = 1280, height = 800, wait = 0 } = options;
7
+ const browser = await chromium.launch({ headless: true });
8
+ const context = await browser.newContext({ viewport: { width, height } });
9
+ const page = await context.newPage();
10
+
11
+ await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
12
+ if (wait > 0) await page.waitForTimeout(wait);
13
+ await page.evaluate(() => document.fonts.ready);
14
+
15
+ const results = { buttons: [], links: [], inputs: [] };
16
+
17
+ // Extract button states
18
+ const buttons = await page.$$('button, [role="button"], a[class*="btn"]');
19
+ for (const btn of buttons.slice(0, 10)) {
20
+ try {
21
+ const base = await getStyles(page, btn);
22
+ if (!base || base.display === 'none') continue;
23
+
24
+ // Hover
25
+ await btn.hover();
26
+ await page.waitForTimeout(100);
27
+ const hover = await getStyles(page, btn);
28
+
29
+ // Focus
30
+ await btn.focus();
31
+ await page.waitForTimeout(100);
32
+ const focus = await getStyles(page, btn);
33
+
34
+ const diffs = diffStates(base, hover, focus);
35
+ if (diffs.hasChanges) {
36
+ results.buttons.push({ text: base.text, base: base.styles, ...diffs });
37
+ }
38
+ } catch { /* skip */ }
39
+ }
40
+
41
+ // Extract link states
42
+ const links = await page.$$('a:not([role="button"]):not([class*="btn"])');
43
+ for (const link of links.slice(0, 10)) {
44
+ try {
45
+ const base = await getStyles(page, link);
46
+ if (!base || base.display === 'none') continue;
47
+
48
+ await link.hover();
49
+ await page.waitForTimeout(100);
50
+ const hover = await getStyles(page, link);
51
+
52
+ const diffs = diffStates(base, hover, null);
53
+ if (diffs.hasChanges) {
54
+ results.links.push({ text: base.text, base: base.styles, ...diffs });
55
+ break; // One link pattern is enough
56
+ }
57
+ } catch { /* skip */ }
58
+ }
59
+
60
+ // Extract input states
61
+ const inputs = await page.$$('input[type="text"], input[type="email"], input[type="search"], textarea');
62
+ for (const input of inputs.slice(0, 5)) {
63
+ try {
64
+ const base = await getStyles(page, input);
65
+ if (!base || base.display === 'none') continue;
66
+
67
+ await input.focus();
68
+ await page.waitForTimeout(100);
69
+ const focus = await getStyles(page, input);
70
+
71
+ const diffs = diffStates(base, null, focus);
72
+ if (diffs.hasChanges) {
73
+ results.inputs.push({ base: base.styles, ...diffs });
74
+ break; // One input pattern is enough
75
+ }
76
+ } catch { /* skip */ }
77
+ }
78
+
79
+ await browser.close();
80
+
81
+ return results;
82
+ }
83
+
84
+ async function getStyles(page, element) {
85
+ return element.evaluate(el => {
86
+ const cs = getComputedStyle(el);
87
+ if (cs.display === 'none' || cs.visibility === 'hidden') return null;
88
+ return {
89
+ text: el.textContent?.trim().slice(0, 30) || '',
90
+ display: cs.display,
91
+ styles: {
92
+ color: cs.color,
93
+ backgroundColor: cs.backgroundColor,
94
+ borderColor: cs.borderColor,
95
+ boxShadow: cs.boxShadow,
96
+ transform: cs.transform,
97
+ opacity: cs.opacity,
98
+ outline: cs.outline,
99
+ textDecoration: cs.textDecoration,
100
+ scale: cs.scale,
101
+ },
102
+ };
103
+ });
104
+ }
105
+
106
+ function diffStates(base, hover, focus) {
107
+ const result = { hasChanges: false, hover: {}, focus: {} };
108
+
109
+ if (hover) {
110
+ for (const [prop, val] of Object.entries(hover.styles)) {
111
+ if (val !== base.styles[prop] && val !== 'none' && val !== 'auto') {
112
+ result.hover[prop] = { from: base.styles[prop], to: val };
113
+ result.hasChanges = true;
114
+ }
115
+ }
116
+ }
117
+
118
+ if (focus) {
119
+ for (const [prop, val] of Object.entries(focus.styles)) {
120
+ if (val !== base.styles[prop] && val !== 'none' && val !== 'auto') {
121
+ result.focus[prop] = { from: base.styles[prop], to: val };
122
+ result.hasChanges = true;
123
+ }
124
+ }
125
+ }
126
+
127
+ return result;
128
+ }
@@ -0,0 +1,114 @@
1
+ // Layout extraction — grid, flexbox, container patterns, structural design language
2
+
3
+ export function extractLayout(computedStyles) {
4
+ const containers = [];
5
+ const gridPatterns = [];
6
+ const flexPatterns = [];
7
+ const columnCounts = new Map();
8
+
9
+ for (const el of computedStyles) {
10
+ const isGrid = el.display === 'grid' || el.display === 'inline-grid';
11
+ const isFlex = el.display === 'flex' || el.display === 'inline-flex';
12
+
13
+ if (isGrid) {
14
+ gridPatterns.push({
15
+ tag: el.tag,
16
+ classList: el.classList,
17
+ gridTemplateColumns: el.gridTemplateColumns || 'none',
18
+ gridTemplateRows: el.gridTemplateRows || 'none',
19
+ gap: el.gap,
20
+ area: el.area,
21
+ });
22
+
23
+ // Count column patterns
24
+ const cols = el.gridTemplateColumns;
25
+ if (cols && cols !== 'none') {
26
+ const colCount = cols.split(/\s+/).filter(v => v && v !== 'none').length;
27
+ if (colCount > 0) columnCounts.set(colCount, (columnCounts.get(colCount) || 0) + 1);
28
+ }
29
+ }
30
+
31
+ if (isFlex) {
32
+ flexPatterns.push({
33
+ tag: el.tag,
34
+ classList: el.classList,
35
+ flexDirection: el.flexDirection || 'row',
36
+ flexWrap: el.flexWrap || 'nowrap',
37
+ justifyContent: el.justifyContent || 'normal',
38
+ alignItems: el.alignItems || 'normal',
39
+ gap: el.gap,
40
+ area: el.area,
41
+ });
42
+ }
43
+
44
+ // Detect containers (large centered elements)
45
+ if (el.area > 100000 && el.maxWidth && el.maxWidth !== 'none') {
46
+ containers.push({
47
+ tag: el.tag,
48
+ classList: el.classList,
49
+ maxWidth: el.maxWidth,
50
+ paddingLeft: el.paddingLeft,
51
+ paddingRight: el.paddingRight,
52
+ });
53
+ }
54
+ }
55
+
56
+ // Summarize flex direction usage
57
+ const flexDirections = {};
58
+ for (const f of flexPatterns) {
59
+ const key = `${f.flexDirection}/${f.flexWrap}`;
60
+ flexDirections[key] = (flexDirections[key] || 0) + 1;
61
+ }
62
+
63
+ // Summarize justify/align patterns
64
+ const justifyPatterns = {};
65
+ const alignPatterns = {};
66
+ for (const f of flexPatterns) {
67
+ justifyPatterns[f.justifyContent] = (justifyPatterns[f.justifyContent] || 0) + 1;
68
+ alignPatterns[f.alignItems] = (alignPatterns[f.alignItems] || 0) + 1;
69
+ }
70
+
71
+ // Grid column summary
72
+ const gridColumns = [...columnCounts.entries()]
73
+ .sort((a, b) => b[1] - a[1])
74
+ .map(([cols, count]) => ({ columns: cols, count }));
75
+
76
+ // Container widths
77
+ const containerWidths = [];
78
+ const widthSet = new Set();
79
+ for (const c of containers) {
80
+ if (!widthSet.has(c.maxWidth)) {
81
+ widthSet.add(c.maxWidth);
82
+ containerWidths.push({ maxWidth: c.maxWidth, padding: c.paddingLeft });
83
+ }
84
+ }
85
+
86
+ // Gap values
87
+ const gaps = new Set();
88
+ for (const el of [...gridPatterns, ...flexPatterns]) {
89
+ if (el.gap && el.gap !== 'normal' && el.gap !== '0px') {
90
+ gaps.add(el.gap);
91
+ }
92
+ }
93
+
94
+ return {
95
+ gridCount: gridPatterns.length,
96
+ flexCount: flexPatterns.length,
97
+ gridColumns,
98
+ flexDirections,
99
+ justifyPatterns,
100
+ alignPatterns,
101
+ containerWidths,
102
+ gaps: [...gaps].sort(),
103
+ // Sample grid patterns (top 5 by area)
104
+ topGrids: gridPatterns
105
+ .sort((a, b) => b.area - a.area)
106
+ .slice(0, 5)
107
+ .map(g => ({ columns: g.gridTemplateColumns, rows: g.gridTemplateRows, gap: g.gap })),
108
+ // Sample flex patterns (top 5 by area)
109
+ topFlex: flexPatterns
110
+ .sort((a, b) => b.area - a.area)
111
+ .slice(0, 5)
112
+ .map(f => ({ direction: f.flexDirection, wrap: f.flexWrap, justify: f.justifyContent, align: f.alignItems, gap: f.gap })),
113
+ };
114
+ }
@@ -0,0 +1,132 @@
1
+ // Responsive multi-breakpoint capture — extract at multiple viewports and diff
2
+
3
+ import { chromium } from 'playwright';
4
+
5
+ const VIEWPORTS = [
6
+ { name: 'mobile', width: 375, height: 812 },
7
+ { name: 'tablet', width: 768, height: 1024 },
8
+ { name: 'desktop', width: 1280, height: 800 },
9
+ { name: 'wide', width: 1920, height: 1080 },
10
+ ];
11
+
12
+ export async function captureResponsive(url, options = {}) {
13
+ const { wait = 0 } = options;
14
+ const browser = await chromium.launch({ headless: true });
15
+
16
+ const snapshots = [];
17
+
18
+ for (const vp of VIEWPORTS) {
19
+ const context = await browser.newContext({ viewport: { width: vp.width, height: vp.height } });
20
+ const page = await context.newPage();
21
+
22
+ try {
23
+ await page.goto(url, { waitUntil: 'networkidle', timeout: 20000 });
24
+ if (wait > 0) await page.waitForTimeout(wait);
25
+ await page.evaluate(() => document.fonts.ready);
26
+
27
+ const data = await page.evaluate(() => {
28
+ const body = document.body;
29
+ const cs = getComputedStyle(body);
30
+ const html = document.documentElement;
31
+ const htmlCs = getComputedStyle(html);
32
+
33
+ // Collect key metrics at this viewport
34
+ const headings = {};
35
+ for (let i = 1; i <= 3; i++) {
36
+ const h = document.querySelector(`h1, h2, h3`.split(',')[i - 1]);
37
+ if (h) {
38
+ const hcs = getComputedStyle(h);
39
+ headings[`h${i}`] = { fontSize: hcs.fontSize, lineHeight: hcs.lineHeight };
40
+ }
41
+ }
42
+
43
+ // Body font size
44
+ const bodyFontSize = cs.fontSize;
45
+
46
+ // Navigation visibility
47
+ const nav = document.querySelector('nav, [role="navigation"]');
48
+ const navVisible = nav ? getComputedStyle(nav).display !== 'none' : false;
49
+ const navHeight = nav ? nav.getBoundingClientRect().height : 0;
50
+
51
+ // Count visible grid/flex containers
52
+ let gridCount = 0, flexCount = 0;
53
+ const allEls = document.querySelectorAll('*');
54
+ const sample = Array.from(allEls).slice(0, 2000);
55
+ for (const el of sample) {
56
+ const d = getComputedStyle(el).display;
57
+ if (d === 'grid' || d === 'inline-grid') gridCount++;
58
+ if (d === 'flex' || d === 'inline-flex') flexCount++;
59
+ }
60
+
61
+ // Count columns in grids
62
+ const grids = document.querySelectorAll('*');
63
+ let maxColumns = 0;
64
+ for (const el of Array.from(grids).slice(0, 1000)) {
65
+ const gcs = getComputedStyle(el);
66
+ if (gcs.display === 'grid' && gcs.gridTemplateColumns && gcs.gridTemplateColumns !== 'none') {
67
+ const cols = gcs.gridTemplateColumns.split(/\s+/).length;
68
+ if (cols > maxColumns) maxColumns = cols;
69
+ }
70
+ }
71
+
72
+ // Check for hamburger menu
73
+ const hamburger = document.querySelector('[class*="hamburger"], [class*="menu-toggle"], [aria-label*="menu"], button[class*="mobile"]');
74
+ const hasHamburger = hamburger ? getComputedStyle(hamburger).display !== 'none' : false;
75
+
76
+ return {
77
+ bodyFontSize,
78
+ headings,
79
+ navVisible,
80
+ navHeight,
81
+ gridCount,
82
+ flexCount,
83
+ maxColumns,
84
+ hasHamburger,
85
+ scrollHeight: document.documentElement.scrollHeight,
86
+ };
87
+ });
88
+
89
+ snapshots.push({ ...vp, ...data });
90
+ } catch {
91
+ snapshots.push({ ...vp, error: true });
92
+ }
93
+
94
+ await context.close();
95
+ }
96
+
97
+ await browser.close();
98
+
99
+ // Build responsive map — what changes between breakpoints
100
+ const changes = [];
101
+ for (let i = 1; i < snapshots.length; i++) {
102
+ const prev = snapshots[i - 1];
103
+ const curr = snapshots[i];
104
+ if (prev.error || curr.error) continue;
105
+
106
+ const diffs = [];
107
+ if (prev.bodyFontSize !== curr.bodyFontSize) {
108
+ diffs.push({ property: 'Body font size', from: prev.bodyFontSize, to: curr.bodyFontSize });
109
+ }
110
+ if (prev.headings?.h1?.fontSize !== curr.headings?.h1?.fontSize) {
111
+ diffs.push({ property: 'H1 size', from: prev.headings?.h1?.fontSize || 'n/a', to: curr.headings?.h1?.fontSize || 'n/a' });
112
+ }
113
+ if (prev.navVisible !== curr.navVisible) {
114
+ diffs.push({ property: 'Nav visibility', from: prev.navVisible ? 'visible' : 'hidden', to: curr.navVisible ? 'visible' : 'hidden' });
115
+ }
116
+ if (prev.hasHamburger !== curr.hasHamburger) {
117
+ diffs.push({ property: 'Hamburger menu', from: prev.hasHamburger ? 'shown' : 'hidden', to: curr.hasHamburger ? 'shown' : 'hidden' });
118
+ }
119
+ if (prev.maxColumns !== curr.maxColumns) {
120
+ diffs.push({ property: 'Max grid columns', from: String(prev.maxColumns), to: String(curr.maxColumns) });
121
+ }
122
+ if (Math.abs(prev.scrollHeight - curr.scrollHeight) > 200) {
123
+ diffs.push({ property: 'Page height', from: `${prev.scrollHeight}px`, to: `${curr.scrollHeight}px` });
124
+ }
125
+
126
+ if (diffs.length > 0) {
127
+ changes.push({ from: prev.name, to: curr.name, breakpoint: `${prev.width}px → ${curr.width}px`, diffs });
128
+ }
129
+ }
130
+
131
+ return { viewports: snapshots.filter(s => !s.error), changes };
132
+ }
@@ -0,0 +1,83 @@
1
+ // Figma Variables JSON format (compatible with Figma Variables import)
2
+ export function formatFigma(design) {
3
+ const variables = [];
4
+
5
+ // Colors
6
+ if (design.colors.primary) {
7
+ variables.push(colorVar('color/primary', design.colors.primary.hex));
8
+ }
9
+ if (design.colors.secondary) {
10
+ variables.push(colorVar('color/secondary', design.colors.secondary.hex));
11
+ }
12
+ if (design.colors.accent) {
13
+ variables.push(colorVar('color/accent', design.colors.accent.hex));
14
+ }
15
+ for (let i = 0; i < design.colors.neutrals.length && i < 10; i++) {
16
+ variables.push(colorVar(`color/neutral/${i * 100 || 50}`, design.colors.neutrals[i].hex));
17
+ }
18
+ for (let i = 0; i < design.colors.backgrounds.length; i++) {
19
+ variables.push(colorVar(`color/background/${i === 0 ? 'default' : i}`, design.colors.backgrounds[i]));
20
+ }
21
+ for (let i = 0; i < design.colors.text.length && i < 5; i++) {
22
+ variables.push(colorVar(`color/text/${i === 0 ? 'default' : i}`, design.colors.text[i]));
23
+ }
24
+
25
+ // Spacing
26
+ for (const v of design.spacing.scale.slice(0, 20)) {
27
+ variables.push({ name: `spacing/${v}`, type: 'FLOAT', value: v, scopes: ['GAP', 'ALL_SCOPES'] });
28
+ }
29
+
30
+ // Border radius
31
+ for (const r of design.borders.radii) {
32
+ variables.push({ name: `radius/${r.label}`, type: 'FLOAT', value: r.value, scopes: ['CORNER_RADIUS'] });
33
+ }
34
+
35
+ // Font sizes
36
+ for (const s of design.typography.scale.slice(0, 12)) {
37
+ variables.push({ name: `fontSize/${s.size}`, type: 'FLOAT', value: s.size, scopes: ['FONT_SIZE'] });
38
+ }
39
+
40
+ const collection = {
41
+ name: `Design Language — ${design.meta.title || 'Extracted'}`,
42
+ modes: [{ name: 'Default', variables }],
43
+ };
44
+
45
+ // Add dark mode if available
46
+ if (design.darkMode) {
47
+ const darkVars = [];
48
+ const dc = design.darkMode.colors;
49
+ if (dc.primary) darkVars.push(colorVar('color/primary', dc.primary.hex));
50
+ if (dc.secondary) darkVars.push(colorVar('color/secondary', dc.secondary.hex));
51
+ for (let i = 0; i < dc.neutrals.length && i < 10; i++) {
52
+ darkVars.push(colorVar(`color/neutral/${i * 100 || 50}`, dc.neutrals[i].hex));
53
+ }
54
+ for (let i = 0; i < dc.backgrounds.length; i++) {
55
+ darkVars.push(colorVar(`color/background/${i === 0 ? 'default' : i}`, dc.backgrounds[i]));
56
+ }
57
+ for (let i = 0; i < dc.text.length && i < 5; i++) {
58
+ darkVars.push(colorVar(`color/text/${i === 0 ? 'default' : i}`, dc.text[i]));
59
+ }
60
+ collection.modes.push({ name: 'Dark', variables: darkVars });
61
+ }
62
+
63
+ return JSON.stringify(collection, null, 2);
64
+ }
65
+
66
+ function colorVar(name, hex) {
67
+ const rgb = hexToRgb(hex);
68
+ return {
69
+ name,
70
+ type: 'COLOR',
71
+ value: { r: rgb.r / 255, g: rgb.g / 255, b: rgb.b / 255, a: 1 },
72
+ scopes: ['ALL_SCOPES'],
73
+ };
74
+ }
75
+
76
+ function hexToRgb(hex) {
77
+ const h = hex.replace('#', '');
78
+ return {
79
+ r: parseInt(h.slice(0, 2), 16),
80
+ g: parseInt(h.slice(2, 4), 16),
81
+ b: parseInt(h.slice(4, 6), 16),
82
+ };
83
+ }
@@ -7,7 +7,7 @@ export function formatMarkdown(design) {
7
7
  lines.push(`# Design Language: ${meta.title || 'Unknown Site'}`);
8
8
  lines.push('');
9
9
  lines.push(`> Extracted from \`${meta.url}\` on ${new Date(meta.timestamp).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}`);
10
- lines.push(`> ${meta.elementCount} elements analyzed`);
10
+ lines.push(`> ${meta.elementCount} elements analyzed${meta.pagesAnalyzed > 1 ? ` across ${meta.pagesAnalyzed} pages` : ''}`);
11
11
  lines.push('');
12
12
  lines.push('This document describes the complete design language of the website. It is structured for AI/LLM consumption — use it to faithfully recreate the visual design in any framework.');
13
13
  lines.push('');
@@ -259,6 +259,188 @@ export function formatMarkdown(design) {
259
259
  }
260
260
  }
261
261
 
262
+ // ── Layout ──
263
+ if (design.layout) {
264
+ const l = design.layout;
265
+ lines.push('## Layout System');
266
+ lines.push('');
267
+ lines.push(`**${l.gridCount} grid containers** and **${l.flexCount} flex containers** detected.`);
268
+ lines.push('');
269
+
270
+ if (l.containerWidths.length > 0) {
271
+ lines.push('### Container Widths');
272
+ lines.push('');
273
+ lines.push('| Max Width | Padding |');
274
+ lines.push('|-----------|---------|');
275
+ for (const c of l.containerWidths) {
276
+ lines.push(`| ${c.maxWidth} | ${c.padding} |`);
277
+ }
278
+ lines.push('');
279
+ }
280
+
281
+ if (l.gridColumns.length > 0) {
282
+ lines.push('### Grid Column Patterns');
283
+ lines.push('');
284
+ lines.push('| Columns | Usage Count |');
285
+ lines.push('|---------|-------------|');
286
+ for (const g of l.gridColumns) {
287
+ lines.push(`| ${g.columns}-column | ${g.count}x |`);
288
+ }
289
+ lines.push('');
290
+ }
291
+
292
+ if (l.topGrids.length > 0) {
293
+ lines.push('### Grid Templates');
294
+ lines.push('');
295
+ lines.push('```css');
296
+ for (const g of l.topGrids) {
297
+ lines.push(`grid-template-columns: ${g.columns};`);
298
+ if (g.gap !== 'normal' && g.gap !== '0px') lines.push(`gap: ${g.gap};`);
299
+ }
300
+ lines.push('```');
301
+ lines.push('');
302
+ }
303
+
304
+ if (Object.keys(l.flexDirections).length > 0) {
305
+ lines.push('### Flex Patterns');
306
+ lines.push('');
307
+ lines.push('| Direction/Wrap | Count |');
308
+ lines.push('|----------------|-------|');
309
+ for (const [pattern, count] of Object.entries(l.flexDirections)) {
310
+ lines.push(`| ${pattern} | ${count}x |`);
311
+ }
312
+ lines.push('');
313
+ }
314
+
315
+ if (l.gaps.length > 0) {
316
+ lines.push(`**Gap values:** ${l.gaps.map(g => `\`${g}\``).join(', ')}`);
317
+ lines.push('');
318
+ }
319
+ }
320
+
321
+ // ── Responsive ──
322
+ if (design.responsive) {
323
+ const r = design.responsive;
324
+ lines.push('## Responsive Design');
325
+ lines.push('');
326
+
327
+ if (r.viewports.length > 0) {
328
+ lines.push('### Viewport Snapshots');
329
+ lines.push('');
330
+ lines.push('| Viewport | Body Font | Nav Visible | Max Columns | Hamburger | Page Height |');
331
+ lines.push('|----------|-----------|-------------|-------------|-----------|-------------|');
332
+ for (const vp of r.viewports) {
333
+ lines.push(`| ${vp.name} (${vp.width}px) | ${vp.bodyFontSize} | ${vp.navVisible ? 'Yes' : 'No'} | ${vp.maxColumns} | ${vp.hasHamburger ? 'Yes' : 'No'} | ${vp.scrollHeight}px |`);
334
+ }
335
+ lines.push('');
336
+ }
337
+
338
+ if (r.changes.length > 0) {
339
+ lines.push('### Breakpoint Changes');
340
+ lines.push('');
341
+ for (const change of r.changes) {
342
+ lines.push(`**${change.breakpoint}** (${change.from} → ${change.to}):`);
343
+ for (const d of change.diffs) {
344
+ lines.push(`- ${d.property}: \`${d.from}\` → \`${d.to}\``);
345
+ }
346
+ lines.push('');
347
+ }
348
+ }
349
+ }
350
+
351
+ // ── Interaction States ──
352
+ if (design.interactions) {
353
+ const hasContent = design.interactions.buttons.length > 0 || design.interactions.links.length > 0 || design.interactions.inputs.length > 0;
354
+ if (hasContent) {
355
+ lines.push('## Interaction States');
356
+ lines.push('');
357
+
358
+ if (design.interactions.buttons.length > 0) {
359
+ lines.push('### Button States');
360
+ lines.push('');
361
+ for (const btn of design.interactions.buttons.slice(0, 3)) {
362
+ lines.push(`**"${btn.text}"**`);
363
+ if (Object.keys(btn.hover).length > 0) {
364
+ lines.push('```css');
365
+ lines.push('/* Hover */');
366
+ for (const [prop, val] of Object.entries(btn.hover)) {
367
+ lines.push(`${prop.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${val.from} → ${val.to};`);
368
+ }
369
+ lines.push('```');
370
+ }
371
+ if (Object.keys(btn.focus).length > 0) {
372
+ lines.push('```css');
373
+ lines.push('/* Focus */');
374
+ for (const [prop, val] of Object.entries(btn.focus)) {
375
+ lines.push(`${prop.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${val.from} → ${val.to};`);
376
+ }
377
+ lines.push('```');
378
+ }
379
+ lines.push('');
380
+ }
381
+ }
382
+
383
+ if (design.interactions.links.length > 0) {
384
+ lines.push('### Link Hover');
385
+ lines.push('');
386
+ const link = design.interactions.links[0];
387
+ lines.push('```css');
388
+ for (const [prop, val] of Object.entries(link.hover)) {
389
+ lines.push(`${prop.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${val.from} → ${val.to};`);
390
+ }
391
+ lines.push('```');
392
+ lines.push('');
393
+ }
394
+
395
+ if (design.interactions.inputs.length > 0) {
396
+ lines.push('### Input Focus');
397
+ lines.push('');
398
+ const input = design.interactions.inputs[0];
399
+ lines.push('```css');
400
+ for (const [prop, val] of Object.entries(input.focus)) {
401
+ lines.push(`${prop.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${val.from} → ${val.to};`);
402
+ }
403
+ lines.push('```');
404
+ lines.push('');
405
+ }
406
+ }
407
+ }
408
+
409
+ // ── Accessibility ──
410
+ if (design.accessibility) {
411
+ const a = design.accessibility;
412
+ lines.push('## Accessibility (WCAG 2.1)');
413
+ lines.push('');
414
+ lines.push(`**Overall Score: ${a.score}%** — ${a.passCount} passing, ${a.failCount} failing color pairs`);
415
+ lines.push('');
416
+
417
+ if (a.pairs.length > 0) {
418
+ const failures = a.pairs.filter(p => p.level === 'FAIL');
419
+ if (failures.length > 0) {
420
+ lines.push('### Failing Color Pairs');
421
+ lines.push('');
422
+ lines.push('| Foreground | Background | Ratio | Level | Used On |');
423
+ lines.push('|------------|------------|-------|-------|---------|');
424
+ for (const p of failures.slice(0, 15)) {
425
+ lines.push(`| \`${p.foreground}\` | \`${p.background}\` | ${p.ratio}:1 | ${p.level} | ${p.elements.join(', ')} (${p.count}x) |`);
426
+ }
427
+ lines.push('');
428
+ }
429
+
430
+ const passes = a.pairs.filter(p => p.level !== 'FAIL');
431
+ if (passes.length > 0) {
432
+ lines.push('### Passing Color Pairs');
433
+ lines.push('');
434
+ lines.push('| Foreground | Background | Ratio | Level |');
435
+ lines.push('|------------|------------|-------|-------|');
436
+ for (const p of passes.slice(0, 10)) {
437
+ lines.push(`| \`${p.foreground}\` | \`${p.background}\` | ${p.ratio}:1 | ${p.level} |`);
438
+ }
439
+ lines.push('');
440
+ }
441
+ }
442
+ }
443
+
262
444
  // ── Dark Mode ──
263
445
  if (design.darkMode) {
264
446
  lines.push('## Dark Mode');