designlang 6.0.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.github/FUNDING.yml +1 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
  5. package/CHANGELOG.md +43 -0
  6. package/README.md +111 -1
  7. package/bin/design-extract.js +88 -2
  8. package/docs/superpowers/plans/2026-04-18-designlang-v7.md +1121 -0
  9. package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +150 -0
  10. package/package.json +5 -4
  11. package/src/config.js +23 -0
  12. package/src/crawler.js +116 -0
  13. package/src/extractors/a11y-remediation.js +47 -0
  14. package/src/extractors/component-clusters.js +39 -0
  15. package/src/extractors/css-health.js +151 -0
  16. package/src/extractors/scoring.js +20 -1
  17. package/src/extractors/semantic-regions.js +44 -0
  18. package/src/extractors/stack-fingerprint.js +88 -0
  19. package/src/formatters/_token-ref.js +44 -0
  20. package/src/formatters/agent-rules.js +116 -0
  21. package/src/formatters/android-compose.js +164 -0
  22. package/src/formatters/dtcg-tokens.js +175 -0
  23. package/src/formatters/flutter-dart.js +130 -0
  24. package/src/formatters/ios-swiftui.js +161 -0
  25. package/src/formatters/markdown.js +25 -0
  26. package/src/formatters/wordpress.js +183 -0
  27. package/src/index.js +30 -0
  28. package/src/mcp/resources.js +64 -0
  29. package/src/mcp/server.js +110 -0
  30. package/src/mcp/tools.js +149 -0
  31. package/tests/cli.test.js +50 -0
  32. package/tests/extractors.test.js +131 -0
  33. package/tests/formatters.test.js +232 -0
  34. package/tests/mcp.test.js +68 -0
  35. package/website/app/globals.css +11 -11
@@ -7,6 +7,13 @@ import { formatCssVars } from '../src/formatters/css-vars.js';
7
7
  import { formatPreview } from '../src/formatters/preview.js';
8
8
  import { formatFigma } from '../src/formatters/figma.js';
9
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';
10
17
 
11
18
  // ── Shared mock design object ───────────────────────────────────
12
19
 
@@ -226,6 +233,44 @@ describe('formatTokens', () => {
226
233
  });
227
234
  });
228
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
+
229
274
  // ── formatTailwind ──────────────────────────────────────────────
230
275
 
