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
@@ -21,365 +21,39 @@
21
21
  * --verbose Show detailed progress
22
22
  */
23
23
 
24
- import fs from 'fs/promises';
25
24
  import path from 'path';
26
25
 
27
- import { getBrowser, getPage, closeBrowser, disconnectBrowser, parseArgs, outputJSON, outputError } from '../utils/browser.js';
28
-
29
- // Viewport configurations
30
- const VIEWPORTS = {
31
- mobile: { width: 375, height: 812, deviceScaleFactor: 2 },
32
- tablet: { width: 768, height: 1024, deviceScaleFactor: 1 },
33
- desktop: { width: 1920, height: 1080, deviceScaleFactor: 1 }
34
- };
35
-
36
- // Header element selectors
37
- const HEADER_SELECTORS = {
38
- container: [
39
- 'header',
40
- '[role="banner"]',
41
- '.header',
42
- '#header',
43
- '.site-header',
44
- '.page-header',
45
- '.masthead'
46
- ],
47
- logo: [
48
- 'header img[alt*="logo" i]',
49
- '[role="banner"] img',
50
- '.logo img',
51
- '.site-logo img',
52
- '.logo',
53
- '.site-logo',
54
- 'header a[href="/"] img',
55
- '.brand img',
56
- '.navbar-brand img'
57
- ],
58
- nav: [
59
- 'header nav',
60
- 'header [role="navigation"]',
61
- '.header-nav',
62
- '.main-navigation',
63
- '.primary-nav',
64
- '.site-nav',
65
- '.navbar-nav'
66
- ],
67
- cta: [
68
- 'header button.cta',
69
- 'header a[class*="button"]',
70
- 'header a[class*="btn"]',
71
- '.header-action',
72
- '.nav-cta',
73
- 'header .btn-primary',
74
- 'header a[href*="contact"]',
75
- 'header a[href*="signup"]',
76
- 'header a[href*="login"]'
77
- ],
78
- navLinks: [
79
- 'header nav a',
80
- 'header [role="navigation"] a',
81
- '.main-navigation a',
82
- '.nav-item a',
83
- '.menu-item a'
84
- ]
85
- };
86
-
87
- /**
88
- * Find first matching element from selectors
89
- */
90
- async function findElement(page, selectors) {
91
- for (const selector of selectors) {
92
- try {
93
- const element = await page.$(selector);
94
- if (element) {
95
- return { element, selector };
96
- }
97
- } catch (err) { /* continue - selector not found */ }
98
- }
99
- return null;
100
- }
101
-
102
- /**
103
- * Count visible elements
104
- */
105
- async function countVisibleElements(page, selectors) {
106
- for (const selector of selectors) {
107
- try {
108
- const count = await page.evaluate((sel) => {
109
- const items = document.querySelectorAll(sel);
110
- let visible = 0;
111
- items.forEach(item => {
112
- const style = window.getComputedStyle(item);
113
- const rect = item.getBoundingClientRect();
114
- if (
115
- style.display !== 'none' &&
116
- style.visibility !== 'hidden' &&
117
- style.opacity !== '0' &&
118
- rect.width > 0 &&
119
- rect.height > 0
120
- ) {
121
- visible++;
122
- }
123
- });
124
- return visible;
125
- }, selector);
126
-
127
- if (count > 0) {
128
- return { count, selector };
129
- }
130
- } catch (err) { /* continue - selector not found */ }
131
- }
132
- return { count: 0, selector: null };
133
- }
134
-
135
- /**
136
- * Check header position properties
137
- */
138
- async function checkHeaderPosition(page, headerSelector) {
139
- return await page.evaluate((sel) => {
140
- const header = document.querySelector(sel);
141
- if (!header) return null;
142
-
143
- const style = window.getComputedStyle(header);
144
- const rect = header.getBoundingClientRect();
145
-
146
- return {
147
- position: style.position,
148
- isSticky: style.position === 'sticky',
149
- isFixed: style.position === 'fixed',
150
- zIndex: parseInt(style.zIndex) || 'auto',
151
- top: rect.top,
152
- height: rect.height,
153
- width: rect.width
154
- };
155
- }, headerSelector);
156
- }
157
-
158
- /**
159
- * Check logo position (typically left or center)
160
- */
161
- async function checkLogoPosition(page, logoSelector, headerWidth) {
162
- return await page.evaluate((sel, width) => {
163
- const logo = document.querySelector(sel);
164
- if (!logo) return null;
165
-
166
- const rect = logo.getBoundingClientRect();
167
- const centerThreshold = width * 0.35;
168
-
169
- let position = 'unknown';
170
- if (rect.left < centerThreshold) {
171
- position = 'left';
172
- } else if (rect.left > width - centerThreshold) {
173
- position = 'right';
174
- } else {
175
- position = 'center';
176
- }
177
-
178
- return {
179
- position,
180
- x: rect.left,
181
- y: rect.top,
182
- width: rect.width,
183
- height: rect.height
184
- };
185
- }, logoSelector, headerWidth);
186
- }
187
-
188
- /**
189
- * Test header at specific viewport
190
- */
191
- async function testViewport(page, viewportName, verbose = false) {
192
- const viewport = VIEWPORTS[viewportName];
193
- await page.setViewportSize(viewport);
194
- await new Promise(r => setTimeout(r, 500));
195
-
196
- const result = {
197
- viewport: viewportName,
198
- dimensions: viewport,
199
- tests: [],
200
- passed: 0,
201
- failed: 0,
202
- warnings: []
203
- };
204
-
205
- if (verbose) console.error(`\n📱 Testing ${viewportName} (${viewport.width}x${viewport.height})...`);
206
-
207
- // Test 1: Header container exists
208
- const headerResult = await findElement(page, HEADER_SELECTORS.container);
209
- if (headerResult) {
210
- result.tests.push({
211
- name: 'Header container exists',
212
- passed: true,
213
- selector: headerResult.selector
214
- });
215
- result.passed++;
216
- if (verbose) console.error(` ✓ Header found: ${headerResult.selector}`);
217
-
218
- // Get header position info
219
- const positionInfo = await checkHeaderPosition(page, headerResult.selector);
220
-
221
- // Test 2: Logo presence
222
- const logoResult = await findElement(page, HEADER_SELECTORS.logo);
223
- if (logoResult) {
224
- const logoPosition = await checkLogoPosition(page, logoResult.selector, viewport.width);
225
- result.tests.push({
226
- name: 'Logo present',
227
- passed: true,
228
- selector: logoResult.selector,
229
- position: logoPosition?.position || 'unknown'
230
- });
231
- result.passed++;
232
- if (verbose) console.error(` ✓ Logo found: ${logoResult.selector} (${logoPosition?.position})`);
233
- } else {
234
- result.tests.push({
235
- name: 'Logo present',
236
- passed: false,
237
- error: 'No logo found'
238
- });
239
- result.failed++;
240
- if (verbose) console.error(` ✗ Logo not found`);
241
- }
242
-
243
- // Test 3: Navigation links
244
- const navLinks = await countVisibleElements(page, HEADER_SELECTORS.navLinks);
245
- const expectedLinks = viewportName === 'desktop' ? 2 : 0;
246
-
247
- if (navLinks.count >= expectedLinks) {
248
- result.tests.push({
249
- name: 'Navigation links visible',
250
- passed: true,
251
- count: navLinks.count,
252
- selector: navLinks.selector
253
- });
254
- result.passed++;
255
- if (verbose) console.error(` ✓ ${navLinks.count} nav links visible`);
256
- } else if (viewportName !== 'desktop' && navLinks.count === 0) {
257
- // Mobile/tablet may hide links behind hamburger
258
- result.tests.push({
259
- name: 'Navigation links (may be in hamburger)',
260
- passed: true,
261
- count: navLinks.count,
262
- note: 'Links may be hidden in mobile menu'
263
- });
264
- result.passed++;
265
- if (verbose) console.error(` ✓ Nav links hidden (expected on ${viewportName})`);
266
- } else {
267
- result.tests.push({
268
- name: 'Navigation links visible',
269
- passed: false,
270
- count: navLinks.count,
271
- error: `Expected at least ${expectedLinks} links on ${viewportName}`
272
- });
273
- result.failed++;
274
- if (verbose) console.error(` ✗ Only ${navLinks.count} nav links (expected >= ${expectedLinks})`);
275
- }
276
-
277
- // Test 4: CTA buttons (desktop only)
278
- if (viewportName === 'desktop') {
279
- const ctaResult = await findElement(page, HEADER_SELECTORS.cta);
280
- if (ctaResult) {
281
- result.tests.push({
282
- name: 'CTA button present',
283
- passed: true,
284
- selector: ctaResult.selector
285
- });
286
- result.passed++;
287
- if (verbose) console.error(` ✓ CTA found: ${ctaResult.selector}`);
288
- } else {
289
- result.warnings.push('No CTA button found in header');
290
- if (verbose) console.error(` ⚠ No CTA button found`);
291
- }
292
- }
293
-
294
- // Test 5: Sticky/fixed behavior
295
- if (positionInfo) {
296
- if (positionInfo.isSticky || positionInfo.isFixed) {
297
- result.tests.push({
298
- name: 'Header sticky/fixed behavior',
299
- passed: true,
300
- position: positionInfo.position
301
- });
302
- result.passed++;
303
- if (verbose) console.error(` ✓ Header is ${positionInfo.position}`);
304
- } else {
305
- result.tests.push({
306
- name: 'Header sticky/fixed behavior',
307
- passed: true,
308
- position: positionInfo.position,
309
- note: 'Header uses static/relative positioning'
310
- });
311
- result.passed++;
312
- if (verbose) console.error(` ✓ Header position: ${positionInfo.position}`);
313
- }
314
-
315
- // Test 6: Z-index check (should be high for sticky/fixed)
316
- if ((positionInfo.isSticky || positionInfo.isFixed) && positionInfo.zIndex !== 'auto') {
317
- const zIndexOk = positionInfo.zIndex >= 100;
318
- result.tests.push({
319
- name: 'Z-index layering',
320
- passed: zIndexOk,
321
- zIndex: positionInfo.zIndex,
322
- note: zIndexOk ? 'Header on top layer' : 'Z-index may be too low'
323
- });
324
- if (zIndexOk) result.passed++;
325
- else result.warnings.push(`Header z-index (${positionInfo.zIndex}) may be too low`);
326
- if (verbose) console.error(` ${zIndexOk ? '✓' : '⚠'} Z-index: ${positionInfo.zIndex}`);
327
- }
328
-
329
- // Store height for consistency check
330
- result.headerHeight = positionInfo.height;
331
- }
332
-
333
- } else {
334
- result.tests.push({
335
- name: 'Header container exists',
336
- passed: false,
337
- error: 'No header container found'
338
- });
339
- result.failed++;
340
- if (verbose) console.error(` ✗ Header not found`);
341
- }
342
-
343
- return result;
344
- }
345
-
346
- /**
347
- * Capture component screenshot
348
- */
349
- async function captureHeaderScreenshot(page, outputDir, viewportName) {
350
- if (!outputDir) return null;
351
-
352
- const screenshotPath = path.join(outputDir, `header-test-${viewportName}.png`);
353
- await page.screenshot({
354
- path: screenshotPath,
355
- fullPage: false
356
- });
357
- return screenshotPath;
358
- }
26
+ import { getBrowser, getPage, closeBrowser, disconnectBrowser } from '../utils/browser.js';
27
+ import { parseArgs, outputJSON, outputError } from '../utils/helpers.js';
28
+ import { VIEWPORTS_HD as VIEWPORTS } from '../shared/viewports.js';
29
+ import { testHeaderViewport } from './verify-header-checks.js';
359
30
 
