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.
Files changed (40) hide show
  1. package/README.md +66 -5
  2. package/bin/design-extract.js +269 -70
  3. package/package.json +9 -4
  4. package/src/apply.js +65 -0
  5. package/src/config.js +36 -0
  6. package/src/crawler.js +247 -82
  7. package/src/darkdiff.js +65 -0
  8. package/src/extractors/animations.js +76 -8
  9. package/src/extractors/borders.js +40 -5
  10. package/src/extractors/components.js +100 -1
  11. package/src/extractors/fonts.js +82 -0
  12. package/src/extractors/gradients.js +100 -0
  13. package/src/extractors/icons.js +80 -0
  14. package/src/extractors/images.js +76 -0
  15. package/src/extractors/shadows.js +60 -17
  16. package/src/extractors/spacing.js +31 -2
  17. package/src/extractors/variables.js +20 -1
  18. package/src/extractors/zindex.js +65 -0
  19. package/src/formatters/figma.js +66 -47
  20. package/src/formatters/markdown.js +98 -0
  21. package/src/formatters/preview.js +65 -22
  22. package/src/formatters/svelte-theme.js +40 -0
  23. package/src/formatters/tailwind.js +57 -4
  24. package/src/formatters/theme.js +134 -0
  25. package/src/formatters/vue-theme.js +44 -0
  26. package/src/formatters/wordpress.js +84 -0
  27. package/src/history.js +8 -1
  28. package/src/index.js +54 -16
  29. package/src/utils.js +68 -0
  30. package/tests/cli.test.js +34 -0
  31. package/tests/extractors.test.js +661 -0
  32. package/tests/formatters.test.js +477 -0
  33. package/tests/utils.test.js +413 -0
  34. package/website/app/api/extract/route.js +85 -0
  35. package/website/app/components/Extractor.js +184 -0
  36. package/website/app/globals.css +291 -0
  37. package/website/app/page.js +13 -0
  38. package/website/next.config.mjs +10 -1
  39. package/website/package-lock.json +356 -0
  40. 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 **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';
@@ -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('4.0.1');
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(opts.out);
91
+ const outDir = resolve(merged.out);
54
92
 
55
- console.log('');
56
- console.log(chalk.bold(' designlang'));
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
- const spinner = ora('Launching browser...').start();
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${opts.depth > 0 ? ` (depth: ${opts.depth})` : ''}...`;
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: opts.width,
66
- height: parseInt(opts.height) || 800,
67
- wait: opts.wait,
68
- dark: opts.dark,
69
- depth: opts.depth,
70
- 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,
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 (opts.responsive || opts.full) {
133
+ if (merged.responsive || merged.full) {
76
134
  spinner.text = 'Capturing responsive breakpoints...';
77
- design.responsive = await captureResponsive(url, { wait: opts.wait });
135
+ design.responsive = await captureResponsive(url, { wait: merged.wait });
78
136
  }
79
137
 
80
138
  // Interaction state capture
81
- if (opts.interactions || opts.full) {
139
+ if (merged.interactions || merged.full) {
82
140
  spinner.text = 'Capturing interaction states...';
83
- 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);
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 (opts.framework === 'react') {
164
+ if (merged.framework === 'react') {
100
165
  files.push({ name: `${prefix}-theme.js`, content: formatReactTheme(design), label: 'React Theme' });
101
- } else if (opts.framework === 'shadcn') {
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
- console.log('');
121
- console.log(chalk.bold(' Output files:'));
122
- for (const file of files) {
123
- const size = Buffer.byteLength(file.content);
124
- const sizeStr = size > 1024 ? `${(size / 1024).toFixed(1)}KB` : `${size}B`;
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
- console.log('');
133
- 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}`));
134
215
 
135
- // Summary
136
- console.log('');
137
- console.log(chalk.bold(' Summary:'));
138
- if (design.meta.pagesAnalyzed > 1) {
139
- console.log(` ${chalk.gray('Pages:')} ${design.meta.pagesAnalyzed} pages analyzed`);
140
- }
141
- console.log(` ${chalk.gray('Colors:')} ${design.colors.all.length} unique colors`);
142
- console.log(` ${chalk.gray('Fonts:')} ${design.typography.families.map(f => f.name).join(', ') || 'none detected'}`);
143
- console.log(` ${chalk.gray('Spacing:')} ${design.spacing.scale.length} values${design.spacing.base ? ` (base: ${design.spacing.base}px)` : ''}`);
144
- console.log(` ${chalk.gray('Shadows:')} ${design.shadows.values.length} unique shadows`);
145
- console.log(` ${chalk.gray('Radii:')} ${design.borders.radii.length} unique values`);
146
- console.log(` ${chalk.gray('Breakpoints:')} ${design.breakpoints.length} breakpoints`);
147
- console.log(` ${chalk.gray('Components:')} ${Object.keys(design.components).length} patterns detected`);
148
- console.log(` ${chalk.gray('CSS Vars:')} ${Object.values(design.variables).reduce((s, v) => s + Object.keys(v).length, 0)} custom properties`);
149
- if (design.layout) {
150
- console.log(` ${chalk.gray('Layout:')} ${design.layout.gridCount} grids, ${design.layout.flexCount} flex containers`);
151
- }
152
- if (design.responsive) {
153
- console.log(` ${chalk.gray('Responsive:')} ${design.responsive.viewports.length} viewports, ${design.responsive.changes.length} breakpoint changes`);
154
- }
155
- if (design.interactions) {
156
- const ic = design.interactions;
157
- const total = ic.buttons.length + ic.links.length + ic.inputs.length;
158
- console.log(` ${chalk.gray('Interactions:')} ${total} state changes captured`);
159
- }
160
- if (design.score) {
161
- const s = design.score;
162
- const gradeColor = s.grade === 'A' ? chalk.green : s.grade === 'B' ? chalk.cyan : s.grade === 'C' ? chalk.yellow : chalk.red;
163
- console.log(` ${chalk.gray('Design Score:')} ${gradeColor(`${s.overall}/100 (${s.grade})`)}${s.issues.length > 0 ? ` — ${s.issues.length} issues` : ''}`);
164
- }
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
- // Accessibility summary
167
- if (design.accessibility) {
168
- const a = design.accessibility;
169
- const scoreColor = a.score >= 80 ? chalk.green : a.score >= 50 ? chalk.yellow : chalk.red;
170
- console.log(` ${chalk.gray('A11y:')} ${scoreColor(`${a.score}% WCAG score`)} (${a.failCount} failing pairs)`);
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": "4.0.1",
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"