designlang 3.0.0 → 4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  <p align="center">
2
2
  <h1 align="center">designlang</h1>
3
- <p align="center">Extract the complete design language from any website in seconds.</p>
3
+ <p align="center">Reverse-engineer any website's complete design system in one command.</p>
4
4
  </p>
5
5
 
6
6
  <p align="center">
@@ -11,11 +11,13 @@
11
11
 
12
12
  ---
13
13
 
14
- **designlang** crawls any website with a headless browser, extracts every computed style from the live DOM, and generates **8 output files** — including an AI-optimized markdown file, visual HTML preview, Tailwind config, Figma variables, React theme, shadcn/ui theme, W3C design tokens, and CSS custom properties.
14
+ <p align="center">
15
+ <img src="designlang.png" alt="designlang in action" width="100%">
16
+ </p>
15
17
 
16
- It also does **WCAG accessibility scoring**, **component screenshot capture**, **multi-page crawling**, **design comparison** between two sites, and **historical tracking** of how a site's design evolves over time.
18
+ **designlang** crawls any website with a headless browser, extracts every computed style from the live DOM, and generates **8 output files** including an AI-optimized markdown file, visual HTML preview, Tailwind config, React theme, shadcn/ui theme, Figma variables, W3C design tokens, and CSS custom properties.
17
19
 
18
- **No other tool does all of this from a single command.**
20
+ But unlike every other tool out there, it also extracts **layout patterns** (grids, flexbox, containers), captures **responsive behavior** across 4 breakpoints, records **interaction states** (hover, focus, active), scores **WCAG accessibility**, and lets you **compare multiple brands** or **sync live sites to local tokens**.
19
21
 
20
22
  ## Quick Start
21
23
 
@@ -23,18 +25,26 @@ It also does **WCAG accessibility scoring**, **component screenshot capture**, *
23
25
  npx designlang https://stripe.com
24
26
  ```
25
27
 
28
+ Get everything at once:
29
+
30
+ ```bash
31
+ npx designlang https://stripe.com --full
32
+ ```
33
+
26
34
  ## What You Get (8 Files)
27
35
 
28
36
  | File | What it is |
29
37
  |------|------------|
30
- | `*-design-language.md` | AI-optimized markdown — the full design system described for LLMs |
31
- | `*-preview.html` | Visual HTML report with swatches, type scale, shadows, a11y score |
32
- | `*-design-tokens.json` | [W3C Design Tokens](https://design-tokens.github.io/community-group/format/) for tooling |
33
- | `*-tailwind.config.js` | Drop-in Tailwind CSS theme extension |
34
- | `*-variables.css` | CSS custom properties ready to import |
38
+ | `*-design-language.md` | AI-optimized markdown — feed it to any LLM to recreate the design |
39
+ | `*-preview.html` | Visual report with swatches, type scale, shadows, a11y score |
40
+ | `*-design-tokens.json` | [W3C Design Tokens](https://design-tokens.github.io/community-group/format/) format |
41
+ | `*-tailwind.config.js` | Drop-in Tailwind CSS theme |
42
+ | `*-variables.css` | CSS custom properties |
35
43
  | `*-figma-variables.json` | Figma Variables import (with dark mode support) |
36
- | `*-theme.js` | React/CSS-in-JS theme object (Chakra, Stitches, Vanilla Extract) |
37
- | `*-shadcn-theme.css` | shadcn/ui globals.css theme variables |
44
+ | `*-theme.js` | React/CSS-in-JS theme (Chakra, Stitches, Vanilla Extract) |
45
+ | `*-shadcn-theme.css` | shadcn/ui globals.css variables |
46
+
47
+ The markdown output has **14 sections**: Color Palette, Typography, Spacing, Border Radii, Box Shadows, CSS Custom Properties, Breakpoints, Transitions & Animations, Component Patterns, Layout System, Responsive Design, Interaction States, Accessibility (WCAG 2.1), and Quick Start.
38
48
 
39
49
  ## Install
40
50
 
@@ -44,94 +54,96 @@ npx designlang https://example.com
44
54
 
45
55
  # Or install globally
46
56
  npm install -g designlang
47
- ```
48
57
 
