design-clone 1.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +32 -39
  2. package/SKILL.md +69 -45
  3. package/bin/cli.js +22 -4
  4. package/bin/commands/clone-site.js +31 -106
  5. package/bin/commands/help.js +19 -6
  6. package/bin/commands/init.js +11 -56
  7. package/bin/commands/uninstall.js +105 -0
  8. package/bin/commands/update.js +70 -0
  9. package/bin/commands/verify.js +11 -16
  10. package/bin/utils/paths.js +28 -0
  11. package/bin/utils/validate.js +24 -28
  12. package/bin/utils/version.js +23 -0
  13. package/docs/code-standards.md +789 -0
  14. package/docs/codebase-summary.md +556 -0
  15. package/docs/index.md +74 -0
  16. package/docs/project-overview-pdr.md +797 -0
  17. package/docs/system-architecture.md +718 -0
  18. package/package.json +20 -21
  19. package/src/ai/prompts/design-tokens/basic.md +80 -0
  20. package/src/ai/prompts/design-tokens/section-with-css.md +41 -0
  21. package/src/ai/prompts/design-tokens/section.md +48 -0
  22. package/src/ai/prompts/design-tokens/with-css.md +87 -0
  23. package/src/ai/prompts/structure-analysis/basic.md +55 -0
  24. package/src/ai/prompts/structure-analysis/with-context.md +59 -0
  25. package/src/ai/prompts/structure-analysis/with-dimensions.md +63 -0
  26. package/src/ai/prompts/structure-analysis/with-hierarchy.md +73 -0
  27. package/src/ai/prompts/ux-audit/aggregation.md +42 -0
  28. package/src/ai/prompts/ux-audit/desktop.md +92 -0
  29. package/src/ai/prompts/ux-audit/mobile.md +93 -0
  30. package/src/ai/prompts/ux-audit/tablet.md +92 -0
  31. package/src/core/animation/animation-extractor-ast.js +183 -0
  32. package/src/core/animation/animation-extractor-output.js +152 -0
  33. package/src/core/animation/animation-extractor.js +178 -0
  34. package/src/core/animation/state-capture-detection.js +200 -0
  35. package/src/core/animation/state-capture.js +193 -0
  36. package/src/core/capture/browser-context-pool.js +96 -0
  37. package/src/core/capture/multi-page-screenshot-page.js +110 -0
  38. package/src/core/capture/multi-page-screenshot.js +208 -0
  39. package/src/core/capture/screenshot-extraction.js +186 -0
  40. package/src/core/capture/screenshot-helpers.js +175 -0
  41. package/src/core/capture/screenshot-orchestrator.js +174 -0
  42. package/src/core/capture/screenshot-viewport.js +93 -0
  43. package/src/core/capture/screenshot.js +192 -0
  44. package/src/core/content/content-counter-dom.js +191 -0
  45. package/src/core/content/content-counter.js +76 -0
  46. package/src/core/css/breakpoint-detector.js +66 -0
  47. package/src/core/css/chromium-defaults.json +23 -0
  48. package/src/core/css/computed-style-extractor.js +102 -0
  49. package/src/core/css/css-chunker.js +103 -0
  50. package/src/core/{css-extractor.js → css/css-extractor.js} +4 -4
  51. package/src/core/css/filter-css-dead-code.js +120 -0
  52. package/src/core/css/filter-css-html-analyzer.js +110 -0
  53. package/src/core/css/filter-css-selector-matcher.js +172 -0
  54. package/src/core/css/filter-css.js +206 -0
  55. package/src/core/css/merge-css-atrule-processor.js +158 -0
  56. package/src/core/css/merge-css-file-io.js +68 -0
  57. package/src/core/css/merge-css.js +148 -0
  58. package/src/core/detection/framework-detector-routing.js +68 -0
  59. package/src/core/detection/framework-detector-signals.js +65 -0
  60. package/src/core/detection/framework-detector.js +198 -0
  61. package/src/core/dimension/dimension-extractor-card-detector.js +82 -0
  62. package/src/core/dimension/dimension-extractor.js +317 -0
  63. package/src/core/dimension/dimension-output-ai-summary.js +111 -0
  64. package/src/core/dimension/dimension-output.js +173 -0
  65. package/src/core/dimension/dom-tree-analyzer-tree-builders.js +95 -0
  66. package/src/core/dimension/dom-tree-analyzer.js +191 -0
  67. package/src/core/discovery/app-state-snapshot-capture.js +195 -0
  68. package/src/core/discovery/app-state-snapshot-utils.js +178 -0
  69. package/src/core/discovery/app-state-snapshot.js +131 -0
  70. package/src/core/discovery/discover-pages-routes.js +84 -0
  71. package/src/core/discovery/discover-pages-utils.js +177 -0
  72. package/src/core/discovery/discover-pages.js +191 -0
  73. package/src/core/html/html-extractor-inline-styler.js +70 -0
  74. package/src/core/html/html-extractor.js +147 -0
  75. package/src/core/html/semantic-enhancer-mappings.js +200 -0
  76. package/src/core/html/semantic-enhancer-page.js +148 -0
  77. package/src/core/html/semantic-enhancer.js +135 -0
  78. package/src/core/links/rewrite-links-css-rewriter.js +53 -0
  79. package/src/core/links/rewrite-links.js +173 -0
  80. package/src/core/media/asset-validator.js +118 -0
  81. package/src/core/media/extract-assets-downloader.js +187 -0
  82. package/src/core/media/extract-assets-page-scraper.js +115 -0
  83. package/src/core/media/extract-assets.js +159 -0
  84. package/src/core/media/video-capture-convert.js +200 -0
  85. package/src/core/media/video-capture.js +201 -0
  86. package/src/core/{cookie-handler.js → page-prep/cookie-handler.js} +1 -1
  87. package/src/core/{lazy-loader.js → page-prep/lazy-loader.js} +44 -46
  88. package/src/core/{page-readiness.js → page-prep/page-readiness.js} +8 -8
  89. package/src/core/section/section-cropper-helpers.js +43 -0
  90. package/src/core/section/section-cropper.js +132 -0
  91. package/src/core/section/section-detector-strategies.js +139 -0
  92. package/src/core/section/section-detector-utils.js +100 -0
  93. package/src/core/section/section-detector.js +88 -0
  94. package/src/core/tests/test-section-cropper.js +177 -0
  95. package/src/core/tests/test-section-detector.js +55 -0
  96. package/src/post-process/enhance-assets.js +29 -4
  97. package/src/post-process/fetch-images-unsplash-client.js +123 -0
  98. package/src/post-process/fetch-images.js +60 -263
  99. package/src/post-process/inject-gosnap.js +88 -0
  100. package/src/post-process/inject-icons-svg-replacer.js +76 -0
  101. package/src/post-process/inject-icons.js +47 -200
  102. package/src/route-discoverers/angular-discoverer.js +157 -0
  103. package/src/route-discoverers/astro-discoverer.js +123 -0
  104. package/src/route-discoverers/base-discoverer-utils.js +137 -0
  105. package/src/route-discoverers/base-discoverer.js +153 -0
  106. package/src/route-discoverers/index.js +106 -0
  107. package/src/route-discoverers/next-discoverer.js +130 -0
  108. package/src/route-discoverers/nuxt-discoverer.js +138 -0
  109. package/src/route-discoverers/react-discoverer.js +139 -0
  110. package/src/route-discoverers/svelte-discoverer.js +109 -0
  111. package/src/route-discoverers/universal-discoverer.js +227 -0
  112. package/src/route-discoverers/vue-discoverer.js +118 -0
  113. package/src/shared/config.js +38 -0
  114. package/src/shared/error-codes.js +31 -0
  115. package/src/shared/viewports.js +46 -0
  116. package/src/utils/browser.js +11 -44
  117. package/src/utils/helpers.js +4 -0
  118. package/src/utils/log.js +12 -0
  119. package/src/utils/playwright-loader.js +76 -0
  120. package/src/utils/playwright.js +147 -0
  121. package/src/utils/progress.js +32 -0
  122. package/src/verification/generate-audit-report-css-fixes.js +52 -0
  123. package/src/verification/generate-audit-report-sections.js +158 -0
  124. package/src/verification/generate-audit-report.js +122 -0
  125. package/src/verification/quality-scorer.js +92 -0
  126. package/src/verification/verify-footer-checks.js +103 -0
  127. package/src/verification/verify-footer-helpers.js +178 -0
  128. package/src/verification/verify-footer.js +135 -0
  129. package/src/verification/verify-header-checks.js +104 -0
  130. package/src/verification/verify-header-helpers.js +156 -0
  131. package/src/verification/verify-header.js +144 -0
  132. package/src/verification/verify-layout-report.js +101 -0
  133. package/src/verification/verify-layout.js +14 -260
  134. package/src/verification/verify-menu-checks.js +104 -0
  135. package/src/verification/verify-menu-helpers.js +112 -0
  136. package/src/verification/verify-menu.js +18 -302
  137. package/src/verification/verify-slider-checks.js +115 -0
  138. package/src/verification/verify-slider-constants.js +65 -0
  139. package/src/verification/verify-slider-helpers.js +164 -0
  140. package/src/verification/verify-slider.js +142 -0
  141. package/.env.example +0 -14
  142. package/docs/basic-clone.md +0 -63
  143. package/docs/cli-reference.md +0 -118
  144. package/docs/design-clone-architecture.md +0 -275
  145. package/docs/pixel-perfect.md +0 -86
  146. package/docs/troubleshooting.md +0 -169
  147. package/requirements.txt +0 -5
  148. package/src/ai/analyze-structure.py +0 -305
  149. package/src/ai/extract-design-tokens.py +0 -439
  150. package/src/ai/prompts/__init__.py +0 -2
  151. package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  152. package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
  153. package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
  154. package/src/ai/prompts/design_tokens.py +0 -183
  155. package/src/ai/prompts/structure_analysis.py +0 -273
  156. package/src/core/animation-extractor.js +0 -526
  157. package/src/core/design-tokens.js +0 -103
  158. package/src/core/dimension-extractor.js +0 -366
  159. package/src/core/dimension-output.js +0 -208
  160. package/src/core/discover-pages.js +0 -314
  161. package/src/core/extract-assets.js +0 -468
  162. package/src/core/filter-css.js +0 -499
  163. package/src/core/html-extractor.js +0 -171
  164. package/src/core/merge-css.js +0 -407
  165. package/src/core/multi-page-screenshot.js +0 -377
  166. package/src/core/rewrite-links.js +0 -226
  167. package/src/core/screenshot.js +0 -572
  168. package/src/core/state-capture.js +0 -602
  169. package/src/core/video-capture.js +0 -540
  170. package/src/utils/__init__.py +0 -16
  171. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  172. package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
  173. package/src/utils/env.py +0 -134
  174. package/src/utils/puppeteer.js +0 -281
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shared viewport configurations for Design Clone
3
+ *
4
+ * Two viewport sets are provided:
5
+ * - VIEWPORTS: Standard capture viewports (1440px desktop)
6
+ * - VIEWPORTS_HD: High-resolution verification viewports (1920px desktop)
7
+ */
8
+
9
+ /**
10
+ * Standard viewport configurations for multi-device capture
11
+ * Used by: screenshot.js, dimension-output.js
12
+ * @type {Object.<string, {width: number, height: number, deviceScaleFactor: number}>}
13
+ */
14
+ export const VIEWPORTS = {
15
+ desktop: { width: 1440, height: 900, deviceScaleFactor: 1 },
16
+ tablet: { width: 768, height: 1024, deviceScaleFactor: 1 },
17
+ mobile: { width: 375, height: 812, deviceScaleFactor: 2 }
18
+ };
19
+
20
+ /**
21
+ * High-resolution viewport configurations for verification
22
+ * Used by: verify-menu.js, verify-layout.js, verify-header.js, verify-footer.js, verify-slider.js
23
+ * @type {Object.<string, {width: number, height: number, deviceScaleFactor: number}>}
24
+ */
25
+ export const VIEWPORTS_HD = {
26
+ desktop: { width: 1920, height: 1080, deviceScaleFactor: 1 },
27
+ tablet: { width: 768, height: 1024, deviceScaleFactor: 1 },
28
+ mobile: { width: 375, height: 812, deviceScaleFactor: 2 }
29
+ };
30
+
31
+ /**
32
+ * UX Audit viewport configurations (no deviceScaleFactor)
33
+ * Used by: UX audit prompt templates
34
+ * @type {Object.<string, {width: number, height: number}>}
35
+ */
36
+ export const VIEWPORTS_UX = {
37
+ desktop: { width: 1920, height: 1080 },
38
+ tablet: { width: 768, height: 1024 },
39
+ mobile: { width: 375, height: 812 }
40
+ };
41
+
42
+ /**
43
+ * Viewport names array for iteration
44
+ * @type {string[]}
45
+ */
46
+ export const VIEWPORT_NAMES = ['desktop', 'tablet', 'mobile'];
@@ -1,32 +1,15 @@
1
1
  /**
2
2
  * Browser abstraction facade for design-clone scripts
3
3
  *
4
- * Auto-detects and uses:
5
- * 1. chrome-devtools skill (if installed) - Preferred
6
- * 2. Standalone puppeteer wrapper - Fallback
4
+ * Uses Playwright wrapper for browser automation.
7
5
  *
8
- * Exports same API regardless of provider:
6
+ * Exports same API:
9
7
  * - getBrowser(options)
10
8
  * - getPage(browser)
11
9
  * - closeBrowser()
12
10
  * - disconnectBrowser()
13
- * - parseArgs(argv)
14
- * - outputJSON(data)
15
- * - outputError(error)
16
11
  */
