Skip to content

feat(ui): add ⌘+K command palette for quick nav and actions#2159

Merged
serhalp merged 49 commits intomainfrom
serhalp/cmdk-palette
Apr 6, 2026
Merged

feat(ui): add ⌘+K command palette for quick nav and actions#2159
serhalp merged 49 commits intomainfrom
serhalp/cmdk-palette

Conversation

@serhalp
Copy link
Copy Markdown
Member

@serhalp serhalp commented Mar 20, 2026

🔗 Linked issue

closes #81

supersedes stale #470

🧭 Context

npmx already exposes a ton of useful capabilities (with way more to come), but they require quite a bit of precise clicking around. We always imagined npmx as a power tool for power users. The command palette is a familiar solution to provide discoverable, fast, efficient, repeatable access to an app's capabilities.

📚 Description

This PR adds a command palette with access to every page, every action, and every capability of npmx.

It can be opened from anywhere in the app by pressing ⌘+K on macOS / Ctrl+K on Windows/Linux, or by clicking the new "quick actions" nav item in the header.

The palette includes a set of "global" commands and a composable allowing a page or component to register specific commands that should be made available when that page/component is visible.

The palette supports multi-step flows, such as "change language" → languages are listed.

I should've maybe kept this PR small and added more commands later, but... oops, I believe I covered every single page and capability:

All commands

Global commands (always available)

  • navigation
    • search
    • home
    • compare
    • settings
    • my packages if npm is connected
    • my orgs if npm is connected
    • my profile if atproto is connected
  • connections
    • connect to npm cli if npm not connected
    • connect to atmosphere if atproto not connected
    • disconnect npm if npm is connected
    • disconnect atmosphere if atproto is connected
  • settings
    • language → second step lists all languages + "Help translate" CTA
      • note: there's some magic here to match user input on languages directly on the first screen too!
    • relative dates
    • use system theme
    • use light theme
    • use dark theme
    • accent colors → second step lists all accent colors
    • background shade -> second step lists all bg shades
  • help
    • keyboard shortcuts
    • docs
    • chat
  • npmx
    • about
    • blog
    • privacy
    • accessibility
    • docs
    • chat
    • builders
    • source
    • social

Package context

All pages with a package context also include:

  • package ()
    • copy package name
    • package page
    • docs
    • code
    • compare this package
    • download tarball
    • diff
  • versions of
    • <... every version of this package ...>

Package page

  • package
    • copy install command
    • copy run command if available
    • copy create command if available
    • go to types package if available
    • go to create package if available
    • open skills modal if available
  • links
    • fund if available
    • repo (): <owner/repo>
    • repo stars
    • repo forks
    • homepage
    • issues
    • npm
    • jsr if available
    • <... every playground ...>

Package code page

  • actions
    • copy link
    • copy file contents
    • preview
    • code

Package diff page

  • actions
    • merge modified lines on/off
    • word wrap on/off

Compare page

  • select all facets
  • deselect all facets
  • copy table
  • table
  • charts

Profile page

  • actions
    • edit profile if viewing profile
    • share invite
  • links
    • profile-website if profile has a website

There are two behaviours worth calling out separately:

  • When the user's query is a valid semver range specifier, while in a package context, the package versions listed in the palette are filtered to those that match the semver range.
  • A fallback item always shows up last in the palette results: Search for "<query>". Selecting this submits a search for the user's query.

The palette has full keyboard navigation support and screen reader support.

Screenshots

New header nav item (desktop) Screenshot 2026-03-22 at 14-41-06 npmx - Package Browser for the npm Registry
Global commands (desktop) Screenshot 2026-03-22 at 14 31 21 Screenshot 2026-03-22 at 14 32 58 Screenshot 2026-03-22 at 14 33 29
Global commands — logged in via atproto (desktop) Screenshot 2026-03-22 at 15 02 57
Global commands (mobile, light) Screen Shot 2026-03-22 at 14 37 02
Languages (desktop) Screenshot 2026-03-22 at 14 34 02
Accent colors (desktop) Screenshot 2026-03-22 at 14 34 58
Background shades (desktop, light) Screenshot 2026-03-22 at 14 35 54
New header nav item (desktop, non-homepage) Screenshot 2026-03-22 at 14-43-30 vite - npmx
Package page commands (desktop) Screenshot 2026-03-22 at 14 45 13 Screenshot 2026-03-22 at 14 45 38
Package page - input is valid semver (desktop) Screenshot 2026-03-22 at 14 46 29
Package code page (desktop) Screenshot 2026-03-22 at 14 57 03
Package diff page (desktop) Screenshot 2026-03-22 at 14 59 16
Compare page (desktop) Screenshot 2026-03-22 at 14 59 58
Profile page (desktop) Screenshot 2026-03-22 at 15 05 04
"Search for" fallback command (desktop) Screenshot 2026-03-22 at 14 38 50

