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:
@@ -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-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-name { font-size: 13px; color: var(--text-2); }
|
||||||
.admin-theme-actions { display: flex; gap: 8px; margin-top: 14px; }
|
.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-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); }
|
.admin-brand-label { width: 70px; font-size: 13px; color: var(--text-2); flex-shrink: 0; }
|
||||||
.admin-brand-preview {
|
.admin-brand-preview {
|
||||||
width: 48px; height: 48px; flex-shrink: 0;
|
width: 48px; height: 48px; flex-shrink: 0;
|
||||||
display: flex; align-items: center; justify-content: center;
|
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-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 {
|
.settings-table-section {
|
||||||
font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
|
font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
|
||||||
color: var(--text-3); margin: 18px 0 6px; padding: 0 2px;
|
color: var(--text-3); margin: 18px 0 6px; padding: 0 2px;
|
||||||
|
|||||||
@@ -884,7 +884,9 @@
|
|||||||
<div id="admin-theme-grid" class="admin-theme-grid"></div>
|
<div id="admin-theme-grid" class="admin-theme-grid"></div>
|
||||||
<div class="admin-theme-actions">
|
<div class="admin-theme-actions">
|
||||||
<button class="btn btn-primary btn-sm" id="admin-theme-save" data-i18n="admin_theme_save">Standard speichern</button>
|
<button class="btn btn-primary btn-sm" id="admin-theme-save" data-i18n="admin_theme_save">Standard speichern</button>
|
||||||
|
<button class="btn btn-secondary btn-sm" id="admin-theme-import" data-i18n="admin_theme_import">Theme importieren</button>
|
||||||
<button class="btn btn-ghost btn-sm" id="admin-theme-discard" data-i18n="admin_theme_discard">Verwerfen</button>
|
<button class="btn btn-ghost btn-sm" id="admin-theme-discard" data-i18n="admin_theme_discard">Verwerfen</button>
|
||||||
|
<input type="file" id="admin-theme-file" accept=".theme.json,.json,.theme,application/json" hidden />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Branding: Logo & Favicon -->
|
<!-- Branding: Logo & Favicon -->
|
||||||
@@ -892,16 +894,26 @@
|
|||||||
<p class="panel-desc" data-i18n="admin_branding_desc">Eigenes Logo (oben links) und Favicon (Tab-Symbol) für die ganze Instanz. PNG/JPEG/WebP, max. 5 MB.</p>
|
<p class="panel-desc" data-i18n="admin_branding_desc">Eigenes Logo (oben links) und Favicon (Tab-Symbol) für die ganze Instanz. PNG/JPEG/WebP, max. 5 MB.</p>
|
||||||
<div class="admin-brand-row">
|
<div class="admin-brand-row">
|
||||||
<span class="admin-brand-label" data-i18n="admin_logo">Logo</span>
|
<span class="admin-brand-label" data-i18n="admin_logo">Logo</span>
|
||||||
<div class="admin-brand-preview" id="admin-logo-preview"></div>
|
<div class="admin-brand-preview admin-brand-preview-logo" id="admin-logo-preview"></div>
|
||||||
<button class="btn btn-secondary btn-sm" id="admin-logo-upload" data-i18n="admin_upload">Hochladen</button>
|
<div class="admin-brand-controls">
|
||||||
<button class="btn btn-ghost btn-sm" id="admin-logo-remove" data-i18n="admin_remove">Entfernen</button>
|
<div class="admin-brand-btns">
|
||||||
|
<button class="btn btn-secondary btn-sm" id="admin-logo-upload" data-i18n="admin_upload">Hochladen</button>
|
||||||
|
<button class="btn btn-ghost btn-sm" id="admin-logo-remove" data-i18n="admin_remove">Entfernen</button>
|
||||||
|
</div>
|
||||||
|
<span class="admin-brand-dims" data-i18n="admin_logo_dims">PNG/JPEG/WebP · Anzeige max. 150 × 40 px</span>
|
||||||
|
</div>
|
||||||
<input type="file" id="admin-logo-file" accept="image/png,image/jpeg,image/webp" hidden />
|
<input type="file" id="admin-logo-file" accept="image/png,image/jpeg,image/webp" hidden />
|
||||||
</div>
|
</div>
|
||||||
<div class="admin-brand-row">
|
<div class="admin-brand-row">
|
||||||
<span class="admin-brand-label" data-i18n="admin_favicon">Favicon</span>
|
<span class="admin-brand-label" data-i18n="admin_favicon">Favicon</span>
|
||||||
<div class="admin-brand-preview" id="admin-favicon-preview"></div>
|
<div class="admin-brand-preview" id="admin-favicon-preview"></div>
|
||||||
<button class="btn btn-secondary btn-sm" id="admin-favicon-upload" data-i18n="admin_upload">Hochladen</button>
|
<div class="admin-brand-controls">
|
||||||
<button class="btn btn-ghost btn-sm" id="admin-favicon-remove" data-i18n="admin_remove">Entfernen</button>
|
<div class="admin-brand-btns">
|
||||||
|
<button class="btn btn-secondary btn-sm" id="admin-favicon-upload" data-i18n="admin_upload">Hochladen</button>
|
||||||
|
<button class="btn btn-ghost btn-sm" id="admin-favicon-remove" data-i18n="admin_remove">Entfernen</button>
|
||||||
|
</div>
|
||||||
|
<span class="admin-brand-dims" data-i18n="admin_favicon_dims">PNG/JPEG/WebP · 128 × 128 px (quadratisch)</span>
|
||||||
|
</div>
|
||||||
<input type="file" id="admin-favicon-file" accept="image/png,image/jpeg,image/webp" hidden />
|
<input type="file" id="admin-favicon-file" accept="image/png,image/jpeg,image/webp" hidden />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4505,9 +4505,8 @@ function renderAdminThemeGrid() {
|
|||||||
function renderAdminBranding() {
|
function renderAdminBranding() {
|
||||||
const logoPrev = document.getElementById('admin-logo-preview');
|
const logoPrev = document.getElementById('admin-logo-preview');
|
||||||
const favPrev = document.getElementById('admin-favicon-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="">` : '';
|
||||||
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="">` : '';
|
||||||
if (favPrev) favPrev.innerHTML = instanceConfig.has_favicon ? `<img src="${instanceConfig.favicon_url}" alt="">` : ph;
|
|
||||||
const logoRemove = document.getElementById('admin-logo-remove');
|
const logoRemove = document.getElementById('admin-logo-remove');
|
||||||
const favRemove = document.getElementById('admin-favicon-remove');
|
const favRemove = document.getElementById('admin-favicon-remove');
|
||||||
if (logoRemove) logoRemove.classList.toggle('hidden', !instanceConfig.has_logo);
|
if (logoRemove) logoRemove.classList.toggle('hidden', !instanceConfig.has_logo);
|
||||||
@@ -4521,6 +4520,33 @@ function loadAdminPanel() {
|
|||||||
renderAdminBranding();
|
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) {
|
async function uploadBranding(kind, input) {
|
||||||
const file = input.files && input.files[0];
|
const file = input.files && input.files[0];
|
||||||
input.value = '';
|
input.value = '';
|
||||||
@@ -4561,6 +4587,12 @@ function bindAdminPanel() {
|
|||||||
renderAdminThemeGrid();
|
renderAdminThemeGrid();
|
||||||
restoreOwnTheme();
|
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 bindUpload = (btnId, fileId, kind) => {
|
||||||
const btn = document.getElementById(btnId);
|
const btn = document.getElementById(btnId);
|
||||||
|
|||||||
@@ -69,8 +69,12 @@ const translations = {
|
|||||||
admin_default_theme: 'Standard-Theme',
|
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_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_save: 'Standard speichern',
|
||||||
|
admin_theme_import: 'Theme importieren',
|
||||||
|
admin_theme_imported: '{count} Farben importiert – zum Übernehmen speichern',
|
||||||
admin_theme_discard: 'Verwerfen',
|
admin_theme_discard: 'Verwerfen',
|
||||||
admin_theme_saved: 'Standard-Theme gespeichert',
|
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: 'Branding',
|
||||||
admin_branding_desc: 'Eigenes Logo (oben links) und Favicon für die ganze Instanz. PNG/JPEG/WebP, max. 5 MB.',
|
admin_branding_desc: 'Eigenes Logo (oben links) und Favicon für die ganze Instanz. PNG/JPEG/WebP, max. 5 MB.',
|
||||||
admin_logo: 'Logo',
|
admin_logo: 'Logo',
|
||||||
@@ -462,8 +466,12 @@ const translations = {
|
|||||||
admin_default_theme: 'Default theme',
|
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_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_save: 'Save default',
|
||||||
|
admin_theme_import: 'Import theme',
|
||||||
|
admin_theme_imported: '{count} colours imported – click Save to apply',
|
||||||
admin_theme_discard: 'Discard',
|
admin_theme_discard: 'Discard',
|
||||||
admin_theme_saved: 'Default theme saved',
|
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: 'Branding',
|
||||||
admin_branding_desc: 'Custom logo (top-left) and favicon for the whole instance. PNG/JPEG/WebP, max 5 MB.',
|
admin_branding_desc: 'Custom logo (top-left) and favicon for the whole instance. PNG/JPEG/WebP, max 5 MB.',
|
||||||
admin_logo: 'Logo',
|
admin_logo: 'Logo',
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
// Increment APP_VERSION with every code change
|
// Increment APP_VERSION with every code change
|
||||||
export const APP_VERSION = 'v86';
|
export const APP_VERSION = 'v87';
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
// the entry HTML / version files). New releases take effect on the next
|
// the entry HTML / version files). New releases take effect on the next
|
||||||
// reload, no manual SW unregister required.
|
// reload, no manual SW unregister required.
|
||||||
|
|
||||||
const CACHE_VERSION = 'calendarr-v34';
|
const CACHE_VERSION = 'calendarr-v35';
|
||||||
const OFFLINE_SHELL = ['/', '/index.html'];
|
const OFFLINE_SHELL = ['/', '/index.html'];
|
||||||
|
|
||||||
self.addEventListener('install', event => {
|
self.addEventListener('install', event => {
|
||||||
|
|||||||
Reference in New Issue
Block a user