design-clone 1.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 (47) hide show
  1. package/.env.example +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -0
  4. package/SKILL.md +239 -0
  5. package/bin/cli.js +45 -0
  6. package/bin/commands/help.js +29 -0
  7. package/bin/commands/init.js +126 -0
  8. package/bin/commands/verify.js +99 -0
  9. package/bin/utils/copy.js +65 -0
  10. package/bin/utils/validate.js +122 -0
  11. package/docs/basic-clone.md +63 -0
  12. package/docs/cli-reference.md +94 -0
  13. package/docs/design-clone-architecture.md +247 -0
  14. package/docs/pixel-perfect.md +86 -0
  15. package/docs/troubleshooting.md +97 -0
  16. package/package.json +57 -0
  17. package/requirements.txt +5 -0
  18. package/src/ai/analyze-structure.py +305 -0
  19. package/src/ai/extract-design-tokens.py +439 -0
  20. package/src/ai/prompts/__init__.py +2 -0
  21. package/src/ai/prompts/design_tokens.py +183 -0
  22. package/src/ai/prompts/structure_analysis.py +273 -0
  23. package/src/core/cookie-handler.js +76 -0
  24. package/src/core/css-extractor.js +107 -0
  25. package/src/core/dimension-extractor.js +366 -0
  26. package/src/core/dimension-output.js +208 -0
  27. package/src/core/extract-assets.js +468 -0
  28. package/src/core/filter-css.js +499 -0
  29. package/src/core/html-extractor.js +102 -0
  30. package/src/core/lazy-loader.js +188 -0
  31. package/src/core/page-readiness.js +161 -0
  32. package/src/core/screenshot.js +380 -0
  33. package/src/post-process/enhance-assets.js +157 -0
  34. package/src/post-process/fetch-images.js +398 -0
  35. package/src/post-process/inject-icons.js +311 -0
  36. package/src/utils/__init__.py +16 -0
  37. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  38. package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
  39. package/src/utils/browser.js +103 -0
  40. package/src/utils/env.js +153 -0
  41. package/src/utils/env.py +134 -0
  42. package/src/utils/helpers.js +71 -0
  43. package/src/utils/puppeteer.js +281 -0
  44. package/src/verification/verify-layout.js +424 -0
  45. package/src/verification/verify-menu.js +422 -0
  46. package/templates/base.css +705 -0
  47. package/templates/base.html +293 -0