49
- ## Features
50
-
51
- ### Multi-Page Crawling
58
+ # As an agent skill (Claude Code, Cursor, Codex, 40+ agents)
59
+ npx skills add Manavarya09/design-extract
60
+ ```
52
61
 
53
- Crawl multiple pages for a site-wide design system:
62
+ ## What Makes This Different
54
63
 
55
- ```bash
56
- designlang https://stripe.com --depth 5
57
- ```
64
+ Most design extraction tools give you colors and fonts. That's it. designlang fills 5 market gaps that no other tool addresses:
58
65
 
59
- ### WCAG Accessibility Scoring
66
+ ### 1. Layout System Extraction
60
67
 
61
- Every extraction includes a WCAG 2.1 contrast analysis:
68
+ Extracts the structural skeleton grid column patterns, flex direction usage, container widths, gap values, and justify/align patterns.
62
69
 
63
70
  ```
64
- A11y: 94% WCAG score (7 failing pairs)
71
+ Layout: 55 grids, 492 flex containers
65
72
  ```
66
73
 
67
- Failing color pairs are highlighted in both the markdown and HTML preview with exact contrast ratios.
74
+ Every other tool gives you the paint. designlang gives you the architecture.
68
75
 
69
- ### Component Screenshots
76
+ ### 2. Responsive Multi-Breakpoint Capture
70
77
 
71
- Capture PNG screenshots of detected UI components:
78
+ Crawls the site at 4 viewports (mobile, tablet, desktop, wide) and maps exactly what changes:
72
79
 
73
80
  ```bash
74
- designlang https://vercel.com --screenshots
81
+ designlang https://vercel.com --responsive
75
82
  ```
76
83
 
77
- Saves screenshots of buttons, cards, inputs, navigation, hero sections, and a full-page capture.
78
-
79
- ### Visual HTML Preview
84
+ ```
85
+ Responsive: 4 viewports, 3 breakpoint changes
86
+ 375px 768px: Nav visibility hidden → visible, Hamburger shown → hidden
87
+ 768px → 1280px: Max grid columns 1 → 3, H1 size 32px → 48px
88
+ ```
80
89
 
81
- Every run generates a `*-preview.html` file a gorgeous dark-themed report you can open in your browser with:
82
- - Color swatches for the full palette
83
- - Live type scale rendering
84
- - Spacing scale visualization
85
- - Shadow cards with actual CSS shadows
86
- - Accessibility score and failing pair analysis
87
- - Component screenshots (when `--screenshots` is used)
90
+ No other tool captures how the design *adapts*, just how it looks at one size.
88
91
 
89
- ### Design Comparison
92
+ ### 3. Interaction State Capture
90
93
 
91
- Compare two sites side-by-side:
94
+ Programmatically hovers and focuses interactive elements, capturing the actual style transitions:
92
95
 
93
96
  ```bash
94
- designlang diff https://vercel.com https://stripe.com
97
+ designlang https://stripe.com --interactions
95
98
  ```
96
99
 
97
- Generates `diff.md` and `diff.html` showing color, typography, spacing, and accessibility differences.
100
+ ```css
101
+ /* Button Hover */
102
+ background-color: rgb(83, 58, 253) → rgb(67, 47, 202);
103
+ box-shadow: none → 0 4px 12px rgba(83, 58, 253, 0.4);
98
104
 
99
- ### Historical Tracking
105
+ /* Input Focus */
106
+ border-color: rgb(200, 200, 200) → rgb(83, 58, 253);
107
+ outline: none → 2px solid rgb(83, 58, 253);
108
+ ```
100
109
 
101
- Track how a site's design evolves over time:
110
+ ### 4. Live Site Sync
102
111
 
103
- ```bash
104
- # Each extraction auto-saves a snapshot
105
- designlang https://stripe.com
112
+ Treat the deployed site as your source of truth, not Figma:
106
113
 
