From e775ed28001bbf7d948c115ac4826b6b7f7becc3 Mon Sep 17 00:00:00 2001 From: fs Date: Sat, 25 Apr 2026 21:16:26 +0200 Subject: [PATCH] refactor(login): Stripe-style split layout + multi-step polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reworks the auth flow from a single centered card into a two-pane layout — form on the left, tenant brand on the right — and tightens the multi-step login UX along the way. Major changes: LAYOUT - templates/login.phtml splits into a flex-column body so the footer spans both panes at the bottom instead of getting clipped under main. - New .login-form-pane and .login-brand-pane on a 1fr/1fr grid above 768px; mobile stacks brand on top as a slim band, form below. - Brand pane carries a soft halo + dot grid + tinted base, all derived from --app-primary so it tracks the tenant accent automatically. - Login card gets a Stripe-style hairline border + soft shadow, no Pico article > header sectioning band, h1 in semibold + tracking-tight. - The "body > main" global padding is overridden for login so the brand panel reaches the very top + bottom edges of its column. OS THEME FALLBACK - New appExplicitTheme() returns the user/tenant theme or empty string, used by login.phtml + error.phtml to OMIT data-theme entirely when no preference exists. CSS prefers-color-scheme media query then drives the theme — DB stays the source of truth, no browser-side persistence. MULTI-STEP UX - Heading is stage-aware: "Login" / "Select tenant" / "Login to {tenant}". Drops the redundant "Login credentials" subtitle. - Stage 3 gets an identity pill (icon + email + compact "use different email" button) replacing the old tenant-context block, so the user always sees which account they're signing in with regardless of multi-tenant status. - Stage 2 tenant selection drops avatars + initials — just radio + name with text-overflow ellipsis for long tenant names. - Tightens primary CTA: full-width on every stage incl.