360
31
  /**
361
32
  * Validate HTML file path (security: prevent path traversal)
362
33
  */
363
34
  function validateHtmlPath(htmlPath) {
364
35
  const absolutePath = path.resolve(htmlPath);
365
- const cwd = process.cwd();
366
-
367
- // Allow paths within CWD or common output directories
368
36
  const allowedPrefixes = [
369
- cwd,
37
+ process.cwd(),
370
38
  path.join(process.env.HOME || '', '.claude'),
371
39
  '/tmp',
372
40
  path.join(process.env.HOME || '', 'cloned-designs')
373
41
  ];
374
-
375
42
  const isAllowed = allowedPrefixes.some(prefix => absolutePath.startsWith(prefix));
376
- if (!isAllowed) {
377
- throw new Error(`Path "${htmlPath}" is outside allowed directories`);
378
- }
379
-
43
+ if (!isAllowed) throw new Error(`Path "${htmlPath}" is outside allowed directories`);
380
44
  return absolutePath;
381
45
  }
382
46
 
47
+ /**
48
+ * Capture header screenshot
49
+ */
50
+ async function captureHeaderScreenshot(page, outputDir, viewportName) {
51
+ if (!outputDir) return null;
52
+ const screenshotPath = path.join(outputDir, `header-test-${viewportName}.png`);
53
+ await page.screenshot({ path: screenshotPath, fullPage: false });
54
+ return screenshotPath;
55
+ }
56
+
383
57
  /**
384
58
  * Main verification function
385
59
  */