107
- # View history
108
- designlang history https://stripe.com
114
+ ```bash
115
+ designlang sync https://stripe.com --out ./src/tokens
109
116
  ```
110
117
 
111
- Shows color changes, font swaps, accessibility score trends, and CSS variable count over time.
112
-
113
- ### Framework Themes
118
+ Detects design changes and auto-updates your local `design-tokens.json`, `tailwind.config.js`, and `variables.css`.
114
119
 
115
- Generates ready-to-use theme files for:
120
+ ### 5. Multi-Brand Comparison
116
121
 
117
- - **React/CSS-in-JS** — theme object compatible with Chakra UI, Stitches, Vanilla Extract
118
- - **shadcn/ui** — CSS variables in the exact format shadcn expects (paste into globals.css)
119
- - **Tailwind** — full theme extension with colors, fonts, spacing, radii, shadows, screens
122
+ Compare N brands side-by-side:
120
123
 
121
- ## What It Extracts
124
+ ```bash
125
+ designlang brands stripe.com vercel.com github.com linear.app
126
+ ```
122
127
 
123
- | Category | Details |
124
- |----------|---------|
125
- | **Colors** | Full palette with primary/secondary/accent/neutral classification, gradients |
126
- | **Typography** | Font families, type scale, heading/body styles, weight distribution |
127
- | **Spacing** | All unique values with automatic base-unit detection (4px/8px grid) |
128
- | **Border Radii** | Unique values labeled xs through full |
129
- | **Box Shadows** | Parsed and classified by visual weight |
130
- | **CSS Variables** | All `:root` custom properties, categorized |
131
- | **Breakpoints** | Media query breakpoints with standard labels |
132
- | **Animations** | Transitions, easing functions, durations, `@keyframes` |
133
- | **Components** | Buttons, cards, inputs, links with base styles |
134
- | **Accessibility** | WCAG 2.1 contrast ratios for all fg/bg color pairs |
128
+ Generates a matrix with color overlap analysis, typography comparison, spacing systems, and accessibility scores. Outputs both `brands.md` and `brands.html`.
129
+
130
+ ## All Features
131
+
132
+ | Feature | Flag / Command | Description |
133
+ |---------|---------------|-------------|
134
+ | Base extraction | `designlang <url>` | Colors, typography, spacing, shadows, radii, CSS vars, breakpoints, animations, components |
135
+ | Layout system | automatic | Grid patterns, flex usage, container widths, gap values |
136
+ | Accessibility | automatic | WCAG 2.1 contrast ratios for all fg/bg pairs |
137
+ | Dark mode | `--dark` | Extracts dark color scheme |
138
+ | Multi-page | `--depth <n>` | Crawl N internal pages for site-wide tokens |
139
+ | Screenshots | `--screenshots` | Capture buttons, cards, inputs, nav, hero, full page |
140
+ | Responsive | `--responsive` | Crawl at 4 viewports, map breakpoint changes |
141
+ | Interactions | `--interactions` | Capture hover/focus/active state transitions |
142
+ | Everything | `--full` | Enable screenshots + responsive + interactions |
143
+ | Diff | `designlang diff <A> <B>` | Compare two sites (MD + HTML) |
144
+ | Multi-brand | `designlang brands <urls...>` | N-site comparison matrix |
145
+ | Sync | `designlang sync <url>` | Update local tokens from live site |
146
+ | History | `designlang history <url>` | Track design changes over time |
135
147
 
136
148
  ## Full CLI Reference
137
149
 
@@ -145,29 +157,78 @@ Options:
145
157
  --height <px> Viewport height (default: 800)
146
158
  --wait <ms> Wait after page load for SPAs (default: 0)
147
159
  --dark Also extract dark mode styles
148
- --depth <n> Pages to crawl (default: 0, just the URL)
160
+ --depth <n> Internal pages to crawl (default: 0)
149
161
  --screenshots Capture component screenshots
162
+ --responsive Capture at multiple breakpoints
163
+ --interactions Capture hover/focus/active states
164
+ --full Enable all captures
150
165
  --framework <type> Only generate specific theme (react, shadcn)
