Session 33: Design system Phase A+B — tokens, fonts, glitch CSS, shared components (1792 tests)

VYNDR 2.0 design-system conversion, foundation only (pages/mobile/systems are
Sessions 34+). Frontend-only; zero backend changes.

Phase A: §2 token set + token-wired typography + Inter/JetBrains Mono fonts +
§4 glitch keyframes (ported verbatim from the prototype's vyndr.css) + scanline
texture + §10 a11y data-* layer in globals.css. Legacy alias block kept.

Phase B: web/src/lib/vyndrTokens.js (CommonJS helpers) + 9 shared components
under web/src/components/vyndr/ (Wordmark, GradeBadge, SportBadge, TerminalInput,
SectionHead, VBtn, Card, Sparkline, Ticker).

74 new design-system tests. Backend 1718 -> 1792, 141 suites, zero regressions.
Web build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-06-15 22:58:57 -04:00
parent f0c8b4f29b
commit a74b5dd1ed
16 changed files with 1234 additions and 10 deletions
+185
View File
@@ -0,0 +1,185 @@
// VYNDR 2.0 design system — Phase A (tokens/CSS/fonts/glitch) + Phase B
// (shared components). The .tsx components can't run under the plain-JS Jest
// config (no Babel/TS transform), so component contracts are asserted against
// their source text; the helper LOGIC is exercised directly via the CommonJS
// vyndrTokens module that the components actually import.
const fs = require('fs');
const path = require('path');
const WEB = path.join(__dirname, '..', '..', 'web', 'src');
const read = (rel) => fs.readFileSync(path.join(WEB, rel), 'utf8');
const css = read('app/globals.css');
const layout = read('app/layout.tsx');
const tokens = require('../../web/src/lib/vyndrTokens');
describe('Phase A — design tokens (§2)', () => {
const REQUIRED_TOKENS = [
'--bg-0', '--bg-1', '--bg-2', '--bg-3', '--border', '--border-hi',
'--text-0', '--text-1', '--text-2',
'--g-ap', '--g-a', '--g-b', '--g-c', '--g-d',
'--s-nba', '--s-mlb', '--s-wnba', '--s-soccer',
'--acc-0', '--acc-1', '--amber', '--amber-glow',
'--live', '--hit', '--miss',
'--glitch', '--scan-op', '--grade-hero', '--sans', '--mono',
];
it.each(REQUIRED_TOKENS)('defines %s in :root', (token) => {
expect(css).toMatch(new RegExp(`${token.replace(/[-]/g, '\\-')}\\s*:`));
});
it('uses the exact §2 grade hex values', () => {
expect(css).toContain('--g-ap: #00ffb8');
expect(css).toContain('--g-a: #00d4a0');
expect(css).toContain('--g-b: #4a9eff');
expect(css).toContain('--g-c: #ffb347');
expect(css).toContain('--g-d: #ff5252');
});
it('sets --sans to Inter and --mono to JetBrains Mono', () => {
expect(css).toMatch(/--sans:\s*'Inter'/);
expect(css).toMatch(/--mono:\s*'JetBrains Mono'/);
});
it('sets glitch intensity to the §2 baseline of 1 and scan-op to 0.04', () => {
expect(css).toMatch(/--glitch:\s*1;/);
expect(css).toMatch(/--scan-op:\s*0\.04;/);
});
});
describe('Phase A — fonts (§2)', () => {
it('loads Inter (400900) and JetBrains Mono (400800) from Google Fonts', () => {
expect(layout).toContain('Inter:wght@400;500;600;700;800;900');
expect(layout).toContain('JetBrains+Mono:wght@400;500;600;700;800');
});
});
describe('Phase A — glitch + animation keyframes (§4, ported verbatim)', () => {
const KEYFRAMES = [
'wm-tear', 'glitch-shift-r', 'glitch-shift-b', 'wm-caret', 'head-tear',
'crt-sweep', 'crt-sweep-local', 'phosphor-pulse', 'grade-reveal',
'ticker-scroll', 'live-pulse', 'flash-up', 'flash-down', 'ekg-scroll',
'node-pulse', 'synapse-travel', 'count-tick', 'factor-ignite',
'toast-in', 'sheet-up', 'cmd-in', 'fade-in', 'scan-drift',
];
it.each(KEYFRAMES)('defines @keyframes %s', (name) => {
expect(css).toContain(`@keyframes ${name}`);
});
it('floors fade-in at the visible state (opacity .6, never 0)', () => {
expect(css).toMatch(/@keyframes fade-in\s*\{\s*from\s*\{\s*opacity:\s*0\.6;/);
});
});
describe('Phase A — scanline texture + reduced motion (§4, §10)', () => {
it('has the .scanlines::after CRT texture class', () => {
expect(css).toContain('.scanlines::after');
expect(css).toMatch(/repeating-linear-gradient\(0deg, transparent 50%, rgba\(0,0,0,1\) 50%\)/);
expect(css).toMatch(/opacity: var\(--scan-op\)/);
});
it('respects prefers-reduced-motion and the in-app data-motion toggle', () => {
expect(css).toContain('@media (prefers-reduced-motion: reduce)');
expect(css).toContain('html[data-motion="reduced"]');
});
it('ships the a11y preference layer (contrast / text / colorblind / readable font)', () => {
expect(css).toContain('html[data-contrast="high"]');
expect(css).toContain('html[data-text="xl"]');
expect(css).toContain('html[data-cb="1"]');
expect(css).toContain('html[data-font="readable"]');
});
});
describe('Phase B — token helpers (vyndrTokens)', () => {
it('gradeColor returns the correct CSS variable for each grade tier', () => {
expect(tokens.gradeColor('A+')).toBe('var(--g-ap)');
expect(tokens.gradeColor('A')).toBe('var(--g-a)');
expect(tokens.gradeColor('A-')).toBe('var(--g-a)');
expect(tokens.gradeColor('B+')).toBe('var(--g-b)');
expect(tokens.gradeColor('B')).toBe('var(--g-b)');
expect(tokens.gradeColor('C')).toBe('var(--g-c)');
expect(tokens.gradeColor('D')).toBe('var(--g-d)');
});
it('gradeColor falls back to primary text for unknown grades', () => {
expect(tokens.gradeColor('Z')).toBe('var(--text-0)');
});
it('gradeHex returns the exact §2 hex for each grade', () => {
expect(tokens.gradeHex('A+')).toBe('#00ffb8');
expect(tokens.gradeHex('A')).toBe('#00d4a0');
expect(tokens.gradeHex('B')).toBe('#4a9eff');
expect(tokens.gradeHex('C')).toBe('#ffb347');
expect(tokens.gradeHex('D')).toBe('#ff5252');
});
it('SPORT map carries every sport with its token color and label', () => {
expect(tokens.SPORT.nba).toEqual({ label: 'NBA', color: 'var(--s-nba)', hex: '#e94b3c' });
expect(tokens.SPORT.mlb.hex).toBe('#1e90ff');
expect(tokens.SPORT.wnba.hex).toBe('#f7944a');
expect(tokens.SPORT.soccer.hex).toBe('#3ddc84');
});
it('gradeBadgeSize maps variants and passes raw numbers through; hero is 80120px', () => {
expect(tokens.gradeBadgeSize('sm')).toBe(16);
expect(tokens.gradeBadgeSize('lg')).toBe(48);
expect(tokens.gradeBadgeSize(72)).toBe(72);
const hero = tokens.gradeBadgeSize('hero');
expect(hero).toBeGreaterThanOrEqual(80);
expect(hero).toBeLessThanOrEqual(120);
});
});
describe('Phase B — shared component contracts (§5)', () => {
const comp = (name) => read(path.join('components', 'vyndr', name));
it('Wordmark renders the green R, caret, VYNDR data-text and BETA pill', () => {
const src = comp('Wordmark.tsx');
expect(src).toContain('data-text="VYNDR"');
expect(src).toContain('className="wm-r"');
expect(src).toContain('wm-cursor');
expect(src).toContain('wm-beta');
});
it('GradeBadge colors by grade token and supports the hero size', () => {
const src = comp('GradeBadge.tsx');
expect(src).toContain('gradeColor(grade)');
expect(src).toContain('gradeBadgeSize(size)');
expect(src).toMatch(/'hero'/);
});
it('SportBadge derives its color from the SPORT token map', () => {
expect(comp('SportBadge.tsx')).toContain('SPORT[sport');
});
it('VBtn defines all five variants', () => {
const src = comp('VBtn.tsx');
['primary', 'outline', 'ghost', 'amber', 'danger'].forEach((v) => {
expect(src).toContain(`${v}:`);
});
expect(src).toContain("background: 'var(--g-a)'"); // primary = grade-green filled
});
it('Card is a Level-1 (bg-1) surface with the scanline skin', () => {
const src = comp('Card.tsx');
expect(src).toContain("background: 'var(--bg-1)'");
expect(src).toContain('scanlines');
});
it('TerminalInput uses a phosphor cursor while empty', () => {
expect(comp('TerminalInput.tsx')).toContain('phosphor-cursor');
});
it('Sparkline renders an SVG path from its data points', () => {
const src = comp('Sparkline.tsx');
expect(src).toContain('<svg');
expect(src).toContain('<path');
});
it('Ticker uses the ticker-track marquee and duplicates content for a seamless loop', () => {
const src = comp('Ticker.tsx');
expect(src).toContain('ticker-track');
expect(src).toMatch(/\{content\}\s*\{content\}/);
});
});