@niscorp/loom
v0.2.2
Published
Schema-driven editors that run on Nova — turn any schema into an editing UI
Readme
@niscorp/loom
Loom builds editing UIs from schemas. Give it a Zod schema and it produces a form that views, creates, and edits JSON matching that schema. The form is built from the schema's structure — nested objects become nested sections, arrays get add and remove controls, unions become a type picker plus the fields for the chosen type — so the form has a control for every field the schema describes. It does not validate for you: a new document, and a document mid-edit, can fail the schema, so parse it before you keep it. There is no hand-written form code; a shape Loom does not model gets a raw JSON box.
The forms render on Nova, this stack's layout engine. Loom compiles a schema into a Nova layout; Nova draws it and routes each edit back into the data.
Install
pnpm add @niscorp/loom @niscorp/nova zod@niscorp/nova is required. The React surface (@niscorp/loom/react) also needs
react. Domain plugins each need their own library — @niscorp/vex for the Vex
plugin, @niscorp/prism for the Prism plugin. All three are optional peers of Loom: a consumer that only
uses the compiler needs none of them for Loom itself. (Nova has required peers
of its own — @niscorp/prism and @niscorp/strata — which pnpm installs with
it.)
Two ways to use Loom
The compiler — turn a schema into a Nova editor
parse reads a schema into a field model. toNova turns that model into a Nova
editor: an action that holds the data being edited, plus the layouts that render
it.
import { parse, toNova } from '@niscorp/loom';
import { z } from 'zod';
const schema = z.object({
name: z.string(),
age: z.number().int(),
tags: z.array(z.string()),
});
const { action, layouts } = toNova(parse(schema));
// `action.data` is the document the form starts from; hand `action` and
// `layouts` to a Nova runtime to render the form. Edits go to the runtime's
// copy (`shell.getRuntime(id).getData()`), not to `action.data`, and the
// document can fail `schema` at any point: parse it before you keep it.A new document starts from each field's empty value: '', 0, false, [],
the schema's default where it has one, and null for a kind Loom does not
model. A required enum with no default is left out. These can fail the schema
('' is not an email, 0 is not min(1), null is not a record), and
optional fields get them too. toNova(model, { empty, includeOptional })
changes both: empty supplies a kind's starting value, includeOptional: false
leaves optional fields out.
This half is headless — no React, no DOM. Use it when you have your own Nova host or only need the compiled output.
The editor — a ready-made React surface
<LoomEditor> is the integrated surface. It loads a set of plugins, compiles
each plugin's schemas into forms, and renders everything as one Nova shell: the
forms on the left, a live preview and JSON panes alongside.
import { LoomEditor, defaultPlugins } from '@niscorp/loom/react';
import { prism } from '@niscorp/loom/plugins/prism/react';
<LoomEditor
plugins={[...defaultPlugins(), prism({ input })]}
artifact={{ type: 'prism', documents: { config } }}
onChange={(documents) => console.log(documents)}
/>;pluginsis the loaded set. They load in order, and a later plugin can override or remove an earlier one.defaultPlugins()adds the Data and Validations JSON panes — spread it first so domain plugins load after it.artifactsays what to edit.typenames the plugin that handles it;documentsseeds the starting values (omit it to start from the schema's defaults).onChangefires with the live document values on every edit.- To switch to a different artifact, give the element a new React
key.
Two things the built-in kit draws are worth knowing before you rely on them:
- Messages follow edits. The inline messages and the Validations pane show
what the schema said after the last edit. They are empty until the first one,
a problem with no field path (a
.refine()on the whole object, without apath) is shown nowhere, and a problem in a list row shows in the Validations pane only. They are not the verdict on a document; the schema is (DESIGN.md, "Validation"). - A select has no row for "none". An enum with no
.default()starts with no value in the document, and the select displays its first option all the same. Give the enum a.default()and the two start out agreeing.
A plugin wires one domain into the editor. It contributes the schemas to edit (its documents), optional custom field widgets, and usually a preview. The three reference plugins:
prism({ input })— edit a Prism transform config; the preview applies it toinputand shows the output.vex({ run, db })— edit a Vex query; the preview runs it withrunand shows the rows. Passdbto get column-aware field pickers.nova({ manifest })— edit a Nova layout built from the components the manifest lists (each{ name, props, container?, render },propsa Zod schema); the preview renders the layout with them.
Entry points
| Import | What it gives you |
|---|---|
| @niscorp/loom | The compiler (parse, toNova), the editor controller (createLoomEditor), and the types. Headless. |
| @niscorp/loom/react | <LoomEditor>, defaultPlugins(), and the widget kit. |
| @niscorp/loom/plugins/{vex,nova,prism} | A domain plugin's framework-free core. |
| @niscorp/loom/plugins/{vex,nova,prism}/react | A domain plugin's React surface (preview and widget components). |
Documentation
- DESIGN.md — how Loom is built and why. Read it before the source.
License
Apache-2.0