@@ -400,37 +74,27 @@ async function verifyHeader() {
400
74
 
401
75
  let targetUrl;
402
76
  if (args.html) {
403
- const absolutePath = validateHtmlPath(args.html);
404
- targetUrl = `file://${absolutePath}`;
77
+ targetUrl = `file://${validateHtmlPath(args.html)}`;
405
78
  } else {
406
79
  targetUrl = args.url;
407
80
  }
408
81
 
409
82
  if (verbose) console.error(`\n🔍 Verifying header: ${targetUrl}\n`);
410
83
 
411
- await page.goto(targetUrl, {
412
- waitUntil: 'networkidle',
413
- timeout: 30000
414
- });
84
+ await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 30000 });
415
85
 
416
86
  const results = {
417
87
  success: true,
418
88
  component: 'header',
419
89
  url: targetUrl,
420
90
  viewports: {},
421
- summary: {
422
- totalTests: 0,
423
- passed: 0,
424
- failed: 0,
425
- warnings: []
426
- },
91
+ summary: { totalTests: 0, passed: 0, failed: 0, warnings: [] },
427
92
  screenshots: [],
428
93
  heightConsistency: {}
429
94
  };
430
95
 
431
- // Test all viewports
432
96
  for (const viewportName of ['mobile', 'tablet', 'desktop']) {
433
- const viewportResult = await testViewport(page, viewportName, verbose);
97
+ const viewportResult = await testHeaderViewport(page, viewportName, VIEWPORTS, verbose);
434
98
  results.viewports[viewportName] = viewportResult;
435
99
 
436
100
  results.summary.totalTests += viewportResult.tests.length;
@@ -448,7 +112,7 @@ async function verifyHeader() {
448
112
  }
449
113
  }
