Canvases, and Back
An app is actions on canvases. Going somewhere puts an action on top of another, and going back takes it off.
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';
const registry = defaultRegistry();
registry.register('CanvasSlot', ({ children }) => ({ lines: children.flatMap((child) => child.lines) }));Two actions that lead to each other
There are no screens and no routes in Nova. A canvas holds a stack of actions, and the one on top is what is drawn.
Moving is a step too, an effect this time. push puts an action on top of the one that asked, with input to open it with. pop: true takes the asking action off again.
const show: ActionDefinition = {
id: 'show',
data: { title: 'The Tempest' },
layout: {
component: 'Stack',
children: [
{ component: 'Text', children: '$.title' },
{ component: 'Button', ref: 'seats', children: 'Choose seats' },
],
},
triggers: [{ event: 'ui:click', ref: 'seats', do: [{ push: { action: 'seats', input: { title: '$.title' } } }] }],
};
const seats: ActionDefinition = {
id: 'seats',
data: { title: '' },
layout: {
component: 'Stack',
children: [
{ component: 'Text', children: 'Seats for {{$.title}}' },
{ component: 'Button', ref: 'back', children: 'Back' },
],
},
triggers: [{ event: 'ui:click', ref: 'back', do: [{ pop: true }] }],
};A shell, and what stands on its canvas
The shell’s state says what every canvas holds, bottom to top. held prints that for main, and then the screen.
const shell = createShell({ canvases: [{ id: 'main' }], registry, actions: { show, seats } });
shell.push('main', 'show');
await shellSettled(shell);
const held = (): void => {
console.log(shell.getState().canvases['main']?.stack.map((instance) => instance.definitionId).join(' › '));
console.log(createTtyView(registry, shellView(shell).api).render().text);
};
held();Forward, and back
Choosing seats does not replace the show. It puts another action on top of it, and the show underneath is suspended, not gone. That is why Back needs no code: there is something to go back to.
shell.dispatch({ type: 'ui:click', ref: 'seats' });
await shellSettled(shell);
held();
shell.dispatch({ type: 'ui:click', ref: 'back' });
await shellSettled(shell);
held();Run it.
The stack three times: the show alone, the seats on top of it, and the show alone again, exactly as it was left.
npx tsx canvases.mtsshow ── main ──────────────────────────────────────────────────── The Tempest [1] (Choose seats) show › seats ── main ──────────────────────────────────────────────────── Seats for The Tempest [1] (Back) show ── main ──────────────────────────────────────────────────── The Tempest [1] (Choose seats)