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 => {