designlang 7.2.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +17 -0
  3. package/bin/design-extract.js +5 -1
  4. package/package.json +1 -1
  5. package/src/config.js +2 -0
  6. package/src/crawler.js +35 -6
  7. package/src/extractors/accessibility.js +44 -1
  8. package/src/extractors/colors.js +50 -12
  9. package/src/extractors/scoring.js +49 -30
  10. package/.github/FUNDING.yml +0 -1
  11. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -62
  12. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  13. package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -28
  14. package/.github/og-preview.png +0 -0
  15. package/.github/workflows/manavarya-bot.yml +0 -17
  16. package/chrome-extension/README.md +0 -41
  17. package/chrome-extension/icons/favicon.svg +0 -7
  18. package/chrome-extension/icons/icon-128.png +0 -0
  19. package/chrome-extension/icons/icon-16.png +0 -0
  20. package/chrome-extension/icons/icon-32.png +0 -0
  21. package/chrome-extension/icons/icon-48.png +0 -0
  22. package/chrome-extension/manifest.json +0 -26
  23. package/chrome-extension/popup.html +0 -167
  24. package/chrome-extension/popup.js +0 -59
  25. package/docs/superpowers/plans/2026-04-18-designlang-v7.md +0 -1121
  26. package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +0 -150
  27. package/docs/superpowers/specs/2026-04-18-website-redesign-design.md +0 -120
  28. package/docs/superpowers/specs/2026-04-19-designlang-v7-1-design.md +0 -111
  29. package/tests/cli.test.js +0 -84
  30. package/tests/cookies.test.js +0 -98
  31. package/tests/extractors.test.js +0 -792
  32. package/tests/formatters.test.js +0 -709
  33. package/tests/interaction-states.test.js +0 -62
  34. package/tests/mcp.test.js +0 -68
  35. package/tests/modern-css.test.js +0 -104
  36. package/tests/routes-reconciliation.test.js +0 -120
  37. package/tests/utils.test.js +0 -413
  38. package/tests/wide-gamut.test.js +0 -90
  39. package/website/.claude/launch.json +0 -11
  40. package/website/AGENTS.md +0 -5
  41. package/website/CLAUDE.md +0 -1
  42. package/website/README.md +0 -36
  43. package/website/app/api/extract/route.js +0 -245
  44. package/website/app/components/A11ySlider.js +0 -369
  45. package/website/app/components/Comparison.js +0 -286
  46. package/website/app/components/CssHealth.js +0 -243
  47. package/website/app/components/Extractor.js +0 -184
  48. package/website/app/components/HeroExtractor.js +0 -455
  49. package/website/app/components/Marginalia.js +0 -3
  50. package/website/app/components/McpSection.js +0 -223
  51. package/website/app/components/PlatformTabs.js +0 -250
  52. package/website/app/components/RegionsComponents.js +0 -429
  53. package/website/app/components/Rule.js +0 -13
  54. package/website/app/components/Specimens.js +0 -237
  55. package/website/app/components/StructuredData.js +0 -144
  56. package/website/app/components/TokenBrowser.js +0 -344
  57. package/website/app/components/token-browser-sample.js +0 -65
  58. package/website/app/globals.css +0 -505
  59. package/website/app/icon.svg +0 -7
  60. package/website/app/layout.js +0 -126
  61. package/website/app/opengraph-image.js +0 -170
  62. package/website/app/page.js +0 -399
  63. package/website/app/robots.js +0 -15
  64. package/website/app/seo-config.js +0 -82
  65. package/website/app/sitemap.js +0 -18
  66. package/website/jsconfig.json +0 -7
  67. package/website/lib/cache.js +0 -73
  68. package/website/lib/rate-limit.js +0 -30
  69. package/website/lib/rate-limit.test.js +0 -55
  70. package/website/lib/specimens.json +0 -86
  71. package/website/lib/token-helpers.js +0 -70
  72. package/website/lib/url-safety.js +0 -103
  73. package/website/lib/url-safety.test.js +0 -116
  74. package/website/lib/zip-files.js +0 -15
  75. package/website/next.config.mjs +0 -15
  76. package/website/package-lock.json +0 -1353
  77. package/website/package.json +0 -19
  78. package/website/public/favicon.svg +0 -7
  79. package/website/public/logo-specimen.svg +0 -76
  80. package/website/public/mark.svg +0 -12
  81. 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
- });