refactor(login): Stripe-style split layout + multi-step polish
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. <p>-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) <noreply@anthropic.com>
This commit is contained in:
@@ -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');
|
||||
?>
|
||||
|
||||
<article class="login-card">
|
||||
<?php require templatePath('partials/auth-logo.phtml'); ?>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h1><?php e(t('Forgot password')); ?></h1>
|
||||
|
||||
@@ -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 ?? '');
|
||||
|
||||
?>
|
||||
|
||||
<article class="login-card">
|
||||
<?php require templatePath('partials/auth-logo.phtml'); ?>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h1><?php e($loginHeading); ?></h1>
|
||||
<p><?php e($loginSubheading); ?></p>
|
||||
</hgroup>
|
||||
<h1><?php e($loginHeading); ?></h1>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -115,22 +97,10 @@ $returnToSanitized = (string) ($returnToSanitized ?? '');
|
||||
if ($tenantLabel === '') {
|
||||
$tenantLabel = t('Select tenant');
|
||||
}
|
||||
$tenantAvatarUrl = (string) ($tenantCandidate['avatar_url'] ?? '');
|
||||
$tenantInitial = strtoupper(substr($tenantLabel, 0, 1));
|
||||
if ($tenantInitial === '') {
|
||||
$tenantInitial = '?';
|
||||
}
|
||||
?>
|
||||
<label class="app-field login-tenant-choice" data-tooltip="<?php e($tenantLabel); ?>" data-tooltip-pos="top">
|
||||
<label class="app-field login-tenant-choice">
|
||||
<input type="radio" name="tenant_id" value="<?php e((string) $tenantId); ?>" <?php e($tenantId === $selectedTenantId ? 'checked autofocus' : ''); ?>>
|
||||
<span class="login-tenant-choice-body">
|
||||
<?php if ($tenantAvatarUrl !== ''): ?>
|
||||
<img class="login-tenant-choice-avatar" src="<?php e($tenantAvatarUrl); ?>" alt="" loading="lazy">
|
||||
<?php else: ?>
|
||||
<span class="login-tenant-choice-avatar-placeholder"><?php e($tenantInitial); ?></span>
|
||||
<?php endif; ?>
|
||||
<span class="login-tenant-choice-text"><?php e($tenantLabel); ?></span>
|
||||
</span>
|
||||
<span class="login-tenant-choice-text"><?php e($tenantLabel); ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
@@ -149,31 +119,23 @@ $returnToSanitized = (string) ($returnToSanitized ?? '');
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<?php $hasSelectedTenantAvatar = $selectedTenantAvatarUrl !== ''; ?>
|
||||
<?php if ($canSwitchTenant): ?>
|
||||
<section class="login-tenant-context" role="status" aria-live="polite">
|
||||
<div class="login-tenant-context-main">
|
||||
<?php if ($hasSelectedTenantAvatar): ?>
|
||||
<img class="login-tenant-context-avatar" src="<?php e($selectedTenantAvatarUrl); ?>" alt="" loading="lazy">
|
||||
<?php else: ?>
|
||||
<span class="login-tenant-context-avatar-placeholder login-tenant-context-avatar-placeholder--name"><?php e($selectedTenantLabel); ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($hasSelectedTenantAvatar): ?>
|
||||
<div class="login-tenant-context-text">
|
||||
<p class="login-tenant-context-name"><?php e($selectedTenantLabel); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<form method="post" class="login-tenant-context-form">
|
||||
<input type="hidden" name="stage" value="resolve_email">
|
||||
<input type="hidden" name="email" value="<?php e($email); ?>">
|
||||
<input type="hidden" name="tenant_hint" value="<?php e($tenantHint); ?>">
|
||||
<?php if ($returnToSanitized !== ''): ?><input type="hidden" name="return_to" value="<?php e($returnToSanitized); ?>"><?php endif; ?>
|
||||
<button type="submit" class="secondary outline small"><?php e(t('Switch tenant')); ?></button>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
<aside class="login-identity-pill" role="status" aria-live="polite">
|
||||
<span class="login-identity-pill-icon" aria-hidden="true">
|
||||
<i class="bi bi-person-circle"></i>
|
||||
</span>
|
||||
<span class="login-identity-pill-email"><?php e($email); ?></span>
|
||||
<form method="post" class="login-identity-pill-reset-form">
|
||||
<input type="hidden" name="stage" value="reset">
|
||||
<?php if ($returnToSanitized !== ''): ?><input type="hidden" name="return_to" value="<?php e($returnToSanitized); ?>"><?php endif; ?>
|
||||
<button type="submit" class="secondary outline small"
|
||||
aria-label="<?php e(t('Use different email')); ?>"
|
||||
data-tooltip="<?php e(t('Use different email')); ?>"
|
||||
data-tooltip-pos="top">
|
||||
<i class="bi bi-arrow-left-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
<div class="login-methods-stack">
|
||||
<?php if (!empty($selectedTenantMethods['microsoft']) && $selectedTenantSlug !== ''): ?>
|
||||
@@ -200,7 +162,7 @@ $returnToSanitized = (string) ($returnToSanitized ?? '');
|
||||
<?php if ($returnToSanitized !== ''): ?><input type="hidden" name="return_to" value="<?php e($returnToSanitized); ?>"><?php endif; ?>
|
||||
<label for="ldap_username">
|
||||
<span><?php e(t('LDAP Username')); ?></span>
|
||||
<input required type="text" name="ldap_username" id="ldap_username" autocomplete="username" />
|
||||
<input required type="text" name="ldap_username" id="ldap_username" autocomplete="username" autofocus />
|
||||
</label>
|
||||
<label for="ldap_password">
|
||||
<span><?php e(t('Password')); ?></span>
|
||||
@@ -237,7 +199,7 @@ $returnToSanitized = (string) ($returnToSanitized ?? '');
|
||||
<?php if ($returnToSanitized !== ''): ?><input type="hidden" name="return_to" value="<?php e($returnToSanitized); ?>"><?php endif; ?>
|
||||
<label for="password">
|
||||
<span><?php e(t('Password')); ?></span>
|
||||
<input required type="password" name="password" id="password" autocomplete="current-password" <?php if ($errors): ?>aria-invalid="true" aria-describedby="<?php e($errorNoticeId); ?>"<?php endif; ?> />
|
||||
<input required type="password" name="password" id="password" autocomplete="current-password" autofocus <?php if ($errors): ?>aria-invalid="true" aria-describedby="<?php e($errorNoticeId); ?>"<?php endif; ?> />
|
||||
</label>
|
||||
<p>
|
||||
<label class="app-field inline">
|
||||
@@ -261,10 +223,4 @@ $returnToSanitized = (string) ($returnToSanitized ?? '');
|
||||
<?php endif; ?>
|
||||
|
||||
<?php require templatePath('partials/auth-help-links.phtml'); ?>
|
||||
<hr>
|
||||
<p class="login-security-note">
|
||||
<i class="bi bi-lock-fill" aria-hidden="true"></i>
|
||||
<span><?php e(t('Encrypted connection')); ?></span>
|
||||
</p>
|
||||
<small class="login-security-note-detail muted"><?php e(t('HTTPS/TLS 1.2+ in transit')); ?></small>
|
||||
</article>
|
||||
|
||||
@@ -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');
|
||||
?>
|
||||
<article class="login-card">
|
||||
<?php require templatePath('partials/auth-logo.phtml'); ?>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h1><?php e(t('Register')); ?></h1>
|
||||
|
||||
@@ -14,13 +14,10 @@ $errorNoticeId = !empty($errors) ? 'auth-reset-error-notice' : '';
|
||||
$passwordHintId = 'auth-reset-password-hints';
|
||||
$authHelpContext = [
|
||||
'show_back_to_login' => true,
|
||||
'show_support' => true,
|
||||
];
|
||||
$authLogoHref = lurl('login');
|
||||
?>
|
||||
|
||||
<article class="login-card">
|
||||
<?php require templatePath('partials/auth-logo.phtml'); ?>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h1><?php e(t('Reset password')); ?></h1>
|
||||
|
||||
@@ -13,13 +13,10 @@ Buffer::set('title', t('Verify code'));
|
||||
$errorNoticeId = !empty($errors) ? 'auth-verify-error-notice' : '';
|
||||
$authHelpContext = [
|
||||
'show_back_to_login' => true,
|
||||
'show_support' => true,
|
||||
];
|
||||
$authLogoHref = lurl('login');
|
||||
?>
|
||||
|
||||
<article class="login-card">
|
||||
<?php require templatePath('partials/auth-logo.phtml'); ?>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h1><?php e(t('Verify code')); ?></h1>
|
||||
|
||||
@@ -13,13 +13,10 @@ Buffer::set('title', t('Verify email'));
|
||||
$errorNoticeId = !empty($errors) ? 'auth-verify-email-error-notice' : '';
|
||||
$authHelpContext = [
|
||||
'show_back_to_login' => true,
|
||||
'show_support' => true,
|
||||
];
|
||||
$authLogoHref = lurl('login');
|
||||
?>
|
||||
|
||||
<article class="login-card">
|
||||
<?php require templatePath('partials/auth-logo.phtml'); ?>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h1><?php e(t('Verify email')); ?></h1>
|
||||
|
||||
Reference in New Issue
Block a user