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
+89 -49
View File
@@ -99,6 +99,7 @@ describe('API response mappers (snake_case → camelCase)', () => {
})
const result = await api.getParentalLeave('pd1', 'child1')
if (!result) throw new Error('expected a mapped ParentalLeaveFetch, got null')
expect(result.personalDataUuid).toBe('pd1')
expect(result.workspaceId).toBe('ws1')
@@ -114,12 +115,24 @@ describe('API response mappers (snake_case → camelCase)', () => {
expect(result.createdAt).toBe('2026-06-14T17:09:47')
expect(result.updatedAt).toBe('2026-06-14T17:09:47')
expect(result.tierData!.sjukpenningTotal).toBe(390)
expect(result.tierData!.sjukpenningUsed).toBe(50)
expect(result.tierData!.sjukpenningLeft).toBe(340)
expect(result.tierData!.lagstaNivaTotal).toBe(90)
expect(result.tierData!.lagstaNivaUsed).toBe(10)
expect(result.tierData!.lagstaNivaLeft).toBe(80)
expect(result.tierData.sjukpenningTotal).toBe(390)
expect(result.tierData.sjukpenningUsed).toBe(50)
expect(result.tierData.sjukpenningLeft).toBe(340)
expect(result.tierData.lagstaNivaTotal).toBe(90)
expect(result.tierData.lagstaNivaUsed).toBe(10)
expect(result.tierData.lagstaNivaLeft).toBe(80)
})
it('returns null when nothing has been saved yet (the backend returns 200 with a null body)', async () => {
fetchMock.mockResolvedValue({
ok: true,
status: 200,
json: async () => null,
})
const result = await api.getParentalLeave('pd1', 'child1')
expect(result).toBeNull()
})
})
@@ -476,7 +489,6 @@ describe('API response mappers (snake_case → camelCase)', () => {
const rawResponse = {
id: 'ws1',
name: 'My Workspace',
planning_mode: 'couple' as const,
created_at: '2026-06-14T17:09:47',
}
@@ -490,7 +502,6 @@ describe('API response mappers (snake_case → camelCase)', () => {
expect(result.id).toBe('ws1')
expect(result.name).toBe('My Workspace')
expect(result.planningMode).toBe('couple')
expect(result.createdAt).toBe('2026-06-14T17:09:47')
})
})
@@ -849,7 +860,6 @@ describe('endpoint functions (representative coverage)', () => {
json: async () => ({
id: 'ws1',
name: 'Home',
planning_mode: 'solo',
created_at: '2026-01-01',
}),
})
@@ -867,22 +877,19 @@ describe('endpoint functions (representative coverage)', () => {
json: async () => ({
id: 'ws1',
name: 'New name',
planning_mode: 'couple',
created_at: '2026-01-01',
}),
})
const result = await api.updateWorkspace('ws1', {
name: 'New name',
planningMode: 'couple',
})
expect(lastRequest()).toEqual({ url: '/api/workspace/ws1', method: 'PUT' })
expect(lastRequestBody()).toEqual({
name: 'New name',
planning_mode: 'couple',
})
expect(result.planningMode).toBe('couple')
expect(result.name).toBe('New name')
})
it('addPartner posts the display name', async () => {
@@ -929,54 +936,87 @@ describe('endpoint functions (representative coverage)', () => {
expect(lastRequestBody()).toMatchObject({ saved_days_in: 0 })
})
it('saveManualFetch applies its documented defaults for omitted optional fields', async () => {
function makeManualFetchResponse() {
return {
id: 'pf1',
personal_data_uuid: 'pd1',
workspace_id: 'ws1',
child_id: 'child1',
source: 'manual',
fetched_at: '2026-01-01',
total_days: 480,
days_used: 0,
days_left: 480,
reserved_days_total: 90,
reserved_days_left: 90,
transferable_days_left: 100,
dubbeldagar_left: 60,
tier_data: {
sjukpenning_total: 390,
sjukpenning_used: 0,
sjukpenning_left: 390,
lagsta_niva_total: 90,
lagsta_niva_used: 0,
lagsta_niva_left: 90,
},
created_at: '2026-01-01',
updated_at: '2026-01-01',
}
}
it('saveManualFetch omits optional fields entirely when not provided', async () => {
fetchMock.mockResolvedValue({
ok: true,
status: 200,
json: async () => ({
id: 'pf1',
personal_data_uuid: 'pd1',
workspace_id: 'ws1',
child_id: 'child1',
source: 'manual',
fetched_at: '2026-01-01',
total_days: 480,
days_used: 0,
days_left: 480,
reserved_days_total: 90,
reserved_days_left: 90,
transferable_days_left: 100,
dubbeldagar_left: 60,
tier_data: {
sjukpenning_total: 390,
sjukpenning_used: 0,
sjukpenning_left: 390,
lagsta_niva_total: 90,
lagsta_niva_used: 0,
lagsta_niva_left: 90,
},
created_at: '2026-01-01',
updated_at: '2026-01-01',
}),
json: async () => makeManualFetchResponse(),
})
await api.saveManualFetch({
personalDataUuid: 'pd1',
workspaceId: 'ws1',
childId: 'child1',
totalDays: 480,
daysUsed: 0,
reservedDaysLeft: 90,
transferableDaysLeft: 100,
sjukpenningUsed: 0,
lagstaNivaUsed: 0,
sjukpenningLeft: 195,
lagstaNivaLeft: 45,
dubbeldagarLeft: 60,
})
const body = lastRequestBody()
expect(body).toMatchObject({
personal_data_uuid: 'pd1',
workspace_id: 'ws1',
child_id: 'child1',
sjukpenning_left: 195,
lagsta_niva_left: 45,
dubbeldagar_left: 60,
})
expect(body).not.toHaveProperty('reserved_days_left')
expect(body).not.toHaveProperty('total_days')
expect(body).not.toHaveProperty('source')
})
it('saveManualFetch sends optional fields, including a null reservedDaysLeft, when provided', async () => {
fetchMock.mockResolvedValue({
ok: true,
status: 200,
json: async () => makeManualFetchResponse(),
})
await api.saveManualFetch({
personalDataUuid: 'pd1',
workspaceId: 'ws1',
childId: 'child1',
sjukpenningLeft: 195,
lagstaNivaLeft: 45,
dubbeldagarLeft: 60,
reservedDaysLeft: null,
totalDays: 240,
source: 'deduced',
})
expect(lastRequestBody()).toMatchObject({
reserved_days_total: 90,
dubbeldagar_left: 60,
sjukpenning_total: 390,
lagsta_niva_total: 90,
reserved_days_left: null,
total_days: 240,
source: 'deduced',
})
})