fix: mobile UX — GPS permission flow, traffic toggle, overflow

GPS permission:
- Remove silent mount-time getCurrentPosition calls that cause iOS Safari
  to cache a "denied" state without ever prompting the user
- LocateButton always retries getCurrentPosition on tap (user gesture)
- Only show "denied" toast on PERMISSION_DENIED (code 1), not timeout
- MapView watchPosition now starts only after confirmed grant, not unconditionally

Traffic toggle:
- Fix isStyleLoaded() race in LayerControl — if style not loaded when
  toggle fires, defer to map.once("style.load") instead of silently bailing
- Change outside-click handler from mousedown to pointerdown for mobile

Mobile UX (from prior session):
- Add LocateButton component (crosshair GPS locate/re-center)
- Reposition layer control + locate button to top-right on mobile
  (below MapLibre nav controls, above bottom sheet)
- ModeSelector: add min-w-0 to prevent flex overflow at 390px
- StopItem: remove button visible on touch (60% opacity vs hover-only)
- Panel: overflow-x-hidden + safe-area-inset-bottom on mobile sheet
- Body overflow-x guard on mobile viewports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt 2026-04-22 03:27:21 +00:00
commit 03e9780834
8 changed files with 216 additions and 72 deletions

View file

@ -7,6 +7,7 @@ import MapView from './components/MapView'
import Panel from './components/Panel'
import PlaceDetail from './components/PlaceDetail'
import LayerControl from './components/LayerControl'
import LocateButton from './components/LocateButton'
export default function App() {
const mapViewRef = useRef(null)
@ -24,24 +25,6 @@ export default function App() {
const setRouteLoading = useStore((s) => s.setRouteLoading)
const setRouteError = useStore((s) => s.setRouteError)
const clearRoute = useStore((s) => s.clearRoute)
const setUserLocation = useStore((s) => s.setUserLocation)
const setGeoPermission = useStore((s) => s.setGeoPermission)
// Proactive geolocation request on mount
useEffect(() => {
if (!navigator.geolocation) {
setGeoPermission('denied')
return
}
navigator.geolocation.getCurrentPosition(
(pos) => {
setUserLocation({ lat: pos.coords.latitude, lon: pos.coords.longitude })
setGeoPermission('granted')
},
() => setGeoPermission('denied'),
{ enableHighAccuracy: false, timeout: 8000, maximumAge: 300000 }
)
}, [setUserLocation, setGeoPermission])
// Fetch route when stops, mode, gpsOrigin, or geoPermission change (debounced 500ms)
// NOTE: userLocation is NOT a dep read from store inside the callback to avoid re-routing on every GPS update
@ -108,6 +91,7 @@ export default function App() {
<Panel onManeuverClick={handleManeuverClick} />
<PlaceDetail />
<LayerControl mapRef={mapViewRef} />
<LocateButton mapRef={mapViewRef} />
</div>
)
}