design-clone 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -34
- package/SKILL.md +69 -45
- package/bin/cli.js +22 -4
- package/bin/commands/clone-site.js +31 -171
- package/bin/commands/help.js +19 -6
- package/bin/commands/init.js +9 -86
- package/bin/commands/uninstall.js +105 -0
- package/bin/commands/update.js +70 -0
- package/bin/commands/verify.js +7 -14
- package/bin/utils/paths.js +28 -0
- package/bin/utils/validate.js +2 -22
- package/bin/utils/version.js +23 -0
- package/docs/code-standards.md +789 -0
- package/docs/codebase-summary.md +533 -286
- package/docs/index.md +74 -0
- package/docs/project-overview-pdr.md +797 -0
- package/docs/system-architecture.md +718 -0
- package/package.json +14 -17
- package/src/ai/prompts/design-tokens/basic.md +80 -0
- package/src/ai/prompts/design-tokens/section-with-css.md +41 -0
- package/src/ai/prompts/design-tokens/section.md +48 -0
- package/src/ai/prompts/design-tokens/with-css.md +87 -0
- package/src/ai/prompts/structure-analysis/basic.md +55 -0
- package/src/ai/prompts/structure-analysis/with-context.md +59 -0
- package/src/ai/prompts/structure-analysis/with-dimensions.md +63 -0
- package/src/ai/prompts/structure-analysis/with-hierarchy.md +73 -0
- package/src/ai/prompts/ux-audit/aggregation.md +42 -0
- package/src/ai/prompts/ux-audit/desktop.md +92 -0
- package/src/ai/prompts/ux-audit/mobile.md +93 -0
- package/src/ai/prompts/ux-audit/tablet.md +92 -0
- package/src/core/animation/animation-extractor-ast.js +183 -0
- package/src/core/animation/animation-extractor-output.js +152 -0
- package/src/core/animation/animation-extractor.js +178 -0
- package/src/core/animation/state-capture-detection.js +200 -0
- package/src/core/animation/state-capture.js +193 -0
- package/src/core/capture/browser-context-pool.js +96 -0
- package/src/core/capture/multi-page-screenshot-page.js +110 -0
- package/src/core/capture/multi-page-screenshot.js +208 -0
- package/src/core/capture/screenshot-extraction.js +186 -0
- package/src/core/capture/screenshot-helpers.js +175 -0
- package/src/core/capture/screenshot-orchestrator.js +174 -0
- package/src/core/capture/screenshot-viewport.js +93 -0
- package/src/core/capture/screenshot.js +192 -0
- package/src/core/content/content-counter-dom.js +191 -0
- package/src/core/content/content-counter.js +76 -0
- package/src/core/css/breakpoint-detector.js +66 -0
- package/src/core/css/chromium-defaults.json +23 -0
- package/src/core/css/computed-style-extractor.js +102 -0
- package/src/core/css/css-chunker.js +103 -0
- package/src/core/css/filter-css-dead-code.js +120 -0
- package/src/core/css/filter-css-html-analyzer.js +110 -0
- package/src/core/css/filter-css-selector-matcher.js +172 -0
- package/src/core/css/filter-css.js +206 -0
- package/src/core/css/merge-css-atrule-processor.js +158 -0
- package/src/core/css/merge-css-file-io.js +68 -0
- package/src/core/css/merge-css.js +148 -0
- package/src/core/detection/framework-detector-routing.js +68 -0
- package/src/core/detection/framework-detector-signals.js +65 -0
- package/src/core/detection/framework-detector.js +198 -0
- package/src/core/dimension/dimension-extractor-card-detector.js +82 -0
- package/src/core/dimension/dimension-extractor.js +317 -0
- package/src/core/dimension/dimension-output-ai-summary.js +111 -0
- package/src/core/dimension/dimension-output.js +173 -0
- package/src/core/dimension/dom-tree-analyzer-tree-builders.js +95 -0
- package/src/core/dimension/dom-tree-analyzer.js +191 -0
- package/src/core/discovery/app-state-snapshot-capture.js +195 -0
- package/src/core/discovery/app-state-snapshot-utils.js +178 -0
- package/src/core/discovery/app-state-snapshot.js +131 -0
- package/src/core/discovery/discover-pages-routes.js +84 -0
- package/src/core/discovery/discover-pages-utils.js +177 -0
- package/src/core/discovery/discover-pages.js +191 -0
- package/src/core/html/html-extractor-inline-styler.js +70 -0
- package/src/core/html/html-extractor.js +147 -0
- package/src/core/html/semantic-enhancer-mappings.js +200 -0
- package/src/core/html/semantic-enhancer-page.js +148 -0
- package/src/core/html/semantic-enhancer.js +135 -0
- package/src/core/links/rewrite-links-css-rewriter.js +53 -0
- package/src/core/links/rewrite-links.js +173 -0
- package/src/core/media/asset-validator.js +118 -0
- package/src/core/media/extract-assets-downloader.js +187 -0
- package/src/core/media/extract-assets-page-scraper.js +115 -0
- package/src/core/media/extract-assets.js +159 -0
- package/src/core/media/video-capture-convert.js +200 -0
- package/src/core/media/video-capture.js +201 -0
- package/src/core/{lazy-loader.js → page-prep/lazy-loader.js} +37 -39
- package/src/core/section/section-cropper-helpers.js +43 -0
- package/src/core/{section-cropper.js → section/section-cropper.js} +11 -88
- package/src/core/section/section-detector-strategies.js +139 -0
- package/src/core/section/section-detector-utils.js +100 -0
- package/src/core/section/section-detector.js +88 -0
- package/src/core/tests/test-section-cropper.js +2 -2
- package/src/core/tests/test-section-detector.js +2 -2
- package/src/post-process/enhance-assets.js +29 -4
- package/src/post-process/fetch-images-unsplash-client.js +123 -0
- package/src/post-process/fetch-images.js +60 -263
- package/src/post-process/inject-gosnap.js +88 -0
- package/src/post-process/inject-icons-svg-replacer.js +76 -0
- package/src/post-process/inject-icons.js +47 -200
- package/src/route-discoverers/base-discoverer-utils.js +137 -0
- package/src/route-discoverers/base-discoverer.js +29 -118
- package/src/route-discoverers/index.js +1 -1
- package/src/shared/config.js +38 -0
- package/src/shared/error-codes.js +31 -0
- package/src/shared/viewports.js +46 -0
- package/src/utils/browser.js +0 -7
- package/src/utils/helpers.js +4 -0
- package/src/utils/log.js +12 -0
- package/src/utils/playwright-loader.js +76 -0
- package/src/utils/playwright.js +3 -69
- package/src/utils/progress.js +32 -0
- package/src/verification/generate-audit-report-css-fixes.js +52 -0
- package/src/verification/generate-audit-report-sections.js +158 -0
- package/src/verification/generate-audit-report.js +5 -281
- package/src/verification/quality-scorer.js +92 -0
- package/src/verification/verify-footer-checks.js +103 -0
- package/src/verification/verify-footer-helpers.js +178 -0
- package/src/verification/verify-footer.js +23 -381
- package/src/verification/verify-header-checks.js +104 -0
- package/src/verification/verify-header-helpers.js +156 -0
- package/src/verification/verify-header.js +23 -365
- package/src/verification/verify-layout-report.js +101 -0
- package/src/verification/verify-layout.js +13 -259
- package/src/verification/verify-menu-checks.js +104 -0
- package/src/verification/verify-menu-helpers.js +112 -0
- package/src/verification/verify-menu.js +17 -285
- package/src/verification/verify-slider-checks.js +115 -0
- package/src/verification/verify-slider-constants.js +65 -0
- package/src/verification/verify-slider-helpers.js +164 -0
- package/src/verification/verify-slider.js +23 -414
- package/.env.example +0 -14
- package/docs/basic-clone.md +0 -63
- package/docs/cli-reference.md +0 -316
- package/docs/design-clone-architecture.md +0 -492
- package/docs/pixel-perfect.md +0 -117
- package/docs/project-roadmap.md +0 -382
- package/docs/troubleshooting.md +0 -170
- package/requirements.txt +0 -5
- 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 +0 -375
- package/src/ai/extract-design-tokens.py +0 -782
- package/src/ai/prompts/__init__.py +0 -2
- package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
- 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 +0 -316
- package/src/ai/prompts/structure_analysis.py +0 -592
- package/src/ai/prompts/ux_audit.py +0 -198
- package/src/ai/ux-audit.js +0 -596
- package/src/core/animation-extractor.js +0 -526
- package/src/core/app-state-snapshot.js +0 -511
- package/src/core/content-counter.js +0 -342
- package/src/core/design-tokens.js +0 -103
- package/src/core/dimension-extractor.js +0 -438
- package/src/core/dimension-output.js +0 -305
- package/src/core/discover-pages.js +0 -542
- package/src/core/dom-tree-analyzer.js +0 -298
- package/src/core/extract-assets.js +0 -468
- package/src/core/filter-css.js +0 -499
- package/src/core/framework-detector.js +0 -538
- package/src/core/html-extractor.js +0 -212
- package/src/core/merge-css.js +0 -407
- package/src/core/multi-page-screenshot.js +0 -380
- package/src/core/rewrite-links.js +0 -226
- package/src/core/screenshot.js +0 -701
- package/src/core/section-detector.js +0 -386
- package/src/core/semantic-enhancer.js +0 -492
- package/src/core/state-capture.js +0 -598
- package/src/core/video-capture.js +0 -546
- package/src/utils/__init__.py +0 -16
- package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
- package/src/utils/env.py +0 -134
- /package/src/core/{css-extractor.js → css/css-extractor.js} +0 -0
- /package/src/core/{cookie-handler.js → page-prep/cookie-handler.js} +0 -0
- /package/src/core/{page-readiness.js → page-prep/page-readiness.js} +0 -0
|
@@ -1,438 +0,0 @@
|
|
|
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 - Playwright 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
|
-
/**
|
|
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
|
-
|
|
125
|
-
// 1. Extract containers
|
|
126
|
-
const containerSelectors = [
|
|
127
|
-
'section', 'main', 'article', 'header', 'footer',
|
|
128
|
-
'[role="main"]', '[role="region"]',
|
|
129
|
-
'div[class*="container"]', 'div[class*="wrapper"]',
|
|
130
|
-
'div[class*="section"]', 'div[class*="content"]',
|
|
131
|
-
'div[class*="grid"]', 'div[class*="card"]'
|
|
132
|
-
];
|
|
133
|
-
|
|
134
|
-
const seenContainers = new Set();
|
|
135
|
-
containerSelectors.forEach(selector => {
|
|
136
|
-
try {
|
|
137
|
-
document.querySelectorAll(selector).forEach((el) => {
|
|
138
|
-
if (seenContainers.has(el)) return;
|
|
139
|
-
const rect = el.getBoundingClientRect();
|
|
140
|
-
if (rect.width < 100 || rect.height < 50) return;
|
|
141
|
-
|
|
142
|
-
const children = Array.from(el.children).filter(c => {
|
|
143
|
-
const cr = c.getBoundingClientRect();
|
|
144
|
-
return cr.width > 50 && cr.height > 30;
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
if (children.length >= 2) {
|
|
148
|
-
seenContainers.add(el);
|
|
149
|
-
const dims = extractDimensions(el);
|
|
150
|
-
dims.selector = el.className
|
|
151
|
-
? `.${el.className.split(' ').filter(c => c && !c.includes(':')).slice(0, 2).join('.')}`
|
|
152
|
-
: el.tagName.toLowerCase();
|
|
153
|
-
dims.childCount = children.length;
|
|
154
|
-
dims.section = detectSection(el); // Add section context
|
|
155
|
-
|
|
156
|
-
if (children.length >= 2 && (dims.display === 'flex' || dims.display === 'grid')) {
|
|
157
|
-
const firstRect = children[0].getBoundingClientRect();
|
|
158
|
-
const secondRect = children[1].getBoundingClientRect();
|
|
159
|
-
const calculatedGap = Math.round(
|
|
160
|
-
dims.flexDirection === 'column'
|
|
161
|
-
? secondRect.top - firstRect.bottom
|
|
162
|
-
: secondRect.left - firstRect.right
|
|
163
|
-
);
|
|
164
|
-
if (calculatedGap > 0 && calculatedGap < 200) {
|
|
165
|
-
dims.calculatedGap = calculatedGap;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
results.containers.push(cleanObject(dims));
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
} catch (e) { /* ignore */ }
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// 2. Extract typography (grouped by section context)
|
|
176
|
-
const typographySelectors = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'];
|
|
177
|
-
typographySelectors.forEach(tag => {
|
|
178
|
-
try {
|
|
179
|
-
const elements = document.querySelectorAll(tag);
|
|
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
|
-
});
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Flatten section groups into typography array
|
|
213
|
-
for (const items of Object.values(bySection)) {
|
|
214
|
-
results.typography.push(...items);
|
|
215
|
-
}
|
|
216
|
-
} catch (e) { /* ignore */ }
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// Sort typography by position for consistent output
|
|
220
|
-
results.typography.sort((a, b) => a.y - b.y);
|
|
221
|
-
|
|
222
|
-
// 3. Extract buttons
|
|
223
|
-
const buttonSelectors = [
|
|
224
|
-
'button', 'a[class*="btn"]', 'a[class*="button"]',
|
|
225
|
-
'[role="button"]', 'input[type="submit"]'
|
|
226
|
-
];
|
|
227
|
-
const seenButtons = new Set();
|
|
228
|
-
buttonSelectors.forEach(selector => {
|
|
229
|
-
try {
|
|
230
|
-
document.querySelectorAll(selector).forEach((el) => {
|
|
231
|
-
if (seenButtons.has(el)) return;
|
|
232
|
-
const rect = el.getBoundingClientRect();
|
|
233
|
-
if (rect.width < 40 || rect.height < 20) return;
|
|
234
|
-
if (results.buttons.length >= 10) return;
|
|
235
|
-
|
|
236
|
-
seenButtons.add(el);
|
|
237
|
-
const dims = extractDimensions(el);
|
|
238
|
-
results.buttons.push({
|
|
239
|
-
width: dims.width,
|
|
240
|
-
height: dims.height,
|
|
241
|
-
paddingTop: dims.paddingTop,
|
|
242
|
-
paddingRight: dims.paddingRight,
|
|
243
|
-
paddingBottom: dims.paddingBottom,
|
|
244
|
-
paddingLeft: dims.paddingLeft,
|
|
245
|
-
fontSize: dims.fontSize,
|
|
246
|
-
fontWeight: dims.fontWeight,
|
|
247
|
-
borderRadius: dims.borderRadius,
|
|
248
|
-
backgroundColor: dims.backgroundColor,
|
|
249
|
-
color: dims.color,
|
|
250
|
-
text: el.textContent?.trim().slice(0, 30)
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
} catch (e) { /* ignore */ }
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
// 4. Extract images
|
|
257
|
-
try {
|
|
258
|
-
document.querySelectorAll('img').forEach((el) => {
|
|
259
|
-
const rect = el.getBoundingClientRect();
|
|
260
|
-
if (rect.width < 80 || rect.height < 80) return;
|
|
261
|
-
if (results.images.length >= 15) return;
|
|
262
|
-
|
|
263
|
-
results.images.push({
|
|
264
|
-
width: Math.round(rect.width),
|
|
265
|
-
height: Math.round(rect.height),
|
|
266
|
-
aspectRatio: (rect.width / rect.height).toFixed(2),
|
|
267
|
-
x: Math.round(rect.x),
|
|
268
|
-
y: Math.round(rect.y + window.scrollY)
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
} catch (e) { /* ignore */ }
|
|
272
|
-
|
|
273
|
-
// 5. Card pattern detection
|
|
274
|
-
function calculateSimilarity(a, b) {
|
|
275
|
-
const widthSim = 1 - Math.abs(a.width - b.width) / Math.max(a.width, b.width, 1);
|
|
276
|
-
const heightSim = 1 - Math.abs(a.height - b.height) / Math.max(a.height, b.height, 1);
|
|
277
|
-
const marginA = a.marginTop + a.marginBottom;
|
|
278
|
-
const marginB = b.marginTop + b.marginBottom;
|
|
279
|
-
const marginSim = 1 - Math.abs(marginA - marginB) / Math.max(marginA, marginB, 1);
|
|
280
|
-
const radiusSim = a.borderRadius === b.borderRadius ? 1 : 0.5;
|
|
281
|
-
return (widthSim * 0.4) + (heightSim * 0.3) + (marginSim * 0.15) + (radiusSim * 0.15);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function detectLayoutType(elements) {
|
|
285
|
-
if (elements.length < 2) return 'single';
|
|
286
|
-
const yPositions = elements.map(el => el.y);
|
|
287
|
-
const xPositions = elements.map(el => el.x);
|
|
288
|
-
const yVariance = Math.max(...yPositions) - Math.min(...yPositions);
|
|
289
|
-
const xVariance = Math.max(...xPositions) - Math.min(...xPositions);
|
|
290
|
-
const avgHeight = elements.reduce((s, el) => s + el.height, 0) / elements.length;
|
|
291
|
-
const avgWidth = elements.reduce((s, el) => s + el.width, 0) / elements.length;
|
|
292
|
-
|
|
293
|
-
if (yVariance < avgHeight * 0.3 && xVariance > avgWidth) return 'row';
|
|
294
|
-
if (xVariance < avgWidth * 0.3 && yVariance > avgHeight) return 'column';
|
|
295
|
-
return 'grid';
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function calculateGap(elements, layout) {
|
|
299
|
-
if (elements.length < 2) return 0;
|
|
300
|
-
const sorted = layout === 'column'
|
|
301
|
-
? [...elements].sort((a, b) => a.y - b.y)
|
|
302
|
-
: [...elements].sort((a, b) => a.x - b.x);
|
|
303
|
-
|
|
304
|
-
let totalGap = 0, gapCount = 0;
|
|
305
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
306
|
-
const gap = layout === 'column'
|
|
307
|
-
? sorted[i].y - (sorted[i-1].y + sorted[i-1].height)
|
|
308
|
-
: sorted[i].x - (sorted[i-1].x + sorted[i-1].width);
|
|
309
|
-
if (gap > 0 && gap < 200) { totalGap += gap; gapCount++; }
|
|
310
|
-
}
|
|
311
|
-
return gapCount > 0 ? Math.round(totalGap / gapCount) : 0;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
let cardGroupId = 0;
|
|
315
|
-
results.containers.forEach(container => {
|
|
316
|
-
if (container.childCount >= 2) {
|
|
317
|
-
try {
|
|
318
|
-
const parent = document.querySelector(container.selector);
|
|
319
|
-
if (!parent) return;
|
|
320
|
-
|
|
321
|
-
const children = Array.from(parent.children).filter(c => {
|
|
322
|
-
const cr = c.getBoundingClientRect();
|
|
323
|
-
return cr.width > 80 && cr.height > 60;
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
if (children.length >= 2) {
|
|
327
|
-
const childDims = children.map(c => {
|
|
328
|
-
const cr = c.getBoundingClientRect();
|
|
329
|
-
const cs = window.getComputedStyle(c);
|
|
330
|
-
return {
|
|
331
|
-
width: Math.round(cr.width),
|
|
332
|
-
height: Math.round(cr.height),
|
|
333
|
-
x: Math.round(cr.x),
|
|
334
|
-
y: Math.round(cr.y),
|
|
335
|
-
paddingTop: parseFloat(cs.paddingTop) || 0,
|
|
336
|
-
paddingRight: parseFloat(cs.paddingRight) || 0,
|
|
337
|
-
paddingBottom: parseFloat(cs.paddingBottom) || 0,
|
|
338
|
-
paddingLeft: parseFloat(cs.paddingLeft) || 0,
|
|
339
|
-
marginTop: parseFloat(cs.marginTop) || 0,
|
|
340
|
-
marginBottom: parseFloat(cs.marginBottom) || 0,
|
|
341
|
-
borderRadius: cs.borderRadius,
|
|
342
|
-
boxShadow: cs.boxShadow !== 'none' ? cs.boxShadow : undefined,
|
|
343
|
-
backgroundColor: cs.backgroundColor !== 'rgba(0, 0, 0, 0)' ? cs.backgroundColor : undefined
|
|
344
|
-
};
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
const used = new Set();
|
|
348
|
-
const groups = [];
|
|
349
|
-
|
|
350
|
-
for (let i = 0; i < childDims.length; i++) {
|
|
351
|
-
if (used.has(i)) continue;
|
|
352
|
-
const group = [childDims[i]];
|
|
353
|
-
used.add(i);
|
|
354
|
-
|
|
355
|
-
for (let j = i + 1; j < childDims.length; j++) {
|
|
356
|
-
if (used.has(j)) continue;
|
|
357
|
-
if (calculateSimilarity(childDims[i], childDims[j]) >= 0.70) {
|
|
358
|
-
group.push(childDims[j]);
|
|
359
|
-
used.add(j);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (group.length >= 2) groups.push(group);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
groups.forEach(group => {
|
|
367
|
-
const avg = (arr, key) => Math.round(arr.reduce((s, el) => s + (el[key] || 0), 0) / arr.length);
|
|
368
|
-
const layout = detectLayoutType(group);
|
|
369
|
-
const gap = calculateGap(group, layout);
|
|
370
|
-
|
|
371
|
-
results.cards.push({
|
|
372
|
-
id: `card-group-${++cardGroupId}`,
|
|
373
|
-
parentSelector: container.selector,
|
|
374
|
-
count: group.length,
|
|
375
|
-
layout,
|
|
376
|
-
gap,
|
|
377
|
-
avgDimensions: {
|
|
378
|
-
width: avg(group, 'width'),
|
|
379
|
-
height: avg(group, 'height'),
|
|
380
|
-
paddingTop: avg(group, 'paddingTop'),
|
|
381
|
-
paddingRight: avg(group, 'paddingRight'),
|
|
382
|
-
paddingBottom: avg(group, 'paddingBottom'),
|
|
383
|
-
paddingLeft: avg(group, 'paddingLeft')
|
|
384
|
-
},
|
|
385
|
-
borderRadius: group[0].borderRadius !== '0px' ? group[0].borderRadius : undefined,
|
|
386
|
-
boxShadow: group[0].boxShadow,
|
|
387
|
-
backgroundColor: group[0].backgroundColor
|
|
388
|
-
});
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
} catch (e) { /* ignore */ }
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
// 6. Grid layouts
|
|
396
|
-
results.gridLayouts = [];
|
|
397
|
-
results.containers.forEach(container => {
|
|
398
|
-
if (container.display === 'grid' || container.display === 'flex') {
|
|
399
|
-
try {
|
|
400
|
-
const parent = document.querySelector(container.selector);
|
|
401
|
-
if (!parent) return;
|
|
402
|
-
const computed = window.getComputedStyle(parent);
|
|
403
|
-
const children = parent.children;
|
|
404
|
-
|
|
405
|
-
if (children.length >= 2) {
|
|
406
|
-
if (computed.display === 'grid') {
|
|
407
|
-
const columns = computed.gridTemplateColumns;
|
|
408
|
-
const colCount = columns && columns !== 'none'
|
|
409
|
-
? columns.split(' ').filter(c => c && c !== 'none').length
|
|
410
|
-
: Math.ceil(children.length / 2);
|
|
411
|
-
|
|
412
|
-
results.gridLayouts.push({
|
|
413
|
-
selector: container.selector,
|
|
414
|
-
display: 'grid',
|
|
415
|
-
columns: colCount,
|
|
416
|
-
rows: Math.ceil(children.length / colCount),
|
|
417
|
-
columnGap: parseFloat(computed.columnGap) || parseFloat(computed.gap) || 0,
|
|
418
|
-
rowGap: parseFloat(computed.rowGap) || parseFloat(computed.gap) || 0,
|
|
419
|
-
childCount: children.length
|
|
420
|
-
});
|
|
421
|
-
} else if (computed.display === 'flex') {
|
|
422
|
-
results.gridLayouts.push({
|
|
423
|
-
selector: container.selector,
|
|
424
|
-
display: 'flex',
|
|
425
|
-
flexDirection: computed.flexDirection,
|
|
426
|
-
flexWrap: computed.flexWrap,
|
|
427
|
-
gap: parseFloat(computed.gap) || container.calculatedGap || 0,
|
|
428
|
-
childCount: children.length
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
} catch (e) { /* ignore */ }
|
|
433
|
-
}
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
return results;
|
|
437
|
-
}, viewportName);
|
|
438
|
-
}
|