-wrapped buttons. autofocus moves to the right input per stage (ldap_username / password). NOTICE / HELP LINKS - The placeholder "Problems logging in" link is gone (it used to point at the imprint route — misleading). show_support wired through 6 auth pages and the partial removed; architecture tests adjusted. - Help-links centered with bullet separators between items, hairline border-top so they read as secondary navigation under the main CTA. - The "Encrypted / HTTPS/TLS 1.2+" trust badge at the card bottom is removed — modern users assume HTTPS, and the badge added noise. DEAD CODE - $authLogoHref, $selectedTenantAvatarUrl, $selectedTenantInitial, $hasSelectedTenantAvatar, $canSwitchTenant — unused after the identity-pill / brand-pane move, removed from all 6 auth pages. Co-Authored-By: Claude Opus 4.7 (1M context) --- core/Support/helpers/branding.php | 29 ++ i18n/default_de.json | 1 + i18n/default_en.json | 1 + pages/auth/forgot(login).phtml | 3 - pages/auth/login(login).phtml | 96 ++---- pages/auth/register(login).phtml | 3 - pages/auth/reset(login).phtml | 3 - pages/auth/verify(login).phtml | 3 - pages/auth/verify-email(login).phtml | 3 - templates/error.phtml | 4 +- templates/login.phtml | 15 +- templates/partials/auth-help-links.phtml | 4 - .../AuthHelpLinksPageContractTest.php | 4 +- .../AuthHelpLinksPartialContractTest.php | 1 - web/css/pages/app-login.css | 294 ++++++++++-------- 15 files changed, 242 insertions(+), 222 deletions(-) diff --git a/core/Support/helpers/branding.php b/core/Support/helpers/branding.php index cd1b986..05597fd 100644 --- a/core/Support/helpers/branding.php +++ b/core/Support/helpers/branding.php @@ -53,6 +53,35 @@ function currentTheme(): string return $theme; } +/** + * Resolve the theme value to set on the html data-theme attribute. + * + * Returns an empty string when no explicit preference has been set by either + * the user or the tenant — callers should then OMIT data-theme entirely so + * the browser's prefers-color-scheme media query can drive the theme. Use + * this only for the html element. Internal consumers (theme toggle, + * logo URL resolution) keep using currentTheme() / appDefaultTheme() which + * always return a concrete theme string. + */ +function appExplicitTheme(): string +{ + $themes = appThemes(); + + if (allowUserTheme()) { + $userTheme = strtolower(trim((string) ($_SESSION['user']['theme'] ?? ''))); + if ($userTheme !== '' && isset($themes[$userTheme])) { + return $userTheme; + } + } + + $tenantTheme = strtolower(trim((string) ($_SESSION['current_tenant']['default_theme'] ?? ''))); + if ($tenantTheme !== '' && isset($themes[$tenantTheme])) { + return $tenantTheme; + } + + return ''; +} + /** * Resolve active primary color from the current tenant. * diff --git a/i18n/default_de.json b/i18n/default_de.json index 44fd906..43becf7 100644 --- a/i18n/default_de.json +++ b/i18n/default_de.json @@ -587,6 +587,7 @@ "Login provider": "Login-Anbieter", "Login required": "Login erforderlich", "Login status": "Anmeldestatus", + "Login to %s": "Login bei %s", "Login tokens": "Login-Tokens", "Login tokens cleared": "Login-Tokens gelöscht", "Logo": "Logo", diff --git a/i18n/default_en.json b/i18n/default_en.json index fc68ae2..9450ff5 100644 --- a/i18n/default_en.json +++ b/i18n/default_en.json @@ -587,6 +587,7 @@ "Login provider": "Login provider", "Login required": "Login required", "Login status": "Login status", + "Login to %s": "Login to %s", "Login tokens": "Login tokens", "Login tokens cleared": "Login tokens cleared", "Logo": "Logo", diff --git a/pages/auth/forgot(login).phtml b/pages/auth/forgot(login).phtml index 1868d1e..f8dd245 100644 --- a/pages/auth/forgot(login).phtml +++ b/pages/auth/forgot(login).phtml @@ -12,13 +12,10 @@ Buffer::set('title', t('Forgot password')); $errorNoticeId = !empty($errors) ? 'auth-forgot-error-notice' : ''; $authHelpContext = [ 'show_back_to_login' => true, - 'show_support' => true, ]; -$authLogoHref = lurl('login'); ?>

-

diff --git a/pages/auth/login(login).phtml b/pages/auth/login(login).phtml index 07544fb..7c91e52 100644 --- a/pages/auth/login(login).phtml +++ b/pages/auth/login(login).phtml @@ -28,47 +28,29 @@ $selectedTenantLabel = trim((string) ($selectedTenant['description'] ?? '')); if ($selectedTenantLabel === '') { $selectedTenantLabel = t('Select tenant'); } -$selectedTenantAvatarUrl = (string) ($selectedTenant['avatar_url'] ?? ''); -$selectedTenantInitial = strtoupper(substr($selectedTenantLabel, 0, 1)); -if ($selectedTenantInitial === '') { - $selectedTenantInitial = '?'; -} -$canSwitchTenant = count($tenantCandidates) > 1; $errors = is_array($errors ?? null) ? $errors : []; $errorNoticeId = $errors ? 'auth-login-error-notice' : ''; -$loginHeading = t('Login'); -$loginSubheading = t('Login credentials'); -$authHelpContext = ['show_support' => true]; +$authHelpContext = []; if ($stage === 'resolve_email') { + $loginHeading = t('Login'); $authHelpContext = [ 'show_register' => true, - 'show_support' => true, ]; } elseif ($stage === 'choose_tenant') { - $authHelpContext = [ - 'show_support' => true, - ]; + $loginHeading = t('Select tenant'); } else { + $loginHeading = sprintf(t('Login to %s'), $selectedTenantLabel); $authHelpContext = [ 'show_forgot' => !empty($selectedTenantMethods['local']), - 'show_support' => true, ]; } -$authLogoHref = lurl('login'); -if ($tenantHint !== '') { - $authLogoHref .= '?tenant=' . rawurlencode($tenantHint); -} $returnToSanitized = (string) ($returnToSanitized ?? ''); ?> diff --git a/pages/auth/register(login).phtml b/pages/auth/register(login).phtml index 92419d8..bf9ffa1 100644 --- a/pages/auth/register(login).phtml +++ b/pages/auth/register(login).phtml @@ -19,12 +19,9 @@ $errorNoticeId = !empty($error) ? 'auth-register-error-notice' : ''; $passwordHintId = 'auth-register-password-hints'; $authHelpContext = [ 'show_back_to_login' => true, - 'show_support' => true, ]; -$authLogoHref = lurl('login'); ?>