designlang 6.0.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/.github/FUNDING.yml +1 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
  5. package/.vercel/README.txt +11 -0
  6. package/.vercel/project.json +1 -0
  7. package/CHANGELOG.md +58 -0
  8. package/CONTRIBUTING.md +25 -0
  9. package/README.md +120 -8
  10. package/bin/design-extract.js +106 -3
  11. package/chrome-extension/README.md +41 -0
  12. package/chrome-extension/icons/favicon.svg +7 -0
  13. package/chrome-extension/icons/icon-128.png +0 -0
  14. package/chrome-extension/icons/icon-16.png +0 -0
  15. package/chrome-extension/icons/icon-32.png +0 -0
  16. package/chrome-extension/icons/icon-48.png +0 -0
  17. package/chrome-extension/manifest.json +26 -0
  18. package/chrome-extension/popup.html +167 -0
  19. package/chrome-extension/popup.js +59 -0
  20. package/docs/superpowers/plans/2026-04-18-designlang-v7.md +1121 -0
  21. package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +150 -0
  22. package/docs/superpowers/specs/2026-04-18-website-redesign-design.md +120 -0
  23. package/docs/superpowers/specs/2026-04-19-designlang-v7-1-design.md +111 -0
  24. package/package.json +5 -4
  25. package/src/config.js +26 -0
  26. package/src/crawler.js +136 -2
  27. package/src/extractors/a11y-remediation.js +47 -0
  28. package/src/extractors/component-clusters.js +39 -0
  29. package/src/extractors/css-health.js +151 -0
  30. package/src/extractors/scoring.js +20 -1
  31. package/src/extractors/semantic-regions.js +44 -0
  32. package/src/extractors/stack-fingerprint.js +88 -0
  33. package/src/formatters/_token-ref.js +44 -0
  34. package/src/formatters/agent-rules.js +116 -0
  35. package/src/formatters/android-compose.js +164 -0
  36. package/src/formatters/dtcg-tokens.js +175 -0
  37. package/src/formatters/flutter-dart.js +130 -0
  38. package/src/formatters/ios-swiftui.js +161 -0
  39. package/src/formatters/markdown.js +25 -0
  40. package/src/formatters/wordpress.js +183 -0
  41. package/src/index.js +30 -0
  42. package/src/mcp/resources.js +64 -0
  43. package/src/mcp/server.js +110 -0
  44. package/src/mcp/tools.js +149 -0
  45. package/src/utils-cookies.js +73 -0
  46. package/tests/cli.test.js +50 -0
  47. package/tests/cookies.test.js +98 -0
  48. package/tests/extractors.test.js +131 -0
  49. package/tests/formatters.test.js +232 -0
  50. package/tests/mcp.test.js +68 -0
  51. package/website/app/api/extract/route.js +216 -56
  52. package/website/app/components/A11ySlider.js +369 -0
  53. package/website/app/components/Comparison.js +286 -0
  54. package/website/app/components/CssHealth.js +243 -0
  55. package/website/app/components/HeroExtractor.js +455 -0
  56. package/website/app/components/Marginalia.js +3 -0
  57. package/website/app/components/McpSection.js +223 -0
  58. package/website/app/components/PlatformTabs.js +250 -0
  59. package/website/app/components/RegionsComponents.js +429 -0
  60. package/website/app/components/Rule.js +13 -0
  61. package/website/app/components/Specimens.js +237 -0
  62. package/website/app/components/StructuredData.js +144 -0
  63. package/website/app/components/TokenBrowser.js +344 -0
  64. package/website/app/components/token-browser-sample.js +65 -0
  65. package/website/app/globals.css +415 -633
  66. package/website/app/icon.svg +7 -0
  67. package/website/app/layout.js +113 -6
  68. package/website/app/opengraph-image.js +170 -0
  69. package/website/app/page.js +325 -148
  70. package/website/app/robots.js +15 -0
  71. package/website/app/seo-config.js +82 -0
  72. package/website/app/sitemap.js +18 -0
  73. package/website/lib/cache.js +73 -0
  74. package/website/lib/rate-limit.js +30 -0
  75. package/website/lib/rate-limit.test.js +55 -0
  76. package/website/lib/specimens.json +86 -0
  77. package/website/lib/token-helpers.js +70 -0
  78. package/website/lib/url-safety.js +103 -0
  79. package/website/lib/url-safety.test.js +116 -0
  80. package/website/lib/zip-files.js +15 -0
  81. package/website/package-lock.json +85 -0
  82. package/website/package.json +1 -0
  83. package/website/public/favicon.svg +7 -0
  84. package/website/public/logo-specimen.svg +76 -0
  85. package/website/public/mark.svg +12 -0
  86. package/website/public/site.webmanifest +13 -0
  87. package/website/app/favicon.ico +0 -0
  88. package/website/public/file.svg +0 -1
  89. package/website/public/globe.svg +0 -1
  90. package/website/public/next.svg +0 -1
  91. package/website/public/vercel.svg +0 -1
  92. package/website/public/window.svg +0 -1
