Nova · guide

Canvases, and Back

An app is actions on canvases. Going somewhere puts an action on top of another, and going back takes it off.

Install it
pnpm add @niscorp/nova @niscorp/prism @niscorp/strata zod
Step 1 of 4

The 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.

The kit
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) }));
Step 2 of 4

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.

Two actions that lead to each other
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 }] }],
};
Step 3 of 4

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.

A shell, and what stands on its canvas
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();
Step 4 of 4

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.

Forward, and back
shell.dispatch({ type: 'ui:click', ref: 'seats' });
await shellSettled(shell);
held();

shell.dispatch({ type: 'ui:click', ref: 'back' });
await shellSettled(shell);
held();
Then

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.

Run it
npx tsx canvases.mts
It prints
show
── main ────────────────────────────────────────────────────
The Tempest
[1] (Choose seats)
show › seats
── main ────────────────────────────────────────────────────
Seats for The Tempest
[1] (Back)
show
── main ────────────────────────────────────────────────────
The Tempest
[1] (Choose seats)