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.
@@ -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 };