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:
@@ -22,7 +22,20 @@ export interface CalendarWeekProps {
|
||||
selectedDates: Set<string>
|
||||
parentName: string
|
||||
personalDataUuid: string
|
||||
isCurrentMonth: boolean
|
||||
/** Every workspace member's display name keyed by `personalDataUuid`, used
|
||||
* to label a band owned by someone other than the current user (a
|
||||
* partner's band must show their actual name, not the generic
|
||||
* `calendar.partner` string). Falls back to that generic string for a
|
||||
* `parentUuid` matching no known member (stale data, removed partner). */
|
||||
parentNames?: Map<string, string> | undefined
|
||||
/** The calendar's currently displayed year/month — each day in the week
|
||||
* is compared against these directly (not against `weekDates[0]`) so a
|
||||
* leading/trailing week that straddles a month boundary still activates
|
||||
* its in-month cells. See Calendar.tsx's week-building loop: a leading
|
||||
* week's `weekDates[0]` can belong entirely to the previous month even
|
||||
* though most of the row is the displayed month. */
|
||||
displayedYear: number
|
||||
displayedMonth: number
|
||||
showWeekNumbers: boolean
|
||||
onDayClick: (date: string) => void
|
||||
onPointerDown: (date: string) => void
|
||||
@@ -238,7 +251,9 @@ export function CalendarWeek({
|
||||
selectedDates,
|
||||
parentName,
|
||||
personalDataUuid,
|
||||
isCurrentMonth,
|
||||
parentNames,
|
||||
displayedYear,
|
||||
displayedMonth,
|
||||
showWeekNumbers,
|
||||
onDayClick,
|
||||
onPointerDown,
|
||||
@@ -253,32 +268,42 @@ export function CalendarWeek({
|
||||
[weekDates, plannedDays, calendarDaysByDate],
|
||||
)
|
||||
|
||||
// Bands overlay the day-cell row itself (grid-row: 1, position: absolute)
|
||||
// instead of occupying separate rows below it — see the comment on
|
||||
// `.calendar-week` in calendar.css. `barHeight`/`laneGap`/`bandBottomInset`
|
||||
// together define both each lane's vertical offset from the cell's bottom
|
||||
// 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 laneGap = 2
|
||||
const bandBottomInset = 4
|
||||
const laneCount = bandSegments.length > 0 ? Math.max(...bandSegments.map((b) => b.lane)) + 1 : 0
|
||||
const bandsReservedHeight = laneCount > 0 ? bandBottomInset + laneCount * (barHeight + laneGap) : 0
|
||||
const colOffset = showWeekNumbers ? 1 : 0
|
||||
const gridTemplateRows = laneCount > 0 ? `auto repeat(${laneCount}, ${barHeight}px)` : 'auto'
|
||||
|
||||
const firstDate = weekDates[0]
|
||||
const weekNumber = getISOWeekNumber(new Date(firstDate + 'T00:00:00Z'))
|
||||
|
||||
const weekStyle = {
|
||||
'--band-reserved-height': `${bandsReservedHeight}px`,
|
||||
} as React.CSSProperties & Record<'--band-reserved-height', string>
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`calendar-week${showWeekNumbers ? ' calendar-week--with-week-numbers' : ''}`}
|
||||
style={{ gridTemplateRows }}
|
||||
style={weekStyle}
|
||||
>
|
||||
{showWeekNumbers && (
|
||||
<div
|
||||
className="calendar__week-number"
|
||||
style={{ gridColumn: '1', gridRow: `1 / ${laneCount + 2}` }}
|
||||
>
|
||||
<div className="calendar__week-number" style={{ gridColumn: '1', gridRow: '1' }}>
|
||||
<span className="calendar__week-number-text">v.{weekNumber}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{weekDates.map((date, colIdx) => {
|
||||
const weekDateObj = new Date(weekDates[0] + 'T00:00:00Z')
|
||||
const dateObj = new Date(date + 'T00:00:00Z')
|
||||
const dayIsCurrentMonth = dateObj.getMonth() === weekDateObj.getMonth()
|
||||
const dayIsCurrentMonth =
|
||||
dateObj.getUTCFullYear() === displayedYear && dateObj.getUTCMonth() === displayedMonth
|
||||
const calendarInfo = calendarDaysByDate?.get(date)
|
||||
|
||||
return (
|
||||
@@ -289,35 +314,40 @@ export function CalendarWeek({
|
||||
>
|
||||
<CalendarDay
|
||||
date={date}
|
||||
isCurrentMonth={isCurrentMonth && dayIsCurrentMonth}
|
||||
isCurrentMonth={dayIsCurrentMonth}
|
||||
isToday={date === formatDateLocalISO(new Date())}
|
||||
isSelected={selectedDates.has(date)}
|
||||
dayType={calendarInfo?.dayType}
|
||||
holidayName={calendarInfo?.name ?? null}
|
||||
isOverride={calendarInfo?.isOverride ?? false}
|
||||
overrideIsWorkingDay={calendarInfo?.overrideIsWorkingDay ?? null}
|
||||
onClick={() => {
|
||||
if (isCurrentMonth && dayIsCurrentMonth) {
|
||||
onDayClick(date)
|
||||
}
|
||||
}}
|
||||
onPointerDown={(d) => {
|
||||
if (isCurrentMonth && dayIsCurrentMonth) {
|
||||
onPointerDown(d)
|
||||
}
|
||||
}}
|
||||
onPointerEnter={(d) => {
|
||||
if (isCurrentMonth && dayIsCurrentMonth) {
|
||||
onPointerEnter(d)
|
||||
}
|
||||
}}
|
||||
// Every cell is clickable/drag-selectable regardless of which
|
||||
// month it visually belongs to — `dayIsCurrentMonth` only
|
||||
// controls the dimmed `calendar-day--disabled` styling above.
|
||||
// Two failure modes this avoids, both seen in production:
|
||||
// (1) gating on `weekDates[0]`'s month killed an entire
|
||||
// leading week; (2) gating per-cell on the day's own month
|
||||
// (the previous fix here) still killed every adjacent-month
|
||||
// cell, so a drag could never cross a month boundary — see
|
||||
// the regression this closes in Calendar.tsx's pointerup
|
||||
// handler / drag-create-delete.spec.ts. Clicking a visible
|
||||
// adjacent-month day plans that day, same as clicking any
|
||||
// other visible day; that's a deliberate call, not an
|
||||
// oversight — silently doing nothing on a visible cell is
|
||||
// worse than acting on it.
|
||||
onClick={() => onDayClick(date)}
|
||||
onPointerDown={(d) => onPointerDown(d)}
|
||||
onPointerEnter={(d) => onPointerEnter(d)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{bandSegments.map((segment) => {
|
||||
const label = segment.parentUuid === personalDataUuid ? parentName : t('calendar.partner')
|
||||
const label =
|
||||
segment.parentUuid === personalDataUuid
|
||||
? parentName
|
||||
: (parentNames?.get(segment.parentUuid) ?? t('calendar.partner'))
|
||||
const firstRealDay = segment.dayInfos.find((d) => !d.isGap)
|
||||
const representativeType = firstRealDay?.dayType ?? 'vacation'
|
||||
const typeLabel = getLabelForType(representativeType, t)
|
||||
@@ -347,7 +377,9 @@ export function CalendarWeek({
|
||||
className={classNames}
|
||||
style={{
|
||||
gridColumn: `${segment.startCol + 1 + colOffset} / ${segment.endCol + 2 + colOffset}`,
|
||||
gridRow: String(segment.lane + 2),
|
||||
gridRow: '1',
|
||||
bottom: `${bandBottomInset + segment.lane * (barHeight + laneGap)}px`,
|
||||
height: `${barHeight}px`,
|
||||
backgroundColor: color,
|
||||
}}
|
||||
data-testid={`calendar-band-${segment.id}`}
|
||||
@@ -384,7 +416,13 @@ export function CalendarWeek({
|
||||
))}
|
||||
</div>
|
||||
<div className="calendar-band__label">
|
||||
<CalendarEvent leaveType={typeLabel} color={color} isDubbeldag={anyDubbeldag} parentLabel={label} />
|
||||
<CalendarEvent
|
||||
leaveType={typeLabel}
|
||||
color={color}
|
||||
isDubbeldag={anyDubbeldag}
|
||||
parentLabel={label}
|
||||
paintBackground={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user