Diagram edges
Edges as a diagramming primitive — text nodes wired with directional arrowhead markers, edge labels (pass / fail / retry), and an animated flowing dash on the main path. Toggle the flow.
{
"version": "xenolith.v1",
"nodes": [
{ "id": "start", "type": "Note", "position": { "x": 0, "y": 140 },
"render": { "title": "Start", "category": "logic" },
"pins": [
{ "id": "start:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "start:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] },
{ "id": "build", "type": "Note", "position": { "x": 250, "y": 140 },
"render": { "title": "Build", "category": "logic" },
"pins": [
{ "id": "build:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "build:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] },
{ "id": "test", "type": "Note", "position": { "x": 500, "y": 140 },
"render": { "title": "Run tests", "category": "logic" },
"pins": [
{ "id": "test:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "test:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] },
{ "id": "ship", "type": "Note", "position": { "x": 780, "y": 40 },
"render": { "title": "Ship 🚀", "category": "logic" },
"pins": [
{ "id": "ship:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "ship:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] },
{ "id": "fix", "type": "Note", "position": { "x": 780, "y": 250 },
"render": { "title": "Fix & retry", "category": "logic" },
"pins": [
{ "id": "fix:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "fix:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] }
],
"edges": [
{ "id": "e1", "from": { "node": "start", "pin": "start:out" }, "to": { "node": "build", "pin": "build:in" }, "opts": { "markerEnd": "arrow", "animated": true } },
{ "id": "e2", "from": { "node": "build", "pin": "build:out" }, "to": { "node": "test", "pin": "test:in" }, "opts": { "markerEnd": "arrow", "animated": true } },
{ "id": "e3", "from": { "node": "test", "pin": "test:out" }, "to": { "node": "ship", "pin": "ship:in" }, "opts": { "markerEnd": "arrow", "animated": true, "label": "pass" } },
{ "id": "e4", "from": { "node": "test", "pin": "test:out" }, "to": { "node": "fix", "pin": "fix:in" }, "opts": { "markerEnd": "arrow", "animated": true, "label": "fail" } },
{ "id": "e5", "from": { "node": "fix", "pin": "fix:out" }, "to": { "node": "build", "pin": "build:in" }, "opts": { "markerEnd": "arrow", "animated": true, "label": "retry" } }
]
} import { useEffect, useState } from 'react'
import { XenolithGraph, XenolithControls, XenolithPanel, XenolithButton, useEditor, useEdges } from '@xenolithengine/graph-react'
import { buildDiagram } from '@xenolithengine/demo/diagram'
import { DemoStage } from '../Layout.js'
// Showcase: edges as a diagramming primitive. Text nodes wired with directional edges — arrowhead
// markers, edge labels (pass / fail / retry), and an animated flowing dash on the main path.
//
// Canon: state lives where it's used. The panel owns `animated` AND owns the side-effect that
// pushes it into the editor, because the panel IS inside <XenolithGraph> — `useEditor()` gives
// it the editor directly. `useEdges()` is the public live edge list — no @internal `editor.graph`.
function DiagramPanel() {
const editor = useEditor()
const edges = useEdges()
const [animated, setAnimated] = useState(true)
useEffect(() => {
for (const e of edges) editor.setEdgeOptions(e.id, { animated })
}, [editor, edges, animated])
return (
<XenolithPanel position="top-left" style={{ display: 'flex', flexDirection: 'column', gap: 6, width: 200 }}>
<p style={{ margin: 0, fontSize: 11, textTransform: 'uppercase', letterSpacing: '.05em', color: 'var(--xeno-muted)' }}>Diagram edges</p>
<XenolithButton active={animated} style={{ width: '100%' }} onClick={() => setAnimated(!animated)}>
{animated ? '⏸ Stop flow' : '▶ Animate flow'}
</XenolithButton>
<span style={{ color: 'var(--xeno-muted)', fontSize: 11, lineHeight: 1.45 }}>
Directional edges with arrowheads + labels. The main path animates a flowing dash.
</span>
</XenolithPanel>
)
}
/** Showcase: flowchart-style directional edges (markers, labels, animation). */
export function DiagramDemo(): React.ReactElement {
return (
<DemoStage>
<XenolithGraph className="xeno" resizeToWindow={false} onReady={buildDiagram}>
<XenolithControls position="bottom-left" />
<DiagramPanel />
</XenolithGraph>
</DemoStage>
)
} // The flowchart showcase is just DATA: a xenolith.v1 file loaded with editor.loadJSON. No imperative
// node/edge wiring — the same JSON round-trips through serialization and loads on any host. Edges
// carry the render opts (arrowhead marker, animated flow, label) right in the file.
import type { XenolithEditor, EdgeId } from '@xenolithengine/graph-editor'
import diagram from './diagram.json'
export interface DiagramHandle {
/** Toggle the flowing-dash animation on every edge. */
setAnimated(on: boolean): void
}
export function buildDiagram(editor: XenolithEditor): DiagramHandle {
editor.loadJSON(diagram)
editor.fitView({ padding: 72, maxZoom: 1 })
const edgeIds = diagram.edges.map((e) => e.id as unknown as EdgeId)
return { setAnimated: (on) => { for (const id of edgeIds) editor.setEdgeOptions(id, { animated: on }) } }
} {
"version": "xenolith.v1",
"nodes": [
{ "id": "start", "type": "Note", "position": { "x": 0, "y": 140 },
"render": { "title": "Start", "category": "logic" },
"pins": [
{ "id": "start:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "start:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] },
{ "id": "build", "type": "Note", "position": { "x": 250, "y": 140 },
"render": { "title": "Build", "category": "logic" },
"pins": [
{ "id": "build:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "build:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] },
{ "id": "test", "type": "Note", "position": { "x": 500, "y": 140 },
"render": { "title": "Run tests", "category": "logic" },
"pins": [
{ "id": "test:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "test:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] },
{ "id": "ship", "type": "Note", "position": { "x": 780, "y": 40 },
"render": { "title": "Ship 🚀", "category": "logic" },
"pins": [
{ "id": "ship:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "ship:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] },
{ "id": "fix", "type": "Note", "position": { "x": 780, "y": 250 },
"render": { "title": "Fix & retry", "category": "logic" },
"pins": [
{ "id": "fix:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "fix:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] }
],
"edges": [
{ "id": "e1", "from": { "node": "start", "pin": "start:out" }, "to": { "node": "build", "pin": "build:in" }, "opts": { "markerEnd": "arrow", "animated": true } },
{ "id": "e2", "from": { "node": "build", "pin": "build:out" }, "to": { "node": "test", "pin": "test:in" }, "opts": { "markerEnd": "arrow", "animated": true } },
{ "id": "e3", "from": { "node": "test", "pin": "test:out" }, "to": { "node": "ship", "pin": "ship:in" }, "opts": { "markerEnd": "arrow", "animated": true, "label": "pass" } },
{ "id": "e4", "from": { "node": "test", "pin": "test:out" }, "to": { "node": "fix", "pin": "fix:in" }, "opts": { "markerEnd": "arrow", "animated": true, "label": "fail" } },
{ "id": "e5", "from": { "node": "fix", "pin": "fix:out" }, "to": { "node": "build", "pin": "build:in" }, "opts": { "markerEnd": "arrow", "animated": true, "label": "retry" } }
]
} <script setup lang="ts">
// Vue — the flow toggle lives in a child so useEdges() injects inside <XenolithGraph>.
import { XenolithGraph, XenolithControls } from '@xenolithengine/graph-vue'
import { buildDiagram } from '@xenolithengine/demo/diagram'
import DiagramPanel from './DiagramPanel.vue'
</script>
<template>
<div class="app" style="position:absolute;inset:0;">
<XenolithGraph class="xeno" :resize-to-window="false" @ready="buildDiagram">
<XenolithControls position="bottom-left" />
<DiagramPanel />
</XenolithGraph>
</div>
</template> <script setup lang="ts">
import { ref, watch } from 'vue'
import { XenolithPanel, XenolithButton, useEditor, useEdges } from '@xenolithengine/graph-vue'
const editor = useEditor()
const edges = useEdges()
const animated = ref(true)
// Same contract as the React demo: the flag is pushed onto every current edge.
// setEdgeOptions does not emit, so this watch does not re-enter itself.
watch([edges, animated], () => {
const live = editor.value
if (!live) return
const on = animated.value
for (const edge of edges.value) live.setEdgeOptions(edge.id, { animated: on })
})
</script>
<template>
<XenolithPanel position="top-left">
<div style="display:flex;flex-direction:column;gap:6px;width:200px;">
<p style="margin:0;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--xeno-muted);">Diagram edges</p>
<XenolithButton :active="animated" style="width:100%" @click="animated = !animated">
{{ animated ? '⏸ Stop flow' : '▶ Animate flow' }}
</XenolithButton>
<span style="color:var(--xeno-muted);font-size:11px;line-height:1.45;">
Directional edges with arrowheads + labels. The main path animates a flowing dash.
The toggle writes useEdges() through setEdgeOptions.
</span>
</div>
</XenolithPanel>
</template> // The flowchart showcase is just DATA: a xenolith.v1 file loaded with editor.loadJSON. No imperative
// node/edge wiring — the same JSON round-trips through serialization and loads on any host. Edges
// carry the render opts (arrowhead marker, animated flow, label) right in the file.
import type { XenolithEditor, EdgeId } from '@xenolithengine/graph-editor'
import diagram from './diagram.json'
export interface DiagramHandle {
/** Toggle the flowing-dash animation on every edge. */
setAnimated(on: boolean): void
}
export function buildDiagram(editor: XenolithEditor): DiagramHandle {
editor.loadJSON(diagram)
editor.fitView({ padding: 72, maxZoom: 1 })
const edgeIds = diagram.edges.map((e) => e.id as unknown as EdgeId)
return { setAnimated: (on) => { for (const id of edgeIds) editor.setEdgeOptions(id, { animated: on }) } }
} {
"version": "xenolith.v1",
"nodes": [
{ "id": "start", "type": "Note", "position": { "x": 0, "y": 140 },
"render": { "title": "Start", "category": "logic" },
"pins": [
{ "id": "start:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "start:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] },
{ "id": "build", "type": "Note", "position": { "x": 250, "y": 140 },
"render": { "title": "Build", "category": "logic" },
"pins": [
{ "id": "build:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "build:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] },
{ "id": "test", "type": "Note", "position": { "x": 500, "y": 140 },
"render": { "title": "Run tests", "category": "logic" },
"pins": [
{ "id": "test:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "test:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] },
{ "id": "ship", "type": "Note", "position": { "x": 780, "y": 40 },
"render": { "title": "Ship 🚀", "category": "logic" },
"pins": [
{ "id": "ship:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "ship:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] },
{ "id": "fix", "type": "Note", "position": { "x": 780, "y": 250 },
"render": { "title": "Fix & retry", "category": "logic" },
"pins": [
{ "id": "fix:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "fix:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] }
],
"edges": [
{ "id": "e1", "from": { "node": "start", "pin": "start:out" }, "to": { "node": "build", "pin": "build:in" }, "opts": { "markerEnd": "arrow", "animated": true } },
{ "id": "e2", "from": { "node": "build", "pin": "build:out" }, "to": { "node": "test", "pin": "test:in" }, "opts": { "markerEnd": "arrow", "animated": true } },
{ "id": "e3", "from": { "node": "test", "pin": "test:out" }, "to": { "node": "ship", "pin": "ship:in" }, "opts": { "markerEnd": "arrow", "animated": true, "label": "pass" } },
{ "id": "e4", "from": { "node": "test", "pin": "test:out" }, "to": { "node": "fix", "pin": "fix:in" }, "opts": { "markerEnd": "arrow", "animated": true, "label": "fail" } },
{ "id": "e5", "from": { "node": "fix", "pin": "fix:out" }, "to": { "node": "build", "pin": "build:in" }, "opts": { "markerEnd": "arrow", "animated": true, "label": "retry" } }
]
} <!-- Svelte — edges is a store. The effect pushes `animated` onto every current edge. -->
<script lang="ts">
import { xenolith, createXenolithStores, createXenolithEditorContext } from '@xenolithengine/graph-svelte'
import { XenolithPanel, XenolithButton, XenolithControls } from '@xenolithengine/graph-svelte/components'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import { buildDiagram } from '@xenolithengine/demo/diagram'
const stores = createXenolithStores()
const edges = stores.edges
const panelEditor = createXenolithEditorContext()
let editor = $state<XenolithEditor | null>(null)
let animated = $state(true)
$effect(() => {
const live = editor
const list = $edges
const on = animated
if (!live) return
for (const edge of list) live.setEdgeOptions(edge.id, { animated: on })
})
</script>
<div
use:xenolith={{ resizeToWindow: false }}
onready={(e) => { editor = e.detail; panelEditor.set(e.detail); stores.editor.set(e.detail); buildDiagram(e.detail) }}
style="position:absolute;inset:0"
></div>
<XenolithControls position="bottom-left" />
<XenolithPanel position="top-left">
<div style="display:flex;flex-direction:column;gap:6px;width:200px;">
<p style="margin:0;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--xeno-muted);">Diagram edges</p>
<XenolithButton active={animated} style="width:100%" onclick={() => { animated = !animated }}>
{animated ? '⏸ Stop flow' : '▶ Animate flow'}
</XenolithButton>
<span style="color:var(--xeno-muted);font-size:11px;line-height:1.45;">
Directional edges with arrowheads + labels. The main path animates a flowing dash.
The toggle writes the edges store through setEdgeOptions.
</span>
</div>
</XenolithPanel> // The flowchart showcase is just DATA: a xenolith.v1 file loaded with editor.loadJSON. No imperative
// node/edge wiring — the same JSON round-trips through serialization and loads on any host. Edges
// carry the render opts (arrowhead marker, animated flow, label) right in the file.
import type { XenolithEditor, EdgeId } from '@xenolithengine/graph-editor'
import diagram from './diagram.json'
export interface DiagramHandle {
/** Toggle the flowing-dash animation on every edge. */
setAnimated(on: boolean): void
}
export function buildDiagram(editor: XenolithEditor): DiagramHandle {
editor.loadJSON(diagram)
editor.fitView({ padding: 72, maxZoom: 1 })
const edgeIds = diagram.edges.map((e) => e.id as unknown as EdgeId)
return { setAnimated: (on) => { for (const id of edgeIds) editor.setEdgeOptions(id, { animated: on }) } }
} {
"version": "xenolith.v1",
"nodes": [
{ "id": "start", "type": "Note", "position": { "x": 0, "y": 140 },
"render": { "title": "Start", "category": "logic" },
"pins": [
{ "id": "start:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "start:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] },
{ "id": "build", "type": "Note", "position": { "x": 250, "y": 140 },
"render": { "title": "Build", "category": "logic" },
"pins": [
{ "id": "build:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "build:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] },
{ "id": "test", "type": "Note", "position": { "x": 500, "y": 140 },
"render": { "title": "Run tests", "category": "logic" },
"pins": [
{ "id": "test:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "test:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] },
{ "id": "ship", "type": "Note", "position": { "x": 780, "y": 40 },
"render": { "title": "Ship 🚀", "category": "logic" },
"pins": [
{ "id": "ship:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "ship:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] },
{ "id": "fix", "type": "Note", "position": { "x": 780, "y": 250 },
"render": { "title": "Fix & retry", "category": "logic" },
"pins": [
{ "id": "fix:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "fix:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] }
],
"edges": [
{ "id": "e1", "from": { "node": "start", "pin": "start:out" }, "to": { "node": "build", "pin": "build:in" }, "opts": { "markerEnd": "arrow", "animated": true } },
{ "id": "e2", "from": { "node": "build", "pin": "build:out" }, "to": { "node": "test", "pin": "test:in" }, "opts": { "markerEnd": "arrow", "animated": true } },
{ "id": "e3", "from": { "node": "test", "pin": "test:out" }, "to": { "node": "ship", "pin": "ship:in" }, "opts": { "markerEnd": "arrow", "animated": true, "label": "pass" } },
{ "id": "e4", "from": { "node": "test", "pin": "test:out" }, "to": { "node": "fix", "pin": "fix:in" }, "opts": { "markerEnd": "arrow", "animated": true, "label": "fail" } },
{ "id": "e5", "from": { "node": "fix", "pin": "fix:out" }, "to": { "node": "build", "pin": "build:in" }, "opts": { "markerEnd": "arrow", "animated": true, "label": "retry" } }
]
} // Solid — no panel package. stores.edges() is the live list; the effect writes the flag.
import { createEffect, createSignal } from 'solid-js'
import { xenolith, createXenolithStores } from '@xenolithengine/graph-solid'
import { buildDiagram } from '@xenolithengine/demo/diagram'
export function DiagramDemo() {
const stores = createXenolithStores()
const [animated, setAnimated] = createSignal(true)
createEffect(() => {
const live = stores.editor()
const list = stores.edges()
const on = animated()
if (!live) return
for (const edge of list) live.setEdgeOptions(edge.id, { animated: on })
})
const btn = (on: boolean) =>
`font:inherit;font-size:13px;padding:7px 12px;border-radius:8px;cursor:pointer;width:100%;border:1px solid ${on ? 'var(--xeno-accent)' : 'var(--xeno-border)'};background:${on ? 'var(--xeno-accent)' : 'var(--xeno-elevated)'};color:${on ? 'var(--xeno-canvas)' : 'var(--xeno-text)'};`
return (
<div
use:xenolith={{ resizeToWindow: false }}
on:ready={(e) => {
stores.setEditor(e.detail)
e.detail.chrome.setControls({ position: 'bottom-left' })
buildDiagram(e.detail)
}}
style="position:absolute;inset:0"
>
<div style="position:absolute;top:12px;left:12px;z-index:5;display:flex;flex-direction:column;gap:6px;width:200px;padding:10px;background:var(--xeno-panel);border:1px solid var(--xeno-border);border-radius:10px;color:var(--xeno-text);font:13px system-ui,sans-serif;">
<p style="margin:0;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--xeno-muted);">Diagram edges</p>
<button type="button" style={btn(animated())} onClick={() => setAnimated(!animated())}>
{animated() ? '⏸ Stop flow' : '▶ Animate flow'}
</button>
<span style="color:var(--xeno-muted);font-size:11px;line-height:1.45;">
Directional edges with arrowheads + labels. The main path animates a flowing dash.
The toggle writes stores.edges() through setEdgeOptions.
</span>
</div>
</div>
)
} // The flowchart showcase is just DATA: a xenolith.v1 file loaded with editor.loadJSON. No imperative
// node/edge wiring — the same JSON round-trips through serialization and loads on any host. Edges
// carry the render opts (arrowhead marker, animated flow, label) right in the file.
import type { XenolithEditor, EdgeId } from '@xenolithengine/graph-editor'
import diagram from './diagram.json'
export interface DiagramHandle {
/** Toggle the flowing-dash animation on every edge. */
setAnimated(on: boolean): void
}
export function buildDiagram(editor: XenolithEditor): DiagramHandle {
editor.loadJSON(diagram)
editor.fitView({ padding: 72, maxZoom: 1 })
const edgeIds = diagram.edges.map((e) => e.id as unknown as EdgeId)
return { setAnimated: (on) => { for (const id of edgeIds) editor.setEdgeOptions(id, { animated: on }) } }
} {
"version": "xenolith.v1",
"nodes": [
{ "id": "start", "type": "Note", "position": { "x": 0, "y": 140 },
"render": { "title": "Start", "category": "logic" },
"pins": [
{ "id": "start:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "start:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] },
{ "id": "build", "type": "Note", "position": { "x": 250, "y": 140 },
"render": { "title": "Build", "category": "logic" },
"pins": [
{ "id": "build:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "build:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] },
{ "id": "test", "type": "Note", "position": { "x": 500, "y": 140 },
"render": { "title": "Run tests", "category": "logic" },
"pins": [
{ "id": "test:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "test:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] },
{ "id": "ship", "type": "Note", "position": { "x": 780, "y": 40 },
"render": { "title": "Ship 🚀", "category": "logic" },
"pins": [
{ "id": "ship:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "ship:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] },
{ "id": "fix", "type": "Note", "position": { "x": 780, "y": 250 },
"render": { "title": "Fix & retry", "category": "logic" },
"pins": [
{ "id": "fix:in", "kind": "data", "direction": "in", "type": "flow", "multiple": true },
{ "id": "fix:out", "kind": "data", "direction": "out", "type": "flow", "multiple": true }
] }
],
"edges": [
{ "id": "e1", "from": { "node": "start", "pin": "start:out" }, "to": { "node": "build", "pin": "build:in" }, "opts": { "markerEnd": "arrow", "animated": true } },
{ "id": "e2", "from": { "node": "build", "pin": "build:out" }, "to": { "node": "test", "pin": "test:in" }, "opts": { "markerEnd": "arrow", "animated": true } },
{ "id": "e3", "from": { "node": "test", "pin": "test:out" }, "to": { "node": "ship", "pin": "ship:in" }, "opts": { "markerEnd": "arrow", "animated": true, "label": "pass" } },
{ "id": "e4", "from": { "node": "test", "pin": "test:out" }, "to": { "node": "fix", "pin": "fix:in" }, "opts": { "markerEnd": "arrow", "animated": true, "label": "fail" } },
{ "id": "e5", "from": { "node": "fix", "pin": "fix:out" }, "to": { "node": "build", "pin": "build:in" }, "opts": { "markerEnd": "arrow", "animated": true, "label": "retry" } }
]
} // Angular — edges$ re-applies the flow flag. No panel component: the toggle is a DOM card,
// the viewport controls are editor.chrome.
import { AfterViewInit, Component, ElementRef, OnDestroy, inject, signal, viewChild } from '@angular/core'
import { Subscription } from 'rxjs'
import { XenolithGraphService } from '@xenolithengine/graph-angular'
import { buildDiagram } from '@xenolithengine/demo/diagram'
@Component({
selector: 'diagram-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">
<p class="label">Diagram edges</p>
<button type="button" class="btn" [class.on]="animated()" (click)="toggle()">
{{ animated() ? '⏸ Stop flow' : '▶ Animate flow' }}
</button>
<span class="note">
Directional edges with arrowheads + labels. The main path animates a flowing dash.
The toggle writes edges$ through setEdgeOptions.
</span>
</div>
</div>
`,
styles: [`
.card { position:absolute; top:12px; left:12px; z-index:5; display:flex; flex-direction:column;
gap:6px; width:200px; padding:10px; 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; }
.label { margin:0; font-size:11px; text-transform:uppercase; letter-spacing:.05em;
color:var(--xeno-muted,#9a9a9a); }
.btn { font:inherit; font-size:13px; padding:7px 12px; cursor:pointer; border-radius:8px;
border:1px solid var(--xeno-border,#333); background:var(--xeno-elevated,#262626);
color:var(--xeno-text,#cfcfcf); }
.btn.on { border-color:var(--xeno-accent,#FCB400); background:var(--xeno-accent,#FCB400);
color:var(--xeno-canvas,#111); }
.note { color:var(--xeno-muted,#9a9a9a); font-size:11px; line-height:1.45; }
`],
})
export class DiagramDemoComponent implements AfterViewInit, OnDestroy {
private graph = inject(XenolithGraphService)
private host = viewChild.required<ElementRef<HTMLDivElement>>('host')
private sub = new Subscription()
animated = signal(true)
async ngAfterViewInit(): Promise<void> {
const editor = await this.graph.mount(this.host().nativeElement, { resizeToWindow: false })
editor.chrome.setControls({ position: 'bottom-left' })
buildDiagram(editor)
this.sub.add(this.graph.edges$.subscribe(() => this.applyFlow()))
}
toggle(): void {
this.animated.update((on) => !on)
this.applyFlow()
}
private applyFlow(): void {
const editor = this.graph.editor
if (!editor) return
const on = this.animated()
for (const edge of editor.graphEdges()) editor.setEdgeOptions(edge.id, { animated: on })
}
ngOnDestroy(): void {
this.sub.unsubscribe()
this.graph.destroy()
}
} // The flowchart showcase is just DATA: a xenolith.v1 file loaded with editor.loadJSON. No imperative
// node/edge wiring — the same JSON round-trips through serialization and loads on any host. Edges
// carry the render opts (arrowhead marker, animated flow, label) right in the file.
import type { XenolithEditor, EdgeId } from '@xenolithengine/graph-editor'
import diagram from './diagram.json'
export interface DiagramHandle {
/** Toggle the flowing-dash animation on every edge. */
setAnimated(on: boolean): void
}
export function buildDiagram(editor: XenolithEditor): DiagramHandle {
editor.loadJSON(diagram)
editor.fitView({ padding: 72, maxZoom: 1 })
const edgeIds = diagram.edges.map((e) => e.id as unknown as EdgeId)
return { setAnimated: (on) => { for (const id of edgeIds) editor.setEdgeOptions(id, { animated: on }) } }
}