real-prototypes-skill 2.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.
Files changed (60) hide show
  1. package/.claude/skills/agent-browser-skill/SKILL.md +252 -0
  2. package/.claude/skills/real-prototypes-skill/.gitignore +188 -0
  3. package/.claude/skills/real-prototypes-skill/ACCESSIBILITY.md +668 -0
  4. package/.claude/skills/real-prototypes-skill/INSTALL.md +259 -0
  5. package/.claude/skills/real-prototypes-skill/LICENSE +21 -0
  6. package/.claude/skills/real-prototypes-skill/PUBLISH.md +310 -0
  7. package/.claude/skills/real-prototypes-skill/QUICKSTART.md +240 -0
  8. package/.claude/skills/real-prototypes-skill/README.md +442 -0
  9. package/.claude/skills/real-prototypes-skill/SKILL.md +329 -0
  10. package/.claude/skills/real-prototypes-skill/capture/capture-engine.js +1153 -0
  11. package/.claude/skills/real-prototypes-skill/capture/config.schema.json +170 -0
  12. package/.claude/skills/real-prototypes-skill/cli.js +596 -0
  13. package/.claude/skills/real-prototypes-skill/docs/TROUBLESHOOTING.md +278 -0
  14. package/.claude/skills/real-prototypes-skill/docs/schemas/capture-config.md +167 -0
  15. package/.claude/skills/real-prototypes-skill/docs/schemas/design-tokens.md +183 -0
  16. package/.claude/skills/real-prototypes-skill/docs/schemas/manifest.md +169 -0
  17. package/.claude/skills/real-prototypes-skill/examples/CLAUDE.md.example +73 -0
  18. package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/CLAUDE.md +136 -0
  19. package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/FEATURES.md +222 -0
  20. package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/README.md +82 -0
  21. package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/references/design-tokens.json +87 -0
  22. package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/references/screenshots/homepage-viewport.png +0 -0
  23. package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/references/screenshots/prototype-chatbot-final.png +0 -0
  24. package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/references/screenshots/prototype-fullpage-v2.png +0 -0
  25. package/.claude/skills/real-prototypes-skill/references/accessibility-fixes.md +298 -0
  26. package/.claude/skills/real-prototypes-skill/references/accessibility-report.json +253 -0
  27. package/.claude/skills/real-prototypes-skill/scripts/CAPTURE-ENHANCEMENTS.md +344 -0
  28. package/.claude/skills/real-prototypes-skill/scripts/IMPLEMENTATION-SUMMARY.md +517 -0
  29. package/.claude/skills/real-prototypes-skill/scripts/QUICK-START.md +229 -0
  30. package/.claude/skills/real-prototypes-skill/scripts/QUICKSTART-layout-analysis.md +148 -0
  31. package/.claude/skills/real-prototypes-skill/scripts/README-analyze-layout.md +407 -0
  32. package/.claude/skills/real-prototypes-skill/scripts/analyze-layout.js +880 -0
  33. package/.claude/skills/real-prototypes-skill/scripts/capture-platform.js +203 -0
  34. package/.claude/skills/real-prototypes-skill/scripts/comprehensive-capture.js +597 -0
  35. package/.claude/skills/real-prototypes-skill/scripts/create-manifest.js +338 -0
  36. package/.claude/skills/real-prototypes-skill/scripts/enterprise-pipeline.js +428 -0
  37. package/.claude/skills/real-prototypes-skill/scripts/extract-tokens.js +468 -0
  38. package/.claude/skills/real-prototypes-skill/scripts/full-site-capture.js +738 -0
  39. package/.claude/skills/real-prototypes-skill/scripts/generate-tailwind-config.js +296 -0
  40. package/.claude/skills/real-prototypes-skill/scripts/integrate-accessibility.sh +161 -0
  41. package/.claude/skills/real-prototypes-skill/scripts/manifest-schema.json +302 -0
  42. package/.claude/skills/real-prototypes-skill/scripts/setup-prototype.sh +167 -0
  43. package/.claude/skills/real-prototypes-skill/scripts/test-analyze-layout.js +338 -0
  44. package/.claude/skills/real-prototypes-skill/scripts/test-validation.js +307 -0
  45. package/.claude/skills/real-prototypes-skill/scripts/validate-accessibility.js +598 -0
  46. package/.claude/skills/real-prototypes-skill/scripts/validate-manifest.js +499 -0
  47. package/.claude/skills/real-prototypes-skill/scripts/validate-output.js +361 -0
  48. package/.claude/skills/real-prototypes-skill/scripts/validate-prerequisites.js +319 -0
  49. package/.claude/skills/real-prototypes-skill/scripts/verify-layout-analysis.sh +77 -0
  50. package/.claude/skills/real-prototypes-skill/templates/dashboard-widget.tsx.template +91 -0
  51. package/.claude/skills/real-prototypes-skill/templates/data-table.tsx.template +193 -0
  52. package/.claude/skills/real-prototypes-skill/templates/form-section.tsx.template +250 -0
  53. package/.claude/skills/real-prototypes-skill/templates/modal-dialog.tsx.template +239 -0
  54. package/.claude/skills/real-prototypes-skill/templates/nav-item.tsx.template +265 -0
  55. package/.claude/skills/real-prototypes-skill/validation/validation-engine.js +559 -0
  56. package/.env.example +74 -0
  57. package/LICENSE +21 -0
  58. package/README.md +444 -0
  59. package/bin/cli.js +319 -0
  60. package/package.json +59 -0
