design-clone 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +13 -34
  2. package/SKILL.md +69 -45
  3. package/bin/cli.js +22 -4
  4. package/bin/commands/clone-site.js +31 -171
  5. package/bin/commands/help.js +19 -6
  6. package/bin/commands/init.js +9 -86
  7. package/bin/commands/uninstall.js +105 -0
  8. package/bin/commands/update.js +70 -0
  9. package/bin/commands/verify.js +7 -14
  10. package/bin/utils/paths.js +28 -0
  11. package/bin/utils/validate.js +2 -22
  12. package/bin/utils/version.js +23 -0
  13. package/docs/code-standards.md +789 -0
  14. package/docs/codebase-summary.md +533 -286
  15. package/docs/index.md +74 -0
  16. package/docs/project-overview-pdr.md +797 -0
  17. package/docs/system-architecture.md +718 -0
  18. package/package.json +14 -17
  19. package/src/ai/prompts/design-tokens/basic.md +80 -0
  20. package/src/ai/prompts/design-tokens/section-with-css.md +41 -0
  21. package/src/ai/prompts/design-tokens/section.md +48 -0
  22. package/src/ai/prompts/design-tokens/with-css.md +87 -0
  23. package/src/ai/prompts/structure-analysis/basic.md +55 -0
  24. package/src/ai/prompts/structure-analysis/with-context.md +59 -0
  25. package/src/ai/prompts/structure-analysis/with-dimensions.md +63 -0
  26. package/src/ai/prompts/structure-analysis/with-hierarchy.md +73 -0
  27. package/src/ai/prompts/ux-audit/aggregation.md +42 -0
  28. package/src/ai/prompts/ux-audit/desktop.md +92 -0
  29. package/src/ai/prompts/ux-audit/mobile.md +93 -0
  30. package/src/ai/prompts/ux-audit/tablet.md +92 -0
  31. package/src/core/animation/animation-extractor-ast.js +183 -0
  32. package/src/core/animation/animation-extractor-output.js +152 -0
  33. package/src/core/animation/animation-extractor.js +178 -0
  34. package/src/core/animation/state-capture-detection.js +200 -0
  35. package/src/core/animation/state-capture.js +193 -0
  36. package/src/core/capture/browser-context-pool.js +96 -0
  37. package/src/core/capture/multi-page-screenshot-page.js +110 -0
  38. package/src/core/capture/multi-page-screenshot.js +208 -0
  39. package/src/core/capture/screenshot-extraction.js +186 -0
  40. package/src/core/capture/screenshot-helpers.js +175 -0
  41. package/src/core/capture/screenshot-orchestrator.js +174 -0
  42. package/src/core/capture/screenshot-viewport.js +93 -0
  43. package/src/core/capture/screenshot.js +192 -0
  44. package/src/core/content/content-counter-dom.js +191 -0
  45. package/src/core/content/content-counter.js +76 -0
  46. package/src/core/css/breakpoint-detector.js +66 -0
  47. package/src/core/css/chromium-defaults.json +23 -0
  48. package/src/core/css/computed-style-extractor.js +102 -0
  49. package/src/core/css/css-chunker.js +103 -0
  50. package/src/core/css/filter-css-dead-code.js +120 -0
  51. package/src/core/css/filter-css-html-analyzer.js +110 -0
  52. package/src/core/css/filter-css-selector-matcher.js +172 -0
  53. package/src/core/css/filter-css.js +206 -0
  54. package/src/core/css/merge-css-atrule-processor.js +158 -0
  55. package/src/core/css/merge-css-file-io.js +68 -0
  56. package/src/core/css/merge-css.js +148 -0
  57. package/src/core/detection/framework-detector-routing.js +68 -0
  58. package/src/core/detection/framework-detector-signals.js +65 -0
  59. package/src/core/detection/framework-detector.js +198 -0
  60. package/src/core/dimension/dimension-extractor-card-detector.js +82 -0
  61. package/src/core/dimension/dimension-extractor.js +317 -0
  62. package/src/core/dimension/dimension-output-ai-summary.js +111 -0
  63. package/src/core/dimension/dimension-output.js +173 -0
  64. package/src/core/dimension/dom-tree-analyzer-tree-builders.js +95 -0
  65. package/src/core/dimension/dom-tree-analyzer.js +191 -0
  66. package/src/core/discovery/app-state-snapshot-capture.js +195 -0
  67. package/src/core/discovery/app-state-snapshot-utils.js +178 -0
  68. package/src/core/discovery/app-state-snapshot.js +131 -0
  69. package/src/core/discovery/discover-pages-routes.js +84 -0
  70. package/src/core/discovery/discover-pages-utils.js +177 -0
  71. package/src/core/discovery/discover-pages.js +191 -0
  72. package/src/core/html/html-extractor-inline-styler.js +70 -0
  73. package/src/core/html/html-extractor.js +147 -0
  74. package/src/core/html/semantic-enhancer-mappings.js +200 -0
  75. package/src/core/html/semantic-enhancer-page.js +148 -0
  76. package/src/core/html/semantic-enhancer.js +135 -0
  77. package/src/core/links/rewrite-links-css-rewriter.js +53 -0
  78. package/src/core/links/rewrite-links.js +173 -0
  79. package/src/core/media/asset-validator.js +118 -0
  80. package/src/core/media/extract-assets-downloader.js +187 -0
  81. package/src/core/media/extract-assets-page-scraper.js +115 -0
  82. package/src/core/media/extract-assets.js +159 -0
  83. package/src/core/media/video-capture-convert.js +200 -0
  84. package/src/core/media/video-capture.js +201 -0
  85. package/src/core/{lazy-loader.js → page-prep/lazy-loader.js} +37 -39
  86. package/src/core/section/section-cropper-helpers.js +43 -0
  87. package/src/core/{section-cropper.js → section/section-cropper.js} +11 -88
  88. package/src/core/section/section-detector-strategies.js +139 -0
  89. package/src/core/section/section-detector-utils.js +100 -0
  90. package/src/core/section/section-detector.js +88 -0
  91. package/src/core/tests/test-section-cropper.js +2 -2
  92. package/src/core/tests/test-section-detector.js +2 -2
  93. package/src/post-process/enhance-assets.js +29 -4
  94. package/src/post-process/fetch-images-unsplash-client.js +123 -0
  95. package/src/post-process/fetch-images.js +60 -263
  96. package/src/post-process/inject-gosnap.js +88 -0
  97. package/src/post-process/inject-icons-svg-replacer.js +76 -0
  98. package/src/post-process/inject-icons.js +47 -200
  99. package/src/route-discoverers/base-discoverer-utils.js +137 -0
  100. package/src/route-discoverers/base-discoverer.js +29 -118
  101. package/src/route-discoverers/index.js +1 -1
  102. package/src/shared/config.js +38 -0
  103. package/src/shared/error-codes.js +31 -0
  104. package/src/shared/viewports.js +46 -0
  105. package/src/utils/browser.js +0 -7
  106. package/src/utils/helpers.js +4 -0
  107. package/src/utils/log.js +12 -0
  108. package/src/utils/playwright-loader.js +76 -0
  109. package/src/utils/playwright.js +3 -69
  110. package/src/utils/progress.js +32 -0
  111. package/src/verification/generate-audit-report-css-fixes.js +52 -0
  112. package/src/verification/generate-audit-report-sections.js +158 -0
  113. package/src/verification/generate-audit-report.js +5 -281
  114. package/src/verification/quality-scorer.js +92 -0
  115. package/src/verification/verify-footer-checks.js +103 -0
  116. package/src/verification/verify-footer-helpers.js +178 -0
  117. package/src/verification/verify-footer.js +23 -381
  118. package/src/verification/verify-header-checks.js +104 -0
  119. package/src/verification/verify-header-helpers.js +156 -0
  120. package/src/verification/verify-header.js +23 -365
  121. package/src/verification/verify-layout-report.js +101 -0
  122. package/src/verification/verify-layout.js +13 -259
  123. package/src/verification/verify-menu-checks.js +104 -0
  124. package/src/verification/verify-menu-helpers.js +112 -0
  125. package/src/verification/verify-menu.js +17 -285
  126. package/src/verification/verify-slider-checks.js +115 -0
  127. package/src/verification/verify-slider-constants.js +65 -0
  128. package/src/verification/verify-slider-helpers.js +164 -0
  129. package/src/verification/verify-slider.js +23 -414
  130. package/.env.example +0 -14
  131. package/docs/basic-clone.md +0 -63
  132. package/docs/cli-reference.md +0 -316
  133. package/docs/design-clone-architecture.md +0 -492
  134. package/docs/pixel-perfect.md +0 -117
  135. package/docs/project-roadmap.md +0 -382
  136. package/docs/troubleshooting.md +0 -170
  137. package/requirements.txt +0 -5
  138. package/src/ai/__pycache__/analyze-structure.cpython-313.pyc +0 -0
  139. package/src/ai/__pycache__/extract-design-tokens.cpython-313.pyc +0 -0
  140. package/src/ai/analyze-structure.py +0 -375
  141. package/src/ai/extract-design-tokens.py +0 -782
  142. package/src/ai/prompts/__init__.py +0 -2
  143. package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  144. package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
  145. package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
  146. package/src/ai/prompts/__pycache__/ux_audit.cpython-313.pyc +0 -0
  147. package/src/ai/prompts/design_tokens.py +0 -316
  148. package/src/ai/prompts/structure_analysis.py +0 -592
  149. package/src/ai/prompts/ux_audit.py +0 -198
  150. package/src/ai/ux-audit.js +0 -596
  151. package/src/core/animation-extractor.js +0 -526
  152. package/src/core/app-state-snapshot.js +0 -511
  153. package/src/core/content-counter.js +0 -342
  154. package/src/core/design-tokens.js +0 -103
  155. package/src/core/dimension-extractor.js +0 -438
  156. package/src/core/dimension-output.js +0 -305
  157. package/src/core/discover-pages.js +0 -542
  158. package/src/core/dom-tree-analyzer.js +0 -298
  159. package/src/core/extract-assets.js +0 -468
  160. package/src/core/filter-css.js +0 -499
  161. package/src/core/framework-detector.js +0 -538
  162. package/src/core/html-extractor.js +0 -212
  163. package/src/core/merge-css.js +0 -407
  164. package/src/core/multi-page-screenshot.js +0 -380
  165. package/src/core/rewrite-links.js +0 -226
  166. package/src/core/screenshot.js +0 -701
  167. package/src/core/section-detector.js +0 -386
  168. package/src/core/semantic-enhancer.js +0 -492
  169. package/src/core/state-capture.js +0 -598
  170. package/src/core/video-capture.js +0 -546
  171. package/src/utils/__init__.py +0 -16
  172. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  173. package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
  174. package/src/utils/env.py +0 -134
  175. /package/src/core/{css-extractor.js → css/css-extractor.js} +0 -0
  176. /package/src/core/{cookie-handler.js → page-prep/cookie-handler.js} +0 -0
  177. /package/src/core/{page-readiness.js → page-prep/page-readiness.js} +0 -0
