Nova · guide

A fragment

Chrome that is written once and put around any action when it is opened: a frame, a title, a way to close.

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, ActionFragment } 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

A frame, and something to put in it

A fragment is a partial action: a layout, and the triggers that go with it. Its layout has a slot, and that is where the action it is put around lands.

It exists only merged. The frame here brings a panel, a Close button and the trigger that answers it. The action inside knows nothing of any of that.

A frame, and something to put in it
const frame: ActionFragment = {
  kind: 'fragment',
  id: 'frame',
  layout: {
    component: 'Panel',
    props: { title: 'Box office' },
    children: [{ slot: 'body' }, { component: 'Button', ref: 'close', children: 'Close' }],
  },
  triggers: [{ event: 'ui:click', ref: 'close', do: [{ pop: true }] }],
};
const ticket: ActionDefinition = {
  id: 'ticket',
  data: { seat: 'C4' },
  layout: { component: 'Text', children: 'Seat {{$.seat}}' },
};
Step 3 of 4

Open the action in the frame

The shell is told what fragments there are, and the action is opened with the one it should wear. A step does the same with with: ['frame'].

Open the action in the frame
const shell = createShell({ canvases: [{ id: 'main' }], registry, actions: { ticket }, fragments: { frame } });
shell.push('main', 'ticket', undefined, ['frame']);
await shellSettled(shell);
const draw = (): void => console.log(createTtyView(registry, shellView(shell).api).render().text);
draw();
Step 4 of 4

Close it

The press is answered by the fragment’s trigger, which is now part of the action. The action is popped, and the canvas is empty.

Close it
shell.dispatch({ type: 'ui:click', ref: 'close' });
await shellSettled(shell);
console.log(shell.getState().canvases['main']?.stack.length, 'on the canvas');
Then

Run it.

The ticket in its frame, and then nothing on the canvas. A fragment is data, so it can be stored and named instead of repeated.

Run it
npx tsx fragment.mts
It prints
── main ────────────────────────────────────────────────────
┌─ Box office ──────────────────────────────────
│ Seat C4
│ [1] (Close)
0 on the canvas