SDK Editor

@nuforge/editor adalah SDK tingkat tinggi untuk membangun page builder visual Anda sendiri di atas runtime. Setiap operasi dapat di-undo — di balik layar tiap metode membungkus nu.change, sehingga menyatu rapi dengan history runtime. Catatan: situs nuforge tidak menyediakan editor bawaan. SDK ini adalah perangkat yang Anda gunakan untuk membangunnya sendiri.

Memulai

Buat sebuah editor dengan membungkus instance runtime Nu:

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

const editor = createEditor(nu);

Mutasi

Setiap mutasi di bawah ini dapat di-undo. Masing-masing adalah pembungkus tipis dan eksplisit di atas 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)

Komponen & state

Kelola komponen dan state reaktifnya. Mengganti nama komponen juga menulis ulang setiap referensi <Name/> yang menunjuk ke komponen itu:

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(...)

Kueri

Baca tree saat ini tanpa mengubahnya. Ini berguna untuk merender panel, breadcrumb, dan target drag:

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

Block

Editor menyimpan registry block bangunan yang dapat disisipkan. Daftarkan sebuah block dengan defineBlock, lalu sisipkan ke node induk mana pun:

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

editor.insertBlock('button', parentNode);

Di dalam create sebuah block, Anda membangun node template dengan helper builder: el(tag, props?, children?) membuat node elemen, text(value) membuat node teks, dan lit(value) membuat sebuah nilai literal.

Command & subscription

Daftarkan command bernama dan berlangganan perubahan agar UI Anda tetap sinkron:

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

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

Gunakan editor.onChange untuk pembaruan berbutir halus dan editor.onStructureChange ketika bentuk tree berubah (node disisipkan, dipindah, atau dihapus).

Canvas React

Subpath @nuforge/editor/react menyediakan canvas iframe yang dapat dipilih dan terisolasi gaya:

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

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

IframeCanvas merender frame di dalam iframe yang terisolasi gaya, sehingga CSS aplikasi host tidak pernah bocor ke canvas (dan sebaliknya). Ketika pengguna mengklik sebuah elemen, ia memetakan elemen DOM yang diklik kembali ke template id-nya dengan templateIdFromElement dan melaporkannya melalui onSelect. Gunakan useStructureRevision untuk membaca ulang canvas ketika struktur tree berubah.

Ini primitif yang fleksibel, bukan UI tetap:

  • renderBox — bangun overlay sendiri dari rect selected / hovered / drop dan selectedLabel (tag elemen terpilih). Gambar border, badge tag, atau toolbar aksi (set pointerEvents: 'auto' pada bagian interaktif). Kosongkan untuk tanpa overlay.
  • srcDoc — sediakan dokumen awal iframe, sehingga <head>-nya bisa memuat web font, stylesheet CDN, tag <script>, dll. Frame di-portal ke <body>.
  • onDropOnNode(targetId, position, event) — aktifkan drop-ke-canvas; padukan dengan editor.insertBlockAt(blockId, target, position) untuk menjatuhkan block palette (rect drop menggerakkan indikator live).

Lihat panduan builder bertahap untuk kode lengkapnya.

Komponen eksternal didekorasi dengan cara yang sama seperti tag biasa: elemen root-nya juga membawa data-nu-id, jadi mereka independen dapat dipilih dan disorot di canvas seperti node lainnya — tidak perlu penanganan khusus di sisi host.

useCanvasWindow()

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

const { doc, window } = useCanvasWindow();

Meresolusi window/document asli tempat sebuah elemen yang di-render di canvas benar-benar berada. IframeCanvas mem-portal kontennya ke dalam <iframe>, tapi portal cuma memindahkan node DOM — JavaScript yang membangunnya tetap berjalan di realm halaman host, jadi library yang membaca global window/document polos (scroll listener, matchMedia, default ResizeObserver/IntersectionObserver) akan diam-diam mengamati halaman host, bukan iframe yang sebenarnya discroll pengguna. Panggil dari dalam komponen eksternal untuk mendapat realm yang benar terlepas dari jalur render-nya — di dalam IframeCanvas hasilnya { doc, window } milik iframe sendiri; di tempat lain (mis. <NuFrame/> langsung tanpa iframe) hasilnya cuma global ambient. Lihat Externals → komponen eksternal berbasis hook untuk contoh lengkapnya.

