designlang 6.0.0 → 7.1.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 (92) 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/.vercel/README.txt +11 -0
  6. package/.vercel/project.json +1 -0
  7. package/CHANGELOG.md +58 -0
  8. package/CONTRIBUTING.md +25 -0
  9. package/README.md +120 -8
  10. package/bin/design-extract.js +106 -3
  11. package/chrome-extension/README.md +41 -0
  12. package/chrome-extension/icons/favicon.svg +7 -0
  13. package/chrome-extension/icons/icon-128.png +0 -0
  14. package/chrome-extension/icons/icon-16.png +0 -0
  15. package/chrome-extension/icons/icon-32.png +0 -0
  16. package/chrome-extension/icons/icon-48.png +0 -0
  17. package/chrome-extension/manifest.json +26 -0
  18. package/chrome-extension/popup.html +167 -0
  19. package/chrome-extension/popup.js +59 -0
  20. package/docs/superpowers/plans/2026-04-18-designlang-v7.md +1121 -0
  21. package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +150 -0
  22. package/docs/superpowers/specs/2026-04-18-website-redesign-design.md +120 -0
  23. package/docs/superpowers/specs/2026-04-19-designlang-v7-1-design.md +111 -0
  24. package/package.json +5 -4
  25. package/src/config.js +26 -0
  26. package/src/crawler.js +136 -2
  27. package/src/extractors/a11y-remediation.js +47 -0
  28. package/src/extractors/component-clusters.js +39 -0
  29. package/src/extractors/css-health.js +151 -0
  30. package/src/extractors/scoring.js +20 -1
  31. package/src/extractors/semantic-regions.js +44 -0
  32. package/src/extractors/stack-fingerprint.js +88 -0
  33. package/src/formatters/_token-ref.js +44 -0
  34. package/src/formatters/agent-rules.js +116 -0
  35. package/src/formatters/android-compose.js +164 -0
  36. package/src/formatters/dtcg-tokens.js +175 -0
  37. package/src/formatters/flutter-dart.js +130 -0
  38. package/src/formatters/ios-swiftui.js +161 -0
  39. package/src/formatters/markdown.js +25 -0
  40. package/src/formatters/wordpress.js +183 -0
  41. package/src/index.js +30 -0
  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-cookies.js +73 -0
  46. package/tests/cli.test.js +50 -0
  47. package/tests/cookies.test.js +98 -0
  48. package/tests/extractors.test.js +131 -0
  49. package/tests/formatters.test.js +232 -0
  50. package/tests/mcp.test.js +68 -0
  51. package/website/app/api/extract/route.js +216 -56
  52. package/website/app/components/A11ySlider.js +369 -0
  53. package/website/app/components/Comparison.js +286 -0
  54. package/website/app/components/CssHealth.js +243 -0
  55. package/website/app/components/HeroExtractor.js +455 -0
  56. package/website/app/components/Marginalia.js +3 -0
  57. package/website/app/components/McpSection.js +223 -0
  58. package/website/app/components/PlatformTabs.js +250 -0
  59. package/website/app/components/RegionsComponents.js +429 -0
  60. package/website/app/components/Rule.js +13 -0
  61. package/website/app/components/Specimens.js +237 -0
  62. package/website/app/components/StructuredData.js +144 -0
  63. package/website/app/components/TokenBrowser.js +344 -0
  64. package/website/app/components/token-browser-sample.js +65 -0
  65. package/website/app/globals.css +415 -633
  66. package/website/app/icon.svg +7 -0
  67. package/website/app/layout.js +113 -6
  68. package/website/app/opengraph-image.js +170 -0
  69. package/website/app/page.js +325 -148
  70. package/website/app/robots.js +15 -0
  71. package/website/app/seo-config.js +82 -0
  72. package/website/app/sitemap.js +18 -0
  73. package/website/lib/cache.js +73 -0
  74. package/website/lib/rate-limit.js +30 -0
  75. package/website/lib/rate-limit.test.js +55 -0
  76. package/website/lib/specimens.json +86 -0
  77. package/website/lib/token-helpers.js +70 -0
  78. package/website/lib/url-safety.js +103 -0
  79. package/website/lib/url-safety.test.js +116 -0
  80. package/website/lib/zip-files.js +15 -0
  81. package/website/package-lock.json +85 -0
  82. package/website/package.json +1 -0
  83. package/website/public/favicon.svg +7 -0
  84. package/website/public/logo-specimen.svg +76 -0
  85. package/website/public/mark.svg +12 -0
  86. package/website/public/site.webmanifest +13 -0
  87. package/website/app/favicon.ico +0 -0
  88. package/website/public/file.svg +0 -1
  89. package/website/public/globe.svg +0 -1
  90. package/website/public/next.svg +0 -1
  91. package/website/public/vercel.svg +0 -1
  92. package/website/public/window.svg +0 -1
