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.
Files changed (70) hide show
  1. package/README.md +42 -20
  2. package/SKILL.md +74 -0
  3. package/bin/commands/clone-site.js +75 -10
  4. package/bin/commands/init.js +33 -1
  5. package/bin/commands/verify.js +5 -3
  6. package/bin/utils/validate.js +24 -8
  7. package/docs/cli-reference.md +224 -2
  8. package/docs/codebase-summary.md +309 -0
  9. package/docs/design-clone-architecture.md +290 -45
  10. package/docs/pixel-perfect.md +35 -4
  11. package/docs/project-roadmap.md +382 -0
  12. package/docs/troubleshooting.md +5 -4
  13. package/package.json +12 -6
  14. package/src/ai/__pycache__/analyze-structure.cpython-313.pyc +0 -0
  15. package/src/ai/__pycache__/extract-design-tokens.cpython-313.pyc +0 -0
  16. package/src/ai/analyze-structure.py +73 -3
  17. package/src/ai/extract-design-tokens.py +356 -13
  18. package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  19. package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
  20. package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
  21. package/src/ai/prompts/__pycache__/ux_audit.cpython-313.pyc +0 -0
  22. package/src/ai/prompts/design_tokens.py +133 -0
  23. package/src/ai/prompts/structure_analysis.py +329 -10
  24. package/src/ai/prompts/ux_audit.py +198 -0
  25. package/src/ai/ux-audit.js +596 -0
  26. package/src/core/animation-extractor.js +526 -0
  27. package/src/core/app-state-snapshot.js +511 -0
  28. package/src/core/content-counter.js +342 -0
  29. package/src/core/cookie-handler.js +1 -1
  30. package/src/core/css-extractor.js +4 -4
  31. package/src/core/dimension-extractor.js +93 -21
  32. package/src/core/dimension-output.js +103 -6
  33. package/src/core/discover-pages.js +242 -14
  34. package/src/core/dom-tree-analyzer.js +298 -0
  35. package/src/core/extract-assets.js +1 -1
  36. package/src/core/framework-detector.js +538 -0
  37. package/src/core/html-extractor.js +45 -4
  38. package/src/core/lazy-loader.js +7 -7
  39. package/src/core/multi-page-screenshot.js +9 -6
  40. package/src/core/page-readiness.js +8 -8
  41. package/src/core/screenshot.js +311 -7
  42. package/src/core/section-cropper.js +209 -0
  43. package/src/core/section-detector.js +386 -0
  44. package/src/core/semantic-enhancer.js +492 -0
  45. package/src/core/state-capture.js +598 -0
  46. package/src/core/tests/test-section-cropper.js +177 -0
  47. package/src/core/tests/test-section-detector.js +55 -0
  48. package/src/core/video-capture.js +546 -0
  49. package/src/route-discoverers/angular-discoverer.js +157 -0
  50. package/src/route-discoverers/astro-discoverer.js +123 -0
  51. package/src/route-discoverers/base-discoverer.js +242 -0
  52. package/src/route-discoverers/index.js +106 -0
  53. package/src/route-discoverers/next-discoverer.js +130 -0
  54. package/src/route-discoverers/nuxt-discoverer.js +138 -0
  55. package/src/route-discoverers/react-discoverer.js +139 -0
  56. package/src/route-discoverers/svelte-discoverer.js +109 -0
  57. package/src/route-discoverers/universal-discoverer.js +227 -0
  58. package/src/route-discoverers/vue-discoverer.js +118 -0
  59. package/src/utils/__init__.py +1 -1
  60. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  61. package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
  62. package/src/utils/browser.js +11 -37
  63. package/src/utils/playwright.js +213 -0
  64. package/src/verification/generate-audit-report.js +398 -0
  65. package/src/verification/verify-footer.js +493 -0
  66. package/src/verification/verify-header.js +486 -0
  67. package/src/verification/verify-layout.js +2 -2
  68. package/src/verification/verify-menu.js +4 -20
  69. package/src/verification/verify-slider.js +533 -0
  70. 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 };
@@ -364,7 +364,7 @@ async function extractAssets() {
364
364
  if (verbose) console.error(`\nšŸ“¦ Extracting assets from: ${args.url}\n`);
365
365
 
366
366
  await page.goto(args.url, {
367
- waitUntil: 'networkidle2',
367
+ waitUntil: 'networkidle',
368
368
  timeout: 30000
369
369
  });
370
370