refactor(planner): a day head's pill row moves into a module of its own

The stays, hire cars and plugin label under a day's name now live in DayHeadPills, so DayPlanSidebar shrinks back under its size baseline. The stay fields are read through their types instead of an any cast, and the size, eslint and theme-disable baselines are lowered to the new counts.
This commit is contained in:
Maurice
2026-10-11 14:22:46 +02:00
parent 32606d20a6
commit c6f45b9be6
5 changed files with 119 additions and 57 deletions
+2 -2
View File
@@ -1,10 +1,10 @@
{
"src": {
"@typescript-eslint/no-non-null-assertion": 407,
"@typescript-eslint/no-non-null-assertion": 406,
"@typescript-eslint/no-misused-promises": 876,
"no-console": 88,
"complexity": 231,
"@typescript-eslint/no-explicit-any": 154,
"@typescript-eslint/no-explicit-any": 149,
"trek/no-swallowed-catch": 135,
"max-params": 35,
"trek/store-without-selector": 69,
+1 -1
View File
@@ -12,7 +12,7 @@
"src/components/PDF/TripPDF.tsx": 1097,
"src/components/Planner/DayDetailPanel.test.tsx": 2333,
"src/components/Planner/DayPlanSidebar.test.tsx": 5381,
"src/components/Planner/DayPlanSidebar.tsx": 3052,
"src/components/Planner/DayPlanSidebar.tsx": 3001,
"src/components/Planner/PlaceFormModal.test.tsx": 2864,
"src/components/Planner/PlaceFormModal.tsx": 1250,
"src/components/Planner/PlaceInspector.tsx": 1308,
@@ -30,7 +30,6 @@
"components/Roadtrip/RoadtripCategoryPicker.tsx": 2,
"components/Roadtrip/RoadtripCorridorPanel.tsx": 3,
"components/Roadtrip/RoadtripSidebarStopRows.tsx": 4,
"components/Roadtrip/RoadtripStopPopup.tsx": 1,
"components/Roadtrip/RoadtripTrackModal.tsx": 1,
"components/Roadtrip/StopKindPicker.tsx": 2,
"components/Roadtrip/dayColors.ts": 8,
@@ -0,0 +1,112 @@
import { Car, Hotel, Ticket } from 'lucide-react';
import React from 'react';
import { useTranslation } from '../../i18n';
import type { Accommodation, Reservation } from '../../types';
import { fs } from '../shared/DialogShell';
import { Tooltip } from '../shared/Tooltip';
import { SoftPill } from './planParts';
/** The second line of a day head: its stays, its hire cars and a plugin's name for the day. */
export function DayHeadPills({
dayId,
stays,
rentals,
pendingStays,
reservations,
label,
onPlaceClick,
onOpenBooking,
onOpenRental,
}: {
dayId: number;
stays: Accommodation[];
rentals: Reservation[];
pendingStays: Set<number>;
reservations: Reservation[];
label?: string | null;
onPlaceClick: (placeId: number) => void;
onOpenBooking?: (reservation: Reservation) => void;
onOpenRental: (reservation: Reservation) => void;
}): React.ReactElement {
const { t } = useTranslation();
return (
<div className="flex min-w-0 flex-nowrap items-center gap-1" style={{ gridColumn: '2 / 4' }}>
{stays.map((acc) => {
const isCheckIn = acc.start_day_id === dayId;
const isCheckOut = acc.end_day_id === dayId;
const accName = acc.place_name || acc.reservation_title || '';
// The state is said in words as well as colour: check-in green,
// check-out red, a stay in between quiet.
const state = isCheckOut && !isCheckIn ? t('day.checkOut') : isCheckIn ? t('day.checkIn') : null;
const iconTone = isCheckOut && !isCheckIn ? 'text-danger' : isCheckIn ? 'text-success' : 'text-content-faint';
// Not confirmed yet: a dashed ring says so at a glance, the tooltip in words (#2281).
const pending = pendingStays.has(acc.id);
const named = state ? `${state}: ${accName}` : accName;
const tip = pending ? `${named} (${t('reservations.pending')})` : named;
// The name opens the place, as before; a stay with a booking gets a
// second target in the same pill that opens the booking (#2363).
const stayBooking = onOpenBooking
? reservations.find((r) => r.accommodation_id != null && Number(r.accommodation_id) === acc.id)
: undefined;
return (
<span
key={acc.id}
className={`inline-flex min-w-0 max-w-max flex-[1_1_0%] items-stretch overflow-hidden rounded-full ${pending ? 'border border-dashed border-warning bg-warning-soft' : 'bg-surface-card shadow-sm'}`}
>
<Tooltip label={tip}>
<button
type="button"
data-dp="day-pill"
data-pending={pending || undefined}
onClick={(e) => {
e.stopPropagation();
if (acc.place_id) onPlaceClick(acc.place_id);
}}
className={`inline-flex min-w-0 items-center gap-1 py-[2px] font-geist font-normal text-content-secondary hover:text-content ${stayBooking ? 'pe-1.5 ps-2' : 'px-2'}`}
style={{ ...fs(10.5), cursor: acc.place_id ? 'pointer' : 'default' }}
>
<Hotel size={11} strokeWidth={2} className={`flex-none ${iconTone}`} />
<span className="truncate">{accName}</span>
</button>
</Tooltip>
{stayBooking && onOpenBooking && (
<Tooltip label={t('day.openStayBooking')}>
<button
type="button"
data-dp="day-pill-booking"
aria-label={t('day.openStayBooking')}
onClick={(e) => {
e.stopPropagation();
onOpenBooking(stayBooking);
}}
className="flex flex-none items-center border-s border-edge-faint pe-2 ps-1.5 text-content-faint hover:text-content"
>
<Ticket size={11} strokeWidth={2} />
</button>
</Tooltip>
)}
</span>
);
})}
{/* Active rental car badges */}
{rentals.map((r) => (
<button
type="button"
data-dp="day-pill"
key={`rental-${r.id}`}
onClick={(e) => {
e.stopPropagation();
onOpenRental(r);
}}
className="inline-flex min-w-0 max-w-max flex-[1_1_0%] items-center gap-1 rounded-full bg-surface-card px-2 py-[2px] font-geist font-normal text-content-secondary shadow-sm hover:text-content"
style={fs(10.5)}
>
<Car size={11} strokeWidth={2} className="flex-none text-content-faint" />
<span className="truncate">{r.title}</span>
</button>
))}
{/* A plugin's name for this day (which leg of the trip it belongs to). */}
{label && <SoftPill>{label}</SoftPill>}
</div>
);
}
@@ -70,6 +70,7 @@ import { projectDayItinerary } from '../Map/dayTourProjection'
import { usePluginDaySchedule, usePluginDayTints, dayTintBackground, dayTinted, PluginDayScheduleRow, formatScheduleMinutes } from '../Plugins/PluginDaySchedule'
import { MobileAddPlaceButton } from './DayPlanSidebarMobileAddPlaceButton'
import { DayPlanSidebarToolbar } from './DayPlanSidebarToolbar'
import { DayHeadPills } from './DayHeadPills'
import { DayPlanSidebarNoteModal } from './DayPlanSidebarNoteModal'
import { DayPlanSidebarTimeConfirmModal } from './DayPlanSidebarTimeConfirmModal'
import { DayPlanSidebarTransportDetailModal } from './DayPlanSidebarTransportDetailModal'
@@ -1735,59 +1736,9 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
</button>
</div>
{hasDayPills && (
<div className="flex min-w-0 flex-nowrap items-center gap-1" style={{ gridColumn: '2 / 4' }}>
{dayAccs.map(acc => {
const isCheckIn = acc.start_day_id === day.id
const isCheckOut = acc.end_day_id === day.id
const accName = (acc as any).place_name || (acc as any).reservation_title
// The state is said in words as well as colour: check-in green,
// check-out red, a stay in between quiet.
const state = isCheckOut && !isCheckIn ? t('day.checkOut') : isCheckIn ? t('day.checkIn') : null
const iconTone = isCheckOut && !isCheckIn ? 'text-danger' : isCheckIn ? 'text-success' : 'text-content-faint'
// Not confirmed yet: a dashed ring says so at a glance, the tooltip in words (#2281).
const pending = pendingStays.has(acc.id)
const named = state ? `${state}: ${accName}` : accName
const tip = pending ? `${named} (${t('reservations.pending')})` : named
// The name opens the place, as before; a stay with a booking gets a
// second target in the same pill that opens the booking (#2363).
const stayBooking = onOpenBooking
? reservations.find(r => r.accommodation_id != null && Number(r.accommodation_id) === acc.id)
: undefined
return (
<span key={acc.id} className={`inline-flex min-w-0 max-w-max flex-[1_1_0%] items-stretch overflow-hidden rounded-full ${pending ? 'border border-dashed border-warning bg-warning-soft' : 'bg-surface-card shadow-sm'}`}>
<Tooltip label={tip}>
<button type="button" data-dp="day-pill" data-pending={pending || undefined}
onClick={e => { e.stopPropagation(); if ((acc as any).place_id) onPlaceClick((acc as any).place_id) }}
className={`inline-flex min-w-0 items-center gap-1 py-[2px] font-geist font-normal text-content-secondary hover:text-content ${stayBooking ? 'ps-2 pe-1.5' : 'px-2'}`}
style={{ ...fs(10.5), cursor: (acc as any).place_id ? 'pointer' : 'default' }}>
<Hotel size={11} strokeWidth={2} className={`flex-none ${iconTone}`} />
<span className="truncate">{accName}</span>
</button>
</Tooltip>
{stayBooking && (
<Tooltip label={t('day.openStayBooking')}>
<button type="button" data-dp="day-pill-booking" aria-label={t('day.openStayBooking')}
onClick={e => { e.stopPropagation(); onOpenBooking!(stayBooking) }}
className="flex flex-none items-center border-s border-edge-faint ps-1.5 pe-2 text-content-faint hover:text-content">
<Ticket size={11} strokeWidth={2} />
</button>
</Tooltip>
)}
</span>
)
})}
{/* Active rental car badges */}
{activeRentals.map(r => (
<button type="button" data-dp="day-pill" key={`rental-${r.id}`} onClick={e => { e.stopPropagation(); if (onOpenBooking) onOpenBooking(r); else setTransportDetail(r) }}
className="inline-flex min-w-0 max-w-max flex-[1_1_0%] items-center gap-1 rounded-full bg-surface-card px-2 py-[2px] font-geist font-normal text-content-secondary shadow-sm hover:text-content"
style={fs(10.5)}>
<Car size={11} strokeWidth={2} className="flex-none text-content-faint" />
<span className="truncate">{r.title}</span>
</button>
))}
{/* A plugin's name for this day (which leg of the trip it belongs to). */}
{dayTint?.label && <SoftPill>{dayTint.label}</SoftPill>}
</div>
<DayHeadPills dayId={day.id} stays={dayAccs} rentals={activeRentals} pendingStays={pendingStays}
reservations={reservations} label={dayTint?.label} onPlaceClick={onPlaceClick} onOpenBooking={onOpenBooking}
onOpenRental={r => { if (onOpenBooking) onOpenBooking(r); else setTransportDetail(r) }} />
)}
</div>