151
166
  --no-history Skip saving to history
152
167
  --verbose Detailed progress output
153
168
 
154
169
  Commands:
155
170
  diff <urlA> <urlB> Compare two sites' design languages
156
- history <url> View design history for a site
171
+ brands <urls...> Multi-brand comparison matrix
172
+ sync <url> Sync local tokens with live site
173
+ history <url> View design change history
174
+ ```
175
+
176
+ ## Example Output
177
+
178
+ Running `designlang https://vercel.com --full`:
179
+
180
+ ```
181
+ designlang
182
+ https://vercel.com
183
+
184
+ Output files:
185
+ ✓ vercel-com-design-language.md (32.6KB)
186
+ ✓ vercel-com-design-tokens.json (5.6KB)
187
+ ✓ vercel-com-tailwind.config.js (3.4KB)
188
+ ✓ vercel-com-variables.css (18.6KB)
189
+ ✓ vercel-com-preview.html (31.8KB)
190
+ ✓ vercel-com-figma-variables.json (12.4KB)
191
+ ✓ vercel-com-theme.js (1.4KB)
192
+ ✓ vercel-com-shadcn-theme.css (477B)
193
+ ✓ screenshots/button.png
194
+ ✓ screenshots/card.png
195
+ ✓ screenshots/nav.png
196
+ ✓ screenshots/hero.png
197
+ ✓ screenshots/full-page.png
198
+
199
+ Summary:
200
+ Colors: 27 unique colors
201
+ Fonts: Geist, Geist Mono
202
+ Spacing: 18 values (base: 2px)
203
+ Shadows: 11 unique shadows
204
+ Radii: 10 unique values
205
+ Breakpoints: 45 breakpoints
206
+ Components: 4 patterns detected
207
+ CSS Vars: 407 custom properties
208
+ Layout: 55 grids, 492 flex containers
209
+ Responsive: 4 viewports, 3 breakpoint changes
210
+ Interactions: 8 state changes captured
211
+ A11y: 94% WCAG score (7 failing pairs)
157
212
  ```
158
213
 
159
214
  ## How It Works
160
215
 
161
- 1. **Crawl** — Launches headless Chromium via Playwright
162
- 2. **Extract** — `page.evaluate()` walks up to 5,000 DOM elements collecting computed styles
163
- 3. **Process** — 10 extractor modules parse, deduplicate, cluster, and classify the raw data
164
- 4. **Format** — 8 formatter modules generate the output files
165
- 5. **Score** — Accessibility extractor calculates WCAG contrast ratios
166
- 6. **Capture** — Optional Playwright screenshots of detected components
216
+ 1. **Crawl** — Launches headless Chromium via Playwright, waits for network idle and fonts
217
+ 2. **Extract** — Single `page.evaluate()` walks up to 5,000 DOM elements collecting 25+ computed style properties including layout (grid, flex, container) data
218
+ 3. **Process** — 12 extractor modules parse, deduplicate, cluster, and classify the raw data
219
+ 4. **Format** — 8 formatter modules generate output files
220
+ 5. **Score** — Accessibility extractor calculates WCAG contrast ratios for all color pairs
221
+ 6. **Capture** — Optional: screenshots, responsive viewport crawling, interaction state recording
222
+
223
+ ## Agent Skill
167
224
 
168
- ## Claude Code Plugin
225
+ Works with **Claude Code, Cursor, Codex, and 40+ AI coding agents** via the skills ecosystem:
226
+
227
+ ```bash
228
+ npx skills add Manavarya09/design-extract
229
+ ```
169
230
 
