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 +138 -77
- package/bin/design-extract.js +149 -1
- package/designlang.png +0 -0
- package/package.json +1 -1
- package/src/clone.js +218 -0
- package/src/extractors/components.js +86 -0
- package/src/extractors/scoring.js +132 -0
- package/src/formatters/markdown.js +31 -0
- package/src/index.js +7 -0
- package/src/watch.js +47 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<h1 align="center">designlang</h1>
|
|
3
|
-
<p align="center">
|
|
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
|
-
|
|
14
|
+
<p align="center">
|
|
15
|
+
<img src="designlang.png" alt="designlang in action" width="100%">
|
|
16
|
+
</p>
|
|
15
17
|
|
|
16
|
-
|
|
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
|
-
|
|
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 —
|
|
31
|
-
| `*-preview.html` | Visual
|
|
32
|
-
| `*-design-tokens.json` | [W3C Design Tokens](https://design-tokens.github.io/community-group/format/)
|
|
33
|
-
| `*-tailwind.config.js` | Drop-in Tailwind CSS theme
|
|
34
|
-
| `*-variables.css` | CSS custom properties
|
|
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
|
|
37
|
-
| `*-shadcn-theme.css` | shadcn/ui globals.css
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
# As an agent skill (Claude Code, Cursor, Codex, 40+ agents)
|
|
59
|
+
npx skills add Manavarya09/design-extract
|
|
60
|
+
```
|
|
52
61
|
|
|
53
|
-
|
|
62
|
+
## What Makes This Different
|
|
54
63
|
|
|
55
|
-
|
|
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
|
-
###
|
|
66
|
+
### 1. Layout System Extraction
|
|
60
67
|
|
|
61
|
-
|
|
68
|
+
Extracts the structural skeleton — grid column patterns, flex direction usage, container widths, gap values, and justify/align patterns.
|
|
62
69
|
|
|
63
70
|
```
|
|
64
|
-
|
|
71
|
+
Layout: 55 grids, 492 flex containers
|
|
65
72
|
```
|
|
66
73
|
|
|
67
|
-
|
|
74
|
+
Every other tool gives you the paint. designlang gives you the architecture.
|
|
68
75
|
|
|
69
|
-
###
|
|
76
|
+
### 2. Responsive Multi-Breakpoint Capture
|
|
70
77
|
|
|
71
|
-
|
|
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 --
|
|
81
|
+
designlang https://vercel.com --responsive
|
|
75
82
|
```
|
|
76
83
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
92
|
+
### 3. Interaction State Capture
|
|
90
93
|
|
|
91
|
-
|
|
94
|
+
Programmatically hovers and focuses interactive elements, capturing the actual style transitions:
|
|
92
95
|
|
|
93
96
|
```bash
|
|
94
|
-
designlang
|
|
97
|
+
designlang https://stripe.com --interactions
|
|
95
98
|
```
|
|
96
99
|
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
+
### 4. Live Site Sync
|
|
102
111
|
|
|
103
|
-
|
|
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
|
-
|
|
108
|
-
designlang
|
|
114
|
+
```bash
|
|
115
|
+
designlang sync https://stripe.com --out ./src/tokens
|
|
109
116
|
```
|
|
110
117
|
|
|
111
|
-
|
|
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
|
-
|
|
120
|
+
### 5. Multi-Brand Comparison
|
|
116
121
|
|
|
117
|
-
|
|
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
|
-
|
|
124
|
+
```bash
|
|
125
|
+
designlang brands stripe.com vercel.com github.com linear.app
|
|
126
|
+
```
|
|
122
127
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
|
128
|
-
|
|
129
|
-
|
|
|
130
|
-
|
|
|
131
|
-
|
|
|
132
|
-
|
|
|
133
|
-
|
|
|
134
|
-
|
|
|
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>
|
|
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
|
-
|
|
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
|
|
163
|
-
3. **Process** —
|
|
164
|
-
4. **Format** — 8 formatter modules generate
|
|
165
|
-
5. **Score** — Accessibility extractor calculates WCAG contrast ratios
|
|
166
|
-
6. **Capture** — Optional
|
|
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
|
-
|
|
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
|
-
|
|
231
|
+
In Claude Code, use `/extract-design <url>`.
|
|
171
232
|
|
|
172
233
|
## Contributing
|
|
173
234
|
|
package/bin/design-extract.js
CHANGED
|
@@ -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('
|
|
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
|
+
"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
|
+
}
|