Skip to content

feat(frontend): add responsive mobile drawer and touch gestures to Co… - #1685

Merged
emdevelopa merged 2 commits into
emdevelopa:mainfrom
Thosine-01:feature/fe-add-responsive-mobile-drawer-and-touch-gestures-to-command-palette
Oct 24, 2026
Merged

emdevelopa merged 2 commits into
emdevelopa:mainfrom
Thosine-01:feature/fe-add-responsive-mobile-drawer-and-touch-gestures-to-command-palette

Conversation

@Thosine-01

Copy link
Copy Markdown
Contributor

Summary

Closes #1508

Makes the Command Palette usable on phones. Before this change it could only be opened with Cmd/Ctrl+K, so touch users couldn't reach it at all. It now has an on-screen trigger, opens as a bottom drawer on mobile, and can be swiped down to close. Desktop behaviour is unchanged.

What changed

Opening on touch devices

  • New CommandPaletteTrigger search button in the dashboard header, shown below lg next to the notification bell
  • New openCommandPalette() helper (dispatches a command-palette:open window event), so any component can open the palette

Responsive drawer (CommandPalette.tsx)

  • Below sm the palette is a bottom sheet with a drag handle, a 85dvh max height and env(safe-area-inset-bottom) padding. It slides up with a motion-safe animation.
  • From sm up, the centered palette is unchanged
  • The command list fills the drawer height on mobile and keeps max-h-72 on desktop

Touch gestures

  • Swipe down to dismiss: the drawer follows the finger and the backdrop fades. It closes after a 100px drag or a fast flick (≥ 0.5 px/ms) and snaps back otherwise.
  • Scroll-safe: dragging inside the command list only moves the drawer once the list is scrolled to the top
  • Each touch locks to one axis after 10px, so horizontal swipes are ignored
  • Converter view: the palette doesn't handle swipes there, because AssetConverter already uses swipe-down for "back". The palette shell also drops overflow-hidden / backdrop-blur on mobile in that view so the converter's fixed drawer isn't clipped.

New reusable hook: useSwipeToDismiss (src/hooks/useSwipeToDismiss.ts)

  • Documented with JSDoc. Returns offset, dragging and touch handlers, and accepts enabled, scrollRef, distance and velocity.
  • Pure shouldDismissSwipe() function for testing the thresholds
  • Other drawers in the app (several use their own touchStart/touchEnd logic) can adopt it

Mobile UX details

  • A Cancel button replaces the ESC hint on mobile, since touch users have no Escape key
  • The keyboard-hint footer is replaced on mobile with "Tap a command to run it · swipe down to close"
  • On coarse pointers the dialog is focused instead of the search input, so the on-screen keyboard doesn't pop up over the drawer. Users tap the input to search.
  • The search input is 16px on mobile to prevent iOS zoom on focus
  • Tap feedback (active: states) and touch-manipulation on options
  • Body scroll is locked while the palette is open

Accessibility

  • All existing keyboard behaviour is unchanged: Cmd/Ctrl+K, arrows, Tab, Home/End, Enter, 1–9, Escape
  • Every gesture has a non-gesture alternative, per WCAG 2.5.1: Cancel button, backdrop tap and Escape
  • The trigger has aria-label and aria-haspopup="dialog"; the drag handle is aria-hidden
  • Focus is still restored to the previously focused element on close

Test fixes

