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

@ox0/query

v0.8.0

Published

A search-and-filter language for lists of records: plain words, `field:value`, exclusions, number and date comparisons and ranges, and several values per field — without boolean logic to learn. Parses to a JSON-serializable `Query` that a backend, the dat

Readme

@ox0/query

A search-and-filter language for lists of records: plain words, field:value, exclusions, number and date comparisons and ranges, and several values per field — without boolean logic to learn. Parses to a JSON-serializable Query that a backend, the data grid, or a URL can carry.

Documentation: https://esturcke.github.io/ox0-docs/query

pnpm add @ox0/query

There is no @ox0/query stylesheet to import: QueryBar injects its own styles. Like the rest of ox0, it needs the design tokens (@ox0/tokens/styles.css) loaded once by the app.

Syntax

| Type | Examples | | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Text | john smith · "john smith" · -john · name:john · name:john* · name:*john* · name:-john · name:=John · name:-=Jo* · name:ada,grace · name:"Smith, John",ada | | Numeric | amount:1000 · amount:>1000 · amount:<=50 · amount:10..50 · amount:100.. · amount:5,10 · age:..5,60.. · -age:8..9 | | Date | created:2026-01-15 · created:2026-01 · created:2026-01..2026-06 · created:-30d · created:<2026 · created:>=2026-01 · created:2026-01,2026-03 · -created:2026-01 | | Enum | status:active · status:active,pending · status:act* · status:-closed · -status:closed,archived | | Any | name:* (has a value) · name:-* (empty) |

Plain words match anywhere; a field value matches the whole value unless you add * ("*" in quotes is literal). Matching is case-insensitive; = after a text field's colon (and after any -) matches case: name:=John doesn't match "john". Different fields AND together; values within one field OR; plain words AND.

A comma separates the values of one filter, for every type of field; quote a value that contains one (name:"Smith, John"). Numbers are written without thousands separators — amount:1,000 is 1 or 0. Number and date list items can be ranges (age:..5,60..); comparisons and relative dates can't be in a list. Numbers and dates are excluded with - before the field (-amount:5), because amount:-5 means "equals −5".

Usage

import {
  defineQuerySchema,
  normalizeQuery,
  parseQuery,
  serializeQuery,
  syntaxGuide,
} from "@ox0/query"

const orders = defineQuerySchema({
  fields: [
    { id: "customer", type: "text", aliases: ["who"] },
    { id: "amount", type: "numeric" },
    { id: "created", type: "date" },
    { id: "status", type: "enum", values: ["active", "pending", "closed"] },
  ],
})

const { committed, rejected, diagnostics } = parseQuery("status:active amount:>1000 acme", orders, {
  final: true,
})
serializeQuery(committed) // "status:active amount:>1000 acme"

// Tokens from outside the parser (props, saved state): canonical ids, enum casing, slot rule.
const { query } = normalizeQuery(committed, orders)

// Help content with examples from these fields; the UI words each entry from its id and params.
syntaxGuide(orders)

Diagnostics carry a kind, a stable reason code, and raw params — never user-facing text.

Shareable links

A query lives in the URL fragment as #0:status:active+amount:%3E100 (readable, hand-editable) or, when that would pass 200 characters and compression is shorter, #1:<compressed JSON>. The number is the format: every format ever written stays decodable.

import { useQueryFragment } from "@ox0/query/react"

function Orders() {
  // Reads the fragment on load and on back/forward; writes committed changes with replaceState.
  const { query, setQuery, diagnostics } = useQueryFragment(orders) // or { key: "orders" }
  // …
}

Without React: encodeQuery(query) → payload, decodeQuery(payload, schema) → { query, diagnostics } (never throws; a token that no longer fits the schema stays as an invalid token that matches nothing, with a diagnostic), and readQueryFragment / writeQueryFragment for the fragment itself. Field ids end up in links, so rename a field by moving its old id to previousIds.

