/** * Contraste WCAG AA (seção 6 do schema de arquitetura). Usado para * garantir que a cor de marca de um tenant, quando usada como texto/ícone * sobre fundo claro, continue legível — mesmo que o tenant tenha * escolhido uma cor originalmente clara demais para isso. */ interface Rgb { r: number; g: number; b: number; } function hexToRgb(hex: string): Rgb { const normalized = hex.replace('#', ''); const full = normalized.length === 3 ? normalized.replace(/(.)/g, '$1$1') : normalized; const value = parseInt(full, 16); return { r: (value >> 16) & 255, g: (value >> 8) & 255, b: value & 255 }; } function rgbToHex({ r, g, b }: Rgb): string { return `#${[r, g, b].map((c) => Math.max(0, Math.min(255, c)).toString(16).padStart(2, '0')).join('')}`; } function relativeLuminance({ r, g, b }: Rgb): number { const [rs, gs, bs] = [r, g, b].map((c) => { const channel = c / 255; return channel <= 0.03928 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4); }); return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs; } export function contrastRatio(hexA: string, hexB: string): number { const lA = relativeLuminance(hexToRgb(hexA)); const lB = relativeLuminance(hexToRgb(hexB)); const lighter = Math.max(lA, lB); const darker = Math.min(lA, lB); return (lighter + 0.05) / (darker + 0.05); } const WCAG_AA_MIN_CONTRAST = 4.5; /** * Se `hex` já tem contraste AA (≥4.5:1) contra `background`, devolve ele * mesmo. Senão, escurece progressivamente até passar (ou até chegar em * preto). Não deixa a cor original para uso como fundo — só gera a * variante seguro para texto/ícone. */ export function ensureAccessibleTextColor(hex: string, background = '#FFFFFF'): string { if (contrastRatio(hex, background) >= WCAG_AA_MIN_CONTRAST) { return hex; } const base = hexToRgb(hex); for (let step = 1; step <= 20; step += 1) { const factor = step / 20; const darkened = rgbToHex({ r: Math.round(base.r * (1 - factor)), g: Math.round(base.g * (1 - factor)), b: Math.round(base.b * (1 - factor)), }); if (contrastRatio(darkened, background) >= WCAG_AA_MIN_CONTRAST) { return darkened; } } return '#000000'; }