/** * Utilitários para manipulação de cores e garantia de acessibilidade */ /** * Converte hex para RGB */ export function hexToRgb(hex: string): { r: number; g: number; b: number } | null { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16), } : null; } /** * Converte RGB para hex */ export function rgbToHex(r: number, g: number, b: number): string { return '#' + [r, g, b].map((x) => { const hex = Math.round(x).toString(16); return hex.length === 1 ? '0' + hex : hex; }).join(''); } /** * Calcula luminosidade relativa (0-1) - WCAG 2.0 */ export function getLuminance(hex: string): number { const rgb = hexToRgb(hex); if (!rgb) return 0; const [r, g, b] = [rgb.r, rgb.g, rgb.b].map((val) => { const v = val / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }); return 0.2126 * r + 0.7152 * g + 0.0722 * b; } /** * Calcula contraste entre duas cores (1-21) - WCAG 2.0 */ export function getContrast(color1: string, color2: string): number { const lum1 = getLuminance(color1); const lum2 = getLuminance(color2); const lighter = Math.max(lum1, lum2); const darker = Math.min(lum1, lum2); return (lighter + 0.05) / (darker + 0.05); } /** * Verifica se a cor é clara (luminosidade > 0.5) */ export function isLight(hex: string): boolean { return getLuminance(hex) > 0.5; } /** * Escurece uma cor em uma porcentagem */ export function darken(hex: string, amount: number): string { const rgb = hexToRgb(hex); if (!rgb) return hex; const factor = 1 - amount; return rgbToHex( rgb.r * factor, rgb.g * factor, rgb.b * factor ); } /** * Clareia uma cor em uma porcentagem */ export function lighten(hex: string, amount: number): string { const rgb = hexToRgb(hex); if (!rgb) return hex; const factor = amount; return rgbToHex( rgb.r + (255 - rgb.r) * factor, rgb.g + (255 - rgb.g) * factor, rgb.b + (255 - rgb.b) * factor ); } /** * Gera cor de hover automática baseada na luminosidade * Se a cor for clara, escurece 15% * Se a cor for escura, clareia 15% */ export function generateHoverColor(hex: string): string { return isLight(hex) ? darken(hex, 0.15) : lighten(hex, 0.15); } /** * Determina se deve usar texto branco ou preto sobre uma cor de fundo * Prioriza branco para cores vibrantes/saturadas */ export function getTextColor(backgroundColor: string): string { const contrastWithWhite = getContrast(backgroundColor, '#FFFFFF'); const contrastWithBlack = getContrast(backgroundColor, '#000000'); // Se o contraste com branco for >= 3.5, prefere branco (mais comum em UIs modernas) // WCAG AA requer 4.5:1, mas 3:1 para textos grandes if (contrastWithWhite >= 3.5) { return '#FFFFFF'; } // Se não, usa a cor com melhor contraste return contrastWithWhite > contrastWithBlack ? '#FFFFFF' : '#000000'; } /** * Gera paleta completa de cores com hover e variações */ export function generateColorPalette(primaryHex: string, secondaryHex: string) { const primaryRgb = hexToRgb(primaryHex); const secondaryRgb = hexToRgb(secondaryHex); if (!primaryRgb || !secondaryRgb) { throw new Error('Cores inválidas'); } const primaryHover = generateHoverColor(primaryHex); const secondaryHover = generateHoverColor(secondaryHex); const primaryRgbString = `${primaryRgb.r} ${primaryRgb.g} ${primaryRgb.b}`; const secondaryRgbString = `${secondaryRgb.r} ${secondaryRgb.g} ${secondaryRgb.b}`; const hoverRgb = hexToRgb(primaryHover); const hoverRgbString = hoverRgb ? `${hoverRgb.r} ${hoverRgb.g} ${hoverRgb.b}` : secondaryRgbString; return { primary: primaryHex, secondary: secondaryHex, primaryHover, secondaryHover, primaryRgb: primaryRgbString, secondaryRgb: secondaryRgbString, hoverRgb: hoverRgbString, gradient: `linear-gradient(135deg, ${primaryHex}, ${secondaryHex})`, textOnPrimary: getTextColor(primaryHex), textOnSecondary: getTextColor(secondaryHex), isLightPrimary: isLight(primaryHex), isLightSecondary: isLight(secondaryHex), contrast: getContrast(primaryHex, secondaryHex), }; } /** * Valida se as cores têm contraste suficiente */ export function validateColorContrast(primary: string, secondary: string): { valid: boolean; warnings: string[]; } { const warnings: string[] = []; const contrast = getContrast(primary, secondary); if (contrast < 3) { warnings.push('As cores são muito similares e podem causar problemas de legibilidade'); } const primaryContrast = getContrast(primary, '#FFFFFF'); if (primaryContrast < 4.5 && !isLight(primary)) { warnings.push('A cor primária pode ter baixo contraste com texto branco'); } const secondaryContrast = getContrast(secondary, '#FFFFFF'); if (secondaryContrast < 4.5 && !isLight(secondary)) { warnings.push('A cor secundária pode ter baixo contraste com texto branco'); } return { valid: warnings.length === 0, warnings, }; }