RemoteCanvasHost

IframeCanvas mem-portal frame ke DOM iframe, tapi JavaScript yang membangunnya tetap berjalan di realm aplikasi Anda — useCanvasWindow() memperbaiki separuh masalah window/document untuk kode Anda sendiri, tapi tidak bisa memperbaiki library pihak ketiga yang internalnya membaca global bare di module-scope tanpa jalur yang bisa dialihkan (contohnya internal scroll-linked milik sebuah library animasi). Untuk kelas masalah itu, RemoteCanvasHost merender iframe yang benar-benar navigasi src — halaman terpisah sungguhan dengan realm JS-nya sendiri, jadi setiap modul (React, library pihak ketiga, kode Anda sendiri) dievaluasi ulang segar terhadap global milik canvas sendiri:

// halaman host
import { RemoteCanvasHost } from '@nuforge/editor/react';

<RemoteCanvasHost
  nu={nu}
  activeComponent="App"
  src="/canvas/my-project"
  onSelect={setSelectedId}
  onDropOnNode={(targetId, position) => editor.moveNode(dragged, targetId, position)}
/>;
// route /canvas/my-project, bundle-nya sendiri
import { createCanvasReceiver } from '@nuforge/editor';
import { NuProvider, NuFrame } from '@nuforge/react';

const receiver = createCanvasReceiver({ src: '/canvas/my-project', externals });
receiver.postReady();
// receiver.nu / receiver.volatile.activeComponent memberi Frame + <NuFrame/>

Karena navigasi sungguhan menempatkan canvas di realm terpisah, tidak ada identitas objek yang bisa dibagi untuk di-render — state mengalir satu arah, host → canvas, lewat BroadcastChannel sebagai JSON hasil t.flatten, dan Nu milik canvas adalah replica yang di-merge-patch (t.merge, primitif yang sama dipakai @nuforge/collaboration untuk binding CRDT-nya — tidak perlu CRDT di sini, karena cuma ada satu penulis). Interaksi (select/hover/drop/keydown/link-click) di-relay arah sebaliknya sebagai pesan kecil bertipe, bukan listener DOM native yang melintasi boundary.

nu milik canvas tetap Nu sungguhan yang loaded dan mutable — handler onClick DSL yang dirender darinya (mis. toggle "preview mode" live) tetap jalan dan memutasinya secara lokal seperti Nu lain mana pun. Ini disengaja: mutasi itu memang tidak pernah di-relay balik, jadi init/patch berikutnya dari host akan menimpanya. Paksa init penuh yang segar (bukan cuma patch inkremental) di setiap transisi activeComponent atau mode-preview supaya mutasi lokal-saja tidak pernah tertinggal begitu kembali ke mode edit.

Clipboard

Copy / cut / paste / duplicate tingkat-node — semuanya undoable:

editor.copyNode(node); // atau editor.cutNode(node) untuk copy + hapus
editor.canPaste(); // true

// tempel salinan baru (id baru) sebelum / sesudah / di dalam target
const pasted = editor.pasteNode(target, 'after');

// duplikasi node tepat setelah dirinya
const copy = editor.duplicateNode(node);

pasteNode dan duplicateNode mengembalikan node yang disisipkan (dengan id baru), jadi Anda bisa langsung menyeleksinya.

Drag-and-drop di canvas

dropPositionFromPointer mengubah posisi pointer menjadi posisi drop before / after / inside terhadap rect elemen — inti reusable dari DnD canvas. Pakai untuk menggerakkan indikator drop dan mutasi akhirnya:

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

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

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

