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.
- package/.env.example +14 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/SKILL.md +239 -0
- package/bin/cli.js +45 -0
- package/bin/commands/help.js +29 -0
- package/bin/commands/init.js +126 -0
- package/bin/commands/verify.js +99 -0
- package/bin/utils/copy.js +65 -0
- package/bin/utils/validate.js +122 -0
- package/docs/basic-clone.md +63 -0
- package/docs/cli-reference.md +94 -0
- package/docs/design-clone-architecture.md +247 -0
- package/docs/pixel-perfect.md +86 -0
- package/docs/troubleshooting.md +97 -0
- package/package.json +57 -0
- package/requirements.txt +5 -0
- package/src/ai/analyze-structure.py +305 -0
- package/src/ai/extract-design-tokens.py +439 -0
- package/src/ai/prompts/__init__.py +2 -0
- package/src/ai/prompts/design_tokens.py +183 -0
- package/src/ai/prompts/structure_analysis.py +273 -0
- package/src/core/cookie-handler.js +76 -0
- package/src/core/css-extractor.js +107 -0
- package/src/core/dimension-extractor.js +366 -0
- package/src/core/dimension-output.js +208 -0
- package/src/core/extract-assets.js +468 -0
- package/src/core/filter-css.js +499 -0
- package/src/core/html-extractor.js +102 -0
- package/src/core/lazy-loader.js +188 -0
- package/src/core/page-readiness.js +161 -0
- package/src/core/screenshot.js +380 -0
- package/src/post-process/enhance-assets.js +157 -0
- package/src/post-process/fetch-images.js +398 -0
- package/src/post-process/inject-icons.js +311 -0
- package/src/utils/__init__.py +16 -0
- package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
- package/src/utils/browser.js +103 -0
- package/src/utils/env.js +153 -0
- package/src/utils/env.py +134 -0
- package/src/utils/helpers.js +71 -0
- package/src/utils/puppeteer.js +281 -0
- package/src/verification/verify-layout.js +424 -0
- package/src/verification/verify-menu.js +422 -0
- package/templates/base.css +705 -0
- 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
|
+
}
|