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
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Audit Report Section Generators
3
+ *
4
+ * Pure functions for generating markdown table and section content
5
+ * extracted from generate-audit-report.js to keep each file under 200 lines.
6
+ */
7
+
8
+ import path from 'path';
9
+ import { generateCSSFixes } from './generate-audit-report-css-fixes.js';
10
+
11
+ // Re-export so callers only need this one module
12
+ export { generateCSSFixes };
13
+
14
+ // Status icons
15
+ export const STATUS_ICONS = {
16
+ pass: '✅',
17
+ warn: '⚠️',
18
+ fail: '❌',
19
+ info: 'ℹ️'
20
+ };
21
+
22
+ /**
23
+ * Calculate component status from verification result
24
+ * @param {Object|null} result
25
+ * @returns {{status: string, icon: string, label: string}}
26
+ */
27
+ export function getComponentStatus(result) {
28
+ if (!result) return { status: 'skip', icon: STATUS_ICONS.info, label: 'Not tested' };
29
+ const { summary } = result;
30
+ if (!summary) return { status: 'skip', icon: STATUS_ICONS.info, label: 'No data' };
31
+ if (summary.failed > 0) return { status: 'fail', icon: STATUS_ICONS.fail, label: `${summary.failed} failed` };
32
+ if (summary.warnings?.length > 0) return { status: 'warn', icon: STATUS_ICONS.warn, label: `${summary.warnings.length} warnings` };
33
+ return { status: 'pass', icon: STATUS_ICONS.pass, label: 'Passed' };
34
+ }
35
+
36
+ /**
37
+ * Generate summary table markdown
38
+ * @param {Object} results - Map of component name to result
39
+ * @returns {string}
40
+ */
41
+ export function generateSummaryTable(results) {
42
+ let table = `| Component | Status | Tests | Details |\n|-----------|--------|-------|----------|\n`;
43
+ for (const [component, result] of Object.entries(results)) {
44
+ const status = getComponentStatus(result);
45
+ const tests = result?.summary ? `${result.summary.passed}/${result.summary.totalTests}` : '-';
46
+ const label = component.charAt(0).toUpperCase() + component.slice(1);
47
+ table += `| ${label} | ${status.icon} ${status.label} | ${tests} | ${result?.url || '-'} |\n`;
48
+ }
49
+ return table;
50
+ }
51
+
52
+ /**
53
+ * Generate responsive viewport breakdown table
54
+ * @param {Object} results
55
+ * @returns {string}
56
+ */
57
+ export function generateViewportTable(results) {
58
+ const viewports = ['mobile', 'tablet', 'desktop'];
59
+ const components = Object.keys(results).filter(c => results[c]?.viewports);
60
+ if (components.length === 0) return '';
61
+
62
+ let table = `| Component | Mobile | Tablet | Desktop |\n|-----------|--------|--------|----------|\n`;
63
+ for (const component of components) {
64
+ const result = results[component];
65
+ const row = [component.charAt(0).toUpperCase() + component.slice(1)];
66
+ for (const vp of viewports) {
67
+ const vpResult = result.viewports?.[vp];
68
+ if (vpResult) {
69
+ const icon = vpResult.failed > 0 ? STATUS_ICONS.fail
70
+ : vpResult.warnings?.length > 0 ? STATUS_ICONS.warn
71
+ : STATUS_ICONS.pass;
72
+ row.push(`${icon} ${vpResult.passed}/${vpResult.tests?.length || 0}`);
73
+ } else {
74
+ row.push('-');
75
+ }
76
+ }
77
+ table += `| ${row.join(' | ')} |\n`;
78
+ }
79
+ return table;
80
+ }
81
+
82
+ /**
83
+ * Generate a markdown section for a single component
84
+ * @param {string} component
85
+ * @param {Object|null} result
86
+ * @returns {string}
87
+ */
88
+ export function generateComponentSection(component, result) {
89
+ const label = component.charAt(0).toUpperCase() + component.slice(1);
90
+ if (!result) return `### ${label}\n\n${STATUS_ICONS.info} Not tested\n\n---\n\n`;
91
+
92
+ const status = getComponentStatus(result);
93
+ let section = `### ${label} ${status.icon}\n\n`;
94
+
95
+ if (component === 'slider' && result.sliderLibrary) {
96
+ section += `**Library:** ${result.sliderLibrary}\n\n`;
97
+ }
98
+
99
+ if (result.viewports) {
100
+ for (const [viewport, vpResult] of Object.entries(result.viewports)) {
101
+ const vpLabel = viewport.charAt(0).toUpperCase() + viewport.slice(1);
102
+ section += `#### ${vpLabel} (${vpResult.dimensions?.width}x${vpResult.dimensions?.height})\n\n`;
103
+
104
+ if (vpResult.tests?.length > 0) {
105
+ for (const test of vpResult.tests) {
106
+ const icon = test.passed ? STATUS_ICONS.pass : STATUS_ICONS.fail;
107
+ section += `- ${icon} **${test.name}**`;
108
+ if (test.selector) section += ` - \`${test.selector}\``;
109
+ if (test.count !== undefined) section += ` (${test.count} found)`;
110
+ if (test.note) section += ` - ${test.note}`;
111
+ if (test.error) section += ` - ⚠️ ${test.error}`;
112
+ section += '\n';
113
+ }
114
+ }
115
+
116
+ if (vpResult.warnings?.length > 0) {
117
+ section += '\n**Warnings:**\n';
118
+ for (const warning of vpResult.warnings) {
119
+ section += `- ${STATUS_ICONS.warn} ${warning}\n`;
120
+ }
121
+ }
122
+ section += '\n';
123
+ }
124
+ }
125
+
126
+ if (result.screenshots?.length > 0) {
127
+ section += `#### Screenshots\n\n| Viewport | Screenshot |\n|----------|------------|\n`;
128
+ for (const screenshot of result.screenshots) {
129
+ const name = path.basename(screenshot);
130
+ const viewport = name.replace(/^[a-z]+-test-/, '').replace('.png', '');
131
+ section += `| ${viewport} | [${name}](${screenshot}) |\n`;
132
+ }
133
+ section += '\n';
134
+ }
135
+
136
+ section += '---\n\n';
137
+ return section;
138
+ }
139
+
140
+ /**
141
+ * Generate the full markdown audit report string
142
+ * @param {Object} results
143
+ * @param {string|undefined} url
144
+ * @returns {string}
145
+ */
146
+ export function generateMarkdownReport(results, url) {
147
+ const timestamp = new Date().toISOString();
148
+ let report = `# Component Audit Report\n\n**Generated:** ${timestamp}\n**URL:** ${url || 'N/A'}\n\n`;
149
+ report += `## Summary\n\n${generateSummaryTable(results)}\n`;
150
+ report += `## Responsive Breakdown\n\n${generateViewportTable(results)}\n`;
151
+ report += `## Component Details\n\n`;
152
+ for (const [component, result] of Object.entries(results)) {
153
+ report += generateComponentSection(component, result);
154
+ }
155
+ report += generateCSSFixes(results);
156
+ report += `---\n\n*Report generated by design-clone verification suite*\n`;
157
+ return report;
158
+ }
@@ -20,7 +20,8 @@
20
20
 
