feat: migrate login page to design system styling
All checks were successful
CI / build (push) Successful in 1m26s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 57s
CI / deploy (push) Successful in 38s
CI / deploy-feature (push) Has been skipped

Replace inline styles with CSS module matching the design system's
LoginForm visual patterns. Uses proper DS class structure (divider,
social section, form fields) while keeping username-based auth
instead of the DS component's email validation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-24 16:44:52 +01:00
parent 81f85aa82d
commit e54d20bcb7
2 changed files with 146 additions and 38 deletions

View File

@@ -0,0 +1,85 @@
.page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: var(--bg-base);
}
.card {
width: 100%;
max-width: 400px;
padding: 32px;
}
.loginForm {
display: flex;
flex-direction: column;
align-items: center;
font-family: var(--font-body);
width: 100%;
}
.logo {
margin-bottom: 8px;
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
}
.subtitle {
font-size: 13px;
color: var(--text-muted);
margin: 0 0 24px;
}
.error {
width: 100%;
margin-bottom: 16px;
}
.socialSection {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
margin-bottom: 20px;
}
.divider {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
margin-bottom: 20px;
}
.dividerLine {
flex: 1;
height: 1px;
background: var(--border);
}
.dividerText {
color: var(--text-muted);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 500;
}
.fields {
display: flex;
flex-direction: column;
gap: 14px;
width: 100%;
}
.submitButton {
width: 100%;
}
.ssoButton {
width: 100%;
justify-content: center;
}

View File

@@ -3,6 +3,7 @@ import { Navigate } from 'react-router';
import { useAuthStore } from './auth-store'; import { useAuthStore } from './auth-store';
import { api } from '../api/client'; import { api } from '../api/client';
import { Card, Input, Button, Alert, FormField } from '@cameleer/design-system'; import { Card, Input, Button, Alert, FormField } from '@cameleer/design-system';
import styles from './LoginPage.module.css';
interface OidcInfo { interface OidcInfo {
clientId: string; clientId: string;
@@ -50,53 +51,75 @@ export function LoginPage() {
}; };
return ( return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: 'var(--surface-ground)' }}> <div className={styles.page}>
<Card> <Card className={styles.card}>
<form onSubmit={handleSubmit} style={{ padding: '2rem', minWidth: 360 }}> <div className={styles.loginForm}>
<div style={{ textAlign: 'center', marginBottom: '1.5rem' }}> <div className={styles.logo}>cameleer3</div>
<h1 style={{ fontSize: '1.5rem', fontWeight: 600 }}>cameleer3</h1> <p className={styles.subtitle}>Sign in to access the observability dashboard</p>
<p style={{ color: 'var(--text-secondary)', marginTop: '0.25rem', fontSize: '0.875rem' }}>
Sign in to access the observability dashboard {error && (
</p> <div className={styles.error}>
</div> <Alert variant="error">{error}</Alert>
</div>
)}
{oidc && ( {oidc && (
<> <>
<Button variant="secondary" onClick={handleOidcLogin} disabled={oidcLoading} style={{ width: '100%', marginBottom: '1rem' }}> <div className={styles.socialSection}>
{oidcLoading ? 'Redirecting...' : 'Sign in with SSO'} <Button
</Button> variant="secondary"
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', margin: '1rem 0' }}> className={styles.ssoButton}
<hr style={{ flex: 1, border: 'none', borderTop: '1px solid var(--border)' }} /> onClick={handleOidcLogin}
<span style={{ color: 'var(--text-tertiary)', fontSize: '0.75rem' }}>or</span> disabled={oidcLoading}
<hr style={{ flex: 1, border: 'none', borderTop: '1px solid var(--border)' }} /> type="button"
>
{oidcLoading ? 'Redirecting...' : 'Sign in with SSO'}
</Button>
</div>
<div className={styles.divider}>
<div className={styles.dividerLine} />
<span className={styles.dividerText}>or</span>
<div className={styles.dividerLine} />
</div> </div>
</> </>
)} )}
<FormField label="Username"> <form className={styles.fields} onSubmit={handleSubmit} aria-label="Sign in" noValidate>
<Input <FormField label="Username" htmlFor="login-username">
value={username} <Input
onChange={(e) => setUsername(e.target.value)} id="login-username"
autoFocus value={username}
autoComplete="username" onChange={(e) => setUsername(e.target.value)}
/> placeholder="Enter your username"
</FormField> autoFocus
autoComplete="username"
disabled={loading}
/>
</FormField>
<FormField label="Password"> <FormField label="Password" htmlFor="login-password">
<Input <Input
type="password" id="login-password"
value={password} type="password"
onChange={(e) => setPassword(e.target.value)} value={password}
autoComplete="current-password" onChange={(e) => setPassword(e.target.value)}
/> placeholder="••••••••"
</FormField> autoComplete="current-password"
disabled={loading}
/>
</FormField>
<Button variant="primary" disabled={loading || !username || !password} style={{ width: '100%', marginTop: '0.5rem' }}> <Button
{loading ? 'Signing in...' : 'Sign In'} variant="primary"
</Button> type="submit"
loading={loading}
{error && <div style={{ marginTop: '1rem' }}><Alert variant="error">{error}</Alert></div>} disabled={loading || !username || !password}
</form> className={styles.submitButton}
>
Sign in
</Button>
</form>
</div>
</Card> </Card>
</div> </div>
); );