feat(web): admin theme import + branding dimension hints

- 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 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-20 14:33:59 +02:00
parent 22a58e0d9f
commit 37e5507261
6 changed files with 71 additions and 14 deletions

View File

@@ -4505,9 +4505,8 @@ function renderAdminThemeGrid() {
function renderAdminBranding() {
const logoPrev = document.getElementById('admin-logo-preview');
const favPrev = document.getElementById('admin-favicon-preview');
const ph = `<span class="admin-brand-none">${escHtml(t('admin_brand_default'))}</span>`;
if (logoPrev) logoPrev.innerHTML = instanceConfig.has_logo ? `<img src="${instanceConfig.logo_url}" alt="">` : ph;
if (favPrev) favPrev.innerHTML = instanceConfig.has_favicon ? `<img src="${instanceConfig.favicon_url}" alt="">` : ph;
if (logoPrev) logoPrev.innerHTML = instanceConfig.has_logo ? `<img src="${instanceConfig.logo_url}" alt="">` : '';
if (favPrev) favPrev.innerHTML = instanceConfig.has_favicon ? `<img src="${instanceConfig.favicon_url}" alt="">` : '';
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);

View File

@@ -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',

View File

@@ -1,2 +1,2 @@
// Increment APP_VERSION with every code change
export const APP_VERSION = 'v86';
export const APP_VERSION = 'v87';