{"_id":"@astrapi69/entity-kit-core","name":"@astrapi69/entity-kit-core","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@astrapi69/entity-kit-core","version":"0.1.0","description":"Framework-agnostic core types, utilities and design tokens for the entity-kit BeanInfo pattern","type":"module","main":"dist/index.cjs","module":"dist/index.js","types":"dist/index.d.ts","license":"MIT","author":{"name":"Asterios Raptis"},"sideEffects":["**/*.css"],"repository":{"type":"git","url":"git+https://github.com/astrapi69/entity-kit-core.git"},"keywords":["entity","descriptor","beaninfo","headless","typescript","design-tokens","framework-agnostic"],"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./styles":"./styles/index.css","./styles/*":"./styles/*","./package.json":"./package.json"},"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","typecheck":"tsc --noEmit","lint":"eslint src/","lint:fix":"eslint src/ --fix","prepublishOnly":"npm run build"},"publishConfig":{"access":"public"},"peerDependencies":{},"devDependencies":{"@eslint/js":"^10.0.1","@vitest/coverage-v8":"^4.1.8","eslint":"^10.4.1","tsup":"^8.5.1","typescript":"^6.0.3","typescript-eslint":"^8.60.1","vitest":"^4.1.8"},"gitHead":"674d226bd05cc294894f4fa1701ed6243320c157","_id":"@astrapi69/entity-kit-core@0.1.0","bugs":{"url":"https://github.com/astrapi69/entity-kit-core/issues"},"homepage":"https://github.com/astrapi69/entity-kit-core#readme","_nodeVersion":"24.15.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-zM6sNnBPde6Q04qbkeJaYmixqzcfkGKndem753p0Tc6lkBUtONiKPoY9hSjW4fji1BU8srEHkCXk2AqfbzCH4w==","shasum":"3d80800b039f3b7b2f46c675df68a0c74e486c50","tarball":"https://registry.npmjs.org/@astrapi69/entity-kit-core/-/entity-kit-core-0.1.0.tgz","fileCount":16,"unpackedSize":98158,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIQCBHiXksyYzDZwfNjwAjWVZjaIoxn+BI4gQJl0KebLzUAIfP+vdDuDXWIdQD08ZMVkq2+jAovXgFj+NieMPRe43pg=="}]},"_npmUser":{"name":"astrapi69","email":"asterios.raptis@gmx.net"},"directories":{},"maintainers":[{"name":"astrapi69","email":"asterios.raptis@gmx.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/entity-kit-core_0.1.0_1781002823825_0.2679028538832282"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-09T11:00:23.607Z","0.1.0":"2026-06-09T11:00:23.984Z","modified":"2026-06-09T11:00:24.170Z"},"maintainers":[{"name":"astrapi69","email":"asterios.raptis@gmx.net"}],"description":"Framework-agnostic core types, utilities and design tokens for the entity-kit BeanInfo pattern","homepage":"https://github.com/astrapi69/entity-kit-core#readme","keywords":["entity","descriptor","beaninfo","headless","typescript","design-tokens","framework-agnostic"],"repository":{"type":"git","url":"git+https://github.com/astrapi69/entity-kit-core.git"},"author":{"name":"Asterios Raptis"},"bugs":{"url":"https://github.com/astrapi69/entity-kit-core/issues"},"license":"MIT","readme":"# @astrapi69/entity-kit-core\n\n**Framework-agnostic core** for the entity-kit **BeanInfo pattern**: the types,\nregistry, pure utilities and design tokens that let any object type describe\nitself once and be rendered, sorted, searched and acted on by generic UI — in\nany framework.\n\nThis package contains **zero React, zero JSX, zero UI framework code**. It is\npure TypeScript plus a set of optional CSS design tokens. Framework bindings\nbuild on top of it:\n\n| Package | Role |\n| --- | --- |\n| **`@astrapi69/entity-kit-core`** (this) | Framework-agnostic types, registry, utils, CSS tokens |\n| [`@astrapi69/entity-kit`](https://github.com/astrapi69/entity-kit) | React components & hooks built on the core |\n| `@astrapi69/entity-kit-vue` *(future)* | Vue bindings built on the core |\n\nThe idea is BeanInfo, from the Java Beans world: an `EntityDescriptor<T>`\ndeclares how to identify, label, list, search, sort and act on items of `T`.\nGeneric components consume the descriptor and never need to know the concrete\ntype.\n\n## Install\n\n```bash\nnpm install @astrapi69/entity-kit-core\n```\n\nNo peer dependencies — nothing to add.\n\n## Quick start\n\n```ts\nimport {\n  type EntityDescriptor,\n  searchEntities,\n  sortEntities,\n  resolveLabel,\n  descriptorRegistry,\n} from \"@astrapi69/entity-kit-core\";\n\ninterface Book {\n  id: string;\n  title: string;\n  author: string;\n  year: number;\n  deleted: boolean;\n}\n\n// 1. Describe the entity once.\nconst bookDescriptor: EntityDescriptor<Book> = {\n  entityName: \"book\",\n  displayName: (b) => b.title,\n  getId: (b) => b.id,\n  isDeleted: (b) => b.deleted,\n  listFields: [\n    { key: \"title\", label: () => t(\"book.title\"), sortable: true }, // i18n label\n    { key: \"author\", label: \"Author\", sortable: true },\n    { key: \"year\", label: \"Year\", sortable: true },\n  ],\n  searchableFields: [\"title\", \"author\"],\n};\n\n// 2. Register it (optional — for decoupled lookup by name).\ndescriptorRegistry.register(bookDescriptor);\n\n// 3. Use the pure utilities.\nconst filtered = searchEntities(books, bookDescriptor, \"tolkien\");\nconst sorted = sortEntities(filtered, bookDescriptor, \"year\", \"desc\");\nconst header = resolveLabel(bookDescriptor.listFields[0].label);\n```\n\n## Concepts\n\n### Descriptors\n\nA descriptor's **required** members are `entityName`, `displayName`, `getId`,\n`listFields` and `isDeleted`. Everything else is optional and defaults are\ndocumented below. Use `withDescriptorDefaults` to materialise a descriptor with\nall optional fields filled in:\n\n```ts\nimport { withDescriptorDefaults } from \"@astrapi69/entity-kit-core\";\n\nconst resolved = withDescriptorDefaults(bookDescriptor);\nresolved.detailFields;     // [] when omitted\nresolved.actions;          // [] when omitted\nresolved.searchableFields; // [] when omitted\nresolved.shortDescription; // () => \"\" when omitted\n```\n\n| Optional field | Default |\n| --- | --- |\n| `shortDescription` | `() => \"\"` |\n| `detailFields` | `[]` |\n| `searchableFields` | `[]` |\n| `actions` | `[]` |\n| `thumbnail` | `undefined` |\n| `deletedAt` | `undefined` |\n| `icon` | `undefined` |\n\n### The `Node` type parameter (renderable nodes)\n\n`EntityDescriptor`, `FieldDescriptor` and `ActionDescriptor` take an optional\nsecond type parameter `Node` for renderable things (`icon`, `thumbnail`,\n`render`). It defaults to `unknown`, keeping the core framework-agnostic. A\nbinding supplies its own node type:\n\n```ts\n// Inside the React binding:\nimport type { ReactNode } from \"react\";\nimport type { EntityDescriptor as CoreDescriptor } from \"@astrapi69/entity-kit-core\";\n\nexport type EntityDescriptor<T> = CoreDescriptor<T, ReactNode>;\n```\n\n### i18n labels\n\n`FieldDescriptor.label` and `ActionDescriptor.label` are typed as\n`string | (() => string)` (`LabelValue`). The factory form lets you wire up\ni18n. Resolve it with `resolveLabel`, which **invokes the function on every\ncall** — never caches — so labels always reflect the active locale:\n\n```ts\nresolveLabel(\"Author\");          // \"Author\"\nresolveLabel(() => t(\"author\")); // current translation, re-evaluated each call\n```\n\n### Registry\n\n`DescriptorRegistry` is a `Map`-keyed store of descriptors by `entityName`,\ndecoupling rendering code from definitions. A shared `descriptorRegistry`\ninstance is exported for apps that need only one.\n\n```ts\nregistry.register(bookDescriptor);\nregistry.get<Book>(\"book\");     // throws if not registered\nregistry.tryGet<Book>(\"book\");  // undefined if not registered\nregistry.has(\"book\");\nregistry.list();                // all descriptors, insertion order\nregistry.names();               // all entity names\nregistry.unregister(\"book\");\nregistry.clear();\n```\n\n### Soft delete / trash\n\n`isDeleted(item)` drives trash views; `deletedAt(item)` optionally supplies a\ntimestamp. The `TrashViewOptions.prefiltered` flag (defined here, consumed by\nbindings) tells a trash view that the items are already the trashed set, so the\ninternal `isDeleted` filter is skipped.\n\n## API reference\n\n### Types\n\n- `EntityDescriptor<T, Node = unknown>` — the self-description of an entity type.\n- `FieldDescriptor<T, Node = unknown>` — a single field/column.\n- `ActionDescriptor<T, Node = unknown>` — a single action.\n- `RequiredEntityDescriptor<T, Node = unknown>` — the required members plus\n  optional rest (input to `withDescriptorDefaults`).\n- `ResolvedEntityDescriptor<T, Node = unknown>` — a descriptor with all\n  optional fields filled (output of `withDescriptorDefaults`).\n- `LabelValue` — `string | (() => string)`.\n- `ActionVariant` — `\"default\" | \"danger\"`.\n- `ViewMode` — `\"list\" | \"tile\" | \"detail\"`.\n- `TrashViewOptions` — `{ prefiltered?: boolean }`.\n- ClassNames interfaces: `TileClassNames`, `ListClassNames`, `DetailClassNames`,\n  `TrashClassNames`, `SearchClassNames`, `ViewSwitcherClassNames`,\n  `EmptyStateClassNames`, `ActionsClassNames`.\n\n### Functions\n\n- `resolveLabel(label: LabelValue): string`\n- `searchEntities<T>(items, descriptor, query): T[]` — case-insensitive\n  substring filter over `searchableFields`. Pure; returns a new array.\n- `toSearchString(value: unknown): string` — the coercion used by search.\n- `sortEntities<T>(items, descriptor, key, direction?): T[]` — stable sort by a\n  **sortable** list field; non-sortable keys return an unchanged copy. Pure.\n- `isSortable<T>(descriptor, key): boolean`\n- `generateTestId(entityName, id, actionId?): string` — `\"book-42\"` or\n  `\"book-42-delete\"`.\n- `generateSearchTestId(entityName): string` — `\"book-search\"`.\n- `generateViewTestId(mode): string` — `\"view-list\"`.\n- `withDescriptorDefaults<T>(descriptor): ResolvedEntityDescriptor<T>`\n\n### Registry\n\n- `class DescriptorRegistry`\n- `const descriptorRegistry: DescriptorRegistry`\n\n## Styling & theming\n\nThe styles live in this package and are entirely optional — bindings render\nfine with no stylesheet at all (headless-first). Import the full default theme:\n\n```ts\nimport \"@astrapi69/entity-kit-core/styles\";\n```\n\nOr import only the component sheets you need:\n\n```ts\nimport \"@astrapi69/entity-kit-core/styles/entity-tile.css\";\nimport \"@astrapi69/entity-kit-core/styles/entity-list.css\";\n```\n\nEvery visual property resolves through an `--entity-*` custom property with a\nsensible fallback, named `--entity-{component}-{property}`, e.g.\n`var(--entity-tile-bg, #ffffff)`. There are four ways to theme:\n\n### 1. CSS Variables (recommended)\n\nOverride tokens on `:root` (or any scope):\n\n```css\n:root {\n  --entity-tile-bg: #fffbe6;\n  --entity-tile-border: 1px solid #f0d36b;\n  --entity-radius: 0.75rem;\n}\n```\n\nDark mode is built in — set `data-theme=\"dark\"` on a container or `<html>`:\n\n```html\n<html data-theme=\"dark\">\n```\n\n### 2. Tailwind\n\nSkip the CSS entirely and pass Tailwind classes via the `classNames` prop your\nbinding accepts (typed by the ClassNames interfaces exported here):\n\n```ts\nconst classNames: TileClassNames = {\n  grid: \"grid grid-cols-3 gap-4\",\n  tile: \"rounded-xl border p-4 shadow\",\n  title: \"text-lg font-semibold\",\n};\n```\n\n### 3. CSS Modules\n\n```ts\nimport styles from \"./Books.module.css\";\n\nconst classNames: ListClassNames = {\n  table: styles.table,\n  row: styles.row,\n  header: styles.header,\n};\n```\n\n### 4. CSS-in-JS\n\nGenerate class names with your styling library and pass them through the same\n`classNames` interfaces:\n\n```ts\nconst classNames: DetailClassNames = {\n  container: css({ background: \"white\", padding: 20 }),\n  title: css({ fontWeight: 700 }),\n};\n```\n\n## Development\n\n```bash\nmake install     # install deps\nmake check-all   # typecheck + lint + test + build\nmake test        # tests only\nmake build       # ESM + CJS + d.ts via tsup\n```\n\n## License\n\nMIT © Asterios Raptis\n","readmeFilename":"README.md","_rev":"1-853a292fb22e1fa4adc28b4b0ef1e4b3"}