design-clone 2.1.0 → 2.3.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 (177) hide show
  1. package/README.md +13 -34
  2. package/SKILL.md +69 -45
  3. package/bin/cli.js +22 -4
  4. package/bin/commands/clone-site.js +31 -171
  5. package/bin/commands/help.js +19 -6
  6. package/bin/commands/init.js +9 -86
  7. package/bin/commands/uninstall.js +105 -0
  8. package/bin/commands/update.js +70 -0
  9. package/bin/commands/verify.js +7 -14
  10. package/bin/utils/paths.js +28 -0
  11. package/bin/utils/validate.js +2 -22
  12. package/bin/utils/version.js +23 -0
  13. package/docs/code-standards.md +789 -0
  14. package/docs/codebase-summary.md +533 -286
  15. package/docs/index.md +74 -0
  16. package/docs/project-overview-pdr.md +797 -0
  17. package/docs/system-architecture.md +718 -0
  18. package/package.json +14 -17
  19. package/src/ai/prompts/design-tokens/basic.md +80 -0
  20. package/src/ai/prompts/design-tokens/section-with-css.md +41 -0
  21. package/src/ai/prompts/design-tokens/section.md +48 -0
  22. package/src/ai/prompts/design-tokens/with-css.md +87 -0
  23. package/src/ai/prompts/structure-analysis/basic.md +55 -0
  24. package/src/ai/prompts/structure-analysis/with-context.md +59 -0
  25. package/src/ai/prompts/structure-analysis/with-dimensions.md +63 -0
  26. package/src/ai/prompts/structure-analysis/with-hierarchy.md +73 -0
  27. package/src/ai/prompts/ux-audit/aggregation.md +42 -0
  28. package/src/ai/prompts/ux-audit/desktop.md +92 -0
  29. package/src/ai/prompts/ux-audit/mobile.md +93 -0
  30. package/src/ai/prompts/ux-audit/tablet.md +92 -0
  31. package/src/core/animation/animation-extractor-ast.js +183 -0
  32. package/src/core/animation/animation-extractor-output.js +152 -0
  33. package/src/core/animation/animation-extractor.js +178 -0
  34. package/src/core/animation/state-capture-detection.js +200 -0
  35. package/src/core/animation/state-capture.js +193 -0
  36. package/src/core/capture/browser-context-pool.js +96 -0
  37. package/src/core/capture/multi-page-screenshot-page.js +110 -0
  38. package/src/core/capture/multi-page-screenshot.js +208 -0
  39. package/src/core/capture/screenshot-extraction.js +186 -0
  40. package/src/core/capture/screenshot-helpers.js +175 -0
  41. package/src/core/capture/screenshot-orchestrator.js +174 -0
  42. package/src/core/capture/screenshot-viewport.js +93 -0
  43. package/src/core/capture/screenshot.js +192 -0
  44. package/src/core/content/content-counter-dom.js +191 -0
  45. package/src/core/content/content-counter.js +76 -0
  46. package/src/core/css/breakpoint-detector.js +66 -0
  47. package/src/core/css/chromium-defaults.json +23 -0
  48. package/src/core/css/computed-style-extractor.js +102 -0
  49. package/src/core/css/css-chunker.js +103 -0
  50. package/src/core/css/filter-css-dead-code.js +120 -0
  51. package/src/core/css/filter-css-html-analyzer.js +110 -0
  52. package/src/core/css/filter-css-selector-matcher.js +172 -0
  53. package/src/core/css/filter-css.js +206 -0
  54. package/src/core/css/merge-css-atrule-processor.js +158 -0
  55. package/src/core/css/merge-css-file-io.js +68 -0
  56. package/src/core/css/merge-css.js +148 -0
  57. package/src/core/detection/framework-detector-routing.js +68 -0
  58. package/src/core/detection/framework-detector-signals.js +65 -0
  59. package/src/core/detection/framework-detector.js +198 -0
  60. package/src/core/dimension/dimension-extractor-card-detector.js +82 -0
  61. package/src/core/dimension/dimension-extractor.js +317 -0
  62. package/src/core/dimension/dimension-output-ai-summary.js +111 -0
  63. package/src/core/dimension/dimension-output.js +173 -0
  64. package/src/core/dimension/dom-tree-analyzer-tree-builders.js +95 -0
  65. package/src/core/dimension/dom-tree-analyzer.js +191 -0
  66. package/src/core/discovery/app-state-snapshot-capture.js +195 -0
  67. package/src/core/discovery/app-state-snapshot-utils.js +178 -0
  68. package/src/core/discovery/app-state-snapshot.js +131 -0
  69. package/src/core/discovery/discover-pages-routes.js +84 -0
  70. package/src/core/discovery/discover-pages-utils.js +177 -0
  71. package/src/core/discovery/discover-pages.js +191 -0
  72. package/src/core/html/html-extractor-inline-styler.js +70 -0
  73. package/src/core/html/html-extractor.js +147 -0
  74. package/src/core/html/semantic-enhancer-mappings.js +200 -0
  75. package/src/core/html/semantic-enhancer-page.js +148 -0
  76. package/src/core/html/semantic-enhancer.js +135 -0
  77. package/src/core/links/rewrite-links-css-rewriter.js +53 -0
  78. package/src/core/links/rewrite-links.js +173 -0
  79. package/src/core/media/asset-validator.js +118 -0
  80. package/src/core/media/extract-assets-downloader.js +187 -0
  81. package/src/core/media/extract-assets-page-scraper.js +115 -0
  82. package/src/core/media/extract-assets.js +159 -0
  83. package/src/core/media/video-capture-convert.js +200 -0
  84. package/src/core/media/video-capture.js +201 -0
  85. package/src/core/{lazy-loader.js → page-prep/lazy-loader.js} +37 -39
  86. package/src/core/section/section-cropper-helpers.js +43 -0
  87. package/src/core/{section-cropper.js → section/section-cropper.js} +11 -88
  88. package/src/core/section/section-detector-strategies.js +139 -0
  89. package/src/core/section/section-detector-utils.js +100 -0
  90. package/src/core/section/section-detector.js +88 -0
  91. package/src/core/tests/test-section-cropper.js +2 -2
  92. package/src/core/tests/test-section-detector.js +2 -2
  93. package/src/post-process/enhance-assets.js +29 -4
  94. package/src/post-process/fetch-images-unsplash-client.js +123 -0
  95. package/src/post-process/fetch-images.js +60 -263
  96. package/src/post-process/inject-gosnap.js +88 -0
  97. package/src/post-process/inject-icons-svg-replacer.js +76 -0
  98. package/src/post-process/inject-icons.js +47 -200
  99. package/src/route-discoverers/base-discoverer-utils.js +137 -0
  100. package/src/route-discoverers/base-discoverer.js +29 -118
  101. package/src/route-discoverers/index.js +1 -1
  102. package/src/shared/config.js +38 -0
  103. package/src/shared/error-codes.js +31 -0
  104. package/src/shared/viewports.js +46 -0
  105. package/src/utils/browser.js +0 -7
  106. package/src/utils/helpers.js +4 -0
  107. package/src/utils/log.js +12 -0
  108. package/src/utils/playwright-loader.js +76 -0
  109. package/src/utils/playwright.js +3 -69
  110. package/src/utils/progress.js +32 -0
  111. package/src/verification/generate-audit-report-css-fixes.js +52 -0
  112. package/src/verification/generate-audit-report-sections.js +158 -0
  113. package/src/verification/generate-audit-report.js +5 -281
  114. package/src/verification/quality-scorer.js +92 -0
  115. package/src/verification/verify-footer-checks.js +103 -0
  116. package/src/verification/verify-footer-helpers.js +178 -0
  117. package/src/verification/verify-footer.js +23 -381
  118. package/src/verification/verify-header-checks.js +104 -0
  119. package/src/verification/verify-header-helpers.js +156 -0
  120. package/src/verification/verify-header.js +23 -365
  121. package/src/verification/verify-layout-report.js +101 -0
  122. package/src/verification/verify-layout.js +13 -259
  123. package/src/verification/verify-menu-checks.js +104 -0
  124. package/src/verification/verify-menu-helpers.js +112 -0
  125. package/src/verification/verify-menu.js +17 -285
  126. package/src/verification/verify-slider-checks.js +115 -0
  127. package/src/verification/verify-slider-constants.js +65 -0
  128. package/src/verification/verify-slider-helpers.js +164 -0
  129. package/src/verification/verify-slider.js +23 -414
  130. package/.env.example +0 -14
  131. package/docs/basic-clone.md +0 -63
  132. package/docs/cli-reference.md +0 -316
  133. package/docs/design-clone-architecture.md +0 -492
  134. package/docs/pixel-perfect.md +0 -117
  135. package/docs/project-roadmap.md +0 -382
  136. package/docs/troubleshooting.md +0 -170
  137. package/requirements.txt +0 -5
  138. package/src/ai/__pycache__/analyze-structure.cpython-313.pyc +0 -0
  139. package/src/ai/__pycache__/extract-design-tokens.cpython-313.pyc +0 -0
  140. package/src/ai/analyze-structure.py +0 -375
  141. package/src/ai/extract-design-tokens.py +0 -782
  142. package/src/ai/prompts/__init__.py +0 -2
  143. package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  144. package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
  145. package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
  146. package/src/ai/prompts/__pycache__/ux_audit.cpython-313.pyc +0 -0
  147. package/src/ai/prompts/design_tokens.py +0 -316
  148. package/src/ai/prompts/structure_analysis.py +0 -592
  149. package/src/ai/prompts/ux_audit.py +0 -198
  150. package/src/ai/ux-audit.js +0 -596
  151. package/src/core/animation-extractor.js +0 -526
  152. package/src/core/app-state-snapshot.js +0 -511
  153. package/src/core/content-counter.js +0 -342
  154. package/src/core/design-tokens.js +0 -103
  155. package/src/core/dimension-extractor.js +0 -438
  156. package/src/core/dimension-output.js +0 -305
  157. package/src/core/discover-pages.js +0 -542
  158. package/src/core/dom-tree-analyzer.js +0 -298
  159. package/src/core/extract-assets.js +0 -468
  160. package/src/core/filter-css.js +0 -499
  161. package/src/core/framework-detector.js +0 -538
  162. package/src/core/html-extractor.js +0 -212
  163. package/src/core/merge-css.js +0 -407
  164. package/src/core/multi-page-screenshot.js +0 -380
  165. package/src/core/rewrite-links.js +0 -226
  166. package/src/core/screenshot.js +0 -701
  167. package/src/core/section-detector.js +0 -386
  168. package/src/core/semantic-enhancer.js +0 -492
  169. package/src/core/state-capture.js +0 -598
  170. package/src/core/video-capture.js +0 -546
  171. package/src/utils/__init__.py +0 -16
  172. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  173. package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
  174. package/src/utils/env.py +0 -134
  175. /package/src/core/{css-extractor.js → css/css-extractor.js} +0 -0
  176. /package/src/core/{cookie-handler.js → page-prep/cookie-handler.js} +0 -0
  177. /package/src/core/{page-readiness.js → page-prep/page-readiness.js} +0 -0
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Slider Verification Helpers
3
+ *
4
+ * DOM inspection utilities for verify-slider.js:
5
+ * element visibility checks, slide index detection, and autoplay monitoring.
6
+ * Constants live in verify-slider-constants.js.
7
+ */
8
+
9
+ export { SLIDER_PATTERNS, AUTOPLAY_CONFIG } from './verify-slider-constants.js';
10
+ import { SLIDER_PATTERNS, AUTOPLAY_CONFIG } from './verify-slider-constants.js';
11
+
12
+ /**
13
+ * Detect which slider library is used on the page
14
+ * @param {import('playwright').Page} page
15
+ * @returns {Promise<{library: string, patterns: Object}|null>}
16
+ */
17
+ export async function detectSliderLibrary(page) {
18
+ for (const [name, patterns] of Object.entries(SLIDER_PATTERNS)) {
19
+ try {
20
+ const count = await page.locator(patterns.container).count();
21
+ if (count > 0) {
22
+ return { library: name, patterns };
23
+ }
24
+ } catch (err) { /* continue - selector not found */ }
25
+ }
26
+ return null;
27
+ }
28
+
29
+ /**
30
+ * Check element visibility via computed style and bounding rect
31
+ * @param {import('playwright').Page} page
32
+ * @param {string} selector
33
+ * @returns {Promise<boolean>}
34
+ */
35
+ export async function isElementVisible(page, selector) {
36
+ try {
37
+ const element = await page.$(selector);
38
+ if (!element) return false;
39
+
40
+ return await page.evaluate((sel) => {
41
+ const el = document.querySelector(sel);
42
+ if (!el) return false;
43
+
44
+ const style = window.getComputedStyle(el);
45
+ const rect = el.getBoundingClientRect();
46
+ return (
47
+ style.display !== 'none' &&
48
+ style.visibility !== 'hidden' &&
49
+ style.opacity !== '0' &&
50
+ rect.width > 0 &&
51
+ rect.height > 0
52
+ );
53
+ }, selector);
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Count visible elements matching a selector
61
+ * @param {import('playwright').Page} page
62
+ * @param {string} selector
63
+ * @returns {Promise<number>}
64
+ */
65
+ export async function countVisibleElements(page, selector) {
66
+ try {
67
+ return await page.evaluate((sel) => {
68
+ const items = document.querySelectorAll(sel);
69
+ let visible = 0;
70
+ items.forEach(item => {
71
+ const style = window.getComputedStyle(item);
72
+ const rect = item.getBoundingClientRect();
73
+ if (
74
+ style.display !== 'none' &&
75
+ style.visibility !== 'hidden' &&
76
+ style.opacity !== '0' &&
77
+ rect.width > 0 &&
78
+ rect.height > 0
79
+ ) visible++;
80
+ });
81
+ return visible;
82
+ }, selector);
83
+ } catch {
84
+ return 0;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Get the index of the currently active slide
90
+ * @param {import('playwright').Page} page
91
+ * @param {Object} patterns - Slider patterns object
92
+ * @returns {Promise<number>} Active slide index or -1
93
+ */
94
+ export async function getActiveSlideIndex(page, patterns) {
95
+ try {
96
+ return await page.evaluate((selectors) => {
97
+ const active = document.querySelector(selectors.active);
98
+ if (active) {
99
+ const slides = document.querySelectorAll(selectors.slide);
100
+ for (let i = 0; i < slides.length; i++) {
101
+ if (slides[i] === active || slides[i].contains(active)) return i;
102
+ }
103
+ }
104
+
105
+ const container = document.querySelector(selectors.container);
106
+ if (container) {
107
+ const slides = container.querySelectorAll(selectors.slide);
108
+ for (let i = 0; i < slides.length; i++) {
109
+ const rect = slides[i].getBoundingClientRect();
110
+ const containerRect = container.getBoundingClientRect();
111
+ if (rect.left >= containerRect.left - 10 && rect.left < containerRect.right) return i;
112
+ }
113
+ }
114
+
115
+ return -1;
116
+ }, patterns);
117
+ } catch {
118
+ return -1;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Check autoplay by monitoring slide changes over AUTOPLAY_CONFIG.waitTime.
124
+ * Exits early when requiredChanges are detected.
125
+ * @param {import('playwright').Page} page
126
+ * @param {Object} patterns - Slider patterns
127
+ * @param {boolean} verbose
128
+ * @returns {Promise<{hasAutoplay: boolean, changes: number, slideIndices: Array, duration: number}>}
129
+ */
130
+ export async function checkAutoplay(page, patterns, verbose) {
131
+ const slideIndices = [];
132
+ const startTime = Date.now();
133
+
134
+ const initialIndex = await getActiveSlideIndex(page, patterns);
135
+ slideIndices.push({ time: 0, index: initialIndex });
136
+
137
+ if (verbose) console.error(` Starting autoplay detection (max ${AUTOPLAY_CONFIG.waitTime / 1000}s)...`);
138
+
139
+ while (Date.now() - startTime < AUTOPLAY_CONFIG.waitTime) {
140
+ await new Promise(r => setTimeout(r, AUTOPLAY_CONFIG.checkInterval));
141
+
142
+ const currentIndex = await getActiveSlideIndex(page, patterns);
143
+ const elapsed = Date.now() - startTime;
144
+
145
+ if (currentIndex !== slideIndices[slideIndices.length - 1].index) {
146
+ slideIndices.push({ time: elapsed, index: currentIndex });
147
+ if (verbose) console.error(` Slide changed to ${currentIndex} at ${elapsed}ms`);
148
+
149
+ if (slideIndices.length - 1 >= AUTOPLAY_CONFIG.requiredChanges) {
150
+ if (verbose) console.error(` Early exit: ${AUTOPLAY_CONFIG.requiredChanges} changes detected`);
151
+ break;
152
+ }
153
+ }
154
+ }
155
+
156
+ const changes = slideIndices.length - 1;
157
+ return {
158
+ hasAutoplay: changes >= AUTOPLAY_CONFIG.requiredChanges,
159
+ changes,
160
+ slideIndices,
161
+ duration: Date.now() - startTime
162
+ };
163
+ }
164
+
@@ -20,375 +20,27 @@
20
20
  * --verbose Show detailed progress
21
21
  */
22
22
 
23
- import fs from 'fs/promises';
24
23
  import path from 'path';
25
24
 
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
- }
25
+ import { getBrowser, getPage, closeBrowser, disconnectBrowser } from '../utils/browser.js';
26
+ import { parseArgs, outputJSON, outputError } from '../utils/helpers.js';
27
+ import { VIEWPORTS_HD as VIEWPORTS } from '../shared/viewports.js';
28
+ import { testSliderViewport } from './verify-slider-checks.js';
108
29
 
109
30
  /**
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
31
+ * Validate HTML file path (security: prevent path traversal)
249
32
  */
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;
33
+ function validateHtmlPath(htmlPath) {
34
+ const absolutePath = path.resolve(htmlPath);
35
+ const allowedPrefixes = [
36
+ process.cwd(),
37
+ path.join(process.env.HOME || '', '.claude'),
38
+ '/tmp',
39
+ path.join(process.env.HOME || '', 'cloned-designs')
40
+ ];
41
+ const isAllowed = allowedPrefixes.some(prefix => absolutePath.startsWith(prefix));
42
+ if (!isAllowed) throw new Error(`Path "${htmlPath}" is outside allowed directories`);
43
+ return absolutePath;
392
44
  }
393
45
 
394
46
  /**
@@ -396,44 +48,16 @@ async function testViewport(page, viewportName, verbose = false) {
396
48
  */
397
49
  async function captureSliderScreenshot(page, outputDir, viewportName) {
398
50
  if (!outputDir) return null;
399
-
400
- // Try to scroll slider into view
401
51
  await page.evaluate(() => {
402
52
  const slider = document.querySelector('[class*="swiper"], [class*="slick"], [class*="owl"], [class*="carousel"], [class*="slider"]');
403
53
  if (slider) slider.scrollIntoView({ behavior: 'instant', block: 'center' });
404
54
  });
405
55
  await new Promise(r => setTimeout(r, 200));
406
-
407
56
  const screenshotPath = path.join(outputDir, `slider-test-${viewportName}.png`);
408
- await page.screenshot({
409
- path: screenshotPath,
410
- fullPage: false
411
- });
57
+ await page.screenshot({ path: screenshotPath, fullPage: false });
412
58
  return screenshotPath;
413
59
  }
414
60
 
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
61
  /**
438
62
  * Main verification function
439
63
  */
@@ -454,37 +78,28 @@ async function verifySlider() {
454
78
 
455
79
  let targetUrl;
456
80
  if (args.html) {
457
- const absolutePath = validateHtmlPath(args.html);
458
- targetUrl = `file://${absolutePath}`;
81
+ targetUrl = `file://${validateHtmlPath(args.html)}`;
459
82
  } else {
460
83
  targetUrl = args.url;
461
84
  }
462
85
 
463
86
  if (verbose) console.error(`\n🔍 Verifying slider: ${targetUrl}\n`);
464
87
 
465
- await page.goto(targetUrl, {
466
- waitUntil: 'networkidle',
467
- timeout: 30000
468
- });
88
+ await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 30000 });
469
89
 
470
90
  const results = {
471
91
  success: true,
472
92
  component: 'slider',
473
93
  url: targetUrl,
474
94
  viewports: {},
475
- summary: {
476
- totalTests: 0,
477
- passed: 0,
478
- failed: 0,
479
- warnings: []
480
- },
95
+ summary: { totalTests: 0, passed: 0, failed: 0, warnings: [] },
481
96
  screenshots: [],
482
97
  sliderDetected: false,
483
98
  sliderLibrary: null
484
99
  };
485
100
 
486
101
  for (const viewportName of ['mobile', 'tablet', 'desktop']) {
487
- const viewportResult = await testViewport(page, viewportName, verbose);
102
+ const viewportResult = await testSliderViewport(page, viewportName, VIEWPORTS, verbose);
488
103
  results.viewports[viewportName] = viewportResult;
489
104
 
490
105
  results.summary.totalTests += viewportResult.tests.length;
@@ -505,19 +120,13 @@ async function verifySlider() {
505
120
 
506
121
  results.success = results.summary.failed === 0;
507
122
 
508
- if (args.close === 'true') {
509
- await closeBrowser();
510
- } else {
511
- await disconnectBrowser();
512
- }
123
+ if (args.close === 'true') { await closeBrowser(); } else { await disconnectBrowser(); }
513
124
 
514
125
  if (verbose) {
515
126
  console.error('\n📊 Summary:');
516
127
  console.error(` Slider: ${results.sliderDetected ? results.sliderLibrary : 'Not detected'}`);
517
128
  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
- }
129
+ if (results.summary.warnings.length > 0) console.error(` Warnings: ${results.summary.warnings.length}`);
521
130
  console.error(` Status: ${results.success ? '✓ PASS' : '✗ FAIL'}\n`);
522
131
  }
523
132
 
package/.env.example DELETED
@@ -1,14 +0,0 @@
1
- # Design Clone Skill - Environment Variables
2
- # Copy to .env and fill in your values
3
-
4
- # Gemini API Key (optional but recommended)
5
- # Get from: https://aistudio.google.com/apikey
6
- # Enables AI structure analysis and design token extraction
7
- GEMINI_API_KEY=your-api-key-here
8
-
9
- # Chrome Path (optional - auto-detected on most systems)
10
- # Set if Chrome is installed in non-standard location
11
- # CHROME_PATH=/path/to/chrome
12
-
13
- # Puppeteer Options (optional)
14
- # PUPPETEER_NO_SANDBOX=1 # Required for Docker/CI