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.
Files changed (47) hide show
  1. package/.env.example +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -0
  4. package/SKILL.md +239 -0
  5. package/bin/cli.js +45 -0
  6. package/bin/commands/help.js +29 -0
  7. package/bin/commands/init.js +126 -0
  8. package/bin/commands/verify.js +99 -0
  9. package/bin/utils/copy.js +65 -0
  10. package/bin/utils/validate.js +122 -0
  11. package/docs/basic-clone.md +63 -0
  12. package/docs/cli-reference.md +94 -0
  13. package/docs/design-clone-architecture.md +247 -0
  14. package/docs/pixel-perfect.md +86 -0
  15. package/docs/troubleshooting.md +97 -0
  16. package/package.json +57 -0
  17. package/requirements.txt +5 -0
  18. package/src/ai/analyze-structure.py +305 -0
  19. package/src/ai/extract-design-tokens.py +439 -0
  20. package/src/ai/prompts/__init__.py +2 -0
  21. package/src/ai/prompts/design_tokens.py +183 -0
  22. package/src/ai/prompts/structure_analysis.py +273 -0
  23. package/src/core/cookie-handler.js +76 -0
  24. package/src/core/css-extractor.js +107 -0
  25. package/src/core/dimension-extractor.js +366 -0
  26. package/src/core/dimension-output.js +208 -0
  27. package/src/core/extract-assets.js +468 -0
  28. package/src/core/filter-css.js +499 -0
  29. package/src/core/html-extractor.js +102 -0
  30. package/src/core/lazy-loader.js +188 -0
  31. package/src/core/page-readiness.js +161 -0
  32. package/src/core/screenshot.js +380 -0
  33. package/src/post-process/enhance-assets.js +157 -0
  34. package/src/post-process/fetch-images.js +398 -0
  35. package/src/post-process/inject-icons.js +311 -0
  36. package/src/utils/__init__.py +16 -0
  37. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  38. package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
  39. package/src/utils/browser.js +103 -0
  40. package/src/utils/env.js +153 -0
  41. package/src/utils/env.py +134 -0
  42. package/src/utils/helpers.js +71 -0
  43. package/src/utils/puppeteer.js +281 -0
  44. package/src/verification/verify-layout.js +424 -0
  45. package/src/verification/verify-menu.js +422 -0
  46. package/templates/base.css +705 -0
  47. 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();