Skip to content

feat(search): destination-first search + mobile bottom sheet#68

Merged
GeiserX merged 4 commits into
mainfrom
feat/dest-first-search-mobile-sheet
Jun 21, 2026
Merged

feat(search): destination-first search + mobile bottom sheet#68
GeiserX merged 4 commits into
mainfrom
feat/dest-first-search-mobile-sheet

Conversation

@GeiserX

@GeiserX GeiserX commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Two UX improvements to the route planner, both requested from mobile use.

1. Destination-first single box

The single search box on open is now the destination ("Where to?" / "¿A dónde vas?"), matching Google/Apple Maps. Picking a destination:

  • Location shared → routes straight from your current location (origin auto-seeded as "My location"), no extra typing.
  • Location unavailable → reveals the origin box and focuses it so you can type a start.

Implementation:

  • Inverted the search phase machine: search/destination/routedest/planning/route. dest shows the single destination box; planning slides the origin in above it; route unchanged.
  • Origin auto-seeds from userLocation (one-shot; yields to manual edits and deep-link routes).
  • Threaded userLocation from HomeClient into SearchPanel.
  • New i18n keys search.origin + search.whereTo across all 16 locales (correct accents).

2. Mobile bottom sheet

On phones (≤640px) the route alternatives + station list now ride in a draggable bottom sheet over a full-height map — fixing the reported bug where the controls (sort chips, basis toggle, two sliders) pushed the first station off-screen.

  • New BottomSheet: drag-to-snap between peek / half / full; tap the handle to cycle. Pointer events (touch + mouse). Summary strip shows distance · duration · station count. The station list scrolls inside the sheet.
  • New useMediaQuery hook (useSyncExternalStore, SSR-safe, defaults to desktop).
  • SearchPanel: route/station JSX extracted into a shared routeContent; desktop keeps the side-card layout, mobile renders it in the sheet. Collapse toggle is desktop-only (the sheet handle replaces it). Selecting a station drops the sheet to peek so the map shows.

Desktop layout is unchangedisMobile is false under SSR and jsdom, so the desktop path renders byte-for-byte as before.

Verification

  • tsc --noEmit — pass
  • npm run lint — 0 errors (73 pre-existing warnings)
  • npm test538/538 (new: destination-first single box, route-from-location, origin-reveal fallback, sheet-renders-on-mobile, no-sheet-on-desktop)
  • npm run build — compiled, 31/31 static pages
  • Dev-server SSR smoke: /en + /es return 200, no React errors, correct placeholders (Where to? / ¿A dónde vas?), accents intact

Summary by CodeRabbit

  • New Features

    • Implemented destination-first search flow where users select their destination first, then origin.
    • Auto-seeded origin field with "My location" when user location is available.
    • Added mobile bottom sheet interface for route content with snap-to-height interactions.
  • Tests

    • Added comprehensive test coverage for destination-first flow, mobile/desktop layouts, and routing behavior.
  • Chores

    • Added missing localization strings for search labels across supported languages.

GeiserX added 2 commits June 21, 2026 23:01
The single search box on open is now the DESTINATION ('Where to?'), matching
Google/Apple Maps. Picking a destination:
  - routes straight from the user's shared location (origin auto-seeded), or
  - reveals the origin box to fill in when location is unavailable.

- Invert the phase machine: search/destination/route -> dest/planning/route.
  'dest' shows the single destination box; 'planning' slides the origin in
  above it; 'route' unchanged.
- Auto-seed origin from userLocation (one-shot, yields to manual edits and
  deep-link routes).
- Thread userLocation from HomeClient into SearchPanel.
- New i18n keys search.origin + search.whereTo across all 16 locales.
- Update + extend tests (destination-first single box, route-from-location,
  origin-reveal fallback). 536 tests pass.

Verified: SSR renders the 'Where to?' box (es: '¿A dónde vas?') with the origin
box collapsed; tsc, lint, build all clean.
On phones (<=640px) the route alternatives and station list now ride in a
draggable bottom sheet over a full-height map, instead of stacking in a tall
column that pushed the first station off-screen (reported issue).

- New BottomSheet: drag-to-snap between peek/half/full; tap the handle to cycle.
  Pointer events (touch + mouse), summary strip shows distance/duration + count.
- New useMediaQuery hook (useSyncExternalStore, SSR-safe, desktop baseline).
- SearchPanel: extract route/station JSX into shared routeContent; desktop keeps
  the side-card layout, mobile renders routeContent inside the sheet. Collapse
  toggle is desktop-only (the sheet handle replaces it on mobile). Selecting a
  station lowers the sheet to peek so the map shows.
