@bitbaum/listkit

中文文档
0.2.2发布于 2026/10/28MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 3661

A list as a query: a closed set of facet kinds, a URL codec that copies rather than rebuilds, a comparator that puts missing values last, and page arithmetic that cannot produce a negative offset. No markup, no React, no HTTP client.

原始使用文档
文档目录7 节
  1. What it ships
  2. Five facet kinds, and no magic sentinel
  3. Rules the tests pin
  4. Search that finds what people type
  5. What it deliberately does not ship
  6. Install
  7. Develop

@bitbaum/listkit

A list as a query. Filtering, sorting, searching and paging are the same problem everywhere, and they were solved separately everywhere.

A survey of twelve repositories in this fleet on 2026-09-11 found:

whatcount
incompatible filter-state shapes12, no two alike
hand-built URL builders~34
copies of "toggle a value in a set"~25, in two encodings
debounces, at five different delays~11
copies of Math.ceil(total / pageSize)~11, three in one repo
repos that escape % and _ before a SQL LIKE3 of 7 that need to

Two of the toggle copies were character-for-character identical in repos that share no code. One repo wrote the same three-line body eight times for eight facets, plus eleven more for its boolean flags — about a hundred and twenty lines expressing one idea.

Four live bugs were among the URL builders, and this codec exists so they cannot be written again:

  • one built new URLSearchParams() from scratch, so applying a preset silently wiped the reader's active search and five other facets;
  • one changed a date range without clearing page, landing the reader on page 4 of a two-page result;
  • one preserved exactly three params by name, so every param added later had to be remembered in six separate template strings;
  • one used push rather than replace for a debounced search box, so the back button walked backwards through every intermediate spelling of the word.

What it ships

The decisions, and nothing else.

import { parseQuery, writeQuery, applyQuery, type ListSpec } from "@bitbaum/listkit";

const spec: ListSpec<Project> = {
  facets: [
    { key: "kind", kind: "one", value: (p) => p.kind, options: ["product", "client", "demo"] },
    { key: "status", kind: "many", value: (p) => p.status, options: ["live", "prospect"] },
    { key: "nosite", kind: "flag", value: (p) => !p.site },
  ],
  search: { text: (p) => [p.name, p.description, p.host] },
  sorts: [
    { key: "name", by: [(p) => p.name] },
    { key: "newest", by: [(p) => p.since, (p) => p.name] },
  ],
  defaultSort: "name",
  defaultPageSize: 25,
};

const query = parseQuery(searchParams, spec); // validated, clamped
const { rows, matched, page, counts } = applyQuery(projects, spec, query);
const url = `?${writeQuery(searchParams, next, spec, query)}`;

Five facet kinds, and no magic sentinel

one · many · flag · range · ordinal. The value for "no filter" is the empty selection, never a magic string. One surveyed repo used the translated label ("Alle") as its all-value, which made filter identity depend on the reader's language.

Rules the tests pin

  • An empty selection filters nothing, not everything.
  • Missing values sort last in both directions. A record with no date is not the oldest record.
  • ?page=0 can never become a negative offset, and an empty list still has one page rather than "page 1 of 0".
  • Facet counts are computed with that facet's own selection lifted, so while filtering by kind=product the count beside kind=demo still says how many demos exist, instead of zero.
  • Writing copies the params you were given. A locale or a referrer survives.
  • page is dropped when the result set changes, kept when only the page moves.
  • A superseded async search resolves to undefined, so a slow "ab" cannot overwrite a fast "abc".
  • Every search word must match, folded, and a non-match scores 0.
  • A reader's chosen sort is never overridden by relevance.

Search that finds what people type

  • Every word, any order. bike sell finds "Sell my old bike".
  • Accents and ß fold. zurich finds "Zürich", strasse finds "Straße".
  • Best match first. While a search is active and the reader has not picked a sort, results rank by where a word matched — search.text lists fields most important first — and how well: the whole field, its start, a word's start, anywhere inside. A phrase match earns a bonus; ties keep the default order. A sort the reader chose wins. ?sort=relevance asks for it explicitly; search: { rank: false } opts a list out.
  • One rule for SQL too. searchTerms(q) gives the folded words; apply one likeContains per term, ANDed, and the database agrees with the array. (Fold the column the same way — unaccent() in Postgres.)

No stemming, no typo tolerance, no index: that is a search engine's job.

What it deliberately does not ship

Markup, tokens, React, a search engine, or an HTTP client.

Nine token vocabularies and six chip treatments were found among those same repos, several different on purpose — one marketplace is monochrome by design, one product reserves an accent colour for Bitcoin UI, one is the only RTL-aware surface in the fleet. The fleet has already run the experiment where a shared renderer owns the markup: it reached two consumers out of twenty and shipped the same two defects to both, unfixable downstream because a consumer cannot patch markup it does not own.

Centralize the rule; render it locally.

The execution engine is also yours. applyQuery is the in-memory engine, for bounded collections. The same ListSpec describes what a SQL builder should do with a large one — escapeLike and likeContains are exported for exactly that. Forcing one execution model would serve one repo and strand the rest.

Install

pnpm add @bitbaum/listkit

Develop

pnpm install
pnpm run verify   # format, lint, typecheck, build, test

Tests import the package by name, so a broken exports or files map fails here rather than at the first consumer's install.

MIT.


Part of bitbaum — AI-native products on open infrastructure, built in Zürich. Every package here lists the apps that use it: which apps use listkit.

Git releases include the compiled dist/ artifact and require no consumer-side prepare build. Package CI rebuilds it and refuses a mismatch with committed output, so consumers can retain pnpm's default build-script restrictions. The same public exports work for Git and npm installs.

版本与发布记录

@bitbaum/listkit 共发布 3 个版本,首次发布于 2026/10/26;过去 12 个月发布了 3 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.2.2latest2026/10/2811 天前
0.2.12026/10/2613 天前
0.0.0-stage2026/10/2613 天前

关于 @bitbaum/listkit 的常见问题

@bitbaum/listkit 是什么?

A list as a query: a closed set of facet kinds, a URL codec that copies rather than rebuilds, a comparator that puts missing values last, and page arithmetic that cannot produce a negative offset. No markup, no React, no HTTP client.

如何安装 @bitbaum/listkit?

在项目目录运行 npm install @bitbaum/listkit,或使用 pnpm add / yarn add / bun add。

@bitbaum/listkit 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@bitbaum/listkit 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@bitbaum/listkit 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@bitbaum/listkit 可以商用吗?

@bitbaum/listkit 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@bitbaum/listkit 的下载量有多少?

根据 npm 官方下载统计,@bitbaum/listkit 在 2026/10/08 – 2026/11/06 这 30 天内被下载 3,661 次,日均约 122 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@bitbaum/listkit 还在维护吗?

最新版本 0.2.2 发布于 2026/10/28(11 天前);至今共发布 3 个版本,其中过去 12 个月发布了 3 个;首次发布于 2026/10/26。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。