designlang 3.0.0 → 4.0.1

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/src/clone.js ADDED
@@ -0,0 +1,218 @@
1
+ // Clone command — generate a working Next.js starter from extracted design
2
+
3
+ import { mkdirSync, writeFileSync } from 'fs';
4
+ import { join } from 'path';
5
+
6
+ export function generateClone(design, outDir) {
7
+ const projectDir = outDir;
8
+ mkdirSync(join(projectDir, 'src/app'), { recursive: true });
9
+ mkdirSync(join(projectDir, 'public'), { recursive: true });
10
+
11
+ const { colors, typography, spacing, borders, shadows } = design;
12
+
13
+ // Package.json
14
+ writeFileSync(join(projectDir, 'package.json'), JSON.stringify({
15
+ name: `${design.meta.title?.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').slice(0, 40) || 'cloned-design'}-clone`,
16
+ version: '0.1.0',
17
+ private: true,
18
+ scripts: {
19
+ dev: 'next dev',
20
+ build: 'next build',
21
+ start: 'next start',
22
+ },
23
+ dependencies: {
24
+ next: '^15.0.0',
25
+ react: '^19.0.0',
26
+ 'react-dom': '^19.0.0',
27
+ },
28
+ devDependencies: {
29
+ tailwindcss: '^4.0.0',
30
+ '@tailwindcss/postcss': '^4.0.0',
31
+ },
32
+ }, null, 2), 'utf-8');
33
+
34
+ // Globals CSS with extracted design tokens
35
+ const primaryHex = colors.primary?.hex || '#3b82f6';
36
+ const secondaryHex = colors.secondary?.hex || '#8b5cf6';
37
+ const accentHex = colors.accent?.hex || '#f59e0b';
38
+ const bgColor = colors.backgrounds[0] || '#ffffff';
39
+ const textColor = colors.text[0] || '#171717';
40
+ const fontFamily = typography.families[0]?.name || 'Inter';
41
+ const monoFont = typography.families.find(f => f.name.toLowerCase().includes('mono'))?.name || 'monospace';
42
+ const radiusMd = borders.radii.find(r => r.label === 'md')?.value || 8;
43
+ const shadowMd = shadows.values.find(s => s.label === 'md')?.raw || '0 4px 6px rgba(0,0,0,0.1)';
44
+
45
+ const neutrals = colors.neutrals.slice(0, 5);
46
+
47
+ writeFileSync(join(projectDir, 'src/app/globals.css'), `@import "tailwindcss";
48
+
49
+ :root {
50
+ --color-primary: ${primaryHex};
51
+ --color-secondary: ${secondaryHex};
52
+ --color-accent: ${accentHex};
53
+ --color-background: ${bgColor};
54
+ --color-foreground: ${textColor};
55
+ ${neutrals.map((n, i) => ` --color-neutral-${i + 1}: ${n.hex};`).join('\n')}
56
+ --font-sans: '${fontFamily}', system-ui, sans-serif;
57
+ --font-mono: '${monoFont}', monospace;
58
+ --radius: ${radiusMd}px;
59
+ --shadow: ${shadowMd};
60
+ }
61
+
62
+ body {
63
+ background: var(--color-background);
64
+ color: var(--color-foreground);
65
+ font-family: var(--font-sans);
66
+ }
67
+ `, 'utf-8');
68
+
69
+ // Layout
70
+ writeFileSync(join(projectDir, 'src/app/layout.js'), `export const metadata = {
71
+ title: '${(design.meta.title || 'Cloned Design').replace(/'/g, "\\'")}',
72
+ description: 'Design cloned from ${design.meta.url}',
73
+ };
74
+
75
+ export default function RootLayout({ children }) {
76
+ return (
77
+ <html lang="en">
78
+ <head>
79
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
80
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
81
+ <link href="https://fonts.googleapis.com/css2?family=${encodeURIComponent(fontFamily)}:wght@400;500;600;700&display=swap" rel="stylesheet" />
82
+ </head>
83
+ <body>{children}</body>
84
+ </html>
85
+ );
86
+ }
87
+ `, 'utf-8');
88
+
89
+ // Demo page showcasing the design system
90
+ const headingScale = typography.headings.slice(0, 3);
91
+ const bodySize = typography.body?.size || 16;
92
+ const spacingVals = spacing.scale.slice(0, 8);
93
+
94
+ writeFileSync(join(projectDir, 'src/app/page.js'), `import './globals.css';
95
+
96
+ export default function Home() {
97
+ return (
98
+ <main style={{ maxWidth: '1200px', margin: '0 auto', padding: '48px 24px' }}>
99
+ {/* Hero */}
100
+ <section style={{ textAlign: 'center', padding: '80px 0' }}>
101
+ <h1 style={{
102
+ fontSize: '${headingScale[0]?.size || 48}px',
103
+ fontWeight: ${headingScale[0]?.weight || 700},
104
+ lineHeight: '${headingScale[0]?.lineHeight || '1.1'}',
105
+ color: 'var(--color-foreground)',
106
+ marginBottom: '16px',
107
+ }}>
108
+ Design System Clone
109
+ </h1>
110
+ <p style={{
111
+ fontSize: '${bodySize + 4}px',
112
+ color: 'var(--color-neutral-1)',
113
+ maxWidth: '600px',
114
+ margin: '0 auto 32px',
115
+ }}>
116
+ Extracted from <a href="${design.meta.url}" style={{ color: 'var(--color-primary)' }}>${design.meta.url}</a>
117
+ </p>
118
+ <div style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}>
119
+ <button style={{
120
+ background: 'var(--color-primary)',
121
+ color: '#fff',
122
+ border: 'none',
123
+ padding: '12px 24px',
124
+ borderRadius: 'var(--radius)',
125
+ fontSize: '${bodySize}px',
126
+ fontWeight: 500,
127
+ cursor: 'pointer',
128
+ }}>
129
+ Primary Button
130
+ </button>
131
+ <button style={{
132
+ background: 'transparent',
133
+ color: 'var(--color-foreground)',
134
+ border: '1px solid var(--color-neutral-${Math.min(neutrals.length, 3)})',
135
+ padding: '12px 24px',
136
+ borderRadius: 'var(--radius)',
137
+ fontSize: '${bodySize}px',
138
+ fontWeight: 500,
139
+ cursor: 'pointer',
140
+ }}>
141
+ Secondary Button
142
+ </button>
143
+ </div>
144
+ </section>
145
+
146
+ {/* Color Palette */}
147
+ <section style={{ padding: '48px 0' }}>
148
+ <h2 style={{ fontSize: '${headingScale[1]?.size || 24}px', fontWeight: 600, marginBottom: '24px' }}>Color Palette</h2>
149
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gap: '12px' }}>
150
+ <div style={{ background: 'var(--color-primary)', borderRadius: 'var(--radius)', padding: '40px 16px 12px', color: '#fff', fontSize: '12px' }}>Primary<br/>${primaryHex}</div>
151
+ <div style={{ background: 'var(--color-secondary)', borderRadius: 'var(--radius)', padding: '40px 16px 12px', color: '#fff', fontSize: '12px' }}>Secondary<br/>${secondaryHex}</div>
152
+ <div style={{ background: 'var(--color-accent)', borderRadius: 'var(--radius)', padding: '40px 16px 12px', color: '#fff', fontSize: '12px' }}>Accent<br/>${accentHex}</div>
153
+ ${neutrals.map((n, i) => ` <div style={{ background: '${n.hex}', borderRadius: 'var(--radius)', padding: '40px 16px 12px', color: '${n.hsl.l > 50 ? '#000' : '#fff'}', fontSize: '12px' }}>Neutral ${i + 1}<br/>${n.hex}</div>`).join('\n')}
154
+ </div>
155
+ </section>
156
+
157
+ {/* Typography */}
158
+ <section style={{ padding: '48px 0' }}>
159
+ <h2 style={{ fontSize: '${headingScale[1]?.size || 24}px', fontWeight: 600, marginBottom: '24px' }}>Typography</h2>
160
+ ${headingScale.map((h, i) => ` <p style={{ fontSize: '${h.size}px', fontWeight: ${h.weight}, lineHeight: '${h.lineHeight}', marginBottom: '16px' }}>Heading ${i + 1} — ${h.size}px / ${h.weight}</p>`).join('\n')}
161
+ <p style={{ fontSize: '${bodySize}px', lineHeight: '1.6', color: 'var(--color-neutral-1)', marginTop: '24px' }}>
162
+ Body text at ${bodySize}px. This is what most content on the site looks like.
163
+ The quick brown fox jumps over the lazy dog. Pack my box with five dozen liquor jugs.
164
+ </p>
165
+ </section>
166
+
167
+ {/* Cards */}
168
+ <section style={{ padding: '48px 0' }}>
169
+ <h2 style={{ fontSize: '${headingScale[1]?.size || 24}px', fontWeight: 600, marginBottom: '24px' }}>Cards</h2>
170
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '24px' }}>
171
+ {[1, 2, 3].map(i => (
172
+ <div key={i} style={{
173
+ background: 'var(--color-background)',
174
+ border: '1px solid var(--color-neutral-${Math.min(neutrals.length, 3)})',
175
+ borderRadius: 'var(--radius)',
176
+ padding: '24px',
177
+ boxShadow: 'var(--shadow)',
178
+ }}>
179
+ <h3 style={{ fontSize: '${(headingScale[2]?.size || 18)}px', fontWeight: 600, marginBottom: '8px' }}>Card Title {i}</h3>
180
+ <p style={{ fontSize: '${bodySize}px', color: 'var(--color-neutral-1)', lineHeight: '1.5' }}>
181
+ This card uses the extracted border radius, shadow, and spacing values from the original site.
182
+ </p>
183
+ </div>
184
+ ))}
185
+ </div>
186
+ </section>
187
+
188
+ {/* Footer */}
189
+ <footer style={{ padding: '48px 0', borderTop: '1px solid var(--color-neutral-${Math.min(neutrals.length, 3)})', marginTop: '48px', textAlign: 'center' }}>
190
+ <p style={{ fontSize: '${bodySize - 2}px', color: 'var(--color-neutral-1)' }}>
191
+ Design extracted from ${design.meta.url} with <a href="https://github.com/Manavarya09/design-extract" style={{ color: 'var(--color-primary)' }}>designlang</a>
192
+ </p>
193
+ </footer>
194
+ </main>
195
+ );
196
+ }
197
+ `, 'utf-8');
198
+
199
+ // Next config
200
+ writeFileSync(join(projectDir, 'next.config.mjs'), `/** @type {import('next').NextConfig} */
201
+ const nextConfig = {};
202
+ export default nextConfig;
203
+ `, 'utf-8');
204
+
205
+ // PostCSS config for Tailwind v4
206
+ writeFileSync(join(projectDir, 'postcss.config.mjs'), `const config = {
207
+ plugins: {
208
+ "@tailwindcss/postcss": {},
209
+ },
210
+ };
211
+ export default config;
212
+ `, 'utf-8');
213
+
214
+ return {
215
+ dir: projectDir,
216
+ files: ['package.json', 'src/app/globals.css', 'src/app/layout.js', 'src/app/page.js', 'next.config.mjs', 'postcss.config.mjs'],
217
+ };
218
+ }
package/src/crawler.js CHANGED
@@ -14,9 +14,11 @@ export async function crawlPage(url, options = {}) {
14
14
  });
15
15
  const page = await context.newPage();
16
16
 
17
- await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
17
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
18
+ // Wait for network to settle — but don't hang on sites with persistent connections
19
+ await page.waitForLoadState('networkidle').catch(() => {});
18
20
  if (wait > 0) await page.waitForTimeout(wait);
19
- await page.evaluate(() => document.fonts.ready);
21
+ await page.evaluate(() => document.fonts.ready).catch(() => {});
20
22
 
21
23
  const title = await page.title();
22
24
  const lightData = await extractPageData(page);
@@ -33,8 +35,9 @@ export async function crawlPage(url, options = {}) {
33
35
  const internalLinks = await discoverInternalLinks(page, url, depth);
34
36
  for (const link of internalLinks) {
35
37
  try {
36
- await page.goto(link, { waitUntil: 'networkidle', timeout: 20000 });
37
- await page.evaluate(() => document.fonts.ready);
38
+ await page.goto(link, { waitUntil: 'domcontentloaded', timeout: 20000 });
39
+ await page.waitForLoadState('networkidle').catch(() => {});
40
+ await page.evaluate(() => document.fonts.ready).catch(() => {});
38
41
  const pageData = await extractPageData(page);
39
42
  additionalPages.push({ url: link, data: pageData });
40
43
  } catch { /* skip failed pages */ }
@@ -50,8 +53,9 @@ export async function crawlPage(url, options = {}) {
50
53
  colorScheme: 'dark',
51
54
  });
52
55
  const darkPage = await darkContext.newPage();
53
- await darkPage.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
54
- await darkPage.evaluate(() => document.fonts.ready);
56
+ await darkPage.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
57
+ await darkPage.waitForLoadState('networkidle').catch(() => {});
58
+ await darkPage.evaluate(() => document.fonts.ready).catch(() => {});
55
59
  darkData = await extractPageData(darkPage);
56
60
  await darkContext.close();
57
61
  } else {
@@ -45,6 +45,92 @@ export function extractComponents(computedStyles) {
45
45
  };
46
46
  }
47
47
 
48
+ // Navigation
49
+ const navs = computedStyles.filter(el =>
50
+ el.tag === 'nav' || el.role === 'navigation' ||
51
+ /nav|navbar|header/i.test(el.classList)
52
+ );
53
+ if (navs.length > 0) {
54
+ components.navigation = {
55
+ count: navs.length,
56
+ baseStyle: mostCommonStyle(navs, ['backgroundColor', 'color', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight', 'position', 'boxShadow']),
57
+ };
58
+ }
59
+
60
+ // Footer
61
+ const footers = computedStyles.filter(el =>
62
+ el.tag === 'footer' || el.role === 'contentinfo' ||
63
+ /footer/i.test(el.classList)
64
+ );
65
+ if (footers.length > 0) {
66
+ components.footer = {
67
+ count: footers.length,
68
+ baseStyle: mostCommonStyle(footers, ['backgroundColor', 'color', 'paddingTop', 'paddingBottom', 'fontSize']),
69
+ };
70
+ }
71
+
72
+ // Modals / Dialogs
73
+ const modals = computedStyles.filter(el =>
74
+ el.tag === 'dialog' || el.role === 'dialog' || el.role === 'alertdialog' ||
75
+ /modal|dialog|overlay|popup/i.test(el.classList)
76
+ );
77
+ if (modals.length > 0) {
78
+ components.modals = {
79
+ count: modals.length,
80
+ baseStyle: mostCommonStyle(modals, ['backgroundColor', 'borderRadius', 'boxShadow', 'paddingTop', 'paddingRight', 'maxWidth']),
81
+ };
82
+ }
83
+
84
+ // Dropdowns / Menus
85
+ const dropdowns = computedStyles.filter(el =>
86
+ el.role === 'menu' || el.role === 'listbox' ||
87
+ /dropdown|menu|popover|combobox/i.test(el.classList)
88
+ );
89
+ if (dropdowns.length > 0) {
90
+ components.dropdowns = {
91
+ count: dropdowns.length,
92
+ baseStyle: mostCommonStyle(dropdowns, ['backgroundColor', 'borderRadius', 'boxShadow', 'borderColor', 'paddingTop']),
93
+ };
94
+ }
95
+
96
+ // Tables
97
+ const tables = computedStyles.filter(el => el.tag === 'table' || el.role === 'table');
98
+ const tableCells = computedStyles.filter(el => ['td', 'th'].includes(el.tag));
99
+ if (tables.length > 0 || tableCells.length > 10) {
100
+ components.tables = {
101
+ count: tables.length,
102
+ cellCount: tableCells.length,
103
+ baseStyle: {
104
+ ...mostCommonStyle(tables, ['borderColor', 'backgroundColor']),
105
+ cellStyle: mostCommonStyle(tableCells, ['paddingTop', 'paddingRight', 'borderColor', 'fontSize']),
106
+ },
107
+ };
108
+ }
109
+
110
+ // Badges / Tags / Pills
111
+ const badges = computedStyles.filter(el =>
112
+ /badge|tag|pill|chip|label/i.test(el.classList) &&
113
+ el.area < 5000 && el.area > 100
114
+ );
115
+ if (badges.length > 0) {
116
+ components.badges = {
117
+ count: badges.length,
118
+ baseStyle: mostCommonStyle(badges, ['backgroundColor', 'color', 'fontSize', 'fontWeight', 'paddingTop', 'paddingRight', 'borderRadius']),
119
+ };
120
+ }
121
+
122
+ // Avatars
123
+ const avatars = computedStyles.filter(el =>
124
+ /avatar/i.test(el.classList) ||
125
+ (el.tag === 'img' && el.borderRadius === '9999px' && el.area < 10000 && el.area > 400)
126
+ );
127
+ if (avatars.length > 0) {
128
+ components.avatars = {
129
+ count: avatars.length,
130
+ baseStyle: mostCommonStyle(avatars, ['borderRadius', 'backgroundColor']),
131
+ };
132
+ }
133
+
48
134
  return components;
49
135
  }
50
136
 
@@ -8,9 +8,10 @@ export async function captureInteractions(url, options = {}) {
8
8
  const context = await browser.newContext({ viewport: { width, height } });
9
9
  const page = await context.newPage();
10
10
 
11
- await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
11
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
12
+ await page.waitForLoadState('networkidle').catch(() => {});
12
13
  if (wait > 0) await page.waitForTimeout(wait);
13
- await page.evaluate(() => document.fonts.ready);
14
+ await page.evaluate(() => document.fonts.ready).catch(() => {});
14
15
 
15
16
  const results = { buttons: [], links: [], inputs: [] };
16
17
 
@@ -20,9 +20,10 @@ export async function captureResponsive(url, options = {}) {
20
20
  const page = await context.newPage();
21
21
 
22
22
  try {
23
- await page.goto(url, { waitUntil: 'networkidle', timeout: 20000 });
23
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 });
24
+ await page.waitForLoadState('networkidle').catch(() => {});
24
25
  if (wait > 0) await page.waitForTimeout(wait);
25
- await page.evaluate(() => document.fonts.ready);
26
+ await page.evaluate(() => document.fonts.ready).catch(() => {});
26
27
 
27
28
  const data = await page.evaluate(() => {
28
29
  const body = document.body;
@@ -0,0 +1,132 @@
1
+ // Design system scoring — rate consistency and quality
2
+
3
+ export function scoreDesignSystem(design) {
4
+ const scores = {};
5
+ const issues = [];
6
+
7
+ // 1. Color discipline (0-100)
8
+ // Fewer unique colors = more disciplined
9
+ const colorCount = design.colors.all.length;
10
+ if (colorCount <= 8) scores.colorDiscipline = 100;
11
+ else if (colorCount <= 15) scores.colorDiscipline = 85;
12
+ else if (colorCount <= 25) scores.colorDiscipline = 70;
13
+ else if (colorCount <= 40) scores.colorDiscipline = 50;
14
+ else { scores.colorDiscipline = 30; issues.push(`${colorCount} unique colors detected — consider consolidating to a tighter palette`); }
15
+
16
+ if (!design.colors.primary) {
17
+ scores.colorDiscipline -= 15;
18
+ issues.push('No clear primary brand color detected');
19
+ }
20
+
21
+ // 2. Typography consistency (0-100)
22
+ const fontCount = design.typography.families.length;
23
+ if (fontCount <= 2) scores.typographyConsistency = 100;
24
+ else if (fontCount <= 3) scores.typographyConsistency = 80;
25
+ else { scores.typographyConsistency = 50; issues.push(`${fontCount} font families — consider limiting to 2 (heading + body)`); }
26
+
27
+ const weightCount = design.typography.weights.length;
28
+ if (weightCount <= 3) scores.typographyConsistency = Math.min(scores.typographyConsistency, 100);
29
+ else if (weightCount <= 5) scores.typographyConsistency = Math.min(scores.typographyConsistency, 80);
30
+ else { scores.typographyConsistency -= 15; issues.push(`${weightCount} font weights in use — consider standardizing to 3 (regular, medium, bold)`); }
31
+
32
+ const scaleSize = design.typography.scale.length;
33
+ if (scaleSize <= 6) scores.typographyConsistency = Math.min(scores.typographyConsistency, 100);
34
+ else if (scaleSize <= 10) scores.typographyConsistency = Math.min(scores.typographyConsistency, 85);
35
+ else { scores.typographyConsistency -= 10; issues.push(`${scaleSize} distinct font sizes — consider a tighter type scale`); }
36
+
37
+ // 3. Spacing system (0-100)
38
+ if (design.spacing.base) {
39
+ scores.spacingSystem = 90;
40
+ // Check how many values fit the base
41
+ const fittingValues = design.spacing.scale.filter(v => v % design.spacing.base === 0).length;
42
+ const fitRatio = fittingValues / design.spacing.scale.length;
43
+ if (fitRatio >= 0.8) scores.spacingSystem = 100;
44
+ else if (fitRatio >= 0.6) scores.spacingSystem = 80;
45
+ else scores.spacingSystem = 65;
46
+ } else {
47
+ scores.spacingSystem = 40;
48
+ issues.push('No consistent spacing base unit detected — values appear arbitrary');
49
+ }
50
+
51
+ if (design.spacing.scale.length > 20) {
52
+ scores.spacingSystem -= 15;
53
+ issues.push(`${design.spacing.scale.length} unique spacing values — too many one-off values`);
54
+ }
55
+
56
+ // 4. Shadow consistency (0-100)
57
+ const shadowCount = design.shadows.values.length;
58
+ if (shadowCount === 0) scores.shadowConsistency = 80; // no shadows is fine
59
+ else if (shadowCount <= 4) scores.shadowConsistency = 100;
60
+ else if (shadowCount <= 8) scores.shadowConsistency = 75;
61
+ else { scores.shadowConsistency = 50; issues.push(`${shadowCount} unique shadows — consider a 3-level elevation scale (sm/md/lg)`); }
62
+
63
+ // 5. Border radius consistency (0-100)
64
+ const radiiCount = design.borders.radii.length;
65
+ if (radiiCount <= 3) scores.radiusConsistency = 100;
66
+ else if (radiiCount <= 5) scores.radiusConsistency = 85;
67
+ else if (radiiCount <= 8) scores.radiusConsistency = 65;
68
+ else { scores.radiusConsistency = 40; issues.push(`${radiiCount} unique border radii — standardize to 3-4 values`); }
69
+
70
+ // 6. Accessibility (from existing extractor)
71
+ scores.accessibility = design.accessibility?.score || 0;
72
+ if (design.accessibility?.failCount > 0) {
73
+ issues.push(`${design.accessibility.failCount} WCAG contrast failures`);
74
+ }
75
+
76
+ // 7. CSS variable usage (0-100)
77
+ const varCount = Object.values(design.variables).reduce((s, v) => s + Object.keys(v).length, 0);
78
+ if (varCount >= 20) scores.tokenization = 100;
79
+ else if (varCount >= 10) scores.tokenization = 75;
80
+ else if (varCount >= 1) scores.tokenization = 50;
81
+ else { scores.tokenization = 20; issues.push('No CSS custom properties found — design is not tokenized'); }
82
+
83
+ // Overall score (weighted average)
84
+ const weights = {
85
+ colorDiscipline: 20,
86
+ typographyConsistency: 20,
87
+ spacingSystem: 20,
88
+ shadowConsistency: 10,
89
+ radiusConsistency: 10,
90
+ accessibility: 15,
91
+ tokenization: 5,
92
+ };
93
+
94
+ let totalWeight = 0;
95
+ let weightedSum = 0;
96
+ for (const [key, weight] of Object.entries(weights)) {
97
+ if (scores[key] !== undefined) {
98
+ weightedSum += Math.max(0, scores[key]) * weight;
99
+ totalWeight += weight;
100
+ }
101
+ }
102
+
103
+ const overall = totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 0;
104
+
105
+ // Grade
106
+ let grade;
107
+ if (overall >= 90) grade = 'A';
108
+ else if (overall >= 80) grade = 'B';
109
+ else if (overall >= 70) grade = 'C';
110
+ else if (overall >= 60) grade = 'D';
111
+ else grade = 'F';
112
+
113
+ return {
114
+ overall,
115
+ grade,
116
+ scores,
117
+ issues,
118
+ strengths: getStrengths(scores),
119
+ };
120
+ }
121
+
122
+ function getStrengths(scores) {
123
+ const strengths = [];
124
+ if (scores.colorDiscipline >= 85) strengths.push('Tight, disciplined color palette');
125
+ if (scores.typographyConsistency >= 85) strengths.push('Consistent typography system');
126
+ if (scores.spacingSystem >= 85) strengths.push('Well-defined spacing scale');
127
+ if (scores.shadowConsistency >= 85) strengths.push('Clean elevation system');
128
+ if (scores.radiusConsistency >= 85) strengths.push('Consistent border radii');
129
+ if (scores.accessibility >= 90) strengths.push('Strong accessibility compliance');
130
+ if (scores.tokenization >= 75) strengths.push('Good CSS variable tokenization');
131
+ return strengths;
132
+ }
@@ -469,6 +469,37 @@ export function formatMarkdown(design) {
469
469
  }
470
470
  }
471
471
 
472
+ // ── Design Score ──
473
+ if (design.score) {
474
+ const s = design.score;
475
+ lines.push('## Design System Score');
476
+ lines.push('');
477
+ lines.push(`**Overall: ${s.overall}/100 (Grade: ${s.grade})**`);
478
+ lines.push('');
479
+ lines.push('| Category | Score |');
480
+ lines.push('|----------|-------|');
481
+ if (s.scores.colorDiscipline !== undefined) lines.push(`| Color Discipline | ${s.scores.colorDiscipline}/100 |`);
482
+ if (s.scores.typographyConsistency !== undefined) lines.push(`| Typography Consistency | ${s.scores.typographyConsistency}/100 |`);
483
+ if (s.scores.spacingSystem !== undefined) lines.push(`| Spacing System | ${s.scores.spacingSystem}/100 |`);
484
+ if (s.scores.shadowConsistency !== undefined) lines.push(`| Shadow Consistency | ${s.scores.shadowConsistency}/100 |`);
485
+ if (s.scores.radiusConsistency !== undefined) lines.push(`| Border Radius Consistency | ${s.scores.radiusConsistency}/100 |`);
486
+ if (s.scores.accessibility !== undefined) lines.push(`| Accessibility | ${s.scores.accessibility}/100 |`);
487
+ if (s.scores.tokenization !== undefined) lines.push(`| CSS Tokenization | ${s.scores.tokenization}/100 |`);
488
+ lines.push('');
489
+
490
+ if (s.strengths.length > 0) {
491
+ lines.push('**Strengths:** ' + s.strengths.join(', '));
492
+ lines.push('');
493
+ }
494
+ if (s.issues.length > 0) {
495
+ lines.push('**Issues:**');
496
+ for (const issue of s.issues) {
497
+ lines.push(`- ${issue}`);
498
+ }
499
+ lines.push('');
500
+ }
501
+ }
502
+
472
503
  // ── Quick Start ──
473
504
  lines.push('## Quick Start');
474
505
  lines.push('');
package/src/index.js CHANGED
@@ -10,6 +10,7 @@ import { extractAnimations } from './extractors/animations.js';
10
10
  import { extractComponents } from './extractors/components.js';
11
11
  import { extractAccessibility } from './extractors/accessibility.js';
12
12
  import { extractLayout } from './extractors/layout.js';
13
+ import { scoreDesignSystem } from './extractors/scoring.js';
13
14
 
14
15
  export async function extractDesignLanguage(url, options = {}) {
15
16
  const rawData = await crawlPage(url, options);
@@ -35,6 +36,7 @@ export async function extractDesignLanguage(url, options = {}) {
35
36
  accessibility: extractAccessibility(styles),
36
37
  layout: extractLayout(styles),
37
38
  componentScreenshots: rawData.componentScreenshots || {},
39
+ score: null, // populated below
38
40
  };
39
41
 
40
42
  if (rawData.dark) {
@@ -44,6 +46,8 @@ export async function extractDesignLanguage(url, options = {}) {
44
46
  };
45
47
  }
46
48
 
49
+ design.score = scoreDesignSystem(design);
50
+
47
51
  return design;
48
52
  }
49
53
 
@@ -61,3 +65,6 @@ export { captureResponsive } from './extractors/responsive.js';
61
65
  export { captureInteractions } from './extractors/interactions.js';
62
66
  export { syncDesign } from './sync.js';
63
67
  export { compareBrands, formatBrandMatrix, formatBrandMatrixHtml } from './multibrand.js';
68
+ export { generateClone } from './clone.js';
69
+ export { scoreDesignSystem } from './extractors/scoring.js';
70
+ export { watchSite } from './watch.js';
package/src/watch.js ADDED
@@ -0,0 +1,47 @@
1
+ // Watch command — monitor a site for design changes on a schedule
2
+
3
+ import { extractDesignLanguage } from './index.js';
4
+ import { saveSnapshot, getHistory } from './history.js';
5
+
6
+ export async function watchSite(url, options = {}) {
7
+ const { intervalMs = 3600000 } = options; // default 1 hour
8
+
9
+ const design = await extractDesignLanguage(url);
10
+ const history = getHistory(url);
11
+ const previous = history.length > 0 ? history[history.length - 1] : null;
12
+
13
+ const snapshot = saveSnapshot(design);
14
+ const changes = [];
15
+
16
+ if (previous) {
17
+ if (previous.colors.primary !== design.colors.primary?.hex) {
18
+ changes.push({ type: 'color', what: 'Primary color', from: previous.colors.primary, to: design.colors.primary?.hex });
19
+ }
20
+ if (previous.colors.secondary !== design.colors.secondary?.hex) {
21
+ changes.push({ type: 'color', what: 'Secondary color', from: previous.colors.secondary, to: design.colors.secondary?.hex });
22
+ }
23
+ if (previous.colors.count !== design.colors.all.length) {
24
+ changes.push({ type: 'color', what: 'Color count', from: String(previous.colors.count), to: String(design.colors.all.length) });
25
+ }
26
+ if (previous.typography.families.join(',') !== design.typography.families.map(f => f.name).join(',')) {
27
+ changes.push({ type: 'typography', what: 'Font families', from: previous.typography.families.join(', '), to: design.typography.families.map(f => f.name).join(', ') });
28
+ }
29
+ if (previous.a11yScore !== design.accessibility?.score) {
30
+ changes.push({ type: 'accessibility', what: 'A11y score', from: `${previous.a11yScore}%`, to: `${design.accessibility?.score}%` });
31
+ }
32
+ if (previous.spacing.base !== design.spacing.base) {
33
+ changes.push({ type: 'spacing', what: 'Spacing base', from: `${previous.spacing.base}px`, to: `${design.spacing.base}px` });
34
+ }
35
+ if (Math.abs(previous.cssVarCount - Object.values(design.variables).reduce((s, v) => s + Object.keys(v).length, 0)) > 10) {
36
+ const newCount = Object.values(design.variables).reduce((s, v) => s + Object.keys(v).length, 0);
37
+ changes.push({ type: 'tokens', what: 'CSS var count', from: String(previous.cssVarCount), to: String(newCount) });
38
+ }
39
+ }
40
+
41
+ return {
42
+ changes,
43
+ isFirstRun: !previous,
44
+ snapshot,
45
+ design,
46
+ };
47
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": "0.0.1",
3
+ "configurations": [
4
+ {
5
+ "name": "website",
6
+ "runtimeExecutable": "npm",
7
+ "runtimeArgs": ["run", "dev"],
8
+ "port": 3000
9
+ }
10
+ ]
11
+ }
@@ -0,0 +1,5 @@
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+ # This is NOT the Next.js you know
3
+
4
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
5
+ <!-- END:nextjs-agent-rules -->
@@ -0,0 +1 @@
1
+ @AGENTS.md