Redesign the setup flow and consolidate the CSS into one primitives layer

Onboarding asks explicitly for one or two parents and shows one card per
parent for balances; the balance form is per parent with inline
validation; the partner can be filled from the FK deduction or added
from it. Settings and Dashboard reuse the same form.

CSS: bare buttons carry no colour any more; buttons, cards, choice
cards, fields, alerts, badges, eyebrow and modal chrome are defined once
in primitives.css. Media queries live only in responsive.css. A style
guard test blocks hex, !important and stray media queries. Bilingual
fixes: calendar selects size by content, header no longer wraps. Vitest
is capped at four workers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 12:27:29 +02:00
parent e4719cb9ae
commit ae6db7e16f
105 changed files with 5683 additions and 3690 deletions
+72 -26
View File
@@ -38,45 +38,91 @@ export function Auth() {
}
}
// Switching tabs clears any error from the previous tab's attempt — a
// failed login's error text should not linger, confusingly, once the
// person has moved on to creating an account (or back).
const switchTab = (register: boolean) => {
setIsRegister(register)
setError(null)
}
return (
<main className="auth-page">
<div className="auth-page__card">
<p className="auth-page__brand">{t('app.name')}</p>
<div className="auth-page__card card">
<p className="eyebrow auth-page__brand">{t('app.name')}</p>
<div className="auth-page__tabs">
<button
type="button"
className="btn btn--ghost auth-page__tab"
aria-pressed={!isRegister}
onClick={() => switchTab(false)}
data-testid="auth-tab-login"
>
{t('auth.loginButton')}
</button>
<button
type="button"
className="btn btn--ghost auth-page__tab"
aria-pressed={isRegister}
onClick={() => switchTab(true)}
data-testid="auth-tab-register"
>
{t('auth.createAccountButton')}
</button>
</div>
<h1 className="auth-page__title">{isRegister ? t('auth.createAccountTitle') : t('auth.loginTitle')}</h1>
<form className="auth-page__form" onSubmit={(e) => void handleSubmit(e)}>
{isRegister && (
<input
placeholder={t('auth.namePlaceholder')}
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
/>
<div className="field">
<label htmlFor="auth-name" className="field__label">
{t('auth.nameLabel')}
</label>
<input
id="auth-name"
className="field__control"
placeholder={t('auth.namePlaceholder')}
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
/>
</div>
)}
<input
type="email"
data-testid="auth-email"
placeholder={t('auth.emailPlaceholder')}
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
data-testid="auth-password"
placeholder={t('auth.passwordPlaceholder')}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<div className="field">
<label htmlFor="auth-email" className="field__label">
{t('auth.emailLabel')}
</label>
<input
id="auth-email"
type="email"
data-testid="auth-email"
className="field__control"
placeholder={t('auth.emailPlaceholder')}
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="field">
<label htmlFor="auth-password" className="field__label">
{t('auth.passwordLabel')}
</label>
<input
id="auth-password"
type="password"
data-testid="auth-password"
className="field__control"
placeholder={t('auth.passwordPlaceholder')}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{error && (
<p role="alert" className="auth-page__error">
<p role="alert" className="alert alert--error">
{error}
</p>
)}
<button type="submit" data-testid="auth-submit">
<button type="submit" className="btn btn--primary btn--block" data-testid="auth-submit">
{isRegister ? t('auth.registerButton') : t('auth.loginButton')}
</button>
</form>
<button type="button" className="auth-page__switch button--ghost" onClick={() => setIsRegister((v) => !v)}>
{isRegister ? t('auth.haveAccountButton') : t('auth.createAccountButton')}
</button>
</div>
</main>
)