designlang 7.1.0 → 8.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/CHANGELOG.md +49 -0
- package/README.md +46 -4
- package/bin/design-extract.js +28 -2
- package/package.json +1 -1
- package/src/config.js +4 -1
- package/src/crawler.js +376 -6
- package/src/extractors/accessibility.js +44 -1
- package/src/extractors/colors.js +50 -12
- package/src/extractors/interaction-states.js +57 -0
- package/src/extractors/modern-css.js +100 -0
- package/src/extractors/scoring.js +49 -30
- package/src/extractors/token-sources.js +65 -0
- package/src/extractors/wide-gamut.js +47 -0
- package/src/formatters/routes-reconciliation.js +160 -0
- package/src/index.js +29 -0
- package/src/utils/color-gamut.js +82 -0
- package/.github/FUNDING.yml +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -62
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -28
- package/chrome-extension/README.md +0 -41
- package/chrome-extension/icons/favicon.svg +0 -7
- package/chrome-extension/icons/icon-128.png +0 -0
- package/chrome-extension/icons/icon-16.png +0 -0
- package/chrome-extension/icons/icon-32.png +0 -0
- package/chrome-extension/icons/icon-48.png +0 -0
- package/chrome-extension/manifest.json +0 -26
- package/chrome-extension/popup.html +0 -167
- package/chrome-extension/popup.js +0 -59
- package/docs/superpowers/plans/2026-04-18-designlang-v7.md +0 -1121
- package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +0 -150
- package/docs/superpowers/specs/2026-04-18-website-redesign-design.md +0 -120
- package/docs/superpowers/specs/2026-04-19-designlang-v7-1-design.md +0 -111
- package/tests/cli.test.js +0 -84
- package/tests/cookies.test.js +0 -98
- package/tests/extractors.test.js +0 -792
- package/tests/formatters.test.js +0 -709
- package/tests/mcp.test.js +0 -68
- package/tests/utils.test.js +0 -413
- package/website/.claude/launch.json +0 -11
- package/website/AGENTS.md +0 -5
- package/website/CLAUDE.md +0 -1
- package/website/README.md +0 -36
- package/website/app/api/extract/route.js +0 -245
- package/website/app/components/A11ySlider.js +0 -369
- package/website/app/components/Comparison.js +0 -286
- package/website/app/components/CssHealth.js +0 -243
- package/website/app/components/Extractor.js +0 -184
- package/website/app/components/HeroExtractor.js +0 -455
- package/website/app/components/Marginalia.js +0 -3
- package/website/app/components/McpSection.js +0 -223
- package/website/app/components/PlatformTabs.js +0 -250
- package/website/app/components/RegionsComponents.js +0 -429
- package/website/app/components/Rule.js +0 -13
- package/website/app/components/Specimens.js +0 -237
- package/website/app/components/StructuredData.js +0 -144
- package/website/app/components/TokenBrowser.js +0 -344
- package/website/app/components/token-browser-sample.js +0 -65
- package/website/app/globals.css +0 -505
- package/website/app/icon.svg +0 -7
- package/website/app/layout.js +0 -126
- package/website/app/opengraph-image.js +0 -170
- package/website/app/page.js +0 -352
- package/website/app/robots.js +0 -15
- package/website/app/seo-config.js +0 -82
- package/website/app/sitemap.js +0 -18
- package/website/jsconfig.json +0 -7
- package/website/lib/cache.js +0 -73
- package/website/lib/rate-limit.js +0 -30
- package/website/lib/rate-limit.test.js +0 -55
- package/website/lib/specimens.json +0 -86
- package/website/lib/token-helpers.js +0 -70
- package/website/lib/url-safety.js +0 -103
- package/website/lib/url-safety.test.js +0 -116
- package/website/lib/zip-files.js +0 -15
- package/website/next.config.mjs +0 -15
- package/website/package-lock.json +0 -1353
- package/website/package.json +0 -19
- package/website/public/favicon.svg +0 -7
- package/website/public/logo-specimen.svg +0 -76
- package/website/public/mark.svg +0 -12
- package/website/public/site.webmanifest +0 -13
package/tests/formatters.test.js
DELETED
|
@@ -1,709 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { formatMarkdown } from '../src/formatters/markdown.js';
|
|
4
|
-
import { formatTokens } from '../src/formatters/tokens.js';
|
|
5
|
-
import { formatTailwind } from '../src/formatters/tailwind.js';
|
|
6
|
-
import { formatCssVars } from '../src/formatters/css-vars.js';
|
|
7
|
-
import { formatPreview } from '../src/formatters/preview.js';
|
|
8
|
-
import { formatFigma } from '../src/formatters/figma.js';
|
|
9
|
-
import { formatReactTheme, formatShadcnTheme } from '../src/formatters/theme.js';
|
|
10
|
-
import { formatDtcgTokens } from '../src/formatters/dtcg-tokens.js';
|
|
11
|
-
import { resolveRef } from '../src/formatters/_token-ref.js';
|
|
12
|
-
import { formatIosSwiftUI } from '../src/formatters/ios-swiftui.js';
|
|
13
|
-
import { formatAndroidCompose } from '../src/formatters/android-compose.js';
|
|
14
|
-
import { formatFlutterDart } from '../src/formatters/flutter-dart.js';
|
|
15
|
-
import { formatWordPressTheme } from '../src/formatters/wordpress.js';
|
|
16
|
-
import { formatAgentRules } from '../src/formatters/agent-rules.js';
|
|
17
|
-
|
|
18
|
-
// ── Shared mock design object ───────────────────────────────────
|
|
19
|
-
|
|
20
|
-
const mockDesign = {
|
|
21
|
-
meta: {
|
|
22
|
-
url: 'https://example.com',
|
|
23
|
-
title: 'Test Site',
|
|
24
|
-
timestamp: new Date().toISOString(),
|
|
25
|
-
elementCount: 100,
|
|
26
|
-
pagesAnalyzed: 1,
|
|
27
|
-
},
|
|
28
|
-
colors: {
|
|
29
|
-
primary: { hex: '#0066cc', rgb: { r: 0, g: 102, b: 204 }, hsl: { h: 210, s: 100, l: 40 }, count: 50 },
|
|
30
|
-
secondary: { hex: '#cc6600', rgb: { r: 204, g: 102, b: 0 }, hsl: { h: 30, s: 100, l: 40 }, count: 25 },
|
|
31
|
-
accent: { hex: '#00cc66', rgb: { r: 0, g: 204, b: 102 }, hsl: { h: 150, s: 100, l: 40 }, count: 10 },
|
|
32
|
-
neutrals: [
|
|
33
|
-
{ hex: '#333333', rgb: { r: 51, g: 51, b: 51 }, hsl: { h: 0, s: 0, l: 20 }, count: 30 },
|
|
34
|
-
{ hex: '#666666', rgb: { r: 102, g: 102, b: 102 }, hsl: { h: 0, s: 0, l: 40 }, count: 20 },
|
|
35
|
-
],
|
|
36
|
-
backgrounds: ['#ffffff', '#f5f5f5'],
|
|
37
|
-
text: ['#333333', '#666666'],
|
|
38
|
-
gradients: ['linear-gradient(to right, #0066cc, #00cc66)'],
|
|
39
|
-
all: [
|
|
40
|
-
{ hex: '#0066cc', rgb: { r: 0, g: 102, b: 204 }, hsl: { h: 210, s: 100, l: 40 }, count: 50, contexts: ['text', 'background'] },
|
|
41
|
-
{ hex: '#333333', rgb: { r: 51, g: 51, b: 51 }, hsl: { h: 0, s: 0, l: 20 }, count: 30, contexts: ['text'] },
|
|
42
|
-
],
|
|
43
|
-
},
|
|
44
|
-
typography: {
|
|
45
|
-
families: [
|
|
46
|
-
{ name: 'Inter', count: 80, usage: 'all' },
|
|
47
|
-
{ name: 'Playfair Display', count: 20, usage: 'headings' },
|
|
48
|
-
],
|
|
49
|
-
scale: [
|
|
50
|
-
{ size: 48, weight: '700', lineHeight: '1.2', letterSpacing: '-0.02em', tags: ['h1'], count: 5 },
|
|
51
|
-
{ size: 36, weight: '700', lineHeight: '1.3', letterSpacing: 'normal', tags: ['h2'], count: 8 },
|
|
52
|
-
{ size: 24, weight: '600', lineHeight: '1.4', letterSpacing: 'normal', tags: ['h3'], count: 12 },
|
|
53
|
-
{ size: 16, weight: '400', lineHeight: '1.5', letterSpacing: 'normal', tags: ['p', 'span'], count: 60 },
|
|
54
|
-
],
|
|
55
|
-
headings: [
|
|
56
|
-
{ size: 48, weight: '700', lineHeight: '1.2', letterSpacing: '-0.02em', tags: ['h1'], count: 5 },
|
|
57
|
-
{ size: 36, weight: '700', lineHeight: '1.3', letterSpacing: 'normal', tags: ['h2'], count: 8 },
|
|
58
|
-
],
|
|
59
|
-
body: { size: 16, weight: '400', lineHeight: '1.5', letterSpacing: 'normal', tags: ['p'], count: 60 },
|
|
60
|
-
weights: [{ weight: '400', count: 60 }, { weight: '600', count: 12 }, { weight: '700', count: 13 }],
|
|
61
|
-
},
|
|
62
|
-
spacing: {
|
|
63
|
-
base: 4,
|
|
64
|
-
scale: [4, 8, 12, 16, 24, 32, 48, 64],
|
|
65
|
-
tokens: { '1': '4px', '2': '8px', '3': '12px', '4': '16px', '6': '24px', '8': '32px', '12': '48px', '16': '64px' },
|
|
66
|
-
raw: [4, 8, 12, 16, 24, 32, 48, 64],
|
|
67
|
-
},
|
|
68
|
-
shadows: {
|
|
69
|
-
values: [
|
|
70
|
-
{ raw: '0 1px 3px rgba(0,0,0,0.1)', blur: 3, inset: false, label: 'sm' },
|
|
71
|
-
{ raw: '0 4px 12px rgba(0,0,0,0.15)', blur: 12, inset: false, label: 'md' },
|
|
72
|
-
],
|
|
73
|
-
},
|
|
74
|
-
borders: {
|
|
75
|
-
radii: [
|
|
76
|
-
{ value: 4, label: 'sm', count: 20 },
|
|
77
|
-
{ value: 8, label: 'md', count: 15 },
|
|
78
|
-
{ value: 16, label: 'lg', count: 5 },
|
|
79
|
-
],
|
|
80
|
-
widths: [1, 2],
|
|
81
|
-
styles: ['solid'],
|
|
82
|
-
},
|
|
83
|
-
variables: { colors: { '--color-primary': '#0066cc' }, spacing: {}, typography: {} },
|
|
84
|
-
breakpoints: [
|
|
85
|
-
{ value: 640, label: 'mobile', type: 'min-width' },
|
|
86
|
-
{ value: 768, label: 'tablet', type: 'min-width' },
|
|
87
|
-
{ value: 1024, label: 'desktop', type: 'min-width' },
|
|
88
|
-
],
|
|
89
|
-
animations: {
|
|
90
|
-
transitions: ['all 0.2s ease', 'opacity 0.3s ease-in-out'],
|
|
91
|
-
keyframes: [],
|
|
92
|
-
easings: ['ease', 'ease-in-out'],
|
|
93
|
-
durations: ['0.2s', '0.3s'],
|
|
94
|
-
},
|
|
95
|
-
components: {
|
|
96
|
-
buttons: {
|
|
97
|
-
count: 10,
|
|
98
|
-
baseStyle: { backgroundColor: '#0066cc', color: '#ffffff', borderRadius: '4px', fontSize: '14px' },
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
accessibility: { score: 90, passCount: 45, failCount: 5, totalPairs: 50, pairs: [] },
|
|
102
|
-
layout: {
|
|
103
|
-
gridCount: 5,
|
|
104
|
-
flexCount: 20,
|
|
105
|
-
gridColumns: [{ columns: 3, count: 5 }],
|
|
106
|
-
flexDirections: { 'row/nowrap': 15, 'column/nowrap': 5 },
|
|
107
|
-
justifyPatterns: {},
|
|
108
|
-
alignPatterns: {},
|
|
109
|
-
containerWidths: [{ maxWidth: '1200px', padding: '16px' }],
|
|
110
|
-
gaps: ['16px', '24px'],
|
|
111
|
-
topGrids: [{ columns: 'repeat(3, 1fr)', rows: 'none', gap: '24px' }],
|
|
112
|
-
topFlex: [],
|
|
113
|
-
},
|
|
114
|
-
gradients: { count: 0, gradients: [] },
|
|
115
|
-
zIndex: { allValues: [], layers: [], issues: [], scale: [] },
|
|
116
|
-
icons: { icons: [], count: 0 },
|
|
117
|
-
fonts: { fonts: [], systemFonts: [] },
|
|
118
|
-
images: { patterns: [], aspectRatios: [] },
|
|
119
|
-
componentScreenshots: {},
|
|
120
|
-
score: {
|
|
121
|
-
overall: 85,
|
|
122
|
-
grade: 'B',
|
|
123
|
-
scores: {
|
|
124
|
-
colorDiscipline: 85,
|
|
125
|
-
typographyConsistency: 100,
|
|
126
|
-
spacingSystem: 90,
|
|
127
|
-
shadowConsistency: 100,
|
|
128
|
-
radiusConsistency: 100,
|
|
129
|
-
accessibility: 90,
|
|
130
|
-
tokenization: 50,
|
|
131
|
-
},
|
|
132
|
-
issues: ['No CSS custom properties found'],
|
|
133
|
-
strengths: ['Tight, disciplined color palette'],
|
|
134
|
-
},
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
// ── formatMarkdown ──────────────────────────────────────────────
|
|
138
|
-
|
|
139
|
-
describe('formatMarkdown', () => {
|
|
140
|
-
it('returns a string', () => {
|
|
141
|
-
const result = formatMarkdown(mockDesign);
|
|
142
|
-
assert.equal(typeof result, 'string');
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('contains the site title', () => {
|
|
146
|
-
const result = formatMarkdown(mockDesign);
|
|
147
|
-
assert.ok(result.includes('Test Site'));
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('contains color palette section', () => {
|
|
151
|
-
const result = formatMarkdown(mockDesign);
|
|
152
|
-
assert.ok(result.includes('## Color Palette'));
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('contains typography section', () => {
|
|
156
|
-
const result = formatMarkdown(mockDesign);
|
|
157
|
-
assert.ok(result.includes('## Typography'));
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('contains spacing section', () => {
|
|
161
|
-
const result = formatMarkdown(mockDesign);
|
|
162
|
-
assert.ok(result.includes('## Spacing'));
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('contains the primary color hex', () => {
|
|
166
|
-
const result = formatMarkdown(mockDesign);
|
|
167
|
-
assert.ok(result.includes('#0066cc'));
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('contains font family names', () => {
|
|
171
|
-
const result = formatMarkdown(mockDesign);
|
|
172
|
-
assert.ok(result.includes('Inter'));
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('contains component patterns section', () => {
|
|
176
|
-
const result = formatMarkdown(mockDesign);
|
|
177
|
-
assert.ok(result.includes('## Component Patterns'));
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('contains design system score section', () => {
|
|
181
|
-
const result = formatMarkdown(mockDesign);
|
|
182
|
-
assert.ok(result.includes('## Design System Score'));
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('contains layout section', () => {
|
|
186
|
-
const result = formatMarkdown(mockDesign);
|
|
187
|
-
assert.ok(result.includes('## Layout System'));
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
// ── formatTokens ────────────────────────────────────────────────
|
|
192
|
-
|
|
193
|
-
describe('formatTokens', () => {
|
|
194
|
-
it('returns valid JSON', () => {
|
|
195
|
-
const result = formatTokens(mockDesign);
|
|
196
|
-
const parsed = JSON.parse(result);
|
|
197
|
-
assert.ok(typeof parsed === 'object');
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('contains color tokens', () => {
|
|
201
|
-
const parsed = JSON.parse(formatTokens(mockDesign));
|
|
202
|
-
assert.ok(parsed.color);
|
|
203
|
-
assert.ok(parsed.color.primary);
|
|
204
|
-
assert.equal(parsed.color.primary.$value, '#0066cc');
|
|
205
|
-
assert.equal(parsed.color.primary.$type, 'color');
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('contains fontFamily tokens', () => {
|
|
209
|
-
const parsed = JSON.parse(formatTokens(mockDesign));
|
|
210
|
-
assert.ok(parsed.fontFamily);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('contains spacing tokens', () => {
|
|
214
|
-
const parsed = JSON.parse(formatTokens(mockDesign));
|
|
215
|
-
assert.ok(parsed.spacing);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('contains borderRadius tokens', () => {
|
|
219
|
-
const parsed = JSON.parse(formatTokens(mockDesign));
|
|
220
|
-
assert.ok(parsed.borderRadius);
|
|
221
|
-
assert.ok(parsed.borderRadius.sm);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it('contains shadow tokens', () => {
|
|
225
|
-
const parsed = JSON.parse(formatTokens(mockDesign));
|
|
226
|
-
assert.ok(parsed.shadow);
|
|
227
|
-
assert.ok(parsed.shadow.sm);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('contains breakpoint tokens', () => {
|
|
231
|
-
const parsed = JSON.parse(formatTokens(mockDesign));
|
|
232
|
-
assert.ok(parsed.breakpoint);
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// ── formatDtcgTokens ────────────────────────────────────────────
|
|
237
|
-
|
|
238
|
-
describe('formatDtcgTokens', () => {
|
|
239
|
-
const minimalDesign = {
|
|
240
|
-
colors: { primary: '#3b82f6', secondary: '#10b981', neutrals: ['#111','#888','#eee'], backgrounds: ['#fff'], text: ['#111'], all: [] },
|
|
241
|
-
typography: { families: ['Inter'], scale: [{ size:'16px', weight:'400', lineHeight:'1.5' }] },
|
|
242
|
-
spacing: { scale: ['4px','8px','16px'], base: '4px' },
|
|
243
|
-
shadows: { values: ['0 1px 2px rgba(0,0,0,0.1)'] },
|
|
244
|
-
borders: { radii: ['4px','8px'] },
|
|
245
|
-
variables: {},
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
it('emits $value/$type for every leaf', () => {
|
|
249
|
-
const out = formatDtcgTokens(minimalDesign);
|
|
250
|
-
assert.equal(out.primitive.color.brand.primary.$value, '#3b82f6');
|
|
251
|
-
assert.equal(out.primitive.color.brand.primary.$type, 'color');
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('emits semantic aliases referencing primitives', () => {
|
|
255
|
-
const out = formatDtcgTokens(minimalDesign);
|
|
256
|
-
assert.match(out.semantic.color.action.primary.$value, /^\{primitive\.color\.brand\.primary\}$/);
|
|
257
|
-
assert.equal(out.semantic.color.action.primary.$type, 'color');
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('emits composite typography tokens', () => {
|
|
261
|
-
const out = formatDtcgTokens(minimalDesign);
|
|
262
|
-
const body = out.semantic.typography.body;
|
|
263
|
-
assert.equal(body.$type, 'typography');
|
|
264
|
-
assert.equal(body.$value.fontFamily, 'Inter');
|
|
265
|
-
assert.equal(body.$value.fontSize, '16px');
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it('round-trips through JSON unchanged', () => {
|
|
269
|
-
const out = formatDtcgTokens(minimalDesign);
|
|
270
|
-
assert.deepEqual(JSON.parse(JSON.stringify(out)), out);
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
// ── formatTailwind ──────────────────────────────────────────────
|
|
275
|
-
|
|
276
|
-
describe('formatTailwind', () => {
|
|
277
|
-
it('returns a string', () => {
|
|
278
|
-
const result = formatTailwind(mockDesign);
|
|
279
|
-
assert.equal(typeof result, 'string');
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it('contains export default', () => {
|
|
283
|
-
const result = formatTailwind(mockDesign);
|
|
284
|
-
assert.ok(result.includes('export default'));
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('contains primary color', () => {
|
|
288
|
-
const result = formatTailwind(mockDesign);
|
|
289
|
-
assert.ok(result.includes('#0066cc'));
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it('contains font family', () => {
|
|
293
|
-
const result = formatTailwind(mockDesign);
|
|
294
|
-
assert.ok(result.includes('Inter'));
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it('contains spacing values', () => {
|
|
298
|
-
const result = formatTailwind(mockDesign);
|
|
299
|
-
assert.ok(result.includes('4px'));
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it('contains screen breakpoints', () => {
|
|
303
|
-
const result = formatTailwind(mockDesign);
|
|
304
|
-
assert.ok(result.includes('768px'));
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
// ── formatCssVars ───────────────────────────────────────────────
|
|
309
|
-
|
|
310
|
-
describe('formatCssVars', () => {
|
|
311
|
-
it('returns a string', () => {
|
|
312
|
-
const result = formatCssVars(mockDesign);
|
|
313
|
-
assert.equal(typeof result, 'string');
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
it('starts with :root {', () => {
|
|
317
|
-
const result = formatCssVars(mockDesign);
|
|
318
|
-
assert.ok(result.startsWith(':root {'));
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it('ends with closing brace', () => {
|
|
322
|
-
const result = formatCssVars(mockDesign);
|
|
323
|
-
assert.ok(result.trimEnd().endsWith('}'));
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it('contains color variables', () => {
|
|
327
|
-
const result = formatCssVars(mockDesign);
|
|
328
|
-
assert.ok(result.includes('--color-primary: #0066cc;'));
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it('contains spacing variables', () => {
|
|
332
|
-
const result = formatCssVars(mockDesign);
|
|
333
|
-
assert.ok(result.includes('--spacing-'));
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
it('contains font variables', () => {
|
|
337
|
-
const result = formatCssVars(mockDesign);
|
|
338
|
-
assert.ok(result.includes('--font-'));
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it('contains radius variables', () => {
|
|
342
|
-
const result = formatCssVars(mockDesign);
|
|
343
|
-
assert.ok(result.includes('--radius-'));
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it('contains shadow variables', () => {
|
|
347
|
-
const result = formatCssVars(mockDesign);
|
|
348
|
-
assert.ok(result.includes('--shadow-'));
|
|
349
|
-
});
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
// ── formatPreview ───────────────────────────────────────────────
|
|
353
|
-
|
|
354
|
-
describe('formatPreview', () => {
|
|
355
|
-
it('returns a string', () => {
|
|
356
|
-
const result = formatPreview(mockDesign);
|
|
357
|
-
assert.equal(typeof result, 'string');
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
it('returns valid HTML with doctype', () => {
|
|
361
|
-
const result = formatPreview(mockDesign);
|
|
362
|
-
assert.ok(result.includes('<!DOCTYPE html>'));
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
it('contains html and body tags', () => {
|
|
366
|
-
const result = formatPreview(mockDesign);
|
|
367
|
-
assert.ok(result.includes('<html'));
|
|
368
|
-
assert.ok(result.includes('<body>'));
|
|
369
|
-
assert.ok(result.includes('</body>'));
|
|
370
|
-
assert.ok(result.includes('</html>'));
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it('contains the site title', () => {
|
|
374
|
-
const result = formatPreview(mockDesign);
|
|
375
|
-
assert.ok(result.includes('Test Site'));
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
it('contains color swatches section', () => {
|
|
379
|
-
const result = formatPreview(mockDesign);
|
|
380
|
-
assert.ok(result.includes('Color Palette'));
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
it('contains typography section', () => {
|
|
384
|
-
const result = formatPreview(mockDesign);
|
|
385
|
-
assert.ok(result.includes('Typography'));
|
|
386
|
-
});
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
// ── formatFigma ─────────────────────────────────────────────────
|
|
390
|
-
|
|
391
|
-
describe('formatFigma', () => {
|
|
392
|
-
it('returns valid JSON', () => {
|
|
393
|
-
const result = formatFigma(mockDesign);
|
|
394
|
-
const parsed = JSON.parse(result);
|
|
395
|
-
assert.ok(typeof parsed === 'object');
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
it('has collections array with Brand collection', () => {
|
|
399
|
-
const parsed = JSON.parse(formatFigma(mockDesign));
|
|
400
|
-
assert.ok(Array.isArray(parsed.collections));
|
|
401
|
-
const brand = parsed.collections.find(c => c.name === 'Brand');
|
|
402
|
-
assert.ok(brand);
|
|
403
|
-
assert.ok(Array.isArray(brand.modes));
|
|
404
|
-
assert.ok(brand.variables.length > 0);
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
it('has Typography and Spacing collections', () => {
|
|
408
|
-
const parsed = JSON.parse(formatFigma(mockDesign));
|
|
409
|
-
const typo = parsed.collections.find(c => c.name === 'Typography');
|
|
410
|
-
const spacing = parsed.collections.find(c => c.name === 'Spacing');
|
|
411
|
-
assert.ok(typo);
|
|
412
|
-
assert.ok(spacing);
|
|
413
|
-
assert.ok(typo.variables.length > 0);
|
|
414
|
-
assert.ok(spacing.variables.length > 0);
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
it('contains color variables with normalized RGB in light mode', () => {
|
|
418
|
-
const parsed = JSON.parse(formatFigma(mockDesign));
|
|
419
|
-
const brand = parsed.collections.find(c => c.name === 'Brand');
|
|
420
|
-
const primary = brand.variables.find(v => v.name === 'color/primary');
|
|
421
|
-
assert.ok(primary);
|
|
422
|
-
assert.ok(primary.values.light.r >= 0 && primary.values.light.r <= 1);
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
it('contains spacing variables', () => {
|
|
426
|
-
const parsed = JSON.parse(formatFigma(mockDesign));
|
|
427
|
-
const spacing = parsed.collections.find(c => c.name === 'Spacing');
|
|
428
|
-
const spacingVars = spacing.variables.filter(v => v.name.startsWith('spacing/'));
|
|
429
|
-
assert.ok(spacingVars.length > 0);
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
it('contains radius variables', () => {
|
|
433
|
-
const parsed = JSON.parse(formatFigma(mockDesign));
|
|
434
|
-
const spacing = parsed.collections.find(c => c.name === 'Spacing');
|
|
435
|
-
const radiusVars = spacing.variables.filter(v => v.name.startsWith('radius/'));
|
|
436
|
-
assert.ok(radiusVars.length > 0);
|
|
437
|
-
});
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
// ── formatReactTheme ────────────────────────────────────────────
|
|
441
|
-
|
|
442
|
-
describe('formatReactTheme', () => {
|
|
443
|
-
it('returns a string', () => {
|
|
444
|
-
const result = formatReactTheme(mockDesign);
|
|
445
|
-
assert.equal(typeof result, 'string');
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
it('contains export const theme', () => {
|
|
449
|
-
const result = formatReactTheme(mockDesign);
|
|
450
|
-
assert.ok(result.includes('export const theme'));
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
it('contains export default theme', () => {
|
|
454
|
-
const result = formatReactTheme(mockDesign);
|
|
455
|
-
assert.ok(result.includes('export default theme'));
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
it('contains the primary color', () => {
|
|
459
|
-
const result = formatReactTheme(mockDesign);
|
|
460
|
-
assert.ok(result.includes('#0066cc'));
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
it('contains font family', () => {
|
|
464
|
-
const result = formatReactTheme(mockDesign);
|
|
465
|
-
assert.ok(result.includes('Inter'));
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
it('contains spacing values', () => {
|
|
469
|
-
const result = formatReactTheme(mockDesign);
|
|
470
|
-
assert.ok(result.includes('4px'));
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
it('contains the embedded JSON as valid JS', () => {
|
|
474
|
-
const result = formatReactTheme(mockDesign);
|
|
475
|
-
// Extract the JSON object from the template
|
|
476
|
-
const jsonMatch = result.match(/export const theme = ({[\s\S]+?});/);
|
|
477
|
-
assert.ok(jsonMatch, 'Should contain a theme object');
|
|
478
|
-
// The JSON should be parseable
|
|
479
|
-
const parsed = JSON.parse(jsonMatch[1]);
|
|
480
|
-
assert.ok(parsed.colors);
|
|
481
|
-
assert.ok(parsed.fonts);
|
|
482
|
-
});
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
// ── formatShadcnTheme ───────────────────────────────────────────
|
|
486
|
-
|
|
487
|
-
describe('formatShadcnTheme', () => {
|
|
488
|
-
it('returns a string', () => {
|
|
489
|
-
const result = formatShadcnTheme(mockDesign);
|
|
490
|
-
assert.equal(typeof result, 'string');
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
it('contains @layer base', () => {
|
|
494
|
-
const result = formatShadcnTheme(mockDesign);
|
|
495
|
-
assert.ok(result.includes('@layer base'));
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
it('contains :root', () => {
|
|
499
|
-
const result = formatShadcnTheme(mockDesign);
|
|
500
|
-
assert.ok(result.includes(':root'));
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
it('contains --primary variable', () => {
|
|
504
|
-
const result = formatShadcnTheme(mockDesign);
|
|
505
|
-
assert.ok(result.includes('--primary:'));
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
it('contains --background variable', () => {
|
|
509
|
-
const result = formatShadcnTheme(mockDesign);
|
|
510
|
-
assert.ok(result.includes('--background:'));
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
it('contains --radius variable', () => {
|
|
514
|
-
const result = formatShadcnTheme(mockDesign);
|
|
515
|
-
assert.ok(result.includes('--radius:'));
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
it('contains shadcn/ui comment', () => {
|
|
519
|
-
const result = formatShadcnTheme(mockDesign);
|
|
520
|
-
assert.ok(result.includes('shadcn/ui'));
|
|
521
|
-
});
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
// ── resolveRef (token reference helper) ─────────────────────────
|
|
525
|
-
|
|
526
|
-
describe('resolveRef', () => {
|
|
527
|
-
const tokens = {
|
|
528
|
-
primitive: {
|
|
529
|
-
color: { brand: { primary: { $value: '#3B82F6', $type: 'color' } } },
|
|
530
|
-
spacing: { s0: { $value: '4px', $type: 'dimension' } },
|
|
531
|
-
},
|
|
532
|
-
semantic: {
|
|
533
|
-
color: {
|
|
534
|
-
action: {
|
|
535
|
-
primary: { $value: '{primitive.color.brand.primary}', $type: 'color' },
|
|
536
|
-
},
|
|
537
|
-
},
|
|
538
|
-
alias: {
|
|
539
|
-
ref: { $value: '{semantic.color.action.primary}', $type: 'color' },
|
|
540
|
-
},
|
|
541
|
-
},
|
|
542
|
-
};
|
|
543
|
-
|
|
544
|
-
it('returns the raw $value for non-reference tokens', () => {
|
|
545
|
-
assert.equal(resolveRef(tokens, 'primitive.color.brand.primary'), '#3B82F6');
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
it('follows one-level references', () => {
|
|
549
|
-
assert.equal(resolveRef(tokens, 'semantic.color.action.primary'), '#3B82F6');
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
it('follows chained references', () => {
|
|
553
|
-
assert.equal(resolveRef(tokens, 'semantic.alias.ref'), '#3B82F6');
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
it('returns undefined for missing paths', () => {
|
|
557
|
-
assert.equal(resolveRef(tokens, 'primitive.does.not.exist'), undefined);
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
it('resolves dimension tokens', () => {
|
|
561
|
-
assert.equal(resolveRef(tokens, 'primitive.spacing.s0'), '4px');
|
|
562
|
-
});
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
// ── formatIosSwiftUI ────────────────────────────────────────────
|
|
566
|
-
|
|
567
|
-
describe('formatIosSwiftUI', () => {
|
|
568
|
-
const tokens = formatDtcgTokens(mockDesign);
|
|
569
|
-
|
|
570
|
-
it('emits import SwiftUI and extension Color', () => {
|
|
571
|
-
const result = formatIosSwiftUI(tokens);
|
|
572
|
-
assert.ok(result.includes('import SwiftUI'));
|
|
573
|
-
assert.ok(result.includes('extension Color'));
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
it('emits actionPrimary with resolved primitive hex', () => {
|
|
577
|
-
const result = formatIosSwiftUI(tokens);
|
|
578
|
-
// mockDesign primary is #0066cc → semantic.color.action.primary resolves to #0066cc
|
|
579
|
-
assert.ok(
|
|
580
|
-
/static let actionPrimary = Color\(hex: 0x0066CC\)/.test(result),
|
|
581
|
-
'expected actionPrimary with resolved hex',
|
|
582
|
-
);
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
it('resolves semantic references (no raw {...} strings in output)', () => {
|
|
586
|
-
const result = formatIosSwiftUI(tokens);
|
|
587
|
-
assert.ok(!result.includes('{primitive.'), 'should not leak DTCG refs');
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
it('is idempotent', () => {
|
|
591
|
-
const a = formatIosSwiftUI(tokens);
|
|
592
|
-
const b = formatIosSwiftUI(tokens);
|
|
593
|
-
assert.equal(a, b);
|
|
594
|
-
});
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
// ── formatAndroidCompose ────────────────────────────────────────
|
|
598
|
-
|
|
599
|
-
describe('formatAndroidCompose', () => {
|
|
600
|
-
const tokens = formatDtcgTokens(mockDesign);
|
|
601
|
-
|
|
602
|
-
it('Theme.kt contains object DesignTokens and ActionPrimary', () => {
|
|
603
|
-
const out = formatAndroidCompose(tokens);
|
|
604
|
-
assert.ok(out['Theme.kt'].includes('object DesignTokens'));
|
|
605
|
-
assert.ok(/val ActionPrimary = Color\(0xFF0066CC\)/.test(out['Theme.kt']));
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
it('colors.xml has <color name="action_primary">', () => {
|
|
609
|
-
const out = formatAndroidCompose(tokens);
|
|
610
|
-
assert.ok(out['colors.xml'].includes('<color name="action_primary">#FF0066CC</color>'));
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
it('dimens.xml has <dimen name="spacing_s0"> with dp unit', () => {
|
|
614
|
-
const out = formatAndroidCompose(tokens);
|
|
615
|
-
assert.ok(/<dimen name="spacing_s0">\d+dp<\/dimen>/.test(out['dimens.xml']));
|
|
616
|
-
});
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
// ── formatFlutterDart ───────────────────────────────────────────
|
|
620
|
-
|
|
621
|
-
describe('formatFlutterDart', () => {
|
|
622
|
-
const tokens = formatDtcgTokens(mockDesign);
|
|
623
|
-
|
|
624
|
-
it('contains class DesignTokens', () => {
|
|
625
|
-
const out = formatFlutterDart(tokens);
|
|
626
|
-
assert.ok(out.includes('class DesignTokens'));
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
it('emits actionPrimary with resolved ARGB hex', () => {
|
|
630
|
-
const out = formatFlutterDart(tokens);
|
|
631
|
-
assert.ok(
|
|
632
|
-
/static const Color actionPrimary = Color\(0xFF0066CC\);/.test(out),
|
|
633
|
-
'expected actionPrimary with resolved hex',
|
|
634
|
-
);
|
|
635
|
-
});
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
// ── formatWordPressTheme (block-theme skeleton) ─────────────────
|
|
639
|
-
|
|
640
|
-
describe('formatWordPressTheme', () => {
|
|
641
|
-
const tokens = formatDtcgTokens(mockDesign);
|
|
642
|
-
const out = formatWordPressTheme(tokens, mockDesign);
|
|
643
|
-
|
|
644
|
-
it('theme.json parses and has color palette with at least one entry', () => {
|
|
645
|
-
const parsed = JSON.parse(out['theme.json']);
|
|
646
|
-
assert.ok(Array.isArray(parsed.settings.color.palette));
|
|
647
|
-
assert.ok(parsed.settings.color.palette.length > 0);
|
|
648
|
-
// At least one entry should have a hex color derived from semantics
|
|
649
|
-
const actionPrimary = parsed.settings.color.palette.find(p => p.slug === 'action-primary');
|
|
650
|
-
assert.ok(actionPrimary);
|
|
651
|
-
assert.equal(actionPrimary.color.toLowerCase(), '#0066cc');
|
|
652
|
-
});
|
|
653
|
-
|
|
654
|
-
it('theme.json is version 3', () => {
|
|
655
|
-
const parsed = JSON.parse(out['theme.json']);
|
|
656
|
-
assert.equal(parsed.version, 3);
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
it('style.css contains Theme Name and --action-primary custom prop', () => {
|
|
660
|
-
assert.ok(out['style.css'].includes('Theme Name:'));
|
|
661
|
-
assert.ok(out['style.css'].includes('--action-primary:'));
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
it('functions.php starts with <?php', () => {
|
|
665
|
-
assert.ok(out['functions.php'].startsWith('<?php'));
|
|
666
|
-
});
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
// ── formatAgentRules ────────────────────────────────────────────
|
|
670
|
-
|
|
671
|
-
describe('formatAgentRules', () => {
|
|
672
|
-
const tokens = formatDtcgTokens(mockDesign);
|
|
673
|
-
const url = 'https://example.com';
|
|
674
|
-
const designWithRegions = { ...mockDesign, regions: [{ role: 'hero' }, { role: 'footer' }] };
|
|
675
|
-
const out = formatAgentRules({ design: designWithRegions, tokens, url });
|
|
676
|
-
|
|
677
|
-
it('emits all four files, each non-empty', () => {
|
|
678
|
-
for (const key of [
|
|
679
|
-
'.cursor/rules/designlang.mdc',
|
|
680
|
-
'.claude/skills/designlang/SKILL.md',
|
|
681
|
-
'CLAUDE.md.fragment',
|
|
682
|
-
'agents.md',
|
|
683
|
-
]) {
|
|
684
|
-
assert.ok(typeof out[key] === 'string' && out[key].length > 0, `missing ${key}`);
|
|
685
|
-
}
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
it('Cursor .mdc begins with frontmatter containing alwaysApply: true', () => {
|
|
689
|
-
const mdc = out['.cursor/rules/designlang.mdc'];
|
|
690
|
-
assert.ok(mdc.startsWith('---'), 'expected frontmatter start');
|
|
691
|
-
const fm = mdc.split('---')[1];
|
|
692
|
-
assert.ok(fm.includes('alwaysApply: true'), 'expected alwaysApply: true');
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
it('all four files reference the source URL', () => {
|
|
696
|
-
for (const key of Object.keys(out)) {
|
|
697
|
-
assert.ok(out[key].includes(url), `${key} missing url`);
|
|
698
|
-
}
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
it('all four files contain resolved hex for semantic.color.action.primary', () => {
|
|
702
|
-
// mockDesign primary is #0066cc — resolved value should appear verbatim,
|
|
703
|
-
// and the raw DTCG reference must not leak.
|
|
704
|
-
for (const key of Object.keys(out)) {
|
|
705
|
-
assert.ok(out[key].toLowerCase().includes('#0066cc'), `${key} missing resolved hex`);
|
|
706
|
-
assert.ok(!out[key].includes('{primitive.color.brand.primary}'), `${key} leaked raw DTCG ref`);
|
|
707
|
-
}
|
|
708
|
-
});
|
|
709
|
-
});
|