- Tests: sheet renders (role=dialog) on mobile viewport, never on desktop. 538 pass.

Desktop layout is unchanged (isMobile=false in jsdom/SSR). Verified: SSR 200, no
errors; tsc, lint, build all clean.
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@GeiserX, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 26 minutes and 13 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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 refill rate.

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, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 00193af4-a95a-46b7-9649-0608d1c21994

📥 Commits

Reviewing files that changed from the base of the PR and between 1809b4e and 7b3bb19.

📒 Files selected for processing (5)
  • src/components/search/bottom-sheet.test.tsx
  • src/components/search/bottom-sheet.tsx
  • src/components/search/search-panel.test.tsx
  • src/components/search/search-panel.tsx
  • src/lib/i18n.tsx
📝 Walkthrough

Walkthrough

SearchPanel is refactored from a search/destination/route phase model to a dest/planning/route destination-first model. A new userLocation prop auto-seeds origin. A new BottomSheet component handles mobile snap interactions. A new SSR-safe useMediaQuery hook drives desktop/mobile branching. Translations for search.origin and search.whereTo are added across 16 locales.

Changes

Destination-first search flow with mobile bottom sheet

Layer / File(s) Summary
useMediaQuery SSR-safe hook
src/lib/use-media-query.ts
New hook using useSyncExternalStore returning matchMedia boolean with a false server baseline and legacy browser fallback.
BottomSheet component and SheetSnap type
src/components/search/bottom-sheet.tsx
New mobile-only bottom sheet with peek/half/full snap positions, viewport-relative translate math, pointer drag/tap handling, and animated transitions.
i18n translations for search.origin and search.whereTo
src/lib/i18n.tsx
search.origin and search.whereTo entries added for 16 locales.
SearchPanel phase machine and prop contracts
src/components/search/search-panel.tsx
Phase type updated to dest/planning/route; SearchPanelProps gains optional userLocation; component initializes isMobile and bottom-sheet snap state.
SearchPanel destination-first flow logic
src/components/search/search-panel.tsx
originSeededRef introduced; handleLocationSelect rewritten; deep-link init and auto-seed effect updated; origin/destination editing, enter handlers, and blur behavior reworked for destination-first semantics.
SearchPanel UI: routeContent, desktop layout, mobile BottomSheet
src/components/search/search-panel.tsx, src/components/home-client.tsx
Shared routeContent block extracted; destination made always-visible primary field; origin slides in above it; BottomSheet renders route on mobile; userLocation wired from HomeClient.
SearchPanel tests
src/components/search/search-panel.test.tsx
renderPanel accepts props; new destination-first flow and mobile bottom sheet describe blocks added.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • GeiserX/Pumperly#17: Modifies SearchPanel/HomeClient routing wiring with onRoute and station-leg preview support — direct overlap with the same component and prop interface modified here.
  • GeiserX/Pumperly#23: Adds onSelectStation and selection/toggle behavior to SearchPanel with wiring from home-client.tsx — same component/props surface touched by this PR.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main features: destination-first search and mobile bottom sheet implementation, matching the core changes across SearchPanel, BottomSheet, and related files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dest-first-search-mobile-sheet

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/search/search-panel.tsx (1)

155-196: ⚠️ Potential issue | 🟠 Major

Remove calculateRoute from the eslint-disable and add it to the dependency list.

The dependency array at line 185 omits calculateRoute with a comment claiming it's stable, but this creates a stale closure risk. The callback captures an outdated calculateRoute reference if onRoute (its only dependency) changes. Including calculateRoute in the deps ensures the callback reflects the latest routing logic.

- }, [t, onFlyTo, destination, waypoints]);
+ }, [t, onFlyTo, destination, waypoints, calculateRoute]);

Remove the eslint-disable comment on line 184 — the dependency omission is not justified.

