design-clone 1.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +32 -39
  2. package/SKILL.md +69 -45
  3. package/bin/cli.js +22 -4
  4. package/bin/commands/clone-site.js +31 -106
  5. package/bin/commands/help.js +19 -6
  6. package/bin/commands/init.js +11 -56
  7. package/bin/commands/uninstall.js +105 -0
  8. package/bin/commands/update.js +70 -0
  9. package/bin/commands/verify.js +11 -16
  10. package/bin/utils/paths.js +28 -0
  11. package/bin/utils/validate.js +24 -28
  12. package/bin/utils/version.js +23 -0
  13. package/docs/code-standards.md +789 -0
  14. package/docs/codebase-summary.md +556 -0
  15. package/docs/index.md +74 -0
  16. package/docs/project-overview-pdr.md +797 -0
  17. package/docs/system-architecture.md +718 -0
  18. package/package.json +20 -21
  19. package/src/ai/prompts/design-tokens/basic.md +80 -0
  20. package/src/ai/prompts/design-tokens/section-with-css.md +41 -0
  21. package/src/ai/prompts/design-tokens/section.md +48 -0
  22. package/src/ai/prompts/design-tokens/with-css.md +87 -0
  23. package/src/ai/prompts/structure-analysis/basic.md +55 -0
  24. package/src/ai/prompts/structure-analysis/with-context.md +59 -0
  25. package/src/ai/prompts/structure-analysis/with-dimensions.md +63 -0
  26. package/src/ai/prompts/structure-analysis/with-hierarchy.md +73 -0
  27. package/src/ai/prompts/ux-audit/aggregation.md +42 -0
  28. package/src/ai/prompts/ux-audit/desktop.md +92 -0
  29. package/src/ai/prompts/ux-audit/mobile.md +93 -0
  30. package/src/ai/prompts/ux-audit/tablet.md +92 -0
  31. package/src/core/animation/animation-extractor-ast.js +183 -0
  32. package/src/core/animation/animation-extractor-output.js +152 -0
  33. package/src/core/animation/animation-extractor.js +178 -0
  34. package/src/core/animation/state-capture-detection.js +200 -0
  35. package/src/core/animation/state-capture.js +193 -0
  36. package/src/core/capture/browser-context-pool.js +96 -0
  37. package/src/core/capture/multi-page-screenshot-page.js +110 -0
  38. package/src/core/capture/multi-page-screenshot.js +208 -0
  39. package/src/core/capture/screenshot-extraction.js +186 -0
  40. package/src/core/capture/screenshot-helpers.js +175 -0
  41. package/src/core/capture/screenshot-orchestrator.js +174 -0
  42. package/src/core/capture/screenshot-viewport.js +93 -0
  43. package/src/core/capture/screenshot.js +192 -0
  44. package/src/core/content/content-counter-dom.js +191 -0
  45. package/src/core/content/content-counter.js +76 -0
  46. package/src/core/css/breakpoint-detector.js +66 -0
  47. package/src/core/css/chromium-defaults.json +23 -0
  48. package/src/core/css/computed-style-extractor.js +102 -0
  49. package/src/core/css/css-chunker.js +103 -0
  50. package/src/core/{css-extractor.js → css/css-extractor.js} +4 -4
  51. package/src/core/css/filter-css-dead-code.js +120 -0
  52. package/src/core/css/filter-css-html-analyzer.js +110 -0
  53. package/src/core/css/filter-css-selector-matcher.js +172 -0
  54. package/src/core/css/filter-css.js +206 -0
  55. package/src/core/css/merge-css-atrule-processor.js +158 -0
  56. package/src/core/css/merge-css-file-io.js +68 -0
  57. package/src/core/css/merge-css.js +148 -0
  58. package/src/core/detection/framework-detector-routing.js +68 -0
  59. package/src/core/detection/framework-detector-signals.js +65 -0
  60. package/src/core/detection/framework-detector.js +198 -0
  61. package/src/core/dimension/dimension-extractor-card-detector.js +82 -0
  62. package/src/core/dimension/dimension-extractor.js +317 -0
  63. package/src/core/dimension/dimension-output-ai-summary.js +111 -0
  64. package/src/core/dimension/dimension-output.js +173 -0
  65. package/src/core/dimension/dom-tree-analyzer-tree-builders.js +95 -0
  66. package/src/core/dimension/dom-tree-analyzer.js +191 -0
  67. package/src/core/discovery/app-state-snapshot-capture.js +195 -0
  68. package/src/core/discovery/app-state-snapshot-utils.js +178 -0
  69. package/src/core/discovery/app-state-snapshot.js +131 -0
  70. package/src/core/discovery/discover-pages-routes.js +84 -0
  71. package/src/core/discovery/discover-pages-utils.js +177 -0
  72. package/src/core/discovery/discover-pages.js +191 -0
  73. package/src/core/html/html-extractor-inline-styler.js +70 -0
  74. package/src/core/html/html-extractor.js +147 -0
  75. package/src/core/html/semantic-enhancer-mappings.js +200 -0
  76. package/src/core/html/semantic-enhancer-page.js +148 -0
  77. package/src/core/html/semantic-enhancer.js +135 -0
  78. package/src/core/links/rewrite-links-css-rewriter.js +53 -0
  79. package/src/core/links/rewrite-links.js +173 -0
  80. package/src/core/media/asset-validator.js +118 -0
  81. package/src/core/media/extract-assets-downloader.js +187 -0
  82. package/src/core/media/extract-assets-page-scraper.js +115 -0
  83. package/src/core/media/extract-assets.js +159 -0
  84. package/src/core/media/video-capture-convert.js +200 -0
  85. package/src/core/media/video-capture.js +201 -0
  86. package/src/core/{cookie-handler.js → page-prep/cookie-handler.js} +1 -1
  87. package/src/core/{lazy-loader.js → page-prep/lazy-loader.js} +44 -46
  88. package/src/core/{page-readiness.js → page-prep/page-readiness.js} +8 -8
  89. package/src/core/section/section-cropper-helpers.js +43 -0
  90. package/src/core/section/section-cropper.js +132 -0
  91. package/src/core/section/section-detector-strategies.js +139 -0
  92. package/src/core/section/section-detector-utils.js +100 -0
  93. package/src/core/section/section-detector.js +88 -0
  94. package/src/core/tests/test-section-cropper.js +177 -0
  95. package/src/core/tests/test-section-detector.js +55 -0
  96. package/src/post-process/enhance-assets.js +29 -4
  97. package/src/post-process/fetch-images-unsplash-client.js +123 -0
  98. package/src/post-process/fetch-images.js +60 -263
  99. package/src/post-process/inject-gosnap.js +88 -0
  100. package/src/post-process/inject-icons-svg-replacer.js +76 -0
  101. package/src/post-process/inject-icons.js +47 -200
  102. package/src/route-discoverers/angular-discoverer.js +157 -0
  103. package/src/route-discoverers/astro-discoverer.js +123 -0
  104. package/src/route-discoverers/base-discoverer-utils.js +137 -0
  105. package/src/route-discoverers/base-discoverer.js +153 -0
  106. package/src/route-discoverers/index.js +106 -0
  107. package/src/route-discoverers/next-discoverer.js +130 -0
  108. package/src/route-discoverers/nuxt-discoverer.js +138 -0
  109. package/src/route-discoverers/react-discoverer.js +139 -0
  110. package/src/route-discoverers/svelte-discoverer.js +109 -0
  111. package/src/route-discoverers/universal-discoverer.js +227 -0
  112. package/src/route-discoverers/vue-discoverer.js +118 -0
  113. package/src/shared/config.js +38 -0
  114. package/src/shared/error-codes.js +31 -0
  115. package/src/shared/viewports.js +46 -0
  116. package/src/utils/browser.js +11 -44
  117. package/src/utils/helpers.js +4 -0
  118. package/src/utils/log.js +12 -0
  119. package/src/utils/playwright-loader.js +76 -0
  120. package/src/utils/playwright.js +147 -0
  121. package/src/utils/progress.js +32 -0
  122. package/src/verification/generate-audit-report-css-fixes.js +52 -0
  123. package/src/verification/generate-audit-report-sections.js +158 -0
  124. package/src/verification/generate-audit-report.js +122 -0
  125. package/src/verification/quality-scorer.js +92 -0
  126. package/src/verification/verify-footer-checks.js +103 -0
  127. package/src/verification/verify-footer-helpers.js +178 -0
  128. package/src/verification/verify-footer.js +135 -0
  129. package/src/verification/verify-header-checks.js +104 -0
  130. package/src/verification/verify-header-helpers.js +156 -0
  131. package/src/verification/verify-header.js +144 -0
  132. package/src/verification/verify-layout-report.js +101 -0
  133. package/src/verification/verify-layout.js +14 -260
  134. package/src/verification/verify-menu-checks.js +104 -0
  135. package/src/verification/verify-menu-helpers.js +112 -0
  136. package/src/verification/verify-menu.js +18 -302
  137. package/src/verification/verify-slider-checks.js +115 -0
  138. package/src/verification/verify-slider-constants.js +65 -0
  139. package/src/verification/verify-slider-helpers.js +164 -0
  140. package/src/verification/verify-slider.js +142 -0
  141. package/.env.example +0 -14
  142. package/docs/basic-clone.md +0 -63
  143. package/docs/cli-reference.md +0 -118
  144. package/docs/design-clone-architecture.md +0 -275
  145. package/docs/pixel-perfect.md +0 -86
  146. package/docs/troubleshooting.md +0 -169
  147. package/requirements.txt +0 -5
  148. package/src/ai/analyze-structure.py +0 -305
  149. package/src/ai/extract-design-tokens.py +0 -439
  150. package/src/ai/prompts/__init__.py +0 -2
  151. package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  152. package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
  153. package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
  154. package/src/ai/prompts/design_tokens.py +0 -183
  155. package/src/ai/prompts/structure_analysis.py +0 -273
  156. package/src/core/animation-extractor.js +0 -526
  157. package/src/core/design-tokens.js +0 -103
  158. package/src/core/dimension-extractor.js +0 -366
  159. package/src/core/dimension-output.js +0 -208
  160. package/src/core/discover-pages.js +0 -314
  161. package/src/core/extract-assets.js +0 -468
  162. package/src/core/filter-css.js +0 -499
  163. package/src/core/html-extractor.js +0 -171
  164. package/src/core/merge-css.js +0 -407
  165. package/src/core/multi-page-screenshot.js +0 -377
  166. package/src/core/rewrite-links.js +0 -226
  167. package/src/core/screenshot.js +0 -572
  168. package/src/core/state-capture.js +0 -602
  169. package/src/core/video-capture.js +0 -540
  170. package/src/utils/__init__.py +0 -16
  171. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  172. package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
  173. package/src/utils/env.py +0 -134
  174. package/src/utils/puppeteer.js +0 -281
@@ -0,0 +1,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
+ }