A fragment
Chrome that is written once and put around any action when it is opened: a frame, a title, a way to close.
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, 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) }));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.
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}}' },
};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'].
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();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.
shell.dispatch({ type: 'ui:click', ref: 'close' });
await shellSettled(shell);
console.log(shell.getState().canvases['main']?.stack.length, 'on the canvas');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.
npx tsx fragment.mts── main ──────────────────────────────────────────────────── ┌─ Box office ────────────────────────────────── │ Seat C4 │ [1] (Close) 0 on the canvas