mirror of
https://github.com/zvx-echo6/refactored-recon.git
synced 2026-05-20 14:44:39 +02:00
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:
parent
18ee0fc738
commit
c121559e01
1 changed files with 135 additions and 47 deletions
|
|
@ -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.*
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue