design-clone 1.2.0 → 2.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.
Files changed (66) hide show
  1. package/README.md +26 -12
  2. package/bin/commands/clone-site.js +75 -10
  3. package/bin/commands/init.js +33 -1
  4. package/bin/commands/verify.js +5 -3
  5. package/bin/utils/validate.js +24 -8
  6. package/docs/cli-reference.md +200 -2
  7. package/docs/codebase-summary.md +309 -0
  8. package/docs/design-clone-architecture.md +259 -42
  9. package/docs/pixel-perfect.md +35 -4
  10. package/docs/project-roadmap.md +382 -0
  11. package/docs/troubleshooting.md +5 -4
  12. package/package.json +10 -8
  13. package/src/ai/__pycache__/analyze-structure.cpython-313.pyc +0 -0
  14. package/src/ai/__pycache__/extract-design-tokens.cpython-313.pyc +0 -0
  15. package/src/ai/analyze-structure.py +73 -3
  16. package/src/ai/extract-design-tokens.py +356 -13
  17. package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
  18. package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
  19. package/src/ai/prompts/__pycache__/ux_audit.cpython-313.pyc +0 -0
  20. package/src/ai/prompts/design_tokens.py +133 -0
  21. package/src/ai/prompts/structure_analysis.py +329 -10
  22. package/src/ai/prompts/ux_audit.py +198 -0
  23. package/src/ai/ux-audit.js +596 -0
  24. package/src/core/app-state-snapshot.js +511 -0
  25. package/src/core/content-counter.js +342 -0
  26. package/src/core/cookie-handler.js +1 -1
  27. package/src/core/css-extractor.js +4 -4
  28. package/src/core/dimension-extractor.js +93 -21
  29. package/src/core/dimension-output.js +103 -6
  30. package/src/core/discover-pages.js +242 -14
  31. package/src/core/dom-tree-analyzer.js +298 -0
  32. package/src/core/extract-assets.js +1 -1
  33. package/src/core/framework-detector.js +538 -0
  34. package/src/core/html-extractor.js +45 -4
  35. package/src/core/lazy-loader.js +7 -7
  36. package/src/core/multi-page-screenshot.js +9 -6
  37. package/src/core/page-readiness.js +8 -8
  38. package/src/core/screenshot.js +138 -9
  39. package/src/core/section-cropper.js +209 -0
  40. package/src/core/section-detector.js +386 -0
  41. package/src/core/semantic-enhancer.js +492 -0
  42. package/src/core/state-capture.js +18 -22
  43. package/src/core/tests/test-section-cropper.js +177 -0
  44. package/src/core/tests/test-section-detector.js +55 -0
  45. package/src/core/video-capture.js +152 -146
  46. package/src/route-discoverers/angular-discoverer.js +157 -0
  47. package/src/route-discoverers/astro-discoverer.js +123 -0
  48. package/src/route-discoverers/base-discoverer.js +242 -0
  49. package/src/route-discoverers/index.js +106 -0
  50. package/src/route-discoverers/next-discoverer.js +130 -0
  51. package/src/route-discoverers/nuxt-discoverer.js +138 -0
  52. package/src/route-discoverers/react-discoverer.js +139 -0
  53. package/src/route-discoverers/svelte-discoverer.js +109 -0
  54. package/src/route-discoverers/universal-discoverer.js +227 -0
  55. package/src/route-discoverers/vue-discoverer.js +118 -0
  56. package/src/utils/__init__.py +1 -1
  57. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  58. package/src/utils/browser.js +11 -37
  59. package/src/utils/playwright.js +213 -0
  60. package/src/verification/generate-audit-report.js +398 -0
  61. package/src/verification/verify-footer.js +493 -0
  62. package/src/verification/verify-header.js +486 -0
  63. package/src/verification/verify-layout.js +2 -2
  64. package/src/verification/verify-menu.js +4 -20
  65. package/src/verification/verify-slider.js +533 -0
  66. package/src/utils/puppeteer.js +0 -281