🤖 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/components/search/search-panel.tsx` around lines 155 - 196, The
handleLocationSelect useCallback has an eslint-disable comment on line 184 that
justifies omitting calculateRoute from the dependency array, but this creates a
stale closure risk. Remove the eslint-disable comment entirely and add
calculateRoute to the dependency array of handleLocationSelect so the callback
always references the current version of calculateRoute, which depends on
onRoute changes. This ensures the routing logic is never stale when
handleLocationSelect calls calculateRoute.

Source: Linters/SAST tools

🤖 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/components/search/bottom-sheet.tsx`:
- Around line 127-129: The aria-label attribute in the bottom-sheet.tsx
component contains a hardcoded user-facing string "Route and stations" instead
of using localization. To fix this, import and use the useI18n hook to get the t
translator function, then replace the hardcoded string in the aria-label with a
call to t() passing an appropriate translation key for the route and stations
label. This ensures the accessibility label is properly localized for all
supported languages.

In `@src/components/search/search-panel.test.tsx`:
- Around line 96-110: The test assertion in "reveals the origin box (no route
yet) when location is unavailable" uses toBeInTheDocument() to verify the origin
input is revealed, but this only checks if the element exists in the DOM, not if
it's actually visible. The origin input may stay mounted but hidden via CSS,
allowing the test to false-pass. Replace the toBeInTheDocument() matcher with
toBeVisible() when asserting that screen.getByPlaceholderText("search.origin")
is present after destination selection to properly verify the UI reveal behavior
and prevent regressions in the destination-first fallback experience.

In `@src/components/search/search-panel.tsx`:
- Around line 178-181: The cleanup callback around lines 178-181 that clears
originText and origin on geolocation failure is leaving the active route state
inconsistent with the UI. Instead of only clearing setOriginText and
setOrigin(null), also clear or reset the active route state in the same callback
to maintain consistency. Either preserve the existing origin while clearing the
route state, or clear both the origin and route to reset the component to its
planning state.

---

Outside diff comments:
In `@src/components/search/search-panel.tsx`:
- Around line 155-196: The handleLocationSelect useCallback has an
eslint-disable comment on line 184 that justifies omitting calculateRoute from
the dependency array, but this creates a stale closure risk. Remove the
eslint-disable comment entirely and add calculateRoute to the dependency array
of handleLocationSelect so the callback always references the current version of
calculateRoute, which depends on onRoute changes. This ensures the routing logic
is never stale when handleLocationSelect calls calculateRoute.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4091c097-2755-4190-a25c-483cba2be861

📥 Commits

Reviewing files that changed from the base of the PR and between ae94aeb and 1809b4e.

📒 Files selected for processing (6)
  • src/components/home-client.tsx
  • src/components/search/bottom-sheet.tsx
  • src/components/search/search-panel.test.tsx
  • src/components/search/search-panel.tsx
  • src/lib/i18n.tsx
  • src/lib/use-media-query.ts

Comment thread src/components/search/bottom-sheet.tsx Outdated
Comment thread src/components/search/search-panel.test.tsx
Comment thread src/components/search/search-panel.tsx
GeiserX added 2 commits June 22, 2026 00:11
Multi-reviewer panel (+CodeRabbit) findings:

- CRITICAL: remove `touch-none` from the bottom-sheet container — it blocked
  touch-scrolling the station list (the bug this PR set out to fix). touch-none
  now lives only on the drag handle; body gets touch-pan-y.
- HIGH: 'My location' geolocation failure now shows a transient geo.denied toast
  and focuses the origin box, instead of silently blanking origin / orphaning a
  route. Never wipes a manual origin or active route.
- HIGH: bottom-sheet drag handle is now a real <button> — keyboard operable
  (↑/↓/Enter/Space), aria-expanded, focusable. role=dialog → role=region
  (map stays interactive; no false modal semantics). aria-label localized via
  new sheet.routeAndStations / sheet.resize keys across 16 locales.
- MED: collapsed leak — routeCollapsed = collapsed && !isMobile, so collapsing
  on desktop then resizing to mobile no longer yields an empty sheet.
- MED: drag uses a ref for live translate + snapshotted clamp basis (fixes
  stale-flick snap + mid-drag resize); vh===0 guard + lazy init (no dead drag /
  first-paint flash); pointercancel restores instead of cycling.
- Simplify: inline peekHeight constant (was an unused prop), drop dead sheetRef.
- Tests: new bottom-sheet.test.tsx (snap math, tap-cycle, drag, keyboard,
  pointercancel, aria); fixed false-pass origin-reveal assertion; added
  geo-failure + auto-seed-guard tests. 548 pass.

tsc/lint/build clean; SSR smoke 200.
@GeiserX GeiserX merged commit 35c041f into main Jun 21, 2026
8 checks passed
@GeiserX GeiserX deleted the feat/dest-first-search-mobile-sheet branch June 21, 2026 22:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant