| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
function typoCssVarsForEl(typo, prefix, el) { |
| 5 |
if (!typo) return; |
| 6 |
if (typo.family) el.style.setProperty(prefix + 'font-family', "'" + typo.family + "', sans-serif"); |
| 7 |
if (typo.weight) el.style.setProperty(prefix + 'font-weight', typo.weight); |
| 8 |
if (typo.transform) el.style.setProperty(prefix + 'text-transform', typo.transform); |
| 9 |
if (typo.style) el.style.setProperty(prefix + 'font-style', typo.style); |
| 10 |
if (typo.decoration) el.style.setProperty(prefix + 'text-decoration', typo.decoration); |
| 11 |
var su = typo.sizeUnit || 'px'; |
| 12 |
if (typo.sizeDesktop !== '' && typo.sizeDesktop != null) el.style.setProperty(prefix + 'font-size-d', typo.sizeDesktop + su); |
| 13 |
if (typo.sizeTablet !== '' && typo.sizeTablet != null) el.style.setProperty(prefix + 'font-size-t', typo.sizeTablet + su); |
| 14 |
if (typo.sizeMobile !== '' && typo.sizeMobile != null) el.style.setProperty(prefix + 'font-size-m', typo.sizeMobile + su); |
| 15 |
var lhu = typo.lineHeightUnit || 'px'; |
| 16 |
if (typo.lineHeightDesktop !== '' && typo.lineHeightDesktop != null) el.style.setProperty(prefix + 'line-height-d', typo.lineHeightDesktop + lhu); |
| 17 |
if (typo.lineHeightTablet !== '' && typo.lineHeightTablet != null) el.style.setProperty(prefix + 'line-height-t', typo.lineHeightTablet + lhu); |
| 18 |
if (typo.lineHeightMobile !== '' && typo.lineHeightMobile != null) el.style.setProperty(prefix + 'line-height-m', typo.lineHeightMobile + lhu); |
| 19 |
var lsu = typo.letterSpacingUnit || 'px'; |
| 20 |
if (typo.letterSpacingDesktop !== '' && typo.letterSpacingDesktop != null) { |
| 21 |
el.style.setProperty(prefix + 'letter-spacing-d', typo.letterSpacingDesktop + lsu); |
| 22 |
el.style.setProperty(prefix + 'letter-spacing', typo.letterSpacingDesktop + lsu); |
| 23 |
} |
| 24 |
if (typo.letterSpacingTablet !== '' && typo.letterSpacingTablet != null) el.style.setProperty(prefix + 'letter-spacing-t', typo.letterSpacingTablet + lsu); |
| 25 |
if (typo.letterSpacingMobile !== '' && typo.letterSpacingMobile != null) el.style.setProperty(prefix + 'letter-spacing-m', typo.letterSpacingMobile + lsu); |
| 26 |
var wsu = typo.wordSpacingUnit || 'px'; |
| 27 |
if (typo.wordSpacingDesktop !== '' && typo.wordSpacingDesktop != null) { |
| 28 |
el.style.setProperty(prefix + 'word-spacing-d', typo.wordSpacingDesktop + wsu); |
| 29 |
el.style.setProperty(prefix + 'word-spacing', typo.wordSpacingDesktop + wsu); |
| 30 |
} |
| 31 |
if (typo.wordSpacingTablet !== '' && typo.wordSpacingTablet != null) el.style.setProperty(prefix + 'word-spacing-t', typo.wordSpacingTablet + wsu); |
| 32 |
if (typo.wordSpacingMobile !== '' && typo.wordSpacingMobile != null) el.style.setProperty(prefix + 'word-spacing-m', typo.wordSpacingMobile + wsu); |
| 33 |
} |
| 34 |
|
| 35 |
function hexToRgb(hex) { |
| 36 |
hex = hex.replace('#', ''); |
| 37 |
if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2]; |
| 38 |
var n = parseInt(hex, 16); |
| 39 |
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 }; |
| 40 |
} |
| 41 |
|
| 42 |
function luminance(hex) { |
| 43 |
try { |
| 44 |
var rgb = hexToRgb(hex); |
| 45 |
var vals = [rgb.r, rgb.g, rgb.b].map(function(v) { |
| 46 |
v = v / 255; |
| 47 |
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); |
| 48 |
}); |
| 49 |
return 0.2126 * vals[0] + 0.7152 * vals[1] + 0.0722 * vals[2]; |
| 50 |
} catch(e) { return 0; } |
| 51 |
} |
| 52 |
|
| 53 |
function contrastRatio(fg, bg) { |
| 54 |
var l1 = luminance(fg), l2 = luminance(bg); |
| 55 |
var lighter = Math.max(l1, l2), darker = Math.min(l1, l2); |
| 56 |
return (lighter + 0.05) / (darker + 0.05); |
| 57 |
} |
| 58 |
|
| 59 |
function isValidHex(hex) { |
| 60 |
return /^#([0-9A-Fa-f]{3}){1,2}$/.test(hex); |
| 61 |
} |
| 62 |
|
| 63 |
// Suggestions: darken fg or lighten bg to reach AA |
| 64 |
function suggestColors(fg, bg) { |
| 65 |
var suggestions = []; |
| 66 |
// Try some darker versions of fg |
| 67 |
var fgRgb = hexToRgb(fg); |
| 68 |
var steps = [0.8, 0.6, 0.4, 0.2, 0]; |
| 69 |
steps.forEach(function(factor) { |
| 70 |
var r = Math.round(fgRgb.r * factor); |
| 71 |
var g = Math.round(fgRgb.g * factor); |
| 72 |
var b = Math.round(fgRgb.b * factor); |
| 73 |
var hex = '#' + [r,g,b].map(function(v){ return ('0'+v.toString(16)).slice(-2); }).join(''); |
| 74 |
var ratio = contrastRatio(hex, bg); |
| 75 |
if (ratio >= 4.5) { |
| 76 |
suggestions.push({ hex: hex, ratio: ratio, label: 'Darker Fg' }); |
| 77 |
return; |
| 78 |
} |
| 79 |
}); |
| 80 |
// Classic black/white |
| 81 |
[['#000000','Black'],['#ffffff','White'],['#1e293b','Dark'],['#f8fafc','Light']].forEach(function(pair) { |
| 82 |
var ratio = contrastRatio(pair[0], bg); |
| 83 |
if (ratio >= 4.5) suggestions.push({ hex: pair[0], ratio: ratio, label: pair[1] }); |
| 84 |
}); |
| 85 |
// Deduplicate |
| 86 |
var seen = {}; |
| 87 |
return suggestions.filter(function(s) { |
| 88 |
if (seen[s.hex]) return false; |
| 89 |
seen[s.hex] = true; |
| 90 |
return true; |
| 91 |
}).slice(0, 5); |
| 92 |
} |
| 93 |
|
| 94 |
var WCAG_LEVELS = [ |
| 95 |
{ id: 'aa-normal', label: 'AA', sub: 'Normal text ≥ 4.5:1', min: 4.5 }, |
| 96 |
{ id: 'aa-large', label: 'AA', sub: 'Large text ≥ 3:1', min: 3.0 }, |
| 97 |
{ id: 'aaa-normal', label: 'AAA', sub: 'Normal text ≥ 7:1', min: 7.0 }, |
| 98 |
{ id: 'aaa-large', label: 'AAA', sub: 'Large text ≥ 4.5:1', min: 4.5 } |
| 99 |
]; |
| 100 |
|
| 101 |
document.querySelectorAll('.bkbg-cc-app').forEach(function (root) { |
| 102 |
var opts = {}; |
| 103 |
try { opts = JSON.parse(root.getAttribute('data-opts') || '{}'); } catch(e) {} |
| 104 |
|
| 105 |
/* Apply typography CSS vars on root (survives innerHTML replacements) */ |
| 106 |
typoCssVarsForEl(opts.typoTitle, '--bkcchk-title-', root); |
| 107 |
typoCssVarsForEl(opts.typoSubtitle, '--bkcchk-sub-', root); |
| 108 |
|
| 109 |
var showPreview = opts.showPreview !== false; |
| 110 |
var showSuggestions = opts.showSuggestions !== false; |
| 111 |
var previewText = opts.previewText || 'The quick brown fox jumps over the lazy dog'; |
| 112 |
var passColor = opts.passColor || '#16a34a'; |
| 113 |
var failColor = opts.failColor || '#dc2626'; |
| 114 |
var accentColor = opts.accentColor || '#6c3fb5'; |
| 115 |
var cardBg = opts.cardBg || '#ffffff'; |
| 116 |
var sectionBg = opts.sectionBg || '#f5f3ff'; |
| 117 |
var titleColor = opts.titleColor || '#111827'; |
| 118 |
var subtitleColor = opts.subtitleColor || '#6b7280'; |
| 119 |
var labelColor = opts.labelColor || '#374151'; |
| 120 |
|
| 121 |
var fg = opts.defaultFg || '#1e1b4b'; |
| 122 |
var bg = opts.defaultBg || '#ffffff'; |
| 123 |
|
| 124 |
function gradeColor(ratio) { |
| 125 |
return ratio >= 4.5 ? passColor : ratio >= 3 ? '#d97706' : failColor; |
| 126 |
} |
| 127 |
function gradeLabel(ratio) { |
| 128 |
return ratio >= 4.5 ? 'Excellent' : ratio >= 3 ? 'Good' : ratio >= 2 ? 'Poor' : 'Fail'; |
| 129 |
} |
| 130 |
|
| 131 |
function render() { |
| 132 |
var validFg = isValidHex(fg) ? fg : '#000000'; |
| 133 |
var validBg = isValidHex(bg) ? bg : '#ffffff'; |
| 134 |
var ratio = contrastRatio(validFg, validBg); |
| 135 |
var gc = gradeColor(ratio); |
| 136 |
var sug = showSuggestions && ratio < 4.5 ? suggestColors(validFg, validBg) : []; |
| 137 |
|
| 138 |
root.innerHTML = |
| 139 |
'<div class="bkbg-cc-card" style="background:' + cardBg + ';max-width:' + (opts.contentMaxWidth||680) + 'px;">' + |
| 140 |
'<h2 class="bkbg-cc-title" style="color:' + titleColor + ';">' + (opts.title || 'Color Contrast Checker') + '</h2>' + |
| 141 |
'<p class="bkbg-cc-subtitle" style="color:' + subtitleColor + ';">' + (opts.subtitle || 'Test accessibility compliance with WCAG 2.1 standards') + '</p>' + |
| 142 |
|
| 143 |
'<div class="bkbg-cc-pickers" style="background:' + sectionBg + ';">' + |
| 144 |
'<div class="bkbg-cc-picker-group">' + |
| 145 |
'<label class="bkbg-cc-label" style="color:' + labelColor + ';">� |
| 146 |
� Foreground Color</label>' + |
| 147 |
'<div class="bkbg-cc-color-row">' + |
| 148 |
'<input type="color" class="bkbg-cc-color-input" id="cc-fg" value="' + validFg + '">' + |
| 149 |
'<input type="text" class="bkbg-cc-hex-input" id="cc-fg-text" value="' + validFg.toUpperCase() + '" maxlength="7" style="color:' + labelColor + ';font-weight:700;font-size:14px;border:1.5px solid #e5e7eb;border-radius:8px;padding:8px 10px;width:100px;font-family:monospace;background:' + cardBg + ';">' + |
| 150 |
'</div>' + |
| 151 |
'</div>' + |
| 152 |
'<button class="bkbg-cc-swap" id="cc-swap" style="background:' + accentColor + ';" title="Swap colors">⇆</button>' + |
| 153 |
'<div class="bkbg-cc-picker-group">' + |
| 154 |
'<label class="bkbg-cc-label" style="color:' + labelColor + ';">◻ Background Color</label>' + |
| 155 |
'<div class="bkbg-cc-color-row">' + |
| 156 |
'<input type="color" class="bkbg-cc-color-input" id="cc-bg" value="' + validBg + '">' + |
| 157 |
'<input type="text" class="bkbg-cc-hex-input" id="cc-bg-text" value="' + validBg.toUpperCase() + '" maxlength="7" style="color:' + labelColor + ';font-weight:700;font-size:14px;border:1.5px solid #e5e7eb;border-radius:8px;padding:8px 10px;width:100px;font-family:monospace;background:' + cardBg + ';">' + |
| 158 |
'</div>' + |
| 159 |
'</div>' + |
| 160 |
'</div>' + |
| 161 |
|
| 162 |
(showPreview ? |
| 163 |
'<div class="bkbg-cc-preview" style="background:' + validBg + ';">' + |
| 164 |
'<p class="bkbg-cc-preview-normal" style="color:' + validFg + ';">' + previewText + ' — Normal (16px)</p>' + |
| 165 |
'<p class="bkbg-cc-preview-large" style="color:' + validFg + ';">' + previewText + ' — Large (24px)</p>' + |
| 166 |
'<p class="bkbg-cc-preview-bold" style="color:' + validFg + ';">' + previewText + ' — Bold (14px)</p>' + |
| 167 |
'</div>' : '') + |
| 168 |
|
| 169 |
'<div class="bkbg-cc-ratio-box" style="border-color:' + gc + '44;background:' + gc + '10;">' + |
| 170 |
'<div class="bkbg-cc-ratio-val" style="color:' + gc + ';">' + ratio.toFixed(2) + ':1</div>' + |
| 171 |
'<div class="bkbg-cc-ratio-lbl" style="color:' + labelColor + ';">Contrast Ratio</div>' + |
| 172 |
'<div class="bkbg-cc-ratio-grade" style="background:' + gc + ';color:#fff;">' + gradeLabel(ratio) + '</div>' + |
| 173 |
'</div>' + |
| 174 |
|
| 175 |
'<div class="bkbg-cc-wcag-grid">' + |
| 176 |
WCAG_LEVELS.map(function(level) { |
| 177 |
var pass = ratio >= level.min; |
| 178 |
var col = pass ? passColor : failColor; |
| 179 |
return '<div class="bkbg-cc-wcag-row" style="border-color:' + col + '33;background:' + col + '09;">' + |
| 180 |
'<div class="bkbg-cc-wcag-badge" style="background:' + col + ';color:#fff;">' + level.label + '</div>' + |
| 181 |
'<div class="bkbg-cc-wcag-info">' + |
| 182 |
'<div class="bkbg-cc-wcag-sub" style="color:' + labelColor + ';">' + level.sub + '</div>' + |
| 183 |
'</div>' + |
| 184 |
'<div class="bkbg-cc-wcag-status" style="color:' + col + ';">' + (pass ? '✓ Pass' : '✗ Fail') + '</div>' + |
| 185 |
'</div>'; |
| 186 |
}).join('') + |
| 187 |
'</div>' + |
| 188 |
|
| 189 |
(sug.length > 0 ? |
| 190 |
'<div class="bkbg-cc-suggestions">' + |
| 191 |
'<div class="bkbg-cc-suggestions-title" style="color:' + labelColor + ';">💡 Suggested foreground colors to reach AA:</div>' + |
| 192 |
'<div class="bkbg-cc-sug-chips">' + |
| 193 |
sug.map(function(s) { |
| 194 |
return '<button class="bkbg-cc-sug-chip" data-sugfg="' + s.hex + '" style="background:' + cardBg + ';color:' + labelColor + ';">' + |
| 195 |
'<span class="bkbg-cc-sug-swatch" style="background:' + s.hex + ';"></span>' + |
| 196 |
s.hex.toUpperCase() + ' (' + s.ratio.toFixed(1) + ':1)' + |
| 197 |
'</button>'; |
| 198 |
}).join('') + |
| 199 |
'</div>' + |
| 200 |
'</div>' : '') + |
| 201 |
'</div>'; |
| 202 |
|
| 203 |
bindEvents(); |
| 204 |
} |
| 205 |
|
| 206 |
function update(newFg, newBg) { |
| 207 |
fg = newFg || fg; |
| 208 |
bg = newBg || bg; |
| 209 |
render(); |
| 210 |
} |
| 211 |
|
| 212 |
function bindEvents() { |
| 213 |
var fgPicker = root.querySelector('#cc-fg'); |
| 214 |
var bgPicker = root.querySelector('#cc-bg'); |
| 215 |
var fgText = root.querySelector('#cc-fg-text'); |
| 216 |
var bgText = root.querySelector('#cc-bg-text'); |
| 217 |
var swapBtn = root.querySelector('#cc-swap'); |
| 218 |
|
| 219 |
if (fgPicker) fgPicker.addEventListener('input', function() { |
| 220 |
fg = this.value; if (fgText) fgText.value = fg.toUpperCase(); render(); |
| 221 |
}); |
| 222 |
if (bgPicker) bgPicker.addEventListener('input', function() { |
| 223 |
bg = this.value; if (bgText) bgText.value = bg.toUpperCase(); render(); |
| 224 |
}); |
| 225 |
if (fgText) fgText.addEventListener('input', function() { |
| 226 |
var v = this.value.trim(); |
| 227 |
if (!v.startsWith('#')) v = '#' + v; |
| 228 |
if (isValidHex(v)) { fg = v; if (fgPicker) fgPicker.value = fg; render(); } |
| 229 |
}); |
| 230 |
if (bgText) bgText.addEventListener('input', function() { |
| 231 |
var v = this.value.trim(); |
| 232 |
if (!v.startsWith('#')) v = '#' + v; |
| 233 |
if (isValidHex(v)) { bg = v; if (bgPicker) bgPicker.value = bg; render(); } |
| 234 |
}); |
| 235 |
if (swapBtn) swapBtn.addEventListener('click', function() { |
| 236 |
var t = fg; fg = bg; bg = t; render(); |
| 237 |
}); |
| 238 |
|
| 239 |
root.querySelectorAll('[data-sugfg]').forEach(function(btn) { |
| 240 |
btn.addEventListener('click', function() { |
| 241 |
fg = this.getAttribute('data-sugfg'); |
| 242 |
render(); |
| 243 |
}); |
| 244 |
}); |
| 245 |
} |
| 246 |
|
| 247 |
render(); |
| 248 |
}); |
| 249 |
|
| 250 |
})(); |
| 251 |
|