@@ -0,0 +1,175 @@
1
+ // DTCG v1 token formatter.
2
+ // Input: design object from extractDesignLanguage.
3
+ // Output: { $metadata, primitive, semantic } — every leaf is { $value, $type }.
4
+
5
+ function token(value, type, extensions) {
6
+ const t = { $value: value, $type: type };
7
+ if (extensions) t.$extensions = extensions;
8
+ return t;
9
+ }
10
+
11
+ function ref(path) { return `{${path}}`; }
12
+
13
+ // Normalize a color entry — may be a string hex or { hex } object.
14
+ function colorValue(v) {
15
+ if (v == null) return null;
16
+ if (typeof v === 'string') return v;
17
+ if (typeof v === 'object' && typeof v.hex === 'string') return v.hex;
18
+ return null;
19
+ }
20
+
21
+ // Normalize a dimension entry — may be a string ('4px'), a number (4), or { value } object.
22
+ function dimensionValue(v) {
23
+ if (v == null) return null;
24
+ if (typeof v === 'string') return v;
25
+ if (typeof v === 'number') return `${v}px`;
26
+ if (typeof v === 'object') {
27
+ if (typeof v.value === 'number') return `${v.value}px`;
28
+ if (typeof v.value === 'string') return v.value;
29
+ }
30
+ return null;
31
+ }
32
+
33
+ // Normalize a shadow entry — may be a string or { raw } object.
34
+ function shadowValue(v) {
35
+ if (v == null) return null;
36
+ if (typeof v === 'string') return v;
37
+ if (typeof v === 'object' && typeof v.raw === 'string') return v.raw;
38
+ return null;
39
+ }
40
+
41
+ // Normalize a font family entry — may be a string or { name } object.
42
+ function fontFamilyValue(v) {
43
+ if (v == null) return null;
44
+ if (typeof v === 'string') return v;
45
+ if (typeof v === 'object' && typeof v.name === 'string') return v.name;
46
+ return null;
47
+ }
48
+
49
+ // Normalize a typography scale size — may be number (px) or string ('16px').
50
+ function fontSizeValue(v) {
51
+ if (v == null) return null;
52
+ if (typeof v === 'string') return v;
53
+ if (typeof v === 'number') return `${v}px`;
54
+ return null;
55
+ }
56
+
57
+ function buildPrimitive(design) {
58
+ const brand = colorValue(design.colors?.primary) || colorValue(design.colors?.all?.[0]) || '#000000';
59
+ const secondary = colorValue(design.colors?.secondary);
60
+
61
+ const neutrals = {};
62
+ const rawNeutrals = design.colors?.neutrals || [];
63
+ for (let i = 0; i < rawNeutrals.length; i++) {
64
+ const cv = colorValue(rawNeutrals[i]);
65
+ if (cv) neutrals[`n${i * 100 + 100}`] = token(cv, 'color');
66
+ }
67
+
68
+ const background = {};
69
+ const rawBackgrounds = design.colors?.backgrounds || [];
70
+ for (let i = 0; i < rawBackgrounds.length; i++) {
71
+ const cv = colorValue(rawBackgrounds[i]);
72
+ if (cv) background[`bg${i}`] = token(cv, 'color');
73
+ }
74
+
75
+ const text = {};
76
+ const rawText = design.colors?.text || [];
77
+ for (let i = 0; i < rawText.length; i++) {
78
+ const cv = colorValue(rawText[i]);
79
+ if (cv) text[`text${i}`] = token(cv, 'color');
80
+ }
81
+
82
+ const color = {
83
+ brand: {
84
+ primary: token(brand, 'color'),
85
+ ...(secondary && { secondary: token(secondary, 'color') }),
86
+ },
87
+ neutral: neutrals,
88
+ background,
89
+ text,
90
+ };
91
+
92
+ const spacing = {};
93
+ const rawSpacing = design.spacing?.scale || [];
94
+ for (let i = 0; i < rawSpacing.length; i++) {
95
+ const dv = dimensionValue(rawSpacing[i]);
96
+ if (dv) spacing[`s${i}`] = token(dv, 'dimension');
97
+ }
98
+
99
+ const radius = {};
100
+ const rawRadii = design.borders?.radii || [];
101
+ for (let i = 0; i < rawRadii.length; i++) {
102
+ const dv = dimensionValue(rawRadii[i]);
103
+ if (dv) radius[`r${i}`] = token(dv, 'dimension');
104
+ }
105
+
106
+ const shadow = {};
107
+ const rawShadows = design.shadows?.values || [];
108
+ for (let i = 0; i < rawShadows.length; i++) {
109
+ const sv = shadowValue(rawShadows[i]);
110
+ if (sv) shadow[`sh${i}`] = token(sv, 'shadow');
111
+ }
112
+
113
+ const fontFamily = {};
114
+ const rawFamilies = design.typography?.families || [];
115
+ for (let i = 0; i < rawFamilies.length; i++) {
116
+ const fv = fontFamilyValue(rawFamilies[i]);
117
+ if (fv) fontFamily[`f${i}`] = token(fv, 'fontFamily');
118
+ }
119
+
120
+ return { color, spacing, radius, shadow, fontFamily };
121
+ }
122
+
123
+ function buildSemantic(design, primitive) {
124
+ const firstRadiusKey = Object.keys(primitive.radius)[0] || 'r0';
125
+ const firstShadowKey = Object.keys(primitive.shadow)[0] || 'sh0';
126
+
127
+ const color = {
128
+ action: {
129
+ primary: token(ref('primitive.color.brand.primary'), 'color'),
130
+ },
131
+ surface: {
132
+ default: token(ref('primitive.color.background.bg0'), 'color'),
133
+ },
134
+ text: {
135
+ body: token(ref('primitive.color.text.text0'), 'color'),
136
+ },
137
+ };
138
+ if (primitive.color.brand.secondary) {
139
+ color.action.secondary = token(ref('primitive.color.brand.secondary'), 'color');
140
+ }
141
+
142
+ const firstFamily = fontFamilyValue(design.typography?.families?.[0]) || 'system-ui';
143
+ const firstScale = design.typography?.scale?.[0] || {};
144
+ const typography = {
145
+ body: token({
146
+ fontFamily: firstFamily,
147
+ fontSize: fontSizeValue(firstScale.size) || '16px',
148
+ fontWeight: firstScale.weight || '400',
149
+ lineHeight: firstScale.lineHeight || '1.5',
150
+ }, 'typography'),
151
+ };
152
+
153
+ const radius = {
154
+ control: token(ref(`primitive.radius.${firstRadiusKey}`), 'dimension'),
155
+ };
156
+
157
+ const shadow = {
158
+ elevated: token(ref(`primitive.shadow.${firstShadowKey}`), 'shadow'),
159
+ };
160
+
161
+ return { color, typography, radius, shadow };
162
+ }
163
+
164
+ export function formatDtcgTokens(design) {
165
+ const primitive = buildPrimitive(design);
166
+ const semantic = buildSemantic(design, primitive);
167
+ const $metadata = {
168
+ generator: 'designlang',
169
+ version: '7.0.0',
170
+ spec: 'https://design-tokens.github.io/community-group/format/',
171
+ };
172
+ if (design.meta?.url) $metadata.source = design.meta.url;
173
+ if (design.meta?.timestamp) $metadata.generatedAt = design.meta.timestamp;
174
+ return { $metadata, primitive, semantic };
175
+ }
@@ -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