/** * WCAG 2.1 Contrast Ratio Utilities * * Pure math functions for calculating contrast ratios between colors. * Used for automated accessibility testing of theme color tokens. * * @see https://www.w3.org/TR/WCAG21/#contrast-minimum */ /** * Check if a string is a valid hex color (#fff, #ffffff, #fffa, #ffffff80). * Rejects rgba(), hsl(), named colors, etc. */ export function isHexColor(value) { return /^#([0-9a-fA-F]{3,8})$/.test(value); } /** * Check if a string is an opaque hex color (#rgb or #rrggbb only). * Rejects alpha-bearing hex (#rgba, #rrggbbaa) since contrast * calculation requires compositing against a known background. */ export function isOpaqueHexColor(value) { return /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value); } /** * Parse a hex color string to RGB tuple. * Handles shorthand (#fff), full (#ffffff), and alpha (#ffffff80). * Strips alpha channel — contrast calculation uses only RGB. * * @throws {Error} If input is not a valid hex color */ export function hexToRgb(hex) { if (!isHexColor(hex)) { throw new Error(`Not a hex color: "${hex}". Only #rgb, #rrggbb, #rrggbbaa formats supported.`); } let h = hex.slice(1); // Remove # // Expand shorthand: #fff → ffffff, #fffa → ffffffaa if (h.length === 3 || h.length === 4) { h = h .slice(0, 3) .split('') .map((c) => c + c) .join(''); } else { // Strip alpha from 8-digit hex h = h.slice(0, 6); } const num = parseInt(h, 16); return [(num >> 16) & 0xff, (num >> 8) & 0xff, num & 0xff]; } /** * Calculate relative luminance per WCAG 2.1. * Applies sRGB linearization then weighted sum. * * @see https://www.w3.org/TR/WCAG21/#dfn-relative-luminance */ export function relativeLuminance(r, g, b) { const [rs, gs, bs] = [r, g, b].map((c) => { const s = c / 255; return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); }); return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs; } /** * Calculate WCAG 2.1 contrast ratio between two hex colors. * Returns a number >= 1, where 21:1 is maximum (black on white). * * @example contrastRatio('#000000', '#ffffff') // 21 */ export function contrastRatio(color1, color2) { const [r1, g1, b1] = hexToRgb(color1); const [r2, g2, b2] = hexToRgb(color2); const l1 = relativeLuminance(r1, g1, b1); const l2 = relativeLuminance(r2, g2, b2); const lighter = Math.max(l1, l2); const darker = Math.min(l1, l2); return (lighter + 0.05) / (darker + 0.05); } const WCAG_THRESHOLDS = { AA: 4.5, 'AA-large': 3.0, AAA: 7.0, }; /** * Check whether a foreground/background pair meets a WCAG contrast level. * * - AA: 4.5:1 (normal text < 18pt) * - AA-large: 3.0:1 (large text >= 18pt, UI components) * - AAA: 7.0:1 (enhanced contrast) */ export function meetsWCAG(fg, bg, level) { return contrastRatio(fg, bg) >= WCAG_THRESHOLDS[level]; } //# sourceMappingURL=contrast.js.map