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
+38 -9
View File
@@ -46,23 +46,50 @@ describe('Auth', () => {
personalDataUuid: null,
parentName: null,
refreshContext: vi.fn(),
planVersion: 0,
bumpPlanVersion: vi.fn(),
})
})
it('renders the login form by default', () => {
it('renders the login form by default, with the login tab active', () => {
render(<Auth />)
expect(screen.getByRole('heading', { name: 'Logga in' })).toBeInTheDocument()
expect(screen.getByPlaceholderText('E-post')).toBeInTheDocument()
expect(screen.getByPlaceholderText('Lösenord')).toBeInTheDocument()
expect(screen.queryByPlaceholderText('Namn')).not.toBeInTheDocument()
// Both tabs' text is unique except "Logga in", which the login-mode
// submit button also renders — the tabs carry their own testids so
// these queries don't collide with it.
expect(screen.getByTestId('auth-tab-login')).toHaveAttribute('aria-pressed', 'true')
expect(screen.getByTestId('auth-tab-register')).toHaveAttribute('aria-pressed', 'false')
})
it('switches to the registration form', () => {
it('switches to the registration form via the tab', () => {
render(<Auth />)
fireEvent.click(screen.getByRole('button', { name: 'Skapa konto' }))
fireEvent.click(screen.getByTestId('auth-tab-register'))
expect(screen.getByRole('heading', { name: 'Skapa konto' })).toBeInTheDocument()
expect(screen.getByPlaceholderText('Namn')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Har redan konto' })).toBeInTheDocument()
expect(screen.getByTestId('auth-tab-register')).toHaveAttribute('aria-pressed', 'true')
expect(screen.getByTestId('auth-tab-login')).toHaveAttribute('aria-pressed', 'false')
})
it('switches back to the login form via the tab', () => {
render(<Auth />)
fireEvent.click(screen.getByTestId('auth-tab-register'))
fireEvent.click(screen.getByTestId('auth-tab-login'))
expect(screen.getByRole('heading', { name: 'Logga in' })).toBeInTheDocument()
expect(screen.queryByPlaceholderText('Namn')).not.toBeInTheDocument()
})
it('clears the error banner when switching between the Log in and Create account tabs', async () => {
mockLogin.mockRejectedValue(new Error('Fel användarnamn'))
render(<Auth />)
fireEvent.click(screen.getByTestId('auth-submit'))
expect(await screen.findByRole('alert')).toHaveTextContent('Fel användarnamn')
fireEvent.click(screen.getByTestId('auth-tab-register'))
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
})
it('logs in with the entered credentials and navigates home', async () => {
@@ -71,7 +98,7 @@ describe('Auth', () => {
fireEvent.change(screen.getByPlaceholderText('E-post'), { target: { value: 'a@b.com' } })
fireEvent.change(screen.getByPlaceholderText('Lösenord'), { target: { value: 'secret' } })
fireEvent.click(screen.getByRole('button', { name: 'Logga in' }))
fireEvent.click(screen.getByTestId('auth-submit'))
await waitFor(() => expect(mockLogin).toHaveBeenCalledWith({ email: 'a@b.com', password: 'secret' }))
expect(mockNavigate).toHaveBeenCalledWith('/', { replace: true })
@@ -81,11 +108,11 @@ describe('Auth', () => {
mockRegister.mockResolvedValue(undefined)
render(<Auth />)
fireEvent.click(screen.getByRole('button', { name: 'Skapa konto' }))
fireEvent.click(screen.getByTestId('auth-tab-register'))
fireEvent.change(screen.getByPlaceholderText('Namn'), { target: { value: 'Alex' } })
fireEvent.change(screen.getByPlaceholderText('E-post'), { target: { value: 'a@b.com' } })
fireEvent.change(screen.getByPlaceholderText('Lösenord'), { target: { value: 'secret' } })
fireEvent.click(screen.getByRole('button', { name: 'Registrera' }))
fireEvent.click(screen.getByTestId('auth-submit'))
await waitFor(() =>
expect(mockRegister).toHaveBeenCalledWith({ email: 'a@b.com', password: 'secret', displayName: 'Alex' }),
@@ -97,7 +124,7 @@ describe('Auth', () => {
mockLogin.mockRejectedValue(new Error('Fel användarnamn'))
render(<Auth />)
fireEvent.click(screen.getByRole('button', { name: 'Logga in' }))
fireEvent.click(screen.getByTestId('auth-submit'))
expect(await screen.findByRole('alert')).toHaveTextContent('Fel användarnamn')
})
@@ -106,7 +133,7 @@ describe('Auth', () => {
mockLogin.mockRejectedValue('nope')
render(<Auth />)
fireEvent.click(screen.getByRole('button', { name: 'Logga in' }))
fireEvent.click(screen.getByTestId('auth-submit'))
expect(await screen.findByRole('alert')).toHaveTextContent('Något gick fel')
})
@@ -146,6 +173,8 @@ describe('Auth', () => {
personalDataUuid: null,
parentName: null,
refreshContext: vi.fn(),
planVersion: 0,
bumpPlanVersion: vi.fn(),
})
render(<Auth />)
expect(mockNavigate).not.toHaveBeenCalled()