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 | Drop pin | Pin | Red |
| Top-right | Directions to here | Arrow-in | Blue | | 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 | What's here | Info | Orange |
| Bottom-left | Add as stop | Plus | Yellow | | Bottom-left | Add as stop | Plus | Yellow |
| Top-left | Directions from here | Arrow-out | Green | | Top-left | Directions from here | Arrow-out | Green |
### Behavior ### 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 - **Center disc:** ~40px diameter, shows coordinates immediately, reverse-geocoded label async
- **Wedge highlight:** On hover (desktop) or drag-over (mobile) - **Wedge highlight:** On hover (desktop) or drag-over (mobile)
- **Commit:** Release on wedge (mobile) or click wedge (desktop) - **Commit:** Release on wedge (mobile) or click wedge (desktop)
@ -260,6 +260,11 @@ radialMenuState: {
lon: number, lon: number,
label: string | null // Reverse-geocoded, async populated 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 ### 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 | | **Directions to here** | Opens directions if closed, fills To with coords, focuses From if empty |
| **Add as stop** | Inserts new stop before destination | | **Add as stop** | Inserts new stop before destination |
| **What's here** | Reverse geocode → opens place panel | | **What's here** | Reverse geocode → opens place panel |
| **Drop pin** | Creates transient marker (session-only) | | **Drop pin** | Creates transient marker (session-only by default) |
| **Save place** | Opens save dialog (auth required) | | **Save place** | Guest: opens login flow; Authed: opens save dialog |
5. Release outside or Escape → dismisses without action 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`): 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` 4. Input loses focus, `activeInputSlot = null`
5. Route recalculates 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 ### Swap button
1. Click swap button 1. Click swap button
@ -374,22 +392,24 @@ handleDirections = () => {
| Platform | Gesture | Duration | | Platform | Gesture | Duration |
|----------|---------|----------| |----------|---------|----------|
| Desktop | Right-click | Instant | | Desktop | Right-click | Instant |
| Mobile | Long-press | 400-500ms | | Mobile | Long-press | 450ms |
### Conflict Avoidance ### Conflict Avoidance
Long-press must NOT fire during active pan: Long-press must NOT fire during active pan:
- Track touch start position - 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 - Pan gesture takes priority
- Matches iOS Safari default contextmenu synthesis behavior
### Geometry ### Geometry
``` ```
Outer radius: ~80px from center Outer radius: ~80px from center (desktop), ~100px (mobile)
Inner radius: ~40px (center disc) Inner radius: ~40px (center disc, desktop), ~50px (mobile)
Wedge angle: 60° each (6 wedges) Wedge angle: 60° each (6 wedges)
Gap between wedges: 2px Gap between wedges: 2px
Minimum touch target: 48px per wedge
``` ```
### Visual States ### Visual States
@ -400,6 +420,7 @@ Gap between wedges: 2px
| Wedge icon | White, 50% opacity | White, 100% opacity | White | | Wedge icon | White, 50% opacity | White, 100% opacity | White |
| Wedge label | Hidden | Shown (tooltip) | Shown | | Wedge label | Hidden | Shown (tooltip) | Shown |
| Center disc | Dark, coords visible | — | — | | Center disc | Dark, coords visible | — | — |
| Save place (guest) | Lock icon overlay | — | — |
### Animation ### Animation
@ -414,30 +435,36 @@ Gap between wedges: 2px
### Panel Layout ### Panel Layout
**Decision needed:** Bottom sheet vs side panel **Decision: Bottom sheet on mobile (<768px), side panel on desktop.**
| Option | Pros | Cons | Bottom sheet states:
|--------|------|------| - **Peek:** Route summary only (collapsed)
| Bottom sheet | Familiar (Google Maps), thumb-friendly | Complex sheet state management | - **Half:** Inputs + summary visible
| Side panel | Consistent with desktop, more vertical space | Covers more map, less thumb-friendly | - **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 ### Long-press Timing
**Decision needed:** Exact timing **Decision: 450ms with 5-10px movement threshold.**
| Duration | Feel | 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.
|----------|------|
| 400ms | Snappy, risk of accidental trigger |
| 450ms | Balanced |
| 500ms | Deliberate, slightly sluggish |
**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 ### Radial Sizing
Mobile radial should be larger for finger touch: Mobile radial larger for finger touch:
- Outer radius: ~100px (vs 80px desktop) - Outer radius: ~100px (vs 80px desktop)
- Center disc: ~50px (vs 40px desktop) - Center disc: ~50px (vs 40px desktop)
- Minimum wedge touch target: 48px - 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?** **Decision:** Bottom sheet on mobile (<768px breakpoint), side panel on desktop.
- Bottom sheet recommended but adds complexity
2. **Long-press timing exactly?** Three sheet states:
- 400ms / 450ms / 500ms - **Peek:** Summary only (route time/distance)
- Recommend 450ms - **Half:** Inputs + summary visible
- **Full:** Inputs + turn-by-turn maneuvers
3. **Should "Save place" wedge be visible to guests or hidden?** Drag-to-resize between states via handle at top of sheet.
- Visible with login prompt = more discoverable
- Hidden = cleaner for guests
- Recommend: visible, shows "Sign in to save" toast
4. **Inner ring of secondary actions in radial v2?** **Implementation notes:**
- Could add less-common actions in inner ring - `sheetState` in store: `'peek' | 'half' | 'full'`
- Recommend: stay single-ring for v1, evaluate need later - 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?** ### 2. Long-press Timing
- Session only (lost on refresh)
- localStorage (persists locally)
- Auth-only saved (sync across devices)
- Recommend: session-only for v1, localStorage for v2
6. **Radial on map click during active input?** **Decision:** 450ms with 5-10px movement threshold.
- Option A: No radial, click fills input directly
- Option B: Radial appears, "Use this location" wedge fills input 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.
- Recommend: Option A (direct fill) for simplicity
**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.*