feat(navi): GPS origin + place detail panel + basic actions

Adds synthetic "Your location" stop A when GPS granted; place
detail panel slides in on search result click with Directions /
Add stop / Save (stub) / Share actions; elevation via Valhalla
/height; react-hot-toast for feedback; pendingDestination state
for GPS-denied Directions flow.

Phase 3 Step 5 C1 of Navi.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ubuntu 2026-04-20 20:59:18 +00:00
commit 02f2b25db3
18 changed files with 1207 additions and 274 deletions

View file

@ -14,11 +14,17 @@ import {
} from '@dnd-kit/sortable'
import { useStore } from '../store'
import StopItem from './StopItem'
import GpsOriginItem from './GpsOriginItem'
export default function StopList() {
const stops = useStore((s) => s.stops)
const reorderStops = useStore((s) => s.reorderStops)
const geoPermission = useStore((s) => s.geoPermission)
const gpsOrigin = useStore((s) => s.gpsOrigin)
const pendingDestination = useStore((s) => s.pendingDestination)
const hasGpsOrigin = gpsOrigin && geoPermission === 'granted'
const indexOffset = hasGpsOrigin ? 1 : 0
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
@ -34,25 +40,34 @@ export default function StopList() {
reorderStops(arrayMove(stops, oldIndex, newIndex))
}
if (stops.length === 0) {
if (stops.length === 0 && !hasGpsOrigin) {
return (
<div className="text-gray-500 text-xs px-2 py-3 text-center">
{geoPermission === 'denied'
? 'Add a starting point and destination above'
: 'Search and add stops to build your route'}
<div className="text-xs px-2 py-3 text-center" style={{ color: 'var(--text-tertiary)' }}>
{pendingDestination
? 'Search for a starting point above'
: geoPermission === 'denied'
? 'Add a starting point and destination above'
: 'Search and add stops to build your route'}
</div>
)
}
return (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={stops.map((s) => s.id)} strategy={verticalListSortingStrategy}>
<div className="flex flex-col gap-1">
<div className="flex flex-col gap-1">
{hasGpsOrigin && <GpsOriginItem />}
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={stops.map((s) => s.id)} strategy={verticalListSortingStrategy}>
{stops.map((stop, i) => (
<StopItem key={stop.id} stop={stop} index={i} total={stops.length} />
<StopItem
key={stop.id}
stop={stop}
index={i}
total={stops.length}
indexOffset={indexOffset}
/>
))}
</div>
</SortableContext>
</DndContext>
</SortableContext>
</DndContext>
</div>
)
}