diff --git a/client/scripts/rtl-baseline.json b/client/scripts/rtl-baseline.json
index 82e209e06..a18e7c7cc 100644
--- a/client/scripts/rtl-baseline.json
+++ b/client/scripts/rtl-baseline.json
@@ -105,7 +105,7 @@
"pages/JourneyPublicPage.tsx": 3,
"pages/LoginPage.tsx": 23,
"pages/sharedTrip/SharedMap.tsx": 4,
- "pages/TripPlannerPage.tsx": 17,
+ "pages/TripPlannerPage.tsx": 13,
"pages/VacayPage.tsx": 1,
"styles/collections.css": 1,
"styles/studio.css": 7,
diff --git a/client/src/pages/TripPlannerPage.tsx b/client/src/pages/TripPlannerPage.tsx
index 31758fc4a..6f197c1b9 100644
--- a/client/src/pages/TripPlannerPage.tsx
+++ b/client/src/pages/TripPlannerPage.tsx
@@ -4,9 +4,7 @@ import { useParams, useNavigate, useSearchParams } from 'react-router'
import { useTripStore } from '../store/tripStore'
import { useSettingsStore } from '../store/settingsStore'
import { MapViewAuto as MapView } from '../components/Map/MapViewAuto'
-import { MapCompassPill, type CompassMap } from '../components/Map/MapCompassPill'
-import { TripRouteOverviewPill, TripRouteOverviewPanel } from '../components/Map/TripRouteOverview'
-import { DawarichTrailPill } from '../components/Map/DawarichTrailPill'
+import type { CompassMap } from '../components/Map/MapCompassPill'
import { getCached, fetchPhoto } from '../services/photoService'
import DayPlanSidebar from '../components/Planner/DayPlanSidebar'
import RoadtripModeSwitch from '../components/Roadtrip/RoadtripModeSwitch'
@@ -53,13 +51,13 @@ import type { Accommodation, TripMember, Day, Place, Reservation } from '../type
import { useTripPlannerPage } from './tripPlanner/useTripPlannerPage'
import {
ReservationsPanel, FileManager, CostsPanel, ExpenseModal, CollabPanel,
- RoadtripSidebar, RoadtripCorridorPanel, RoadtripLimitsCard, RoadtripStopPopup, RoadtripStayModal, RoadtripTrackModal, RoadtripAlternativesBar,
+ RoadtripSidebar, RoadtripCorridorPanel, RoadtripLimitsCard, RoadtripStopPopup, RoadtripStayModal, RoadtripTrackModal,
TourPlannerRail, TourPlannerToursRail, TransportModal,
} from './tripPlanner/plannerLazy'
import { LazyPanel } from './tripPlanner/LazyPanel'
import { ListsContainer } from './tripPlanner/ListsContainer'
+import { PlanMapStage } from './tripPlanner/PlanMapStage'
import { useMergedMapPois } from '../components/Map/useMergedMapPois'
-import PoiCategoryPill from '../components/Map/PoiCategoryPill'
import { useTouchDragBridge } from '../hooks/useTouchDragBridge'
import PanelResizeHandle from '../components/Planner/PanelResizeHandle'
@@ -266,164 +264,24 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
{activeTab === 'plan' && (
-
{
- const r = reservations.find(x => x.id === rid)
- if (!r) return
- if (isMobile) setMapTransportDetail(r)
- else openBookingDetail(r)
- }}
- /* In road trip mode the corridor's `visible` (not `search.results`: the map is
- the picture of that very list, and filtering the list while seventy pins stay
- on the map no longer answers "which of these") plus whatever the category
- pill found in view. Outside it, only the pill's hits. */
- pois={mapPois}
- onPoiClick={handlePoiClick}
- // Only while road trip mode is on: outside it there is no drive to drop onto.
- onPoiDropOnRoute={roadtripActive ? dropPoiOnRoute : undefined}
- // Clicking the drawn route puts a via there; only in road trip mode, where
- // the route is the thing being worked on.
- onRouteClick={roadtripActive && can('day_edit', trip) ? addRoadtripVia : undefined}
- roadtripVias={roadtripActive ? roadtripVias.byDay : undefined}
- alternativeRoutes={alternativeOverlays}
- focusPoints={overviewActive ? tripOverview.focusPoints : mapFocusPoints}
- clusterLoosely={roadtripActive}
- activeAlternative={highlightedAlternative}
- onChooseAlternative={chooseRouteAlternative}
- onHighlightAlternative={setHighlightedAlternative}
- onMoveVia={can('day_edit', trip) ? moveRoadtripVia : undefined}
- onRemoveVia={can('day_edit', trip) ? removeRoadtripVia : undefined}
- onViewportChange={tourMap.captureViewport}
- onMapReady={setGlMap}
+
- {/* Over the map rather than in a dialog: the answer to "which of these" is the
- roads drawn behind it, so covering them to ask would hide the point. */}
- {routeAlternatives.open && (
-
-
-
-
-
- )}
-
- {/* Bottom-RIGHT. Not the top corridor between the panels, which is already
- contested by the POI bar and the collapse tabs (#2247); and not the
- bottom-left corner, where Leaflet's base-layer switcher sits at
- z-index 1000 and would cover this. The right corner is free on both
- renderers — the locate button that lives there is phone-only. */}
- {(!roadtripActive || dawarichEnabled) && (
-
- {!roadtripActive && overviewActive && (
-
- )}
- {!roadtripActive && (
-
- )}
- {/* Stays in road-trip mode, unlike the overview: the route that was
- actually driven is the thing you most want beside the planned
- one. It is drawn, never applied — correcting the plan from the
- recording is a different feature and deliberately not this one. */}
- {dawarichEnabled && (
-
- )}
-
- )}
-
- {(poiPillEnabled || glMap) && (
-
- {poiPillEnabled && (
-
- )}
- {glMap &&
}
-
- )}
-
- {/* Mobile: the compass/reset-orientation control lives centre-top on its own
- (the desktop cluster above is hidden below md), between the edge Plan/Places tabs. */}
- {glMap && (
-
-
-
- )}
-
- {/* Mobile POI search controls live in a portal like the Plan/Places
- buttons so map touch handlers cannot swallow the tap targets. */}
- {poiPillEnabled && !mobileSidebarOpen && !showPlaceForm && !showMembersModal && !showReservationModal && createPortal(
- ,
- document.body
- )}
-
{/* The panel's tab: a flap on its edge while open, a raised accent tile once it is tucked away. */}
diff --git a/client/src/pages/tripPlanner/PlanMapStage.test.tsx b/client/src/pages/tripPlanner/PlanMapStage.test.tsx
new file mode 100644
index 000000000..23d633775
--- /dev/null
+++ b/client/src/pages/tripPlanner/PlanMapStage.test.tsx
@@ -0,0 +1,213 @@
+// FE-PAGE-PLANNER-MAPSTAGE-001 to FE-PAGE-PLANNER-MAPSTAGE-010
+//
+// The plan tab's map and its floating controls, rendered on their own. The map
+// and the pills are stubs that record their props, so each mode the stage
+// switches between (day route, road trip, trip overview) is read off directly.
+import React from 'react'
+import { render, screen } from '../../../tests/helpers/render'
+import { buildTrip, buildPlace, buildReservation } from '../../../tests/helpers/factories'
+import { PlanMapStage, type PlanMapStageProps } from './PlanMapStage'
+
+type Recorded = Record & Record<`on${string}`, (...args: unknown[]) => unknown>
+const captured = vi.hoisted(() => ({} as Record>))
+const stub = vi.hoisted(() => (name: string, testId: string) => (props: Record) => {
+ captured[name] = props
+ return React.createElement('div', { 'data-testid': testId })
+})
+vi.mock('../../components/Map/MapViewAuto', () => ({ MapViewAuto: stub('map', 'map-view') }))
+vi.mock('../../components/Map/MapCompassPill', () => ({ MapCompassPill: stub('compass', 'compass-pill') }))
+vi.mock('../../components/Map/PoiCategoryPill', () => ({ default: stub('poiPill', 'poi-pill') }))
+vi.mock('../../components/Map/TripRouteOverview', () => ({
+ TripRouteOverviewPill: stub('overviewPill', 'overview-pill'),
+ TripRouteOverviewPanel: stub('overviewPanel', 'overview-panel'),
+}))
+vi.mock('../../components/Map/DawarichTrailPill', () => ({ DawarichTrailPill: stub('dawarichPill', 'dawarich-pill') }))
+vi.mock('../../components/Roadtrip/RoadtripAlternativesBar', () => ({ default: stub('alternativesBar', 'alternatives-bar') }))
+
+function recorded(name: string): Recorded {
+ return captured[name] as Recorded
+}
+
+const trip = buildTrip({ id: 12 })
+const place = buildPlace({ id: 4 })
+const flight = buildReservation({ id: 70, trip_id: 12 })
+
+// The fixture holds what the stage reads; the hook's full types do not matter here.
+function stageProps(overrides: Partial> = {}): PlanMapStageProps {
+ const base: Partial> = {
+ tripId: 12, trip, can: () => true, days: [], reservations: [flight], isMobile: false,
+ selectedDayId: 3, selectedPlaceId: null, selectedPlace: null, showDayDetail: null, handleSelectDay: vi.fn(),
+ mapPlaces: ['day places'], dayPlaces: [], dayOrderMap: {}, mapTileUrl: 'tiles', fitKey: 1,
+ mapLocked: false, toggleMapLocked: vi.fn(), mapFocusPoints: ['day focus'],
+ route: ['day route'], routeWalking: ['walking'], routeSegments: ['day segments'], routeVias: ['day vias'], transitRoutesShown: true,
+ handleMarkerClick: vi.fn(), handleMapClick: vi.fn(), handleMapContextMenu: vi.fn(), handlePoiClick: vi.fn(),
+ visibleConnections: ['day connections'], setMapTransportDetail: vi.fn(), openBookingDetail: vi.fn(),
+ roadtripActive: false, roadtripMapPlaces: ['stops'], roadtripMapLines: ['drive'], roadtripLineColors: ['red'],
+ roadtripMapVias: ['drive vias'], roadtripRoutes: { accessLines: ['access'], segments: ['drive segments'] },
+ roadtripConnections: ['rides'], roadtripVias: { byDay: { 3: [] } },
+ dayBoundaryControls: ['boundaries'], dropPoiOnRoute: vi.fn(), addRoadtripVia: vi.fn(), moveRoadtripVia: vi.fn(), removeRoadtripVia: vi.fn(),
+ routeAlternatives: { open: null, close: vi.fn() }, alternativeOverlays: ['overlays'], highlightedAlternative: null,
+ chooseRouteAlternative: vi.fn(), setHighlightedAlternative: vi.fn(),
+ overviewActive: false, overviewShown: false, toggleOverview: vi.fn(),
+ tripOverview: { lines: ['overview'], lineColors: ['blue'], segments: ['overview segments'], focusPoints: ['overview focus'] },
+ dawarichEnabled: false, dawarichTrail: { track: ['recorded'], status: 'idle' }, dawarichHiddenDates: ['2025-06-02'],
+ dawarichTrailShown: false, toggleDawarichTrail: vi.fn(),
+ mobileSidebarOpen: null, showPlaceForm: false, showMembersModal: false, showReservationModal: false,
+ poi: { categories: { core: [], plugin: [] }, active: [], toggle: vi.fn(), loadingKeys: [], errorKeys: [], moved: false, searchArea: vi.fn() },
+ tourMap: { captureViewport: vi.fn() },
+ leftPanelPx: 320, rightPanelPx: 0, mapInsetLeft: 330, mapInsetRight: 0,
+ glMap: null, setGlMap: vi.fn(), poiPillEnabled: true, mapPois: ['pois'], distanceUnit: 'metric',
+ }
+ return { ...base, ...overrides } as unknown as PlanMapStageProps
+}
+
+function renderStage(overrides: Partial> = {}) {
+ const props = stageProps(overrides)
+ return { props, ...render() }
+}
+
+beforeEach(() => {
+ for (const key of Object.keys(captured)) delete captured[key]
+})
+
+describe('PlanMapStage', () => {
+ it('FE-PAGE-PLANNER-MAPSTAGE-001: on an ordinary day the map draws that day', () => {
+ renderStage()
+ const map = recorded('map')
+
+ expect(map).toMatchObject({
+ places: ['day places'], route: ['day route'], routeColors: undefined, routeWalking: ['walking'],
+ routeSegments: ['day segments'], routeVias: ['day vias'], focusPoints: ['day focus'],
+ visibleConnectionIds: ['day connections'], dayBoundaryControls: undefined, accessLines: undefined,
+ roadtripVias: undefined, onPoiDropOnRoute: undefined, onRouteClick: undefined, clusterLoosely: false,
+ followSelection: true, leftWidth: 320, rightWidth: 0, hasInspector: false, hasDayDetail: false,
+ dawarichTrack: ['recorded'], dawarichHiddenDates: ['2025-06-02'], pois: ['pois'], tileUrl: 'tiles',
+ })
+ expect(screen.getByTestId('overview-pill')).toBeInTheDocument()
+ expect(screen.queryByTestId('overview-panel')).toBeNull()
+ expect(screen.queryByTestId('dawarich-pill')).toBeNull()
+ expect(screen.queryByTestId('alternatives-bar')).toBeNull()
+ })
+
+ it('FE-PAGE-PLANNER-MAPSTAGE-002: in road trip mode the map draws the drive and edits it', () => {
+ const { props } = renderStage({ roadtripActive: true })
+ const map = recorded('map')
+
+ expect(map).toMatchObject({
+ places: ['stops'], route: ['drive'], routeColors: ['red'], routeWalking: undefined,
+ routeSegments: ['drive segments'], routeVias: ['drive vias'], dayBoundaryControls: ['boundaries'],
+ accessLines: ['access'], visibleConnectionIds: ['rides'], roadtripVias: { 3: [] }, clusterLoosely: true,
+ })
+ expect(map.onPoiDropOnRoute).toBe(props.dropPoiOnRoute)
+ expect(map.onRouteClick).toBe(props.addRoadtripVia)
+ expect(map.onMoveVia).toBe(props.moveRoadtripVia)
+ expect(map.onRemoveVia).toBe(props.removeRoadtripVia)
+ // The overview belongs to the plan, so the whole stack goes while it is the only thing in it.
+ expect(screen.queryByTestId('overview-pill')).toBeNull()
+ })
+
+ it('FE-PAGE-PLANNER-MAPSTAGE-003: without the day right the drive cannot be edited from the map', () => {
+ const can = vi.fn((action: string) => action !== 'day_edit')
+ renderStage({ roadtripActive: true, can })
+ const map = recorded('map')
+
+ expect(can).toHaveBeenCalledWith('day_edit', trip)
+ expect(map.onRouteClick).toBeUndefined()
+ expect(map.onMoveVia).toBeUndefined()
+ expect(map.onRemoveVia).toBeUndefined()
+ })
+
+ it('FE-PAGE-PLANNER-MAPSTAGE-004: the trip overview replaces the day route and opens its panel', () => {
+ const { props } = renderStage({ overviewActive: true, overviewShown: true })
+ const map = recorded('map')
+
+ expect(map).toMatchObject({
+ route: ['overview'], routeColors: ['blue'], routeSegments: ['overview segments'],
+ focusPoints: ['overview focus'], routeWalking: undefined,
+ })
+ expect(recorded('overviewPanel')).toMatchObject({ overview: props.tripOverview, unit: 'metric', selectedDayId: 3 })
+ recorded('overviewPanel').onSelectDay(5)
+ expect(props.handleSelectDay).toHaveBeenCalledWith(5)
+ expect(recorded('overviewPill')).toMatchObject({ active: true, onToggle: props.toggleOverview })
+ })
+
+ it('FE-PAGE-PLANNER-MAPSTAGE-005: the recorded trail stays beside the drive in road trip mode', () => {
+ const { props } = renderStage({ roadtripActive: true, dawarichEnabled: true, dawarichTrailShown: true, mapInsetRight: 290 })
+
+ expect(recorded('dawarichPill')).toMatchObject({ active: true, status: 'idle', onToggle: props.toggleDawarichTrail })
+ expect(screen.queryByTestId('overview-pill')).toBeNull()
+ const stack = screen.getByTestId('dawarich-pill').parentElement as HTMLElement
+ expect(stack.style.right).toBe('304px')
+ expect(stack.style.left).toBe('')
+ })
+
+ it('FE-PAGE-PLANNER-MAPSTAGE-006: an open choice of routes shows the alternatives bar over the map', async () => {
+ const open = { dayId: 3 }
+ const { props } = renderStage({ routeAlternatives: { open, close: vi.fn() } })
+
+ expect(await screen.findByTestId('alternatives-bar')).toBeInTheDocument()
+ expect(recorded('alternativesBar')).toMatchObject({
+ open,
+ overlays: ['overlays'],
+ onChoose: props.chooseRouteAlternative,
+ onClose: (props.routeAlternatives as { close: unknown }).close,
+ onHighlight: props.setHighlightedAlternative,
+ })
+ })
+
+ it('FE-PAGE-PLANNER-MAPSTAGE-007: a booking clicked on the map opens where the layout shows bookings', () => {
+ const desktop = renderStage()
+ recorded('map').onReservationClick(999)
+ expect(desktop.props.openBookingDetail).not.toHaveBeenCalled()
+ recorded('map').onReservationClick(70)
+ expect(desktop.props.openBookingDetail).toHaveBeenCalledWith(flight)
+ expect(desktop.props.setMapTransportDetail).not.toHaveBeenCalled()
+ desktop.unmount()
+
+ const phone = renderStage({ isMobile: true, mapLocked: true })
+ expect(recorded('map')).toMatchObject({ followSelection: true, onToggleFollow: undefined })
+ recorded('map').onReservationClick(70)
+ expect(phone.props.setMapTransportDetail).toHaveBeenCalledWith(flight)
+ expect(phone.props.openBookingDetail).not.toHaveBeenCalled()
+ })
+
+ it('FE-PAGE-PLANNER-MAPSTAGE-008: the compass appears once the map hands itself over', () => {
+ const { props, rerender } = renderStage({ poiPillEnabled: false })
+ expect(screen.queryByTestId('compass-pill')).toBeNull()
+ expect(screen.queryByTestId('poi-pill')).toBeNull()
+ expect(recorded('map').onMapReady).toBe(props.setGlMap)
+
+ const glMap = { getBearing: () => 0 }
+ rerender()
+ // The desktop cluster and the phone's own centre-top copy.
+ expect(screen.getAllByTestId('compass-pill')).toHaveLength(2)
+ expect(recorded('compass').map).toBe(glMap)
+ })
+
+ it('FE-PAGE-PLANNER-MAPSTAGE-009: the phone POI pill steps aside for drawers and dialogs', () => {
+ const { rerender } = renderStage()
+ const portal = screen.getByTestId('mobile-poi-category-pill')
+ expect(portal.parentElement).toBe(document.body)
+ expect(portal).toContainElement(screen.getAllByTestId('poi-pill')[1])
+
+ for (const cover of [{ mobileSidebarOpen: 'left' }, { showPlaceForm: true }, { showMembersModal: true }, { showReservationModal: true }]) {
+ rerender()
+ expect(screen.queryByTestId('mobile-poi-category-pill')).toBeNull()
+ }
+ })
+
+ it('FE-PAGE-PLANNER-MAPSTAGE-010: the top cluster is centred on the corridor between the panels', () => {
+ const { props, container } = renderStage({ mapInsetLeft: 330, mapInsetRight: 290, selectedPlace: place, showDayDetail: { id: 3 } })
+ const cluster = container.querySelector('div[style*="translateX(-50%)"][style*="z-index: 25"]') as HTMLElement
+
+ // As the browser normalizes it: half the corridor past the left inset, and no wider than the corridor.
+ expect(cluster.style.left).toBe('calc(330px + 0.5 * (100% - 330px - 290px))')
+ expect(cluster.style.maxWidth).toBe('calc(100% - 644px)')
+ expect(recorded('map')).toMatchObject({ hasInspector: true, hasDayDetail: false, selectedPlace: place })
+ const poi = props.poi as unknown as Record
+ expect(recorded('poiPill')).toMatchObject({
+ categories: poi.categories, active: poi.active, onToggle: poi.toggle,
+ loadingKeys: poi.loadingKeys, errorKeys: poi.errorKeys, moved: false, onSearchArea: poi.searchArea,
+ })
+ })
+})
diff --git a/client/src/pages/tripPlanner/PlanMapStage.tsx b/client/src/pages/tripPlanner/PlanMapStage.tsx
new file mode 100644
index 000000000..aa23ae5c3
--- /dev/null
+++ b/client/src/pages/tripPlanner/PlanMapStage.tsx
@@ -0,0 +1,237 @@
+import type { Dispatch, ReactElement, SetStateAction } from 'react'
+import { createPortal } from 'react-dom'
+import { MapViewAuto as MapView } from '../../components/Map/MapViewAuto'
+import { MapCompassPill, type CompassMap } from '../../components/Map/MapCompassPill'
+import { TripRouteOverviewPill, TripRouteOverviewPanel } from '../../components/Map/TripRouteOverview'
+import { DawarichTrailPill } from '../../components/Map/DawarichTrailPill'
+import PoiCategoryPill from '../../components/Map/PoiCategoryPill'
+import type { Poi } from '../../components/Map/poiCategories'
+import type { Settings } from '../../types'
+import { RoadtripAlternativesBar } from './plannerLazy'
+import { LazyPanel } from './LazyPanel'
+import type { useTripPlanner } from './useTripPlanner'
+import type { useTripPlannerPage } from './useTripPlannerPage'
+
+type Planner = ReturnType
+type PageState = ReturnType
+
+// Picked per concern so the key lists stay short and typed by the hook that owns them.
+type StageTrip = Pick
+type StageMap = Pick
+type StageRoadtrip = Pick
+type StageAlternatives = Pick
+type StageOverlays = Pick
+
+/** What the page works out itself: the panel geometry, the compass map and the POI settings. */
+interface StagePageLocals {
+ leftPanelPx: number
+ rightPanelPx: number
+ mapInsetLeft: number
+ mapInsetRight: number
+ glMap: CompassMap | null
+ setGlMap: Dispatch>
+ poiPillEnabled: boolean
+ mapPois: Poi[]
+ distanceUnit: Settings['distance_unit']
+}
+
+export interface PlanMapStageProps
+ extends StageTrip, StageMap, StageRoadtrip, StageAlternatives, StageOverlays, Pick, StagePageLocals {}
+
+/**
+ * The plan tab's map and the controls floating over it. It returns a fragment on
+ * purpose: the children sit straight in the plan container, ahead of the side
+ * panels, so their stacking and the order of the portals in document.body stay
+ * exactly what they were when this lived in the page.
+ */
+export function PlanMapStage({
+ tripId, trip, can, days, reservations, isMobile,
+ selectedDayId, selectedPlaceId, selectedPlace, showDayDetail, handleSelectDay,
+ mapPlaces, dayPlaces, dayOrderMap, mapTileUrl, fitKey, mapLocked, toggleMapLocked, mapFocusPoints,
+ route, routeWalking, routeSegments, routeVias, transitRoutesShown,
+ handleMarkerClick, handleMapClick, handleMapContextMenu, handlePoiClick,
+ visibleConnections, setMapTransportDetail, openBookingDetail,
+ roadtripActive, roadtripMapPlaces, roadtripMapLines, roadtripLineColors, roadtripMapVias,
+ roadtripRoutes, roadtripConnections, roadtripVias,
+ dayBoundaryControls, dropPoiOnRoute, addRoadtripVia, moveRoadtripVia, removeRoadtripVia,
+ routeAlternatives, alternativeOverlays, highlightedAlternative, chooseRouteAlternative, setHighlightedAlternative,
+ overviewActive, tripOverview, overviewShown, toggleOverview,
+ dawarichEnabled, dawarichTrail, dawarichHiddenDates, dawarichTrailShown, toggleDawarichTrail,
+ mobileSidebarOpen, showPlaceForm, showMembersModal, showReservationModal,
+ poi, tourMap, leftPanelPx, rightPanelPx, mapInsetLeft, mapInsetRight,
+ glMap, setGlMap, poiPillEnabled, mapPois, distanceUnit,
+}: Readonly): ReactElement {
+ return (
+ <>
+ {
+ const r = reservations.find(x => x.id === rid)
+ if (!r) return
+ if (isMobile) setMapTransportDetail(r)
+ else openBookingDetail(r)
+ }}
+ /* In road trip mode the corridor's `visible` (not `search.results`: the map is
+ the picture of that very list, and filtering the list while seventy pins stay
+ on the map no longer answers "which of these") plus whatever the category
+ pill found in view. Outside it, only the pill's hits. */
+ pois={mapPois}
+ onPoiClick={handlePoiClick}
+ // Only while road trip mode is on: outside it there is no drive to drop onto.
+ onPoiDropOnRoute={roadtripActive ? dropPoiOnRoute : undefined}
+ // Clicking the drawn route puts a via there; only in road trip mode, where
+ // the route is the thing being worked on.
+ onRouteClick={roadtripActive && can('day_edit', trip) ? addRoadtripVia : undefined}
+ roadtripVias={roadtripActive ? roadtripVias.byDay : undefined}
+ alternativeRoutes={alternativeOverlays}
+ focusPoints={overviewActive ? tripOverview.focusPoints : mapFocusPoints}
+ clusterLoosely={roadtripActive}
+ activeAlternative={highlightedAlternative}
+ onChooseAlternative={chooseRouteAlternative}
+ onHighlightAlternative={setHighlightedAlternative}
+ onMoveVia={can('day_edit', trip) ? moveRoadtripVia : undefined}
+ onRemoveVia={can('day_edit', trip) ? removeRoadtripVia : undefined}
+ onViewportChange={tourMap.captureViewport}
+ onMapReady={setGlMap}
+ />
+
+ {/* Over the map rather than in a dialog: the answer to "which of these" is the
+ roads drawn behind it, so covering them to ask would hide the point. */}
+ {routeAlternatives.open && (
+
+
+
+
+
+ )}
+
+ {/* Bottom-RIGHT. Not the top corridor between the panels, which is already
+ contested by the POI bar and the collapse tabs (#2247); and not the
+ bottom-left corner, where Leaflet's base-layer switcher sits at
+ z-index 1000 and would cover this. The right corner is free on both
+ renderers: the locate button that lives there is phone-only. */}
+ {(!roadtripActive || dawarichEnabled) && (
+
+ {!roadtripActive && overviewActive && (
+
+ )}
+ {!roadtripActive && (
+
+ )}
+ {/* Stays in road-trip mode, unlike the overview: the route that was
+ actually driven is the thing you most want beside the planned
+ one. It is drawn, never applied. Correcting the plan from the
+ recording is a different feature and deliberately not this one. */}
+ {dawarichEnabled && (
+
+ )}
+
+ )}
+
+ {(poiPillEnabled || glMap) && (
+
+ {poiPillEnabled && (
+
+ )}
+ {glMap &&
}
+
+ )}
+
+ {/* Mobile: the compass/reset-orientation control lives centre-top on its own
+ (the desktop cluster above is hidden below md), between the edge Plan/Places tabs. */}
+ {glMap && (
+
+
+
+ )}
+
+ {/* Mobile POI search controls live in a portal like the Plan/Places
+ buttons so map touch handlers cannot swallow the tap targets. */}
+ {poiPillEnabled && !mobileSidebarOpen && !showPlaceForm && !showMembersModal && !showReservationModal && createPortal(
+ ,
+ document.body
+ )}
+ >
+ )
+}