diff --git a/client/src/components/Roadtrip/EndDayControl.tsx b/client/src/components/Roadtrip/EndDayControl.tsx index 750315f74..b05c89e6f 100644 --- a/client/src/components/Roadtrip/EndDayControl.tsx +++ b/client/src/components/Roadtrip/EndDayControl.tsx @@ -17,8 +17,8 @@ export default function EndDayControl({ active, onToggle }: EndDayControlProps) const { t } = useTranslation() const [pending, setPending] = useState(false) return ( -
- {t('roadtrip.window.endHere')} +
+ {t('roadtrip.window.endHere')} { if (pending) return setPending(true) diff --git a/client/src/components/Roadtrip/FigureBadge.tsx b/client/src/components/Roadtrip/FigureBadge.tsx index 5d6080b3e..e65ad322c 100644 --- a/client/src/components/Roadtrip/FigureBadge.tsx +++ b/client/src/components/Roadtrip/FigureBadge.tsx @@ -1,41 +1,42 @@ import React from 'react' import { Tooltip } from '../shared/Tooltip' -import { FS } from './typeScale' +import { fs } from '../shared/DialogShell' /** - * A figure in the two-part shell the rail uses everywhere: what it is on the left, the - * number on the right, one hairline between them. + * A fact on a stop's second line: what it is in the quiet ink (an icon or a word), then + * the figure. The day plan's white pill, the one its transports wear for their times, so + * a stop and the ride beside it carry their facts the same way. * * One component because the shell had been typed out six times, the stay, the walk, the * fill-up, the two findings and the limits button each carrying their own copy, and the - * copies had started to drift. The warning tone keeps the border in the warning colour - * itself, not its soft tint: at this size a tinted edge disappears into the card. + * copies had started to drift. */ const TONES = { - neutral: { - shell: 'border-edge', - lead: 'bg-surface-tertiary text-content-faint', - value: 'border-edge text-content-secondary', - }, - warning: { - shell: 'border-warning', - lead: 'bg-warning-soft text-warning', - value: 'border-warning text-warning', - }, + neutral: { lead: 'text-content-faint', value: 'text-content-secondary' }, + warning: { lead: 'text-warning', value: 'text-warning' }, + /** A confirmed booking: the word in the success colour, the figure plain. */ + success: { lead: 'text-success', value: 'text-content-secondary' }, + /** A booking still pending: the word in the warning colour, the figure plain. */ + pending: { lead: 'text-warning', value: 'text-content-secondary' }, } as const +// White with a light shadow and a hairline, so it still stands off the white card it sits on. +const PILL = 'inline-flex h-[20px] flex-none items-center gap-1 self-start whitespace-nowrap rounded-full bg-surface-card px-2 font-geist font-normal leading-none tabular-nums shadow-sm ring-1 ring-inset ring-edge-faint' + interface FigureBadgeProps { - /** An icon, or a word set as a caption (`caption`). */ + /** An icon, or a word (`caption`). */ lead: React.ReactNode /** The figure. Absent draws the lead alone, a badge that is only a sign. */ value?: React.ReactNode - tone?: 'neutral' | 'warning' + tone?: 'neutral' | 'warning' | 'success' | 'pending' tooltip?: string - /** The lead is a word, set small, wide and in capitals like the stay's. */ + /** The lead is a word rather than an icon, set in capitals like the day plan's phase pills. */ caption?: boolean /** The figure in the quiet ink: an invitation, or a value inherited rather than set. */ faint?: boolean valueSize?: 'label' | 'micro' + /** May give up width to the pills beside it, its words cut short rather than wrapping the row. */ + shrink?: boolean dir?: 'ltr' /** * Makes the badge the control that changes its figure. A span with the button role, not @@ -49,25 +50,24 @@ interface FigureBadgeProps { } export default function FigureBadge({ - lead, value, tone = 'neutral', tooltip, caption, faint, valueSize = 'label', dir, onActivate, ariaLabel, + lead, value, tone = 'neutral', tooltip, caption, faint, dir, onActivate, ariaLabel, shrink, }: FigureBadgeProps): React.ReactElement { const colours = TONES[tone] - const shell = `inline-flex h-[16px] items-stretch self-start overflow-hidden rounded border ${colours.shell}` + const pill = shrink ? PILL.replace('flex-none', 'min-w-0 shrink overflow-hidden') : PILL const body = ( <> {lead} {value === undefined ? null : ( - - {value} - + <> + {/* A straight cut between what it is and the figure, the split the dialogs' pills use. */} + + {value} + )} ) @@ -84,12 +84,13 @@ export default function FigureBadge({ e.stopPropagation() onActivate(e.currentTarget) }} - className={`${shell} cursor-pointer transition-colors hover:border-content-faint focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent`} + className={`${pill} cursor-pointer transition-colors hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent`} + style={fs(10.5)} > {body} ) : ( - {body} + {body} ) return tooltip ? {badge} : badge } diff --git a/client/src/components/Roadtrip/RoadtripLimitsCard.tsx b/client/src/components/Roadtrip/RoadtripLimitsCard.tsx index de5214eb9..e8e016916 100644 --- a/client/src/components/Roadtrip/RoadtripLimitsCard.tsx +++ b/client/src/components/Roadtrip/RoadtripLimitsCard.tsx @@ -4,7 +4,7 @@ import { Clock, Fuel, CalendarClock, SlidersHorizontal, ChevronRight, Coins, Signpost, Ship, Check, Car, Zap, BatteryCharging, BatteryFull, BatteryWarning, Gauge, Route, Sparkles, Link2, Palette, } from 'lucide-react' -import Modal from '../shared/Modal' +import { DialogButton, DialogFooter, DialogHeader, DialogShell, DialogTile, FooterSpacer, NEUTRAL_TINT, fs } from '../shared/DialogShell' import { useTranslation } from '../../i18n/TranslationContext' import { useSettingsStore } from '../../store/settingsStore' import { convertDistance, formatDistance } from '../../utils/units' @@ -110,11 +110,11 @@ function LimitRow({ icon: Icon, label, suffix, value, placeholder, step, derived "min" does — and that column was the reason every section had a right edge of its own. In the flow it simply wraps under a long label and costs nothing. */} - {label} + {label} {/* The unit as a chip rather than as more grey words after the label. "kWh/100 km" set as prose reads as part of the question; on a chip it reads as what the number beside it will be measured in, which is what it is. */} - + {suffix} @@ -129,7 +129,7 @@ function LimitRow({ icon: Icon, label, suffix, value, placeholder, step, derived // screen-reader text. - {value ?? 0} + {value ?? 0} {derived} ) : ( @@ -151,7 +151,8 @@ function LimitRow({ icon: Icon, label, suffix, value, placeholder, step, derived onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); commit(); (e.target as HTMLInputElement).blur() } }} // The spinners are the other half of the width problem: they eat a third of a // narrow field and nobody sets a driving limit by clicking an arrow. - className="w-full rounded-lg border border-edge bg-surface px-2.5 py-1.5 text-end text-body tabular-nums text-content [appearance:textfield] focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent disabled:opacity-50 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" + className="w-full rounded-[10px] border border-edge bg-surface-input px-3 py-1.5 text-end tabular-nums text-content outline-none [appearance:textfield] placeholder:text-content-faint focus:ring-2 focus:ring-[color:var(--text-primary)] disabled:opacity-50 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" + style={fs(13, 'body')} /> )} @@ -183,7 +184,7 @@ function AvoidRow({ icon: Icon, label, hint, on, disabled, onToggle }: {
- {label} + {label} {hint ? {hint} : null} {disabled @@ -213,7 +214,7 @@ function Panel({ icon: Icon, title, note, children }: { children: React.ReactNode }): React.ReactElement { return ( -
+
{title} {children} {note ? {note} : null} @@ -227,8 +228,8 @@ function SectionTitle({ icon: Icon, children }: { children: React.ReactNode }): React.ReactElement { return ( -

- +

+ {children}

) @@ -286,6 +287,8 @@ export default function RoadtripLimitsCard({ onSave, onResetDayBoundaries, loadi const settings = useRoadtripSettings(s => s) const unit = useSettingsStore(s => s.settings.distance_unit) const [open, setOpen] = useState(false) + const labelId = useId() + const close = () => { setOpen(false); setPreview(null) } const distanceUnit: DistanceUnit = unit === 'imperial' ? 'imperial' : 'metric' const imperial = distanceUnit === 'imperial' // No way to save is the caller saying the reader may look but not change. Every @@ -428,7 +431,7 @@ export default function RoadtripLimitsCard({ onSave, onResetDayBoundaries, loadi {badges.map(({ key, Icon, text, label }) => ( : } + lead={label ? : } value={text} tooltip={label} /> @@ -444,12 +447,27 @@ export default function RoadtripLimitsCard({ onSave, onResetDayBoundaries, loadi {open ? ( - { setOpen(false); setPreview(null) }} size="4xl" title={ - - - {t('roadtrip.limit.title')} - - }> + } + tint={NEUTRAL_TINT} + labelId={labelId} + onClose={close} + title={t('roadtrip.limit.title')} + /> + )} + footer={( + + + {t('common.close')} + + )} + >
{/* Two columns, kept. One column reads more tidily but turns the dialog into something that has to be scrolled, and a setting you cannot see while you @@ -530,7 +548,7 @@ export default function RoadtripLimitsCard({ onSave, onResetDayBoundaries, loadi from the browser instead of from a rebuilt listbox. */}
{t('roadtrip.limit.vehicleLabel')} -
+
{VEHICLES.map(({ key, labelKey, Icon }) => (
-
+ ) : null} ) diff --git a/client/src/components/Roadtrip/RoadtripSidebar.constants.ts b/client/src/components/Roadtrip/RoadtripSidebar.constants.ts index 5dc4049b4..24e2a3dd3 100644 --- a/client/src/components/Roadtrip/RoadtripSidebar.constants.ts +++ b/client/src/components/Roadtrip/RoadtripSidebar.constants.ts @@ -21,19 +21,15 @@ export const RAIL_DASH: CSSProperties = { export const DISC = 'grid h-6 w-6 shrink-0 place-items-center rounded-full' /** - * The small capitalised caption over a number, and the badges in a day's header. - * - * Wide letter-spacing and uppercase rather than a size change: the labels have to stay - * legible at a third of the column's width in 23 languages, and shrinking them further - * was what made "Driving time" unreadable before it was ever clipped. + * The small caption over a number: the eyebrow the planner's dialogs and cards use, so + * the trip head reads like the cards beside it. */ -export const STAT_LABEL = 'font-geist font-semibold uppercase tracking-[0.15em] text-content-faint' +export const STAT_LABEL = 'font-geist font-bold uppercase tracking-[.08em] text-content-faint' /** - * A day-header badge: the date, the drive, the count. + * A pill in a day's head: the date, the drive, the count, a finding. * - * Medium weight in the quiet ink, not semibold in the strong one. Three uppercase badges - * with wide tracking already carry as much emphasis as a line can take; adding weight and - * contrast on top made the day's supporting facts shout louder than the day's own name. + * The day plan's pill: round, sentence case, quiet ink on the card colour with a soft + * shadow, so a day head reads the same in both tabs. */ -export const DAY_BADGE = 'inline-flex h-[20px] items-center rounded-lg px-2 font-geist font-medium uppercase tracking-[0.09em] text-content-muted' +export const DAY_BADGE = 'inline-flex h-[20px] items-center gap-1 rounded-full px-2 font-geist font-normal uppercase tracking-[.05em] text-content-secondary' diff --git a/client/src/components/Roadtrip/RoadtripSidebar.tsx b/client/src/components/Roadtrip/RoadtripSidebar.tsx index bb7a1bc23..c971b5bef 100644 --- a/client/src/components/Roadtrip/RoadtripSidebar.tsx +++ b/client/src/components/Roadtrip/RoadtripSidebar.tsx @@ -22,7 +22,7 @@ import { dayBookings } from './stopBookings' import type { Reservation } from '../../types' import type { StayDraft } from './RoadtripStayModal' import { missedLeaveOf, stayDraftOf } from './stayReading' -import { DAY_BADGE, RAIL_GRID, STAT_LABEL } from './RoadtripSidebar.constants' +import { RAIL_GRID, STAT_LABEL } from './RoadtripSidebar.constants' import { RefuelBand } from './RoadtripSidebarRefuelBand' import { DriveBand, SpillBlock } from './RoadtripSidebarDriveBands' import { BookendStop, BookingChips, RideBlock, TerminalStop, UndatedRides } from './RoadtripSidebarBookingRows' @@ -294,6 +294,7 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onOpenBooking, ca if (stop.carrier.role === 'arrival' && prev?.carrier?.role === 'departure' && sameRide(prev)) return null const ride = stop.carrier.role === 'departure' && next?.carrier?.role === 'arrival' && sameRide(next) const open = onOpenBooking ? () => onOpenBooking(stop.carrier!.reservationId) : undefined + const booking = reservations?.find(res => res.id === stop.carrier!.reservationId) // The index the road leaves from: the arrival's when the block holds both ends. const tail = ride ? i + 1 : i const after = day.stops[tail + 1] @@ -301,6 +302,7 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onOpenBooking, ca
  • {ride ? ( ) : ( onEditStay(stayDraftOf(stop, day.schedule.entries[i], missedLeaveOf(day, i))) : undefined} onPickKind={onSetStopKind ? anchor => setPicking({ anchor, stop }) : undefined} onPickFill={onSetStopFill ? anchor => setFilling({ anchor, stop }) : undefined} + bookings={bookings.atStop.get(i)} + canEditBookings={canEditBookings} + onOpenBooking={onOpenBooking} /> ) : ( onEditStay(stayDraftOf(stop, day.schedule.entries[i], missedLeaveOf(day, i))) : undefined} onPickKind={onSetStopKind ? anchor => setPicking({ anchor, stop }) : undefined} onPickFill={onSetStopFill ? anchor => setFilling({ anchor, stop }) : undefined} + bookings={bookings.atStop.get(i)} + canEditBookings={canEditBookings} + onOpenBooking={onOpenBooking} /> )} - {bookings.atStop.has(i) ? ( - - ) : null} {i < last && (!day.stops[i + 1].automaticNight || day.legs[i]?.distance !== 0) ? ( {day.date ? ( -