From bf03d91345d4a54b23ad12eabc60b256e2d78127 Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 16 Sep 2026 22:23:01 +0000 Subject: [PATCH] feat(roadtrip): the phone dry band gets the desk's mascot and reserve lamp "Tank runs out here" on the phone was a pink card with a black pill. It now has the desktop band's design sized for a thumb: the mascot on its skateboard and not enjoying it, a small uppercase warning, and the reserve lamp as a 44px button. While a search runs or its offers show, the lamp steps aside for a close (nothing in the band could clear the offer pins before), and an empty answer leaves a retry. Which control shows, which offers and which empty sentence is decided in refuelBandState in refuelSuggestion.ts, next to the fuel and battery words. The desktop RefuelBand reads the same helper now, with no visual change, so the two shells hold markup only. --- .../components/Roadtrip/RoadtripSidebar.tsx | 55 +++--- .../Roadtrip/refuelSuggestion.test.ts | 69 +++++++- .../components/Roadtrip/refuelSuggestion.ts | 100 +++++++++++ .../screens/trip/roadtrip/MRoadtripRows.tsx | 159 ++++++++++++------ .../unit/mobile/trip/MRoadtripRows.test.tsx | 118 ++++++++++++- 5 files changed, 416 insertions(+), 85 deletions(-) diff --git a/client/src/components/Roadtrip/RoadtripSidebar.tsx b/client/src/components/Roadtrip/RoadtripSidebar.tsx index 41a798387..df5afdddd 100644 --- a/client/src/components/Roadtrip/RoadtripSidebar.tsx +++ b/client/src/components/Roadtrip/RoadtripSidebar.tsx @@ -8,7 +8,7 @@ import { } from 'lucide-react' import MDancingTrek from '../../mobile/components/MDancingTrek' import type { RefuelSearch } from './useRefuelSearch' -import type { RefuelCandidate } from './refuelSuggestion' +import { REFUEL_EMPTY_KEY, REFUEL_WORDS, refuelBandState, type RefuelCandidate } from './refuelSuggestion' import type { DryPoint } from './roadtripModel' import { useTranslation } from '../../i18n/TranslationContext' import { Tooltip } from '../shared/Tooltip' @@ -445,9 +445,10 @@ function RefuelBand({ dry, refuel, dayId, onAsk, onAccept }: { const { vehicleKind } = useVehicleRange() const electric = vehicleKind === 'electric' const DryIcon = electric ? Zap : Fuel - const key = `${dayId}:${dry.legIndex}` - const open = refuel.openFor === key - const settled = open && !refuel.loading && refuel.outcome + const words = REFUEL_WORDS[electric ? 'electric' : 'fuel'] + // Which control, which sentence and which offers: decided in refuelSuggestion so the + // phone band reads the same answer instead of keeping its own copy of the conditions. + const band = refuelBandState(refuel, dayId, dry.legIndex) return (
@@ -490,7 +491,7 @@ function RefuelBand({ dry, refuel, dayId, onAsk, onAccept }: { className="truncate font-geist font-semibold uppercase tracking-[0.16em] text-danger" style={{ fontSize: FS.label }} > - {t(electric ? 'roadtrip.refuel.dryElectric' : 'roadtrip.refuel.dry')} + {t(words.dry)}
{/* How far INTO this leg, where a drive band keeps its figures. Not the range that was crossed: that is the traveller's own setting, says @@ -513,7 +514,7 @@ function RefuelBand({ dry, refuel, dayId, onAsk, onAccept }: { found nothing leaves a button rather than a dead end — the place search is a shared public service that does time out, and "it did not answer" with no way to retry reads as broken rather than as busy. */} - {open && (refuel.loading || refuel.results.length) ? ( + {band.control === 'close' ? ( - {/* Blunt rather than greyed out: the button keeps its shape and says why. */} - {offline && ( - {t('mobileTrip.rtSearchOffline')} - )} - + ) : ( + + )} + + + {/* Blunt rather than silent: the lamp keeps its place and this says why it is out. */} + {onSearch && offline && band.control !== 'close' && ( + {t('mobileTrip.rtSearchOffline')} )} - {open && refuel.loading && ( + {band.loading && ( {t('roadtrip.refuel.looking')} )} {/* The answer, in the band that asked. The desktop puts it in the rail row for the same reason: a station offered somewhere other than where the tank runs out is - an offer somebody has to go and find. Three at most: this is an offer beside a - plan, and the search sheet above is where all of them live. */} - {settled && (refuel.results.length ? ( + an offer somebody has to go and find. */} + {band.offers.length > 0 && ( - ) : ( - /* Three sentences for three different facts. "Nothing on this stretch" after a - request that failed states something that was never checked. */ - - {refuel.outcome === 'none' - ? t('roadtrip.refuel.none') - : refuel.outcome === 'incomplete' - ? t('roadtrip.refuel.incomplete') - : t('roadtrip.refuel.failed')} - - ))} + )} + + {/* Three sentences for three different facts. "Nothing on this stretch" after a + request that failed states something that was never checked. */} + {band.empty && ( + {t(REFUEL_EMPTY_KEY[band.empty])} + )} ) } @@ -303,7 +364,7 @@ function RefuelOffer({ poi, chrome, electric, onAccept }: {