From 37e550726139f48f38d7b9bcc011de1713674a94 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Mon, 20 Jul 2026 14:33:59 +0200 Subject: [PATCH] feat(web): admin theme import + branding dimension hints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Admin default-theme editor: "Theme importieren" button loads a .theme.json into the editor (colours only, live preview) so an imported theme can be set as the instance default (still confirmed via "Standard speichern"). - Branding: drop the "Standard" placeholder; show pixel-dimension hints instead (logo shown at max 150×40, favicon 128×128). Logo preview box now mirrors its real topbar footprint. Co-Authored-By: Claude Opus 4.8 --- frontend/css/app.css | 13 +++++++++---- frontend/index.html | 22 +++++++++++++++++----- frontend/js/calendar.js | 38 +++++++++++++++++++++++++++++++++++--- frontend/js/i18n.js | 8 ++++++++ frontend/js/version.js | 2 +- frontend/sw.js | 2 +- 6 files changed, 71 insertions(+), 14 deletions(-) diff --git a/frontend/css/app.css b/frontend/css/app.css index de2b3e4..d361690 100644 --- a/frontend/css/app.css +++ b/frontend/css/app.css @@ -1401,15 +1401,20 @@ a { color: var(--primary); text-decoration: none; } .admin-theme-row { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 12px; } .admin-theme-name { font-size: 13px; color: var(--text-2); } .admin-theme-actions { display: flex; gap: 8px; margin-top: 14px; } -.admin-brand-row { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; } -.admin-brand-label { width: 70px; font-size: 13px; color: var(--text-2); } +.admin-brand-row { display: flex; align-items: center; gap: 12px; margin-top: 12px; flex-wrap: wrap; } +.admin-brand-label { width: 70px; font-size: 13px; color: var(--text-2); flex-shrink: 0; } .admin-brand-preview { width: 48px; height: 48px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; - background: var(--bg-hover); border-radius: var(--radius-sm); overflow: hidden; + background: var(--bg-hover); border: 1px solid var(--border-light); + border-radius: var(--radius-sm); overflow: hidden; } +/* Logo preview mirrors its real topbar footprint (max 150×40). */ +.admin-brand-preview-logo { width: 150px; height: 40px; } .admin-brand-preview img { max-width: 100%; max-height: 100%; object-fit: contain; } -.admin-brand-none { font-size: 10px; color: var(--text-3); text-align: center; padding: 2px; } +.admin-brand-controls { display: flex; flex-direction: column; gap: 4px; } +.admin-brand-btns { display: flex; gap: 8px; } +.admin-brand-dims { font-size: 11px; color: var(--text-3); } .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; diff --git a/frontend/index.html b/frontend/index.html index 267614b..27facd7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -884,7 +884,9 @@
+ +
@@ -892,16 +894,26 @@

Eigenes Logo (oben links) und Favicon (Tab-Symbol) für die ganze Instanz. PNG/JPEG/WebP, max. 5 MB.

