diff --git a/src/components/MapView.jsx b/src/components/MapView.jsx
index cc717de..9cc0498 100644
--- a/src/components/MapView.jsx
+++ b/src/components/MapView.jsx
@@ -1352,8 +1352,8 @@ const MapView = forwardRef(function MapView(_, ref) {
(b, c) => b.extend(c),
new maplibregl.LngLatBounds(allCoords[0], allCoords[0])
)
- const hasDetail = useStore.getState().selectedPlace != null
- const leftPad = hasDetail ? 700 : 340
+ // Single-panel: no floating detail
+ const leftPad = 420 // 360px panel + margin
map.fitBounds(bounds, { padding: { top: 60, bottom: 60, left: leftPad, right: 60 } })
}
}
@@ -1426,7 +1426,7 @@ const MapView = forwardRef(function MapView(_, ref) {
(b, s) => b.extend([s.lon, s.lat]),
new maplibregl.LngLatBounds([stops[0].lon, stops[0].lat], [stops[0].lon, stops[0].lat])
)
- map.fitBounds(bounds, { padding: { top: 60, bottom: 60, left: 340, right: 60 } })
+ map.fitBounds(bounds, { padding: { top: 60, bottom: 60, left: 420, right: 60 } })
}
}
}, [stops, route, gpsOrigin, geoPermission])
diff --git a/src/components/Panel.jsx b/src/components/Panel.jsx
index 43c58a5..d06c1de 100644
--- a/src/components/Panel.jsx
+++ b/src/components/Panel.jsx
@@ -1,15 +1,18 @@
import { useRef, useCallback, useEffect, useState } from 'react'
import { Sun, Moon } from 'lucide-react'
-import { useStore } from '../store'
+import { useStore, usePanelState } from '../store'
import { hasFeature } from '../config'
import SearchBar from './SearchBar'
import StopList from './StopList'
import ModeSelector from './ModeSelector'
import ManeuverList from './ManeuverList'
import ContactList from './ContactList'
+import { PlaceCard } from './PlaceCard'
import { requestOptimizedRoute } from '../api'
export default function Panel({ onManeuverClick }) {
+ const selectedPlace = useStore((s) => s.selectedPlace)
+ const clearSelectedPlace = useStore((s) => s.clearSelectedPlace)
const stops = useStore((s) => s.stops)
const mode = useStore((s) => s.mode)
const route = useStore((s) => s.route)
@@ -29,6 +32,8 @@ export default function Panel({ onManeuverClick }) {
const activeTab = useStore((s) => s.activeTab)
const setActiveTab = useStore((s) => s.setActiveTab)
+ const panelState = usePanelState()
+
const [isMobile, setIsMobile] = useState(false)
const [optimizing, setOptimizing] = useState(false)
const sheetRef = useRef(null)
@@ -121,38 +126,62 @@ export default function Panel({ onManeuverClick }) {
const showOptimize = effectiveCount >= 3
+ // Determine what to show based on panel state
+ const showPreviewCard = panelState === 'PREVIEW' || panelState === 'PREVIEW_ROUTING'
+ const showRouteSection = panelState === 'ROUTING' || panelState === 'PREVIEW_ROUTING' || panelState === 'ROUTE_CALCULATED'
+ const showManeuvers = panelState === 'ROUTE_CALCULATED'
+ const showEmptyState = panelState === 'IDLE'
+
+ // Routes tab content - now state-driven
const routesContent = (
<>
)
- // Desktop: side panel
+ // Desktop: side panel (now 360px to accommodate PlaceCard)
if (!isMobile) {
return (
0 ? `${h}h ${m}m` : `${h}h`
+}
+
+const DAY_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
+
+function parseHours(hoursStr) {
+ try {
+ const oh = new OpeningHours(hoursStr, { address: { country_code: "us", state: "Idaho" } })
+ const now = new Date()
+ const isOpen = oh.getState(now)
+ const nextChange = oh.getNextChange(now)
+ let todayStr = ""
+ if (isOpen) {
+ todayStr = "Open now"
+ if (nextChange) {
+ const closeTime = nextChange.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" })
+ todayStr += " \u00b7 Closes " + closeTime
+ }
+ } else {
+ todayStr = "Closed"
+ if (nextChange) {
+ const openTime = nextChange.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" })
+ const isTodayOpen = nextChange.getDate() === now.getDate()
+ todayStr += " \u00b7 Opens " + (isTodayOpen ? "at " : "tomorrow ") + openTime
+ }
+ }
+ const week = []
+ for (let d = 0; d < 7; d++) {
+ const date = new Date(now)
+ const diff = (d - now.getDay() + 7) % 7
+ date.setDate(now.getDate() + diff)
+ date.setHours(0, 0, 0, 0)
+ const intervals = oh.getOpenIntervals(date, new Date(date.getTime() + 86400000))
+ if (intervals.length === 0) {
+ week.push({ day: DAY_SHORT[d], hours: "Closed", isTodayRow: d === now.getDay() })
+ } else {
+ const parts = intervals.map(([start, end]) => {
+ const s = start.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" })
+ const e = end.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" })
+ return s + " \u2013 " + e
+ })
+ week.push({ day: DAY_SHORT[d], hours: parts.join(", "), isTodayRow: d === now.getDay() })
+ }
+ }
+ return { isOpen, todayStr, week }
+ } catch {
+ return null
+ }
+}
+
+function formatPhone(phone) {
+ if (!phone) return null
+ const digits = phone.replace(/[^\d]/g, "")
+ if (digits.length === 11 && digits[0] === "1") {
+ return "(" + digits.slice(1, 4) + ") " + digits.slice(4, 7) + "-" + digits.slice(7)
+ }
+ if (digits.length === 10) {
+ return "(" + digits.slice(0, 3) + ") " + digits.slice(3, 6) + "-" + digits.slice(6)
+ }
+ return phone
+}
+
+function wheelchairLabel(val) {
+ if (!val) return null
+ const map = { yes: "Accessible", limited: "Limited access", no: "Not accessible" }
+ return map[val.toLowerCase()] || null
+}
+
+function wikiUrl(wp) {
+ if (!wp) return null
+ const [lang, ...rest] = wp.split(":")
+ const title = rest.join(":").replace(/ /g, "_")
+ return "https://" + lang + ".wikipedia.org/wiki/" + encodeURIComponent(title)
+}
+
+function wikiLabel(wp) {
+ if (!wp) return null
+ const [, ...rest] = wp.split(":")
+ return rest.join(":").replace(/_/g, " ")
+}
+
+function DetailSection({ label, icon: Icon, first, children }) {
+ return (
+
+
+
+ {label}
+
+ {children}
+
+ )
+}
+
+function HoursDisplay({ hoursStr, first }) {
+ const [expanded, setExpanded] = useState(false)
+ const parsed = parseHours(hoursStr)
+ if (!parsed) return null
+ const { isOpen, todayStr, week } = parsed
+ return (
+
+ setExpanded((v) => !v)} className="w-full flex items-center justify-between text-xs" style={{ color: "var(--text-primary)" }}>
+ {todayStr}
+ {expanded ? : }
+
+ {expanded && (
+
+ {week.map((w) => (
+
+ {w.day}
+ {w.hours}
+
+ ))}
+
+ )}
+
+ )
+}
+
+function LandclassSection({ data }) {
+ if (!data || !data.summary) return null
+ return (
+
+
+
+ {data.summary}
+ {data.unit_name && {data.unit_name} }
+
+
+ )
+}
+
+function PrivateLandIndicator({ data }) {
+ if (!data || data.gap_status !== "4") return null
+ return (
+
+ Private land — permission required
+
+ )
+}
+
+function EnrichmentSkeleton() {
+ return (
+
+ )
+}
+
+function EnrichmentSections({ details }) {
+ if (!details) return null
+ const { category, extratags } = details
+ const et = extratags || {}
+ const hasAbout = category
+ const hasHours = et.opening_hours
+ const hasContact = et.phone || et.website || et.email
+ const hasDetails = et.cuisine || et.operator || et.fee || et.wheelchair || et.takeaway
+ const hasLinks = et.wikipedia || et.wikidata
+ if (!hasAbout && !hasHours && !hasContact && !hasDetails && !hasLinks) return null
+ let idx = 0
+ return (
+
+ {hasAbout && (
+
+ {category}
+
+ )}
+ {hasHours &&
}
+ {hasContact && (
+
+
+
+ )}
+ {hasDetails && (
+
+
+ {et.cuisine && Cuisine: {et.cuisine.replace(/_/g, " ").replace(/;/g, ", ")} }
+ {et.operator && Operated by {et.operator} }
+ {et.fee && {et.fee === "no" ? "Free" : "Fee: " + et.fee} }
+ {et.wheelchair && wheelchairLabel(et.wheelchair) && {wheelchairLabel(et.wheelchair)} }
+ {et.takeaway === "yes" && Takeaway available }
+
+
+ )}
+ {hasLinks && (
+
+
+
+ )}
+
+ )
+}
+
+function CopyPopover({ address, place, onClose }) {
+ const ref = useRef(null)
+ useEffect(() => {
+ function handleClick(e) { if (ref.current && !ref.current.contains(e.target)) onClose() }
+ document.addEventListener("mousedown", handleClick)
+ return () => document.removeEventListener("mousedown", handleClick)
+ }, [onClose])
+ const copyAddress = () => {
+ const text = [place.name, address].filter(Boolean).join("\n")
+ navigator.clipboard.writeText(text).then(() => toast("Address copied"), () => toast.error("Failed to copy"))
+ onClose()
+ }
+ const copyCoords = () => {
+ const text = place.lat.toFixed(6) + ", " + place.lon.toFixed(6)
+ navigator.clipboard.writeText(text).then(() => toast("Coordinates copied"), () => toast.error("Failed to copy"))
+ onClose()
+ }
+ return (
+
+ Address
+ Coordinates
+
+ )
+}
+
+export function PlaceCard({ place, variant = "preview", expanded = true, onToggleExpand, onClose, onRemove, stopIndex, draggable = false, dragHandleProps = {} }) {
+ const { contacts, userLocation, stops, geoPermission, addStop, startDirections, clearSelectedPlace, setEditingContact } = useStore()
+ const [elevResult, setElevResult] = useState({ lat: null, lon: null, value: null })
+ const [placeDetails, setPlaceDetails] = useState(null)
+ const [driveTime, setDriveTime] = useState(null)
+ const [nearbyLabel, setNearbyLabel] = useState(null)
+ const [landclass, setLandclass] = useState(null)
+ const [copyOpen, setCopyOpen] = useState(false)
+
+ const placeLat = place?.lat
+ const placeLon = place?.lon
+ const osmType = place?.raw?.osm_type
+ const osmId = place?.raw?.osm_id
+ const wikidataId = place?.wikidata || place?.raw?.wikidata
+
+ useEffect(() => {
+ if (placeLat == null || placeLon == null) return
+ let cancelled = false
+ fetchElevation(placeLat, placeLon).then((h) => { if (!cancelled) setElevResult({ lat: placeLat, lon: placeLon, value: h }) })
+ return () => { cancelled = true }
+ }, [placeLat, placeLon])
+
+ useEffect(() => {
+ if (!hasFeature("has_nominatim_details") || !osmType || !osmId) { setPlaceDetails(null); return }
+ const controller = new AbortController()
+ setPlaceDetails("loading")
+ fetchPlaceDetails(osmType, osmId, controller.signal).then((data) => {
+ if (!controller.signal.aborted) {
+ setPlaceDetails(data || null)
+ if (data?.boundary) {
+ const current = useStore.getState().selectedPlace
+ if (current && current.lat === placeLat && current.lon === placeLon) {
+ useStore.getState().setSelectedPlace({ ...current, boundary: data.boundary })
+ }
+ }
+ }
+ })
+ return () => controller.abort()
+ }, [osmType, osmId, placeLat, placeLon])
+
+ useEffect(() => {
+ if (osmType && osmId) return
+ if (!wikidataId) return
+ const controller = new AbortController()
+ fetchPlaceByWikidata(wikidataId, controller.signal).then((data) => {
+ if (!controller.signal.aborted && data) {
+ setPlaceDetails((prev) => ({
+ ...(prev === "loading" ? {} : prev || {}),
+ description: data.description,
+ population: data.population,
+ osm_relation_id: data.osm_relation_id,
+ extratags: { ...(prev && prev !== "loading" ? prev.extratags : {}), ...data.extratags },
+ }))
+ if (data?.boundary) {
+ const current = useStore.getState().selectedPlace
+ if (current && current.lat === placeLat && current.lon === placeLon) {
+ useStore.getState().setSelectedPlace({ ...current, boundary: data.boundary })
+ }
+ }
+ }
+ })
+ return () => controller.abort()
+ }, [wikidataId, osmType, osmId, placeLat, placeLon])
+
+ useEffect(() => {
+ if (variant !== "preview" || !userLocation || placeLat == null || placeLon == null) { setDriveTime(null); return }
+ setDriveTime(null)
+ const controller = new AbortController()
+ const timeout = setTimeout(() => controller.abort(), 3000)
+ fetchDriveTime(userLocation.lat, userLocation.lon, placeLat, placeLon, controller.signal).then((time) => { if (!controller.signal.aborted) setDriveTime(time) })
+ return () => { controller.abort(); clearTimeout(timeout) }
+ }, [variant, userLocation?.lat, userLocation?.lon, placeLat, placeLon])
+
+ useEffect(() => {
+ if (!hasFeature("has_contacts") || placeLat == null || placeLon == null) { setNearbyLabel(null); return }
+ const controller = new AbortController()
+ fetchNearbyContacts(placeLat, placeLon, 75, controller.signal).then((nearby) => {
+ if (!controller.signal.aborted && nearby.length > 0) setNearbyLabel(nearby[0].label)
+ else if (!controller.signal.aborted) setNearbyLabel(null)
+ })
+ return () => controller.abort()
+ }, [placeLat, placeLon])
+
+ useEffect(() => {
+ if (!hasFeature("has_landclass") || placeLat == null || placeLon == null) { setLandclass(null); return }
+ const controller = new AbortController()
+ fetchLandclass(placeLat, placeLon, controller.signal).then((data) => {
+ if (!controller.signal.aborted && data) {
+ setLandclass(data)
+ if (data.summary && useStore.getState().selectedPlace?.name === "Dropped pin") {
+ const current = useStore.getState().selectedPlace
+ useStore.getState().setSelectedPlace({ ...current, name: data.summary })
+ }
+ } else if (!controller.signal.aborted) setLandclass(null)
+ })
+ return () => controller.abort()
+ }, [placeLat, placeLon])
+
+ if (!place) return null
+
+ const address = buildAddress(place)
+ const elevLoading = placeLat != null && (elevResult.lat !== placeLat || elevResult.lon !== placeLon)
+ const elevation = !elevLoading ? elevResult.value : null
+ const elevFeet = elevation != null ? Math.round(elevation * M_TO_FT) : null
+ const existingStopIndex = stops.findIndex((s) => s.lat === place.lat && s.lon === place.lon)
+ const savedContact = contacts.find((c) => c.lat === place.lat && c.lon === place.lon)
+
+ const handleDirections = () => {
+ // No toast - empty origin slot is the visual prompt
+ startDirections(place)
+ }
+ const handleAddStop = () => {
+ addStop({ lat: place.lat, lon: place.lon, name: place.name, source: place.source, matchCode: place.matchCode })
+ clearSelectedPlace()
+ }
+ const handleSave = () => {
+ if (!hasFeature("has_contacts")) { toast("Saved places coming soon"); return }
+ if (savedContact) setEditingContact(savedContact)
+ else setEditingContact({ label: "", lat: place.lat, lon: place.lon, osm_type: osmType || null, osm_id: osmId || null, address: address || "", name: place.type === "poi" && place.raw?.name ? place.raw.name : "" })
+ }
+ const closeCopy = useCallback(() => setCopyOpen(false), [])
+ const stopLetter = stopIndex != null ? String.fromCharCode(65 + stopIndex) : null
+
+ if (!expanded) {
+ return (
+
+ {draggable &&
}
+ {stopLetter &&
{stopLetter}
}
+
{(place.raw?.name || place.name) || "Unknown place"}
+
+ {onRemove &&
{ e.stopPropagation(); onRemove() }} className="p-1 rounded hover:opacity-80" style={{ color: "var(--text-tertiary)" }}> }
+
+ )
+ }
+
+ return (
+
+
+
+ {draggable &&
}
+ {stopLetter &&
{stopLetter}
}
+
+
{(place.raw?.name || place.name) || "Unknown place"}
+
+ {place.type && {place.type} }
+ {driveTime != null && <>{"\u00b7"} {formatDriveTime(driveTime)} drive >}
+ {nearbyLabel && <>{"\u00b7"} Near {nearbyLabel} >}
+
+
+
+
+ {onToggleExpand && variant === "stop" && }
+ {onClose && }
+
+
+ {address &&
{address}
}
+
+ {place.lat.toFixed(6)}, {place.lon.toFixed(6)}
+ {"\u00b7"}
+ {elevLoading ? "..." : elevFeet != null ? elevFeet.toLocaleString() + " ft" : "\u2014"}
+
+
+
+ {placeDetails === "loading" &&
}
+ {placeDetails && placeDetails !== "loading" &&
}
+
+ {variant === "preview" && (
+ <>
+ {stops.length < 2 &&
Get Directions}
+ {existingStopIndex >= 0 ? (
+
Stop {String.fromCharCode(65 + existingStopIndex)}
+ ) : (
+
Add stop
+ )}
+ >
+ )}
+ {variant === "stop" && onRemove &&
Remove}
+
+
+ setCopyOpen((v) => !v)} className="p-2 rounded-lg flex items-center gap-0.5" style={{ background: "var(--tan-muted)", color: "var(--tan)", border: "1px solid var(--border)" }} aria-label="Copy">
+ {copyOpen && }
+
+
+
+ )
+}
+
+export default PlaceCard
diff --git a/src/components/StopList.jsx b/src/components/StopList.jsx
index a370b09..80cb538 100644
--- a/src/components/StopList.jsx
+++ b/src/components/StopList.jsx
@@ -1,73 +1,117 @@
-import {
- DndContext,
- closestCenter,
- KeyboardSensor,
- PointerSensor,
- useSensor,
- useSensors,
-} from '@dnd-kit/core'
-import {
- arrayMove,
- SortableContext,
- sortableKeyboardCoordinates,
- verticalListSortingStrategy,
-} 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 } }),
- useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
- )
-
- function handleDragEnd(event) {
- const { active, over } = event
- if (!over || active.id === over.id) return
-
- const oldIndex = stops.findIndex((s) => s.id === active.id)
- const newIndex = stops.findIndex((s) => s.id === over.id)
- reorderStops(arrayMove(stops, oldIndex, newIndex))
- }
-
- if (stops.length === 0 && !hasGpsOrigin) {
- return (
-
- {pendingDestination
- ? 'Search for a starting point above'
- : geoPermission === 'denied'
- ? 'Add a starting point and destination above'
- : 'Search and add stops to build your route'}
-
- )
- }
-
- return (
-
- {hasGpsOrigin && }
-
- s.id)} strategy={verticalListSortingStrategy}>
- {stops.map((stop, i) => (
-
- ))}
-
-
-
- )
-}
+import { useState } from 'react'
+import {
+ DndContext,
+ closestCenter,
+ KeyboardSensor,
+ PointerSensor,
+ useSensor,
+ useSensors,
+} from '@dnd-kit/core'
+import {
+ arrayMove,
+ SortableContext,
+ sortableKeyboardCoordinates,
+ verticalListSortingStrategy,
+ useSortable,
+} from '@dnd-kit/sortable'
+import { CSS } from '@dnd-kit/utilities'
+import { useStore } from '../store'
+import { PlaceCard } from './PlaceCard'
+import GpsOriginItem from './GpsOriginItem'
+
+// Wrapper to make PlaceCard sortable
+function SortableStopCard({ stop, index, indexOffset }) {
+ const removeStop = useStore((s) => s.removeStop)
+ const [expanded, setExpanded] = useState(false)
+
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
+ useSortable({ id: stop.id })
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ opacity: isDragging ? 0.5 : 1,
+ }
+
+ // Convert stop to place format for PlaceCard
+ const place = {
+ lat: stop.lat,
+ lon: stop.lon,
+ name: stop.name,
+ source: stop.source,
+ matchCode: stop.matchCode,
+ type: stop.type || null,
+ raw: stop.raw || null,
+ wikidata: stop.wikidata || null,
+ }
+
+ return (
+
+
setExpanded(!expanded)}
+ onRemove={() => removeStop(stop.id)}
+ stopIndex={index + indexOffset}
+ draggable={true}
+ dragHandleProps={{ ...attributes, ...listeners }}
+ />
+
+ )
+}
+
+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 } }),
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
+ )
+
+ function handleDragEnd(event) {
+ const { active, over } = event
+ if (!over || active.id === over.id) return
+
+ const oldIndex = stops.findIndex((s) => s.id === active.id)
+ const newIndex = stops.findIndex((s) => s.id === over.id)
+ reorderStops(arrayMove(stops, oldIndex, newIndex))
+ }
+
+ if (stops.length === 0 && !hasGpsOrigin) {
+ return (
+
+ {pendingDestination
+ ? 'Search for a starting point above'
+ : geoPermission === 'denied'
+ ? 'Add a starting point and destination above'
+ : 'Search and add stops to build your route'}
+
+ )
+ }
+
+ return (
+
+ {hasGpsOrigin && }
+
+ s.id)} strategy={verticalListSortingStrategy}>
+ {stops.map((stop, i) => (
+
+ ))}
+
+
+
+ )
+}
diff --git a/src/store.js b/src/store.js
index 8e310d0..8ce6fcc 100644
--- a/src/store.js
+++ b/src/store.js
@@ -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"
+ })
+}