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 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 **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.
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
- | Dark mode | `--dark` | Extracts dark color scheme |
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 including layout (grid, flex, container) data
268
- 3. **Process** — 12 extractor modules parse, deduplicate, cluster, and classify the raw data
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
@@ -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('5.0.0');
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(opts.out);
91
+ const outDir = resolve(merged.out);
58
92
 
59
- console.log('');
60
- console.log(chalk.bold(' designlang'));
61
- console.log(chalk.gray(` ${url}${opts.depth > 0 ? ` (+ ${opts.depth} pages)` : ''}`));
62
- console.log('');
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 = ora('Launching browser...').start();
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${opts.depth > 0 ? ` (depth: ${opts.depth})` : ''}...`;
108
+ spinner.text = `Crawling${merged.depth > 0 ? ` (depth: ${merged.depth})` : ''}...`;
68
109
  // Parse auth options
69
- const cookies = opts.cookie || [];
110
+ const cookies = merged.cookie || [];
70
111
  const headers = {};
71
- if (opts.header) {
72
- for (const h of opts.header) {
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: opts.width,
80
- height: parseInt(opts.height) || 800,
81
- wait: opts.wait,
82
- dark: opts.dark,
83
- depth: opts.depth,
84
- screenshots: opts.screenshots || opts.full,
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 (opts.responsive || opts.full) {
133
+ if (merged.responsive || merged.full) {
92
134
  spinner.text = 'Capturing responsive breakpoints...';
93
- design.responsive = await captureResponsive(url, { wait: opts.wait });
135
+ design.responsive = await captureResponsive(url, { wait: merged.wait });
94
136
  }
95
137
 
96
138
  // Interaction state capture
97
- if (opts.interactions || opts.full) {
139
+ if (merged.interactions || merged.full) {
98
140
  spinner.text = 'Capturing interaction states...';
99
- design.interactions = await captureInteractions(url, { width: opts.width, height: parseInt(opts.height) || 800, wait: opts.wait });
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 (opts.framework === 'react') {
164
+ if (merged.framework === 'react') {
116
165
  files.push({ name: `${prefix}-theme.js`, content: formatReactTheme(design), label: 'React Theme' });
117
- } else if (opts.framework === 'shadcn') {
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
- console.log('');
137
- console.log(chalk.bold(' Output files:'));
138
- for (const file of files) {
139
- const size = Buffer.byteLength(file.content);
140
- const sizeStr = size > 1024 ? `${(size / 1024).toFixed(1)}KB` : `${size}B`;
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
- console.log('');
149
- console.log(chalk.gray(` Saved to ${outDir}`));
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
- // Summary
152
- console.log('');
153
- console.log(chalk.bold(' Summary:'));
154
- if (design.meta.pagesAnalyzed > 1) {
155
- console.log(` ${chalk.gray('Pages:')} ${design.meta.pagesAnalyzed} pages analyzed`);
156
- }
157
- console.log(` ${chalk.gray('Colors:')} ${design.colors.all.length} unique colors`);
158
- console.log(` ${chalk.gray('Fonts:')} ${design.typography.families.map(f => f.name).join(', ') || 'none detected'}`);
159
- console.log(` ${chalk.gray('Spacing:')} ${design.spacing.scale.length} values${design.spacing.base ? ` (base: ${design.spacing.base}px)` : ''}`);
160
- console.log(` ${chalk.gray('Shadows:')} ${design.shadows.values.length} unique shadows`);
161
- console.log(` ${chalk.gray('Radii:')} ${design.borders.radii.length} unique values`);
162
- console.log(` ${chalk.gray('Breakpoints:')} ${design.breakpoints.length} breakpoints`);
163
- console.log(` ${chalk.gray('Components:')} ${Object.keys(design.components).length} patterns detected`);
164
- console.log(` ${chalk.gray('CSS Vars:')} ${Object.values(design.variables).reduce((s, v) => s + Object.keys(v).length, 0)} custom properties`);
165
- if (design.layout) {
166
- console.log(` ${chalk.gray('Layout:')} ${design.layout.gridCount} grids, ${design.layout.flexCount} flex containers`);
167
- }
168
- if (design.responsive) {
169
- console.log(` ${chalk.gray('Responsive:')} ${design.responsive.viewports.length} viewports, ${design.responsive.changes.length} breakpoint changes`);
170
- }
171
- if (design.interactions) {
172
- const ic = design.interactions;
173
- const total = ic.buttons.length + ic.links.length + ic.inputs.length;
174
- console.log(` ${chalk.gray('Interactions:')} ${total} state changes captured`);
175
- }
176
- if (design.score) {
177
- const s = design.score;
178
- const gradeColor = s.grade === 'A' ? chalk.green : s.grade === 'B' ? chalk.cyan : s.grade === 'C' ? chalk.yellow : chalk.red;
179
- console.log(` ${chalk.gray('Design Score:')} ${gradeColor(`${s.overall}/100 (${s.grade})`)}${s.issues.length > 0 ? ` — ${s.issues.length} issues` : ''}`);
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
- // New v5 extractors
183
- if (design.gradients && design.gradients.count > 0) {
184
- console.log(` ${chalk.gray('Gradients:')} ${design.gradients.count} unique gradients`);
185
- }
186
- if (design.zIndex && design.zIndex.allValues.length > 0) {
187
- console.log(` ${chalk.gray('Z-Index:')} ${design.zIndex.allValues.length} layers${design.zIndex.issues.length > 0 ? ` (${design.zIndex.issues.length} issues)` : ''}`);
188
- }
189
- if (design.icons && design.icons.count > 0) {
190
- console.log(` ${chalk.gray('Icons:')} ${design.icons.count} SVG icons (${design.icons.dominantStyle || 'mixed'})`);
191
- }
192
- if (design.fonts && design.fonts.fonts.length > 0) {
193
- const sources = design.fonts.fonts.map(f => f.source).filter((v, i, a) => a.indexOf(v) === i);
194
- console.log(` ${chalk.gray('Font Files:')} ${design.fonts.fonts.length} fonts (${sources.join(', ')})`);
195
- }
196
- if (design.images && design.images.patterns.length > 0) {
197
- const total = design.images.patterns.reduce((s, p) => s + p.count, 0);
198
- console.log(` ${chalk.gray('Images:')} ${total} images, ${design.images.patterns.length} style patterns`);
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
- // Accessibility summary
202
- if (design.accessibility) {
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": "5.0.0",
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": ">=18"
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
+ }