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,533 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Slider/Carousel Verification Script
4
+ *
5
+ * Tests slider components:
6
+ * - Library detection (Swiper, Slick, Owl, native)
7
+ * - Arrow/navigation visibility
8
+ * - Pagination dots presence
9
+ * - Autoplay detection (requires 2 slide changes in 6s)
10
+ * - Current slide indicator
11
+ *
12
+ * Usage:
13
+ * node verify-slider.js --html <path> [--verbose]
14
+ * node verify-slider.js --url <url> [--verbose]
15
+ *
16
+ * Options:
17
+ * --html Path to local HTML file
18
+ * --url URL to test
19
+ * --output Output directory for screenshots
20
+ * --verbose Show detailed progress
21
+ */
22
+
23
+ import fs from 'fs/promises';
24
+ import path from 'path';
25
+
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
+ // Slider library patterns
36
+ const SLIDER_PATTERNS = {
37
+ swiper: {
38
+ container: '[class*="swiper"]',
39
+ slide: '.swiper-slide',
40
+ active: '.swiper-slide-active',
41
+ prev: '.swiper-button-prev',
42
+ next: '.swiper-button-next',
43
+ pagination: '.swiper-pagination'
44
+ },
45
+ slick: {
46
+ container: '[class*="slick"]',
47
+ slide: '.slick-slide',
48
+ active: '.slick-active, .slick-current',
49
+ prev: '.slick-prev',
50
+ next: '.slick-next',
51
+ pagination: '.slick-dots'
52
+ },
53
+ owl: {
54
+ container: '[class*="owl"]',
55
+ slide: '.owl-item',
56
+ active: '.owl-item.active',
57
+ prev: '.owl-prev',
58
+ next: '.owl-next',
59
+ pagination: '.owl-dots'
60
+ },
61
+ splide: {
62
+ container: '.splide',
63
+ slide: '.splide__slide',
64
+ active: '.splide__slide.is-active',
65
+ prev: '.splide__arrow--prev',
66
+ next: '.splide__arrow--next',
67
+ pagination: '.splide__pagination'
68
+ },
69
+ glide: {
70
+ container: '.glide',
71
+ slide: '.glide__slide',
72
+ active: '.glide__slide--active',
73
+ prev: '[data-glide-dir="<"]',
74
+ next: '[data-glide-dir=">"]',
75
+ pagination: '.glide__bullets'
76
+ },
77
+ native: {
78
+ container: '[style*="scroll-snap"], [class*="carousel"], [class*="slider"]',
79
+ slide: '[style*="scroll-snap"] > *, .carousel-item, .slider-item',
80
+ active: '.active, [aria-current="true"]',
81
+ prev: '[class*="prev"], [aria-label*="prev" i]',
82
+ next: '[class*="next"], [aria-label*="next" i]',
83
+ pagination: '[class*="indicator"], [class*="dot"], [role="tablist"]'
84
+ }
85
+ };
86
+
87
+ // Autoplay detection config
88
+ const AUTOPLAY_CONFIG = {
89
+ waitTime: 6000, // Total wait time in ms
90
+ checkInterval: 1000, // Check every 1s
91
+ requiredChanges: 2 // Require 2 slide changes (per validation)
92
+ };
93
+
94
+ /**
95
+ * Detect which slider library is used
96
+ */
97
+ async function detectSliderLibrary(page) {
98
+ for (const [name, patterns] of Object.entries(SLIDER_PATTERNS)) {
99
+ try {
100
+ const count = await page.locator(patterns.container).count();
101
+ if (count > 0) {
102
+ return { library: name, patterns };
103
+ }
104
+ } catch (err) { /* continue - selector not found */ }
105
+ }
106
+ return null;
107
+ }
108
+
109
+ /**
110
+ * Check element visibility
111
+ */
112
+ async function isElementVisible(page, selector) {
113
+ try {
114
+ const element = await page.$(selector);
115
+ if (!element) return false;
116
+
117
+ return await page.evaluate((sel) => {
118
+ const el = document.querySelector(sel);
119
+ if (!el) return false;
120
+
121
+ const style = window.getComputedStyle(el);
122
+ const rect = el.getBoundingClientRect();
123
+ return (
124
+ style.display !== 'none' &&
125
+ style.visibility !== 'hidden' &&
126
+ style.opacity !== '0' &&
127
+ rect.width > 0 &&
128
+ rect.height > 0
129
+ );
130
+ }, selector);
131
+ } catch {
132
+ return false;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Count visible elements
138
+ */
139
+ async function countVisibleElements(page, selector) {
140
+ try {
141
+ return await page.evaluate((sel) => {
142
+ const items = document.querySelectorAll(sel);
143
+ let visible = 0;
144
+ items.forEach(item => {
145
+ const style = window.getComputedStyle(item);
146
+ const rect = item.getBoundingClientRect();
147
+ if (
148
+ style.display !== 'none' &&
149
+ style.visibility !== 'hidden' &&
150
+ style.opacity !== '0' &&
151
+ rect.width > 0 &&
152
+ rect.height > 0
153
+ ) {
154
+ visible++;
155
+ }
156
+ });
157
+ return visible;
158
+ }, selector);
159
+ } catch {
160
+ return 0;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Get current active slide index
166
+ */
167
+ async function getActiveSlideIndex(page, patterns) {
168
+ try {
169
+ return await page.evaluate((selectors) => {
170
+ // Try active selector
171
+ const active = document.querySelector(selectors.active);
172
+ if (active) {
173
+ const slides = document.querySelectorAll(selectors.slide);
174
+ for (let i = 0; i < slides.length; i++) {
175
+ if (slides[i] === active || slides[i].contains(active)) {
176
+ return i;
177
+ }
178
+ }
179
+ }
180
+
181
+ // Fallback: check transform or scroll position
182
+ const container = document.querySelector(selectors.container);
183
+ if (container) {
184
+ const slides = container.querySelectorAll(selectors.slide);
185
+ for (let i = 0; i < slides.length; i++) {
186
+ const rect = slides[i].getBoundingClientRect();
187
+ const containerRect = container.getBoundingClientRect();
188
+ // Check if slide is in view
189
+ if (rect.left >= containerRect.left - 10 && rect.left < containerRect.right) {
190
+ return i;
191
+ }
192
+ }
193
+ }
194
+
195
+ return -1;
196
+ }, patterns);
197
+ } catch {
198
+ return -1;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Check autoplay by monitoring slide changes
204
+ * Requires 2 changes in 6 seconds (per validated decision)
205
+ * Early exit when required changes detected (performance optimization)
206
+ */
207
+ async function checkAutoplay(page, patterns, verbose) {
208
+ const slideIndices = [];
209
+ const startTime = Date.now();
210
+
211
+ // Get initial slide
212
+ const initialIndex = await getActiveSlideIndex(page, patterns);
213
+ slideIndices.push({ time: 0, index: initialIndex });
214
+
215
+ if (verbose) console.error(` Starting autoplay detection (max ${AUTOPLAY_CONFIG.waitTime / 1000}s)...`);
216
+
217
+ // Monitor for changes with early exit
218
+ while (Date.now() - startTime < AUTOPLAY_CONFIG.waitTime) {
219
+ await new Promise(r => setTimeout(r, AUTOPLAY_CONFIG.checkInterval));
220
+
221
+ const currentIndex = await getActiveSlideIndex(page, patterns);
222
+ const elapsed = Date.now() - startTime;
223
+
224
+ if (currentIndex !== slideIndices[slideIndices.length - 1].index) {
225
+ slideIndices.push({ time: elapsed, index: currentIndex });
226
+ if (verbose) console.error(` Slide changed to ${currentIndex} at ${elapsed}ms`);
227
+
228
+ // Early exit: if we have required changes, no need to wait longer
229
+ if (slideIndices.length - 1 >= AUTOPLAY_CONFIG.requiredChanges) {
230
+ if (verbose) console.error(` Early exit: ${AUTOPLAY_CONFIG.requiredChanges} changes detected`);
231
+ break;
232
+ }
233
+ }
234
+ }
235
+
236
+ const changes = slideIndices.length - 1;
237
+ const actualDuration = Date.now() - startTime;
238
+
239
+ return {
240
+ hasAutoplay: changes >= AUTOPLAY_CONFIG.requiredChanges,
241
+ changes,
242
+ slideIndices,
243
+ duration: actualDuration
244
+ };
245
+ }
246
+
247
+ /**
248
+ * Test slider at specific viewport
249
+ */
250
+ async function testViewport(page, viewportName, verbose = false) {
251
+ const viewport = VIEWPORTS[viewportName];
252
+ await page.setViewportSize(viewport);
253
+ await new Promise(r => setTimeout(r, 500));
254
+
255
+ const result = {
256
+ viewport: viewportName,
257
+ dimensions: viewport,
258
+ tests: [],
259
+ passed: 0,
260
+ failed: 0,
261
+ warnings: [],
262
+ sliderInfo: null
263
+ };
264
+
265
+ if (verbose) console.error(`\nšŸ“± Testing ${viewportName} (${viewport.width}x${viewport.height})...`);
266
+
267
+ // Test 1: Detect slider library
268
+ const sliderDetection = await detectSliderLibrary(page);
269
+
270
+ if (!sliderDetection) {
271
+ result.tests.push({
272
+ name: 'Slider detection',
273
+ passed: true,
274
+ note: 'No slider/carousel detected on page'
275
+ });
276
+ result.passed++;
277
+ if (verbose) console.error(` ℹ No slider detected`);
278
+ return result;
279
+ }
280
+
281
+ const { library, patterns } = sliderDetection;
282
+ result.sliderInfo = { library };
283
+
284
+ result.tests.push({
285
+ name: 'Slider detection',
286
+ passed: true,
287
+ library,
288
+ selector: patterns.container
289
+ });
290
+ result.passed++;
291
+ if (verbose) console.error(` āœ“ Slider detected: ${library}`);
292
+
293
+ // Test 2: Slides present
294
+ const slideCount = await countVisibleElements(page, patterns.slide);
295
+ if (slideCount > 0) {
296
+ result.tests.push({
297
+ name: 'Slides present',
298
+ passed: true,
299
+ count: slideCount
300
+ });
301
+ result.passed++;
302
+ result.sliderInfo.slideCount = slideCount;
303
+ if (verbose) console.error(` āœ“ ${slideCount} slides found`);
304
+ } else {
305
+ result.tests.push({
306
+ name: 'Slides present',
307
+ passed: false,
308
+ error: 'No slides found in slider'
309
+ });
310
+ result.failed++;
311
+ if (verbose) console.error(` āœ— No slides found`);
312
+ }
313
+
314
+ // Test 3: Navigation arrows
315
+ const hasPrev = await isElementVisible(page, patterns.prev);
316
+ const hasNext = await isElementVisible(page, patterns.next);
317
+
318
+ if (hasPrev || hasNext) {
319
+ result.tests.push({
320
+ name: 'Navigation arrows',
321
+ passed: true,
322
+ hasPrev,
323
+ hasNext
324
+ });
325
+ result.passed++;
326
+ if (verbose) console.error(` āœ“ Navigation arrows: prev=${hasPrev}, next=${hasNext}`);
327
+ } else {
328
+ result.warnings.push('No navigation arrows visible');
329
+ if (verbose) console.error(` ⚠ No navigation arrows found`);
330
+ }
331
+
332
+ // Test 4: Pagination dots
333
+ const hasPagination = await isElementVisible(page, patterns.pagination);
334
+ if (hasPagination) {
335
+ result.tests.push({
336
+ name: 'Pagination dots',
337
+ passed: true,
338
+ selector: patterns.pagination
339
+ });
340
+ result.passed++;
341
+ if (verbose) console.error(` āœ“ Pagination dots found`);
342
+ } else {
343
+ result.warnings.push('No pagination dots visible');
344
+ if (verbose) console.error(` ⚠ No pagination dots found`);
345
+ }
346
+
347
+ // Test 5: Active slide indicator
348
+ const activeIndex = await getActiveSlideIndex(page, patterns);
349
+ if (activeIndex >= 0) {
350
+ result.tests.push({
351
+ name: 'Active slide indicator',
352
+ passed: true,
353
+ activeIndex
354
+ });
355
+ result.passed++;
356
+ result.sliderInfo.currentSlide = activeIndex;
357
+ if (verbose) console.error(` āœ“ Active slide: ${activeIndex}`);
358
+ } else {
359
+ result.warnings.push('Could not determine active slide');
360
+ if (verbose) console.error(` ⚠ Could not detect active slide`);
361
+ }
362
+
363
+ // Test 6: Autoplay detection (only on desktop to save time)
364
+ if (viewportName === 'desktop' && slideCount > 1) {
365
+ if (verbose) console.error(` Testing autoplay...`);
366
+ const autoplayResult = await checkAutoplay(page, patterns, verbose);
367
+
368
+ if (autoplayResult.hasAutoplay) {
369
+ result.tests.push({
370
+ name: 'Autoplay functionality',
371
+ passed: true,
372
+ changes: autoplayResult.changes,
373
+ duration: autoplayResult.duration
374
+ });
375
+ result.passed++;
376
+ result.sliderInfo.hasAutoplay = true;
377
+ if (verbose) console.error(` āœ“ Autoplay detected (${autoplayResult.changes} changes)`);
378
+ } else {
379
+ result.tests.push({
380
+ name: 'Autoplay functionality',
381
+ passed: true,
382
+ note: `No autoplay detected (${autoplayResult.changes} changes in ${autoplayResult.duration}ms)`,
383
+ changes: autoplayResult.changes
384
+ });
385
+ result.passed++;
386
+ result.sliderInfo.hasAutoplay = false;
387
+ if (verbose) console.error(` ℹ No autoplay (${autoplayResult.changes} changes)`);
388
+ }
389
+ }
390
+
391
+ return result;
392
+ }
393
+
394
+ /**
395
+ * Capture slider screenshot
396
+ */
397
+ async function captureSliderScreenshot(page, outputDir, viewportName) {
398
+ if (!outputDir) return null;
399
+
400
+ // Try to scroll slider into view
401
+ await page.evaluate(() => {
402
+ const slider = document.querySelector('[class*="swiper"], [class*="slick"], [class*="owl"], [class*="carousel"], [class*="slider"]');
403
+ if (slider) slider.scrollIntoView({ behavior: 'instant', block: 'center' });
404
+ });
405
+ await new Promise(r => setTimeout(r, 200));
406
+
407
+ const screenshotPath = path.join(outputDir, `slider-test-${viewportName}.png`);
408
+ await page.screenshot({
409
+ path: screenshotPath,
410
+ fullPage: false
411
+ });
412
+ return screenshotPath;
413
+ }
414
+
415
+ /**
416
+ * Validate HTML file path (security: prevent path traversal)
417
+ */
418
+ function validateHtmlPath(htmlPath) {
419
+ const absolutePath = path.resolve(htmlPath);
420
+ const cwd = process.cwd();
421
+
422
+ const allowedPrefixes = [
423
+ cwd,
424
+ path.join(process.env.HOME || '', '.claude'),
425
+ '/tmp',
426
+ path.join(process.env.HOME || '', 'cloned-designs')
427
+ ];
428
+
429
+ const isAllowed = allowedPrefixes.some(prefix => absolutePath.startsWith(prefix));
430
+ if (!isAllowed) {
431
+ throw new Error(`Path "${htmlPath}" is outside allowed directories`);
432
+ }
433
+
434
+ return absolutePath;
435
+ }
436
+
437
+ /**
438
+ * Main verification function
439
+ */
440
+ async function verifySlider() {
441
+ const args = parseArgs(process.argv.slice(2));
442
+
443
+ if (!args.html && !args.url) {
444
+ outputError(new Error('Either --html or --url is required'));
445
+ process.exit(1);
446
+ }
447
+
448
+ const verbose = args.verbose === 'true';
449
+ const outputDir = args.output;
450
+
451
+ try {
452
+ const browser = await getBrowser({ headless: args.headless !== 'false' });
453
+ const page = await getPage(browser);
454
+
455
+ let targetUrl;
456
+ if (args.html) {
457
+ const absolutePath = validateHtmlPath(args.html);
458
+ targetUrl = `file://${absolutePath}`;
459
+ } else {
460
+ targetUrl = args.url;
461
+ }
462
+
463
+ if (verbose) console.error(`\nšŸ” Verifying slider: ${targetUrl}\n`);
464
+
465
+ await page.goto(targetUrl, {
466
+ waitUntil: 'networkidle',
467
+ timeout: 30000
468
+ });
469
+
470
+ const results = {
471
+ success: true,
472
+ component: 'slider',
473
+ url: targetUrl,
474
+ viewports: {},
475
+ summary: {
476
+ totalTests: 0,
477
+ passed: 0,
478
+ failed: 0,
479
+ warnings: []
480
+ },
481
+ screenshots: [],
482
+ sliderDetected: false,
483
+ sliderLibrary: null
484
+ };
485
+
486
+ for (const viewportName of ['mobile', 'tablet', 'desktop']) {
487
+ const viewportResult = await testViewport(page, viewportName, verbose);
488
+ results.viewports[viewportName] = viewportResult;
489
+
490
+ results.summary.totalTests += viewportResult.tests.length;
491
+ results.summary.passed += viewportResult.passed;
492
+ results.summary.failed += viewportResult.failed;
493
+ results.summary.warnings.push(...viewportResult.warnings);
494
+
495
+ if (viewportResult.sliderInfo) {
496
+ results.sliderDetected = true;
497
+ results.sliderLibrary = viewportResult.sliderInfo.library;
498
+ }
499
+
500
+ if (outputDir) {
501
+ const screenshotPath = await captureSliderScreenshot(page, outputDir, viewportName);
502
+ if (screenshotPath) results.screenshots.push(screenshotPath);
503
+ }
504
+ }
505
+
506
+ results.success = results.summary.failed === 0;
507
+
508
+ if (args.close === 'true') {
509
+ await closeBrowser();
510
+ } else {
511
+ await disconnectBrowser();
512
+ }
513
+
514
+ if (verbose) {
515
+ console.error('\nšŸ“Š Summary:');
516
+ console.error(` Slider: ${results.sliderDetected ? results.sliderLibrary : 'Not detected'}`);
517
+ console.error(` Tests: ${results.summary.passed}/${results.summary.totalTests} passed`);
518
+ if (results.summary.warnings.length > 0) {
519
+ console.error(` Warnings: ${results.summary.warnings.length}`);
520
+ }
521
+ console.error(` Status: ${results.success ? 'āœ“ PASS' : 'āœ— FAIL'}\n`);
522
+ }
523
+
524
+ outputJSON(results);
525
+ process.exit(results.success ? 0 : 1);
526
+
527
+ } catch (error) {
528
+ outputError(error);
529
+ process.exit(1);
530
+ }
531
+ }
532
+
533
+ verifySlider();