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.
- package/.github/FUNDING.yml +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
- package/CHANGELOG.md +43 -0
- package/README.md +111 -1
- package/bin/design-extract.js +88 -2
- package/docs/superpowers/plans/2026-04-18-designlang-v7.md +1121 -0
- package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +150 -0
- package/package.json +5 -4
- package/src/config.js +23 -0
- package/src/crawler.js +116 -0
- package/src/extractors/a11y-remediation.js +47 -0
- package/src/extractors/component-clusters.js +39 -0
- package/src/extractors/css-health.js +151 -0
- package/src/extractors/scoring.js +20 -1
- package/src/extractors/semantic-regions.js +44 -0
- package/src/extractors/stack-fingerprint.js +88 -0
- package/src/formatters/_token-ref.js +44 -0
- package/src/formatters/agent-rules.js +116 -0
- package/src/formatters/android-compose.js +164 -0
- package/src/formatters/dtcg-tokens.js +175 -0
- package/src/formatters/flutter-dart.js +130 -0
- package/src/formatters/ios-swiftui.js +161 -0
- package/src/formatters/markdown.js +25 -0
- package/src/formatters/wordpress.js +183 -0
- package/src/index.js +30 -0
- package/src/mcp/resources.js +64 -0
- package/src/mcp/server.js +110 -0
- package/src/mcp/tools.js +149 -0
- package/tests/cli.test.js +50 -0
- package/tests/extractors.test.js +131 -0
- package/tests/formatters.test.js +232 -0
- package/tests/mcp.test.js +68 -0
- package/website/app/globals.css +11 -11
package/tests/formatters.test.js
CHANGED
|
@@ -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
|
+
});
|
package/website/app/globals.css
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
--red: #
|
|
2
|
+
--red: #ffffff;
|
|
3
3
|
--black: #0a0a0a;
|
|
4
|
-
--white: #
|
|
5
|
-
--cream: #
|
|
6
|
-
--yellow: #
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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: #
|
|
672
|
-
.extractor-a11y-score.ok { color:
|
|
673
|
-
.extractor-a11y-score.bad { color:
|
|
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;
|