UNPKG

@productshiv/baapui

Version:

A truly cross-platform multi-design UI framework that works with React, Next.js, React Native, and any React-based framework.

168 lines (142 loc) 4.7 kB
/** * Accessibility utilities for color contrast validation and WCAG compliance */ export interface ColorContrastResult { ratio: number; wcagAA: boolean; wcagAAA: boolean; level: 'fail' | 'aa' | 'aaa'; } /** * Convert hex color to RGB values */ 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; } /** * Calculate relative luminance of a color * Based on WCAG 2.1 specification */ export function getRelativeLuminance(r: number, g: number, b: number): number { const [rs, gs, bs] = [r, g, b].map((c) => { const sRGB = c / 255; return sRGB <= 0.03928 ? sRGB / 12.92 : Math.pow((sRGB + 0.055) / 1.055, 2.4); }); return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs; } /** * Calculate contrast ratio between two colors * Returns a value between 1 and 21 */ export function getContrastRatio(color1: string, color2: string): number { const rgb1 = hexToRgb(color1); const rgb2 = hexToRgb(color2); if (!rgb1 || !rgb2) { throw new Error('Invalid color format. Please use hex colors.'); } const lum1 = getRelativeLuminance(rgb1.r, rgb1.g, rgb1.b); const lum2 = getRelativeLuminance(rgb2.r, rgb2.g, rgb2.b); const brightest = Math.max(lum1, lum2); const darkest = Math.min(lum1, lum2); return (brightest + 0.05) / (darkest + 0.05); } /** * Check if color combination meets WCAG accessibility standards */ export function checkColorContrast( foreground: string, background: string, fontSize: 'normal' | 'large' = 'normal' ): ColorContrastResult { const ratio = getContrastRatio(foreground, background); // WCAG AA requirements: 4.5:1 for normal text, 3:1 for large text // WCAG AAA requirements: 7:1 for normal text, 4.5:1 for large text const aaThreshold = fontSize === 'large' ? 3 : 4.5; const aaaThreshold = fontSize === 'large' ? 4.5 : 7; const wcagAA = ratio >= aaThreshold; const wcagAAA = ratio >= aaaThreshold; let level: 'fail' | 'aa' | 'aaa'; if (wcagAAA) { level = 'aaa'; } else if (wcagAA) { level = 'aa'; } else { level = 'fail'; } return { ratio: Math.round(ratio * 100) / 100, wcagAA, wcagAAA, level, }; } /** * Validate all color combinations in a theme */ export function validateThemeAccessibility( colors: Record<string, string>, backgroundColors: string[] = ['#FFFFFF', '#000000'] ): Record<string, ColorContrastResult[]> { const results: Record<string, ColorContrastResult[]> = {}; Object.entries(colors).forEach(([colorName, colorValue]) => { results[colorName] = backgroundColors.map((bg) => checkColorContrast(colorValue, bg) ); }); return results; } /** * Generate accessibility report for a color palette */ export function generateAccessibilityReport( colors: Record<string, string>, backgroundColors: string[] = ['#FFFFFF', '#000000'] ): string { const results = validateThemeAccessibility(colors, backgroundColors); let report = '# Color Accessibility Report\n\n'; Object.entries(results).forEach(([colorName, contrastResults]) => { report += `## ${colorName}: ${colors[colorName]}\n`; contrastResults.forEach((result, index) => { const bgColor = backgroundColors[index]; const status = result.level === 'fail' ? '❌' : result.level === 'aa' ? '✅' : '🌟'; report += `- vs ${bgColor}: ${status} ${result.ratio}:1 (${result.level.toUpperCase()})\n`; }); report += '\n'; }); return report; } /** * Suggest better color alternatives for failed contrast ratios */ export function suggestBetterColors( foreground: string, background: string, targetRatio: number = 4.5 ): { lighter: string; darker: string } { const fgRgb = hexToRgb(foreground); if (!fgRgb) throw new Error('Invalid foreground color'); // Simple approach: adjust lightness const lighterRgb = { r: Math.min(255, Math.round(fgRgb.r * 1.2)), g: Math.min(255, Math.round(fgRgb.g * 1.2)), b: Math.min(255, Math.round(fgRgb.b * 1.2)), }; const darkerRgb = { r: Math.max(0, Math.round(fgRgb.r * 0.8)), g: Math.max(0, Math.round(fgRgb.g * 0.8)), b: Math.max(0, Math.round(fgRgb.b * 0.8)), }; const rgbToHex = (r: number, g: number, b: number) => `#${[r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('')}`; return { lighter: rgbToHex(lighterRgb.r, lighterRgb.g, lighterRgb.b), darker: rgbToHex(darkerRgb.r, darkerRgb.g, darkerRgb.b), }; }