@@ -0,0 +1,486 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Header Verification Script
4
+ *
5
+ * Tests header components across viewports:
6
+ * - Logo presence and positioning
7
+ * - Navigation links visibility
8
+ * - CTA buttons
9
+ * - Sticky/fixed behavior
10
+ * - Z-index layering
11
+ * - Height consistency
12
+ *
13
+ * Usage:
14
+ * node verify-header.js --html <path> [--verbose]
15
+ * node verify-header.js --url <url> [--verbose]
16
+ *
17
+ * Options:
18
+ * --html Path to local HTML file
19
+ * --url URL to test
20
+ * --output Output directory for screenshots
21
+ * --verbose Show detailed progress
22
+ */
23
+
24
+ import fs from 'fs/promises';
25
+ import path from 'path';
26
+
27
+ import { getBrowser, getPage, closeBrowser, disconnectBrowser, parseArgs, outputJSON, outputError } from '../utils/browser.js';
28
+
29
+ // Viewport configurations
30
+ const VIEWPORTS = {
31
+ mobile: { width: 375, height: 812, deviceScaleFactor: 2 },
32
+ tablet: { width: 768, height: 1024, deviceScaleFactor: 1 },
33
+ desktop: { width: 1920, height: 1080, deviceScaleFactor: 1 }
34
+ };
35
+
36
+ // Header element selectors
37
+ const HEADER_SELECTORS = {
38
+ container: [
39
+ 'header',
40
+ '[role="banner"]',
41
+ '.header',
42
+ '#header',
43
+ '.site-header',
44
+ '.page-header',
45
+ '.masthead'
46
+ ],
47
+ logo: [
48
+ 'header img[alt*="logo" i]',
49
+ '[role="banner"] img',
50
+ '.logo img',
51
+ '.site-logo img',
52
+ '.logo',
53
+ '.site-logo',
54
+ 'header a[href="/"] img',
55
+ '.brand img',
56
+ '.navbar-brand img'
57
+ ],
58
+ nav: [
59
+ 'header nav',
60
+ 'header [role="navigation"]',
61
+ '.header-nav',
62
+ '.main-navigation',
63
+ '.primary-nav',
64
+ '.site-nav',
65
+ '.navbar-nav'
66
+ ],
67
+ cta: [
68
+ 'header button.cta',
69
+ 'header a[class*="button"]',
70
+ 'header a[class*="btn"]',
71
+ '.header-action',
72
+ '.nav-cta',
73
+ 'header .btn-primary',
74
+ 'header a[href*="contact"]',
75
+ 'header a[href*="signup"]',
76
+ 'header a[href*="login"]'
77
+ ],
78
+ navLinks: [
79
+ 'header nav a',
80
+ 'header [role="navigation"] a',
81
+ '.main-navigation a',
82
+ '.nav-item a',
83
+ '.menu-item a'
84
+ ]
85
+ };
86
+
87
+ /**
88
+ * Find first matching element from selectors
89
+ */
90
+ async function findElement(page, selectors) {
91
+ for (const selector of selectors) {
92
+ try {
93
+ const element = await page.$(selector);
94
+ if (element) {
95
+ return { element, selector };
96
+ }
97
+ } catch (err) { /* continue - selector not found */ }
98
+ }
99
+ return null;
100
+ }
101
+
102
+ /**
103
+ * Count visible elements
104
+ */
105
+ async function countVisibleElements(page, selectors) {
106
+ for (const selector of selectors) {
107
+ try {
108
+ const count = await page.evaluate((sel) => {
109
+ const items = document.querySelectorAll(sel);
110
+ let visible = 0;
111
+ items.forEach(item => {
112
+ const style = window.getComputedStyle(item);
113
+ const rect = item.getBoundingClientRect();
114
+ if (
115
+ style.display !== 'none' &&
116
+ style.visibility !== 'hidden' &&
117
+ style.opacity !== '0' &&
118
+ rect.width > 0 &&
119
+ rect.height > 0
120
+ ) {
121
+ visible++;
122
+ }
123
+ });
124
+ return visible;
125
+ }, selector);
126
+
127
+ if (count > 0) {
128
+ return { count, selector };
129
+ }
130
+ } catch (err) { /* continue - selector not found */ }
131
+ }
132
+ return { count: 0, selector: null };
133
+ }
134
+
135
+ /**
136
+ * Check header position properties
137
+ */
138
+ async function checkHeaderPosition(page, headerSelector) {
139
+ return await page.evaluate((sel) => {
140
+ const header = document.querySelector(sel);
141
+ if (!header) return null;
142
+
143
+ const style = window.getComputedStyle(header);
144
+ const rect = header.getBoundingClientRect();
145
+
146
+ return {
147
+ position: style.position,
148
+ isSticky: style.position === 'sticky',
149
+ isFixed: style.position === 'fixed',
150
+ zIndex: parseInt(style.zIndex) || 'auto',
151
+ top: rect.top,
152
+ height: rect.height,
153
+ width: rect.width
154
+ };
155
+ }, headerSelector);
156
+ }
157
+
158
+ /**
159
+ * Check logo position (typically left or center)
160
+ */
161
+ async function checkLogoPosition(page, logoSelector, headerWidth) {
162
+ return await page.evaluate((sel, width) => {
163
+ const logo = document.querySelector(sel);
164
+ if (!logo) return null;
165
+
166
+ const rect = logo.getBoundingClientRect();
167
+ const centerThreshold = width * 0.35;
168
+
169
+ let position = 'unknown';
170
+ if (rect.left < centerThreshold) {
171
+ position = 'left';
172
+ } else if (rect.left > width - centerThreshold) {
173
+ position = 'right';
174
+ } else {
175
+ position = 'center';
176
+ }
177
+
178
+ return {
179
+ position,
180
+ x: rect.left,
181
+ y: rect.top,
182
+ width: rect.width,
183
+ height: rect.height
184
+ };
185
+ }, logoSelector, headerWidth);
186
+ }
187
+
188
+ /**
189
+ * Test header at specific viewport
190
+ */
191
+ async function testViewport(page, viewportName, verbose = false) {
192
+ const viewport = VIEWPORTS[viewportName];
193
+ await page.setViewportSize(viewport);
194
+ await new Promise(r => setTimeout(r, 500));
195
+
196
+ const result = {
197
+ viewport: viewportName,
198
+ dimensions: viewport,
199
+ tests: [],
200
+ passed: 0,
201
+ failed: 0,
202
+ warnings: []
203
+ };
204
+
205
+ if (verbose) console.error(`\nšŸ“± Testing ${viewportName} (${viewport.width}x${viewport.height})...`);
206
+
207
+ // Test 1: Header container exists
208
+ const headerResult = await findElement(page, HEADER_SELECTORS.container);
209
+ if (headerResult) {
210
+ result.tests.push({
211
+ name: 'Header container exists',
212
+ passed: true,
213
+ selector: headerResult.selector
214
+ });
215
+ result.passed++;
216
+ if (verbose) console.error(` āœ“ Header found: ${headerResult.selector}`);
217
+
218
+ // Get header position info
219
+ const positionInfo = await checkHeaderPosition(page, headerResult.selector);
220
+
221
+ // Test 2: Logo presence
222
+ const logoResult = await findElement(page, HEADER_SELECTORS.logo);
223
+ if (logoResult) {
224
+ const logoPosition = await checkLogoPosition(page, logoResult.selector, viewport.width);
225
+ result.tests.push({
226
+ name: 'Logo present',
227
+ passed: true,
228
+ selector: logoResult.selector,
229
+ position: logoPosition?.position || 'unknown'
230
+ });
231
+ result.passed++;
232
+ if (verbose) console.error(` āœ“ Logo found: ${logoResult.selector} (${logoPosition?.position})`);
233
+ } else {
234
+ result.tests.push({
235
+ name: 'Logo present',
236
+ passed: false,
237
+ error: 'No logo found'
238
+ });
239
+ result.failed++;
240
+ if (verbose) console.error(` āœ— Logo not found`);
241
+ }
242
+
243
+ // Test 3: Navigation links
244
+ const navLinks = await countVisibleElements(page, HEADER_SELECTORS.navLinks);
245
+ const expectedLinks = viewportName === 'desktop' ? 2 : 0;
246
+
247
+ if (navLinks.count >= expectedLinks) {
248
+ result.tests.push({
249
+ name: 'Navigation links visible',
250
+ passed: true,
251
+ count: navLinks.count,
252
+ selector: navLinks.selector
253
+ });
254
+ result.passed++;
255
+ if (verbose) console.error(` āœ“ ${navLinks.count} nav links visible`);
256
+ } else if (viewportName !== 'desktop' && navLinks.count === 0) {
257
+ // Mobile/tablet may hide links behind hamburger
258
+ result.tests.push({
259
+ name: 'Navigation links (may be in hamburger)',
260
+ passed: true,
261
+ count: navLinks.count,
262
+ note: 'Links may be hidden in mobile menu'
263
+ });
264
+ result.passed++;
265
+ if (verbose) console.error(` āœ“ Nav links hidden (expected on ${viewportName})`);
266
+ } else {
267
+ result.tests.push({
268
+ name: 'Navigation links visible',
269
+ passed: false,
270
+ count: navLinks.count,
271
+ error: `Expected at least ${expectedLinks} links on ${viewportName}`
272
+ });
273
+ result.failed++;
274
+ if (verbose) console.error(` āœ— Only ${navLinks.count} nav links (expected >= ${expectedLinks})`);
275
+ }
276
+
277
+ // Test 4: CTA buttons (desktop only)
278
+ if (viewportName === 'desktop') {
279
+ const ctaResult = await findElement(page, HEADER_SELECTORS.cta);
280
+ if (ctaResult) {
281
+ result.tests.push({
282
+ name: 'CTA button present',
283
+ passed: true,
284
+ selector: ctaResult.selector
285
+ });
286
+ result.passed++;
287
+ if (verbose) console.error(` āœ“ CTA found: ${ctaResult.selector}`);
288
+ } else {
289
+ result.warnings.push('No CTA button found in header');
290
+ if (verbose) console.error(` ⚠ No CTA button found`);
291
+ }
292
+ }
293
+
294
+ // Test 5: Sticky/fixed behavior
295
+ if (positionInfo) {
296
+ if (positionInfo.isSticky || positionInfo.isFixed) {
297
+ result.tests.push({
298
+ name: 'Header sticky/fixed behavior',
299
+ passed: true,
300
+ position: positionInfo.position
301
+ });
302
+ result.passed++;
303
+ if (verbose) console.error(` āœ“ Header is ${positionInfo.position}`);
304
+ } else {
305
+ result.tests.push({
306
+ name: 'Header sticky/fixed behavior',
307
+ passed: true,
308
+ position: positionInfo.position,
309
+ note: 'Header uses static/relative positioning'
310
+ });
311
+ result.passed++;
312
+ if (verbose) console.error(` āœ“ Header position: ${positionInfo.position}`);
313
+ }
314
+
315
+ // Test 6: Z-index check (should be high for sticky/fixed)
316
+ if ((positionInfo.isSticky || positionInfo.isFixed) && positionInfo.zIndex !== 'auto') {
317
+ const zIndexOk = positionInfo.zIndex >= 100;
318
+ result.tests.push({
319
+ name: 'Z-index layering',
320
+ passed: zIndexOk,
321
+ zIndex: positionInfo.zIndex,
322
+ note: zIndexOk ? 'Header on top layer' : 'Z-index may be too low'
323
+ });
324
+ if (zIndexOk) result.passed++;
325
+ else result.warnings.push(`Header z-index (${positionInfo.zIndex}) may be too low`);
326
+ if (verbose) console.error(` ${zIndexOk ? 'āœ“' : '⚠'} Z-index: ${positionInfo.zIndex}`);
327
+ }
328
+
329
+ // Store height for consistency check
330
+ result.headerHeight = positionInfo.height;
331
+ }
332
+
333
+ } else {
334
+ result.tests.push({
335
+ name: 'Header container exists',
336
+ passed: false,
337
+ error: 'No header container found'
338
+ });
339
+ result.failed++;
340
+ if (verbose) console.error(` āœ— Header not found`);
341
+ }
342
+
343
+ return result;
344
+ }
345
+
346
+ /**
347
+ * Capture component screenshot
348
+ */
349
+ async function captureHeaderScreenshot(page, outputDir, viewportName) {
350
+ if (!outputDir) return null;
351
+
352
+ const screenshotPath = path.join(outputDir, `header-test-${viewportName}.png`);
353
+ await page.screenshot({
354
+ path: screenshotPath,
355
+ fullPage: false
356
+ });
357
+ return screenshotPath;
358
+ }
359
+
360
+ /**
361
+ * Validate HTML file path (security: prevent path traversal)
362
+ */
363
+ function validateHtmlPath(htmlPath) {
364
+ const absolutePath = path.resolve(htmlPath);
365
+ const cwd = process.cwd();
366
+
367
+ // Allow paths within CWD or common output directories
368
+ const allowedPrefixes = [
369
+ cwd,
370
+ path.join(process.env.HOME || '', '.claude'),
371
+ '/tmp',
372
+ path.join(process.env.HOME || '', 'cloned-designs')
373
+ ];
374
+
375
+ const isAllowed = allowedPrefixes.some(prefix => absolutePath.startsWith(prefix));
376
+ if (!isAllowed) {
377
+ throw new Error(`Path "${htmlPath}" is outside allowed directories`);
378
+ }
379
+
380
+ return absolutePath;
381
+ }
382
+
383
+ /**
384
+ * Main verification function
385
+ */
386
+ async function verifyHeader() {
387
+ const args = parseArgs(process.argv.slice(2));
388
+
389
+ if (!args.html && !args.url) {
390
+ outputError(new Error('Either --html or --url is required'));
391
+ process.exit(1);
392
+ }
393
+
394
+ const verbose = args.verbose === 'true';
395
+ const outputDir = args.output;
396
+
397
+ try {
398
+ const browser = await getBrowser({ headless: args.headless !== 'false' });
399
+ const page = await getPage(browser);
400
+
401
+ let targetUrl;
402
+ if (args.html) {
403
+ const absolutePath = validateHtmlPath(args.html);
404
+ targetUrl = `file://${absolutePath}`;
405
+ } else {
406
+ targetUrl = args.url;
407
+ }
408
+
409
+ if (verbose) console.error(`\nšŸ” Verifying header: ${targetUrl}\n`);
410
+
411
+ await page.goto(targetUrl, {
412
+ waitUntil: 'networkidle',
413
+ timeout: 30000
414
+ });
415
+
416
+ const results = {
417
+ success: true,
418
+ component: 'header',
419
+ url: targetUrl,
420
+ viewports: {},
421
+ summary: {
422
+ totalTests: 0,
423
+ passed: 0,
424
+ failed: 0,
425
+ warnings: []
426
+ },
427
+ screenshots: [],
428
+ heightConsistency: {}
429
+ };
430
+
431
+ // Test all viewports
432
+ for (const viewportName of ['mobile', 'tablet', 'desktop']) {
433
+ const viewportResult = await testViewport(page, viewportName, verbose);
434
+ results.viewports[viewportName] = viewportResult;
435
+
436
+ results.summary.totalTests += viewportResult.tests.length;
437
+ results.summary.passed += viewportResult.passed;
438
+ results.summary.failed += viewportResult.failed;
439
+ results.summary.warnings.push(...viewportResult.warnings);
440
+
441
+ if (viewportResult.headerHeight) {
442
+ results.heightConsistency[viewportName] = viewportResult.headerHeight;
443
+ }
444
+
445
+ if (outputDir) {
446
+ const screenshotPath = await captureHeaderScreenshot(page, outputDir, viewportName);
447
+ if (screenshotPath) results.screenshots.push(screenshotPath);
448
+ }
449
+ }
450
+
451
+ // Check height consistency
452
+ const heights = Object.values(results.heightConsistency);
453
+ if (heights.length >= 2) {
454
+ const maxDiff = Math.max(...heights) - Math.min(...heights);
455
+ if (maxDiff > 20) {
456
+ results.summary.warnings.push(`Header height varies by ${maxDiff}px across viewports`);
457
+ }
458
+ }
459
+
460
+ results.success = results.summary.failed === 0;
461
+
462
+ if (args.close === 'true') {
463
+ await closeBrowser();
464
+ } else {
465
+ await disconnectBrowser();
466
+ }
467
+
468
+ if (verbose) {
469
+ console.error('\nšŸ“Š Summary:');
470
+ console.error(` Tests: ${results.summary.passed}/${results.summary.totalTests} passed`);
471
+ if (results.summary.warnings.length > 0) {
472
+ console.error(` Warnings: ${results.summary.warnings.length}`);
473
+ }
474
+ console.error(` Status: ${results.success ? 'āœ“ PASS' : 'āœ— FAIL'}\n`);
475
+ }
476
+
477
+ outputJSON(results);
478
+ process.exit(results.success ? 0 : 1);
479
+
480
+ } catch (error) {
481
+ outputError(error);
482
+ process.exit(1);
483
+ }
484
+ }
485
+
486
+ verifyHeader();
@@ -36,7 +36,7 @@ const VIEWPORTS = {
36
36
  * Capture screenshot of generated HTML at specific viewport
37
37
  */
38
38
  async function captureGeneratedScreenshot(page, viewport, outputPath) {
39
- await page.setViewport(viewport);
39
+ await page.setViewportSize(viewport);
40
40
  await new Promise(r => setTimeout(r, 500)); // Wait for CSS to apply
41
41
 
42
42
  await page.screenshot({
@@ -328,7 +328,7 @@ async function verifyLayout() {
328
328
  const targetUrl = `file://${absolutePath}`;
329
329
 
330
330
  await page.goto(targetUrl, {
331
- waitUntil: 'networkidle2',
331
+ waitUntil: 'networkidle',
332
332
  timeout: 30000
333
333
  });
334
334
 
@@ -81,27 +81,11 @@ const MENU_SELECTORS = {
81
81
  };
82
82
 
83
83
  /**
84
- * Check if element is visible
84
+ * Check if element is visible using Playwright locator API
85
85
  */
86
86
  async function isElementVisible(page, selector) {
87
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;
88
+ return await page.locator(selector).isVisible();
105
89
  } catch {
106
90
  return false;
107
91
  }
@@ -158,7 +142,7 @@ async function countVisibleMenuItems(page) {
158
142
  */
159
143
  async function testViewport(page, viewportName, verbose = false) {
160
144
  const viewport = VIEWPORTS[viewportName];
161
- await page.setViewport(viewport);
145
+ await page.setViewportSize(viewport);
162
146
  await new Promise(r => setTimeout(r, 500)); // Wait for CSS to apply
163
147
 
164
148
  const result = {
@@ -355,7 +339,7 @@ async function verifyMenu() {
355
339
  if (verbose) console.error(`\nšŸ” Verifying responsive menu: ${targetUrl}\n`);
356
340
 
357
341
  await page.goto(targetUrl, {
358
- waitUntil: 'networkidle2',
342
+ waitUntil: 'networkidle',
359
343
  timeout: 30000
360
344
  });
361
345