Theming
Theme is a reactive prop — flip Xen ⇄ Liquid Glass at runtime; panels/widgets restyle via --xeno-*. Angular applies the first theme at mount and later flips with editor.setTheme.
import { useState } from 'react'
import { XenolithGraph, XenolithPanel, XenolithButton, XenolithControls } from '@xenolithengine/graph-react'
import { xenTheme } from '@xenolithengine/graph-render-pixi'
import { liquidGlassTheme } from '@xenolithengine/graph-theme-liquid-glass'
import { DemoStage } from '../Layout.js'
import { loadDemo } from '../demo-data.js'
/** Island: theme is a reactive prop — flip Xen ⇄ Liquid Glass from an in-editor panel whose own
* buttons restyle with the theme via --xeno-*. */
export function ThemingDemo() {
const [theme, setTheme] = useState<'xen' | 'lg'>('xen')
return (
<DemoStage>
<XenolithGraph
className="xeno"
theme={theme === 'xen' ? xenTheme : liquidGlassTheme}
resizeToWindow={false}
onReady={loadDemo}
>
<XenolithControls position="top-right" orientation="horizontal" />
<XenolithPanel position="top-left" style={{ display: 'flex', gap: 6, padding: 6 }}>
<XenolithButton active={theme === 'xen'} onClick={() => setTheme('xen')}>Xen</XenolithButton>
<XenolithButton active={theme === 'lg'} onClick={() => setTheme('lg')}>Liquid Glass</XenolithButton>
</XenolithPanel>
</XenolithGraph>
</DemoStage>
)
} <script setup lang="ts">
// Vue — theme is a watched prop. Flip it and the graph plus the panel restyle together.
import { computed, ref } from 'vue'
import { XenolithGraph, XenolithControls, XenolithPanel, XenolithButton } from '@xenolithengine/graph-vue'
import { xenTheme } from '@xenolithengine/graph-render-pixi'
import { liquidGlassTheme } from '@xenolithengine/graph-theme-liquid-glass'
import { loadDemo } from '@xenolithengine/demo/scene'
const name = ref<'xen' | 'lg'>('xen')
const theme = computed(() => (name.value === 'xen' ? xenTheme : liquidGlassTheme))
</script>
<template>
<div class="app" style="position:absolute;inset:0;">
<XenolithGraph class="xeno" :theme="theme" :resize-to-window="false" @ready="loadDemo">
<XenolithControls position="top-right" orientation="horizontal" />
<XenolithPanel position="top-left">
<div style="display:flex;gap:6px;">
<XenolithButton :active="name === 'xen'" @click="name = 'xen'">Xen</XenolithButton>
<XenolithButton :active="name === 'lg'" @click="name = 'lg'">Liquid Glass</XenolithButton>
</div>
</XenolithPanel>
</XenolithGraph>
</div>
</template> <!-- Svelte — the action's update() forwards a new theme object, so the bound prop restyles live. -->
<script lang="ts">
import { xenolith, createXenolithEditorContext } from '@xenolithengine/graph-svelte'
import { XenolithPanel, XenolithButton, XenolithControls } from '@xenolithengine/graph-svelte/components'
import { xenTheme } from '@xenolithengine/graph-render-pixi'
import { liquidGlassTheme } from '@xenolithengine/graph-theme-liquid-glass'
import { loadDemo } from '@xenolithengine/demo/scene'
const panelEditor = createXenolithEditorContext()
let name = $state<'xen' | 'lg'>('xen')
</script>
<div
use:xenolith={{ theme: name === 'xen' ? xenTheme : liquidGlassTheme, resizeToWindow: false }}
onready={(e) => { panelEditor.set(e.detail); loadDemo(e.detail) }}
style="position:absolute;inset:0"
></div>
<XenolithControls position="top-right" orientation="horizontal" />
<XenolithPanel position="top-left">
<div style="display:flex;gap:6px;">
<XenolithButton active={name === 'xen'} onclick={() => { name = 'xen' }}>Xen</XenolithButton>
<XenolithButton active={name === 'lg'} onclick={() => { name = 'lg' }}>Liquid Glass</XenolithButton>
</div>
</XenolithPanel> // Solid — the directive tracks the bound object. Reading the signal inside it re-runs setProps,
// which is how a theme flip reaches the editor after mount.
import { createSignal } from 'solid-js'
import { xenolith } from '@xenolithengine/graph-solid'
import { xenTheme } from '@xenolithengine/graph-render-pixi'
import { liquidGlassTheme } from '@xenolithengine/graph-theme-liquid-glass'
import { loadDemo } from '@xenolithengine/demo/scene'
export function ThemingDemo() {
const [name, setName] = createSignal<'xen' | 'lg'>('xen')
return (
<div
use:xenolith={{
theme: name() === 'xen' ? xenTheme : liquidGlassTheme,
resizeToWindow: false,
}}
on:ready={(e) => {
e.detail.chrome.setControls({ position: 'top-right', orientation: 'horizontal' })
loadDemo(e.detail)
}}
style="position:absolute;inset:0"
>
<div style="position:absolute;top:12px;left:12px;z-index:5;display:flex;gap:6px;padding:6px;background:var(--xeno-panel);border:1px solid var(--xeno-border);border-radius:10px;">
<button
type="button"
style={`font:inherit;font-size:13px;padding:7px 12px;cursor:pointer;border-radius:8px;border:1px solid ${name() === 'xen' ? 'var(--xeno-accent)' : 'var(--xeno-border)'};background:${name() === 'xen' ? 'var(--xeno-accent)' : 'var(--xeno-elevated)'};color:${name() === 'xen' ? 'var(--xeno-canvas)' : 'var(--xeno-text)'};`}
onClick={() => setName('xen')}
>Xen</button>
<button
type="button"
style={`font:inherit;font-size:13px;padding:7px 12px;cursor:pointer;border-radius:8px;border:1px solid ${name() === 'lg' ? 'var(--xeno-accent)' : 'var(--xeno-border)'};background:${name() === 'lg' ? 'var(--xeno-accent)' : 'var(--xeno-elevated)'};color:${name() === 'lg' ? 'var(--xeno-canvas)' : 'var(--xeno-text)'};`}
onClick={() => setName('lg')}
>Liquid Glass</button>
</div>
</div>
)
} // Angular — mount applies theme once. Later flips go through editor.setTheme (props are not re-watched).
import { AfterViewInit, Component, ElementRef, OnDestroy, inject, signal, viewChild } from '@angular/core'
import { XenolithGraphService } from '@xenolithengine/graph-angular'
import { xenTheme } from '@xenolithengine/graph-render-pixi'
import { liquidGlassTheme } from '@xenolithengine/graph-theme-liquid-glass'
import { loadDemo } from '@xenolithengine/demo/scene'
@Component({
selector: 'theming-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" [class.on]="name() === 'xen'" (click)="setTheme('xen')">Xen</button>
<button type="button" class="btn" [class.on]="name() === 'lg'" (click)="setTheme('lg')">Liquid Glass</button>
</div>
</div>
`,
styles: [`
.card { position:absolute; top:12px; left:12px; z-index:5; display:flex; gap:6px; padding:6px;
background:var(--xeno-panel,#1d1d1d); border:1px solid var(--xeno-border,#333);
border-radius:10px; font:13px system-ui,sans-serif; }
.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); }
`],
})
export class ThemingDemoComponent implements AfterViewInit, OnDestroy {
private graph = inject(XenolithGraphService)
private host = viewChild.required<ElementRef<HTMLDivElement>>('host')
name = signal<'xen' | 'lg'>('xen')
async ngAfterViewInit(): Promise<void> {
const editor = await this.graph.mount(this.host().nativeElement, {
resizeToWindow: false,
theme: xenTheme,
})
editor.chrome.setControls({ position: 'top-right', orientation: 'horizontal' })
loadDemo(editor)
}
setTheme(next: 'xen' | 'lg'): void {
const editor = this.graph.editor
if (!editor) return
this.name.set(next)
editor.setTheme(next === 'xen' ? xenTheme : liquidGlassTheme)
}
ngOnDestroy(): void { this.graph.destroy() }
}