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,62 +0,0 @@
1
- import { describe, it } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { extractInteractionStates } from '../src/extractors/interaction-states.js';
4
-
5
- describe('extractInteractionStates', () => {
6
- it('returns a default shape on empty input', () => {
7
- const r = extractInteractionStates(null);
8
- assert.equal(r.scrollSettled, false);
9
- assert.equal(r.menusOpened, 0);
10
- assert.deepEqual(r.modals, []);
11
- assert.equal(r.hover.sampled, 0);
12
- });
13
-
14
- it('computes hover deltas between before/after style snapshots', () => {
15
- const r = extractInteractionStates({
16
- scrollSettled: true,
17
- menusOpened: 2,
18
- hoverSamples: [
19
- {
20
- selector: 'button:nth-of-type(1)',
21
- before: { color: 'rgb(0,0,0)', backgroundColor: 'rgb(255,255,255)' },
22
- after: { color: 'rgb(0,0,0)', backgroundColor: 'rgb(240,240,240)' },
23
- },
24
- {
25
- selector: 'a:nth-of-type(1)',
26
- before: { color: 'rgb(0,0,0)' },
27
- after: { color: 'rgb(0,0,0)' },
28
- },
29
- ],
30
- accordionsOpened: 3,
31
- modals: [],
32
- });
33
- assert.equal(r.hover.sampled, 2);
34
- assert.equal(r.hover.changed, 1);
35
- assert.equal(r.hover.deltas[0].changes.backgroundColor.from, 'rgb(255,255,255)');
36
- assert.equal(r.hover.deltas[0].changes.backgroundColor.to, 'rgb(240,240,240)');
37
- assert.equal(r.accordionsOpened, 3);
38
- assert.equal(r.menusOpened, 2);
39
- assert.equal(r.scrollSettled, true);
40
- });
41
-
42
- it('normalizes modal snapshots', () => {
43
- const r = extractInteractionStates({
44
- modals: [
45
- { trigger: 'Sign in', snapshot: { tag: 'dialog', role: 'dialog', bg: 'rgb(255,255,255)', color: 'rgb(17,17,17)', boxShadow: '0 10px 30px rgba(0,0,0,.2)', borderRadius: '12px', width: 400, height: 300 } },
46
- ],
47
- });
48
- assert.equal(r.modals.length, 1);
49
- assert.equal(r.modals[0].trigger, 'Sign in');
50
- assert.equal(r.modals[0].bg, 'rgb(255,255,255)');
51
- assert.equal(r.modals[0].width, 400);
52
- });
53
-
54
- it('handles missing snapshot fields gracefully', () => {
55
- const r = extractInteractionStates({
56
- hoverSamples: [{ selector: 'x', before: null, after: null }],
57
- modals: [{ trigger: 'Menu' }],
58
- });
59
- assert.equal(r.hover.changed, 0);
60
- assert.equal(r.modals[0].bg, '');
61
- });
62
- });
package/tests/mcp.test.js DELETED
@@ -1,68 +0,0 @@
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,104 +0,0 @@
1
- import { describe, it } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { extractModernCss } from '../src/extractors/modern-css.js';
4
-
5
- function mkStyle(overrides = {}) {
6
- return {
7
- tag: 'div',
8
- classList: '',
9
- fontVariationSettings: 'normal',
10
- fontFeatureSettings: 'normal',
11
- textWrap: '',
12
- textDecorationStyle: '',
13
- textDecorationThickness: '',
14
- textUnderlineOffset: '',
15
- pseudo: null,
16
- ...overrides,
17
- };
18
- }
19
-
20
- describe('extractModernCss', () => {
21
- it('returns zeroed structure for empty payload', () => {
22
- const r = extractModernCss({});
23
- assert.equal(r.pseudoElements.count, 0);
24
- assert.equal(r.variableFonts.count, 0);
25
- assert.equal(r.containerQueries.count, 0);
26
- assert.deepEqual(r.envUsage, []);
27
- });
28
-
29
- it('counts pseudo-elements and captures samples', () => {
30
- const light = {
31
- computedStyles: [
32
- mkStyle({ tag: 'a', pseudo: { before: { content: '"→"', color: 'red' }, after: null } }),
33
- mkStyle({ tag: 'li', pseudo: { before: { content: '"•"' }, after: { content: '"x"' } } }),
34
- ],
35
- };
36
- const r = extractModernCss({ light });
37
- assert.equal(r.pseudoElements.count, 3);
38
- assert.ok(r.pseudoElements.samples.length >= 2);
39
- assert.equal(r.pseudoElements.samples[0].which, '::before');
40
- });
41
-
42
- it('aggregates variable-font axes with min/max', () => {
43
- const light = {
44
- computedStyles: [
45
- mkStyle({ fontVariationSettings: '"wght" 400, "slnt" 0' }),
46
- mkStyle({ fontVariationSettings: '"wght" 700, "slnt" -8' }),
47
- ],
48
- };
49
- const r = extractModernCss({ light });
50
- assert.equal(r.variableFonts.count, 2);
51
- const wght = r.variableFonts.axes.find(a => a.axis === 'wght');
52
- assert.ok(wght);
53
- assert.equal(wght.min, 400);
54
- assert.equal(wght.max, 700);
55
- const slnt = r.variableFonts.axes.find(a => a.axis === 'slnt');
56
- assert.equal(slnt.min, -8);
57
- assert.equal(slnt.max, 0);
58
- });
59
-
60
- it('collects OpenType features and counts', () => {
61
- const light = {
62
- computedStyles: [
63
- mkStyle({ fontFeatureSettings: '"ss01" on, "cv11"' }),
64
- mkStyle({ fontFeatureSettings: '"ss01"' }),
65
- ],
66
- };
67
- const r = extractModernCss({ light });
68
- const ss01 = r.openTypeFeatures.find(f => f.feature === 'ss01');
69
- assert.equal(ss01.count, 2);
70
- const cv11 = r.openTypeFeatures.find(f => f.feature === 'cv11');
71
- assert.equal(cv11.count, 1);
72
- });
73
-
74
- it('collects modern text-layout properties', () => {
75
- const light = {
76
- computedStyles: [
77
- mkStyle({ textWrap: 'balance', textDecorationStyle: 'wavy', textDecorationThickness: '2px', textUnderlineOffset: '3px' }),
78
- mkStyle({ textWrap: 'balance' }),
79
- mkStyle({ textWrap: 'pretty' }),
80
- ],
81
- };
82
- const r = extractModernCss({ light });
83
- const balance = r.textWrap.wrap.find(w => w.value === 'balance');
84
- assert.equal(balance.count, 2);
85
- assert.equal(r.textWrap.decorationStyle[0].value, 'wavy');
86
- assert.equal(r.textWrap.decorationThickness[0].value, '2px');
87
- assert.equal(r.textWrap.underlineOffset[0].value, '3px');
88
- });
89
-
90
- it('passes through container queries and env() usage', () => {
91
- const light = {
92
- computedStyles: [],
93
- containerQueries: [
94
- { condition: '(min-width: 480px)', selectorText: '.card', declarationCount: 3 },
95
- ],
96
- envUsage: ['safe-area-inset-top', 'safe-area-inset-top', 'viewport-segment-top'],
97
- };
98
- const r = extractModernCss({ light });
99
- assert.equal(r.containerQueries.count, 1);
100
- assert.equal(r.containerQueries.rules[0].condition, '(min-width: 480px)');
101
- assert.equal(r.envUsage.length, 2);
102
- assert.ok(r.envUsage.includes('safe-area-inset-top'));
103
- });
104
- });
@@ -1,120 +0,0 @@
1
- import { describe, it } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { reconcileRoutes, slugForPath, formatRoutesReport } from '../src/formatters/routes-reconciliation.js';
4
-
5
- function leaf(v) { return { $value: v, $type: 'color' }; }
6
-
7
- const baseTokens = {
8
- primitive: {
9
- color: {
10
- brand: { primary: leaf('#ff0000'), secondary: leaf('#00ff00') },
11
- neutral: { 100: leaf('#ffffff') },
12
- },
13
- },
14
- semantic: {},
15
- };
16
-
17
- describe('slugForPath', () => {
18
- it('treats / and empty as index', () => {
19
- assert.equal(slugForPath('/'), 'index');
20
- assert.equal(slugForPath(''), 'index');
21
- assert.equal(slugForPath(null), 'index');
22
- });
23
- it('slugifies standard paths', () => {
24
- assert.equal(slugForPath('/pricing'), 'pricing');
25
- assert.equal(slugForPath('/docs/getting-started'), 'docs-getting-started');
26
- });
27
- });
28
-
29
- describe('reconcileRoutes', () => {
30
- it('returns empty-safe output for no routes', () => {
31
- const r = reconcileRoutes([]);
32
- assert.equal(r.summary.routeCount, 0);
33
- assert.equal(r.summary.sharedTokenCount, 0);
34
- assert.deepEqual(r.perRoute, {});
35
- });
36
-
37
- it('intersects identical tokens across routes into shared', () => {
38
- const routes = [
39
- { url: 'https://x.com/', path: '/', tokens: baseTokens },
40
- { url: 'https://x.com/about', path: '/about', tokens: baseTokens },
41
- ];
42
- const r = reconcileRoutes(routes);
43
- assert.equal(r.summary.routeCount, 2);
44
- assert.ok(r.summary.sharedTokenCount >= 3);
45
- // shared tree has the primitive.color.brand.primary leaf
46
- assert.equal(r.shared.primitive.color.brand.primary.$value, '#ff0000');
47
- // neither route should have "added" or "changed" entries
48
- assert.deepEqual(r.perRoute.index.added, {});
49
- assert.deepEqual(r.perRoute.index.changed, {});
50
- assert.deepEqual(r.perRoute.about.added, {});
51
- });
52
-
53
- it('emits `added` tokens for a route that has unique tokens', () => {
54
- const priceTokens = {
55
- primitive: {
56
- color: {
57
- brand: { primary: leaf('#ff0000'), secondary: leaf('#00ff00') },
58
- neutral: { 100: leaf('#ffffff') },
59
- accent: { gold: leaf('#ffd700') }, // unique to /pricing
60
- },
61
- },
62
- semantic: {},
63
- };
64
- const routes = [
65
- { url: 'https://x.com/', path: '/', tokens: baseTokens },
66
- { url: 'https://x.com/pricing', path: '/pricing', tokens: priceTokens },
67
- ];
68
- const r = reconcileRoutes(routes);
69
- assert.equal(r.perRoute.pricing.added.primitive.color.accent.gold.$value, '#ffd700');
70
- // The base route has no "added" since all its tokens are shared or absent.
71
- assert.deepEqual(r.perRoute.index.added, {});
72
- });
73
-
74
- it('emits `changed` when a route overrides an otherwise-shared token value', () => {
75
- const darkTokens = {
76
- primitive: {
77
- color: {
78
- brand: { primary: leaf('#aa0000'), secondary: leaf('#00ff00') }, // primary differs
79
- neutral: { 100: leaf('#ffffff') },
80
- },
81
- },
82
- semantic: {},
83
- };
84
- const routes = [
85
- { url: 'https://x.com/', path: '/', tokens: baseTokens },
86
- { url: 'https://x.com/dark', path: '/dark', tokens: darkTokens },
87
- ];
88
- const r = reconcileRoutes(routes);
89
- // primary shouldn't be shared because values disagree
90
- assert.ok(!('primary' in (r.shared.primitive?.color?.brand || {})),
91
- 'disagreeing primary should not be shared');
92
- // both routes should have primary in their `changed` bucket
93
- assert.equal(r.perRoute.index.changed.primitive.color.brand.primary.$value, '#ff0000');
94
- assert.equal(r.perRoute.dark.changed.primitive.color.brand.primary.$value, '#aa0000');
95
- assert.ok(r.summary.drift.length >= 2);
96
- });
97
-
98
- it('resolves slug collisions by appending a numeric suffix', () => {
99
- const routes = [
100
- { url: 'https://x.com/docs/a', path: '/docs-a', tokens: baseTokens },
101
- { url: 'https://x.com/docs-a/', path: '/docs-a', tokens: baseTokens },
102
- ];
103
- const r = reconcileRoutes(routes);
104
- const slugs = Object.keys(r.perRoute);
105
- assert.equal(slugs.length, 2);
106
- assert.ok(slugs.includes('docs-a'));
107
- assert.ok(slugs.some(s => s.startsWith('docs-a-')));
108
- });
109
-
110
- it('formatRoutesReport produces readable markdown', () => {
111
- const routes = [
112
- { url: 'https://x.com/', path: '/', tokens: baseTokens },
113
- { url: 'https://x.com/pricing', path: '/pricing', tokens: baseTokens },
114
- ];
115
- const md = formatRoutesReport(reconcileRoutes(routes));
116
- assert.match(md, /Routes crawled.*2/);
117
- assert.match(md, /Shared tokens/);
118
- assert.match(md, /pricing/);
119
- });
120
- });