170
- **designlang** also works as a [Claude Code](https://claude.ai/claude-code) plugin. Use `/extract-design <url>` in your coding session.
231
+ In Claude Code, use `/extract-design <url>`.
171
232
 
172
233
  ## Contributing
173
234
 
@@ -19,6 +19,8 @@ import { captureResponsive } from '../src/extractors/responsive.js';
19
19
  import { captureInteractions } from '../src/extractors/interactions.js';
20
20
  import { syncDesign } from '../src/sync.js';
21
21
  import { compareBrands, formatBrandMatrix, formatBrandMatrixHtml } from '../src/multibrand.js';
22
+ import { generateClone } from '../src/clone.js';
23
+ import { watchSite } from '../src/watch.js';
22
24
  import { nameFromUrl } from '../src/utils.js';
23
25
 
24
26
  const program = new Command();
@@ -26,7 +28,7 @@ const program = new Command();
26
28
  program
27
29
  .name('designlang')
28
30
  .description('Extract the complete design language from any website')
29
- .version('3.0.0');
31
+ .version('4.0.0');
30
32
 
31
33
  // ── Main command: extract ──────────────────────────────────────
32
34
  program
@@ -155,6 +157,11 @@ program
155
157
  const total = ic.buttons.length + ic.links.length + ic.inputs.length;
156
158
  console.log(` ${chalk.gray('Interactions:')} ${total} state changes captured`);
157
159
  }
160
+ if (design.score) {
161
+ const s = design.score;
162
+ const gradeColor = s.grade === 'A' ? chalk.green : s.grade === 'B' ? chalk.cyan : s.grade === 'C' ? chalk.yellow : chalk.red;
163
+ console.log(` ${chalk.gray('Design Score:')} ${gradeColor(`${s.overall}/100 (${s.grade})`)}${s.issues.length > 0 ? ` — ${s.issues.length} issues` : ''}`);
164
+ }
158
165
 
159
166
  // Accessibility summary
160
167
  if (design.accessibility) {
@@ -338,4 +345,145 @@ program
338
345
  }
339
346
  });
340
347
 
