design-clone 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +14 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/SKILL.md +239 -0
- package/bin/cli.js +45 -0
- package/bin/commands/help.js +29 -0
- package/bin/commands/init.js +126 -0
- package/bin/commands/verify.js +99 -0
- package/bin/utils/copy.js +65 -0
- package/bin/utils/validate.js +122 -0
- package/docs/basic-clone.md +63 -0
- package/docs/cli-reference.md +94 -0
- package/docs/design-clone-architecture.md +247 -0
- package/docs/pixel-perfect.md +86 -0
- package/docs/troubleshooting.md +97 -0
- package/package.json +57 -0
- package/requirements.txt +5 -0
- package/src/ai/analyze-structure.py +305 -0
- package/src/ai/extract-design-tokens.py +439 -0
- package/src/ai/prompts/__init__.py +2 -0
- package/src/ai/prompts/design_tokens.py +183 -0
- package/src/ai/prompts/structure_analysis.py +273 -0
- package/src/core/cookie-handler.js +76 -0
- package/src/core/css-extractor.js +107 -0
- package/src/core/dimension-extractor.js +366 -0
- package/src/core/dimension-output.js +208 -0
- package/src/core/extract-assets.js +468 -0
- package/src/core/filter-css.js +499 -0
- package/src/core/html-extractor.js +102 -0
- package/src/core/lazy-loader.js +188 -0
- package/src/core/page-readiness.js +161 -0
- package/src/core/screenshot.js +380 -0
- package/src/post-process/enhance-assets.js +157 -0
- package/src/post-process/fetch-images.js +398 -0
- package/src/post-process/inject-icons.js +311 -0
- package/src/utils/__init__.py +16 -0
- package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
- package/src/utils/browser.js +103 -0
- package/src/utils/env.js +153 -0
- package/src/utils/env.py +134 -0
- package/src/utils/helpers.js +71 -0
- package/src/utils/puppeteer.js +281 -0
- package/src/verification/verify-layout.js +424 -0
- package/src/verification/verify-menu.js +422 -0
- package/templates/base.css +705 -0
- package/templates/base.html +293 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Responsive Menu Verification Script
|
|
4
|
+
*
|
|
5
|
+
* Tests menu functionality across viewports:
|
|
6
|
+
* - Mobile (375px) - hamburger menu toggle
|
|
7
|
+
* - Tablet (768px) - responsive behavior
|
|
8
|
+
* - Desktop (1920px) - full menu visibility
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node verify-menu.js --html <path> [--verbose]
|
|
12
|
+
* node verify-menu.js --url <url> [--verbose]
|
|
13
|
+
*
|
|
14
|
+
* Options:
|
|
15
|
+
* --html Path to local HTML file (required if no --url)
|
|
16
|
+
* --url URL to test (required if no --html)
|
|
17
|
+
* --output Output directory for screenshots (optional)
|
|
18
|
+
* --verbose Show detailed progress
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from 'fs/promises';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
|
|
25
|
+
// Import browser abstraction (auto-detects chrome-devtools or standalone)
|
|
26
|
+
import { getBrowser, getPage, closeBrowser, disconnectBrowser, parseArgs, outputJSON, outputError } from '../utils/browser.js';
|
|
27
|
+
|
|
28
|
+
// Viewport configurations
|
|
29
|
+
const VIEWPORTS = {
|
|
30
|
+
mobile: { width: 375, height: 812, deviceScaleFactor: 2 },
|
|
31
|
+
tablet: { width: 768, height: 1024, deviceScaleFactor: 1 },
|
|
32
|
+
desktop: { width: 1920, height: 1080, deviceScaleFactor: 1 }
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Common menu element selectors
|
|
36
|
+
const MENU_SELECTORS = {
|
|
37
|
+
// Hamburger/toggle buttons
|
|
38
|
+
toggleButtons: [
|
|
39
|
+
'[aria-label*="menu" i]',
|
|
40
|
+
'[aria-label*="nav" i]',
|
|
41
|
+
'button.hamburger',
|
|
42
|
+
'.hamburger',
|
|
43
|
+
'.menu-toggle',
|
|
44
|
+
'.nav-toggle',
|
|
45
|
+
'.mobile-menu-toggle',
|
|
46
|
+
'button[class*="hamburger"]',
|
|
47
|
+
'button[class*="menu"]',
|
|
48
|
+
'[data-toggle="nav"]',
|
|
49
|
+
'[data-menu-toggle]',
|
|
50
|
+
'.header__toggle',
|
|
51
|
+
'.header-toggle',
|
|
52
|
+
'#menu-toggle',
|
|
53
|
+
'.burger',
|
|
54
|
+
'.burger-menu'
|
|
55
|
+
],
|
|
56
|
+
// Navigation containers
|
|
57
|
+
navContainers: [
|
|
58
|
+
'nav',
|
|
59
|
+
'[role="navigation"]',
|
|
60
|
+
'.nav',
|
|
61
|
+
'.navigation',
|
|
62
|
+
'.main-nav',
|
|
63
|
+
'.site-nav',
|
|
64
|
+
'.header-nav',
|
|
65
|
+
'.primary-nav',
|
|
66
|
+
'#nav',
|
|
67
|
+
'#navigation',
|
|
68
|
+
'.menu',
|
|
69
|
+
'.main-menu'
|
|
70
|
+
],
|
|
71
|
+
// Menu items
|
|
72
|
+
menuItems: [
|
|
73
|
+
'nav a',
|
|
74
|
+
'nav li',
|
|
75
|
+
'.nav-item',
|
|
76
|
+
'.menu-item',
|
|
77
|
+
'.nav-link',
|
|
78
|
+
'.menu-link',
|
|
79
|
+
'[role="navigation"] a'
|
|
80
|
+
]
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if element is visible
|
|
85
|
+
*/
|
|
86
|
+
async function isElementVisible(page, selector) {
|
|
87
|
+
try {
|
|
88
|
+
const element = await page.$(selector);
|
|
89
|
+
if (!element) return false;
|
|
90
|
+
|
|
91
|
+
const isVisible = await page.evaluate(el => {
|
|
92
|
+
const style = window.getComputedStyle(el);
|
|
93
|
+
const rect = el.getBoundingClientRect();
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
style.display !== 'none' &&
|
|
97
|
+
style.visibility !== 'hidden' &&
|
|
98
|
+
style.opacity !== '0' &&
|
|
99
|
+
rect.width > 0 &&
|
|
100
|
+
rect.height > 0
|
|
101
|
+
);
|
|
102
|
+
}, element);
|
|
103
|
+
|
|
104
|
+
return isVisible;
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Find first matching selector
|
|
112
|
+
*/
|
|
113
|
+
async function findElement(page, selectors) {
|
|
114
|
+
for (const selector of selectors) {
|
|
115
|
+
const element = await page.$(selector);
|
|
116
|
+
if (element) {
|
|
117
|
+
return { element, selector };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Count visible menu items
|
|
125
|
+
*/
|
|
126
|
+
async function countVisibleMenuItems(page) {
|
|
127
|
+
for (const selector of MENU_SELECTORS.menuItems) {
|
|
128
|
+
try {
|
|
129
|
+
const count = await page.evaluate((sel) => {
|
|
130
|
+
const items = document.querySelectorAll(sel);
|
|
131
|
+
let visible = 0;
|
|
132
|
+
items.forEach(item => {
|
|
133
|
+
const style = window.getComputedStyle(item);
|
|
134
|
+
const rect = item.getBoundingClientRect();
|
|
135
|
+
if (
|
|
136
|
+
style.display !== 'none' &&
|
|
137
|
+
style.visibility !== 'hidden' &&
|
|
138
|
+
style.opacity !== '0' &&
|
|
139
|
+
rect.width > 0 &&
|
|
140
|
+
rect.height > 0
|
|
141
|
+
) {
|
|
142
|
+
visible++;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
return visible;
|
|
146
|
+
}, selector);
|
|
147
|
+
|
|
148
|
+
if (count > 0) {
|
|
149
|
+
return { count, selector };
|
|
150
|
+
}
|
|
151
|
+
} catch { /* continue */ }
|
|
152
|
+
}
|
|
153
|
+
return { count: 0, selector: null };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Test menu at specific viewport
|
|
158
|
+
*/
|
|
159
|
+
async function testViewport(page, viewportName, verbose = false) {
|
|
160
|
+
const viewport = VIEWPORTS[viewportName];
|
|
161
|
+
await page.setViewport(viewport);
|
|
162
|
+
await new Promise(r => setTimeout(r, 500)); // Wait for CSS to apply
|
|
163
|
+
|
|
164
|
+
const result = {
|
|
165
|
+
viewport: viewportName,
|
|
166
|
+
dimensions: viewport,
|
|
167
|
+
tests: [],
|
|
168
|
+
passed: 0,
|
|
169
|
+
failed: 0,
|
|
170
|
+
warnings: []
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (verbose) console.error(`\n📱 Testing ${viewportName} (${viewport.width}x${viewport.height})...`);
|
|
174
|
+
|
|
175
|
+
// Test 1: Navigation container exists
|
|
176
|
+
const navResult = await findElement(page, MENU_SELECTORS.navContainers);
|
|
177
|
+
if (navResult) {
|
|
178
|
+
result.tests.push({
|
|
179
|
+
name: 'Navigation container exists',
|
|
180
|
+
passed: true,
|
|
181
|
+
selector: navResult.selector
|
|
182
|
+
});
|
|
183
|
+
result.passed++;
|
|
184
|
+
if (verbose) console.error(` ✓ Navigation container found: ${navResult.selector}`);
|
|
185
|
+
} else {
|
|
186
|
+
result.tests.push({
|
|
187
|
+
name: 'Navigation container exists',
|
|
188
|
+
passed: false,
|
|
189
|
+
error: 'No navigation container found'
|
|
190
|
+
});
|
|
191
|
+
result.failed++;
|
|
192
|
+
if (verbose) console.error(` ✗ Navigation container not found`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Test 2: Menu items visibility
|
|
196
|
+
const menuItems = await countVisibleMenuItems(page);
|
|
197
|
+
|
|
198
|
+
// Different expectations based on viewport
|
|
199
|
+
if (viewportName === 'desktop') {
|
|
200
|
+
// Desktop should have visible menu items
|
|
201
|
+
if (menuItems.count >= 2) {
|
|
202
|
+
result.tests.push({
|
|
203
|
+
name: 'Desktop menu items visible',
|
|
204
|
+
passed: true,
|
|
205
|
+
count: menuItems.count,
|
|
206
|
+
selector: menuItems.selector
|
|
207
|
+
});
|
|
208
|
+
result.passed++;
|
|
209
|
+
if (verbose) console.error(` ✓ ${menuItems.count} menu items visible`);
|
|
210
|
+
} else {
|
|
211
|
+
result.tests.push({
|
|
212
|
+
name: 'Desktop menu items visible',
|
|
213
|
+
passed: false,
|
|
214
|
+
count: menuItems.count,
|
|
215
|
+
error: 'Expected at least 2 visible menu items on desktop'
|
|
216
|
+
});
|
|
217
|
+
result.failed++;
|
|
218
|
+
if (verbose) console.error(` ✗ Only ${menuItems.count} menu items visible (expected >= 2)`);
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
// Mobile/Tablet - check for hamburger menu
|
|
222
|
+
const toggleResult = await findElement(page, MENU_SELECTORS.toggleButtons);
|
|
223
|
+
|
|
224
|
+
if (toggleResult) {
|
|
225
|
+
const isToggleVisible = await isElementVisible(page, toggleResult.selector);
|
|
226
|
+
|
|
227
|
+
if (isToggleVisible) {
|
|
228
|
+
result.tests.push({
|
|
229
|
+
name: 'Mobile menu toggle visible',
|
|
230
|
+
passed: true,
|
|
231
|
+
selector: toggleResult.selector
|
|
232
|
+
});
|
|
233
|
+
result.passed++;
|
|
234
|
+
if (verbose) console.error(` ✓ Menu toggle visible: ${toggleResult.selector}`);
|
|
235
|
+
|
|
236
|
+
// Test toggle functionality
|
|
237
|
+
try {
|
|
238
|
+
// Get initial menu state
|
|
239
|
+
const initialMenuItems = await countVisibleMenuItems(page);
|
|
240
|
+
|
|
241
|
+
// Click toggle
|
|
242
|
+
await toggleResult.element.click();
|
|
243
|
+
await new Promise(r => setTimeout(r, 500)); // Wait for animation
|
|
244
|
+
|
|
245
|
+
// Check menu state after click
|
|
246
|
+
const afterClickItems = await countVisibleMenuItems(page);
|
|
247
|
+
|
|
248
|
+
// Menu should either show more items or we can detect state change
|
|
249
|
+
if (afterClickItems.count !== initialMenuItems.count || afterClickItems.count >= 2) {
|
|
250
|
+
result.tests.push({
|
|
251
|
+
name: 'Menu toggle functionality',
|
|
252
|
+
passed: true,
|
|
253
|
+
before: initialMenuItems.count,
|
|
254
|
+
after: afterClickItems.count
|
|
255
|
+
});
|
|
256
|
+
result.passed++;
|
|
257
|
+
if (verbose) console.error(` ✓ Toggle works: ${initialMenuItems.count} -> ${afterClickItems.count} items`);
|
|
258
|
+
|
|
259
|
+
// Click again to close
|
|
260
|
+
await toggleResult.element.click();
|
|
261
|
+
await new Promise(r => setTimeout(r, 300));
|
|
262
|
+
} else {
|
|
263
|
+
result.tests.push({
|
|
264
|
+
name: 'Menu toggle functionality',
|
|
265
|
+
passed: false,
|
|
266
|
+
before: initialMenuItems.count,
|
|
267
|
+
after: afterClickItems.count,
|
|
268
|
+
warning: 'Toggle may not be functional - no state change detected'
|
|
269
|
+
});
|
|
270
|
+
result.warnings.push('Menu toggle click did not change visible items');
|
|
271
|
+
if (verbose) console.error(` ⚠ Toggle click had no effect`);
|
|
272
|
+
}
|
|
273
|
+
} catch (err) {
|
|
274
|
+
result.tests.push({
|
|
275
|
+
name: 'Menu toggle functionality',
|
|
276
|
+
passed: false,
|
|
277
|
+
error: err.message
|
|
278
|
+
});
|
|
279
|
+
result.failed++;
|
|
280
|
+
if (verbose) console.error(` ✗ Toggle click failed: ${err.message}`);
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
result.warnings.push('Menu toggle found but not visible');
|
|
284
|
+
if (verbose) console.error(` ⚠ Menu toggle found but not visible`);
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
// No hamburger - check if menu items are still visible (maybe it's a small visible menu)
|
|
288
|
+
if (menuItems.count >= 2) {
|
|
289
|
+
result.tests.push({
|
|
290
|
+
name: 'Mobile menu visible without toggle',
|
|
291
|
+
passed: true,
|
|
292
|
+
count: menuItems.count,
|
|
293
|
+
note: 'Menu shows items without hamburger toggle'
|
|
294
|
+
});
|
|
295
|
+
result.passed++;
|
|
296
|
+
if (verbose) console.error(` ✓ ${menuItems.count} menu items visible (no toggle needed)`);
|
|
297
|
+
} else {
|
|
298
|
+
result.tests.push({
|
|
299
|
+
name: 'Mobile menu accessibility',
|
|
300
|
+
passed: false,
|
|
301
|
+
error: 'No hamburger toggle found and menu items hidden'
|
|
302
|
+
});
|
|
303
|
+
result.failed++;
|
|
304
|
+
if (verbose) console.error(` ✗ No hamburger toggle and menu items hidden`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Capture screenshot for debugging
|
|
314
|
+
*/
|
|
315
|
+
async function captureDebugScreenshot(page, outputDir, viewportName) {
|
|
316
|
+
if (!outputDir) return null;
|
|
317
|
+
|
|
318
|
+
const screenshotPath = path.join(outputDir, `menu-test-${viewportName}.png`);
|
|
319
|
+
await page.screenshot({
|
|
320
|
+
path: screenshotPath,
|
|
321
|
+
fullPage: false
|
|
322
|
+
});
|
|
323
|
+
return screenshotPath;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Main verification function
|
|
328
|
+
*/
|
|
329
|
+
async function verifyMenu() {
|
|
330
|
+
const args = parseArgs(process.argv.slice(2));
|
|
331
|
+
|
|
332
|
+
if (!args.html && !args.url) {
|
|
333
|
+
outputError(new Error('Either --html or --url is required'));
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const verbose = args.verbose === 'true';
|
|
338
|
+
const outputDir = args.output;
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
// Launch browser
|
|
342
|
+
const browser = await getBrowser({ headless: args.headless !== 'false' });
|
|
343
|
+
const page = await getPage(browser);
|
|
344
|
+
|
|
345
|
+
// Navigate to page
|
|
346
|
+
let targetUrl;
|
|
347
|
+
if (args.html) {
|
|
348
|
+
// Local file
|
|
349
|
+
const absolutePath = path.resolve(args.html);
|
|
350
|
+
targetUrl = `file://${absolutePath}`;
|
|
351
|
+
} else {
|
|
352
|
+
targetUrl = args.url;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (verbose) console.error(`\n🔍 Verifying responsive menu: ${targetUrl}\n`);
|
|
356
|
+
|
|
357
|
+
await page.goto(targetUrl, {
|
|
358
|
+
waitUntil: 'networkidle2',
|
|
359
|
+
timeout: 30000
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Test all viewports
|
|
363
|
+
const results = {
|
|
364
|
+
success: true,
|
|
365
|
+
url: targetUrl,
|
|
366
|
+
viewports: {},
|
|
367
|
+
summary: {
|
|
368
|
+
totalTests: 0,
|
|
369
|
+
passed: 0,
|
|
370
|
+
failed: 0,
|
|
371
|
+
warnings: []
|
|
372
|
+
},
|
|
373
|
+
screenshots: []
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
for (const viewportName of ['mobile', 'tablet', 'desktop']) {
|
|
377
|
+
const viewportResult = await testViewport(page, viewportName, verbose);
|
|
378
|
+
results.viewports[viewportName] = viewportResult;
|
|
379
|
+
|
|
380
|
+
results.summary.totalTests += viewportResult.tests.length;
|
|
381
|
+
results.summary.passed += viewportResult.passed;
|
|
382
|
+
results.summary.failed += viewportResult.failed;
|
|
383
|
+
results.summary.warnings.push(...viewportResult.warnings);
|
|
384
|
+
|
|
385
|
+
// Capture debug screenshot if output dir provided
|
|
386
|
+
if (outputDir) {
|
|
387
|
+
const screenshotPath = await captureDebugScreenshot(page, outputDir, viewportName);
|
|
388
|
+
if (screenshotPath) results.screenshots.push(screenshotPath);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Determine overall success
|
|
393
|
+
results.success = results.summary.failed === 0;
|
|
394
|
+
|
|
395
|
+
// Close browser
|
|
396
|
+
if (args.close === 'true') {
|
|
397
|
+
await closeBrowser();
|
|
398
|
+
} else {
|
|
399
|
+
await disconnectBrowser();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Final summary
|
|
403
|
+
if (verbose) {
|
|
404
|
+
console.error('\n📊 Summary:');
|
|
405
|
+
console.error(` Tests: ${results.summary.passed}/${results.summary.totalTests} passed`);
|
|
406
|
+
if (results.summary.warnings.length > 0) {
|
|
407
|
+
console.error(` Warnings: ${results.summary.warnings.length}`);
|
|
408
|
+
}
|
|
409
|
+
console.error(` Status: ${results.success ? '✓ PASS' : '✗ FAIL'}\n`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
outputJSON(results);
|
|
413
|
+
process.exit(results.success ? 0 : 1);
|
|
414
|
+
|
|
415
|
+
} catch (error) {
|
|
416
|
+
outputError(error);
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Run
|
|
422
|
+
verifyMenu();
|