Palette sidebar (drag to spawn)
16 schemas across 5 categories (data / math / transform / logic / io) listed in a docked palette on the left. Drag any tile onto the canvas — the editor inserts the node at the drop point via its built-in `node:drop` handler. Configure with `editor.setPaletteSidebar({ side, filter })`.
// Vanilla mount — Palette sidebar. The schemas + sidebar config live in the shared package; this
// file just spins up an editor and points it at them. Drag any tile from the left rail onto the
// canvas to spawn a node at the drop point.
import { XenolithEditor } from '@xenolithengine/graph-editor'
import { buildPaletteSidebar } from '@xenolithengine/demo/palette-sidebar'
export async function mount(target: HTMLElement): Promise<() => void> {
const editor = await XenolithEditor.init(target, { resizeToWindow: false, minimap: false })
buildPaletteSidebar(editor)
editor.view.fitView({ padding: 80, maxZoom: 1 })
return () => editor.destroy()
} // Palette sidebar showcase: rich registry across 5 categories so users see the panel grouping
// them, search them, and drag-spawn into the canvas. No special host code — the editor's default
// drop handler inserts the node at the world position automatically when the user drops a tile.
//
// Shared between vanilla + React demos.
import type { NodeSchema, XenolithEditor } from '@xenolithengine/graph-editor'
const stringPin = (dir: 'in' | 'out', label: string) =>
({ kind: 'data' as const, direction: dir, type: 'string', label, multiple: dir === 'out' })
const numberPin = (dir: 'in' | 'out', label: string) =>
({ kind: 'data' as const, direction: dir, type: 'number', label, multiple: dir === 'out' })
const boolPin = (dir: 'in' | 'out', label: string) =>
({ kind: 'data' as const, direction: dir, type: 'boolean', label, multiple: dir === 'out' })
export const paletteSchemas: NodeSchema[] = [
// DATA — leaf sources, no inputs
{ type: 'Const', title: 'Constant', category: 'data', description: 'A fixed number.', keywords: ['number', 'value'], pins: [numberPin('out', 'Value')],
widgets: [{ id: 'value', type: 'number', key: 'value', label: 'Value', step: 1, freeFloating: true }] },
{ type: 'TextValue', title: 'Text', category: 'data', description: 'A fixed text string.', keywords: ['string', 'literal'], pins: [stringPin('out', 'Text')],
widgets: [{ id: 'text', type: 'text', key: 'text', label: 'Text', placeholder: 'hello', freeFloating: true }] },
{ type: 'Random', title: 'Random', category: 'data', description: 'Pseudo-random 0..1 on read.', keywords: ['rand', 'noise'], pins: [numberPin('out', 'Out')] },
// MATH — arithmetic & trig
{ type: 'Add', title: 'Add', category: 'math', description: 'A + B', keywords: ['plus', 'sum'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Sum')] },
{ type: 'Subtract', title: 'Subtract', category: 'math', description: 'A − B', keywords: ['minus', 'diff'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Diff')] },
{ type: 'Multiply', title: 'Multiply', category: 'math', description: 'A × B', keywords: ['times', 'mul'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Product')] },
{ type: 'Divide', title: 'Divide', category: 'math', description: 'A ÷ B', keywords: ['div', 'fraction'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Quotient')] },
{ type: 'Sin', title: 'Sin', category: 'math', description: 'sin(x), radians.', keywords: ['trig'], pins: [numberPin('in', 'X'), numberPin('out', 'Out')] },
// TRANSFORM — string ops
{ type: 'ToUpper', title: 'To Upper', category: 'transform', description: 'Uppercase a string.', keywords: ['caps', 'upper'], pins: [stringPin('in', 'In'), stringPin('out', 'Out')] },
{ type: 'ToLower', title: 'To Lower', category: 'transform', description: 'Lowercase a string.', keywords: ['case'], pins: [stringPin('in', 'In'), stringPin('out', 'Out')] },
{ type: 'Trim', title: 'Trim', category: 'transform', description: 'Strip leading/trailing whitespace.', keywords: ['strip'], pins: [stringPin('in', 'In'), stringPin('out', 'Out')] },
{ type: 'Concat', title: 'Concat', category: 'transform', description: 'Join two strings.', keywords: ['join'], pins: [stringPin('in', 'A'), stringPin('in', 'B'), stringPin('out', 'Out')] },
// LOGIC — booleans + control
{ type: 'IfElse', title: 'If / Else', category: 'logic', description: 'Pick A when cond is true, else B.', keywords: ['cond', 'pick'],
pins: [boolPin('in', 'Cond'), numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Out')] },
{ type: 'Equals', title: 'Equals', category: 'logic', description: 'A == B', keywords: ['eq', 'cmp'],
pins: [numberPin('in', 'A'), numberPin('in', 'B'), boolPin('out', 'Out')] },
{ type: 'Not', title: 'Not', category: 'logic', description: 'Invert a boolean.', keywords: ['invert'],
pins: [boolPin('in', 'In'), boolPin('out', 'Out')] },
// IO — terminal sinks / readouts
{ type: 'Output', title: 'Output', category: 'io', description: 'Show the value in a read-only widget.', keywords: ['print', 'sink'],
pins: [numberPin('in', 'In')],
widgets: [{ id: 'result', type: 'text', key: 'result', label: 'Result', placeholder: '—', freeFloating: true, disabled: true }] },
{ type: 'Log', title: 'Log', category: 'io', description: 'Console.log on every change.', keywords: ['debug', 'print'],
pins: [stringPin('in', 'In')] },
]
/** Register schemas + mount palette sidebar. Filters out built-ins ($templateInstance, $reroute,
* etc) so the panel only shows the user-facing types this demo cares about. */
export function buildPaletteSidebar(editor: XenolithEditor): void {
for (const s of paletteSchemas) editor.registry.register(s)
const ourTypes = new Set(paletteSchemas.map((s) => s.type))
editor.setPaletteSidebar({ side: 'left', filter: (s) => ourTypes.has(s.type) })
} // React demo — Palette sidebar. Same setup as the vanilla version: schemas + sidebar config from
// the shared package, mounted in `onReady`. The default `node:drop` handler the editor wires up
// is enough — drag a tile from the rail and drop on the canvas; a fresh node spawns at the world
// position. Listen to `node:drop` yourself if you want to layer on validation / snapping.
import { XenolithGraph } from '@xenolithengine/graph-react'
import { buildPaletteSidebar } from '@xenolithengine/demo/palette-sidebar'
import { DemoStage } from '../Layout.js'
export function PaletteSidebarDemo() {
return (
<DemoStage>
<XenolithGraph
className="xeno"
resizeToWindow={false}
onReady={(editor) => {
buildPaletteSidebar(editor)
editor.view.fitView({ padding: 80, maxZoom: 1 })
}}
/>
</DemoStage>
)
} // Palette sidebar showcase: rich registry across 5 categories so users see the panel grouping
// them, search them, and drag-spawn into the canvas. No special host code — the editor's default
// drop handler inserts the node at the world position automatically when the user drops a tile.
//
// Shared between vanilla + React demos.
import type { NodeSchema, XenolithEditor } from '@xenolithengine/graph-editor'
const stringPin = (dir: 'in' | 'out', label: string) =>
({ kind: 'data' as const, direction: dir, type: 'string', label, multiple: dir === 'out' })
const numberPin = (dir: 'in' | 'out', label: string) =>
({ kind: 'data' as const, direction: dir, type: 'number', label, multiple: dir === 'out' })
const boolPin = (dir: 'in' | 'out', label: string) =>
({ kind: 'data' as const, direction: dir, type: 'boolean', label, multiple: dir === 'out' })
export const paletteSchemas: NodeSchema[] = [
// DATA — leaf sources, no inputs
{ type: 'Const', title: 'Constant', category: 'data', description: 'A fixed number.', keywords: ['number', 'value'], pins: [numberPin('out', 'Value')],
widgets: [{ id: 'value', type: 'number', key: 'value', label: 'Value', step: 1, freeFloating: true }] },
{ type: 'TextValue', title: 'Text', category: 'data', description: 'A fixed text string.', keywords: ['string', 'literal'], pins: [stringPin('out', 'Text')],
widgets: [{ id: 'text', type: 'text', key: 'text', label: 'Text', placeholder: 'hello', freeFloating: true }] },
{ type: 'Random', title: 'Random', category: 'data', description: 'Pseudo-random 0..1 on read.', keywords: ['rand', 'noise'], pins: [numberPin('out', 'Out')] },
// MATH — arithmetic & trig
{ type: 'Add', title: 'Add', category: 'math', description: 'A + B', keywords: ['plus', 'sum'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Sum')] },
{ type: 'Subtract', title: 'Subtract', category: 'math', description: 'A − B', keywords: ['minus', 'diff'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Diff')] },
{ type: 'Multiply', title: 'Multiply', category: 'math', description: 'A × B', keywords: ['times', 'mul'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Product')] },
{ type: 'Divide', title: 'Divide', category: 'math', description: 'A ÷ B', keywords: ['div', 'fraction'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Quotient')] },
{ type: 'Sin', title: 'Sin', category: 'math', description: 'sin(x), radians.', keywords: ['trig'], pins: [numberPin('in', 'X'), numberPin('out', 'Out')] },
// TRANSFORM — string ops
{ type: 'ToUpper', title: 'To Upper', category: 'transform', description: 'Uppercase a string.', keywords: ['caps', 'upper'], pins: [stringPin('in', 'In'), stringPin('out', 'Out')] },
{ type: 'ToLower', title: 'To Lower', category: 'transform', description: 'Lowercase a string.', keywords: ['case'], pins: [stringPin('in', 'In'), stringPin('out', 'Out')] },
{ type: 'Trim', title: 'Trim', category: 'transform', description: 'Strip leading/trailing whitespace.', keywords: ['strip'], pins: [stringPin('in', 'In'), stringPin('out', 'Out')] },
{ type: 'Concat', title: 'Concat', category: 'transform', description: 'Join two strings.', keywords: ['join'], pins: [stringPin('in', 'A'), stringPin('in', 'B'), stringPin('out', 'Out')] },
// LOGIC — booleans + control
{ type: 'IfElse', title: 'If / Else', category: 'logic', description: 'Pick A when cond is true, else B.', keywords: ['cond', 'pick'],
pins: [boolPin('in', 'Cond'), numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Out')] },
{ type: 'Equals', title: 'Equals', category: 'logic', description: 'A == B', keywords: ['eq', 'cmp'],
pins: [numberPin('in', 'A'), numberPin('in', 'B'), boolPin('out', 'Out')] },
{ type: 'Not', title: 'Not', category: 'logic', description: 'Invert a boolean.', keywords: ['invert'],
pins: [boolPin('in', 'In'), boolPin('out', 'Out')] },
// IO — terminal sinks / readouts
{ type: 'Output', title: 'Output', category: 'io', description: 'Show the value in a read-only widget.', keywords: ['print', 'sink'],
pins: [numberPin('in', 'In')],
widgets: [{ id: 'result', type: 'text', key: 'result', label: 'Result', placeholder: '—', freeFloating: true, disabled: true }] },
{ type: 'Log', title: 'Log', category: 'io', description: 'Console.log on every change.', keywords: ['debug', 'print'],
pins: [stringPin('in', 'In')] },
]
/** Register schemas + mount palette sidebar. Filters out built-ins ($templateInstance, $reroute,
* etc) so the panel only shows the user-facing types this demo cares about. */
export function buildPaletteSidebar(editor: XenolithEditor): void {
for (const s of paletteSchemas) editor.registry.register(s)
const ourTypes = new Set(paletteSchemas.map((s) => s.type))
editor.setPaletteSidebar({ side: 'left', filter: (s) => ourTypes.has(s.type) })
} <script setup lang="ts">
// Vue SFC — palette sidebar. Schemas + sidebar config from the shared package; the editor's
// built-in `node:drop` handler spawns the dragged node at the drop point.
import { XenolithGraph } from '@xenolithengine/graph-vue'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import { buildPaletteSidebar } from '@xenolithengine/demo/palette-sidebar'
function onReady(editor: XenolithEditor): void {
buildPaletteSidebar(editor)
editor.view.fitView({ padding: 80, maxZoom: 1 })
}
</script>
<template>
<div class="app" style="position:absolute;inset:0;">
<XenolithGraph class="xeno" :resize-to-window="false" @ready="onReady" />
</div>
</template> // Palette sidebar showcase: rich registry across 5 categories so users see the panel grouping
// them, search them, and drag-spawn into the canvas. No special host code — the editor's default
// drop handler inserts the node at the world position automatically when the user drops a tile.
//
// Shared between vanilla + React demos.
import type { NodeSchema, XenolithEditor } from '@xenolithengine/graph-editor'
const stringPin = (dir: 'in' | 'out', label: string) =>
({ kind: 'data' as const, direction: dir, type: 'string', label, multiple: dir === 'out' })
const numberPin = (dir: 'in' | 'out', label: string) =>
({ kind: 'data' as const, direction: dir, type: 'number', label, multiple: dir === 'out' })
const boolPin = (dir: 'in' | 'out', label: string) =>
({ kind: 'data' as const, direction: dir, type: 'boolean', label, multiple: dir === 'out' })
export const paletteSchemas: NodeSchema[] = [
// DATA — leaf sources, no inputs
{ type: 'Const', title: 'Constant', category: 'data', description: 'A fixed number.', keywords: ['number', 'value'], pins: [numberPin('out', 'Value')],
widgets: [{ id: 'value', type: 'number', key: 'value', label: 'Value', step: 1, freeFloating: true }] },
{ type: 'TextValue', title: 'Text', category: 'data', description: 'A fixed text string.', keywords: ['string', 'literal'], pins: [stringPin('out', 'Text')],
widgets: [{ id: 'text', type: 'text', key: 'text', label: 'Text', placeholder: 'hello', freeFloating: true }] },
{ type: 'Random', title: 'Random', category: 'data', description: 'Pseudo-random 0..1 on read.', keywords: ['rand', 'noise'], pins: [numberPin('out', 'Out')] },
// MATH — arithmetic & trig
{ type: 'Add', title: 'Add', category: 'math', description: 'A + B', keywords: ['plus', 'sum'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Sum')] },
{ type: 'Subtract', title: 'Subtract', category: 'math', description: 'A − B', keywords: ['minus', 'diff'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Diff')] },
{ type: 'Multiply', title: 'Multiply', category: 'math', description: 'A × B', keywords: ['times', 'mul'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Product')] },
{ type: 'Divide', title: 'Divide', category: 'math', description: 'A ÷ B', keywords: ['div', 'fraction'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Quotient')] },
{ type: 'Sin', title: 'Sin', category: 'math', description: 'sin(x), radians.', keywords: ['trig'], pins: [numberPin('in', 'X'), numberPin('out', 'Out')] },
// TRANSFORM — string ops
{ type: 'ToUpper', title: 'To Upper', category: 'transform', description: 'Uppercase a string.', keywords: ['caps', 'upper'], pins: [stringPin('in', 'In'), stringPin('out', 'Out')] },
{ type: 'ToLower', title: 'To Lower', category: 'transform', description: 'Lowercase a string.', keywords: ['case'], pins: [stringPin('in', 'In'), stringPin('out', 'Out')] },
{ type: 'Trim', title: 'Trim', category: 'transform', description: 'Strip leading/trailing whitespace.', keywords: ['strip'], pins: [stringPin('in', 'In'), stringPin('out', 'Out')] },
{ type: 'Concat', title: 'Concat', category: 'transform', description: 'Join two strings.', keywords: ['join'], pins: [stringPin('in', 'A'), stringPin('in', 'B'), stringPin('out', 'Out')] },
// LOGIC — booleans + control
{ type: 'IfElse', title: 'If / Else', category: 'logic', description: 'Pick A when cond is true, else B.', keywords: ['cond', 'pick'],
pins: [boolPin('in', 'Cond'), numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Out')] },
{ type: 'Equals', title: 'Equals', category: 'logic', description: 'A == B', keywords: ['eq', 'cmp'],
pins: [numberPin('in', 'A'), numberPin('in', 'B'), boolPin('out', 'Out')] },
{ type: 'Not', title: 'Not', category: 'logic', description: 'Invert a boolean.', keywords: ['invert'],
pins: [boolPin('in', 'In'), boolPin('out', 'Out')] },
// IO — terminal sinks / readouts
{ type: 'Output', title: 'Output', category: 'io', description: 'Show the value in a read-only widget.', keywords: ['print', 'sink'],
pins: [numberPin('in', 'In')],
widgets: [{ id: 'result', type: 'text', key: 'result', label: 'Result', placeholder: '—', freeFloating: true, disabled: true }] },
{ type: 'Log', title: 'Log', category: 'io', description: 'Console.log on every change.', keywords: ['debug', 'print'],
pins: [stringPin('in', 'In')] },
]
/** Register schemas + mount palette sidebar. Filters out built-ins ($templateInstance, $reroute,
* etc) so the panel only shows the user-facing types this demo cares about. */
export function buildPaletteSidebar(editor: XenolithEditor): void {
for (const s of paletteSchemas) editor.registry.register(s)
const ourTypes = new Set(paletteSchemas.map((s) => s.type))
editor.setPaletteSidebar({ side: 'left', filter: (s) => ourTypes.has(s.type) })
} // Svelte adapter — palette sidebar. Schemas + sidebar config come from the shared package; the
// editor's built-in `node:drop` handler spawns the dragged node at the drop point.
import { createXenolithGraph } from '@xenolithengine/graph-svelte'
import { buildPaletteSidebar } from '@xenolithengine/demo/palette-sidebar'
export async function mount(target: HTMLElement): Promise<() => void> {
const slot = document.createElement('div')
slot.style.cssText = 'position:absolute;inset:0;'
target.appendChild(slot)
const binding = await createXenolithGraph(slot, { resizeToWindow: false })
buildPaletteSidebar(binding.editor)
binding.editor.view.fitView({ padding: 80, maxZoom: 1 })
return () => { binding.destroy(); slot.remove() }
} // Palette sidebar showcase: rich registry across 5 categories so users see the panel grouping
// them, search them, and drag-spawn into the canvas. No special host code — the editor's default
// drop handler inserts the node at the world position automatically when the user drops a tile.
//
// Shared between vanilla + React demos.
import type { NodeSchema, XenolithEditor } from '@xenolithengine/graph-editor'
const stringPin = (dir: 'in' | 'out', label: string) =>
({ kind: 'data' as const, direction: dir, type: 'string', label, multiple: dir === 'out' })
const numberPin = (dir: 'in' | 'out', label: string) =>
({ kind: 'data' as const, direction: dir, type: 'number', label, multiple: dir === 'out' })
const boolPin = (dir: 'in' | 'out', label: string) =>
({ kind: 'data' as const, direction: dir, type: 'boolean', label, multiple: dir === 'out' })
export const paletteSchemas: NodeSchema[] = [
// DATA — leaf sources, no inputs
{ type: 'Const', title: 'Constant', category: 'data', description: 'A fixed number.', keywords: ['number', 'value'], pins: [numberPin('out', 'Value')],
widgets: [{ id: 'value', type: 'number', key: 'value', label: 'Value', step: 1, freeFloating: true }] },
{ type: 'TextValue', title: 'Text', category: 'data', description: 'A fixed text string.', keywords: ['string', 'literal'], pins: [stringPin('out', 'Text')],
widgets: [{ id: 'text', type: 'text', key: 'text', label: 'Text', placeholder: 'hello', freeFloating: true }] },
{ type: 'Random', title: 'Random', category: 'data', description: 'Pseudo-random 0..1 on read.', keywords: ['rand', 'noise'], pins: [numberPin('out', 'Out')] },
// MATH — arithmetic & trig
{ type: 'Add', title: 'Add', category: 'math', description: 'A + B', keywords: ['plus', 'sum'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Sum')] },
{ type: 'Subtract', title: 'Subtract', category: 'math', description: 'A − B', keywords: ['minus', 'diff'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Diff')] },
{ type: 'Multiply', title: 'Multiply', category: 'math', description: 'A × B', keywords: ['times', 'mul'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Product')] },
{ type: 'Divide', title: 'Divide', category: 'math', description: 'A ÷ B', keywords: ['div', 'fraction'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Quotient')] },
{ type: 'Sin', title: 'Sin', category: 'math', description: 'sin(x), radians.', keywords: ['trig'], pins: [numberPin('in', 'X'), numberPin('out', 'Out')] },
// TRANSFORM — string ops
{ type: 'ToUpper', title: 'To Upper', category: 'transform', description: 'Uppercase a string.', keywords: ['caps', 'upper'], pins: [stringPin('in', 'In'), stringPin('out', 'Out')] },
{ type: 'ToLower', title: 'To Lower', category: 'transform', description: 'Lowercase a string.', keywords: ['case'], pins: [stringPin('in', 'In'), stringPin('out', 'Out')] },
{ type: 'Trim', title: 'Trim', category: 'transform', description: 'Strip leading/trailing whitespace.', keywords: ['strip'], pins: [stringPin('in', 'In'), stringPin('out', 'Out')] },
{ type: 'Concat', title: 'Concat', category: 'transform', description: 'Join two strings.', keywords: ['join'], pins: [stringPin('in', 'A'), stringPin('in', 'B'), stringPin('out', 'Out')] },
// LOGIC — booleans + control
{ type: 'IfElse', title: 'If / Else', category: 'logic', description: 'Pick A when cond is true, else B.', keywords: ['cond', 'pick'],
pins: [boolPin('in', 'Cond'), numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Out')] },
{ type: 'Equals', title: 'Equals', category: 'logic', description: 'A == B', keywords: ['eq', 'cmp'],
pins: [numberPin('in', 'A'), numberPin('in', 'B'), boolPin('out', 'Out')] },
{ type: 'Not', title: 'Not', category: 'logic', description: 'Invert a boolean.', keywords: ['invert'],
pins: [boolPin('in', 'In'), boolPin('out', 'Out')] },
// IO — terminal sinks / readouts
{ type: 'Output', title: 'Output', category: 'io', description: 'Show the value in a read-only widget.', keywords: ['print', 'sink'],
pins: [numberPin('in', 'In')],
widgets: [{ id: 'result', type: 'text', key: 'result', label: 'Result', placeholder: '—', freeFloating: true, disabled: true }] },
{ type: 'Log', title: 'Log', category: 'io', description: 'Console.log on every change.', keywords: ['debug', 'print'],
pins: [stringPin('in', 'In')] },
]
/** Register schemas + mount palette sidebar. Filters out built-ins ($templateInstance, $reroute,
* etc) so the panel only shows the user-facing types this demo cares about. */
export function buildPaletteSidebar(editor: XenolithEditor): void {
for (const s of paletteSchemas) editor.registry.register(s)
const ourTypes = new Set(paletteSchemas.map((s) => s.type))
editor.setPaletteSidebar({ side: 'left', filter: (s) => ourTypes.has(s.type) })
} // Solid adapter — palette sidebar.
import { createXenolithGraph } from '@xenolithengine/graph-solid'
import { buildPaletteSidebar } from '@xenolithengine/demo/palette-sidebar'
export async function mount(target: HTMLElement): Promise<() => void> {
const slot = document.createElement('div')
slot.style.cssText = 'position:absolute;inset:0;'
target.appendChild(slot)
const binding = await createXenolithGraph(slot, { resizeToWindow: false })
buildPaletteSidebar(binding.editor)
binding.editor.view.fitView({ padding: 80, maxZoom: 1 })
return () => { binding.destroy(); slot.remove() }
} // Palette sidebar showcase: rich registry across 5 categories so users see the panel grouping
// them, search them, and drag-spawn into the canvas. No special host code — the editor's default
// drop handler inserts the node at the world position automatically when the user drops a tile.
//
// Shared between vanilla + React demos.
import type { NodeSchema, XenolithEditor } from '@xenolithengine/graph-editor'
const stringPin = (dir: 'in' | 'out', label: string) =>
({ kind: 'data' as const, direction: dir, type: 'string', label, multiple: dir === 'out' })
const numberPin = (dir: 'in' | 'out', label: string) =>
({ kind: 'data' as const, direction: dir, type: 'number', label, multiple: dir === 'out' })
const boolPin = (dir: 'in' | 'out', label: string) =>
({ kind: 'data' as const, direction: dir, type: 'boolean', label, multiple: dir === 'out' })
export const paletteSchemas: NodeSchema[] = [
// DATA — leaf sources, no inputs
{ type: 'Const', title: 'Constant', category: 'data', description: 'A fixed number.', keywords: ['number', 'value'], pins: [numberPin('out', 'Value')],
widgets: [{ id: 'value', type: 'number', key: 'value', label: 'Value', step: 1, freeFloating: true }] },
{ type: 'TextValue', title: 'Text', category: 'data', description: 'A fixed text string.', keywords: ['string', 'literal'], pins: [stringPin('out', 'Text')],
widgets: [{ id: 'text', type: 'text', key: 'text', label: 'Text', placeholder: 'hello', freeFloating: true }] },
{ type: 'Random', title: 'Random', category: 'data', description: 'Pseudo-random 0..1 on read.', keywords: ['rand', 'noise'], pins: [numberPin('out', 'Out')] },
// MATH — arithmetic & trig
{ type: 'Add', title: 'Add', category: 'math', description: 'A + B', keywords: ['plus', 'sum'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Sum')] },
{ type: 'Subtract', title: 'Subtract', category: 'math', description: 'A − B', keywords: ['minus', 'diff'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Diff')] },
{ type: 'Multiply', title: 'Multiply', category: 'math', description: 'A × B', keywords: ['times', 'mul'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Product')] },
{ type: 'Divide', title: 'Divide', category: 'math', description: 'A ÷ B', keywords: ['div', 'fraction'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Quotient')] },
{ type: 'Sin', title: 'Sin', category: 'math', description: 'sin(x), radians.', keywords: ['trig'], pins: [numberPin('in', 'X'), numberPin('out', 'Out')] },
// TRANSFORM — string ops
{ type: 'ToUpper', title: 'To Upper', category: 'transform', description: 'Uppercase a string.', keywords: ['caps', 'upper'], pins: [stringPin('in', 'In'), stringPin('out', 'Out')] },
{ type: 'ToLower', title: 'To Lower', category: 'transform', description: 'Lowercase a string.', keywords: ['case'], pins: [stringPin('in', 'In'), stringPin('out', 'Out')] },
{ type: 'Trim', title: 'Trim', category: 'transform', description: 'Strip leading/trailing whitespace.', keywords: ['strip'], pins: [stringPin('in', 'In'), stringPin('out', 'Out')] },
{ type: 'Concat', title: 'Concat', category: 'transform', description: 'Join two strings.', keywords: ['join'], pins: [stringPin('in', 'A'), stringPin('in', 'B'), stringPin('out', 'Out')] },
// LOGIC — booleans + control
{ type: 'IfElse', title: 'If / Else', category: 'logic', description: 'Pick A when cond is true, else B.', keywords: ['cond', 'pick'],
pins: [boolPin('in', 'Cond'), numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Out')] },
{ type: 'Equals', title: 'Equals', category: 'logic', description: 'A == B', keywords: ['eq', 'cmp'],
pins: [numberPin('in', 'A'), numberPin('in', 'B'), boolPin('out', 'Out')] },
{ type: 'Not', title: 'Not', category: 'logic', description: 'Invert a boolean.', keywords: ['invert'],
pins: [boolPin('in', 'In'), boolPin('out', 'Out')] },
// IO — terminal sinks / readouts
{ type: 'Output', title: 'Output', category: 'io', description: 'Show the value in a read-only widget.', keywords: ['print', 'sink'],
pins: [numberPin('in', 'In')],
widgets: [{ id: 'result', type: 'text', key: 'result', label: 'Result', placeholder: '—', freeFloating: true, disabled: true }] },
{ type: 'Log', title: 'Log', category: 'io', description: 'Console.log on every change.', keywords: ['debug', 'print'],
pins: [stringPin('in', 'In')] },
]
/** Register schemas + mount palette sidebar. Filters out built-ins ($templateInstance, $reroute,
* etc) so the panel only shows the user-facing types this demo cares about. */
export function buildPaletteSidebar(editor: XenolithEditor): void {
for (const s of paletteSchemas) editor.registry.register(s)
const ourTypes = new Set(paletteSchemas.map((s) => s.type))
editor.setPaletteSidebar({ side: 'left', filter: (s) => ourTypes.has(s.type) })
} // Angular standalone component — palette sidebar. Schemas + sidebar config from the shared
// package; the editor's built-in `node:drop` handler spawns the dragged node at the drop point.
import { Component } from '@angular/core'
import { XenolithGraphComponent } from '@xenolithengine/graph-angular'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import { buildPaletteSidebar } from '@xenolithengine/demo/palette-sidebar'
@Component({
selector: 'palette-sidebar-demo',
standalone: true,
imports: [XenolithGraphComponent],
template: `
<div class="app" style="position:absolute;inset:0;">
<xenolith-graph
class="xeno"
[resizeToWindow]="false"
(ready)="onReady($event)">
</xenolith-graph>
</div>
`,
})
export class PaletteSidebarDemoComponent {
onReady(editor: XenolithEditor): void {
buildPaletteSidebar(editor)
editor.view.fitView({ padding: 80, maxZoom: 1 })
}
} // Palette sidebar showcase: rich registry across 5 categories so users see the panel grouping
// them, search them, and drag-spawn into the canvas. No special host code — the editor's default
// drop handler inserts the node at the world position automatically when the user drops a tile.
//
// Shared between vanilla + React demos.
import type { NodeSchema, XenolithEditor } from '@xenolithengine/graph-editor'
const stringPin = (dir: 'in' | 'out', label: string) =>
({ kind: 'data' as const, direction: dir, type: 'string', label, multiple: dir === 'out' })
const numberPin = (dir: 'in' | 'out', label: string) =>
({ kind: 'data' as const, direction: dir, type: 'number', label, multiple: dir === 'out' })
const boolPin = (dir: 'in' | 'out', label: string) =>
({ kind: 'data' as const, direction: dir, type: 'boolean', label, multiple: dir === 'out' })
export const paletteSchemas: NodeSchema[] = [
// DATA — leaf sources, no inputs
{ type: 'Const', title: 'Constant', category: 'data', description: 'A fixed number.', keywords: ['number', 'value'], pins: [numberPin('out', 'Value')],
widgets: [{ id: 'value', type: 'number', key: 'value', label: 'Value', step: 1, freeFloating: true }] },
{ type: 'TextValue', title: 'Text', category: 'data', description: 'A fixed text string.', keywords: ['string', 'literal'], pins: [stringPin('out', 'Text')],
widgets: [{ id: 'text', type: 'text', key: 'text', label: 'Text', placeholder: 'hello', freeFloating: true }] },
{ type: 'Random', title: 'Random', category: 'data', description: 'Pseudo-random 0..1 on read.', keywords: ['rand', 'noise'], pins: [numberPin('out', 'Out')] },
// MATH — arithmetic & trig
{ type: 'Add', title: 'Add', category: 'math', description: 'A + B', keywords: ['plus', 'sum'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Sum')] },
{ type: 'Subtract', title: 'Subtract', category: 'math', description: 'A − B', keywords: ['minus', 'diff'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Diff')] },
{ type: 'Multiply', title: 'Multiply', category: 'math', description: 'A × B', keywords: ['times', 'mul'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Product')] },
{ type: 'Divide', title: 'Divide', category: 'math', description: 'A ÷ B', keywords: ['div', 'fraction'], pins: [numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Quotient')] },
{ type: 'Sin', title: 'Sin', category: 'math', description: 'sin(x), radians.', keywords: ['trig'], pins: [numberPin('in', 'X'), numberPin('out', 'Out')] },
// TRANSFORM — string ops
{ type: 'ToUpper', title: 'To Upper', category: 'transform', description: 'Uppercase a string.', keywords: ['caps', 'upper'], pins: [stringPin('in', 'In'), stringPin('out', 'Out')] },
{ type: 'ToLower', title: 'To Lower', category: 'transform', description: 'Lowercase a string.', keywords: ['case'], pins: [stringPin('in', 'In'), stringPin('out', 'Out')] },
{ type: 'Trim', title: 'Trim', category: 'transform', description: 'Strip leading/trailing whitespace.', keywords: ['strip'], pins: [stringPin('in', 'In'), stringPin('out', 'Out')] },
{ type: 'Concat', title: 'Concat', category: 'transform', description: 'Join two strings.', keywords: ['join'], pins: [stringPin('in', 'A'), stringPin('in', 'B'), stringPin('out', 'Out')] },
// LOGIC — booleans + control
{ type: 'IfElse', title: 'If / Else', category: 'logic', description: 'Pick A when cond is true, else B.', keywords: ['cond', 'pick'],
pins: [boolPin('in', 'Cond'), numberPin('in', 'A'), numberPin('in', 'B'), numberPin('out', 'Out')] },
{ type: 'Equals', title: 'Equals', category: 'logic', description: 'A == B', keywords: ['eq', 'cmp'],
pins: [numberPin('in', 'A'), numberPin('in', 'B'), boolPin('out', 'Out')] },
{ type: 'Not', title: 'Not', category: 'logic', description: 'Invert a boolean.', keywords: ['invert'],
pins: [boolPin('in', 'In'), boolPin('out', 'Out')] },
// IO — terminal sinks / readouts
{ type: 'Output', title: 'Output', category: 'io', description: 'Show the value in a read-only widget.', keywords: ['print', 'sink'],
pins: [numberPin('in', 'In')],
widgets: [{ id: 'result', type: 'text', key: 'result', label: 'Result', placeholder: '—', freeFloating: true, disabled: true }] },
{ type: 'Log', title: 'Log', category: 'io', description: 'Console.log on every change.', keywords: ['debug', 'print'],
pins: [stringPin('in', 'In')] },
]
/** Register schemas + mount palette sidebar. Filters out built-ins ($templateInstance, $reroute,
* etc) so the panel only shows the user-facing types this demo cares about. */
export function buildPaletteSidebar(editor: XenolithEditor): void {
for (const s of paletteSchemas) editor.registry.register(s)
const ourTypes = new Set(paletteSchemas.map((s) => s.type))
editor.setPaletteSidebar({ side: 'left', filter: (s) => ourTypes.has(s.type) })
}