designlang 5.0.0 → 7.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 (51) hide show
  1. package/.github/FUNDING.yml +1 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
  5. package/CHANGELOG.md +43 -0
  6. package/README.md +177 -6
  7. package/bin/design-extract.js +302 -92
  8. package/docs/superpowers/plans/2026-04-18-designlang-v7.md +1121 -0
  9. package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +150 -0
  10. package/package.json +13 -7
  11. package/src/config.js +59 -0
  12. package/src/crawler.js +297 -95
  13. package/src/extractors/a11y-remediation.js +47 -0
  14. package/src/extractors/animations.js +37 -5
  15. package/src/extractors/borders.js +40 -5
  16. package/src/extractors/component-clusters.js +39 -0
  17. package/src/extractors/components.js +77 -1
  18. package/src/extractors/css-health.js +151 -0
  19. package/src/extractors/gradients.js +25 -5
  20. package/src/extractors/scoring.js +20 -1
  21. package/src/extractors/semantic-regions.js +44 -0
  22. package/src/extractors/shadows.js +60 -17
  23. package/src/extractors/spacing.js +31 -2
  24. package/src/extractors/stack-fingerprint.js +88 -0
  25. package/src/extractors/variables.js +20 -1
  26. package/src/formatters/_token-ref.js +44 -0
  27. package/src/formatters/agent-rules.js +116 -0
  28. package/src/formatters/android-compose.js +164 -0
  29. package/src/formatters/dtcg-tokens.js +175 -0
  30. package/src/formatters/figma.js +66 -47
  31. package/src/formatters/flutter-dart.js +130 -0
  32. package/src/formatters/ios-swiftui.js +161 -0
  33. package/src/formatters/markdown.js +25 -0
  34. package/src/formatters/preview.js +65 -22
  35. package/src/formatters/svelte-theme.js +40 -0
  36. package/src/formatters/tailwind.js +57 -4
  37. package/src/formatters/theme.js +134 -0
  38. package/src/formatters/vue-theme.js +44 -0
  39. package/src/formatters/wordpress.js +267 -0
  40. package/src/history.js +8 -1
  41. package/src/index.js +76 -20
  42. package/src/mcp/resources.js +64 -0
  43. package/src/mcp/server.js +110 -0
  44. package/src/mcp/tools.js +149 -0
  45. package/src/utils.js +68 -0
  46. package/tests/cli.test.js +84 -0
  47. package/tests/extractors.test.js +792 -0
  48. package/tests/formatters.test.js +709 -0
  49. package/tests/mcp.test.js +68 -0
  50. package/tests/utils.test.js +413 -0
  51. package/website/app/globals.css +11 -11
@@ -46,15 +46,149 @@ export function formatReactTheme(design) {
46
46
  theme.shadows[s.label] = s.raw;
47
47
  }
48
48
 
49
+ // Component variant tokens for interactive states
50
+ theme.states = {
51
+ hover: { opacity: 0.08 },
52
+ focus: { opacity: 0.12 },
53
+ active: { opacity: 0.16 },
54
+ disabled: { opacity: 0.38 },
55
+ };
56
+
57
+ // Build MUI v5 theme
58
+ const muiTheme = buildMuiTheme(design);
59
+
60
+ // Build TypeScript type definition comment
61
+ const tsType = buildTypeComment(theme);
62
+
49
63
  return `// React Theme — extracted from ${design.meta.url}
50
64
  // Compatible with: Chakra UI, Stitches, Vanilla Extract, or any CSS-in-JS
51
65
 
66
+ ${tsType}
67
+
52
68
  export const theme = ${JSON.stringify(theme, null, 2)};
53
69
 
70
+ // MUI v5 theme
71
+ export const muiTheme = ${JSON.stringify(muiTheme, null, 2)};
72
+
54
73
  export default theme;
55
74
  `;
56
75
  }
57
76
 
