mirror of
https://github.com/zvx-echo6/navi.git
synced 2026-06-10 17:04:50 +02:00
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:
parent
4020d5ae0a
commit
03e9780834
8 changed files with 216 additions and 72 deletions
54
src/components/LocateButton.jsx
Normal file
54
src/components/LocateButton.jsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { Locate } from 'lucide-react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useStore } from '../store'
|
||||
|
||||
export default function LocateButton({ mapRef }) {
|
||||
const handleClick = () => {
|
||||
const { userLocation } = useStore.getState()
|
||||
|
||||
// If we have a cached location, fly immediately for instant feedback
|
||||
if (userLocation) {
|
||||
mapRef.current?.flyTo(userLocation.lat, userLocation.lon, 14)
|
||||
}
|
||||
|
||||
// Always request fresh position — never trust cached permission state.
|
||||
// iOS Safari can "forget" a silent mount-time denial between requests,
|
||||
// and a user-gesture-triggered call is more likely to prompt.
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(pos) => {
|
||||
const loc = { lat: pos.coords.latitude, lon: pos.coords.longitude }
|
||||
useStore.getState().setUserLocation(loc)
|
||||
useStore.getState().setGeoPermission('granted')
|
||||
// Fly to fresh position if we didn't have a cached one
|
||||
if (!userLocation) {
|
||||
mapRef.current?.flyTo(loc.lat, loc.lon, 14)
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
if (err.code === 1) {
|
||||
// PERMISSION_DENIED — user explicitly denied
|
||||
useStore.getState().setGeoPermission('denied')
|
||||
toast('Location access denied.\nEnable in browser settings.', { icon: '\u{1F4CD}' })
|
||||
} else if (err.code === 3 && !userLocation) {
|
||||
// TIMEOUT — only toast if we have no cached location
|
||||
toast('Location timed out. Try again.', { icon: '\u23F1\uFE0F' })
|
||||
}
|
||||
// POSITION_UNAVAILABLE (code 2): silent, likely temporary
|
||||
},
|
||||
{ enableHighAccuracy: true, timeout: 10000 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!navigator.geolocation) return null
|
||||
|
||||
return (
|
||||
<button
|
||||
className="locate-btn"
|
||||
onClick={handleClick}
|
||||
title="My location"
|
||||
aria-label="Center map on my location"
|
||||
>
|
||||
<Locate size={18} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue