designlang 5.0.0 → 6.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 +66 -5
- package/bin/design-extract.js +215 -91
- package/package.json +9 -4
- package/src/config.js +36 -0
- package/src/crawler.js +181 -95
- package/src/extractors/animations.js +37 -5
- package/src/extractors/borders.js +40 -5
- package/src/extractors/components.js +77 -1
- package/src/extractors/gradients.js +25 -5
- package/src/extractors/shadows.js +60 -17
- package/src/extractors/spacing.js +31 -2
- package/src/extractors/variables.js +20 -1
- package/src/formatters/figma.js +66 -47
- package/src/formatters/preview.js +65 -22
- package/src/formatters/svelte-theme.js +40 -0
- package/src/formatters/tailwind.js +57 -4
- package/src/formatters/theme.js +134 -0
- package/src/formatters/vue-theme.js +44 -0
- package/src/formatters/wordpress.js +84 -0
- package/src/history.js +8 -1
- package/src/index.js +46 -20
- package/src/utils.js +68 -0
- package/tests/cli.test.js +34 -0
- package/tests/extractors.test.js +661 -0
- package/tests/formatters.test.js +477 -0
- package/tests/utils.test.js +413 -0
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ npx designlang https://stripe.com --full
|
|
|
45
45
|
| `*-theme.js` | React/CSS-in-JS theme (Chakra, Stitches, Vanilla Extract) |
|
|
46
46
|
| `*-shadcn-theme.css` | shadcn/ui globals.css variables |
|
|
47
47
|
|
|
48
|
-
The markdown output has **
|
|
48
|
+
The markdown output has **19 sections**: Color Palette, Typography, Spacing, Border Radii, Box Shadows, CSS Custom Properties, Breakpoints, Transitions & Animations, Component Patterns (with full CSS snippets), Layout System, Responsive Design, Interaction States, Accessibility (WCAG 2.1), Gradients, Z-Index Map, SVG Icons, Font Files, Image Style Patterns, and Quick Start.
|
|
49
49
|
|
|
50
50
|
## Install
|
|
51
51
|
|
|
@@ -169,6 +169,52 @@ designlang watch https://stripe.com --interval 60
|
|
|
169
169
|
|
|
170
170
|
Checks hourly and alerts when colors, fonts, or accessibility scores change.
|
|
171
171
|
|
|
172
|
+
### 9. Apply Command (NEW in v5)
|
|
173
|
+
|
|
174
|
+
Extract a site's design and write tokens directly into your project — auto-detects your framework:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
designlang apply https://stripe.com --dir ./my-app
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Detects Tailwind, shadcn/ui, or plain CSS and writes to the right config files automatically.
|
|
181
|
+
|
|
182
|
+
### 10. Auth Extraction (NEW in v5)
|
|
183
|
+
|
|
184
|
+
Extract from authenticated or protected pages with cookies and custom headers:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
designlang https://internal-app.com --cookie "session=abc123" --header "Authorization:Bearer token"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 11. Gradient Extraction (NEW in v5)
|
|
191
|
+
|
|
192
|
+
Detects all CSS gradients — type (linear/radial/conic), direction, color stops, and classifies them as subtle, brand, bold, or complex.
|
|
193
|
+
|
|
194
|
+
### 12. Z-Index Map (NEW in v5)
|
|
195
|
+
|
|
196
|
+
Builds a layer hierarchy from all z-index values, groups them into layers (base, sticky, dropdown, modal, etc.), and flags z-index wars or excessive values (>9999).
|
|
197
|
+
|
|
198
|
+
### 13. SVG Icon Extraction (NEW in v5)
|
|
199
|
+
|
|
200
|
+
Finds and deduplicates all inline SVGs, classifies them by size and style (outline/solid/duotone), and extracts the icon color palette.
|
|
201
|
+
|
|
202
|
+
### 14. Font File Detection (NEW in v5)
|
|
203
|
+
|
|
204
|
+
Identifies every font source — Google Fonts, self-hosted, CDN, or system — and generates ready-to-use `@font-face` CSS.
|
|
205
|
+
|
|
206
|
+
### 15. Image Style Patterns (NEW in v5)
|
|
207
|
+
|
|
208
|
+
Detects image aspect ratios, border treatments, filters, and classifies patterns like avatar, hero, thumbnail, and gallery.
|
|
209
|
+
|
|
210
|
+
### 16. Dark Mode Diffing (NEW in v5)
|
|
211
|
+
|
|
212
|
+
Compare light and dark mode side-by-side — see exactly which colors change and which CSS variables are overridden:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
designlang https://vercel.com --dark
|
|
216
|
+
```
|
|
217
|
+
|
|
172
218
|
## All Features
|
|
173
219
|
|
|
174
220
|
| Feature | Flag / Command | Description |
|
|
@@ -177,12 +223,19 @@ Checks hourly and alerts when colors, fonts, or accessibility scores change.
|
|
|
177
223
|
| Layout system | automatic | Grid patterns, flex usage, container widths, gap values |
|
|
178
224
|
| Accessibility | automatic | WCAG 2.1 contrast ratios for all fg/bg pairs |
|
|
179
225
|
| Design scoring | automatic | 7-category quality rating (A-F) with actionable issues |
|
|
180
|
-
|
|
|
226
|
+
| Gradients | automatic | Gradient type, direction, stops, classification |
|
|
227
|
+
| Z-index map | automatic | Layer hierarchy, z-index wars detection |
|
|
228
|
+
| SVG icons | automatic | Deduplicated icons, size/style classification, color palette |
|
|
229
|
+
| Font files | automatic | Source detection (Google/self-hosted/CDN/system), @font-face CSS |
|
|
230
|
+
| Image styles | automatic | Aspect ratios, shapes, filters, pattern classification |
|
|
231
|
+
| Dark mode | `--dark` | Extracts dark color scheme + light/dark diff |
|
|
232
|
+
| Auth pages | `--cookie`, `--header` | Extract from authenticated/protected pages |
|
|
181
233
|
| Multi-page | `--depth <n>` | Crawl N internal pages for site-wide tokens |
|
|
182
234
|
| Screenshots | `--screenshots` | Capture buttons, cards, inputs, nav, hero, full page |
|
|
183
235
|
| Responsive | `--responsive` | Crawl at 4 viewports, map breakpoint changes |
|
|
184
236
|
| Interactions | `--interactions` | Capture hover/focus/active state transitions |
|
|
185
237
|
| Everything | `--full` | Enable screenshots + responsive + interactions |
|
|
238
|
+
| Apply | `designlang apply <url>` | Auto-detect framework and write tokens to your project |
|
|
186
239
|
| Clone | `designlang clone <url>` | Generate a working Next.js starter with extracted design |
|
|
187
240
|
| Score | `designlang score <url>` | Rate design quality with visual bar chart breakdown |
|
|
188
241
|
| Watch | `designlang watch <url>` | Monitor for design changes on interval |
|
|
@@ -208,11 +261,14 @@ Options:
|
|
|
208
261
|
--responsive Capture at multiple breakpoints
|
|
209
262
|
--interactions Capture hover/focus/active states
|
|
210
263
|
--full Enable all captures
|
|
264
|
+
--cookie <cookies...> Cookies for authenticated pages (name=value)
|
|
265
|
+
--header <headers...> Custom headers (name:value)
|
|
211
266
|
--framework <type> Only generate specific theme (react, shadcn)
|
|
212
267
|
--no-history Skip saving to history
|
|
213
268
|
--verbose Detailed progress output
|
|
214
269
|
|
|
215
270
|
Commands:
|
|
271
|
+
apply <url> Extract and apply design directly to your project
|
|
216
272
|
clone <url> Generate a working Next.js starter from extracted design
|
|
217
273
|
score <url> Rate design quality (7 categories, A-F, bar chart)
|
|
218
274
|
watch <url> Monitor for design changes on interval
|
|
@@ -252,9 +308,14 @@ Running `designlang https://vercel.com --full`:
|
|
|
252
308
|
Shadows: 11 unique shadows
|
|
253
309
|
Radii: 10 unique values
|
|
254
310
|
Breakpoints: 45 breakpoints
|
|
255
|
-
Components: 11 types detected
|
|
311
|
+
Components: 11 types detected (with CSS snippets)
|
|
256
312
|
CSS Vars: 407 custom properties
|
|
257
313
|
Layout: 55 grids, 492 flex containers
|
|
314
|
+
Gradients: 4 unique gradients
|
|
315
|
+
Z-Index: 8 layers mapped
|
|
316
|
+
Icons: 23 unique SVGs
|
|
317
|
+
Font Files: 4 font sources detected
|
|
318
|
+
Images: 6 style patterns
|
|
258
319
|
Responsive: 4 viewports, 3 breakpoint changes
|
|
259
320
|
Interactions: 8 state changes captured
|
|
260
321
|
A11y: 94% WCAG score (7 failing pairs)
|
|
@@ -264,8 +325,8 @@ Running `designlang https://vercel.com --full`:
|
|
|
264
325
|
## How It Works
|
|
265
326
|
|
|
266
327
|
1. **Crawl** — Launches headless Chromium via Playwright, waits for network idle and fonts
|
|
267
|
-
2. **Extract** — Single `page.evaluate()` walks up to 5,000 DOM elements collecting 25+ computed style properties
|
|
268
|
-
3. **Process** —
|
|
328
|
+
2. **Extract** — Single `page.evaluate()` walks up to 5,000 DOM elements collecting 25+ computed style properties, layout data, inline SVGs, font sources, and image metadata
|
|
329
|
+
3. **Process** — 17 extractor modules parse, deduplicate, cluster, and classify the raw data (including gradients, z-index layers, icons, fonts, and image patterns)
|
|
269
330
|
4. **Format** — 8 formatter modules generate output files
|
|
270
331
|
5. **Score** — Accessibility extractor calculates WCAG contrast ratios for all color pairs
|
|
271
332
|
6. **Capture** — Optional: screenshots, responsive viewport crawling, interaction state recording
|
package/bin/design-extract.js
CHANGED
|
@@ -13,6 +13,10 @@ import { formatCssVars } from '../src/formatters/css-vars.js';
|
|
|
13
13
|
import { formatPreview } from '../src/formatters/preview.js';
|
|
14
14
|
import { formatFigma } from '../src/formatters/figma.js';
|
|
15
15
|
import { formatReactTheme, formatShadcnTheme } from '../src/formatters/theme.js';
|
|
16
|
+
import { formatWordPress } from '../src/formatters/wordpress.js';
|
|
17
|
+
import { formatVueTheme } from '../src/formatters/vue-theme.js';
|
|
18
|
+
import { formatSvelteTheme } from '../src/formatters/svelte-theme.js';
|
|
19
|
+
import { loadConfig, mergeConfig } from '../src/config.js';
|
|
16
20
|
import { diffDesigns, formatDiffMarkdown, formatDiffHtml } from '../src/diff.js';
|
|
17
21
|
import { saveSnapshot, getHistory, formatHistoryMarkdown } from '../src/history.js';
|
|
18
22
|
import { captureResponsive } from '../src/extractors/responsive.js';
|
|
@@ -25,12 +29,20 @@ import { diffDarkMode } from '../src/darkdiff.js';
|
|
|
25
29
|
import { applyDesign } from '../src/apply.js';
|
|
26
30
|
import { nameFromUrl } from '../src/utils.js';
|
|
27
31
|
|
|
32
|
+
function validateUrl(url) {
|
|
33
|
+
try { new URL(url); } catch {
|
|
34
|
+
console.error(chalk.red(`\n Invalid URL: ${url}\n`));
|
|
35
|
+
console.error(chalk.gray(' Example: designlang https://example.com\n'));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
const program = new Command();
|
|
29
41
|
|
|
30
42
|
program
|
|
31
43
|
.name('designlang')
|
|
32
44
|
.description('Extract the complete design language from any website')
|
|
33
|
-
.version('
|
|
45
|
+
.version('6.0.0');
|
|
34
46
|
|
|
35
47
|
// ── Main command: extract ──────────────────────────────────────
|
|
36
48
|
program
|
|
@@ -43,60 +55,97 @@ program
|
|
|
43
55
|
.option('--dark', 'also extract dark mode styles')
|
|
44
56
|
.option('--depth <n>', 'number of internal pages to also crawl', parseInt, 0)
|
|
45
57
|
.option('--screenshots', 'capture component screenshots')
|
|
46
|
-
.option('--framework <type>', 'generate framework theme (react, shadcn)')
|
|
58
|
+
.option('--framework <type>', 'generate framework theme (react, shadcn, vue, svelte)')
|
|
47
59
|
.option('--responsive', 'capture design at multiple breakpoints')
|
|
48
60
|
.option('--interactions', 'capture hover/focus/active states')
|
|
49
61
|
.option('--full', 'enable all extra captures (screenshots, responsive, interactions)')
|
|
50
62
|
.option('--cookie <cookies...>', 'cookies for authenticated pages (name=value)')
|
|
51
63
|
.option('--header <headers...>', 'custom headers (name:value)')
|
|
64
|
+
.option('--ignore <selectors...>', 'CSS selectors to remove before extraction')
|
|
65
|
+
.option('--json', 'output raw JSON to stdout (for CI/CD)')
|
|
66
|
+
.option('--json-pretty', 'output formatted JSON to stdout')
|
|
52
67
|
.option('--no-history', 'skip saving to history')
|
|
53
68
|
.option('--verbose', 'show detailed progress')
|
|
69
|
+
.option('-q, --quiet', 'suppress output except file paths')
|
|
54
70
|
.action(async (url, opts) => {
|
|
55
71
|
if (!url.startsWith('http')) url = `https://${url}`;
|
|
72
|
+
|
|
73
|
+
// Load config file and merge with CLI opts
|
|
74
|
+
const config = loadConfig();
|
|
75
|
+
const merged = mergeConfig(opts, config);
|
|
76
|
+
|
|
77
|
+
// Validate URL
|
|
78
|
+
validateUrl(url);
|
|
79
|
+
|
|
80
|
+
// Validate numeric options
|
|
81
|
+
if (isNaN(merged.width) || merged.width < 100) {
|
|
82
|
+
console.error(chalk.red('\n Invalid width. Must be >= 100\n'));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
if (merged.depth < 0 || merged.depth > 50) {
|
|
86
|
+
console.error(chalk.red('\n Invalid depth. Must be 0-50\n'));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
56
90
|
const prefix = opts.name || nameFromUrl(url);
|
|
57
|
-
const outDir = resolve(
|
|
91
|
+
const outDir = resolve(merged.out);
|
|
58
92
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
93
|
+
const jsonMode = opts.json || opts.jsonPretty;
|
|
94
|
+
const startTime = Date.now();
|
|
95
|
+
|
|
96
|
+
if (!jsonMode && !opts.quiet) {
|
|
97
|
+
console.log('');
|
|
98
|
+
console.log(chalk.bold(' designlang'));
|
|
99
|
+
console.log(chalk.gray(` ${url}${merged.depth > 0 ? ` (+ ${merged.depth} pages)` : ''}`));
|
|
100
|
+
console.log('');
|
|
101
|
+
}
|
|
63
102
|
|
|
64
|
-
const spinner =
|
|
103
|
+
const spinner = jsonMode || opts.quiet
|
|
104
|
+
? { start() { return this; }, set text(v) {}, succeed() {}, fail() {}, info() {}, stop() {} }
|
|
105
|
+
: ora('Launching browser...').start();
|
|
65
106
|
|
|
66
107
|
try {
|
|
67
|
-
spinner.text = `Crawling${
|
|
108
|
+
spinner.text = `Crawling${merged.depth > 0 ? ` (depth: ${merged.depth})` : ''}...`;
|
|
68
109
|
// Parse auth options
|
|
69
|
-
const cookies =
|
|
110
|
+
const cookies = merged.cookie || [];
|
|
70
111
|
const headers = {};
|
|
71
|
-
if (
|
|
72
|
-
for (const h of
|
|
112
|
+
if (merged.header) {
|
|
113
|
+
for (const h of merged.header) {
|
|
73
114
|
const [name, ...rest] = h.split(':');
|
|
74
115
|
if (name && rest.length) headers[name.trim()] = rest.join(':').trim();
|
|
75
116
|
}
|
|
76
117
|
}
|
|
77
118
|
|
|
78
119
|
const design = await extractDesignLanguage(url, {
|
|
79
|
-
width:
|
|
80
|
-
height: parseInt(
|
|
81
|
-
wait:
|
|
82
|
-
dark:
|
|
83
|
-
depth:
|
|
84
|
-
screenshots:
|
|
120
|
+
width: merged.width,
|
|
121
|
+
height: parseInt(merged.height) || 800,
|
|
122
|
+
wait: merged.wait,
|
|
123
|
+
dark: merged.dark,
|
|
124
|
+
depth: merged.depth,
|
|
125
|
+
screenshots: merged.screenshots || merged.full,
|
|
85
126
|
outDir,
|
|
127
|
+
ignore: merged.ignore,
|
|
86
128
|
cookies: cookies.length > 0 ? cookies : undefined,
|
|
87
129
|
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
88
130
|
});
|
|
89
131
|
|
|
90
132
|
// Responsive capture
|
|
91
|
-
if (
|
|
133
|
+
if (merged.responsive || merged.full) {
|
|
92
134
|
spinner.text = 'Capturing responsive breakpoints...';
|
|
93
|
-
design.responsive = await captureResponsive(url, { wait:
|
|
135
|
+
design.responsive = await captureResponsive(url, { wait: merged.wait });
|
|
94
136
|
}
|
|
95
137
|
|
|
96
138
|
// Interaction state capture
|
|
97
|
-
if (
|
|
139
|
+
if (merged.interactions || merged.full) {
|
|
98
140
|
spinner.text = 'Capturing interaction states...';
|
|
99
|
-
design.interactions = await captureInteractions(url, { width:
|
|
141
|
+
design.interactions = await captureInteractions(url, { width: merged.width, height: parseInt(merged.height) || 800, wait: merged.wait });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// JSON mode: output and exit
|
|
145
|
+
if (jsonMode) {
|
|
146
|
+
const output = opts.jsonPretty ? JSON.stringify(design, null, 2) : JSON.stringify(design);
|
|
147
|
+
process.stdout.write(output + '\n');
|
|
148
|
+
process.exit(0);
|
|
100
149
|
}
|
|
101
150
|
|
|
102
151
|
spinner.text = 'Generating outputs...';
|
|
@@ -112,16 +161,23 @@ program
|
|
|
112
161
|
];
|
|
113
162
|
|
|
114
163
|
// Framework-specific themes
|
|
115
|
-
if (
|
|
164
|
+
if (merged.framework === 'react') {
|
|
116
165
|
files.push({ name: `${prefix}-theme.js`, content: formatReactTheme(design), label: 'React Theme' });
|
|
117
|
-
} else if (
|
|
166
|
+
} else if (merged.framework === 'shadcn') {
|
|
118
167
|
files.push({ name: `${prefix}-shadcn-theme.css`, content: formatShadcnTheme(design), label: 'shadcn/ui Theme' });
|
|
168
|
+
} else if (merged.framework === 'vue') {
|
|
169
|
+
files.push({ name: `${prefix}-vue-theme.js`, content: formatVueTheme(design), label: 'Vue/Vuetify Theme' });
|
|
170
|
+
} else if (merged.framework === 'svelte') {
|
|
171
|
+
files.push({ name: `${prefix}-svelte-theme.css`, content: formatSvelteTheme(design), label: 'Svelte Theme' });
|
|
119
172
|
} else {
|
|
120
173
|
// Generate both by default
|
|
121
174
|
files.push({ name: `${prefix}-theme.js`, content: formatReactTheme(design), label: 'React Theme' });
|
|
122
175
|
files.push({ name: `${prefix}-shadcn-theme.css`, content: formatShadcnTheme(design), label: 'shadcn/ui Theme' });
|
|
123
176
|
}
|
|
124
177
|
|
|
178
|
+
// WordPress theme (always generated)
|
|
179
|
+
files.push({ name: `${prefix}-wordpress-theme.json`, content: formatWordPress(design), label: 'WordPress Theme' });
|
|
180
|
+
|
|
125
181
|
for (const file of files) {
|
|
126
182
|
writeFileSync(join(outDir, file.name), file.content, 'utf-8');
|
|
127
183
|
}
|
|
@@ -132,81 +188,111 @@ program
|
|
|
132
188
|
if (opts.verbose) spinner.info(`Snapshot #${histInfo.snapshotCount} saved for ${histInfo.hostname}`);
|
|
133
189
|
}
|
|
134
190
|
|
|
191
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
192
|
+
|
|
135
193
|
spinner.succeed('Extraction complete!');
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
console.log(` ${chalk.green('✓')} ${chalk.cyan(file.name)} ${chalk.gray(`(${sizeStr})`)} — ${file.label}`);
|
|
142
|
-
}
|
|
143
|
-
if (opts.screenshots && design.componentScreenshots && Object.keys(design.componentScreenshots).length > 0) {
|
|
144
|
-
for (const [, info] of Object.entries(design.componentScreenshots)) {
|
|
145
|
-
console.log(` ${chalk.green('✓')} ${chalk.cyan(info.path)} — ${info.label} screenshot`);
|
|
194
|
+
|
|
195
|
+
if (opts.quiet) {
|
|
196
|
+
// Quiet mode: only show file paths
|
|
197
|
+
for (const file of files) {
|
|
198
|
+
console.log(join(outDir, file.name));
|
|
146
199
|
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
200
|
+
} else {
|
|
201
|
+
console.log('');
|
|
202
|
+
console.log(chalk.bold(' Output files:'));
|
|
203
|
+
for (const file of files) {
|
|
204
|
+
const size = Buffer.byteLength(file.content);
|
|
205
|
+
const sizeStr = size > 1024 ? `${(size / 1024).toFixed(1)}KB` : `${size}B`;
|
|
206
|
+
console.log(` ${chalk.green('✓')} ${chalk.cyan(file.name)} ${chalk.gray(`(${sizeStr})`)} — ${file.label}`);
|
|
207
|
+
}
|
|
208
|
+
if (opts.screenshots && design.componentScreenshots && Object.keys(design.componentScreenshots).length > 0) {
|
|
209
|
+
for (const [, info] of Object.entries(design.componentScreenshots)) {
|
|
210
|
+
console.log(` ${chalk.green('✓')} ${chalk.cyan(info.path)} — ${info.label} screenshot`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
console.log('');
|
|
214
|
+
console.log(chalk.gray(` Saved to ${outDir}`));
|
|
150
215
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
216
|
+
// Summary
|
|
217
|
+
console.log('');
|
|
218
|
+
console.log(chalk.bold(' Summary:'));
|
|
219
|
+
if (design.meta.pagesAnalyzed > 1) {
|
|
220
|
+
console.log(` ${chalk.gray('Pages:')} ${design.meta.pagesAnalyzed} pages analyzed`);
|
|
221
|
+
}
|
|
222
|
+
console.log(` ${chalk.gray('Colors:')} ${design.colors.all.length} unique colors`);
|
|
223
|
+
console.log(` ${chalk.gray('Fonts:')} ${design.typography.families.map(f => f.name).join(', ') || 'none detected'}`);
|
|
224
|
+
console.log(` ${chalk.gray('Spacing:')} ${design.spacing.scale.length} values${design.spacing.base ? ` (base: ${design.spacing.base}px)` : ''}`);
|
|
225
|
+
console.log(` ${chalk.gray('Shadows:')} ${design.shadows.values.length} unique shadows`);
|
|
226
|
+
console.log(` ${chalk.gray('Radii:')} ${design.borders.radii.length} unique values`);
|
|
227
|
+
console.log(` ${chalk.gray('Breakpoints:')} ${design.breakpoints.length} breakpoints`);
|
|
228
|
+
console.log(` ${chalk.gray('Components:')} ${Object.keys(design.components).length} patterns detected`);
|
|
229
|
+
console.log(` ${chalk.gray('CSS Vars:')} ${Object.values(design.variables).reduce((s, v) => s + Object.keys(v).length, 0)} custom properties`);
|
|
230
|
+
if (design.layout) {
|
|
231
|
+
console.log(` ${chalk.gray('Layout:')} ${design.layout.gridCount} grids, ${design.layout.flexCount} flex containers`);
|
|
232
|
+
}
|
|
233
|
+
if (design.responsive) {
|
|
234
|
+
console.log(` ${chalk.gray('Responsive:')} ${design.responsive.viewports.length} viewports, ${design.responsive.changes.length} breakpoint changes`);
|
|
235
|
+
}
|
|
236
|
+
if (design.interactions) {
|
|
237
|
+
const ic = design.interactions;
|
|
238
|
+
const total = ic.buttons.length + ic.links.length + ic.inputs.length;
|
|
239
|
+
console.log(` ${chalk.gray('Interactions:')} ${total} state changes captured`);
|
|
240
|
+
}
|
|
241
|
+
if (design.score) {
|
|
242
|
+
const s = design.score;
|
|
243
|
+
const gradeColor = s.grade === 'A' ? chalk.green : s.grade === 'B' ? chalk.cyan : s.grade === 'C' ? chalk.yellow : chalk.red;
|
|
244
|
+
console.log(` ${chalk.gray('Design Score:')} ${gradeColor(`${s.overall}/100 (${s.grade})`)}${s.issues.length > 0 ? ` — ${s.issues.length} issues` : ''}`);
|
|
245
|
+
}
|
|
181
246
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
247
|
+
// Score change vs last snapshot
|
|
248
|
+
const history = getHistory(url);
|
|
249
|
+
if (history.length > 1 && design.score) {
|
|
250
|
+
const prev = history[history.length - 2];
|
|
251
|
+
if (prev.score !== undefined) {
|
|
252
|
+
const delta = design.score.overall - prev.score;
|
|
253
|
+
if (delta !== 0) {
|
|
254
|
+
const sign = delta > 0 ? '+' : '';
|
|
255
|
+
const color = delta > 0 ? chalk.green : chalk.red;
|
|
256
|
+
console.log(` ${chalk.gray('Score \u0394:')} ${color(`${sign}${delta} from last scan`)}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// New v5 extractors
|
|
262
|
+
if (design.gradients && design.gradients.count > 0) {
|
|
263
|
+
console.log(` ${chalk.gray('Gradients:')} ${design.gradients.count} unique gradients`);
|
|
264
|
+
}
|
|
265
|
+
if (design.zIndex && design.zIndex.allValues.length > 0) {
|
|
266
|
+
console.log(` ${chalk.gray('Z-Index:')} ${design.zIndex.allValues.length} layers${design.zIndex.issues.length > 0 ? ` (${design.zIndex.issues.length} issues)` : ''}`);
|
|
267
|
+
}
|
|
268
|
+
if (design.icons && design.icons.count > 0) {
|
|
269
|
+
console.log(` ${chalk.gray('Icons:')} ${design.icons.count} SVG icons (${design.icons.dominantStyle || 'mixed'})`);
|
|
270
|
+
}
|
|
271
|
+
if (design.fonts && design.fonts.fonts.length > 0) {
|
|
272
|
+
const sources = design.fonts.fonts.map(f => f.source).filter((v, i, a) => a.indexOf(v) === i);
|
|
273
|
+
console.log(` ${chalk.gray('Font Files:')} ${design.fonts.fonts.length} fonts (${sources.join(', ')})`);
|
|
274
|
+
}
|
|
275
|
+
if (design.images && design.images.patterns.length > 0) {
|
|
276
|
+
const total = design.images.patterns.reduce((s, p) => s + p.count, 0);
|
|
277
|
+
console.log(` ${chalk.gray('Images:')} ${total} images, ${design.images.patterns.length} style patterns`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Accessibility summary
|
|
281
|
+
if (design.accessibility) {
|
|
282
|
+
const a = design.accessibility;
|
|
283
|
+
const scoreColor = a.score >= 80 ? chalk.green : a.score >= 50 ? chalk.yellow : chalk.red;
|
|
284
|
+
console.log(` ${chalk.gray('A11y:')} ${scoreColor(`${a.score}% WCAG score`)} (${a.failCount} failing pairs)`);
|
|
285
|
+
}
|
|
200
286
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const a = design.accessibility;
|
|
204
|
-
const scoreColor = a.score >= 80 ? chalk.green : a.score >= 50 ? chalk.yellow : chalk.red;
|
|
205
|
-
console.log(` ${chalk.gray('A11y:')} ${scoreColor(`${a.score}% WCAG score`)} (${a.failCount} failing pairs)`);
|
|
287
|
+
console.log(chalk.gray(` Completed in ${duration}s`));
|
|
288
|
+
console.log('');
|
|
206
289
|
}
|
|
207
|
-
console.log('');
|
|
208
290
|
|
|
209
291
|
} catch (err) {
|
|
292
|
+
if (jsonMode) {
|
|
293
|
+
process.stderr.write(JSON.stringify({ error: err.message }) + '\n');
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
210
296
|
spinner.fail('Extraction failed');
|
|
211
297
|
if (err.message.includes('playwright')) {
|
|
212
298
|
console.error(chalk.red('\n Playwright is not installed.'));
|
|
@@ -227,6 +313,8 @@ program
|
|
|
227
313
|
.action(async (urlA, urlB, opts) => {
|
|
228
314
|
if (!urlA.startsWith('http')) urlA = `https://${urlA}`;
|
|
229
315
|
if (!urlB.startsWith('http')) urlB = `https://${urlB}`;
|
|
316
|
+
validateUrl(urlA);
|
|
317
|
+
validateUrl(urlB);
|
|
230
318
|
|
|
231
319
|
console.log('');
|
|
232
320
|
console.log(chalk.bold(' designlang diff'));
|
|
@@ -283,6 +371,7 @@ program
|
|
|
283
371
|
.description('View design history for a website')
|
|
284
372
|
.action(async (url) => {
|
|
285
373
|
if (!url.startsWith('http')) url = `https://${url}`;
|
|
374
|
+
validateUrl(url);
|
|
286
375
|
const history = getHistory(url);
|
|
287
376
|
console.log('');
|
|
288
377
|
console.log(formatHistoryMarkdown(url, history));
|
|
@@ -341,6 +430,7 @@ program
|
|
|
341
430
|
.option('-o, --out <dir>', 'directory with token files to update', '.')
|
|
342
431
|
.action(async (url, opts) => {
|
|
343
432
|
if (!url.startsWith('http')) url = `https://${url}`;
|
|
433
|
+
validateUrl(url);
|
|
344
434
|
|
|
345
435
|
console.log('');
|
|
346
436
|
console.log(chalk.bold(' designlang sync'));
|
|
@@ -387,6 +477,7 @@ program
|
|
|
387
477
|
.option('-o, --out <dir>', 'output directory', './cloned-design')
|
|
388
478
|
.action(async (url, opts) => {
|
|
389
479
|
if (!url.startsWith('http')) url = `https://${url}`;
|
|
480
|
+
validateUrl(url);
|
|
390
481
|
|
|
391
482
|
console.log('');
|
|
392
483
|
console.log(chalk.bold(' designlang clone'));
|
|
@@ -425,6 +516,7 @@ program
|
|
|
425
516
|
.option('--interval <minutes>', 'check interval in minutes', parseInt, 60)
|
|
426
517
|
.action(async (url, opts) => {
|
|
427
518
|
if (!url.startsWith('http')) url = `https://${url}`;
|
|
519
|
+
validateUrl(url);
|
|
428
520
|
const intervalMs = (opts.interval || 60) * 60 * 1000;
|
|
429
521
|
|
|
430
522
|
console.log('');
|
|
@@ -463,6 +555,7 @@ program
|
|
|
463
555
|
.description('Score a website\'s design system quality')
|
|
464
556
|
.action(async (url) => {
|
|
465
557
|
if (!url.startsWith('http')) url = `https://${url}`;
|
|
558
|
+
validateUrl(url);
|
|
466
559
|
|
|
467
560
|
const spinner = ora('Analyzing design...').start();
|
|
468
561
|
|
|
@@ -531,6 +624,7 @@ program
|
|
|
531
624
|
.option('--header <headers...>', 'custom headers')
|
|
532
625
|
.action(async (url, opts) => {
|
|
533
626
|
if (!url.startsWith('http')) url = `https://${url}`;
|
|
627
|
+
validateUrl(url);
|
|
534
628
|
|
|
535
629
|
console.log('');
|
|
536
630
|
console.log(chalk.bold(' designlang apply'));
|
|
@@ -561,4 +655,34 @@ program
|
|
|
561
655
|
}
|
|
562
656
|
});
|
|
563
657
|
|
|
658
|
+
// ── Export command ─────────────────────────────────────────
|
|
659
|
+
program
|
|
660
|
+
.command('export <url>')
|
|
661
|
+
.description('Export raw design data in various formats')
|
|
662
|
+
.option('-f, --format <type>', 'output format (json, csv)', 'json')
|
|
663
|
+
.option('--pretty', 'pretty-print output')
|
|
664
|
+
.action(async (url, opts) => {
|
|
665
|
+
if (!url.startsWith('http')) url = `https://${url}`;
|
|
666
|
+
validateUrl(url);
|
|
667
|
+
|
|
668
|
+
try {
|
|
669
|
+
const design = await extractDesignLanguage(url);
|
|
670
|
+
|
|
671
|
+
if (opts.format === 'csv') {
|
|
672
|
+
// Export colors as CSV
|
|
673
|
+
const rows = ['hex,rgb_r,rgb_g,rgb_b,hsl_h,hsl_s,hsl_l,count,contexts'];
|
|
674
|
+
for (const c of design.colors.all) {
|
|
675
|
+
rows.push(`${c.hex},${c.rgb.r},${c.rgb.g},${c.rgb.b},${c.hsl.h},${c.hsl.s},${c.hsl.l},${c.count},"${c.contexts.join(';')}"`);
|
|
676
|
+
}
|
|
677
|
+
process.stdout.write(rows.join('\n') + '\n');
|
|
678
|
+
} else {
|
|
679
|
+
const output = opts.pretty ? JSON.stringify(design, null, 2) : JSON.stringify(design);
|
|
680
|
+
process.stdout.write(output + '\n');
|
|
681
|
+
}
|
|
682
|
+
} catch (err) {
|
|
683
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
|
|
564
688
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "designlang",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.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": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"postinstall": "npx playwright install chromium --with-deps 2>/dev/null || npx playwright install chromium",
|
|
12
12
|
"start": "node bin/design-extract.js",
|
|
13
|
-
"test": "node --test tests
|
|
13
|
+
"test": "node --test tests/*.test.js"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"playwright": "^1.42.0",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"ora": "^8.0.0"
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
22
|
-
"node": ">=
|
|
22
|
+
"node": ">=20"
|
|
23
23
|
},
|
|
24
24
|
"keywords": [
|
|
25
25
|
"design-system",
|
|
@@ -32,7 +32,12 @@
|
|
|
32
32
|
"colors",
|
|
33
33
|
"typography",
|
|
34
34
|
"claude-code",
|
|
35
|
-
"plugin"
|
|
35
|
+
"plugin",
|
|
36
|
+
"wordpress",
|
|
37
|
+
"vue",
|
|
38
|
+
"svelte",
|
|
39
|
+
"json",
|
|
40
|
+
"ci-cd"
|
|
36
41
|
],
|
|
37
42
|
"author": "masyv",
|
|
38
43
|
"license": "MIT"
|
package/src/config.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
const CONFIG_FILES = ['.designlangrc', 'designlang.config.json', '.designlangrc.json'];
|
|
5
|
+
|
|
6
|
+
export function loadConfig(dir = process.cwd()) {
|
|
7
|
+
for (const name of CONFIG_FILES) {
|
|
8
|
+
const path = join(dir, name);
|
|
9
|
+
if (existsSync(path)) {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
12
|
+
} catch { return {}; }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function mergeConfig(cliOpts, config) {
|
|
19
|
+
// CLI flags take precedence over config file
|
|
20
|
+
return {
|
|
21
|
+
ignore: cliOpts.ignore || config.ignore || [],
|
|
22
|
+
width: cliOpts.width || config.width || 1280,
|
|
23
|
+
height: cliOpts.height || config.height || 800,
|
|
24
|
+
wait: cliOpts.wait || config.wait || 0,
|
|
25
|
+
dark: cliOpts.dark || config.dark || false,
|
|
26
|
+
depth: cliOpts.depth || config.depth || 0,
|
|
27
|
+
screenshots: cliOpts.screenshots || config.screenshots || false,
|
|
28
|
+
framework: cliOpts.framework || config.framework,
|
|
29
|
+
responsive: cliOpts.responsive || config.responsive || false,
|
|
30
|
+
interactions: cliOpts.interactions || config.interactions || false,
|
|
31
|
+
full: cliOpts.full || config.full || false,
|
|
32
|
+
cookie: cliOpts.cookie || config.cookies,
|
|
33
|
+
header: cliOpts.header || config.headers,
|
|
34
|
+
out: cliOpts.out || config.out || './design-extract-output',
|
|
35
|
+
};
|
|
36
|
+
}
|