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,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
- });