designlang 4.0.1 → 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 +269 -70
- package/package.json +9 -4
- package/src/apply.js +65 -0
- package/src/config.js +36 -0
- package/src/crawler.js +247 -82
- package/src/darkdiff.js +65 -0
- package/src/extractors/animations.js +76 -8
- package/src/extractors/borders.js +40 -5
- package/src/extractors/components.js +100 -1
- package/src/extractors/fonts.js +82 -0
- package/src/extractors/gradients.js +100 -0
- package/src/extractors/icons.js +80 -0
- package/src/extractors/images.js +76 -0
- package/src/extractors/shadows.js +60 -17
- package/src/extractors/spacing.js +31 -2
- package/src/extractors/variables.js +20 -1
- package/src/extractors/zindex.js +65 -0
- package/src/formatters/figma.js +66 -47
- package/src/formatters/markdown.js +98 -0
- 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 +54 -16
- 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/website/app/api/extract/route.js +85 -0
- package/website/app/components/Extractor.js +184 -0
- package/website/app/globals.css +291 -0
- package/website/app/page.js +13 -0
- package/website/next.config.mjs +10 -1
- package/website/package-lock.json +356 -0
- package/website/package.json +4 -1
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';
|
|
@@ -21,14 +25,24 @@ import { syncDesign } from '../src/sync.js';
|
|
|
21
25
|
import { compareBrands, formatBrandMatrix, formatBrandMatrixHtml } from '../src/multibrand.js';
|
|
22
26
|
import { generateClone } from '../src/clone.js';
|
|
23
27
|
import { watchSite } from '../src/watch.js';
|
|
28
|
+
import { diffDarkMode } from '../src/darkdiff.js';
|
|
29
|
+
import { applyDesign } from '../src/apply.js';
|
|
24
30
|
import { nameFromUrl } from '../src/utils.js';
|
|
25
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
|
+
|
|
26
40
|
const program = new Command();
|
|
27
41
|
|
|
28
42
|
program
|
|
29
43
|
.name('designlang')
|
|
30
44
|
.description('Extract the complete design language from any website')
|
|
31
|
-
.version('
|
|
45
|
+
.version('6.0.0');
|
|
32
46
|
|
|
33
47
|
// ── Main command: extract ──────────────────────────────────────
|
|
34
48
|
program
|
|
@@ -41,46 +55,97 @@ program
|
|
|
41
55
|
.option('--dark', 'also extract dark mode styles')
|
|
42
56
|
.option('--depth <n>', 'number of internal pages to also crawl', parseInt, 0)
|
|
43
57
|
.option('--screenshots', 'capture component screenshots')
|
|
44
|
-
.option('--framework <type>', 'generate framework theme (react, shadcn)')
|
|
58
|
+
.option('--framework <type>', 'generate framework theme (react, shadcn, vue, svelte)')
|
|
45
59
|
.option('--responsive', 'capture design at multiple breakpoints')
|
|
46
60
|
.option('--interactions', 'capture hover/focus/active states')
|
|
47
61
|
.option('--full', 'enable all extra captures (screenshots, responsive, interactions)')
|
|
62
|
+
.option('--cookie <cookies...>', 'cookies for authenticated pages (name=value)')
|
|
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')
|
|
48
67
|
.option('--no-history', 'skip saving to history')
|
|
49
68
|
.option('--verbose', 'show detailed progress')
|
|
69
|
+
.option('-q, --quiet', 'suppress output except file paths')
|
|
50
70
|
.action(async (url, opts) => {
|
|
51
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
|
+
|
|
52
90
|
const prefix = opts.name || nameFromUrl(url);
|
|
53
|
-
const outDir = resolve(
|
|
91
|
+
const outDir = resolve(merged.out);
|
|
54
92
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
console.log(chalk.gray(` ${url}${opts.depth > 0 ? ` (+ ${opts.depth} pages)` : ''}`));
|
|
58
|
-
console.log('');
|
|
93
|
+
const jsonMode = opts.json || opts.jsonPretty;
|
|
94
|
+
const startTime = Date.now();
|
|
59
95
|
|
|
60
|
-
|
|
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
|
+
}
|
|
102
|
+
|
|
103
|
+
const spinner = jsonMode || opts.quiet
|
|
104
|
+
? { start() { return this; }, set text(v) {}, succeed() {}, fail() {}, info() {}, stop() {} }
|
|
105
|
+
: ora('Launching browser...').start();
|
|
61
106
|
|
|
62
107
|
try {
|
|
63
|
-
spinner.text = `Crawling${
|
|
108
|
+
spinner.text = `Crawling${merged.depth > 0 ? ` (depth: ${merged.depth})` : ''}...`;
|
|
109
|
+
// Parse auth options
|
|
110
|
+
const cookies = merged.cookie || [];
|
|
111
|
+
const headers = {};
|
|
112
|
+
if (merged.header) {
|
|
113
|
+
for (const h of merged.header) {
|
|
114
|
+
const [name, ...rest] = h.split(':');
|
|
115
|
+
if (name && rest.length) headers[name.trim()] = rest.join(':').trim();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
64
119
|
const design = await extractDesignLanguage(url, {
|
|
65
|
-
width:
|
|
66
|
-
height: parseInt(
|
|
67
|
-
wait:
|
|
68
|
-
dark:
|
|
69
|
-
depth:
|
|
70
|
-
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,
|
|
71
126
|
outDir,
|
|
127
|
+
ignore: merged.ignore,
|
|
128
|
+
cookies: cookies.length > 0 ? cookies : undefined,
|
|
129
|
+
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
72
130
|
});
|
|
73
131
|
|
|
74
132
|
// Responsive capture
|
|
75
|
-
if (
|
|
133
|
+
if (merged.responsive || merged.full) {
|
|
76
134
|
spinner.text = 'Capturing responsive breakpoints...';
|
|
77
|
-
design.responsive = await captureResponsive(url, { wait:
|
|
135
|
+
design.responsive = await captureResponsive(url, { wait: merged.wait });
|
|
78
136
|
}
|
|
79
137
|
|
|
80
138
|
// Interaction state capture
|
|
81
|
-
if (
|
|
139
|
+
if (merged.interactions || merged.full) {
|
|
82
140
|
spinner.text = 'Capturing interaction states...';
|
|
83
|
-
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);
|
|
84
149
|
}
|
|
85
150
|
|
|
86
151
|
spinner.text = 'Generating outputs...';
|
|
@@ -96,16 +161,23 @@ program
|
|
|
96
161
|
];
|
|
97
162
|
|
|
98
163
|
// Framework-specific themes
|
|
99
|
-
if (
|
|
164
|
+
if (merged.framework === 'react') {
|
|
100
165
|
files.push({ name: `${prefix}-theme.js`, content: formatReactTheme(design), label: 'React Theme' });
|
|
101
|
-
} else if (
|
|
166
|
+
} else if (merged.framework === 'shadcn') {
|
|
102
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' });
|
|
103
172
|
} else {
|
|
104
173
|
// Generate both by default
|
|
105
174
|
files.push({ name: `${prefix}-theme.js`, content: formatReactTheme(design), label: 'React Theme' });
|
|
106
175
|
files.push({ name: `${prefix}-shadcn-theme.css`, content: formatShadcnTheme(design), label: 'shadcn/ui Theme' });
|
|
107
176
|
}
|
|
108
177
|
|
|
178
|
+
// WordPress theme (always generated)
|
|
179
|
+
files.push({ name: `${prefix}-wordpress-theme.json`, content: formatWordPress(design), label: 'WordPress Theme' });
|
|
180
|
+
|
|
109
181
|
for (const file of files) {
|
|
110
182
|
writeFileSync(join(outDir, file.name), file.content, 'utf-8');
|
|
111
183
|
}
|
|
@@ -116,62 +188,111 @@ program
|
|
|
116
188
|
if (opts.verbose) spinner.info(`Snapshot #${histInfo.snapshotCount} saved for ${histInfo.hostname}`);
|
|
117
189
|
}
|
|
118
190
|
|
|
191
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
192
|
+
|
|
119
193
|
spinner.succeed('Extraction complete!');
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
console.log(` ${chalk.green('✓')} ${chalk.cyan(file.name)} ${chalk.gray(`(${sizeStr})`)} — ${file.label}`);
|
|
126
|
-
}
|
|
127
|
-
if (opts.screenshots && design.componentScreenshots && Object.keys(design.componentScreenshots).length > 0) {
|
|
128
|
-
for (const [, info] of Object.entries(design.componentScreenshots)) {
|
|
129
|
-
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));
|
|
130
199
|
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
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}`));
|
|
134
215
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
+
}
|
|
246
|
+
|
|
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
|
+
}
|
|
165
279
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
}
|
|
286
|
+
|
|
287
|
+
console.log(chalk.gray(` Completed in ${duration}s`));
|
|
288
|
+
console.log('');
|
|
171
289
|
}
|
|
172
|
-
console.log('');
|
|
173
290
|
|
|
174
291
|
} catch (err) {
|
|
292
|
+
if (jsonMode) {
|
|
293
|
+
process.stderr.write(JSON.stringify({ error: err.message }) + '\n');
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
175
296
|
spinner.fail('Extraction failed');
|
|
176
297
|
if (err.message.includes('playwright')) {
|
|
177
298
|
console.error(chalk.red('\n Playwright is not installed.'));
|
|
@@ -192,6 +313,8 @@ program
|
|
|
192
313
|
.action(async (urlA, urlB, opts) => {
|
|
193
314
|
if (!urlA.startsWith('http')) urlA = `https://${urlA}`;
|
|
194
315
|
if (!urlB.startsWith('http')) urlB = `https://${urlB}`;
|
|
316
|
+
validateUrl(urlA);
|
|
317
|
+
validateUrl(urlB);
|
|
195
318
|
|
|
196
319
|
console.log('');
|
|
197
320
|
console.log(chalk.bold(' designlang diff'));
|
|
@@ -248,6 +371,7 @@ program
|
|
|
248
371
|
.description('View design history for a website')
|
|
249
372
|
.action(async (url) => {
|
|
250
373
|
if (!url.startsWith('http')) url = `https://${url}`;
|
|
374
|
+
validateUrl(url);
|
|
251
375
|
const history = getHistory(url);
|
|
252
376
|
console.log('');
|
|
253
377
|
console.log(formatHistoryMarkdown(url, history));
|
|
@@ -306,6 +430,7 @@ program
|
|
|
306
430
|
.option('-o, --out <dir>', 'directory with token files to update', '.')
|
|
307
431
|
.action(async (url, opts) => {
|
|
308
432
|
if (!url.startsWith('http')) url = `https://${url}`;
|
|
433
|
+
validateUrl(url);
|
|
309
434
|
|
|
310
435
|
console.log('');
|
|
311
436
|
console.log(chalk.bold(' designlang sync'));
|
|
@@ -352,6 +477,7 @@ program
|
|
|
352
477
|
.option('-o, --out <dir>', 'output directory', './cloned-design')
|
|
353
478
|
.action(async (url, opts) => {
|
|
354
479
|
if (!url.startsWith('http')) url = `https://${url}`;
|
|
480
|
+
validateUrl(url);
|
|
355
481
|
|
|
356
482
|
console.log('');
|
|
357
483
|
console.log(chalk.bold(' designlang clone'));
|
|
@@ -390,6 +516,7 @@ program
|
|
|
390
516
|
.option('--interval <minutes>', 'check interval in minutes', parseInt, 60)
|
|
391
517
|
.action(async (url, opts) => {
|
|
392
518
|
if (!url.startsWith('http')) url = `https://${url}`;
|
|
519
|
+
validateUrl(url);
|
|
393
520
|
const intervalMs = (opts.interval || 60) * 60 * 1000;
|
|
394
521
|
|
|
395
522
|
console.log('');
|
|
@@ -428,6 +555,7 @@ program
|
|
|
428
555
|
.description('Score a website\'s design system quality')
|
|
429
556
|
.action(async (url) => {
|
|
430
557
|
if (!url.startsWith('http')) url = `https://${url}`;
|
|
558
|
+
validateUrl(url);
|
|
431
559
|
|
|
432
560
|
const spinner = ora('Analyzing design...').start();
|
|
433
561
|
|
|
@@ -486,4 +614,75 @@ program
|
|
|
486
614
|
}
|
|
487
615
|
});
|
|
488
616
|
|
|
617
|
+
// ── Apply command ──────────────────────────────────────────
|
|
618
|
+
program
|
|
619
|
+
.command('apply <url>')
|
|
620
|
+
.description('Extract and apply design directly to your project')
|
|
621
|
+
.option('-d, --dir <path>', 'project directory', '.')
|
|
622
|
+
.option('--framework <type>', 'force framework (tailwind, shadcn, css)')
|
|
623
|
+
.option('--cookie <cookies...>', 'cookies for authenticated pages')
|
|
624
|
+
.option('--header <headers...>', 'custom headers')
|
|
625
|
+
.action(async (url, opts) => {
|
|
626
|
+
if (!url.startsWith('http')) url = `https://${url}`;
|
|
627
|
+
validateUrl(url);
|
|
628
|
+
|
|
629
|
+
console.log('');
|
|
630
|
+
console.log(chalk.bold(' designlang apply'));
|
|
631
|
+
console.log(chalk.gray(` ${url} → ${resolve(opts.dir)}`));
|
|
632
|
+
console.log('');
|
|
633
|
+
|
|
634
|
+
const spinner = ora('Extracting design...').start();
|
|
635
|
+
|
|
636
|
+
try {
|
|
637
|
+
const result = await applyDesign(url, {
|
|
638
|
+
dir: resolve(opts.dir),
|
|
639
|
+
framework: opts.framework,
|
|
640
|
+
cookies: opts.cookie,
|
|
641
|
+
headers: opts.header ? Object.fromEntries(opts.header.map(h => { const [k, ...v] = h.split(':'); return [k.trim(), v.join(':').trim()]; })) : undefined,
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
spinner.succeed(`Applied ${result.framework} design!`);
|
|
645
|
+
console.log('');
|
|
646
|
+
for (const f of result.applied) {
|
|
647
|
+
console.log(` ${chalk.green('✓')} ${chalk.cyan(f.file)} — ${f.type}`);
|
|
648
|
+
}
|
|
649
|
+
console.log('');
|
|
650
|
+
|
|
651
|
+
} catch (err) {
|
|
652
|
+
spinner.fail('Apply failed');
|
|
653
|
+
console.error(chalk.red(`\n ${err.message}\n`));
|
|
654
|
+
process.exit(1);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
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
|
+
|
|
489
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"
|