feat: extract typography tokens into dedicated file with semantic aliases and a11y fallbacks
Create typography.tokens.css as single source of truth for the type system: - Extract all primitive tokens from variables.base.css (--text-*, --leading-*, --font-*, --tracking-*) - Add semantic aliases (--text-body, --text-label, --text-caption, --text-title, --text-page-title) - Add --text-md (15px) escape hatch and --tracking-wider (0.05em) token - Add prefers-reduced-motion fallbacks locking fluid headings to fixed midpoints - Document scale ratio (1.25x major third, 14px base) with reference table - Migrate h1/h2 in app-shell.css to semantic aliases - Migrate 4 raw letter-spacing values to --tracking-* tokens - Extend TypographyTokenContractTest with 3 new tests (letter-spacing, aliases, reduced-motion) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,15 +6,15 @@ use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Typography token contract — ensures all core CSS files use design-system tokens
|
||||
* (--text-*, --leading-*, --font-*) instead of raw font-size, line-height, and
|
||||
* font-weight values.
|
||||
* (--text-*, --leading-*, --font-*, --tracking-*) instead of raw font-size,
|
||||
* line-height, font-weight, and letter-spacing values.
|
||||
*
|
||||
* Documented exceptions:
|
||||
* - Icon-font sizes in px (Bootstrap Icons at 10px, 15px, 16px) — annotated with
|
||||
* "icon-font metric — intentional px"
|
||||
* - Badge em-relative sizing (0.85em) — annotated with "em-relative sizing — intentional"
|
||||
* - Vendor layer (web/css/vendor/) — third-party styles are not under our control
|
||||
* - The tokens layer itself (variables.base.css) — defines the tokens
|
||||
* - The token definition files (variables.base.css, typography.tokens.css) — define the tokens
|
||||
* - CSS custom property fallback definitions (--app-font-size, --app-font-weight, --app-line-height)
|
||||
*/
|
||||
class TypographyTokenContractTest extends TestCase
|
||||
@@ -30,6 +30,7 @@ class TypographyTokenContractTest extends TestCase
|
||||
'web/css/vendor/',
|
||||
'web/css/vendor-overrides/',
|
||||
'web/css/base/variables.base.css',
|
||||
'web/css/base/typography.tokens.css',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -43,6 +44,11 @@ class TypographyTokenContractTest extends TestCase
|
||||
'intentional px',
|
||||
];
|
||||
|
||||
/**
|
||||
* The file that defines all typography tokens.
|
||||
*/
|
||||
private const TYPOGRAPHY_TOKENS_FILE = 'web/css/base/typography.tokens.css';
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// font-size
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
@@ -97,35 +103,53 @@ class TypographyTokenContractTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// letter-spacing
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function testLetterSpacingUsesTokens(): void
|
||||
{
|
||||
$violations = $this->scanForRawValues(
|
||||
'/letter-spacing\s*:\s*+(?!var\(--tracking-|var\(--app-|inherit|unset|initial|revert|normal|0[;\s])/',
|
||||
self::EXCEPTION_MARKERS,
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
[],
|
||||
$violations,
|
||||
"Raw letter-spacing values found instead of --tracking-* tokens:\n" . implode("\n", $violations),
|
||||
);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// Token definitions exist
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function testTypographyTokensAreDefined(): void
|
||||
{
|
||||
$content = $this->readProjectFile('web/css/base/variables.base.css');
|
||||
$content = $this->readProjectFile(self::TYPOGRAPHY_TOKENS_FILE);
|
||||
|
||||
$requiredTokens = [
|
||||
'--text-2xs', '--text-xs', '--text-sm', '--text-base', '--text-lg',
|
||||
'--text-2xs', '--text-xs', '--text-sm', '--text-base', '--text-md', '--text-lg',
|
||||
'--text-xl', '--text-2xl', '--text-3xl', '--text-4xl', '--text-5xl',
|
||||
'--leading-none', '--leading-tight', '--leading-snug', '--leading-normal',
|
||||
'--leading-relaxed', '--leading-loose',
|
||||
'--font-regular', '--font-medium', '--font-semibold', '--font-bold',
|
||||
'--tracking-tight', '--tracking-normal', '--tracking-wide',
|
||||
'--tracking-tight', '--tracking-normal', '--tracking-wide', '--tracking-wider',
|
||||
];
|
||||
|
||||
foreach ($requiredTokens as $token) {
|
||||
$this->assertStringContainsString(
|
||||
$token . ':',
|
||||
$content,
|
||||
"Typography token {$token} is missing from variables.base.css",
|
||||
"Typography token {$token} is missing from " . self::TYPOGRAPHY_TOKENS_FILE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function testHeadingTokensUseClamp(): void
|
||||
{
|
||||
$content = $this->readProjectFile('web/css/base/variables.base.css');
|
||||
$content = $this->readProjectFile(self::TYPOGRAPHY_TOKENS_FILE);
|
||||
|
||||
$clampTokens = ['--text-2xl', '--text-3xl', '--text-4xl', '--text-5xl'];
|
||||
|
||||
@@ -138,6 +162,61 @@ class TypographyTokenContractTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// Semantic aliases
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function testSemanticAliasesAreDefined(): void
|
||||
{
|
||||
$content = $this->readProjectFile(self::TYPOGRAPHY_TOKENS_FILE);
|
||||
|
||||
$aliases = [
|
||||
'--text-body' => '--text-base',
|
||||
'--text-label' => '--text-sm',
|
||||
'--text-caption' => '--text-xs',
|
||||
'--text-title' => '--text-xl',
|
||||
'--text-page-title' => '--text-2xl',
|
||||
];
|
||||
|
||||
foreach ($aliases as $alias => $primitive) {
|
||||
$this->assertMatchesRegularExpression(
|
||||
'/' . preg_quote($alias, '/') . ':\s*var\(' . preg_quote($primitive, '/') . '\)/',
|
||||
$content,
|
||||
"Semantic alias {$alias} must resolve to {$primitive} in " . self::TYPOGRAPHY_TOKENS_FILE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// Reduced-motion fallbacks
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function testReducedMotionFallbacksExist(): void
|
||||
{
|
||||
$content = $this->readProjectFile(self::TYPOGRAPHY_TOKENS_FILE);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'prefers-reduced-motion: reduce',
|
||||
$content,
|
||||
'Typography tokens must include a prefers-reduced-motion fallback block',
|
||||
);
|
||||
|
||||
$fluidTokens = ['--text-2xl', '--text-3xl', '--text-4xl', '--text-5xl'];
|
||||
|
||||
// Extract the reduced-motion block content
|
||||
preg_match('/prefers-reduced-motion:\s*reduce\).*?\{(.*?)\}\s*\}/s', $content, $matches);
|
||||
$this->assertNotEmpty($matches, 'Could not parse the prefers-reduced-motion block');
|
||||
|
||||
$block = $matches[1];
|
||||
foreach ($fluidTokens as $token) {
|
||||
$this->assertStringContainsString(
|
||||
$token . ':',
|
||||
$block,
|
||||
"Fluid token {$token} must have a fixed fallback in the prefers-reduced-motion block",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// Helpers
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user