Skip to content

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

Open
serhalp wants to merge 28 commits intomainfrom
serhalp/cmdk-palette
Open

feat(ui): add ⌘+K command palette for quick nav and actions#2159
serhalp wants to merge 28 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 Mar 30, 2026 0:41am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Mar 30, 2026 0:41am
npmx-lunaria Ignored Ignored Mar 30, 2026 0:41am

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.

@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
Copy link
Copy Markdown
Member

@knowler knowler left a comment

Choose a reason for hiding this comment

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

When interacting with settings, I don’t think the modal should close after applying one. Often, I might want to try out a few options before settling on one or it also could be disorienting if a setting is not what you wanted and you have to navigate back to where you were. I realize this might not seem conventional though (e.g. Raycast, GitHub close the modal after the command). Curious to hear what others think. Maybe for power users who’d use this feature, closing it would align with their expectations. It seems overwhelming conventional to close after applying the setting, so ignore me.

Similar to Alex’s comment, some feedback, especially for screen readers would be great after running a command. I think if the modal stayed open, that would be sufficient visually (so no real necessity for a toast), but for screen readers feedback in a live region would be helpful (I’m not sure if you tried to do this as I see commits that make me think it was implemented, but I’m not hearing anything which could be due to the modal closing or maybe the live region being inert—I haven’t looked into the code specifically).

@serhalp
Copy link
Copy Markdown
Member Author

serhalp commented Mar 29, 2026

2. Navigation from [compare](https://npmx-1pgebu46f-npmx.vercel.app/compare?facets=packageSize) _and other pages_ to search doesn't work

D'oh, good catch, thanks! I broke this when I added the "Search for $query" fallback item. I should've made this a separate palette command entirely.
13e6fab

3. Keyboard shortcut does nothing too

🤦🏼 wow I broke a lot of things at the last minute before putting this up for review, oof.
e797864

4. I would like to see some response that action was successful (like "package name copied" toast) - but it's probably better to postpone to the next iteration

🙏🏼 yes let's iterate on this please.

5. I can copy table on compare page even if there are only one package selected (should work only after two)

Damn, another good catch.
e867eb1

6. The language selected via the command panel is reset after reload

Good catch! Fixed in 3e2e8a5. We should refactor that.

@serhalp
Copy link
Copy Markdown
Member Author

serhalp commented Mar 30, 2026

UI/UX details

  1. Strange scroll behavior

How about just make header sticky and have scroll on whole dialog?

🤔 https://github.com/user-attachments/assets/85c7527f-0030-404f-8d2d-6f4db5e18a00

7. Some options (links without badges) not in the center vertically

fixed in 2c673632

@serhalp serhalp requested a review from alexdln March 30, 2026 00:39
@serhalp
Copy link
Copy Markdown
Member Author

serhalp commented Mar 30, 2026

@knowler I think I've got all the 32393942 palette commands triggering appropriate SR announcements now 🙏🏼

name="accent-color"
class="sr-only"
:value="color.id"
:checked="selectedAccentColor === color.id || (!selectedAccentColor && color.id === 'sky')"
Copy link
Copy Markdown
Member

@alexdln alexdln Mar 30, 2026

Choose a reason for hiding this comment

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

Looks like still broken for a bit

Should show sky on first visit
Image

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

async function doLoad(name: string) {
try {
const allVersions = await fetchAllPackageVersions(name)
if (requestToken !== loadToken || toValue(packageName) !== name) return
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It seems better to abort the request instead of checking requestToken, but this can probably also be done in the next iteration

I'm not sure about need for requestToken. Probably if commandPalette now again with the same package as before (package-a -> package-b -> package-a) - we can continue with versions from the first request.

Also just noticed that above (if (pendingLoad) return pendingLoad) - if user changed the package and they have an active request from the previous one - you will never call doLoad for the current package

Copy link
Copy Markdown
Member

@alexdln alexdln Mar 30, 2026

Choose a reason for hiding this comment

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

Oh, I see that you clean data on package change. I have a feeling that this logic might not work the way you intended, can you double check please?

Like if I called ensure loaded on package A, then went to package B, the logic will continue with previous ensure loaded or even if I will rerun it will stop on if (pendingLoad) return pendingLoad - then [after ensureloaded()] I will receive empty versions because they were cleaned on package change and never updated (hope I compiled everything correctly in my head)

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