Skip to content

feat(map): simplify search UX/UI#1061

Open
escapedcat wants to merge 9 commits into
mainfrom
search-ux-ui
Open

feat(map): simplify search UX/UI#1061
escapedcat wants to merge 9 commits into
mainfrom
search-ux-ui

Conversation

@escapedcat

@escapedcat escapedcat commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Mobile search sheet interface with expandable peek state and touch gestures
    • Nearby merchant count displayed as a pill badge in search
    • "Search worldwide" call-to-action when no nearby results match
    • Unified search placeholder across all search scopes
  • Bug Fixes

    • Fixed search input trailing slot to prevent passive elements from blocking interactions
  • Documentation

    • Updated localization strings across multiple languages (English, German, Spanish, French, Italian, Dutch, Portuguese, Russian, Bulgarian)
    • Added accessibility labels for merchant list expansion

escapedcat and others added 9 commits June 12, 2026 18:32
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-code-review

Copy link
Copy Markdown
Contributor

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@netlify

netlify Bot commented Jun 12, 2026

Copy link
Copy Markdown

Deploy Preview for btcmap ready!

Name Link
🔨 Latest commit 1a3bf65
🔍 Latest deploy log https://app.netlify.com/projects/btcmap/deploys/6a2c4206d95398000893746d
😎 Deploy Preview https://deploy-preview-1061--btcmap.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 53 (🔴 down 40 from production)
Accessibility: 97 (no change from production)
Best Practices: 92 (🔴 down 8 from production)
SEO: 96 (no change from production)
PWA: 90 (no change from production)
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This 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.

Changes

Mobile Search Sheet with Peek/Expand Gestures

Layer / File(s) Summary
Gesture infrastructure and analytics foundation
src/lib/drawerConfig.ts, src/lib/drawerGestureController.ts, src/lib/drawerGestureUtils.ts, src/lib/analytics.ts
SEARCH_SHEET_PEEK_HEIGHT constant and createDrawerGestureController now accept configurable peekHeight, canDismiss, and analytics event names. determineSnapState parameterizes peek height throughout snap decisions. EventName is exported with new search sheet and CTA click events.
Gesture controller implementation and utilities
src/lib/drawerGestureController.ts, src/lib/drawerGestureUtils.ts, src/lib/drawerGestureUtils.test.ts
Pointer-up dismiss, pointer-cancel snap-back, and content-driven drag clamping logic now respect configurable peekHeight and canDismiss. Public collapse and resetToPeek methods derive height from peekHeight. Snap state computation uses provided peekHeight in velocity-based and position-threshold paths. New test case validates custom peek height handling.
Input styling and utility helpers
src/components/SearchInput.svelte, src/lib/utils.ts, src/lib/utils.test.ts
SearchInput trailing container uses pointer-events-none by default so passive content (pills, spinners) does not block taps. formatNearbyPillCount helper formats merchant counts for badge display: empty string for ≤0, >99 when above cap, otherwise numeric string. Includes test coverage for edge cases.
Map page responsive layout split
src/routes/map/+page.svelte
Adds isMobileLayout flag computed from viewport width breakpoint. Floating MapSearchBar renders only on desktop. MerchantListPanel receives mapReady and isMobile props. Sets --search-sheet-peek CSS variable and offsets MapLibre controls on mobile by peek height plus safe-area inset.
MapSearchBar simplification: remove mode switching
src/routes/map/components/MapSearchBar.svelte
Removes Worldwide/Nearby segmented tabs and mode-switching handler. Simplifies to single neutral-placeholder search input that appears only when panel is closed. Displays clear button, search spinner, or NearbyCountPill in trailing slot based on state. Removes onNearbyClick and isLoadingCount exports.
NearbyCountPill component
src/routes/map/components/NearbyCountPill.svelte
New reusable component displaying location icon and localized nearby count text in a styled pill. Accepts count string prop for badge display.
MerchantListPanel sheet refactor with gestures and filtering
src/routes/map/components/MerchantListPanel.svelte, tests/merchant-list-panel.spec.ts
Mobile now supports peek/expanded sheet model with grabber, facade button, draggable header, and touch/gesture handlers coordinated with drawer controller. Nearby filtering uses reactive nearbyFilter producing filteredMerchants with count reflecting filtered results (including 0-count display). New handleSearchWorldwideCta switches scope to worldwide. Template reworked to render peek facade with count pill, expanded input/scope controls, and nearby empty-state CTA. Tests updated to assert peek-sheet visibility, facade-tap expansion, Escape collapse, drawer coordination, and zero-results CTA flow.
Internationalization updates across 8 locales
src/lib/i18n/locales/{bg,de,en,es,fr,it,nl,pt-BR,ru}.json
Unified search placeholder from worldwide/nearby keys to single placeholderPlaces. Added noNearbyMatches and noNearbyMatchesHint for empty nearby states. Introduced searchWorldwide and nearbyCount labels for scope switching UI. Added aria.expandMerchantList accessibility string across all 9 locale files.