17
12
 
18
- import fs from 'fs';
19
- import path from 'path';
20
- import { fileURLToPath } from 'url';
21
-
22
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
-
24
- // Chrome DevTools skill path
25
- const CHROME_DEVTOOLS_PATH = path.join(
26
- process.env.HOME,
27
- '.claude/skills/chrome-devtools/scripts/lib/browser.js'
28
- );
29
-
30
13
  let browserModule = null;
31
14
  let providerName = 'unknown';
32
15
 
@@ -36,31 +19,14 @@ let providerName = 'unknown';
36
19
  async function initProvider() {
37
20
  if (browserModule) return;
38
21
 
39
- // Check for chrome-devtools skill
40
- if (fs.existsSync(CHROME_DEVTOOLS_PATH)) {
41
- try {
42
- browserModule = await import(CHROME_DEVTOOLS_PATH);
43
- providerName = 'chrome-devtools';
44
- console.error('[browser] Using chrome-devtools skill');
45
- return;
46
- } catch (e) {
47
- console.error('[browser] chrome-devtools found but failed to load:', e.message);
48
- }
49
- }
50
-
51
- // Fall back to standalone puppeteer wrapper
52
- browserModule = await import('./puppeteer.js');
53
- providerName = 'standalone';
54
- console.error('[browser] Using standalone puppeteer wrapper');
22
+ browserModule = await import('./playwright.js');
23
+ providerName = 'playwright';
24
+ console.error('[browser] Using Playwright wrapper');
55
25
  }
