From 36301018656d9fe97e7df692199f768cc2081c0f Mon Sep 17 00:00:00 2001 From: Maarten Draijer Date: Mon, 29 Jun 2026 12:16:23 +0000 Subject: [PATCH] feat(login): configurable logo size + hideable heading/subtitle Login header customization for white-label deployments, all defaults preserve current behaviour: - LOGIN_LOGO_MAX_HEIGHT / LOGIN_LOGO_MAX_WIDTH (any CSS length): the logo box is otherwise a fixed 64x64 (w-16/h-16), which fits a wide wordmark to ~13px tall. When either is set, the fixed box is dropped and the logo renders at the configured size. - LOGIN_SHOW_HEADING / LOGIN_SHOW_SUBTITLE (default true): hide the {appName} heading and/or the subtitle when the logo already reads as the brand (e.g. a wordmark) and they'd be redundant. Applied to the standard login header; wired through the existing config registry (CONFIG_ENV_MAP) -> /api/config -> useConfig. Refs #519. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/(main)/[locale]/login/page.tsx | 30 +++++++++++++++++++++--------- app/api/config/route.ts | 4 ++++ hooks/use-config.ts | 16 ++++++++++++++++ lib/admin/types.ts | 9 +++++++++ 4 files changed, 50 insertions(+), 9 deletions(-) diff --git a/app/(main)/[locale]/login/page.tsx b/app/(main)/[locale]/login/page.tsx index 6cdc3838..5160a11f 100644 --- a/app/(main)/[locale]/login/page.tsx +++ b/app/(main)/[locale]/login/page.tsx @@ -133,9 +133,16 @@ 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, 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, loginCompanyName, loginImprintUrl, loginPrivacyPolicyUrl, loginWebsiteUrl, loginLogoMaxHeight, loginLogoMaxWidth, loginShowHeading, loginShowSubtitle, isLoading: configLoading, error: configError, autoSsoEnabled, embeddedMode: _embeddedMode, allowCustomJmapEndpoint, jmapServers, jmapServerAutoPickByDomain } = useConfig(); const resolvedTheme = useThemeStore((s) => s.resolvedTheme); + // 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. + const hasLogoSize = Boolean(loginLogoMaxHeight || loginLogoMaxWidth); + const loginLogoStyle = hasLogoSize + ? { maxHeight: loginLogoMaxHeight || undefined, maxWidth: loginLogoMaxWidth || undefined } + : undefined; + const [formData, setFormData] = useState({ username: "", password: "", @@ -881,19 +888,24 @@ export default function LoginPage() {
{/* Header section with logo */}
-
+
{appName}
-

- {isAddAccountMode ? t("add_account_title") : appName} -

-

- {isAddAccountMode ? t("add_account_subtitle") : (t("title") !== appName ? t("title") : "Sign in to your account")} -

+ {loginShowHeading && ( +

+ {isAddAccountMode ? t("add_account_title") : appName} +

+ )} + {loginShowSubtitle && ( +

+ {isAddAccountMode ? t("add_account_subtitle") : (t("title") !== appName ? t("title") : "Sign in to your account")} +

+ )}
{/* Form section */} diff --git a/app/api/config/route.ts b/app/api/config/route.ts index d0c79255..d9a7efd7 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -74,6 +74,10 @@ export async function GET(request: NextRequest) { loginImprintUrl: branded('loginImprintUrl', ''), loginPrivacyPolicyUrl: branded('loginPrivacyPolicyUrl', ''), loginWebsiteUrl: branded('loginWebsiteUrl', ''), + loginLogoMaxHeight: configManager.get('loginLogoMaxHeight', ''), + loginLogoMaxWidth: configManager.get('loginLogoMaxWidth', ''), + loginShowHeading: configManager.get('loginShowHeading', true), + loginShowSubtitle: configManager.get('loginShowSubtitle', true), demoMode: configManager.get('demoMode', false), allowCustomJmapEndpoint: configManager.get('allowCustomJmapEndpoint', false), jmapServers: redactJmapServers(parseJmapServers(configManager.get('jmapServers', []))), diff --git a/hooks/use-config.ts b/hooks/use-config.ts index db30f363..ccb52394 100644 --- a/hooks/use-config.ts +++ b/hooks/use-config.ts @@ -26,6 +26,10 @@ interface ConfigData { loginImprintUrl: string; loginPrivacyPolicyUrl: string; loginWebsiteUrl: string; + loginLogoMaxHeight: string; + loginLogoMaxWidth: string; + loginShowHeading: boolean; + loginShowSubtitle: boolean; demoMode: boolean; autoSsoEnabled: boolean; allowCustomJmapEndpoint: boolean; @@ -105,6 +109,10 @@ export function useConfig(): AppConfig { loginImprintUrl: configCache?.loginImprintUrl || '', loginPrivacyPolicyUrl: configCache?.loginPrivacyPolicyUrl || '', loginWebsiteUrl: configCache?.loginWebsiteUrl || '', + loginLogoMaxHeight: configCache?.loginLogoMaxHeight || '', + loginLogoMaxWidth: configCache?.loginLogoMaxWidth || '', + loginShowHeading: configCache?.loginShowHeading ?? true, + loginShowSubtitle: configCache?.loginShowSubtitle ?? true, demoMode: configCache?.demoMode || false, autoSsoEnabled: configCache?.autoSsoEnabled || false, allowCustomJmapEndpoint: configCache?.allowCustomJmapEndpoint || false, @@ -140,6 +148,10 @@ export function useConfig(): AppConfig { loginImprintUrl: configCache.loginImprintUrl, loginPrivacyPolicyUrl: configCache.loginPrivacyPolicyUrl, loginWebsiteUrl: configCache.loginWebsiteUrl, + loginLogoMaxHeight: configCache.loginLogoMaxHeight, + loginLogoMaxWidth: configCache.loginLogoMaxWidth, + loginShowHeading: configCache.loginShowHeading, + loginShowSubtitle: configCache.loginShowSubtitle, demoMode: configCache.demoMode, autoSsoEnabled: configCache.autoSsoEnabled, allowCustomJmapEndpoint: configCache.allowCustomJmapEndpoint, @@ -176,6 +188,10 @@ export function useConfig(): AppConfig { loginImprintUrl: data.loginImprintUrl, loginPrivacyPolicyUrl: data.loginPrivacyPolicyUrl, loginWebsiteUrl: data.loginWebsiteUrl, + loginLogoMaxHeight: data.loginLogoMaxHeight, + loginLogoMaxWidth: data.loginLogoMaxWidth, + loginShowHeading: data.loginShowHeading, + loginShowSubtitle: data.loginShowSubtitle, demoMode: data.demoMode, autoSsoEnabled: data.autoSsoEnabled, allowCustomJmapEndpoint: data.allowCustomJmapEndpoint, diff --git a/lib/admin/types.ts b/lib/admin/types.ts index 1a415502..57c9aa61 100644 --- a/lib/admin/types.ts +++ b/lib/admin/types.ts @@ -166,6 +166,15 @@ export const CONFIG_ENV_MAP: Record