77
+ function buildTypeComment(theme) {
78
+ const colorKeys = Object.keys(theme.colors || {}).map(k => ` ${k}: string;`).join('\n');
79
+ const fontKeys = Object.keys(theme.fonts || {}).map(k => ` ${k}: string;`).join('\n');
80
+ const sizeKeys = Object.keys(theme.fontSizes || {}).map(k => ` '${k}': string;`).join('\n');
81
+ const spaceKeys = Object.keys(theme.space || {}).map(k => ` '${k}': string;`).join('\n');
82
+ const radiiKeys = Object.keys(theme.radii || {}).map(k => ` ${k}: string;`).join('\n');
83
+ const shadowKeys = Object.keys(theme.shadows || {}).map(k => ` ${k}: string;`).join('\n');
84
+
85
+ return `/**
86
+ * TypeScript type definition for this theme:
87
+ *
88
+ * interface Theme {
89
+ * colors: {
90
+ ${colorKeys}
91
+ * };
92
+ * fonts: {
93
+ ${fontKeys}
94
+ * };
95
+ * fontSizes: {
96
+ ${sizeKeys}
97
+ * };
98
+ * space: {
99
+ ${spaceKeys}
100
+ * };
101
+ * radii: {
102
+ ${radiiKeys}
103
+ * };
104
+ * shadows: {
105
+ ${shadowKeys}
106
+ * };
107
+ * states: {
108
+ * hover: { opacity: number };
109
+ * focus: { opacity: number };
110
+ * active: { opacity: number };
111
+ * disabled: { opacity: number };
112
+ * };
113
+ * }
114
+ */`;
115
+ }
116
+
117
+ function buildMuiTheme(design) {
118
+ const { colors, typography, borders, shadows } = design;
119
+ const mui = { palette: {}, typography: {}, shape: {}, shadows: [] };
120
+
121
+ // Palette
122
+ if (colors.primary) {
123
+ mui.palette.primary = { main: colors.primary.hex };
124
+ const pHsl = toHslParts(colors.primary.hex);
125
+ if (pHsl) {
126
+ mui.palette.primary.light = `hsl(${pHsl.h}, ${pHsl.s}%, ${Math.min(pHsl.l + 15, 95)}%)`;
127
+ mui.palette.primary.dark = `hsl(${pHsl.h}, ${pHsl.s}%, ${Math.max(pHsl.l - 15, 10)}%)`;
128
+ }
129
+ }
130
+ if (colors.secondary) {
131
+ mui.palette.secondary = { main: colors.secondary.hex };
132
+ const sHsl = toHslParts(colors.secondary.hex);
133
+ if (sHsl) {
134
+ mui.palette.secondary.light = `hsl(${sHsl.h}, ${sHsl.s}%, ${Math.min(sHsl.l + 15, 95)}%)`;
135
+ mui.palette.secondary.dark = `hsl(${sHsl.h}, ${sHsl.s}%, ${Math.max(sHsl.l - 15, 10)}%)`;
136
+ }
137
+ }
138
+ mui.palette.background = {};
139
+ if (colors.backgrounds.length > 0) mui.palette.background.default = colors.backgrounds[0];
140
+ if (colors.backgrounds.length > 1) mui.palette.background.paper = colors.backgrounds[1];
141
+ else if (colors.backgrounds.length > 0) mui.palette.background.paper = colors.backgrounds[0];
142
+ mui.palette.text = {};
143
+ if (colors.text.length > 0) mui.palette.text.primary = colors.text[0];
144
+ if (colors.text.length > 1) mui.palette.text.secondary = colors.text[1];
145
+
146
+ // Typography
147
+ const bodyFont = typography.families.find(f => f.usage === 'body');
148
+ const headingFont = typography.families.find(f => f.usage === 'headings');
149
+ mui.typography.fontFamily = bodyFont ? `'${bodyFont.name}', sans-serif` : undefined;
150
+ for (const s of typography.scale.slice(0, 6)) {
151
+ const level = s.size >= 32 ? 'h1' : s.size >= 24 ? 'h2' : s.size >= 20 ? 'h3' : s.size >= 16 ? 'body1' : 'body2';
152
+ mui.typography[level] = {
153
+ fontSize: `${s.size}px`,
154
+ fontWeight: s.weight || 400,
155
+ lineHeight: s.lineHeight || 1.5,
156
+ };
157
+ if (headingFont && level.startsWith('h')) {
158
+ mui.typography[level].fontFamily = `'${headingFont.name}', sans-serif`;
159
+ }
160
+ }
161
+
162
+ // Shape
163
+ if (borders.radii.length > 0) {
164
+ const md = borders.radii.find(r => r.label === 'md') || borders.radii[0];
165
+ mui.shape.borderRadius = md.value;
166
+ }
167
+
168
+ // Shadows (first few)
169
+ mui.shadows = shadows.values.slice(0, 5).map(s => s.raw);
170
+
171
+ return mui;
172
+ }
173
+
174
+ function toHslParts(hex) {
175
+ if (!hex) return null;
176
+ const h = hex.replace('#', '');
177
+ const r = parseInt(h.slice(0, 2), 16) / 255;
178
+ const g = parseInt(h.slice(2, 4), 16) / 255;
179
+ const b = parseInt(h.slice(4, 6), 16) / 255;
180
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
181
+ const l = (max + min) / 2;
182
+ if (max === min) return { h: 0, s: 0, l: Math.round(l * 100) };
183
+ const d = max - min;
184
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
185
+ let hue;
186
+ if (max === r) hue = ((g - b) / d + (g < b ? 6 : 0)) / 6;
187
+ else if (max === g) hue = ((b - r) / d + 2) / 6;
188
+ else hue = ((r - g) / d + 4) / 6;
189
+ return { h: Math.round(hue * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
190
+ }
191
+
58
192
  export function formatShadcnTheme(design) {
59
193
  const { colors, borders } = design;
60
194
  const lines = ['@layer base {', ' :root {'];
@@ -0,0 +1,44 @@
1
+ import { rgbToHsl } from '../utils.js';
2
+
3
+ export function formatVueTheme(design) {
4
+ const { colors, typography, spacing, borders, shadows } = design;
5
+ const lines = [];
6
+
7
+ lines.push('// Vuetify 3 theme configuration');
8
+ lines.push('// Generated by designlang');
9
+ lines.push('');
10
+ lines.push('export const designlangTheme = {');
11
+ lines.push(' dark: false,');
12
+ lines.push(' colors: {');
13
+ if (colors.primary) lines.push(` primary: '${colors.primary.hex}',`);
14
+ if (colors.secondary) lines.push(` secondary: '${colors.secondary.hex}',`);
15
+ if (colors.accent) lines.push(` accent: '${colors.accent.hex}',`);
16
+ if (colors.backgrounds.length > 0) lines.push(` background: '${colors.backgrounds[0]}',`);
17
+ if (colors.text.length > 0) lines.push(` 'on-background': '${colors.text[0]}',`);
18
+ lines.push(' },');
19
+ lines.push('};');
20
+ lines.push('');
21
+
22
+ // CSS variables for non-Vuetify usage
23
+ lines.push('// CSS custom properties');
24
+ lines.push('export const cssVariables = `');
25
+ lines.push(':root {');
26
+ if (colors.primary) lines.push(` --dl-color-primary: ${colors.primary.hex};`);
27
+ if (colors.secondary) lines.push(` --dl-color-secondary: ${colors.secondary.hex};`);
28
+ for (const [i, n] of colors.neutrals.slice(0, 8).entries()) {
29
+ lines.push(` --dl-color-neutral-${i + 1}: ${n.hex};`);
30
+ }
31
+ for (const [i, val] of spacing.scale.slice(0, 12).entries()) {
32
+ lines.push(` --dl-spacing-${i + 1}: ${val}px;`);
33
+ }
34
+ for (const r of borders.radii) {
35
+ lines.push(` --dl-radius-${r.label}: ${r.value}px;`);
36
+ }
37
+ if (typography.families.length > 0) {
38
+ lines.push(` --dl-font-primary: '${typography.families[0].name}', sans-serif;`);
39
+ }
40
+ lines.push('}');
41
+ lines.push('`;');
42
+
43
+ return lines.join('\n');
44
+ }
@@ -0,0 +1,267 @@
1
+ import { resolveRef } from './_token-ref.js';
2
+
3
+ const HEADER_VERSION = '7.0.0';
4
+
5
+ function* walkLeaves(node, prefix) {
6
+ if (node == null || typeof node !== 'object') return;
7
+ if ('$value' in node && '$type' in node) {
8
+ yield { path: prefix, token: node };
9
+ return;
10
+ }
11
+ for (const key of Object.keys(node)) {
12
+ yield* walkLeaves(node[key], prefix ? `${prefix}.${key}` : key);
13
+ }
14
+ }
15
+
16
+ // "semantic.color.action.primary" → "action-primary"
17
+ // "primitive.spacing.s0" → "s0"
18
+ function slugFromPath(path) {
19
+ const parts = path.split('.');
20
+ const trimmed = parts.slice(1);
21
+ let segs;
22
+ if (trimmed[0] === 'color' && trimmed.length >= 3) {
23
+ segs = trimmed.slice(1);
24
+ } else if (trimmed[0] === 'spacing' || trimmed[0] === 'radius') {
25
+ segs = trimmed.slice(1);
26
+ } else {
27
+ segs = trimmed;
28
+ }
29
+ return segs
30
+ .map((s) => s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase())
31
+ .join('-')
32
+ .replace(/[^a-z0-9-]/g, '');
33
+ }
34
+
35
+ function titleFromPath(path) {
36
+ return slugFromPath(path)
37
+ .split('-')
38
+ .filter(Boolean)
39
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
40
+ .join(' ');
41
+ }
42
+
43
+ function collectWpColors(tokens) {
44
+ const entries = [];
45
+ const sem = tokens?.semantic?.color;
46
+ if (sem) {
47
+ for (const leaf of walkLeaves(sem, 'semantic.color')) {
48
+ if (leaf.token.$type !== 'color') continue;
49
+ const resolved = resolveRef(tokens, leaf.path);
50
+ if (typeof resolved !== 'string') continue;
51
+ entries.push({ slug: slugFromPath(leaf.path), color: resolved, name: titleFromPath(leaf.path) });
52
+ }
53
+ }
54
+ return entries;
55
+ }
56
+
57
+ function collectWpSpacing(tokens) {
58
+ const entries = [];
59
+ const spacing = tokens?.primitive?.spacing || {};
60
+ for (const key of Object.keys(spacing)) {
61
+ const tok = spacing[key];
62
+ if (!tok || tok.$type !== 'dimension') continue;
63
+ const resolved = resolveRef(tokens, `primitive.spacing.${key}`);
64
+ if (typeof resolved !== 'string') continue;
65
+ entries.push({ slug: key, size: resolved, name: key.toUpperCase() });
66
+ }
67
+ return entries;
68
+ }
69
+
70
+ function collectWpFontSizes(tokens, design) {
71
+ const entries = [];
72
+ const scale = design?.typography?.scale || [];
73
+ const labelFor = (s) => (s.tags && s.tags[0]) || `fs-${s.size}`;
74
+ for (const s of scale) {
75
+ const size = typeof s.size === 'number' ? `${s.size}px` : s.size;
76
+ const label = String(labelFor(s));
77
+ entries.push({ slug: label.toLowerCase(), size, name: label });
78
+ }
79
+ return entries;
80
+ }
81
+
82
+ function collectWpFontFamilies(tokens, design) {
83
+ const entries = [];
84
+ const fams = design?.typography?.families || [];
85
+ for (const f of fams) {
86
+ const name = typeof f === 'string' ? f : f?.name;
87
+ if (!name) continue;
88
+ const slug = name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
89
+ entries.push({ slug, fontFamily: `${name}, sans-serif`, name });
90
+ }
91
+ return entries;
92
+ }
93
+
94
+ function buildThemeJson(tokens, design) {
95
+ const palette = collectWpColors(tokens);
96
+ const spacingSizes = collectWpSpacing(tokens);
97
+ const fontSizes = collectWpFontSizes(tokens, design);
98
+ const fontFamilies = collectWpFontFamilies(tokens, design);
99
+
100
+ // Semantic surface/text for styles.color
101
+ const surfaceDefault = resolveRef(tokens, 'semantic.color.surface.default') || '#ffffff';
102
+ const textBody = resolveRef(tokens, 'semantic.color.text.body') || '#111111';
103
+
104
+ const theme = {
105
+ $schema: 'https://schemas.wp.org/trunk/theme.json',
106
+ version: 3,
107
+ settings: {
108
+ color: { palette },
109
+ typography: { fontSizes, fontFamilies },
110
+ spacing: { spacingSizes },
111
+ },
112
+ styles: {
113
+ color: {
114
+ background: `var(--wp--preset--color--surface-default, ${surfaceDefault})`,
115
+ text: `var(--wp--preset--color--text-body, ${textBody})`,
116
+ },
117
+ },
118
+ };
119
+ return JSON.stringify(theme, null, 2) + '\n';
120
+ }
121
+
122
+ function buildStyleCss(tokens, design) {
123
+ const source = tokens?.$metadata?.source || (design?.meta?.url ?? '');
124
+ const header = `/*
125
+ Theme Name: designlang extracted theme
126
+ Theme URI: https://github.com/Manavarya09/design-extract
127
+ Description: Block theme generated from ${source} by designlang v${HEADER_VERSION}
128
+ Version: 1.0.0
129
+ Author: designlang
130
+ License: MIT
131
+ Text Domain: designlang-theme
132
+ */
133
+ `;
134
+ const lines = [header, ':root {'];
135
+ for (const c of collectWpColors(tokens)) {
136
+ lines.push(` --${c.slug}: ${c.color};`);
137
+ }
138
+ for (const s of collectWpSpacing(tokens)) {
139
+ lines.push(` --spacing-${s.slug}: ${s.size};`);
140
+ }
141
+ lines.push('}');
142
+ return lines.join('\n') + '\n';
143
+ }
144
+
145
+ function buildFunctionsPhp() {
146
+ return `<?php
147
+ if (!function_exists('designlang_theme_support')) {
148
+ function designlang_theme_support() {
149
+ add_theme_support('wp-block-styles');
150
+ add_theme_support('editor-styles');
151
+ add_theme_support('responsive-embeds');
152
+ }
153
+ add_action('after_setup_theme', 'designlang_theme_support');
154
+ }
155
+ `;
156
+ }
157
+
158
+ function buildIndexPhp() {
159
+ return `<?php get_header(); get_template_part('template-parts/content'); get_footer(); ?>
160
+ `;
161
+ }
162
+
163
+ function buildIndexHtml() {
164
+ return `<!-- wp:template-part {"slug":"header"} /-->
165
+ <!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
166
+ <main class="wp-block-group"><!-- wp:post-content /--></main>
167
+ <!-- /wp:group -->
168
+ <!-- wp:template-part {"slug":"footer"} /-->
169
+ `;
170
+ }
171
+
172
+ // Full block-theme skeleton. `design` is optional context for typography
173
+ // (font families, type scale) that isn't in the DTCG token tree yet.
174
+ export function formatWordPressTheme(tokens, design = {}) {
175
+ return {
176
+ 'theme.json': buildThemeJson(tokens, design),
177
+ 'style.css': buildStyleCss(tokens, design),
178
+ 'functions.php': buildFunctionsPhp(),
179
+ 'index.php': buildIndexPhp(),
180
+ 'templates/index.html': buildIndexHtml(),
181
+ };
182
+ }
183
+
184
+ export function formatWordPress(design) {
185
+ const { colors, typography, spacing } = design;
186
+
187
+ const theme = {
188
+ $schema: "https://schemas.wp.org/trunk/theme.json",
189
+ version: 3,
190
+ settings: {
191
+ color: {
192
+ palette: [],
193
+ gradients: [],
194
+ },
195
+ typography: {
196
+ fontFamilies: [],
197
+ fontSizes: [],
198
+ },
199
+ spacing: {
200
+ spacingSizes: [],
201
+ },
202
+ layout: {
203
+ contentSize: "1200px",
204
+ wideSize: "1400px",
205
+ },
206
+ },
207
+ styles: {
208
+ color: {},
209
+ typography: {},
210
+ spacing: {},
211
+ },
212
+ };
213
+
214
+ // Colors
215
+ if (colors.primary) theme.settings.color.palette.push({ slug: 'primary', color: colors.primary.hex, name: 'Primary' });
216
+ if (colors.secondary) theme.settings.color.palette.push({ slug: 'secondary', color: colors.secondary.hex, name: 'Secondary' });
217
+ if (colors.accent) theme.settings.color.palette.push({ slug: 'accent', color: colors.accent.hex, name: 'Accent' });
218
+ for (let i = 0; i < Math.min(colors.neutrals.length, 5); i++) {
219
+ theme.settings.color.palette.push({ slug: `neutral-${i + 1}`, color: colors.neutrals[i].hex, name: `Neutral ${i + 1}` });
220
+ }
221
+ for (const bg of colors.backgrounds.slice(0, 3)) {
222
+ theme.settings.color.palette.push({ slug: `bg-${bg.replace('#', '')}`, color: bg, name: `Background ${bg}` });
223
+ }
224
+
225
+ // Typography
226
+ for (const fam of typography.families) {
227
+ theme.settings.typography.fontFamilies.push({ fontFamily: fam.name, slug: fam.name.toLowerCase().replace(/\s+/g, '-'), name: fam.name });
228
+ }
229
+ for (const s of typography.scale.slice(0, 8)) {
230
+ theme.settings.typography.fontSizes.push({ size: `${s.size}px`, slug: `size-${s.size}`, name: `${s.size}px` });
231
+ }
232
+
233
+ // Spacing
234
+ for (let i = 0; i < Math.min(spacing.scale.length, 8); i++) {
235
+ const val = spacing.scale[i];
236
+ theme.settings.spacing.spacingSizes.push({ size: `${val}px`, slug: `spacing-${val}`, name: `${val}px` });
237
+ }
238
+
239
+ // Layout from extracted containers
240
+ if (design.layout && design.layout.containerWidths.length > 0) {
241
+ theme.settings.layout.contentSize = design.layout.containerWidths[0].maxWidth;
242
+ }
243
+
244
+ // Body styles
245
+ if (typography.body) {
246
+ theme.styles.typography.fontSize = `${typography.body.size}px`;
247
+ theme.styles.typography.lineHeight = typography.body.lineHeight;
248
+ }
249
+ if (typography.families.length > 0) {
250
+ theme.styles.typography.fontFamily = typography.families[0].name;
251
+ }
252
+ if (colors.backgrounds.length > 0) {
253
+ theme.styles.color.background = colors.backgrounds[0];
254
+ }
255
+ if (colors.text.length > 0) {
256
+ theme.styles.color.text = colors.text[0];
257
+ }
258
+
259
+ // Gradients from design
260
+ if (design.gradients && design.gradients.gradients) {
261
+ for (const g of design.gradients.gradients.slice(0, 5)) {
262
+ theme.settings.color.gradients.push({ slug: `gradient-${theme.settings.color.gradients.length + 1}`, gradient: g.raw, name: `Gradient ${theme.settings.color.gradients.length + 1}` });
263
+ }
264
+ }
265
+
266
+ return JSON.stringify(theme, null, 2);
267
+ }
package/src/history.js CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
4
4
  import { join } from 'path';
5
+ import { homedir } from 'os';
5
6
 
6
- const HISTORY_DIR = join(process.env.HOME || process.env.USERPROFILE || '.', '.designlang');
7
+ const HISTORY_DIR = join(homedir(), '.designlang');
7
8
 
8
9
  function ensureDir() {
9
10
  mkdirSync(HISTORY_DIR, { recursive: true });
@@ -50,6 +51,12 @@ export function saveSnapshot(design) {
50
51
  };
51
52
 
52
53
  history.push(snapshot);
54
+
55
+ // Prune oldest entries if history exceeds 50 snapshots
56
+ if (history.length > 50) {
57
+ history = history.slice(history.length - 50);
58
+ }
59
+
53
60
  writeFileSync(file, JSON.stringify(history, null, 2), 'utf-8');
54
61
  return { hostname, snapshotCount: history.length, file };
55
62
  }
package/src/index.js CHANGED
@@ -16,10 +16,23 @@ import { extractZIndex } from './extractors/zindex.js';
16
16
  import { extractIcons } from './extractors/icons.js';
17
17
  import { extractFonts } from './extractors/fonts.js';
18
18
  import { extractImageStyles } from './extractors/images.js';
19
+ import { extractStackFingerprint } from './extractors/stack-fingerprint.js';
20
+ import { extractCssHealth } from './extractors/css-health.js';
21
+ import { remediateFailingPairs } from './extractors/a11y-remediation.js';
22
+ import { extractSemanticRegions } from './extractors/semantic-regions.js';
23
+ import { clusterComponents } from './extractors/component-clusters.js';
24
+
25
+ function safeExtract(fn, ...args) {
26
+ try { return fn(...args); } catch { return null; }
27
+ }
19
28
 
20
29
  export async function extractDesignLanguage(url, options = {}) {
21
- const rawData = await crawlPage(url, options);
30
+ const rawData = await crawlPage(url, {
31
+ ...options,
32
+ ignore: options.ignore,
33
+ });
22
34
  const styles = rawData.light.computedStyles;
35
+ const warnings = [];
23
36
 
24
37
  const design = {
25
38
  meta: {
@@ -29,46 +42,88 @@ export async function extractDesignLanguage(url, options = {}) {
29
42
  elementCount: styles.length,
30
43
  pagesAnalyzed: rawData.pagesAnalyzed || 1,
31
44
  },
32
- colors: extractColors(styles),
33
- typography: extractTypography(styles),
34
- spacing: extractSpacing(styles),
35
- shadows: extractShadows(styles),
36
- borders: extractBorders(styles),
37
- variables: extractVariables(rawData.light.cssVariables),
38
- breakpoints: extractBreakpoints(rawData.light.mediaQueries),
39
- animations: extractAnimations(styles, rawData.light.keyframes),
40
- components: extractComponents(styles),
41
- accessibility: extractAccessibility(styles),
42
- layout: extractLayout(styles),
43
- gradients: extractGradients(styles),
44
- zIndex: extractZIndex(styles),
45
- icons: rawData.light.icons ? extractIcons(rawData.light.icons) : { icons: [], count: 0 },
46
- fonts: rawData.light.fontData ? extractFonts(rawData.light.fontData) : { fonts: [], systemFonts: [] },
47
- images: rawData.light.images ? extractImageStyles(rawData.light.images) : { patterns: [], aspectRatios: [] },
45
+ colors: safeExtract(extractColors, styles) || { primary: null, secondary: null, accent: null, neutrals: [], backgrounds: [], text: [], gradients: [], all: [] },
46
+ typography: safeExtract(extractTypography, styles) || { families: [], scale: [] },
47
+ spacing: safeExtract(extractSpacing, styles) || { scale: [], base: null },
48
+ shadows: safeExtract(extractShadows, styles) || { values: [] },
49
+ borders: safeExtract(extractBorders, styles) || { radii: [] },
50
+ variables: safeExtract(extractVariables, rawData.light.cssVariables) || {},
51
+ breakpoints: safeExtract(extractBreakpoints, rawData.light.mediaQueries) || [],
52
+ animations: safeExtract(extractAnimations, styles, rawData.light.keyframes) || { transitions: [], keyframes: [] },
53
+ components: safeExtract(extractComponents, styles) || {},
54
+ accessibility: safeExtract(extractAccessibility, styles) || { score: 0, failCount: 0 },
55
+ layout: safeExtract(extractLayout, styles) || { gridCount: 0, flexCount: 0 },
56
+ gradients: safeExtract(extractGradients, styles) || { count: 0 },
57
+ zIndex: safeExtract(extractZIndex, styles) || { allValues: [], issues: [] },
58
+ icons: rawData.light.icons ? (safeExtract(extractIcons, rawData.light.icons) || { icons: [], count: 0 }) : { icons: [], count: 0 },
59
+ fonts: rawData.light.fontData ? (safeExtract(extractFonts, rawData.light.fontData) || { fonts: [], systemFonts: [] }) : { fonts: [], systemFonts: [] },
60
+ images: rawData.light.images ? (safeExtract(extractImageStyles, rawData.light.images) || { patterns: [], aspectRatios: [] }) : { patterns: [], aspectRatios: [] },
48
61
  componentScreenshots: rawData.componentScreenshots || {},
62
+ stack: safeExtract(extractStackFingerprint, rawData.light.stack) || { framework: 'unknown', css: { layer: 'unknown', tailwind: null }, analytics: [], detectedFrom: { globalCount: 0, scriptCount: 0, classSampleSize: 0 } },
63
+ cssHealth: safeExtract(extractCssHealth, rawData.light.cssCoverage) || null,
64
+ regions: safeExtract(extractSemanticRegions, rawData.light.sections) || [],
65
+ componentClusters: safeExtract(clusterComponents, rawData.light.componentCandidates) || [],
49
66
  score: null,
50
67
  };
51
68
 
69
+ // Track which extractors failed
70
+ const extractorChecks = [
71
+ ['colors', design.colors], ['typography', design.typography], ['spacing', design.spacing],
72
+ ['shadows', design.shadows], ['borders', design.borders], ['variables', design.variables],
73
+ ['breakpoints', design.breakpoints], ['animations', design.animations], ['components', design.components],
74
+ ['accessibility', design.accessibility], ['layout', design.layout], ['gradients', design.gradients],
75
+ ['zIndex', design.zIndex],
76
+ ];
77
+ for (const [name, result] of extractorChecks) {
78
+ if (result === null) warnings.push(`${name} extractor failed`);
79
+ }
80
+ design.warnings = warnings;
81
+
52
82
  if (rawData.dark) {
53
83
  design.darkMode = {
54
- colors: extractColors(rawData.dark.computedStyles),
55
- variables: extractVariables(rawData.dark.cssVariables),
84
+ colors: safeExtract(extractColors, rawData.dark.computedStyles) || { primary: null, secondary: null, accent: null, neutrals: [], backgrounds: [], text: [], gradients: [], all: [] },
85
+ variables: safeExtract(extractVariables, rawData.dark.cssVariables) || {},
56
86
  };
57
87
  }
58
88
 
59
- design.score = scoreDesignSystem(design);
89
+ // A11y remediation: derive failing pairs from accessibility extractor output
90
+ // and propose palette colors that pass the matching WCAG rule.
91
+ try {
92
+ const a11y = design.accessibility || {};
93
+ const palette = (design.colors?.all || []).map(c => c.hex).filter(Boolean);
94
+ const failingPairs = (a11y.pairs || [])
95
+ .filter(p => p.level === 'FAIL')
96
+ .map(p => ({
97
+ fg: p.foreground,
98
+ bg: p.background,
99
+ ratio: p.ratio,
100
+ rule: p.isLargeText ? 'AA-large' : 'AA-normal',
101
+ }));
102
+ design.accessibility = {
103
+ ...a11y,
104
+ failingPairs,
105
+ remediation: remediateFailingPairs(failingPairs, palette),
106
+ };
107
+ } catch { /* non-fatal */ }
108
+
109
+ design.score = safeExtract(scoreDesignSystem, design);
110
+ if (design.score === null) warnings.push('scoring failed');
60
111
 
61
112
  return design;
62
113
  }
63
114
 
64
115
  export { crawlPage } from './crawler.js';
65
116
  export { formatTokens } from './formatters/tokens.js';
117
+ export { formatDtcgTokens } from './formatters/dtcg-tokens.js';
66
118
  export { formatMarkdown } from './formatters/markdown.js';
67
119
  export { formatTailwind } from './formatters/tailwind.js';
68
120
  export { formatCssVars } from './formatters/css-vars.js';
69
121
  export { formatPreview } from './formatters/preview.js';
70
122
  export { formatFigma } from './formatters/figma.js';
71
123
  export { formatReactTheme, formatShadcnTheme } from './formatters/theme.js';
124
+ export { formatWordPress } from './formatters/wordpress.js';
125
+ export { formatVueTheme } from './formatters/vue-theme.js';
126
+ export { formatSvelteTheme } from './formatters/svelte-theme.js';
72
127
  export { diffDesigns, formatDiffMarkdown, formatDiffHtml } from './diff.js';
73
128
  export { saveSnapshot, getHistory, formatHistoryMarkdown } from './history.js';
74
129
  export { captureResponsive } from './extractors/responsive.js';
@@ -80,3 +135,4 @@ export { scoreDesignSystem } from './extractors/scoring.js';
80
135
  export { watchSite } from './watch.js';
81
136
  export { diffDarkMode } from './darkdiff.js';
82
137
  export { applyDesign } from './apply.js';
138
+ export { loadConfig, mergeConfig } from './config.js';
@@ -0,0 +1,64 @@
1
+ // MCP resources builder. Pure/testable — returns { list, read } over the
2
+ // loaded design + tokens. No transport concerns here.
3
+
4
+ const URIS = {
5
+ 'designlang://tokens/primitive': {
6
+ name: 'Primitive tokens',
7
+ description: 'DTCG primitive tier (raw colors, spacing, fonts, etc.)',
8
+ },
9
+ 'designlang://tokens/semantic': {
10
+ name: 'Semantic tokens',
11
+ description: 'DTCG semantic tier (aliases like action.primary, surface.default)',
12
+ },
13
+ 'designlang://regions': {
14
+ name: 'Semantic regions',
15
+ description: 'Detected page regions (hero, nav, footer, etc.) with bounds',
16
+ },
17
+ 'designlang://components': {
18
+ name: 'Component clusters',
19
+ description: 'Clustered component instances with variant CSS',
20
+ },
21
+ 'designlang://health': {
22
+ name: 'CSS health',
23
+ description: 'Coverage, dead-rule, and z-index-stack diagnostics',
24
+ },
25
+ };
26
+
27
+ function rpcError(code, message) {
28
+ const e = new Error(message);
29
+ e.code = code;
30
+ return e;
31
+ }
32
+
33
+ export function buildResources({ design, tokens }) {
34
+ function payloadFor(uri) {
35
+ switch (uri) {
36
+ case 'designlang://tokens/primitive': return tokens?.primitive ?? null;
37
+ case 'designlang://tokens/semantic': return tokens?.semantic ?? null;
38
+ case 'designlang://regions': return design?.regions ?? [];
39
+ case 'designlang://components': return design?.componentClusters ?? [];
40
+ case 'designlang://health': return design?.cssHealth ?? null;
41
+ default: return undefined;
42
+ }
43
+ }
44
+
45
+ return {
46
+ list() {
47
+ return Object.keys(URIS).map((uri) => ({
48
+ uri,
49
+ name: URIS[uri].name,
50
+ description: URIS[uri].description,
51
+ mimeType: 'application/json',
52
+ }));
53
+ },
54
+ read(uri) {
55
+ const payload = payloadFor(uri);
56
+ if (payload === undefined) throw rpcError(-32602, `Unknown resource URI: ${uri}`);
57
+ return {
58
+ uri,
59
+ mimeType: 'application/json',
60
+ text: JSON.stringify(payload),
61
+ };
62
+ },
63
+ };
64
+ }