navi/src/App.jsx
Matt 67779dbbf7 feat(ui): Redesign bottom-right map control cluster
- Increase touch targets from 36px to 44px (meets accessibility guidelines)
- Wrap Locate and Layers buttons in unified .map-controls-br container
- Layer popover now opens LEFT of buttons (avoids collision with Locate)
- Add hover and active states with theme-aware styling
- Proper spacing for scale control below the cluster
- Increased icon sizes from 18px to 20px
- Mobile-responsive with proper max-height on layer popover

Layout:
  [Locate] 44x44
  [Layers] 44x44
  ──────────────
  Scale: 0.5 mi

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-01 23:17:13 +00:00

107 lines
3.3 KiB
JavaScript

import { useEffect, useRef, useCallback } from 'react'
import { useStore } from './store'
import { useTheme } from './hooks/useTheme'
import { requestRoute, fetchAuthState } from './api'
import { decodePolyline } from './utils/decode'
import MapView from './components/MapView'
import Panel from './components/Panel'
import ContactModal from './components/ContactModal'
import LayerControl from './components/LayerControl'
import LocateButton from './components/LocateButton'
export default function App() {
const mapViewRef = useRef(null)
const routeDebounceRef = useRef(null)
// Initialize theme system
useTheme()
const stops = useStore((s) => s.stops)
const mode = useStore((s) => s.mode)
const route = useStore((s) => s.route)
const gpsOrigin = useStore((s) => s.gpsOrigin)
const geoPermission = useStore((s) => s.geoPermission)
const setRoute = useStore((s) => s.setRoute)
const setRouteLoading = useStore((s) => s.setRouteLoading)
const setRouteError = useStore((s) => s.setRouteError)
const clearRoute = useStore((s) => s.clearRoute)
const setAuth = useStore((s) => s.setAuth)
// Initialize auth state on app load (single fetch, no polling)
useEffect(() => {
fetchAuthState().then(setAuth)
}, [setAuth])
// Fetch route when stops, mode, gpsOrigin, or geoPermission change (debounced 500ms)
useEffect(() => {
if (routeDebounceRef.current) clearTimeout(routeDebounceRef.current)
routeDebounceRef.current = setTimeout(async () => {
const { userLocation } = useStore.getState()
let effective = stops.map((s) => ({ lat: s.lat, lon: s.lon }))
if (gpsOrigin && geoPermission === 'granted' && userLocation) {
effective = [{ lat: userLocation.lat, lon: userLocation.lon }, ...effective]
}
if (effective.length < 2) {
clearRoute()
return
}
setRouteLoading(true)
try {
const data = await requestRoute(effective, mode)
if (data.trip) {
setRoute(data.trip)
} else {
setRouteError('No route returned')
}
} catch (e) {
setRouteError(e.message || 'Route request failed')
} finally {
setRouteLoading(false)
}
}, 500)
return () => {
if (routeDebounceRef.current) clearTimeout(routeDebounceRef.current)
}
}, [stops, mode, gpsOrigin, geoPermission, clearRoute, setRoute, setRouteLoading, setRouteError])
// Handle maneuver click
const handleManeuverClick = useCallback(
(maneuver) => {
if (!route || !route.legs) return
const legIdx = maneuver._legIndex || 0
const leg = route.legs[legIdx]
if (!leg || !leg.shape) return
const coords = decodePolyline(leg.shape, 6)
const idx = maneuver.begin_shape_index
if (idx >= 0 && idx < coords.length) {
const [lng, lat] = coords[idx]
mapViewRef.current?.flyTo(lat, lng, 15)
}
},
[route]
)
return (
<div className="relative w-screen h-screen overflow-hidden" style={{ background: 'var(--bg-base)' }}>
<MapView ref={mapViewRef} />
<Panel onManeuverClick={handleManeuverClick} />
<ContactModal />
{/* Bottom-right map controls */}
<div className="map-controls-br">
<LocateButton mapRef={mapViewRef} />
<LayerControl mapRef={mapViewRef} />
</div>
</div>
)
}