fix(theme): Defaults weiss-auf-schwarz + Kontrast-Sicherheitsbremse
- Default-Schriftfarbe = #FFFFFF, Default-Hintergrund = #000000 - Wenn Schrift- und Hintergrundfarbe zu wenig Kontrast haben (< 2.5:1), wird automatisch auf weiss-auf-schwarz zurueckgefallen. So kann man sich nicht mehr in eine unsichtbare Seite manoevrieren. - Color-Picker zeigt jetzt die wirksame Default-Farbe in der Vorschau (statt leer/transparent), auch wenn keine Override gesetzt ist.
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Increment APP_VERSION with every code change
|
||||
export const APP_VERSION = 'v19';
|
||||
export const APP_VERSION = 'v20';
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user