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 `