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.
- package/CHANGELOG.md +69 -0
- package/README.md +154 -13
- package/bin/design-extract.js +94 -1
- package/package.json +9 -3
- package/src/config.js +2 -0
- package/src/crawler.js +55 -6
- package/src/drift.js +137 -0
- package/src/extractors/accessibility.js +44 -1
- package/src/extractors/colors.js +50 -12
- package/src/extractors/component-anatomy.js +123 -0
- package/src/extractors/motion.js +184 -0
- package/src/extractors/scoring.js +49 -30
- package/src/extractors/voice.js +96 -0
- package/src/formatters/markdown.js +88 -0
- package/src/formatters/motion-tokens.js +22 -0
- package/src/index.js +14 -0
- package/src/lint.js +198 -0
- package/src/visual-diff.js +116 -0
- package/.github/FUNDING.yml +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -62
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -28
- package/.github/og-preview.png +0 -0
- package/.github/workflows/manavarya-bot.yml +0 -17
- package/chrome-extension/README.md +0 -41
- package/chrome-extension/icons/favicon.svg +0 -7
- package/chrome-extension/icons/icon-128.png +0 -0
- package/chrome-extension/icons/icon-16.png +0 -0
- package/chrome-extension/icons/icon-32.png +0 -0
- package/chrome-extension/icons/icon-48.png +0 -0
- package/chrome-extension/manifest.json +0 -26
- package/chrome-extension/popup.html +0 -167
- package/chrome-extension/popup.js +0 -59
- package/docs/superpowers/plans/2026-04-18-designlang-v7.md +0 -1121
- package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +0 -150
- package/docs/superpowers/specs/2026-04-18-website-redesign-design.md +0 -120
- package/docs/superpowers/specs/2026-04-19-designlang-v7-1-design.md +0 -111
- package/tests/cli.test.js +0 -84
- package/tests/cookies.test.js +0 -98
- package/tests/extractors.test.js +0 -792
- package/tests/formatters.test.js +0 -709
- package/tests/interaction-states.test.js +0 -62
- package/tests/mcp.test.js +0 -68
- package/tests/modern-css.test.js +0 -104
- package/tests/routes-reconciliation.test.js +0 -120
- package/tests/utils.test.js +0 -413
- package/tests/wide-gamut.test.js +0 -90
- package/website/.claude/launch.json +0 -11
- package/website/AGENTS.md +0 -5
- package/website/CLAUDE.md +0 -1
- package/website/README.md +0 -36
- package/website/app/api/extract/route.js +0 -245
- package/website/app/components/A11ySlider.js +0 -369
- package/website/app/components/Comparison.js +0 -286
- package/website/app/components/CssHealth.js +0 -243
- package/website/app/components/Extractor.js +0 -184
- package/website/app/components/HeroExtractor.js +0 -455
- package/website/app/components/Marginalia.js +0 -3
- package/website/app/components/McpSection.js +0 -223
- package/website/app/components/PlatformTabs.js +0 -250
- package/website/app/components/RegionsComponents.js +0 -429
- package/website/app/components/Rule.js +0 -13
- package/website/app/components/Specimens.js +0 -237
- package/website/app/components/StructuredData.js +0 -144
- package/website/app/components/TokenBrowser.js +0 -344
- package/website/app/components/token-browser-sample.js +0 -65
- package/website/app/globals.css +0 -505
- package/website/app/icon.svg +0 -7
- package/website/app/layout.js +0 -126
- package/website/app/opengraph-image.js +0 -170
- package/website/app/page.js +0 -399
- package/website/app/robots.js +0 -15
- package/website/app/seo-config.js +0 -82
- package/website/app/sitemap.js +0 -18
- package/website/jsconfig.json +0 -7
- package/website/lib/cache.js +0 -73
- package/website/lib/rate-limit.js +0 -30
- package/website/lib/rate-limit.test.js +0 -55
- package/website/lib/specimens.json +0 -86
- package/website/lib/token-helpers.js +0 -70
- package/website/lib/url-safety.js +0 -103
- package/website/lib/url-safety.test.js +0 -116
- package/website/lib/zip-files.js +0 -15
- package/website/next.config.mjs +0 -15
- package/website/package-lock.json +0 -1353
- package/website/package.json +0 -19
- package/website/public/favicon.svg +0 -7
- package/website/public/logo-specimen.svg +0 -76
- package/website/public/mark.svg +0 -12
- 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
|
-
});
|
package/tests/modern-css.test.js
DELETED
|
@@ -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
|
-
});
|