From c121559e018d4524a1d5d829dd99f12fa5d9686f Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 26 Apr 2026 05:14:22 +0000 Subject: [PATCH] design: resolve open questions in directions redesign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- NAVI-DIRECTIONS-REDESIGN.md | 182 ++++++++++++++++++++++++++---------- 1 file changed, 135 insertions(+), 47 deletions(-) diff --git a/NAVI-DIRECTIONS-REDESIGN.md b/NAVI-DIRECTIONS-REDESIGN.md index 705201f..0274b93 100644 --- a/NAVI-DIRECTIONS-REDESIGN.md +++ b/NAVI-DIRECTIONS-REDESIGN.md @@ -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.*