designlang 6.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 +111 -1
- package/bin/design-extract.js +88 -2
- 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 +5 -4
- package/src/config.js +23 -0
- package/src/crawler.js +116 -0
- package/src/extractors/a11y-remediation.js +47 -0
- package/src/extractors/component-clusters.js +39 -0
- package/src/extractors/css-health.js +151 -0
- package/src/extractors/scoring.js +20 -1
- package/src/extractors/semantic-regions.js +44 -0
- package/src/extractors/stack-fingerprint.js +88 -0
- 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/flutter-dart.js +130 -0
- package/src/formatters/ios-swiftui.js +161 -0
- package/src/formatters/markdown.js +25 -0
- package/src/formatters/wordpress.js +183 -0
- package/src/index.js +30 -0
- package/src/mcp/resources.js +64 -0
- package/src/mcp/server.js +110 -0
- package/src/mcp/tools.js +149 -0
- package/tests/cli.test.js +50 -0
- package/tests/extractors.test.js +131 -0
- package/tests/formatters.test.js +232 -0
- package/tests/mcp.test.js +68 -0
- package/website/app/globals.css +11 -11
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Flutter Dart emitter — consumes a DTCG token object and emits a Dart file
|
|
2
|
+
// with a DesignTokens class and a buildDesignlangTheme() ThemeData helper.
|
|
3
|
+
|
|
4
|
+
import { resolveRef } from './_token-ref.js';
|
|
5
|
+
|
|
6
|
+
const HEADER_VERSION = '7.0.0';
|
|
7
|
+
|
|
8
|
+
function* walkLeaves(node, prefix) {
|
|
9
|
+
if (node == null || typeof node !== 'object') return;
|
|
10
|
+
if ('$value' in node && '$type' in node) {
|
|
11
|
+
yield { path: prefix, token: node };
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
for (const key of Object.keys(node)) {
|
|
15
|
+
yield* walkLeaves(node[key], prefix ? `${prefix}.${key}` : key);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function camelFromPath(path) {
|
|
20
|
+
const parts = path.split('.');
|
|
21
|
+
const trimmed = parts.slice(1);
|
|
22
|
+
let segs;
|
|
23
|
+
if (trimmed[0] === 'color' && trimmed.length >= 3) {
|
|
24
|
+
segs = trimmed.slice(1);
|
|
25
|
+
} else {
|
|
26
|
+
segs = trimmed;
|
|
27
|
+
}
|
|
28
|
+
return segs
|
|
29
|
+
.map((s, i) => (i === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1)))
|
|
30
|
+
.join('')
|
|
31
|
+
.replace(/[^a-zA-Z0-9]/g, '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hexToDartArgb(hex) {
|
|
35
|
+
if (typeof hex !== 'string') return null;
|
|
36
|
+
let h = hex.trim().replace(/^#/, '');
|
|
37
|
+
if (h.length === 3) h = h.split('').map((c) => c + c).join('');
|
|
38
|
+
if (h.length === 6 && /^[0-9a-fA-F]{6}$/.test(h)) return `0xFF${h.toUpperCase()}`;
|
|
39
|
+
if (h.length === 8 && /^[0-9a-fA-F]{8}$/.test(h)) return `0x${h.toUpperCase()}`;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function dimensionToNumber(value) {
|
|
44
|
+
if (typeof value === 'number') return value;
|
|
45
|
+
if (typeof value !== 'string') return null;
|
|
46
|
+
const m = value.match(/^(-?\d+(?:\.\d+)?)(px|pt|dp|rem|em)?$/);
|
|
47
|
+
if (!m) return null;
|
|
48
|
+
const n = parseFloat(m[1]);
|
|
49
|
+
if (m[2] === 'rem' || m[2] === 'em') return n * 16;
|
|
50
|
+
return n;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function formatFlutterDart(tokens) {
|
|
54
|
+
const source = tokens?.$metadata?.source || '';
|
|
55
|
+
const lines = [];
|
|
56
|
+
lines.push(`// Generated by designlang v${HEADER_VERSION}`);
|
|
57
|
+
if (source) lines.push(`// Source: ${source}`);
|
|
58
|
+
lines.push("import 'package:flutter/material.dart';");
|
|
59
|
+
lines.push('');
|
|
60
|
+
lines.push('class DesignTokens {');
|
|
61
|
+
lines.push(' DesignTokens._();');
|
|
62
|
+
lines.push('');
|
|
63
|
+
|
|
64
|
+
// Semantic colors
|
|
65
|
+
const semColor = tokens?.semantic?.color;
|
|
66
|
+
const semEntries = [];
|
|
67
|
+
if (semColor) {
|
|
68
|
+
lines.push(' // Semantic');
|
|
69
|
+
for (const leaf of walkLeaves(semColor, 'semantic.color')) {
|
|
70
|
+
if (leaf.token.$type !== 'color') continue;
|
|
71
|
+
const resolved = resolveRef(tokens, leaf.path);
|
|
72
|
+
const argb = hexToDartArgb(resolved);
|
|
73
|
+
if (!argb) continue;
|
|
74
|
+
const name = camelFromPath(leaf.path);
|
|
75
|
+
lines.push(` static const Color ${name} = Color(${argb});`);
|
|
76
|
+
semEntries.push({ name, path: leaf.path });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Primitive spacing
|
|
81
|
+
const spacing = tokens?.primitive?.spacing || {};
|
|
82
|
+
for (const key of Object.keys(spacing)) {
|
|
83
|
+
const tok = spacing[key];
|
|
84
|
+
if (!tok || tok.$type !== 'dimension') continue;
|
|
85
|
+
const n = dimensionToNumber(resolveRef(tokens, `primitive.spacing.${key}`));
|
|
86
|
+
if (n == null) continue;
|
|
87
|
+
lines.push(` static const double ${camelFromPath(`primitive.spacing.${key}`)} = ${n};`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Primitive radius
|
|
91
|
+
const radius = tokens?.primitive?.radius || {};
|
|
92
|
+
for (const key of Object.keys(radius)) {
|
|
93
|
+
const tok = radius[key];
|
|
94
|
+
if (!tok || tok.$type !== 'dimension') continue;
|
|
95
|
+
const n = dimensionToNumber(resolveRef(tokens, `primitive.radius.${key}`));
|
|
96
|
+
if (n == null) continue;
|
|
97
|
+
lines.push(` static const double ${camelFromPath(`primitive.radius.${key}`)} = ${n};`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Primitive colors for direct use
|
|
101
|
+
const primColor = tokens?.primitive?.color;
|
|
102
|
+
if (primColor) {
|
|
103
|
+
lines.push('');
|
|
104
|
+
lines.push(' // Primitive palette');
|
|
105
|
+
for (const leaf of walkLeaves(primColor, 'primitive.color')) {
|
|
106
|
+
if (leaf.token.$type !== 'color') continue;
|
|
107
|
+
const resolved = resolveRef(tokens, leaf.path);
|
|
108
|
+
const argb = hexToDartArgb(resolved);
|
|
109
|
+
if (!argb) continue;
|
|
110
|
+
lines.push(` static const Color ${camelFromPath(leaf.path)} = Color(${argb});`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
lines.push('}');
|
|
115
|
+
lines.push('');
|
|
116
|
+
|
|
117
|
+
// ThemeData helper
|
|
118
|
+
const hasActionPrimary = semEntries.some((e) => e.name === 'actionPrimary');
|
|
119
|
+
const hasSurfaceDefault = semEntries.some((e) => e.name === 'surfaceDefault');
|
|
120
|
+
lines.push('ThemeData buildDesignlangTheme() {');
|
|
121
|
+
lines.push(' return ThemeData(');
|
|
122
|
+
lines.push(' colorScheme: const ColorScheme.light(');
|
|
123
|
+
if (hasActionPrimary) lines.push(' primary: DesignTokens.actionPrimary,');
|
|
124
|
+
if (hasSurfaceDefault) lines.push(' surface: DesignTokens.surfaceDefault,');
|
|
125
|
+
lines.push(' ),');
|
|
126
|
+
lines.push(' );');
|
|
127
|
+
lines.push('}');
|
|
128
|
+
|
|
129
|
+
return lines.join('\n') + '\n';
|
|
130
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// iOS SwiftUI emitter — consumes a DTCG token object and produces a single
|
|
2
|
+
// Swift file exposing semantic Colors, primitive spacing/radius as CGFloat, and
|
|
3
|
+
// primitive palette colors.
|
|
4
|
+
|
|
5
|
+
import { resolveRef } from './_token-ref.js';
|
|
6
|
+
|
|
7
|
+
const HEADER_VERSION = '7.0.0';
|
|
8
|
+
|
|
9
|
+
// Convert a dotted DTCG path to camelCase identifier.
|
|
10
|
+
// e.g. "semantic.color.action.primary" → "actionPrimary"
|
|
11
|
+
// "primitive.color.brand.primary" → "brandPrimary"
|
|
12
|
+
// "primitive.spacing.s0" → "spacingS0"
|
|
13
|
+
// "primitive.radius.r0" → "radiusR0"
|
|
14
|
+
function camelFromPath(path) {
|
|
15
|
+
const parts = path.split('.');
|
|
16
|
+
// Drop leading "primitive"/"semantic" and the type bucket.
|
|
17
|
+
// We keep meaningful parts.
|
|
18
|
+
const trimmed = parts.slice(1); // drop primitive/semantic
|
|
19
|
+
// Drop the type segment for semantic colors ("color.action.primary" → "action.primary")
|
|
20
|
+
// For primitive colors ("color.brand.primary" → keep "brand.primary")
|
|
21
|
+
// For primitive spacing ("spacing.s0") we want "spacingS0" — keep all.
|
|
22
|
+
let segments;
|
|
23
|
+
if (trimmed[0] === 'color' && trimmed.length >= 3) {
|
|
24
|
+
segments = trimmed.slice(1);
|
|
25
|
+
} else {
|
|
26
|
+
segments = trimmed;
|
|
27
|
+
}
|
|
28
|
+
return segments
|
|
29
|
+
.map((s, i) => (i === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1)))
|
|
30
|
+
.join('')
|
|
31
|
+
.replace(/[^a-zA-Z0-9]/g, '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Convert "#rrggbb" or "#rgb" to "0xRRGGBB" for SwiftUI Color(hex:).
|
|
35
|
+
function hexToSwiftHex(hex) {
|
|
36
|
+
if (typeof hex !== 'string') return null;
|
|
37
|
+
let h = hex.trim().replace(/^#/, '');
|
|
38
|
+
if (h.length === 3) {
|
|
39
|
+
h = h.split('').map((c) => c + c).join('');
|
|
40
|
+
}
|
|
41
|
+
if (h.length === 6 && /^[0-9a-fA-F]{6}$/.test(h)) {
|
|
42
|
+
return `0x${h.toUpperCase()}`;
|
|
43
|
+
}
|
|
44
|
+
if (h.length === 8 && /^[0-9a-fA-F]{8}$/.test(h)) {
|
|
45
|
+
// drop alpha
|
|
46
|
+
return `0x${h.slice(2).toUpperCase()}`;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function dimensionToNumber(value) {
|
|
52
|
+
if (typeof value === 'number') return value;
|
|
53
|
+
if (typeof value !== 'string') return null;
|
|
54
|
+
const m = value.match(/^(-?\d+(?:\.\d+)?)(px|pt|rem|em)?$/);
|
|
55
|
+
if (!m) return null;
|
|
56
|
+
const n = parseFloat(m[1]);
|
|
57
|
+
const unit = m[2];
|
|
58
|
+
if (unit === 'rem' || unit === 'em') return n * 16;
|
|
59
|
+
return n;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Walk leaves of a subtree producing { path, token } pairs.
|
|
63
|
+
function* walkLeaves(node, prefix) {
|
|
64
|
+
if (node == null || typeof node !== 'object') return;
|
|
65
|
+
if ('$value' in node && '$type' in node) {
|
|
66
|
+
yield { path: prefix, token: node };
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
for (const key of Object.keys(node)) {
|
|
70
|
+
yield* walkLeaves(node[key], prefix ? `${prefix}.${key}` : key);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function formatIosSwiftUI(tokens) {
|
|
75
|
+
const lines = [];
|
|
76
|
+
const source = tokens?.$metadata?.source || '';
|
|
77
|
+
lines.push(`// Generated by designlang v${HEADER_VERSION} — https://github.com/Manavarya09/design-extract`);
|
|
78
|
+
if (source) lines.push(`// Source: ${source}`);
|
|
79
|
+
lines.push('import SwiftUI');
|
|
80
|
+
lines.push('');
|
|
81
|
+
lines.push('extension Color {');
|
|
82
|
+
lines.push(' init(hex: UInt32) {');
|
|
83
|
+
lines.push(' let r = Double((hex >> 16) & 0xFF) / 255');
|
|
84
|
+
lines.push(' let g = Double((hex >> 8) & 0xFF) / 255');
|
|
85
|
+
lines.push(' let b = Double(hex & 0xFF) / 255');
|
|
86
|
+
lines.push(' self.init(red: r, green: g, blue: b)');
|
|
87
|
+
lines.push(' }');
|
|
88
|
+
lines.push('');
|
|
89
|
+
lines.push(' // MARK: Semantic');
|
|
90
|
+
|
|
91
|
+
// Semantic colors
|
|
92
|
+
const semanticColor = tokens?.semantic?.color;
|
|
93
|
+
if (semanticColor) {
|
|
94
|
+
for (const leaf of walkLeaves(semanticColor, 'semantic.color')) {
|
|
95
|
+
if (leaf.token.$type !== 'color') continue;
|
|
96
|
+
const resolved = resolveRef(tokens, leaf.path);
|
|
97
|
+
const swiftHex = hexToSwiftHex(resolved);
|
|
98
|
+
if (!swiftHex) continue;
|
|
99
|
+
lines.push(` static let ${camelFromPath(leaf.path)} = Color(hex: ${swiftHex})`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
lines.push('}');
|
|
103
|
+
lines.push('');
|
|
104
|
+
|
|
105
|
+
// CGFloat spacing + radius
|
|
106
|
+
lines.push('extension CGFloat {');
|
|
107
|
+
lines.push(' // MARK: Primitive spacing');
|
|
108
|
+
const spacing = tokens?.primitive?.spacing || {};
|
|
109
|
+
for (const key of Object.keys(spacing)) {
|
|
110
|
+
const tok = spacing[key];
|
|
111
|
+
if (!tok || tok.$type !== 'dimension') continue;
|
|
112
|
+
const n = dimensionToNumber(resolveRef(tokens, `primitive.spacing.${key}`));
|
|
113
|
+
if (n == null) continue;
|
|
114
|
+
const name = camelFromPath(`primitive.spacing.${key}`);
|
|
115
|
+
lines.push(` static let ${name}: CGFloat = ${n}`);
|
|
116
|
+
}
|
|
117
|
+
lines.push('');
|
|
118
|
+
lines.push(' // MARK: Primitive radius');
|
|
119
|
+
const radius = tokens?.primitive?.radius || {};
|
|
120
|
+
for (const key of Object.keys(radius)) {
|
|
121
|
+
const tok = radius[key];
|
|
122
|
+
if (!tok || tok.$type !== 'dimension') continue;
|
|
123
|
+
const n = dimensionToNumber(resolveRef(tokens, `primitive.radius.${key}`));
|
|
124
|
+
if (n == null) continue;
|
|
125
|
+
const name = camelFromPath(`primitive.radius.${key}`);
|
|
126
|
+
lines.push(` static let ${name}: CGFloat = ${n}`);
|
|
127
|
+
}
|
|
128
|
+
lines.push('}');
|
|
129
|
+
lines.push('');
|
|
130
|
+
|
|
131
|
+
// Primitive palette colors for direct use
|
|
132
|
+
lines.push('// MARK: Primitive palette (for direct use)');
|
|
133
|
+
lines.push('extension Color {');
|
|
134
|
+
const primColor = tokens?.primitive?.color;
|
|
135
|
+
if (primColor) {
|
|
136
|
+
for (const leaf of walkLeaves(primColor, 'primitive.color')) {
|
|
137
|
+
if (leaf.token.$type !== 'color') continue;
|
|
138
|
+
const resolved = resolveRef(tokens, leaf.path);
|
|
139
|
+
const swiftHex = hexToSwiftHex(resolved);
|
|
140
|
+
if (!swiftHex) continue;
|
|
141
|
+
lines.push(` static let ${camelFromPath(leaf.path)} = Color(hex: ${swiftHex})`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
lines.push('}');
|
|
145
|
+
lines.push('');
|
|
146
|
+
|
|
147
|
+
// Typography hint comments (composite typography tokens)
|
|
148
|
+
const typography = tokens?.semantic?.typography;
|
|
149
|
+
if (typography) {
|
|
150
|
+
for (const key of Object.keys(typography)) {
|
|
151
|
+
const tok = typography[key];
|
|
152
|
+
if (!tok || tok.$type !== 'typography') continue;
|
|
153
|
+
const v = tok.$value || {};
|
|
154
|
+
lines.push(
|
|
155
|
+
`// typography.${key}: family=${v.fontFamily ?? ''} size=${v.fontSize ?? ''} weight=${v.fontWeight ?? ''} lineHeight=${v.lineHeight ?? ''}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return lines.join('\n') + '\n';
|
|
161
|
+
}
|
|
@@ -3,6 +3,7 @@ import { pxToRem } from '../utils.js';
|
|
|
3
3
|
export function formatMarkdown(design) {
|
|
4
4
|
const lines = [];
|
|
5
5
|
const { meta, colors, typography, spacing, shadows, borders, variables, breakpoints, animations, components } = design;
|
|
6
|
+
const componentClusters = Array.isArray(design.componentClusters) ? design.componentClusters : [];
|
|
6
7
|
|
|
7
8
|
lines.push(`# Design Language: ${meta.title || 'Unknown Site'}`);
|
|
8
9
|
lines.push('');
|
|
@@ -259,6 +260,30 @@ export function formatMarkdown(design) {
|
|
|
259
260
|
}
|
|
260
261
|
}
|
|
261
262
|
|
|
263
|
+
// ── Component Clusters (v7) ──
|
|
264
|
+
if (componentClusters.length > 0) {
|
|
265
|
+
lines.push('## Component Clusters');
|
|
266
|
+
lines.push('');
|
|
267
|
+
lines.push('Reusable component instances grouped by DOM structure and style similarity:');
|
|
268
|
+
lines.push('');
|
|
269
|
+
for (const cluster of componentClusters) {
|
|
270
|
+
const kindLabel = cluster.kind.charAt(0).toUpperCase() + cluster.kind.slice(1);
|
|
271
|
+
lines.push(`### ${kindLabel} — ${cluster.instanceCount} instance${cluster.instanceCount === 1 ? '' : 's'}, ${cluster.variants.length} variant${cluster.variants.length === 1 ? '' : 's'}`);
|
|
272
|
+
lines.push('');
|
|
273
|
+
cluster.variants.forEach((v, i) => {
|
|
274
|
+
lines.push(`**Variant ${i + 1}** (${v.instanceCount} instance${v.instanceCount === 1 ? '' : 's'})`);
|
|
275
|
+
lines.push('');
|
|
276
|
+
lines.push('```css');
|
|
277
|
+
for (const [prop, val] of Object.entries(v.css || {})) {
|
|
278
|
+
const cssProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
279
|
+
lines.push(` ${cssProp}: ${val};`);
|
|
280
|
+
}
|
|
281
|
+
lines.push('```');
|
|
282
|
+
lines.push('');
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
262
287
|
// ── Layout ──
|
|
263
288
|
if (design.layout) {
|
|
264
289
|
const l = design.layout;
|
|
@@ -1,3 +1,186 @@
|
|
|
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
|
+
|
|
1
184
|
export function formatWordPress(design) {
|
|
2
185
|
const { colors, typography, spacing } = design;
|
|
3
186
|
|
package/src/index.js
CHANGED
|
@@ -16,6 +16,11 @@ 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';
|
|
19
24
|
|
|
20
25
|
function safeExtract(fn, ...args) {
|
|
21
26
|
try { return fn(...args); } catch { return null; }
|
|
@@ -54,6 +59,10 @@ export async function extractDesignLanguage(url, options = {}) {
|
|
|
54
59
|
fonts: rawData.light.fontData ? (safeExtract(extractFonts, rawData.light.fontData) || { fonts: [], systemFonts: [] }) : { fonts: [], systemFonts: [] },
|
|
55
60
|
images: rawData.light.images ? (safeExtract(extractImageStyles, rawData.light.images) || { patterns: [], aspectRatios: [] }) : { patterns: [], aspectRatios: [] },
|
|
56
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) || [],
|
|
57
66
|
score: null,
|
|
58
67
|
};
|
|
59
68
|
|
|
@@ -77,6 +86,26 @@ export async function extractDesignLanguage(url, options = {}) {
|
|
|
77
86
|
};
|
|
78
87
|
}
|
|
79
88
|
|
|
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
|
+
|
|
80
109
|
design.score = safeExtract(scoreDesignSystem, design);
|
|
81
110
|
if (design.score === null) warnings.push('scoring failed');
|
|
82
111
|
|
|
@@ -85,6 +114,7 @@ export async function extractDesignLanguage(url, options = {}) {
|
|
|
85
114
|
|
|
86
115
|
export { crawlPage } from './crawler.js';
|
|
87
116
|
export { formatTokens } from './formatters/tokens.js';
|
|
117
|
+
export { formatDtcgTokens } from './formatters/dtcg-tokens.js';
|
|
88
118
|
export { formatMarkdown } from './formatters/markdown.js';
|
|
89
119
|
export { formatTailwind } from './formatters/tailwind.js';
|
|
90
120
|
export { formatCssVars } from './formatters/css-vars.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
|
+
}
|