@viewkitjs/fields
v0.2.0
Published
ViewKit fields — typed field definitions, validation, acceptance and query evaluation for record lists. By Sheaf.
Downloads
105
Readme
ViewKit fields
Typed field definitions, validation, acceptance and query evaluation for record lists. By Sheaf.
A field is what a record's value is: its type, its vocabulary, what it may hold, how it presents, and who may change it. You define it once, and every surface that meets the value — a grid cell, a form, a filter, a saved view — reads the same definition. Pure TypeScript: no React, no DOM, and one peer dependency, zod 4.
Install
pnpm add @viewkitjs/fields zodESM only. Node 20 or later. zod ^4.1.12 is a peer dependency.
Define a registry
import { defineFields, zodFor, acceptFieldCommit } from "@viewkitjs/fields";
export const DEAL_FIELDS = defineFields({
entity: "deal",
identity: "title",
fields: {
title: { type: "text", label: "Title", required: true },
value: { type: "currency", label: "Value", code: { fixed: "EUR" } },
stage: {
type: "status",
label: "Stage",
options: [
{ value: "new", label: "New", slot: "primary" },
{ value: "won", label: "Won", slot: "success" },
{ value: "lost", label: "Lost", slot: "error" },
],
},
},
});
// The zod schema for one field's stored value.
const title = zodFor(DEAL_FIELDS.title);
// What an editor committed, accepted or refused — never thrown.
const result = acceptFieldCommit(DEAL_FIELDS.title, " ");
// { ok: false, reason: "…", message: { key: "field.cannot-empty" } }defineFields stamps each definition with its key and entity, freezes the registry, and refuses an invalid definition when the module loads, not at the first write. Eighteen types: text, number, percent, currency, date, datetime, boolean, select, status, multiselect, relation, user, url, email, phone, location, rollup and formula.
The library does no money arithmetic: a host converts inside its own reader and declares codeOf so the library knows what currency the number it read is in. A currency column whose rows carry different codes sorts clustered by code, then by amount inside a code. A rollup or formula may declare code: { perCode: true } for one amount per currency, which the host computes — the library holds the shape, never the sum. And storage: "decimal" on a number, percent or currency field stores exact decimal text ("120.50") instead of a JavaScript number, for a column where no digit may be lost to a binary float.
State your conventions once
The library never reads the runtime's locale or time zone. A host states its conventions once and adds the per-request part with .with(). A host that states nothing gets fixed neutral defaults — en-GB, UTC, both decimal styles read, any non-empty id, English messages — the same on a server and in a browser.
import { z } from "zod";
import { createFieldKit, defineConventions, resolveFieldMessage } from "@viewkitjs/fields";
export const conventions = defineConventions({
timeZone: "Europe/Bucharest",
decimal: ",",
ids: { user: z.cuid() },
});
export function kitFor(locale: string, translate: (key: string) => string | undefined) {
return createFieldKit(
conventions.with({
locale,
messages: (message) => translate(message.key) ?? resolveFieldMessage(message),
})
);
}
kitFor("ro", () => undefined).parseNumber("82,3"); // 82.3 — and never 823Every read-time function takes the conventions last, or inside its options bag. A kit method is the bare function of the same name with the conventions filled in.
Filter, sort and group in memory
import { evaluateView } from "@viewkitjs/fields/query";
type Deal = { title: string; value: number; stage: string };
type DealKey = keyof typeof DEAL_FIELDS;
const { rows, buckets } = evaluateView(
deals,
{
filter: { op: "and", items: [{ fieldKey: "title", operator: "contains", value: "stefan" }] },
sorts: [{ fieldKey: "title", direction: "asc" }],
group: null,
},
{
// A key the registry does not know is skipped, never an error.
fieldOf: (key) => (key in DEAL_FIELDS ? DEAL_FIELDS[key as DealKey] : undefined),
read: (deal: Deal, key) => deal[key as keyof Deal],
},
{ conventions }
);Text filters ignore case and diacritics, text sorts by the host's locale, and an instant lands on the host's calendar day.
Entry points
| Import | Holds |
| --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| @viewkitjs/fields | Definitions, registries, conventions, validation, acceptance, codecs, relation helpers, the field kit |
| @viewkitjs/fields/query | Filters, sorts, grouping, evaluateView, the query kit |
| @viewkitjs/fields/types | The types, for module augmentation — narrowing entity, relation and provider names to your own |
| @viewkitjs/fields/design/status-slots, /design/stage-marks, /design/category-colors, /format/time | Presentation leaves: Tailwind class names keyed by semantic slot, and duration and time formatting. Your stylesheet defines the tokens |
There are no other subpaths.
For coding agents
The package ships a usage skill at skills/viewkit/SKILL.md. Its examples are compiled against the published artifact on every release.
Status
0.x — the API may change between minor versions.
Licence
Proprietary. See LICENSE.md.