348
+ // ── Clone command ───────────────────────────────────────────
349
+ program
350
+ .command('clone <url>')
351
+ .description('Generate a working Next.js starter from a site\'s design')
352
+ .option('-o, --out <dir>', 'output directory', './cloned-design')
353
+ .action(async (url, opts) => {
354
+ if (!url.startsWith('http')) url = `https://${url}`;
355
+
356
+ console.log('');
357
+ console.log(chalk.bold(' designlang clone'));
358
+ console.log(chalk.gray(` ${url}`));
359
+ console.log('');
360
+
361
+ const spinner = ora('Extracting design...').start();
362
+
363
+ try {
364
+ const design = await extractDesignLanguage(url);
365
+ spinner.text = 'Generating Next.js project...';
366
+
367
+ const result = generateClone(design, resolve(opts.out));
368
+
369
+ spinner.succeed('Clone generated!');
370
+ console.log('');
371
+ for (const f of result.files) {
372
+ console.log(` ${chalk.green('✓')} ${chalk.cyan(f)}`);
373
+ }
374
+ console.log('');
375
+ console.log(chalk.bold(' To run:'));
376
+ console.log(chalk.gray(` cd ${opts.out} && npm install && npm run dev`));
377
+ console.log('');
378
+
379
+ } catch (err) {
380
+ spinner.fail('Clone failed');
381
+ console.error(chalk.red(`\n ${err.message}\n`));
382
+ process.exit(1);
383
+ }
384
+ });
385
+
386
+ // ── Watch command ───────────────────────────────────────────
387
+ program
388
+ .command('watch <url>')
389
+ .description('Monitor a site for design changes')
390
+ .option('--interval <minutes>', 'check interval in minutes', parseInt, 60)
391
+ .action(async (url, opts) => {
392
+ if (!url.startsWith('http')) url = `https://${url}`;
393
+ const intervalMs = (opts.interval || 60) * 60 * 1000;
394
+
395
+ console.log('');
396
+ console.log(chalk.bold(' designlang watch'));
397
+ console.log(chalk.gray(` ${url} (every ${opts.interval || 60}min)`));
398
+ console.log('');
399
+
400
+ const check = async () => {
401
+ const spinner = ora('Checking for design changes...').start();
402
+ try {
403
+ const result = await watchSite(url);
404
+
405
+ if (result.isFirstRun) {
406
+ spinner.succeed('Baseline captured. Watching for changes...');
407
+ } else if (result.changes.length === 0) {
408
+ spinner.succeed(`No changes — ${new Date().toLocaleTimeString()}`);
409
+ } else {
410
+ spinner.warn(`${result.changes.length} changes detected!`);
411
+ for (const c of result.changes) {
412
+ console.log(` ${chalk.yellow('≠')} ${c.what}: ${c.from} → ${c.to}`);
413
+ }
414
+ }
415
+ } catch (err) {
416
+ spinner.fail(`Check failed: ${err.message}`);
417
+ }
418
+ };
419
+
420
+ await check();
421
+ console.log(chalk.gray(`\n Next check in ${opts.interval || 60} minutes. Press Ctrl+C to stop.\n`));
422
+ setInterval(check, intervalMs);
423
+ });
424
+
425
+ // ── Score command ───────────────────────────────────────────
426
+ program
427
+ .command('score <url>')
428
+ .description('Score a website\'s design system quality')
429
+ .action(async (url) => {
430
+ if (!url.startsWith('http')) url = `https://${url}`;
431
+
432
+ const spinner = ora('Analyzing design...').start();
433
+
434
+ try {
435
+ const design = await extractDesignLanguage(url);
436
+ const s = design.score;
437
+
438
+ spinner.stop();
439
+ console.log('');
440
+ console.log(chalk.bold(' Design System Score'));
441
+ console.log(chalk.gray(` ${url}`));
442
+ console.log('');
443
+
444
+ const gradeColor = s.grade === 'A' ? chalk.green : s.grade === 'B' ? chalk.cyan : s.grade === 'C' ? chalk.yellow : chalk.red;
445
+ console.log(` ${gradeColor.bold(` ${s.overall}/100 Grade: ${s.grade}`)}`);
446
+ console.log('');
447
+
448
+ // Category breakdown
449
+ const cats = [
450
+ ['Color Discipline', s.scores.colorDiscipline],
451
+ ['Typography', s.scores.typographyConsistency],
452
+ ['Spacing System', s.scores.spacingSystem],
453
+ ['Shadows', s.scores.shadowConsistency],
454
+ ['Border Radii', s.scores.radiusConsistency],
455
+ ['Accessibility', s.scores.accessibility],
456
+ ['Tokenization', s.scores.tokenization],
457
+ ];
458
+
459
+ for (const [name, score] of cats) {
460
+ const bar = '█'.repeat(Math.round(score / 5)) + '░'.repeat(20 - Math.round(score / 5));
461
+ const color = score >= 80 ? chalk.green : score >= 60 ? chalk.yellow : chalk.red;
462
+ console.log(` ${chalk.gray(name.padEnd(20))} ${color(bar)} ${score}`);
463
+ }
464
+
465
+ if (s.strengths.length > 0) {
466
+ console.log('');
467
+ console.log(chalk.bold(' Strengths:'));
468
+ for (const str of s.strengths) {
469
+ console.log(` ${chalk.green('✓')} ${str}`);
470
+ }
471
+ }
472
+
473
+ if (s.issues.length > 0) {
474
+ console.log('');
475
+ console.log(chalk.bold(' Issues:'));
476
+ for (const issue of s.issues) {
477
+ console.log(` ${chalk.yellow('!')} ${issue}`);
478
+ }
479
+ }
480
+ console.log('');
481
+
482
+ } catch (err) {
483
+ spinner.fail('Scoring failed');
484
+ console.error(chalk.red(`\n ${err.message}\n`));
485
+ process.exit(1);
486
+ }
487
+ });
488
+
341
489
  program.parse();
package/designlang.png ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "designlang",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "Extract the complete design language from any website — colors, typography, spacing, shadows, and more. Outputs AI-optimized markdown, W3C design tokens, Tailwind config, and CSS variables.",
5
5
  "type": "module",
6
6
  "bin": {
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
+ }
@@ -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
 
@@ -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
+ }