@@ -0,0 +1,880 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Layout & Component Structure Analysis
5
+ *
6
+ * Analyzes captured pages to extract:
7
+ * - Layout patterns (grid, flex, columns)
8
+ * - Component patterns (buttons, forms, cards, etc.)
9
+ * - HTML structure with bounding boxes
10
+ * - UI library detection
11
+ *
12
+ * Usage:
13
+ * node analyze-layout.js [references-dir] [output-file]
14
+ *
15
+ * Output:
16
+ * - structure-manifest.json (complete layout and component analysis)
17
+ * - component-map-[page].json (per-page component mapping)
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const { chromium } = require('playwright');
23
+
24
+ // UI Library signatures
25
+ const UI_LIBRARY_PATTERNS = {
26
+ 'material-ui': [
27
+ /Mui[A-Z][a-zA-Z]+-/,
28
+ /makeStyles/,
29
+ /@mui\//,
30
+ ],
31
+ 'ant-design': [
32
+ /ant-[a-z]+/,
33
+ /anticon/,
34
+ /@ant-design\//,
35
+ ],
36
+ 'bootstrap': [
37
+ /\bbtn\b/,
38
+ /\bcard\b/,
39
+ /\bcontainer\b/,
40
+ /\brow\b/,
41
+ /\bcol-/,
42
+ ],
43
+ 'tailwind': [
44
+ /\b(flex|grid|p-\d+|m-\d+|text-|bg-|border-)/,
45
+ /\bhover:/,
46
+ /\bfocus:/,
47
+ ],
48
+ 'chakra-ui': [
49
+ /chakra-/,
50
+ /@chakra-ui\//,
51
+ ],
52
+ 'semantic-ui': [
53
+ /\bui\s+[a-z]+/,
54
+ /semantic-ui/,
55
+ ],
56
+ };
57
+
58
+ // Component patterns to detect
59
+ const COMPONENT_PATTERNS = {
60
+ button: {
61
+ selectors: ['button', '[role="button"]', 'a.btn', '.button'],
62
+ attributes: ['type', 'disabled', 'aria-label'],
63
+ },
64
+ input: {
65
+ selectors: ['input', 'textarea', 'select', '[contenteditable="true"]'],
66
+ attributes: ['type', 'placeholder', 'required', 'disabled'],
67
+ },
68
+ card: {
69
+ selectors: ['.card', '.panel', '[class*="card"]', '[class*="Card"]'],
70
+ attributes: ['role'],
71
+ },
72
+ modal: {
73
+ selectors: ['[role="dialog"]', '.modal', '[class*="modal"]', '[class*="Modal"]'],
74
+ attributes: ['aria-modal', 'aria-labelledby'],
75
+ },
76
+ navigation: {
77
+ selectors: ['nav', '[role="navigation"]', '.nav', '.navbar', '.sidebar'],
78
+ attributes: ['aria-label'],
79
+ },
80
+ table: {
81
+ selectors: ['table', '[role="table"]', '.table', '[class*="table"]'],
82
+ attributes: ['role'],
83
+ },
84
+ form: {
85
+ selectors: ['form', '[role="form"]'],
86
+ attributes: ['method', 'action'],
87
+ },
88
+ };
89
+
90
+ /**
91
+ * Analyze a single page for layout and components
92
+ */
93
+ async function analyzePage(browser, htmlPath, screenshotPath) {
94
+ const page = await browser.newPage();
95
+
96
+ try {
97
+ // Load the captured HTML
98
+ const htmlContent = fs.readFileSync(htmlPath, 'utf-8');
99
+ await page.setContent(htmlContent, {
100
+ waitUntil: 'domcontentloaded',
101
+ });
102
+
103
+ // Wait a bit for any dynamic styles to apply
104
+ await page.waitForTimeout(1000);
105
+
106
+ const analysis = {
107
+ url: htmlPath,
108
+ screenshot: screenshotPath,
109
+ layout: await analyzeLayout(page),
110
+ components: await analyzeComponents(page),
111
+ library: await detectUILibrary(page),
112
+ structure: await extractHTMLStructure(page),
113
+ landmarks: await extractLandmarks(page),
114
+ };
115
+
116
+ return analysis;
117
+ } catch (error) {
118
+ console.error(`Error analyzing ${htmlPath}:`, error.message);
119
+ return null;
120
+ } finally {
121
+ await page.close();
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Analyze layout patterns (grid, flex, positioning)
127
+ */
128
+ async function analyzeLayout(page) {
129
+ return await page.evaluate(() => {
130
+ const layout = {
131
+ type: 'unknown',
132
+ grid: null,
133
+ flex: null,
134
+ positioning: {
135
+ fixed: [],
136
+ sticky: [],
137
+ absolute: [],
138
+ },
139
+ containers: [],
140
+ sidebar: null,
141
+ header: null,
142
+ footer: null,
143
+ };
144
+
145
+ // Find main container
146
+ const body = document.body;
147
+ const bodyStyles = window.getComputedStyle(body);
148
+
149
+ // Detect overall layout type
150
+ const mainElements = document.querySelectorAll('main, [role="main"], .main, #main');
151
+ const sidebarElements = document.querySelectorAll('aside, .sidebar, [role="complementary"]');
152
+
153
+ if (mainElements.length > 0 && sidebarElements.length > 0) {
154
+ layout.type = 'sidebar-main';
155
+
156
+ // Analyze sidebar
157
+ const sidebar = sidebarElements[0];
158
+ const sidebarStyles = window.getComputedStyle(sidebar);
159
+ const sidebarRect = sidebar.getBoundingClientRect();
160
+
161
+ layout.sidebar = {
162
+ width: sidebarStyles.width,
163
+ position: sidebarStyles.position,
164
+ top: sidebarStyles.top,
165
+ left: sidebarStyles.left,
166
+ boundingBox: {
167
+ x: sidebarRect.x,
168
+ y: sidebarRect.y,
169
+ width: sidebarRect.width,
170
+ height: sidebarRect.height,
171
+ },
172
+ };
173
+ } else if (mainElements.length > 0) {
174
+ layout.type = 'single-main';
175
+ }
176
+
177
+ // Analyze grid systems
178
+ const gridElements = Array.from(document.querySelectorAll('*')).filter(el => {
179
+ const styles = window.getComputedStyle(el);
180
+ return styles.display === 'grid';
181
+ });
182
+
183
+ if (gridElements.length > 0) {
184
+ const gridEl = gridElements[0];
185
+ const gridStyles = window.getComputedStyle(gridEl);
186
+
187
+ layout.grid = {
188
+ columns: gridStyles.gridTemplateColumns,
189
+ rows: gridStyles.gridTemplateRows,
190
+ gap: gridStyles.gap || gridStyles.gridGap,
191
+ columnGap: gridStyles.columnGap || gridStyles.gridColumnGap,
192
+ rowGap: gridStyles.rowGap || gridStyles.gridRowGap,
193
+ };
194
+ }
195
+
196
+ // Analyze flex patterns
197
+ const flexElements = Array.from(document.querySelectorAll('*')).filter(el => {
198
+ const styles = window.getComputedStyle(el);
199
+ return styles.display === 'flex' || styles.display === 'inline-flex';
200
+ });
201
+
202
+ if (flexElements.length > 0) {
203
+ const flexEl = flexElements[0];
204
+ const flexStyles = window.getComputedStyle(flexEl);
205
+
206
+ layout.flex = {
207
+ direction: flexStyles.flexDirection,
208
+ wrap: flexStyles.flexWrap,
209
+ justifyContent: flexStyles.justifyContent,
210
+ alignItems: flexStyles.alignItems,
211
+ gap: flexStyles.gap,
212
+ };
213
+ }
214
+
215
+ // Find positioned elements
216
+ Array.from(document.querySelectorAll('*')).forEach(el => {
217
+ const styles = window.getComputedStyle(el);
218
+ const position = styles.position;
219
+
220
+ if (['fixed', 'sticky', 'absolute'].includes(position)) {
221
+ const rect = el.getBoundingClientRect();
222
+ const info = {
223
+ selector: el.tagName.toLowerCase() + (el.className ? '.' + Array.from(el.classList).join('.') : ''),
224
+ position,
225
+ top: styles.top,
226
+ right: styles.right,
227
+ bottom: styles.bottom,
228
+ left: styles.left,
229
+ zIndex: styles.zIndex,
230
+ boundingBox: {
231
+ x: rect.x,
232
+ y: rect.y,
233
+ width: rect.width,
234
+ height: rect.height,
235
+ },
236
+ };
237
+
238
+ layout.positioning[position].push(info);
239
+ }
240
+ });
241
+
242
+ // Find container patterns
243
+ const containerSelectors = ['.container', '[class*="container"]', 'main', '.main'];
244
+ containerSelectors.forEach(selector => {
245
+ try {
246
+ document.querySelectorAll(selector).forEach(el => {
247
+ const styles = window.getComputedStyle(el);
248
+ const rect = el.getBoundingClientRect();
249
+
250
+ layout.containers.push({
251
+ selector,
252
+ maxWidth: styles.maxWidth,
253
+ width: styles.width,
254
+ padding: styles.padding,
255
+ margin: styles.margin,
256
+ boundingBox: {
257
+ x: rect.x,
258
+ y: rect.y,
259
+ width: rect.width,
260
+ height: rect.height,
261
+ },
262
+ });
263
+ });
264
+ } catch (e) {
265
+ // Invalid selector, skip
266
+ }
267
+ });
268
+
269
+ // Analyze header
270
+ const headerElements = document.querySelectorAll('header, [role="banner"], .header');
271
+ if (headerElements.length > 0) {
272
+ const header = headerElements[0];
273
+ const headerStyles = window.getComputedStyle(header);
274
+ const headerRect = header.getBoundingClientRect();
275
+
276
+ layout.header = {
277
+ height: headerStyles.height,
278
+ position: headerStyles.position,
279
+ backgroundColor: headerStyles.backgroundColor,
280
+ boundingBox: {
281
+ x: headerRect.x,
282
+ y: headerRect.y,
283
+ width: headerRect.width,
284
+ height: headerRect.height,
285
+ },
286
+ };
287
+ }
288
+
289
+ // Analyze footer
290
+ const footerElements = document.querySelectorAll('footer, [role="contentinfo"], .footer');
291
+ if (footerElements.length > 0) {
292
+ const footer = footerElements[0];
293
+ const footerStyles = window.getComputedStyle(footer);
294
+ const footerRect = footer.getBoundingClientRect();
295
+
296
+ layout.footer = {
297
+ height: footerStyles.height,
298
+ position: footerStyles.position,
299
+ backgroundColor: footerStyles.backgroundColor,
300
+ boundingBox: {
301
+ x: footerRect.x,
302
+ y: footerRect.y,
303
+ width: footerRect.width,
304
+ height: footerRect.height,
305
+ },
306
+ };
307
+ }
308
+
309
+ return layout;
310
+ });
311
+ }
312
+
313
+ /**
314
+ * Analyze components (buttons, forms, cards, etc.)
315
+ */
316
+ async function analyzeComponents(page) {
317
+ return await page.evaluate((patterns) => {
318
+ const components = {};
319
+
320
+ Object.entries(patterns).forEach(([type, config]) => {
321
+ const elements = [];
322
+
323
+ config.selectors.forEach(selector => {
324
+ try {
325
+ document.querySelectorAll(selector).forEach(el => {
326
+ const styles = window.getComputedStyle(el);
327
+ const rect = el.getBoundingClientRect();
328
+
329
+ // Extract relevant styles
330
+ const componentInfo = {
331
+ selector,
332
+ tag: el.tagName.toLowerCase(),
333
+ classes: Array.from(el.classList),
334
+ styles: {
335
+ display: styles.display,
336
+ padding: styles.padding,
337
+ margin: styles.margin,
338
+ border: styles.border,
339
+ borderRadius: styles.borderRadius,
340
+ backgroundColor: styles.backgroundColor,
341
+ color: styles.color,
342
+ fontSize: styles.fontSize,
343
+ fontWeight: styles.fontWeight,
344
+ fontFamily: styles.fontFamily,
345
+ boxShadow: styles.boxShadow,
346
+ width: styles.width,
347
+ height: styles.height,
348
+ },
349
+ boundingBox: {
350
+ x: rect.x,
351
+ y: rect.y,
352
+ width: rect.width,
353
+ height: rect.height,
354
+ },
355
+ };
356
+
357
+ // Extract configured attributes
358
+ const attributes = {};
359
+ config.attributes.forEach(attr => {
360
+ if (el.hasAttribute(attr)) {
361
+ attributes[attr] = el.getAttribute(attr);
362
+ }
363
+ });
364
+ componentInfo.attributes = attributes;
365
+
366
+ // Detect variants (for buttons)
367
+ if (type === 'button') {
368
+ componentInfo.variant = detectButtonVariant(el);
369
+ componentInfo.size = detectButtonSize(rect.height);
370
+ }
371
+
372
+ elements.push(componentInfo);
373
+ });
374
+ } catch (e) {
375
+ // Invalid selector, skip
376
+ }
377
+ });
378
+
379
+ if (elements.length > 0) {
380
+ components[type] = {
381
+ count: elements.length,
382
+ variants: groupByVariants(elements),
383
+ examples: elements.slice(0, 5), // First 5 examples
384
+ };
385
+ }
386
+ });
387
+
388
+ return components;
389
+
390
+ // Helper functions
391
+ function detectButtonVariant(button) {
392
+ const classes = Array.from(button.classList).join(' ');
393
+ const styles = window.getComputedStyle(button);
394
+
395
+ if (classes.match(/primary|btn-primary/i)) return 'primary';
396
+ if (classes.match(/secondary|btn-secondary/i)) return 'secondary';
397
+ if (classes.match(/ghost|outline|btn-outline/i)) return 'ghost';
398
+ if (classes.match(/danger|error|btn-danger/i)) return 'danger';
399
+ if (classes.match(/success|btn-success/i)) return 'success';
400
+
401
+ // Detect by background color
402
+ const bgColor = styles.backgroundColor;
403
+ if (bgColor === 'transparent' || bgColor === 'rgba(0, 0, 0, 0)') {
404
+ return 'ghost';
405
+ }
406
+
407
+ return 'default';
408
+ }
409
+
410
+ function detectButtonSize(height) {
411
+ if (height < 30) return 'sm';
412
+ if (height > 45) return 'lg';
413
+ return 'md';
414
+ }
415
+
416
+ function groupByVariants(elements) {
417
+ const variantMap = {};
418
+
419
+ elements.forEach(el => {
420
+ const key = el.variant || 'default';
421
+ if (!variantMap[key]) {
422
+ variantMap[key] = [];
423
+ }
424
+ variantMap[key].push(el);
425
+ });
426
+
427
+ return Object.entries(variantMap).map(([variant, items]) => ({
428
+ variant,
429
+ count: items.length,
430
+ example: items[0],
431
+ }));
432
+ }
433
+ }, COMPONENT_PATTERNS);
434
+ }
435
+
436
+ /**
437
+ * Detect UI library
438
+ */
439
+ async function detectUILibrary(page) {
440
+ return await page.evaluate((patterns) => {
441
+ const detectedLibraries = [];
442
+
443
+ // Check HTML classes
444
+ const allClasses = Array.from(document.querySelectorAll('*'))
445
+ .flatMap(el => Array.from(el.classList))
446
+ .join(' ');
447
+
448
+ // Check for library patterns
449
+ Object.entries(patterns).forEach(([library, regexPatterns]) => {
450
+ const matches = regexPatterns.some(pattern => pattern.test(allClasses));
451
+ if (matches) {
452
+ detectedLibraries.push(library);
453
+ }
454
+ });
455
+
456
+ // Check for library-specific elements
457
+ if (document.querySelector('[class*="Mui"]')) {
458
+ detectedLibraries.push('material-ui');
459
+ }
460
+ if (document.querySelector('[class*="ant-"]')) {
461
+ detectedLibraries.push('ant-design');
462
+ }
463
+ if (document.querySelector('.chakra-')) {
464
+ detectedLibraries.push('chakra-ui');
465
+ }
466
+
467
+ // Return most likely library or custom
468
+ if (detectedLibraries.length === 0) {
469
+ return 'custom';
470
+ }
471
+
472
+ // If multiple libraries detected, return the most prevalent
473
+ const libraryCounts = {};
474
+ detectedLibraries.forEach(lib => {
475
+ libraryCounts[lib] = (libraryCounts[lib] || 0) + 1;
476
+ });
477
+
478
+ const mostPrevalent = Object.entries(libraryCounts).sort((a, b) => b[1] - a[1])[0][0];
479
+
480
+ return {
481
+ primary: mostPrevalent,
482
+ detected: [...new Set(detectedLibraries)],
483
+ };
484
+ }, UI_LIBRARY_PATTERNS);
485
+ }
486
+
487
+ /**
488
+ * Extract HTML structure with semantic preservation
489
+ */
490
+ async function extractHTMLStructure(page) {
491
+ return await page.evaluate(() => {
492
+ function extractElement(el, depth = 0) {
493
+ if (depth > 10) return null; // Limit depth to avoid huge structures
494
+
495
+ const styles = window.getComputedStyle(el);
496
+ const rect = el.getBoundingClientRect();
497
+
498
+ const node = {
499
+ tag: el.tagName.toLowerCase(),
500
+ classes: Array.from(el.classList),
501
+ id: el.id || null,
502
+ attributes: {},
503
+ styles: {
504
+ display: styles.display,
505
+ position: styles.position,
506
+ },
507
+ boundingBox: {
508
+ x: Math.round(rect.x),
509
+ y: Math.round(rect.y),
510
+ width: Math.round(rect.width),
511
+ height: Math.round(rect.height),
512
+ },
513
+ children: [],
514
+ };
515
+
516
+ // Extract important attributes
517
+ const importantAttrs = ['role', 'aria-label', 'aria-labelledby', 'aria-describedby', 'data-testid'];
518
+ importantAttrs.forEach(attr => {
519
+ if (el.hasAttribute(attr)) {
520
+ node.attributes[attr] = el.getAttribute(attr);
521
+ }
522
+ });
523
+
524
+ // Extract data attributes
525
+ Array.from(el.attributes).forEach(attr => {
526
+ if (attr.name.startsWith('data-')) {
527
+ node.attributes[attr.name] = attr.value;
528
+ }
529
+ });
530
+
531
+ // Recursively extract children (only for structural elements)
532
+ const structuralTags = ['header', 'nav', 'main', 'aside', 'footer', 'section', 'article', 'div'];
533
+ if (structuralTags.includes(node.tag)) {
534
+ Array.from(el.children).forEach(child => {
535
+ const childNode = extractElement(child, depth + 1);
536
+ if (childNode) {
537
+ node.children.push(childNode);
538
+ }
539
+ });
540
+ }
541
+
542
+ return node;
543
+ }
544
+
545
+ return extractElement(document.body);
546
+ });
547
+ }
548
+
549
+ /**
550
+ * Extract landmark elements
551
+ */
552
+ async function extractLandmarks(page) {
553
+ return await page.evaluate(() => {
554
+ const landmarks = {
555
+ header: null,
556
+ navigation: [],
557
+ main: null,
558
+ aside: [],
559
+ footer: null,
560
+ };
561
+
562
+ // Header
563
+ const header = document.querySelector('header, [role="banner"]');
564
+ if (header) {
565
+ const rect = header.getBoundingClientRect();
566
+ landmarks.header = {
567
+ selector: header.tagName.toLowerCase() + (header.className ? '.' + Array.from(header.classList).join('.') : ''),
568
+ boundingBox: {
569
+ x: rect.x,
570
+ y: rect.y,
571
+ width: rect.width,
572
+ height: rect.height,
573
+ },
574
+ };
575
+ }
576
+
577
+ // Navigation
578
+ document.querySelectorAll('nav, [role="navigation"]').forEach(nav => {
579
+ const rect = nav.getBoundingClientRect();
580
+ landmarks.navigation.push({
581
+ selector: nav.tagName.toLowerCase() + (nav.className ? '.' + Array.from(nav.classList).join('.') : ''),
582
+ boundingBox: {
583
+ x: rect.x,
584
+ y: rect.y,
585
+ width: rect.width,
586
+ height: rect.height,
587
+ },
588
+ });
589
+ });
590
+
591
+ // Main
592
+ const main = document.querySelector('main, [role="main"]');
593
+ if (main) {
594
+ const rect = main.getBoundingClientRect();
595
+ landmarks.main = {
596
+ selector: main.tagName.toLowerCase() + (main.className ? '.' + Array.from(main.classList).join('.') : ''),
597
+ boundingBox: {
598
+ x: rect.x,
599
+ y: rect.y,
600
+ width: rect.width,
601
+ height: rect.height,
602
+ },
603
+ };
604
+ }
605
+
606
+ // Aside
607
+ document.querySelectorAll('aside, [role="complementary"]').forEach(aside => {
608
+ const rect = aside.getBoundingClientRect();
609
+ landmarks.aside.push({
610
+ selector: aside.tagName.toLowerCase() + (aside.className ? '.' + Array.from(aside.classList).join('.') : ''),
611
+ boundingBox: {
612
+ x: rect.x,
613
+ y: rect.y,
614
+ width: rect.width,
615
+ height: rect.height,
616
+ },
617
+ });
618
+ });
619
+
620
+ // Footer
621
+ const footer = document.querySelector('footer, [role="contentinfo"]');
622
+ if (footer) {
623
+ const rect = footer.getBoundingClientRect();
624
+ landmarks.footer = {
625
+ selector: footer.tagName.toLowerCase() + (footer.className ? '.' + Array.from(footer.classList).join('.') : ''),
626
+ boundingBox: {
627
+ x: rect.x,
628
+ y: rect.y,
629
+ width: rect.width,
630
+ height: rect.height,
631
+ },
632
+ };
633
+ }
634
+
635
+ return landmarks;
636
+ });
637
+ }
638
+
639
+ /**
640
+ * Generate structure manifest from all analyzed pages
641
+ */
642
+ function generateStructureManifest(pageAnalyses) {
643
+ const manifest = {
644
+ generatedAt: new Date().toISOString(),
645
+ pagesAnalyzed: pageAnalyses.length,
646
+ layout: null,
647
+ components: {},
648
+ library: null,
649
+ commonPatterns: {},
650
+ };
651
+
652
+ // Aggregate layout patterns (use most common)
653
+ const layouts = pageAnalyses.map(p => p.layout).filter(Boolean);
654
+ if (layouts.length > 0) {
655
+ manifest.layout = aggregateLayouts(layouts);
656
+ }
657
+
658
+ // Aggregate component patterns
659
+ pageAnalyses.forEach(page => {
660
+ if (page.components) {
661
+ Object.entries(page.components).forEach(([type, data]) => {
662
+ if (!manifest.components[type]) {
663
+ manifest.components[type] = {
664
+ totalCount: 0,
665
+ variants: {},
666
+ examples: [],
667
+ };
668
+ }
669
+
670
+ manifest.components[type].totalCount += data.count;
671
+
672
+ // Aggregate variants
673
+ if (data.variants) {
674
+ data.variants.forEach(variant => {
675
+ const key = variant.variant;
676
+ if (!manifest.components[type].variants[key]) {
677
+ manifest.components[type].variants[key] = {
678
+ count: 0,
679
+ example: variant.example,
680
+ };
681
+ }
682
+ manifest.components[type].variants[key].count += variant.count;
683
+ });
684
+ }
685
+
686
+ // Keep first few examples
687
+ if (manifest.components[type].examples.length < 3 && data.examples) {
688
+ manifest.components[type].examples.push(...data.examples.slice(0, 3 - manifest.components[type].examples.length));
689
+ }
690
+ });
691
+ }
692
+ });
693
+
694
+ // Detect primary UI library (most common across pages)
695
+ const libraries = pageAnalyses.map(p => p.library).filter(Boolean);
696
+ manifest.library = detectPrimaryLibrary(libraries);
697
+
698
+ return manifest;
699
+ }
700
+
701
+ /**
702
+ * Aggregate layout data from multiple pages
703
+ */
704
+ function aggregateLayouts(layouts) {
705
+ // Find most common layout type
706
+ const types = layouts.map(l => l.type);
707
+ const typeCount = {};
708
+ types.forEach(type => {
709
+ typeCount[type] = (typeCount[type] || 0) + 1;
710
+ });
711
+ const mostCommonType = Object.entries(typeCount).sort((a, b) => b[1] - a[1])[0][0];
712
+
713
+ // Find sidebar data (if sidebar-main layout)
714
+ const sidebar = layouts.find(l => l.sidebar)?.sidebar || null;
715
+
716
+ // Find grid data (use first found)
717
+ const grid = layouts.find(l => l.grid)?.grid || null;
718
+
719
+ // Find flex data (use first found)
720
+ const flex = layouts.find(l => l.flex)?.flex || null;
721
+
722
+ // Aggregate container widths
723
+ const containers = layouts.flatMap(l => l.containers || []);
724
+ const maxWidths = containers.map(c => c.maxWidth).filter(Boolean);
725
+ const mostCommonMaxWidth = maxWidths.length > 0 ? mode(maxWidths) : null;
726
+
727
+ return {
728
+ type: mostCommonType,
729
+ sidebar,
730
+ grid,
731
+ flex,
732
+ containers: {
733
+ maxWidth: mostCommonMaxWidth,
734
+ },
735
+ };
736
+ }
737
+
738
+ /**
739
+ * Detect primary UI library
740
+ */
741
+ function detectPrimaryLibrary(libraries) {
742
+ if (libraries.length === 0) return 'custom';
743
+
744
+ const libNames = libraries.map(lib => {
745
+ if (typeof lib === 'string') return lib;
746
+ return lib.primary || lib;
747
+ });
748
+
749
+ const counts = {};
750
+ libNames.forEach(lib => {
751
+ counts[lib] = (counts[lib] || 0) + 1;
752
+ });
753
+
754
+ const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
755
+ return sorted[0][0];
756
+ }
757
+
758
+ /**
759
+ * Find mode (most common value) in array
760
+ */
761
+ function mode(arr) {
762
+ const counts = {};
763
+ arr.forEach(val => {
764
+ counts[val] = (counts[val] || 0) + 1;
765
+ });
766
+ const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
767
+ return sorted[0][0];
768
+ }
769
+
770
+ /**
771
+ * Main execution
772
+ */
773
+ async function main() {
774
+ const args = process.argv.slice(2);
775
+ const referencesDir = args[0] || '../../../references';
776
+ const outputFile = args[1] || path.join(referencesDir, 'structure-manifest.json');
777
+
778
+ console.log('šŸ” Analyzing layout and component structure...');
779
+ console.log(`References: ${referencesDir}`);
780
+ console.log(`Output: ${outputFile}`);
781
+
782
+ // Check if references directory exists
783
+ if (!fs.existsSync(referencesDir)) {
784
+ console.error(`āŒ Error: References directory not found at ${referencesDir}`);
785
+ console.error('Run full-site-capture.js first to capture pages.');
786
+ process.exit(1);
787
+ }
788
+
789
+ const htmlDir = path.join(referencesDir, 'html');
790
+ const screenshotsDir = path.join(referencesDir, 'screenshots');
791
+
792
+ if (!fs.existsSync(htmlDir)) {
793
+ console.error(`āŒ Error: HTML directory not found at ${htmlDir}`);
794
+ process.exit(1);
795
+ }
796
+
797
+ // Get all HTML files
798
+ const htmlFiles = fs.readdirSync(htmlDir)
799
+ .filter(f => f.endsWith('.html'))
800
+ .map(f => path.join(htmlDir, f));
801
+
802
+ if (htmlFiles.length === 0) {
803
+ console.error('āŒ Error: No HTML files found to analyze');
804
+ process.exit(1);
805
+ }
806
+
807
+ console.log(`\nšŸ“„ Found ${htmlFiles.length} pages to analyze\n`);
808
+
809
+ // Launch browser
810
+ const browser = await chromium.launch();
811
+
812
+ // Analyze each page
813
+ const pageAnalyses = [];
814
+ for (const htmlFile of htmlFiles) {
815
+ const fileName = path.basename(htmlFile, '.html');
816
+ const screenshotFile = path.join(screenshotsDir, `${fileName}.png`);
817
+
818
+ console.log(`Analyzing: ${fileName}...`);
819
+
820
+ const analysis = await analyzePage(browser, htmlFile, screenshotFile);
821
+ if (analysis) {
822
+ pageAnalyses.push(analysis);
823
+
824
+ // Save per-page component map
825
+ const componentMapFile = path.join(referencesDir, `component-map-${fileName}.json`);
826
+ fs.writeFileSync(componentMapFile, JSON.stringify({
827
+ page: fileName,
828
+ layout: analysis.layout,
829
+ components: analysis.components,
830
+ library: analysis.library,
831
+ landmarks: analysis.landmarks,
832
+ }, null, 2));
833
+
834
+ console.log(` āœ“ Layout: ${analysis.layout.type}`);
835
+ console.log(` āœ“ Library: ${typeof analysis.library === 'string' ? analysis.library : analysis.library.primary}`);
836
+ console.log(` āœ“ Components: ${Object.keys(analysis.components).length} types detected`);
837
+ console.log(` āœ“ Component map saved: ${componentMapFile}\n`);
838
+ }
839
+ }
840
+
841
+ await browser.close();
842
+
843
+ // Generate structure manifest
844
+ console.log('\nšŸ“¦ Generating structure manifest...');
845
+ const structureManifest = generateStructureManifest(pageAnalyses);
846
+
847
+ // Save structure manifest
848
+ fs.writeFileSync(outputFile, JSON.stringify(structureManifest, null, 2));
849
+
850
+ console.log('\nāœ… Analysis complete!');
851
+ console.log(`\nStructure manifest: ${outputFile}`);
852
+ console.log(`Pages analyzed: ${pageAnalyses.length}`);
853
+ console.log(`Primary layout: ${structureManifest.layout?.type || 'unknown'}`);
854
+ console.log(`UI library: ${structureManifest.library}`);
855
+ console.log(`Component types: ${Object.keys(structureManifest.components).join(', ')}`);
856
+
857
+ // Print component summary
858
+ console.log('\nšŸ“Š Component Summary:');
859
+ Object.entries(structureManifest.components).forEach(([type, data]) => {
860
+ console.log(` ${type}: ${data.totalCount} instances`);
861
+ if (data.variants && Object.keys(data.variants).length > 0) {
862
+ console.log(` Variants: ${Object.keys(data.variants).join(', ')}`);
863
+ }
864
+ });
865
+
866
+ console.log('\nšŸ’” Next steps:');
867
+ console.log(' 1. Review structure-manifest.json for layout patterns');
868
+ console.log(' 2. Review component-map-*.json for per-page component details');
869
+ console.log(' 3. Use this data to generate pixel-perfect prototypes');
870
+ }
871
+
872
+ // Run if called directly
873
+ if (require.main === module) {
874
+ main().catch(error => {
875
+ console.error('āŒ Fatal error:', error);
876
+ process.exit(1);
877
+ });
878
+ }
879
+
880
+ module.exports = { analyzePage, generateStructureManifest };