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.
- package/README.md +108 -83
- package/bin/design-extract.js +247 -10
- package/package.json +1 -1
- package/skills/extract-design/SKILL.md +52 -25
- package/src/crawler.js +132 -20
- package/src/diff.js +146 -0
- package/src/extractors/accessibility.js +95 -0
- package/src/extractors/interactions.js +128 -0
- package/src/extractors/layout.js +114 -0
- package/src/extractors/responsive.js +132 -0
- package/src/formatters/figma.js +83 -0
- package/src/formatters/markdown.js +183 -1
- package/src/formatters/preview.js +237 -0
- package/src/formatters/theme.js +128 -0
- package/src/history.js +103 -0
- package/src/index.js +15 -0
- package/src/multibrand.js +151 -0
- package/src/sync.js +69 -0
|
@@ -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');
|