feat(settings): Schriftfarbe, Linienfarbe und Hintergrundfarbe per Color-Picker

Die bisherigen Stufen-Wähler ("Dunkel/Mittel/Hell/Maximum" und
"Kaum/Subtil/Normal/Stark") für Schrift- bzw. Linienkontrast sind durch
echte Hex-Color-Picker ersetzt. Zusätzlich kann jetzt auch die
Hintergrundfarbe der Seite frei gewählt werden.

Wenn ein Override gesetzt ist:
- text_color → setzt --text-1 direkt, --text-2/--text-3 werden
  daraus per shadeHex(-0.25 / -0.55) abgeleitet, damit der Hue passt
- line_color → setzt --border, --border-light wird leicht abgedunkelt
- bg_color → setzt --bg-app, daraus werden Topbar/Sidebar/Surface/
  Hover/Active per shadeHex(+0.10…+0.40) konsistent hochskaliert

Per "Reset"-Knopf wird der Override geleert und die alte Stufen-Logik
(falls noch vorhanden) bzw. der Default-Theme greift wieder.

Backend:
- 3 neue nullable VARCHAR(7)-Spalten in user_settings (text_color,
  line_color, bg_color) inkl. Migrationen in main.py
- settings_router nutzt model_dump(exclude_unset=True) und respektiert
  explizite null-Werte nur für diese 3 Override-Felder, damit Reset
  funktioniert

Auch enthalten: Auflösen der Merge-Konflikte in sw.js, index.html,
version.js (HEAD-Stand v17 behalten) und Bump auf v18.
This commit is contained in:
Scarriffle
2026-05-19 09:49:45 +02:00
parent d3fa591bef
commit 8f9eafe561
9 changed files with 187 additions and 170 deletions

View File

@@ -2045,6 +2045,24 @@ function openSettingsModal() {
document.getElementById(id + '-hex').value = val.toUpperCase();
document.getElementById(id + '-preview').style.background = val;
});
// Optional colour overrides — empty hex input means "auto"
[
{ 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 }) => {
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';
}
});
document.getElementById('cfg-dim-past').checked = !!s.dim_past_events;
document.getElementById('cfg-language').value = getLang();
@@ -2376,6 +2394,32 @@ function bindSettingsModal() {
});
});
// Optional override colours (text / line / background) — empty = use default
[
{ prefix: 'cfg-text-color', defaultColor: '#c8c8d8' },
{ prefix: 'cfg-line-color', defaultColor: '#3a3a52' },
{ prefix: 'cfg-bg-color', defaultColor: '#0e0e14' },
].forEach(({ prefix, defaultColor }) => {
const preview = document.getElementById(prefix + '-preview');
const hex = document.getElementById(prefix + '-hex');
const reset = document.getElementById(prefix + '-reset');
if (!preview || !hex || !reset) return;
preview.addEventListener('click', async () => {
const picked = await openColorPicker(preview, hex.value || defaultColor);
if (picked) { hex.value = picked.toUpperCase(); preview.style.background = picked; }
});
hex.addEventListener('change', () => {
let val = hex.value.trim();
if (!val) { preview.style.background = 'transparent'; return; }
if (!val.startsWith('#')) val = '#' + val;
if (/^#[0-9a-fA-F]{6}$/.test(val)) { hex.value = val.toUpperCase(); preview.style.background = val; }
});
reset.addEventListener('click', () => {
hex.value = '';
preview.style.background = 'transparent';
});
});
// Panel navigation
document.querySelectorAll('.settings-nav-btn').forEach(btn => {
btn.addEventListener('click', () => activateSettingsPanel(btn.dataset.panel));
@@ -2415,6 +2459,11 @@ function bindSettingsModal() {
const btn = document.querySelector(`#${id} .contrast-btn.active`);
return btn ? Number(btn.dataset.val) : null;
};
// Optional override colours: empty input → null (use default)
const colourOrNull = (id) => {
const v = (document.getElementById(id).value || '').trim();
return /^#[0-9a-fA-F]{6}$/.test(v) ? v : null;
};
const settings = {
default_view: document.getElementById('cfg-default-view').value,
week_start_day: document.getElementById('cfg-week-start').value,
@@ -2423,9 +2472,10 @@ function bindSettingsModal() {
today_color: document.getElementById('cfg-today-hex').value,
month_divider_color: document.getElementById('cfg-month-divider-hex').value,
month_label_color: document.getElementById('cfg-month-label-hex').value,
text_color: colourOrNull('cfg-text-color-hex'),
line_color: colourOrNull('cfg-line-color-hex'),
bg_color: colourOrNull('cfg-bg-color-hex'),
dim_past_events: document.getElementById('cfg-dim-past').checked,
text_contrast: getActive('cfg-text-contrast') || 3,
line_contrast: getActive('cfg-line-contrast') || 3,
hour_height: getActive('cfg-hour-height') || 44,
language: document.getElementById('cfg-language').value,
};