design-md-generator 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.
- package/README.md +186 -0
- package/package.json +35 -0
- package/src/cli.js +127 -0
- package/src/extractor.js +721 -0
- package/src/generator.js +904 -0
- package/src/index.js +195 -0
- package/src/local-extractor.js +985 -0
package/src/extractor.js
ADDED
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design Token Extractor
|
|
3
|
+
* Uses Puppeteer to visit a website and extract design tokens (colors, typography,
|
|
4
|
+
* components, layout, shadows, etc.) from the live DOM and computed styles.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const puppeteer = require('puppeteer');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Main extraction function - visits the URL and extracts all design tokens
|
|
11
|
+
* @param {string} url - The website URL to extract from
|
|
12
|
+
* @param {object} options - Extraction options
|
|
13
|
+
* @returns {object} Extracted design tokens
|
|
14
|
+
*/
|
|
15
|
+
async function extractDesignTokens(url, options = {}) {
|
|
16
|
+
const { timeout = 30000, waitForSelector = null } = options;
|
|
17
|
+
|
|
18
|
+
console.log(`🚀 Launching browser...`);
|
|
19
|
+
const browser = await puppeteer.launch({
|
|
20
|
+
headless: true,
|
|
21
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-web-security'],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const page = await browser.newPage();
|
|
26
|
+
|
|
27
|
+
// Set a realistic viewport
|
|
28
|
+
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 2 });
|
|
29
|
+
|
|
30
|
+
// Set a realistic user agent
|
|
31
|
+
await page.setUserAgent(
|
|
32
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
console.log(`📡 Navigating to ${url}...`);
|
|
36
|
+
await page.goto(url, {
|
|
37
|
+
waitUntil: 'networkidle2',
|
|
38
|
+
timeout,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (waitForSelector) {
|
|
42
|
+
await page.waitForSelector(waitForSelector, { timeout: 10000 }).catch(() => {});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Wait a bit for any animations/transitions to settle
|
|
46
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
47
|
+
|
|
48
|
+
console.log(`🔍 Extracting design tokens...`);
|
|
49
|
+
|
|
50
|
+
// Extract all tokens in a single page.evaluate call for efficiency
|
|
51
|
+
const tokens = await page.evaluate(() => {
|
|
52
|
+
// ============================================================
|
|
53
|
+
// Helper utilities
|
|
54
|
+
// ============================================================
|
|
55
|
+
|
|
56
|
+
function rgbToHex(r, g, b) {
|
|
57
|
+
return '#' + [r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseColor(colorStr) {
|
|
61
|
+
if (!colorStr || colorStr === 'transparent' || colorStr === 'none') return null;
|
|
62
|
+
// Already hex
|
|
63
|
+
if (colorStr.startsWith('#')) return colorStr.toLowerCase();
|
|
64
|
+
// rgb/rgba
|
|
65
|
+
const rgbMatch = colorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
66
|
+
if (rgbMatch) {
|
|
67
|
+
return rgbToHex(parseInt(rgbMatch[1]), parseInt(rgbMatch[2]), parseInt(rgbMatch[3]));
|
|
68
|
+
}
|
|
69
|
+
return colorStr;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getComputedStyleSafe(el) {
|
|
73
|
+
try {
|
|
74
|
+
return window.getComputedStyle(el);
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ============================================================
|
|
81
|
+
// 1. Extract CSS Custom Properties (CSS Variables)
|
|
82
|
+
// ============================================================
|
|
83
|
+
function extractCSSVariables() {
|
|
84
|
+
const variables = {};
|
|
85
|
+
try {
|
|
86
|
+
for (const sheet of document.styleSheets) {
|
|
87
|
+
try {
|
|
88
|
+
const rules = sheet.cssRules || sheet.rules;
|
|
89
|
+
if (!rules) continue;
|
|
90
|
+
for (const rule of rules) {
|
|
91
|
+
if (rule.style) {
|
|
92
|
+
for (let i = 0; i < rule.style.length; i++) {
|
|
93
|
+
const prop = rule.style[i];
|
|
94
|
+
if (prop.startsWith('--')) {
|
|
95
|
+
const value = rule.style.getPropertyValue(prop).trim();
|
|
96
|
+
if (value) {
|
|
97
|
+
variables[prop] = value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// Cross-origin stylesheet, skip
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// Fallback: read from :root computed style
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Also get computed values from :root
|
|
112
|
+
const rootStyle = getComputedStyle(document.documentElement);
|
|
113
|
+
for (const key of Object.keys(variables)) {
|
|
114
|
+
const computed = rootStyle.getPropertyValue(key).trim();
|
|
115
|
+
if (computed) {
|
|
116
|
+
variables[key] = computed;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return variables;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ============================================================
|
|
124
|
+
// 2. Extract Colors from elements
|
|
125
|
+
// ============================================================
|
|
126
|
+
function extractColors() {
|
|
127
|
+
const colorMap = new Map(); // hex -> { count, roles: Set }
|
|
128
|
+
const allElements = document.querySelectorAll('*');
|
|
129
|
+
const sampleSize = Math.min(allElements.length, 500);
|
|
130
|
+
|
|
131
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
132
|
+
const el = allElements[Math.floor((i / sampleSize) * allElements.length)];
|
|
133
|
+
const style = getComputedStyleSafe(el);
|
|
134
|
+
if (!style) continue;
|
|
135
|
+
|
|
136
|
+
const colorProps = [
|
|
137
|
+
{ prop: 'color', role: 'text' },
|
|
138
|
+
{ prop: 'backgroundColor', role: 'background' },
|
|
139
|
+
{ prop: 'borderColor', role: 'border' },
|
|
140
|
+
{ prop: 'borderTopColor', role: 'border' },
|
|
141
|
+
{ prop: 'borderBottomColor', role: 'border' },
|
|
142
|
+
{ prop: 'outlineColor', role: 'outline' },
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
for (const { prop, role } of colorProps) {
|
|
146
|
+
const raw = style[prop];
|
|
147
|
+
const hex = parseColor(raw);
|
|
148
|
+
if (hex && hex !== '#000000' && hex !== '#ffffff' && hex.startsWith('#')) {
|
|
149
|
+
if (!colorMap.has(hex)) {
|
|
150
|
+
colorMap.set(hex, { count: 0, roles: new Set() });
|
|
151
|
+
}
|
|
152
|
+
const entry = colorMap.get(hex);
|
|
153
|
+
entry.count++;
|
|
154
|
+
entry.roles.add(role);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Also extract background/text from key semantic elements
|
|
160
|
+
const semanticSelectors = {
|
|
161
|
+
'body': 'page-background',
|
|
162
|
+
'header, nav, [role="banner"]': 'navigation',
|
|
163
|
+
'main, [role="main"]': 'main-content',
|
|
164
|
+
'footer, [role="contentinfo"]': 'footer',
|
|
165
|
+
'h1, h2, h3': 'heading',
|
|
166
|
+
'p': 'body-text',
|
|
167
|
+
'a': 'link',
|
|
168
|
+
'button, [role="button"]': 'button',
|
|
169
|
+
'input, textarea, select': 'input',
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const semanticColors = {};
|
|
173
|
+
for (const [selector, role] of Object.entries(semanticSelectors)) {
|
|
174
|
+
try {
|
|
175
|
+
const el = document.querySelector(selector);
|
|
176
|
+
if (el) {
|
|
177
|
+
const style = getComputedStyleSafe(el);
|
|
178
|
+
if (style) {
|
|
179
|
+
const bg = parseColor(style.backgroundColor);
|
|
180
|
+
const fg = parseColor(style.color);
|
|
181
|
+
if (bg) semanticColors[`${role}-bg`] = bg;
|
|
182
|
+
if (fg) semanticColors[`${role}-fg`] = fg;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} catch {}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Sort by frequency
|
|
189
|
+
const sorted = [...colorMap.entries()]
|
|
190
|
+
.sort((a, b) => b[1].count - a[1].count)
|
|
191
|
+
.slice(0, 40)
|
|
192
|
+
.map(([hex, data]) => ({
|
|
193
|
+
hex,
|
|
194
|
+
count: data.count,
|
|
195
|
+
roles: [...data.roles],
|
|
196
|
+
}));
|
|
197
|
+
|
|
198
|
+
// Also capture black and white if used
|
|
199
|
+
const bodyStyle = getComputedStyleSafe(document.body);
|
|
200
|
+
const pageBg = bodyStyle ? parseColor(bodyStyle.backgroundColor) : '#ffffff';
|
|
201
|
+
const pageColor = bodyStyle ? parseColor(bodyStyle.color) : '#000000';
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
palette: sorted,
|
|
205
|
+
semantic: semanticColors,
|
|
206
|
+
pageBg: pageBg || '#ffffff',
|
|
207
|
+
pageColor: pageColor || '#000000',
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ============================================================
|
|
212
|
+
// 3. Extract Typography
|
|
213
|
+
// ============================================================
|
|
214
|
+
function extractTypography() {
|
|
215
|
+
const fontFamilies = new Map(); // family -> count
|
|
216
|
+
const typographyScale = [];
|
|
217
|
+
|
|
218
|
+
const textElements = document.querySelectorAll(
|
|
219
|
+
'h1, h2, h3, h4, h5, h6, p, a, button, span, label, li, td, th, code, pre, blockquote, figcaption, small'
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const seen = new Set();
|
|
223
|
+
|
|
224
|
+
for (const el of textElements) {
|
|
225
|
+
const style = getComputedStyleSafe(el);
|
|
226
|
+
if (!style) continue;
|
|
227
|
+
|
|
228
|
+
const family = style.fontFamily;
|
|
229
|
+
if (family) {
|
|
230
|
+
const primary = family.split(',')[0].trim().replace(/['"]/g, '');
|
|
231
|
+
fontFamilies.set(primary, (fontFamilies.get(primary) || 0) + 1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Build typography scale entry
|
|
235
|
+
const fontSize = style.fontSize;
|
|
236
|
+
const fontWeight = style.fontWeight;
|
|
237
|
+
const lineHeight = style.lineHeight;
|
|
238
|
+
const letterSpacing = style.letterSpacing;
|
|
239
|
+
const tag = el.tagName.toLowerCase();
|
|
240
|
+
|
|
241
|
+
const key = `${tag}-${fontSize}-${fontWeight}`;
|
|
242
|
+
if (!seen.has(key)) {
|
|
243
|
+
seen.add(key);
|
|
244
|
+
typographyScale.push({
|
|
245
|
+
tag,
|
|
246
|
+
role: getTypoRole(tag, el),
|
|
247
|
+
fontFamily: family ? family.split(',')[0].trim().replace(/['"]/g, '') : 'inherit',
|
|
248
|
+
fontSize,
|
|
249
|
+
fontWeight,
|
|
250
|
+
lineHeight,
|
|
251
|
+
letterSpacing: letterSpacing === 'normal' ? 'normal' : letterSpacing,
|
|
252
|
+
fontFeatureSettings: style.fontFeatureSettings || 'normal',
|
|
253
|
+
textTransform: style.textTransform || 'none',
|
|
254
|
+
sampleText: el.textContent?.trim().slice(0, 60) || '',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function getTypoRole(tag, el) {
|
|
260
|
+
const roleMap = {
|
|
261
|
+
h1: 'Display / Hero',
|
|
262
|
+
h2: 'Section Heading',
|
|
263
|
+
h3: 'Sub-heading',
|
|
264
|
+
h4: 'Sub-heading Small',
|
|
265
|
+
h5: 'Label',
|
|
266
|
+
h6: 'Label Small',
|
|
267
|
+
p: 'Body',
|
|
268
|
+
a: 'Link',
|
|
269
|
+
button: 'Button',
|
|
270
|
+
span: 'Inline',
|
|
271
|
+
label: 'Label',
|
|
272
|
+
li: 'List Item',
|
|
273
|
+
td: 'Table Cell',
|
|
274
|
+
th: 'Table Header',
|
|
275
|
+
code: 'Code',
|
|
276
|
+
pre: 'Code Block',
|
|
277
|
+
small: 'Caption',
|
|
278
|
+
figcaption: 'Caption',
|
|
279
|
+
blockquote: 'Blockquote',
|
|
280
|
+
};
|
|
281
|
+
return roleMap[tag] || 'Text';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Sort font families by usage
|
|
285
|
+
const sortedFamilies = [...fontFamilies.entries()]
|
|
286
|
+
.sort((a, b) => b[1] - a[1])
|
|
287
|
+
.map(([family, count]) => ({ family, count }));
|
|
288
|
+
|
|
289
|
+
// Sort typography scale by font size (descending)
|
|
290
|
+
typographyScale.sort((a, b) => {
|
|
291
|
+
const sizeA = parseFloat(a.fontSize);
|
|
292
|
+
const sizeB = parseFloat(b.fontSize);
|
|
293
|
+
return sizeB - sizeA;
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Deduplicate by similar sizes
|
|
297
|
+
const deduped = [];
|
|
298
|
+
const sizesSeen = new Set();
|
|
299
|
+
for (const entry of typographyScale) {
|
|
300
|
+
const sizeKey = `${entry.role}-${Math.round(parseFloat(entry.fontSize))}`;
|
|
301
|
+
if (!sizesSeen.has(sizeKey)) {
|
|
302
|
+
sizesSeen.add(sizeKey);
|
|
303
|
+
deduped.push(entry);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
fontFamilies: sortedFamilies,
|
|
309
|
+
scale: deduped.slice(0, 25), // Top 25 entries
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ============================================================
|
|
314
|
+
// 4. Extract Component Styles
|
|
315
|
+
// ============================================================
|
|
316
|
+
function extractComponents() {
|
|
317
|
+
const components = {};
|
|
318
|
+
|
|
319
|
+
// Buttons
|
|
320
|
+
const buttons = document.querySelectorAll('button, [role="button"], a.btn, .button, [class*="btn"]');
|
|
321
|
+
const buttonStyles = [];
|
|
322
|
+
const btnSeen = new Set();
|
|
323
|
+
|
|
324
|
+
for (const btn of buttons) {
|
|
325
|
+
const style = getComputedStyleSafe(btn);
|
|
326
|
+
if (!style) continue;
|
|
327
|
+
|
|
328
|
+
const key = `${parseColor(style.backgroundColor)}-${parseColor(style.color)}-${style.borderRadius}`;
|
|
329
|
+
if (btnSeen.has(key)) continue;
|
|
330
|
+
btnSeen.add(key);
|
|
331
|
+
|
|
332
|
+
buttonStyles.push({
|
|
333
|
+
text: btn.textContent?.trim().slice(0, 30) || '',
|
|
334
|
+
background: parseColor(style.backgroundColor) || 'transparent',
|
|
335
|
+
color: parseColor(style.color),
|
|
336
|
+
padding: style.padding,
|
|
337
|
+
borderRadius: style.borderRadius,
|
|
338
|
+
border: style.border,
|
|
339
|
+
fontSize: style.fontSize,
|
|
340
|
+
fontWeight: style.fontWeight,
|
|
341
|
+
fontFamily: style.fontFamily?.split(',')[0].trim().replace(/['"]/g, ''),
|
|
342
|
+
boxShadow: style.boxShadow !== 'none' ? style.boxShadow : null,
|
|
343
|
+
cursor: style.cursor,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
components.buttons = buttonStyles.slice(0, 8);
|
|
347
|
+
|
|
348
|
+
// Cards / Containers
|
|
349
|
+
const cardSelectors = [
|
|
350
|
+
'[class*="card"]', '[class*="Card"]',
|
|
351
|
+
'[class*="panel"]', '[class*="Panel"]',
|
|
352
|
+
'[class*="tile"]', '[class*="Tile"]',
|
|
353
|
+
'[class*="container"]',
|
|
354
|
+
'article', 'section > div',
|
|
355
|
+
];
|
|
356
|
+
const cards = document.querySelectorAll(cardSelectors.join(', '));
|
|
357
|
+
const cardStyles = [];
|
|
358
|
+
const cardSeen = new Set();
|
|
359
|
+
|
|
360
|
+
for (const card of cards) {
|
|
361
|
+
const style = getComputedStyleSafe(card);
|
|
362
|
+
if (!style) continue;
|
|
363
|
+
|
|
364
|
+
const bg = parseColor(style.backgroundColor);
|
|
365
|
+
const radius = style.borderRadius;
|
|
366
|
+
const shadow = style.boxShadow;
|
|
367
|
+
const border = style.border;
|
|
368
|
+
|
|
369
|
+
const key = `${bg}-${radius}-${shadow?.slice(0, 30)}`;
|
|
370
|
+
if (cardSeen.has(key)) continue;
|
|
371
|
+
cardSeen.add(key);
|
|
372
|
+
|
|
373
|
+
if (bg || (shadow && shadow !== 'none') || (radius && radius !== '0px')) {
|
|
374
|
+
cardStyles.push({
|
|
375
|
+
background: bg || 'transparent',
|
|
376
|
+
borderRadius: radius,
|
|
377
|
+
boxShadow: shadow !== 'none' ? shadow : null,
|
|
378
|
+
border: border !== 'none' ? border : null,
|
|
379
|
+
padding: style.padding,
|
|
380
|
+
overflow: style.overflow,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
components.cards = cardStyles.slice(0, 6);
|
|
385
|
+
|
|
386
|
+
// Inputs
|
|
387
|
+
const inputs = document.querySelectorAll('input, textarea, select');
|
|
388
|
+
const inputStyles = [];
|
|
389
|
+
const inputSeen = new Set();
|
|
390
|
+
|
|
391
|
+
for (const input of inputs) {
|
|
392
|
+
const style = getComputedStyleSafe(input);
|
|
393
|
+
if (!style) continue;
|
|
394
|
+
|
|
395
|
+
const key = `${style.border}-${style.borderRadius}-${parseColor(style.backgroundColor)}`;
|
|
396
|
+
if (inputSeen.has(key)) continue;
|
|
397
|
+
inputSeen.add(key);
|
|
398
|
+
|
|
399
|
+
inputStyles.push({
|
|
400
|
+
background: parseColor(style.backgroundColor),
|
|
401
|
+
border: style.border,
|
|
402
|
+
borderRadius: style.borderRadius,
|
|
403
|
+
padding: style.padding,
|
|
404
|
+
fontSize: style.fontSize,
|
|
405
|
+
color: parseColor(style.color),
|
|
406
|
+
outline: style.outline,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
components.inputs = inputStyles.slice(0, 4);
|
|
410
|
+
|
|
411
|
+
// Navigation
|
|
412
|
+
const nav = document.querySelector('nav, header, [role="navigation"], [role="banner"]');
|
|
413
|
+
if (nav) {
|
|
414
|
+
const navStyle = getComputedStyleSafe(nav);
|
|
415
|
+
if (navStyle) {
|
|
416
|
+
components.navigation = {
|
|
417
|
+
background: parseColor(navStyle.backgroundColor),
|
|
418
|
+
position: navStyle.position,
|
|
419
|
+
height: navStyle.height,
|
|
420
|
+
padding: navStyle.padding,
|
|
421
|
+
boxShadow: navStyle.boxShadow !== 'none' ? navStyle.boxShadow : null,
|
|
422
|
+
borderBottom: navStyle.borderBottom !== 'none' ? navStyle.borderBottom : null,
|
|
423
|
+
backdropFilter: navStyle.backdropFilter || navStyle.webkitBackdropFilter || null,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Nav links
|
|
428
|
+
const navLinks = nav.querySelectorAll('a');
|
|
429
|
+
if (navLinks.length > 0) {
|
|
430
|
+
const linkStyle = getComputedStyleSafe(navLinks[0]);
|
|
431
|
+
if (linkStyle) {
|
|
432
|
+
components.navLinks = {
|
|
433
|
+
color: parseColor(linkStyle.color),
|
|
434
|
+
fontSize: linkStyle.fontSize,
|
|
435
|
+
fontWeight: linkStyle.fontWeight,
|
|
436
|
+
textDecoration: linkStyle.textDecoration,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Badges / Tags
|
|
443
|
+
const badges = document.querySelectorAll(
|
|
444
|
+
'[class*="badge"], [class*="Badge"], [class*="tag"], [class*="Tag"], [class*="chip"], [class*="Chip"], [class*="pill"], [class*="Pill"]'
|
|
445
|
+
);
|
|
446
|
+
const badgeStyles = [];
|
|
447
|
+
for (const badge of badges) {
|
|
448
|
+
const style = getComputedStyleSafe(badge);
|
|
449
|
+
if (!style) continue;
|
|
450
|
+
badgeStyles.push({
|
|
451
|
+
background: parseColor(style.backgroundColor),
|
|
452
|
+
color: parseColor(style.color),
|
|
453
|
+
padding: style.padding,
|
|
454
|
+
borderRadius: style.borderRadius,
|
|
455
|
+
fontSize: style.fontSize,
|
|
456
|
+
fontWeight: style.fontWeight,
|
|
457
|
+
border: style.border !== 'none' ? style.border : null,
|
|
458
|
+
});
|
|
459
|
+
if (badgeStyles.length >= 4) break;
|
|
460
|
+
}
|
|
461
|
+
components.badges = badgeStyles;
|
|
462
|
+
|
|
463
|
+
return components;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ============================================================
|
|
467
|
+
// 5. Extract Layout & Spacing
|
|
468
|
+
// ============================================================
|
|
469
|
+
function extractLayout() {
|
|
470
|
+
const layout = {};
|
|
471
|
+
|
|
472
|
+
// Max width of main content
|
|
473
|
+
const contentContainers = document.querySelectorAll(
|
|
474
|
+
'main, [role="main"], .container, [class*="container"], [class*="wrapper"], [class*="content"]'
|
|
475
|
+
);
|
|
476
|
+
const maxWidths = [];
|
|
477
|
+
for (const el of contentContainers) {
|
|
478
|
+
const style = getComputedStyleSafe(el);
|
|
479
|
+
if (style) {
|
|
480
|
+
const mw = style.maxWidth;
|
|
481
|
+
if (mw && mw !== 'none' && mw !== '0px') {
|
|
482
|
+
maxWidths.push(mw);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
layout.maxContentWidth = maxWidths.length > 0 ? maxWidths[0] : 'not detected';
|
|
487
|
+
|
|
488
|
+
// Collect spacing values used
|
|
489
|
+
const spacingValues = new Map();
|
|
490
|
+
const sampleElements = document.querySelectorAll('*');
|
|
491
|
+
const sampleCount = Math.min(sampleElements.length, 300);
|
|
492
|
+
|
|
493
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
494
|
+
const el = sampleElements[Math.floor((i / sampleCount) * sampleElements.length)];
|
|
495
|
+
const style = getComputedStyleSafe(el);
|
|
496
|
+
if (!style) continue;
|
|
497
|
+
|
|
498
|
+
for (const prop of ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
|
|
499
|
+
'marginTop', 'marginBottom', 'marginLeft', 'marginRight', 'gap', 'rowGap', 'columnGap']) {
|
|
500
|
+
const val = style[prop];
|
|
501
|
+
if (val && val !== '0px' && val !== 'auto' && val !== 'normal') {
|
|
502
|
+
const px = parseFloat(val);
|
|
503
|
+
if (!isNaN(px) && px > 0 && px <= 200) {
|
|
504
|
+
spacingValues.set(px, (spacingValues.get(px) || 0) + 1);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Sort and find the spacing scale
|
|
511
|
+
const sortedSpacing = [...spacingValues.entries()]
|
|
512
|
+
.sort((a, b) => b[1] - a[1])
|
|
513
|
+
.slice(0, 20)
|
|
514
|
+
.map(([px]) => px)
|
|
515
|
+
.sort((a, b) => a - b);
|
|
516
|
+
|
|
517
|
+
layout.spacingScale = sortedSpacing;
|
|
518
|
+
|
|
519
|
+
// Detect base unit (most common small spacing)
|
|
520
|
+
const smallSpacing = sortedSpacing.filter((v) => v >= 4 && v <= 16);
|
|
521
|
+
if (smallSpacing.length > 0) {
|
|
522
|
+
// Find GCD-like base
|
|
523
|
+
const candidates = [4, 8, 6, 5, 10];
|
|
524
|
+
let bestBase = 8;
|
|
525
|
+
let bestScore = 0;
|
|
526
|
+
for (const base of candidates) {
|
|
527
|
+
const score = smallSpacing.filter((v) => v % base === 0).length;
|
|
528
|
+
if (score > bestScore) {
|
|
529
|
+
bestScore = score;
|
|
530
|
+
bestBase = base;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
layout.baseUnit = bestBase;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Border radius values
|
|
537
|
+
const radiusValues = new Map();
|
|
538
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
539
|
+
const el = sampleElements[Math.floor((i / sampleCount) * sampleElements.length)];
|
|
540
|
+
const style = getComputedStyleSafe(el);
|
|
541
|
+
if (!style) continue;
|
|
542
|
+
const radius = style.borderRadius;
|
|
543
|
+
if (radius && radius !== '0px') {
|
|
544
|
+
radiusValues.set(radius, (radiusValues.get(radius) || 0) + 1);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
layout.borderRadiusScale = [...radiusValues.entries()]
|
|
549
|
+
.sort((a, b) => b[1] - a[1])
|
|
550
|
+
.slice(0, 10)
|
|
551
|
+
.map(([radius, count]) => ({ radius, count }));
|
|
552
|
+
|
|
553
|
+
// Grid / Flexbox usage
|
|
554
|
+
let gridCount = 0;
|
|
555
|
+
let flexCount = 0;
|
|
556
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
557
|
+
const el = sampleElements[Math.floor((i / sampleCount) * sampleElements.length)];
|
|
558
|
+
const style = getComputedStyleSafe(el);
|
|
559
|
+
if (!style) continue;
|
|
560
|
+
if (style.display === 'grid' || style.display === 'inline-grid') gridCount++;
|
|
561
|
+
if (style.display === 'flex' || style.display === 'inline-flex') flexCount++;
|
|
562
|
+
}
|
|
563
|
+
layout.gridUsage = gridCount;
|
|
564
|
+
layout.flexUsage = flexCount;
|
|
565
|
+
|
|
566
|
+
return layout;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ============================================================
|
|
570
|
+
// 6. Extract Shadows & Depth
|
|
571
|
+
// ============================================================
|
|
572
|
+
function extractShadows() {
|
|
573
|
+
const shadowMap = new Map();
|
|
574
|
+
const allElements = document.querySelectorAll('*');
|
|
575
|
+
const sampleSize = Math.min(allElements.length, 400);
|
|
576
|
+
|
|
577
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
578
|
+
const el = allElements[Math.floor((i / sampleSize) * allElements.length)];
|
|
579
|
+
const style = getComputedStyleSafe(el);
|
|
580
|
+
if (!style) continue;
|
|
581
|
+
|
|
582
|
+
const shadow = style.boxShadow;
|
|
583
|
+
if (shadow && shadow !== 'none') {
|
|
584
|
+
shadowMap.set(shadow, (shadowMap.get(shadow) || 0) + 1);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return [...shadowMap.entries()]
|
|
589
|
+
.sort((a, b) => b[1] - a[1])
|
|
590
|
+
.slice(0, 10)
|
|
591
|
+
.map(([shadow, count]) => ({ shadow, count }));
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ============================================================
|
|
595
|
+
// 7. Extract Meta Information
|
|
596
|
+
// ============================================================
|
|
597
|
+
function extractMeta() {
|
|
598
|
+
const title = document.title || '';
|
|
599
|
+
const metaDesc = document.querySelector('meta[name="description"]')?.content || '';
|
|
600
|
+
const themeColor = document.querySelector('meta[name="theme-color"]')?.content || '';
|
|
601
|
+
const ogImage = document.querySelector('meta[property="og:image"]')?.content || '';
|
|
602
|
+
const ogTitle = document.querySelector('meta[property="og:title"]')?.content || '';
|
|
603
|
+
const favicon = document.querySelector('link[rel="icon"], link[rel="shortcut icon"]')?.href || '';
|
|
604
|
+
|
|
605
|
+
// Detect dark mode preference
|
|
606
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
607
|
+
const bodyBg = getComputedStyleSafe(document.body)?.backgroundColor;
|
|
608
|
+
const bodyBgHex = parseColor(bodyBg);
|
|
609
|
+
const isDarkTheme = bodyBgHex && parseInt(bodyBgHex.slice(1), 16) < 0x808080;
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
title,
|
|
613
|
+
description: metaDesc,
|
|
614
|
+
themeColor,
|
|
615
|
+
ogImage,
|
|
616
|
+
ogTitle,
|
|
617
|
+
favicon,
|
|
618
|
+
isDarkTheme,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ============================================================
|
|
623
|
+
// 8. Extract Responsive / Media Queries
|
|
624
|
+
// ============================================================
|
|
625
|
+
function extractMediaQueries() {
|
|
626
|
+
const breakpoints = new Set();
|
|
627
|
+
|
|
628
|
+
for (const sheet of document.styleSheets) {
|
|
629
|
+
try {
|
|
630
|
+
const rules = sheet.cssRules || sheet.rules;
|
|
631
|
+
if (!rules) continue;
|
|
632
|
+
for (const rule of rules) {
|
|
633
|
+
if (rule.type === CSSRule.MEDIA_RULE) {
|
|
634
|
+
const media = rule.conditionText || rule.media?.mediaText || '';
|
|
635
|
+
const widthMatch = media.match(/(?:min|max)-width:\s*(\d+(?:\.\d+)?)(px|em|rem)/g);
|
|
636
|
+
if (widthMatch) {
|
|
637
|
+
for (const m of widthMatch) {
|
|
638
|
+
const val = m.match(/(\d+(?:\.\d+)?)(px|em|rem)/);
|
|
639
|
+
if (val) {
|
|
640
|
+
let px = parseFloat(val[1]);
|
|
641
|
+
if (val[2] === 'em' || val[2] === 'rem') px *= 16;
|
|
642
|
+
breakpoints.add(Math.round(px));
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
} catch {
|
|
649
|
+
// Cross-origin
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return [...breakpoints].sort((a, b) => a - b);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ============================================================
|
|
657
|
+
// 9. Extract full page HTML structure summary
|
|
658
|
+
// ============================================================
|
|
659
|
+
function extractStructureSummary() {
|
|
660
|
+
const body = document.body;
|
|
661
|
+
if (!body) return '';
|
|
662
|
+
|
|
663
|
+
function summarize(el, depth = 0) {
|
|
664
|
+
if (depth > 3) return '';
|
|
665
|
+
const tag = el.tagName?.toLowerCase();
|
|
666
|
+
if (!tag) return '';
|
|
667
|
+
const cls = el.className && typeof el.className === 'string'
|
|
668
|
+
? el.className.split(/\s+/).slice(0, 3).join('.')
|
|
669
|
+
: '';
|
|
670
|
+
const id = el.id ? `#${el.id}` : '';
|
|
671
|
+
const indent = ' '.repeat(depth);
|
|
672
|
+
let result = `${indent}<${tag}${id}${cls ? '.' + cls : ''}>\n`;
|
|
673
|
+
|
|
674
|
+
for (const child of el.children) {
|
|
675
|
+
result += summarize(child, depth + 1);
|
|
676
|
+
}
|
|
677
|
+
return result;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Only top-level structure
|
|
681
|
+
let structure = '';
|
|
682
|
+
for (const child of body.children) {
|
|
683
|
+
structure += summarize(child, 0);
|
|
684
|
+
}
|
|
685
|
+
return structure.slice(0, 3000); // Limit size
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ============================================================
|
|
689
|
+
// Run all extractors
|
|
690
|
+
// ============================================================
|
|
691
|
+
return {
|
|
692
|
+
cssVariables: extractCSSVariables(),
|
|
693
|
+
colors: extractColors(),
|
|
694
|
+
typography: extractTypography(),
|
|
695
|
+
components: extractComponents(),
|
|
696
|
+
layout: extractLayout(),
|
|
697
|
+
shadows: extractShadows(),
|
|
698
|
+
meta: extractMeta(),
|
|
699
|
+
breakpoints: extractMediaQueries(),
|
|
700
|
+
structure: extractStructureSummary(),
|
|
701
|
+
url: window.location.href,
|
|
702
|
+
timestamp: new Date().toISOString(),
|
|
703
|
+
};
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
console.log(`✅ Extraction complete!`);
|
|
707
|
+
|
|
708
|
+
// Also take a screenshot for reference
|
|
709
|
+
const screenshotPath = options.screenshotPath;
|
|
710
|
+
if (screenshotPath) {
|
|
711
|
+
await page.screenshot({ path: screenshotPath, fullPage: false });
|
|
712
|
+
console.log(`📸 Screenshot saved to ${screenshotPath}`);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return tokens;
|
|
716
|
+
} finally {
|
|
717
|
+
await browser.close();
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
module.exports = { extractDesignTokens };
|