npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

foldkit-command-palette

v0.1.0

Published

Command palette for Foldkit: a dialog with a query input and a sectioned listbox; ⌘K / Ctrl+K opens it, the host supplies the items.

Readme

foldkit-command-palette

A command palette for Foldkit: a dialog with a query input and a sectioned listbox. ⌘K (macOS) or Ctrl+K opens it from anywhere; the arrow keys move through every section, Enter or a click chooses, Escape closes.

It is a Submodel in the Elm shape (Model, Message, update, view, subscriptions) built on the @foldkit/ui Dialog, so focus trapping, scroll lock and outside-click close come from there.

The palette owns no data. The host passes the sections and items to the view on every render and hears what happened through OutMessages. That keeps remote search, caching and stale-result handling in the host, where the data lives, and keeps the package free of any domain type. Every string on screen comes from the host, already translated.

License: MIT.

Install

pnpm add foldkit-command-palette

Peer dependencies: effect (4.x), foldkit and @foldkit/ui (0.158.x). Import the default look once:

import "foldkit-command-palette/styles.css"

Usage

A complete host, compiled against the published package:

import { Option, Schema } from "effect"
import { Subscription, Update } from "foldkit"
import type { Html, HtmlBuilder } from "foldkit/html"
import { defineMessageUnion } from "foldkit/message"
import { evo } from "foldkit/struct"
import * as Palette from "foldkit-command-palette"

// MODEL: the palette's own state, plus whatever the host searches.
const Model = Schema.Struct({ palette: Palette.Model, query: Schema.String })
type Model = typeof Model.Type
export const init = (): Model => ({ palette: Palette.init({ id: "quick-search" }), query: "" })

// MESSAGE
const Message = defineMessageUnion({
  GotPaletteMessage: { message: Palette.Message },
  ClickedSearch: {}
})
type Message = typeof Message.Type

// UPDATE
const paletteFold = {
  read: (model: Model) => Option.some(model.palette),
  write: (model: Model, palette: Palette.Model) => evo(model, { palette: () => palette }),
  toParentMessage: (message: Palette.Message) => Message.GotPaletteMessage({ message }),
  foldOutMessage: Palette.OutMessage.match<Update.Step<Model, Message>>({
    // Filter the local sections; start a remote search and drop answers to older queries.
    QueryChanged: ({ query }) => (model) => ({ model: evo(model, { query: () => query }) }),
    // Navigate; the palette has already closed.
    ItemChosen: () => (model) => ({ model }),
    // Drop pending results, so a reopened palette does not flash the last search.
    Closed: () => (model) => ({ model: evo(model, { query: () => "" }) })
  })
}
const foldPalette = Update.foldChild({ update: Palette.update, ...paletteFold })
const openPalette = Update.foldChildStep({ update: Palette.open, ...paletteFold })

export const update = (model: Model, message: Message): Update.Return<Model, Message> =>
  Message.match<Update.Return<Model, Message>>(message, {
    ClickedSearch: () => openPalette(model), // the search button in the top bar
    GotPaletteMessage: ({ message }) => foldPalette(model, message)
  })

// SUBSCRIPTIONS: ⌘K / Ctrl+K from anywhere in the document.
export const subscriptions = Subscription.lift(Palette.subscriptions)<Model, Message>({
  toChildModel: (model) => model.palette,
  toParentMessage: (message) => Message.GotPaletteMessage({ message })
})

// VIEW
const sections: ReadonlyArray<Palette.Section> = [
  { title: "Pages", items: [{ id: "page:settings", label: "Settings" }] },
  { title: "People", items: [{ id: "person:7", label: "Alice Carter", detail: "+1 555 0100" }] }
]

export const view = (model: Model, h: HtmlBuilder<Message>): Html =>
  h.submodel({
    slotId: "quick-search",
    model: model.palette,
    view: Palette.view,
    toParentMessage: (message) => Message.GotPaletteMessage({ message }),
    viewInputs: {
      title: "Quick search",
      placeholder: "Pages and people",
      emptyText: "Nothing found",
      loadingText: "Searching…",
      sections,
      status: "idle",
      footer: h.p([], ["↑↓ to move · Enter to open · Esc to close"]) // static markup only
    }
  })

API

