design-clone 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +13 -34
  2. package/SKILL.md +69 -45
  3. package/bin/cli.js +22 -4
  4. package/bin/commands/clone-site.js +31 -171
  5. package/bin/commands/help.js +19 -6
  6. package/bin/commands/init.js +9 -86
  7. package/bin/commands/uninstall.js +105 -0
  8. package/bin/commands/update.js +70 -0
  9. package/bin/commands/verify.js +7 -14
  10. package/bin/utils/paths.js +28 -0
  11. package/bin/utils/validate.js +2 -22
  12. package/bin/utils/version.js +23 -0
  13. package/docs/code-standards.md +789 -0
  14. package/docs/codebase-summary.md +533 -286
  15. package/docs/index.md +74 -0
  16. package/docs/project-overview-pdr.md +797 -0
  17. package/docs/system-architecture.md +718 -0
  18. package/package.json +14 -17
  19. package/src/ai/prompts/design-tokens/basic.md +80 -0
  20. package/src/ai/prompts/design-tokens/section-with-css.md +41 -0
  21. package/src/ai/prompts/design-tokens/section.md +48 -0
  22. package/src/ai/prompts/design-tokens/with-css.md +87 -0
  23. package/src/ai/prompts/structure-analysis/basic.md +55 -0
  24. package/src/ai/prompts/structure-analysis/with-context.md +59 -0
  25. package/src/ai/prompts/structure-analysis/with-dimensions.md +63 -0
  26. package/src/ai/prompts/structure-analysis/with-hierarchy.md +73 -0
  27. package/src/ai/prompts/ux-audit/aggregation.md +42 -0
  28. package/src/ai/prompts/ux-audit/desktop.md +92 -0
  29. package/src/ai/prompts/ux-audit/mobile.md +93 -0
  30. package/src/ai/prompts/ux-audit/tablet.md +92 -0
  31. package/src/core/animation/animation-extractor-ast.js +183 -0
  32. package/src/core/animation/animation-extractor-output.js +152 -0
  33. package/src/core/animation/animation-extractor.js +178 -0
  34. package/src/core/animation/state-capture-detection.js +200 -0
  35. package/src/core/animation/state-capture.js +193 -0
  36. package/src/core/capture/browser-context-pool.js +96 -0
  37. package/src/core/capture/multi-page-screenshot-page.js +110 -0
  38. package/src/core/capture/multi-page-screenshot.js +208 -0
  39. package/src/core/capture/screenshot-extraction.js +186 -0
  40. package/src/core/capture/screenshot-helpers.js +175 -0
  41. package/src/core/capture/screenshot-orchestrator.js +174 -0
  42. package/src/core/capture/screenshot-viewport.js +93 -0
  43. package/src/core/capture/screenshot.js +192 -0
  44. package/src/core/content/content-counter-dom.js +191 -0
  45. package/src/core/content/content-counter.js +76 -0
  46. package/src/core/css/breakpoint-detector.js +66 -0
  47. package/src/core/css/chromium-defaults.json +23 -0
  48. package/src/core/css/computed-style-extractor.js +102 -0
  49. package/src/core/css/css-chunker.js +103 -0
  50. package/src/core/css/filter-css-dead-code.js +120 -0
  51. package/src/core/css/filter-css-html-analyzer.js +110 -0
  52. package/src/core/css/filter-css-selector-matcher.js +172 -0
  53. package/src/core/css/filter-css.js +206 -0
  54. package/src/core/css/merge-css-atrule-processor.js +158 -0
  55. package/src/core/css/merge-css-file-io.js +68 -0
  56. package/src/core/css/merge-css.js +148 -0
  57. package/src/core/detection/framework-detector-routing.js +68 -0
  58. package/src/core/detection/framework-detector-signals.js +65 -0
  59. package/src/core/detection/framework-detector.js +198 -0
  60. package/src/core/dimension/dimension-extractor-card-detector.js +82 -0
  61. package/src/core/dimension/dimension-extractor.js +317 -0
  62. package/src/core/dimension/dimension-output-ai-summary.js +111 -0
  63. package/src/core/dimension/dimension-output.js +173 -0
  64. package/src/core/dimension/dom-tree-analyzer-tree-builders.js +95 -0
  65. package/src/core/dimension/dom-tree-analyzer.js +191 -0
  66. package/src/core/discovery/app-state-snapshot-capture.js +195 -0
  67. package/src/core/discovery/app-state-snapshot-utils.js +178 -0
  68. package/src/core/discovery/app-state-snapshot.js +131 -0
  69. package/src/core/discovery/discover-pages-routes.js +84 -0
  70. package/src/core/discovery/discover-pages-utils.js +177 -0
  71. package/src/core/discovery/discover-pages.js +191 -0
  72. package/src/core/html/html-extractor-inline-styler.js +70 -0
  73. package/src/core/html/html-extractor.js +147 -0
  74. package/src/core/html/semantic-enhancer-mappings.js +200 -0
  75. package/src/core/html/semantic-enhancer-page.js +148 -0
  76. package/src/core/html/semantic-enhancer.js +135 -0
  77. package/src/core/links/rewrite-links-css-rewriter.js +53 -0
  78. package/src/core/links/rewrite-links.js +173 -0
  79. package/src/core/media/asset-validator.js +118 -0
  80. package/src/core/media/extract-assets-downloader.js +187 -0
  81. package/src/core/media/extract-assets-page-scraper.js +115 -0
  82. package/src/core/media/extract-assets.js +159 -0
  83. package/src/core/media/video-capture-convert.js +200 -0
  84. package/src/core/media/video-capture.js +201 -0
  85. package/src/core/{lazy-loader.js → page-prep/lazy-loader.js} +37 -39
  86. package/src/core/section/section-cropper-helpers.js +43 -0
  87. package/src/core/{section-cropper.js → section/section-cropper.js} +11 -88
  88. package/src/core/section/section-detector-strategies.js +139 -0
  89. package/src/core/section/section-detector-utils.js +100 -0
  90. package/src/core/section/section-detector.js +88 -0
  91. package/src/core/tests/test-section-cropper.js +2 -2
  92. package/src/core/tests/test-section-detector.js +2 -2
  93. package/src/post-process/enhance-assets.js +29 -4
  94. package/src/post-process/fetch-images-unsplash-client.js +123 -0
  95. package/src/post-process/fetch-images.js +60 -263
  96. package/src/post-process/inject-gosnap.js +88 -0
  97. package/src/post-process/inject-icons-svg-replacer.js +76 -0
  98. package/src/post-process/inject-icons.js +47 -200
  99. package/src/route-discoverers/base-discoverer-utils.js +137 -0
  100. package/src/route-discoverers/base-discoverer.js +29 -118
  101. package/src/route-discoverers/index.js +1 -1
  102. package/src/shared/config.js +38 -0
  103. package/src/shared/error-codes.js +31 -0
  104. package/src/shared/viewports.js +46 -0
  105. package/src/utils/browser.js +0 -7
  106. package/src/utils/helpers.js +4 -0
  107. package/src/utils/log.js +12 -0
  108. package/src/utils/playwright-loader.js +76 -0
  109. package/src/utils/playwright.js +3 -69
  110. package/src/utils/progress.js +32 -0
  111. package/src/verification/generate-audit-report-css-fixes.js +52 -0
  112. package/src/verification/generate-audit-report-sections.js +158 -0
  113. package/src/verification/generate-audit-report.js +5 -281
  114. package/src/verification/quality-scorer.js +92 -0
  115. package/src/verification/verify-footer-checks.js +103 -0
  116. package/src/verification/verify-footer-helpers.js +178 -0
  117. package/src/verification/verify-footer.js +23 -381
  118. package/src/verification/verify-header-checks.js +104 -0
  119. package/src/verification/verify-header-helpers.js +156 -0
  120. package/src/verification/verify-header.js +23 -365
  121. package/src/verification/verify-layout-report.js +101 -0
  122. package/src/verification/verify-layout.js +13 -259
  123. package/src/verification/verify-menu-checks.js +104 -0
  124. package/src/verification/verify-menu-helpers.js +112 -0
  125. package/src/verification/verify-menu.js +17 -285
  126. package/src/verification/verify-slider-checks.js +115 -0
  127. package/src/verification/verify-slider-constants.js +65 -0
  128. package/src/verification/verify-slider-helpers.js +164 -0
  129. package/src/verification/verify-slider.js +23 -414
  130. package/.env.example +0 -14
  131. package/docs/basic-clone.md +0 -63
  132. package/docs/cli-reference.md +0 -316
  133. package/docs/design-clone-architecture.md +0 -492
  134. package/docs/pixel-perfect.md +0 -117
  135. package/docs/project-roadmap.md +0 -382
  136. package/docs/troubleshooting.md +0 -170
  137. package/requirements.txt +0 -5
  138. package/src/ai/__pycache__/analyze-structure.cpython-313.pyc +0 -0
  139. package/src/ai/__pycache__/extract-design-tokens.cpython-313.pyc +0 -0
  140. package/src/ai/analyze-structure.py +0 -375
  141. package/src/ai/extract-design-tokens.py +0 -782
  142. package/src/ai/prompts/__init__.py +0 -2
  143. package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  144. package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
  145. package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
  146. package/src/ai/prompts/__pycache__/ux_audit.cpython-313.pyc +0 -0
  147. package/src/ai/prompts/design_tokens.py +0 -316
  148. package/src/ai/prompts/structure_analysis.py +0 -592
  149. package/src/ai/prompts/ux_audit.py +0 -198
  150. package/src/ai/ux-audit.js +0 -596
  151. package/src/core/animation-extractor.js +0 -526
  152. package/src/core/app-state-snapshot.js +0 -511
  153. package/src/core/content-counter.js +0 -342
  154. package/src/core/design-tokens.js +0 -103
  155. package/src/core/dimension-extractor.js +0 -438
  156. package/src/core/dimension-output.js +0 -305
  157. package/src/core/discover-pages.js +0 -542
  158. package/src/core/dom-tree-analyzer.js +0 -298
  159. package/src/core/extract-assets.js +0 -468
  160. package/src/core/filter-css.js +0 -499
  161. package/src/core/framework-detector.js +0 -538
  162. package/src/core/html-extractor.js +0 -212
  163. package/src/core/merge-css.js +0 -407
  164. package/src/core/multi-page-screenshot.js +0 -380
  165. package/src/core/rewrite-links.js +0 -226
  166. package/src/core/screenshot.js +0 -701
  167. package/src/core/section-detector.js +0 -386
  168. package/src/core/semantic-enhancer.js +0 -492
  169. package/src/core/state-capture.js +0 -598
  170. package/src/core/video-capture.js +0 -546
  171. package/src/utils/__init__.py +0 -16
  172. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  173. package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
  174. package/src/utils/env.py +0 -134
  175. /package/src/core/{css-extractor.js → css/css-extractor.js} +0 -0
  176. /package/src/core/{cookie-handler.js → page-prep/cookie-handler.js} +0 -0
  177. /package/src/core/{page-readiness.js → page-prep/page-readiness.js} +0 -0
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Component Dimension Extractor
3
+ *
4
+ * Extract exact pixel dimensions from page elements using
5
+ * getBoundingClientRect and getComputedStyle.
6
+ *
7
+ * Split into two page.evaluate passes:
8
+ * 1. Containers, typography, buttons, images (this file)
9
+ * 2. Card patterns + grid layouts (second evaluate using card-detector helpers)
10
+ *
11
+ * Card/grid helper sources live in dimension-extractor-card-detector.js.
12
+ */
13
+
14
+ import { calculateSimilarity, detectLayoutType, calculateGap } from './dimension-extractor-card-detector.js';
15
+
16
+ // Section detection thresholds (passed into browser context)
17
+ const HERO_THRESHOLD = 0.25;
18
+ const FOOTER_THRESHOLD = 0.85;
19
+ const SIDEBAR_MAX_WIDTH = 400;
20
+ const MAX_BUTTONS = 10;
21
+ const MAX_IMAGES = 15;
22
+
23
+ /**
24
+ * Extract component dimensions from page.
25
+ * @param {import('playwright').Page} page
26
+ * @param {string} viewportName - 'desktop' | 'tablet' | 'mobile'
27
+ * @returns {Promise<Object>}
28
+ */
29
+ export async function extractComponentDimensions(page, viewportName) {
30
+ const thresholds = { HERO_THRESHOLD, FOOTER_THRESHOLD, SIDEBAR_MAX_WIDTH, MAX_BUTTONS, MAX_IMAGES };
31
+
32
+ // Pass 1: containers, typography, buttons, images
33
+ const results = await page.evaluate(({ vpName, th }) => {
34
+ const data = {
35
+ viewport: vpName,
36
+ extractedAt: new Date().toISOString(),
37
+ containers: [], cards: [], typography: [], buttons: [], images: []
38
+ };
39
+
40
+ // --- Shared helpers (browser context) ---
41
+
42
+ function extractDimensions(el) {
43
+ const r = el.getBoundingClientRect(), cs = window.getComputedStyle(el);
44
+ const pf = v => parseFloat(v) || 0;
45
+ return {
46
+ width: Math.round(r.width), height: Math.round(r.height),
47
+ x: Math.round(r.x), y: Math.round(r.y),
48
+ absoluteX: Math.round(r.x + window.scrollX), absoluteY: Math.round(r.y + window.scrollY),
49
+ paddingTop: pf(cs.paddingTop), paddingRight: pf(cs.paddingRight),
50
+ paddingBottom: pf(cs.paddingBottom), paddingLeft: pf(cs.paddingLeft),
51
+ marginTop: pf(cs.marginTop), marginRight: pf(cs.marginRight),
52
+ marginBottom: pf(cs.marginBottom), marginLeft: pf(cs.marginLeft),
53
+ display: cs.display, position: cs.position,
54
+ flexDirection: cs.flexDirection !== 'row' ? cs.flexDirection : undefined,
55
+ justifyContent: cs.justifyContent !== 'normal' ? cs.justifyContent : undefined,
56
+ alignItems: cs.alignItems !== 'normal' ? cs.alignItems : undefined,
57
+ gap: pf(cs.gap),
58
+ gridTemplateColumns: cs.gridTemplateColumns !== 'none' ? cs.gridTemplateColumns : undefined,
59
+ gridTemplateRows: cs.gridTemplateRows !== 'none' ? cs.gridTemplateRows : undefined,
60
+ backgroundColor: cs.backgroundColor !== 'rgba(0, 0, 0, 0)' ? cs.backgroundColor : undefined,
61
+ borderRadius: cs.borderRadius !== '0px' ? cs.borderRadius : undefined,
62
+ boxShadow: cs.boxShadow !== 'none' ? cs.boxShadow : undefined,
63
+ fontSize: pf(cs.fontSize), fontWeight: cs.fontWeight, lineHeight: cs.lineHeight,
64
+ letterSpacing: cs.letterSpacing !== 'normal' ? cs.letterSpacing : undefined,
65
+ color: cs.color
66
+ };
67
+ }
68
+
69
+ function cleanObject(obj) {
70
+ return Object.fromEntries(
71
+ Object.entries(obj).filter(([_, v]) => v !== undefined && v !== null && v !== 0 && v !== '')
72
+ );
73
+ }
74
+
75
+ const pageHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
76
+
77
+ function detectSection(el) {
78
+ const r = el.getBoundingClientRect(), cs = window.getComputedStyle(el);
79
+ const tag = el.tagName.toLowerCase(), yr = (r.y + window.scrollY) / pageHeight;
80
+ if (tag === 'header' || el.closest('header')) return 'header';
81
+ if (tag === 'footer' || el.closest('footer')) return 'footer';
82
+ if (tag === 'aside' || el.closest('aside')) return 'sidebar';
83
+ if (tag === 'nav' || el.closest('nav')) return 'nav';
84
+ if (yr < th.HERO_THRESHOLD && r.height > 300) return 'hero';
85
+ if (yr > th.FOOTER_THRESHOLD) return 'footer';
86
+ if ((cs.position === 'fixed' || cs.position === 'sticky') && r.width < th.SIDEBAR_MAX_WIDTH) return 'sidebar';
87
+ return 'content';
88
+ }
89
+
90
+ // --- Extraction functions ---
91
+
92
+ function extractContainers() {
93
+ const selectors = [
94
+ 'section','main','article','header','footer',
95
+ '[role="main"]','[role="region"]',
96
+ 'div[class*="container"]','div[class*="wrapper"]',
97
+ 'div[class*="section"]','div[class*="content"]',
98
+ 'div[class*="grid"]','div[class*="card"]'
99
+ ];
100
+ const seen = new Set();
101
+ selectors.forEach(sel => {
102
+ try {
103
+ document.querySelectorAll(sel).forEach(el => {
104
+ if (seen.has(el)) return;
105
+ const rect = el.getBoundingClientRect();
106
+ if (rect.width < 100 || rect.height < 50) return;
107
+ const children = Array.from(el.children).filter(c => {
108
+ const cr = c.getBoundingClientRect(); return cr.width > 50 && cr.height > 30;
109
+ });
110
+ if (children.length < 2) return;
111
+ seen.add(el);
112
+ const dims = extractDimensions(el);
113
+ dims.selector = el.className
114
+ ? `.${el.className.split(' ').filter(c => c && !c.includes(':')).slice(0, 2).join('.')}`
115
+ : el.tagName.toLowerCase();
116
+ dims.childCount = children.length;
117
+ dims.section = detectSection(el);
118
+ if (children.length >= 2 && (dims.display === 'flex' || dims.display === 'grid')) {
119
+ const fr = children[0].getBoundingClientRect();
120
+ const sr = children[1].getBoundingClientRect();
121
+ const g = Math.round(dims.flexDirection === 'column' ? sr.top - fr.bottom : sr.left - fr.right);
122
+ if (g > 0 && g < 200) dims.calculatedGap = g;
123
+ }
124
+ data.containers.push(cleanObject(dims));
125
+ });
126
+ } catch (e) { /* ignore */ }
127
+ });
128
+ }
129
+
130
+ function extractTypography() {
131
+ ['h1','h2','h3','h4','h5','h6','p'].forEach(tag => {
132
+ try {
133
+ const els = document.querySelectorAll(tag);
134
+ if (!els.length) return;
135
+ const bySection = {};
136
+ for (const el of els) {
137
+ const rect = el.getBoundingClientRect();
138
+ if (rect.width < 50 || rect.height < 10) continue;
139
+ const section = detectSection(el);
140
+ const dims = extractDimensions(el);
141
+ if (!bySection[section]) bySection[section] = [];
142
+ if (bySection[section].length < 2) {
143
+ bySection[section].push({
144
+ selector: tag, section, fontSize: dims.fontSize, fontWeight: dims.fontWeight,
145
+ lineHeight: dims.lineHeight, letterSpacing: dims.letterSpacing,
146
+ color: dims.color, marginTop: dims.marginTop, marginBottom: dims.marginBottom,
147
+ textSample: el.textContent?.trim().slice(0, 40),
148
+ y: Math.round(rect.y + window.scrollY)
149
+ });
150
+ }
151
+ }
152
+ for (const items of Object.values(bySection)) data.typography.push(...items);
153
+ } catch (e) { /* ignore */ }
154
+ });
155
+ data.typography.sort((a, b) => a.y - b.y);
156
+ }
157
+
158
+ function extractButtons() {
159
+ const seen = new Set();
160
+ ['button','a[class*="btn"]','a[class*="button"]','[role="button"]','input[type="submit"]'].forEach(sel => {
161
+ try {
162
+ document.querySelectorAll(sel).forEach(el => {
163
+ if (seen.has(el) || data.buttons.length >= th.MAX_BUTTONS) return;
164
+ const rect = el.getBoundingClientRect();
165
+ if (rect.width < 40 || rect.height < 20) return;
166
+ seen.add(el);
167
+ const dims = extractDimensions(el);
168
+ data.buttons.push({
169
+ width: dims.width, height: dims.height,
170
+ paddingTop: dims.paddingTop, paddingRight: dims.paddingRight,
171
+ paddingBottom: dims.paddingBottom, paddingLeft: dims.paddingLeft,
172
+ fontSize: dims.fontSize, fontWeight: dims.fontWeight,
173
+ borderRadius: dims.borderRadius, backgroundColor: dims.backgroundColor,
174
+ color: dims.color, text: el.textContent?.trim().slice(0, 30)
175
+ });
176
+ });
177
+ } catch (e) { /* ignore */ }
178
+ });
179
+ }
180
+
181
+ function extractImages() {
182
+ try {
183
+ document.querySelectorAll('img').forEach(el => {
184
+ if (data.images.length >= th.MAX_IMAGES) return;
185
+ const rect = el.getBoundingClientRect();
186
+ if (rect.width < 80 || rect.height < 80) return;
187
+ data.images.push({
188
+ width: Math.round(rect.width), height: Math.round(rect.height),
189
+ aspectRatio: (rect.width / rect.height).toFixed(2),
190
+ x: Math.round(rect.x), y: Math.round(rect.y + window.scrollY)
191
+ });
192
+ });
193
+ } catch (e) { /* ignore */ }
194
+ }
195
+
196
+ extractContainers();
197
+ extractTypography();
198
+ extractButtons();
199
+ extractImages();
200
+ return data;
201
+ }, { vpName: viewportName, th: thresholds });
202
+
203
+ // Pass 2: card patterns + grid layouts (using serialized helpers)
204
+ const helpers = {
205
+ calculateSimilarity: calculateSimilarity.toString(),
206
+ detectLayoutType: detectLayoutType.toString(),
207
+ calculateGap: calculateGap.toString()
208
+ };
209
+
210
+ const { cards, gridLayouts } = await page.evaluate(({ containers, helpers }) => {
211
+ // eslint-disable-next-line no-new-func
212
+ const calculateSimilarity = new Function('return (' + helpers.calculateSimilarity + ')')();
213
+ // eslint-disable-next-line no-new-func
214
+ const detectLayoutType = new Function('return (' + helpers.detectLayoutType + ')')();
215
+ // eslint-disable-next-line no-new-func
216
+ const calculateGap = new Function('return (' + helpers.calculateGap + ')')();
217
+
218
+ const cards = [], gridLayouts = [];
219
+ let cardGroupId = 0;
220
+
221
+ containers.forEach(container => {
222
+ // Card patterns
223
+ if (container.childCount >= 2) {
224
+ try {
225
+ const parent = document.querySelector(container.selector);
226
+ if (!parent) return;
227
+ const children = Array.from(parent.children).filter(c => {
228
+ const cr = c.getBoundingClientRect(); return cr.width > 80 && cr.height > 60;
229
+ });
230
+ if (children.length < 2) return;
231
+
232
+ const childDims = children.map(c => {
233
+ const cr = c.getBoundingClientRect(), cs = window.getComputedStyle(c);
234
+ return {
235
+ width: Math.round(cr.width), height: Math.round(cr.height),
236
+ x: Math.round(cr.x), y: Math.round(cr.y),
237
+ paddingTop: parseFloat(cs.paddingTop) || 0,
238
+ paddingRight: parseFloat(cs.paddingRight) || 0,
239
+ paddingBottom: parseFloat(cs.paddingBottom) || 0,
240
+ paddingLeft: parseFloat(cs.paddingLeft) || 0,
241
+ marginTop: parseFloat(cs.marginTop) || 0,
242
+ marginBottom: parseFloat(cs.marginBottom) || 0,
243
+ borderRadius: cs.borderRadius,
244
+ boxShadow: cs.boxShadow !== 'none' ? cs.boxShadow : undefined,
245
+ backgroundColor: cs.backgroundColor !== 'rgba(0, 0, 0, 0)' ? cs.backgroundColor : undefined
246
+ };
247
+ });
248
+
249
+ const used = new Set(), groups = [];
250
+ for (let i = 0; i < childDims.length; i++) {
251
+ if (used.has(i)) continue;
252
+ const group = [childDims[i]]; used.add(i);
253
+ for (let j = i + 1; j < childDims.length; j++) {
254
+ if (!used.has(j) && calculateSimilarity(childDims[i], childDims[j]) >= 0.70) {
255
+ group.push(childDims[j]); used.add(j);
256
+ }
257
+ }
258
+ if (group.length >= 2) groups.push(group);
259
+ }
260
+
261
+ groups.forEach(group => {
262
+ const avg = (arr, key) => Math.round(arr.reduce((s, el) => s + (el[key] || 0), 0) / arr.length);
263
+ const layout = detectLayoutType(group);
264
+ cards.push({
265
+ id: `card-group-${++cardGroupId}`, parentSelector: container.selector,
266
+ count: group.length, layout, gap: calculateGap(group, layout),
267
+ avgDimensions: {
268
+ width: avg(group, 'width'), height: avg(group, 'height'),
269
+ paddingTop: avg(group, 'paddingTop'), paddingRight: avg(group, 'paddingRight'),
270
+ paddingBottom: avg(group, 'paddingBottom'), paddingLeft: avg(group, 'paddingLeft')
271
+ },
272
+ borderRadius: group[0].borderRadius !== '0px' ? group[0].borderRadius : undefined,
273
+ boxShadow: group[0].boxShadow, backgroundColor: group[0].backgroundColor
274
+ });
275
+ });
276
+ } catch (e) { /* ignore */ }
277
+ }
278
+
279
+ // Grid layouts
280
+ if (container.display === 'grid' || container.display === 'flex') {
281
+ try {
282
+ const parent = document.querySelector(container.selector);
283
+ if (!parent) return;
284
+ const cs = window.getComputedStyle(parent);
285
+ if (parent.children.length < 2) return;
286
+
287
+ if (cs.display === 'grid') {
288
+ const cols = cs.gridTemplateColumns;
289
+ const colCount = cols && cols !== 'none'
290
+ ? cols.split(' ').filter(c => c && c !== 'none').length
291
+ : Math.ceil(parent.children.length / 2);
292
+ gridLayouts.push({
293
+ selector: container.selector, display: 'grid',
294
+ columns: colCount, rows: Math.ceil(parent.children.length / colCount),
295
+ columnGap: parseFloat(cs.columnGap) || parseFloat(cs.gap) || 0,
296
+ rowGap: parseFloat(cs.rowGap) || parseFloat(cs.gap) || 0,
297
+ childCount: parent.children.length
298
+ });
299
+ } else if (cs.display === 'flex') {
300
+ gridLayouts.push({
301
+ selector: container.selector, display: 'flex',
302
+ flexDirection: cs.flexDirection, flexWrap: cs.flexWrap,
303
+ gap: parseFloat(cs.gap) || container.calculatedGap || 0,
304
+ childCount: parent.children.length
305
+ });
306
+ }
307
+ } catch (e) { /* ignore */ }
308
+ }
309
+ });
310
+
311
+ return { cards, gridLayouts };
312
+ }, { containers: results.containers, helpers });
313
+
314
+ results.cards = cards;
315
+ results.gridLayouts = gridLayouts;
316
+ return results;
317
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * AI Summary Generator for Dimension Output
3
+ *
4
+ * Generates a compact (<5KB) AI-friendly summary from full component-dimensions.json.
5
+ * Includes section-aware typography, exact measurements, and responsive breakpoints.
6
+ */
7
+
8
+ /**
9
+ * Infer section padding from container data.
10
+ * @param {Array} containers
11
+ * @returns {string} e.g. "64px 0"
12
+ */
13
+ function inferSectionPadding(containers) {
14
+ if (!containers || containers.length === 0) return '64px 0';
15
+ const paddings = containers.slice(0, 5).map(c => ({
16
+ v: c.paddingTop || c.paddingBottom || 64,
17
+ h: c.paddingLeft || c.paddingRight || 0
18
+ }));
19
+ const avgV = Math.round(paddings.reduce((s, p) => s + p.v, 0) / paddings.length);
20
+ const avgH = Math.round(paddings.reduce((s, p) => s + p.h, 0) / paddings.length);
21
+ return `${avgV}px ${avgH}px`;
22
+ }
23
+
24
+ /**
25
+ * Infer card dimensions from card pattern data.
26
+ * @param {Array} cards
27
+ * @returns {{ width: string, height: string, padding: string }}
28
+ */
29
+ function inferCardDimensions(cards) {
30
+ if (!cards || cards.length === 0) {
31
+ return { width: 'auto', height: 'auto', padding: '24px' };
32
+ }
33
+ const first = cards[0].avgDimensions || cards[0];
34
+ return {
35
+ width: first.width ? first.width + 'px' : 'auto',
36
+ height: first.height > 0 ? first.height + 'px' : 'auto',
37
+ padding: (first.paddingTop || first.padding || 24) + 'px'
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Convert typographyBySection to AI-friendly format with px units.
43
+ * Uses desktop size first, then tablet, then mobile.
44
+ * @param {Object} typographyBySection
45
+ * @returns {Object}
46
+ */
47
+ function inferTypographyBySection(typographyBySection) {
48
+ const result = {};
49
+ for (const [section, tags] of Object.entries(typographyBySection || {})) {
50
+ if (!tags || Object.keys(tags).length === 0) continue;
51
+ result[section] = {};
52
+ for (const [tag, sizes] of Object.entries(tags)) {
53
+ const size = sizes.desktop || sizes.tablet || sizes.mobile || 0;
54
+ if (size > 0) result[section][tag] = size + 'px';
55
+ }
56
+ if (Object.keys(result[section]).length === 0) delete result[section];
57
+ }
58
+ return result;
59
+ }
60
+
61
+ /**
62
+ * Generate AI-friendly summary (compact, <5KB).
63
+ * Includes section-aware typography for accurate reconstruction.
64
+ * @param {Object} fullOutput - Full component-dimensions.json
65
+ * @returns {Object} Compact summary for AI prompts
66
+ */
67
+ export function generateAISummary(fullOutput) {
68
+ const { viewports, summary } = fullOutput;
69
+ const desktop = viewports.desktop || {};
70
+
71
+ return {
72
+ _comment: 'USE THESE EXACT VALUES - DO NOT ESTIMATE',
73
+ EXACT_DIMENSIONS: {
74
+ container_max_width: summary.maxContainerWidth + 'px',
75
+ section_padding: inferSectionPadding(desktop.containers),
76
+ card_dimensions: inferCardDimensions(desktop.cards),
77
+ gap: summary.commonGap + 'px'
78
+ },
79
+ EXACT_TYPOGRAPHY: {
80
+ h1: (summary.typography.h1.desktop || 48) + 'px',
81
+ h2: (summary.typography.h2.desktop || 36) + 'px',
82
+ h3: (summary.typography.h3.desktop || 24) + 'px',
83
+ body: (summary.typography.body.desktop || 16) + 'px'
84
+ },
85
+ TYPOGRAPHY_BY_SECTION: inferTypographyBySection(summary.typographyBySection),
86
+ SECTIONS: {
87
+ hero: summary.sections?.hero || { found: false },
88
+ content: summary.sections?.content || { found: false },
89
+ header: summary.sections?.header || { found: false },
90
+ footer: summary.sections?.footer || { found: false },
91
+ sidebar: summary.sections?.sidebar || { found: false }
92
+ },
93
+ RESPONSIVE: {
94
+ desktop_breakpoint: summary.breakpoints.desktop + 'px',
95
+ tablet_breakpoint: summary.breakpoints.tablet + 'px',
96
+ mobile_breakpoint: summary.breakpoints.mobile + 'px',
97
+ typography_scaling: {
98
+ h1: {
99
+ desktop: (summary.typography.h1.desktop || 48) + 'px',
100
+ tablet: (summary.typography.h1.tablet || 36) + 'px',
101
+ mobile: (summary.typography.h1.mobile || 28) + 'px'
102
+ },
103
+ h2: {
104
+ desktop: (summary.typography.h2.desktop || 36) + 'px',
105
+ tablet: (summary.typography.h2.tablet || 28) + 'px',
106
+ mobile: (summary.typography.h2.mobile || 24) + 'px'
107
+ }
108
+ }
109
+ }
110
+ };
111
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Dimension Output Builder
3
+ *
4
+ * Build and format component dimension output for JSON files.
5
+ * Includes sanitization, cross-viewport summary, and AI-friendly format.
6
+ * AI summary generation lives in dimension-output-ai-summary.js.
7
+ */
8
+
9
+ import { VIEWPORTS } from '../../shared/viewports.js';
10
+ export { generateAISummary } from './dimension-output-ai-summary.js';
11
+
12
+ /**
13
+ * Build final component-dimensions.json output with proper schema.
14
+ * @param {Object} allViewportDimensions - Dimensions from all viewports
15
+ * @param {string} url - Source URL
16
+ * @returns {Object} Final JSON structure
17
+ */
18
+ export function buildDimensionsOutput(allViewportDimensions, url) {
19
+ const output = {
20
+ meta: {
21
+ version: '1.0',
22
+ extractedAt: new Date().toISOString(),
23
+ url,
24
+ tool: 'design-clone/screenshot.js'
25
+ },
26
+ viewports: {},
27
+ summary: {}
28
+ };
29
+
30
+ for (const [vpName, vpData] of Object.entries(allViewportDimensions)) {
31
+ output.viewports[vpName] = sanitizeViewportData(vpData, vpName);
32
+ }
33
+
34
+ output.summary = buildCrossViewportSummary(output.viewports);
35
+ return output;
36
+ }
37
+
38
+ /**
39
+ * Sanitize viewport data for JSON output.
40
+ * Rounds numbers, truncates long strings, and limits array sizes.
41
+ * @param {Object} data - Raw viewport dimension data
42
+ * @param {string} vpName - Viewport name key
43
+ * @returns {Object}
44
+ */
45
+ export function sanitizeViewportData(data, vpName) {
46
+ if (!data) return {};
47
+
48
+ const clean = JSON.parse(JSON.stringify(data));
49
+ clean.width = VIEWPORTS[vpName]?.width || 0;
50
+ clean.height = VIEWPORTS[vpName]?.height || 0;
51
+
52
+ function roundNumbers(obj) {
53
+ for (const key in obj) {
54
+ if (typeof obj[key] === 'number') {
55
+ obj[key] = Math.round(obj[key]);
56
+ } else if (Array.isArray(obj[key])) {
57
+ obj[key].forEach(item => roundNumbers(item));
58
+ } else if (typeof obj[key] === 'object' && obj[key] !== null) {
59
+ roundNumbers(obj[key]);
60
+ }
61
+ }
62
+ return obj;
63
+ }
64
+
65
+ function truncateStrings(obj, maxLen = 80) {
66
+ for (const key in obj) {
67
+ if (typeof obj[key] === 'string' && obj[key].length > maxLen) {
68
+ obj[key] = obj[key].slice(0, maxLen) + '...';
69
+ } else if (Array.isArray(obj[key])) {
70
+ obj[key].forEach(item => truncateStrings(item, maxLen));
71
+ } else if (typeof obj[key] === 'object' && obj[key] !== null) {
72
+ truncateStrings(obj[key], maxLen);
73
+ }
74
+ }
75
+ return obj;
76
+ }
77
+
78
+ // Limit array sizes for token efficiency
79
+ if (clean.containers && clean.containers.length > 15) clean.containers = clean.containers.slice(0, 15);
80
+ if (clean.images && clean.images.length > 10) clean.images = clean.images.slice(0, 10);
81
+ if (clean.buttons && clean.buttons.length > 10) clean.buttons = clean.buttons.slice(0, 10);
82
+
83
+ return truncateStrings(roundNumbers(clean));
84
+ }
85
+
86
+ /**
87
+ * Build cross-viewport summary for AI consumption.
88
+ * Includes section-aware typography and container data.
89
+ * @param {Object} viewports - Viewport data keyed by name (desktop, tablet, mobile)
90
+ * @returns {Object} Summary
91
+ */
92
+ export function buildCrossViewportSummary(viewports) {
93
+ const summary = {
94
+ maxContainerWidth: 0,
95
+ commonGap: 0,
96
+ breakpoints: {
97
+ desktop: VIEWPORTS.desktop.width,
98
+ tablet: VIEWPORTS.tablet.width,
99
+ mobile: VIEWPORTS.mobile.width
100
+ },
101
+ typography: { h1: {}, h2: {}, h3: {}, body: {} },
102
+ typographyBySection: { hero: {}, content: {}, header: {}, footer: {}, sidebar: {} },
103
+ cardPatterns: { totalGroups: 0, avgCardSize: null },
104
+ sections: {
105
+ hero: { found: false, containerWidth: null },
106
+ content: { found: false, containerWidth: null },
107
+ header: { found: false, containerWidth: null },
108
+ footer: { found: false, containerWidth: null },
109
+ sidebar: { found: false, width: null }
110
+ }
111
+ };
112
+
113
+ for (const [vpName, vpData] of Object.entries(viewports)) {
114
+ if (!vpData) continue;
115
+
116
+ // Container section mapping
117
+ if (vpData.containers) {
118
+ for (const container of vpData.containers) {
119
+ if (container.width > summary.maxContainerWidth) {
120
+ summary.maxContainerWidth = container.width;
121
+ }
122
+ const section = container.section || 'content';
123
+ if (summary.sections[section]) {
124
+ summary.sections[section].found = true;
125
+ if (section === 'sidebar') {
126
+ if (!summary.sections[section].width || container.width > summary.sections[section].width) {
127
+ summary.sections[section].width = container.width;
128
+ }
129
+ } else {
130
+ if (!summary.sections[section].containerWidth || container.width > summary.sections[section].containerWidth) {
131
+ summary.sections[section].containerWidth = container.width;
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ // Typography by section
139
+ if (vpData.typography) {
140
+ for (const typo of vpData.typography) {
141
+ const tag = typo.selector?.toLowerCase();
142
+ const section = typo.section || 'content';
143
+
144
+ // Flat typography (backward compat)
145
+ if (tag === 'h1' && !summary.typography.h1[vpName]) summary.typography.h1[vpName] = typo.fontSize;
146
+ if (tag === 'h2' && !summary.typography.h2[vpName]) summary.typography.h2[vpName] = typo.fontSize;
147
+ if (tag === 'h3' && !summary.typography.h3[vpName]) summary.typography.h3[vpName] = typo.fontSize;
148
+ if (tag === 'p' && !summary.typography.body[vpName]) summary.typography.body[vpName] = typo.fontSize;
149
+
150
+ // Section-aware typography
151
+ if (!summary.typographyBySection[section]) summary.typographyBySection[section] = {};
152
+ if (!summary.typographyBySection[section][tag]) summary.typographyBySection[section][tag] = {};
153
+ if (!summary.typographyBySection[section][tag][vpName]) {
154
+ summary.typographyBySection[section][tag][vpName] = typo.fontSize;
155
+ }
156
+ }
157
+ }
158
+
159
+ // Card patterns
160
+ if (vpData.cards && vpData.cards.length > 0) {
161
+ summary.cardPatterns.totalGroups += vpData.cards.length;
162
+ if (vpName === 'desktop' && vpData.cards[0]?.avgDimensions) {
163
+ summary.cardPatterns.avgCardSize = vpData.cards[0].avgDimensions;
164
+ }
165
+ const gaps = vpData.cards.map(g => g.gap).filter(g => g > 0);
166
+ if (gaps.length > 0) {
167
+ summary.commonGap = Math.round(gaps.reduce((a, b) => a + b, 0) / gaps.length);
168
+ }
169
+ }
170
+ }
171
+
172
+ return summary;
173
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * DOM Tree Post-Processors
3
+ *
4
+ * Node-side functions that operate on the traversed DOM tree result
5
+ * returned from page.evaluate. Builds the landmarks map and heading
6
+ * tree from the raw root node structure.
7
+ */
8
+
9
+ /**
10
+ * Build a landmarks map from the traversed DOM tree.
11
+ * @param {Object} root - Root node from traverseDOM
12
+ * @returns {{ header, main, footer, nav: Array, aside: Array }}
13
+ */
14
+ export function buildLandmarksMap(root) {
15
+ const landmarks = {
16
+ header: null,
17
+ main: null,
18
+ footer: null,
19
+ nav: [],
20
+ aside: []
21
+ };
22
+
23
+ function walk(node) {
24
+ if (!node) return;
25
+ switch (node.role) {
26
+ case 'header-landmark': landmarks.header = node; break;
27
+ case 'main': landmarks.main = node; break;
28
+ case 'footer-landmark': landmarks.footer = node; break;
29
+ case 'nav': landmarks.nav.push(node); break;
30
+ case 'aside': landmarks.aside.push(node); break;
31
+ }
32
+ node.children.forEach(walk);
33
+ }
34
+
35
+ walk(root);
36
+ return landmarks;
37
+ }
38
+
39
+ /**
40
+ * Build a heading tree with section context and position, sorted by Y.
41
+ * Text and fontSize are filled separately (see extractHeadingData).
42
+ * @param {Object} root - Root node from traverseDOM
43
+ * @returns {Array<{ level, section, nodeId, y, fontSize, text }>}
44
+ */
45
+ export function buildHeadingTree(root) {
46
+ const headings = [];
47
+
48
+ function walk(node, sectionContext) {
49
+ if (!node) return;
50
+
51
+ let ctx = sectionContext;
52
+ if (node.role === 'header-landmark') ctx = 'header';
53
+ else if (node.role === 'main') ctx = 'content';
54
+ else if (node.role === 'footer-landmark') ctx = 'footer';
55
+ else if (node.role === 'aside') ctx = 'sidebar';
56
+ else if (node.role === 'hero') ctx = 'hero';
57
+ if (!ctx) ctx = node.section || 'content';
58
+
59
+ if (node.role?.startsWith('heading-')) {
60
+ headings.push({
61
+ level: parseInt(node.role.slice(-1)),
62
+ section: ctx,
63
+ nodeId: node.id,
64
+ y: node.dimensions.y,
65
+ fontSize: null,
66
+ text: null
67
+ });
68
+ }
69
+
70
+ node.children.forEach(c => walk(c, ctx));
71
+ }
72
+
73
+ walk(root, null);
74
+ return headings.sort((a, b) => a.y - b.y);
75
+ }
76
+
77
+ /**
78
+ * Count total nodes and max depth in the traversed tree.
79
+ * @param {Object} root - Root node
80
+ * @returns {{ totalNodes: number, maxDepth: number }}
81
+ */
82
+ export function countTreeStats(root) {
83
+ let totalNodes = 0;
84
+ let maxActualDepth = 0;
85
+
86
+ function count(n) {
87
+ if (!n) return;
88
+ totalNodes++;
89
+ maxActualDepth = Math.max(maxActualDepth, n.depth);
90
+ n.children.forEach(count);
91
+ }
92
+
93
+ count(root);
94
+ return { totalNodes, maxDepth: maxActualDepth };
95
+ }