diff --git a/src/lib/color-contrast.ts b/src/lib/color-contrast.ts new file mode 100644 index 0000000..bd27419 --- /dev/null +++ b/src/lib/color-contrast.ts @@ -0,0 +1,63 @@ +interface ColorRGB { + r: number; + g: number; + b: number; +} + +interface ContrastResult { + passes: boolean; + ratio: number; + requiredRatio: number; +} + +function hexToRgb(hex: string): ColorRGB { + const cleanHex = hex.replace('#', ''); + const r = parseInt(cleanHex.substring(0, 2), 16); + const g = parseInt(cleanHex.substring(2, 4), 16); + const b = parseInt(cleanHex.substring(4, 6), 16); + return { r, g, b }; +} + +function getLuminance(rgb: ColorRGB): number { + const { r, g, b } = rgb; + const a = [r, g, b].map(v => { + v /= 255; + return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); + }); + return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; +} + +export function calculateContrastRatio(foreground: string, background: string): number { + const fgRgb = hexToRgb(foreground); + const bgRgb = hexToRgb(background); + + const fgLuminance = getLuminance(fgRgb); + const bgLuminance = getLuminance(bgRgb); + + const lighter = Math.max(fgLuminance, bgLuminance); + const darker = Math.min(fgLuminance, bgLuminance); + + return (lighter + 0.05) / (darker + 0.05); +} + +export function meetsWCAGStandard( + foreground: string, + background: string, + level: 'AA' | 'AAA', + textSize: 'normal' | 'large' +): ContrastResult { + const ratio = calculateContrastRatio(foreground, background); + + let requiredRatio: number; + if (level === 'AA') { + requiredRatio = textSize === 'normal' ? 4.5 : 3; + } else { + requiredRatio = textSize === 'normal' ? 7 : 4.5; + } + + return { + passes: ratio >= requiredRatio, + ratio, + requiredRatio + }; +} diff --git a/tests/lib/color-contrast.spec.ts b/tests/lib/color-contrast.spec.ts new file mode 100644 index 0000000..738a5f3 --- /dev/null +++ b/tests/lib/color-contrast.spec.ts @@ -0,0 +1,19 @@ +import { test, expect } from '@playwright/test'; +import { calculateContrastRatio, meetsWCAGStandard } from '@/lib/color-contrast'; + +test('should calculate correct contrast ratio for black on white', () => { + const ratio = calculateContrastRatio('#000000', '#FFFFFF'); + expect(ratio).toBeCloseTo(21, 1); +}); + +test('should identify WCAG AA compliance for normal text', () => { + const result = meetsWCAGStandard('#000000', '#FFFFFF', 'AA', 'normal'); + expect(result.passes).toBe(true); + expect(result.ratio).toBeGreaterThan(4.5); +}); + +test('should fail WCAG AA for low contrast colors', () => { + const result = meetsWCAGStandard('#808080', '#FFFFFF', 'AA', 'normal'); + expect(result.passes).toBe(false); + expect(result.ratio).toBeLessThan(4.5); +});