designlang 4.0.0 → 5.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 +56 -2
- package/bin/design-extract.js +76 -1
- package/package.json +1 -1
- package/src/apply.js +65 -0
- package/src/crawler.js +91 -8
- package/src/darkdiff.js +65 -0
- package/src/extractors/animations.js +40 -4
- package/src/extractors/components.js +23 -0
- package/src/extractors/fonts.js +82 -0
- package/src/extractors/gradients.js +80 -0
- package/src/extractors/icons.js +80 -0
- package/src/extractors/images.js +76 -0
- package/src/extractors/interactions.js +3 -2
- package/src/extractors/responsive.js +3 -2
- package/src/extractors/zindex.js +65 -0
- package/src/formatters/markdown.js +98 -0
- package/src/index.js +13 -1
- package/website/.claude/launch.json +11 -0
- package/website/AGENTS.md +5 -0
- package/website/CLAUDE.md +1 -0
- package/website/README.md +36 -0
- package/website/app/api/extract/route.js +85 -0
- package/website/app/components/Extractor.js +184 -0
- package/website/app/favicon.ico +0 -0
- package/website/app/globals.css +723 -0
- package/website/app/layout.js +19 -0
- package/website/app/page.js +175 -0
- package/website/jsconfig.json +7 -0
- package/website/next.config.mjs +15 -0
- package/website/package-lock.json +1268 -0
- package/website/package.json +18 -0
- package/website/public/file.svg +1 -0
- package/website/public/globe.svg +1 -0
- package/website/public/next.svg +1 -0
- package/website/public/vercel.svg +1 -0
- package/website/public/window.svg +1 -0
- package/designlang.png +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export function extractFonts({ fontFaces = [], googleFontsLinks = [], documentFonts = [] }) {
|
|
2
|
+
const fontMap = new Map();
|
|
3
|
+
|
|
4
|
+
// Parse Google Fonts URLs into a lookup: family -> weights
|
|
5
|
+
const googleFamilies = new Map();
|
|
6
|
+
for (const url of googleFontsLinks) {
|
|
7
|
+
const params = new URL(url).searchParams;
|
|
8
|
+
for (const val of (params.getAll('family'))) {
|
|
9
|
+
const [name, spec] = val.split(':');
|
|
10
|
+
const family = name.replace(/\+/g, ' ');
|
|
11
|
+
const weights = spec?.match(/\d{3}/g) || ['400'];
|
|
12
|
+
googleFamilies.set(family, [...new Set([...(googleFamilies.get(family) || []), ...weights])]);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const getSource = (family, src) => {
|
|
17
|
+
if (googleFamilies.has(family)) return 'google-fonts';
|
|
18
|
+
if (src && /url\(/.test(src)) return /fonts\.(googleapis|gstatic|cdnfonts|bunny)/.test(src) ? 'cdn' : 'self-hosted';
|
|
19
|
+
return 'system';
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const getOrCreate = (family) => {
|
|
23
|
+
if (!fontMap.has(family)) {
|
|
24
|
+
fontMap.set(family, { family, source: 'system', weights: new Set(), styles: new Set(), urls: [], fontFaceCSS: '' });
|
|
25
|
+
}
|
|
26
|
+
return fontMap.get(family);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Process @font-face rules
|
|
30
|
+
for (const ff of fontFaces) {
|
|
31
|
+
const family = ff.family?.replace(/["']/g, '');
|
|
32
|
+
if (!family) continue;
|
|
33
|
+
const entry = getOrCreate(family);
|
|
34
|
+
entry.source = getSource(family, ff.src);
|
|
35
|
+
if (ff.weight) entry.weights.add(String(ff.weight));
|
|
36
|
+
if (ff.style) entry.styles.add(ff.style);
|
|
37
|
+
if (ff.src) entry.urls.push(ff.src);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Process document.fonts entries
|
|
41
|
+
for (const df of documentFonts) {
|
|
42
|
+
const family = df.family?.replace(/["']/g, '');
|
|
43
|
+
if (!family) continue;
|
|
44
|
+
const entry = getOrCreate(family);
|
|
45
|
+
if (entry.source === 'system') entry.source = getSource(family, '');
|
|
46
|
+
if (df.weight) entry.weights.add(String(df.weight));
|
|
47
|
+
if (df.style) entry.styles.add(df.style);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Add Google Fonts families not yet seen
|
|
51
|
+
for (const [family, weights] of googleFamilies) {
|
|
52
|
+
const entry = getOrCreate(family);
|
|
53
|
+
entry.source = 'google-fonts';
|
|
54
|
+
for (const w of weights) entry.weights.add(w);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Build output
|
|
58
|
+
const fonts = [];
|
|
59
|
+
const systemFonts = [];
|
|
60
|
+
|
|
61
|
+
for (const entry of fontMap.values()) {
|
|
62
|
+
const weights = [...entry.weights].sort();
|
|
63
|
+
const styles = [...entry.styles];
|
|
64
|
+
if (!weights.length) weights.push('400');
|
|
65
|
+
if (!styles.length) styles.push('normal');
|
|
66
|
+
|
|
67
|
+
const fontFaceCSS = entry.source === 'self-hosted'
|
|
68
|
+
? entry.urls.map((src, i) =>
|
|
69
|
+
`@font-face {\n font-family: '${entry.family}';\n font-weight: ${weights[i] || weights[0]};\n font-style: ${styles[i] || styles[0]};\n src: ${src};\n}`
|
|
70
|
+
).join('\n\n')
|
|
71
|
+
: '';
|
|
72
|
+
|
|
73
|
+
if (entry.source === 'system') { systemFonts.push(entry.family); continue; }
|
|
74
|
+
fonts.push({ family: entry.family, source: entry.source, weights, styles, urls: entry.urls, fontFaceCSS });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const googleFontsUrl = googleFontsLinks[0] || (googleFamilies.size
|
|
78
|
+
? `https://fonts.googleapis.com/css2?${[...googleFamilies].map(([f, w]) => `family=${f.replace(/ /g, '+')}:wght@${w.join(';')}`).join('&')}&display=swap`
|
|
79
|
+
: '');
|
|
80
|
+
|
|
81
|
+
return { fonts, googleFontsUrl, systemFonts };
|
|
82
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export function extractGradients(styles) {
|
|
2
|
+
const seen = new Set();
|
|
3
|
+
const gradients = [];
|
|
4
|
+
|
|
5
|
+
for (const el of styles) {
|
|
6
|
+
const bg = el.backgroundImage;
|
|
7
|
+
if (!bg || !bg.includes('gradient')) continue;
|
|
8
|
+
const rawGradients = splitGradients(bg);
|
|
9
|
+
for (const raw of rawGradients) {
|
|
10
|
+
if (seen.has(raw)) continue;
|
|
11
|
+
seen.add(raw);
|
|
12
|
+
gradients.push(parseGradient(raw));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return { gradients, count: gradients.length };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function splitGradients(value) {
|
|
20
|
+
// Split comma-separated gradient layers, respecting nested parens
|
|
21
|
+
const results = [];
|
|
22
|
+
let depth = 0, start = 0;
|
|
23
|
+
for (let i = 0; i < value.length; i++) {
|
|
24
|
+
if (value[i] === '(') depth++;
|
|
25
|
+
else if (value[i] === ')') depth--;
|
|
26
|
+
else if (value[i] === ',' && depth === 0) {
|
|
27
|
+
const chunk = value.slice(start, i).trim();
|
|
28
|
+
if (chunk.includes('gradient')) results.push(chunk);
|
|
29
|
+
start = i + 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const last = value.slice(start).trim();
|
|
33
|
+
if (last.includes('gradient')) results.push(last);
|
|
34
|
+
return results;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseGradient(raw) {
|
|
38
|
+
const typeMatch = raw.match(/^(repeating-)?(linear|radial|conic)-gradient/);
|
|
39
|
+
const type = typeMatch ? (typeMatch[1] || '') + typeMatch[2] : 'linear';
|
|
40
|
+
|
|
41
|
+
// Extract content inside outermost parens
|
|
42
|
+
const inner = raw.slice(raw.indexOf('(') + 1, raw.lastIndexOf(')'));
|
|
43
|
+
|
|
44
|
+
// Split top-level arguments by comma (respecting nested parens)
|
|
45
|
+
const args = [];
|
|
46
|
+
let depth = 0, start = 0;
|
|
47
|
+
for (let i = 0; i < inner.length; i++) {
|
|
48
|
+
if (inner[i] === '(') depth++;
|
|
49
|
+
else if (inner[i] === ')') depth--;
|
|
50
|
+
else if (inner[i] === ',' && depth === 0) {
|
|
51
|
+
args.push(inner.slice(start, i).trim());
|
|
52
|
+
start = i + 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
args.push(inner.slice(start).trim());
|
|
56
|
+
|
|
57
|
+
// First arg is direction/angle if it doesn't look like a color
|
|
58
|
+
let direction = null;
|
|
59
|
+
let stopArgs = args;
|
|
60
|
+
const first = args[0] || '';
|
|
61
|
+
if (/^(to |from |\d+deg|at )/.test(first) || /^(circle|ellipse)/.test(first)) {
|
|
62
|
+
direction = first;
|
|
63
|
+
stopArgs = args.slice(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const stops = stopArgs.map(s => {
|
|
67
|
+
const posMatch = s.match(/([\d.]+%?)$/);
|
|
68
|
+
const position = posMatch ? posMatch[1] : null;
|
|
69
|
+
const color = position ? s.slice(0, posMatch.index).trim() : s.trim();
|
|
70
|
+
return { color, position };
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const colorCount = stops.length;
|
|
74
|
+
let classification = 'subtle';
|
|
75
|
+
if (colorCount > 4) classification = 'complex';
|
|
76
|
+
else if (colorCount > 2) classification = 'bold';
|
|
77
|
+
else if (colorCount === 2) classification = 'brand';
|
|
78
|
+
|
|
79
|
+
return { raw, type, direction, stops, classification };
|
|
80
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export function extractIcons(iconData) {
|
|
2
|
+
if (!iconData || !iconData.length) {
|
|
3
|
+
return { icons: [], sizeDistribution: { xs: 0, sm: 0, md: 0, lg: 0, xl: 0 }, dominantStyle: 'none', colorPalette: [], count: 0 };
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function classifySize(w, h) {
|
|
7
|
+
const s = Math.max(w || 0, h || 0);
|
|
8
|
+
if (s < 16) return 'xs';
|
|
9
|
+
if (s < 20) return 'sm';
|
|
10
|
+
if (s < 28) return 'md';
|
|
11
|
+
if (s < 40) return 'lg';
|
|
12
|
+
return 'xl';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function cleanSvg(svg) {
|
|
16
|
+
return svg.replace(/\s*(data-[a-z-]*|class|id)="[^"]*"/g, '').replace(/\s+/g, ' ').trim();
|
|
17
|
+
}
|
|
18
|
+
function simplify(svg) { return cleanSvg(svg); }
|
|
19
|
+
|
|
20
|
+
function detectStyle(svg) {
|
|
21
|
+
const hasStroke = /stroke="(?!none)[^"]+"|stroke-width="[^0"][^"]*"/.test(svg);
|
|
22
|
+
const hasFill = /fill="(?!none|transparent)[^"]+"|<(rect|circle|path)[^>]*(?!fill="none")/.test(svg);
|
|
23
|
+
const fillNone = /fill="none"/.test(svg);
|
|
24
|
+
if (hasStroke && (fillNone || !hasFill)) return 'outlined';
|
|
25
|
+
if (hasStroke && hasFill) return 'duo-tone';
|
|
26
|
+
return 'filled';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function extractColors(svg) {
|
|
30
|
+
const colors = new Set();
|
|
31
|
+
for (const m of svg.matchAll(/(?:fill|stroke)="([^"]+)"/g)) {
|
|
32
|
+
if (m[1] !== 'none' && m[1] !== 'transparent') colors.add(m[1]);
|
|
33
|
+
}
|
|
34
|
+
return [...colors];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Deduplicate
|
|
38
|
+
const seen = new Map();
|
|
39
|
+
for (const icon of iconData) {
|
|
40
|
+
const key = simplify(icon.svg);
|
|
41
|
+
if (!seen.has(key)) seen.set(key, icon);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const sizeDistribution = { xs: 0, sm: 0, md: 0, lg: 0, xl: 0 };
|
|
45
|
+
const styleCounts = { outlined: 0, filled: 0, 'duo-tone': 0 };
|
|
46
|
+
const allColors = new Set();
|
|
47
|
+
|
|
48
|
+
const icons = [];
|
|
49
|
+
for (const icon of seen.values()) {
|
|
50
|
+
const cleaned = cleanSvg(icon.svg);
|
|
51
|
+
const sc = classifySize(icon.width, icon.height);
|
|
52
|
+
const style = detectStyle(icon.svg);
|
|
53
|
+
const colors = extractColors(icon.svg);
|
|
54
|
+
if (icon.fill && icon.fill !== 'none') colors.push(icon.fill);
|
|
55
|
+
if (icon.stroke && icon.stroke !== 'none') colors.push(icon.stroke);
|
|
56
|
+
const uniqueColors = [...new Set(colors)];
|
|
57
|
+
|
|
58
|
+
sizeDistribution[sc]++;
|
|
59
|
+
styleCounts[style]++;
|
|
60
|
+
uniqueColors.forEach(c => allColors.add(c));
|
|
61
|
+
|
|
62
|
+
icons.push({
|
|
63
|
+
svg: cleaned,
|
|
64
|
+
size: { width: icon.width, height: icon.height },
|
|
65
|
+
sizeClass: sc,
|
|
66
|
+
style,
|
|
67
|
+
colors: uniqueColors,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const dominantStyle = Object.entries(styleCounts).sort((a, b) => b[1] - a[1])[0][0];
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
icons,
|
|
75
|
+
sizeDistribution,
|
|
76
|
+
dominantStyle,
|
|
77
|
+
colorPalette: [...allColors],
|
|
78
|
+
count: icons.length,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export function extractImageStyles(imageData) {
|
|
2
|
+
const ratioCount = new Map();
|
|
3
|
+
const shapeCount = new Map();
|
|
4
|
+
const filterCount = new Map();
|
|
5
|
+
const fitCount = new Map();
|
|
6
|
+
const patternCount = new Map();
|
|
7
|
+
|
|
8
|
+
const knownRatios = [
|
|
9
|
+
[1, 1, '1:1'], [4, 3, '4:3'], [3, 4, '3:4'], [16, 9, '16:9'], [9, 16, '9:16'],
|
|
10
|
+
[3, 2, '3:2'], [2, 3, '2:3'], [21, 9, '21:9'],
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function closestRatio(w, h) {
|
|
14
|
+
if (!w || !h) return null;
|
|
15
|
+
const r = w / h;
|
|
16
|
+
let best = null, bestDiff = 0.15;
|
|
17
|
+
for (const [rw, rh, label] of knownRatios) {
|
|
18
|
+
const diff = Math.abs(r - rw / rh);
|
|
19
|
+
if (diff < bestDiff) { best = label; bestDiff = diff; }
|
|
20
|
+
}
|
|
21
|
+
return best || `${Math.round(r * 100) / 100}:1`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function classifyShape(borderRadius) {
|
|
25
|
+
const br = parseFloat(borderRadius) || 0;
|
|
26
|
+
if (br >= 50) return 'circular';
|
|
27
|
+
if (br >= 20) return 'pill';
|
|
28
|
+
if (br > 0) return 'rounded';
|
|
29
|
+
return 'square';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function classifyPattern(img, shape) {
|
|
33
|
+
const w = img.width || 0, h = img.height || 0;
|
|
34
|
+
const area = w * h;
|
|
35
|
+
if (shape === 'circular' && area <= 22500) return 'avatar';
|
|
36
|
+
if (w >= 600 && h >= 200 && img.objectFit === 'cover') return 'hero';
|
|
37
|
+
if (area <= 40000 && (shape === 'rounded' || shape === 'square')) return 'thumbnail';
|
|
38
|
+
if (w >= 400 && h >= 400 && shape === 'square') return 'gallery';
|
|
39
|
+
return 'general';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function incMap(map, key, extra) {
|
|
43
|
+
if (!map.has(key)) map.set(key, { count: 0, ...extra });
|
|
44
|
+
map.get(key).count++;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const img of imageData) {
|
|
48
|
+
const ratio = closestRatio(img.width, img.height);
|
|
49
|
+
if (ratio) incMap(ratioCount, ratio);
|
|
50
|
+
|
|
51
|
+
const shape = classifyShape(img.borderRadius);
|
|
52
|
+
incMap(shapeCount, shape, { borderRadius: img.borderRadius || '0' });
|
|
53
|
+
|
|
54
|
+
if (img.filter && img.filter !== 'none') {
|
|
55
|
+
for (const f of img.filter.match(/[a-z-]+\(/g) || [img.filter]) {
|
|
56
|
+
incMap(filterCount, f.replace('(', ''));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (img.objectFit && img.objectFit !== 'initial') incMap(fitCount, img.objectFit);
|
|
61
|
+
|
|
62
|
+
const pattern = classifyPattern(img, shape);
|
|
63
|
+
incMap(patternCount, pattern, { styles: { objectFit: img.objectFit, borderRadius: img.borderRadius, shape } });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const toArray = (map, keyName) =>
|
|
67
|
+
Array.from(map.entries()).map(([k, v]) => ({ [keyName]: k, ...v })).sort((a, b) => b.count - a.count);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
patterns: toArray(patternCount, 'name'),
|
|
71
|
+
aspectRatios: toArray(ratioCount, 'ratio'),
|
|
72
|
+
shapes: toArray(shapeCount, 'shape'),
|
|
73
|
+
filters: toArray(filterCount, 'filter'),
|
|
74
|
+
objectFitUsage: toArray(fitCount, 'value'),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -8,9 +8,10 @@ export async function captureInteractions(url, options = {}) {
|
|
|
8
8
|
const context = await browser.newContext({ viewport: { width, height } });
|
|
9
9
|
const page = await context.newPage();
|
|
10
10
|
|
|
11
|
-
await page.goto(url, { waitUntil: '
|
|
11
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
12
|
+
await page.waitForLoadState('networkidle').catch(() => {});
|
|
12
13
|
if (wait > 0) await page.waitForTimeout(wait);
|
|
13
|
-
await page.evaluate(() => document.fonts.ready);
|
|
14
|
+
await page.evaluate(() => document.fonts.ready).catch(() => {});
|
|
14
15
|
|
|
15
16
|
const results = { buttons: [], links: [], inputs: [] };
|
|
16
17
|
|
|
@@ -20,9 +20,10 @@ export async function captureResponsive(url, options = {}) {
|
|
|
20
20
|
const page = await context.newPage();
|
|
21
21
|
|
|
22
22
|
try {
|
|
23
|
-
await page.goto(url, { waitUntil: '
|
|
23
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 });
|
|
24
|
+
await page.waitForLoadState('networkidle').catch(() => {});
|
|
24
25
|
if (wait > 0) await page.waitForTimeout(wait);
|
|
25
|
-
await page.evaluate(() => document.fonts.ready);
|
|
26
|
+
await page.evaluate(() => document.fonts.ready).catch(() => {});
|
|
26
27
|
|
|
27
28
|
const data = await page.evaluate(() => {
|
|
28
29
|
const body = document.body;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const LAYER_DEFS = [
|
|
2
|
+
{ name: 'modal', min: 1000, max: Infinity },
|
|
3
|
+
{ name: 'dropdown', min: 100, max: 999 },
|
|
4
|
+
{ name: 'sticky', min: 10, max: 99 },
|
|
5
|
+
{ name: 'base', min: -Infinity, max: 9 },
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
function elLabel(el) {
|
|
9
|
+
const cls = el.classList?.length ? '.' + [...el.classList].join('.') : '';
|
|
10
|
+
return el.tag + cls;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function extractZIndex(styles) {
|
|
14
|
+
// Filter and parse explicit z-index values
|
|
15
|
+
const entries = styles
|
|
16
|
+
.filter(el => el.zIndex !== 'auto')
|
|
17
|
+
.map(el => ({ value: parseInt(el.zIndex, 10), el }))
|
|
18
|
+
.filter(e => !isNaN(e.value));
|
|
19
|
+
|
|
20
|
+
// Group by z-index value
|
|
21
|
+
const byValue = new Map();
|
|
22
|
+
for (const { value, el } of entries) {
|
|
23
|
+
if (!byValue.has(value)) byValue.set(value, []);
|
|
24
|
+
byValue.get(value).push(el);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const allValues = [...byValue.keys()].sort((a, b) => a - b);
|
|
28
|
+
|
|
29
|
+
// Build scale: each unique value with count and representative elements
|
|
30
|
+
const scale = allValues.map(value => ({
|
|
31
|
+
value,
|
|
32
|
+
count: byValue.get(value).length,
|
|
33
|
+
elements: byValue.get(value).map(elLabel),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
// Build layers from predefined ranges
|
|
37
|
+
const layers = LAYER_DEFS
|
|
38
|
+
.map(def => {
|
|
39
|
+
const matching = allValues.filter(v => v >= def.min && v <= def.max);
|
|
40
|
+
if (!matching.length) return null;
|
|
41
|
+
const elements = matching.flatMap(v => byValue.get(v).map(elLabel));
|
|
42
|
+
return {
|
|
43
|
+
name: def.name,
|
|
44
|
+
range: [Math.min(...matching), Math.max(...matching)],
|
|
45
|
+
elements,
|
|
46
|
+
};
|
|
47
|
+
})
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
|
|
50
|
+
// Detect issues
|
|
51
|
+
const issues = [];
|
|
52
|
+
const highValues = allValues.filter(v => v > 9999);
|
|
53
|
+
if (highValues.length) {
|
|
54
|
+
issues.push({ type: 'excessive', message: `Very high z-index values: ${highValues.join(', ')}` });
|
|
55
|
+
}
|
|
56
|
+
if (allValues.length >= 5) {
|
|
57
|
+
const spread = allValues[allValues.length - 1] - allValues[0];
|
|
58
|
+
const density = allValues.length / (spread || 1);
|
|
59
|
+
if (density > 0.3) {
|
|
60
|
+
issues.push({ type: 'z-index-war', message: `${allValues.length} unique values in a narrow range (${allValues[0]}-${allValues[allValues.length - 1]})` });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { layers, allValues, issues, scale };
|
|
65
|
+
}
|
|
@@ -500,6 +500,104 @@ export function formatMarkdown(design) {
|
|
|
500
500
|
}
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
+
// ── Gradients ──
|
|
504
|
+
if (design.gradients && design.gradients.count > 0) {
|
|
505
|
+
lines.push('## Gradients');
|
|
506
|
+
lines.push('');
|
|
507
|
+
lines.push(`**${design.gradients.count} unique gradients** detected.`);
|
|
508
|
+
lines.push('');
|
|
509
|
+
lines.push('| Type | Direction | Stops | Classification |');
|
|
510
|
+
lines.push('|------|-----------|-------|----------------|');
|
|
511
|
+
for (const g of design.gradients.gradients.slice(0, 15)) {
|
|
512
|
+
lines.push(`| ${g.type} | ${g.direction || '—'} | ${g.stops.length} | ${g.classification} |`);
|
|
513
|
+
}
|
|
514
|
+
lines.push('');
|
|
515
|
+
lines.push('```css');
|
|
516
|
+
for (const g of design.gradients.gradients.slice(0, 5)) {
|
|
517
|
+
lines.push(`background: ${g.raw};`);
|
|
518
|
+
}
|
|
519
|
+
lines.push('```');
|
|
520
|
+
lines.push('');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ── Z-Index Map ──
|
|
524
|
+
if (design.zIndex && design.zIndex.allValues.length > 0) {
|
|
525
|
+
lines.push('## Z-Index Map');
|
|
526
|
+
lines.push('');
|
|
527
|
+
lines.push(`**${design.zIndex.allValues.length} unique z-index values** across ${design.zIndex.layers.length} layers.`);
|
|
528
|
+
lines.push('');
|
|
529
|
+
if (design.zIndex.layers.length > 0) {
|
|
530
|
+
lines.push('| Layer | Range | Elements |');
|
|
531
|
+
lines.push('|-------|-------|----------|');
|
|
532
|
+
for (const l of design.zIndex.layers) {
|
|
533
|
+
const elNames = l.elements.slice(0, 3).join(', ');
|
|
534
|
+
lines.push(`| ${l.name} | ${l.range} | ${elNames} |`);
|
|
535
|
+
}
|
|
536
|
+
lines.push('');
|
|
537
|
+
}
|
|
538
|
+
if (design.zIndex.issues.length > 0) {
|
|
539
|
+
lines.push('**Issues:**');
|
|
540
|
+
for (const issue of design.zIndex.issues) {
|
|
541
|
+
lines.push(`- ${issue}`);
|
|
542
|
+
}
|
|
543
|
+
lines.push('');
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ── Icons ──
|
|
548
|
+
if (design.icons && design.icons.count > 0) {
|
|
549
|
+
lines.push('## SVG Icons');
|
|
550
|
+
lines.push('');
|
|
551
|
+
lines.push(`**${design.icons.count} unique SVG icons** detected. Dominant style: **${design.icons.dominantStyle || 'mixed'}**.`);
|
|
552
|
+
lines.push('');
|
|
553
|
+
const dist = design.icons.sizeDistribution;
|
|
554
|
+
if (dist) {
|
|
555
|
+
lines.push('| Size Class | Count |');
|
|
556
|
+
lines.push('|------------|-------|');
|
|
557
|
+
for (const [cls, count] of Object.entries(dist)) {
|
|
558
|
+
if (count > 0) lines.push(`| ${cls} | ${count} |`);
|
|
559
|
+
}
|
|
560
|
+
lines.push('');
|
|
561
|
+
}
|
|
562
|
+
if (design.icons.colorPalette.length > 0) {
|
|
563
|
+
lines.push(`**Icon colors:** ${design.icons.colorPalette.slice(0, 10).map(c => `\`${c}\``).join(', ')}`);
|
|
564
|
+
lines.push('');
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ── Font Files ──
|
|
569
|
+
if (design.fonts && design.fonts.fonts.length > 0) {
|
|
570
|
+
lines.push('## Font Files');
|
|
571
|
+
lines.push('');
|
|
572
|
+
lines.push('| Family | Source | Weights | Styles |');
|
|
573
|
+
lines.push('|--------|--------|---------|--------|');
|
|
574
|
+
for (const f of design.fonts.fonts) {
|
|
575
|
+
lines.push(`| ${f.family} | ${f.source} | ${f.weights.join(', ')} | ${f.styles.join(', ')} |`);
|
|
576
|
+
}
|
|
577
|
+
lines.push('');
|
|
578
|
+
if (design.fonts.googleFontsUrl) {
|
|
579
|
+
lines.push(`**Google Fonts URL:** \`${design.fonts.googleFontsUrl}\``);
|
|
580
|
+
lines.push('');
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ── Image Styles ──
|
|
585
|
+
if (design.images && design.images.patterns.length > 0) {
|
|
586
|
+
lines.push('## Image Style Patterns');
|
|
587
|
+
lines.push('');
|
|
588
|
+
lines.push('| Pattern | Count | Key Styles |');
|
|
589
|
+
lines.push('|---------|-------|------------|');
|
|
590
|
+
for (const p of design.images.patterns) {
|
|
591
|
+
const styles = Object.entries(p.styles || {}).map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
592
|
+
lines.push(`| ${p.name} | ${p.count} | ${styles || '—'} |`);
|
|
593
|
+
}
|
|
594
|
+
lines.push('');
|
|
595
|
+
if (design.images.aspectRatios.length > 0) {
|
|
596
|
+
lines.push(`**Aspect ratios:** ${design.images.aspectRatios.slice(0, 8).map(a => `${a.ratio} (${a.count}x)`).join(', ')}`);
|
|
597
|
+
lines.push('');
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
503
601
|
// ── Quick Start ──
|
|
504
602
|
lines.push('## Quick Start');
|
|
505
603
|
lines.push('');
|
package/src/index.js
CHANGED
|
@@ -11,6 +11,11 @@ import { extractComponents } from './extractors/components.js';
|
|
|
11
11
|
import { extractAccessibility } from './extractors/accessibility.js';
|
|
12
12
|
import { extractLayout } from './extractors/layout.js';
|
|
13
13
|
import { scoreDesignSystem } from './extractors/scoring.js';
|
|
14
|
+
import { extractGradients } from './extractors/gradients.js';
|
|
15
|
+
import { extractZIndex } from './extractors/zindex.js';
|
|
16
|
+
import { extractIcons } from './extractors/icons.js';
|
|
17
|
+
import { extractFonts } from './extractors/fonts.js';
|
|
18
|
+
import { extractImageStyles } from './extractors/images.js';
|
|
14
19
|
|
|
15
20
|
export async function extractDesignLanguage(url, options = {}) {
|
|
16
21
|
const rawData = await crawlPage(url, options);
|
|
@@ -35,8 +40,13 @@ export async function extractDesignLanguage(url, options = {}) {
|
|
|
35
40
|
components: extractComponents(styles),
|
|
36
41
|
accessibility: extractAccessibility(styles),
|
|
37
42
|
layout: extractLayout(styles),
|
|
43
|
+
gradients: extractGradients(styles),
|
|
44
|
+
zIndex: extractZIndex(styles),
|
|
45
|
+
icons: rawData.light.icons ? extractIcons(rawData.light.icons) : { icons: [], count: 0 },
|
|
46
|
+
fonts: rawData.light.fontData ? extractFonts(rawData.light.fontData) : { fonts: [], systemFonts: [] },
|
|
47
|
+
images: rawData.light.images ? extractImageStyles(rawData.light.images) : { patterns: [], aspectRatios: [] },
|
|
38
48
|
componentScreenshots: rawData.componentScreenshots || {},
|
|
39
|
-
score: null,
|
|
49
|
+
score: null,
|
|
40
50
|
};
|
|
41
51
|
|
|
42
52
|
if (rawData.dark) {
|
|
@@ -68,3 +78,5 @@ export { compareBrands, formatBrandMatrix, formatBrandMatrixHtml } from './multi
|
|
|
68
78
|
export { generateClone } from './clone.js';
|
|
69
79
|
export { scoreDesignSystem } from './extractors/scoring.js';
|
|
70
80
|
export { watchSite } from './watch.js';
|
|
81
|
+
export { diffDarkMode } from './darkdiff.js';
|
|
82
|
+
export { applyDesign } from './apply.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<!-- BEGIN:nextjs-agent-rules -->
|
|
2
|
+
# This is NOT the Next.js you know
|
|
3
|
+
|
|
4
|
+
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
|
|
5
|
+
<!-- END:nextjs-agent-rules -->
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
|
2
|
+
|
|
3
|
+
## Getting Started
|
|
4
|
+
|
|
5
|
+
First, run the development server:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm run dev
|
|
9
|
+
# or
|
|
10
|
+
yarn dev
|
|
11
|
+
# or
|
|
12
|
+
pnpm dev
|
|
13
|
+
# or
|
|
14
|
+
bun dev
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
|
18
|
+
|
|
19
|
+
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
|
|
20
|
+
|
|
21
|
+
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
|
22
|
+
|
|
23
|
+
## Learn More
|
|
24
|
+
|
|
25
|
+
To learn more about Next.js, take a look at the following resources:
|
|
26
|
+
|
|
27
|
+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
|
28
|
+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
29
|
+
|
|
30
|
+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
|
31
|
+
|
|
32
|
+
## Deploy on Vercel
|
|
33
|
+
|
|
34
|
+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
|
35
|
+
|
|
36
|
+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|