231
276
  describe('formatTailwind', () => {
@@ -475,3 +520,190 @@ describe('formatShadcnTheme', () => {
475
520
  assert.ok(result.includes('shadcn/ui'));
476
521
  });
477
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
+ });
@@ -0,0 +1,68 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { buildResources } from '../src/mcp/resources.js';
4
+ import { buildTools } from '../src/mcp/tools.js';
5
+
6
+ const tokens = {
7
+ $metadata: { source: 'https://x.com' },
8
+ primitive: { color: { brand: { primary: { $value: '#3b82f6', $type: 'color' } } } },
9
+ semantic: { color: { action: { primary: { $value: '{primitive.color.brand.primary}', $type: 'color' } } } },
10
+ };
11
+ const design = {
12
+ colors: { all: ['#000', '#111', '#555', '#fff'] },
13
+ regions: [{ role: 'hero', bounds: { x:0,y:0,w:1280,h:600 }, heading: 'Build' }],
14
+ componentClusters: [{ kind: 'button', instanceCount: 5, variants: [{ css: { bg: '#3b82f6' }, instanceCount: 3 }] }],
15
+ accessibility: { remediation: [{ fg:'#eee', bg:'#fff', ratio:1.1, rule:'AA-normal', suggestion:{replace:'fg',color:'#000',newRatio:21}}] },
16
+ cssHealth: null,
17
+ };
18
+
19
+ describe('MCP resources', () => {
20
+ it('lists five URIs', () => {
21
+ const r = buildResources({ design, tokens });
22
+ assert.equal(r.list().length, 5);
23
+ assert.ok(r.list().find(x => x.uri === 'designlang://tokens/semantic'));
24
+ });
25
+ it('reads semantic tokens', () => {
26
+ const r = buildResources({ design, tokens });
27
+ const out = r.read('designlang://tokens/semantic');
28
+ const body = JSON.parse(out.text);
29
+ assert.ok(body.color?.action?.primary);
30
+ });
31
+ it('throws for unknown uri', () => {
32
+ const r = buildResources({ design, tokens });
33
+ assert.throws(() => r.read('designlang://nope'));
34
+ });
35
+ });
36
+
37
+ describe('MCP tools', () => {
38
+ it('search_tokens finds semantic token by substring', async () => {
39
+ const t = buildTools({ design, tokens });
40
+ const res = await t.call('search_tokens', { query: 'action.primary' });
41
+ assert.ok(JSON.stringify(res.matches).includes('action.primary'));
42
+ });
43
+ it('find_nearest_color returns a palette color passing AA', async () => {
44
+ const t = buildTools({ design, tokens });
45
+ const res = await t.call('find_nearest_color', { hex: '#ffffff', level: 'AA-normal' });
46
+ assert.ok(res.color);
47
+ assert.ok(res.newRatio >= 4.5);
48
+ });
49
+ it('get_region returns hero', async () => {
50
+ const t = buildTools({ design, tokens });
51
+ const res = await t.call('get_region', { name: 'hero' });
52
+ assert.equal(res.role, 'hero');
53
+ });
54
+ it('get_component returns button cluster', async () => {
55
+ const t = buildTools({ design, tokens });
56
+ const res = await t.call('get_component', { name: 'button' });
57
+ assert.equal(res.kind, 'button');
58
+ });
59
+ it('list_failing_contrast_pairs returns remediation array', async () => {
60
+ const t = buildTools({ design, tokens });
61
+ const res = await t.call('list_failing_contrast_pairs');
62
+ assert.equal(res.length, 1);
63
+ });
64
+ it('throws on unknown tool', async () => {
65
+ const t = buildTools({ design, tokens });
66
+ await assert.rejects(() => t.call('nope', {}));
67
+ });
68
+ });
@@ -1,9 +1,9 @@
1
1
  :root {
2
- --red: #ff0000;
2
+ --red: #ffffff;
3
3
  --black: #0a0a0a;
4
- --white: #f5f0e8;
5
- --cream: #e8e0d0;
6
- --yellow: #ffdd00;
4
+ --white: #ffffff;
5
+ --cream: #ffffff;
6
+ --yellow: #ffffff;
7
7
  --gray: #333;
8
8
  }
9
9
 
@@ -47,7 +47,7 @@ a { color: inherit; }
47
47
  font-family: 'Unbounded', sans-serif;
48
48
  font-size: 14vw;
49
49
  font-weight: 900;
50
- color: rgba(255, 0, 0, 0.03);
50
+ color: rgba(255, 255, 255, 0.03);
51
51
  white-space: nowrap;
52
52
  pointer-events: none;
53
53
  animation: scroll-text 20s linear infinite;
@@ -216,7 +216,7 @@ section {
216
216
  font-family: 'Unbounded', sans-serif;
217
217
  font-size: 80px;
218
218
  font-weight: 900;
219
- color: rgba(255, 0, 0, 0.06);
219
+ color: rgba(255, 255, 255, 0.06);
220
220
  line-height: 1;
221
221
  }
222
222
 
@@ -302,13 +302,13 @@ section {
302
302
  .command-card {
303
303
  background: var(--black);
304
304
  padding: 32px;
305
- border: 2px solid rgba(255,0,0,0.3);
305
+ border: 2px solid rgba(255,255,255,0.3);
306
306
  transition: all 0.15s;
307
307
  }
308
308
 
309
309
  .command-card:hover {
310
310
  border-color: var(--red);
311
- background: rgba(255, 0, 0, 0.05);
311
+ background: rgba(255, 255, 255, 0.05);
312
312
  }
313
313
 
314
314
  .command-name {
@@ -668,9 +668,9 @@ footer a:hover {
668
668
  font-weight: 700;
669
669
  }
670
670
 
671
- .extractor-a11y-score.good { color: #4ade80; }
672
- .extractor-a11y-score.ok { color: var(--yellow); }
673
- .extractor-a11y-score.bad { color: var(--red); }
671
+ .extractor-a11y-score.good { color: #ffffff; }
672
+ .extractor-a11y-score.ok { color: #aaaaaa; }
673
+ .extractor-a11y-score.bad { color: #666666; }
674
674
 
675
675
  .extractor-a11y-fails {
676
676
  font-family: 'JetBrains Mono', monospace;