21
21
  import fs from 'fs/promises';
22
22
  import path from 'path';
23
- import { parseArgs, outputJSON, outputError } from '../utils/browser.js';
23
+ import { parseArgs, outputJSON, outputError } from '../utils/helpers.js';
24
+ import { generateMarkdownReport } from './generate-audit-report-sections.js';
24
25
 
25
26
  // Component types and their result files
26
27
  const COMPONENT_FILES = {
@@ -31,16 +32,10 @@ const COMPONENT_FILES = {
31
32
  layout: 'layout-results.json'
32
33
  };
33
34
 
34
- // Status icons
35
- const STATUS_ICONS = {
36
- pass: '✅',
37
- warn: '⚠️',
38
- fail: '❌',
39
- info: 'ℹ️'
40
- };
41
-
42
35
  /**
43
36
  * Load verification results from directory
37
+ * @param {string} dir
38
+ * @returns {Promise<Object>} Map of component name to parsed result or null
44
39
  */
45
40
  async function loadVerificationResults(dir) {
46
41
  const results = {};
@@ -61,264 +56,6 @@ async function loadVerificationResults(dir) {
61
56
  return results;
62
57
  }
63
58
 
64
- /**
65
- * Calculate component status
66
- */
67
- function getComponentStatus(result) {
68
- if (!result) return { status: 'skip', icon: STATUS_ICONS.info, label: 'Not tested' };
69
-
70
- const { summary } = result;
71
- if (!summary) return { status: 'skip', icon: STATUS_ICONS.info, label: 'No data' };
72
-
73
- if (summary.failed > 0) {
74
- return { status: 'fail', icon: STATUS_ICONS.fail, label: `${summary.failed} failed` };
75
- }
76
- if (summary.warnings?.length > 0) {
77
- return { status: 'warn', icon: STATUS_ICONS.warn, label: `${summary.warnings.length} warnings` };
78
- }
79
- return { status: 'pass', icon: STATUS_ICONS.pass, label: 'Passed' };
80
- }
81
-
82
- /**
83
- * Generate summary table
84
- */
85
- function generateSummaryTable(results) {
86
- let table = `| Component | Status | Tests | Details |
87
- |-----------|--------|-------|---------|
88
- `;
89
-
90
- for (const [component, result] of Object.entries(results)) {
91
- const status = getComponentStatus(result);
92
- const tests = result?.summary
93
- ? `${result.summary.passed}/${result.summary.totalTests}`
94
- : '-';
95
- table += `| ${component.charAt(0).toUpperCase() + component.slice(1)} | ${status.icon} ${status.label} | ${tests} | ${result?.url || '-'} |\n`;
96
- }
97
-
98
- return table;
99
- }
100
-
101
- /**
102
- * Generate viewport breakdown table
103
- */
104
- function generateViewportTable(results) {
105
- const viewports = ['mobile', 'tablet', 'desktop'];
106
- const components = Object.keys(results).filter(c => results[c]?.viewports);
107
-
108
- if (components.length === 0) return '';
109
-
110
- let table = `| Component | Mobile | Tablet | Desktop |
111
- |-----------|--------|--------|---------|
112
- `;
113
-
114
- for (const component of components) {
115
- const result = results[component];
116
- const row = [component.charAt(0).toUpperCase() + component.slice(1)];
117
-
118
- for (const vp of viewports) {
119
- const vpResult = result.viewports?.[vp];
120
- if (vpResult) {
121
- const icon = vpResult.failed > 0 ? STATUS_ICONS.fail
122
- : vpResult.warnings?.length > 0 ? STATUS_ICONS.warn
123
- : STATUS_ICONS.pass;
124
- row.push(`${icon} ${vpResult.passed}/${vpResult.tests?.length || 0}`);
125
- } else {
126
- row.push('-');
127
- }
128
- }
129
-
130
- table += `| ${row.join(' | ')} |\n`;
131
- }
132
-
133
- return table;
134
- }
135
-
136
- /**
137
- * Generate component section
138
- */
139
- function generateComponentSection(component, result) {
140
- if (!result) {
141
- return `### ${component.charAt(0).toUpperCase() + component.slice(1)}
142
-
143
- ${STATUS_ICONS.info} Not tested
144
-
145
- ---
146
-
147
- `;
148
- }
149
-
150
- const status = getComponentStatus(result);
151
- let section = `### ${component.charAt(0).toUpperCase() + component.slice(1)} ${status.icon}
152
-
153
- `;
154
-
155
- // Add component-specific info
156
- if (component === 'slider' && result.sliderLibrary) {
157
- section += `**Library:** ${result.sliderLibrary}\n\n`;
158
- }
159
-
160
- // Test results by viewport
161
- if (result.viewports) {
162
- for (const [viewport, vpResult] of Object.entries(result.viewports)) {
163
- section += `#### ${viewport.charAt(0).toUpperCase() + viewport.slice(1)} (${vpResult.dimensions?.width}x${vpResult.dimensions?.height})\n\n`;
164
-
165
- // List tests
166
- if (vpResult.tests?.length > 0) {
167
- for (const test of vpResult.tests) {
168
- const icon = test.passed ? STATUS_ICONS.pass : STATUS_ICONS.fail;
169
- section += `- ${icon} **${test.name}**`;
170
- if (test.selector) section += ` - \`${test.selector}\``;
171
- if (test.count !== undefined) section += ` (${test.count} found)`;
172
- if (test.note) section += ` - ${test.note}`;
173
- if (test.error) section += ` - ⚠️ ${test.error}`;
174
- section += '\n';
175
- }
176
- }
177
-
178
- // Warnings
179
- if (vpResult.warnings?.length > 0) {
180
- section += '\n**Warnings:**\n';
181
- for (const warning of vpResult.warnings) {
182
- section += `- ${STATUS_ICONS.warn} ${warning}\n`;
183
- }
184
- }
185
-
186
- section += '\n';
187
- }
188
- }
189
-
190
- // Screenshots
191
- if (result.screenshots?.length > 0) {
192
- section += `#### Screenshots\n\n`;
193
- section += `| Viewport | Screenshot |\n|----------|------------|\n`;
194
- for (const screenshot of result.screenshots) {
195
- const name = path.basename(screenshot);
196
- const viewport = name.replace(/^[a-z]+-test-/, '').replace('.png', '');
197
- section += `| ${viewport} | [${name}](${screenshot}) |\n`;
198
- }
199
- section += '\n';
200
- }
201
-
202
- section += '---\n\n';
203
- return section;
204
- }
205
-
206
- /**
207
- * Generate CSS fixes section
208
- */
209
- function generateCSSFixes(results) {
210
- const fixes = [];
211
-
212
- // Collect issues that might need CSS fixes
213
- for (const [component, result] of Object.entries(results)) {
214
- if (!result?.viewports) continue;
215
-
216
- for (const [viewport, vpResult] of Object.entries(result.viewports)) {
217
- // Check for layout issues
218
- if (component === 'header' && vpResult.headerHeight) {
219
- // Height consistency check already done in verify-header
220
- }
221
-
222
- if (component === 'footer') {
223
- const positionTest = vpResult.tests?.find(t => t.name === 'Footer at page bottom');
224
- if (positionTest && !positionTest.passed) {
225
- fixes.push({
226
- component,
227
- viewport,
228
- issue: 'Footer not at page bottom',
229
- suggestion: `/* Ensure footer sticks to bottom */
230
- footer {
231
- margin-top: auto;
232
- }
233
- body {
234
- min-height: 100vh;
235
- display: flex;
236
- flex-direction: column;
237
- }`
238
- });
239
- }
240
- }
241
-
242
- // Check warnings
243
- if (vpResult.warnings) {
244
- for (const warning of vpResult.warnings) {
245
- if (warning.includes('z-index')) {
246
- fixes.push({
247
- component,
248
- viewport,
249
- issue: warning,
250
- suggestion: `/* Increase header z-index */
251
- header, .header, [role="banner"] {
252
- z-index: 1000;
253
- }`
254
- });
255
- }
256
- }
257
- }
258
- }
259
- }
260
-
261
- if (fixes.length === 0) return '';
262
-
263
- let section = `## Suggested CSS Fixes
264
-
265
- `;
266
-
267
- for (const fix of fixes) {
268
- section += `### ${fix.component} (${fix.viewport})
269
-
270
- **Issue:** ${fix.issue}
271
-
272
- \`\`\`css
273
- ${fix.suggestion}
274
- \`\`\`
275
-
276
- `;
277
- }
278
-
279
- return section;
280
- }
281
-
282
- /**
283
- * Generate full markdown report
284
- */
285
- function generateMarkdownReport(results, url) {
286
- const timestamp = new Date().toISOString();
287
-
288
- let report = `# Component Audit Report
289
-
290
- **Generated:** ${timestamp}
291
- **URL:** ${url || 'N/A'}
292
-
293
- ## Summary
294
-
295
- ${generateSummaryTable(results)}
296
-
297
- ## Responsive Breakdown
298
-
299
- ${generateViewportTable(results)}
300
-
301
- ## Component Details
302
-
303
- `;
304
-
305
- // Add each component section
306
- for (const [component, result] of Object.entries(results)) {
307
- report += generateComponentSection(component, result);
308
- }
309
-
310
- // Add CSS fixes if any
311
- report += generateCSSFixes(results);
312
-
313
- // Footer
314
- report += `---
315
-
316
- *Report generated by design-clone verification suite*
317
- `;
318
-
319
- return report;
320
- }
321
-
322
59
  /**
323
60
  * Main function
324
61
  */
@@ -336,24 +73,17 @@ async function generateAuditReport() {
336
73
  try {
337
74
  if (verbose) console.error(`\n📊 Generating audit report from ${args.dir}\n`);
338
75
 
339
- // Load all verification results
340
76
  const results = await loadVerificationResults(args.dir);
341
-
342
- // Get URL from first available result
343
77
  const url = Object.values(results).find(r => r?.url)?.url;
344
78
 
345
- // Count loaded components
346
79
  const loadedCount = Object.values(results).filter(r => r !== null).length;
347
80
  if (verbose) console.error(` Loaded ${loadedCount}/${Object.keys(COMPONENT_FILES).length} component results`);
348
81
 
349
- // Generate report
350
82
  const report = generateMarkdownReport(results, url);
351
83
 
352
- // Write report
353
84
  await fs.writeFile(outputPath, report, 'utf-8');
354
85
  if (verbose) console.error(` ✓ Report written to ${outputPath}`);
355
86
 
356
- // Calculate overall stats
357
87
  let totalTests = 0, totalPassed = 0, totalFailed = 0, totalWarnings = 0;
358
88
  for (const result of Object.values(results)) {
359
89
  if (result?.summary) {
@@ -368,13 +98,7 @@ async function generateAuditReport() {
368
98
  success: totalFailed === 0,
369
99
  reportPath: outputPath,
370
100
  url,
371
- summary: {
372
- components: loadedCount,
373
- totalTests,
374
- passed: totalPassed,
375
- failed: totalFailed,
376
- warnings: totalWarnings
377
- }
101
+ summary: { components: loadedCount, totalTests, passed: totalPassed, failed: totalFailed, warnings: totalWarnings }
378
102
  };
379
103
 
380
104
  if (verbose) {
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Output Quality Scorer
3
+ *
4
+ * Scores the quality of a design clone capture on 5 metrics (0-100 scale).
5
+ * Auto-runs for clone-px mode, opt-in for basic clone.
6
+ */
7
+
8
+ import fs from 'fs/promises';
9
+
10
+ const WEIGHTS = {
11
+ cssCoverage: 0.30,
12
+ assetCompleteness: 0.25,
13
+ responsiveFidelity: 0.20,
14
+ htmlSemantics: 0.15,
15
+ accessibility: 0.10
16
+ };
17
+
18
+ const SEMANTIC_TAGS = new Set([
19
+ 'header', 'footer', 'nav', 'main', 'article', 'section', 'aside', 'figure', 'figcaption'
20
+ ]);
21
+
22
+ function scoreCssCoverage(extraction) {
23
+ if (!extraction?.css || !extraction?.filtered) return 0;
24
+ const reduction = parseInt(extraction.filtered.reduction) || 0;
25
+ const kept = 100 - reduction;
26
+ if (kept >= 80) return 100;
27
+ if (kept >= 50) return 80;
28
+ return 60;
29
+ }
30
+
31
+ function scoreAssetCompleteness(stats) {
32
+ if (!stats || stats.total === 0) return 100;
33
+ const ratio = stats.downloaded / stats.total;
34
+ return Math.round(ratio * 100);
35
+ }
36
+
37
+ function scoreResponsiveFidelity(screenshots) {
38
+ if (!screenshots) return 0;
39
+ const count = Array.isArray(screenshots) ? screenshots.length : 0;
40
+ if (count >= 3) return 100;
41
+ if (count >= 2) return 70;
42
+ return 40;
43
+ }
44
+
45
+ function scoreHtmlSemantics(htmlContent) {
46
+ if (!htmlContent) return 0;
47
+ let found = 0;
48
+ for (const tag of SEMANTIC_TAGS) {
49
+ if (htmlContent.includes(`<${tag}`)) found++;
50
+ }
51
+ return Math.min(100, Math.round((found / SEMANTIC_TAGS.size) * 100));
52
+ }
53
+
54
+ function scoreAccessibility(htmlContent) {
55
+ if (!htmlContent) return 0;
56
+ let score = 0;
57
+ const imgCount = (htmlContent.match(/<img/g) || []).length;
58
+ const altCount = (htmlContent.match(/<img[^>]+alt=/g) || []).length;
59
+ if (imgCount === 0) score += 50;
60
+ else score += Math.round((altCount / imgCount) * 50);
61
+ if (htmlContent.includes('<h1')) score += 25;
62
+ if (/<html[^>]+lang=/.test(htmlContent)) score += 25;
63
+ return Math.min(100, score);
64
+ }
65
+
66
+ /**
67
+ * Score the quality of a design clone capture
68
+ * @param {{ extraction: Object, screenshots: Array, assetStats: Object, outputDir: string }} params
69
+ * @returns {Promise<{overall: number, metrics: Object, weights: Object, maxScore: number}>}
70
+ */
71
+ export async function scoreCapture({ extraction, screenshots, assetStats, outputDir }) {
72
+ let htmlContent = '';
73
+ if (extraction?.html?.path) {
74
+ try { htmlContent = await fs.readFile(extraction.html.path, 'utf-8'); } catch { /* ok */ }
75
+ }
76
+
77
+ const metrics = {
78
+ cssCoverage: scoreCssCoverage(extraction),
79
+ assetCompleteness: scoreAssetCompleteness(assetStats),
80
+ responsiveFidelity: scoreResponsiveFidelity(screenshots),
81
+ htmlSemantics: scoreHtmlSemantics(htmlContent),
82
+ accessibility: scoreAccessibility(htmlContent)
83
+ };
84
+
85
+ const overall = Math.round(
86
+ Object.entries(WEIGHTS).reduce(
87
+ (sum, [key, weight]) => sum + (metrics[key] * weight), 0
88
+ )
89
+ );
90
+
91
+ return { overall, metrics, weights: WEIGHTS, maxScore: 100 };
92
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Footer Viewport Checks
3
+ *
4
+ * Orchestrates all per-viewport footer checks using helpers.
5
+ * Separated from verify-footer-helpers.js to keep each file under 200 lines.
6
+ */
7
+
8
+ import {
9
+ FOOTER_SELECTORS,
10
+ findElement,
11
+ countElements,
12
+ countVisibleElements,
13
+ checkFooterPosition,
14
+ checkCopyright
15
+ } from './verify-footer-helpers.js';
16
+
17
+ /**
18
+ * Test footer at a specific viewport — runs all footer checks
19
+ * @param {import('playwright').Page} page
20
+ * @param {string} viewportName
21
+ * @param {Object} VIEWPORTS - Viewport map
22
+ * @param {boolean} verbose
23
+ * @returns {Promise<Object>} Viewport result object
24
+ */
25
+ export async function testFooterViewport(page, viewportName, VIEWPORTS, verbose = false) {
26
+ const viewport = VIEWPORTS[viewportName];
27
+ await page.setViewportSize(viewport);
28
+ await new Promise(r => setTimeout(r, 500));
29
+ await page.evaluate(() => { window.scrollTo(0, document.body.scrollHeight); });
30
+ await new Promise(r => setTimeout(r, 300));
31
+
32
+ const result = { viewport: viewportName, dimensions: viewport, tests: [], passed: 0, failed: 0, warnings: [] };
33
+ if (verbose) console.error(`\n📱 Testing ${viewportName} (${viewport.width}x${viewport.height})...`);
34
+
35
+ const footerResult = await findElement(page, FOOTER_SELECTORS.container);
36
+ if (!footerResult) {
37
+ result.tests.push({ name: 'Footer container exists', passed: false, error: 'No footer container found' });
38
+ result.failed++;
39
+ if (verbose) console.error(` ✗ Footer not found`);
40
+ return result;
41
+ }
42
+
43
+ result.tests.push({ name: 'Footer container exists', passed: true, selector: footerResult.selector });
44
+ result.passed++;
45
+ if (verbose) console.error(` ✓ Footer found: ${footerResult.selector}`);
46
+
47
+ const positionInfo = await checkFooterPosition(page, footerResult.selector);
48
+ if (positionInfo) {
49
+ if (positionInfo.isAtBottom) {
50
+ result.tests.push({ name: 'Footer at page bottom', passed: true, y: positionInfo.y, pageHeight: positionInfo.pageHeight });
51
+ result.passed++;
52
+ if (verbose) console.error(` ✓ Footer at bottom (y: ${Math.round(positionInfo.y)})`);
53
+ } else {
54
+ result.tests.push({ name: 'Footer at page bottom', passed: false, y: positionInfo.y, footerBottom: positionInfo.footerBottom, pageHeight: positionInfo.pageHeight, error: 'Footer not at page bottom' });
55
+ result.failed++;
56
+ if (verbose) console.error(` ✗ Footer not at bottom (gap: ${positionInfo.pageHeight - positionInfo.footerBottom}px)`);
57
+ }
58
+ result.footerDimensions = { height: positionInfo.height, width: positionInfo.width, backgroundColor: positionInfo.backgroundColor, color: positionInfo.color };
59
+ }
60
+
61
+ if (viewportName !== 'mobile') {
62
+ const columns = await countElements(page, FOOTER_SELECTORS.columns);
63
+ if (columns.count >= 1) {
64
+ result.tests.push({ name: 'Multi-column layout', passed: true, count: columns.count, selector: columns.selector, note: columns.count === 1 ? 'Single column layout' : undefined });
65
+ result.passed++;
66
+ if (verbose) console.error(` ✓ ${columns.count} column(s) found`);
67
+ } else {
68
+ result.warnings.push('No clear column structure detected');
69
+ if (verbose) console.error(` ⚠ No column structure detected`);
70
+ }
71
+ }
72
+
73
+ const links = await countVisibleElements(page, FOOTER_SELECTORS.links);
74
+ if (links.count >= 1) {
75
+ result.tests.push({ name: 'Footer links present', passed: true, count: links.count, selector: links.selector });
76
+ result.passed++;
77
+ if (verbose) console.error(` ✓ ${links.count} links found`);
78
+ } else {
79
+ result.warnings.push('No links found in footer');
80
+ if (verbose) console.error(` ⚠ No links found`);
81
+ }
82
+
83
+ const copyrightInfo = await checkCopyright(page);
84
+ if (copyrightInfo && (copyrightInfo.hasCopyright || copyrightInfo.hasYear)) {
85
+ result.tests.push({ name: 'Copyright text present', passed: true, hasCopyright: copyrightInfo.hasCopyright, hasCurrentYear: copyrightInfo.hasCurrentYear });
86
+ result.passed++;
87
+ if (verbose) console.error(` ✓ Copyright found (current year: ${copyrightInfo.hasCurrentYear})`);
88
+ } else {
89
+ result.warnings.push('No copyright text found');
90
+ if (verbose) console.error(` ⚠ No copyright text`);
91
+ }
92
+
93
+ const socialIcons = await countVisibleElements(page, FOOTER_SELECTORS.socialIcons);
94
+ if (socialIcons.count > 0) {
95
+ result.tests.push({ name: 'Social icons present', passed: true, count: socialIcons.count });
96
+ result.passed++;
97
+ if (verbose) console.error(` ✓ ${socialIcons.count} social icons found`);
98
+ } else {
99
+ if (verbose) console.error(` ℹ No social icons found`);
100
+ }
101
+
102
+ return result;
103
+ }