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.
- package/.github/FUNDING.yml +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
- package/CHANGELOG.md +43 -0
- package/README.md +177 -6
- package/bin/design-extract.js +302 -92
- package/docs/superpowers/plans/2026-04-18-designlang-v7.md +1121 -0
- package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +150 -0
- package/package.json +13 -7
- package/src/config.js +59 -0
- package/src/crawler.js +297 -95
- package/src/extractors/a11y-remediation.js +47 -0
- package/src/extractors/animations.js +37 -5
- package/src/extractors/borders.js +40 -5
- package/src/extractors/component-clusters.js +39 -0
- package/src/extractors/components.js +77 -1
- package/src/extractors/css-health.js +151 -0
- package/src/extractors/gradients.js +25 -5
- package/src/extractors/scoring.js +20 -1
- package/src/extractors/semantic-regions.js +44 -0
- package/src/extractors/shadows.js +60 -17
- package/src/extractors/spacing.js +31 -2
- package/src/extractors/stack-fingerprint.js +88 -0
- package/src/extractors/variables.js +20 -1
- package/src/formatters/_token-ref.js +44 -0
- package/src/formatters/agent-rules.js +116 -0
- package/src/formatters/android-compose.js +164 -0
- package/src/formatters/dtcg-tokens.js +175 -0
- package/src/formatters/figma.js +66 -47
- package/src/formatters/flutter-dart.js +130 -0
- package/src/formatters/ios-swiftui.js +161 -0
- package/src/formatters/markdown.js +25 -0
- package/src/formatters/preview.js +65 -22
- package/src/formatters/svelte-theme.js +40 -0
- package/src/formatters/tailwind.js +57 -4
- package/src/formatters/theme.js +134 -0
- package/src/formatters/vue-theme.js +44 -0
- package/src/formatters/wordpress.js +267 -0
- package/src/history.js +8 -1
- package/src/index.js +76 -20
- package/src/mcp/resources.js +64 -0
- package/src/mcp/server.js +110 -0
- package/src/mcp/tools.js +149 -0
- package/src/utils.js +68 -0
- package/tests/cli.test.js +84 -0
- package/tests/extractors.test.js +792 -0
- package/tests/formatters.test.js +709 -0
- package/tests/mcp.test.js +68 -0
- package/tests/utils.test.js +413 -0
- package/website/app/globals.css +11 -11
package/src/formatters/theme.js
CHANGED
|
@@ -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(
|
|
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,
|
|
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
|
|
33
|
-
typography: extractTypography
|
|
34
|
-
spacing: extractSpacing
|
|
35
|
-
shadows: extractShadows
|
|
36
|
-
borders: extractBorders
|
|
37
|
-
variables: extractVariables
|
|
38
|
-
breakpoints: extractBreakpoints
|
|
39
|
-
animations: extractAnimations
|
|
40
|
-
components: extractComponents
|
|
41
|
-
accessibility: extractAccessibility
|
|
42
|
-
layout: extractLayout
|
|
43
|
-
gradients: extractGradients
|
|
44
|
-
zIndex: extractZIndex
|
|
45
|
-
icons: rawData.light.icons ? extractIcons
|
|
46
|
-
fonts: rawData.light.fontData ? extractFonts
|
|
47
|
-
images: rawData.light.images ? extractImageStyles
|
|
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
|
|
55
|
-
variables: extractVariables
|
|
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
|
-
|
|
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
|
+
}
|