@@ -1,438 +0,0 @@
1
- /**
2
- * Component Dimension Extractor
3
- *
4
- * Extract exact pixel dimensions from page elements using
5
- * getBoundingClientRect and getComputedStyle.
6
- */
7
-
8
- /**
9
- * Extract component dimensions from page
10
- * @param {Page} page - Playwright page
11
- * @param {string} viewportName - 'desktop', 'tablet', or 'mobile'
12
- * @returns {Promise<Object>} Dimension data for this viewport
13
- */
14
- export async function extractComponentDimensions(page, viewportName) {
15
- return await page.evaluate((vpName) => {
16
- const results = {
17
- viewport: vpName,
18
- extractedAt: new Date().toISOString(),
19
- containers: [],
20
- cards: [],
21
- typography: [],
22
- buttons: [],
23
- images: []
24
- };
25
-
26
- // Helper: extract dimensions from element
27
- function extractDimensions(el) {
28
- const rect = el.getBoundingClientRect();
29
- const computed = window.getComputedStyle(el);
30
-
31
- return {
32
- width: Math.round(rect.width),
33
- height: Math.round(rect.height),
34
- x: Math.round(rect.x),
35
- y: Math.round(rect.y),
36
- absoluteX: Math.round(rect.x + window.scrollX),
37
- absoluteY: Math.round(rect.y + window.scrollY),
38
- paddingTop: parseFloat(computed.paddingTop) || 0,
39
- paddingRight: parseFloat(computed.paddingRight) || 0,
40
- paddingBottom: parseFloat(computed.paddingBottom) || 0,
41
- paddingLeft: parseFloat(computed.paddingLeft) || 0,
42
- marginTop: parseFloat(computed.marginTop) || 0,
43
- marginRight: parseFloat(computed.marginRight) || 0,
44
- marginBottom: parseFloat(computed.marginBottom) || 0,
45
- marginLeft: parseFloat(computed.marginLeft) || 0,
46
- display: computed.display,
47
- position: computed.position,
48
- flexDirection: computed.flexDirection !== 'row' ? computed.flexDirection : undefined,
49
- justifyContent: computed.justifyContent !== 'normal' ? computed.justifyContent : undefined,
50
- alignItems: computed.alignItems !== 'normal' ? computed.alignItems : undefined,
51
- gap: parseFloat(computed.gap) || 0,
52
- gridTemplateColumns: computed.gridTemplateColumns !== 'none' ? computed.gridTemplateColumns : undefined,
53
- gridTemplateRows: computed.gridTemplateRows !== 'none' ? computed.gridTemplateRows : undefined,
54
- backgroundColor: computed.backgroundColor !== 'rgba(0, 0, 0, 0)' ? computed.backgroundColor : undefined,
55
- borderRadius: computed.borderRadius !== '0px' ? computed.borderRadius : undefined,
56
- boxShadow: computed.boxShadow !== 'none' ? computed.boxShadow : undefined,
57
- fontSize: parseFloat(computed.fontSize) || 0,
58
- fontWeight: computed.fontWeight,
59
- lineHeight: computed.lineHeight,
60
- letterSpacing: computed.letterSpacing !== 'normal' ? computed.letterSpacing : undefined,
61
- color: computed.color
62
- };
63
- }
64
-
65
- // Helper: clean object by removing undefined/null values
66
- function cleanObject(obj) {
67
- return Object.fromEntries(
68
- Object.entries(obj).filter(([_, v]) => v !== undefined && v !== null && v !== 0 && v !== '')
69
- );
70
- }
71
-
72
- /**
73
- * Section Detection Configuration
74
- * These thresholds determine how elements are classified into page sections.
75
- * Semantic tags (<header>, <footer>, etc.) always take priority over position.
76
- * Position-based detection is used as fallback for non-semantic elements.
77
- */
78
- const HERO_THRESHOLD = 0.25; // Elements in top 25% with height >300px → hero
79
- const FOOTER_THRESHOLD = 0.85; // Elements below 85% of page height → footer
80
- const SIDEBAR_MAX_WIDTH = 400; // Max px width for fixed/sticky sidebar detection
81
-
82
- // Page dimensions for section context
83
- const pageHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
84
- const pageWidth = document.documentElement.clientWidth;
85
-
86
- /**
87
- * Detect section context for an element.
88
- *
89
- * Priority order (per validation decision):
90
- * 1. Semantic HTML tags: <header>, <footer>, <aside>, <nav> (highest priority)
91
- * 2. Ancestor semantic tags: element inside <header>, <footer>, etc.
92
- * 3. Position-based heuristics: hero (top 25%), footer (bottom 15%)
93
- * 4. Layout-based: fixed/sticky narrow elements → sidebar
94
- * 5. Default: 'content'
95
- *
96
- * @param {Element} el - DOM element to classify
97
- * @returns {string} Section context: 'header' | 'hero' | 'content' | 'sidebar' | 'footer' | 'nav'
98
- */
99
- function detectSection(el) {
100
- const rect = el.getBoundingClientRect();
101
- const computed = window.getComputedStyle(el);
102
- const yRatio = (rect.y + window.scrollY) / pageHeight;
103
-
104
- // Semantic tags have priority (per validation decision)
105
- const tag = el.tagName.toLowerCase();
106
- if (tag === 'header' || el.closest('header')) return 'header';
107
- if (tag === 'footer' || el.closest('footer')) return 'footer';
108
- if (tag === 'aside' || el.closest('aside')) return 'sidebar';
109
- if (tag === 'nav' || el.closest('nav')) return 'nav';
110
-
111
- // Hero detection (large element in top 25%)
112
- if (yRatio < HERO_THRESHOLD && rect.height > 300) return 'hero';
113
-
114
- // Footer detection (bottom 15%)
115
- if (yRatio > FOOTER_THRESHOLD) return 'footer';
116
-
117
- // Sidebar detection (narrow fixed/sticky)
118
- if ((computed.position === 'fixed' || computed.position === 'sticky') && rect.width < SIDEBAR_MAX_WIDTH) {
119
- return 'sidebar';
120
- }
121
-
122
- return 'content';
123
- }
124
-
125
- // 1. Extract containers
126
- const containerSelectors = [
127
- 'section', 'main', 'article', 'header', 'footer',
128
- '[role="main"]', '[role="region"]',
129
- 'div[class*="container"]', 'div[class*="wrapper"]',
130
- 'div[class*="section"]', 'div[class*="content"]',
131
- 'div[class*="grid"]', 'div[class*="card"]'
132
- ];
133
-
134
- const seenContainers = new Set();
135
- containerSelectors.forEach(selector => {
136
- try {
137
- document.querySelectorAll(selector).forEach((el) => {
138
- if (seenContainers.has(el)) return;
139
- const rect = el.getBoundingClientRect();
140
- if (rect.width < 100 || rect.height < 50) return;
141
-
142
- const children = Array.from(el.children).filter(c => {
143
- const cr = c.getBoundingClientRect();
144
- return cr.width > 50 && cr.height > 30;
145
- });
146
-
147
- if (children.length >= 2) {
148
- seenContainers.add(el);
149
- const dims = extractDimensions(el);
150
- dims.selector = el.className
151
- ? `.${el.className.split(' ').filter(c => c && !c.includes(':')).slice(0, 2).join('.')}`
152
- : el.tagName.toLowerCase();
153
- dims.childCount = children.length;
154
- dims.section = detectSection(el); // Add section context
155
-
156
- if (children.length >= 2 && (dims.display === 'flex' || dims.display === 'grid')) {
157
- const firstRect = children[0].getBoundingClientRect();
158
- const secondRect = children[1].getBoundingClientRect();
159
- const calculatedGap = Math.round(
160
- dims.flexDirection === 'column'
161
- ? secondRect.top - firstRect.bottom
162
- : secondRect.left - firstRect.right
163
- );
164
- if (calculatedGap > 0 && calculatedGap < 200) {
165
- dims.calculatedGap = calculatedGap;
166
- }
167
- }
168
-
169
- results.containers.push(cleanObject(dims));
170
- }
171
- });
172
- } catch (e) { /* ignore */ }
173
- });
174
-
175
- // 2. Extract typography (grouped by section context)
176
- const typographySelectors = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'];
177
- typographySelectors.forEach(tag => {
178
- try {
179
- const elements = document.querySelectorAll(tag);
180
- if (elements.length === 0) return;
181
-
182
- const bySection = {}; // Group by section
183
-
184
- for (const el of elements) {
185
- const rect = el.getBoundingClientRect();
186
- if (rect.width < 50 || rect.height < 10) continue;
187
-
188
- const section = detectSection(el);
189
- const dims = extractDimensions(el);
190
-
191
- // Create section group if not exists
192
- if (!bySection[section]) bySection[section] = [];
193
-
194
- // Add to section group (limit 2 per section per tag for token efficiency)
195
- if (bySection[section].length < 2) {
196
- bySection[section].push({
197
- selector: tag,
198
- section,
199
- fontSize: dims.fontSize,
200
- fontWeight: dims.fontWeight,
201
- lineHeight: dims.lineHeight,
202
- letterSpacing: dims.letterSpacing,
203
- color: dims.color,
204
- marginTop: dims.marginTop,
205
- marginBottom: dims.marginBottom,
206
- textSample: el.textContent?.trim().slice(0, 40),
207
- y: Math.round(rect.y + window.scrollY)
208
- });
209
- }
210
- }
211
-
212
- // Flatten section groups into typography array
213
- for (const items of Object.values(bySection)) {
214
- results.typography.push(...items);
215
- }
216
- } catch (e) { /* ignore */ }
217
- });
218
-
219
- // Sort typography by position for consistent output
220
- results.typography.sort((a, b) => a.y - b.y);
221
-
222
- // 3. Extract buttons
223
- const buttonSelectors = [
224
- 'button', 'a[class*="btn"]', 'a[class*="button"]',
225
- '[role="button"]', 'input[type="submit"]'
226
- ];
227
- const seenButtons = new Set();
228
- buttonSelectors.forEach(selector => {
229
- try {
230
- document.querySelectorAll(selector).forEach((el) => {
231
- if (seenButtons.has(el)) return;
232
- const rect = el.getBoundingClientRect();
233
- if (rect.width < 40 || rect.height < 20) return;
234
- if (results.buttons.length >= 10) return;
235
-
236
- seenButtons.add(el);
237
- const dims = extractDimensions(el);
238
- results.buttons.push({
239
- width: dims.width,
240
- height: dims.height,
241
- paddingTop: dims.paddingTop,
242
- paddingRight: dims.paddingRight,
243
- paddingBottom: dims.paddingBottom,
244
- paddingLeft: dims.paddingLeft,
245
- fontSize: dims.fontSize,
246
- fontWeight: dims.fontWeight,
247
- borderRadius: dims.borderRadius,
248
- backgroundColor: dims.backgroundColor,
249
- color: dims.color,
250
- text: el.textContent?.trim().slice(0, 30)
251
- });
252
- });
253
- } catch (e) { /* ignore */ }
254
- });
255
-
256
- // 4. Extract images
257
- try {
258
- document.querySelectorAll('img').forEach((el) => {
259
- const rect = el.getBoundingClientRect();
260
- if (rect.width < 80 || rect.height < 80) return;
261
- if (results.images.length >= 15) return;
262
-
263
- results.images.push({
264
- width: Math.round(rect.width),
265
- height: Math.round(rect.height),
266
- aspectRatio: (rect.width / rect.height).toFixed(2),
267
- x: Math.round(rect.x),
268
- y: Math.round(rect.y + window.scrollY)
269
- });
270
- });
271
- } catch (e) { /* ignore */ }
272
-
273
- // 5. Card pattern detection
274
- function calculateSimilarity(a, b) {
275
- const widthSim = 1 - Math.abs(a.width - b.width) / Math.max(a.width, b.width, 1);
276
- const heightSim = 1 - Math.abs(a.height - b.height) / Math.max(a.height, b.height, 1);
277
- const marginA = a.marginTop + a.marginBottom;
278
- const marginB = b.marginTop + b.marginBottom;
279
- const marginSim = 1 - Math.abs(marginA - marginB) / Math.max(marginA, marginB, 1);
280
- const radiusSim = a.borderRadius === b.borderRadius ? 1 : 0.5;
281
- return (widthSim * 0.4) + (heightSim * 0.3) + (marginSim * 0.15) + (radiusSim * 0.15);
282
- }
283
-
284
- function detectLayoutType(elements) {
285
- if (elements.length < 2) return 'single';
286
- const yPositions = elements.map(el => el.y);
287
- const xPositions = elements.map(el => el.x);
288
- const yVariance = Math.max(...yPositions) - Math.min(...yPositions);
289
- const xVariance = Math.max(...xPositions) - Math.min(...xPositions);
290
- const avgHeight = elements.reduce((s, el) => s + el.height, 0) / elements.length;
291
- const avgWidth = elements.reduce((s, el) => s + el.width, 0) / elements.length;
292
-
293
- if (yVariance < avgHeight * 0.3 && xVariance > avgWidth) return 'row';
294
- if (xVariance < avgWidth * 0.3 && yVariance > avgHeight) return 'column';
295
- return 'grid';
296
- }
297
-
298
- function calculateGap(elements, layout) {
299
- if (elements.length < 2) return 0;
300
- const sorted = layout === 'column'
301
- ? [...elements].sort((a, b) => a.y - b.y)
302
- : [...elements].sort((a, b) => a.x - b.x);
303
-
304
- let totalGap = 0, gapCount = 0;
305
- for (let i = 1; i < sorted.length; i++) {
306
- const gap = layout === 'column'
307
- ? sorted[i].y - (sorted[i-1].y + sorted[i-1].height)
308
- : sorted[i].x - (sorted[i-1].x + sorted[i-1].width);
309
- if (gap > 0 && gap < 200) { totalGap += gap; gapCount++; }
310
- }
311
- return gapCount > 0 ? Math.round(totalGap / gapCount) : 0;
312
- }
313
-
314
- let cardGroupId = 0;
315
- results.containers.forEach(container => {
316
- if (container.childCount >= 2) {
317
- try {
318
- const parent = document.querySelector(container.selector);
319
- if (!parent) return;
320
-
321
- const children = Array.from(parent.children).filter(c => {
322
- const cr = c.getBoundingClientRect();
323
- return cr.width > 80 && cr.height > 60;
324
- });
325
-
326
- if (children.length >= 2) {
327
- const childDims = children.map(c => {
328
- const cr = c.getBoundingClientRect();
329
- const cs = window.getComputedStyle(c);
330
- return {
331
- width: Math.round(cr.width),
332
- height: Math.round(cr.height),
333
- x: Math.round(cr.x),
334
- y: Math.round(cr.y),
335
- paddingTop: parseFloat(cs.paddingTop) || 0,
336
- paddingRight: parseFloat(cs.paddingRight) || 0,
337
- paddingBottom: parseFloat(cs.paddingBottom) || 0,
338
- paddingLeft: parseFloat(cs.paddingLeft) || 0,
339
- marginTop: parseFloat(cs.marginTop) || 0,
340
- marginBottom: parseFloat(cs.marginBottom) || 0,
341
- borderRadius: cs.borderRadius,
342
- boxShadow: cs.boxShadow !== 'none' ? cs.boxShadow : undefined,
343
- backgroundColor: cs.backgroundColor !== 'rgba(0, 0, 0, 0)' ? cs.backgroundColor : undefined
344
- };
345
- });
346
-
347
- const used = new Set();
348
- const groups = [];
349
-
350
- for (let i = 0; i < childDims.length; i++) {
351
- if (used.has(i)) continue;
352
- const group = [childDims[i]];
353
- used.add(i);
354
-
355
- for (let j = i + 1; j < childDims.length; j++) {
356
- if (used.has(j)) continue;
357
- if (calculateSimilarity(childDims[i], childDims[j]) >= 0.70) {
358
- group.push(childDims[j]);
359
- used.add(j);
360
- }
361
- }
362
-
363
- if (group.length >= 2) groups.push(group);
364
- }
365
-
366
- groups.forEach(group => {
367
- const avg = (arr, key) => Math.round(arr.reduce((s, el) => s + (el[key] || 0), 0) / arr.length);
368
- const layout = detectLayoutType(group);
369
- const gap = calculateGap(group, layout);
370
-
371
- results.cards.push({
372
- id: `card-group-${++cardGroupId}`,
373
- parentSelector: container.selector,
374
- count: group.length,
375
- layout,
376
- gap,
377
- avgDimensions: {
378
- width: avg(group, 'width'),
379
- height: avg(group, 'height'),
380
- paddingTop: avg(group, 'paddingTop'),
381
- paddingRight: avg(group, 'paddingRight'),
382
- paddingBottom: avg(group, 'paddingBottom'),
383
- paddingLeft: avg(group, 'paddingLeft')
384
- },
385
- borderRadius: group[0].borderRadius !== '0px' ? group[0].borderRadius : undefined,
386
- boxShadow: group[0].boxShadow,
387
- backgroundColor: group[0].backgroundColor
388
- });
389
- });
390
- }
391
- } catch (e) { /* ignore */ }
392
- }
393
- });
394
-
395
- // 6. Grid layouts
396
- results.gridLayouts = [];
397
- results.containers.forEach(container => {
398
- if (container.display === 'grid' || container.display === 'flex') {
399
- try {
400
- const parent = document.querySelector(container.selector);
401
- if (!parent) return;
402
- const computed = window.getComputedStyle(parent);
403
- const children = parent.children;
404
-
405
- if (children.length >= 2) {
406
- if (computed.display === 'grid') {
407
- const columns = computed.gridTemplateColumns;
408
- const colCount = columns && columns !== 'none'
409
- ? columns.split(' ').filter(c => c && c !== 'none').length
410
- : Math.ceil(children.length / 2);
411
-
412
- results.gridLayouts.push({
413
- selector: container.selector,
414
- display: 'grid',
415
- columns: colCount,
416
- rows: Math.ceil(children.length / colCount),
417
- columnGap: parseFloat(computed.columnGap) || parseFloat(computed.gap) || 0,
418
- rowGap: parseFloat(computed.rowGap) || parseFloat(computed.gap) || 0,
419
- childCount: children.length
420
- });
421
- } else if (computed.display === 'flex') {
422
- results.gridLayouts.push({
423
- selector: container.selector,
424
- display: 'flex',
425
- flexDirection: computed.flexDirection,
426
- flexWrap: computed.flexWrap,
427
- gap: parseFloat(computed.gap) || container.calculatedGap || 0,
428
- childCount: children.length
429
- });
430
- }
431
- }
432
- } catch (e) { /* ignore */ }
433
- }
434
- });
435
-
436
- return results;
437
- }, viewportName);
438
- }