Logo -
- - + +
+
+ + +
+ PNG/JPEG/WebP · Anzeige max. 150 × 40 px +
Favicon
- - +
+
+ + +
+ PNG/JPEG/WebP · 128 × 128 px (quadratisch) +
diff --git a/frontend/js/calendar.js b/frontend/js/calendar.js index a63006c..8809984 100644 --- a/frontend/js/calendar.js +++ b/frontend/js/calendar.js @@ -4505,9 +4505,8 @@ function renderAdminThemeGrid() { function renderAdminBranding() { const logoPrev = document.getElementById('admin-logo-preview'); const favPrev = document.getElementById('admin-favicon-preview'); - const ph = `${escHtml(t('admin_brand_default'))}`; - if (logoPrev) logoPrev.innerHTML = instanceConfig.has_logo ? `` : ph; - if (favPrev) favPrev.innerHTML = instanceConfig.has_favicon ? `` : ph; + if (logoPrev) logoPrev.innerHTML = instanceConfig.has_logo ? `` : ''; + if (favPrev) favPrev.innerHTML = instanceConfig.has_favicon ? `` : ''; const logoRemove = document.getElementById('admin-logo-remove'); const favRemove = document.getElementById('admin-favicon-remove'); if (logoRemove) logoRemove.classList.toggle('hidden', !instanceConfig.has_logo); @@ -4521,6 +4520,33 @@ function loadAdminPanel() { renderAdminBranding(); } +// Import a .theme.json into the default-theme editor (colours only). Fills the +// draft + live preview; the admin still clicks "Standard speichern" to persist. +async function importAdminTheme(input) { + const file = input.files && input.files[0]; + input.value = ''; + if (!file) return; + let incoming; + try { + const data = JSON.parse(await file.text()); + incoming = (data && data.settings && typeof data.settings === 'object') ? data.settings : data; + if (!incoming || typeof incoming !== 'object') throw new Error('bad'); + } catch (e) { showToast(t('theme_import_invalid'), true); return; } + const colorKeys = colorSettingKeys(); + let applied = 0; + for (const key of colorKeys) { + if (!(key in incoming)) continue; + const norm = normalizeHex(incoming[key], null); + if (!norm) continue; + adminThemeDraft[key] = norm; + applied++; + } + if (!applied) { showToast(t('theme_import_invalid'), true); return; } + renderAdminThemeGrid(); + previewAdminTheme(); + showToast(t('admin_theme_imported', { count: applied })); +} + async function uploadBranding(kind, input) { const file = input.files && input.files[0]; input.value = ''; @@ -4561,6 +4587,12 @@ function bindAdminPanel() { renderAdminThemeGrid(); restoreOwnTheme(); }); + const themeImport = document.getElementById('admin-theme-import'); + const themeFile = document.getElementById('admin-theme-file'); + if (themeImport && themeFile) { + themeImport.addEventListener('click', () => themeFile.click()); + themeFile.addEventListener('change', () => importAdminTheme(themeFile)); + } const bindUpload = (btnId, fileId, kind) => { const btn = document.getElementById(btnId); diff --git a/frontend/js/i18n.js b/frontend/js/i18n.js index c4713b0..93e6396 100644 --- a/frontend/js/i18n.js +++ b/frontend/js/i18n.js @@ -69,8 +69,12 @@ const translations = { admin_default_theme: 'Standard-Theme', admin_default_theme_desc: 'Gilt für alle Nutzer ohne eigene Farbe. „Zurück" bei einer Farbe springt auf diesen Standard.', admin_theme_save: 'Standard speichern', + admin_theme_import: 'Theme importieren', + admin_theme_imported: '{count} Farben importiert – zum Übernehmen speichern', admin_theme_discard: 'Verwerfen', admin_theme_saved: 'Standard-Theme gespeichert', + admin_logo_dims: 'PNG/JPEG/WebP · Anzeige max. 150 × 40 px', + admin_favicon_dims: 'PNG/JPEG/WebP · 128 × 128 px (quadratisch)', admin_branding: 'Branding', admin_branding_desc: 'Eigenes Logo (oben links) und Favicon für die ganze Instanz. PNG/JPEG/WebP, max. 5 MB.', admin_logo: 'Logo', @@ -462,8 +466,12 @@ const translations = { admin_default_theme: 'Default theme', admin_default_theme_desc: 'Applies to all users who have not set their own colour. A colour "Reset" returns to this default.', admin_theme_save: 'Save default', + admin_theme_import: 'Import theme', + admin_theme_imported: '{count} colours imported – click Save to apply', admin_theme_discard: 'Discard', admin_theme_saved: 'Default theme saved', + admin_logo_dims: 'PNG/JPEG/WebP · shown at max 150 × 40 px', + admin_favicon_dims: 'PNG/JPEG/WebP · 128 × 128 px (square)', admin_branding: 'Branding', admin_branding_desc: 'Custom logo (top-left) and favicon for the whole instance. PNG/JPEG/WebP, max 5 MB.', admin_logo: 'Logo', diff --git a/frontend/js/version.js b/frontend/js/version.js index 38a1d90..4015f3e 100644 --- a/frontend/js/version.js +++ b/frontend/js/version.js @@ -1,2 +1,2 @@ // Increment APP_VERSION with every code change -export const APP_VERSION = 'v86'; +export const APP_VERSION = 'v87'; diff --git a/frontend/sw.js b/frontend/sw.js index be4f4f3..b8f415e 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-v34'; +const CACHE_VERSION = 'calendarr-v35'; const OFFLINE_SHELL = ['/', '/index.html']; self.addEventListener('install', event => {