""" Color Contrast Compliance System This module ensures WCAG color contrast compliance, provides high contrast modes, and validates color accessibility across the interface. """ from typing import Dict, List, Any, Optional, Tuple from flask import Blueprint, request, jsonify import colorsys class ColorContrastManager: """Manages color contrast compliance and accessibility.""" def __init__(self, app=None): self.app = app self.wcag_ratios = { 'AA': {'normal': 4.5, 'large': 3.0}, 'AAA': {'normal': 7.0, 'large': 4.5} } self.color_palette = {} def init_app(self, app): """Initialize with Flask app.""" self.app = app def calculate_contrast_ratio(self, color1: str, color2: str) -> float: """Calculate contrast ratio between two colors.""" # Convert colors to RGB rgb1 = self.hex_to_rgb(color1) rgb2 = self.hex_to_rgb(color2) # Calculate relative luminance lum1 = self.relative_luminance(rgb1) lum2 = self.relative_luminance(rgb2) # Calculate contrast ratio lighter = max(lum1, lum2) darker = min(lum1, lum2) return (lighter + 0.05) / (darker + 0.05) def hex_to_rgb(self, hex_color: str) -> Tuple[int, int, int]: """Convert hex color to RGB.""" hex_color = hex_color.lstrip('#') return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) def relative_luminance(self, rgb: Tuple[int, int, int]) -> float: """Calculate relative luminance of RGB color.""" def gamma_correct(channel): channel = channel / 255.0 return channel / 12.92 if channel <= 0.03928 else pow((channel + 0.055) / 1.055, 2.4) r, g, b = rgb return 0.2126 * gamma_correct(r) + 0.7152 * gamma_correct(g) + 0.0722 * gamma_correct(b) def get_contrast_css(self): """Generate CSS for color contrast compliance.""" return """ /* WCAG Color Contrast Compliance Styles */ /* Base color variables with WCAG AA compliant ratios */ :root { /* Primary colors - AA compliant */ --color-primary: #0066cc; --color-primary-contrast: #ffffff; --color-primary-hover: #004d99; --color-primary-light: #e6f2ff; /* Secondary colors - AA compliant */ --color-secondary: #6c757d; --color-secondary-contrast: #ffffff; --color-secondary-hover: #545b62; --color-secondary-light: #f8f9fa; /* Status colors - AA compliant */ --color-success: #28a745; --color-success-contrast: #ffffff; --color-success-light: #d4edda; --color-danger: #dc3545; --color-danger-contrast: #ffffff; --color-danger-light: #f8d7da; --color-warning: #ffc107; --color-warning-contrast: #000000; --color-warning-light: #fff3cd; --color-info: #17a2b8; --color-info-contrast: #ffffff; --color-info-light: #d1ecf1; /* Background colors */ --color-bg-primary: #ffffff; --color-bg-secondary: #f8f9fa; --color-bg-tertiary: #e9ecef; /* Text colors */ --color-text-primary: #212529; --color-text-secondary: #6c757d; --color-text-tertiary: #495057; --color-text-muted: #868e96; /* Border colors */ --color-border: #dee2e6; --color-border-dark: #6c757d; /* Link colors */ --color-link: #0066cc; --color-link-hover: #004d99; --color-link-visited: #551a8b; /* Focus colors */ --color-focus: #0066cc; --color-focus-shadow: rgba(0, 102, 204, 0.25); } /* Dark theme with AAA compliance where possible */ [data-bs-theme="dark"] { --color-primary: #4dabf7; --color-primary-contrast: #000000; --color-primary-hover: #339af0; --color-primary-light: #0c3653; --color-secondary: #adb5bd; --color-secondary-contrast: #000000; --color-secondary-hover: #95a3b0; --color-secondary-light: #343a40; --color-success: #51cf66; --color-success-contrast: #000000; --color-success-light: #0f3f1f; --color-danger: #ff6b6b; --color-danger-contrast: #000000; --color-danger-light: #4a1a1a; --color-warning: #ffd43b; --color-warning-contrast: #000000; --color-warning-light: #4a3a00; --color-info: #74c0fc; --color-info-contrast: #000000; --color-info-light: #0f2b3c; --color-bg-primary: #212529; --color-bg-secondary: #343a40; --color-bg-tertiary: #495057; --color-text-primary: #ffffff; --color-text-secondary: #adb5bd; --color-text-tertiary: #ced4da; --color-text-muted: #6c757d; --color-border: #495057; --color-border-dark: #343a40; --color-link: #4dabf7; --color-link-hover: #339af0; --color-link-visited: #9775fa; --color-focus: #4dabf7; --color-focus-shadow: rgba(77, 171, 247, 0.25); } /* High contrast mode - AAA compliance */ .high-contrast-mode { --color-primary: #000000; --color-primary-contrast: #ffffff; --color-primary-hover: #333333; --color-primary-light: #f0f0f0; --color-secondary: #000000; --color-secondary-contrast: #ffffff; --color-secondary-hover: #333333; --color-secondary-light: #f0f0f0; --color-success: #000000; --color-success-contrast: #ffffff; --color-success-light: #e6ffe6; --color-danger: #ffffff; --color-danger-contrast: #000000; --color-danger-light: #ffe6e6; --color-warning: #000000; --color-warning-contrast: #ffff00; --color-warning-light: #ffffcc; --color-info: #000000; --color-info-contrast: #ffffff; --color-info-light: #e6f7ff; --color-bg-primary: #ffffff; --color-bg-secondary: #f0f0f0; --color-bg-tertiary: #e0e0e0; --color-text-primary: #000000; --color-text-secondary: #000000; --color-text-tertiary: #000000; --color-text-muted: #666666; --color-border: #000000; --color-border-dark: #000000; --color-link: #0000ee; --color-link-hover: #000080; --color-link-visited: #800080; --color-focus: #ffff00; --color-focus-shadow: rgba(255, 255, 0, 0.5); } /* Apply WCAG compliant colors */ body { background-color: var(--color-bg-primary); color: var(--color-text-primary); } /* Button contrast compliance */ .btn-primary { background-color: var(--color-primary); border-color: var(--color-primary); color: var(--color-primary-contrast); } .btn-primary:hover, .btn-primary:focus { background-color: var(--color-primary-hover); border-color: var(--color-primary-hover); color: var(--color-primary-contrast); } .btn-secondary { background-color: var(--color-secondary); border-color: var(--color-secondary); color: var(--color-secondary-contrast); } .btn-secondary:hover, .btn-secondary:focus { background-color: var(--color-secondary-hover); border-color: var(--color-secondary-hover); color: var(--color-secondary-contrast); } /* Status button colors */ .btn-success { background-color: var(--color-success); border-color: var(--color-success); color: var(--color-success-contrast); } .btn-danger { background-color: var(--color-danger); border-color: var(--color-danger); color: var(--color-danger-contrast); } .btn-warning { background-color: var(--color-warning); border-color: var(--color-warning); color: var(--color-warning-contrast); } .btn-info { background-color: var(--color-info); border-color: var(--color-info); color: var(--color-info-contrast); } /* Link contrast compliance */ a { color: var(--color-link); } a:hover, a:focus { color: var(--color-link-hover); } a:visited { color: var(--color-link-visited); } /* Form element contrast */ .form-control { background-color: var(--color-bg-primary); border-color: var(--color-border); color: var(--color-text-primary); } .form-control:focus { background-color: var(--color-bg-primary); border-color: var(--color-focus); color: var(--color-text-primary); box-shadow: 0 0 0 0.25rem var(--color-focus-shadow); } .form-control::placeholder { color: var(--color-text-muted); } /* Alert contrast compliance */ .alert { border-left: 4px solid currentColor; } .alert-success { background-color: var(--color-success-light); border-color: var(--color-success); color: var(--color-text-primary); } .alert-danger { background-color: var(--color-danger-light); border-color: var(--color-danger); color: var(--color-text-primary); } .alert-warning { background-color: var(--color-warning-light); border-color: var(--color-warning); color: var(--color-text-primary); } .alert-info { background-color: var(--color-info-light); border-color: var(--color-info); color: var(--color-text-primary); } /* Card contrast */ .card { background-color: var(--color-bg-primary); border-color: var(--color-border); color: var(--color-text-primary); } .card-header { background-color: var(--color-bg-secondary); border-bottom-color: var(--color-border); } /* Table contrast */ .table { color: var(--color-text-primary); } .table th { background-color: var(--color-bg-secondary); border-color: var(--color-border); } .table td { border-color: var(--color-border); } .table-striped tbody tr:nth-of-type(odd) { background-color: var(--color-bg-secondary); } /* Navigation contrast */ .navbar { background-color: var(--color-bg-primary); border-color: var(--color-border); } .navbar .nav-link { color: var(--color-text-secondary); } .navbar .nav-link:hover, .navbar .nav-link:focus { color: var(--color-text-primary); } .navbar .nav-link.active { color: var(--color-primary); } /* Dropdown contrast */ .dropdown-menu { background-color: var(--color-bg-primary); border-color: var(--color-border); } .dropdown-item { color: var(--color-text-primary); } .dropdown-item:hover, .dropdown-item:focus { background-color: var(--color-primary-light); color: var(--color-text-primary); } /* Progress bar contrast */ .progress { background-color: var(--color-bg-secondary); } .progress-bar { background-color: var(--color-primary); color: var(--color-primary-contrast); } /* Badge contrast */ .badge { color: var(--color-primary-contrast); } .badge-primary { background-color: var(--color-primary); } .badge-secondary { background-color: var(--color-secondary); } .badge-success { background-color: var(--color-success); } .badge-danger { background-color: var(--color-danger); } .badge-warning { background-color: var(--color-warning); color: var(--color-warning-contrast); } .badge-info { background-color: var(--color-info); } /* Series card contrast */ .series-card { background-color: var(--color-bg-primary); border-color: var(--color-border); color: var(--color-text-primary); } .series-card:hover { border-color: var(--color-border-dark); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .series-card .series-title { color: var(--color-text-primary); } .series-card .series-meta { color: var(--color-text-secondary); } /* Focus indicators with proper contrast */ :focus { outline: 2px solid var(--color-focus); outline-offset: 2px; } :focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; box-shadow: 0 0 0 4px var(--color-focus-shadow); } /* Modal contrast */ .modal-content { background-color: var(--color-bg-primary); border-color: var(--color-border); color: var(--color-text-primary); } .modal-header { border-bottom-color: var(--color-border); } .modal-footer { border-top-color: var(--color-border); } /* Tooltip contrast */ .tooltip .tooltip-inner { background-color: var(--color-text-primary); color: var(--color-bg-primary); } /* Breadcrumb contrast */ .breadcrumb { background-color: var(--color-bg-secondary); } .breadcrumb-item { color: var(--color-text-secondary); } .breadcrumb-item.active { color: var(--color-text-primary); } .breadcrumb-item a { color: var(--color-link); } .breadcrumb-item a:hover { color: var(--color-link-hover); } /* Pagination contrast */ .pagination .page-link { background-color: var(--color-bg-primary); border-color: var(--color-border); color: var(--color-link); } .pagination .page-link:hover { background-color: var(--color-bg-secondary); border-color: var(--color-border-dark); color: var(--color-link-hover); } .pagination .page-item.active .page-link { background-color: var(--color-primary); border-color: var(--color-primary); color: var(--color-primary-contrast); } .pagination .page-item.disabled .page-link { background-color: var(--color-bg-secondary); border-color: var(--color-border); color: var(--color-text-muted); } /* List group contrast */ .list-group-item { background-color: var(--color-bg-primary); border-color: var(--color-border); color: var(--color-text-primary); } .list-group-item:hover { background-color: var(--color-bg-secondary); } .list-group-item.active { background-color: var(--color-primary); border-color: var(--color-primary); color: var(--color-primary-contrast); } /* Spinner contrast */ .spinner-border { border-color: var(--color-border); border-left-color: var(--color-primary); } /* Text utilities with proper contrast */ .text-primary { color: var(--color-primary) !important; } .text-secondary { color: var(--color-text-secondary) !important; } .text-success { color: var(--color-success) !important; } .text-danger { color: var(--color-danger) !important; } .text-warning { color: var(--color-warning-contrast) !important; } .text-info { color: var(--color-info) !important; } .text-muted { color: var(--color-text-muted) !important; } /* Background utilities with proper contrast */ .bg-primary { background-color: var(--color-primary) !important; color: var(--color-primary-contrast) !important; } .bg-secondary { background-color: var(--color-secondary) !important; color: var(--color-secondary-contrast) !important; } .bg-success { background-color: var(--color-success) !important; color: var(--color-success-contrast) !important; } .bg-danger { background-color: var(--color-danger) !important; color: var(--color-danger-contrast) !important; } .bg-warning { background-color: var(--color-warning) !important; color: var(--color-warning-contrast) !important; } .bg-info { background-color: var(--color-info) !important; color: var(--color-info-contrast) !important; } /* Selection highlight with proper contrast */ ::selection { background-color: var(--color-primary); color: var(--color-primary-contrast); } ::-moz-selection { background-color: var(--color-primary); color: var(--color-primary-contrast); } /* Scrollbar contrast (webkit) */ ::-webkit-scrollbar { background-color: var(--color-bg-secondary); } ::-webkit-scrollbar-thumb { background-color: var(--color-border-dark); } ::-webkit-scrollbar-thumb:hover { background-color: var(--color-text-muted); } /* High contrast mode enhancements */ .high-contrast-mode .btn { border-width: 2px; font-weight: bold; } .high-contrast-mode .form-control { border-width: 2px; } .high-contrast-mode .card { border-width: 2px; } .high-contrast-mode .series-card { border-width: 2px; } .high-contrast-mode :focus { outline-width: 3px; outline-color: var(--color-focus); box-shadow: 0 0 0 5px var(--color-focus-shadow); } /* Media queries for contrast preferences */ @media (prefers-contrast: high) { :root { --color-focus-shadow: rgba(0, 102, 204, 0.5); } .btn { border-width: 2px; } .form-control { border-width: 2px; } :focus { outline-width: 3px; box-shadow: 0 0 0 5px var(--color-focus-shadow); } } @media (prefers-contrast: more) { body { --color-text-primary: #000000; --color-bg-primary: #ffffff; --color-border: #000000; } [data-bs-theme="dark"] { --color-text-primary: #ffffff; --color-bg-primary: #000000; --color-border: #ffffff; } } /* Force colors mode support (Windows High Contrast) */ @media (forced-colors: active) { .btn { forced-color-adjust: none; border: 2px solid ButtonBorder; background: ButtonFace; color: ButtonText; } .btn:hover, .btn:focus { background: Highlight; color: HighlightText; border-color: HighlightText; } .form-control { forced-color-adjust: none; border: 2px solid FieldText; background: Field; color: FieldText; } .form-control:focus { outline: 2px solid Highlight; outline-offset: 2px; } .series-card { forced-color-adjust: none; border: 2px solid CanvasText; background: Canvas; color: CanvasText; } .series-card:focus { outline: 2px solid Highlight; outline-offset: 2px; } } /* Print contrast optimization */ @media print { :root { --color-text-primary: #000000; --color-bg-primary: #ffffff; --color-border: #000000; --color-link: #000080; } .btn { border: 2px solid #000000; background: #ffffff; color: #000000; } .series-card { border: 1px solid #000000; } } /* Reduced transparency for better contrast */ .contrast-enhanced .modal-backdrop { opacity: 0.8; } .contrast-enhanced .dropdown-menu { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } .contrast-enhanced .card { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); } /* Color blind friendly alternatives */ .colorblind-friendly .text-success { text-decoration: underline; } .colorblind-friendly .text-danger { font-weight: bold; text-decoration: underline; } .colorblind-friendly .badge-success::before { content: "✓ "; } .colorblind-friendly .badge-danger::before { content: "✗ "; } .colorblind-friendly .badge-warning::before { content: "⚠ "; } /* Ensure minimum 3:1 ratio for large text */ h1, h2, h3, .h1, .h2, .h3, .btn-lg, .display-1, .display-2, .display-3 { /* These elements use large text AA standard (3:1 ratio) */ } /* Ensure minimum 4.5:1 ratio for normal text */ p, .btn, .form-control, .card-text, h4, h5, h6, .h4, .h5, .h6 { /* These elements use normal text AA standard (4.5:1 ratio) */ } """ def get_contrast_js(self): """Generate JavaScript for contrast management.""" return """ // AniWorld Color Contrast Manager class ColorContrastManager { constructor() { this.contrastLevel = 'AA'; // AA or AAA this.highContrastMode = false; this.colorBlindMode = false; this.customColors = {}; this.wcagRatios = { 'AA': { normal: 4.5, large: 3.0 }, 'AAA': { normal: 7.0, large: 4.5 } }; this.init(); } init() { this.detectContrastPreferences(); this.setupContrastControls(); this.validatePageContrast(); this.setupContrastMonitoring(); console.log('Color contrast manager initialized'); } detectContrastPreferences() { // Detect system contrast preferences if (window.matchMedia('(prefers-contrast: high)').matches) { this.enableHighContrast(); } if (window.matchMedia('(prefers-contrast: more)').matches) { this.contrastLevel = 'AAA'; } // Listen for changes window.matchMedia('(prefers-contrast: high)').addEventListener('change', (e) => { if (e.matches) { this.enableHighContrast(); } else { this.disableHighContrast(); } }); // Check for forced colors (Windows High Contrast) if (window.matchMedia('(forced-colors: active)').matches) { this.enableForcedColors(); } } setupContrastControls() { // Create contrast control panel this.createContrastPanel(); // Add keyboard shortcuts this.setupKeyboardShortcuts(); } createContrastPanel() { const panel = document.createElement('div'); panel.id = 'contrast-panel'; panel.className = 'contrast-panel'; panel.innerHTML = `
Contrast Options
`; document.body.appendChild(panel); this.setupPanelEvents(panel); } setupPanelEvents(panel) { // Toggle panel visibility const toggle = panel.querySelector('.panel-toggle'); const content = panel.querySelector('.panel-content'); toggle.addEventListener('click', () => { const isOpen = content.style.display !== 'none'; content.style.display = isOpen ? 'none' : 'block'; toggle.setAttribute('aria-expanded', (!isOpen).toString()); }); // High contrast toggle const highContrastCheck = panel.querySelector('#high-contrast'); highContrastCheck.addEventListener('change', (e) => { if (e.target.checked) { this.enableHighContrast(); } else { this.disableHighContrast(); } }); // Color blind mode toggle const colorBlindCheck = panel.querySelector('#colorblind-mode'); colorBlindCheck.addEventListener('change', (e) => { if (e.target.checked) { this.enableColorBlindMode(); } else { this.disableColorBlindMode(); } }); // Contrast level change const levelSelect = panel.querySelector('#contrast-level'); levelSelect.addEventListener('change', (e) => { this.contrastLevel = e.target.value; this.validatePageContrast(); }); // Check contrast button const checkButton = panel.querySelector('#check-contrast'); checkButton.addEventListener('click', () => { this.validatePageContrast(); }); } setupKeyboardShortcuts() { document.addEventListener('keydown', (e) => { // Ctrl+Shift+C: Toggle high contrast if (e.ctrlKey && e.shiftKey && e.key === 'C') { e.preventDefault(); this.toggleHighContrast(); } // Ctrl+Shift+B: Toggle color blind mode if (e.ctrlKey && e.shiftKey && e.key === 'B') { e.preventDefault(); this.toggleColorBlindMode(); } // Ctrl+Shift+V: Validate contrast if (e.ctrlKey && e.shiftKey && e.key === 'V') { e.preventDefault(); this.validatePageContrast(); } }); } enableHighContrast() { this.highContrastMode = true; document.body.classList.add('high-contrast-mode', 'contrast-enhanced'); // Update checkbox state const checkbox = document.querySelector('#high-contrast'); if (checkbox) { checkbox.checked = true; } this.announceChange('High contrast mode enabled'); } disableHighContrast() { this.highContrastMode = false; document.body.classList.remove('high-contrast-mode', 'contrast-enhanced'); // Update checkbox state const checkbox = document.querySelector('#high-contrast'); if (checkbox) { checkbox.checked = false; } this.announceChange('High contrast mode disabled'); } toggleHighContrast() { if (this.highContrastMode) { this.disableHighContrast(); } else { this.enableHighContrast(); } } enableColorBlindMode() { this.colorBlindMode = true; document.body.classList.add('colorblind-friendly'); // Add symbols to status indicators this.addColorBlindSymbols(); const checkbox = document.querySelector('#colorblind-mode'); if (checkbox) { checkbox.checked = true; } this.announceChange('Color blind friendly mode enabled'); } disableColorBlindMode() { this.colorBlindMode = false; document.body.classList.remove('colorblind-friendly'); // Remove symbols this.removeColorBlindSymbols(); const checkbox = document.querySelector('#colorblind-mode'); if (checkbox) { checkbox.checked = false; } this.announceChange('Color blind friendly mode disabled'); } toggleColorBlindMode() { if (this.colorBlindMode) { this.disableColorBlindMode(); } else { this.enableColorBlindMode(); } } addColorBlindSymbols() { // Add symbols to elements that rely on color document.querySelectorAll('.text-success, .bg-success, .btn-success').forEach(el => { if (!el.querySelector('.cb-symbol')) { const symbol = document.createElement('span'); symbol.className = 'cb-symbol'; symbol.textContent = '✓ '; symbol.setAttribute('aria-hidden', 'true'); el.insertBefore(symbol, el.firstChild); } }); document.querySelectorAll('.text-danger, .bg-danger, .btn-danger').forEach(el => { if (!el.querySelector('.cb-symbol')) { const symbol = document.createElement('span'); symbol.className = 'cb-symbol'; symbol.textContent = '✗ '; symbol.setAttribute('aria-hidden', 'true'); el.insertBefore(symbol, el.firstChild); } }); document.querySelectorAll('.text-warning, .bg-warning, .btn-warning').forEach(el => { if (!el.querySelector('.cb-symbol')) { const symbol = document.createElement('span'); symbol.className = 'cb-symbol'; symbol.textContent = '⚠ '; symbol.setAttribute('aria-hidden', 'true'); el.insertBefore(symbol, el.firstChild); } }); } removeColorBlindSymbols() { document.querySelectorAll('.cb-symbol').forEach(symbol => { symbol.remove(); }); } enableForcedColors() { document.body.classList.add('forced-colors-mode'); this.announceChange('System high contrast mode detected'); } validatePageContrast() { const issues = []; const elements = this.getTextElements(); elements.forEach(element => { const contrast = this.analyzeElementContrast(element); if (contrast.ratio < contrast.required) { issues.push({ element: element, ratio: contrast.ratio, required: contrast.required, colors: contrast.colors }); } }); this.reportContrastIssues(issues); return issues; } getTextElements() { // Get all elements with text content const selector = 'p, span, div, a, button, h1, h2, h3, h4, h5, h6, .btn, .form-control, .card-text, .nav-link, .dropdown-item'; return Array.from(document.querySelectorAll(selector)).filter(el => { return el.textContent.trim().length > 0 && this.isVisible(el) && !el.closest('.sr-only, .visually-hidden'); }); } isVisible(element) { const style = window.getComputedStyle(element); return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && element.offsetWidth > 0 && element.offsetHeight > 0; } analyzeElementContrast(element) { const style = window.getComputedStyle(element); const textColor = this.rgbToHex(style.color); const backgroundColor = this.getBackgroundColor(element); const ratio = this.calculateContrastRatio(textColor, backgroundColor); const fontSize = parseFloat(style.fontSize); const fontWeight = style.fontWeight; const isLargeText = fontSize >= 18 || (fontSize >= 14 && (fontWeight === 'bold' || fontWeight >= 700)); const requiredRatio = this.wcagRatios[this.contrastLevel][isLargeText ? 'large' : 'normal']; return { ratio: ratio, required: requiredRatio, colors: { text: textColor, background: backgroundColor }, isLargeText: isLargeText, passes: ratio >= requiredRatio }; } getBackgroundColor(element) { let bgColor = 'rgba(0, 0, 0, 0)'; let currentElement = element; while (currentElement && currentElement !== document.body) { const style = window.getComputedStyle(currentElement); const currentBg = style.backgroundColor; if (currentBg && currentBg !== 'rgba(0, 0, 0, 0)' && currentBg !== 'transparent') { bgColor = currentBg; break; } currentElement = currentElement.parentElement; } // Default to white if no background found if (bgColor === 'rgba(0, 0, 0, 0)') { bgColor = 'rgb(255, 255, 255)'; } return this.rgbToHex(bgColor); } rgbToHex(rgb) { // Handle different RGB formats if (rgb.startsWith('#')) { return rgb; } const match = rgb.match(/rgb\\((\\d+),\\s*(\\d+),\\s*(\\d+)\\)/); if (match) { const r = parseInt(match[1]); const g = parseInt(match[2]); const b = parseInt(match[3]); return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; } return '#000000'; // fallback } calculateContrastRatio(color1, color2) { const lum1 = this.getLuminance(color1); const lum2 = this.getLuminance(color2); const brightest = Math.max(lum1, lum2); const darkest = Math.min(lum1, lum2); return (brightest + 0.05) / (darkest + 0.05); } getLuminance(color) { const rgb = this.hexToRgb(color); const [r, g, b] = rgb.map(c => { c = c / 255; return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); }); return 0.2126 * r + 0.7152 * g + 0.0722 * b; } hexToRgb(hex) { const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex); return result ? [ parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16) ] : [0, 0, 0]; } reportContrastIssues(issues) { // Remove existing contrast warnings document.querySelectorAll('.contrast-warning').forEach(warning => { warning.remove(); }); if (issues.length === 0) { this.showContrastReport('All contrast ratios meet WCAG ' + this.contrastLevel + ' standards.', 'success'); return; } // Add warnings to problematic elements issues.forEach((issue, index) => { this.addContrastWarning(issue.element, issue, index); }); this.showContrastReport(`Found ${issues.length} contrast issues.`, 'warning'); } addContrastWarning(element, issue, index) { const warning = document.createElement('div'); warning.className = 'contrast-warning'; warning.innerHTML = ` Contrast warning: Ratio ${issue.ratio.toFixed(2)}:1 (needs ${issue.required}:1) `; warning.style.cssText = ` position: absolute; top: -25px; left: 0; background: #ff6b6b; color: white; padding: 2px 6px; font-size: 11px; border-radius: 3px; z-index: 1000; pointer-events: none; `; // Position relative to element element.style.position = element.style.position || 'relative'; element.appendChild(warning); // Remove after 5 seconds setTimeout(() => { warning.remove(); }, 5000); } showContrastReport(message, type) { const existingReport = document.querySelector('.contrast-report'); if (existingReport) { existingReport.remove(); } const report = document.createElement('div'); report.className = `contrast-report alert alert-${type}`; report.innerHTML = `
${message}
`; report.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 1060; min-width: 300px; `; document.body.appendChild(report); // Auto-remove and close button const closeBtn = report.querySelector('.btn-close'); closeBtn.addEventListener('click', () => report.remove()); setTimeout(() => { if (document.contains(report)) { report.remove(); } }, 8000); } setupContrastMonitoring() { // Monitor for dynamic content changes const observer = new MutationObserver((mutations) => { mutations.forEach(mutation => { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { this.checkNewElement(node); } }); } }); }); observer.observe(document.body, { childList: true, subtree: true }); } checkNewElement(element) { // Check contrast for newly added elements if (element.textContent && element.textContent.trim()) { const contrast = this.analyzeElementContrast(element); if (!contrast.passes) { console.warn('New element added with poor contrast:', element, contrast); } } } announceChange(message) { // Announce changes to screen readers if (window.accessibilityManager) { window.accessibilityManager.announce(message, 'polite'); } } // Public API methods getContrastLevel() { return this.contrastLevel; } setContrastLevel(level) { if (['AA', 'AAA'].includes(level)) { this.contrastLevel = level; this.validatePageContrast(); } } isHighContrastMode() { return this.highContrastMode; } isColorBlindMode() { return this.colorBlindMode; } checkElementContrast(element) { return this.analyzeElementContrast(element); } getPageContrastIssues() { return this.validatePageContrast(); } } // Initialize color contrast manager when DOM is loaded document.addEventListener('DOMContentLoaded', () => { window.colorContrastManager = new ColorContrastManager(); console.log('Color contrast manager loaded'); }); """ def get_contrast_api_blueprint(self): """Create Flask blueprint for contrast API endpoints.""" bp = Blueprint('contrast', __name__, url_prefix='/api/contrast') @bp.route('/check', methods=['POST']) def check_contrast(): """Check contrast ratio between two colors.""" data = request.get_json() color1 = data.get('color1') color2 = data.get('color2') if not color1 or not color2: return jsonify({'error': 'Both color1 and color2 are required'}), 400 try: ratio = self.calculate_contrast_ratio(color1, color2) aa_normal = ratio >= self.wcag_ratios['AA']['normal'] aa_large = ratio >= self.wcag_ratios['AA']['large'] aaa_normal = ratio >= self.wcag_ratios['AAA']['normal'] aaa_large = ratio >= self.wcag_ratios['AAA']['large'] return jsonify({ 'ratio': round(ratio, 2), 'passes': { 'AA': {'normal': aa_normal, 'large': aa_large}, 'AAA': {'normal': aaa_normal, 'large': aaa_large} } }) except Exception as e: return jsonify({'error': str(e)}), 400 @bp.route('/palette', methods=['GET']) def get_color_palette(): """Get WCAG compliant color palette.""" return jsonify(self.color_palette) return bp # Export the color contrast manager color_contrast_manager = ColorContrastManager()