Improve mobile map UI and add controls onboarding tour#3680
Conversation
Align map controls with dashboard card styling, fix asymmetric edge spacing, and introduce a first-time spotlight tour for map layer, location, and zoom actions. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Warning Review limit reached
More reviews will be available in 36 minutes and 50 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughDashboard cards, map controls, and map air-quality/search surfaces were restyled. MapScreen now tracks a persisted controls tour, anchors it to keyed controls, and sequences the forecast modal with explicit async checks. ChangesMobile map and dashboard updates
Sequence Diagram(s)sequenceDiagram
participant User
participant MapScreen
participant ForecastOverviewPage
User->>MapScreen: tap forecast view
MapScreen->>MapScreen: hide text input overlay
MapScreen->>MapScreen: await frame delay and mounted checks
MapScreen->>ForecastOverviewPage: showForMeasurement(...)
ForecastOverviewPage-->>MapScreen: close
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsErrors were encountered while retrieving linked issues. Errors (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/mobile/lib/src/app/map/widgets/map_controls.dart (1)
76-93: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winThe 56px tap wrapper isn't actually tappable — effective hit area is 44px.
The
GestureDetectorwraps the inner 44pxContainer(defaultdeferToChildhit testing), so the surrounding 12px inside the_mapControlTapSize(56px)SizedBoxis inert. The net tap target is 44px, which is below the 48px Material minimum and doesn't match the intent encoded in the sizing constants and the inset comment on Line 15.Move the gesture to the outer wrapper with an opaque hit test so the full 56px responds:
♿ Proposed fix to honor the 56px tap target
- return SizedBox( - width: _mapControlTapSize, - height: _mapControlTapSize, - child: Center( - child: GestureDetector( - onTap: onTap, - child: Container( + return SizedBox( + width: _mapControlTapSize, + height: _mapControlTapSize, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: Center( + child: Container( width: _mapControlVisualSize, height: _mapControlVisualSize, decoration: _mapControlDecoration( background: bg, borderRadius: BorderRadius.circular(10), ), child: Icon(icon, size: _mapControlIconSize, color: iconColor), ), ), ), );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/mobile/lib/src/app/map/widgets/map_controls.dart` around lines 76 - 93, The tap target in the map control builder is too small because GestureDetector is attached to the inner 44px Container instead of the outer 56px SizedBox. Update the tap handling in the map control widget so the full _mapControlTapSize area responds, using the outer wrapper with an opaque hit test behavior rather than the inner visual container. Keep the visual Container and icon sizing unchanged, and ensure the hit area matches the sizing constants and inset intent in the map controls helper.
🧹 Nitpick comments (1)
src/mobile/lib/src/app/dashboard/pages/location_selection/components/swipeable_analytics_card.dart (1)
212-223: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the chevron widget into a shared dashboard component.
Line 212 introduces the same chevron rendering logic that now also appears in
analytics_card.dart(Line 58) andnearby_measurement_card.dart(Line 61). Centralizing this avoids style drift (size/tint/asset path) across card headers.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/mobile/lib/src/app/dashboard/pages/location_selection/components/swipeable_analytics_card.dart` around lines 212 - 223, The chevron rendering logic is duplicated in _chevron within swipeable_analytics_card.dart and the same pattern also exists in analytics_card and nearby_measurement_card, so extract it into a shared dashboard component. Create a single reusable chevron widget in the dashboard shared UI layer, then replace the local implementations in the affected card header widgets with that shared component so the asset path, size, and tint stay consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/mobile/lib/src/app/map/widgets/map_air_quality_card.dart`:
- Around line 58-59: The aqLabel fallback in map_air_quality_card.dart is too
weak because MapAirQualityCard can render an empty AQI chip when pmValue exists
but measurement.aqiCategory is null or empty. Update the aqLabel logic in the
widget that builds the AQI badge so it uses a non-empty fallback string whenever
a reading exists but the category is missing, and keep the existing “No data”
behavior only for null pmValue. Use the MapAirQualityCard build logic and the
aqLabel assignment as the place to apply the fix.
---
Outside diff comments:
In `@src/mobile/lib/src/app/map/widgets/map_controls.dart`:
- Around line 76-93: The tap target in the map control builder is too small
because GestureDetector is attached to the inner 44px Container instead of the
outer 56px SizedBox. Update the tap handling in the map control widget so the
full _mapControlTapSize area responds, using the outer wrapper with an opaque
hit test behavior rather than the inner visual container. Keep the visual
Container and icon sizing unchanged, and ensure the hit area matches the sizing
constants and inset intent in the map controls helper.
---
Nitpick comments:
In
`@src/mobile/lib/src/app/dashboard/pages/location_selection/components/swipeable_analytics_card.dart`:
- Around line 212-223: The chevron rendering logic is duplicated in _chevron
within swipeable_analytics_card.dart and the same pattern also exists in
analytics_card and nearby_measurement_card, so extract it into a shared
dashboard component. Create a single reusable chevron widget in the dashboard
shared UI layer, then replace the local implementations in the affected card
header widgets with that shared component so the asset path, size, and tint stay
consistent.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 35d9b199-1e26-4256-8904-aed51d636876
📒 Files selected for processing (10)
src/mobile/lib/src/app/dashboard/pages/location_selection/components/swipeable_analytics_card.dartsrc/mobile/lib/src/app/dashboard/widgets/analytics_card.dartsrc/mobile/lib/src/app/dashboard/widgets/nearby_measurement_card.dartsrc/mobile/lib/src/app/dashboard/widgets/nearby_view.dartsrc/mobile/lib/src/app/map/pages/map_page.dartsrc/mobile/lib/src/app/map/widgets/map_air_quality_card.dartsrc/mobile/lib/src/app/map/widgets/map_controls.dartsrc/mobile/lib/src/app/map/widgets/map_controls_tour.dartsrc/mobile/lib/src/app/map/widgets/map_overlay_controls.dartsrc/mobile/lib/src/app/map/widgets/map_search_sheet.dart
💤 Files with no reviewable changes (1)
- src/mobile/lib/src/app/dashboard/widgets/nearby_view.dart
There was a problem hiding this comment.
Pull request overview
Updates the mobile Map UI to improve dark-mode consistency and control ergonomics, and introduces a first-time onboarding tour that spotlights map controls.
Changes:
- Refines map card, search sheet, and dashboard card styling (text/icon colors, chevrons, muted location text).
- Reworks map overlay controls into unified “pill” controls with aligned insets and larger intended tap areas.
- Adds a guided spotlight tour for map controls, persisted via
SharedPreferences, with an optional “Your location” step.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/mobile/lib/src/app/map/widgets/map_search_sheet.dart | Uses AppTextColors.headline for better theme consistency in search results. |
| src/mobile/lib/src/app/map/widgets/map_overlay_controls.dart | Adds keys for controls and aligns right-side inset using shared constants. |
| src/mobile/lib/src/app/map/widgets/map_controls.dart | Introduces new control sizing constants and refreshed control visuals. |
| src/mobile/lib/src/app/map/widgets/map_controls_tour.dart | New guided spotlight tour overlay implementation for map controls. |
| src/mobile/lib/src/app/map/widgets/map_air_quality_card.dart | Refreshes map AQ card layout and forecast CTA styling. |
| src/mobile/lib/src/app/map/pages/map_page.dart | Wires up the map controls tour and aligns legend with controls inset. |
| src/mobile/lib/src/app/dashboard/widgets/nearby_view.dart | Removes unused/commented distance argument. |
| src/mobile/lib/src/app/dashboard/widgets/nearby_measurement_card.dart | Updates muted location styling and adds chevron affordance. |
| src/mobile/lib/src/app/dashboard/widgets/analytics_card.dart | Updates muted location styling and adds chevron affordance. |
| src/mobile/lib/src/app/dashboard/pages/location_selection/components/swipeable_analytics_card.dart | Updates muted location styling and adds chevron affordance. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (pmValue != null) | ||
| SvgPicture.asset( | ||
| getAirQualityIcon(measurement, pmValue), | ||
| width: 36, | ||
| height: 36, | ||
| ) | ||
| else |
| return SizedBox( | ||
| width: 48, | ||
| height: 48, | ||
| width: _mapControlTapSize, | ||
| height: _mapControlTapSize, | ||
| child: Center( | ||
| child: GestureDetector( |
| return SizedBox( | ||
| width: _mapControlTapSize, | ||
| height: _mapZoomTapHeight, | ||
| child: Center( | ||
| child: GestureDetector( | ||
| behavior: HitTestBehavior.opaque, | ||
| onTapUp: (details) { |
| final screen = MediaQuery.of(context).size; | ||
| final bubbleBg = isDark ? AppColors.darkHighlight : Colors.white; | ||
| final textColor = isDark ? Colors.white : const Color(0xFF1A1D23); | ||
| final subColor = | ||
| isDark ? AppColors.boldHeadlineColor2 : AppColors.boldHeadlineColor3; | ||
|
|
||
| final bubbleWidth = | ||
| (screen.width - _hPad * 2 - _arrowW - 12).clamp(220.0, _maxBubbleWidth); | ||
| final left = (targetRect.left - bubbleWidth - _arrowW - 12) | ||
| .clamp(_hPad, screen.width - bubbleWidth - _hPad); | ||
|
|
||
| final bubbleTop = (targetRect.center.dy - 72) | ||
| .clamp(_hPad + 8, screen.height * 0.55); | ||
|
|
| this.layersKey, | ||
| this.locateKey, | ||
| this.zoomKey, | ||
| }); | ||
|
|
||
| final bool isDark; | ||
| final VoidCallback onLayersTap; | ||
| final VoidCallback? onLocateTap; | ||
| final VoidCallback onZoomIn; | ||
| final VoidCallback onZoomOut; | ||
| final Key? layersKey; | ||
| final Key? locateKey; | ||
| final Key? zoomKey; |
Use Unknown/unavailable fallbacks when category or icon is missing, expand map control hit areas to the full tap wrapper, respect safe-area padding in the controls tour, and type tour keys as GlobalKey. Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Test plan
map_controls_tour_seenin SharedPreferences and confirm the 2–3 step tour appearsSummary by CodeRabbit
New Features
Bug Fixes
Style