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

@astrapi69/entity-kit-core

v0.1.0

Published

Framework-agnostic core types, utilities and design tokens for the entity-kit BeanInfo pattern

Readme

@astrapi69/entity-kit-core

Framework-agnostic core for the entity-kit BeanInfo pattern: the types, registry, pure utilities and design tokens that let any object type describe itself once and be rendered, sorted, searched and acted on by generic UI — in any framework.

This package contains zero React, zero JSX, zero UI framework code. It is pure TypeScript plus a set of optional CSS design tokens. Framework bindings build on top of it:

| Package | Role | | --- | --- | | @astrapi69/entity-kit-core (this) | Framework-agnostic types, registry, utils, CSS tokens | | @astrapi69/entity-kit | React components & hooks built on the core | | @astrapi69/entity-kit-vue (future) | Vue bindings built on the core |

The idea is BeanInfo, from the Java Beans world: an EntityDescriptor<T> declares how to identify, label, list, search, sort and act on items of T. Generic components consume the descriptor and never need to know the concrete type.

Install

npm install @astrapi69/entity-kit-core

No peer dependencies — nothing to add.

Quick start

import {
  type EntityDescriptor,
  searchEntities,
  sortEntities,
  resolveLabel,
  descriptorRegistry,
} from "@astrapi69/entity-kit-core";

interface Book {
  id: string;
  title: string;
  author: string;
  year: number;
  deleted: boolean;
}

// 1. Describe the entity once.
const bookDescriptor: EntityDescriptor<Book> = {
  entityName: "book",
  displayName: (b) => b.title,
  getId: (b) => b.id,
  isDeleted: (b) => b.deleted,
  listFields: [
    { key: "title", label: () => t("book.title"), sortable: true }, // i18n label
    { key: "author", label: "Author", sortable: true },
    { key: "year", label: "Year", sortable: true },
  ],
  searchableFields: ["title", "author"],
};

// 2. Register it (optional — for decoupled lookup by name).
descriptorRegistry.register(bookDescriptor);

// 3. Use the pure utilities.
const filtered = searchEntities(books, bookDescriptor, "tolkien");
const sorted = sortEntities(filtered, bookDescriptor, "year", "desc");
const header = resolveLabel(bookDescriptor.listFields[0].label);

Concepts

Descriptors

A descriptor's required members are entityName, displayName, getId, listFields and isDeleted. Everything else is optional and defaults are documented below. Use withDescriptorDefaults to materialise a descriptor with all optional fields filled in:

import { withDescriptorDefaults } from "@astrapi69/entity-kit-core";

const resolved = withDescriptorDefaults(bookDescriptor);
resolved.detailFields;     // [] when omitted
resolved.actions;          // [] when omitted
resolved.searchableFields; // [] when omitted
resolved.shortDescription; // () => "" when omitted

| Optional field | Default | | --- | --- | | shortDescription | () => "" | | detailFields | [] | | searchableFields | [] | | actions | [] | | thumbnail | undefined | | deletedAt | undefined | | icon | undefined |

The Node type parameter (renderable nodes)

EntityDescriptor, FieldDescriptor and ActionDescriptor take an optional second type parameter Node for renderable things (icon, thumbnail, render). It defaults to unknown, keeping the core framework-agnostic. A binding supplies its own node type:

// Inside the React binding:
import type { ReactNode } from "react";
import type { EntityDescriptor as CoreDescriptor } from "@astrapi69/entity-kit-core";

export type EntityDescriptor<T> = CoreDescriptor<T, ReactNode>;

i18n labels

FieldDescriptor.label and ActionDescriptor.label are typed as string | (() => string) (LabelValue). The factory form lets you wire up i18n. Resolve it with resolveLabel, which invokes the function on every call — never caches — so labels always reflect the active locale:

resolveLabel("Author");          // "Author"
resolveLabel(() => t("author")); // current translation, re-evaluated each call

Registry

DescriptorRegistry is a Map-keyed store of descriptors by entityName, decoupling rendering code from definitions. A shared descriptorRegistry instance is exported for apps that need only one.

