From 131f6b496d9f4bcc5603e35f299b681f404691d0 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Tue, 14 Jul 2026 22:01:16 +0200 Subject: [PATCH] Add configurable surface/sidebar colour + clearer sync-icon state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - new user_settings.surface_color (nullable, device-local default, not synced): drives the web sidebar / top bar / surfaces; NULL derives from bg_color as before. Added to schema, migration, GET/PUT, NULLABLE_OVERRIDES, DEFAULT_SYNC. - web: surface colour row in the settings table; applyTheme derives the whole surface family from it - web: per-row sync icon now has real styling — ON shows the primary-tinted glyph on a pill, OFF shows a slashed, muted glyph (was previously unstyled, so synced/not-synced looked identical) Co-Authored-By: Claude Opus 4.8 --- backend/main.py | 1 + backend/models.py | 3 +++ backend/routers/settings_router.py | 5 ++++- frontend/css/app.css | 17 +++++++++++++++++ frontend/js/calendar.js | 4 +++- frontend/js/i18n.js | 2 ++ frontend/js/settings-sync.js | 2 ++ frontend/js/utils.js | 13 ++++++++----- frontend/sw.js | 2 +- 9 files changed, 41 insertions(+), 8 deletions(-) diff --git a/backend/main.py b/backend/main.py index 01d28fe..a61e6ba 100644 --- a/backend/main.py +++ b/backend/main.py @@ -231,6 +231,7 @@ def _migrate(): ("cache_months", "ALTER TABLE user_settings ADD COLUMN cache_months INTEGER DEFAULT 3"), ("month_view_paged", "ALTER TABLE user_settings ADD COLUMN month_view_paged BOOLEAN DEFAULT 0"), ("sync_flags", "ALTER TABLE user_settings ADD COLUMN sync_flags TEXT"), + ("surface_color", "ALTER TABLE user_settings ADD COLUMN surface_color VARCHAR(7)"), ): try: conn.execute(text(ddl)) diff --git a/backend/models.py b/backend/models.py index 8b2fb7b..4a1fe17 100644 --- a/backend/models.py +++ b/backend/models.py @@ -100,6 +100,9 @@ class UserSettings(Base): text_color = Column(String(7), nullable=True) # Override für --text-1 (NULL = nutze text_contrast) line_color = Column(String(7), nullable=True) # Override für --border (NULL = nutze line_contrast) bg_color = Column(String(7), nullable=True) # Override für --bg-app (NULL = Default) + # Surface/sidebar/topbar colour (web sidebar + top bar, iOS top bar). + # NULL = derive from bg_color. Device-local by default (not synced). + surface_color = Column(String(7), nullable=True) # How this user's private events appear to other group members: # 'hidden' = invisible, 'busy' = anonymous busy block (default). private_event_visibility = Column(String(10), default="busy") diff --git a/backend/routers/settings_router.py b/backend/routers/settings_router.py index 9fa827f..4486cd6 100644 --- a/backend/routers/settings_router.py +++ b/backend/routers/settings_router.py @@ -36,6 +36,7 @@ DEFAULT_SYNC = { "share_calendar_icon": False, "cache_months": False, "month_view_paged": False, + "surface_color": False, } @@ -70,6 +71,7 @@ class SettingsUpdate(BaseModel): text_color: Optional[str] = None line_color: Optional[str] = None bg_color: Optional[str] = None + surface_color: Optional[str] = None private_event_visibility: Optional[str] = None group_visible_calendar_id: Optional[int] = None default_reminder_minutes: Optional[int] = None # null = off @@ -97,6 +99,7 @@ def _settings_dict(s: models.UserSettings) -> dict: "text_color": s.text_color, "line_color": s.line_color, "bg_color": s.bg_color, + "surface_color": s.surface_color, "private_event_visibility": s.private_event_visibility or "busy", "group_visible_calendar_id": s.group_visible_calendar_id, "default_reminder_minutes": s.default_reminder_minutes, @@ -162,7 +165,7 @@ def update_settings( # For these three override colours, an explicit null is meaningful # ("reset to default") and must be persisted as NULL. All other fields # keep the previous behaviour where a null/missing value is ignored. - NULLABLE_OVERRIDES = {"text_color", "line_color", "bg_color", "group_visible_calendar_id", "default_reminder_minutes", "default_event_duration_minutes", "share_calendar_icon"} + NULLABLE_OVERRIDES = {"text_color", "line_color", "bg_color", "surface_color", "group_visible_calendar_id", "default_reminder_minutes", "default_event_duration_minutes", "share_calendar_icon"} update_data = data.model_dump(exclude_unset=True) # Merge sync-flag overrides into the stored account-wide JSON map. Only known diff --git a/frontend/css/app.css b/frontend/css/app.css index c11836d..aee04a7 100644 --- a/frontend/css/app.css +++ b/frontend/css/app.css @@ -1410,6 +1410,23 @@ a { color: var(--primary); text-decoration: none; } .sync-toggle-lg.on::after { transform: translateX(18px); } .settings-row .sync-toggle { justify-self: center; } +/* Per-row sync toggle icon: ON = primary + pill, OFF = muted + slashed icon */ +.sync-icon { + display: inline-flex; align-items: center; justify-content: center; + width: 32px; height: 28px; padding: 0; + border: none; border-radius: 8px; cursor: pointer; + background: transparent; color: var(--text-3); + transition: color var(--transition), background var(--transition); + justify-self: center; +} +.sync-icon:hover { background: var(--bg-hover); color: var(--text-2); } +.sync-icon.on { color: var(--primary); background: var(--primary-dim); } +.sync-icon .si-on, .sync-icon .si-off { align-items: center; } +.sync-icon .si-on { display: none; } +.sync-icon .si-off { display: inline-flex; } +.sync-icon.on .si-off { display: none; } +.sync-icon.on .si-on { display: inline-flex; } + /* Per-row colour control: swatch + hex + reset, right-aligned */ .settings-color-ctl { display: flex; align-items: center; gap: 12px; } .settings-color-ctl .ev-color-hex { width: 92px; } diff --git a/frontend/js/calendar.js b/frontend/js/calendar.js index cc5d9ba..88a7ac5 100644 --- a/frontend/js/calendar.js +++ b/frontend/js/calendar.js @@ -3341,6 +3341,8 @@ function populateSettings() { // ── Settings sync table ─────────────────────────────────── const SYNC_ICON_SVG = ''; +// Same sync glyph with a diagonal slash — the clear "not synced" state. +const SYNC_OFF_ICON_SVG = ''; const RESET_ICON_SVG = ''; function normalizeHex(raw, fallback) { @@ -3387,7 +3389,7 @@ function settingRowHtml(def) { ).join('') + ''; } return `
- +
${escHtml(t(def.labelKey))}
${valueHtml}
`; diff --git a/frontend/js/i18n.js b/frontend/js/i18n.js index b818c19..b4bcdd6 100644 --- a/frontend/js/i18n.js +++ b/frontend/js/i18n.js @@ -77,6 +77,7 @@ const translations = { settings_text_color: 'Schriftfarbe', settings_line_color: 'Linienfarbe', settings_bg_color: 'Hintergrundfarbe', + settings_surface_color: 'Seitenleisten-/Oberflächenfarbe', reset: 'Reset', settings_text_contrast: 'Schriftkontrast', settings_text_contrast_desc: 'Helligkeit der Beschriftungen und Texte', @@ -430,6 +431,7 @@ const translations = { settings_text_color: 'Text color', settings_line_color: 'Line color', settings_bg_color: 'Background color', + settings_surface_color: 'Sidebar / surface color', reset: 'Reset', settings_text_contrast: 'Text contrast', settings_text_contrast_desc: 'Brightness of labels and text', diff --git a/frontend/js/settings-sync.js b/frontend/js/settings-sync.js index 7e0deb2..1810c07 100644 --- a/frontend/js/settings-sync.js +++ b/frontend/js/settings-sync.js @@ -13,6 +13,7 @@ export const DEFAULT_COLORS = { text_color: '#FFFFFF', bg_color: '#000000', line_color: '#3A3A52', + surface_color: '#1A1A1A', month_divider_color: '#7090C0', month_label_color: '#7090C0', }; @@ -60,6 +61,7 @@ export const SETTING_GROUPS = [ { key: 'today_color', labelKey: 'settings_today_color', type: 'color' }, { key: 'text_color', labelKey: 'settings_text_color', type: 'color' }, { key: 'bg_color', labelKey: 'settings_bg_color', type: 'color' }, + { key: 'surface_color', labelKey: 'settings_surface_color', type: 'color' }, { key: 'line_color', labelKey: 'settings_line_color', type: 'color' }, { key: 'month_divider_color', labelKey: 'settings_month_divider_color', type: 'color' }, { key: 'month_label_color', labelKey: 'settings_month_label_color', type: 'color' }, diff --git a/frontend/js/utils.js b/frontend/js/utils.js index f245bfd..d7ffd14 100644 --- a/frontend/js/utils.js +++ b/frontend/js/utils.js @@ -132,12 +132,15 @@ export function applyTheme(settings) { root.style.setProperty('--border', lineColor); root.style.setProperty('--border-light', shadeHex(lineColor, -0.25)); + // Surface family (sidebar / top bar / cards). Explicit surface_color drives + // it; otherwise derive from the app background as before. + const surfaceBase = settings.surface_color || shadeHex(bgColor, 0.10); 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)); + root.style.setProperty('--bg-topbar', surfaceBase); + root.style.setProperty('--bg-sidebar', surfaceBase); + root.style.setProperty('--bg-surface', shadeHex(surfaceBase, 0.10)); + root.style.setProperty('--bg-hover', shadeHex(surfaceBase, 0.20)); + root.style.setProperty('--bg-active', shadeHex(surfaceBase, 0.34)); const hh = settings.hour_height || 44; root.style.setProperty('--hour-h', hh + 'px'); diff --git a/frontend/sw.js b/frontend/sw.js index 9ce3525..38bf15d 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-v27'; +const CACHE_VERSION = 'calendarr-v28'; const OFFLINE_SHELL = ['/', '/index.html']; self.addEventListener('install', event => {