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:
@@ -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 (400–900) and JetBrains Mono (400–800) 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 80–120px', () => {
|
||||
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\}/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user