refactored-recon/NAVI-DIRECTIONS-REDESIGN.md
Matt 82d19e7fb8 design: extend with single-panel architecture (Phases k-o)
Adds the broader panel design that consolidates today's two-panel
layout (Routes/Contacts + floating PlaceDetail) into one always-visible
left column with state-driven content. Defines five panel states
(IDLE, PREVIEW, ROUTING, PREVIEW+ROUTING, ROUTE_CALCULATED), shared
place-card component, search bar behavior including aspirational
search history, and implementation phases k-o.
2026-04-26 19:58:55 +00:00

842 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Navi Directions UX Redesign
**Status:** Draft
**Author:** Claude + Matt
**Date:** 2026-04-26
**Implementation:** Deferred to dedicated session
---
## 1. Current State
### Components
| Component | File | Role |
|-----------|------|------|
| SearchBar | `SearchBar.jsx` | Overloaded: search, add stop, set origin (hidden modes) |
| StopList | `StopList.jsx` | Drag-drop reordering of stops |
| GpsOriginItem | `GpsOriginItem.jsx` | "Your location" row when GPS granted |
| StopItem | `StopItem.jsx` | Individual stop with delete button |
| ModeSelector | `ModeSelector.jsx` | auto/pedestrian/bicycle toggle |
| ManeuverList | `ManeuverList.jsx` | Turn-by-turn directions display |
| PlaceDetail | `PlaceDetail.jsx` | "Directions" button for selected place |
### State Model
```javascript
stops: [] // Array of {id, lat, lon, name, source, matchCode}
gpsOrigin: true // Use GPS as origin when available
pendingDestination: null // Place waiting for origin (GPS-denied flow)
route: null // Valhalla trip response
routeLoading: false
routeError: null
```
### Failure Modes
1. **No visible from/to inputs** — Users cannot see or directly edit origin/destination
2. **SearchBar hidden mode-switching** — Three different behaviors based on invisible state:
- Normal: opens place detail
- With `pendingDestination`: first result becomes origin
- After adding stops: unclear which role next selection plays
3. **GPS-denied flow uses ephemeral toast** — "Set a starting point" disappears, no persistent UI guidance
4. **No swap button** — Cannot reverse route direction
5. **No map context menu** — Right-click/long-press does nothing
6. **No waypoint addition UI** — Only drag-drop reordering, no insert-between
7. **Place panel "Directions" silently sets up route** — Based on hidden state, no confirmation
---
## 2. Design Principles
1. **Direct manipulation over hidden modes** — Every action should have visible UI
2. **Two visible inputs always** — When in directions mode, From and To fields are always visible
3. **Spatial interactions over linear** — Radial menu for map interactions, not dropdowns
4. **Same gesture model everywhere** — Right-click (desktop) = long-press (mobile)
5. **Preserve existing state model**`stops[]` array stays, just better UI on top
---
## 3. Visual Mockup — Directions Panel
```
┌─────────────────────────────────────┐
│ DIRECTIONS │
├─────────────────────────────────────┤
│ │
│ From: [📍 Your location ][×] │
│ ──────────────────────── │
│ [⇅] │ ← Swap button
│ ──────────────────────── │
│ To: [Coffee shop on Main St][×] │
│ │
│ [+ Add stop] │
│ │
├─────────────────────────────────────┤
│ [🚗 Auto] [🚶 Walk] [🚲 Bike] │
├─────────────────────────────────────┤
│ ┌─────────────────────────────┐ │
│ │ 12 min · 4.2 mi │ │
│ │ via W Main St │ │
│ └─────────────────────────────┘ │
│ │
│ ▼ Turn-by-turn (expandable) │
│ → Head north on Oak Ave │
│ ↱ Turn right onto Main St │
│ ◉ Arrive at destination │
│ │
└─────────────────────────────────────┘
```
### Input States
**From field:**
- GPS granted: Shows "📍 Your location" pill with clear button
- GPS denied/cleared: Empty, placeholder "Starting point..."
- Filled: Shows place name with clear button
**To field:**
- Empty: Placeholder "Destination..."
- Filled: Shows place name with clear button
**Active input:**
- Blue border highlight
- Search dropdown appears on typing
- Map click populates this field
---
## 4. Visual Mockup — Radial Map Menu
```
Drop pin
🔴
Directions ╲ Directions
from here 🟢──────────────🔵 to here
│ 43.6166 │
│ -116.2008 │
│ [loading…] │ ← Center disc with coords/label
Add as 🟡──────────────🟣 Save place
stop ╲
🟠
What's here
```
### Wedge Layout (60° each)
| Position | Action | Icon | Color |
|----------|--------|------|-------|
| Top | Drop pin | Pin | Red |
| Top-right | Directions to here | Arrow-in | Blue |
| Bottom-right | Save place | Star + 🔒 | Purple |
| Bottom | What's here | Info | Orange |
| Bottom-left | Add as stop | Plus | Yellow |
| Top-left | Directions from here | Arrow-out | Green |
### Behavior
- **Trigger:** Right-click (desktop) or long-press 450ms (mobile)
- **Center disc:** ~40px diameter, shows coordinates immediately, reverse-geocoded label async
- **Wedge highlight:** On hover (desktop) or drag-over (mobile)
- **Commit:** Release on wedge (mobile) or click wedge (desktop)
- **Cancel:** Release outside, Escape key, tap elsewhere
---
## 5. Component Breakdown
### DirectionsPanel
Replaces current Panel directions mode.
```
Props: none (reads from store)
State: none (all in global store)
Children:
- LocationInput (from)
- SwapButton
- LocationInput (to)
- WaypointList (if stops.length > 2)
- AddStopButton
- ModeSelector
- RouteSummary
- ManeuverList (collapsible)
```
### LocationInput
Reusable component for from, to, and waypoint inputs.
```
Props:
- slot: 'from' | 'to' | `waypoint:${index}`
- value: { lat, lon, name, source } | null
- placeholder: string
- showGpsPill: boolean
- onClear: () => void
Features:
- Search-as-you-type (Photon geocoder)
- GPS pill state with clear button
- Active-input visual state (blue border)
- Reverse-geocoded labels for coord-only entries
- Dropdown for search results
```
### SwapButton
Simple button between From and To inputs.
```
Props: none
Action: Swaps stops[0] and stops[stops.length - 1]
Visual: ⇅ icon, hover highlight
```
### WaypointList
Refactored from existing StopList, preserves drag-drop.
```
Props: none (reads stops from store)
Features:
- Only renders stops[1..n-1] (middle waypoints)
- Drag-drop reordering via @dnd-kit
- Delete button per waypoint
- "Via" label prefix
```
### RadialMenu
New general-purpose component.
```
Props:
- open: boolean
- x: number (screen X)
- y: number (screen Y)
- lat: number
- lon: number
- wedges: Array<{ id, icon, label, action: (lat, lon) => void }>
- onClose: () => void
Features:
- Configurable wedge count and actions
- Async center label (reverse geocode)
- Keyboard dismissal (Escape)
- Touch-friendly sizing on mobile
- Fade in/out animations
```
---
## 6. State Model
### Existing (unchanged)
```javascript
stops: [] // Origin = stops[0], destination = stops[last], waypoints in between
gpsOrigin: boolean // Whether GPS should be used as origin
route: object | null // Valhalla trip response
routeLoading: boolean
routeError: string | null
```
### New
```javascript
activeInputSlot: 'from' | 'to' | `waypoint:${N}` | null
// Which input is currently focused/active for map-click-to-fill
radialMenuState: {
open: boolean,
x: number, // Screen coordinates
y: number,
lat: number, // Map coordinates
lon: number,
label: string | null // Reverse-geocoded, async populated
}
// Pin persistence (three-tier)
transientPins: [] // In-memory, lost on refresh
// localStorage: 'navi_saved_pins' key for guest saves
// Backend sync: future work for authed users
```
### Removed
```javascript
pendingDestination: null // No longer needed — explicit inputs replace hidden state
```
---
## 7. Interaction Flows
### Open directions tab fresh
1. From field shows GPS pill if `geoPermission === 'granted'`, else empty
2. To field is empty, focused by default
3. No route calculated yet
### Click "Directions" from place panel
1. Directions panel opens (if not already)
2. To field auto-fills with selected place
3. From field:
- If GPS granted: shows GPS pill
- Else: empty, receives focus
4. Route calculates if both filled
### Type in input
1. Input receives focus, becomes `activeInputSlot`
2. Photon search fires on debounce (300ms)
3. Dropdown shows results
4. Select result → populates input, clears dropdown
5. Route recalculates
### Right-click / long-press on map
1. Radial menu appears centered on click point
2. Center disc shows coordinates immediately
3. Reverse geocode fires async, populates label
4. User hovers/drags to wedge:
| Wedge | Action |
|-------|--------|
| **Directions from here** | Opens directions if closed, fills From with coords, focuses To |
| **Directions to here** | Opens directions if closed, fills To with coords, focuses From if empty |
| **Add as stop** | Inserts new stop before destination |
| **What's here** | Reverse geocode → opens place panel |
| **Drop pin** | Creates transient marker (session-only by default) |
| **Save place** | Guest: opens login flow; Authed: opens save dialog |
5. Release outside or Escape → dismisses without action
### Map click with active input
When directions panel is open and an input is focused (`activeInputSlot !== null`):
1. Single click on map
2. Clicked coordinates populate the active input
3. Reverse geocode fires to get display name
4. Input loses focus, `activeInputSlot = null`
5. Route recalculates
### Map long-press / right-click with active input
Even when an input is active:
1. Long-press / right-click opens radial menu (overrides click-to-fill)
2. User can select "Directions from here" or "Directions to here"
3. Explicitly overrides the active input — intentional action takes priority
### Map click with no active input
1. No action (map interaction only)
2. Future v2 consideration: reverse-geocode and show place panel
### Swap button
1. Click swap button
2. `stops[0]` and `stops[stops.length - 1]` swap positions
3. If GPS was origin, GPS pill moves to destination (unusual but allowed)
4. Route recalculates
---
## 8. Place Panel "Directions" Handoff
**Current behavior:** Calls `startDirections(place)` with complex conditional logic, may show toast.
**New behavior:**
```javascript
handleDirections = () => {
// Always open directions panel
setActiveTab('directions')
// Fill destination
setStop(stops.length, { // Appends or replaces last
lat: place.lat,
lon: place.lon,
name: place.name,
source: place.source
})
// Handle origin
if (geoPermission === 'granted') {
setGpsOrigin(true) // GPS pill in From
} else if (stops.length === 0) {
setActiveInputSlot('from') // Focus From input
}
// Close place panel
clearSelectedPlace()
}
```
**No toast needed** — UI is self-explanatory with visible From/To fields.
---
## 9. Radial Menu Specifics
### Trigger
| Platform | Gesture | Duration |
|----------|---------|----------|
| Desktop | Right-click | Instant |
| Mobile | Long-press | 450ms |
### Conflict Avoidance
Long-press must NOT fire during active pan:
- Track touch start position
- If touch moves >5-10px before timer fires, cancel long-press
- Pan gesture takes priority
- Matches iOS Safari default contextmenu synthesis behavior
### Geometry
```
Outer radius: ~80px from center (desktop), ~100px (mobile)
Inner radius: ~40px (center disc, desktop), ~50px (mobile)
Wedge angle: 60° each (6 wedges)
Gap between wedges: 2px
Minimum touch target: 48px per wedge
```
### Visual States
| Element | Default | Hover/Active | Selected |
|---------|---------|--------------|----------|
| Wedge background | `rgba(0,0,0,0.7)` | `rgba(0,0,0,0.85)` | Wedge accent color |
| Wedge icon | White, 50% opacity | White, 100% opacity | White |
| Wedge label | Hidden | Shown (tooltip) | Shown |
| Center disc | Dark, coords visible | — | — |
| Save place (guest) | Lock icon overlay | — | — |
### Animation
- **Fade in:** <100ms ease-out
- **Fade out:** <150ms ease-in
- **Wedge hover:** Instant background change
- **Center label:** Fade in when reverse geocode completes
---
## 10. Mobile Considerations
### Panel Layout
**Decision: Bottom sheet on mobile (<768px), side panel on desktop.**
Bottom sheet states:
- **Peek:** Route summary only (collapsed)
- **Half:** Inputs + summary visible
- **Full:** Inputs + turn-by-turn maneuvers
Drag handle at top for resize between states.
**Implementation notes:**
- Use React state for sheet position: `sheetState: 'peek' | 'half' | 'full'`
- CSS transforms for smooth transitions: `transform: translateY()`
- Touch gesture detection for drag-to-resize
### Long-press Timing
**Decision: 450ms with 5-10px movement threshold.**
If finger moves more than threshold during press window, abort long-press and treat as pan. This matches iOS Safari's default contextmenu synthesis behavior.
**Implementation notes:**
- Create reusable `useLongPress` hook
- Track `touchstart` position
- `setTimeout` for 450ms
- Clear timeout on `touchmove` if delta > threshold
- Fire callback on timeout completion
### Radial Sizing
Mobile radial larger for finger touch:
- Outer radius: ~100px (vs 80px desktop)
- Center disc: ~50px (vs 40px desktop)
- Minimum wedge touch target: 48px
### Compact Directions Mode
When route is calculated and user is navigating:
1. Collapse From/To inputs to single-line summary
2. Show prominent next maneuver
3. Expand on tap to edit inputs
4. Maneuver list scrollable
### Keyboard Awareness
- Detect keyboard open via `visualViewport` API
- Shift panel content up to keep active input visible
- Don't let keyboard overlap input being typed in
---
## 11. Place Panel Restructure
**Out of scope for this document.**
Separate session will address:
- Cleaner info card layout (Google Maps style)
- Better visual hierarchy
- Action button placement
- No new data sources, just CSS/JSX polish
---
## 12. Out of Scope (Future Phases)
| Feature | Notes |
|---------|-------|
| Saved routes | Auth required, dedicated work |
| Route alternatives | Valhalla supports, surface in v2 |
| Avoid tolls/highways | Valhalla supports via costing options |
| Real-time rerouting | Requires location tracking loop |
| Multi-modal | Drive + transit + walk hybrids |
| Traffic-aware routing | Requires traffic data source |
| Offline routing | Requires local Valhalla instance |
| Search history backend persistence | Frontend localStorage first |
| Saved places browsing UI | Future dedicated work |
| Contacts view redesign | Separate from route-building |
---
## 13. Implementation Sequence
| Phase | Task | Depends On |
|-------|------|------------|
| **a** | Build RadialMenu component (general-purpose, no actions wired) | — |
| **b** | Wire "What's here" action to validate trigger + reverse-geocode flow | a |
| **c** | Refactor SearchBar to single-mode (search-only, remove pending* logic) | — |
| **d** | Build LocationInput component (reusable) | c |
| **e** | Build DirectionsPanel layout with two LocationInputs | d |
| **f** | Wire remaining radial actions to directions flow | b, e |
| **g** | Wire place panel "Directions" handoff to new flow | e |
| **h** | Add SwapButton | e |
| **i** | Add map-click-to-fill-active-input | e |
| **j** | Mobile polish (long-press timing, bottom sheet, keyboard) | a-i |
### Phases k-o: Single-Panel Architecture
| Phase | Task | Depends On |
|-------|------|------------|
| **k** | Refactor Panel.jsx to single-column state-driven content, remove right PlaceDetail panel | — |
| **l** | Build shared place card component (preview + stop cards) | k |
| **m** | Wire panel state transitions (IDLE → PREVIEW → ROUTING → ROUTE_CALCULATED) | l |
| **n** | Mobile bottom sheet state mapping | m |
| **o** | Search history (frontend localStorage scope first) | m |
**Estimated phases:** 15 discrete tasks, can be done incrementally.
---
## 14. Resolved Decisions
### 1. Mobile Layout
**Decision:** Bottom sheet on mobile (<768px breakpoint), side panel on desktop.
Three sheet states:
- **Peek:** Summary only (route time/distance)
- **Half:** Inputs + summary visible
- **Full:** Inputs + turn-by-turn maneuvers
Drag-to-resize between states via handle at top of sheet.
**Implementation notes:**
- `sheetState` in store: `'peek' | 'half' | 'full'`
- CSS transforms (`translateY`) for smooth animation
- Touch gesture handler for drag detection
- Snap to nearest state on release
### 2. Long-press Timing
**Decision:** 450ms with 5-10px movement threshold.
If finger moves more than threshold during the press window, abort long-press and treat as pan gesture. This matches iOS Safari's default contextmenu synthesis behavior.
**Implementation notes:**
- Create `useLongPress(callback, delay = 450)` hook
- Track `touchstart` coordinates
- On `touchmove`, check if `Math.hypot(dx, dy) > threshold`
- If exceeded, clear timeout (abort)
- If timeout fires without abort, invoke callback with coords
### 3. Save Place for Guests
**Decision:** Visible wedge with subtle lock icon overlay to indicate auth required.
Tapping as guest opens login flow with return-to-action after auth completes.
**Implementation notes:**
- "Save place" wedge always visible
- Lock icon (🔒) overlaid at 50% opacity on wedge icon
- On tap: check auth state
- Authed: open save dialog
- Guest: trigger login modal with `returnAction: 'save-place'`
- After successful auth, resume save flow
### 4. Radial Ring Structure
**Decision:** Single ring only. Six wedges maximum.
Future expansion via contextual wedge sets (different actions based on what was clicked — e.g., clicking on a route segment could show "Avoid this road" instead of "Drop pin") rather than deeper nested rings.
**Implementation notes:**
- `wedges` prop on RadialMenu is array of 6 max
- Context-aware wedge selection handled by parent component
- No inner ring in v1; revisit if six actions prove insufficient
### 5. Drop Pin Persistence
**Decision:** Three-tier model.
| Tier | Scope | Storage | Behavior |
|------|-------|---------|----------|
| Transient | Session | In-memory (`transientPins[]`) | Default. Lost on refresh. |
| Guest save | Device | `localStorage` (`navi_saved_pins`) | Survives refresh, lost on cache clear or other device. |
| Authed sync | Account | Backend API | Syncs across devices. Future work. |
**Implementation notes:**
- Transient pins: `transientPins: []` in store
- Guest save: on "Save" tap, copy pin to localStorage
- localStorage schema: `[{ id, lat, lon, name, createdAt }]`
- Backend sync: TODO placeholder; initial implementation is localStorage only
- Authed users see "Sync to account" option (greyed out with "Coming soon" for v1)
### 6. Radial vs Click During Active Input
**Decision:** Different gestures, different behaviors.
| State | Gesture | Behavior |
|-------|---------|----------|
| Active input | Map click | Fills active input with click coords (reverse-geocoded) |
| Active input | Map long-press / right-click | Opens radial (explicit override allowed) |
| No active input | Map click | No action (v2: reverse-geocode + place panel) |
| No active input | Map long-press / right-click | Opens radial (primary use case) |
**Implementation notes:**
- Click handler checks `activeInputSlot`
- If set: fill input, reverse-geocode for label
- If null: no-op (or v2 place panel)
- Long-press / contextmenu handler always opens radial
- Radial "Directions from/to here" explicitly sets input, overriding prior value
---
## 15. Panel Architecture (Single-Column Model)
This section defines the broader panel design that consolidates today's
two-panel layout (Routes/Contacts + floating PlaceDetail) into one
always-visible left column with state-driven content.
### Overview
The left panel is one always-visible column with state-driven content.
The right-side PlaceDetail panel is **removed**; its content moves into
the left panel.
**Width:** ~360px desktop. Mobile: panel becomes bottom sheet per
Section 10.
**Visual style:** Panel is always anchored at the left edge of the
viewport with consistent panel background. When idle (no preview, no
route), only the search bar is visible at the top — the rest of the
panel is empty space. The panel never disappears; it just shows less
or more content depending on state.
### Panel States
Panel states are mutually exclusive content modes:
#### IDLE (no preview, no route)
- Search bar at top
- Empty body (or subtle empty-state prompt: "Search or click a place
to begin")
- Recent activity / search history (when implemented)
#### PREVIEW (place selected via map click or search, not committed to route)
- Search bar at top
- Preview card: full place detail (name, type, coords, elevation,
land class, about, contact, links)
- Action buttons inside or below the card: [Directions] [Add stop]
[Save] [Share] [x]
- Click another place: preview replaced (previous lost — search
history retains it)
- Click x or Escape: preview dismissed, return to IDLE
#### ROUTING (1+ stops, no preview)
- Search bar at top
- Section: "Route" with stop cards
- Each stop card: collapsed by default (header: pin + name +
drag handle + remove); expandable to show full detail
- Drag-to-reorder via card headers (preserves existing StopList)
- Per-card actions: Save, Share, Remove
- Bottom: [Get Directions] (when 2+ stops with valid mode), mode
selector (auto/walk/bike), trip summary placeholder
#### PREVIEW + ROUTING (place clicked while route exists)
- Search bar at top
- Preview card directly below search
- Route section below preview
- Both visible simultaneously
- Multiple cards can be expanded at once (preview + a stop)
#### ROUTE_CALCULATED (after route fetch)
- Search bar at top
- Route summary (total time, distance)
- Mode selector
- Stop cards (collapsed; expand for detail)
- Turn-by-turn maneuvers
- Preview can appear on top if user clicks a place
### Search Bar Behavior
- Pinned to top of panel
- Always for browsing (search -> preview, never auto-add as stop)
#### Search History (aspirational — design now, implement later)
- Empty search -> dropdown shows 5 most recent searches
- Authed: persisted to backend
- Guest: localStorage
- Typing: recent matches sorted to top of search results
### Card Pattern
Shared between preview and stop cards. Both use the same component,
parameterized by role.
#### Header (always visible)
- Pin/marker icon (color varies by role — preview vs stop number)
- Place name
- Expand/collapse chevron
- For stops: drag handle, remove (x)
#### Body (collapsed by default for stops, expanded by default for preview)
- Type / category
- Coordinates + elevation + land class
- About (description, Wikipedia excerpt)
- Contact (phone, website, hours)
- Links (Wikipedia, OSM, Wikidata)
- Action buttons:
- Preview: [Directions] [Add stop] [Save] [Share]
- Stop: [Save] [Share]
### State Transitions
```
IDLE -> PREVIEW : click place / search-select
PREVIEW -> IDLE : x button / Escape
PREVIEW -> PREVIEW (different) : click another place
PREVIEW -> ROUTING : "Add stop" on preview
PREVIEW -> ROUTE_CALCULATED : "Directions" (becomes destination,
route auto-calculates if From available)
ROUTING -> IDLE : remove all stops
ROUTING -> PREVIEW + ROUTING : click place
ROUTING -> ROUTE_CALCULATED : "Get Directions" / auto-route trigger
PREVIEW + ROUTING -> ROUTING : dismiss preview
PREVIEW + ROUTING -> ROUTING : "Add stop" on preview (adds new stop)
ROUTE_CALCULATED -> ROUTING : edit route
ROUTE_CALCULATED -> IDLE : clear route
```
### Routes / Contacts Tabs
Tentative: Routes is default panel. Contacts is a separate view
accessible via a tab or icon at the top. Contacts view replaces
route/preview content with a contact list — does not combine with
route-building.
### Map Zoom-to-Feature
With single panel, padding becomes simpler — just one panel width
(~360px) on the left. Top/right/bottom can be small (~50px each).
### Mobile Bottom Sheet State Mapping
Same logic as desktop, layout rotated:
| State | Sheet Position |
|---------------------|----------------|
| IDLE | peek (search bar visible) |
| PREVIEW | half (preview card) |
| ROUTING | half (stops list) |
| PREVIEW + ROUTING | full (both, scrollable) |
| ROUTE_CALCULATED | half/full (summary + maneuvers) |
---
## 16. Open Questions
### From Single-Panel Architecture
- Routes/Contacts tab location and behavior in single-panel model
- Preview-of-already-routed-place: probably auto-expands the existing
stop card, no duplicate preview
- Preview card position confirmed above route section (per mock)
---
## Appendix A: Current Code References
| File | Lines | Relevance |
|------|-------|-----------|
| `store.js` | 72-86 | `startDirections()` logic to replace |
| `store.js` | 16-34 | `stops[]` management to preserve |
| `SearchBar.jsx` | 140-170 | `pendingDestination` logic to remove |
| `PlaceDetail.jsx` | 574-579 | `handleDirections()` to rewrite |
| `App.jsx` | 31-66 | Route fetch effect to preserve |
| `api.js` | 29-56 | `requestRoute()` unchanged |
---
## Appendix B: Radial Menu SVG Structure
```svg
<svg viewBox="0 0 200 200">
<!-- Wedge paths -->
<g class="wedges">
<path d="M100,100 L100,20 A80,80 0 0,1 169,60 Z" class="wedge" data-action="drop-pin" />
<path d="M100,100 L169,60 A80,80 0 0,1 169,140 Z" class="wedge" data-action="to-here" />
<!-- ... 4 more wedges ... -->
</g>
<!-- Center disc -->
<circle cx="100" cy="100" r="40" class="center-disc" />
<text x="100" y="95" class="coords">43.6166</text>
<text x="100" y="110" class="coords">-116.2008</text>
<text x="100" y="125" class="label">Loading...</text>
<!-- Icons (positioned in wedge centers) -->
<g class="icons">
<use href="#pin-icon" x="100" y="40" />
<!-- ... more icons ... -->
</g>
</svg>
```
---
*Document created 2026-04-26. Updated with resolved decisions and single-panel architecture (Phases k-o). Implementation to follow in dedicated session.*