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.
Files changed (47) hide show
  1. package/.env.example +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -0
  4. package/SKILL.md +239 -0
  5. package/bin/cli.js +45 -0
  6. package/bin/commands/help.js +29 -0
  7. package/bin/commands/init.js +126 -0
  8. package/bin/commands/verify.js +99 -0
  9. package/bin/utils/copy.js +65 -0
  10. package/bin/utils/validate.js +122 -0
  11. package/docs/basic-clone.md +63 -0
  12. package/docs/cli-reference.md +94 -0
  13. package/docs/design-clone-architecture.md +247 -0
  14. package/docs/pixel-perfect.md +86 -0
  15. package/docs/troubleshooting.md +97 -0
  16. package/package.json +57 -0
  17. package/requirements.txt +5 -0
  18. package/src/ai/analyze-structure.py +305 -0
  19. package/src/ai/extract-design-tokens.py +439 -0
  20. package/src/ai/prompts/__init__.py +2 -0
  21. package/src/ai/prompts/design_tokens.py +183 -0
  22. package/src/ai/prompts/structure_analysis.py +273 -0
  23. package/src/core/cookie-handler.js +76 -0
  24. package/src/core/css-extractor.js +107 -0
  25. package/src/core/dimension-extractor.js +366 -0
  26. package/src/core/dimension-output.js +208 -0
  27. package/src/core/extract-assets.js +468 -0
  28. package/src/core/filter-css.js +499 -0
  29. package/src/core/html-extractor.js +102 -0
  30. package/src/core/lazy-loader.js +188 -0
  31. package/src/core/page-readiness.js +161 -0
  32. package/src/core/screenshot.js +380 -0
  33. package/src/post-process/enhance-assets.js +157 -0
  34. package/src/post-process/fetch-images.js +398 -0
  35. package/src/post-process/inject-icons.js +311 -0
  36. package/src/utils/__init__.py +16 -0
  37. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  38. package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
  39. package/src/utils/browser.js +103 -0
  40. package/src/utils/env.js +153 -0
  41. package/src/utils/env.py +134 -0
  42. package/src/utils/helpers.js +71 -0
  43. package/src/utils/puppeteer.js +281 -0
  44. package/src/verification/verify-layout.js +424 -0
  45. package/src/verification/verify-menu.js +422 -0
  46. package/templates/base.css +705 -0
  47. package/templates/base.html +293 -0
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Lazy Loading Utilities
3
+ *
4
+ * Functions to trigger lazy-loaded content including images,
5
+ * animations, and IntersectionObserver-based content.
6
+ */
7
+
8
+ // Constants
9
+ export const LAZY_LOAD_MAX_ITERATIONS = 15;
10
+ export const IMAGE_LOAD_TIMEOUT = 20000;
11
+
12
+ /**
13
+ * Force all lazy images to load
14
+ * - Sets loading="eager" on all images
15
+ * - Copies data-src to src if exists
16
+ * - Triggers IntersectionObserver by scrolling
17
+ * @param {Page} page - Puppeteer page
18
+ */
19
+ export async function forceLazyImages(page) {
20
+ return await page.evaluate(async () => {
21
+ const stats = { forced: 0, dataSrc: 0, eager: 0 };
22
+ const images = document.querySelectorAll('img');
23
+
24
+ for (const img of images) {
25
+ if (img.loading === 'lazy') {
26
+ img.loading = 'eager';
27
+ stats.eager++;
28
+ }
29
+
30
+ const dataSrc = img.dataset.src || img.dataset.lazySrc || img.getAttribute('data-lazy-src');
31
+ if (dataSrc && !img.src) {
32
+ img.src = dataSrc;
33
+ stats.dataSrc++;
34
+ }
35
+
36
+ img.scrollIntoView({ block: 'center', behavior: 'instant' });
37
+ stats.forced++;
38
+ }
39
+
40
+ // Handle background images with data attributes
41
+ document.querySelectorAll('[data-bg], [data-background]').forEach(el => {
42
+ const bgUrl = el.dataset.bg || el.dataset.background;
43
+ if (bgUrl) {
44
+ el.style.backgroundImage = `url(${bgUrl})`;
45
+ }
46
+ });
47
+
48
+ return stats;
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Force all hidden animated elements to be visible
54
+ * @param {Page} page - Puppeteer page
55
+ */
56
+ export async function forceAnimatedElementsVisible(page) {
57
+ return await page.evaluate(() => {
58
+ let forcedCount = 0;
59
+
60
+ document.querySelectorAll('[class*="appear"], [class*="fade"], [class*="animate"]').forEach(el => {
61
+ const rect = el.getBoundingClientRect();
62
+ const absoluteTop = rect.top + window.scrollY;
63
+
64
+ // Only force elements below the hero area (first 500px)
65
+ if (absoluteTop > 500) {
66
+ const style = getComputedStyle(el);
67
+ if (style.opacity === '0' || parseFloat(style.opacity) < 0.5) {
68
+ el.style.setProperty('opacity', '1', 'important');
69
+ el.style.setProperty('visibility', 'visible', 'important');
70
+ forcedCount++;
71
+ }
72
+ }
73
+ });
74
+
75
+ return { forcedCount };
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Trigger lazy loading by scrolling through entire page
81
+ * @param {Page} page - Puppeteer page
82
+ * @param {number} maxIterations - Max scroll iterations
83
+ * @param {number} scrollDelay - Pause time between scrolls
84
+ */
85
+ export async function triggerLazyLoad(page, maxIterations = 20, scrollDelay = 1500) {
86
+ return await page.evaluate(async (maxIter, pauseMs) => {
87
+ return new Promise(async (resolve) => {
88
+ const viewportHeight = window.innerHeight;
89
+ const totalHeight = document.body.scrollHeight;
90
+ const scrollStep = viewportHeight * 0.5;
91
+ const pauseTime = pauseMs;
92
+
93
+ let position = 0;
94
+ let iterations = 0;
95
+
96
+ // First pass: scroll through entire page
97
+ while (position < totalHeight && iterations < maxIter) {
98
+ window.scrollTo({ top: position, behavior: 'instant' });
99
+ await new Promise(r => setTimeout(r, pauseTime));
100
+ position += scrollStep;
101
+ iterations++;
102
+ }
103
+
104
+ // Scroll to bottom
105
+ window.scrollTo({ top: document.body.scrollHeight, behavior: 'instant' });
106
+ await new Promise(r => setTimeout(r, 1000));
107
+
108
+ // Second pass: scroll back up
109
+ position = document.body.scrollHeight;
110
+ while (position > 0) {
111
+ position -= scrollStep;
112
+ window.scrollTo({ top: Math.max(0, position), behavior: 'instant' });
113
+ await new Promise(r => setTimeout(r, 300));
114
+ }
115
+
116
+ // Return to top
117
+ window.scrollTo({ top: 0, behavior: 'instant' });
118
+ await new Promise(r => setTimeout(r, 1500));
119
+
120
+ if (window.scrollY !== 0) {
121
+ window.scrollTo({ top: 0, behavior: 'instant' });
122
+ await new Promise(r => setTimeout(r, 500));
123
+ }
124
+
125
+ resolve({
126
+ scrolled: iterations,
127
+ height: document.body.scrollHeight,
128
+ stableAt: iterations
129
+ });
130
+ });
131
+ }, maxIterations, scrollDelay);
132
+ }
133
+
134
+ /**
135
+ * Wait for all images to finish loading
136
+ * @param {Page} page - Puppeteer page
137
+ * @param {number} timeout - Max wait time
138
+ */
139
+ export async function waitForAllImages(page, timeout = IMAGE_LOAD_TIMEOUT) {
140
+ const imgStats = await page.evaluate(async (maxWait) => {
141
+ const startTime = Date.now();
142
+ const images = Array.from(document.querySelectorAll('img'));
143
+ const pendingImages = images.filter(img => img.src && !img.complete);
144
+
145
+ if (pendingImages.length === 0) {
146
+ return { loaded: images.length, pending: 0, timedOut: false };
147
+ }
148
+
149
+ const loadPromises = pendingImages.map(img => {
150
+ return new Promise((resolve) => {
151
+ if (img.complete) {
152
+ resolve(true);
153
+ return;
154
+ }
155
+
156
+ const checkComplete = () => {
157
+ if (img.complete || Date.now() - startTime > maxWait) {
158
+ resolve(img.complete);
159
+ } else {
160
+ setTimeout(checkComplete, 100);
161
+ }
162
+ };
163
+
164
+ img.onload = () => resolve(true);
165
+ img.onerror = () => resolve(false);
166
+ setTimeout(checkComplete, 100);
167
+ });
168
+ });
169
+
170
+ await Promise.all(loadPromises);
171
+
172
+ const stillPending = images.filter(img => img.src && !img.complete).length;
173
+ return {
174
+ loaded: images.length - stillPending,
175
+ pending: stillPending,
176
+ timedOut: Date.now() - startTime >= maxWait
177
+ };
178
+ }, timeout);
179
+
180
+ try {
181
+ await page.waitForNetworkIdle({ timeout: Math.min(timeout, 10000) });
182
+ } catch {
183
+ // Network didn't become idle, continue anyway
184
+ }
185
+
186
+ await new Promise(r => setTimeout(r, 1500));
187
+ return imgStats;
188
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Page Readiness Detection
3
+ *
4
+ * Utilities for detecting when a page has fully loaded and stabilized.
5
+ * Handles SPA hydration, CSS-in-JS, fonts, and animations.
6
+ */
7
+
8
+ // Page readiness constants
9
+ export const PAGE_READY_TIMEOUT = 45000; // 45s max wait for slow SPAs
10
+ export const DOM_STABLE_THRESHOLD = 800; // 800ms stability check
11
+ export const POST_READY_DELAY = 2000; // Extra animation buffer
12
+ export const FONT_LOAD_TIMEOUT = 5000; // Font loading timeout
13
+
14
+ // Loading indicator selectors
15
+ export const LOADING_SELECTORS = [
16
+ '.loading',
17
+ '[class*="loading"]',
18
+ '[class*="spinner"]',
19
+ '[class*="skeleton"]',
20
+ '[class*="placeholder"]'
21
+ ];
22
+
23
+ // Content presence selectors
24
+ export const CONTENT_SELECTORS = [
25
+ 'main',
26
+ 'article',
27
+ '[role="main"]',
28
+ '.StudioCanvas', // studio.site specific
29
+ '.sd.appear', // studio.site appeared elements
30
+ 'header nav a' // typical nav links
31
+ ];
32
+
33
+ /**
34
+ * Wait for fonts to finish loading
35
+ * @param {Page} page - Puppeteer page
36
+ * @param {number} timeout - Max wait time in ms
37
+ */
38
+ export async function waitForFontsLoaded(page, timeout = FONT_LOAD_TIMEOUT) {
39
+ try {
40
+ await page.evaluate(async (timeoutMs) => {
41
+ if (!document.fonts) return;
42
+ await Promise.race([
43
+ document.fonts.ready,
44
+ new Promise(resolve => setTimeout(resolve, timeoutMs))
45
+ ]);
46
+ }, timeout);
47
+ } catch {
48
+ // Font API not available or error, continue anyway
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Wait for styles to stabilize (no new style mutations)
54
+ * @param {Page} page - Puppeteer page
55
+ * @param {number} stableMs - How long to wait without changes
56
+ * @param {number} timeout - Max wait time in ms
57
+ */
58
+ export async function waitForStylesStable(page, stableMs = 500, timeout = 5000) {
59
+ try {
60
+ await page.evaluate(async (stable, max) => {
61
+ return new Promise((resolve) => {
62
+ let lastChange = Date.now();
63
+ let checkInterval;
64
+
65
+ const observer = new MutationObserver((mutations) => {
66
+ for (const m of mutations) {
67
+ if (m.target.tagName === 'STYLE' ||
68
+ m.target.tagName === 'LINK' ||
69
+ m.addedNodes.length > 0) {
70
+ lastChange = Date.now();
71
+ }
72
+ }
73
+ });
74
+
75
+ observer.observe(document.head, {
76
+ childList: true,
77
+ subtree: true,
78
+ characterData: true
79
+ });
80
+
81
+ const startTime = Date.now();
82
+ checkInterval = setInterval(() => {
83
+ const now = Date.now();
84
+ if (now - lastChange >= stable || now - startTime >= max) {
85
+ clearInterval(checkInterval);
86
+ observer.disconnect();
87
+ resolve();
88
+ }
89
+ }, 100);
90
+ });
91
+ }, stableMs, timeout);
92
+ } catch {
93
+ // Error in style stability check, continue anyway
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Wait for DOM to stabilize (element count unchanged)
99
+ * @param {Page} page - Puppeteer page
100
+ * @param {number} threshold - Stability duration in ms
101
+ * @param {number} timeout - Max wait time in ms
102
+ */
103
+ export async function waitForDomStable(page, threshold = DOM_STABLE_THRESHOLD, timeout = 10000) {
104
+ let lastCount = 0;
105
+ let stableTime = 0;
106
+ const checkInterval = 100;
107
+ const startTime = Date.now();
108
+
109
+ while (stableTime < threshold && (Date.now() - startTime) < timeout) {
110
+ const count = await page.evaluate(() => document.querySelectorAll('*').length);
111
+ stableTime = (count === lastCount) ? stableTime + checkInterval : 0;
112
+ lastCount = count;
113
+ await new Promise(r => setTimeout(r, checkInterval));
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Wait for page to be ready (loading complete, content visible)
119
+ * @param {Page} page - Puppeteer page
120
+ * @param {number} timeout - Max wait time
121
+ */
122
+ export async function waitForPageReady(page, timeout = PAGE_READY_TIMEOUT) {
123
+ const startTime = Date.now();
124
+ const initialCount = await page.evaluate(() => document.querySelectorAll('*').length);
125
+ const minContentElements = Math.max(100, initialCount * 2);
126
+
127
+ while (Date.now() - startTime < timeout) {
128
+ const result = await page.evaluate((loadingSels, contentSels, minElements) => {
129
+ const elementCount = document.querySelectorAll('*').length;
130
+
131
+ const loadingGone = loadingSels.every(sel => {
132
+ const el = document.querySelector(sel);
133
+ if (!el) return true;
134
+ const style = getComputedStyle(el);
135
+ return style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0';
136
+ });
137
+
138
+ const contentExists = contentSels.some(sel => {
139
+ const el = document.querySelector(sel);
140
+ return el && getComputedStyle(el).display !== 'none';
141
+ });
142
+
143
+ const hasEnoughElements = elementCount >= minElements;
144
+
145
+ return {
146
+ ready: (loadingGone && hasEnoughElements) || contentExists || hasEnoughElements,
147
+ elementCount,
148
+ loadingGone,
149
+ contentExists
150
+ };
151
+ }, LOADING_SELECTORS, CONTENT_SELECTORS, minContentElements);
152
+
153
+ if (result.ready) break;
154
+ await new Promise(r => setTimeout(r, 200));
155
+ }
156
+
157
+ await waitForDomStable(page, 300);
158
+ await waitForFontsLoaded(page);
159
+ await waitForStylesStable(page, 300, 3000);
160
+ await new Promise(r => setTimeout(r, POST_READY_DELAY));
161
+ }