Merge remote-tracking branch 'gitlab/claude/src-branding' into dev-merge-batch1

This commit is contained in:
Bernd Rodler
2026-08-05 17:58:14 +02:00
9 changed files with 67 additions and 13 deletions
+11 -3
View File
@@ -134,12 +134,20 @@ export default function LoginPage() {
const isMobileHandoff = Boolean(mobileRedirectUri);
const { login, loginDemo, isLoading, error, clearError, isAuthenticated } = useAuthStore();
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 { appName, jmapServerUrl: configuredServerUrl, oauthEnabled, oauthOnly, oauthClientId: globalOauthClientId, oauthIssuerUrl: globalOauthIssuerUrl, oauthScopes, rememberMeEnabled, devMode, demoMode, loginLogoLightUrl, loginLogoDarkUrl, loginLogoLightUrlIsCustom, loginLogoDarkUrlIsCustom, 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);
// an explicitly-configured logo (Branding tab / LOGIN_LOGO_*_URL) wins
// over that, falling back to the theme's logo only when nothing was set.
const effLoginLogo = resolveThemeLogo(
installedThemes,
activeThemeId,
resolvedTheme === 'dark',
loginLogoLightUrl,
loginLogoDarkUrl,
loginLogoLightUrlIsCustom || loginLogoDarkUrlIsCustom,
);
// 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.
+15
View File
@@ -44,6 +44,17 @@ export async function GET(request: NextRequest) {
return configManager.get<T>(key, fallback);
};
// Whether a logo field was actually set by an operator (Branding tab,
// an env var, or a per-domain override) rather than left at its default -
// consumed by resolveThemeLogo() so an explicit choice here wins over the
// active theme's own built-in logo, instead of being silently shadowed by
// it. See lib/theme-logo.ts.
const configSources = configManager.getAllWithSources();
const isLogoOverridden = (key: BrandingOverrideKey): boolean =>
typeof domainOverrides[key] === 'string' && domainOverrides[key]!.length > 0
? true
: configSources[key]?.source !== 'default';
const appName =
branded<string>('appName', '') || process.env.NEXT_PUBLIC_APP_NAME || 'Webmail';
const jmapServerUrl = configManager.get<string>('jmapServerUrl') || process.env.NEXT_PUBLIC_JMAP_SERVER_URL || '';
@@ -68,8 +79,12 @@ export async function GET(request: NextRequest) {
faviconUrl: branded<string>('faviconUrl', '/branding/Bulwark_Favicon.svg'),
appLogoLightUrl: branded<string>('appLogoLightUrl', ''),
appLogoDarkUrl: branded<string>('appLogoDarkUrl', ''),
appLogoLightUrlIsCustom: isLogoOverridden('appLogoLightUrl'),
appLogoDarkUrlIsCustom: isLogoOverridden('appLogoDarkUrl'),
loginLogoLightUrl: branded<string>('loginLogoLightUrl', '/branding/Bulwark_Logo_Color.svg'),
loginLogoDarkUrl: branded<string>('loginLogoDarkUrl', '/branding/Bulwark_Logo_White.svg'),
loginLogoLightUrlIsCustom: isLogoOverridden('loginLogoLightUrl'),
loginLogoDarkUrlIsCustom: isLogoOverridden('loginLogoDarkUrl'),
loginCompanyName: branded<string>('loginCompanyName', ''),
loginImprintUrl: branded<string>('loginImprintUrl', ''),
loginPrivacyPolicyUrl: branded<string>('loginPrivacyPolicyUrl', ''),
+2 -2
View File
@@ -189,7 +189,7 @@ export function NavigationRail({
const t = useTranslations("sidebar");
const pathname = usePathname();
const router = useRouter();
const { appLogoLightUrl, appLogoDarkUrl } = useConfig();
const { appLogoLightUrl, appLogoDarkUrl, appLogoLightUrlIsCustom, appLogoDarkUrlIsCustom } = useConfig();
const resolvedTheme = useThemeStore((s) => s.resolvedTheme);
const activeThemeId = useThemeStore((s) => s.activeThemeId);
const installedThemes = useThemeStore((s) => s.installedThemes);
@@ -465,7 +465,7 @@ export function NavigationRail({
)}
>
{(() => {
const logoUrl = withBasePath(resolveThemeLogo(installedThemes, activeThemeId, resolvedTheme === 'dark', appLogoLightUrl, appLogoDarkUrl));
const logoUrl = withBasePath(resolveThemeLogo(installedThemes, activeThemeId, resolvedTheme === 'dark', appLogoLightUrl, appLogoDarkUrl, appLogoLightUrlIsCustom || appLogoDarkUrlIsCustom));
return logoUrl ? (
<div className="flex items-center justify-center py-3 px-1">
<img
+16
View File
@@ -20,8 +20,12 @@ interface ConfigData {
faviconUrl: string;
appLogoLightUrl: string;
appLogoDarkUrl: string;
appLogoLightUrlIsCustom: boolean;
appLogoDarkUrlIsCustom: boolean;
loginLogoLightUrl: string;
loginLogoDarkUrl: string;
loginLogoLightUrlIsCustom: boolean;
loginLogoDarkUrlIsCustom: boolean;
loginCompanyName: string;
loginImprintUrl: string;
loginPrivacyPolicyUrl: string;
@@ -105,8 +109,12 @@ export function useConfig(): AppConfig {
faviconUrl: configCache?.faviconUrl || '/branding/Bulwark_Favicon.svg',
appLogoLightUrl: configCache?.appLogoLightUrl || '',
appLogoDarkUrl: configCache?.appLogoDarkUrl || '',
appLogoLightUrlIsCustom: configCache?.appLogoLightUrlIsCustom || false,
appLogoDarkUrlIsCustom: configCache?.appLogoDarkUrlIsCustom || false,
loginLogoLightUrl: configCache?.loginLogoLightUrl || '/branding/Bulwark_Logo_Color.svg',
loginLogoDarkUrl: configCache?.loginLogoDarkUrl || '/branding/Bulwark_Logo_White.svg',
loginLogoLightUrlIsCustom: configCache?.loginLogoLightUrlIsCustom || false,
loginLogoDarkUrlIsCustom: configCache?.loginLogoDarkUrlIsCustom || false,
loginCompanyName: configCache?.loginCompanyName || '',
loginImprintUrl: configCache?.loginImprintUrl || '',
loginPrivacyPolicyUrl: configCache?.loginPrivacyPolicyUrl || '',
@@ -146,8 +154,12 @@ export function useConfig(): AppConfig {
faviconUrl: configCache.faviconUrl,
appLogoLightUrl: configCache.appLogoLightUrl,
appLogoDarkUrl: configCache.appLogoDarkUrl,
appLogoLightUrlIsCustom: configCache.appLogoLightUrlIsCustom,
appLogoDarkUrlIsCustom: configCache.appLogoDarkUrlIsCustom,
loginLogoLightUrl: configCache.loginLogoLightUrl,
loginLogoDarkUrl: configCache.loginLogoDarkUrl,
loginLogoLightUrlIsCustom: configCache.loginLogoLightUrlIsCustom,
loginLogoDarkUrlIsCustom: configCache.loginLogoDarkUrlIsCustom,
loginCompanyName: configCache.loginCompanyName,
loginImprintUrl: configCache.loginImprintUrl,
loginPrivacyPolicyUrl: configCache.loginPrivacyPolicyUrl,
@@ -188,8 +200,12 @@ export function useConfig(): AppConfig {
faviconUrl: data.faviconUrl,
appLogoLightUrl: data.appLogoLightUrl,
appLogoDarkUrl: data.appLogoDarkUrl,
appLogoLightUrlIsCustom: data.appLogoLightUrlIsCustom,
appLogoDarkUrlIsCustom: data.appLogoDarkUrlIsCustom,
loginLogoLightUrl: data.loginLogoLightUrl,
loginLogoDarkUrl: data.loginLogoDarkUrl,
loginLogoLightUrlIsCustom: data.loginLogoLightUrlIsCustom,
loginLogoDarkUrlIsCustom: data.loginLogoDarkUrlIsCustom,
loginCompanyName: data.loginCompanyName,
loginImprintUrl: data.loginImprintUrl,
loginPrivacyPolicyUrl: data.loginPrivacyPolicyUrl,
+1 -1
View File
@@ -106,7 +106,7 @@ export interface ThemePolicy {
export const DEFAULT_THEME_POLICY: ThemePolicy = {
disabledBuiltinThemes: [],
disabledThemes: [],
defaultThemeId: 'builtin-vnclagoon',
defaultThemeId: 'builtin-src',
};
export interface SettingsPolicy {
+2 -2
View File
@@ -1343,8 +1343,8 @@ export const BUILTIN_THEMES: InstalledTheme[] = [
description: 'SRC Advisory brand theme — Swiss red on white, MD3 components, light-first',
css: srcCSS,
skin: srcSkin,
logoLightUrl: '/branding/src-logo.svg',
logoDarkUrl: '/branding/src-logo.svg',
logoLightUrl: '/branding/SRC_Symbol.png',
logoDarkUrl: '/branding/SRC_Symbol.png',
variants: ['light', 'dark'],
typography: { fontSans: '"DM Sans", system-ui, -apple-system, "Segoe UI", sans-serif' },
enabled: true,
+20 -5
View File
@@ -1,10 +1,21 @@
import type { InstalledTheme } from './plugin-types';
/**
* Resolve which logo to show. When the active theme carries brand logos
* (logoLightUrl/logoDarkUrl), those win — so switching theme switches the whole
* brand (VNClagoon wordmark ↔ SRC mark). Otherwise falls back to the globally
* configured logo. Returns a raw path; the caller applies withBasePath().
* Resolve which logo to show.
*
* Precedence:
* 1. An admin-configured global logo (Branding tab / APP_LOGO_*_URL /
* LOGIN_LOGO_*_URL) - set `hasGlobalOverride` when the caller's config
* source for that field is 'admin' or 'env', not 'default'. This is
* what makes the Branding tab's existing logo fields actually take
* effect for customers who want their own mark regardless of theme.
* 2. The active theme's own brand logo (logoLightUrl/logoDarkUrl) - so
* switching theme still switches the whole brand (VNClagoon wordmark
* vs SRC mark) for anyone who HASN'T set an explicit override.
* 3. The fallback value itself (theme has no logo and nothing was
* configured).
*
* Returns a raw path; the caller applies withBasePath().
*/
export function resolveThemeLogo(
themes: InstalledTheme[],
@@ -12,7 +23,11 @@ export function resolveThemeLogo(
isDark: boolean,
fallbackLight: string,
fallbackDark: string,
hasGlobalOverride = false,
): string {
const fallback = isDark ? (fallbackDark || fallbackLight) : (fallbackLight || fallbackDark);
if (hasGlobalOverride && fallback) return fallback;
const theme = activeThemeId ? themes.find((t) => t.id === activeThemeId) : undefined;
if (theme) {
const themed = isDark
@@ -20,5 +35,5 @@ export function resolveThemeLogo(
: (theme.logoLightUrl ?? theme.logoDarkUrl);
if (themed) return themed;
}
return isDark ? (fallbackDark || fallbackLight) : (fallbackLight || fallbackDark);
return fallback;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 48 KiB