feat(roadtrip): bring the road trip addon to the phone

A road trip tab on the mobile trip screen: the stage as a chain, the same
map underneath it, the sheets a passenger needs, and a search along the
route.

The addon was desktop only because road trip MODE is a data switch rather
than a view switch. It rewrites `assignments` and `places` for the whole
planner hook, and every mobile sheet is permanently mounted and reads those
same two lists, so turning it on there brought back documented regressions.
The mode flag is therefore untouched. A second flag, `roadtripFeedActive`,
feeds only the routing round and does it off the STORED assignments, so the
phone gets the drive without what the mode drags along with it.

The map is one instance shared by the plan tab and the road trip tab,
hoisted into a single JSX expression so React keeps it in the same position
and the WebGL context is never torn down on a tab switch.

The corridor search is scoped to what a phone can afford: a window of the
day rather than all of it, a smaller tile ceiling, a capped retry pass and a
deadline. The standard search is three requests where the desk makes
fifteen. The categories are deliberately not narrowed, because they all ride
one query per box, so a second one costs nothing.

Two gaps found while wiring it up are closed here as well. The fuel search
drew its offers nowhere on the phone, so the button did nothing visible;
they are in the dry band now, with the detour, the spare range and an accept
button. And the map credit opened as a two line ribbon across a screen the
map fills: both GL engines now start it collapsed, and Leaflet, which has no
collapsed mode of its own, is given one.
This commit is contained in:
Maurice
2026-09-16 21:55:18 +02:00
parent bd44197974
commit aaff8f9db4
96 changed files with 7544 additions and 97 deletions
+21 -1
View File
@@ -34,6 +34,7 @@ function alternativeLabelIcon(label: string, note: string, background: string, a
})
}
import L from 'leaflet'
import MapAttributionToggle from './MapAttributionToggle'
import 'leaflet.markercluster/dist/MarkerCluster.css'
import 'leaflet.markercluster/dist/MarkerCluster.Default.css'
import { mapsApi } from '../../api/client'
@@ -978,6 +979,16 @@ export const MapView = memo(function MapView({
? 'calc(var(--bottom-nav-h, 84px) + 20px + var(--day-panel-h, 0px) + 12px)'
: 'calc(var(--bottom-nav-h, 84px) + 12px)'
/**
* Whether the credit is showing, on a phone where it starts as the (i).
*
* Held here rather than pushed into Leaflet's own container: Leaflet rewrites that
* container's markup from scratch whenever a layer with an attribution is added or
* removed, so anything injected into it dies on the next basemap switch. A class on
* the wrapper survives that, and the CSS lives with the rest of the mobile chrome.
*/
const [attribOpen, setAttribOpen] = useState(false)
const baseLayer = useSettingsStore(s => s.settings.map_base_layer) || 'default'
const updateSetting = useSettingsStore(s => s.updateSetting)
const isSatellite = baseLayer === 'satellite'
@@ -996,7 +1007,7 @@ export const MapView = memo(function MapView({
return (
<>
<div className="w-full h-full relative">
<div className={`w-full h-full relative${isMobile && attribOpen ? ' m-attrib-open' : ''}`}>
<MapContainer
// The datum is in the element's identity, because react-leaflet builds the
// map once and Leaflet cannot change a CRS afterwards. Settings can arrive
@@ -1215,6 +1226,15 @@ export const MapView = memo(function MapView({
onClick={cycleTrackingMode}
bottomOffset={locationButtonBottom as unknown as number}
/>}
{/* The credit as the little (i), which is what both GL engines do on a narrow map
and Leaflet has no mode for. */}
{isMobile && (
<MapAttributionToggle
open={attribOpen}
onToggle={() => setAttribOpen(v => !v)}
bottomOffset={locationButtonBottom}
/>
)}
{/* 20px off the sidebar, not 12: the pill is round and frosted, so at the
smaller gap its shadow ran into the sidebar edge and the two read as one
surface. */}