feat(map): simplify search UX/UI#1061
Conversation
Parameterize peek height, dismissability, and analytics event names so a second bottom sheet (mobile search) can reuse the drawer snap mechanics. Defaults preserve the merchant drawer behavior exactly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mode tabs
The Worldwide/Nearby scope toggle now lives inside the list panel only.
At rest the input carries a teal '{n} nearby' pill; it yields the slot to
the clear button as soon as the user types.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
While a nearby filter is active the Nearby tab shows the filtered count, including (0). A zero-match filter renders an empty state with a 'Search worldwide' CTA instead of a blank list. Panel placeholder becomes the neutral 'Search places...' (single-input metaphor; scope lives in the toggle), retiring the per-mode placeholder keys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The merchant list panel becomes an always-present bottom sheet on mobile: peek (grabber + single input + count pill) <-> full panel, driven by a second drawer-gesture-controller instance (canDismiss false, search_sheet_* analytics). Tapping the peek facade opens the list without popping the keyboard; the floating search bar is now desktop-only. Bottom map controls (scale, OSM attribution) are lifted above the peek sheet on mobile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…fter review
Review findings from the multi-agent pass:
- determineSnapState now honors the controller's peek height, so the
search sheet snaps back to 110px instead of the drawer's 140px
(+ regression test)
- reset the sheet gesture when the merchant drawer takes the bottom
edge, so an unmount mid-drag can't strand the captured pointer
- the whole peek facade is a swipe surface, not just the grabber strip;
facade exposes aria-expanded and its visible label (incl. count)
- floating bar and sheet now derive from one init-locked layout
decision, so resizes can't yield zero or two search surfaces
- touchcancel handling on the list collapse-drag; grabber aria-controls
- trailing slot is click-transparent so the count pill no longer blocks
taps on the input
- panel input emits search_input_focus {source: panel}
- attribution lift reads SEARCH_SHEET_PEEK_HEIGHT via a CSS custom
property; sheet bottom padding uses the max() safe-area floor
- rewrite stale merchant-list-panel e2e specs for the single-input UX
(9 passing) and add a zero-match CTA spec
- translation polish: terminology consistency for the new keys in
fr/it/nl/ru/bg/pt-BR/es/de
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The expanded sheet header (search input row, scope toggle, category chips) now acts as a drag surface like the grabber, mirroring the merchant drawer's draggable header. An 8px vertical slop separates taps from drags so the input, toggle and chips stay tappable; once dragging, the pointer is captured so releasing over a chip doesn't click it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Render height as calc(spring + env(safe-area-inset-bottom)) capped at 100dvh, so the home-indicator inset is added only on devices that have one instead of being budgeted into the constant. Attribution lift mirrors the same calc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
✅ Deploy Preview for btcmap ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughThis PR refactors the map search interface to use a mobile-optimized peek/expanded sheet model with configurable gesture controls, while simplifying the desktop search experience into a unified neutral-placeholder input that displays nearby merchant counts. ChangesMobile Search Sheet with Peek/Expand Gestures
Sequence DiagramsequenceDiagram
participant User
participant MapPage as Map Page
participant MerchantPanel as MerchantListPanel
participant GestureCtrl as Gesture Controller
participant Analytics as Analytics
User->>MerchantPanel: Tap peek facade
MerchantPanel->>GestureCtrl: Expand sheet
GestureCtrl->>MerchantPanel: Update height to expanded
MerchantPanel->>User: Show input & scope toggle
User->>MerchantPanel: Type query + select Nearby
MerchantPanel->>MerchantPanel: Filter merchants by query
MerchantPanel->>User: Show filtered results (0 count)
User->>MerchantPanel: Click "Search worldwide" CTA
MerchantPanel->>Analytics: Track CTA click event
MerchantPanel->>MerchantPanel: Switch to worldwide mode
MerchantPanel->>User: Show unfiltered worldwide results
User->>MerchantPanel: Swipe down / press Escape
GestureCtrl->>MerchantPanel: Collapse to peek
MerchantPanel->>User: Hide input, show facade with count
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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/routes/map/components/MerchantListPanel.svelte (1)
10-15: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winSeparate type imports from value imports.
The import statement mixes type and value imports using inline
typekeywords. According to the coding guidelines, type imports should be in separateimport typestatements.♻️ Refactor to separate imports
+import type { CategoryCounts, CategoryKey } from "$lib/categoryMapping"; import { CATEGORY_ENTRIES, - type CategoryCounts, - type CategoryKey, placeMatchesCategory, } from "$lib/categoryMapping";As per coding guidelines: "Separate type imports from value imports using
import type { ... }for types only andimport { ... }for values only".🤖 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/routes/map/components/MerchantListPanel.svelte` around lines 10 - 15, The current import mixes runtime values and TypeScript types; split them into two imports: keep runtime symbols (CATEGORY_ENTRIES, placeMatchesCategory) in the regular import and move types (CategoryCounts, CategoryKey) into a separate "import type" statement. Update the import that currently references CATEGORY_ENTRIES, CategoryCounts, CategoryKey, placeMatchesCategory so you have one line importing CATEGORY_ENTRIES and placeMatchesCategory, and another "import type" line importing CategoryCounts and CategoryKey.Source: Coding guidelines
🤖 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/routes/map/components/MerchantListPanel.svelte`:
- Around line 892-916: The peek facade button currently (bound to facadeElement
and handled by handlePeekTap) only has aria-expanded; add aria-label and
aria-controls for screen readers by giving the button a descriptive aria-label
matching the grabber's label and set aria-controls to the id of the peek
panel/sheet element it toggles (use the same panel id used elsewhere for the
grabber). Ensure the aria-controls value exactly matches the target panel's id
and keep aria-expanded in sync when state changes in handlePeekTap/sheetGesture
logic.
---
Outside diff comments:
In `@src/routes/map/components/MerchantListPanel.svelte`:
- Around line 10-15: The current import mixes runtime values and TypeScript
types; split them into two imports: keep runtime symbols (CATEGORY_ENTRIES,
placeMatchesCategory) in the regular import and move types (CategoryCounts,
CategoryKey) into a separate "import type" statement. Update the import that
currently references CATEGORY_ENTRIES, CategoryCounts, CategoryKey,
placeMatchesCategory so you have one line importing CATEGORY_ENTRIES and
placeMatchesCategory, and another "import type" line importing CategoryCounts
and CategoryKey.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5c835d3a-1a75-45f1-9f30-d26bcfe811ac
📒 Files selected for processing (22)
src/components/SearchInput.sveltesrc/lib/analytics.tssrc/lib/drawerConfig.tssrc/lib/drawerGestureController.tssrc/lib/drawerGestureUtils.test.tssrc/lib/drawerGestureUtils.tssrc/lib/i18n/locales/bg.jsonsrc/lib/i18n/locales/de.jsonsrc/lib/i18n/locales/en.jsonsrc/lib/i18n/locales/es.jsonsrc/lib/i18n/locales/fr.jsonsrc/lib/i18n/locales/it.jsonsrc/lib/i18n/locales/nl.jsonsrc/lib/i18n/locales/pt-BR.jsonsrc/lib/i18n/locales/ru.jsonsrc/lib/utils.test.tssrc/lib/utils.tssrc/routes/map/+page.sveltesrc/routes/map/components/MapSearchBar.sveltesrc/routes/map/components/MerchantListPanel.sveltesrc/routes/map/components/NearbyCountPill.sveltetests/merchant-list-panel.spec.ts
| <button | ||
| bind:this={facadeElement} | ||
| type="button" | ||
| on:click={handlePeekTap} | ||
| on:pointerdown={onFacadePointerDown} | ||
| on:pointermove={sheetGesture.handlePointerMove} | ||
| on:pointerup={onFacadePointerUp} | ||
| on:pointercancel={sheetGesture.handlePointerCancel} | ||
| class="relative flex w-full touch-none items-center rounded-lg py-3 pr-3 pl-10 text-left" | ||
| aria-expanded="false" | ||
| > | ||
| <Icon | ||
| w="18" | ||
| h="18" | ||
| icon="search" | ||
| type="material" | ||
| class="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-gray-600 dark:text-white/70" | ||
| /> | ||
| <span class="flex-1 truncate text-base text-gray-400 dark:text-white/50"> | ||
| {$_('search.placeholderPlaces')} | ||
| </span> | ||
| {#if pillCount} | ||
| <NearbyCountPill count={pillCount} /> | ||
| {/if} | ||
| </button> |
There was a problem hiding this comment.
Add aria-label and aria-controls to peek facade button.
The peek facade button has aria-expanded="false" but is missing aria-label and aria-controls attributes. For consistency with the grabber element (lines 545-547) and better screen reader support, add these accessibility attributes.
♿ Add accessibility attributes
<button
bind:this={facadeElement}
type="button"
on:click={handlePeekTap}
on:pointerdown={onFacadePointerDown}
on:pointermove={sheetGesture.handlePointerMove}
on:pointerup={onFacadePointerUp}
on:pointercancel={sheetGesture.handlePointerCancel}
class="relative flex w-full touch-none items-center rounded-lg py-3 pr-3 pl-10 text-left"
aria-expanded="false"
+ aria-label={$_('aria.expandMerchantList')}
+ aria-controls="merchant-sheet-content"
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button | |
| bind:this={facadeElement} | |
| type="button" | |
| on:click={handlePeekTap} | |
| on:pointerdown={onFacadePointerDown} | |
| on:pointermove={sheetGesture.handlePointerMove} | |
| on:pointerup={onFacadePointerUp} | |
| on:pointercancel={sheetGesture.handlePointerCancel} | |
| class="relative flex w-full touch-none items-center rounded-lg py-3 pr-3 pl-10 text-left" | |
| aria-expanded="false" | |
| > | |
| <Icon | |
| w="18" | |
| h="18" | |
| icon="search" | |
| type="material" | |
| class="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-gray-600 dark:text-white/70" | |
| /> | |
| <span class="flex-1 truncate text-base text-gray-400 dark:text-white/50"> | |
| {$_('search.placeholderPlaces')} | |
| </span> | |
| {#if pillCount} | |
| <NearbyCountPill count={pillCount} /> | |
| {/if} | |
| </button> | |
| <button | |
| bind:this={facadeElement} | |
| type="button" | |
| on:click={handlePeekTap} | |
| on:pointerdown={onFacadePointerDown} | |
| on:pointermove={sheetGesture.handlePointerMove} | |
| on:pointerup={onFacadePointerUp} | |
| on:pointercancel={sheetGesture.handlePointerCancel} | |
| class="relative flex w-full touch-none items-center rounded-lg py-3 pr-3 pl-10 text-left" | |
| aria-expanded="false" | |
| aria-label={$_('aria.expandMerchantList')} | |
| aria-controls="merchant-sheet-content" | |
| > | |
| <Icon | |
| w="18" | |
| h="18" | |
| icon="search" | |
| type="material" | |
| class="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-gray-600 dark:text-white/70" | |
| /> | |
| <span class="flex-1 truncate text-base text-gray-400 dark:text-white/50"> | |
| {$_('search.placeholderPlaces')} | |
| </span> | |
| {`#if` pillCount} | |
| <NearbyCountPill count={pillCount} /> | |
| {/if} | |
| </button> |
🤖 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/routes/map/components/MerchantListPanel.svelte` around lines 892 - 916,
The peek facade button currently (bound to facadeElement and handled by
handlePeekTap) only has aria-expanded; add aria-label and aria-controls for
screen readers by giving the button a descriptive aria-label matching the
grabber's label and set aria-controls to the id of the peek panel/sheet element
it toggles (use the same panel id used elsewhere for the grabber). Ensure the
aria-controls value exactly matches the target panel's id and keep aria-expanded
in sync when state changes in handlePeekTap/sheetGesture logic.

Summary by CodeRabbit
New Features
Bug Fixes
Documentation