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
@@ -1,5 +1,5 @@
import { ReactNode, useState, useEffect } from 'react'
import type { CalendarDayEntry, PlannedLeaveDay } from '@/types'
import type { PlannedLeaveDay } from '@/types'
import { useTranslation } from '@/i18n'
import './planned-day-modal.css'
@@ -21,14 +21,6 @@ export interface PlannedDayModalProps {
isRangeMode?: boolean | undefined
dayCount?: number | undefined
defaultDayType?: 'sjukpenning' | 'lagsta' | 'vacation' | undefined
// §Task7 day popover — this date's calendar metadata (röda dagar/weekend/
// overrides), driving the "arbetsdag/ledig" switch below. `undefined`
// while the calendar window hasn't loaded metadata for this date yet;
// `null`/absent `onToggleOverride` hides the switch entirely (e.g. in
// range mode, where there is no single day to toggle).
dayInfo?: CalendarDayEntry | null | undefined
onToggleOverride?: (() => Promise<void>) | undefined
overrideError?: string | null | undefined
}
export function PlannedDayModal({
@@ -45,16 +37,12 @@ export function PlannedDayModal({
isRangeMode = false,
dayCount = 1,
defaultDayType,
dayInfo,
onToggleOverride,
overrideError = null,
}: PlannedDayModalProps): ReactNode {
const { t, locale } = useTranslation()
const [dayType, setDayType] = useState<'sjukpenning' | 'lagsta' | 'vacation'>('vacation')
const [parentId, setParentId] = useState(selectedParentId)
const [isDubbeldag, setIsDubbeldag] = useState(false)
const [localError, setLocalError] = useState<string | null>(null)
const [isTogglingOverride, setIsTogglingOverride] = useState(false)
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
useEffect(() => {
@@ -111,22 +99,6 @@ export function PlannedDayModal({
}
}
// §Task7 — the "Den här dagen är: arbetsdag / ledig" switch. There are
// only two effective states, so flipping to whichever option ISN'T
// currently selected is exactly the toggle `onToggleOverride` performs —
// no separate "set to X" call is needed. A no-op click on the
// already-selected option is ignored.
const handleSelectDayStatus = async (wantFree: boolean) => {
if (!onToggleOverride || !dayInfo) return
if (dayInfo.isFree === wantFree) return
try {
setIsTogglingOverride(true)
await onToggleOverride()
} finally {
setIsTogglingOverride(false)
}
}
if (!isOpen) return null
const displayDate = new Date(date + 'T00:00:00Z').toLocaleDateString(locale === 'sv' ? 'sv-SE' : 'en-US', {
@@ -143,137 +115,104 @@ export function PlannedDayModal({
: t('plannedDayModal.newTitle')
return (
<div className="planned-day-modal__overlay" onClick={onClose} data-testid="planned-day-modal__overlay" role="dialog">
<div className="planned-day-modal__content" onClick={(e) => e.stopPropagation()} data-testid="planned-day-modal__content">
<div className="planned-day-modal__header">
<div className="modal__overlay" onClick={onClose} data-testid="planned-day-modal__overlay" role="dialog">
<div
className="modal__content modal__content--sm"
onClick={(e) => e.stopPropagation()}
data-testid="planned-day-modal__content"
>
<div className="modal__header">
<h2 data-testid="modal-title">{modalTitle}</h2>
<button className="planned-day-modal__close" onClick={onClose} aria-label={t('plannedDayModal.closeAria')}>
<button type="button" className="btn btn--icon" onClick={onClose} aria-label={t('plannedDayModal.closeAria')}>
×
</button>
</div>
<div className="planned-day-modal__body">
<div className="modal__body">
{!isRangeMode && <p className="planned-day-modal__date">{displayDate}</p>}
{(error || localError) && (
<div className="planned-day-modal__error" role="alert">
<div className="alert alert--error" role="alert">
{error || localError}
</div>
)}
<div className="planned-day-modal__field">
<label htmlFor="day-type" className="planned-day-modal__label">
{t('plannedDayModal.leaveTypeLabel')}
</label>
<select
id="day-type"
value={dayType}
onChange={(e) => setDayType(e.target.value as 'sjukpenning' | 'lagsta' | 'vacation')}
className="planned-day-modal__select"
disabled={isSaving}
data-testid="day-type-select"
>
<option value="vacation">{t('plannedDayModal.vacationOption')}</option>
<option value="sjukpenning">{t('plannedDayModal.sjukpenningOption')}</option>
<option value="lagsta">{t('plannedDayModal.lagstaOption')}</option>
</select>
</div>
{parents.length > 1 && (
<div className="planned-day-modal__field">
<label htmlFor="parent-select" className="planned-day-modal__label">
{t('plannedDayModal.parentLabel')}
<div className="stack">
<div className="field">
<label htmlFor="day-type" className="field__label">
{t('plannedDayModal.leaveTypeLabel')}
</label>
<select
id="parent-select"
value={parentId}
onChange={(e) => setParentId(e.target.value)}
className="planned-day-modal__select"
id="day-type"
value={dayType}
onChange={(e) => setDayType(e.target.value as 'sjukpenning' | 'lagsta' | 'vacation')}
className="field__control"
disabled={isSaving}
data-testid="parent-select"
data-testid="day-type-select"
>
{parents.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
<option value="vacation">{t('plannedDayModal.vacationOption')}</option>
<option value="sjukpenning">{t('plannedDayModal.sjukpenningOption')}</option>
<option value="lagsta">{t('plannedDayModal.lagstaOption')}</option>
</select>
</div>
)}
<div className="planned-day-modal__field">
<label className="planned-day-modal__checkbox-label">
<input
type="checkbox"
checked={isDubbeldag}
onChange={(e) => setIsDubbeldag(e.target.checked)}
disabled={isSaving || dayType === 'vacation'}
data-testid="dubbeldag-checkbox"
/>
{' '}{t('plannedDayModal.doubleDayLabel')}
</label>
{dayType === 'vacation' && (
<p className="planned-day-modal__hint">
{t('plannedDayModal.doubleDayHint')}
</p>
)}
</div>
{existingDay?.isPinned && (
<p className="planned-day-modal__pinned-notice" data-testid="pinned-notice">
📌 {t('plannedDayModal.pinnedNotice')}
</p>
)}
{!isRangeMode && dayInfo && onToggleOverride && (
<div className="planned-day-modal__field">
<span className="planned-day-modal__label">{t('plannedDayModal.dayStatusLabel')}</span>
<div
className="planned-day-modal__override-switch"
role="radiogroup"
aria-label={t('plannedDayModal.dayStatusLabel')}
data-testid="day-status-switch"
>
<label className="planned-day-modal__checkbox-label">
<input
type="radio"
name="day-status"
checked={!dayInfo.isFree}
onChange={() => void handleSelectDayStatus(false)}
disabled={isSaving || isTogglingOverride}
data-testid="day-status-workday"
/>
{' '}{t('plannedDayModal.workdayOption')}
</label>
<label className="planned-day-modal__checkbox-label">
<input
type="radio"
name="day-status"
checked={dayInfo.isFree}
onChange={() => void handleSelectDayStatus(true)}
disabled={isSaving || isTogglingOverride}
data-testid="day-status-off"
/>
{' '}{t('plannedDayModal.dayOffOption')}
{parents.length > 1 && (
<div className="field">
<label htmlFor="parent-select" className="field__label">
{t('plannedDayModal.parentLabel')}
</label>
<select
id="parent-select"
value={parentId}
onChange={(e) => setParentId(e.target.value)}
className="field__control"
disabled={isSaving}
data-testid="parent-select"
>
{parents.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
</div>
{overrideError && (
<p className="planned-day-modal__error" role="alert" data-testid="day-status-error">
{overrideError}
)}
<div className="field">
<label className="planned-day-modal__checkbox-label">
<input
type="checkbox"
checked={isDubbeldag}
onChange={(e) => setIsDubbeldag(e.target.checked)}
disabled={isSaving || dayType === 'vacation'}
data-testid="dubbeldag-checkbox"
/>
{' '}{t('plannedDayModal.doubleDayLabel')}
</label>
{dayType === 'vacation' && (
<p className="planned-day-modal__hint">
{t('plannedDayModal.doubleDayHint')}
</p>
)}
</div>
)}
{existingDay?.isPinned && (
<p className="planned-day-modal__pinned-notice" data-testid="pinned-notice">
📌 {t('plannedDayModal.pinnedNotice')}
</p>
)}
</div>
</div>
<div className="planned-day-modal__footer">
<div className="modal__footer">
{!isRangeMode && existingDay && onDelete && isConfirmingDelete && (
<div className="planned-day-modal__delete-confirm" data-testid="delete-confirm">
<span className="planned-day-modal__delete-confirm-text">
<div className="modal__delete-confirm" data-testid="delete-confirm">
<span className="modal__delete-confirm-text">
{t('plannedDayModal.confirmDelete')}
</span>
<button
className="planned-day-modal__button planned-day-modal__button--secondary"
type="button"
className="btn btn--secondary"
onClick={handleCancelDelete}
disabled={isSaving}
data-testid="delete-cancel-button"
@@ -281,7 +220,8 @@ export function PlannedDayModal({
{t('plannedDayModal.cancelButton')}
</button>
<button
className="planned-day-modal__button planned-day-modal__button--danger"
type="button"
className="btn btn--danger"
onClick={() => void handleConfirmDelete()}
disabled={isSaving}
data-testid="delete-confirm-button"
@@ -294,7 +234,8 @@ export function PlannedDayModal({
<>
{!isRangeMode && existingDay && onDelete && (
<button
className="planned-day-modal__button planned-day-modal__button--danger"
type="button"
className="btn btn--danger btn--start"
onClick={handleDeleteClick}
disabled={isSaving}
data-testid="delete-button"
@@ -303,7 +244,8 @@ export function PlannedDayModal({
</button>
)}
<button
className="planned-day-modal__button planned-day-modal__button--secondary"
type="button"
className="btn btn--secondary"
onClick={onClose}
disabled={isSaving}
data-testid="cancel-button"
@@ -311,7 +253,8 @@ export function PlannedDayModal({
{t('plannedDayModal.cancelButton')}
</button>
<button
className="planned-day-modal__button planned-day-modal__button--primary"
type="button"
className="btn btn--primary"
onClick={handleSave}
disabled={isSaving}
data-testid="save-button"