feat(web): theme import/export, more themable colors, hideable local/ical calendars

Theme:
- Export/import themes via UI as <date>_<time>.theme (JSON, readable keys,
  link to THEME.md). Import is partial-aware: only present params are written
  (server accepts partial); prompts before importing files with unknown params.
- Dynamic favicon + theme-color tinted to the primary colour on load/save.
- New themable colours: general hover-highlight, day hover/selected/bg,
  today background, plus two unified sidebar action-icon colours
  (inactive/active) covering bell, hide, delete and read-only icons.
- All new colours are per-setting syncable; documented in THEME.md.

UX:
- Styled confirm dialog (#modal-confirm) replaces window.confirm() for
  calendar delete and account disconnect.
- Birthday/local calendars and iCal subscriptions can now be hidden from the
  sidebar via Settings (new sidebar_hidden column + hide toggle).

Backend: additive nullable columns + idempotent migrations for user_settings
colours and local_calendars/ical_subscriptions.sidebar_hidden.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-20 13:20:30 +02:00
parent 12c869451b
commit 6316ed3a6b
15 changed files with 475 additions and 47 deletions

View File

@@ -23,6 +23,17 @@
--border-light: #242438;
--scrollbar: #30303c;
/* Fine-grained element colours (customisable via Settings → Farben).
Defaults reference the derived values so the look is unchanged until the
user overrides them; applyTheme() writes concrete overrides on top. */
--hover-highlight: var(--bg-hover); /* general interactive hover */
--icon-inactive-color: var(--text-2); /* sidebar action icons: resting/off/not-hovered */
--icon-active-color: var(--text-1); /* sidebar action icons: hovered / on / pressed */
--day-hover-color: var(--bg-hover); /* calendar day-cell hover */
--day-selected-base: var(--primary); /* selected day (tinted via color-mix) */
--day-bg: transparent; /* normal day background */
--today-bg-base: var(--today-color); /* today's day (tinted via color-mix) */
--topbar-h: 64px;
--sidebar-w: 256px;
--shadow: 0 2px 12px rgba(0,0,0,.45);
@@ -201,7 +212,7 @@ a { color: var(--primary); text-decoration: none; }
transform .1s ease;
}
.icon-btn svg { width: 20px; height: 20px; fill: currentColor; }
.icon-btn:hover { background: var(--bg-hover); color: var(--text-1); }
.icon-btn:hover { background: var(--hover-highlight); color: var(--text-1); }
.icon-btn:active { transform: scale(.92); }
.icon-btn:focus-visible {
outline: 2px solid var(--primary);
@@ -310,7 +321,7 @@ a { color: var(--primary); text-decoration: none; }
display: flex; align-items: center; gap: 8px;
padding: 9px 10px; border-radius: 8px; cursor: pointer;
}
.cal-select-option:hover { background: var(--bg-hover); }
.cal-select-option:hover { background: var(--hover-highlight); }
.cal-select-option.selected { background: var(--primary-dim); }
/* ── Date/time input dark mode ──────────────────────────── */
@@ -481,7 +492,7 @@ a { color: var(--primary); text-decoration: none; }
transition: background var(--transition), color var(--transition);
border-radius: 20px;
}
.view-btn:hover { background: var(--bg-hover); color: var(--text-1); }
.view-btn:hover { background: var(--hover-highlight); color: var(--text-1); }
.view-btn.active { background: var(--primary-dim); color: var(--primary); }
.user-menu-wrapper { position: relative; }
@@ -510,7 +521,7 @@ a { color: var(--primary); text-decoration: none; }
background: none; color: var(--text-2); font-size: 13px;
cursor: pointer; transition: background var(--transition);
}
.dropdown-item:hover { background: var(--bg-hover); color: var(--text-1); }
.dropdown-item:hover { background: var(--hover-highlight); color: var(--text-1); }
.dropdown-item svg { flex-shrink: 0;
}
@@ -555,7 +566,7 @@ a { color: var(--primary); text-decoration: none; }
.mini-cal-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.mini-title { font-size: 13px; font-weight: 500; color: var(--text-1); }
.mini-btn { width: 28px; height: 28px; font-size: 18px; color: var(--text-2); }
.mini-btn:hover { color: var(--text-1); background: var(--bg-hover); }
.mini-btn:hover { color: var(--text-1); background: var(--hover-highlight); }
.mini-cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); text-align: center; }
.mini-dow { font-size: 11px; color: var(--text-3); padding: 2px 0; font-weight: 500; }
.mini-cal-days { display: grid; grid-template-columns: repeat(7, 1fr); text-align: center; }
@@ -566,13 +577,13 @@ a { color: var(--primary); text-decoration: none; }
display: flex; align-items: center; justify-content: center;
margin: 1px auto; position: relative;
}
.mini-day:hover { background: var(--bg-hover); color: var(--text-1); }
.mini-day:hover { background: var(--day-hover-color); color: var(--text-1); }
.mini-day.other-month { color: var(--text-3); }
.mini-day.today {
background: var(--today-color);
color: #fff; font-weight: 700;
}
.mini-day.selected:not(.today) { background: var(--primary-dim); color: var(--primary); font-weight: 600; }
.mini-day.selected:not(.today) { background: color-mix(in srgb, var(--day-selected-base) 15%, transparent); color: var(--primary); font-weight: 600; }
.mini-day.has-events::after {
content: ''; position: absolute; bottom: 2px; left: 50%; transform: translateX(-50%);
width: 4px; height: 4px; border-radius: 50%; background: var(--primary);
@@ -599,7 +610,7 @@ a { color: var(--primary); text-decoration: none; }
text-align: left; font-size: 13px; color: var(--text-1);
background: none; border: none; cursor: pointer;
}
.add-cal-dropdown button:hover { background: var(--bg-hover); }
.add-cal-dropdown button:hover { background: var(--hover-highlight); }
.cal-item {
display: flex; align-items: center; gap: 10px;
padding: 6px 16px; cursor: pointer;
@@ -607,7 +618,7 @@ a { color: var(--primary); text-decoration: none; }
border-radius: 0 20px 20px 0;
margin-right: 12px;
}
.cal-item:hover { background: var(--bg-hover); }
.cal-item:hover { background: var(--hover-highlight); }
.cal-item-dot {
width: 14px; height: 14px; border-radius: 3px; flex-shrink: 0; cursor: pointer;
}
@@ -624,11 +635,16 @@ a { color: var(--primary); text-decoration: none; }
.cal-account-name { font-size: 11px; color: var(--text-3); padding: 4px 16px 2px; font-weight: 500; }
/* Fixed min-height so 28px mini-btns never change row height when they appear. */
.cal-item { position: relative; min-height: 40px; }
.cal-item-bell { display: none; flex-shrink: 0; }
/* Sidebar action icons (bell / hide / delete / read-only flag) share two theme
colours: "inactive" (resting/off) and "active" (hovered/on/pressed). */
.cal-item-bell { display: none; flex-shrink: 0; color: var(--icon-active-color); } /* bell shown = reminders on = active */
.cal-item:hover .cal-item-remove,
.cal-item:hover .cal-item-bell { display: inline-flex; }
.cal-item-bell.off { display: inline-flex; opacity: .55; color: var(--text-3); }
.cal-item:hover .cal-item-bell.off { opacity: 1; color: inherit; }
.cal-item-bell.off { display: inline-flex; opacity: .55; color: var(--icon-inactive-color); }
.cal-item:hover .cal-item-bell.off { opacity: 1; color: var(--icon-inactive-color); }
.cal-item-bell:hover, .cal-item-bell.off:hover { color: var(--icon-active-color); }
.cal-item-remove { color: var(--icon-inactive-color); }
.cal-item-remove:hover { color: var(--icon-active-color); }
/* ── Month View ─────────────────────────────────────────── */
.month-view { display: flex; flex-direction: column; flex: 1; min-height: 0; }
@@ -661,11 +677,12 @@ a { color: var(--primary); text-decoration: none; }
flex: 1; border-right: 1px solid var(--border);
cursor: pointer; transition: background var(--transition);
padding: 4px 4px 0; min-width: 0;
background: var(--day-bg);
}
.month-col:last-child { border-right: none; }
.month-col:hover { background: var(--bg-hover); }
.month-col.today { background: color-mix(in srgb, var(--today-color) 10%, transparent); }
.month-col.month-selected { background: var(--primary-dim); }
.month-col:hover { background: var(--day-hover-color); }
.month-col.today { background: color-mix(in srgb, var(--today-bg-base) 10%, transparent); }
.month-col.month-selected { background: color-mix(in srgb, var(--day-selected-base) 15%, transparent); }
.month-col.month-selected .cell-day { border: 2px solid var(--primary); color: var(--primary); font-weight: 700; }
.month-col.month-selected .cell-day.today { background: var(--today-color); color: #fff; border: none; }
.month-col.other-month .cell-day { color: var(--text-3); }
@@ -786,7 +803,7 @@ a { color: var(--primary); text-decoration: none; }
padding: 4px 12px; cursor: pointer;
border-radius: 6px; margin: 0 4px;
}
.mop-row:hover { background: var(--bg-hover); }
.mop-row:hover { background: var(--hover-highlight); }
.mop-dot {
width: 8px; height: 8px; border-radius: 50%;
flex-shrink: 0;
@@ -847,7 +864,7 @@ a { color: var(--primary); text-decoration: none; }
border-left: 1px solid var(--border); cursor: pointer;
transition: background var(--transition);
}
.week-day-header:hover { background: var(--bg-hover); }
.week-day-header:hover { background: var(--day-hover-color); }
.week-day-header .day-name {
font-size: 11px; font-weight: 600; text-transform: uppercase;
letter-spacing: .5px; color: var(--text-2);
@@ -999,7 +1016,7 @@ a { color: var(--primary); text-decoration: none; }
border-radius: 4px;
min-height: 36px;
}
.qtr-cell:hover { background: var(--bg-hover); }
.qtr-cell:hover { background: var(--day-hover-color); }
.qtr-cell.today .qtr-day-num {
background: var(--today-color, var(--primary));
color: #fff;
@@ -1062,7 +1079,7 @@ a { color: var(--primary); text-decoration: none; }
cursor: pointer; transition: background var(--transition);
margin-left: 54px; margin-bottom: 4px;
}
.agenda-event:hover { background: var(--bg-hover); }
.agenda-event:hover { background: var(--day-hover-color); }
.agenda-event.past { opacity: .45; }
.agenda-ev-color { width: 10px; height: 10px; border-radius: 50%; margin-top: 4px; flex-shrink: 0; }
.agenda-ev-info { flex: 1; }
@@ -1114,7 +1131,7 @@ a { color: var(--primary); text-decoration: none; }
display: flex; align-items: center; justify-content: center;
transition: background var(--transition), color var(--transition);
}
.rec-day-btn:hover { background: var(--bg-hover); }
.rec-day-btn:hover { background: var(--hover-highlight); }
.rec-day-btn.active { background: var(--primary); color: #fff; border-color: var(--primary); }
/* ── Day Context Menu ──────────────────────────────────── */
@@ -1127,7 +1144,7 @@ a { color: var(--primary); text-decoration: none; }
.ctx-item {
padding: 8px 16px; font-size: 13px; color: var(--text-1); cursor: pointer;
}
.ctx-item:hover { background: var(--bg-hover); }
.ctx-item:hover { background: var(--hover-highlight); }
/* ── Event Popup ──────────────────────────────────────────
Layout: Color-Dot + Title links, kleine Icon-Toolbar rechts oben.
@@ -1251,7 +1268,7 @@ a { color: var(--primary); text-decoration: none; }
border-radius: 10px;
transition: background var(--transition);
}
.popup-copy-item:hover { background: var(--bg-hover); }
.popup-copy-item:hover { background: var(--hover-highlight); }
.popup-copy-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.popup-copy-edit-toggle {
display: flex; align-items: center; gap: 8px;
@@ -1300,7 +1317,7 @@ a { color: var(--primary); text-decoration: none; }
background: none; border: none; cursor: pointer;
transition: background .15s, color .15s;
}
.settings-nav-btn:hover { background: var(--bg-hover); color: var(--text-1); }
.settings-nav-btn:hover { background: var(--hover-highlight); color: var(--text-1); }
.settings-nav-btn.active { background: var(--primary-dim); color: var(--primary); font-weight: 600; }
.settings-panels {
flex: 1; overflow-y: auto; padding: 24px 28px;
@@ -1338,7 +1355,7 @@ a { color: var(--primary); text-decoration: none; }
transition: background var(--transition), color var(--transition);
}
.contrast-btn:last-child { border-right: none; }
.contrast-btn:hover { background: var(--bg-hover); color: var(--text-1); }
.contrast-btn:hover { background: var(--hover-highlight); color: var(--text-1); }
.contrast-btn.active { background: var(--primary); color: #fff; }
.contrast-btn span { font-size: 18px; font-weight: 700; line-height: 1; }
.contrast-btn.active span { color: #fff !important; }
@@ -1365,6 +1382,14 @@ a { color: var(--primary); text-decoration: none; }
.sync-global-row .panel-desc { margin: 0; }
.settings-table { display: flex; flex-direction: column; }
.theme-io-row { display: flex; align-items: center; gap: 8px; margin-top: 16px; flex-wrap: wrap; }
.theme-io-help {
display: inline-flex; align-items: center; justify-content: center;
width: 20px; height: 20px; border-radius: 50%;
background: var(--bg-hover); color: var(--text-2);
font-size: 12px; font-weight: 700; text-decoration: none;
}
.theme-io-help:hover { background: var(--hover-highlight); color: var(--text-1); }
.settings-table-section {
font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
color: var(--text-3); margin: 18px 0 6px; padding: 0 2px;
@@ -1421,7 +1446,7 @@ a { color: var(--primary); text-decoration: none; }
transition: color var(--transition), background var(--transition);
justify-self: center;
}
.sync-icon:hover { background: var(--bg-hover); color: var(--text-2); }
.sync-icon:hover { background: var(--hover-highlight); 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; }
@@ -1713,7 +1738,7 @@ a { color: var(--primary); text-decoration: none; }
padding: 2px 6px; border-radius: var(--radius-sm);
transition: background var(--transition), color var(--transition);
}
.dtp-nav-btn:hover { background: var(--bg-hover); color: var(--text-1); }
.dtp-nav-btn:hover { background: var(--hover-highlight); color: var(--text-1); }
/* Day-of-week headers */
.dtp-grid {
display: grid; grid-template-columns: repeat(7, 1fr);
@@ -1730,9 +1755,9 @@ a { color: var(--primary); text-decoration: none; }
font-size: 13px; font-weight: 500; color: var(--text-1);
cursor: pointer; transition: background var(--transition);
}
.dtp-day:hover { background: var(--bg-hover); }
.dtp-day:hover { background: var(--day-hover-color); }
.dtp-day.other { color: var(--text-3); }
.dtp-day.other:hover { background: var(--bg-hover); }
.dtp-day.other:hover { background: var(--day-hover-color); }
.dtp-day.today { color: var(--primary); font-weight: 700; }
.dtp-day.selected {
background: var(--primary) !important;
@@ -2327,7 +2352,7 @@ a { color: var(--primary); text-decoration: none; }
.group-emoji { flex: 0 0 auto; font-size: 16px; cursor: pointer; line-height: 1; }
.cal-shared-flag { flex: 0 0 auto; font-size: 12px; opacity: .8; }
/* Read-only (shared with me) indicator: a struck-through pencil. */
.cal-readonly-flag { color: var(--text-3); opacity: .7; margin-left: 2px; }
.cal-readonly-flag { color: var(--icon-inactive-color); opacity: .7; margin-left: 2px; }
.group-icon-picker { display: flex; flex-wrap: wrap; gap: 6px; }
.group-icon-opt {
width: 38px; height: 38px;