56
26
 
57
- // Import utilities (always use local helpers)
58
- import { parseArgs, outputJSON, outputError } from './helpers.js';
59
- export { parseArgs, outputJSON, outputError };
60
-
61
27
  /**
62
28
  * Get current browser provider name
63
- * @returns {string} 'chrome-devtools' or 'standalone'
29
+ * @returns {string} 'playwright'
64
30
  */
65
31
  export function getProviderName() {
66
32
  return providerName;
@@ -79,15 +45,16 @@ export async function getBrowser(options = {}) {
79
45
  /**
80
46
  * Get page from browser
81
47
  * @param {Browser} browser - Browser instance
48
+ * @param {Object} [options] - Page options
82
49
  * @returns {Promise<Page>} Page instance
83
50
  */
84
- export async function getPage(browser) {
51
+ export async function getPage(browser, options = {}) {
85
52
  await initProvider();
86
- return browserModule.getPage(browser);
53
+ return browserModule.getPage(browser, options);
87
54
  }
88
55
 
89
56
  /**
90
- * Close browser and clear session
57
+ * Close browser
91
58
  */
92
59
  export async function closeBrowser() {
93
60
  await initProvider();
@@ -95,7 +62,7 @@ export async function closeBrowser() {
95
62
  }
96
63
 
97
64
  /**
98
- * Disconnect from browser without closing
65
+ * Disconnect from browser (alias for close in Playwright)
99
66
  */
100
67
  export async function disconnectBrowser() {
101
68
  await initProvider();
@@ -61,10 +61,14 @@ export function outputJSON(data) {
61
61
  export function outputError(error) {
62
62
  const errorMessage = error instanceof Error ? error.message : String(error);
63
63
  const errorStack = error instanceof Error ? error.stack : undefined;
64
+ const isDesignCloneError = error?.name === 'DesignCloneError';
64
65
 
65
66
  console.error(JSON.stringify({
66
67
  success: false,
67
68
  error: errorMessage,
69
+ code: isDesignCloneError ? error.code : undefined,
70
+ suggestion: isDesignCloneError ? error.suggestion : undefined,
71
+ context: isDesignCloneError ? error.context : undefined,
68
72
  stack: process.env.DEBUG ? errorStack : undefined
69
73
  }, null, 2));
70
74
  process.exit(1);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Centralized TTY-aware logging for CLI output.
3
+ * Logs to stderr only when attached to a terminal.
4
+ * Keeps stdout clean for JSON output.
5
+ */
6
+
7
+ const isTTY = process.stderr.isTTY;
8
+
9
+ export function logInfo(msg) { if (isTTY) console.error(`[INFO] ${msg}`); }
10
+ export function logWarn(msg) { if (isTTY) console.error(`[WARN] ${msg}`); }
11
+ export function logError(msg) { if (isTTY) console.error(`[ERROR] ${msg}`); }
12
+ export { isTTY };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Playwright Loader Helpers
3
+ *
4
+ * Chrome path detection and playwright module loading utilities.
5
+ * Extracted from playwright.js to keep each file under 200 lines.
6
+ */
7
+
8
+ import fs from 'fs';
9
+
10
+ /**
11
+ * Detect Chrome executable path by platform
12
+ * Used for playwright-core fallback when full playwright is not installed
13
+ * @returns {string|null} Chrome path or null if not found
14
+ */
15
+ export function detectChromePath() {
16
+ const platform = process.platform;
17
+
18
+ const paths = {
19
+ darwin: [
20
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
21
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
22
+ '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'
23
+ ],
24
+ linux: [
25
+ '/usr/bin/google-chrome',
26
+ '/usr/bin/google-chrome-stable',
27
+ '/usr/bin/chromium',
28
+ '/usr/bin/chromium-browser',
29
+ '/snap/bin/chromium'
30
+ ],
31
+ win32: [
32
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
33
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
34
+ ...(process.env.LOCALAPPDATA
35
+ ? [`${process.env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`]
36
+ : [])
37
+ ]
38
+ };
39
+
40
+ const candidates = paths[platform] || [];
41
+ for (const chromePath of candidates) {
42
+ if (fs.existsSync(chromePath)) {
43
+ return chromePath;
44
+ }
45
+ }
46
+
47
+ return null;
48
+ }
49
+
50
+ /** @type {typeof import('playwright')|null} */
51
+ let playwright = null;
52
+
53
+ /**
54
+ * Load playwright module (try playwright first, then playwright-core)
55
+ * @returns {Promise<Object>} Playwright module with chromium browser type
56
+ * @throws {Error} If neither playwright nor playwright-core is installed
57
+ */
58
+ export async function loadPlaywright() {
59
+ if (playwright) return playwright;
60
+
61
+ try {
62
+ playwright = await import('playwright');
63
+ return playwright;
64
+ } catch (e1) {
65
+ try {
66
+ playwright = await import('playwright-core');
67
+ return playwright;
68
+ } catch (e2) {
69
+ throw new Error(
70
+ 'Playwright not found. Install with: npm install playwright\n' +
71
+ 'Or for smaller install: npm install playwright-core\n' +
72
+ `Details: playwright: ${e1.message}, playwright-core: ${e2.message}`
73
+ );
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Standalone Playwright browser wrapper for design-clone scripts
3
+ * Provides browser automation with Playwright
4
+ *
5
+ * Features:
6
+ * - Auto-detects Chrome installation path (macOS, Linux, Windows)
7
+ * - Fast browser launch (no session persistence needed)
8
+ * - Compatible API with previous Puppeteer wrapper
9
+ */
10
+
11
+ import { VIEWPORTS_HD } from '../shared/viewports.js';
12
+ import { detectChromePath, loadPlaywright } from './playwright-loader.js';
13
+
14
+ /** @type {import('playwright').Browser|null} */
15
+ let browserInstance = null;
16
+ /** @type {import('playwright').Page|null} */
17
+ let pageInstance = null;
18
+
19
+ /** Default viewport dimensions */
20
+ const DEFAULT_VIEWPORT = { width: VIEWPORTS_HD.desktop.width, height: VIEWPORTS_HD.desktop.height };
21
+
22
+ /**
23
+ * Launch browser instance
24
+ *
25
+ * @param {Object} options - Browser options
26
+ * @param {boolean} [options.headless=true] - Run in headless mode
27
+ * @param {Object} [options.viewport] - Default viewport dimensions (applied per context)
28
+ * @param {string} [options.executablePath] - Chrome executable path override
29
+ * @param {string[]} [options.args] - Additional Chrome arguments
30
+ * @returns {Promise<Browser>} Playwright browser instance
31
+ * @throws {Error} If Chrome not found and no executablePath provided (playwright-core)
32
+ */
33
+ export async function getBrowser(options = {}) {
34
+ const pw = await loadPlaywright();
35
+
36
+ // Reuse existing browser if connected
37
+ if (browserInstance && browserInstance.isConnected()) {
38
+ return browserInstance;
39
+ }
40
+
41
+ // Determine executable path for playwright-core
42
+ let executablePath = options.executablePath;
43
+ if (!executablePath) {
44
+ // Check if we're using playwright-core (no bundled browser)
45
+ const isCore = !pw.chromium?.executablePath;
46
+ if (isCore) {
47
+ executablePath = detectChromePath();
48
+ if (!executablePath) {
49
+ throw new Error(
50
+ 'Chrome not found. Either:\n' +
51
+ '1. Install Google Chrome\n' +
52
+ '2. Use full playwright (npm install playwright)\n' +
53
+ '3. Set executablePath option'
54
+ );
55
+ }
56
+ }
57
+ }
58
+
59
+ // Build launch options
60
+ const launchOptions = {
61
+ headless: options.headless !== false,
62
+ args: [
63
+ '--no-sandbox',
64
+ '--disable-setuid-sandbox',
65
+ '--disable-dev-shm-usage',
66
+ ...(options.args || [])
67
+ ]
68
+ };
69
+
70
+ // Only set executablePath if needed (playwright-core or override)
71
+ if (executablePath) {
72
+ launchOptions.executablePath = executablePath;
73
+ }
74
+
75
+ // Launch browser
76
+ browserInstance = await pw.chromium.launch(launchOptions);
77
+ console.error('[browser] Launched Playwright browser');
78
+
79
+ return browserInstance;
80
+ }
81
+
82
+ /**
83
+ * Get current page or create new one
84
+ * Reuses existing page if available
85
+ *
86
+ * @param {import('playwright').Browser} browser - Playwright browser instance
87
+ * @param {Object} [options] - Page options
88
+ * @param {{width: number, height: number}} [options.viewport] - Viewport dimensions
89
+ * @returns {Promise<import('playwright').Page>} Playwright page instance
90
+ * @throws {Error} If browser is null or disconnected
91
+ */
92
+ export async function getPage(browser, options = {}) {
93
+ if (!browser || !browser.isConnected()) {
94
+ throw new Error('Browser not connected. Call getBrowser() first.');
95
+ }
96
+
97
+ if (pageInstance && !pageInstance.isClosed()) {
98
+ return pageInstance;
99
+ }
100
+
101
+ // Get existing pages or create new context + page
102
+ const contexts = browser.contexts();
103
+ if (contexts.length > 0) {
104
+ const pages = contexts[0].pages();
105
+ if (pages.length > 0) {
106
+ pageInstance = pages[0];
107
+ return pageInstance;
108
+ }
109
+ }
110
+
111
+ // Create new context with default viewport
112
+ const contextOptions = {
113
+ viewport: options.viewport || DEFAULT_VIEWPORT
114
+ };
115
+
116
+ const context = await browser.newContext(contextOptions);
117
+ pageInstance = await context.newPage();
118
+
119
+ return pageInstance;
120
+ }
121
+
122
+ /**
123
+ * Close browser
124
+ * Use when completely done with browser
125
+ */
126
+ export async function closeBrowser() {
127
+ if (browserInstance) {
128
+ try {
129
+ await browserInstance.close();
130
+ } catch (err) {
131
+ console.error(`[browser] Error closing browser: ${err.message}`);
132
+ }
133
+ browserInstance = null;
134
+ pageInstance = null;
135
+ console.error('[browser] Closed browser');
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Disconnect from browser (alias for closeBrowser in Playwright)
141
+ * Playwright doesn't support disconnect without close, so this is an alias
142
+ */
143
+ export async function disconnectBrowser() {
144
+ // Playwright doesn't have disconnect concept like Puppeteer
145
+ // Just close the browser for API compatibility
146
+ return closeBrowser();
147
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * TTY-aware progress reporting for extraction/capture pipelines.
3
+ * Writes to stderr only when attached to a terminal.
4
+ * Keeps stdout clean for JSON output.
5
+ */
6
+
7
+ import { isTTY } from './log.js';
8
+
9
+ export class ProgressReporter {
10
+ #current = 0;
11
+ #total = 0;
12
+ #label = '';
13
+
14
+ start(totalSteps, label = '') {
15
+ this.#total = totalSteps;
16
+ this.#current = 0;
17
+ this.#label = label;
18
+ if (isTTY) process.stderr.write(`[0/${totalSteps}] ${label}\n`);
19
+ }
20
+
21
+ step(label, details = '') {
22
+ this.#current++;
23
+ const detailStr = details ? ` (${details})` : '';
24
+ if (isTTY) process.stderr.write(`[${this.#current}/${this.#total}] ${label}${detailStr}\n`);
25
+ }
26
+
27
+ complete(summary = '') {
28
+ if (isTTY) process.stderr.write(`[done] ${summary}\n`);
29
+ }
30
+ }
31
+
32
+ export function createProgress() { return new ProgressReporter(); }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Audit Report CSS Fix Suggestions
3
+ *
4
+ * Analyzes verification results and generates CSS fix suggestions.
5
+ * Extracted from generate-audit-report-sections.js to keep files under 200 lines.
6
+ */
7
+
8
+ /**
9
+ * Generate CSS fixes section markdown from all component results
10
+ * @param {Object} results - Map of component name to verification result
11
+ * @returns {string} Markdown section or empty string if no fixes
12
+ */
13
+ export function generateCSSFixes(results) {
14
+ const fixes = [];
15
+
16
+ for (const [component, result] of Object.entries(results)) {
17
+ if (!result?.viewports) continue;
18
+
19
+ for (const [viewport, vpResult] of Object.entries(result.viewports)) {
20
+ if (component === 'footer') {
21
+ const positionTest = vpResult.tests?.find(t => t.name === 'Footer at page bottom');
22
+ if (positionTest && !positionTest.passed) {
23
+ fixes.push({
24
+ component, viewport,
25
+ issue: 'Footer not at page bottom',
26
+ suggestion: `/* Ensure footer sticks to bottom */\nfooter {\n margin-top: auto;\n}\nbody {\n min-height: 100vh;\n display: flex;\n flex-direction: column;\n}`
27
+ });
28
+ }
29
+ }
30
+
31
+ if (vpResult.warnings) {
32
+ for (const warning of vpResult.warnings) {
33
+ if (warning.includes('z-index')) {
34
+ fixes.push({
35
+ component, viewport,
36
+ issue: warning,
37
+ suggestion: `/* Increase header z-index */\nheader, .header, [role="banner"] {\n z-index: 1000;\n}`
38
+ });
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+
45
+ if (fixes.length === 0) return '';
46
+
47
+ let section = `## Suggested CSS Fixes\n\n`;
48
+ for (const fix of fixes) {
49
+ section += `### ${fix.component} (${fix.viewport})\n\n**Issue:** ${fix.issue}\n\n\`\`\`css\n${fix.suggestion}\n\`\`\`\n\n`;
50
+ }
51
+ return section;
52
+ }
@@ -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
+ }