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

@@ -31,6 +31,7 @@ class SubscriptionUpdate(BaseModel):
url: Optional[str] = None
color: Optional[str] = None
enabled: Optional[bool] = None
sidebar_hidden: Optional[bool] = None
refresh_minutes: Optional[int] = None
reminders_enabled: Optional[bool] = None
@@ -42,6 +43,7 @@ def _sub_dict(sub: models.ICalSubscription) -> dict:
"url": sub.url,
"color": sub.color,
"enabled": sub.enabled,
"sidebar_hidden": bool(sub.sidebar_hidden),
"reminders_enabled": bool(sub.reminders_enabled),
"refresh_minutes": sub.refresh_minutes,
"last_fetched": sub.last_fetched.isoformat() if sub.last_fetched else None,
@@ -273,6 +275,8 @@ def update_subscription(
sub.color = data.color
if data.enabled is not None:
sub.enabled = data.enabled
if data.sidebar_hidden is not None:
sub.sidebar_hidden = data.sidebar_hidden
if data.refresh_minutes is not None:
sub.refresh_minutes = data.refresh_minutes
if data.reminders_enabled is not None: