Redraw the calendar as an almanac: gridless cells, typed bands

Day cells lose their borders and fills; red days, klämdagar and
Saturdays are told apart by the numeral's ink with a 12px R/K mark,
today is a filled circle, overrides and exclusions mark the numeral.
Bands carry their funding type by fill, ink and hatching and their
owner by a left cap. The preset rail becomes a chip row, the legend a
help popover, the period popover's three delete buttons fold behind
one "Ta bort…", and solo users pick a type directly in the day modal.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBRLomdLK9jF7wU7a8AA6H
This commit is contained in:
2026-09-19 19:02:02 +02:00
parent d14f86478a
commit dbef997da4
28 changed files with 1486 additions and 1109 deletions
@@ -1,6 +1,6 @@
import { ReactNode, useMemo } from 'react'
import type { PlannedLeaveDay, CalendarDayEntry } from '@/types'
import { getColorForType, getInkForType, getLabelForType, getTypeModifier } from '@/utils/leaveTypes'
import { getColorForType, getInkForType, getLabelForType, getTypeKey } from '@/utils/leaveTypes'
import { getISOWeekNumber, formatDateLocalISO } from '@/utils/dates'
import { useTranslation } from '@/i18n'
import { CalendarDay } from './CalendarDay'
@@ -311,7 +311,7 @@ export function CalendarWeek({
// edge AND, via `bandsReservedHeight`, how much bottom padding
// `.calendar-day` reserves so its number/badges/holiday name never sit
// under a band.
const barHeight = 20
const barHeight = 18
const laneGap = 2
const bandBottomInset = 4
const laneCount = bandSegments.length > 0 ? Math.max(...bandSegments.map((b) => b.lane)) + 1 : 0
@@ -398,9 +398,14 @@ export function CalendarWeek({
const ink = getInkForType(representativeType)
const anyDubbeldag = segment.dayInfos.some((d) => !d.isGap && d.isDubbeldag)
// Type by fill/ink/outline (`--vacation|--sjukpenning|--lagsta`,
// see the --c-leave-* tokens) and owner by a 2px left cap
// (`--self`/`--partner`) — never by hue, so the two axes stay
// readable side by side and in greyscale.
const classNames = [
'calendar-band',
`calendar-band--${getTypeModifier(representativeType)}`,
`calendar-band--${getTypeKey(representativeType)}`,
segment.parentUuid === personalDataUuid ? 'calendar-band--self' : 'calendar-band--partner',
segment.isBandStart ? 'calendar-band--start' : '',
segment.isBandEnd ? 'calendar-band--end' : '',
segment.wishId && segment.wishId === flashWishId ? 'calendar-band--flash' : '',
@@ -453,7 +458,7 @@ export function CalendarWeek({
key={info.date}
className={[
'calendar-band__day',
info.isGap ? 'calendar-band__day--gap' : '',
info.isGap ? 'calendar-band__day--gap' : `calendar-band__day--${getTypeKey(info.dayType ?? 'vacation')}`,
info.isPinned ? 'calendar-band__day--pinned' : '',
]
.filter(Boolean)