diff --git a/frontend/js/calendar.js b/frontend/js/calendar.js index 23e905b..702a33d 100644 --- a/frontend/js/calendar.js +++ b/frontend/js/calendar.js @@ -1,5 +1,5 @@ import { api } from './api.js'; -import { applyTheme, isToday, isSameDay, toLocalDatetimeInput, toDateInput, dateKey, dayOfWeek, weekStart } from './utils.js'; +import { applyTheme, isToday, isSameDay, toLocalDatetimeInput, toDateInput, dateKey, dayOfWeek, weekStart, DEFAULT_TEXT_COLOR, DEFAULT_LINE_COLOR, DEFAULT_BG_COLOR } from './utils.js'; import { renderMonth } from './views/month.js'; import { renderWeek } from './views/week.js'; import { renderAgenda } from './views/agenda.js'; @@ -2046,22 +2046,18 @@ function openSettingsModal() { document.getElementById(id + '-preview').style.background = val; }); - // Optional colour overrides — empty hex input means "auto" + // Override-Farben — leeres Hex-Input bedeutet "Default verwenden", + // aber die Preview zeigt trotzdem die aktuell wirksame Farbe. [ - { id: 'cfg-text-color', val: s.text_color }, - { id: 'cfg-line-color', val: s.line_color }, - { id: 'cfg-bg-color', val: s.bg_color }, - ].forEach(({ id, val }) => { + { id: 'cfg-text-color', val: s.text_color, fallback: DEFAULT_TEXT_COLOR }, + { id: 'cfg-line-color', val: s.line_color, fallback: DEFAULT_LINE_COLOR }, + { id: 'cfg-bg-color', val: s.bg_color, fallback: DEFAULT_BG_COLOR }, + ].forEach(({ id, val, fallback }) => { const hex = document.getElementById(id + '-hex'); const prev = document.getElementById(id + '-preview'); if (!hex || !prev) return; - if (val) { - hex.value = String(val).toUpperCase(); - prev.style.background = val; - } else { - hex.value = ''; - prev.style.background = 'transparent'; - } + hex.value = val ? String(val).toUpperCase() : ''; + prev.style.background = val || fallback; }); document.getElementById('cfg-dim-past').checked = !!s.dim_past_events; document.getElementById('cfg-language').value = getLang(); @@ -2408,9 +2404,9 @@ function bindSettingsModal() { applyTheme(state.settings); }; [ - { prefix: 'cfg-text-color', defaultColor: '#c8c8d8' }, - { prefix: 'cfg-line-color', defaultColor: '#3a3a52' }, - { prefix: 'cfg-bg-color', defaultColor: '#0e0e14' }, + { prefix: 'cfg-text-color', defaultColor: DEFAULT_TEXT_COLOR }, + { prefix: 'cfg-line-color', defaultColor: DEFAULT_LINE_COLOR }, + { prefix: 'cfg-bg-color', defaultColor: DEFAULT_BG_COLOR }, ].forEach(({ prefix, defaultColor }) => { const preview = document.getElementById(prefix + '-preview'); const hex = document.getElementById(prefix + '-hex'); @@ -2436,7 +2432,7 @@ function bindSettingsModal() { const onTyped = () => { const norm = normalize(hex.value); if (norm === '') { - preview.style.background = 'transparent'; + preview.style.background = defaultColor; liveApplyOverride(prefix, null); } else if (norm) { preview.style.background = norm; @@ -2451,7 +2447,7 @@ function bindSettingsModal() { reset.addEventListener('click', () => { hex.value = ''; - preview.style.background = 'transparent'; + preview.style.background = defaultColor; liveApplyOverride(prefix, null); }); }); diff --git a/frontend/js/utils.js b/frontend/js/utils.js index 4b71860..b0e9b36 100644 --- a/frontend/js/utils.js +++ b/frontend/js/utils.js @@ -76,6 +76,12 @@ const LINE_CONTRAST = { 4: { border: '#5a5a78', light: '#484860' }, }; +// Defaults wenn kein Custom-Override gesetzt ist. +// Bewusst hart "weiss auf schwarz" damit man nie unsichtbar landet. +export const DEFAULT_TEXT_COLOR = '#FFFFFF'; +export const DEFAULT_LINE_COLOR = '#3A3A52'; +export const DEFAULT_BG_COLOR = '#000000'; + export function applyTheme(settings) { const root = document.documentElement; root.style.setProperty('--primary', settings.primary_color || '#4285f4'); @@ -83,47 +89,33 @@ export function applyTheme(settings) { root.style.setProperty('--accent', settings.accent_color || '#ea4335'); root.style.setProperty('--today-color', settings.today_color || '#4285f4'); - // Text colour: a custom hex (settings.text_color) wins over the legacy - // 1–4 contrast step. We derive --text-2/--text-3 by darkening the - // chosen colour so the secondary/tertiary text stays in the same hue. - if (settings.text_color) { - root.style.setProperty('--text-1', settings.text_color); - root.style.setProperty('--text-2', shadeHex(settings.text_color, -0.25)); - root.style.setProperty('--text-3', shadeHex(settings.text_color, -0.55)); - } else { - const tc = TEXT_CONTRAST[settings.text_contrast || 3]; - root.style.setProperty('--text-1', tc.t1); - root.style.setProperty('--text-2', tc.t2); - root.style.setProperty('--text-3', tc.t3); + // Effektive Farben bestimmen (Override > Default). + let textColor = settings.text_color || DEFAULT_TEXT_COLOR; + let lineColor = settings.line_color || DEFAULT_LINE_COLOR; + let bgColor = settings.bg_color || DEFAULT_BG_COLOR; + + // Sicherheitsbremse: Wenn Schrift- und Hintergrundfarbe nicht genug + // Kontrast haben (passiert wenn man aus Versehen text=bg eingibt), + // erzwinge weiss-auf-schwarz, damit man nicht in einer unbedienbaren + // Seite landet. + if (contrastRatio(textColor, bgColor) < 2.5) { + textColor = DEFAULT_TEXT_COLOR; + bgColor = DEFAULT_BG_COLOR; } - // Line colour: custom hex overrides the legacy contrast step. - if (settings.line_color) { - root.style.setProperty('--border', settings.line_color); - root.style.setProperty('--border-light', shadeHex(settings.line_color, -0.25)); - } else { - const lc = LINE_CONTRAST[settings.line_contrast || 3]; - root.style.setProperty('--border', lc.border); - root.style.setProperty('--border-light', lc.light); - } + root.style.setProperty('--text-1', textColor); + root.style.setProperty('--text-2', shadeHex(textColor, -0.25)); + root.style.setProperty('--text-3', shadeHex(textColor, -0.55)); - // Background colour: optional. If set, also tint the topbar/sidebar - // and surface variants so the whole UI stays coherent. - if (settings.bg_color) { - root.style.setProperty('--bg-app', settings.bg_color); - root.style.setProperty('--bg-topbar', shadeHex(settings.bg_color, 0.10)); - root.style.setProperty('--bg-sidebar', shadeHex(settings.bg_color, 0.10)); - root.style.setProperty('--bg-surface', shadeHex(settings.bg_color, 0.18)); - root.style.setProperty('--bg-hover', shadeHex(settings.bg_color, 0.26)); - root.style.setProperty('--bg-active', shadeHex(settings.bg_color, 0.40)); - } else { - root.style.removeProperty('--bg-app'); - root.style.removeProperty('--bg-topbar'); - root.style.removeProperty('--bg-sidebar'); - root.style.removeProperty('--bg-surface'); - root.style.removeProperty('--bg-hover'); - root.style.removeProperty('--bg-active'); - } + root.style.setProperty('--border', lineColor); + root.style.setProperty('--border-light', shadeHex(lineColor, -0.25)); + + root.style.setProperty('--bg-app', bgColor); + root.style.setProperty('--bg-topbar', shadeHex(bgColor, 0.10)); + root.style.setProperty('--bg-sidebar', shadeHex(bgColor, 0.10)); + root.style.setProperty('--bg-surface', shadeHex(bgColor, 0.18)); + root.style.setProperty('--bg-hover', shadeHex(bgColor, 0.26)); + root.style.setProperty('--bg-active', shadeHex(bgColor, 0.40)); const hh = settings.hour_height || 44; root.style.setProperty('--hour-h', hh + 'px'); @@ -132,6 +124,24 @@ export function applyTheme(settings) { root.style.setProperty('--month-label-color', settings.month_label_color || '#7090c0'); } +function luminance(hex) { + const c = (n) => { + const v = n / 255; + return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); + }; + const r = c(parseInt(hex.slice(1, 3), 16)); + const g = c(parseInt(hex.slice(3, 5), 16)); + const b = c(parseInt(hex.slice(5, 7), 16)); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; +} +function contrastRatio(c1, c2) { + try { + const l1 = luminance(c1); + const l2 = luminance(c2); + return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05); + } catch { return 21; } +} + function hexToRgba(hex, alpha) { const r = parseInt(hex.slice(1,3), 16); const g = parseInt(hex.slice(3,5), 16); diff --git a/frontend/js/version.js b/frontend/js/version.js index fb9a0e6..b8dfc41 100644 --- a/frontend/js/version.js +++ b/frontend/js/version.js @@ -1,2 +1,2 @@ // Increment APP_VERSION with every code change -export const APP_VERSION = 'v19'; +export const APP_VERSION = 'v20'; diff --git a/frontend/sw.js b/frontend/sw.js index d1e5ca9..4c460d0 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -7,7 +7,7 @@ // the entry HTML / version files). New releases take effect on the next // reload, no manual SW unregister required. -const CACHE_VERSION = 'calendarr-v19'; +const CACHE_VERSION = 'calendarr-v20'; const OFFLINE_SHELL = ['/', '/index.html']; self.addEventListener('install', event => {