Add configurable surface/sidebar colour + clearer sync-icon state
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -231,6 +231,7 @@ def _migrate():
|
|||||||
("cache_months", "ALTER TABLE user_settings ADD COLUMN cache_months INTEGER DEFAULT 3"),
|
("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"),
|
("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"),
|
("sync_flags", "ALTER TABLE user_settings ADD COLUMN sync_flags TEXT"),
|
||||||
|
("surface_color", "ALTER TABLE user_settings ADD COLUMN surface_color VARCHAR(7)"),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
conn.execute(text(ddl))
|
conn.execute(text(ddl))
|
||||||
|
|||||||
@@ -100,6 +100,9 @@ class UserSettings(Base):
|
|||||||
text_color = Column(String(7), nullable=True) # Override für --text-1 (NULL = nutze text_contrast)
|
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)
|
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)
|
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:
|
# How this user's private events appear to other group members:
|
||||||
# 'hidden' = invisible, 'busy' = anonymous busy block (default).
|
# 'hidden' = invisible, 'busy' = anonymous busy block (default).
|
||||||
private_event_visibility = Column(String(10), default="busy")
|
private_event_visibility = Column(String(10), default="busy")
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ DEFAULT_SYNC = {
|
|||||||
"share_calendar_icon": False,
|
"share_calendar_icon": False,
|
||||||
"cache_months": False,
|
"cache_months": False,
|
||||||
"month_view_paged": False,
|
"month_view_paged": False,
|
||||||
|
"surface_color": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -70,6 +71,7 @@ class SettingsUpdate(BaseModel):
|
|||||||
text_color: Optional[str] = None
|
text_color: Optional[str] = None
|
||||||
line_color: Optional[str] = None
|
line_color: Optional[str] = None
|
||||||
bg_color: Optional[str] = None
|
bg_color: Optional[str] = None
|
||||||
|
surface_color: Optional[str] = None
|
||||||
private_event_visibility: Optional[str] = None
|
private_event_visibility: Optional[str] = None
|
||||||
group_visible_calendar_id: Optional[int] = None
|
group_visible_calendar_id: Optional[int] = None
|
||||||
default_reminder_minutes: Optional[int] = None # null = off
|
default_reminder_minutes: Optional[int] = None # null = off
|
||||||
@@ -97,6 +99,7 @@ def _settings_dict(s: models.UserSettings) -> dict:
|
|||||||
"text_color": s.text_color,
|
"text_color": s.text_color,
|
||||||
"line_color": s.line_color,
|
"line_color": s.line_color,
|
||||||
"bg_color": s.bg_color,
|
"bg_color": s.bg_color,
|
||||||
|
"surface_color": s.surface_color,
|
||||||
"private_event_visibility": s.private_event_visibility or "busy",
|
"private_event_visibility": s.private_event_visibility or "busy",
|
||||||
"group_visible_calendar_id": s.group_visible_calendar_id,
|
"group_visible_calendar_id": s.group_visible_calendar_id,
|
||||||
"default_reminder_minutes": s.default_reminder_minutes,
|
"default_reminder_minutes": s.default_reminder_minutes,
|
||||||
@@ -162,7 +165,7 @@ def update_settings(
|
|||||||
# For these three override colours, an explicit null is meaningful
|
# For these three override colours, an explicit null is meaningful
|
||||||
# ("reset to default") and must be persisted as NULL. All other fields
|
# ("reset to default") and must be persisted as NULL. All other fields
|
||||||
# keep the previous behaviour where a null/missing value is ignored.
|
# 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)
|
update_data = data.model_dump(exclude_unset=True)
|
||||||
|
|
||||||
# Merge sync-flag overrides into the stored account-wide JSON map. Only known
|
# Merge sync-flag overrides into the stored account-wide JSON map. Only known
|
||||||
|
|||||||
@@ -1410,6 +1410,23 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
.sync-toggle-lg.on::after { transform: translateX(18px); }
|
.sync-toggle-lg.on::after { transform: translateX(18px); }
|
||||||
.settings-row .sync-toggle { justify-self: center; }
|
.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 */
|
/* Per-row colour control: swatch + hex + reset, right-aligned */
|
||||||
.settings-color-ctl { display: flex; align-items: center; gap: 12px; }
|
.settings-color-ctl { display: flex; align-items: center; gap: 12px; }
|
||||||
.settings-color-ctl .ev-color-hex { width: 92px; }
|
.settings-color-ctl .ev-color-hex { width: 92px; }
|
||||||
|
|||||||
@@ -3341,6 +3341,8 @@ function populateSettings() {
|
|||||||
|
|
||||||
// ── Settings sync table ───────────────────────────────────
|
// ── Settings sync table ───────────────────────────────────
|
||||||
const SYNC_ICON_SVG = '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46A7.93 7.93 0 0020 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74A7.93 7.93 0 004 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/></svg>';
|
const SYNC_ICON_SVG = '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46A7.93 7.93 0 0020 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74A7.93 7.93 0 004 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/></svg>';
|
||||||
|
// Same sync glyph with a diagonal slash — the clear "not synced" state.
|
||||||
|
const SYNC_OFF_ICON_SVG = '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46A7.93 7.93 0 0020 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74A7.93 7.93 0 004 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/><path d="M3.2 2.4l18.4 18.4" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" fill="none"/></svg>';
|
||||||
const RESET_ICON_SVG = '<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/></svg>';
|
const RESET_ICON_SVG = '<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/></svg>';
|
||||||
|
|
||||||
function normalizeHex(raw, fallback) {
|
function normalizeHex(raw, fallback) {
|
||||||
@@ -3387,7 +3389,7 @@ function settingRowHtml(def) {
|
|||||||
).join('') + '</div>';
|
).join('') + '</div>';
|
||||||
}
|
}
|
||||||
return `<div class="settings-row" data-row="${def.key}">
|
return `<div class="settings-row" data-row="${def.key}">
|
||||||
<button type="button" class="sync-icon ${synced ? 'on' : ''}" data-sync="${def.key}" role="switch" aria-checked="${synced}" title="${escHtml(t('settings_sync_this'))}">${SYNC_ICON_SVG}</button>
|
<button type="button" class="sync-icon ${synced ? 'on' : ''}" data-sync="${def.key}" role="switch" aria-checked="${synced}" title="${escHtml(t('settings_sync_this'))}"><span class="si-on">${SYNC_ICON_SVG}</span><span class="si-off">${SYNC_OFF_ICON_SVG}</span></button>
|
||||||
<div class="settings-row-name">${escHtml(t(def.labelKey))}</div>
|
<div class="settings-row-name">${escHtml(t(def.labelKey))}</div>
|
||||||
<div class="settings-row-value">${valueHtml}</div>
|
<div class="settings-row-value">${valueHtml}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ const translations = {
|
|||||||
settings_text_color: 'Schriftfarbe',
|
settings_text_color: 'Schriftfarbe',
|
||||||
settings_line_color: 'Linienfarbe',
|
settings_line_color: 'Linienfarbe',
|
||||||
settings_bg_color: 'Hintergrundfarbe',
|
settings_bg_color: 'Hintergrundfarbe',
|
||||||
|
settings_surface_color: 'Seitenleisten-/Oberflächenfarbe',
|
||||||
reset: 'Reset',
|
reset: 'Reset',
|
||||||
settings_text_contrast: 'Schriftkontrast',
|
settings_text_contrast: 'Schriftkontrast',
|
||||||
settings_text_contrast_desc: 'Helligkeit der Beschriftungen und Texte',
|
settings_text_contrast_desc: 'Helligkeit der Beschriftungen und Texte',
|
||||||
@@ -430,6 +431,7 @@ const translations = {
|
|||||||
settings_text_color: 'Text color',
|
settings_text_color: 'Text color',
|
||||||
settings_line_color: 'Line color',
|
settings_line_color: 'Line color',
|
||||||
settings_bg_color: 'Background color',
|
settings_bg_color: 'Background color',
|
||||||
|
settings_surface_color: 'Sidebar / surface color',
|
||||||
reset: 'Reset',
|
reset: 'Reset',
|
||||||
settings_text_contrast: 'Text contrast',
|
settings_text_contrast: 'Text contrast',
|
||||||
settings_text_contrast_desc: 'Brightness of labels and text',
|
settings_text_contrast_desc: 'Brightness of labels and text',
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const DEFAULT_COLORS = {
|
|||||||
text_color: '#FFFFFF',
|
text_color: '#FFFFFF',
|
||||||
bg_color: '#000000',
|
bg_color: '#000000',
|
||||||
line_color: '#3A3A52',
|
line_color: '#3A3A52',
|
||||||
|
surface_color: '#1A1A1A',
|
||||||
month_divider_color: '#7090C0',
|
month_divider_color: '#7090C0',
|
||||||
month_label_color: '#7090C0',
|
month_label_color: '#7090C0',
|
||||||
};
|
};
|
||||||
@@ -60,6 +61,7 @@ export const SETTING_GROUPS = [
|
|||||||
{ key: 'today_color', labelKey: 'settings_today_color', type: 'color' },
|
{ key: 'today_color', labelKey: 'settings_today_color', type: 'color' },
|
||||||
{ key: 'text_color', labelKey: 'settings_text_color', type: 'color' },
|
{ key: 'text_color', labelKey: 'settings_text_color', type: 'color' },
|
||||||
{ key: 'bg_color', labelKey: 'settings_bg_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: 'line_color', labelKey: 'settings_line_color', type: 'color' },
|
||||||
{ key: 'month_divider_color', labelKey: 'settings_month_divider_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' },
|
{ key: 'month_label_color', labelKey: 'settings_month_label_color', type: 'color' },
|
||||||
|
|||||||
@@ -132,12 +132,15 @@ export function applyTheme(settings) {
|
|||||||
root.style.setProperty('--border', lineColor);
|
root.style.setProperty('--border', lineColor);
|
||||||
root.style.setProperty('--border-light', shadeHex(lineColor, -0.25));
|
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-app', bgColor);
|
||||||
root.style.setProperty('--bg-topbar', shadeHex(bgColor, 0.10));
|
root.style.setProperty('--bg-topbar', surfaceBase);
|
||||||
root.style.setProperty('--bg-sidebar', shadeHex(bgColor, 0.10));
|
root.style.setProperty('--bg-sidebar', surfaceBase);
|
||||||
root.style.setProperty('--bg-surface', shadeHex(bgColor, 0.18));
|
root.style.setProperty('--bg-surface', shadeHex(surfaceBase, 0.10));
|
||||||
root.style.setProperty('--bg-hover', shadeHex(bgColor, 0.26));
|
root.style.setProperty('--bg-hover', shadeHex(surfaceBase, 0.20));
|
||||||
root.style.setProperty('--bg-active', shadeHex(bgColor, 0.40));
|
root.style.setProperty('--bg-active', shadeHex(surfaceBase, 0.34));
|
||||||
|
|
||||||
const hh = settings.hour_height || 44;
|
const hh = settings.hour_height || 44;
|
||||||
root.style.setProperty('--hour-h', hh + 'px');
|
root.style.setProperty('--hour-h', hh + 'px');
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
// the entry HTML / version files). New releases take effect on the next
|
// the entry HTML / version files). New releases take effect on the next
|
||||||
// reload, no manual SW unregister required.
|
// reload, no manual SW unregister required.
|
||||||
|
|
||||||
const CACHE_VERSION = 'calendarr-v27';
|
const CACHE_VERSION = 'calendarr-v28';
|
||||||
const OFFLINE_SHELL = ['/', '/index.html'];
|
const OFFLINE_SHELL = ['/', '/index.html'];
|
||||||
|
|
||||||
self.addEventListener('install', event => {
|
self.addEventListener('install', event => {
|
||||||
|
|||||||
Reference in New Issue
Block a user