On main, CommandPalette.test.tsx had 2 failing tests:

  • The snapshot was out of date, so it has been regenerated (it also includes this PR's markup changes)
  • The focus tests were flaky: openPalette() waited for the dialog but not for the requestAnimationFrame focus. It now waits for focus inside the dialog, and the suite passes reliably (verified 3 consecutive runs).

Files

  • frontend/src/components/CommandPalette.tsx: drawer layout, gestures, trigger, open event, touch focus handling
  • frontend/src/hooks/useSwipeToDismiss.ts: new reusable hook
  • frontend/src/hooks/useSwipeToDismiss.test.ts: new, 9 tests
  • frontend/src/components/CommandPalette.test.tsx: 9 new mobile drawer tests plus the openPalette() fix
  • frontend/src/components/__snapshots__/CommandPalette.test.tsx.snap: regenerated
  • frontend/src/app/(authenticated)/layout.tsx: header trigger
  • frontend/tailwind.config.js: palette-drawer-in keyframe

Testing

npx vitest run src/components/CommandPalette.test.tsx src/components/CommandPalette.test.ts src/hooks/useSwipeToDismiss.test.ts: 53/53 passing. Type check and lint are clean.

New test coverage:

  • Opens from the header trigger and via openCommandPalette()
  • Drag handle renders; Cancel closes the palette
  • Body scroll is locked and restored
  • Swiping down dismisses the drawer
  • The drawer follows the finger while dragging and snaps back on a short, slow drag
  • No dismiss when dragging inside a scrolled command list
  • The converter view keeps its own swipe handling
  • Touch devices focus the dialog, not the input
  • Hook: thresholds, horizontal-swipe rejection, enabled: false, scrollRef guard, touch cancel

Manual testing

  • iOS Safari: open from the header, swipe to close, safe-area padding, no zoom when the input is focused
  • Android Chrome
  • Header fits at 375px with the new search button
  • Converter view on mobile (opens, swipe-down returns to the list)
  • Desktop: Chrome / Firefox / Safari, with Cmd/Ctrl+K and all keyboard navigation unchanged
  • Reduced motion enabled
  • Lighthouse / axe accessibility audit

Screenshots

Mobile Desktop
Header trigger n/a
Drawer open
Swipe to dismiss n/a

Notes

This PR and # both add keyframes in the same place in tailwind.config.js, so whichever merges second will probably need a small conflict resolved.

🤖 Generated with Claude Code

…mmand Palette

- Render the palette as a bottom drawer below sm (drag handle, 85dvh max
  height, safe-area padding, slide-up entry) and keep the centered palette
  on larger screens
- Swipe down to dismiss via a new reusable useSwipeToDismiss hook: the drawer
  follows the finger, snaps back below the distance/velocity threshold, and
  only drags from the command list when it is scrolled to the top
- Add a header search button (below lg) and openCommandPalette() event so
  touch users can open the palette without Cmd/Ctrl+K
- Add a Cancel button and touch-specific footer hint; hide keyboard hints on
  mobile; tap feedback and touch-manipulation on options
- Focus the dialog instead of the input on coarse pointers so the on-screen
  keyboard does not cover the drawer; lock body scroll while open
- Leave swipe handling to the converter view, which has its own drawer
- Fix flaky focus tests by waiting for the rAF focus in openPalette(), and
  update the outdated snapshot

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 22, 2026

Copy link
Copy Markdown

@Thosine-01 is attempting to deploy a commit to the Emmanuel's projects Team on Vercel.

A member of the Team first needs to authorize it.

@emdevelopa
emdevelopa merged commit d4d59c0 into emdevelopa:main Oct 24, 2026
0 of 5 checks passed
@gitguardian

gitguardian Bot commented Oct 24, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 4 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

Since your pull request originates from a forked repository, GitGuardian is not able to associate the secrets uncovered with secret incidents on your GitGuardian dashboard.
Skipping this check run and merging your pull request will create secret incidents on your GitGuardian dashboard.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
38865302 Triggered Generic High Entropy Secret 2905902 backend/src/routes/sep0001.test.js View secret
38865302 Triggered Generic High Entropy Secret 2905902 backend/src/lib/sep0001-generator.sanitization.test.js View secret
38864685 Triggered Generic High Entropy Secret 2905902 backend/src/lib/exchange-rate-oracle-sanitizer.test.js View secret
38865486 Triggered Generic High Entropy Secret 2905902 backend/src/lib/websocket-relay-retry.test.js View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

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.

[Frontend] Add responsive mobile drawer and touch gestures to Command Palette

2 participants