| export | shape | |---|---| | Model | { dialog: Dialog.Model, query: string, activeIndex: number } | | init(config) | { id: string, isOpen?: boolean } → Model | | update(model, message) | → { model, commands?, outMessage? }. Commands: the Dialog's own, and ScrollActiveIntoView after an arrow key | | open(model) / close(model) | the same return, for a host's own button or route change | | view | Submodel view taking ViewInputs | | subscriptions | the ⌘K / Ctrl+K document listener | | toShortcutMessage(event) | the shortcut filter, for a host that routes keys itself | | isOpen, inputId, listboxId, optionId(model, index) | read-only helpers | | ScrollActiveIntoView | the scroll Command, for hosts' Story tests: resolve it with Message.CompletedScrollActiveIntoView() |

ViewInputs

| field | type | | |---|---|---| | title | string | the dialog's accessible name (visually hidden) | | description? | string | the dialog's accessible description (visually hidden); omit it rather than repeat the title | | placeholder | string | | | inputLabel? | string | the input's accessible name; defaults to the placeholder | | emptyText | string | shown when no section has an item and nothing is loading or failed | | loadingText | string | announced through the role="status" live region while status is "loading" | | sections | ReadonlyArray<{ title, items: ReadonlyArray<{ id, label, detail?, icon? }> }> | empty sections are not rendered | | status | "idle" \| "loading" \| { error: string } | loading keeps the sections on screen; an error renders as role="alert" inside the dialog | | footer? | Html | under the list, e.g. keyboard hints. Static markup only: handlers inside it would dispatch into the palette, not the host |

Item ids must be unique across sections: ItemChosen carries the id back. icon is decorative, static markup, rendered aria-hidden; the option is named by its label and described by its detail.

OutMessages

| OutMessage | when | the host should | |---|---|---| | QueryChanged{query} | every edit of the query | filter local sections; run remote searches (and drop results for older queries) | | ItemChosen{itemId} | Enter on the active option, or a click | act on it. The palette has already closed and cleared its query, and no Closed follows | | Closed | Escape, outside click, or the host's close | drop pending results. The query is already cleared |

Closed exists because the host keeps results for a query the palette no longer shows: without it, a reopened palette would flash the last search.

Keyboard and ARIA

  • The input is a combobox controlling a listbox; aria-activedescendant points at the active option, which carries aria-selected="true". Focus never leaves the input.
  • Sections are role="group", labelled by their title.
  • ↓ / ↑ move through all sections and wrap at both ends, and scroll the new active option into view when the list hides it; the pointer over an option makes it active without scrolling.
  • Typing resets the active option to the first. When the host's list shrinks under the active index, the view clamps it to the last option.
  • Enter chooses; Escape closes (the input claims Escape so the dialog closes once).
  • ⌘K / Ctrl+K is claimed (preventDefault) and opens the palette; with Shift or Alt, and every other key, it is left to the page. On a layout that types Latin letters the key labelled K counts (Dvorak, AZERTY); on one that does not (Cyrillic, Greek, Hebrew…) the key in the K position does, so the shortcut works without switching layouts.
  • "Searching…" and "Nothing found" are spoken through one role="status" live region that stays in the dialog; an error is a role="alert".

Theming

Everything the stylesheet colours or sizes is a custom property with a neutral fallback. Set them on :root (or any ancestor) to follow your theme:

| variable | default | used for | |---|---|---| | --fk-command-palette-surface | #ffffff | panel background | | --fk-command-palette-text | #1a1a1a | text | | --fk-command-palette-muted | #6b6b6b | section titles, details, placeholder, status | | --fk-command-palette-line | #e5e5e5 | borders | | --fk-command-palette-active | #f0f0f0 | active option background | | --fk-command-palette-active-text | inherit | active option text | | --fk-command-palette-danger | #b3261e | error text | | --fk-command-palette-backdrop | rgba(0,0,0,.4) | backdrop | | --fk-command-palette-radius | 0.75rem | panel corners | | --fk-command-palette-width | 36rem | panel width | | --fk-command-palette-top | 12vh | panel offset from the top |

Below 640 px the panel fills the screen. Class names (fk-command-palette__panel, __input, __option with data-active, __section-title, __detail, __status, __error, __footer, …) are stable for hosts that prefer their own stylesheet.