registry.register(bookDescriptor);
registry.get<Book>("book");     // throws if not registered
registry.tryGet<Book>("book");  // undefined if not registered
registry.has("book");
registry.list();                // all descriptors, insertion order
registry.names();               // all entity names
registry.unregister("book");
registry.clear();

Soft delete / trash

isDeleted(item) drives trash views; deletedAt(item) optionally supplies a timestamp. The TrashViewOptions.prefiltered flag (defined here, consumed by bindings) tells a trash view that the items are already the trashed set, so the internal isDeleted filter is skipped.

API reference

Types

  • EntityDescriptor<T, Node = unknown> — the self-description of an entity type.
  • FieldDescriptor<T, Node = unknown> — a single field/column.
  • ActionDescriptor<T, Node = unknown> — a single action.
  • RequiredEntityDescriptor<T, Node = unknown> — the required members plus optional rest (input to withDescriptorDefaults).
  • ResolvedEntityDescriptor<T, Node = unknown> — a descriptor with all optional fields filled (output of withDescriptorDefaults).
  • LabelValuestring | (() => string).
  • ActionVariant"default" | "danger".
  • ViewMode"list" | "tile" | "detail".
  • TrashViewOptions{ prefiltered?: boolean }.
  • ClassNames interfaces: TileClassNames, ListClassNames, DetailClassNames, TrashClassNames, SearchClassNames, ViewSwitcherClassNames, EmptyStateClassNames, ActionsClassNames.

Functions

  • resolveLabel(label: LabelValue): string
  • searchEntities<T>(items, descriptor, query): T[] — case-insensitive substring filter over searchableFields. Pure; returns a new array.
  • toSearchString(value: unknown): string — the coercion used by search.
  • sortEntities<T>(items, descriptor, key, direction?): T[] — stable sort by a sortable list field; non-sortable keys return an unchanged copy. Pure.
  • isSortable<T>(descriptor, key): boolean
  • generateTestId(entityName, id, actionId?): string"book-42" or "book-42-delete".
  • generateSearchTestId(entityName): string"book-search".
  • generateViewTestId(mode): string"view-list".
  • withDescriptorDefaults<T>(descriptor): ResolvedEntityDescriptor<T>

Registry

  • class DescriptorRegistry
  • const descriptorRegistry: DescriptorRegistry

Styling & theming

The styles live in this package and are entirely optional — bindings render fine with no stylesheet at all (headless-first). Import the full default theme:

import "@astrapi69/entity-kit-core/styles";

Or import only the component sheets you need:

import "@astrapi69/entity-kit-core/styles/entity-tile.css";
import "@astrapi69/entity-kit-core/styles/entity-list.css";

Every visual property resolves through an --entity-* custom property with a sensible fallback, named --entity-{component}-{property}, e.g. var(--entity-tile-bg, #ffffff). There are four ways to theme:

1. CSS Variables (recommended)

Override tokens on :root (or any scope):

:root {
  --entity-tile-bg: #fffbe6;
  --entity-tile-border: 1px solid #f0d36b;
  --entity-radius: 0.75rem;
}

Dark mode is built in — set data-theme="dark" on a container or <html>:

<html data-theme="dark">

2. Tailwind

Skip the CSS entirely and pass Tailwind classes via the classNames prop your binding accepts (typed by the ClassNames interfaces exported here):

const classNames: TileClassNames = {
  grid: "grid grid-cols-3 gap-4",
  tile: "rounded-xl border p-4 shadow",
  title: "text-lg font-semibold",
};

3. CSS Modules

import styles from "./Books.module.css";

const classNames: ListClassNames = {
  table: styles.table,
  row: styles.row,
  header: styles.header,
};

4. CSS-in-JS

Generate class names with your styling library and pass them through the same classNames interfaces:

const classNames: DetailClassNames = {
  container: css({ background: "white", padding: 20 }),
  title: css({ fontWeight: 700 }),
};

Development

make install     # install deps
make check-all   # typecheck + lint + test + build
make test        # tests only
make build       # ESM + CJS + d.ts via tsup

License

MIT © Asterios Raptis