feat(theme): per-theme brand logos (VNClagoon wordmark ↔ SRC mark)

Add optional logoLightUrl/logoDarkUrl to InstalledTheme + resolveThemeLogo()
helper; set logos on vnclagoon + src themes; login page and nav-rail prefer the
active theme's logo, falling back to the global config logo. So switching theme
switches the whole brand. 0 type errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Bernd Rodler
2026-08-03 18:23:42 +02:00
co-authored by Claude Opus 4.8
parent 1bc5a6a0ce
commit 88670d4bcf
6 changed files with 51 additions and 3 deletions
+7 -2
View File
@@ -9,6 +9,7 @@ import { Input } from "@/components/ui/input";
import { useAuthStore } from "@/stores/auth-store";
import { useAccountStore } from "@/stores/account-store";
import { useThemeStore } from "@/stores/theme-store";
import { resolveThemeLogo } from "@/lib/theme-logo";
import { useShallow } from "zustand/react/shallow";
import { useConfig } from "@/hooks/use-config";
import { apiFetch, getPathPrefix, toRouterPath, withBasePath } from "@/lib/browser-navigation";
@@ -135,6 +136,10 @@ export default function LoginPage() {
const { theme, setTheme, initializeTheme } = useThemeStore(useShallow((s) => ({ theme: s.theme, setTheme: s.setTheme, initializeTheme: s.initializeTheme })));
const { appName, jmapServerUrl: configuredServerUrl, oauthEnabled, oauthOnly, oauthClientId: globalOauthClientId, oauthIssuerUrl: globalOauthIssuerUrl, oauthScopes, rememberMeEnabled, devMode, demoMode, loginLogoLightUrl, loginLogoDarkUrl, loginCompanyName, loginImprintUrl, loginPrivacyPolicyUrl, loginWebsiteUrl, loginLogoMaxHeight, loginLogoMaxWidth, loginShowHeading, loginShowSubtitle, loginShowTotp, loginShowVersion, isLoading: configLoading, error: configError, autoSsoEnabled, embeddedMode: _embeddedMode, allowCustomJmapEndpoint, jmapServers, jmapServerAutoPickByDomain } = useConfig();
const resolvedTheme = useThemeStore((s) => s.resolvedTheme);
const { activeThemeId, installedThemes } = useThemeStore(useShallow((s) => ({ activeThemeId: s.activeThemeId, installedThemes: s.installedThemes })));
// Active theme may carry its own brand logo (VNClagoon wordmark, SRC mark);
// fall back to the globally configured login logo.
const effLoginLogo = resolveThemeLogo(installedThemes, activeThemeId, resolvedTheme === 'dark', loginLogoLightUrl, loginLogoDarkUrl);
// Login logo sizing: when a max height/width is configured, drop the fixed
// 64×64 box so the logo (e.g. a wide wordmark) can render at its true size.
@@ -740,7 +745,7 @@ export default function LoginPage() {
<div className="px-8 pt-12 pb-4 text-center">
<div className="inline-flex items-center justify-center w-20 h-20 mb-6">
<img
src={withBasePath(resolvedTheme === 'dark' ? loginLogoDarkUrl : loginLogoLightUrl)}
src={withBasePath(effLoginLogo)}
alt={appName}
className="max-w-20 max-h-20 object-contain"
/>
@@ -890,7 +895,7 @@ export default function LoginPage() {
<div className="px-8 pt-10 pb-6 text-center">
<div className={cn("inline-flex items-center justify-center mb-5", !hasLogoSize && "w-16 h-16")}>
<img
src={withBasePath(resolvedTheme === 'dark' ? loginLogoDarkUrl : loginLogoLightUrl)}
src={withBasePath(effLoginLogo)}
alt={appName}
className={cn("object-contain", !hasLogoSize && "max-w-16 max-h-16")}
style={loginLogoStyle}