navi/src/components/LocateButton.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

54 lines
1.8 KiB
JavaScript

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={20} />
</button>
)
}