real-prototypes-skill 0.1.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 +375 -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,597 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* COMPREHENSIVE PLATFORM CAPTURE
|
|
4
|
+
*
|
|
5
|
+
* This script captures ALL states of a platform by:
|
|
6
|
+
* 1. Capturing every page
|
|
7
|
+
* 2. Clicking every interactive element (buttons, tabs, dropdowns)
|
|
8
|
+
* 3. Capturing each state change
|
|
9
|
+
* 4. Extracting design tokens from ALL captured HTML
|
|
10
|
+
*
|
|
11
|
+
* Output Structure:
|
|
12
|
+
* references/
|
|
13
|
+
* ├── screenshots/
|
|
14
|
+
* │ ├── page-name.png # Base state
|
|
15
|
+
* │ ├── page-name--button-1.png # After clicking button 1
|
|
16
|
+
* │ ├── page-name--modal-open.png # Modal state
|
|
17
|
+
* │ └── ...
|
|
18
|
+
* ├── html/
|
|
19
|
+
* │ └── (same structure as screenshots)
|
|
20
|
+
* ├── design-tokens.json # Extracted colors, fonts, spacing
|
|
21
|
+
* ├── component-styles.json # Button, input, card styles
|
|
22
|
+
* ├── interactions.json # Map of all interactive elements
|
|
23
|
+
* └── manifest.json # Complete capture manifest
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const { execSync } = require('child_process');
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
// Configuration
|
|
31
|
+
const CONFIG = {
|
|
32
|
+
outputDir: 'references',
|
|
33
|
+
screenshotDir: 'references/screenshots',
|
|
34
|
+
htmlDir: 'references/html',
|
|
35
|
+
waitAfterClick: 1000,
|
|
36
|
+
waitAfterLoad: 2000,
|
|
37
|
+
maxInteractionsPerPage: 50,
|
|
38
|
+
viewport: { width: 1920, height: 1080 }
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Agent-browser wrapper
|
|
42
|
+
function browser(cmd) {
|
|
43
|
+
try {
|
|
44
|
+
const result = execSync(`agent-browser ${cmd}`, {
|
|
45
|
+
encoding: 'utf8',
|
|
46
|
+
maxBuffer: 50 * 1024 * 1024 // 50MB buffer for large outputs
|
|
47
|
+
});
|
|
48
|
+
return result.trim();
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error(`Browser command failed: ${cmd}`);
|
|
51
|
+
console.error(error.message);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Ensure directories exist
|
|
57
|
+
function ensureDirectories() {
|
|
58
|
+
[CONFIG.outputDir, CONFIG.screenshotDir, CONFIG.htmlDir].forEach(dir => {
|
|
59
|
+
if (!fs.existsSync(dir)) {
|
|
60
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Extract all interactive elements from current page
|
|
66
|
+
function getInteractiveElements() {
|
|
67
|
+
const snapshot = browser('snapshot -i --json');
|
|
68
|
+
if (!snapshot) return [];
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const data = JSON.parse(snapshot);
|
|
72
|
+
const elements = [];
|
|
73
|
+
|
|
74
|
+
// Recursively find all interactive elements
|
|
75
|
+
function traverse(node, path = []) {
|
|
76
|
+
if (!node) return;
|
|
77
|
+
|
|
78
|
+
const isInteractive =
|
|
79
|
+
node.role === 'button' ||
|
|
80
|
+
node.role === 'link' ||
|
|
81
|
+
node.role === 'tab' ||
|
|
82
|
+
node.role === 'menuitem' ||
|
|
83
|
+
node.role === 'checkbox' ||
|
|
84
|
+
node.role === 'radio' ||
|
|
85
|
+
node.role === 'combobox' ||
|
|
86
|
+
node.role === 'switch' ||
|
|
87
|
+
(node.tag === 'button') ||
|
|
88
|
+
(node.tag === 'a' && node.attributes?.href) ||
|
|
89
|
+
(node.attributes?.onclick) ||
|
|
90
|
+
(node.attributes?.['data-action']);
|
|
91
|
+
|
|
92
|
+
if (isInteractive && node.ref) {
|
|
93
|
+
elements.push({
|
|
94
|
+
ref: node.ref,
|
|
95
|
+
role: node.role,
|
|
96
|
+
name: node.name || node.text || `${node.role}-${elements.length}`,
|
|
97
|
+
tag: node.tag,
|
|
98
|
+
path: [...path, node.role || node.tag]
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Traverse children
|
|
103
|
+
if (node.children) {
|
|
104
|
+
node.children.forEach((child, i) => {
|
|
105
|
+
traverse(child, [...path, `${node.role || node.tag}[${i}]`]);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
traverse(data);
|
|
111
|
+
return elements;
|
|
112
|
+
} catch (e) {
|
|
113
|
+
console.error('Failed to parse snapshot:', e.message);
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Capture current state (screenshot + HTML)
|
|
119
|
+
function captureState(name) {
|
|
120
|
+
const screenshotPath = path.join(CONFIG.screenshotDir, `${name}.png`);
|
|
121
|
+
const htmlPath = path.join(CONFIG.htmlDir, `${name}.html`);
|
|
122
|
+
|
|
123
|
+
// Capture screenshot
|
|
124
|
+
browser(`screenshot --full "${screenshotPath}"`);
|
|
125
|
+
|
|
126
|
+
// Capture HTML
|
|
127
|
+
const html = browser('eval "document.documentElement.outerHTML"');
|
|
128
|
+
if (html) {
|
|
129
|
+
fs.writeFileSync(htmlPath, html);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(` ✓ Captured: ${name}`);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
screenshot: screenshotPath,
|
|
136
|
+
html: htmlPath,
|
|
137
|
+
timestamp: new Date().toISOString()
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Extract colors from HTML content
|
|
142
|
+
function extractColorsFromHTML(htmlContent) {
|
|
143
|
+
const colors = new Map();
|
|
144
|
+
|
|
145
|
+
// Match hex colors
|
|
146
|
+
const hexMatches = htmlContent.match(/#[0-9a-fA-F]{3,8}/g) || [];
|
|
147
|
+
hexMatches.forEach(color => {
|
|
148
|
+
const normalized = color.toLowerCase();
|
|
149
|
+
colors.set(normalized, (colors.get(normalized) || 0) + 1);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Match rgb/rgba colors
|
|
153
|
+
const rgbMatches = htmlContent.match(/rgba?\([^)]+\)/g) || [];
|
|
154
|
+
rgbMatches.forEach(color => {
|
|
155
|
+
colors.set(color, (colors.get(color) || 0) + 1);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return colors;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Extract font information from HTML
|
|
162
|
+
function extractFontsFromHTML(htmlContent) {
|
|
163
|
+
const fonts = new Set();
|
|
164
|
+
|
|
165
|
+
// Match font-family declarations
|
|
166
|
+
const fontMatches = htmlContent.match(/font-family:\s*([^;}"]+)/gi) || [];
|
|
167
|
+
fontMatches.forEach(match => {
|
|
168
|
+
const font = match.replace(/font-family:\s*/i, '').trim();
|
|
169
|
+
fonts.add(font);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return Array.from(fonts);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Extract component styles
|
|
176
|
+
function extractComponentStyles(htmlContent) {
|
|
177
|
+
const styles = {
|
|
178
|
+
buttons: [],
|
|
179
|
+
inputs: [],
|
|
180
|
+
cards: [],
|
|
181
|
+
tables: []
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// This would be more sophisticated in production
|
|
185
|
+
// For now, extract inline styles from specific element types
|
|
186
|
+
|
|
187
|
+
// Button styles
|
|
188
|
+
const buttonStyleMatches = htmlContent.match(/<button[^>]*style="([^"]+)"/gi) || [];
|
|
189
|
+
buttonStyleMatches.forEach(match => {
|
|
190
|
+
const style = match.match(/style="([^"]+)"/);
|
|
191
|
+
if (style) styles.buttons.push(style[1]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Input styles
|
|
195
|
+
const inputStyleMatches = htmlContent.match(/<input[^>]*style="([^"]+)"/gi) || [];
|
|
196
|
+
inputStyleMatches.forEach(match => {
|
|
197
|
+
const style = match.match(/style="([^"]+)"/);
|
|
198
|
+
if (style) styles.inputs.push(style[1]);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return styles;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Generate design tokens from all captured HTML
|
|
205
|
+
function generateDesignTokens(htmlFiles) {
|
|
206
|
+
console.log('\n📊 Extracting design tokens from captured HTML...');
|
|
207
|
+
|
|
208
|
+
const allColors = new Map();
|
|
209
|
+
const allFonts = new Set();
|
|
210
|
+
const allComponentStyles = {
|
|
211
|
+
buttons: [],
|
|
212
|
+
inputs: [],
|
|
213
|
+
cards: [],
|
|
214
|
+
tables: []
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
htmlFiles.forEach(htmlPath => {
|
|
218
|
+
if (fs.existsSync(htmlPath)) {
|
|
219
|
+
const content = fs.readFileSync(htmlPath, 'utf8');
|
|
220
|
+
|
|
221
|
+
// Extract colors
|
|
222
|
+
const colors = extractColorsFromHTML(content);
|
|
223
|
+
colors.forEach((count, color) => {
|
|
224
|
+
allColors.set(color, (allColors.get(color) || 0) + count);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Extract fonts
|
|
228
|
+
const fonts = extractFontsFromHTML(content);
|
|
229
|
+
fonts.forEach(font => allFonts.add(font));
|
|
230
|
+
|
|
231
|
+
// Extract component styles
|
|
232
|
+
const compStyles = extractComponentStyles(content);
|
|
233
|
+
Object.keys(compStyles).forEach(key => {
|
|
234
|
+
allComponentStyles[key].push(...compStyles[key]);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Sort colors by frequency
|
|
240
|
+
const sortedColors = Array.from(allColors.entries())
|
|
241
|
+
.sort((a, b) => b[1] - a[1]);
|
|
242
|
+
|
|
243
|
+
// Categorize colors
|
|
244
|
+
const categorizedColors = categorizeColors(sortedColors);
|
|
245
|
+
|
|
246
|
+
const tokens = {
|
|
247
|
+
extractedAt: new Date().toISOString(),
|
|
248
|
+
totalColorsFound: sortedColors.length,
|
|
249
|
+
colors: categorizedColors,
|
|
250
|
+
fonts: {
|
|
251
|
+
families: Array.from(allFonts),
|
|
252
|
+
primary: Array.from(allFonts)[0] || 'Inter, system-ui'
|
|
253
|
+
},
|
|
254
|
+
rawColors: sortedColors.slice(0, 100) // Top 100 colors with counts
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Write design tokens
|
|
258
|
+
const tokensPath = path.join(CONFIG.outputDir, 'design-tokens.json');
|
|
259
|
+
fs.writeFileSync(tokensPath, JSON.stringify(tokens, null, 2));
|
|
260
|
+
console.log(` ✓ Written: ${tokensPath}`);
|
|
261
|
+
|
|
262
|
+
// Write component styles
|
|
263
|
+
const stylesPath = path.join(CONFIG.outputDir, 'component-styles.json');
|
|
264
|
+
fs.writeFileSync(stylesPath, JSON.stringify(allComponentStyles, null, 2));
|
|
265
|
+
console.log(` ✓ Written: ${stylesPath}`);
|
|
266
|
+
|
|
267
|
+
return tokens;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Categorize colors by their likely usage
|
|
271
|
+
function categorizeColors(sortedColors) {
|
|
272
|
+
const colors = {
|
|
273
|
+
// Will be filled based on analysis
|
|
274
|
+
primary: null,
|
|
275
|
+
secondary: null,
|
|
276
|
+
background: {},
|
|
277
|
+
text: {},
|
|
278
|
+
border: {},
|
|
279
|
+
status: {},
|
|
280
|
+
sidebar: {},
|
|
281
|
+
all: {}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
sortedColors.forEach(([color, count]) => {
|
|
285
|
+
// Store all colors
|
|
286
|
+
colors.all[color] = count;
|
|
287
|
+
|
|
288
|
+
// Categorize based on color characteristics
|
|
289
|
+
const hex = color.startsWith('#') ? color : null;
|
|
290
|
+
if (!hex) return;
|
|
291
|
+
|
|
292
|
+
const rgb = hexToRgb(hex);
|
|
293
|
+
if (!rgb) return;
|
|
294
|
+
|
|
295
|
+
const brightness = (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
|
|
296
|
+
|
|
297
|
+
// Very dark colors (likely sidebar, dark text)
|
|
298
|
+
if (brightness < 50) {
|
|
299
|
+
if (!colors.sidebar.dark) colors.sidebar.dark = color;
|
|
300
|
+
if (!colors.text.primary && count > 100) colors.text.primary = color;
|
|
301
|
+
}
|
|
302
|
+
// Dark colors (likely text, headings)
|
|
303
|
+
else if (brightness < 100) {
|
|
304
|
+
if (!colors.text.heading) colors.text.heading = color;
|
|
305
|
+
}
|
|
306
|
+
// Very light colors (likely backgrounds)
|
|
307
|
+
else if (brightness > 240) {
|
|
308
|
+
if (!colors.background.white) colors.background.white = color;
|
|
309
|
+
}
|
|
310
|
+
// Light colors (likely light backgrounds, borders)
|
|
311
|
+
else if (brightness > 200) {
|
|
312
|
+
if (!colors.background.light) colors.background.light = color;
|
|
313
|
+
if (!colors.border.light) colors.border.light = color;
|
|
314
|
+
}
|
|
315
|
+
// Medium colors (likely borders, muted text)
|
|
316
|
+
else if (brightness > 150) {
|
|
317
|
+
if (!colors.border.default) colors.border.default = color;
|
|
318
|
+
if (!colors.text.muted) colors.text.muted = color;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Check for saturated colors (likely primary/accent)
|
|
322
|
+
const saturation = getColorSaturation(rgb);
|
|
323
|
+
if (saturation > 50 && count > 50) {
|
|
324
|
+
// Blue-ish (likely primary)
|
|
325
|
+
if (rgb.b > rgb.r && rgb.b > rgb.g) {
|
|
326
|
+
if (!colors.primary) colors.primary = color;
|
|
327
|
+
}
|
|
328
|
+
// Green-ish (likely success)
|
|
329
|
+
else if (rgb.g > rgb.r && rgb.g > rgb.b) {
|
|
330
|
+
if (!colors.status.success) colors.status.success = color;
|
|
331
|
+
}
|
|
332
|
+
// Red-ish (likely error)
|
|
333
|
+
else if (rgb.r > rgb.g && rgb.r > rgb.b) {
|
|
334
|
+
if (!colors.status.error) colors.status.error = color;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
return colors;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Helper: hex to RGB
|
|
343
|
+
function hexToRgb(hex) {
|
|
344
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
345
|
+
return result ? {
|
|
346
|
+
r: parseInt(result[1], 16),
|
|
347
|
+
g: parseInt(result[2], 16),
|
|
348
|
+
b: parseInt(result[3], 16)
|
|
349
|
+
} : null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Helper: get color saturation
|
|
353
|
+
function getColorSaturation(rgb) {
|
|
354
|
+
const max = Math.max(rgb.r, rgb.g, rgb.b);
|
|
355
|
+
const min = Math.min(rgb.r, rgb.g, rgb.b);
|
|
356
|
+
const l = (max + min) / 2;
|
|
357
|
+
|
|
358
|
+
if (max === min) return 0;
|
|
359
|
+
|
|
360
|
+
const d = max - min;
|
|
361
|
+
return l > 0.5 ? d / (510 - max - min) * 100 : d / (max + min) * 100;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Capture all states of a single page
|
|
365
|
+
async function capturePageStates(pageUrl, pageName) {
|
|
366
|
+
console.log(`\n📸 Capturing page: ${pageName}`);
|
|
367
|
+
|
|
368
|
+
const captures = [];
|
|
369
|
+
const interactions = [];
|
|
370
|
+
|
|
371
|
+
// Navigate to page
|
|
372
|
+
browser(`open "${pageUrl}"`);
|
|
373
|
+
browser(`wait ${CONFIG.waitAfterLoad}`);
|
|
374
|
+
|
|
375
|
+
// Capture base state
|
|
376
|
+
captures.push(captureState(pageName));
|
|
377
|
+
|
|
378
|
+
// Get all interactive elements
|
|
379
|
+
const elements = getInteractiveElements();
|
|
380
|
+
console.log(` Found ${elements.length} interactive elements`);
|
|
381
|
+
|
|
382
|
+
// Track clicked elements to avoid duplicates
|
|
383
|
+
const clickedElements = new Set();
|
|
384
|
+
let interactionCount = 0;
|
|
385
|
+
|
|
386
|
+
for (const element of elements) {
|
|
387
|
+
if (interactionCount >= CONFIG.maxInteractionsPerPage) {
|
|
388
|
+
console.log(` ⚠ Reached max interactions limit (${CONFIG.maxInteractionsPerPage})`);
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Skip if already clicked similar element
|
|
393
|
+
const elementKey = `${element.role}-${element.name}`;
|
|
394
|
+
if (clickedElements.has(elementKey)) continue;
|
|
395
|
+
clickedElements.add(elementKey);
|
|
396
|
+
|
|
397
|
+
// Skip navigation links that would leave the page
|
|
398
|
+
if (element.role === 'link' && element.tag === 'a') {
|
|
399
|
+
continue; // Will be captured as separate pages
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
// Click the element
|
|
404
|
+
console.log(` → Clicking: ${element.name} (${element.ref})`);
|
|
405
|
+
browser(`click ${element.ref}`);
|
|
406
|
+
browser(`wait ${CONFIG.waitAfterClick}`);
|
|
407
|
+
|
|
408
|
+
// Capture the new state
|
|
409
|
+
const stateName = `${pageName}--${sanitizeFileName(element.name)}`;
|
|
410
|
+
const capture = captureState(stateName);
|
|
411
|
+
|
|
412
|
+
captures.push(capture);
|
|
413
|
+
interactions.push({
|
|
414
|
+
element: element,
|
|
415
|
+
stateName: stateName,
|
|
416
|
+
capture: capture
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
interactionCount++;
|
|
420
|
+
|
|
421
|
+
// Try to close any modals/dialogs that opened
|
|
422
|
+
closeAnyModals();
|
|
423
|
+
|
|
424
|
+
// Return to base state if page changed significantly
|
|
425
|
+
const currentUrl = browser('get url');
|
|
426
|
+
if (currentUrl && !currentUrl.includes(pageUrl)) {
|
|
427
|
+
browser(`open "${pageUrl}"`);
|
|
428
|
+
browser(`wait ${CONFIG.waitAfterLoad}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
} catch (error) {
|
|
432
|
+
console.log(` ⚠ Failed to interact with: ${element.name}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return { captures, interactions };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Try to close any open modals
|
|
440
|
+
function closeAnyModals() {
|
|
441
|
+
// Look for common close button patterns
|
|
442
|
+
const closePatterns = [
|
|
443
|
+
'button[aria-label*="close"]',
|
|
444
|
+
'button[aria-label*="Close"]',
|
|
445
|
+
'.modal-close',
|
|
446
|
+
'.dialog-close',
|
|
447
|
+
'[data-dismiss="modal"]'
|
|
448
|
+
];
|
|
449
|
+
|
|
450
|
+
// Try pressing Escape
|
|
451
|
+
browser('press Escape');
|
|
452
|
+
browser('wait 300');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Sanitize filename
|
|
456
|
+
function sanitizeFileName(name) {
|
|
457
|
+
return name
|
|
458
|
+
.toLowerCase()
|
|
459
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
460
|
+
.replace(/^-|-$/g, '')
|
|
461
|
+
.substring(0, 50);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Main capture function
|
|
465
|
+
async function runCapture(config) {
|
|
466
|
+
console.log('🚀 Starting Comprehensive Platform Capture\n');
|
|
467
|
+
console.log('='.repeat(50));
|
|
468
|
+
|
|
469
|
+
ensureDirectories();
|
|
470
|
+
|
|
471
|
+
const manifest = {
|
|
472
|
+
platform: {
|
|
473
|
+
name: config.platformName || 'Unknown Platform',
|
|
474
|
+
baseUrl: config.baseUrl,
|
|
475
|
+
capturedAt: new Date().toISOString()
|
|
476
|
+
},
|
|
477
|
+
pages: [],
|
|
478
|
+
totalScreenshots: 0,
|
|
479
|
+
totalInteractions: 0
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const allHtmlFiles = [];
|
|
483
|
+
|
|
484
|
+
// Capture each page
|
|
485
|
+
for (const page of config.pages) {
|
|
486
|
+
const pageUrl = page.startsWith('http') ? page : `${config.baseUrl}${page}`;
|
|
487
|
+
const pageName = sanitizeFileName(page.replace(config.baseUrl, '').replace(/^\//, '') || 'home');
|
|
488
|
+
|
|
489
|
+
const { captures, interactions } = await capturePageStates(pageUrl, pageName);
|
|
490
|
+
|
|
491
|
+
manifest.pages.push({
|
|
492
|
+
name: pageName,
|
|
493
|
+
url: pageUrl,
|
|
494
|
+
captures: captures,
|
|
495
|
+
interactions: interactions.length
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
manifest.totalScreenshots += captures.length;
|
|
499
|
+
manifest.totalInteractions += interactions.length;
|
|
500
|
+
|
|
501
|
+
// Collect HTML files for token extraction
|
|
502
|
+
captures.forEach(c => {
|
|
503
|
+
if (c.html) allHtmlFiles.push(c.html);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Generate design tokens from all captured HTML
|
|
508
|
+
const tokens = generateDesignTokens(allHtmlFiles);
|
|
509
|
+
|
|
510
|
+
// Validate tokens
|
|
511
|
+
validateExtractedTokens(tokens);
|
|
512
|
+
|
|
513
|
+
// Write manifest
|
|
514
|
+
const manifestPath = path.join(CONFIG.outputDir, 'manifest.json');
|
|
515
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
516
|
+
console.log(`\n✓ Written: ${manifestPath}`);
|
|
517
|
+
|
|
518
|
+
// Write interactions map
|
|
519
|
+
const interactionsPath = path.join(CONFIG.outputDir, 'interactions.json');
|
|
520
|
+
const allInteractions = manifest.pages.flatMap(p => p.interactions || []);
|
|
521
|
+
fs.writeFileSync(interactionsPath, JSON.stringify(allInteractions, null, 2));
|
|
522
|
+
console.log(`✓ Written: ${interactionsPath}`);
|
|
523
|
+
|
|
524
|
+
// Summary
|
|
525
|
+
console.log('\n' + '='.repeat(50));
|
|
526
|
+
console.log('📊 CAPTURE SUMMARY');
|
|
527
|
+
console.log('='.repeat(50));
|
|
528
|
+
console.log(`Pages captured: ${manifest.pages.length}`);
|
|
529
|
+
console.log(`Total screenshots: ${manifest.totalScreenshots}`);
|
|
530
|
+
console.log(`Total interactions: ${manifest.totalInteractions}`);
|
|
531
|
+
console.log(`Colors extracted: ${tokens.totalColorsFound}`);
|
|
532
|
+
console.log(`Font families: ${tokens.fonts.families.length}`);
|
|
533
|
+
console.log('='.repeat(50));
|
|
534
|
+
|
|
535
|
+
return manifest;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Validate extracted tokens meet minimum requirements
|
|
539
|
+
function validateExtractedTokens(tokens) {
|
|
540
|
+
const errors = [];
|
|
541
|
+
|
|
542
|
+
if (!tokens.colors.primary) {
|
|
543
|
+
errors.push('Could not identify primary color');
|
|
544
|
+
}
|
|
545
|
+
if (!tokens.colors.text.primary) {
|
|
546
|
+
errors.push('Could not identify primary text color');
|
|
547
|
+
}
|
|
548
|
+
if (tokens.totalColorsFound < 10) {
|
|
549
|
+
errors.push(`Only ${tokens.totalColorsFound} colors found (minimum 10 required)`);
|
|
550
|
+
}
|
|
551
|
+
if (tokens.fonts.families.length === 0) {
|
|
552
|
+
errors.push('No font families extracted');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (errors.length > 0) {
|
|
556
|
+
console.log('\n⚠️ TOKEN EXTRACTION WARNINGS:');
|
|
557
|
+
errors.forEach(e => console.log(` - ${e}`));
|
|
558
|
+
} else {
|
|
559
|
+
console.log('\n✅ Token extraction passed validation');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return errors.length === 0;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// CLI interface
|
|
566
|
+
if (require.main === module) {
|
|
567
|
+
const args = process.argv.slice(2);
|
|
568
|
+
|
|
569
|
+
if (args.length === 0) {
|
|
570
|
+
console.log(`
|
|
571
|
+
Usage: node comprehensive-capture.js <base-url> [pages...]
|
|
572
|
+
|
|
573
|
+
Example:
|
|
574
|
+
node comprehensive-capture.js https://app.example.com /dashboard /settings /profile
|
|
575
|
+
|
|
576
|
+
Or with config file:
|
|
577
|
+
node comprehensive-capture.js --config capture-config.json
|
|
578
|
+
`);
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (args[0] === '--config') {
|
|
583
|
+
const config = JSON.parse(fs.readFileSync(args[1], 'utf8'));
|
|
584
|
+
runCapture(config);
|
|
585
|
+
} else {
|
|
586
|
+
const baseUrl = args[0];
|
|
587
|
+
const pages = args.slice(1).length > 0 ? args.slice(1) : ['/'];
|
|
588
|
+
|
|
589
|
+
runCapture({
|
|
590
|
+
baseUrl,
|
|
591
|
+
pages,
|
|
592
|
+
platformName: new URL(baseUrl).hostname
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
module.exports = { runCapture, generateDesignTokens, extractColorsFromHTML };
|