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 + )} + + ) +}