design-clone 1.1.1 ā 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 +42 -20
- package/SKILL.md +74 -0
- 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 +224 -2
- package/docs/codebase-summary.md +309 -0
- package/docs/design-clone-architecture.md +290 -45
- package/docs/pixel-perfect.md +35 -4
- package/docs/project-roadmap.md +382 -0
- package/docs/troubleshooting.md +5 -4
- package/package.json +12 -6
- 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__/__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 +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/animation-extractor.js +526 -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 +311 -7
- 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 +598 -0
- 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 +546 -0
- 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/__pycache__/env.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,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM Tree Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Traverse DOM tree hierarchically to capture structure,
|
|
5
|
+
* semantic landmarks, and parent-child relationships.
|
|
6
|
+
*
|
|
7
|
+
* Key features:
|
|
8
|
+
* - PreOrder traversal (parent before children)
|
|
9
|
+
* - W3C landmark detection (header, main, footer, nav, aside)
|
|
10
|
+
* - Section context mapping (hero, content, sidebar, footer)
|
|
11
|
+
* - Bidirectional parent-child refs
|
|
12
|
+
* - Configurable max depth (default: 8)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Constants
|
|
16
|
+
const MAX_DEPTH = 8;
|
|
17
|
+
const LANDMARK_TAGS = ['header', 'main', 'footer', 'nav', 'aside', 'section', 'article'];
|
|
18
|
+
const HEADING_TAGS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
|
19
|
+
|
|
20
|
+
// Section detection thresholds (ratios of page height/width)
|
|
21
|
+
const HERO_THRESHOLD = 0.15; // Top 15% of page is considered hero area
|
|
22
|
+
const FOOTER_THRESHOLD = 0.85; // Bottom 15% of page is considered footer area
|
|
23
|
+
const SIDEBAR_MAX_WIDTH = 400; // Max width in px for sidebar detection
|
|
24
|
+
const Y_POSITION_TOLERANCE = 5; // Tolerance in px for heading Y-position matching
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extract DOM tree hierarchy from page
|
|
28
|
+
* @param {import('playwright').Page} page - Playwright page
|
|
29
|
+
* @param {Object} options - Configuration options
|
|
30
|
+
* @param {number} [options.maxDepth=8] - Maximum traversal depth
|
|
31
|
+
* @param {boolean} [options.includeHidden=false] - Include hidden elements (useful for accessibility audits)
|
|
32
|
+
* @returns {Promise<Object>} DOMHierarchy with root, landmarks, headingTree, stats
|
|
33
|
+
*/
|
|
34
|
+
export async function extractDOMHierarchy(page, options = {}) {
|
|
35
|
+
const { maxDepth = MAX_DEPTH, includeHidden = false } = options;
|
|
36
|
+
const startTime = Date.now();
|
|
37
|
+
|
|
38
|
+
const result = await page.evaluate(({ maxDepth, includeHidden, LANDMARK_TAGS, HEADING_TAGS, HERO_THRESHOLD, FOOTER_THRESHOLD, SIDEBAR_MAX_WIDTH }) => {
|
|
39
|
+
// Page dimensions for section context
|
|
40
|
+
const pageHeight = Math.max(
|
|
41
|
+
document.body.scrollHeight,
|
|
42
|
+
document.documentElement.scrollHeight
|
|
43
|
+
);
|
|
44
|
+
const pageWidth = document.documentElement.clientWidth;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Detect semantic role of element
|
|
48
|
+
* Priority: ARIA role > semantic tag > class patterns
|
|
49
|
+
*/
|
|
50
|
+
function detectRole(el) {
|
|
51
|
+
const tag = el.tagName.toLowerCase();
|
|
52
|
+
const ariaRole = el.getAttribute('role');
|
|
53
|
+
|
|
54
|
+
// ARIA role takes precedence
|
|
55
|
+
if (ariaRole) return ariaRole;
|
|
56
|
+
|
|
57
|
+
// W3C landmarks - check nesting context
|
|
58
|
+
if (LANDMARK_TAGS.includes(tag)) {
|
|
59
|
+
const isTopLevel = !el.closest('main, section, article, aside');
|
|
60
|
+
if (tag === 'header' || tag === 'footer') {
|
|
61
|
+
return isTopLevel ? `${tag}-landmark` : `${tag}-section`;
|
|
62
|
+
}
|
|
63
|
+
return tag;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Headings
|
|
67
|
+
if (HEADING_TAGS.includes(tag)) {
|
|
68
|
+
return `heading-${tag.slice(1)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Content containers via class patterns
|
|
72
|
+
if (tag === 'div' || tag === 'span') {
|
|
73
|
+
const cls = (el.className || '').toString().toLowerCase();
|
|
74
|
+
if (cls.includes('container')) return 'container';
|
|
75
|
+
if (cls.includes('wrapper')) return 'wrapper';
|
|
76
|
+
if (cls.includes('card')) return 'card';
|
|
77
|
+
if (cls.includes('grid')) return 'grid';
|
|
78
|
+
if (cls.includes('hero')) return 'hero';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Detect section context based on semantic tags (priority) and position
|
|
86
|
+
*/
|
|
87
|
+
function detectSectionContext(el, yPos) {
|
|
88
|
+
// Semantic tags have priority (per validation)
|
|
89
|
+
const tag = el.tagName.toLowerCase();
|
|
90
|
+
if (tag === 'header' || el.closest('header')) return 'header';
|
|
91
|
+
if (tag === 'footer' || el.closest('footer')) return 'footer';
|
|
92
|
+
if (tag === 'aside' || el.closest('aside')) return 'sidebar';
|
|
93
|
+
if (tag === 'nav' || el.closest('nav')) return 'nav';
|
|
94
|
+
|
|
95
|
+
// Position-based fallback (when no semantic tag found)
|
|
96
|
+
const yRatio = yPos / pageHeight;
|
|
97
|
+
if (yRatio < HERO_THRESHOLD) return 'hero';
|
|
98
|
+
if (yRatio > FOOTER_THRESHOLD) return 'footer';
|
|
99
|
+
|
|
100
|
+
// Check for narrow fixed/sticky elements (sidebar pattern)
|
|
101
|
+
const computed = window.getComputedStyle(el);
|
|
102
|
+
const rect = el.getBoundingClientRect();
|
|
103
|
+
if ((computed.position === 'fixed' || computed.position === 'sticky') && rect.width < SIDEBAR_MAX_WIDTH) {
|
|
104
|
+
return 'sidebar';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return 'content';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* PreOrder DOM traversal
|
|
112
|
+
*/
|
|
113
|
+
function traverseDOM(el, depth, parentId, path) {
|
|
114
|
+
if (depth > maxDepth) return null;
|
|
115
|
+
|
|
116
|
+
const rect = el.getBoundingClientRect();
|
|
117
|
+
// Skip hidden elements unless includeHidden
|
|
118
|
+
if (!includeHidden && (rect.width === 0 && rect.height === 0)) return null;
|
|
119
|
+
|
|
120
|
+
const id = path.join('-');
|
|
121
|
+
const computed = window.getComputedStyle(el);
|
|
122
|
+
const yPos = rect.y + window.scrollY;
|
|
123
|
+
|
|
124
|
+
const node = {
|
|
125
|
+
id,
|
|
126
|
+
tagName: el.tagName.toLowerCase(),
|
|
127
|
+
depth,
|
|
128
|
+
role: detectRole(el),
|
|
129
|
+
section: detectSectionContext(el, yPos),
|
|
130
|
+
attributes: {
|
|
131
|
+
id: el.id || null,
|
|
132
|
+
className: el.className ? el.className.toString().split(' ').slice(0, 3).join(' ') : null,
|
|
133
|
+
role: el.getAttribute('role')
|
|
134
|
+
},
|
|
135
|
+
dimensions: {
|
|
136
|
+
width: Math.round(rect.width),
|
|
137
|
+
height: Math.round(rect.height),
|
|
138
|
+
x: Math.round(rect.x),
|
|
139
|
+
y: Math.round(yPos)
|
|
140
|
+
},
|
|
141
|
+
layout: {
|
|
142
|
+
display: computed.display,
|
|
143
|
+
position: computed.position !== 'static' ? computed.position : undefined,
|
|
144
|
+
flexDirection: computed.flexDirection !== 'row' ? computed.flexDirection : undefined,
|
|
145
|
+
gridTemplateColumns: computed.gridTemplateColumns !== 'none' ? computed.gridTemplateColumns : undefined
|
|
146
|
+
},
|
|
147
|
+
children: [],
|
|
148
|
+
parentId
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Recurse children (PreOrder)
|
|
152
|
+
let childIdx = 0;
|
|
153
|
+
for (const child of el.children) {
|
|
154
|
+
const childNode = traverseDOM(child, depth + 1, id, [...path, childIdx]);
|
|
155
|
+
if (childNode) {
|
|
156
|
+
node.children.push(childNode);
|
|
157
|
+
childIdx++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return node;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Build landmarks map from traversed tree
|
|
166
|
+
*/
|
|
167
|
+
function buildLandmarksMap(root) {
|
|
168
|
+
const landmarks = {
|
|
169
|
+
header: null,
|
|
170
|
+
main: null,
|
|
171
|
+
footer: null,
|
|
172
|
+
nav: [],
|
|
173
|
+
aside: []
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
function walk(node) {
|
|
177
|
+
if (!node) return;
|
|
178
|
+
|
|
179
|
+
switch (node.role) {
|
|
180
|
+
case 'header-landmark': landmarks.header = node; break;
|
|
181
|
+
case 'main': landmarks.main = node; break;
|
|
182
|
+
case 'footer-landmark': landmarks.footer = node; break;
|
|
183
|
+
case 'nav': landmarks.nav.push(node); break;
|
|
184
|
+
case 'aside': landmarks.aside.push(node); break;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
node.children.forEach(walk);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
walk(root);
|
|
191
|
+
return landmarks;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Build heading tree with section context and text
|
|
196
|
+
*/
|
|
197
|
+
function buildHeadingTree(root) {
|
|
198
|
+
const headings = [];
|
|
199
|
+
|
|
200
|
+
function walk(node, sectionContext) {
|
|
201
|
+
if (!node) return;
|
|
202
|
+
|
|
203
|
+
// Update section context based on landmarks
|
|
204
|
+
let ctx = sectionContext;
|
|
205
|
+
if (node.role === 'header-landmark') ctx = 'header';
|
|
206
|
+
else if (node.role === 'main') ctx = 'content';
|
|
207
|
+
else if (node.role === 'footer-landmark') ctx = 'footer';
|
|
208
|
+
else if (node.role === 'aside') ctx = 'sidebar';
|
|
209
|
+
else if (node.role === 'hero') ctx = 'hero';
|
|
210
|
+
|
|
211
|
+
// Use node's detected section as fallback
|
|
212
|
+
if (!ctx) ctx = node.section || 'content';
|
|
213
|
+
|
|
214
|
+
// Collect headings
|
|
215
|
+
if (node.role?.startsWith('heading-')) {
|
|
216
|
+
headings.push({
|
|
217
|
+
level: parseInt(node.role.slice(-1)),
|
|
218
|
+
section: ctx,
|
|
219
|
+
nodeId: node.id,
|
|
220
|
+
y: node.dimensions.y,
|
|
221
|
+
fontSize: null, // Set separately for perf
|
|
222
|
+
text: null // Set separately for perf
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
node.children.forEach(c => walk(c, ctx));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
walk(root, null);
|
|
230
|
+
return headings.sort((a, b) => a.y - b.y);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Execute traversal
|
|
234
|
+
const root = traverseDOM(document.body, 0, null, [0]);
|
|
235
|
+
const landmarks = buildLandmarksMap(root);
|
|
236
|
+
const headingTree = buildHeadingTree(root);
|
|
237
|
+
|
|
238
|
+
// Calculate stats
|
|
239
|
+
let totalNodes = 0, maxActualDepth = 0;
|
|
240
|
+
function countNodes(n) {
|
|
241
|
+
if (!n) return;
|
|
242
|
+
totalNodes++;
|
|
243
|
+
maxActualDepth = Math.max(maxActualDepth, n.depth);
|
|
244
|
+
n.children.forEach(countNodes);
|
|
245
|
+
}
|
|
246
|
+
countNodes(root);
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
root,
|
|
250
|
+
landmarks,
|
|
251
|
+
headingTree,
|
|
252
|
+
stats: {
|
|
253
|
+
totalNodes,
|
|
254
|
+
maxDepth: maxActualDepth,
|
|
255
|
+
landmarkCount: [landmarks.header, landmarks.main, landmarks.footer].filter(Boolean).length +
|
|
256
|
+
landmarks.nav.length + landmarks.aside.length,
|
|
257
|
+
pageHeight,
|
|
258
|
+
pageWidth
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
}, { maxDepth, includeHidden, LANDMARK_TAGS, HEADING_TAGS, HERO_THRESHOLD, FOOTER_THRESHOLD, SIDEBAR_MAX_WIDTH });
|
|
262
|
+
|
|
263
|
+
// Extract heading text and fontSize separately (reduces main traversal complexity)
|
|
264
|
+
const headingData = await page.evaluate(({ headingTree, yTolerance }) => {
|
|
265
|
+
return headingTree.map(h => {
|
|
266
|
+
// Find heading by its position (nodeId is path-based, harder to query)
|
|
267
|
+
const headings = document.querySelectorAll(`h${h.level}`);
|
|
268
|
+
for (const el of headings) {
|
|
269
|
+
const rect = el.getBoundingClientRect();
|
|
270
|
+
const yPos = Math.round(rect.y + window.scrollY);
|
|
271
|
+
// Match by Y position (within tolerance)
|
|
272
|
+
if (Math.abs(yPos - h.y) < yTolerance) {
|
|
273
|
+
const computed = window.getComputedStyle(el);
|
|
274
|
+
return {
|
|
275
|
+
...h,
|
|
276
|
+
text: el.textContent?.trim().slice(0, 60) || null,
|
|
277
|
+
fontSize: parseFloat(computed.fontSize) || null
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return h;
|
|
282
|
+
});
|
|
283
|
+
}, { headingTree: result.headingTree, yTolerance: Y_POSITION_TOLERANCE });
|
|
284
|
+
|
|
285
|
+
result.headingTree = headingData;
|
|
286
|
+
|
|
287
|
+
// Performance tracking
|
|
288
|
+
const duration = Date.now() - startTime;
|
|
289
|
+
if (duration > 500) {
|
|
290
|
+
console.error(`[WARN] DOM hierarchy extraction took ${duration}ms (>500ms target)`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
result.stats.extractionTimeMs = duration;
|
|
294
|
+
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export { MAX_DEPTH, LANDMARK_TAGS, HEADING_TAGS };
|