mirror of
https://github.com/zvx-echo6/refactored-recon.git
synced 2026-05-20 06:34:34 +02:00
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.
842 lines
27 KiB
Markdown
842 lines
27 KiB
Markdown
# 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.*
|