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.
- package/.claude/skills/agent-browser-skill/SKILL.md +252 -0
- package/.claude/skills/real-prototypes-skill/.gitignore +188 -0
- package/.claude/skills/real-prototypes-skill/ACCESSIBILITY.md +668 -0
- package/.claude/skills/real-prototypes-skill/INSTALL.md +259 -0
- package/.claude/skills/real-prototypes-skill/LICENSE +21 -0
- package/.claude/skills/real-prototypes-skill/PUBLISH.md +310 -0
- package/.claude/skills/real-prototypes-skill/QUICKSTART.md +240 -0
- package/.claude/skills/real-prototypes-skill/README.md +442 -0
- package/.claude/skills/real-prototypes-skill/SKILL.md +329 -0
- package/.claude/skills/real-prototypes-skill/capture/capture-engine.js +1153 -0
- package/.claude/skills/real-prototypes-skill/capture/config.schema.json +170 -0
- package/.claude/skills/real-prototypes-skill/cli.js +596 -0
- package/.claude/skills/real-prototypes-skill/docs/TROUBLESHOOTING.md +278 -0
- package/.claude/skills/real-prototypes-skill/docs/schemas/capture-config.md +167 -0
- package/.claude/skills/real-prototypes-skill/docs/schemas/design-tokens.md +183 -0
- package/.claude/skills/real-prototypes-skill/docs/schemas/manifest.md +169 -0
- package/.claude/skills/real-prototypes-skill/examples/CLAUDE.md.example +73 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/CLAUDE.md +136 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/FEATURES.md +222 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/README.md +82 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/references/design-tokens.json +87 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/references/screenshots/homepage-viewport.png +0 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/references/screenshots/prototype-chatbot-final.png +0 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/references/screenshots/prototype-fullpage-v2.png +0 -0
- package/.claude/skills/real-prototypes-skill/references/accessibility-fixes.md +298 -0
- package/.claude/skills/real-prototypes-skill/references/accessibility-report.json +253 -0
- package/.claude/skills/real-prototypes-skill/scripts/CAPTURE-ENHANCEMENTS.md +344 -0
- package/.claude/skills/real-prototypes-skill/scripts/IMPLEMENTATION-SUMMARY.md +517 -0
- package/.claude/skills/real-prototypes-skill/scripts/QUICK-START.md +229 -0
- package/.claude/skills/real-prototypes-skill/scripts/QUICKSTART-layout-analysis.md +148 -0
- package/.claude/skills/real-prototypes-skill/scripts/README-analyze-layout.md +407 -0
- package/.claude/skills/real-prototypes-skill/scripts/analyze-layout.js +880 -0
- package/.claude/skills/real-prototypes-skill/scripts/capture-platform.js +203 -0
- package/.claude/skills/real-prototypes-skill/scripts/comprehensive-capture.js +597 -0
- package/.claude/skills/real-prototypes-skill/scripts/create-manifest.js +338 -0
- package/.claude/skills/real-prototypes-skill/scripts/enterprise-pipeline.js +428 -0
- package/.claude/skills/real-prototypes-skill/scripts/extract-tokens.js +468 -0
- package/.claude/skills/real-prototypes-skill/scripts/full-site-capture.js +738 -0
- package/.claude/skills/real-prototypes-skill/scripts/generate-tailwind-config.js +296 -0
- package/.claude/skills/real-prototypes-skill/scripts/integrate-accessibility.sh +161 -0
- package/.claude/skills/real-prototypes-skill/scripts/manifest-schema.json +302 -0
- package/.claude/skills/real-prototypes-skill/scripts/setup-prototype.sh +167 -0
- package/.claude/skills/real-prototypes-skill/scripts/test-analyze-layout.js +338 -0
- package/.claude/skills/real-prototypes-skill/scripts/test-validation.js +307 -0
- package/.claude/skills/real-prototypes-skill/scripts/validate-accessibility.js +598 -0
- package/.claude/skills/real-prototypes-skill/scripts/validate-manifest.js +499 -0
- package/.claude/skills/real-prototypes-skill/scripts/validate-output.js +361 -0
- package/.claude/skills/real-prototypes-skill/scripts/validate-prerequisites.js +319 -0
- package/.claude/skills/real-prototypes-skill/scripts/verify-layout-analysis.sh +77 -0
- package/.claude/skills/real-prototypes-skill/templates/dashboard-widget.tsx.template +91 -0
- package/.claude/skills/real-prototypes-skill/templates/data-table.tsx.template +193 -0
- package/.claude/skills/real-prototypes-skill/templates/form-section.tsx.template +250 -0
- package/.claude/skills/real-prototypes-skill/templates/modal-dialog.tsx.template +239 -0
- package/.claude/skills/real-prototypes-skill/templates/nav-item.tsx.template +265 -0
- package/.claude/skills/real-prototypes-skill/validation/validation-engine.js +559 -0
- package/.env.example +74 -0
- package/LICENSE +21 -0
- package/README.md +444 -0
- package/bin/cli.js +319 -0
- 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 };
|