Sequence Diagram

sequenceDiagram
  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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • teambtcmap/btcmap.org#677: Modifies EventName union in src/lib/analytics.ts by adding different analytics event strings, overlapping at the same type-definition level.
  • teambtcmap/btcmap.org#695: Updates MerchantListPanel's nearby/no-matches UI and empty-state result rendering logic that overlaps with this PR's nearby-filter refactor.
  • teambtcmap/btcmap.org#1057: Modifies worldwide/nearby control markup in MapSearchBar and MerchantListPanel, though this PR performs a broader search-sheet UX refactor beyond tab changes.

Suggested labels

Review effort 4/5

Suggested reviewers

  • dadofsambonzuki
  • bubelov

Poem

🐰 A sheet that peeks and rises high,
With gestures smooth, no tap to pry—
Nearby or wide, the choice is clear,
Locales sing out their welcome cheer!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is completely empty; the author provided no description despite the template requiring information about related issues, proposed changes, screenshots, and additional context. Add a comprehensive PR description including: related issue reference, clear explanation of the search UX/UI simplifications, any relevant screenshots, and additional context about design changes and migration notes for reviewers.
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(map): simplify search UX/UI' accurately describes the main objective of the changeset, which involves refactoring the search interface across multiple components (MapSearchBar, MerchantListPanel, SearchInput) and configuration to improve usability.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch search-ux-ui

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: 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 win

Separate type imports from value imports.

The import statement mixes type and value imports using inline type keywords. According to the coding guidelines, type imports should be in separate import type statements.

♻️ 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 and import { ... } 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

📥 Commits

Reviewing files that changed from the base of the PR and between 91d29ed and 1a3bf65.

📒 Files selected for processing (22)
  • src/components/SearchInput.svelte
  • src/lib/analytics.ts
  • src/lib/drawerConfig.ts
  • src/lib/drawerGestureController.ts
  • src/lib/drawerGestureUtils.test.ts
  • src/lib/drawerGestureUtils.ts
  • src/lib/i18n/locales/bg.json
  • src/lib/i18n/locales/de.json
  • src/lib/i18n/locales/en.json
  • src/lib/i18n/locales/es.json
  • src/lib/i18n/locales/fr.json
  • src/lib/i18n/locales/it.json
  • src/lib/i18n/locales/nl.json
  • src/lib/i18n/locales/pt-BR.json
  • src/lib/i18n/locales/ru.json
  • src/lib/utils.test.ts
  • src/lib/utils.ts
  • src/routes/map/+page.svelte
  • src/routes/map/components/MapSearchBar.svelte
  • src/routes/map/components/MerchantListPanel.svelte
  • src/routes/map/components/NearbyCountPill.svelte
  • tests/merchant-list-panel.spec.ts

Comment on lines +892 to +916
<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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
<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.

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