An endpoint
How an action asks for something: a named call, what it sends, where the answer goes, and what happens after.
pnpm add @niscorp/nova @niscorp/prism @niscorp/strata zodThe kit
Nova draws nothing itself. It resolves a layout to a tree, and an adapter’s components draw the tree. This file uses the terminal kit, so it runs anywhere Node does; in an app the registry holds your own components.
import { createShell, shellSettled, shellView } from '@niscorp/nova';
import type { ActionDefinition } from '@niscorp/nova';
import { createTtyView } from '@niscorp/nova/adapters/tty';
import { defaultRegistry } from '@niscorp/nova/adapters/tty/components';
import { evaluate } from '@niscorp/prism';
const registry = defaultRegistry();
registry.register('CanvasSlot', ({ children }) => ({ lines: children.flatMap((child) => child.lines) }));An action with an endpoint
An endpoint is declared by name. request is a Prism document that builds the body from the action’s data, response picks what is kept of the answer, and target is where in the data it goes.
A call step invokes it. onSuccess runs after the answer has landed, and onError if it failed, with the failure in scope as @error.
const price: ActionDefinition = {
id: 'price',
data: { seats: 2, total: 0, said: 'Not asked yet' },
layout: {
component: 'Stack',
children: [
{ component: 'Text', children: '{{$.seats}} seats: {{$.total}} euros' },
{ component: 'Text', children: '$.said' },
{ component: 'Button', ref: 'ask', children: 'Ask the price' },
],
},
endpoints: {
quote: {
url: 'https://theatre.example/quote',
method: 'POST',
request: { seats: { $ref: '$.seats' } },
response: { $ref: '$.total' },
target: 'total',
},
},
triggers: [
{
event: 'ui:click',
ref: 'ask',
do: [
{
call: 'quote',
onSuccess: [{ set: 'said', value: 'Quoted' }],
onError: [{ set: 'said', value: '{{@error.message}}' }],
},
],
},
],
};A shell that can reach the world
The document names a URL, and the host decides how it is reached: fetch is the shell’s to give, and transform is what evaluates the request and the response. Here fetch is a stand-in that prices a seat at 45 euros, so the file runs with no server.
const shell = createShell({
canvases: [{ id: 'main' }],
registry,
actions: { price },
transform: evaluate,
fetch: async (_url, init) => new Response(JSON.stringify({ total: JSON.parse(String(init?.body)).seats * 45 })),
});
shell.push('main', 'price');
await shellSettled(shell);
const draw = (): void => console.log(createTtyView(registry, shellView(shell).api).render().text);
draw();Ask
One press. The request is built, the call is made, the answer is written to total, and then onSuccess runs.
shell.dispatch({ type: 'ui:click', ref: 'ask' });
await shellSettled(shell);
draw();Run it.
Before the call and after it. The component that was pressed knows nothing of the endpoint: it said ui:click, and the document did the rest.
npx tsx endpoint.mts── main ──────────────────────────────────────────────────── 2 seats: 0 euros Not asked yet [1] (Ask the price) ── main ──────────────────────────────────────────────────── 2 seats: 90 euros Quoted [1] (Ask the price)