Merge remote-tracking branch 'gitlab/claude/src-branding' into dev-merge-batch1
This commit is contained in:
@@ -134,12 +134,20 @@ export default function LoginPage() {
|
|||||||
const isMobileHandoff = Boolean(mobileRedirectUri);
|
const isMobileHandoff = Boolean(mobileRedirectUri);
|
||||||
const { login, loginDemo, isLoading, error, clearError, isAuthenticated } = useAuthStore();
|
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 { 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 resolvedTheme = useThemeStore((s) => s.resolvedTheme);
|
||||||
const { activeThemeId, installedThemes } = useThemeStore(useShallow((s) => ({ activeThemeId: s.activeThemeId, installedThemes: s.installedThemes })));
|
const { activeThemeId, installedThemes } = useThemeStore(useShallow((s) => ({ activeThemeId: s.activeThemeId, installedThemes: s.installedThemes })));
|
||||||
// Active theme may carry its own brand logo (VNClagoon wordmark, SRC mark);
|
// Active theme may carry its own brand logo (VNClagoon wordmark, SRC mark);
|
||||||
// fall back to the globally configured login logo.
|
// an explicitly-configured logo (Branding tab / LOGIN_LOGO_*_URL) wins
|
||||||
const effLoginLogo = resolveThemeLogo(installedThemes, activeThemeId, resolvedTheme === 'dark', loginLogoLightUrl, loginLogoDarkUrl);
|
// 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
|
// 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.
|
// 64×64 box so the logo (e.g. a wide wordmark) can render at its true size.
|
||||||
|
|||||||
@@ -44,6 +44,17 @@ export async function GET(request: NextRequest) {
|
|||||||
return configManager.get<T>(key, fallback);
|
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 =
|
const appName =
|
||||||
branded<string>('appName', '') || process.env.NEXT_PUBLIC_APP_NAME || 'Webmail';
|
branded<string>('appName', '') || process.env.NEXT_PUBLIC_APP_NAME || 'Webmail';
|
||||||
const jmapServerUrl = configManager.get<string>('jmapServerUrl') || process.env.NEXT_PUBLIC_JMAP_SERVER_URL || '';
|
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'),
|
faviconUrl: branded<string>('faviconUrl', '/branding/Bulwark_Favicon.svg'),
|
||||||
appLogoLightUrl: branded<string>('appLogoLightUrl', ''),
|
appLogoLightUrl: branded<string>('appLogoLightUrl', ''),
|
||||||
appLogoDarkUrl: branded<string>('appLogoDarkUrl', ''),
|
appLogoDarkUrl: branded<string>('appLogoDarkUrl', ''),
|
||||||
|
appLogoLightUrlIsCustom: isLogoOverridden('appLogoLightUrl'),
|
||||||
|
appLogoDarkUrlIsCustom: isLogoOverridden('appLogoDarkUrl'),
|
||||||
loginLogoLightUrl: branded<string>('loginLogoLightUrl', '/branding/Bulwark_Logo_Color.svg'),
|
loginLogoLightUrl: branded<string>('loginLogoLightUrl', '/branding/Bulwark_Logo_Color.svg'),
|
||||||
loginLogoDarkUrl: branded<string>('loginLogoDarkUrl', '/branding/Bulwark_Logo_White.svg'),
|
loginLogoDarkUrl: branded<string>('loginLogoDarkUrl', '/branding/Bulwark_Logo_White.svg'),
|
||||||
|
loginLogoLightUrlIsCustom: isLogoOverridden('loginLogoLightUrl'),
|
||||||
|
loginLogoDarkUrlIsCustom: isLogoOverridden('loginLogoDarkUrl'),
|
||||||
loginCompanyName: branded<string>('loginCompanyName', ''),
|
loginCompanyName: branded<string>('loginCompanyName', ''),
|
||||||
loginImprintUrl: branded<string>('loginImprintUrl', ''),
|
loginImprintUrl: branded<string>('loginImprintUrl', ''),
|
||||||
loginPrivacyPolicyUrl: branded<string>('loginPrivacyPolicyUrl', ''),
|
loginPrivacyPolicyUrl: branded<string>('loginPrivacyPolicyUrl', ''),
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ export function NavigationRail({
|
|||||||
const t = useTranslations("sidebar");
|
const t = useTranslations("sidebar");
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { appLogoLightUrl, appLogoDarkUrl } = useConfig();
|
const { appLogoLightUrl, appLogoDarkUrl, appLogoLightUrlIsCustom, appLogoDarkUrlIsCustom } = useConfig();
|
||||||
const resolvedTheme = useThemeStore((s) => s.resolvedTheme);
|
const resolvedTheme = useThemeStore((s) => s.resolvedTheme);
|
||||||
const activeThemeId = useThemeStore((s) => s.activeThemeId);
|
const activeThemeId = useThemeStore((s) => s.activeThemeId);
|
||||||
const installedThemes = useThemeStore((s) => s.installedThemes);
|
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 ? (
|
return logoUrl ? (
|
||||||
<div className="flex items-center justify-center py-3 px-1">
|
<div className="flex items-center justify-center py-3 px-1">
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -20,8 +20,12 @@ interface ConfigData {
|
|||||||
faviconUrl: string;
|
faviconUrl: string;
|
||||||
appLogoLightUrl: string;
|
appLogoLightUrl: string;
|
||||||
appLogoDarkUrl: string;
|
appLogoDarkUrl: string;
|
||||||
|
appLogoLightUrlIsCustom: boolean;
|
||||||
|
appLogoDarkUrlIsCustom: boolean;
|
||||||
loginLogoLightUrl: string;
|
loginLogoLightUrl: string;
|
||||||
loginLogoDarkUrl: string;
|
loginLogoDarkUrl: string;
|
||||||
|
loginLogoLightUrlIsCustom: boolean;
|
||||||
|
loginLogoDarkUrlIsCustom: boolean;
|
||||||
loginCompanyName: string;
|
loginCompanyName: string;
|
||||||
loginImprintUrl: string;
|
loginImprintUrl: string;
|
||||||
loginPrivacyPolicyUrl: string;
|
loginPrivacyPolicyUrl: string;
|
||||||
@@ -105,8 +109,12 @@ export function useConfig(): AppConfig {
|
|||||||
faviconUrl: configCache?.faviconUrl || '/branding/Bulwark_Favicon.svg',
|
faviconUrl: configCache?.faviconUrl || '/branding/Bulwark_Favicon.svg',
|
||||||
appLogoLightUrl: configCache?.appLogoLightUrl || '',
|
appLogoLightUrl: configCache?.appLogoLightUrl || '',
|
||||||
appLogoDarkUrl: configCache?.appLogoDarkUrl || '',
|
appLogoDarkUrl: configCache?.appLogoDarkUrl || '',
|
||||||
|
appLogoLightUrlIsCustom: configCache?.appLogoLightUrlIsCustom || false,
|
||||||
|
appLogoDarkUrlIsCustom: configCache?.appLogoDarkUrlIsCustom || false,
|
||||||
loginLogoLightUrl: configCache?.loginLogoLightUrl || '/branding/Bulwark_Logo_Color.svg',
|
loginLogoLightUrl: configCache?.loginLogoLightUrl || '/branding/Bulwark_Logo_Color.svg',
|
||||||
loginLogoDarkUrl: configCache?.loginLogoDarkUrl || '/branding/Bulwark_Logo_White.svg',
|
loginLogoDarkUrl: configCache?.loginLogoDarkUrl || '/branding/Bulwark_Logo_White.svg',
|
||||||
|
loginLogoLightUrlIsCustom: configCache?.loginLogoLightUrlIsCustom || false,
|
||||||
|
loginLogoDarkUrlIsCustom: configCache?.loginLogoDarkUrlIsCustom || false,
|
||||||
loginCompanyName: configCache?.loginCompanyName || '',
|
loginCompanyName: configCache?.loginCompanyName || '',
|
||||||
loginImprintUrl: configCache?.loginImprintUrl || '',
|
loginImprintUrl: configCache?.loginImprintUrl || '',
|
||||||
loginPrivacyPolicyUrl: configCache?.loginPrivacyPolicyUrl || '',
|
loginPrivacyPolicyUrl: configCache?.loginPrivacyPolicyUrl || '',
|
||||||
@@ -146,8 +154,12 @@ export function useConfig(): AppConfig {
|
|||||||
faviconUrl: configCache.faviconUrl,
|
faviconUrl: configCache.faviconUrl,
|
||||||
appLogoLightUrl: configCache.appLogoLightUrl,
|
appLogoLightUrl: configCache.appLogoLightUrl,
|
||||||
appLogoDarkUrl: configCache.appLogoDarkUrl,
|
appLogoDarkUrl: configCache.appLogoDarkUrl,
|
||||||
|
appLogoLightUrlIsCustom: configCache.appLogoLightUrlIsCustom,
|
||||||
|
appLogoDarkUrlIsCustom: configCache.appLogoDarkUrlIsCustom,
|
||||||
loginLogoLightUrl: configCache.loginLogoLightUrl,
|
loginLogoLightUrl: configCache.loginLogoLightUrl,
|
||||||
loginLogoDarkUrl: configCache.loginLogoDarkUrl,
|
loginLogoDarkUrl: configCache.loginLogoDarkUrl,
|
||||||
|
loginLogoLightUrlIsCustom: configCache.loginLogoLightUrlIsCustom,
|
||||||
|
loginLogoDarkUrlIsCustom: configCache.loginLogoDarkUrlIsCustom,
|
||||||
loginCompanyName: configCache.loginCompanyName,
|
loginCompanyName: configCache.loginCompanyName,
|
||||||
loginImprintUrl: configCache.loginImprintUrl,
|
loginImprintUrl: configCache.loginImprintUrl,
|
||||||
loginPrivacyPolicyUrl: configCache.loginPrivacyPolicyUrl,
|
loginPrivacyPolicyUrl: configCache.loginPrivacyPolicyUrl,
|
||||||
@@ -188,8 +200,12 @@ export function useConfig(): AppConfig {
|
|||||||
faviconUrl: data.faviconUrl,
|
faviconUrl: data.faviconUrl,
|
||||||
appLogoLightUrl: data.appLogoLightUrl,
|
appLogoLightUrl: data.appLogoLightUrl,
|
||||||
appLogoDarkUrl: data.appLogoDarkUrl,
|
appLogoDarkUrl: data.appLogoDarkUrl,
|
||||||
|
appLogoLightUrlIsCustom: data.appLogoLightUrlIsCustom,
|
||||||
|
appLogoDarkUrlIsCustom: data.appLogoDarkUrlIsCustom,
|
||||||
loginLogoLightUrl: data.loginLogoLightUrl,
|
loginLogoLightUrl: data.loginLogoLightUrl,
|
||||||
loginLogoDarkUrl: data.loginLogoDarkUrl,
|
loginLogoDarkUrl: data.loginLogoDarkUrl,
|
||||||
|
loginLogoLightUrlIsCustom: data.loginLogoLightUrlIsCustom,
|
||||||
|
loginLogoDarkUrlIsCustom: data.loginLogoDarkUrlIsCustom,
|
||||||
loginCompanyName: data.loginCompanyName,
|
loginCompanyName: data.loginCompanyName,
|
||||||
loginImprintUrl: data.loginImprintUrl,
|
loginImprintUrl: data.loginImprintUrl,
|
||||||
loginPrivacyPolicyUrl: data.loginPrivacyPolicyUrl,
|
loginPrivacyPolicyUrl: data.loginPrivacyPolicyUrl,
|
||||||
|
|||||||
+1
-1
@@ -106,7 +106,7 @@ export interface ThemePolicy {
|
|||||||
export const DEFAULT_THEME_POLICY: ThemePolicy = {
|
export const DEFAULT_THEME_POLICY: ThemePolicy = {
|
||||||
disabledBuiltinThemes: [],
|
disabledBuiltinThemes: [],
|
||||||
disabledThemes: [],
|
disabledThemes: [],
|
||||||
defaultThemeId: 'builtin-vnclagoon',
|
defaultThemeId: 'builtin-src',
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface SettingsPolicy {
|
export interface SettingsPolicy {
|
||||||
|
|||||||
@@ -1343,8 +1343,8 @@ export const BUILTIN_THEMES: InstalledTheme[] = [
|
|||||||
description: 'SRC Advisory brand theme — Swiss red on white, MD3 components, light-first',
|
description: 'SRC Advisory brand theme — Swiss red on white, MD3 components, light-first',
|
||||||
css: srcCSS,
|
css: srcCSS,
|
||||||
skin: srcSkin,
|
skin: srcSkin,
|
||||||
logoLightUrl: '/branding/src-logo.svg',
|
logoLightUrl: '/branding/SRC_Symbol.png',
|
||||||
logoDarkUrl: '/branding/src-logo.svg',
|
logoDarkUrl: '/branding/SRC_Symbol.png',
|
||||||
variants: ['light', 'dark'],
|
variants: ['light', 'dark'],
|
||||||
typography: { fontSans: '"DM Sans", system-ui, -apple-system, "Segoe UI", sans-serif' },
|
typography: { fontSans: '"DM Sans", system-ui, -apple-system, "Segoe UI", sans-serif' },
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
+20
-5
@@ -1,10 +1,21 @@
|
|||||||
import type { InstalledTheme } from './plugin-types';
|
import type { InstalledTheme } from './plugin-types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve which logo to show. When the active theme carries brand logos
|
* Resolve which logo to show.
|
||||||
* (logoLightUrl/logoDarkUrl), those win — so switching theme switches the whole
|
*
|
||||||
* brand (VNClagoon wordmark ↔ SRC mark). Otherwise falls back to the globally
|
* Precedence:
|
||||||
* configured logo. Returns a raw path; the caller applies withBasePath().
|
* 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(
|
export function resolveThemeLogo(
|
||||||
themes: InstalledTheme[],
|
themes: InstalledTheme[],
|
||||||
@@ -12,7 +23,11 @@ export function resolveThemeLogo(
|
|||||||
isDark: boolean,
|
isDark: boolean,
|
||||||
fallbackLight: string,
|
fallbackLight: string,
|
||||||
fallbackDark: string,
|
fallbackDark: string,
|
||||||
|
hasGlobalOverride = false,
|
||||||
): string {
|
): string {
|
||||||
|
const fallback = isDark ? (fallbackDark || fallbackLight) : (fallbackLight || fallbackDark);
|
||||||
|
if (hasGlobalOverride && fallback) return fallback;
|
||||||
|
|
||||||
const theme = activeThemeId ? themes.find((t) => t.id === activeThemeId) : undefined;
|
const theme = activeThemeId ? themes.find((t) => t.id === activeThemeId) : undefined;
|
||||||
if (theme) {
|
if (theme) {
|
||||||
const themed = isDark
|
const themed = isDark
|
||||||
@@ -20,5 +35,5 @@ export function resolveThemeLogo(
|
|||||||
: (theme.logoLightUrl ?? theme.logoDarkUrl);
|
: (theme.logoLightUrl ?? theme.logoDarkUrl);
|
||||||
if (themed) return themed;
|
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 |
Reference in New Issue
Block a user