Editor SDK

@nuforge/editor is a high-level SDK for building your own visual page builder on top of the runtime. Every operation is undoable — under the hood each method wraps nu.change, so it composes cleanly with the runtime's history. Note: the nuforge site does not host an editor of its own. This SDK is the toolkit you use to build one.

Getting started

Create an editor by wrapping a Nu runtime instance:

import { createEditor, defineBlock, el, text } from '@nuforge/editor';

const editor = createEditor(nu);

Mutations

Every mutation below is undoable. Each is a thin, intention-revealing wrapper around nu.change:

insertNode(parent, child, index?)
moveNode(dragged, target, position) // position is 'before' | 'after' | 'inside'
removeNode(node)
setExpression(node, key, source)
setText(node, value)
setTag(node, tag)
setProp(node, key, expr)
removeProp(node, key)
updateProps(node, map)
addClass(node, name)
removeClass(node, name)
setClassCondition(node, name, source)
setCondition(node, source) // sets @if
clearCondition(node)
setEach(node, listSource, itemName, indexName?) // sets @each
clearEach(node)

Components & state

Manage components and their reactive state. Renaming a component also rewrites every <Name/> reference to it:

addComponent(name)
renameComponent(component, name) // also updates <Name/> references
removeComponent(component)
duplicateComponent(component)
addState(component, name, initSource)
setStateInit(...)
removeState(...)
addComponentProp(component, name, initSource)
setComponentPropInit(...)
removeComponentProp(...)

Queries

Read the current tree without mutating it. These are useful for rendering panels, breadcrumbs, and drag targets:

getComponents()
getComponent(name)
getRootTemplate(component)
getChildren(node)
getSiblings(node)
getNodeIndex(node)
getNodePath(node)

Blocks

The editor keeps a registry of insertable building blocks. Register a block with defineBlock, then insert it into any parent node:

editor.blocks.register(
  defineBlock({
    id: 'button',
    label: 'Button',
    create: () => el('button', {}, [text('Click me')]),
  }),
);

editor.insertBlock('button', parentNode);

Inside a block's create you build template nodes with the helper builders: el(tag, props?, children?) constructs an element node, text(value) constructs a text node, and lit(value) constructs a literal value.

Commands & subscriptions

Register named commands and subscribe to changes so your UI stays in sync:

editor.commands.register({ id, label, run });
editor.commands.run(id);

editor.onChange((entries) => {});
editor.onStructureChange(() => {});

Use editor.onChange for fine-grained updates and editor.onStructureChange when the tree's shape changes (nodes inserted, moved, or removed).

React canvas

The @nuforge/editor/react subpath provides a selectable, style-isolated iframe canvas:

import { IframeCanvas, templateIdFromElement, useStructureRevision } from '@nuforge/editor/react';

<IframeCanvas frame={frame} selectedId={selectedId} onSelect={setSelectedId} />

IframeCanvas renders the frame inside a style-isolated iframe, so the host application's CSS never leaks into the canvas (and vice versa). When the user clicks an element, it maps the clicked DOM element back to its template id with templateIdFromElement and reports it through onSelect. Use useStructureRevision to re-read the canvas when the tree's structure changes.

It's a flexible primitive, not a fixed UI:

  • renderBox — build the overlay yourself from the computed selected / hovered / drop rects and selectedLabel (the selected element's tag). Draw borders, a tag badge, or an action toolbar (set pointerEvents: 'auto' on interactive bits). Omit it for no overlay.
  • srcDoc — supply the iframe's initial document, so its <head> can load web fonts, a CDN stylesheet, <script> tags, etc. The frame is portaled into the <body>.
  • onDropOnNode(targetId, position, event) — enable drop-on-canvas; combine with editor.insertBlockAt(blockId, target, position) to drop palette blocks (the drop rect drives a live indicator).

See the step-by-step builder guide for working code.

Clipboard

Node-level copy / cut / paste / duplicate — all undoable:

editor.copyNode(node); // or editor.cutNode(node) to copy + remove
editor.canPaste(); // true

// paste a fresh copy (new ids) before / after / inside a target
const pasted = editor.pasteNode(target, 'after');

// duplicate a node right after itself
const copy = editor.duplicateNode(node);

pasteNode and duplicateNode return the inserted node (with fresh ids), so you can immediately select it.

Canvas drag-and-drop

dropPositionFromPointer turns a pointer position into a before / after / inside drop position against an element's rect — the reusable core of canvas DnD. Use it to drive a drop indicator and the final mutation:

import { dropPositionFromPointer } from '@nuforge/editor';

function onDragOver(e: React.DragEvent, targetEl: HTMLElement) {
  const rect = targetEl.getBoundingClientRect();
  const position = dropPositionFromPointer(e.clientY, rect, { allowInside: true });
  // → highlight the drop indicator for `position`
}

function onDrop(draggedNode, targetNode, position) {
  editor.moveNode(draggedNode, targetNode, position); // or editor.insertBlock(id, targetNode)
}

Schema-driven inspector

A block declares its editable props as a schema (props: PropSchemaField[]). <PropFields> renders a control per field (text / number / checkbox / select / expression), so you don't hand-write a form per block:

import { PropFields } from '@nuforge/editor/react';

<PropFields
  fields={block.props ?? []}
  values={{ label: 'Click me', disabled: false }}
  onChange={(name, value) => editor.setProp(node, name, t.literal({ value }))}
/>;

PropFields is headless — style it with className and your own CSS, and wire onChange to editor.setProp / setExpression.

Next steps

  • Runtime — the reactive core the editor mutates.
  • Codegen — turn the edited AST into shippable code.