@@ -0,0 +1,366 @@
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 - Puppeteer 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
+ // 1. Extract containers
73
+ const containerSelectors = [
74
+ 'section', 'main', 'article', 'header', 'footer',
75
+ '[role="main"]', '[role="region"]',
76
+ 'div[class*="container"]', 'div[class*="wrapper"]',
77
+ 'div[class*="section"]', 'div[class*="content"]',
78
+ 'div[class*="grid"]', 'div[class*="card"]'
79
+ ];
80
+
81
+ const seenContainers = new Set();
82
+ containerSelectors.forEach(selector => {
83
+ try {
84
+ document.querySelectorAll(selector).forEach((el) => {
85
+ if (seenContainers.has(el)) return;
86
+ const rect = el.getBoundingClientRect();
87
+ if (rect.width < 100 || rect.height < 50) return;
88
+
89
+ const children = Array.from(el.children).filter(c => {
90
+ const cr = c.getBoundingClientRect();
91
+ return cr.width > 50 && cr.height > 30;
92
+ });
93
+
94
+ if (children.length >= 2) {
95
+ seenContainers.add(el);
96
+ const dims = extractDimensions(el);
97
+ dims.selector = el.className
98
+ ? `.${el.className.split(' ').filter(c => c && !c.includes(':')).slice(0, 2).join('.')}`
99
+ : el.tagName.toLowerCase();
100
+ dims.childCount = children.length;
101
+
102
+ if (children.length >= 2 && (dims.display === 'flex' || dims.display === 'grid')) {
103
+ const firstRect = children[0].getBoundingClientRect();
104
+ const secondRect = children[1].getBoundingClientRect();
105
+ const calculatedGap = Math.round(
106
+ dims.flexDirection === 'column'
107
+ ? secondRect.top - firstRect.bottom
108
+ : secondRect.left - firstRect.right
109
+ );
110
+ if (calculatedGap > 0 && calculatedGap < 200) {
111
+ dims.calculatedGap = calculatedGap;
112
+ }
113
+ }
114
+
115
+ results.containers.push(cleanObject(dims));
116
+ }
117
+ });
118
+ } catch (e) { /* ignore */ }
119
+ });
120
+
121
+ // 2. Extract typography
122
+ const typographySelectors = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'];
123
+ typographySelectors.forEach(tag => {
124
+ try {
125
+ const elements = document.querySelectorAll(tag);
126
+ if (elements.length > 0) {
127
+ for (const el of elements) {
128
+ const rect = el.getBoundingClientRect();
129
+ if (rect.width > 50 && rect.height > 10) {
130
+ const dims = extractDimensions(el);
131
+ results.typography.push({
132
+ selector: tag,
133
+ fontSize: dims.fontSize,
134
+ fontWeight: dims.fontWeight,
135
+ lineHeight: dims.lineHeight,
136
+ letterSpacing: dims.letterSpacing,
137
+ color: dims.color,
138
+ marginTop: dims.marginTop,
139
+ marginBottom: dims.marginBottom,
140
+ textSample: el.textContent?.trim().slice(0, 40),
141
+ count: elements.length
142
+ });
143
+ break;
144
+ }
145
+ }
146
+ }
147
+ } catch (e) { /* ignore */ }
148
+ });
149
+
150
+ // 3. Extract buttons
151
+ const buttonSelectors = [
152
+ 'button', 'a[class*="btn"]', 'a[class*="button"]',
153
+ '[role="button"]', 'input[type="submit"]'
154
+ ];
155
+ const seenButtons = new Set();
156
+ buttonSelectors.forEach(selector => {
157
+ try {
158
+ document.querySelectorAll(selector).forEach((el) => {
159
+ if (seenButtons.has(el)) return;
160
+ const rect = el.getBoundingClientRect();
161
+ if (rect.width < 40 || rect.height < 20) return;
162
+ if (results.buttons.length >= 10) return;
163
+
164
+ seenButtons.add(el);
165
+ const dims = extractDimensions(el);
166
+ results.buttons.push({
167
+ width: dims.width,
168
+ height: dims.height,
169
+ paddingTop: dims.paddingTop,
170
+ paddingRight: dims.paddingRight,
171
+ paddingBottom: dims.paddingBottom,
172
+ paddingLeft: dims.paddingLeft,
173
+ fontSize: dims.fontSize,
174
+ fontWeight: dims.fontWeight,
175
+ borderRadius: dims.borderRadius,
176
+ backgroundColor: dims.backgroundColor,
177
+ color: dims.color,
178
+ text: el.textContent?.trim().slice(0, 30)
179
+ });
180
+ });
181
+ } catch (e) { /* ignore */ }
182
+ });
183
+
184
+ // 4. Extract images
185
+ try {
186
+ document.querySelectorAll('img').forEach((el) => {
187
+ const rect = el.getBoundingClientRect();
188
+ if (rect.width < 80 || rect.height < 80) return;
189
+ if (results.images.length >= 15) return;
190
+
191
+ results.images.push({
192
+ width: Math.round(rect.width),
193
+ height: Math.round(rect.height),
194
+ aspectRatio: (rect.width / rect.height).toFixed(2),
195
+ x: Math.round(rect.x),
196
+ y: Math.round(rect.y + window.scrollY)
197
+ });
198
+ });
199
+ } catch (e) { /* ignore */ }
200
+
201
+ // 5. Card pattern detection
202
+ function calculateSimilarity(a, b) {
203
+ const widthSim = 1 - Math.abs(a.width - b.width) / Math.max(a.width, b.width, 1);
204
+ const heightSim = 1 - Math.abs(a.height - b.height) / Math.max(a.height, b.height, 1);
205
+ const marginA = a.marginTop + a.marginBottom;
206
+ const marginB = b.marginTop + b.marginBottom;
207
+ const marginSim = 1 - Math.abs(marginA - marginB) / Math.max(marginA, marginB, 1);
208
+ const radiusSim = a.borderRadius === b.borderRadius ? 1 : 0.5;
209
+ return (widthSim * 0.4) + (heightSim * 0.3) + (marginSim * 0.15) + (radiusSim * 0.15);
210
+ }
211
+
212
+ function detectLayoutType(elements) {
213
+ if (elements.length < 2) return 'single';
214
+ const yPositions = elements.map(el => el.y);
215
+ const xPositions = elements.map(el => el.x);
216
+ const yVariance = Math.max(...yPositions) - Math.min(...yPositions);
217
+ const xVariance = Math.max(...xPositions) - Math.min(...xPositions);
218
+ const avgHeight = elements.reduce((s, el) => s + el.height, 0) / elements.length;
219
+ const avgWidth = elements.reduce((s, el) => s + el.width, 0) / elements.length;
220
+
221
+ if (yVariance < avgHeight * 0.3 && xVariance > avgWidth) return 'row';
222
+ if (xVariance < avgWidth * 0.3 && yVariance > avgHeight) return 'column';
223
+ return 'grid';
224
+ }
225
+
226
+ function calculateGap(elements, layout) {
227
+ if (elements.length < 2) return 0;
228
+ const sorted = layout === 'column'
229
+ ? [...elements].sort((a, b) => a.y - b.y)
230
+ : [...elements].sort((a, b) => a.x - b.x);
231
+
232
+ let totalGap = 0, gapCount = 0;
233
+ for (let i = 1; i < sorted.length; i++) {
234
+ const gap = layout === 'column'
235
+ ? sorted[i].y - (sorted[i-1].y + sorted[i-1].height)
236
+ : sorted[i].x - (sorted[i-1].x + sorted[i-1].width);
237
+ if (gap > 0 && gap < 200) { totalGap += gap; gapCount++; }
238
+ }
239
+ return gapCount > 0 ? Math.round(totalGap / gapCount) : 0;
240
+ }
241
+
242
+ let cardGroupId = 0;
243
+ results.containers.forEach(container => {
244
+ if (container.childCount >= 2) {
245
+ try {
246
+ const parent = document.querySelector(container.selector);
247
+ if (!parent) return;
248
+
249
+ const children = Array.from(parent.children).filter(c => {
250
+ const cr = c.getBoundingClientRect();
251
+ return cr.width > 80 && cr.height > 60;
252
+ });
253
+
254
+ if (children.length >= 2) {
255
+ const childDims = children.map(c => {
256
+ const cr = c.getBoundingClientRect();
257
+ const cs = window.getComputedStyle(c);
258
+ return {
259
+ width: Math.round(cr.width),
260
+ height: Math.round(cr.height),
261
+ x: Math.round(cr.x),
262
+ y: Math.round(cr.y),
263
+ paddingTop: parseFloat(cs.paddingTop) || 0,
264
+ paddingRight: parseFloat(cs.paddingRight) || 0,
265
+ paddingBottom: parseFloat(cs.paddingBottom) || 0,
266
+ paddingLeft: parseFloat(cs.paddingLeft) || 0,
267
+ marginTop: parseFloat(cs.marginTop) || 0,
268
+ marginBottom: parseFloat(cs.marginBottom) || 0,
269
+ borderRadius: cs.borderRadius,
270
+ boxShadow: cs.boxShadow !== 'none' ? cs.boxShadow : undefined,
271
+ backgroundColor: cs.backgroundColor !== 'rgba(0, 0, 0, 0)' ? cs.backgroundColor : undefined
272
+ };
273
+ });
274
+
275
+ const used = new Set();
276
+ const groups = [];
277
+
278
+ for (let i = 0; i < childDims.length; i++) {
279
+ if (used.has(i)) continue;
280
+ const group = [childDims[i]];
281
+ used.add(i);
282
+
283
+ for (let j = i + 1; j < childDims.length; j++) {
284
+ if (used.has(j)) continue;
285
+ if (calculateSimilarity(childDims[i], childDims[j]) >= 0.70) {
286
+ group.push(childDims[j]);
287
+ used.add(j);
288
+ }
289
+ }
290
+
291
+ if (group.length >= 2) groups.push(group);
292
+ }
293
+
294
+ groups.forEach(group => {
295
+ const avg = (arr, key) => Math.round(arr.reduce((s, el) => s + (el[key] || 0), 0) / arr.length);
296
+ const layout = detectLayoutType(group);
297
+ const gap = calculateGap(group, layout);
298
+
299
+ results.cards.push({
300
+ id: `card-group-${++cardGroupId}`,
301
+ parentSelector: container.selector,
302
+ count: group.length,
303
+ layout,
304
+ gap,
305
+ avgDimensions: {
306
+ width: avg(group, 'width'),
307
+ height: avg(group, 'height'),
308
+ paddingTop: avg(group, 'paddingTop'),
309
+ paddingRight: avg(group, 'paddingRight'),
310
+ paddingBottom: avg(group, 'paddingBottom'),
311
+ paddingLeft: avg(group, 'paddingLeft')
312
+ },
313
+ borderRadius: group[0].borderRadius !== '0px' ? group[0].borderRadius : undefined,
314
+ boxShadow: group[0].boxShadow,
315
+ backgroundColor: group[0].backgroundColor
316
+ });
317
+ });
318
+ }
319
+ } catch (e) { /* ignore */ }
320
+ }
321
+ });
322
+
323
+ // 6. Grid layouts
324
+ results.gridLayouts = [];
325
+ results.containers.forEach(container => {
326
+ if (container.display === 'grid' || container.display === 'flex') {
327
+ try {
328
+ const parent = document.querySelector(container.selector);
329
+ if (!parent) return;
330
+ const computed = window.getComputedStyle(parent);
331
+ const children = parent.children;
332
+
333
+ if (children.length >= 2) {
334
+ if (computed.display === 'grid') {
335
+ const columns = computed.gridTemplateColumns;
336
+ const colCount = columns && columns !== 'none'
337
+ ? columns.split(' ').filter(c => c && c !== 'none').length
338
+ : Math.ceil(children.length / 2);
339
+
340
+ results.gridLayouts.push({
341
+ selector: container.selector,
342
+ display: 'grid',
343
+ columns: colCount,
344
+ rows: Math.ceil(children.length / colCount),
345
+ columnGap: parseFloat(computed.columnGap) || parseFloat(computed.gap) || 0,
346
+ rowGap: parseFloat(computed.rowGap) || parseFloat(computed.gap) || 0,
347
+ childCount: children.length
348
+ });
349
+ } else if (computed.display === 'flex') {
350
+ results.gridLayouts.push({
351
+ selector: container.selector,
352
+ display: 'flex',
353
+ flexDirection: computed.flexDirection,
354
+ flexWrap: computed.flexWrap,
355
+ gap: parseFloat(computed.gap) || container.calculatedGap || 0,
356
+ childCount: children.length
357
+ });
358
+ }
359
+ }
360
+ } catch (e) { /* ignore */ }
361
+ }
362
+ });
363
+
364
+ return results;
365
+ }, viewportName);
366
+ }
@@ -0,0 +1,208 @@
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
+ */
7
+
8
+ // Default viewport configurations
9
+ const VIEWPORTS = {
10
+ desktop: { width: 1440, height: 900, deviceScaleFactor: 1 },
11
+ tablet: { width: 768, height: 1024, deviceScaleFactor: 1 },
12
+ mobile: { width: 375, height: 812, deviceScaleFactor: 2 }
13
+ };
14
+
15
+ /**
16
+ * Build final component-dimensions.json output with proper schema
17
+ * @param {Object} allViewportDimensions - Dimensions from all viewports
18
+ * @param {string} url - Source URL
19
+ * @returns {Object} Final JSON structure
20
+ */
21
+ export function buildDimensionsOutput(allViewportDimensions, url) {
22
+ const output = {
23
+ meta: {
24
+ version: "1.0",
25
+ extractedAt: new Date().toISOString(),
26
+ url: url,
27
+ tool: "design-clone/screenshot.js"
28
+ },
29
+ viewports: {},
30
+ summary: {}
31
+ };
32
+
33
+ for (const [vpName, vpData] of Object.entries(allViewportDimensions)) {
34
+ output.viewports[vpName] = sanitizeViewportData(vpData, vpName);
35
+ }
36
+
37
+ output.summary = buildCrossViewportSummary(output.viewports);
38
+ return output;
39
+ }
40
+
41
+ /**
42
+ * Sanitize viewport data for JSON output
43
+ */
44
+ export function sanitizeViewportData(data, vpName) {
45
+ if (!data) return {};
46
+
47
+ const clean = JSON.parse(JSON.stringify(data));
48
+ clean.width = VIEWPORTS[vpName]?.width || 0;
49
+ clean.height = VIEWPORTS[vpName]?.height || 0;
50
+
51
+ function roundNumbers(obj) {
52
+ for (const key in obj) {
53
+ if (typeof obj[key] === 'number') {
54
+ obj[key] = Math.round(obj[key]);
55
+ } else if (Array.isArray(obj[key])) {
56
+ obj[key].forEach(item => roundNumbers(item));
57
+ } else if (typeof obj[key] === 'object' && obj[key] !== null) {
58
+ roundNumbers(obj[key]);
59
+ }
60
+ }
61
+ return obj;
62
+ }
63
+
64
+ function truncateStrings(obj, maxLen = 80) {
65
+ for (const key in obj) {
66
+ if (typeof obj[key] === 'string' && obj[key].length > maxLen) {
67
+ obj[key] = obj[key].slice(0, maxLen) + '...';
68
+ } else if (Array.isArray(obj[key])) {
69
+ obj[key].forEach(item => truncateStrings(item, maxLen));
70
+ } else if (typeof obj[key] === 'object' && obj[key] !== null) {
71
+ truncateStrings(obj[key], maxLen);
72
+ }
73
+ }
74
+ return obj;
75
+ }
76
+
77
+ // Limit array sizes for token efficiency
78
+ if (clean.containers && clean.containers.length > 15) {
79
+ clean.containers = clean.containers.slice(0, 15);
80
+ }
81
+ if (clean.images && clean.images.length > 10) {
82
+ clean.images = clean.images.slice(0, 10);
83
+ }
84
+ if (clean.buttons && clean.buttons.length > 10) {
85
+ clean.buttons = clean.buttons.slice(0, 10);
86
+ }
87
+
88
+ return truncateStrings(roundNumbers(clean));
89
+ }
90
+
91
+ /**
92
+ * Build cross-viewport summary for AI consumption
93
+ */
94
+ export function buildCrossViewportSummary(viewports) {
95
+ const summary = {
96
+ maxContainerWidth: 0,
97
+ commonGap: 0,
98
+ breakpoints: {
99
+ desktop: VIEWPORTS.desktop.width,
100
+ tablet: VIEWPORTS.tablet.width,
101
+ mobile: VIEWPORTS.mobile.width
102
+ },
103
+ typography: { h1: {}, h2: {}, h3: {}, body: {} },
104
+ cardPatterns: { totalGroups: 0, avgCardSize: null }
105
+ };
106
+
107
+ for (const [vpName, vpData] of Object.entries(viewports)) {
108
+ if (!vpData) continue;
109
+
110
+ if (vpData.containers) {
111
+ for (const container of vpData.containers) {
112
+ if (container.width > summary.maxContainerWidth) {
113
+ summary.maxContainerWidth = container.width;
114
+ }
115
+ }
116
+ }
117
+
118
+ if (vpData.typography) {
119
+ for (const typo of vpData.typography) {
120
+ const tag = typo.selector?.toLowerCase();
121
+ if (tag === 'h1') summary.typography.h1[vpName] = typo.fontSize;
122
+ if (tag === 'h2') summary.typography.h2[vpName] = typo.fontSize;
123
+ if (tag === 'h3') summary.typography.h3[vpName] = typo.fontSize;
124
+ if (tag === 'p') summary.typography.body[vpName] = typo.fontSize;
125
+ }
126
+ }
127
+
128
+ if (vpData.cards && vpData.cards.length > 0) {
129
+ summary.cardPatterns.totalGroups += vpData.cards.length;
130
+ if (vpName === 'desktop' && vpData.cards[0]?.avgDimensions) {
131
+ summary.cardPatterns.avgCardSize = vpData.cards[0].avgDimensions;
132
+ }
133
+
134
+ const gaps = vpData.cards.map(g => g.gap).filter(g => g > 0);
135
+ if (gaps.length > 0) {
136
+ summary.commonGap = Math.round(gaps.reduce((a, b) => a + b, 0) / gaps.length);
137
+ }
138
+ }
139
+ }
140
+
141
+ return summary;
142
+ }
143
+
144
+ /**
145
+ * Generate AI-friendly summary (compact, <5KB)
146
+ * @param {Object} fullOutput - Full component-dimensions.json
147
+ * @returns {Object} Compact summary for AI prompts
148
+ */
149
+ export function generateAISummary(fullOutput) {
150
+ const { viewports, summary } = fullOutput;
151
+ const desktop = viewports.desktop || {};
152
+
153
+ function inferSectionPadding(containers) {
154
+ if (!containers || containers.length === 0) return "64px 0";
155
+ const paddings = containers.slice(0, 5).map(c => ({
156
+ v: c.paddingTop || c.paddingBottom || 64,
157
+ h: c.paddingLeft || c.paddingRight || 0
158
+ }));
159
+ const avgV = Math.round(paddings.reduce((s, p) => s + p.v, 0) / paddings.length);
160
+ const avgH = Math.round(paddings.reduce((s, p) => s + p.h, 0) / paddings.length);
161
+ return `${avgV}px ${avgH}px`;
162
+ }
163
+
164
+ function inferCardDimensions(cards) {
165
+ if (!cards || cards.length === 0) {
166
+ return { width: "auto", height: "auto", padding: "24px" };
167
+ }
168
+ const first = cards[0].avgDimensions || cards[0];
169
+ return {
170
+ width: first.width ? first.width + "px" : "auto",
171
+ height: first.height > 0 ? first.height + "px" : "auto",
172
+ padding: (first.paddingTop || first.padding || 24) + "px"
173
+ };
174
+ }
175
+
176
+ return {
177
+ _comment: "USE THESE EXACT VALUES - DO NOT ESTIMATE",
178
+ EXACT_DIMENSIONS: {
179
+ container_max_width: summary.maxContainerWidth + "px",
180
+ section_padding: inferSectionPadding(desktop.containers),
181
+ card_dimensions: inferCardDimensions(desktop.cards),
182
+ gap: summary.commonGap + "px"
183
+ },
184
+ EXACT_TYPOGRAPHY: {
185
+ h1: (summary.typography.h1.desktop || 48) + "px",
186
+ h2: (summary.typography.h2.desktop || 36) + "px",
187
+ h3: (summary.typography.h3.desktop || 24) + "px",
188
+ body: (summary.typography.body.desktop || 16) + "px"
189
+ },
190
+ RESPONSIVE: {
191
+ desktop_breakpoint: summary.breakpoints.desktop + "px",
192
+ tablet_breakpoint: summary.breakpoints.tablet + "px",
193
+ mobile_breakpoint: summary.breakpoints.mobile + "px",
194
+ typography_scaling: {
195
+ h1: {
196
+ desktop: (summary.typography.h1.desktop || 48) + "px",
197
+ tablet: (summary.typography.h1.tablet || 36) + "px",
198
+ mobile: (summary.typography.h1.mobile || 28) + "px"
199
+ },
200
+ h2: {
201
+ desktop: (summary.typography.h2.desktop || 36) + "px",
202
+ tablet: (summary.typography.h2.tablet || 28) + "px",
203
+ mobile: (summary.typography.h2.mobile || 24) + "px"
204
+ }
205
+ }
206
+ }
207
+ };
208
+ }