design-clone 1.2.0 → 2.1.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/README.md +26 -12
- package/bin/commands/clone-site.js +75 -10
- package/bin/commands/init.js +33 -1
- package/bin/commands/verify.js +5 -3
- package/bin/utils/validate.js +24 -8
- package/docs/cli-reference.md +200 -2
- package/docs/codebase-summary.md +309 -0
- package/docs/design-clone-architecture.md +259 -42
- package/docs/pixel-perfect.md +35 -4
- package/docs/project-roadmap.md +382 -0
- package/docs/troubleshooting.md +5 -4
- package/package.json +10 -8
- package/src/ai/__pycache__/analyze-structure.cpython-313.pyc +0 -0
- package/src/ai/__pycache__/extract-design-tokens.cpython-313.pyc +0 -0
- package/src/ai/analyze-structure.py +73 -3
- package/src/ai/extract-design-tokens.py +356 -13
- package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
- package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
- package/src/ai/prompts/__pycache__/ux_audit.cpython-313.pyc +0 -0
- package/src/ai/prompts/design_tokens.py +133 -0
- package/src/ai/prompts/structure_analysis.py +329 -10
- package/src/ai/prompts/ux_audit.py +198 -0
- package/src/ai/ux-audit.js +596 -0
- package/src/core/app-state-snapshot.js +511 -0
- package/src/core/content-counter.js +342 -0
- package/src/core/cookie-handler.js +1 -1
- package/src/core/css-extractor.js +4 -4
- package/src/core/dimension-extractor.js +93 -21
- package/src/core/dimension-output.js +103 -6
- package/src/core/discover-pages.js +242 -14
- package/src/core/dom-tree-analyzer.js +298 -0
- package/src/core/extract-assets.js +1 -1
- package/src/core/framework-detector.js +538 -0
- package/src/core/html-extractor.js +45 -4
- package/src/core/lazy-loader.js +7 -7
- package/src/core/multi-page-screenshot.js +9 -6
- package/src/core/page-readiness.js +8 -8
- package/src/core/screenshot.js +138 -9
- package/src/core/section-cropper.js +209 -0
- package/src/core/section-detector.js +386 -0
- package/src/core/semantic-enhancer.js +492 -0
- package/src/core/state-capture.js +18 -22
- package/src/core/tests/test-section-cropper.js +177 -0
- package/src/core/tests/test-section-detector.js +55 -0
- package/src/core/video-capture.js +152 -146
- package/src/route-discoverers/angular-discoverer.js +157 -0
- package/src/route-discoverers/astro-discoverer.js +123 -0
- package/src/route-discoverers/base-discoverer.js +242 -0
- package/src/route-discoverers/index.js +106 -0
- package/src/route-discoverers/next-discoverer.js +130 -0
- package/src/route-discoverers/nuxt-discoverer.js +138 -0
- package/src/route-discoverers/react-discoverer.js +139 -0
- package/src/route-discoverers/svelte-discoverer.js +109 -0
- package/src/route-discoverers/universal-discoverer.js +227 -0
- package/src/route-discoverers/vue-discoverer.js +118 -0
- package/src/utils/__init__.py +1 -1
- package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/utils/browser.js +11 -37
- package/src/utils/playwright.js +213 -0
- package/src/verification/generate-audit-report.js +398 -0
- package/src/verification/verify-footer.js +493 -0
- package/src/verification/verify-header.js +486 -0
- package/src/verification/verify-layout.js +2 -2
- package/src/verification/verify-menu.js +4 -20
- package/src/verification/verify-slider.js +533 -0
- package/src/utils/puppeteer.js +0 -281
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Counter
|
|
3
|
+
*
|
|
4
|
+
* Parse page DOM to extract exact content counts for:
|
|
5
|
+
* - Grid items, list items, cards
|
|
6
|
+
* - Navigation links
|
|
7
|
+
* - Sections/containers
|
|
8
|
+
* - Images, buttons, forms
|
|
9
|
+
*
|
|
10
|
+
* Outputs content-counts.json for use in structure analysis.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Count content items in page DOM
|
|
15
|
+
* @param {Page} page - Playwright page
|
|
16
|
+
* @returns {Promise<object>} Content counts
|
|
17
|
+
*/
|
|
18
|
+
export async function extractContentCounts(page) {
|
|
19
|
+
return await page.evaluate(() => {
|
|
20
|
+
const counts = {
|
|
21
|
+
extractedAt: new Date().toISOString(),
|
|
22
|
+
|
|
23
|
+
// Section counts
|
|
24
|
+
sections: {
|
|
25
|
+
total: 0,
|
|
26
|
+
withBackground: 0,
|
|
27
|
+
details: []
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
// Grid/List containers
|
|
31
|
+
grids: {
|
|
32
|
+
total: 0,
|
|
33
|
+
details: []
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
// Repeated items (cards, list items)
|
|
37
|
+
repeatedItems: {
|
|
38
|
+
total: 0,
|
|
39
|
+
byType: {}
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// Navigation
|
|
43
|
+
navigation: {
|
|
44
|
+
headerLinks: 0,
|
|
45
|
+
footerLinks: 0,
|
|
46
|
+
allLinks: 0
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// Media
|
|
50
|
+
media: {
|
|
51
|
+
images: 0,
|
|
52
|
+
videos: 0,
|
|
53
|
+
svgIcons: 0
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// Interactive
|
|
57
|
+
interactive: {
|
|
58
|
+
buttons: 0,
|
|
59
|
+
inputs: 0,
|
|
60
|
+
forms: 0
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Helper: check if element is visible (not hidden)
|
|
65
|
+
const isVisible = (el) => {
|
|
66
|
+
if (!el) return false;
|
|
67
|
+
const style = getComputedStyle(el);
|
|
68
|
+
return style.display !== 'none' &&
|
|
69
|
+
style.visibility !== 'hidden' &&
|
|
70
|
+
!el.hasAttribute('hidden');
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Helper: get meaningful class name for element
|
|
74
|
+
const getSelector = (el) => {
|
|
75
|
+
if (el.id) return `#${el.id}`;
|
|
76
|
+
const classes = [...el.classList].filter(c =>
|
|
77
|
+
!c.match(/^(js-|is-|has-|data-)/) &&
|
|
78
|
+
c.length > 2
|
|
79
|
+
).slice(0, 3).join('.');
|
|
80
|
+
return classes ? `.${classes}` : el.tagName.toLowerCase();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// 1. Count sections (major containers with padding/background)
|
|
84
|
+
const sectionSelectors = [
|
|
85
|
+
'section',
|
|
86
|
+
'[class*="section"]',
|
|
87
|
+
'[class*="py-lg"]', '[class*="py-xl"]', '[class*="py-2xl"]',
|
|
88
|
+
'[class*="py-md"]',
|
|
89
|
+
'[class*="bg-background"]',
|
|
90
|
+
'[class*="bg-white"]',
|
|
91
|
+
'[class*="bg-gray"]'
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const sectionElements = new Set();
|
|
95
|
+
const MAX_SECTION_DETAILS = 30;
|
|
96
|
+
|
|
97
|
+
sectionSelectors.forEach(sel => {
|
|
98
|
+
try {
|
|
99
|
+
document.querySelectorAll(sel).forEach(el => {
|
|
100
|
+
if (sectionElements.size >= MAX_SECTION_DETAILS) return;
|
|
101
|
+
|
|
102
|
+
// Check if element has significant height and width (major section)
|
|
103
|
+
const rect = el.getBoundingClientRect();
|
|
104
|
+
const isSignificant = rect.height > 100 && rect.width > 200;
|
|
105
|
+
|
|
106
|
+
// Count elements that are either:
|
|
107
|
+
// 1. Direct children of body/main/root
|
|
108
|
+
// 2. Have section-like characteristics (padding, bg, significant size)
|
|
109
|
+
const parent = el.parentElement;
|
|
110
|
+
const isTopLevel = parent?.tagName === 'BODY' ||
|
|
111
|
+
parent?.tagName === 'MAIN' ||
|
|
112
|
+
parent?.id === 'root' ||
|
|
113
|
+
parent?.id === '__next' ||
|
|
114
|
+
parent?.classList.contains('container');
|
|
115
|
+
|
|
116
|
+
if (isTopLevel || (isSignificant && isVisible(el))) {
|
|
117
|
+
sectionElements.add(el);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
} catch (e) { /* invalid selector */ }
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
sectionElements.forEach(el => {
|
|
124
|
+
const style = getComputedStyle(el);
|
|
125
|
+
const hasBg = style.backgroundColor !== 'rgba(0, 0, 0, 0)' &&
|
|
126
|
+
style.backgroundColor !== 'transparent';
|
|
127
|
+
counts.sections.total++;
|
|
128
|
+
if (hasBg) counts.sections.withBackground++;
|
|
129
|
+
|
|
130
|
+
counts.sections.details.push({
|
|
131
|
+
selector: getSelector(el),
|
|
132
|
+
visible: isVisible(el),
|
|
133
|
+
hasBackground: hasBg,
|
|
134
|
+
childCount: el.children.length
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// 2. Count grid/flex containers and their items
|
|
139
|
+
// Only target meaningful containers (not trivial wrappers)
|
|
140
|
+
const gridSelectors = [
|
|
141
|
+
'[class*="grid"]',
|
|
142
|
+
'[style*="display: grid"]'
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
// Separate flex selectors - more selective
|
|
146
|
+
const flexSelectors = [
|
|
147
|
+
'[class*="flex"][class*="gap"]',
|
|
148
|
+
'[class*="flex"][class*="wrap"]',
|
|
149
|
+
'[class*="flex"][class*="col"]'
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const processedGrids = new Set();
|
|
153
|
+
const MIN_ITEMS_FOR_GRID = 2; // Only count containers with 2+ items
|
|
154
|
+
const MAX_GRID_DETAILS = 50; // Limit output size
|
|
155
|
+
|
|
156
|
+
[...gridSelectors, ...flexSelectors].forEach(sel => {
|
|
157
|
+
try {
|
|
158
|
+
document.querySelectorAll(sel).forEach(el => {
|
|
159
|
+
if (processedGrids.has(el)) return;
|
|
160
|
+
if (counts.grids.details.length >= MAX_GRID_DETAILS) return;
|
|
161
|
+
|
|
162
|
+
const style = getComputedStyle(el);
|
|
163
|
+
if (style.display === 'grid' || style.display === 'flex' ||
|
|
164
|
+
style.display === 'inline-grid' || style.display === 'inline-flex') {
|
|
165
|
+
processedGrids.add(el);
|
|
166
|
+
|
|
167
|
+
// Count direct children (grid items)
|
|
168
|
+
const items = [...el.children].filter(child =>
|
|
169
|
+
child.tagName !== 'SCRIPT' &&
|
|
170
|
+
child.tagName !== 'STYLE'
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Skip containers with fewer than MIN_ITEMS
|
|
174
|
+
if (items.length < MIN_ITEMS_FOR_GRID) return;
|
|
175
|
+
|
|
176
|
+
// Count visible vs hidden
|
|
177
|
+
const visibleItems = items.filter(isVisible);
|
|
178
|
+
const hiddenItems = items.filter(i => !isVisible(i));
|
|
179
|
+
|
|
180
|
+
// Only count if has meaningful visible items
|
|
181
|
+
if (visibleItems.length >= MIN_ITEMS_FOR_GRID) {
|
|
182
|
+
counts.grids.total++;
|
|
183
|
+
counts.grids.details.push({
|
|
184
|
+
selector: getSelector(el),
|
|
185
|
+
display: style.display,
|
|
186
|
+
totalItems: items.length,
|
|
187
|
+
visibleItems: visibleItems.length,
|
|
188
|
+
hiddenItems: hiddenItems.length,
|
|
189
|
+
gridCols: style.gridTemplateColumns || null,
|
|
190
|
+
visible: isVisible(el)
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
} catch (e) { /* invalid selector */ }
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// 3. Count repeated items (cards, list items, etc.)
|
|
199
|
+
const repeatPatterns = [
|
|
200
|
+
{ name: 'cards', selectors: ['[class*="card"]', '[class*="Card"]'] },
|
|
201
|
+
{ name: 'listItems', selectors: ['li', '[class*="item"]', '[class*="Item"]'] },
|
|
202
|
+
{ name: 'services', selectors: ['[class*="service"]', '[class*="Service"]'] },
|
|
203
|
+
{ name: 'features', selectors: ['[class*="feature"]', '[class*="Feature"]'] },
|
|
204
|
+
{ name: 'testimonials', selectors: ['[class*="testimonial"]', '[class*="review"]'] },
|
|
205
|
+
{ name: 'teamMembers', selectors: ['[class*="team"]', '[class*="member"]', '[class*="person"]'] },
|
|
206
|
+
{ name: 'faqItems', selectors: ['[class*="faq"]', '[class*="accordion"]', 'details'] },
|
|
207
|
+
{ name: 'pricingCards', selectors: ['[class*="pricing"]', '[class*="plan"]'] },
|
|
208
|
+
{ name: 'blogPosts', selectors: ['[class*="post"]', '[class*="article"]', 'article'] },
|
|
209
|
+
{ name: 'products', selectors: ['[class*="product"]', '[class*="Product"]'] },
|
|
210
|
+
{ name: 'categories', selectors: ['[class*="category"]', '[class*="Category"]'] }
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
repeatPatterns.forEach(({ name, selectors }) => {
|
|
214
|
+
let totalCount = 0;
|
|
215
|
+
let visibleCount = 0;
|
|
216
|
+
|
|
217
|
+
selectors.forEach(sel => {
|
|
218
|
+
try {
|
|
219
|
+
const elements = document.querySelectorAll(sel);
|
|
220
|
+
elements.forEach(el => {
|
|
221
|
+
totalCount++;
|
|
222
|
+
if (isVisible(el)) visibleCount++;
|
|
223
|
+
});
|
|
224
|
+
} catch (e) { /* invalid selector */ }
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (totalCount > 0) {
|
|
228
|
+
counts.repeatedItems.byType[name] = {
|
|
229
|
+
total: totalCount,
|
|
230
|
+
visible: visibleCount,
|
|
231
|
+
hidden: totalCount - visibleCount
|
|
232
|
+
};
|
|
233
|
+
counts.repeatedItems.total += totalCount;
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// 4. Count navigation links
|
|
238
|
+
const header = document.querySelector('header, [class*="header"], nav');
|
|
239
|
+
const footer = document.querySelector('footer, [class*="footer"]');
|
|
240
|
+
|
|
241
|
+
if (header) {
|
|
242
|
+
counts.navigation.headerLinks = header.querySelectorAll('a').length;
|
|
243
|
+
}
|
|
244
|
+
if (footer) {
|
|
245
|
+
counts.navigation.footerLinks = footer.querySelectorAll('a').length;
|
|
246
|
+
}
|
|
247
|
+
counts.navigation.allLinks = document.querySelectorAll('a').length;
|
|
248
|
+
|
|
249
|
+
// 5. Count media elements
|
|
250
|
+
counts.media.images = document.querySelectorAll('img, picture').length;
|
|
251
|
+
counts.media.videos = document.querySelectorAll('video, iframe[src*="youtube"], iframe[src*="vimeo"]').length;
|
|
252
|
+
counts.media.svgIcons = document.querySelectorAll('svg').length;
|
|
253
|
+
|
|
254
|
+
// 6. Count interactive elements
|
|
255
|
+
counts.interactive.buttons = document.querySelectorAll('button, [role="button"], input[type="submit"], input[type="button"]').length;
|
|
256
|
+
counts.interactive.inputs = document.querySelectorAll('input, textarea, select').length;
|
|
257
|
+
counts.interactive.forms = document.querySelectorAll('form').length;
|
|
258
|
+
|
|
259
|
+
// 7. Calculate summary
|
|
260
|
+
counts.summary = {
|
|
261
|
+
majorSections: counts.sections.total,
|
|
262
|
+
gridContainers: counts.grids.total,
|
|
263
|
+
totalRepeatedItems: counts.repeatedItems.total,
|
|
264
|
+
totalLinks: counts.navigation.allLinks,
|
|
265
|
+
totalImages: counts.media.images,
|
|
266
|
+
totalButtons: counts.interactive.buttons,
|
|
267
|
+
|
|
268
|
+
// Provide estimates for generation
|
|
269
|
+
recommendedItemCounts: {}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Calculate recommended item counts per grid
|
|
273
|
+
counts.grids.details.forEach((grid, idx) => {
|
|
274
|
+
if (grid.visibleItems >= 3) {
|
|
275
|
+
counts.summary.recommendedItemCounts[grid.selector] = grid.visibleItems;
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Add repeated items recommendations
|
|
280
|
+
Object.entries(counts.repeatedItems.byType).forEach(([type, data]) => {
|
|
281
|
+
if (data.visible >= 2) {
|
|
282
|
+
counts.summary.recommendedItemCounts[type] = data.visible;
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return counts;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Generate concise content summary for prompt injection
|
|
292
|
+
* @param {object} counts - Content counts from extractContentCounts
|
|
293
|
+
* @returns {string} Summary text
|
|
294
|
+
*/
|
|
295
|
+
export function generateContentSummary(counts) {
|
|
296
|
+
const lines = [
|
|
297
|
+
'## EXACT CONTENT COUNTS (from DOM parsing)',
|
|
298
|
+
''
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
// Sections
|
|
302
|
+
lines.push(`### Sections: ${counts.sections.total} total`);
|
|
303
|
+
counts.sections.details.slice(0, 10).forEach(s => {
|
|
304
|
+
lines.push(`- ${s.selector}: ${s.childCount} children${s.visible ? '' : ' (hidden)'}`);
|
|
305
|
+
});
|
|
306
|
+
lines.push('');
|
|
307
|
+
|
|
308
|
+
// Grids with item counts
|
|
309
|
+
lines.push(`### Grid/Flex Containers: ${counts.grids.total} total`);
|
|
310
|
+
counts.grids.details.slice(0, 15).forEach(g => {
|
|
311
|
+
const visibilityNote = g.hiddenItems > 0 ? ` (+${g.hiddenItems} hidden)` : '';
|
|
312
|
+
lines.push(`- ${g.selector}: ${g.visibleItems} visible items${visibilityNote}`);
|
|
313
|
+
});
|
|
314
|
+
lines.push('');
|
|
315
|
+
|
|
316
|
+
// Repeated items
|
|
317
|
+
if (Object.keys(counts.repeatedItems.byType).length > 0) {
|
|
318
|
+
lines.push('### Repeated Items:');
|
|
319
|
+
Object.entries(counts.repeatedItems.byType).forEach(([type, data]) => {
|
|
320
|
+
const hiddenNote = data.hidden > 0 ? ` (+${data.hidden} hidden)` : '';
|
|
321
|
+
lines.push(`- ${type}: ${data.visible} visible${hiddenNote}`);
|
|
322
|
+
});
|
|
323
|
+
lines.push('');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Links and media
|
|
327
|
+
lines.push('### Navigation & Media:');
|
|
328
|
+
lines.push(`- Header links: ${counts.navigation.headerLinks}`);
|
|
329
|
+
lines.push(`- Footer links: ${counts.navigation.footerLinks}`);
|
|
330
|
+
lines.push(`- Images: ${counts.media.images}`);
|
|
331
|
+
lines.push(`- SVG icons: ${counts.media.svgIcons}`);
|
|
332
|
+
lines.push('');
|
|
333
|
+
|
|
334
|
+
// Critical instruction
|
|
335
|
+
lines.push('### GENERATION INSTRUCTION:');
|
|
336
|
+
lines.push('When generating HTML, use EXACTLY these item counts:');
|
|
337
|
+
Object.entries(counts.summary.recommendedItemCounts).forEach(([selector, count]) => {
|
|
338
|
+
lines.push(`- ${selector}: ${count} items`);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
return lines.join('\n');
|
|
342
|
+
}
|
|
@@ -38,7 +38,7 @@ export const COOKIE_REMOVE_SELECTORS = [
|
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Dismiss cookie banners by clicking accept or removing elements
|
|
41
|
-
* @param {Page} page -
|
|
41
|
+
* @param {Page} page - Playwright page
|
|
42
42
|
* @returns {Promise<{method: string, selector?: string, count?: number}>}
|
|
43
43
|
*/
|
|
44
44
|
export async function dismissCookieBanner(page) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
// Size limits
|
|
9
|
-
export const MAX_CSS_SIZE =
|
|
9
|
+
export const MAX_CSS_SIZE = 10 * 1024 * 1024; // 10MB limit
|
|
10
10
|
export const MAX_CSS_RULES_WARN = 5000; // Warn on large stylesheets
|
|
11
11
|
|
|
12
12
|
// Layout-critical properties for accurate cloning
|
|
@@ -32,12 +32,12 @@ export const ALL_PROPERTIES = Object.values(LAYOUT_PROPERTIES).flat();
|
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Extract all CSS from page
|
|
35
|
-
* @param {Page} page -
|
|
35
|
+
* @param {Page} page - Playwright page
|
|
36
36
|
* @param {string} baseUrl - Base URL for resolving relative paths
|
|
37
37
|
* @returns {Promise<{cssBlocks: Array, corsBlocked: Array, computedStyles: Object, totalRules: number, warnings: Array}>}
|
|
38
38
|
*/
|
|
39
39
|
export async function extractAllCss(page, baseUrl) {
|
|
40
|
-
return await page.evaluate((url, allProps) => {
|
|
40
|
+
return await page.evaluate(({ url, allProps }) => {
|
|
41
41
|
const cssBlocks = [];
|
|
42
42
|
const corsBlocked = [];
|
|
43
43
|
const warnings = [];
|
|
@@ -128,5 +128,5 @@ export async function extractAllCss(page, baseUrl) {
|
|
|
128
128
|
totalRules,
|
|
129
129
|
warnings
|
|
130
130
|
};
|
|
131
|
-
}, baseUrl, ALL_PROPERTIES);
|
|
131
|
+
}, { url: baseUrl, allProps: ALL_PROPERTIES });
|
|
132
132
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Extract component dimensions from page
|
|
10
|
-
* @param {Page} page -
|
|
10
|
+
* @param {Page} page - Playwright page
|
|
11
11
|
* @param {string} viewportName - 'desktop', 'tablet', or 'mobile'
|
|
12
12
|
* @returns {Promise<Object>} Dimension data for this viewport
|
|
13
13
|
*/
|
|
@@ -69,6 +69,59 @@ export async function extractComponentDimensions(page, viewportName) {
|
|
|
69
69
|
);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Section Detection Configuration
|
|
74
|
+
* These thresholds determine how elements are classified into page sections.
|
|
75
|
+
* Semantic tags (<header>, <footer>, etc.) always take priority over position.
|
|
76
|
+
* Position-based detection is used as fallback for non-semantic elements.
|
|
77
|
+
*/
|
|
78
|
+
const HERO_THRESHOLD = 0.25; // Elements in top 25% with height >300px → hero
|
|
79
|
+
const FOOTER_THRESHOLD = 0.85; // Elements below 85% of page height → footer
|
|
80
|
+
const SIDEBAR_MAX_WIDTH = 400; // Max px width for fixed/sticky sidebar detection
|
|
81
|
+
|
|
82
|
+
// Page dimensions for section context
|
|
83
|
+
const pageHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
|
|
84
|
+
const pageWidth = document.documentElement.clientWidth;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Detect section context for an element.
|
|
88
|
+
*
|
|
89
|
+
* Priority order (per validation decision):
|
|
90
|
+
* 1. Semantic HTML tags: <header>, <footer>, <aside>, <nav> (highest priority)
|
|
91
|
+
* 2. Ancestor semantic tags: element inside <header>, <footer>, etc.
|
|
92
|
+
* 3. Position-based heuristics: hero (top 25%), footer (bottom 15%)
|
|
93
|
+
* 4. Layout-based: fixed/sticky narrow elements → sidebar
|
|
94
|
+
* 5. Default: 'content'
|
|
95
|
+
*
|
|
96
|
+
* @param {Element} el - DOM element to classify
|
|
97
|
+
* @returns {string} Section context: 'header' | 'hero' | 'content' | 'sidebar' | 'footer' | 'nav'
|
|
98
|
+
*/
|
|
99
|
+
function detectSection(el) {
|
|
100
|
+
const rect = el.getBoundingClientRect();
|
|
101
|
+
const computed = window.getComputedStyle(el);
|
|
102
|
+
const yRatio = (rect.y + window.scrollY) / pageHeight;
|
|
103
|
+
|
|
104
|
+
// Semantic tags have priority (per validation decision)
|
|
105
|
+
const tag = el.tagName.toLowerCase();
|
|
106
|
+
if (tag === 'header' || el.closest('header')) return 'header';
|
|
107
|
+
if (tag === 'footer' || el.closest('footer')) return 'footer';
|
|
108
|
+
if (tag === 'aside' || el.closest('aside')) return 'sidebar';
|
|
109
|
+
if (tag === 'nav' || el.closest('nav')) return 'nav';
|
|
110
|
+
|
|
111
|
+
// Hero detection (large element in top 25%)
|
|
112
|
+
if (yRatio < HERO_THRESHOLD && rect.height > 300) return 'hero';
|
|
113
|
+
|
|
114
|
+
// Footer detection (bottom 15%)
|
|
115
|
+
if (yRatio > FOOTER_THRESHOLD) return 'footer';
|
|
116
|
+
|
|
117
|
+
// Sidebar detection (narrow fixed/sticky)
|
|
118
|
+
if ((computed.position === 'fixed' || computed.position === 'sticky') && rect.width < SIDEBAR_MAX_WIDTH) {
|
|
119
|
+
return 'sidebar';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return 'content';
|
|
123
|
+
}
|
|
124
|
+
|
|
72
125
|
// 1. Extract containers
|
|
73
126
|
const containerSelectors = [
|
|
74
127
|
'section', 'main', 'article', 'header', 'footer',
|
|
@@ -98,6 +151,7 @@ export async function extractComponentDimensions(page, viewportName) {
|
|
|
98
151
|
? `.${el.className.split(' ').filter(c => c && !c.includes(':')).slice(0, 2).join('.')}`
|
|
99
152
|
: el.tagName.toLowerCase();
|
|
100
153
|
dims.childCount = children.length;
|
|
154
|
+
dims.section = detectSection(el); // Add section context
|
|
101
155
|
|
|
102
156
|
if (children.length >= 2 && (dims.display === 'flex' || dims.display === 'grid')) {
|
|
103
157
|
const firstRect = children[0].getBoundingClientRect();
|
|
@@ -118,35 +172,53 @@ export async function extractComponentDimensions(page, viewportName) {
|
|
|
118
172
|
} catch (e) { /* ignore */ }
|
|
119
173
|
});
|
|
120
174
|
|
|
121
|
-
// 2. Extract typography
|
|
175
|
+
// 2. Extract typography (grouped by section context)
|
|
122
176
|
const typographySelectors = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'];
|
|
123
177
|
typographySelectors.forEach(tag => {
|
|
124
178
|
try {
|
|
125
179
|
const elements = document.querySelectorAll(tag);
|
|
126
|
-
if (elements.length
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
180
|
+
if (elements.length === 0) return;
|
|
181
|
+
|
|
182
|
+
const bySection = {}; // Group by section
|
|
183
|
+
|
|
184
|
+
for (const el of elements) {
|
|
185
|
+
const rect = el.getBoundingClientRect();
|
|
186
|
+
if (rect.width < 50 || rect.height < 10) continue;
|
|
187
|
+
|
|
188
|
+
const section = detectSection(el);
|
|
189
|
+
const dims = extractDimensions(el);
|
|
190
|
+
|
|
191
|
+
// Create section group if not exists
|
|
192
|
+
if (!bySection[section]) bySection[section] = [];
|
|
193
|
+
|
|
194
|
+
// Add to section group (limit 2 per section per tag for token efficiency)
|
|
195
|
+
if (bySection[section].length < 2) {
|
|
196
|
+
bySection[section].push({
|
|
197
|
+
selector: tag,
|
|
198
|
+
section,
|
|
199
|
+
fontSize: dims.fontSize,
|
|
200
|
+
fontWeight: dims.fontWeight,
|
|
201
|
+
lineHeight: dims.lineHeight,
|
|
202
|
+
letterSpacing: dims.letterSpacing,
|
|
203
|
+
color: dims.color,
|
|
204
|
+
marginTop: dims.marginTop,
|
|
205
|
+
marginBottom: dims.marginBottom,
|
|
206
|
+
textSample: el.textContent?.trim().slice(0, 40),
|
|
207
|
+
y: Math.round(rect.y + window.scrollY)
|
|
208
|
+
});
|
|
145
209
|
}
|
|
146
210
|
}
|
|
211
|
+
|
|
212
|
+
// Flatten section groups into typography array
|
|
213
|
+
for (const items of Object.values(bySection)) {
|
|
214
|
+
results.typography.push(...items);
|
|
215
|
+
}
|
|
147
216
|
} catch (e) { /* ignore */ }
|
|
148
217
|
});
|
|
149
218
|
|
|
219
|
+
// Sort typography by position for consistent output
|
|
220
|
+
results.typography.sort((a, b) => a.y - b.y);
|
|
221
|
+
|
|
150
222
|
// 3. Extract buttons
|
|
151
223
|
const buttonSelectors = [
|
|
152
224
|
'button', 'a[class*="btn"]', 'a[class*="button"]',
|