design-clone 2.1.0 → 3.0.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
@@ -1,598 +0,0 @@
1
- /**
2
- * State Capture Module
3
- *
4
- * Capture hover states for interactive elements using Playwright.
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('playwright').Page} page - Playwright 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
- }, { selectors: INTERACTIVE_SELECTORS, maxScan: MAX_DOM_SCAN, maxDepth: 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('playwright').Page} page - Playwright 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('playwright').Page} page - Playwright 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
- }, { sel: selector, props: STYLE_PROPERTIES });
389
- }
390
-
391
- /**
392
- * Capture hover state for a single element.
393
- *
394
- * @param {import('playwright').Page} page - Playwright 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
- // Use Playwright locator API for reliability
419
- const locator = page.locator(selector);
420
-
421
- // Check visibility via locator
422
- const isVisible = await locator.isVisible().catch(() => false);
423
- if (!isVisible) {
424
- result.error = 'Element not visible';
425
- return result;
426
- }
427
-
428
- // Get bounding box via locator
429
- const box = await locator.boundingBox();
430
- if (!box) {
431
- result.error = 'No bounding box';
432
- return result;
433
- }
434
-
435
- // Calculate clip area with padding
436
- const clip = {
437
- x: Math.max(0, box.x - SCREENSHOT_PADDING),
438
- y: Math.max(0, box.y - SCREENSHOT_PADDING),
439
- width: box.width + SCREENSHOT_PADDING * 2,
440
- height: box.height + SCREENSHOT_PADDING * 2
441
- };
442
-
443
- // Capture normal state using helper
444
- result.normalStyles = await captureElementStyles(page, selector);
445
- const normalPath = path.join(outputDir, `hover-${index}-normal.png`);
446
- await page.screenshot({ path: normalPath, clip });
447
- result.normalScreenshot = normalPath;
448
-
449
- // Hover via locator (more reliable in Playwright)
450
- await locator.hover();
451
- await new Promise(r => setTimeout(r, HOVER_SETTLE_DELAY));
452
-
453
- // Capture hover state using same helper
454
- result.hoverStyles = await captureElementStyles(page, selector);
455
- const hoverPath = path.join(outputDir, `hover-${index}-hover.png`);
456
- await page.screenshot({ path: hoverPath, clip });
457
- result.hoverScreenshot = hoverPath;
458
-
459
- // Compute style diff
460
- if (result.normalStyles && result.hoverStyles) {
461
- for (const [prop, normalVal] of Object.entries(result.normalStyles)) {
462
- const hoverVal = result.hoverStyles[prop];
463
- if (hoverVal !== normalVal) {
464
- result.styleDiff[prop] = { from: normalVal, to: hoverVal };
465
- }
466
- }
467
- }
468
-
469
- // Reset mouse position
470
- await page.mouse.move(0, 0);
471
- await new Promise(r => setTimeout(r, MOUSE_RESET_DELAY));
472
-
473
- // Success if any style changed
474
- result.success = Object.keys(result.styleDiff).length > 0;
475
-
476
- } catch (e) {
477
- result.error = e.message;
478
- }
479
-
480
- return result;
481
- }
482
-
483
- // ============================================================================
484
- // Batch Capture
485
- // ============================================================================
486
-
487
- /**
488
- * Capture all hover states for detected interactive elements.
489
- *
490
- * @param {import('playwright').Page} page - Playwright page
491
- * @param {string|null} cssString - Raw CSS for detection
492
- * @param {string} outputDir - Base output directory
493
- * @returns {Promise<HoverCaptureOutput>}
494
- */
495
- export async function captureAllHoverStates(page, cssString, outputDir) {
496
- // Validate inputs
497
- if (!page) {
498
- throw new Error('Page parameter is required');
499
- }
500
- if (!outputDir || typeof outputDir !== 'string') {
501
- throw new Error('Output directory parameter is required');
502
- }
503
-
504
- // Create hover-states directory
505
- const hoverDir = path.join(outputDir, 'hover-states');
506
- await fs.mkdir(hoverDir, { recursive: true });
507
-
508
- // Detect interactive elements
509
- const interactive = await detectInteractiveElements(page, cssString);
510
- const elements = [];
511
- let capturedCount = 0;
512
-
513
- // Capture each element
514
- for (let i = 0; i < interactive.combined.length; i++) {
515
- const selector = interactive.combined[i];
516
-
517
- const result = await captureHoverState(page, selector, hoverDir, i);
518
- elements.push(result);
519
-
520
- if (result.success) {
521
- capturedCount++;
522
- log('info', `[hover] ${capturedCount}: ${selector}`);
523
- }
524
- }
525
-
526
- // Write summary JSON
527
- const summaryPath = path.join(hoverDir, 'hover-diff.json');
528
- await fs.writeFile(summaryPath, JSON.stringify({
529
- detected: interactive.combined.length,
530
- captured: capturedCount,
531
- fromCss: interactive.fromCss.length,
532
- fromDom: interactive.fromDom.length,
533
- elements: elements.filter(e => e.success).map(r => ({
534
- selector: r.selector,
535
- styleDiff: r.styleDiff,
536
- normalScreenshot: r.normalScreenshot ? path.basename(r.normalScreenshot) : null,
537
- hoverScreenshot: r.hoverScreenshot ? path.basename(r.hoverScreenshot) : null
538
- }))
539
- }, null, 2), 'utf-8');
540
-
541
- return {
542
- directory: hoverDir,
543
- detected: interactive.combined.length,
544
- captured: capturedCount,
545
- summaryPath,
546
- elements
547
- };
548
- }
549
-
550
- // ============================================================================
551
- // CSS Generation
552
- // ============================================================================
553
-
554
- /**
555
- * Generate :hover CSS from captured style diffs.
556
- *
557
- * @param {HoverCaptureResult[]} results - Array of capture results
558
- * @returns {string} Generated CSS string
559
- */
560
- export function generateHoverCss(results) {
561
- // Validate input
562
- if (!results || !Array.isArray(results)) {
563
- return '/* No hover style changes detected */\n';
564
- }
565
-
566
- const lines = [
567
- '/**',
568
- ' * Generated :hover Styles',
569
- ' * Captured by design-clone state-capture',
570
- ' */\n'
571
- ];
572
-
573
- const successfulResults = results.filter(r => r.success && Object.keys(r.styleDiff).length > 0);
574
-
575
- if (successfulResults.length === 0) {
576
- return '/* No hover style changes detected */\n';
577
- }
578
-
579
- for (const result of successfulResults) {
580
- lines.push(`/* Element: ${result.selector} */`);
581
- lines.push(`${result.selector}:hover {`);
582
-
583
- for (const [prop, diff] of Object.entries(result.styleDiff)) {
584
- const cssProp = toKebabCase(prop);
585
- lines.push(` ${cssProp}: ${diff.to};`);
586
- }
587
-
588
- lines.push('}\n');
589
- }
590
-
591
- return lines.join('\n');
592
- }
593
-
594
- // ============================================================================
595
- // Exports
596
- // ============================================================================
597
-
598
- export { extractHoverSelectorsFromCss, detectInteractiveElementsFromDom };