feat(panel): single-panel architecture with UX refinements

Major refactor consolidating two-panel layout (Routes/Contacts + floating
PlaceDetail) into one 400px left column with state-driven content.

Architecture:
- New PlaceCard component for preview and stop cards (collapsible)
- Panel states: IDLE, PREVIEW, ROUTING, PREVIEW_ROUTING, ROUTE_CALCULATED
- usePanelState selector in store.js derives state from selectedPlace/stops/route
- StopList now renders stops as PlaceCard with variant=stop
- PlaceDetail.jsx removed from App.jsx (content moved to PlaceCard)

UX refinements:
- Panel width 400px (was 360px) to fit buttons on one line
- Map zoom padding updated to 420px for wider panel
- Body text bumped to text-sm (14px) for readability
- Get Directions button hidden when 2+ stops (route auto-calculates)
- PlaceCard title prefers feature name (raw.name) over formatted address
- Preview card shows above route during PREVIEW_ROUTING state
- Directions flow no longer shows toast when GPS denied
This commit is contained in:
Matt 2026-04-26 21:14:39 +00:00
commit 5eb83e9b4b
6 changed files with 626 additions and 100 deletions

View file

@ -85,6 +85,9 @@ export const useStore = create((set, get) => ({
addStop({ lat: place.lat, lon: place.lon, name: place.name, source: place.source, matchCode: place.matchCode })
set({ selectedPlace: null })
} else {
// GPS denied, no stops: add destination, show empty origin slot
clearStops()
addStop({ lat: place.lat, lon: place.lon, name: place.name, source: place.source, matchCode: place.matchCode })
set({ pendingDestination: place, selectedPlace: null })
}
},
@ -105,6 +108,9 @@ export const useStore = create((set, get) => ({
if (override) {
localStorage.setItem('navi-theme-override', override)
} else {
// GPS denied, no stops: add destination, show empty origin slot
clearStops()
addStop({ lat: place.lat, lon: place.lon, name: place.name, source: place.source, matchCode: place.matchCode })
localStorage.removeItem('navi-theme-override')
}
},
@ -119,3 +125,15 @@ export const useStore = create((set, get) => ({
setEditingContact: (c) => set({ editingContact: c }),
clearEditingContact: () => set({ editingContact: null }),
}))
// ── Panel state selector ──
// IDLE | PREVIEW | ROUTING | PREVIEW_ROUTING | ROUTE_CALCULATED
export const usePanelState = () => {
return useStore((s) => {
if (s.route) return "ROUTE_CALCULATED"
if (s.selectedPlace && s.stops.length >= 1) return "PREVIEW_ROUTING"
if (s.selectedPlace) return "PREVIEW"
if (s.stops.length >= 1) return "ROUTING"
return "IDLE"
})
}