450
114
 
451
- // Check height consistency
115
+ // Height consistency check
452
116
  const heights = Object.values(results.heightConsistency);
453
117
  if (heights.length >= 2) {
454
118
  const maxDiff = Math.max(...heights) - Math.min(...heights);
@@ -459,18 +123,12 @@ async function verifyHeader() {
459
123
 
460
124
  results.success = results.summary.failed === 0;
461
125
 
462
- if (args.close === 'true') {
463
- await closeBrowser();
464
- } else {
465
- await disconnectBrowser();
466
- }
126
+ if (args.close === 'true') { await closeBrowser(); } else { await disconnectBrowser(); }
467
127
 
468
128
  if (verbose) {
469
129
  console.error('\n📊 Summary:');
470
130
  console.error(` Tests: ${results.summary.passed}/${results.summary.totalTests} passed`);
471
- if (results.summary.warnings.length > 0) {
472
- console.error(` Warnings: ${results.summary.warnings.length}`);
473
- }
131
+ if (results.summary.warnings.length > 0) console.error(` Warnings: ${results.summary.warnings.length}`);
474
132
  console.error(` Status: ${results.success ? '✓ PASS' : '✗ FAIL'}\n`);
475
133
  }
476
134
 
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Layout Verification Report Generator
3
+ *
4
+ * Markdown report writing and CSS fix suggestion utilities
5
+ * extracted from verify-layout.js to keep each file under 200 lines.
6
+ */
7
+
8
+ import fs from 'fs/promises';
9
+ import path from 'path';
10
+ import { VIEWPORTS_HD as VIEWPORTS } from '../shared/viewports.js';
11
+
12
+ /**
13
+ * Generate CSS fix suggestions from discrepancy objects
14
+ * @param {Array} discrepancies
15
+ * @returns {Array<{section: string, severity: string, issue: string, fix: string}>}
16
+ */
17
+ export function generateCSSFixes(discrepancies) {
18
+ const fixes = [];
19
+ for (const disc of discrepancies) {
20
+ if (disc.css_fix) {
21
+ fixes.push({
22
+ section: disc.section,
23
+ severity: disc.severity,
24
+ issue: disc.issue,
25
+ fix: disc.css_fix
26
+ });
27
+ }
28
+ }
29
+ return fixes;
30
+ }
31
+
32
+ /**
33
+ * Write a markdown comparison report to outputDir/layout-verification.md
34
+ * @param {string} outputDir
35
+ * @param {Object} results - Verification results object with viewports map
36
+ * @returns {Promise<string>} Path to written report
37
+ */
38
+ export async function writeReport(outputDir, results) {
39
+ const reportPath = path.join(outputDir, 'layout-verification.md');
40
+
41
+ let report = `# Layout Verification Report\n\nGenerated: ${new Date().toISOString()}\n\n## Summary\n\n`;
42
+ report += `| Viewport | Similarity | Issues |\n|----------|------------|--------|\n`;
43
+
44
+ for (const [viewport, result] of Object.entries(results.viewports)) {
45
+ const score = result.similarity_score || 0;
46
+ const issues = result.discrepancies?.length || 0;
47
+ const status = score >= 90 ? '✅' : score >= 70 ? '⚠️' : '❌';
48
+ report += `| ${viewport} | ${status} ${score}% | ${issues} |\n`;
49
+ }
50
+
51
+ report += `\n## Overall Score: ${results.overall_score}%\n\n`;
52
+
53
+ for (const [viewport, result] of Object.entries(results.viewports)) {
54
+ const vp = VIEWPORTS[viewport];
55
+ report += `## ${viewport.charAt(0).toUpperCase() + viewport.slice(1)} (${vp.width}x${vp.height})\n\n`;
56
+
57
+ if (result.overall_assessment) {
58
+ report += `**Assessment:** ${result.overall_assessment}\n\n`;
59
+ }
60
+
61
+ if (result.discrepancies?.length > 0) {
62
+ report += `### Discrepancies\n\n`;
63
+ for (const disc of result.discrepancies) {
64
+ const icon = disc.severity === 'critical' ? '🔴' : disc.severity === 'major' ? '🟠' : '🟡';
65
+ report += `${icon} **${disc.section}** (${disc.severity})\n`;
66
+ report += ` - Issue: ${disc.issue}\n`;
67
+ if (disc.css_fix) report += ` - Fix: \`${disc.css_fix}\`\n`;
68
+ report += '\n';
69
+ }
70
+ } else {
71
+ report += `✅ No significant discrepancies found.\n\n`;
72
+ }
73
+
74
+ if (result.recommendations?.length > 0) {
75
+ report += `### Recommendations\n\n`;
76
+ for (const rec of result.recommendations) {
77
+ report += `- ${rec}\n`;
78
+ }
79
+ report += '\n';
80
+ }
81
+ }
82
+
83
+ // Consolidated CSS fixes
84
+ const allFixes = [];
85
+ for (const result of Object.values(results.viewports)) {
86
+ if (result.discrepancies) {
87
+ allFixes.push(...generateCSSFixes(result.discrepancies));
88
+ }
89
+ }
90
+
91
+ if (allFixes.length > 0) {
92
+ report += `## Suggested CSS Fixes\n\n\`\`\`css\n`;
93
+ for (const fix of allFixes) {
94
+ report += `/* ${fix.section}: ${fix.issue} */\n${fix.fix}\n\n`;
95
+ }
96
+ report += `\`\`\`\n`;
97
+ }
98
+
99
+ await fs.writeFile(reportPath, report);
100
+ return reportPath;
101
+ }