feat(web): marketing Home page and the product component library (ADR 0023)
Product UI components are built once at design size from the Figma file and reused by the marketing mockups and, later, the app. Text contrast tokens now meet WCAG AA in both themes, with a test. Pricing stays hidden until decided. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014SdXCbrN5PtGExkpvMaMHx
This commit is contained in:
@@ -26,7 +26,7 @@ const COLOR_TOKENS = [
|
||||
'--color-text-primary', '--color-text-secondary', '--color-text-tertiary', '--color-text-inverse',
|
||||
'--color-text-on-accent', '--color-text-link',
|
||||
'--color-border-subtle', '--color-border-default', '--color-border-strong',
|
||||
'--color-accent-default', '--color-accent-text', '--color-accent-subtle',
|
||||
'--color-accent-default', '--color-accent-fill', '--color-accent-text', '--color-accent-subtle',
|
||||
'--color-status-working-dot', '--color-status-working-fg', '--color-status-working-bg',
|
||||
'--color-status-needs-you-dot', '--color-status-needs-you-fg', '--color-status-needs-you-bg',
|
||||
'--color-status-success-dot', '--color-status-success-fg', '--color-status-success-bg',
|
||||
@@ -79,3 +79,45 @@ describe('design tokens', () => {
|
||||
expect(css).toMatch(/\.t-display-xl\s*\{[^}]*letter-spacing:\s*-0\.03em/);
|
||||
});
|
||||
});
|
||||
|
||||
/** WCAG 2.x relative luminance and contrast ratio. */
|
||||
function luminance(hex: string): number {
|
||||
const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255) as [number, number, number];
|
||||
const f = (c: number) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
||||
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
||||
}
|
||||
function contrast(a: string, b: string): number {
|
||||
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x) as [number, number];
|
||||
return (hi + 0.05) / (lo + 0.05);
|
||||
}
|
||||
|
||||
/** Text token on the background token it appears on. All are small text, so AA is 4.5:1 (PX-10). */
|
||||
const PAIRS: ReadonlyArray<[string, string]> = [
|
||||
['--color-text-primary', '--color-bg-canvas'], ['--color-text-primary', '--color-bg-surface'],
|
||||
['--color-text-primary', '--color-bg-sunken'], ['--color-text-primary', '--color-accent-subtle'],
|
||||
['--color-text-secondary', '--color-bg-canvas'], ['--color-text-secondary', '--color-bg-surface'],
|
||||
['--color-text-secondary', '--color-bg-sunken'], ['--color-text-secondary', '--color-accent-subtle'],
|
||||
['--color-text-tertiary', '--color-bg-canvas'], ['--color-text-tertiary', '--color-bg-surface'],
|
||||
['--color-text-tertiary', '--color-bg-sunken'],
|
||||
['--color-text-inverse', '--color-bg-inverse'],
|
||||
['--color-text-on-accent', '--color-accent-fill'],
|
||||
['--color-accent-text', '--color-bg-canvas'], ['--color-accent-text', '--color-bg-surface'],
|
||||
['--color-accent-text', '--color-accent-subtle'],
|
||||
['--color-text-link', '--color-bg-canvas'], ['--color-text-link', '--color-bg-surface'],
|
||||
['--color-text-link', '--color-bg-sunken'],
|
||||
['--color-status-working-fg', '--color-status-working-bg'],
|
||||
['--color-status-needs-you-fg', '--color-status-needs-you-bg'],
|
||||
['--color-status-success-fg', '--color-status-success-bg'],
|
||||
['--color-diff-add-fg', '--color-bg-surface'], ['--color-diff-remove-fg', '--color-bg-surface'],
|
||||
];
|
||||
|
||||
describe('contrast (WCAG 2.2 AA, PX-10)', () => {
|
||||
for (const [theme, tokens] of [['light', light], ['dark', dark]] as const) {
|
||||
it(`every text pair is at least 4.5:1 in the ${theme} theme`, () => {
|
||||
const failures = PAIRS.map(([fg, bg]) => ({ fg, bg, ratio: contrast(tokens[fg]!, tokens[bg]!) }))
|
||||
.filter((p) => p.ratio < 4.5)
|
||||
.map((p) => `${p.fg} on ${p.bg} = ${p.ratio.toFixed(2)}`);
|
||||
expect(failures).toEqual([]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user