diff --git a/src/js/settings.js b/src/js/settings.js index a15e969..5bfdefc 100644 --- a/src/js/settings.js +++ b/src/js/settings.js @@ -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.