design-clone 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -34
- package/SKILL.md +69 -45
- package/bin/cli.js +22 -4
- package/bin/commands/clone-site.js +31 -171
- package/bin/commands/help.js +19 -6
- package/bin/commands/init.js +9 -86
- package/bin/commands/uninstall.js +105 -0
- package/bin/commands/update.js +70 -0
- package/bin/commands/verify.js +7 -14
- package/bin/utils/paths.js +28 -0
- package/bin/utils/validate.js +2 -22
- package/bin/utils/version.js +23 -0
- package/docs/code-standards.md +789 -0
- package/docs/codebase-summary.md +533 -286
- package/docs/index.md +74 -0
- package/docs/project-overview-pdr.md +797 -0
- package/docs/system-architecture.md +718 -0
- package/package.json +14 -17
- package/src/ai/prompts/design-tokens/basic.md +80 -0
- package/src/ai/prompts/design-tokens/section-with-css.md +41 -0
- package/src/ai/prompts/design-tokens/section.md +48 -0
- package/src/ai/prompts/design-tokens/with-css.md +87 -0
- package/src/ai/prompts/structure-analysis/basic.md +55 -0
- package/src/ai/prompts/structure-analysis/with-context.md +59 -0
- package/src/ai/prompts/structure-analysis/with-dimensions.md +63 -0
- package/src/ai/prompts/structure-analysis/with-hierarchy.md +73 -0
- package/src/ai/prompts/ux-audit/aggregation.md +42 -0
- package/src/ai/prompts/ux-audit/desktop.md +92 -0
- package/src/ai/prompts/ux-audit/mobile.md +93 -0
- package/src/ai/prompts/ux-audit/tablet.md +92 -0
- package/src/core/animation/animation-extractor-ast.js +183 -0
- package/src/core/animation/animation-extractor-output.js +152 -0
- package/src/core/animation/animation-extractor.js +178 -0
- package/src/core/animation/state-capture-detection.js +200 -0
- package/src/core/animation/state-capture.js +193 -0
- package/src/core/capture/browser-context-pool.js +96 -0
- package/src/core/capture/multi-page-screenshot-page.js +110 -0
- package/src/core/capture/multi-page-screenshot.js +208 -0
- package/src/core/capture/screenshot-extraction.js +186 -0
- package/src/core/capture/screenshot-helpers.js +175 -0
- package/src/core/capture/screenshot-orchestrator.js +174 -0
- package/src/core/capture/screenshot-viewport.js +93 -0
- package/src/core/capture/screenshot.js +192 -0
- package/src/core/content/content-counter-dom.js +191 -0
- package/src/core/content/content-counter.js +76 -0
- package/src/core/css/breakpoint-detector.js +66 -0
- package/src/core/css/chromium-defaults.json +23 -0
- package/src/core/css/computed-style-extractor.js +102 -0
- package/src/core/css/css-chunker.js +103 -0
- package/src/core/css/filter-css-dead-code.js +120 -0
- package/src/core/css/filter-css-html-analyzer.js +110 -0
- package/src/core/css/filter-css-selector-matcher.js +172 -0
- package/src/core/css/filter-css.js +206 -0
- package/src/core/css/merge-css-atrule-processor.js +158 -0
- package/src/core/css/merge-css-file-io.js +68 -0
- package/src/core/css/merge-css.js +148 -0
- package/src/core/detection/framework-detector-routing.js +68 -0
- package/src/core/detection/framework-detector-signals.js +65 -0
- package/src/core/detection/framework-detector.js +198 -0
- package/src/core/dimension/dimension-extractor-card-detector.js +82 -0
- package/src/core/dimension/dimension-extractor.js +317 -0
- package/src/core/dimension/dimension-output-ai-summary.js +111 -0
- package/src/core/dimension/dimension-output.js +173 -0
- package/src/core/dimension/dom-tree-analyzer-tree-builders.js +95 -0
- package/src/core/dimension/dom-tree-analyzer.js +191 -0
- package/src/core/discovery/app-state-snapshot-capture.js +195 -0
- package/src/core/discovery/app-state-snapshot-utils.js +178 -0
- package/src/core/discovery/app-state-snapshot.js +131 -0
- package/src/core/discovery/discover-pages-routes.js +84 -0
- package/src/core/discovery/discover-pages-utils.js +177 -0
- package/src/core/discovery/discover-pages.js +191 -0
- package/src/core/html/html-extractor-inline-styler.js +70 -0
- package/src/core/html/html-extractor.js +147 -0
- package/src/core/html/semantic-enhancer-mappings.js +200 -0
- package/src/core/html/semantic-enhancer-page.js +148 -0
- package/src/core/html/semantic-enhancer.js +135 -0
- package/src/core/links/rewrite-links-css-rewriter.js +53 -0
- package/src/core/links/rewrite-links.js +173 -0
- package/src/core/media/asset-validator.js +118 -0
- package/src/core/media/extract-assets-downloader.js +187 -0
- package/src/core/media/extract-assets-page-scraper.js +115 -0
- package/src/core/media/extract-assets.js +159 -0
- package/src/core/media/video-capture-convert.js +200 -0
- package/src/core/media/video-capture.js +201 -0
- package/src/core/{lazy-loader.js → page-prep/lazy-loader.js} +37 -39
- package/src/core/section/section-cropper-helpers.js +43 -0
- package/src/core/{section-cropper.js → section/section-cropper.js} +11 -88
- package/src/core/section/section-detector-strategies.js +139 -0
- package/src/core/section/section-detector-utils.js +100 -0
- package/src/core/section/section-detector.js +88 -0
- package/src/core/tests/test-section-cropper.js +2 -2
- package/src/core/tests/test-section-detector.js +2 -2
- package/src/post-process/enhance-assets.js +29 -4
- package/src/post-process/fetch-images-unsplash-client.js +123 -0
- package/src/post-process/fetch-images.js +60 -263
- package/src/post-process/inject-gosnap.js +88 -0
- package/src/post-process/inject-icons-svg-replacer.js +76 -0
- package/src/post-process/inject-icons.js +47 -200
- package/src/route-discoverers/base-discoverer-utils.js +137 -0
- package/src/route-discoverers/base-discoverer.js +29 -118
- package/src/route-discoverers/index.js +1 -1
- package/src/shared/config.js +38 -0
- package/src/shared/error-codes.js +31 -0
- package/src/shared/viewports.js +46 -0
- package/src/utils/browser.js +0 -7
- package/src/utils/helpers.js +4 -0
- package/src/utils/log.js +12 -0
- package/src/utils/playwright-loader.js +76 -0
- package/src/utils/playwright.js +3 -69
- package/src/utils/progress.js +32 -0
- package/src/verification/generate-audit-report-css-fixes.js +52 -0
- package/src/verification/generate-audit-report-sections.js +158 -0
- package/src/verification/generate-audit-report.js +5 -281
- package/src/verification/quality-scorer.js +92 -0
- package/src/verification/verify-footer-checks.js +103 -0
- package/src/verification/verify-footer-helpers.js +178 -0
- package/src/verification/verify-footer.js +23 -381
- package/src/verification/verify-header-checks.js +104 -0
- package/src/verification/verify-header-helpers.js +156 -0
- package/src/verification/verify-header.js +23 -365
- package/src/verification/verify-layout-report.js +101 -0
- package/src/verification/verify-layout.js +13 -259
- package/src/verification/verify-menu-checks.js +104 -0
- package/src/verification/verify-menu-helpers.js +112 -0
- package/src/verification/verify-menu.js +17 -285
- package/src/verification/verify-slider-checks.js +115 -0
- package/src/verification/verify-slider-constants.js +65 -0
- package/src/verification/verify-slider-helpers.js +164 -0
- package/src/verification/verify-slider.js +23 -414
- package/.env.example +0 -14
- package/docs/basic-clone.md +0 -63
- package/docs/cli-reference.md +0 -316
- package/docs/design-clone-architecture.md +0 -492
- package/docs/pixel-perfect.md +0 -117
- package/docs/project-roadmap.md +0 -382
- package/docs/troubleshooting.md +0 -170
- package/requirements.txt +0 -5
- package/src/ai/__pycache__/analyze-structure.cpython-313.pyc +0 -0
- package/src/ai/__pycache__/extract-design-tokens.cpython-313.pyc +0 -0
- package/src/ai/analyze-structure.py +0 -375
- package/src/ai/extract-design-tokens.py +0 -782
- package/src/ai/prompts/__init__.py +0 -2
- package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
- package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
- package/src/ai/prompts/__pycache__/ux_audit.cpython-313.pyc +0 -0
- package/src/ai/prompts/design_tokens.py +0 -316
- package/src/ai/prompts/structure_analysis.py +0 -592
- package/src/ai/prompts/ux_audit.py +0 -198
- package/src/ai/ux-audit.js +0 -596
- package/src/core/animation-extractor.js +0 -526
- package/src/core/app-state-snapshot.js +0 -511
- package/src/core/content-counter.js +0 -342
- package/src/core/design-tokens.js +0 -103
- package/src/core/dimension-extractor.js +0 -438
- package/src/core/dimension-output.js +0 -305
- package/src/core/discover-pages.js +0 -542
- package/src/core/dom-tree-analyzer.js +0 -298
- package/src/core/extract-assets.js +0 -468
- package/src/core/filter-css.js +0 -499
- package/src/core/framework-detector.js +0 -538
- package/src/core/html-extractor.js +0 -212
- package/src/core/merge-css.js +0 -407
- package/src/core/multi-page-screenshot.js +0 -380
- package/src/core/rewrite-links.js +0 -226
- package/src/core/screenshot.js +0 -701
- package/src/core/section-detector.js +0 -386
- package/src/core/semantic-enhancer.js +0 -492
- package/src/core/state-capture.js +0 -598
- package/src/core/video-capture.js +0 -546
- package/src/utils/__init__.py +0 -16
- package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
- package/src/utils/env.py +0 -134
- /package/src/core/{css-extractor.js → css/css-extractor.js} +0 -0
- /package/src/core/{cookie-handler.js → page-prep/cookie-handler.js} +0 -0
- /package/src/core/{page-readiness.js → page-prep/page-readiness.js} +0 -0
package/src/ai/ux-audit.js
DELETED
|
@@ -1,596 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* UX Audit Runner
|
|
4
|
-
*
|
|
5
|
-
* Analyzes website screenshots using Gemini Vision to assess UX quality.
|
|
6
|
-
* Generates detailed reports with scores, issues, and recommendations.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* node ux-audit.js --screenshots <dir> [--output <dir>] [--verbose]
|
|
10
|
-
*
|
|
11
|
-
* Options:
|
|
12
|
-
* --screenshots Directory containing viewport screenshots (desktop.png, tablet.png, mobile.png)
|
|
13
|
-
* --output Output directory for report (default: same as screenshots)
|
|
14
|
-
* --verbose Show detailed progress
|
|
15
|
-
* --url Original URL (for report metadata)
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import fs from 'fs/promises';
|
|
19
|
-
import path from 'path';
|
|
20
|
-
import { fileURLToPath } from 'url';
|
|
21
|
-
|
|
22
|
-
// API key detection
|
|
23
|
-
const GEMINI_API_KEY = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
24
|
-
|
|
25
|
-
// Viewport configurations
|
|
26
|
-
const VIEWPORTS = {
|
|
27
|
-
desktop: { width: 1920, height: 1080 },
|
|
28
|
-
tablet: { width: 768, height: 1024 },
|
|
29
|
-
mobile: { width: 375, height: 812 }
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// Score weights for aggregation
|
|
33
|
-
const VIEWPORT_WEIGHTS = {
|
|
34
|
-
desktop: 0.4,
|
|
35
|
-
tablet: 0.3,
|
|
36
|
-
mobile: 0.3
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Parse CLI arguments
|
|
41
|
-
*/
|
|
42
|
-
function parseArgs(args) {
|
|
43
|
-
const options = {
|
|
44
|
-
screenshots: null,
|
|
45
|
-
output: null,
|
|
46
|
-
verbose: false,
|
|
47
|
-
url: null
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
for (let i = 0; i < args.length; i++) {
|
|
51
|
-
const arg = args[i];
|
|
52
|
-
|
|
53
|
-
if (arg === '--screenshots' && args[i + 1]) {
|
|
54
|
-
options.screenshots = args[++i];
|
|
55
|
-
} else if (arg === '--output' && args[i + 1]) {
|
|
56
|
-
options.output = args[++i];
|
|
57
|
-
} else if (arg === '--verbose') {
|
|
58
|
-
options.verbose = true;
|
|
59
|
-
} else if (arg === '--url' && args[i + 1]) {
|
|
60
|
-
options.url = args[++i];
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return options;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Build viewport-specific UX audit prompt
|
|
69
|
-
*
|
|
70
|
-
* Note: This prompt mirrors src/ai/prompts/ux_audit.py for standalone JS execution.
|
|
71
|
-
* The Python file is the canonical source for prompt content.
|
|
72
|
-
*/
|
|
73
|
-
function buildUXAuditPrompt(viewport) {
|
|
74
|
-
const basePrompt = `Analyze this website screenshot for UX quality.
|
|
75
|
-
|
|
76
|
-
Evaluate these categories (score 0-100 each):
|
|
77
|
-
|
|
78
|
-
1. VISUAL HIERARCHY
|
|
79
|
-
- Primary content prominence
|
|
80
|
-
- Clear scanning patterns (F/Z pattern)
|
|
81
|
-
- Call-to-action visibility
|
|
82
|
-
- Information grouping and prioritization
|
|
83
|
-
- White space utilization
|
|
84
|
-
|
|
85
|
-
2. NAVIGATION
|
|
86
|
-
- Tappable area size (44x44px minimum for mobile)
|
|
87
|
-
- Current page indicator clarity
|
|
88
|
-
- Menu discoverability
|
|
89
|
-
- Breadcrumb/location awareness
|
|
90
|
-
- Navigation consistency
|
|
91
|
-
|
|
92
|
-
3. TYPOGRAPHY
|
|
93
|
-
- Body text size (16px+ recommended)
|
|
94
|
-
- Line height (1.4-1.6 ideal)
|
|
95
|
-
- Contrast ratio (WCAG AA: 4.5:1 for text)
|
|
96
|
-
- Font hierarchy clarity
|
|
97
|
-
- Readability at viewport size
|
|
98
|
-
|
|
99
|
-
4. SPACING
|
|
100
|
-
- Consistent padding/margins
|
|
101
|
-
- Element breathing room
|
|
102
|
-
- Touch target spacing (8px minimum between)
|
|
103
|
-
- Grid alignment
|
|
104
|
-
- Section separation
|
|
105
|
-
|
|
106
|
-
5. INTERACTIVE ELEMENTS
|
|
107
|
-
- Button affordance (looks clickable)
|
|
108
|
-
- Link distinguishability
|
|
109
|
-
- Focus state visibility
|
|
110
|
-
- Hover state indication
|
|
111
|
-
- Form field clarity
|
|
112
|
-
|
|
113
|
-
6. RESPONSIVE
|
|
114
|
-
- Content reflow appropriateness
|
|
115
|
-
- No horizontal scroll
|
|
116
|
-
- Image scaling quality
|
|
117
|
-
- Text truncation handling
|
|
118
|
-
- Breakpoint transitions
|
|
119
|
-
|
|
120
|
-
Return ONLY valid JSON in this exact format:
|
|
121
|
-
{
|
|
122
|
-
"viewport": "${viewport}",
|
|
123
|
-
"scores": {
|
|
124
|
-
"visual_hierarchy": <0-100>,
|
|
125
|
-
"navigation": <0-100>,
|
|
126
|
-
"typography": <0-100>,
|
|
127
|
-
"spacing": <0-100>,
|
|
128
|
-
"interactivity": <0-100>,
|
|
129
|
-
"responsive": <0-100>
|
|
130
|
-
},
|
|
131
|
-
"overall_ux_score": <0-100>,
|
|
132
|
-
"accessibility_score": <0-100>,
|
|
133
|
-
"issues": [
|
|
134
|
-
{
|
|
135
|
-
"category": "<visual_hierarchy|navigation|typography|spacing|interactivity|responsive>",
|
|
136
|
-
"severity": "<critical|major|minor>",
|
|
137
|
-
"issue": "<concise description>",
|
|
138
|
-
"fix": "<actionable suggestion>"
|
|
139
|
-
}
|
|
140
|
-
],
|
|
141
|
-
"recommendations": ["<actionable improvement item>"]
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
SEVERITY GUIDELINES:
|
|
145
|
-
- critical: Blocks user tasks or causes confusion (0-30 score range issues)
|
|
146
|
-
- major: Degrades experience significantly (31-60 score range issues)
|
|
147
|
-
- minor: Polish improvements (61-80 score range issues)`;
|
|
148
|
-
|
|
149
|
-
// Add viewport-specific context
|
|
150
|
-
const viewportContext = {
|
|
151
|
-
mobile: `
|
|
152
|
-
|
|
153
|
-
MOBILE-SPECIFIC CHECKS:
|
|
154
|
-
- Touch targets minimum 44x44px
|
|
155
|
-
- Thumb zone accessibility
|
|
156
|
-
- Single-column layout efficiency
|
|
157
|
-
- Mobile navigation pattern (hamburger/tab bar)
|
|
158
|
-
- Text readable without zooming
|
|
159
|
-
- Forms optimized for mobile input`,
|
|
160
|
-
|
|
161
|
-
tablet: `
|
|
162
|
-
|
|
163
|
-
TABLET-SPECIFIC CHECKS:
|
|
164
|
-
- Two-column layout utilization
|
|
165
|
-
- Touch and mouse input support
|
|
166
|
-
- Landscape/portrait adaptability
|
|
167
|
-
- Sidebar vs content balance
|
|
168
|
-
- Split-view readiness`,
|
|
169
|
-
|
|
170
|
-
desktop: `
|
|
171
|
-
|
|
172
|
-
DESKTOP-SPECIFIC CHECKS:
|
|
173
|
-
- Maximum content width (1200-1440px ideal)
|
|
174
|
-
- Multi-column layout efficiency
|
|
175
|
-
- Hover states and micro-interactions
|
|
176
|
-
- Keyboard navigation support
|
|
177
|
-
- Large screen real estate utilization`
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
return basePrompt + (viewportContext[viewport] || '');
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Analyze screenshot with Gemini Vision
|
|
185
|
-
*/
|
|
186
|
-
async function analyzeViewport(screenshotPath, viewport, verbose = false) {
|
|
187
|
-
if (!GEMINI_API_KEY) {
|
|
188
|
-
return {
|
|
189
|
-
success: false,
|
|
190
|
-
error: 'GEMINI_API_KEY not set',
|
|
191
|
-
viewport
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
try {
|
|
196
|
-
const { GoogleGenerativeAI } = await import('@google/generative-ai');
|
|
197
|
-
const genai = new GoogleGenerativeAI(GEMINI_API_KEY);
|
|
198
|
-
const model = genai.getGenerativeModel({ model: 'gemini-2.5-flash' });
|
|
199
|
-
|
|
200
|
-
// Read screenshot as base64
|
|
201
|
-
const imageBuffer = await fs.readFile(screenshotPath);
|
|
202
|
-
const base64 = imageBuffer.toString('base64');
|
|
203
|
-
|
|
204
|
-
const prompt = buildUXAuditPrompt(viewport);
|
|
205
|
-
|
|
206
|
-
if (verbose) console.error(` Sending to Gemini...`);
|
|
207
|
-
|
|
208
|
-
const response = await model.generateContent([
|
|
209
|
-
prompt,
|
|
210
|
-
{
|
|
211
|
-
inlineData: {
|
|
212
|
-
mimeType: 'image/png',
|
|
213
|
-
data: base64
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
]);
|
|
217
|
-
|
|
218
|
-
const text = response.response.text();
|
|
219
|
-
|
|
220
|
-
// Extract JSON from response
|
|
221
|
-
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
222
|
-
if (!jsonMatch) {
|
|
223
|
-
return {
|
|
224
|
-
success: false,
|
|
225
|
-
error: 'Could not parse Gemini response',
|
|
226
|
-
viewport,
|
|
227
|
-
raw: text
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const result = JSON.parse(jsonMatch[0]);
|
|
232
|
-
return {
|
|
233
|
-
success: true,
|
|
234
|
-
viewport,
|
|
235
|
-
...result
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
} catch (error) {
|
|
239
|
-
return {
|
|
240
|
-
success: false,
|
|
241
|
-
error: error.message,
|
|
242
|
-
viewport
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Aggregate results from all viewports
|
|
249
|
-
*/
|
|
250
|
-
function aggregateResults(viewportResults) {
|
|
251
|
-
const categories = ['visual_hierarchy', 'navigation', 'typography', 'spacing', 'interactivity', 'responsive'];
|
|
252
|
-
|
|
253
|
-
const aggregated = {
|
|
254
|
-
overall_scores: {},
|
|
255
|
-
overall_ux_score: 0,
|
|
256
|
-
accessibility_score: 0,
|
|
257
|
-
viewport_breakdown: {},
|
|
258
|
-
top_issues: [],
|
|
259
|
-
prioritized_recommendations: []
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
// Calculate weighted averages
|
|
263
|
-
let totalWeight = 0;
|
|
264
|
-
let weightedUxScore = 0;
|
|
265
|
-
let weightedAccessScore = 0;
|
|
266
|
-
|
|
267
|
-
for (const [viewport, result] of Object.entries(viewportResults)) {
|
|
268
|
-
if (!result.success) continue;
|
|
269
|
-
|
|
270
|
-
const weight = VIEWPORT_WEIGHTS[viewport] || 0.33;
|
|
271
|
-
totalWeight += weight;
|
|
272
|
-
|
|
273
|
-
weightedUxScore += (result.overall_ux_score || 0) * weight;
|
|
274
|
-
weightedAccessScore += (result.accessibility_score || 0) * weight;
|
|
275
|
-
|
|
276
|
-
aggregated.viewport_breakdown[viewport] = result.overall_ux_score || 0;
|
|
277
|
-
|
|
278
|
-
// Aggregate category scores
|
|
279
|
-
for (const category of categories) {
|
|
280
|
-
const score = result.scores?.[category] || 0;
|
|
281
|
-
if (!aggregated.overall_scores[category]) {
|
|
282
|
-
aggregated.overall_scores[category] = 0;
|
|
283
|
-
}
|
|
284
|
-
aggregated.overall_scores[category] += score * weight;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Collect issues
|
|
288
|
-
if (result.issues) {
|
|
289
|
-
for (const issue of result.issues) {
|
|
290
|
-
aggregated.top_issues.push({
|
|
291
|
-
...issue,
|
|
292
|
-
viewports_affected: [viewport]
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Collect recommendations
|
|
298
|
-
if (result.recommendations) {
|
|
299
|
-
aggregated.prioritized_recommendations.push(...result.recommendations);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Normalize scores
|
|
304
|
-
if (totalWeight > 0) {
|
|
305
|
-
aggregated.overall_ux_score = Math.round(weightedUxScore / totalWeight);
|
|
306
|
-
aggregated.accessibility_score = Math.round(weightedAccessScore / totalWeight);
|
|
307
|
-
|
|
308
|
-
for (const category of categories) {
|
|
309
|
-
aggregated.overall_scores[category] = Math.round(aggregated.overall_scores[category] / totalWeight);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Sort issues by severity
|
|
314
|
-
const severityOrder = { critical: 0, major: 1, minor: 2 };
|
|
315
|
-
aggregated.top_issues.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
316
|
-
|
|
317
|
-
// Deduplicate recommendations
|
|
318
|
-
aggregated.prioritized_recommendations = [...new Set(aggregated.prioritized_recommendations)];
|
|
319
|
-
|
|
320
|
-
return aggregated;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Generate markdown report
|
|
325
|
-
*/
|
|
326
|
-
function generateReport(aggregated, viewportResults, url = null) {
|
|
327
|
-
const timestamp = new Date().toISOString();
|
|
328
|
-
|
|
329
|
-
let report = `# UX Audit Report
|
|
330
|
-
|
|
331
|
-
**Generated:** ${timestamp}
|
|
332
|
-
**URL:** ${url || 'N/A'}
|
|
333
|
-
|
|
334
|
-
## Overall Scores
|
|
335
|
-
|
|
336
|
-
| Metric | Score |
|
|
337
|
-
|--------|-------|
|
|
338
|
-
| Overall UX | ${aggregated.overall_ux_score}% |
|
|
339
|
-
| Accessibility | ${aggregated.accessibility_score}% |
|
|
340
|
-
|
|
341
|
-
## Category Breakdown
|
|
342
|
-
|
|
343
|
-
| Category | Score | Desktop | Tablet | Mobile |
|
|
344
|
-
|----------|-------|---------|--------|--------|
|
|
345
|
-
`;
|
|
346
|
-
|
|
347
|
-
const categories = [
|
|
348
|
-
{ key: 'visual_hierarchy', name: 'Visual Hierarchy' },
|
|
349
|
-
{ key: 'navigation', name: 'Navigation' },
|
|
350
|
-
{ key: 'typography', name: 'Typography' },
|
|
351
|
-
{ key: 'spacing', name: 'Spacing' },
|
|
352
|
-
{ key: 'interactivity', name: 'Interactivity' },
|
|
353
|
-
{ key: 'responsive', name: 'Responsive' }
|
|
354
|
-
];
|
|
355
|
-
|
|
356
|
-
for (const cat of categories) {
|
|
357
|
-
const overall = aggregated.overall_scores[cat.key] || 0;
|
|
358
|
-
const desktop = viewportResults.desktop?.scores?.[cat.key] || '-';
|
|
359
|
-
const tablet = viewportResults.tablet?.scores?.[cat.key] || '-';
|
|
360
|
-
const mobile = viewportResults.mobile?.scores?.[cat.key] || '-';
|
|
361
|
-
const icon = overall >= 80 ? '✅' : overall >= 60 ? '⚠️' : '❌';
|
|
362
|
-
|
|
363
|
-
report += `| ${cat.name} | ${icon} ${overall}% | ${desktop}% | ${tablet}% | ${mobile}% |\n`;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Viewport breakdown
|
|
367
|
-
report += `
|
|
368
|
-
## Viewport Scores
|
|
369
|
-
|
|
370
|
-
| Viewport | UX Score |
|
|
371
|
-
|----------|----------|
|
|
372
|
-
`;
|
|
373
|
-
|
|
374
|
-
for (const [viewport, score] of Object.entries(aggregated.viewport_breakdown)) {
|
|
375
|
-
const icon = score >= 80 ? '✅' : score >= 60 ? '⚠️' : '❌';
|
|
376
|
-
report += `| ${viewport.charAt(0).toUpperCase() + viewport.slice(1)} | ${icon} ${score}% |\n`;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Issues
|
|
380
|
-
if (aggregated.top_issues.length > 0) {
|
|
381
|
-
report += `
|
|
382
|
-
## Issues Found
|
|
383
|
-
|
|
384
|
-
`;
|
|
385
|
-
|
|
386
|
-
// Group by severity
|
|
387
|
-
const critical = aggregated.top_issues.filter(i => i.severity === 'critical');
|
|
388
|
-
const major = aggregated.top_issues.filter(i => i.severity === 'major');
|
|
389
|
-
const minor = aggregated.top_issues.filter(i => i.severity === 'minor');
|
|
390
|
-
|
|
391
|
-
if (critical.length > 0) {
|
|
392
|
-
report += `### Critical Issues 🔴
|
|
393
|
-
|
|
394
|
-
`;
|
|
395
|
-
for (const issue of critical) {
|
|
396
|
-
report += `- **${issue.category}**: ${issue.issue}\n - *Fix:* ${issue.fix}\n - *Viewports:* ${issue.viewports_affected.join(', ')}\n\n`;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (major.length > 0) {
|
|
401
|
-
report += `### Major Issues 🟠
|
|
402
|
-
|
|
403
|
-
`;
|
|
404
|
-
for (const issue of major) {
|
|
405
|
-
report += `- **${issue.category}**: ${issue.issue}\n - *Fix:* ${issue.fix}\n - *Viewports:* ${issue.viewports_affected.join(', ')}\n\n`;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
if (minor.length > 0) {
|
|
410
|
-
report += `### Minor Issues 🟡
|
|
411
|
-
|
|
412
|
-
`;
|
|
413
|
-
for (const issue of minor) {
|
|
414
|
-
report += `- **${issue.category}**: ${issue.issue}\n - *Fix:* ${issue.fix}\n - *Viewports:* ${issue.viewports_affected.join(', ')}\n\n`;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Recommendations
|
|
420
|
-
if (aggregated.prioritized_recommendations.length > 0) {
|
|
421
|
-
report += `## Recommendations
|
|
422
|
-
|
|
423
|
-
`;
|
|
424
|
-
for (const rec of aggregated.prioritized_recommendations) {
|
|
425
|
-
report += `- ${rec}\n`;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
report += `
|
|
430
|
-
---
|
|
431
|
-
|
|
432
|
-
*Report generated by design-clone UX Audit*
|
|
433
|
-
`;
|
|
434
|
-
|
|
435
|
-
return report;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Run UX audit on screenshots
|
|
440
|
-
* @param {Object} screenshotPaths - { desktop: path, tablet: path, mobile: path }
|
|
441
|
-
* @param {Object} options - { output, verbose, url }
|
|
442
|
-
*/
|
|
443
|
-
export async function runUXAudit(screenshotPaths, options = {}) {
|
|
444
|
-
const { output, verbose = false, url = null } = options;
|
|
445
|
-
|
|
446
|
-
if (!GEMINI_API_KEY) {
|
|
447
|
-
return {
|
|
448
|
-
success: false,
|
|
449
|
-
error: 'GEMINI_API_KEY not set. Set environment variable to enable UX audit.'
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
const viewportResults = {};
|
|
454
|
-
|
|
455
|
-
// Analyze each viewport
|
|
456
|
-
for (const [viewport, screenshotPath] of Object.entries(screenshotPaths)) {
|
|
457
|
-
if (!screenshotPath) continue;
|
|
458
|
-
|
|
459
|
-
try {
|
|
460
|
-
await fs.access(screenshotPath);
|
|
461
|
-
} catch {
|
|
462
|
-
if (verbose) console.error(` ⚠ Missing ${viewport} screenshot`);
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
if (verbose) console.error(` Analyzing ${viewport}...`);
|
|
467
|
-
|
|
468
|
-
const result = await analyzeViewport(screenshotPath, viewport, verbose);
|
|
469
|
-
viewportResults[viewport] = result;
|
|
470
|
-
|
|
471
|
-
if (result.success) {
|
|
472
|
-
if (verbose) console.error(` ✓ UX Score: ${result.overall_ux_score}%`);
|
|
473
|
-
} else {
|
|
474
|
-
if (verbose) console.error(` ✗ Error: ${result.error}`);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Check if we have any results
|
|
479
|
-
const successfulResults = Object.values(viewportResults).filter(r => r.success);
|
|
480
|
-
if (successfulResults.length === 0) {
|
|
481
|
-
return {
|
|
482
|
-
success: false,
|
|
483
|
-
error: 'No viewport analysis succeeded',
|
|
484
|
-
viewportResults
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Aggregate results
|
|
489
|
-
const aggregated = aggregateResults(viewportResults);
|
|
490
|
-
|
|
491
|
-
// Generate report
|
|
492
|
-
const report = generateReport(aggregated, viewportResults, url);
|
|
493
|
-
|
|
494
|
-
// Write report if output specified
|
|
495
|
-
let reportPath = null;
|
|
496
|
-
if (output) {
|
|
497
|
-
await fs.mkdir(output, { recursive: true });
|
|
498
|
-
reportPath = path.join(output, 'ux-audit.md');
|
|
499
|
-
await fs.writeFile(reportPath, report);
|
|
500
|
-
|
|
501
|
-
// Also save JSON results
|
|
502
|
-
const jsonPath = path.join(output, 'ux-audit.json');
|
|
503
|
-
await fs.writeFile(jsonPath, JSON.stringify({
|
|
504
|
-
aggregated,
|
|
505
|
-
viewportResults,
|
|
506
|
-
url,
|
|
507
|
-
timestamp: new Date().toISOString()
|
|
508
|
-
}, null, 2));
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
return {
|
|
512
|
-
success: true,
|
|
513
|
-
aggregated,
|
|
514
|
-
viewportResults,
|
|
515
|
-
report,
|
|
516
|
-
reportPath,
|
|
517
|
-
summary: {
|
|
518
|
-
uxScore: aggregated.overall_ux_score,
|
|
519
|
-
accessibilityScore: aggregated.accessibility_score,
|
|
520
|
-
issueCount: aggregated.top_issues.length,
|
|
521
|
-
criticalCount: aggregated.top_issues.filter(i => i.severity === 'critical').length
|
|
522
|
-
}
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* CLI entry point
|
|
528
|
-
*/
|
|
529
|
-
async function main() {
|
|
530
|
-
const args = parseArgs(process.argv.slice(2));
|
|
531
|
-
|
|
532
|
-
if (!args.screenshots) {
|
|
533
|
-
console.error('Usage: node ux-audit.js --screenshots <dir> [--output <dir>] [--verbose] [--url <url>]');
|
|
534
|
-
console.error('\nError: --screenshots is required');
|
|
535
|
-
process.exit(1);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
const verbose = args.verbose;
|
|
539
|
-
const outputDir = args.output || args.screenshots;
|
|
540
|
-
|
|
541
|
-
if (verbose) console.error('\n🔍 Starting UX Audit...\n');
|
|
542
|
-
|
|
543
|
-
// Find screenshots
|
|
544
|
-
const screenshotPaths = {};
|
|
545
|
-
for (const viewport of ['desktop', 'tablet', 'mobile']) {
|
|
546
|
-
const screenshotPath = path.join(args.screenshots, `${viewport}.png`);
|
|
547
|
-
try {
|
|
548
|
-
await fs.access(screenshotPath);
|
|
549
|
-
screenshotPaths[viewport] = screenshotPath;
|
|
550
|
-
if (verbose) console.error(` ✓ Found ${viewport}.png`);
|
|
551
|
-
} catch {
|
|
552
|
-
if (verbose) console.error(` ⚠ Missing ${viewport}.png`);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
if (Object.keys(screenshotPaths).length === 0) {
|
|
557
|
-
console.error('Error: No screenshots found in directory');
|
|
558
|
-
process.exit(1);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// Run audit
|
|
562
|
-
const result = await runUXAudit(screenshotPaths, {
|
|
563
|
-
output: outputDir,
|
|
564
|
-
verbose,
|
|
565
|
-
url: args.url
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
if (!result.success) {
|
|
569
|
-
console.error(`\n❌ UX Audit failed: ${result.error}`);
|
|
570
|
-
process.exit(1);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
if (verbose) {
|
|
574
|
-
console.error('\n📊 Summary:');
|
|
575
|
-
console.error(` UX Score: ${result.summary.uxScore}%`);
|
|
576
|
-
console.error(` Accessibility: ${result.summary.accessibilityScore}%`);
|
|
577
|
-
console.error(` Issues: ${result.summary.issueCount} (${result.summary.criticalCount} critical)`);
|
|
578
|
-
if (result.reportPath) {
|
|
579
|
-
console.error(` Report: ${result.reportPath}`);
|
|
580
|
-
}
|
|
581
|
-
console.error();
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Output JSON to stdout
|
|
585
|
-
console.log(JSON.stringify(result, null, 2));
|
|
586
|
-
process.exit(0);
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// Run if called directly
|
|
590
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
591
|
-
if (process.argv[1] === __filename) {
|
|
592
|
-
main().catch(err => {
|
|
593
|
-
console.error('Fatal error:', err);
|
|
594
|
-
process.exit(1);
|
|
595
|
-
});
|
|
596
|
-
}
|