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:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -67,6 +67,10 @@ const translations = {
|
||||
settings_today_color: 'Heutige-Tag-Farbe',
|
||||
settings_month_divider_color: 'Monatswechsel-Linie',
|
||||
settings_month_label_color: 'Monatskürzel-Farbe',
|
||||
settings_text_color: 'Schriftfarbe',
|
||||
settings_line_color: 'Linienfarbe',
|
||||
settings_bg_color: 'Hintergrundfarbe',
|
||||
reset: 'Reset',
|
||||
settings_text_contrast: 'Schriftkontrast',
|
||||
settings_text_contrast_desc: 'Helligkeit der Beschriftungen und Texte',
|
||||
contrast_dark: 'Dunkel', contrast_medium: 'Mittel',
|
||||
@@ -282,6 +286,10 @@ const translations = {
|
||||
settings_today_color: 'Today highlight color',
|
||||
settings_month_divider_color: 'Month divider line',
|
||||
settings_month_label_color: 'Month label color',
|
||||
settings_text_color: 'Text color',
|
||||
settings_line_color: 'Line color',
|
||||
settings_bg_color: 'Background color',
|
||||
reset: 'Reset',
|
||||
settings_text_contrast: 'Text contrast',
|
||||
settings_text_contrast_desc: 'Brightness of labels and text',
|
||||
contrast_dark: 'Dark', contrast_medium: 'Medium',
|
||||
|
||||
@@ -83,14 +83,47 @@ export function applyTheme(settings) {
|
||||
root.style.setProperty('--accent', settings.accent_color || '#ea4335');
|
||||
root.style.setProperty('--today-color', settings.today_color || '#4285f4');
|
||||
|
||||
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);
|
||||
// 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);
|
||||
}
|
||||
|
||||
const lc = LINE_CONTRAST[settings.line_contrast || 3];
|
||||
root.style.setProperty('--border', lc.border);
|
||||
root.style.setProperty('--border-light', lc.light);
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
const hh = settings.hour_height || 44;
|
||||
root.style.setProperty('--hour-h', hh + 'px');
|
||||
@@ -105,3 +138,24 @@ function hexToRgba(hex, alpha) {
|
||||
const b = parseInt(hex.slice(5,7), 16);
|
||||
return `rgba(${r},${g},${b},${alpha})`;
|
||||
}
|
||||
|
||||
// Brighten (positive amount) or darken (negative) a hex colour.
|
||||
// Used to derive supporting shades (sidebar bg, hover bg, secondary text…)
|
||||
// from a single user-picked colour so the whole UI stays in the same family.
|
||||
function shadeHex(hex, amount) {
|
||||
let r = parseInt(hex.slice(1,3), 16);
|
||||
let g = parseInt(hex.slice(3,5), 16);
|
||||
let b = parseInt(hex.slice(5,7), 16);
|
||||
if (amount >= 0) {
|
||||
r = Math.round(r + (255 - r) * amount);
|
||||
g = Math.round(g + (255 - g) * amount);
|
||||
b = Math.round(b + (255 - b) * amount);
|
||||
} else {
|
||||
const a = 1 + amount; // amount is negative: e.g. -0.25 → keep 75%
|
||||
r = Math.round(r * a);
|
||||
g = Math.round(g * a);
|
||||
b = Math.round(b * a);
|
||||
}
|
||||
const h = n => Math.max(0, Math.min(255, n)).toString(16).padStart(2, '0');
|
||||
return '#' + h(r) + h(g) + h(b);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,2 @@
|
||||
// Increment APP_VERSION with every code change
|
||||
<<<<<<< HEAD
|
||||
export const APP_VERSION = 'v17';
|
||||
=======
|
||||
export const APP_VERSION = 'v11';
|
||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
||||
export const APP_VERSION = 'v18';
|
||||
|
||||
Reference in New Issue
Block a user