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,493 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Footer Verification Script
4
+ *
5
+ * Tests footer components across viewports:
6
+ * - Position at bottom of page
7
+ * - Multi-column layout detection
8
+ * - Link sections completeness
9
+ * - Copyright text presence
10
+ * - Social icons
11
+ * - Background contrast
12
+ *
13
+ * Usage:
14
+ * node verify-footer.js --html <path> [--verbose]
15
+ * node verify-footer.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
+ // Footer element selectors
37
+ const FOOTER_SELECTORS = {
38
+ container: [
39
+ 'footer',
40
+ '[role="contentinfo"]',
41
+ '.footer',
42
+ '#footer',
43
+ '.site-footer',
44
+ '.page-footer'
45
+ ],
46
+ columns: [
47
+ 'footer [class*="column"]',
48
+ 'footer [class*="col-"]',
49
+ 'footer .col',
50
+ '.footer-column',
51
+ '.footer-widget',
52
+ '.footer-section',
53
+ 'footer > div > div'
54
+ ],
55
+ links: [
56
+ 'footer a[href]',
57
+ '.footer-links a',
58
+ '.footer-nav a',
59
+ 'footer nav a',
60
+ 'footer ul a'
61
+ ],
62
+ copyright: [
63
+ 'footer [class*="copyright"]',
64
+ '.copyright',
65
+ 'footer small',
66
+ 'footer p:last-child'
67
+ ],
68
+ socialIcons: [
69
+ 'footer a[href*="facebook"]',
70
+ 'footer a[href*="twitter"]',
71
+ 'footer a[href*="instagram"]',
72
+ 'footer a[href*="linkedin"]',
73
+ 'footer a[href*="youtube"]',
74
+ 'footer [class*="social"]',
75
+ '.social-links a',
76
+ '.social-icons a'
77
+ ]
78
+ };
79
+
80
+ /**
81
+ * Find first matching element
82
+ */
83
+ async function findElement(page, selectors) {
84
+ for (const selector of selectors) {
85
+ try {
86
+ const element = await page.$(selector);
87
+ if (element) {
88
+ return { element, selector };
89
+ }
90
+ } catch (err) { /* continue - selector not found */ }
91
+ }
92
+ return null;
93
+ }
94
+
95
+ /**
96
+ * Count matching elements
97
+ */
98
+ async function countElements(page, selectors) {
99
+ let totalCount = 0;
100
+ let matchedSelector = null;
101
+
102
+ for (const selector of selectors) {
103
+ try {
104
+ const count = await page.locator(selector).count();
105
+ if (count > totalCount) {
106
+ totalCount = count;
107
+ matchedSelector = selector;
108
+ }
109
+ } catch (err) { /* continue - selector not found */ }
110
+ }
111
+ return { count: totalCount, selector: matchedSelector };
112
+ }
113
+
114
+ /**
115
+ * Count visible elements
116
+ */
117
+ async function countVisibleElements(page, selectors) {
118
+ for (const selector of selectors) {
119
+ try {
120
+ const count = await page.evaluate((sel) => {
121
+ const items = document.querySelectorAll(sel);
122
+ let visible = 0;
123
+ items.forEach(item => {
124
+ const style = window.getComputedStyle(item);
125
+ const rect = item.getBoundingClientRect();
126
+ if (
127
+ style.display !== 'none' &&
128
+ style.visibility !== 'hidden' &&
129
+ style.opacity !== '0' &&
130
+ rect.width > 0 &&
131
+ rect.height > 0
132
+ ) {
133
+ visible++;
134
+ }
135
+ });
136
+ return visible;
137
+ }, selector);
138
+
139
+ if (count > 0) {
140
+ return { count, selector };
141
+ }
142
+ } catch (err) { /* continue - selector not found */ }
143
+ }
144
+ return { count: 0, selector: null };
145
+ }
146
+
147
+ /**
148
+ * Check footer position (should be at bottom)
149
+ */
150
+ async function checkFooterPosition(page, footerSelector) {
151
+ return await page.evaluate((sel) => {
152
+ const footer = document.querySelector(sel);
153
+ if (!footer) return null;
154
+
155
+ const rect = footer.getBoundingClientRect();
156
+ const scrollHeight = Math.max(
157
+ document.body.scrollHeight,
158
+ document.documentElement.scrollHeight
159
+ );
160
+
161
+ // Scroll to bottom to get accurate position
162
+ window.scrollTo(0, scrollHeight);
163
+
164
+ const style = window.getComputedStyle(footer);
165
+ const footerBottom = rect.y + window.scrollY + rect.height;
166
+ const tolerance = 50; // Allow 50px tolerance
167
+
168
+ return {
169
+ y: rect.y + window.scrollY,
170
+ height: rect.height,
171
+ width: rect.width,
172
+ pageHeight: scrollHeight,
173
+ isAtBottom: footerBottom >= (scrollHeight - tolerance),
174
+ footerBottom,
175
+ backgroundColor: style.backgroundColor,
176
+ color: style.color
177
+ };
178
+ }, footerSelector);
179
+ }
180
+
181
+ /**
182
+ * Check for copyright text
183
+ */
184
+ async function checkCopyright(page) {
185
+ return await page.evaluate(() => {
186
+ const footer = document.querySelector('footer') || document.querySelector('[role="contentinfo"]');
187
+ if (!footer) return null;
188
+
189
+ const text = footer.textContent || '';
190
+ const currentYear = new Date().getFullYear();
191
+
192
+ const hasCopyright = /©|copyright|all rights reserved/i.test(text);
193
+ const hasYear = new RegExp(`20[0-9]{2}|${currentYear}`).test(text);
194
+
195
+ return {
196
+ hasCopyright,
197
+ hasYear,
198
+ hasCurrentYear: text.includes(String(currentYear))
199
+ };
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Test footer at specific viewport
205
+ */
206
+ async function testViewport(page, viewportName, verbose = false) {
207
+ const viewport = VIEWPORTS[viewportName];
208
+ await page.setViewportSize(viewport);
209
+ await new Promise(r => setTimeout(r, 500));
210
+
211
+ // Scroll to bottom to ensure footer is loaded
212
+ await page.evaluate(() => {
213
+ window.scrollTo(0, document.body.scrollHeight);
214
+ });
215
+ await new Promise(r => setTimeout(r, 300));
216
+
217
+ const result = {
218
+ viewport: viewportName,
219
+ dimensions: viewport,
220
+ tests: [],
221
+ passed: 0,
222
+ failed: 0,
223
+ warnings: []
224
+ };
225
+
226
+ if (verbose) console.error(`\n📱 Testing ${viewportName} (${viewport.width}x${viewport.height})...`);
227
+
228
+ // Test 1: Footer container exists
229
+ const footerResult = await findElement(page, FOOTER_SELECTORS.container);
230
+ if (footerResult) {
231
+ result.tests.push({
232
+ name: 'Footer container exists',
233
+ passed: true,
234
+ selector: footerResult.selector
235
+ });
236
+ result.passed++;
237
+ if (verbose) console.error(` ✓ Footer found: ${footerResult.selector}`);
238
+
239
+ // Test 2: Footer position (at bottom)
240
+ const positionInfo = await checkFooterPosition(page, footerResult.selector);
241
+ if (positionInfo) {
242
+ if (positionInfo.isAtBottom) {
243
+ result.tests.push({
244
+ name: 'Footer at page bottom',
245
+ passed: true,
246
+ y: positionInfo.y,
247
+ pageHeight: positionInfo.pageHeight
248
+ });
249
+ result.passed++;
250
+ if (verbose) console.error(` ✓ Footer at bottom (y: ${Math.round(positionInfo.y)})`);
251
+ } else {
252
+ result.tests.push({
253
+ name: 'Footer at page bottom',
254
+ passed: false,
255
+ y: positionInfo.y,
256
+ footerBottom: positionInfo.footerBottom,
257
+ pageHeight: positionInfo.pageHeight,
258
+ error: 'Footer not at page bottom'
259
+ });
260
+ result.failed++;
261
+ if (verbose) console.error(` ✗ Footer not at bottom (gap: ${positionInfo.pageHeight - positionInfo.footerBottom}px)`);
262
+ }
263
+
264
+ // Store dimensions for report
265
+ result.footerDimensions = {
266
+ height: positionInfo.height,
267
+ width: positionInfo.width,
268
+ backgroundColor: positionInfo.backgroundColor,
269
+ color: positionInfo.color
270
+ };
271
+ }
272
+
273
+ // Test 3: Multi-column layout (desktop/tablet)
274
+ if (viewportName !== 'mobile') {
275
+ const columns = await countElements(page, FOOTER_SELECTORS.columns);
276
+ if (columns.count >= 2) {
277
+ result.tests.push({
278
+ name: 'Multi-column layout',
279
+ passed: true,
280
+ count: columns.count,
281
+ selector: columns.selector
282
+ });
283
+ result.passed++;
284
+ if (verbose) console.error(` ✓ ${columns.count} columns found`);
285
+ } else if (columns.count === 1) {
286
+ result.tests.push({
287
+ name: 'Multi-column layout',
288
+ passed: true,
289
+ count: columns.count,
290
+ note: 'Single column layout'
291
+ });
292
+ result.passed++;
293
+ if (verbose) console.error(` ✓ Single column layout`);
294
+ } else {
295
+ result.warnings.push('No clear column structure detected');
296
+ if (verbose) console.error(` ⚠ No column structure detected`);
297
+ }
298
+ }
299
+
300
+ // Test 4: Links present
301
+ const links = await countVisibleElements(page, FOOTER_SELECTORS.links);
302
+ if (links.count >= 1) {
303
+ result.tests.push({
304
+ name: 'Footer links present',
305
+ passed: true,
306
+ count: links.count,
307
+ selector: links.selector
308
+ });
309
+ result.passed++;
310
+ if (verbose) console.error(` ✓ ${links.count} links found`);
311
+ } else {
312
+ result.warnings.push('No links found in footer');
313
+ if (verbose) console.error(` ⚠ No links found`);
314
+ }
315
+
316
+ // Test 5: Copyright text
317
+ const copyrightInfo = await checkCopyright(page);
318
+ if (copyrightInfo) {
319
+ if (copyrightInfo.hasCopyright || copyrightInfo.hasYear) {
320
+ result.tests.push({
321
+ name: 'Copyright text present',
322
+ passed: true,
323
+ hasCopyright: copyrightInfo.hasCopyright,
324
+ hasCurrentYear: copyrightInfo.hasCurrentYear
325
+ });
326
+ result.passed++;
327
+ if (verbose) console.error(` ✓ Copyright found (current year: ${copyrightInfo.hasCurrentYear})`);
328
+ } else {
329
+ result.warnings.push('No copyright text found');
330
+ if (verbose) console.error(` ⚠ No copyright text`);
331
+ }
332
+ }
333
+
334
+ // Test 6: Social icons (optional)
335
+ const socialIcons = await countVisibleElements(page, FOOTER_SELECTORS.socialIcons);
336
+ if (socialIcons.count > 0) {
337
+ result.tests.push({
338
+ name: 'Social icons present',
339
+ passed: true,
340
+ count: socialIcons.count
341
+ });
342
+ result.passed++;
343
+ if (verbose) console.error(` ✓ ${socialIcons.count} social icons found`);
344
+ } else {
345
+ // Not a failure, just informational
346
+ if (verbose) console.error(` ℹ No social icons found`);
347
+ }
348
+
349
+ } else {
350
+ result.tests.push({
351
+ name: 'Footer container exists',
352
+ passed: false,
353
+ error: 'No footer container found'
354
+ });
355
+ result.failed++;
356
+ if (verbose) console.error(` ✗ Footer not found`);
357
+ }
358
+
359
+ return result;
360
+ }
361
+
362
+ /**
363
+ * Capture footer screenshot
364
+ */
365
+ async function captureFooterScreenshot(page, outputDir, viewportName) {
366
+ if (!outputDir) return null;
367
+
368
+ // Scroll to footer first
369
+ await page.evaluate(() => {
370
+ const footer = document.querySelector('footer') || document.querySelector('[role="contentinfo"]');
371
+ if (footer) footer.scrollIntoView({ behavior: 'instant', block: 'center' });
372
+ });
373
+ await new Promise(r => setTimeout(r, 200));
374
+
375
+ const screenshotPath = path.join(outputDir, `footer-test-${viewportName}.png`);
376
+ await page.screenshot({
377
+ path: screenshotPath,
378
+ fullPage: false
379
+ });
380
+ return screenshotPath;
381
+ }
382
+
383
+ /**
384
+ * Validate HTML file path (security: prevent path traversal)
385
+ */
386
+ function validateHtmlPath(htmlPath) {
387
+ const absolutePath = path.resolve(htmlPath);
388
+ const cwd = process.cwd();
389
+
390
+ const allowedPrefixes = [
391
+ cwd,
392
+ path.join(process.env.HOME || '', '.claude'),
393
+ '/tmp',
394
+ path.join(process.env.HOME || '', 'cloned-designs')
395
+ ];
396
+
397
+ const isAllowed = allowedPrefixes.some(prefix => absolutePath.startsWith(prefix));
398
+ if (!isAllowed) {
399
+ throw new Error(`Path "${htmlPath}" is outside allowed directories`);
400
+ }
401
+
402
+ return absolutePath;
403
+ }
404
+
405
+ /**
406
+ * Main verification function
407
+ */
408
+ async function verifyFooter() {
409
+ const args = parseArgs(process.argv.slice(2));
410
+
411
+ if (!args.html && !args.url) {
412
+ outputError(new Error('Either --html or --url is required'));
413
+ process.exit(1);
414
+ }
415
+
416
+ const verbose = args.verbose === 'true';
417
+ const outputDir = args.output;
418
+
419
+ try {
420
+ const browser = await getBrowser({ headless: args.headless !== 'false' });
421
+ const page = await getPage(browser);
422
+
423
+ let targetUrl;
424
+ if (args.html) {
425
+ const absolutePath = validateHtmlPath(args.html);
426
+ targetUrl = `file://${absolutePath}`;
427
+ } else {
428
+ targetUrl = args.url;
429
+ }
430
+
431
+ if (verbose) console.error(`\n🔍 Verifying footer: ${targetUrl}\n`);
432
+
433
+ await page.goto(targetUrl, {
434
+ waitUntil: 'networkidle',
435
+ timeout: 30000
436
+ });
437
+
438
+ const results = {
439
+ success: true,
440
+ component: 'footer',
441
+ url: targetUrl,
442
+ viewports: {},
443
+ summary: {
444
+ totalTests: 0,
445
+ passed: 0,
446
+ failed: 0,
447
+ warnings: []
448
+ },
449
+ screenshots: []
450
+ };
451
+
452
+ for (const viewportName of ['mobile', 'tablet', 'desktop']) {
453
+ const viewportResult = await testViewport(page, viewportName, verbose);
454
+ results.viewports[viewportName] = viewportResult;
455
+
456
+ results.summary.totalTests += viewportResult.tests.length;
457
+ results.summary.passed += viewportResult.passed;
458
+ results.summary.failed += viewportResult.failed;
459
+ results.summary.warnings.push(...viewportResult.warnings);
460
+
461
+ if (outputDir) {
462
+ const screenshotPath = await captureFooterScreenshot(page, outputDir, viewportName);
463
+ if (screenshotPath) results.screenshots.push(screenshotPath);
464
+ }
465
+ }
466
+
467
+ results.success = results.summary.failed === 0;
468
+
469
+ if (args.close === 'true') {
470
+ await closeBrowser();
471
+ } else {
472
+ await disconnectBrowser();
473
+ }
474
+
475
+ if (verbose) {
476
+ console.error('\n📊 Summary:');
477
+ console.error(` Tests: ${results.summary.passed}/${results.summary.totalTests} passed`);
478
+ if (results.summary.warnings.length > 0) {
479
+ console.error(` Warnings: ${results.summary.warnings.length}`);
480
+ }
481
+ console.error(` Status: ${results.success ? '✓ PASS' : '✗ FAIL'}\n`);
482
+ }
483
+
484
+ outputJSON(results);
485
+ process.exit(results.success ? 0 : 1);
486
+
487
+ } catch (error) {
488
+ outputError(error);
489
+ process.exit(1);
490
+ }
491
+ }
492
+
493
+ verifyFooter();