summaryrefslogtreecommitdiff
path: root/docs/superpowers/specs/2026-04-03-map-interaction-design.md
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-04-02 21:08:04 -1000
committerGitHub <noreply@github.com>2026-04-02 21:08:04 -1000
commitbf2223827c53fbc0e77d1af2a7d4654a7c248ee0 (patch)
treeff3a4f5c4e9c0ec9bbefddee605821b1c80c92fd /docs/superpowers/specs/2026-04-03-map-interaction-design.md
parente9df7afa1d96fde80c482e497d7c17617c2d95c3 (diff)
feat(map): interactive map with auto-follow, recenter button, and UI immersive mode (#2)
* docs: add map interaction design spec Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add map interaction implementation plan Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(map): add isFollowing state and gesture-driven manual mode to MapHandler * feat(ui): add fab_recenter pill button to map layout * feat(map): wire UI fade-out and recenter button to MapHandler.isFollowing * fix(map): prevent fadeIn flash on cold start; consolidate fab_mob listener * fix(map): preserve user zoom level on recenter Capture the current camera zoom when the user gestures (entering manual mode) and pass it back to centerOnLocation in recenter(), so tapping Recenter returns to the user's chosen zoom rather than always snapping to the default 14. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(map): capture lastZoom on camera idle, not gesture start OnCameraMoveStartedListener fires before the gesture completes, so it captured the pre-gesture zoom. OnCameraIdleListener fires after the camera settles, giving the user's final intended zoom level. Only update lastZoom while in manual mode (isFollowing=false). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(map): guard recenter against null island and add KDoc - Skip recenter() if no GPS fix received (lastLat/lastLon still 0.0) to avoid animating to 0°N, 0°E - Add KDoc comment to recenter() consistent with other public methods Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'docs/superpowers/specs/2026-04-03-map-interaction-design.md')
-rw-r--r--docs/superpowers/specs/2026-04-03-map-interaction-design.md59
1 files changed, 59 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-04-03-map-interaction-design.md b/docs/superpowers/specs/2026-04-03-map-interaction-design.md
new file mode 100644
index 0000000..02e38e1
--- /dev/null
+++ b/docs/superpowers/specs/2026-04-03-map-interaction-design.md
@@ -0,0 +1,59 @@
+# Map Interaction Design
+_2026-04-03_
+
+## Overview
+
+Enable free map pan/zoom while keeping GPS auto-follow as the default. When the user gestures on the map, the UI hides and a Recenter button appears. Tapping Recenter restores auto-follow and the full UI.
+
+## State
+
+A single `isFollowing: Boolean` flag owned by `MapHandler`. Starts `true`. This is the only piece of new state.
+
+## Entering Manual Mode
+
+`MapLibreMap.addOnCameraMoveStartedListener` fires with `REASON_API_GESTURE` when the user initiates a pan, pinch-zoom, or rotate. On that event:
+
+- `MapHandler.isFollowing` → `false`
+- `MapHandler` emits via a new `StateFlow<Boolean> isFollowing` exposed to MainActivity
+- `MapHandler.centerOnLocation()` becomes a no-op while `!isFollowing` — GPS updates still arrive, last position is stored
+
+## Returning to Auto-Follow
+
+`fab_recenter` click handler:
+
+- Calls `MapHandler.recenter()` — sets `isFollowing = true`, calls `centerOnLocation(lastLat, lastLon)`
+- `isFollowing` StateFlow emits `true` → MainActivity restores UI
+
+## UI Changes
+
+### New element: `fab_recenter`
+
+- Pill-shaped button (`wrap_content` width, 40dp height, 20dp corner radius)
+- Label: "⊙ Recenter"
+- Position: centered horizontally, `24dp` above the bottom of the map `ConstraintLayout`
+- `android:visibility="gone"` by default
+- Elevation: 20dp (above the instrument sheet)
+
+### Visibility toggling (MainActivity)
+
+When `isFollowing` → `false`:
+- Fade out (alpha 0, 150ms): `instrument_bottom_sheet`, `bottom_navigation`, `fab_mob`, `fab_record_track`
+- Show `fab_recenter` (visibility VISIBLE, fade in 150ms)
+
+When `isFollowing` → `true`:
+- Hide `fab_recenter` (fade out 150ms, then GONE)
+- Fade in (alpha 1, 150ms): `instrument_bottom_sheet`, `bottom_navigation`, `fab_mob`, `fab_record_track`
+
+## Files to Change
+
+| File | Change |
+|------|--------|
+| `MapHandler.kt` | Add `isFollowing` StateFlow, `lastLat`/`lastLon` storage, `recenter()`, guard in `centerOnLocation()`, register `OnCameraMoveStartedListener` |
+| `activity_main.xml` | Add `fab_recenter` pill button |
+| `MainActivity.kt` | Observe `mapHandler.isFollowing`, animate UI in/out, wire `fab_recenter` click |
+
+## Out of Scope
+
+- Tapping the map (without gesture) to restore UI — not requested
+- Timeout to auto-restore UI — not requested
+- Zoom-level persistence across recenter — not requested