design-clone 1.2.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 (174) hide show
  1. package/README.md +32 -39
  2. package/SKILL.md +69 -45
  3. package/bin/cli.js +22 -4
  4. package/bin/commands/clone-site.js +31 -106
  5. package/bin/commands/help.js +19 -6
  6. package/bin/commands/init.js +11 -56
  7. package/bin/commands/uninstall.js +105 -0
  8. package/bin/commands/update.js +70 -0
  9. package/bin/commands/verify.js +11 -16
  10. package/bin/utils/paths.js +28 -0
  11. package/bin/utils/validate.js +24 -28
  12. package/bin/utils/version.js +23 -0
  13. package/docs/code-standards.md +789 -0
  14. package/docs/codebase-summary.md +556 -0
  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 +20 -21
  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-extractor.js → css/css-extractor.js} +4 -4
  51. package/src/core/css/filter-css-dead-code.js +120 -0
  52. package/src/core/css/filter-css-html-analyzer.js +110 -0
  53. package/src/core/css/filter-css-selector-matcher.js +172 -0
  54. package/src/core/css/filter-css.js +206 -0
  55. package/src/core/css/merge-css-atrule-processor.js +158 -0
  56. package/src/core/css/merge-css-file-io.js +68 -0
  57. package/src/core/css/merge-css.js +148 -0
  58. package/src/core/detection/framework-detector-routing.js +68 -0
  59. package/src/core/detection/framework-detector-signals.js +65 -0
  60. package/src/core/detection/framework-detector.js +198 -0
  61. package/src/core/dimension/dimension-extractor-card-detector.js +82 -0
  62. package/src/core/dimension/dimension-extractor.js +317 -0
  63. package/src/core/dimension/dimension-output-ai-summary.js +111 -0
  64. package/src/core/dimension/dimension-output.js +173 -0
  65. package/src/core/dimension/dom-tree-analyzer-tree-builders.js +95 -0
  66. package/src/core/dimension/dom-tree-analyzer.js +191 -0
  67. package/src/core/discovery/app-state-snapshot-capture.js +195 -0
  68. package/src/core/discovery/app-state-snapshot-utils.js +178 -0
  69. package/src/core/discovery/app-state-snapshot.js +131 -0
  70. package/src/core/discovery/discover-pages-routes.js +84 -0
  71. package/src/core/discovery/discover-pages-utils.js +177 -0
  72. package/src/core/discovery/discover-pages.js +191 -0
  73. package/src/core/html/html-extractor-inline-styler.js +70 -0
  74. package/src/core/html/html-extractor.js +147 -0
  75. package/src/core/html/semantic-enhancer-mappings.js +200 -0
  76. package/src/core/html/semantic-enhancer-page.js +148 -0
  77. package/src/core/html/semantic-enhancer.js +135 -0
  78. package/src/core/links/rewrite-links-css-rewriter.js +53 -0
  79. package/src/core/links/rewrite-links.js +173 -0
  80. package/src/core/media/asset-validator.js +118 -0
  81. package/src/core/media/extract-assets-downloader.js +187 -0
  82. package/src/core/media/extract-assets-page-scraper.js +115 -0
  83. package/src/core/media/extract-assets.js +159 -0
  84. package/src/core/media/video-capture-convert.js +200 -0
  85. package/src/core/media/video-capture.js +201 -0
  86. package/src/core/{cookie-handler.js → page-prep/cookie-handler.js} +1 -1
  87. package/src/core/{lazy-loader.js → page-prep/lazy-loader.js} +44 -46
  88. package/src/core/{page-readiness.js → page-prep/page-readiness.js} +8 -8
  89. package/src/core/section/section-cropper-helpers.js +43 -0
  90. package/src/core/section/section-cropper.js +132 -0
  91. package/src/core/section/section-detector-strategies.js +139 -0
  92. package/src/core/section/section-detector-utils.js +100 -0
  93. package/src/core/section/section-detector.js +88 -0
  94. package/src/core/tests/test-section-cropper.js +177 -0
  95. package/src/core/tests/test-section-detector.js +55 -0
  96. package/src/post-process/enhance-assets.js +29 -4
  97. package/src/post-process/fetch-images-unsplash-client.js +123 -0
  98. package/src/post-process/fetch-images.js +60 -263
  99. package/src/post-process/inject-gosnap.js +88 -0
  100. package/src/post-process/inject-icons-svg-replacer.js +76 -0
  101. package/src/post-process/inject-icons.js +47 -200
  102. package/src/route-discoverers/angular-discoverer.js +157 -0
  103. package/src/route-discoverers/astro-discoverer.js +123 -0
  104. package/src/route-discoverers/base-discoverer-utils.js +137 -0
  105. package/src/route-discoverers/base-discoverer.js +153 -0
  106. package/src/route-discoverers/index.js +106 -0
  107. package/src/route-discoverers/next-discoverer.js +130 -0
  108. package/src/route-discoverers/nuxt-discoverer.js +138 -0
  109. package/src/route-discoverers/react-discoverer.js +139 -0
  110. package/src/route-discoverers/svelte-discoverer.js +109 -0
  111. package/src/route-discoverers/universal-discoverer.js +227 -0
  112. package/src/route-discoverers/vue-discoverer.js +118 -0
  113. package/src/shared/config.js +38 -0
  114. package/src/shared/error-codes.js +31 -0
  115. package/src/shared/viewports.js +46 -0
  116. package/src/utils/browser.js +11 -44
  117. package/src/utils/helpers.js +4 -0
  118. package/src/utils/log.js +12 -0
  119. package/src/utils/playwright-loader.js +76 -0
  120. package/src/utils/playwright.js +147 -0
  121. package/src/utils/progress.js +32 -0
  122. package/src/verification/generate-audit-report-css-fixes.js +52 -0
  123. package/src/verification/generate-audit-report-sections.js +158 -0
  124. package/src/verification/generate-audit-report.js +122 -0
  125. package/src/verification/quality-scorer.js +92 -0
  126. package/src/verification/verify-footer-checks.js +103 -0
  127. package/src/verification/verify-footer-helpers.js +178 -0
  128. package/src/verification/verify-footer.js +135 -0
  129. package/src/verification/verify-header-checks.js +104 -0
  130. package/src/verification/verify-header-helpers.js +156 -0
  131. package/src/verification/verify-header.js +144 -0
  132. package/src/verification/verify-layout-report.js +101 -0
  133. package/src/verification/verify-layout.js +14 -260
  134. package/src/verification/verify-menu-checks.js +104 -0
  135. package/src/verification/verify-menu-helpers.js +112 -0
  136. package/src/verification/verify-menu.js +18 -302
  137. package/src/verification/verify-slider-checks.js +115 -0
  138. package/src/verification/verify-slider-constants.js +65 -0
  139. package/src/verification/verify-slider-helpers.js +164 -0
  140. package/src/verification/verify-slider.js +142 -0
  141. package/.env.example +0 -14
  142. package/docs/basic-clone.md +0 -63
  143. package/docs/cli-reference.md +0 -118
  144. package/docs/design-clone-architecture.md +0 -275
  145. package/docs/pixel-perfect.md +0 -86
  146. package/docs/troubleshooting.md +0 -169
  147. package/requirements.txt +0 -5
  148. package/src/ai/analyze-structure.py +0 -305
  149. package/src/ai/extract-design-tokens.py +0 -439
  150. package/src/ai/prompts/__init__.py +0 -2
  151. package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  152. package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
  153. package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
  154. package/src/ai/prompts/design_tokens.py +0 -183
  155. package/src/ai/prompts/structure_analysis.py +0 -273
  156. package/src/core/animation-extractor.js +0 -526
  157. package/src/core/design-tokens.js +0 -103
  158. package/src/core/dimension-extractor.js +0 -366
  159. package/src/core/dimension-output.js +0 -208
  160. package/src/core/discover-pages.js +0 -314
  161. package/src/core/extract-assets.js +0 -468
  162. package/src/core/filter-css.js +0 -499
  163. package/src/core/html-extractor.js +0 -171
  164. package/src/core/merge-css.js +0 -407
  165. package/src/core/multi-page-screenshot.js +0 -377
  166. package/src/core/rewrite-links.js +0 -226
  167. package/src/core/screenshot.js +0 -572
  168. package/src/core/state-capture.js +0 -602
  169. package/src/core/video-capture.js +0 -540
  170. package/src/utils/__init__.py +0 -16
  171. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  172. package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
  173. package/src/utils/env.py +0 -134
  174. package/src/utils/puppeteer.js +0 -281
