/** * WCAG relative luminance of a solid hex color (#rgb or #rrggbb, with or * without the leading '#'). https://www.w3.org/TR/WCAG21/#dfn-relative-luminance */ export function relativeLuminance(hex: string): number { const h = hex.replace('#', ''); const full = h.length === 3 ? h.split('').map((c) => c + c).join('') : h; const channel = (offset: number): number => { const c = parseInt(full.slice(offset, offset + 2), 16) / 255; return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; }; return 0.2126 * channel(0) + 0.7152 * channel(2) + 0.0722 * channel(4); } /** WCAG contrast ratio between two solid hex colors (1–21). */ export function contrastRatio(a: string, b: string): number { const la = relativeLuminance(a); const lb = relativeLuminance(b); const [hi, lo] = la >= lb ? [la, lb] : [lb, la]; return (hi + 0.05) / (lo + 0.05); } /** * Pick the higher-contrast foreground text color (white or near-black) for a * solid hex background. Returns whichever of `#ffffff` / `#1a1a1a` has the * greater contrast ratio against `bgHex` — e.g. white on dark badges, dark on * light/mid badges (yellow, orange). The four CorrelationPanel score-badge * backgrounds clear WCAG AA with the chosen color; arbitrary mid-gray inputs * may still need a different palette to reach 4.5:1. */ export function readableTextColor(bgHex: string): '#ffffff' | '#1a1a1a' { return contrastRatio('#ffffff', bgHex) >= contrastRatio('#1a1a1a', bgHex) ? '#ffffff' : '#1a1a1a'; }