feat(theme): applyCustomTheme/clearCustomTheme/syncCustomPickers + Hex-Validierung + WCAG-Kontrast
This commit is contained in:
@@ -116,6 +116,94 @@ function isValidBgUrl(url) {
|
||||
(url.startsWith('blob:') || url.startsWith('data:image/') || url.startsWith('https://'));
|
||||
}
|
||||
|
||||
// ---- THEME-BUILDER: Konstanten + reine Helfer ----
|
||||
const CUSTOM_DEFAULTS = {
|
||||
accent: '#6c8cff', bgPrimary: '#0b0d12', bgBoard: '#141821',
|
||||
textPrimary: '#e6e8ef', textSecondary: '#9aa3b8', textMuted: '#5b6478',
|
||||
};
|
||||
const HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
||||
|
||||
function isValidHexColor(v) { return typeof v === 'string' && HEX_RE.test(v); }
|
||||
function safeHex(v, fallback) { return isValidHexColor(v) ? v : fallback; }
|
||||
|
||||
function hexToRgba(hex, alpha) {
|
||||
let h = hex.replace('#', '');
|
||||
if (h.length === 3) h = h.split('').map(c => c + c).join('');
|
||||
const r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
|
||||
// WCAG 2.x Relativluminanz + Kontrastverhaeltnis
|
||||
function relLuminance(hex) {
|
||||
let h = hex.replace('#', '');
|
||||
if (h.length === 3) h = h.split('').map(c => c + c).join('');
|
||||
const lin = [0, 2, 4].map(i => {
|
||||
const c = parseInt(h.slice(i, i + 2), 16) / 255;
|
||||
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2];
|
||||
}
|
||||
function contrastRatio(hexA, hexB) {
|
||||
const a = relLuminance(hexA), b = relLuminance(hexB);
|
||||
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
|
||||
}
|
||||
|
||||
function updateContrastIndicator(textHex, bgHex) {
|
||||
const el = document.getElementById('tbContrast');
|
||||
if (!el) return;
|
||||
const ratio = contrastRatio(textHex, bgHex);
|
||||
let cls, key;
|
||||
if (ratio >= 4.5) { cls = 'good'; key = 'theme.builder.contrast_good'; }
|
||||
else if (ratio >= 3) { cls = 'ok'; key = 'theme.builder.contrast_ok'; }
|
||||
else { cls = 'bad'; key = 'theme.builder.contrast_bad'; }
|
||||
el.classList.remove('good', 'ok', 'bad');
|
||||
el.classList.add(cls);
|
||||
const txt = document.getElementById('tbContrastText');
|
||||
if (txt) txt.textContent = `${t(key)} (${ratio.toFixed(1)}:1)`;
|
||||
}
|
||||
|
||||
// Setzt data-theme='custom' + 6 validierte Inline-Vars (Gate vor jedem setProperty).
|
||||
function applyCustomTheme(ct) {
|
||||
const root = document.documentElement;
|
||||
const c = ct || {};
|
||||
const accent = safeHex(c.accent, CUSTOM_DEFAULTS.accent);
|
||||
const bgPrimary = safeHex(c.bgPrimary, CUSTOM_DEFAULTS.bgPrimary);
|
||||
const bgBoard = safeHex(c.bgBoard, CUSTOM_DEFAULTS.bgBoard);
|
||||
const textPrimary = safeHex(c.textPrimary, CUSTOM_DEFAULTS.textPrimary);
|
||||
const textSecondary = safeHex(c.textSecondary, CUSTOM_DEFAULTS.textSecondary);
|
||||
const textMuted = safeHex(c.textMuted, CUSTOM_DEFAULTS.textMuted);
|
||||
|
||||
root.setAttribute('data-theme', 'custom');
|
||||
root.style.setProperty('--accent', accent);
|
||||
root.style.setProperty('--bg-primary', bgPrimary);
|
||||
root.style.setProperty('--bg-board', hexToRgba(bgBoard, 0.55));
|
||||
root.style.setProperty('--text-primary', textPrimary);
|
||||
root.style.setProperty('--text-secondary', textSecondary);
|
||||
root.style.setProperty('--text-muted', textMuted);
|
||||
|
||||
document.querySelectorAll('.theme-card').forEach(card => {
|
||||
const on = card.dataset.value === 'custom';
|
||||
card.classList.toggle('active', on);
|
||||
card.setAttribute('aria-pressed', on ? 'true' : 'false');
|
||||
});
|
||||
updateContrastIndicator(textPrimary, bgPrimary);
|
||||
}
|
||||
|
||||
// Entfernt die 6 Inline-Vars (Rueckwechsel auf Preset / Reset).
|
||||
function clearCustomTheme() {
|
||||
const root = document.documentElement;
|
||||
['--accent', '--bg-primary', '--bg-board', '--text-primary', '--text-secondary', '--text-muted']
|
||||
.forEach(v => root.style.removeProperty(v));
|
||||
}
|
||||
|
||||
// Schreibt die gespeicherten (oder Default-) Farben in die 6 Picker-Inputs.
|
||||
function syncCustomPickers() {
|
||||
const ct = settings.customTheme || {};
|
||||
const set = (id, key) => { const el = document.getElementById(id); if (el) el.value = safeHex(ct[key], CUSTOM_DEFAULTS[key]); };
|
||||
set('tbAccent', 'accent'); set('tbBg', 'bgPrimary'); set('tbBoard', 'bgBoard');
|
||||
set('tbText', 'textPrimary'); set('tbTextSec', 'textSecondary'); set('tbTextMuted', 'textMuted');
|
||||
}
|
||||
|
||||
// Eigenes Upload-Bild Quota-schonend verkleinern: auf die laengste Bildschirmkante
|
||||
// (× devicePixelRatio, gedeckelt) herunterrechnen und als WebP neu kodieren. Das spart
|
||||
// gegenueber dem rohen Base64-Upload locker den Grossteil der chrome.storage.local-Quota.
|
||||
|
||||
Reference in New Issue
Block a user