QueryBar

import { QueryBar, useQueryFragment } from "@ox0/query/react"

function Orders() {
  const { query, setQuery } = useQueryFragment(orders)
  return <QueryBar label="Search orders" schema={orders} value={query} onChange={setQuery} />
}

Typing builds a pending query; ⌘/Ctrl+Enter or Apply applies it (onChange), Cancel or Escape drops it, and × clears and applies. apply="live" applies each token as it is completed instead. Tab completes a suggestion. It takes sizing props (width, maxWidth, flex, …) like other ox0 fields. Wording comes from ox0.query.* messages (defaultQueryMessages). The dropdown and the help render in a portal, so a container with overflow: hidden doesn't clip them.

To build the filter from values on the page (a "filter by this value" control), edit the pending query through apiRef with editFieldValue; onPendingChange reports what is pending:

const apiRef = useRef<QueryBarApi>(null)
const include = (status: string) =>
  apiRef.current?.edit((pending) =>
    editFieldValue(pending, orders, { field: "status", value: status, action: "include" }),
  )
// <QueryBar apiRef={apiRef} … />

Text that doesn't parse

A token the parser can't read (created:202, gone:x) stays in the query as { type: "invalid", raw } and matches nothing, so a mistake empties the results rather than silently widening them. It stays in links as typed. QueryBar underlines it and says why in the dropdown, without adding anything to the page's layout. A backend that translates queries must treat an invalid token as matching nothing too.

Recent searches

import { useSearchHistory } from "@ox0/hooks"

const history = useSearchHistory("orders") // one key per box; needs a StorageProvider
<QueryBar history={history} … />

↓ in the empty box lists recent searches (the newest 10, then a "Show all" row) above the field list; typing narrows them, matching the whole history. Focusing or clicking the box doesn't open the dropdown. A query is recorded when it is applied (in live mode, when focus leaves the box), deduplicated, newest first, 15 at most (useSearchHistory(key, { limit }) keeps more). Shift+Delete or the row's × removes one. Searches can name real people: pass { storage: "session" } to forget them when the tab closes.

Saved searches

import { useSavedSearches } from "@ox0/hooks"

const saved = useSavedSearches("orders") // same key as the box's history
<QueryBar history={history} saved={saved} … />

A star in the bar saves the applied query and opens a small popover to name it (optional); on a saved query it renames or removes it. Saved searches list above recent ones, name first and query after it in lighter type, 10 at a time with a "Show all" row. Typing plain words (no field:value yet) filters them by name or query and hides recent ones. A recent search is saved from its row's star or ⌘/Ctrl+S; a saved one is renamed from its pencil and removed with its × or Shift+Delete, with an Undo toast when the app has a ToastProvider.

Filtering records in the browser

import { createMatcher, defineQuerySchema } from "@ox0/query"

// With <Payment>, a field without `get` must be a property of Payment: a typo is a type error.
const payments = defineQuerySchema<Payment>({
  fields: [
    { id: "payer", type: "text" },
    { id: "amount", type: "numeric" },
    // A list: a filter matches if any patient does; -patients:… if none do.
    { id: "patients", type: "text", get: (p) => p.patients.map((patient) => patient.name) },
    // Show and type "bill" for a value stored as "statement".
    {
      id: "kind",
      type: "enum",
      values: ["bill", "receipt"],
      get: (p) => (p.kind === "statement" ? "bill" : p.kind),
    },
  ],
})

const visible = rows.filter(createMatcher(payments, query, { timeZone: "America/New_York" }))

An empty list, null, or "" is no value: it matches no filter, positive or negated, and field:-* finds it. matchesPattern(value, pattern) checks a single * pattern.

Help

QueryBar's dropdown footer has a "Search syntax" button (also ⌘/Ctrl+/) that opens the guide, with examples built from your schema's fields; clicking one applies it. <QueryHelp schema={…} onExample={…} /> renders the same guide anywhere else.