@@ -1,602 +0,0 @@
1
- /**
2
- * State Capture Module
3
- *
4
- * Capture hover states for interactive elements using Puppeteer.
5
- * Screenshots before/after, computes style differences, generates :hover CSS.
6
- *
7
- * Usage:
8
- * import { captureAllHoverStates, generateHoverCss } from './state-capture.js';
9
- * const result = await captureAllHoverStates(page, cssString, outputDir);
10
- * const hoverCss = generateHoverCss(result.elements);
11
- *
12
- * @module state-capture
13
- */
14
-
15
- import path from 'path';
16
- import fs from 'fs/promises';
17
-
18
- // ============================================================================
19
- // Constants
20
- // ============================================================================
21
-
22
- /** Delay after hover for CSS transitions to complete (ms) */
23
- const HOVER_SETTLE_DELAY = 100;
24
-
25
- /** Delay after mouse reset for state to clear (ms) */
26
- const MOUSE_RESET_DELAY = 50;
27
-
28
- /** Padding around element for screenshots (px) */
29
- const SCREENSHOT_PADDING = 20;
30
-
31
- /** Maximum number of elements to capture (performance limit) */
32
- const MAX_ELEMENTS = 50;
33
-
34
- /** Maximum elements to scan in DOM for transitions (performance limit) */
35
- const MAX_DOM_SCAN = 200;
36
-
37
- /** Maximum selector depth when generating unique selectors */
38
- const MAX_SELECTOR_DEPTH = 3;
39
-
40
- /** Interactive element selectors for DOM query */
41
- const INTERACTIVE_SELECTORS = [
42
- 'button:not(:disabled)',
43
- 'a[href]',
44
- '[role="button"]',
45
- '[role="link"]',
46
- 'input[type="submit"]',
47
- 'input[type="button"]',
48
- '.btn',
49
- '.button',
50
- '.card',
51
- '.nav-link'
52
- ];
53
-
54
- /** CSS properties to capture for style diff */
55
- const STYLE_PROPERTIES = [
56
- 'backgroundColor',
57
- 'color',
58
- 'transform',
59
- 'boxShadow',
60
- 'borderColor',
61
- 'opacity',
62
- 'scale',
63
- 'filter',
64
- 'textDecoration',
65
- 'outline'
66
- ];
67
-
68
- // ============================================================================
69
- // Dependency Management
70
- // ============================================================================
71
-
72
- let csstree = null;
73
- try {
74
- csstree = await import('css-tree');
75
- } catch {
76
- console.error(
77
- '[state-capture] css-tree not available. CSS-based hover detection disabled.\n' +
78
- ' Fix: Run "npm install css-tree"'
79
- );
80
- }
81
-
82
- // ============================================================================
83
- // Type Definitions (JSDoc)
84
- // ============================================================================
85
-
86
- /**
87
- * @typedef {Object} InteractiveElement
88
- * @property {string} selector - Unique CSS selector
89
- * @property {string} tag - HTML tag name
90
- * @property {string} [text] - First 30 chars of text content
91
- * @property {boolean} [hasTransition] - True if element has CSS transitions
92
- */
93
-
94
- /**
95
- * @typedef {Object} StyleDiff
96
- * @property {string} from - Value in normal state
97
- * @property {string} to - Value in hover state
98
- */
99
-
100
- /**
101
- * @typedef {Object} HoverCaptureResult
102
- * @property {string} selector - CSS selector for the element
103
- * @property {boolean} success - True if hover state differs from normal
104
- * @property {string|null} normalScreenshot - Path to normal state screenshot
105
- * @property {string|null} hoverScreenshot - Path to hover state screenshot
106
- * @property {Object<string, StyleDiff>} styleDiff - Style differences
107
- * @property {string} [error] - Error message if capture failed
108
- */
109
-
110
- /**
111
- * @typedef {Object} HoverCaptureOutput
112
- * @property {string} directory - Output directory path
113
- * @property {number} detected - Number of detected interactive elements
114
- * @property {number} captured - Number of successfully captured elements
115
- * @property {string} summaryPath - Path to hover-diff.json
116
- * @property {HoverCaptureResult[]} elements - Captured element results
117
- */
118
-
119
- // ============================================================================
120
- // Utility Functions
121
- // ============================================================================
122
-
123
- /**
124
- * Extract base selector from :hover selector (remove :hover pseudo-class).
125
- * Handles patterns like ".btn:hover", ".card:hover .title", "button:hover, button:focus"
126
- *
127
- * @param {string} selectorText - Full selector text with :hover
128
- * @returns {string} Base selector without :hover
129
- */
130
- function extractBaseSelector(selectorText) {
131
- return selectorText.replace(/:hover/g, '').replace(/\s+/g, ' ').trim();
132
- }
133
-
134
- /**
135
- * Convert camelCase to kebab-case.
136
- * Example: backgroundColor -> background-color
137
- *
138
- * @param {string} str - camelCase string
139
- * @returns {string} kebab-case string
140
- */
141
- function toKebabCase(str) {
142
- return str.replace(/([A-Z])/g, '-$1').toLowerCase();
143
- }
144
-
145
- /**
146
- * Validate that a selector is valid CSS syntax.
147
- *
148
- * @param {string} selector - CSS selector to validate
149
- * @returns {boolean} True if selector appears valid
150
- */
151
- function isValidSelector(selector) {
152
- if (!selector || typeof selector !== 'string') return false;
153
- // Basic validation: not empty, not just whitespace, has content after trimming
154
- const trimmed = selector.trim();
155
- if (!trimmed || trimmed.length > 500) return false;
156
- // Check for obviously invalid patterns
157
- if (/[<>{}]/.test(trimmed)) return false;
158
- return true;
159
- }
160
-
161
- /**
162
- * Log message if running in TTY mode.
163
- *
164
- * @param {string} level - Log level (error, warn, info)
165
- * @param {string} message - Message to log
166
- */
167
- function log(level, message) {
168
- if (process.stderr.isTTY) {
169
- const prefix = level === 'error' ? '[ERROR]' : level === 'warn' ? '[WARN]' : '[INFO]';
170
- console.error(`${prefix} ${message}`);
171
- }
172
- }
173
-
174
- // ============================================================================
175
- // CSS-Based Detection
176
- // ============================================================================
177
-
178
- /**
179
- * Extract selectors with :hover from CSS using AST.
180
- *
181
- * @param {string|null|undefined} cssString - Raw CSS string
182
- * @returns {Set<string>} Set of base selectors that have :hover rules
183
- */
184
- function extractHoverSelectorsFromCss(cssString) {
185
- const hoverSelectors = new Set();
186
-
187
- if (!csstree || !cssString || typeof cssString !== 'string') {
188
- return hoverSelectors;
189
- }
190
-
191
- try {
192
- const ast = csstree.parse(cssString, { parseRulePrelude: true });
193
-
194
- csstree.walk(ast, {
195
- visit: 'Rule',
196
- enter(node) {
197
- if (!node.prelude) return;
198
-
199
- const selectorText = csstree.generate(node.prelude);
200
- if (selectorText.includes(':hover')) {
201
- const baseSelector = extractBaseSelector(selectorText);
202
- if (baseSelector && isValidSelector(baseSelector)) {
203
- hoverSelectors.add(baseSelector);
204
- }
205
- }
206
- }
207
- });
208
- } catch (e) {
209
- log('error', `[state-capture] CSS parse error: ${e.message}`);
210
- }
211
-
212
- return hoverSelectors;
213
- }
214
-
215
- // ============================================================================
216
- // DOM-Based Detection
217
- // ============================================================================
218
-
219
- /**
220
- * Detect interactive elements on page via DOM query.
221
- * Uses inline function to avoid new Function() for CSP compliance.
222
- *
223
- * @param {import('puppeteer').Page} page - Puppeteer page
224
- * @returns {Promise<InteractiveElement[]>} Array of interactive elements
225
- */
226
- async function detectInteractiveElementsFromDom(page) {
227
- return await page.evaluate((selectors, maxScan, maxDepth) => {
228
- // Inline getUniqueSelector to avoid new Function()
229
- function getUniqueSelector(element) {
230
- if (element.id) return '#' + element.id;
231
-
232
- const pathArr = [];
233
- let current = element;
234
-
235
- while (current && current.nodeType === 1 && pathArr.length < maxDepth) {
236
- let selector = current.tagName.toLowerCase();
237
-
238
- // Add first 2 class names for specificity
239
- if (current.className && typeof current.className === 'string') {
240
- const classes = current.className.trim().split(/\s+/).slice(0, 2).filter(c => c);
241
- if (classes.length) selector += '.' + classes.join('.');
242
- }
243
-
244
- // Add nth-of-type if siblings exist
245
- const siblings = current.parentNode?.children || [];
246
- const sameTagSiblings = Array.from(siblings).filter(s => s.tagName === current.tagName);
247
- if (sameTagSiblings.length > 1) {
248
- const index = sameTagSiblings.indexOf(current) + 1;
249
- selector += ':nth-of-type(' + index + ')';
250
- }
251
-
252
- pathArr.unshift(selector);
253
- current = current.parentElement;
254
- }
255
-
256
- return pathArr.join(' > ');
257
- }
258
-
259
- const results = [];
260
- const seen = new Set();
261
- let totalScanned = 0;
262
-
263
- // Query by interactive selectors
264
- for (const sel of selectors) {
265
- if (totalScanned >= maxScan) break;
266
-
267
- try {
268
- const elements = document.querySelectorAll(sel);
269
- for (const el of elements) {
270
- if (totalScanned >= maxScan) break;
271
- totalScanned++;
272
-
273
- // Skip hidden elements
274
- if (!el.offsetParent && el.tagName !== 'BODY') continue;
275
-
276
- const uniqueSel = getUniqueSelector(el);
277
- if (!seen.has(uniqueSel)) {
278
- seen.add(uniqueSel);
279
- results.push({
280
- selector: uniqueSel,
281
- tag: el.tagName.toLowerCase(),
282
- text: el.textContent?.slice(0, 30)?.trim() || ''
283
- });
284
- }
285
- }
286
- } catch {
287
- // Invalid selector, skip
288
- }
289
- }
290
-
291
- // Also detect elements with CSS transitions
292
- const allElements = document.querySelectorAll('*');
293
- for (const el of allElements) {
294
- if (totalScanned >= maxScan) break;
295
- totalScanned++;
296
-
297
- if (!el.offsetParent && el.tagName !== 'BODY') continue;
298
-
299
- const style = getComputedStyle(el);
300
- const hasTransition = style.transition &&
301
- style.transition !== 'all 0s ease 0s' &&
302
- style.transition !== 'none' &&
303
- !style.transition.startsWith('none');
304
-
305
- if (hasTransition) {
306
- const uniqueSel = getUniqueSelector(el);
307
- if (!seen.has(uniqueSel)) {
308
- seen.add(uniqueSel);
309
- results.push({
310
- selector: uniqueSel,
311
- tag: el.tagName.toLowerCase(),
312
- hasTransition: true
313
- });
314
- }
315
- }
316
- }
317
-
318
- return results;
319
- }, INTERACTIVE_SELECTORS, MAX_DOM_SCAN, MAX_SELECTOR_DEPTH);
320
- }
321
-
322
- // ============================================================================
323
- // Main Detection Function
324
- // ============================================================================
325
-
326
- /**
327
- * Detect interactive elements using CSS + DOM analysis.
328
- *
329
- * @param {import('puppeteer').Page} page - Puppeteer page
330
- * @param {string|null} cssString - Raw CSS for :hover detection
331
- * @returns {Promise<{fromCss: string[], fromDom: InteractiveElement[], combined: string[]}>}
332
- */
333
- export async function detectInteractiveElements(page, cssString) {
334
- // Validate input
335
- if (!page) {
336
- throw new Error('Page parameter is required');
337
- }
338
-
339
- // Method 1: CSS-based detection (faster, more accurate for :hover)
340
- const hoverSelectors = extractHoverSelectorsFromCss(cssString);
341
-
342
- // Method 2: DOM-based detection
343
- const domInteractive = await detectInteractiveElementsFromDom(page);
344
-
345
- // Merge and dedupe, prioritizing CSS selectors
346
- // Filter invalid selectors before merging
347
- const validDomSelectors = domInteractive
348
- .map(e => e.selector)
349
- .filter(s => isValidSelector(s));
350
-
351
- const allSelectors = new Set([
352
- ...hoverSelectors,
353
- ...validDomSelectors
354
- ]);
355
-
356
- // Limit to MAX_ELEMENTS
357
- const combined = Array.from(allSelectors).slice(0, MAX_ELEMENTS);
358
-
359
- return {
360
- fromCss: Array.from(hoverSelectors),
361
- fromDom: domInteractive,
362
- combined
363
- };
364
- }
365
-
366
- // ============================================================================
367
- // Hover State Capture
368
- // ============================================================================
369
-
370
- /**
371
- * Capture computed styles for an element.
372
- *
373
- * @param {import('puppeteer').Page} page - Puppeteer page
374
- * @param {string} selector - CSS selector
375
- * @returns {Promise<Object<string, string>|null>} Style object or null
376
- */
377
- async function captureElementStyles(page, selector) {
378
- return await page.evaluate((sel, props) => {
379
- const el = document.querySelector(sel);
380
- if (!el) return null;
381
-
382
- const style = getComputedStyle(el);
383
- const result = {};
384
- for (const prop of props) {
385
- result[prop] = style[prop];
386
- }
387
- return result;
388
- }, selector, STYLE_PROPERTIES);
389
- }
390
-
391
- /**
392
- * Capture hover state for a single element.
393
- *
394
- * @param {import('puppeteer').Page} page - Puppeteer page
395
- * @param {string} selector - CSS selector for element
396
- * @param {string} outputDir - Directory for screenshots
397
- * @param {number} index - Element index for filename
398
- * @returns {Promise<HoverCaptureResult>}
399
- */
400
- export async function captureHoverState(page, selector, outputDir, index) {
401
- const result = {
402
- selector,
403
- success: false,
404
- normalScreenshot: null,
405
- hoverScreenshot: null,
406
- normalStyles: null,
407
- hoverStyles: null,
408
- styleDiff: {}
409
- };
410
-
411
- // Validate selector before attempting capture
412
- if (!isValidSelector(selector)) {
413
- result.error = 'Invalid selector';
414
- return result;
415
- }
416
-
417
- try {
418
- // Find element
419
- const element = await page.$(selector);
420
- if (!element) {
421
- result.error = 'Element not found';
422
- return result;
423
- }
424
-
425
- // Check visibility
426
- const isVisible = await element.isVisible().catch(() => false);
427
- if (!isVisible) {
428
- result.error = 'Element not visible';
429
- return result;
430
- }
431
-
432
- // Get bounding box
433
- const box = await element.boundingBox();
434
- if (!box) {
435
- result.error = 'No bounding box';
436
- return result;
437
- }
438
-
439
- // Calculate clip area with padding
440
- const clip = {
441
- x: Math.max(0, box.x - SCREENSHOT_PADDING),
442
- y: Math.max(0, box.y - SCREENSHOT_PADDING),
443
- width: box.width + SCREENSHOT_PADDING * 2,
444
- height: box.height + SCREENSHOT_PADDING * 2
445
- };
446
-
447
- // Capture normal state using helper
448
- result.normalStyles = await captureElementStyles(page, selector);
449
- const normalPath = path.join(outputDir, `hover-${index}-normal.png`);
450
- await page.screenshot({ path: normalPath, clip });
451
- result.normalScreenshot = normalPath;
452
-
453
- // Hover and wait for transition
454
- await page.hover(selector);
455
- await new Promise(r => setTimeout(r, HOVER_SETTLE_DELAY));
456
-
457
- // Capture hover state using same helper
458
- result.hoverStyles = await captureElementStyles(page, selector);
459
- const hoverPath = path.join(outputDir, `hover-${index}-hover.png`);
460
- await page.screenshot({ path: hoverPath, clip });
461
- result.hoverScreenshot = hoverPath;
462
-
463
- // Compute style diff
464
- if (result.normalStyles && result.hoverStyles) {
465
- for (const [prop, normalVal] of Object.entries(result.normalStyles)) {
466
- const hoverVal = result.hoverStyles[prop];
467
- if (hoverVal !== normalVal) {
468
- result.styleDiff[prop] = { from: normalVal, to: hoverVal };
469
- }
470
- }
471
- }
472
-
473
- // Reset mouse position
474
- await page.mouse.move(0, 0);
475
- await new Promise(r => setTimeout(r, MOUSE_RESET_DELAY));
476
-
477
- // Success if any style changed
478
- result.success = Object.keys(result.styleDiff).length > 0;
479
-
480
- } catch (e) {
481
- result.error = e.message;
482
- }
483
-
484
- return result;
485
- }
486
-
487
- // ============================================================================
488
- // Batch Capture
489
- // ============================================================================
490
-
491
- /**
492
- * Capture all hover states for detected interactive elements.
493
- *
494
- * @param {import('puppeteer').Page} page - Puppeteer page
495
- * @param {string|null} cssString - Raw CSS for detection
496
- * @param {string} outputDir - Base output directory
497
- * @returns {Promise<HoverCaptureOutput>}
498
- */
499
- export async function captureAllHoverStates(page, cssString, outputDir) {
500
- // Validate inputs
501
- if (!page) {
502
- throw new Error('Page parameter is required');
503
- }
504
- if (!outputDir || typeof outputDir !== 'string') {
505
- throw new Error('Output directory parameter is required');
506
- }
507
-
508
- // Create hover-states directory
509
- const hoverDir = path.join(outputDir, 'hover-states');
510
- await fs.mkdir(hoverDir, { recursive: true });
511
-
512
- // Detect interactive elements
513
- const interactive = await detectInteractiveElements(page, cssString);
514
- const elements = [];
515
- let capturedCount = 0;
516
-
517
- // Capture each element
518
- for (let i = 0; i < interactive.combined.length; i++) {
519
- const selector = interactive.combined[i];
520
-
521
- const result = await captureHoverState(page, selector, hoverDir, i);
522
- elements.push(result);
523
-
524
- if (result.success) {
525
- capturedCount++;
526
- log('info', `[hover] ${capturedCount}: ${selector}`);
527
- }
528
- }
529
-
530
- // Write summary JSON
531
- const summaryPath = path.join(hoverDir, 'hover-diff.json');
532
- await fs.writeFile(summaryPath, JSON.stringify({
533
- detected: interactive.combined.length,
534
- captured: capturedCount,
535
- fromCss: interactive.fromCss.length,
536
- fromDom: interactive.fromDom.length,
537
- elements: elements.filter(e => e.success).map(r => ({
538
- selector: r.selector,
539
- styleDiff: r.styleDiff,
540
- normalScreenshot: r.normalScreenshot ? path.basename(r.normalScreenshot) : null,
541
- hoverScreenshot: r.hoverScreenshot ? path.basename(r.hoverScreenshot) : null
542
- }))
543
- }, null, 2), 'utf-8');
544
-
545
- return {
546
- directory: hoverDir,
547
- detected: interactive.combined.length,
548
- captured: capturedCount,
549
- summaryPath,
550
- elements
551
- };
552
- }
553
-
554
- // ============================================================================
555
- // CSS Generation
556
- // ============================================================================
557
-
558
- /**
559
- * Generate :hover CSS from captured style diffs.
560
- *
561
- * @param {HoverCaptureResult[]} results - Array of capture results
562
- * @returns {string} Generated CSS string
563
- */
564
- export function generateHoverCss(results) {
565
- // Validate input
566
- if (!results || !Array.isArray(results)) {
567
- return '/* No hover style changes detected */\n';
568
- }
569
-
570
- const lines = [
571
- '/**',
572
- ' * Generated :hover Styles',
573
- ' * Captured by design-clone state-capture',
574
- ' */\n'
575
- ];
576
-
577
- const successfulResults = results.filter(r => r.success && Object.keys(r.styleDiff).length > 0);
578
-
579
- if (successfulResults.length === 0) {
580
- return '/* No hover style changes detected */\n';
581
- }
582
-
583
- for (const result of successfulResults) {
584
- lines.push(`/* Element: ${result.selector} */`);
585
- lines.push(`${result.selector}:hover {`);
586
-
587
- for (const [prop, diff] of Object.entries(result.styleDiff)) {
588
- const cssProp = toKebabCase(prop);
589
- lines.push(` ${cssProp}: ${diff.to};`);
590
- }
591
-
592
- lines.push('}\n');
593
- }
594
-
595
- return lines.join('\n');
596
- }
597
-
598
- // ============================================================================
599
- // Exports
600
- // ============================================================================
601
-
602
- export { extractHoverSelectorsFromCss, detectInteractiveElementsFromDom };