Subgraph breadcrumb
Nested template instances (Pipeline → Stage → primitives). Dive in by double-click OR programmatically; the breadcrumb in the top-left tracks the path (Root › Pipeline › Stage) and pops any segment. Auto-themed via --xeno-*. Opt-out with `editor.setBreadcrumbVisible(false)`.
// Vanilla mount for G7 — subgraph breadcrumb. DOM buttons in overlay root dive in/out.
import { XenolithEditor } from '@xenolithengine/graph-editor'
import { buildBreadcrumbDive } from '@xenolithengine/demo/breadcrumb-dive'
export async function mount(target: HTMLElement): Promise<() => void> {
const editor = await XenolithEditor.init(target, { resizeToWindow: false, minimap: false })
const scene = buildBreadcrumbDive(editor)
const panel = document.createElement('div')
panel.setAttribute('data-xeno-panel', '')
panel.style.cssText = 'position:absolute;pointer-events:auto;top:12px;right:12px;display:flex;flex-direction:column;gap:6px;padding:8px;background:var(--xeno-panel,#1d1d1d);border:1px solid var(--xeno-border,#333);border-radius:8px;font:12px Inter,system-ui,sans-serif;'
const mkBtn = (text: string, onClick: () => void): HTMLButtonElement => {
const b = document.createElement('button')
b.textContent = text
b.style.cssText = 'padding:6px 12px;font-size:12px;border-radius:6px;cursor:pointer;text-align:left;border:1px solid var(--xeno-border,#333);background:transparent;color:var(--xeno-text,#cfcfcf);'
b.addEventListener('click', onClick)
return b
}
panel.appendChild(mkBtn('Dive into Pipeline', () => scene.diveInto('pipeline')))
panel.appendChild(mkBtn('… then into Stage', () => scene.diveInto('stage')))
panel.appendChild(mkBtn('Pop to Root', () => scene.diveOut()))
const hint = document.createElement('div')
hint.textContent = 'Or double-click any $templateInstance node.'
hint.style.cssText = 'font-size:11px;color:var(--xeno-muted,#9a9a9a);'
panel.appendChild(hint)
editor.overlayRoot.appendChild(panel)
return () => { panel.remove(); editor.destroy() }
} // G7 showcase — subgraph breadcrumb. Two-level nested template: a top-level "Pipeline" template
// containing a "Stage" sub-template instance, which itself contains two primitive nodes.
// Double-click the Pipeline node to dive in; you'll see the Stage instance + a primitive.
// Double-click Stage to dive deeper. The breadcrumb auto-appears in the top-left:
// Root › Pipeline › Stage (click "Root" or "Pipeline" to pop back).
import type { XenolithEditor } from '@xenolithengine/graph-editor'
export interface BreadcrumbDiveScene {
/** Programmatic dive into Pipeline (top-level template instance) — same as double-clicking. */
diveInto: (slug: 'pipeline' | 'stage') => boolean
/** Pop back to root. */
diveOut: () => void
depth: () => number
}
/** Dive into a named slug from any depth — pops back to root first so the button works whether
* the user already drilled in or not. Returns true if the dive succeeded. */
export function diveIntoSlug(editor: XenolithEditor, slug: 'pipeline' | 'stage'): boolean {
if (editor.diveDepth !== 0) editor.diveOut(0)
if (slug === 'pipeline') return editor.diveInto('main' as never)
editor.diveInto('main' as never)
return editor.diveInto('p_stage' as never)
}
/** Idempotent setup: load the nested template graph. Safe to pass to `<XenolithGraph onReady>`. */
export function setupBreadcrumbDive(editor: XenolithEditor): void { void buildBreadcrumbDive(editor) }
/** @deprecated Prefer `setupBreadcrumbDive` + `diveIntoSlug`. Kept for vanilla examples. */
export function buildBreadcrumbDive(editor: XenolithEditor): BreadcrumbDiveScene {
// ── Innermost: a Stage template definition with two primitive members. ────────────────────
const stageDef = {
title: 'Stage',
nodes: [
{ id: 'sin', type: '$templateInput', position: { x: 0, y: 60 }, state: {},
render: { title: 'Input · value', category: 'macro' },
pins: [{ id: 'sin_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'value' }] },
{ id: 's_proc', type: 'Op', position: { x: 220, y: 40 }, size: { x: 160, y: 56 }, state: {},
render: { title: 'Process', category: 'logic' },
pins: [
{ id: 's_proc_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: 's_proc_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
] },
{ id: 's_norm', type: 'Op', position: { x: 440, y: 40 }, size: { x: 160, y: 56 }, state: {},
render: { title: 'Normalize', category: 'logic' },
pins: [
{ id: 's_norm_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: 's_norm_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
] },
{ id: 'sout', type: '$templateOutput', position: { x: 660, y: 60 }, state: {},
render: { title: 'Output · result', category: 'macro' },
pins: [{ id: 'sout_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'result' }] },
],
edges: [
{ id: 's_e0', from: { node: 'sin', pin: 'sin_o' }, to: { node: 's_proc', pin: 's_proc_i' } },
{ id: 's_e1', from: { node: 's_proc', pin: 's_proc_o' }, to: { node: 's_norm', pin: 's_norm_i' } },
{ id: 's_e2', from: { node: 's_norm', pin: 's_norm_o' }, to: { node: 'sout', pin: 'sout_i' } },
],
}
// ── Middle: Pipeline definition contains ONE Stage instance + a Source primitive. ────────
const pipelineDef = {
title: 'Pipeline',
nodes: [
{ id: 'pin', type: '$templateInput', position: { x: 0, y: 60 }, state: {},
render: { title: 'Input · input', category: 'macro' },
pins: [{ id: 'pin_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'input' }] },
{ id: 'p_src', type: 'Op', position: { x: 220, y: 40 }, size: { x: 160, y: 56 }, state: {},
render: { title: 'Tokenize', category: 'logic' },
pins: [
{ id: 'p_src_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: 'p_src_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
] },
// Nested template instance — double-clicking THIS dives a level deeper.
{ id: 'p_stage', type: '$templateInstance', position: { x: 440, y: 40 },
state: { definitionId: 'tpl:stage', pinBoundary: { p_stage_in: 'sin', p_stage_out: 'sout' } },
render: { title: 'Stage', category: 'macro' },
pins: [
{ id: 'p_stage_in', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'value' },
{ id: 'p_stage_out', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'result' },
] },
{ id: 'pout', type: '$templateOutput', position: { x: 660, y: 60 }, state: {},
render: { title: 'Output · output', category: 'macro' },
pins: [{ id: 'pout_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'output' }] },
],
edges: [
{ id: 'p_e0', from: { node: 'pin', pin: 'pin_o' }, to: { node: 'p_src', pin: 'p_src_i' } },
{ id: 'p_e1', from: { node: 'p_src', pin: 'p_src_o' }, to: { node: 'p_stage', pin: 'p_stage_in' } },
{ id: 'p_e2', from: { node: 'p_stage', pin: 'p_stage_out' }, to: { node: 'pout', pin: 'pout_i' } },
],
}
// ── Outer document: one Pipeline instance. Double-click to dive in (breadcrumb appears). ──
editor.loadJSON({
version: 'xenolith.v1',
// Templates are an OBJECT keyed by id (matches the file format on disk).
templates: { 'tpl:stage': stageDef, 'tpl:pipeline': pipelineDef } as never,
nodes: [
{ id: 'main', type: '$templateInstance', position: { x: 200, y: 200 },
state: { definitionId: 'tpl:pipeline', pinBoundary: { main_in: 'pin', main_out: 'pout' } },
render: { title: 'Pipeline', category: 'macro' },
pins: [
{ id: 'main_in', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'input' },
{ id: 'main_out', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'output' },
] },
],
edges: [],
})
editor.fitView({ padding: 80, maxZoom: 1 })
return {
diveInto: (slug) => {
// "Dive into Pipeline" must work from any depth — `main` lives in the ROOT graph, so pop
// all the way back first. Without this, pressing the button from inside Stage tried to
// find a `main` node in Stage's definition and silently failed.
if (editor.diveDepth !== 0) editor.diveOut(0)
if (slug === 'pipeline') return editor.diveInto('main' as never)
// For Stage: ensure we're inside Pipeline first.
editor.diveInto('main' as never)
return editor.diveInto('p_stage' as never)
},
diveOut: () => editor.diveOut(0),
depth: () => editor.diveDepth,
}
} import { XenolithGraph, XenolithPanel, useEditor } from '@xenolithengine/graph-react'
import { setupBreadcrumbDive, diveIntoSlug } from '@xenolithengine/demo/breadcrumb-dive'
import { DemoStage } from '../Layout.js'
// Canon: dive operations are pure editor methods + one helper; the panel calls them directly
// via `useEditor()`. No scene handle held in state.
function BreadcrumbPanel() {
const editor = useEditor()
return (
<XenolithPanel position="top-right" style={{ display: 'flex', flexDirection: 'column', gap: 6, padding: 8 }}>
<button onClick={() => diveIntoSlug(editor, 'pipeline')} style={btn}>Dive into Pipeline</button>
<button onClick={() => diveIntoSlug(editor, 'stage')} style={btn}>… then into Stage</button>
<button onClick={() => editor.diveOut(0)} style={btn}>Pop to Root</button>
<div style={{ fontSize: 11, color: 'var(--xeno-muted, #9a9a9a)' }}>Or double-click any $templateInstance node.</div>
</XenolithPanel>
)
}
/** Island: G7 — subgraph breadcrumb. */
export function BreadcrumbDiveDemo() {
return (
<DemoStage>
<XenolithGraph className="xeno" resizeToWindow={false} onReady={setupBreadcrumbDive}>
<BreadcrumbPanel />
</XenolithGraph>
</DemoStage>
)
}
const btn: React.CSSProperties = {
padding: '6px 12px',
fontSize: 12,
borderRadius: 6,
border: '1px solid var(--xeno-border, #333)',
background: 'transparent',
color: 'var(--xeno-text, #cfcfcf)',
cursor: 'pointer',
textAlign: 'left',
} // G7 showcase — subgraph breadcrumb. Two-level nested template: a top-level "Pipeline" template
// containing a "Stage" sub-template instance, which itself contains two primitive nodes.
// Double-click the Pipeline node to dive in; you'll see the Stage instance + a primitive.
// Double-click Stage to dive deeper. The breadcrumb auto-appears in the top-left:
// Root › Pipeline › Stage (click "Root" or "Pipeline" to pop back).
import type { XenolithEditor } from '@xenolithengine/graph-editor'
export interface BreadcrumbDiveScene {
/** Programmatic dive into Pipeline (top-level template instance) — same as double-clicking. */
diveInto: (slug: 'pipeline' | 'stage') => boolean
/** Pop back to root. */
diveOut: () => void
depth: () => number
}
/** Dive into a named slug from any depth — pops back to root first so the button works whether
* the user already drilled in or not. Returns true if the dive succeeded. */
export function diveIntoSlug(editor: XenolithEditor, slug: 'pipeline' | 'stage'): boolean {
if (editor.diveDepth !== 0) editor.diveOut(0)
if (slug === 'pipeline') return editor.diveInto('main' as never)
editor.diveInto('main' as never)
return editor.diveInto('p_stage' as never)
}
/** Idempotent setup: load the nested template graph. Safe to pass to `<XenolithGraph onReady>`. */
export function setupBreadcrumbDive(editor: XenolithEditor): void { void buildBreadcrumbDive(editor) }
/** @deprecated Prefer `setupBreadcrumbDive` + `diveIntoSlug`. Kept for vanilla examples. */
export function buildBreadcrumbDive(editor: XenolithEditor): BreadcrumbDiveScene {
// ── Innermost: a Stage template definition with two primitive members. ────────────────────
const stageDef = {
title: 'Stage',
nodes: [
{ id: 'sin', type: '$templateInput', position: { x: 0, y: 60 }, state: {},
render: { title: 'Input · value', category: 'macro' },
pins: [{ id: 'sin_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'value' }] },
{ id: 's_proc', type: 'Op', position: { x: 220, y: 40 }, size: { x: 160, y: 56 }, state: {},
render: { title: 'Process', category: 'logic' },
pins: [
{ id: 's_proc_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: 's_proc_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
] },
{ id: 's_norm', type: 'Op', position: { x: 440, y: 40 }, size: { x: 160, y: 56 }, state: {},
render: { title: 'Normalize', category: 'logic' },
pins: [
{ id: 's_norm_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: 's_norm_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
] },
{ id: 'sout', type: '$templateOutput', position: { x: 660, y: 60 }, state: {},
render: { title: 'Output · result', category: 'macro' },
pins: [{ id: 'sout_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'result' }] },
],
edges: [
{ id: 's_e0', from: { node: 'sin', pin: 'sin_o' }, to: { node: 's_proc', pin: 's_proc_i' } },
{ id: 's_e1', from: { node: 's_proc', pin: 's_proc_o' }, to: { node: 's_norm', pin: 's_norm_i' } },
{ id: 's_e2', from: { node: 's_norm', pin: 's_norm_o' }, to: { node: 'sout', pin: 'sout_i' } },
],
}
// ── Middle: Pipeline definition contains ONE Stage instance + a Source primitive. ────────
const pipelineDef = {
title: 'Pipeline',
nodes: [
{ id: 'pin', type: '$templateInput', position: { x: 0, y: 60 }, state: {},
render: { title: 'Input · input', category: 'macro' },
pins: [{ id: 'pin_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'input' }] },
{ id: 'p_src', type: 'Op', position: { x: 220, y: 40 }, size: { x: 160, y: 56 }, state: {},
render: { title: 'Tokenize', category: 'logic' },
pins: [
{ id: 'p_src_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: 'p_src_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
] },
// Nested template instance — double-clicking THIS dives a level deeper.
{ id: 'p_stage', type: '$templateInstance', position: { x: 440, y: 40 },
state: { definitionId: 'tpl:stage', pinBoundary: { p_stage_in: 'sin', p_stage_out: 'sout' } },
render: { title: 'Stage', category: 'macro' },
pins: [
{ id: 'p_stage_in', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'value' },
{ id: 'p_stage_out', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'result' },
] },
{ id: 'pout', type: '$templateOutput', position: { x: 660, y: 60 }, state: {},
render: { title: 'Output · output', category: 'macro' },
pins: [{ id: 'pout_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'output' }] },
],
edges: [
{ id: 'p_e0', from: { node: 'pin', pin: 'pin_o' }, to: { node: 'p_src', pin: 'p_src_i' } },
{ id: 'p_e1', from: { node: 'p_src', pin: 'p_src_o' }, to: { node: 'p_stage', pin: 'p_stage_in' } },
{ id: 'p_e2', from: { node: 'p_stage', pin: 'p_stage_out' }, to: { node: 'pout', pin: 'pout_i' } },
],
}
// ── Outer document: one Pipeline instance. Double-click to dive in (breadcrumb appears). ──
editor.loadJSON({
version: 'xenolith.v1',
// Templates are an OBJECT keyed by id (matches the file format on disk).
templates: { 'tpl:stage': stageDef, 'tpl:pipeline': pipelineDef } as never,
nodes: [
{ id: 'main', type: '$templateInstance', position: { x: 200, y: 200 },
state: { definitionId: 'tpl:pipeline', pinBoundary: { main_in: 'pin', main_out: 'pout' } },
render: { title: 'Pipeline', category: 'macro' },
pins: [
{ id: 'main_in', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'input' },
{ id: 'main_out', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'output' },
] },
],
edges: [],
})
editor.fitView({ padding: 80, maxZoom: 1 })
return {
diveInto: (slug) => {
// "Dive into Pipeline" must work from any depth — `main` lives in the ROOT graph, so pop
// all the way back first. Without this, pressing the button from inside Stage tried to
// find a `main` node in Stage's definition and silently failed.
if (editor.diveDepth !== 0) editor.diveOut(0)
if (slug === 'pipeline') return editor.diveInto('main' as never)
// For Stage: ensure we're inside Pipeline first.
editor.diveInto('main' as never)
return editor.diveInto('p_stage' as never)
},
diveOut: () => editor.diveOut(0),
depth: () => editor.diveDepth,
}
} <script setup lang="ts">
// Vue — dive helpers called with the editor from @ready. Double-click still works on the canvas.
import { ref } from 'vue'
import { XenolithGraph, XenolithPanel, XenolithButton } from '@xenolithengine/graph-vue'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import { setupBreadcrumbDive, diveIntoSlug } from '@xenolithengine/demo/breadcrumb-dive'
const editor = ref<XenolithEditor | null>(null)
function onReady(e: XenolithEditor): void {
editor.value = e
setupBreadcrumbDive(e)
}
</script>
<template>
<div class="app" style="position:absolute;inset:0;">
<XenolithGraph class="xeno" :resize-to-window="false" @ready="onReady">
<XenolithPanel position="top-right">
<div style="display:flex;flex-direction:column;gap:6px;min-width:180px;">
<XenolithButton style="text-align:left" @click="editor && diveIntoSlug(editor, 'pipeline')">Dive into Pipeline</XenolithButton>
<XenolithButton style="text-align:left" @click="editor && diveIntoSlug(editor, 'stage')">… then into Stage</XenolithButton>
<XenolithButton style="text-align:left" @click="editor?.diveOut(0)">Pop to Root</XenolithButton>
<div style="font-size:11px;color:var(--xeno-muted);">Or double-click any $templateInstance node.</div>
</div>
</XenolithPanel>
</XenolithGraph>
</div>
</template> // G7 showcase — subgraph breadcrumb. Two-level nested template: a top-level "Pipeline" template
// containing a "Stage" sub-template instance, which itself contains two primitive nodes.
// Double-click the Pipeline node to dive in; you'll see the Stage instance + a primitive.
// Double-click Stage to dive deeper. The breadcrumb auto-appears in the top-left:
// Root › Pipeline › Stage (click "Root" or "Pipeline" to pop back).
import type { XenolithEditor } from '@xenolithengine/graph-editor'
export interface BreadcrumbDiveScene {
/** Programmatic dive into Pipeline (top-level template instance) — same as double-clicking. */
diveInto: (slug: 'pipeline' | 'stage') => boolean
/** Pop back to root. */
diveOut: () => void
depth: () => number
}
/** Dive into a named slug from any depth — pops back to root first so the button works whether
* the user already drilled in or not. Returns true if the dive succeeded. */
export function diveIntoSlug(editor: XenolithEditor, slug: 'pipeline' | 'stage'): boolean {
if (editor.diveDepth !== 0) editor.diveOut(0)
if (slug === 'pipeline') return editor.diveInto('main' as never)
editor.diveInto('main' as never)
return editor.diveInto('p_stage' as never)
}
/** Idempotent setup: load the nested template graph. Safe to pass to `<XenolithGraph onReady>`. */
export function setupBreadcrumbDive(editor: XenolithEditor): void { void buildBreadcrumbDive(editor) }
/** @deprecated Prefer `setupBreadcrumbDive` + `diveIntoSlug`. Kept for vanilla examples. */
export function buildBreadcrumbDive(editor: XenolithEditor): BreadcrumbDiveScene {
// ── Innermost: a Stage template definition with two primitive members. ────────────────────
const stageDef = {
title: 'Stage',
nodes: [
{ id: 'sin', type: '$templateInput', position: { x: 0, y: 60 }, state: {},
render: { title: 'Input · value', category: 'macro' },
pins: [{ id: 'sin_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'value' }] },
{ id: 's_proc', type: 'Op', position: { x: 220, y: 40 }, size: { x: 160, y: 56 }, state: {},
render: { title: 'Process', category: 'logic' },
pins: [
{ id: 's_proc_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: 's_proc_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
] },
{ id: 's_norm', type: 'Op', position: { x: 440, y: 40 }, size: { x: 160, y: 56 }, state: {},
render: { title: 'Normalize', category: 'logic' },
pins: [
{ id: 's_norm_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: 's_norm_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
] },
{ id: 'sout', type: '$templateOutput', position: { x: 660, y: 60 }, state: {},
render: { title: 'Output · result', category: 'macro' },
pins: [{ id: 'sout_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'result' }] },
],
edges: [
{ id: 's_e0', from: { node: 'sin', pin: 'sin_o' }, to: { node: 's_proc', pin: 's_proc_i' } },
{ id: 's_e1', from: { node: 's_proc', pin: 's_proc_o' }, to: { node: 's_norm', pin: 's_norm_i' } },
{ id: 's_e2', from: { node: 's_norm', pin: 's_norm_o' }, to: { node: 'sout', pin: 'sout_i' } },
],
}
// ── Middle: Pipeline definition contains ONE Stage instance + a Source primitive. ────────
const pipelineDef = {
title: 'Pipeline',
nodes: [
{ id: 'pin', type: '$templateInput', position: { x: 0, y: 60 }, state: {},
render: { title: 'Input · input', category: 'macro' },
pins: [{ id: 'pin_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'input' }] },
{ id: 'p_src', type: 'Op', position: { x: 220, y: 40 }, size: { x: 160, y: 56 }, state: {},
render: { title: 'Tokenize', category: 'logic' },
pins: [
{ id: 'p_src_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: 'p_src_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
] },
// Nested template instance — double-clicking THIS dives a level deeper.
{ id: 'p_stage', type: '$templateInstance', position: { x: 440, y: 40 },
state: { definitionId: 'tpl:stage', pinBoundary: { p_stage_in: 'sin', p_stage_out: 'sout' } },
render: { title: 'Stage', category: 'macro' },
pins: [
{ id: 'p_stage_in', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'value' },
{ id: 'p_stage_out', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'result' },
] },
{ id: 'pout', type: '$templateOutput', position: { x: 660, y: 60 }, state: {},
render: { title: 'Output · output', category: 'macro' },
pins: [{ id: 'pout_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'output' }] },
],
edges: [
{ id: 'p_e0', from: { node: 'pin', pin: 'pin_o' }, to: { node: 'p_src', pin: 'p_src_i' } },
{ id: 'p_e1', from: { node: 'p_src', pin: 'p_src_o' }, to: { node: 'p_stage', pin: 'p_stage_in' } },
{ id: 'p_e2', from: { node: 'p_stage', pin: 'p_stage_out' }, to: { node: 'pout', pin: 'pout_i' } },
],
}
// ── Outer document: one Pipeline instance. Double-click to dive in (breadcrumb appears). ──
editor.loadJSON({
version: 'xenolith.v1',
// Templates are an OBJECT keyed by id (matches the file format on disk).
templates: { 'tpl:stage': stageDef, 'tpl:pipeline': pipelineDef } as never,
nodes: [
{ id: 'main', type: '$templateInstance', position: { x: 200, y: 200 },
state: { definitionId: 'tpl:pipeline', pinBoundary: { main_in: 'pin', main_out: 'pout' } },
render: { title: 'Pipeline', category: 'macro' },
pins: [
{ id: 'main_in', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'input' },
{ id: 'main_out', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'output' },
] },
],
edges: [],
})
editor.fitView({ padding: 80, maxZoom: 1 })
return {
diveInto: (slug) => {
// "Dive into Pipeline" must work from any depth — `main` lives in the ROOT graph, so pop
// all the way back first. Without this, pressing the button from inside Stage tried to
// find a `main` node in Stage's definition and silently failed.
if (editor.diveDepth !== 0) editor.diveOut(0)
if (slug === 'pipeline') return editor.diveInto('main' as never)
// For Stage: ensure we're inside Pipeline first.
editor.diveInto('main' as never)
return editor.diveInto('p_stage' as never)
},
diveOut: () => editor.diveOut(0),
depth: () => editor.diveDepth,
}
} <!-- Svelte — dive helpers called with the editor from onready. -->
<script lang="ts">
import { xenolith, createXenolithEditorContext } from '@xenolithengine/graph-svelte'
import { XenolithPanel, XenolithButton } from '@xenolithengine/graph-svelte/components'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import { setupBreadcrumbDive, diveIntoSlug } from '@xenolithengine/demo/breadcrumb-dive'
const panelEditor = createXenolithEditorContext()
let editor = $state<XenolithEditor | null>(null)
</script>
<div
use:xenolith={{ resizeToWindow: false }}
onready={(e) => { editor = e.detail; panelEditor.set(e.detail); setupBreadcrumbDive(e.detail) }}
style="position:absolute;inset:0"
></div>
<XenolithPanel position="top-right">
<div style="display:flex;flex-direction:column;gap:6px;min-width:180px;">
<XenolithButton style="text-align:left" onclick={() => editor && diveIntoSlug(editor, 'pipeline')}>Dive into Pipeline</XenolithButton>
<XenolithButton style="text-align:left" onclick={() => editor && diveIntoSlug(editor, 'stage')}>… then into Stage</XenolithButton>
<XenolithButton style="text-align:left" onclick={() => editor?.diveOut(0)}>Pop to Root</XenolithButton>
<div style="font-size:11px;color:var(--xeno-muted);">Or double-click any $templateInstance node.</div>
</div>
</XenolithPanel> // G7 showcase — subgraph breadcrumb. Two-level nested template: a top-level "Pipeline" template
// containing a "Stage" sub-template instance, which itself contains two primitive nodes.
// Double-click the Pipeline node to dive in; you'll see the Stage instance + a primitive.
// Double-click Stage to dive deeper. The breadcrumb auto-appears in the top-left:
// Root › Pipeline › Stage (click "Root" or "Pipeline" to pop back).
import type { XenolithEditor } from '@xenolithengine/graph-editor'
export interface BreadcrumbDiveScene {
/** Programmatic dive into Pipeline (top-level template instance) — same as double-clicking. */
diveInto: (slug: 'pipeline' | 'stage') => boolean
/** Pop back to root. */
diveOut: () => void
depth: () => number
}
/** Dive into a named slug from any depth — pops back to root first so the button works whether
* the user already drilled in or not. Returns true if the dive succeeded. */
export function diveIntoSlug(editor: XenolithEditor, slug: 'pipeline' | 'stage'): boolean {
if (editor.diveDepth !== 0) editor.diveOut(0)
if (slug === 'pipeline') return editor.diveInto('main' as never)
editor.diveInto('main' as never)
return editor.diveInto('p_stage' as never)
}
/** Idempotent setup: load the nested template graph. Safe to pass to `<XenolithGraph onReady>`. */
export function setupBreadcrumbDive(editor: XenolithEditor): void { void buildBreadcrumbDive(editor) }
/** @deprecated Prefer `setupBreadcrumbDive` + `diveIntoSlug`. Kept for vanilla examples. */
export function buildBreadcrumbDive(editor: XenolithEditor): BreadcrumbDiveScene {
// ── Innermost: a Stage template definition with two primitive members. ────────────────────
const stageDef = {
title: 'Stage',
nodes: [
{ id: 'sin', type: '$templateInput', position: { x: 0, y: 60 }, state: {},
render: { title: 'Input · value', category: 'macro' },
pins: [{ id: 'sin_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'value' }] },
{ id: 's_proc', type: 'Op', position: { x: 220, y: 40 }, size: { x: 160, y: 56 }, state: {},
render: { title: 'Process', category: 'logic' },
pins: [
{ id: 's_proc_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: 's_proc_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
] },
{ id: 's_norm', type: 'Op', position: { x: 440, y: 40 }, size: { x: 160, y: 56 }, state: {},
render: { title: 'Normalize', category: 'logic' },
pins: [
{ id: 's_norm_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: 's_norm_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
] },
{ id: 'sout', type: '$templateOutput', position: { x: 660, y: 60 }, state: {},
render: { title: 'Output · result', category: 'macro' },
pins: [{ id: 'sout_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'result' }] },
],
edges: [
{ id: 's_e0', from: { node: 'sin', pin: 'sin_o' }, to: { node: 's_proc', pin: 's_proc_i' } },
{ id: 's_e1', from: { node: 's_proc', pin: 's_proc_o' }, to: { node: 's_norm', pin: 's_norm_i' } },
{ id: 's_e2', from: { node: 's_norm', pin: 's_norm_o' }, to: { node: 'sout', pin: 'sout_i' } },
],
}
// ── Middle: Pipeline definition contains ONE Stage instance + a Source primitive. ────────
const pipelineDef = {
title: 'Pipeline',
nodes: [
{ id: 'pin', type: '$templateInput', position: { x: 0, y: 60 }, state: {},
render: { title: 'Input · input', category: 'macro' },
pins: [{ id: 'pin_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'input' }] },
{ id: 'p_src', type: 'Op', position: { x: 220, y: 40 }, size: { x: 160, y: 56 }, state: {},
render: { title: 'Tokenize', category: 'logic' },
pins: [
{ id: 'p_src_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: 'p_src_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
] },
// Nested template instance — double-clicking THIS dives a level deeper.
{ id: 'p_stage', type: '$templateInstance', position: { x: 440, y: 40 },
state: { definitionId: 'tpl:stage', pinBoundary: { p_stage_in: 'sin', p_stage_out: 'sout' } },
render: { title: 'Stage', category: 'macro' },
pins: [
{ id: 'p_stage_in', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'value' },
{ id: 'p_stage_out', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'result' },
] },
{ id: 'pout', type: '$templateOutput', position: { x: 660, y: 60 }, state: {},
render: { title: 'Output · output', category: 'macro' },
pins: [{ id: 'pout_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'output' }] },
],
edges: [
{ id: 'p_e0', from: { node: 'pin', pin: 'pin_o' }, to: { node: 'p_src', pin: 'p_src_i' } },
{ id: 'p_e1', from: { node: 'p_src', pin: 'p_src_o' }, to: { node: 'p_stage', pin: 'p_stage_in' } },
{ id: 'p_e2', from: { node: 'p_stage', pin: 'p_stage_out' }, to: { node: 'pout', pin: 'pout_i' } },
],
}
// ── Outer document: one Pipeline instance. Double-click to dive in (breadcrumb appears). ──
editor.loadJSON({
version: 'xenolith.v1',
// Templates are an OBJECT keyed by id (matches the file format on disk).
templates: { 'tpl:stage': stageDef, 'tpl:pipeline': pipelineDef } as never,
nodes: [
{ id: 'main', type: '$templateInstance', position: { x: 200, y: 200 },
state: { definitionId: 'tpl:pipeline', pinBoundary: { main_in: 'pin', main_out: 'pout' } },
render: { title: 'Pipeline', category: 'macro' },
pins: [
{ id: 'main_in', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'input' },
{ id: 'main_out', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'output' },
] },
],
edges: [],
})
editor.fitView({ padding: 80, maxZoom: 1 })
return {
diveInto: (slug) => {
// "Dive into Pipeline" must work from any depth — `main` lives in the ROOT graph, so pop
// all the way back first. Without this, pressing the button from inside Stage tried to
// find a `main` node in Stage's definition and silently failed.
if (editor.diveDepth !== 0) editor.diveOut(0)
if (slug === 'pipeline') return editor.diveInto('main' as never)
// For Stage: ensure we're inside Pipeline first.
editor.diveInto('main' as never)
return editor.diveInto('p_stage' as never)
},
diveOut: () => editor.diveOut(0),
depth: () => editor.diveDepth,
}
} // Solid — dive helpers called with the editor from on:ready. The card is host DOM.
import { createSignal } from 'solid-js'
import { xenolith } from '@xenolithengine/graph-solid'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import { setupBreadcrumbDive, diveIntoSlug } from '@xenolithengine/demo/breadcrumb-dive'
const btn = 'font:inherit;font-size:12px;padding:6px 12px;text-align:left;cursor:pointer;border-radius:6px;border:1px solid var(--xeno-border);background:transparent;color:var(--xeno-text);'
export function BreadcrumbDiveDemo() {
const [editor, setEditor] = createSignal<XenolithEditor | null>(null)
return (
<div
use:xenolith={{ resizeToWindow: false }}
on:ready={(e) => { setEditor(e.detail); setupBreadcrumbDive(e.detail) }}
style="position:absolute;inset:0"
>
<div style="position:absolute;top:12px;right:12px;z-index:5;display:flex;flex-direction:column;gap:6px;padding:8px;min-width:180px;background:var(--xeno-panel);border:1px solid var(--xeno-border);border-radius:10px;color:var(--xeno-text);font:13px system-ui,sans-serif;">
<button type="button" style={btn} onClick={() => { const e = editor(); if (e) diveIntoSlug(e, 'pipeline') }}>Dive into Pipeline</button>
<button type="button" style={btn} onClick={() => { const e = editor(); if (e) diveIntoSlug(e, 'stage') }}>… then into Stage</button>
<button type="button" style={btn} onClick={() => editor()?.diveOut(0)}>Pop to Root</button>
<div style="font-size:11px;color:var(--xeno-muted);">Or double-click any $templateInstance node.</div>
</div>
</div>
)
} // G7 showcase — subgraph breadcrumb. Two-level nested template: a top-level "Pipeline" template
// containing a "Stage" sub-template instance, which itself contains two primitive nodes.
// Double-click the Pipeline node to dive in; you'll see the Stage instance + a primitive.
// Double-click Stage to dive deeper. The breadcrumb auto-appears in the top-left:
// Root › Pipeline › Stage (click "Root" or "Pipeline" to pop back).
import type { XenolithEditor } from '@xenolithengine/graph-editor'
export interface BreadcrumbDiveScene {
/** Programmatic dive into Pipeline (top-level template instance) — same as double-clicking. */
diveInto: (slug: 'pipeline' | 'stage') => boolean
/** Pop back to root. */
diveOut: () => void
depth: () => number
}
/** Dive into a named slug from any depth — pops back to root first so the button works whether
* the user already drilled in or not. Returns true if the dive succeeded. */
export function diveIntoSlug(editor: XenolithEditor, slug: 'pipeline' | 'stage'): boolean {
if (editor.diveDepth !== 0) editor.diveOut(0)
if (slug === 'pipeline') return editor.diveInto('main' as never)
editor.diveInto('main' as never)
return editor.diveInto('p_stage' as never)
}
/** Idempotent setup: load the nested template graph. Safe to pass to `<XenolithGraph onReady>`. */
export function setupBreadcrumbDive(editor: XenolithEditor): void { void buildBreadcrumbDive(editor) }
/** @deprecated Prefer `setupBreadcrumbDive` + `diveIntoSlug`. Kept for vanilla examples. */
export function buildBreadcrumbDive(editor: XenolithEditor): BreadcrumbDiveScene {
// ── Innermost: a Stage template definition with two primitive members. ────────────────────
const stageDef = {
title: 'Stage',
nodes: [
{ id: 'sin', type: '$templateInput', position: { x: 0, y: 60 }, state: {},
render: { title: 'Input · value', category: 'macro' },
pins: [{ id: 'sin_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'value' }] },
{ id: 's_proc', type: 'Op', position: { x: 220, y: 40 }, size: { x: 160, y: 56 }, state: {},
render: { title: 'Process', category: 'logic' },
pins: [
{ id: 's_proc_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: 's_proc_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
] },
{ id: 's_norm', type: 'Op', position: { x: 440, y: 40 }, size: { x: 160, y: 56 }, state: {},
render: { title: 'Normalize', category: 'logic' },
pins: [
{ id: 's_norm_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: 's_norm_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
] },
{ id: 'sout', type: '$templateOutput', position: { x: 660, y: 60 }, state: {},
render: { title: 'Output · result', category: 'macro' },
pins: [{ id: 'sout_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'result' }] },
],
edges: [
{ id: 's_e0', from: { node: 'sin', pin: 'sin_o' }, to: { node: 's_proc', pin: 's_proc_i' } },
{ id: 's_e1', from: { node: 's_proc', pin: 's_proc_o' }, to: { node: 's_norm', pin: 's_norm_i' } },
{ id: 's_e2', from: { node: 's_norm', pin: 's_norm_o' }, to: { node: 'sout', pin: 'sout_i' } },
],
}
// ── Middle: Pipeline definition contains ONE Stage instance + a Source primitive. ────────
const pipelineDef = {
title: 'Pipeline',
nodes: [
{ id: 'pin', type: '$templateInput', position: { x: 0, y: 60 }, state: {},
render: { title: 'Input · input', category: 'macro' },
pins: [{ id: 'pin_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'input' }] },
{ id: 'p_src', type: 'Op', position: { x: 220, y: 40 }, size: { x: 160, y: 56 }, state: {},
render: { title: 'Tokenize', category: 'logic' },
pins: [
{ id: 'p_src_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: 'p_src_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
] },
// Nested template instance — double-clicking THIS dives a level deeper.
{ id: 'p_stage', type: '$templateInstance', position: { x: 440, y: 40 },
state: { definitionId: 'tpl:stage', pinBoundary: { p_stage_in: 'sin', p_stage_out: 'sout' } },
render: { title: 'Stage', category: 'macro' },
pins: [
{ id: 'p_stage_in', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'value' },
{ id: 'p_stage_out', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'result' },
] },
{ id: 'pout', type: '$templateOutput', position: { x: 660, y: 60 }, state: {},
render: { title: 'Output · output', category: 'macro' },
pins: [{ id: 'pout_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'output' }] },
],
edges: [
{ id: 'p_e0', from: { node: 'pin', pin: 'pin_o' }, to: { node: 'p_src', pin: 'p_src_i' } },
{ id: 'p_e1', from: { node: 'p_src', pin: 'p_src_o' }, to: { node: 'p_stage', pin: 'p_stage_in' } },
{ id: 'p_e2', from: { node: 'p_stage', pin: 'p_stage_out' }, to: { node: 'pout', pin: 'pout_i' } },
],
}
// ── Outer document: one Pipeline instance. Double-click to dive in (breadcrumb appears). ──
editor.loadJSON({
version: 'xenolith.v1',
// Templates are an OBJECT keyed by id (matches the file format on disk).
templates: { 'tpl:stage': stageDef, 'tpl:pipeline': pipelineDef } as never,
nodes: [
{ id: 'main', type: '$templateInstance', position: { x: 200, y: 200 },
state: { definitionId: 'tpl:pipeline', pinBoundary: { main_in: 'pin', main_out: 'pout' } },
render: { title: 'Pipeline', category: 'macro' },
pins: [
{ id: 'main_in', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'input' },
{ id: 'main_out', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'output' },
] },
],
edges: [],
})
editor.fitView({ padding: 80, maxZoom: 1 })
return {
diveInto: (slug) => {
// "Dive into Pipeline" must work from any depth — `main` lives in the ROOT graph, so pop
// all the way back first. Without this, pressing the button from inside Stage tried to
// find a `main` node in Stage's definition and silently failed.
if (editor.diveDepth !== 0) editor.diveOut(0)
if (slug === 'pipeline') return editor.diveInto('main' as never)
// For Stage: ensure we're inside Pipeline first.
editor.diveInto('main' as never)
return editor.diveInto('p_stage' as never)
},
diveOut: () => editor.diveOut(0),
depth: () => editor.diveDepth,
}
} // Angular — dive helpers called on the service editor. The card is host DOM.
import { AfterViewInit, Component, ElementRef, OnDestroy, inject, viewChild } from '@angular/core'
import { XenolithGraphService } from '@xenolithengine/graph-angular'
import { setupBreadcrumbDive, diveIntoSlug } from '@xenolithengine/demo/breadcrumb-dive'
@Component({
selector: 'breadcrumb-dive-demo',
standalone: true,
providers: [XenolithGraphService],
template: `
<div style="position:absolute;inset:0;">
<div #host class="xeno" style="position:absolute;inset:0;"></div>
<div class="card">
<button type="button" class="btn" (click)="dive('pipeline')">Dive into Pipeline</button>
<button type="button" class="btn" (click)="dive('stage')">… then into Stage</button>
<button type="button" class="btn" (click)="pop()">Pop to Root</button>
<div class="note">Or double-click any $templateInstance node.</div>
</div>
</div>
`,
styles: [`
.card { position:absolute; top:12px; right:12px; z-index:5; display:flex; flex-direction:column;
gap:6px; padding:8px; min-width:180px; background:var(--xeno-panel,#1d1d1d);
border:1px solid var(--xeno-border,#333); border-radius:10px;
color:var(--xeno-text,#cfcfcf); font:13px system-ui,sans-serif; }
.btn { font:inherit; font-size:12px; padding:6px 12px; text-align:left; cursor:pointer;
border-radius:6px; border:1px solid var(--xeno-border,#333); background:transparent;
color:var(--xeno-text,#cfcfcf); }
.note { font-size:11px; color:var(--xeno-muted,#9a9a9a); }
`],
})
export class BreadcrumbDiveDemoComponent implements AfterViewInit, OnDestroy {
private graph = inject(XenolithGraphService)
private host = viewChild.required<ElementRef<HTMLDivElement>>('host')
async ngAfterViewInit(): Promise<void> {
const editor = await this.graph.mount(this.host().nativeElement, { resizeToWindow: false })
setupBreadcrumbDive(editor)
}
dive(slug: 'pipeline' | 'stage'): void {
const editor = this.graph.editor
if (editor) diveIntoSlug(editor, slug)
}
pop(): void { this.graph.editor?.diveOut(0) }
ngOnDestroy(): void { this.graph.destroy() }
} // G7 showcase — subgraph breadcrumb. Two-level nested template: a top-level "Pipeline" template
// containing a "Stage" sub-template instance, which itself contains two primitive nodes.
// Double-click the Pipeline node to dive in; you'll see the Stage instance + a primitive.
// Double-click Stage to dive deeper. The breadcrumb auto-appears in the top-left:
// Root › Pipeline › Stage (click "Root" or "Pipeline" to pop back).
import type { XenolithEditor } from '@xenolithengine/graph-editor'
export interface BreadcrumbDiveScene {
/** Programmatic dive into Pipeline (top-level template instance) — same as double-clicking. */
diveInto: (slug: 'pipeline' | 'stage') => boolean
/** Pop back to root. */
diveOut: () => void
depth: () => number
}
/** Dive into a named slug from any depth — pops back to root first so the button works whether
* the user already drilled in or not. Returns true if the dive succeeded. */
export function diveIntoSlug(editor: XenolithEditor, slug: 'pipeline' | 'stage'): boolean {
if (editor.diveDepth !== 0) editor.diveOut(0)
if (slug === 'pipeline') return editor.diveInto('main' as never)
editor.diveInto('main' as never)
return editor.diveInto('p_stage' as never)
}
/** Idempotent setup: load the nested template graph. Safe to pass to `<XenolithGraph onReady>`. */
export function setupBreadcrumbDive(editor: XenolithEditor): void { void buildBreadcrumbDive(editor) }
/** @deprecated Prefer `setupBreadcrumbDive` + `diveIntoSlug`. Kept for vanilla examples. */
export function buildBreadcrumbDive(editor: XenolithEditor): BreadcrumbDiveScene {
// ── Innermost: a Stage template definition with two primitive members. ────────────────────
const stageDef = {
title: 'Stage',
nodes: [
{ id: 'sin', type: '$templateInput', position: { x: 0, y: 60 }, state: {},
render: { title: 'Input · value', category: 'macro' },
pins: [{ id: 'sin_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'value' }] },
{ id: 's_proc', type: 'Op', position: { x: 220, y: 40 }, size: { x: 160, y: 56 }, state: {},
render: { title: 'Process', category: 'logic' },
pins: [
{ id: 's_proc_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: 's_proc_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
] },
{ id: 's_norm', type: 'Op', position: { x: 440, y: 40 }, size: { x: 160, y: 56 }, state: {},
render: { title: 'Normalize', category: 'logic' },
pins: [
{ id: 's_norm_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: 's_norm_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
] },
{ id: 'sout', type: '$templateOutput', position: { x: 660, y: 60 }, state: {},
render: { title: 'Output · result', category: 'macro' },
pins: [{ id: 'sout_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'result' }] },
],
edges: [
{ id: 's_e0', from: { node: 'sin', pin: 'sin_o' }, to: { node: 's_proc', pin: 's_proc_i' } },
{ id: 's_e1', from: { node: 's_proc', pin: 's_proc_o' }, to: { node: 's_norm', pin: 's_norm_i' } },
{ id: 's_e2', from: { node: 's_norm', pin: 's_norm_o' }, to: { node: 'sout', pin: 'sout_i' } },
],
}
// ── Middle: Pipeline definition contains ONE Stage instance + a Source primitive. ────────
const pipelineDef = {
title: 'Pipeline',
nodes: [
{ id: 'pin', type: '$templateInput', position: { x: 0, y: 60 }, state: {},
render: { title: 'Input · input', category: 'macro' },
pins: [{ id: 'pin_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'input' }] },
{ id: 'p_src', type: 'Op', position: { x: 220, y: 40 }, size: { x: 160, y: 56 }, state: {},
render: { title: 'Tokenize', category: 'logic' },
pins: [
{ id: 'p_src_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: 'p_src_o', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
] },
// Nested template instance — double-clicking THIS dives a level deeper.
{ id: 'p_stage', type: '$templateInstance', position: { x: 440, y: 40 },
state: { definitionId: 'tpl:stage', pinBoundary: { p_stage_in: 'sin', p_stage_out: 'sout' } },
render: { title: 'Stage', category: 'macro' },
pins: [
{ id: 'p_stage_in', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'value' },
{ id: 'p_stage_out', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'result' },
] },
{ id: 'pout', type: '$templateOutput', position: { x: 660, y: 60 }, state: {},
render: { title: 'Output · output', category: 'macro' },
pins: [{ id: 'pout_i', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'output' }] },
],
edges: [
{ id: 'p_e0', from: { node: 'pin', pin: 'pin_o' }, to: { node: 'p_src', pin: 'p_src_i' } },
{ id: 'p_e1', from: { node: 'p_src', pin: 'p_src_o' }, to: { node: 'p_stage', pin: 'p_stage_in' } },
{ id: 'p_e2', from: { node: 'p_stage', pin: 'p_stage_out' }, to: { node: 'pout', pin: 'pout_i' } },
],
}
// ── Outer document: one Pipeline instance. Double-click to dive in (breadcrumb appears). ──
editor.loadJSON({
version: 'xenolith.v1',
// Templates are an OBJECT keyed by id (matches the file format on disk).
templates: { 'tpl:stage': stageDef, 'tpl:pipeline': pipelineDef } as never,
nodes: [
{ id: 'main', type: '$templateInstance', position: { x: 200, y: 200 },
state: { definitionId: 'tpl:pipeline', pinBoundary: { main_in: 'pin', main_out: 'pout' } },
render: { title: 'Pipeline', category: 'macro' },
pins: [
{ id: 'main_in', kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'input' },
{ id: 'main_out', kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'output' },
] },
],
edges: [],
})
editor.fitView({ padding: 80, maxZoom: 1 })
return {
diveInto: (slug) => {
// "Dive into Pipeline" must work from any depth — `main` lives in the ROOT graph, so pop
// all the way back first. Without this, pressing the button from inside Stage tried to
// find a `main` node in Stage's definition and silently failed.
if (editor.diveDepth !== 0) editor.diveOut(0)
if (slug === 'pipeline') return editor.diveInto('main' as never)
// For Stage: ensure we're inside Pipeline first.
editor.diveInto('main' as never)
return editor.diveInto('p_stage' as never)
},
diveOut: () => editor.diveOut(0),
depth: () => editor.diveDepth,
}
}