Future work

  • This PR was huge enough as is, so I punted on adding hotkeys for quickly triggering commands within the command palette (e.g. typing r and hitting Enter to open the package's repo right away). We should probably do this eventually.
  • I didn't add any palette commands for package/user/org management stuff that users can do when logged in via npm CLI. We should probably do this eventually.

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Apr 6, 2026 11:56am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Apr 6, 2026 11:56am
npmx-lunaria Ignored Ignored Apr 6, 2026 11:56am

Request Review

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 20, 2026

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
i18n/locales/en.json Source changed, localizations will be marked as outdated.
i18n/locales/fr-FR.json Localization changed, will be marked as complete. 🔄️
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 20, 2026

@trueberryless
Copy link
Copy Markdown
Contributor

I love it, super clean and useful! 🥳

@serhalp serhalp force-pushed the serhalp/cmdk-palette branch from 3b1e01b to e280565 Compare March 21, 2026 22:25
@serhalp serhalp force-pushed the serhalp/cmdk-palette branch from e280565 to 10fbdc8 Compare March 22, 2026 01:08
@serhalp serhalp force-pushed the serhalp/cmdk-palette branch from 10fbdc8 to 15a4d07 Compare March 22, 2026 01:23
@serhalp serhalp force-pushed the serhalp/cmdk-palette branch from 15a4d07 to f2b53bf Compare March 22, 2026 01:33
@serhalp serhalp changed the title feat(ui): add ⌘+K command palette for power user fast nav and actions feat(ui): add ⌘+K command palette for quick nav and actions Mar 22, 2026
@serhalp serhalp force-pushed the serhalp/cmdk-palette branch 2 times, most recently from 6777ee1 to 823c9cf Compare March 22, 2026 02:44
@serhalp serhalp force-pushed the serhalp/cmdk-palette branch from 823c9cf to 2667fda Compare March 22, 2026 02:49
@alexdln
Copy link
Copy Markdown
Member

alexdln commented Mar 30, 2026

image Have you pushed all the changes or is it some specific case on mine side only? Let me know if you need more context about the [first point in previous comment](https://github.com//pull/2159#issuecomment-4148167952) - there are two scrolls and the second one scrolls to nowhere.

@serhalp
Copy link
Copy Markdown
Member Author

serhalp commented Mar 31, 2026

Have you pushed all the changes or is it some specific case on mine side only? Let me know if you need more context about the first point in previous comment - there are two scrolls and the second one scrolls to nowhere.

I haven't pushed anything for this, no.

I wasn't able to reproduce it, but now I've just tried it on three browsers and I do see it on Safari 👍🏼. I'll go from there 👀.

serhalp added 3 commits March 31, 2026 21:15
We were ending up with both `overflow-y-auto` and `overflow-hidden`
and for some reason Safari was resolving this "conflict" by showing two scrollbars.
@serhalp
Copy link
Copy Markdown
Member Author

serhalp commented Apr 1, 2026

@alexdln I believe I've fixed it! 8e868d4

Copy link
Copy Markdown
Contributor

@ghostdevv ghostdevv left a comment

Choose a reason for hiding this comment

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

there are a couple oxlint warnings too

Comment on lines +33 to +42
try {
const parsed = new URL(url)
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return null
}

return parsed.href
} catch {
return null
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
try {
const parsed = new URL(url)
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return null
}
return parsed.href
} catch {
return null
}
const parsed = URL.parse(url)
if (!parsed || (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')) {
return null
}
return parsed.href

I think that || is good, but if not we can just do another if above :p

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@@ -1,21 +1,10 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you review the changes to this test file as I'm not 100% on the merge result was correct

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

LGTM

Comment on lines +50 to +53
new Response(JSON.stringify(body), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
new Response(JSON.stringify(body), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
Response.json(body),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

{{ $t('command_palette.title') }}
</span>

<button
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we use button base?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

derp
52679c0

Comment on lines +28 to +33
const dialogId = 'command-palette-modal'
const inputId = `${dialogId}-input`
const descriptionId = `${dialogId}-description`
const statusId = `${dialogId}-status`
const announcementId = `${dialogId}-announcement`
const resultsId = `${dialogId}-results`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

screaming snake? :p

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

const platform = nav.userAgentData?.platform ?? nav.platform ?? ''
const userAgent = navigator.userAgent ?? ''

return /Mac|iPhone|iPad|iPod/i.test(platform) || /Mac|iPhone|iPad|iPod/i.test(userAgent)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we define this regex as a constant

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

cmd+K quick actions command bar

5 participants