@@ -11,6 +11,11 @@ import { extractLayout } from '../src/extractors/layout.js';
11
11
  import { extractGradients } from '../src/extractors/gradients.js';
12
12
  import { extractZIndex } from '../src/extractors/zindex.js';
13
13
  import { scoreDesignSystem } from '../src/extractors/scoring.js';
14
+ import { extractStackFingerprint } from '../src/extractors/stack-fingerprint.js';
15
+ import { extractCssHealth } from '../src/extractors/css-health.js';
16
+ import { remediateFailingPairs } from '../src/extractors/a11y-remediation.js';
17
+ import { extractSemanticRegions } from '../src/extractors/semantic-regions.js';
18
+ import { clusterComponents } from '../src/extractors/component-clusters.js';
14
19
 
15
20
  // ── Shared fixture defaults ─────────────────────────────────────
16
21
 
@@ -659,3 +664,129 @@ describe('scoreDesignSystem', () => {
659
664
  assert.ok(result.issues.some(i => i.includes('primary')));
660
665
  });
661
666
  });
667
+
668
+ // ── extractStackFingerprint ─────────────────────────────────────
669
+
670
+ describe('extractStackFingerprint', () => {
671
+ it('detects Next.js from __NEXT_DATA__', () => {
672
+ const out = extractStackFingerprint({ windowGlobals: ['__NEXT_DATA__'], scripts: [], metas: [], classNameSample: [] });
673
+ assert.equal(out.framework, 'next');
674
+ });
675
+
676
+ it('detects Tailwind from utility-heavy classNames', () => {
677
+ const out = extractStackFingerprint({
678
+ windowGlobals: [],
679
+ scripts: [],
680
+ metas: [],
681
+ classNameSample: [
682
+ 'flex items-center gap-4 text-sm text-gray-600',
683
+ 'px-4 py-2 rounded-md bg-blue-500',
684
+ 'grid grid-cols-3 md:grid-cols-4',
685
+ 'flex justify-center',
686
+ 'p-4 shadow-md',
687
+ 'mt-4 text-lg',
688
+ ],
689
+ });
690
+ assert.equal(out.css.layer, 'tailwind');
691
+ assert.ok(out.css.tailwind.utilities.length > 0);
692
+ });
693
+
694
+ it('returns unknown when nothing matches', () => {
695
+ const out = extractStackFingerprint({ windowGlobals: [], scripts: [], metas: [], classNameSample: ['foo', 'bar'] });
696
+ assert.equal(out.framework, 'unknown');
697
+ assert.equal(out.css.layer, 'unknown');
698
+ });
699
+ });
700
+
701
+ // ── extractCssHealth ────────────────────────────────────────────
702
+
703
+ describe('extractCssHealth', () => {
704
+ const payload = [{
705
+ url: 'https://x.com/a.css',
706
+ text: '.a{color:red}.a{color:red}.b{color:blue !important}.c-webkit-foo{color:x}@keyframes fade{0%{opacity:0}100%{opacity:1}\n}',
707
+ totalBytes: 1000,
708
+ ranges: [{ start: 0, end: 400 }], // 60% unused
709
+ }];
710
+
711
+ it('counts !important', () => {
712
+ const r = extractCssHealth(payload);
713
+ assert.equal(r.importantCount, 1);
714
+ });
715
+
716
+ it('counts duplicate declarations', () => {
717
+ const r = extractCssHealth(payload);
718
+ assert.ok(r.duplicates >= 1);
719
+ });
720
+
721
+ it('reports unused bytes', () => {
722
+ const r = extractCssHealth(payload);
723
+ assert.equal(r.unusedBytes, 600);
724
+ assert.equal(r.usedBytes, 400);
725
+ });
726
+
727
+ it('catalogs keyframes', () => {
728
+ const r = extractCssHealth(payload);
729
+ assert.ok(r.keyframes.some(k => k.name === 'fade'));
730
+ });
731
+ });
732
+
733
+ // ── remediateFailingPairs ───────────────────────────────────────
734
+
735
+ describe('remediateFailingPairs', () => {
736
+ it('suggests a palette color that passes AA', () => {
737
+ const failing = [{ fg: '#777777', bg: '#ffffff', ratio: 3.5, rule: 'AA-normal' }];
738
+ const palette = ['#000000', '#222222', '#555555', '#cccccc'];
739
+ const out = remediateFailingPairs(failing, palette);
740
+ assert.equal(out.length, 1);
741
+ assert.ok(out[0].suggestion);
742
+ assert.ok(out[0].suggestion.newRatio >= 4.5);
743
+ });
744
+
745
+ it('returns null suggestion when no palette color passes', () => {
746
+ const failing = [{ fg: '#eee', bg: '#fff', ratio: 1.1, rule: 'AA-normal' }];
747
+ const palette = ['#dedede'];
748
+ const out = remediateFailingPairs(failing, palette);
749
+ assert.equal(out[0].suggestion, null);
750
+ });
751
+ });
752
+
753
+ // ── extractSemanticRegions ──────────────────────────────────────
754
+
755
+ describe('extractSemanticRegions', () => {
756
+ it('labels header as nav', () => {
757
+ const out = extractSemanticRegions([{ tag: 'header', role: '', className: '', id: '', text: 'Home About', headings: [], buttonCount: 3, cardCount: 0, bounds: { x: 0, y: 0, w: 1280, h: 80 } }]);
758
+ assert.equal(out[0].role, 'nav');
759
+ });
760
+
761
+ it('labels section with CTA + heading as hero', () => {
762
+ const out = extractSemanticRegions([{ tag: 'section', role: '', className: 'hero', id: '', text: 'Welcome', headings: ['Build better'], buttonCount: 2, cardCount: 0, bounds: { x: 0, y: 80, w: 1280, h: 600 } }]);
763
+ assert.equal(out[0].role, 'hero');
764
+ });
765
+
766
+ it('labels pricing based on cards + keyword', () => {
767
+ const out = extractSemanticRegions([{ tag: 'section', role: '', className: '', id: '', text: 'Basic $9/mo Pro $29/mo Team $99/mo', headings: ['Pricing'], buttonCount: 3, cardCount: 3, bounds: { x: 0, y: 0, w: 1280, h: 400 } }]);
768
+ assert.equal(out[0].role, 'pricing');
769
+ });
770
+ });
771
+
772
+ // ── clusterComponents ───────────────────────────────────────────
773
+
774
+ describe('clusterComponents', () => {
775
+ it('collapses identical instances into one entry', () => {
776
+ const els = Array.from({ length: 5 }, () => ({ kind: 'button', structuralHash: 'button>span', styleVector: [16, 8, 0, 0], css: { bg: '#f00' } }));
777
+ const out = clusterComponents(els);
778
+ assert.equal(out.length, 1);
779
+ assert.equal(out[0].instanceCount, 5);
780
+ });
781
+
782
+ it('separates variants with different style vectors', () => {
783
+ const els = [
784
+ { kind: 'button', structuralHash: 'button>span', styleVector: [16, 8, 0, 0], css: { bg: '#f00' } },
785
+ { kind: 'button', structuralHash: 'button>span', styleVector: [16, 8, 0, 0], css: { bg: '#f00' } },
786
+ { kind: 'button', structuralHash: 'button>span', styleVector: [0, 0, 16, 8], css: { bg: '#0f0' } },
787
+ ];
788
+ const out = clusterComponents(els);
789
+ assert.equal(out.length, 1);
790
+ assert.equal(out[0].variants.length, 2);
791
+ });
792
+ });
@@ -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
+ });