From 4215db2908a8012d082f57d226142394aec0b58f Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 4 Sep 2026 22:55:05 +0200 Subject: [PATCH] refactor(roadtrip): the limits as badges, and a quieter empty state The three figures under the button were one run-on string joined by dots, which says nothing about which number is which. Each takes the badge shell the rail already uses, with its own icon: a clock, a calendar, a pump. The icon tile in front of the title goes, because it was eating exactly the width the three badges need to sit on one line. Distances there go through formatDistance, so an imperial traveller reads miles on the button and types miles in the dialog while storage stays kilometres. EmptyState gains a compact variant. A sidebar state sits under its own controls, where a line at page size competes with them; at the caption tier it reads as what it is, a note about why the list below is blank. --- .../Roadtrip/RoadtripCorridorPanel.tsx | 1 + .../Roadtrip/RoadtripLimitsCard.tsx | 52 ++++++++++++++----- client/src/components/shared/EmptyState.tsx | 11 +++- 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx b/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx index 3ced31657..ef6236c50 100644 --- a/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx +++ b/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx @@ -348,6 +348,7 @@ export default function RoadtripCorridorPanel({ corridor, routes, onAddPoi }: Ro scene={search.loading ? 'search' : 'idle'} mood={filteredToNothing ? 'confused' : undefined} size={88} + compact surface="var(--bg-secondary)" title={ search.loading diff --git a/client/src/components/Roadtrip/RoadtripLimitsCard.tsx b/client/src/components/Roadtrip/RoadtripLimitsCard.tsx index 6baeed246..f9c771f24 100644 --- a/client/src/components/Roadtrip/RoadtripLimitsCard.tsx +++ b/client/src/components/Roadtrip/RoadtripLimitsCard.tsx @@ -79,13 +79,16 @@ export default function RoadtripLimitsCard({ onSave }: { onSave?.('roadtrip_range_km', Math.round(km)) } - // What is set, on the button itself, so the form does not have to be open to see it. - // Nothing set reads as "off" rather than as an empty string, which would look broken. - const summary = [ - legMinutes ? formatDurationShort(legMinutes * 60) : null, - dayMinutes ? formatDurationShort(dayMinutes * 60) : null, - rangeKm ? formatDistance(rangeKm, distanceUnit) : null, - ].filter(Boolean).join(' ยท ') + // What is set, on the button itself, so the form does not have to be open to read it. + // Each one keeps its own icon, because three numbers in a row say nothing about which + // is which: a clock, a calendar and a pump do. Distances go through formatDistance, so + // an imperial traveller reads miles here and types miles in the dialog, while what is + // stored stays kilometres either way. + const badges = [ + legMinutes ? { key: 'leg', Icon: Clock, text: formatDurationShort(legMinutes * 60) } : null, + dayMinutes ? { key: 'day', Icon: CalendarClock, text: formatDurationShort(dayMinutes * 60) } : null, + rangeKm ? { key: 'range', Icon: Fuel, text: formatDistance(rangeKm, distanceUnit) } : null, + ].filter(Boolean) as { key: string; Icon: typeof Clock; text: string }[] return ( <> @@ -98,16 +101,37 @@ export default function RoadtripLimitsCard({ onSave }: { onClick={() => setOpen(true)} className="flex w-full items-center gap-3 rounded-xl border border-edge-faint bg-surface-card px-3.5 py-3 text-start transition-colors hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent" > - - - - + {/* No icon tile on the trigger: the three badges below already carry a clock, a + calendar and a pump, and a fourth mark in front of the title only ate the width + they need to sit on one line. */} + {t('roadtrip.limit.title')} - - {summary || t('roadtrip.limit.none')} - + {badges.length ? ( + + {badges.map(({ key, Icon, text }) => ( + + + + + + {text} + + + ))} + + ) : ( + + {t('roadtrip.limit.none')} + + )} diff --git a/client/src/components/shared/EmptyState.tsx b/client/src/components/shared/EmptyState.tsx index d1c54731c..89745ac79 100644 --- a/client/src/components/shared/EmptyState.tsx +++ b/client/src/components/shared/EmptyState.tsx @@ -22,6 +22,7 @@ export default function EmptyState({ size = 104, surface = 'var(--bg-card)', layout = 'stack', + compact = false, className = '', action, }: { @@ -31,6 +32,14 @@ export default function EmptyState({ size?: number surface?: string layout?: 'stack' | 'row' + /** + * Quieter type for a state inside a narrow column rather than on a page. + * + * The sidebar's empty state sits under its own controls, so a line at the page size + * competes with them for the eye; at the caption tier it reads as what it is, a note + * about why the list below is blank. + */ + compact?: boolean className?: string /** Optional call to action under the title, for states that have an obvious next step. */ action?: ReactNode @@ -44,7 +53,7 @@ export default function EmptyState({ style={{ '--m-ink': 'var(--text-primary)', '--m-bg': surface } as CSSProperties} > -

{title}

+

{title}

{action} )