Inspector berbasis schema

Sebuah block mendeklarasikan prop yang dapat disunting sebagai schema (props: PropSchemaField[]). <PropFields> me-render satu kontrol per field (text / number / checkbox / select / expression), jadi Anda tidak perlu menulis form manual 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 bersifat headless — beri gaya lewat className dan CSS Anda sendiri, lalu sambungkan onChange ke editor.setProp / setExpression.

Memvalidasi mutasi programatik (mis. edit yang digerakkan AI)

Seorang manusia yang menyunting lewat UI cuma bisa menghasilkan mutasi yang bisa diekspresikan UI itu sendiri — sebuah AI (atau caller programatik lain yang membangun expression dari teks bebas, seperti tool-call yang mereferensikan identifier yang mungkin tidak ada) bisa menghasilkan mutasi yang secara struktural valid tapi throw saat benar-benar di-render: referensi ke val, prop, atau external yang tidak ada.

Evaluator Nu sudah mengubah throw saat render menjadi node t.ErrorSystemView menggantikan subtree yang gagal, bukan meng-crash seluruh tree (computeTag menangkap per-elemen) — jadi setelah mutasi apa pun yang menyisipkan expression baru ke live tree (event handler, binding, node yang baru disisipkan — tidak pernah dibutuhkan untuk mutasi literal murni seperti setProp dengan nilai literal, moveNode, atau setClassList, karena literal tidak bisa mereferensikan identifier yang tidak ada), jalankan Frame.view hasilnya untuk mencari node ErrorSystemView dan rollback dengan nu.undo() kalau ketemu — supaya caller tidak pernah meninggalkan node yang terlihat rusak di canvas tanpa diberitahu:

import { t, type Frame, type Nu } from 'nuforge';

function collectViewErrors(view: t.View, out: string[] = [], seen = new Set<t.View>()): string[] {
  if (seen.has(view)) return out;
  seen.add(view);
  if (t.is(view, t.ErrorSystemView)) out.push(view.error);
  if (t.is(view, t.NuComponentView)) {
    for (const child of view.render) collectViewErrors(child, out, seen);
  }
  const withChildren = view as unknown as { children?: t.View[] };
  if (Array.isArray(withChildren.children)) {
    for (const child of withChildren.children) collectViewErrors(child, out, seen);
  }
  return out;
}

function checkedOk(nu: Nu, componentName: string): { ok: true } | { ok: false; errors: string[] } {
  // Pakai ulang frame live kalau component sudah ter-mount di canvas;
  // kalau tidak, bangun frame transient cuma untuk cek, lalu buang.
  let frame: Frame | undefined = nu.frames.find((f) => f.opts.component?.name === componentName);
  let transient: Frame | undefined;
  if (!frame) frame = transient = nu.createFrame({ component: { name: componentName } });
  try {
    const errors = collectViewErrors(frame.view);
    if (errors.length) {
      nu.undo();
      return { ok: false, errors };
    }
    return { ok: true };
  } finally {
    if (transient) nu.removeFrame(transient);
  }
}

Alirkan errors balik ke caller (mis. sebagai error tool-call yang bisa dilihat dan diperbaiki AI di giliran berikutnya) alih-alih menampilkan exception mentah — error render hampir selalu berarti mutasi mereferensikan sesuatu yang sebenarnya tidak ada dalam scope, persis jenis kesalahan yang bisa diperbaiki lewat retry. Kalau mutasi bisa menyasar component yang bukan halaman aktif saat ini (mis. reusable component tanpa frame live sendiri), resolve dulu component PEMILIKNYA — jalan ke atas dari nu.getParentNode mulai dari node yang dimutasi sampai ketemu t.NuComponent — alih-alih selalu memvalidasi terhadap halaman mana pun yang kebetulan sedang tampil di layar.

Langkah berikutnya

  • Runtime — inti reaktif yang dimutasi oleh editor.
  • Codegen — ubah AST yang telah disunting menjadi kode siap kirim.