designlang 7.2.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +154 -13
  3. package/bin/design-extract.js +94 -1
  4. package/package.json +9 -3
  5. package/src/config.js +2 -0
  6. package/src/crawler.js +55 -6
  7. package/src/drift.js +137 -0
  8. package/src/extractors/accessibility.js +44 -1
  9. package/src/extractors/colors.js +50 -12
  10. package/src/extractors/component-anatomy.js +123 -0
  11. package/src/extractors/motion.js +184 -0
  12. package/src/extractors/scoring.js +49 -30
  13. package/src/extractors/voice.js +96 -0
  14. package/src/formatters/markdown.js +88 -0
  15. package/src/formatters/motion-tokens.js +22 -0
  16. package/src/index.js +14 -0
  17. package/src/lint.js +198 -0
  18. package/src/visual-diff.js +116 -0
  19. package/.github/FUNDING.yml +0 -1
  20. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -62
  21. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  22. package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -28
  23. package/.github/og-preview.png +0 -0
  24. package/.github/workflows/manavarya-bot.yml +0 -17
  25. package/chrome-extension/README.md +0 -41
  26. package/chrome-extension/icons/favicon.svg +0 -7
  27. package/chrome-extension/icons/icon-128.png +0 -0
  28. package/chrome-extension/icons/icon-16.png +0 -0
  29. package/chrome-extension/icons/icon-32.png +0 -0
  30. package/chrome-extension/icons/icon-48.png +0 -0
  31. package/chrome-extension/manifest.json +0 -26
  32. package/chrome-extension/popup.html +0 -167
  33. package/chrome-extension/popup.js +0 -59
  34. package/docs/superpowers/plans/2026-04-18-designlang-v7.md +0 -1121
  35. package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +0 -150
  36. package/docs/superpowers/specs/2026-04-18-website-redesign-design.md +0 -120
  37. package/docs/superpowers/specs/2026-04-19-designlang-v7-1-design.md +0 -111
  38. package/tests/cli.test.js +0 -84
  39. package/tests/cookies.test.js +0 -98
  40. package/tests/extractors.test.js +0 -792
  41. package/tests/formatters.test.js +0 -709
  42. package/tests/interaction-states.test.js +0 -62
  43. package/tests/mcp.test.js +0 -68
  44. package/tests/modern-css.test.js +0 -104
  45. package/tests/routes-reconciliation.test.js +0 -120
  46. package/tests/utils.test.js +0 -413
  47. package/tests/wide-gamut.test.js +0 -90
  48. package/website/.claude/launch.json +0 -11
  49. package/website/AGENTS.md +0 -5
  50. package/website/CLAUDE.md +0 -1
  51. package/website/README.md +0 -36
  52. package/website/app/api/extract/route.js +0 -245
  53. package/website/app/components/A11ySlider.js +0 -369
  54. package/website/app/components/Comparison.js +0 -286
  55. package/website/app/components/CssHealth.js +0 -243
  56. package/website/app/components/Extractor.js +0 -184
  57. package/website/app/components/HeroExtractor.js +0 -455
  58. package/website/app/components/Marginalia.js +0 -3
  59. package/website/app/components/McpSection.js +0 -223
  60. package/website/app/components/PlatformTabs.js +0 -250
  61. package/website/app/components/RegionsComponents.js +0 -429
  62. package/website/app/components/Rule.js +0 -13
  63. package/website/app/components/Specimens.js +0 -237
  64. package/website/app/components/StructuredData.js +0 -144
  65. package/website/app/components/TokenBrowser.js +0 -344
  66. package/website/app/components/token-browser-sample.js +0 -65
  67. package/website/app/globals.css +0 -505
  68. package/website/app/icon.svg +0 -7
  69. package/website/app/layout.js +0 -126
  70. package/website/app/opengraph-image.js +0 -170
  71. package/website/app/page.js +0 -399
  72. package/website/app/robots.js +0 -15
  73. package/website/app/seo-config.js +0 -82
  74. package/website/app/sitemap.js +0 -18
  75. package/website/jsconfig.json +0 -7
  76. package/website/lib/cache.js +0 -73
  77. package/website/lib/rate-limit.js +0 -30
  78. package/website/lib/rate-limit.test.js +0 -55
  79. package/website/lib/specimens.json +0 -86
  80. package/website/lib/token-helpers.js +0 -70
  81. package/website/lib/url-safety.js +0 -103
  82. package/website/lib/url-safety.test.js +0 -116
  83. package/website/lib/zip-files.js +0 -15
  84. package/website/next.config.mjs +0 -15
  85. package/website/package-lock.json +0 -1353
  86. package/website/package.json +0 -19
  87. package/website/public/favicon.svg +0 -7
  88. package/website/public/logo-specimen.svg +0 -76
  89. package/website/public/mark.svg +0 -12
  90. package/website/public/site.webmanifest +0 -13
@@ -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
- });