design: resolve open questions in directions redesign

Six open questions answered:
- Mobile uses bottom sheet (768px breakpoint), desktop uses side panel
- Long-press timing: 450ms + 5-10px movement threshold
- Save place wedge visible to guests with login prompt
- Single-ring radial; future expansion via contextual wedge sets
- Three-tier pin persistence: transient → localStorage → backend
- Map click fills active input; long-press still opens radial
This commit is contained in:
Matt 2026-04-26 05:14:22 +00:00
commit c121559e01

View file

@ -132,14 +132,14 @@ routeError: null
|----------|--------|------|-------|
| Top | Drop pin | Pin | Red |
| Top-right | Directions to here | Arrow-in | Blue |
| Bottom-right | Save place | Star | Purple |
| 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 400-500ms (mobile)
- **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)
@ -260,6 +260,11 @@ radialMenuState: {
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
@ -308,12 +313,12 @@ pendingDestination: null // No longer needed — explicit inputs replace hidden
| **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) |
| **Save place** | Opens save dialog (auth required) |
| **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
### Click map with active input
### Map click with active input
When directions panel is open and an input is focused (`activeInputSlot !== null`):
@ -323,6 +328,19 @@ When directions panel is open and an input is focused (`activeInputSlot !== null
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
@ -374,22 +392,24 @@ handleDirections = () => {
| Platform | Gesture | Duration |
|----------|---------|----------|
| Desktop | Right-click | Instant |
| Mobile | Long-press | 400-500ms |
| Mobile | Long-press | 450ms |
### Conflict Avoidance
Long-press must NOT fire during active pan:
- Track touch start position
- If touch moves >5px before timer fires, cancel long-press
- 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
Inner radius: ~40px (center disc)
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
@ -400,6 +420,7 @@ Gap between wedges: 2px
| 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
@ -414,30 +435,36 @@ Gap between wedges: 2px
### Panel Layout
**Decision needed:** Bottom sheet vs side panel
**Decision: Bottom sheet on mobile (<768px), side panel on desktop.**
| Option | Pros | Cons |
|--------|------|------|
| Bottom sheet | Familiar (Google Maps), thumb-friendly | Complex sheet state management |
| Side panel | Consistent with desktop, more vertical space | Covers more map, less thumb-friendly |
Bottom sheet states:
- **Peek:** Route summary only (collapsed)
- **Half:** Inputs + summary visible
- **Full:** Inputs + turn-by-turn maneuvers
**Recommendation:** Bottom sheet with three states: collapsed (summary only), half (inputs + summary), full (inputs + 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 needed:** Exact timing
**Decision: 450ms with 5-10px movement threshold.**
| Duration | Feel |
|----------|------|
| 400ms | Snappy, risk of accidental trigger |
| 450ms | Balanced |
| 500ms | Deliberate, slightly sluggish |
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.
**Recommendation:** Start with 450ms, tune based on testing.
**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 should be larger for finger touch:
Mobile radial larger for finger touch:
- Outer radius: ~100px (vs 80px desktop)
- Center disc: ~50px (vs 40px desktop)
- Minimum wedge touch target: 48px
@ -503,36 +530,97 @@ Separate session will address:
---
## 14. Open Questions
## 14. Resolved Decisions
### For Matt to decide:
### 1. Mobile Layout
1. **Bottom sheet vs side panel on mobile?**
- Bottom sheet recommended but adds complexity
**Decision:** Bottom sheet on mobile (<768px breakpoint), side panel on desktop.
2. **Long-press timing exactly?**
- 400ms / 450ms / 500ms
- Recommend 450ms
Three sheet states:
- **Peek:** Summary only (route time/distance)
- **Half:** Inputs + summary visible
- **Full:** Inputs + turn-by-turn maneuvers
3. **Should "Save place" wedge be visible to guests or hidden?**
- Visible with login prompt = more discoverable
- Hidden = cleaner for guests
- Recommend: visible, shows "Sign in to save" toast
Drag-to-resize between states via handle at top of sheet.
4. **Inner ring of secondary actions in radial v2?**
- Could add less-common actions in inner ring
- Recommend: stay single-ring for v1, evaluate need later
**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
5. **What does "Drop pin" persistence look like?**
- Session only (lost on refresh)
- localStorage (persists locally)
- Auth-only saved (sync across devices)
- Recommend: session-only for v1, localStorage for v2
### 2. Long-press Timing
6. **Radial on map click during active input?**
- Option A: No radial, click fills input directly
- Option B: Radial appears, "Use this location" wedge fills input
- Recommend: Option A (direct fill) for simplicity
**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
---
@ -576,4 +664,4 @@ Separate session will address:
---
*Document created 2026-04-26. Implementation to follow in dedicated session.*
*Document created 2026-04-26. Updated 2026-04-26 with resolved decisions. Implementation to follow in dedicated session.*