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
@@ -11,6 +11,7 @@
11
11
 
12
12
  import path from 'path';
13
13
  import fs from 'fs/promises';
14
+ import { validateBounds, sanitizeName } from './section-cropper-helpers.js';
14
15
 
15
16
  // Try to import Sharp
16
17
  let sharp = null;
@@ -21,7 +22,7 @@ try {
21
22
  }
22
23
 
23
24
  // Default configuration
24
- const DEFAULT_OPTIONS = {
25
+ export const DEFAULT_OPTIONS = {
25
26
  minHeight: 100, // Skip sections smaller than this
26
27
  quality: 90, // PNG quality
27
28
  compressionLevel: 6, // PNG compression (0-9)
@@ -34,21 +35,18 @@ const DEFAULT_OPTIONS = {
34
35
  * @param {Array} sections - Array of section objects with bounds
35
36
  * @param {string} outputDir - Base output directory
36
37
  * @param {Object} options - Configuration options
37
- * @returns {Promise<Array>} Array of cropped section info
38
+ * @returns {Promise<{sections: Array, skipped: Array, summary: string, directory: string}>}
38
39
  */
39
40
  export async function cropSections(screenshotPath, sections, outputDir, options = {}) {
40
41
  const config = { ...DEFAULT_OPTIONS, ...options };
41
42
 
42
- // Check Sharp availability
43
43
  if (!sharp) {
44
44
  throw new Error('Sharp is not installed. Run: npm install sharp');
45
45
  }
46
46
 
47
- // Create sections directory
48
47
  const sectionsDir = path.join(outputDir, 'sections');
49
48
  await fs.mkdir(sectionsDir, { recursive: true });
50
49
 
51
- // Get source image metadata
52
50
  const metadata = await sharp(screenshotPath).metadata();
53
51
  const imageWidth = metadata.width;
54
52
  const imageHeight = metadata.height;
@@ -57,47 +55,26 @@ export async function cropSections(screenshotPath, sections, outputDir, options
57
55
  const skipped = [];
58
56
 
59
57
  for (const section of sections) {
60
- // Validate and clamp bounds
61
58
  const bounds = validateBounds(section.bounds, imageWidth, imageHeight);
62
59
 
63
- // Skip tiny sections
64
60
  if (bounds.height < config.minHeight) {
65
- skipped.push({
66
- index: section.index,
67
- name: section.name,
68
- reason: `Height ${bounds.height}px < ${config.minHeight}px minimum`
69
- });
61
+ skipped.push({ index: section.index, name: section.name, reason: `Height ${bounds.height}px < ${config.minHeight}px minimum` });
70
62
  continue;
71
63
  }
72
64
 
73
- // Skip zero-dimension sections
74
65
  if (bounds.width <= 0 || bounds.height <= 0) {
75
- skipped.push({
76
- index: section.index,
77
- name: section.name,
78
- reason: 'Zero or negative dimensions'
79
- });
66
+ skipped.push({ index: section.index, name: section.name, reason: 'Zero or negative dimensions' });
80
67
  continue;
81
68
  }
82
69
 
83
- // Generate output filename
84
70
  const safeName = sanitizeName(section.name);
85
71
  const filename = `section-${section.index}-${safeName}.png`;
86
72
  const outputPath = path.join(sectionsDir, filename);
87
73
 
88
74
  try {
89
- // Crop and save
90
75
  await sharp(screenshotPath)
91
- .extract({
92
- left: bounds.left,
93
- top: bounds.top,
94
- width: bounds.width,
95
- height: bounds.height
96
- })
97
- .png({
98
- quality: config.quality,
99
- compressionLevel: config.compressionLevel
100
- })
76
+ .extract({ left: bounds.left, top: bounds.top, width: bounds.width, height: bounds.height })
77
+ .png({ quality: config.quality, compressionLevel: config.compressionLevel })
101
78
  .toFile(outputPath);
102
79
 
103
80
  results.push({
@@ -106,25 +83,15 @@ export async function cropSections(screenshotPath, sections, outputDir, options
106
83
  filename,
107
84
  path: outputPath,
108
85
  relativePath: path.join('sections', filename),
109
- bounds: {
110
- x: bounds.left,
111
- y: bounds.top,
112
- width: bounds.width,
113
- height: bounds.height
114
- },
86
+ bounds: { x: bounds.left, y: bounds.top, width: bounds.width, height: bounds.height },
115
87
  role: section.role || 'unknown',
116
88
  selector: section.selector || null
117
89
  });
118
90
  } catch (err) {
119
- skipped.push({
120
- index: section.index,
121
- name: section.name,
122
- reason: `Crop error: ${err.message}`
123
- });
91
+ skipped.push({ index: section.index, name: section.name, reason: `Crop error: ${err.message}` });
124
92
  }
125
93
  }
126
94
 
127
- // Write summary JSON
128
95
  const summary = {
129
96
  source: path.basename(screenshotPath),
130
97
  sourceWidth: imageWidth,
@@ -139,49 +106,7 @@ export async function cropSections(screenshotPath, sections, outputDir, options
139
106
  const summaryPath = path.join(sectionsDir, 'sections.json');
140
107
  await fs.writeFile(summaryPath, JSON.stringify(summary, null, 2));
141
108
 
142
- return {
143
- sections: results,
144
- skipped,
145
- summary: summaryPath,
146
- directory: sectionsDir
147
- };
148
- }
149
-
150
- /**
151
- * Validate and clamp bounds to image dimensions
152
- * @param {Object} bounds - Section bounds {x, y, width, height}
153
- * @param {number} imageWidth - Source image width
154
- * @param {number} imageHeight - Source image height
155
- * @returns {Object} Validated bounds {left, top, width, height}
156
- */
157
- function validateBounds(bounds, imageWidth, imageHeight) {
158
- // Clamp starting position
159
- const left = Math.max(0, Math.round(bounds.x));
160
- const top = Math.max(0, Math.round(bounds.y));
161
-
162
- // Calculate max possible dimensions
163
- const maxWidth = imageWidth - left;
164
- const maxHeight = imageHeight - top;
165
-
166
- // Clamp dimensions
167
- const width = Math.min(Math.round(bounds.width), maxWidth);
168
- const height = Math.min(Math.round(bounds.height), maxHeight);
169
-
170
- return { left, top, width, height };
171
- }
172
-
173
- /**
174
- * Sanitize section name for filename
175
- * @param {string} name - Section name
176
- * @returns {string} Safe filename
177
- */
178
- function sanitizeName(name) {
179
- return name
180
- .toLowerCase()
181
- .replace(/[^a-z0-9-]/g, '-')
182
- .replace(/-+/g, '-')
183
- .replace(/^-|-$/g, '')
184
- .substring(0, 50) || 'unnamed';
109
+ return { sections: results, skipped, summary: summaryPath, directory: sectionsDir };
185
110
  }
186
111
 
187
112
  /**
@@ -195,7 +120,7 @@ export function isSharpAvailable() {
195
120
  /**
196
121
  * Get cropper summary for logging
197
122
  * @param {Object} result - Result from cropSections
198
- * @returns {Object} Summary object
123
+ * @returns {Object}
199
124
  */
200
125
  export function getCropperSummary(result) {
201
126
  return {
@@ -205,5 +130,3 @@ export function getCropperSummary(result) {
205
130
  totalSize: result.sections.reduce((sum, s) => sum + (s.bounds.width * s.bounds.height), 0)
206
131
  };
207
132
  }
208
-
209
- export { DEFAULT_OPTIONS };
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Section detection strategies for page analysis.
3
+ *
4
+ * Four progressive strategies: semantic HTML, class patterns,
5
+ * large direct children, viewport chunking fallback.
6
+ * Used by section-detector.js (main orchestrator).
7
+ */
8
+
9
+ import { SECTION_CLASS_PATTERNS } from './section-detector-utils.js';
10
+
11
+ /** @param {Object} rect @param {number} scrollY @returns {Object} */
12
+ function toBounds(rect, scrollY) {
13
+ return { x: Math.round(rect.x), y: Math.round(rect.y + scrollY), width: Math.round(rect.width), height: Math.round(rect.height) };
14
+ }
15
+
16
+ /**
17
+ * Find semantic HTML sections (header, main, section, footer)
18
+ * @param {import('playwright').Page} page
19
+ * @param {Object} pageDimensions
20
+ * @param {Object} config
21
+ * @returns {Promise<Array>}
22
+ */
23
+ export async function findSemanticSections(page, pageDimensions, config) {
24
+ return await page.evaluate(({ minHeight }) => {
25
+ const sections = [];
26
+ const processed = new Set();
27
+ const selectors = [
28
+ 'header:not(header header)', 'main > section', 'main > article',
29
+ 'body > section', 'body > article', '[data-section]', 'footer:not(footer footer)'
30
+ ];
31
+ for (const selector of selectors) {
32
+ for (const el of document.querySelectorAll(selector)) {
33
+ if (processed.has(el)) continue;
34
+ const rect = el.getBoundingClientRect();
35
+ if (rect.height < minHeight) continue;
36
+ let name = el.tagName.toLowerCase();
37
+ if (el.hasAttribute('data-section')) name = el.getAttribute('data-section');
38
+ else if (el.id) name = el.id;
39
+ else if (el.className) {
40
+ const m = el.className.toString().toLowerCase()
41
+ .match(/\b(hero|about|services|features|contact|footer|header|nav|cta|testimonials|pricing|faq|team|blog|news)\b/);
42
+ if (m) name = m[1];
43
+ }
44
+ sections.push({ name, role: el.tagName.toLowerCase(),
45
+ selector: el.id ? `#${el.id}` : el.tagName.toLowerCase(),
46
+ bounds: { x: Math.round(rect.x), y: Math.round(rect.y + window.scrollY), width: Math.round(rect.width), height: Math.round(rect.height) } });
47
+ processed.add(el);
48
+ }
49
+ }
50
+ return sections;
51
+ }, { minHeight: config.minSectionHeight });
52
+ }
53
+
54
+ /**
55
+ * Find sections by class pattern matching
56
+ * @param {import('playwright').Page} page
57
+ * @param {Object} pageDimensions
58
+ * @param {Object} config
59
+ * @returns {Promise<Array>}
60
+ */
61
+ export async function findClassPatternSections(page, pageDimensions, config) {
62
+ return await page.evaluate(({ patterns, minHeight }) => {
63
+ const sections = [];
64
+ const processed = new Set();
65
+ const elements = document.querySelectorAll(patterns.map(p => `[class*="${p}"]`).join(', '));
66
+ for (const el of elements) {
67
+ const parent = el.parentElement;
68
+ if (!parent || (parent.tagName !== 'BODY' && parent.tagName !== 'MAIN')) continue;
69
+ if (processed.has(el)) continue;
70
+ const rect = el.getBoundingClientRect();
71
+ if (rect.height < minHeight) continue;
72
+ const cls = el.className.toString().toLowerCase();
73
+ let name = patterns.find(p => cls.includes(p)) || 'section';
74
+ sections.push({ name, role: 'class-pattern',
75
+ selector: el.id ? `#${el.id}` : `.${el.className.toString().split(' ')[0]}`,
76
+ bounds: { x: Math.round(rect.x), y: Math.round(rect.y + window.scrollY), width: Math.round(rect.width), height: Math.round(rect.height) } });
77
+ processed.add(el);
78
+ }
79
+ return sections;
80
+ }, { patterns: SECTION_CLASS_PATTERNS, minHeight: config.minSectionHeight });
81
+ }
82
+
83
+ /**
84
+ * Find large direct children of main/body as sections
85
+ * @param {import('playwright').Page} page
86
+ * @param {Object} pageDimensions
87
+ * @param {Object} config
88
+ * @returns {Promise<Array>}
89
+ */
90
+ export async function findLargeChildSections(page, pageDimensions, config) {
91
+ return await page.evaluate(({ minHeight }) => {
92
+ const sections = [];
93
+ const skip = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'LINK', 'META'];
94
+ const skipSemantic = ['HEADER', 'FOOTER', 'SECTION', 'ARTICLE'];
95
+ const generic = ['sd', 'container', 'wrapper', 'div', 'block', 'row', 'col', 'section'];
96
+ for (const container of [document.querySelector('main'), document.body].filter(Boolean)) {
97
+ for (const child of container.children) {
98
+ if (skip.includes(child.tagName) || skipSemantic.includes(child.tagName)) continue;
99
+ const rect = child.getBoundingClientRect();
100
+ const absoluteY = rect.y + window.scrollY;
101
+ if (rect.height < Math.max(300, window.innerHeight * 0.2)) continue;
102
+ let name = child.id || '';
103
+ if (!name && child.className) {
104
+ const first = child.className.toString().split(' ')[0].toLowerCase();
105
+ if (!generic.includes(first)) name = first;
106
+ }
107
+ if (!name) {
108
+ const r = absoluteY / (document.body.scrollHeight || 1);
109
+ name = r < 0.15 ? 'top-section' : r < 0.35 ? 'upper-content' : r < 0.55 ? 'middle-content' : r < 0.75 ? 'lower-content' : 'bottom-section';
110
+ name = `${name}-${sections.length}`;
111
+ }
112
+ sections.push({ name: name.toLowerCase().replace(/[^a-z0-9-]/g, '-'), role: 'large-block',
113
+ selector: child.id ? `#${child.id}` : child.tagName.toLowerCase(),
114
+ bounds: { x: Math.round(rect.x), y: Math.round(absoluteY), width: Math.round(rect.width), height: Math.round(rect.height) } });
115
+ }
116
+ if (sections.length > 0 && container.tagName === 'MAIN') break;
117
+ }
118
+ return sections;
119
+ }, { minHeight: config.minSectionHeight });
120
+ }
121
+
122
+ /**
123
+ * Generate viewport chunks as fallback when no sections found.
124
+ * @param {Object} pageDimensions - { width, height }
125
+ * @param {Object} config - { viewportHeight, overlapRatio }
126
+ * @returns {Array}
127
+ */
128
+ export function generateViewportChunks(pageDimensions, config) {
129
+ const { width, height } = pageDimensions;
130
+ const step = config.viewportHeight - Math.round(config.viewportHeight * config.overlapRatio);
131
+ const sections = [];
132
+ let y = 0;
133
+ for (let i = 0; y < height && i < 50; i++) {
134
+ sections.push({ name: `viewport-${i}`, role: 'viewport-chunk', selector: null,
135
+ bounds: { x: 0, y, width, height: Math.min(config.viewportHeight, height - y) } });
136
+ y += step;
137
+ }
138
+ return sections;
139
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Utility helpers and configuration for section detection.
3
+ *
4
+ * Contains SECTION_CLASS_PATTERNS, DEFAULT_OPTIONS, mergeSections (dedup
5
+ * by Y-overlap), applyPadding (clamp bounds to page), and getSectionSummary.
6
+ * Used by section-detector.js and section-detector-strategies.js.
7
+ */
8
+
9
+ // ============================================================================
10
+ // Constants
11
+ // ============================================================================
12
+
13
+ /** Section class patterns to match against element class names */
14
+ export const SECTION_CLASS_PATTERNS = [
15
+ 'hero', 'banner', 'header', 'navigation', 'nav',
16
+ 'services', 'features', 'about', 'team', 'portfolio',
17
+ 'testimonials', 'reviews', 'pricing', 'plans',
18
+ 'faq', 'questions', 'blog', 'news', 'articles',
19
+ 'contact', 'cta', 'call-to-action', 'newsletter',
20
+ 'footer', 'partners', 'clients', 'gallery', 'showcase'
21
+ ];
22
+
23
+ /** Default configuration for section detection */
24
+ export const DEFAULT_OPTIONS = {
25
+ minSections: 3,
26
+ maxSections: 20,
27
+ padding: 40,
28
+ fallbackToViewport: true,
29
+ viewportHeight: 900,
30
+ minSectionHeight: 150,
31
+ overlapRatio: 0.1 // 10% overlap for viewport fallback
32
+ };
33
+
34
+ // ============================================================================
35
+ // Helpers
36
+ // ============================================================================
37
+
38
+ /**
39
+ * Merge sections, removing duplicates based on Y overlap.
40
+ * A new section is skipped if it overlaps >50% with any existing section.
41
+ *
42
+ * @param {Array} existing - Already-accepted sections
43
+ * @param {Array} newSections - Candidates to merge in
44
+ * @returns {Array} Merged deduplicated sections
45
+ */
46
+ export function mergeSections(existing, newSections) {
47
+ const result = [...existing];
48
+
49
+ for (const section of newSections) {
50
+ const overlaps = result.some(s => {
51
+ const yOverlap = Math.max(0,
52
+ Math.min(s.bounds.y + s.bounds.height, section.bounds.y + section.bounds.height) -
53
+ Math.max(s.bounds.y, section.bounds.y)
54
+ );
55
+ const minHeight = Math.min(s.bounds.height, section.bounds.height);
56
+ return yOverlap > minHeight * 0.5; // >50% overlap
57
+ });
58
+
59
+ if (!overlaps) {
60
+ result.push(section);
61
+ }
62
+ }
63
+
64
+ return result;
65
+ }
66
+
67
+ /**
68
+ * Apply padding to bounds, clamping to page dimensions.
69
+ *
70
+ * @param {Object} bounds - { x, y, width, height }
71
+ * @param {number} padding - Pixels to expand on each side
72
+ * @param {Object} pageDimensions - { width, height }
73
+ * @returns {Object} Padded and clamped bounds
74
+ */
75
+ export function applyPadding(bounds, padding, pageDimensions) {
76
+ return {
77
+ x: Math.max(0, bounds.x - padding),
78
+ y: Math.max(0, bounds.y - padding),
79
+ width: Math.min(pageDimensions.width, bounds.width + padding * 2),
80
+ height: Math.min(
81
+ pageDimensions.height - Math.max(0, bounds.y - padding),
82
+ bounds.height + padding * 2
83
+ )
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Get section summary for logging / reporting.
89
+ *
90
+ * @param {Array} sections - Detected sections
91
+ * @returns {Object} Summary with count, names, totalHeight, hasViewportFallback
92
+ */
93
+ export function getSectionSummary(sections) {
94
+ return {
95
+ count: sections.length,
96
+ names: sections.map(s => s.name),
97
+ totalHeight: sections.reduce((sum, s) => sum + s.bounds.height, 0),
98
+ hasViewportFallback: sections.some(s => s.role === 'viewport-chunk')
99
+ };
100
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Section Detector
3
+ *
4
+ * Detect semantic page sections from DOM hierarchy for section-based
5
+ * screenshot analysis. Returns bounding boxes for cropping.
6
+ *
7
+ * Usage:
8
+ * import { detectSections } from './section-detector.js';
9
+ * const sections = await detectSections(page, { padding: 40 });
10
+ *
11
+ * Strategies (in order):
12
+ * 1. Semantic HTML: <header>, <main>, <section>, <footer>
13
+ * 2. data-section attributes
14
+ * 3. Class patterns: hero, services, features, about, contact
15
+ * 4. Large direct children of <main> or <body> (>200px height)
16
+ * 5. Fallback: viewport chunking if <minSections detected
17
+ */
18
+
19
+ import {
20
+ DEFAULT_OPTIONS,
21
+ SECTION_CLASS_PATTERNS,
22
+ mergeSections,
23
+ applyPadding,
24
+ getSectionSummary
25
+ } from './section-detector-utils.js';
26
+
27
+ import {
28
+ findSemanticSections,
29
+ findClassPatternSections,
30
+ findLargeChildSections,
31
+ generateViewportChunks
32
+ } from './section-detector-strategies.js';
33
+
34
+ // Re-export for backward compatibility
35
+ export { DEFAULT_OPTIONS, SECTION_CLASS_PATTERNS, getSectionSummary };
36
+
37
+ /**
38
+ * Detect page sections from DOM hierarchy
39
+ * @param {import('playwright').Page} page - Playwright page instance
40
+ * @param {Object} options - Configuration options
41
+ * @returns {Promise<Array>} Array of section objects with bounds
42
+ */
43
+ export async function detectSections(page, options = {}) {
44
+ const config = { ...DEFAULT_OPTIONS, ...options };
45
+
46
+ const pageDimensions = await page.evaluate(() => ({
47
+ width: document.documentElement.clientWidth,
48
+ height: Math.max(
49
+ document.body.scrollHeight,
50
+ document.documentElement.scrollHeight
51
+ )
52
+ }));
53
+
54
+ // Strategy 1: Semantic HTML sections
55
+ let sections = await findSemanticSections(page, pageDimensions, config);
56
+
57
+ // Strategy 2: Class pattern matching
58
+ if (sections.length < config.minSections) {
59
+ const classSections = await findClassPatternSections(page, pageDimensions, config);
60
+ sections = mergeSections(sections, classSections);
61
+ }
62
+
63
+ // Strategy 3: Large direct children
64
+ if (sections.length < config.minSections) {
65
+ const largeSections = await findLargeChildSections(page, pageDimensions, config);
66
+ sections = mergeSections(sections, largeSections);
67
+ }
68
+
69
+ // Strategy 4: Viewport chunking fallback
70
+ if (sections.length < config.minSections && config.fallbackToViewport) {
71
+ sections = generateViewportChunks(pageDimensions, config);
72
+ }
73
+
74
+ // Apply padding and validate bounds
75
+ sections = sections.map((section, idx) => ({
76
+ ...section,
77
+ index: idx,
78
+ bounds: applyPadding(section.bounds, config.padding, pageDimensions)
79
+ }));
80
+
81
+ // Sort by Y position and limit
82
+ sections = sections
83
+ .sort((a, b) => a.bounds.y - b.bounds.y)
84
+ .slice(0, config.maxSections);
85
+
86
+ // Re-index after sort
87
+ return sections.map((section, idx) => ({ ...section, index: idx }));
88
+ }
@@ -11,8 +11,8 @@ import { chromium } from 'playwright';
11
11
  import path from 'path';
12
12
  import fs from 'fs/promises';
13
13
  import { fileURLToPath } from 'url';
14
- import { detectSections, getSectionSummary } from '../section-detector.js';
15
- import { cropSections, isSharpAvailable, getCropperSummary } from '../section-cropper.js';
14
+ import { detectSections, getSectionSummary } from '../section/section-detector.js';
15
+ import { cropSections, isSharpAvailable, getCropperSummary } from '../section/section-cropper.js';
16
16
 
17
17
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
18
  const projectRoot = path.join(__dirname, '../../..');
@@ -4,8 +4,8 @@
4
4
  * Usage: node src/core/test-section-detector.js [url]
5
5
  */
6
6
 
7
- import { detectSections, getSectionSummary } from './section-detector.js';
8
- import { getBrowser, getPage, closeBrowser } from '../utils/browser.js';
7
+ import { detectSections, getSectionSummary } from '../section/section-detector.js';
8
+ import { getBrowser, getPage, closeBrowser } from '../../utils/browser.js';
9
9
 
10
10
  const url = process.argv[2] || 'https://www.techno-concier.co.jp/';
11
11
 
@@ -19,6 +19,7 @@ import fs from 'fs/promises';
19
19
  import path from 'path';
20
20
  import { fetchImages } from './fetch-images.js';
21
21
  import { injectIcons } from './inject-icons.js';
22
+ import { injectGosnap } from './inject-gosnap.js';
22
23
 
23
24
  /**
24
25
  * Parse command line arguments
@@ -29,7 +30,8 @@ function parseArgs() {
29
30
  outputDir: null,
30
31
  verbose: false,
31
32
  skipImages: false,
32
- skipIcons: false
33
+ skipIcons: false,
34
+ skipGosnap: false
33
35
  };
34
36
 
35
37
  for (let i = 0; i < args.length; i++) {
@@ -40,6 +42,8 @@ function parseArgs() {
40
42
  options.skipImages = true;
41
43
  } else if (arg === '--skip-icons') {
42
44
  options.skipIcons = true;
45
+ } else if (arg === '--skip-gosnap') {
46
+ options.skipGosnap = true;
43
47
  } else if (!arg.startsWith('-')) {
44
48
  options.outputDir = arg;
45
49
  }
@@ -67,7 +71,8 @@ async function enhanceAssets(outputDir, options = {}) {
67
71
  const {
68
72
  verbose = false,
69
73
  skipImages = false,
70
- skipIcons = false
74
+ skipIcons = false,
75
+ skipGosnap = false
71
76
  } = options;
72
77
 
73
78
  const htmlPath = path.join(outputDir, 'index.html');
@@ -87,7 +92,8 @@ async function enhanceAssets(outputDir, options = {}) {
87
92
  const results = {
88
93
  success: true,
89
94
  images: null,
90
- icons: null
95
+ icons: null,
96
+ gosnap: null
91
97
  };
92
98
 
93
99
  // Step 1: Fetch and replace images
@@ -117,6 +123,23 @@ async function enhanceAssets(outputDir, options = {}) {
117
123
  }
118
124
  }
119
125
 
126
+ // Step 3: Inject gosnap-widget
127
+ if (!skipGosnap) {
128
+ const pagesDir = path.join(outputDir, 'pages');
129
+ if (await fileExists(pagesDir)) {
130
+ console.log('Injecting gosnap-widget...');
131
+ try {
132
+ results.gosnap = await injectGosnap(pagesDir, verbose);
133
+ } catch (error) {
134
+ console.warn(` Warning: gosnap injection failed: ${error.message}`);
135
+ results.gosnap = { success: false, error: error.message };
136
+ }
137
+ } else {
138
+ console.log(' -> Skipping gosnap (no pages/ directory)');
139
+ results.gosnap = { skipped: true };
140
+ }
141
+ }
142
+
120
143
  console.log('✅ Asset enhancement complete');
121
144
 
122
145
  return results;
@@ -132,6 +155,7 @@ if (!args.outputDir) {
132
155
  console.error(' --verbose, -v Show detailed progress');
133
156
  console.error(' --skip-images Skip Unsplash image fetching');
134
157
  console.error(' --skip-icons Skip icon injection');
158
+ console.error(' --skip-gosnap Skip gosnap-widget injection');
135
159
  console.error('');
136
160
  console.error('Environment:');
137
161
  console.error(' UNSPLASH_ACCESS_KEY Your Unsplash API key (optional)');
@@ -141,7 +165,8 @@ if (!args.outputDir) {
141
165
  enhanceAssets(args.outputDir, {
142
166
  verbose: args.verbose,
143
167
  skipImages: args.skipImages,
144
- skipIcons: args.skipIcons
168
+ skipIcons: args.skipIcons,
169
+ skipGosnap: args.skipGosnap
145
170
  })
146
171
  .then(result => {
147
172
  if (args.verbose) {