# JavaScript: elements and content

Use the custom element for HTML-driven embeds and [`render`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-core#render) for programmatic mounts. Both give you an engine-positioned box whose inside you paint: your content does not take over dragging, selection, ports or connections. This page mounts connected cards, wires events, and updates a custom dashboard widget without rebuilding the board.

## 1. Choose the embedding surface

[`GrafloriaFlowElement`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-core#grafloriaflowelement) implements `<grafloria-flow>`. Importing `@grafloria/element` registers that tag. Pass arrays through the element's `nodes` and `edges` properties; use JSON attributes when the data lives in markup.

`render()` takes the spec first, a container or CSS selector second, and options third. It returns a live [`DiagramInstance`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-diagraminstance#diagraminstance). Its input is an object or JSON, not Mermaid text; see [Import diagram text and files](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/import-diagram-text-and-files) for text parsing.

| Choose | What you get |
| --- | --- |
| Element attributes | An HTML embed with JSON data and named DOM events |
| Element properties | Array inputs without JSON encoding, plus `diagram` and `fitView()` |
| `render(spec, target, options)` | A direct instance return, instance events, and kit finalization |

Run the JavaScript examples in a browser project with an ESM bundler, such as the project from the [JavaScript quick start](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/javascript-quick-start). Install the packages they import:

```bash
npm install @grafloria/element @grafloria/engine @grafloria/renderer
```

This property-based embed uses [`NodeSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-nodespec#nodespec) and [`EdgeSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-edgespec#edgespec) to draw Build and Publish with a connection between them. Register the DOM listener before appending the element. The ready event's detail contains `{ diagram }`; the example reads the typed `diagram` property instead of casting an untyped DOM event payload.

```ts title="src/element.ts"
import { GrafloriaFlowElement } from '@grafloria/element';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { id: 'build', label: 'Build', position: { x: 60, y: 100 },
    size: { width: 180, height: 80 } },
  { id: 'publish', label: 'Publish', position: { x: 360, y: 100 },
    size: { width: 180, height: 80 } },
];
const edges: EdgeSpec[] = [{ id: 'release', source: 'build', target: 'publish' }];
const flow = document.createElement('grafloria-flow');
if (!(flow instanceof GrafloriaFlowElement)) throw new Error('Element not registered');
flow.style.height = '400px';
flow.setAttribute('theme', 'light');
flow.setAttribute('fit-view', '');
flow.nodes = nodes;
flow.edges = edges;
const status = document.createElement('p');
status.textContent = 'Starting diagram';
flow.addEventListener('grafloria-ready', () => {
  status.textContent = `Ready: ${flow.diagram?.getModel().getNodes().length} nodes`;
});
flow.addEventListener('grafloria-selection-change', () => {
  const selected = flow.diagram?.getModel().getNodes()
    .filter((node) => node.isSelected()).map((node) => node.id) ?? [];
  status.textContent = selected.length ? `Selected: ${selected.join(', ')}` : 'No selection';
});
const close = document.createElement('button');
close.textContent = 'Close diagram';
close.onclick = () => { flow.remove(); status.remove(); close.remove(); };
document.body.append(status, close, flow);
```

![Build connects to Publish below the Ready: 2 nodes status and Close diagram button.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/ee724717516cfd0c222576690ebd69b1.png)

Assigning `flow.nodes` or `flow.edges` after mounting calls the instance's corresponding setter. Removing the element disposes its instance. For parent sizing arrangements, see [Theme a canvas](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/theme-a-canvas).

### Attribute options

The following defaults come from the element's mount logic. Set mount-only attributes before inserting the element.

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `nodes`, `edges` | JSON array strings | Empty arrays | Supplies node and edge specs; prefer properties for rich data |
| `theme` | `'light'` or `'dark'` | `'light'` | Selects a shipped theme; unknown names resolve to light |
| `fit-view` | Presence flag | Absent | Frames the content on mount |
| `readonly` | Presence flag | Absent | Configures read-only interaction on mount |
| `pan` | String | Enabled | `"false"` disables pan on mount |
| `wheel-zoom` | String | Enabled | `"false"` disables zoom interaction on mount |
| `zoom` | Numeric string | Not supplied by the element | Supplies the initial zoom |
| `min-zoom`, `max-zoom` | Numeric strings | Not supplied by the element | Supplies mount-time zoom bounds |
| `highlight-connected` | Flag, `"false"`, `"trace"`, or depth string | Off | Highlights selected nodes' connections; `"trace"` follows every path |

### DOM events

Every forwarded event bubbles and is composed, so it can cross a surrounding shadow boundary. Read the payload from `CustomEvent.detail` when your host supplies a typed event adapter.

| DOM event | Detail |
| --- | --- |
| `grafloria-ready` | `{ diagram }` |
| `grafloria-nodes-change` | `{ nodes }` with live node models |
| `grafloria-edges-change` | `{ edges }` with live link models |
| `grafloria-selection-change` | `{ nodes, edges }` |
| `grafloria-connect` | `{ link }` |
| `grafloria-node-click` | `{ node, world }` |
| `grafloria-edge-click` | `{ edge, world }` |
| `grafloria-viewport-change` | `{ viewport, zoom }` |

For a programmatic mount, use `instance.on('selection:change', handler)` instead. It infers the payload type and returns a function that removes the subscription. The programmatic example in the next step shows that cleanup on Close.

### Rename the tag

[`Grafloria`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-core#grafloria) exposes the intended call `Grafloria.define('project-flow')`.

> **Known issue:** `Grafloria.define('project-flow')` reuses the constructor already registered as `grafloria-flow`, which the browser rejects for a second tag. Until it is fixed, register a subclass under your own tag.

This browser example registers the subclass once and mounts a Release node under `<project-flow>`:

```ts title="src/renamed-element.ts"
import { GrafloriaFlowElement } from '@grafloria/element';
import type { NodeSpec } from '@grafloria/renderer';

if (!customElements.get('project-flow')) {
  class ProjectFlow extends GrafloriaFlowElement {}
  customElements.define('project-flow', ProjectFlow);
}
const flow = document.createElement('project-flow');
if (!(flow instanceof GrafloriaFlowElement)) throw new Error('Unexpected tag constructor');
const nodes: NodeSpec[] = [
  { id: 'release', label: 'Release', position: { x: 80, y: 90 },
    size: { width: 180, height: 80 } },
];
flow.nodes = nodes;
flow.style.height = '400px';
flow.setAttribute('fit-view', '');
document.body.append(flow);
```

![A Release node is centered in the renamed element's canvas.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/03e17d9c53b8449fd7aa963625fb6dfe.png)

## 2. Fill custom node hosts

Register a JavaScript renderer before mounting, and set `custom: true` on each node that uses it. Registering a type alone does not opt JavaScript or React nodes into HTML rendering. React uses `nodeTypes`; Vue exact `#node-<type>` slots and Angular typed templates opt matching specs in automatically. Vue's wildcard `#node` slot still needs `custom: true`.

A JavaScript renderer is a mount hook, not a data binding: it runs once when the host is created. Later node-data changes do not reinvoke it. Update the DOM you own. A missing renderer leaves an empty host, and registering later does not back-fill that host. Style a child of the host; the engine owns the host's position and dimensions.

Use `NodeSpec` and `EdgeSpec` in the shared file below. All framework tabs import it. The cards display Build and Publish, and the wire remains attached when you drag either card.

```ts title="content.ts"
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

export const nodes: NodeSpec[] = [
  { id: 'build', type: 'card', custom: true, position: { x: 60, y: 100 },
    size: { width: 220, height: 110 }, data: { title: 'Build' } },
  { id: 'publish', type: 'card', custom: true, position: { x: 380, y: 100 },
    size: { width: 220, height: 110 }, data: { title: 'Publish' } },
];
export const edges: EdgeSpec[] = [{ id: 'release', source: 'build', target: 'publish' }];
export const cardStyle = {
  boxSizing: 'border-box' as const, height: '100%', padding: '12px',
  border: '1px solid #94a5f0', borderRadius: '10px', background: '#fff',
  color: '#232a3d', fontFamily: 'system-ui',
};
```

Install your binding in addition to the three shared packages above.

JavaScript:

```bash
npm install @grafloria/element @grafloria/engine @grafloria/renderer
```

Angular:

```bash
npm install @grafloria/angular @grafloria/element @grafloria/engine @grafloria/renderer
```

Qwik:

```bash
npm install @grafloria/qwik @grafloria/element @grafloria/engine @grafloria/renderer
```

React:

```bash
npm install @grafloria/react @grafloria/element @grafloria/engine @grafloria/renderer
```

Vue:

```bash
npm install @grafloria/vue @grafloria/element @grafloria/engine @grafloria/renderer
```

Use these components in an existing framework project. React's [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-react#grafloriaflow) maps types to components accepting [`NodeProps`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-react#nodeprops). Qwik's [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#grafloriaflow) uses Qwik components and its own [`NodeProps`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#nodeprops). Vue's [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-vue#grafloriaflow) uses named slots. Angular's [`DiagramCanvasComponent`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-angular-components-diagramcanvascomponent#diagramcanvascomponent) uses `ng-template[grafloriaNode]`, declared by [`GrafloriaNodeDefDirective`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-angular-components-classes#graflorianodedefdirective).

Each tab exposes a card-local Mark complete button. It changes that card's content to Complete, without pretending that a mount hook is a reactive binding. Selection updates the status outside the canvas through the binding's own event.

:::code-group
```ts title="JavaScript"
import { Grafloria, render } from '@grafloria/element';
import { nodes, edges, cardStyle } from './content';

Grafloria.registerNodeType('card', (node, host) => {
  const card = document.createElement('div');
  Object.assign(card.style, cardStyle);
  const title = document.createElement('strong');
  title.textContent = String(node.getData('title') ?? '');
  const complete = document.createElement('button');
  complete.textContent = 'Mark complete';
  complete.onclick = () => { title.textContent = 'Complete'; };
  card.append(title, document.createElement('br'), complete);
  host.replaceChildren(card);
});
const canvas = document.createElement('div');
canvas.style.height = '400px';
const status = document.createElement('p');
status.textContent = 'Select a card';
const close = document.createElement('button');
close.textContent = 'Close diagram';
document.body.append(status, close, canvas);
const instance = render({ nodes, edges }, canvas, { fitView: true });
const off = instance.on('selection:change', ({ nodes: selected }) => {
  status.textContent = selected.map((node) => node.id).join(', ') || 'No selection';
});
close.onclick = () => {
  off();
  instance.dispose();
  canvas.remove();
  status.remove();
  close.remove();
};
```
```ts title="Angular"
import { Component } from '@angular/core';
import { DiagramCanvasComponent, GrafloriaNodeDefDirective } from '@grafloria/angular';
import { nodes, edges } from './content';

@Component({
  selector: 'app-custom-content',
  standalone: true,
  imports: [DiagramCanvasComponent, GrafloriaNodeDefDirective],
  template: `
    <p>{{ status }}</p>
    <grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
      (selectionChange)="status = $event.nodes.length + ' selected'"
      style="display:block;height:400px">
      <ng-template grafloriaNode="card" let-node let-data="data">
        <div style="box-sizing:border-box;height:100%;padding:12px;border:1px solid #94a5f0;
                    border-radius:10px;background:white;color:#232a3d;font-family:system-ui">
          <strong>{{ completed.has(node.id) ? 'Complete' : data['title'] }}</strong><br>
          <button (click)="completed.add(node.id)">Mark complete</button>
        </div>
      </ng-template>
    </grafloria-diagram-canvas>
  `,
})
export class CustomContentComponent {
  nodes = nodes;
  edges = edges;
  status = 'Select a card';
  completed = new Set<string>();
}
```
```tsx title="Qwik"
import { component$, useSignal } from '@builder.io/qwik';
import { GrafloriaFlow, type NodeProps } from '@grafloria/qwik';
import { nodes, edges, cardStyle } from './content';

const Card = component$((props: NodeProps) => {
  const complete = useSignal(false);
  return <div style={cardStyle}>
    <strong>{complete.value ? 'Complete' : String(props.data['title'] ?? '')}</strong><br />
    <button onClick$={() => { complete.value = true; }}>Mark complete</button>
  </div>;
});

export default component$(() => {
  const status = useSignal('Select a card');
  return <>
    <p>{status.value}</p>
    <div style={{ height: '400px' }}>
      <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} nodeTypes={{ card: Card }} fitView
        onSelectionChange$={(change) => {
          status.value = `${change.nodes.length} selected`;
        }} />
    </div>
  </>;
});
```
```tsx title="React"
import { useState } from 'react';
import { GrafloriaFlow, type NodeProps } from '@grafloria/react';
import { nodes, edges, cardStyle } from './content';

function Card({ data }: NodeProps) {
  const [complete, setComplete] = useState(false);
  return <div style={cardStyle}>
    <strong>{complete ? 'Complete' : String(data['title'] ?? '')}</strong><br />
    <button onClick={() => setComplete(true)}>Mark complete</button>
  </div>;
}

export default function CustomContent() {
  const [status, setStatus] = useState('Select a card');
  return <>
    <p>{status}</p>
    <div style={{ height: '400px' }}>
      <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} nodeTypes={{ card: Card }} fitView
        onSelectionChange={({ nodes: selected }) => setStatus(`${selected.length} selected`)} />
    </div>
  </>;
}
```
```vue title="Vue"
<script setup lang="ts">
import { defineComponent, h, ref } from 'vue';
import { GrafloriaFlow } from '@grafloria/vue';
import { nodes, edges, cardStyle } from './content';

const status = ref('Select a card');
const Card = defineComponent({
  props: { title: { type: String, required: true } },
  setup(props) {
    const complete = ref(false);
    return () => h('div', { style: cardStyle }, [
      h('strong', complete.value ? 'Complete' : props.title),
      h('br'),
      h('button', { onClick: () => { complete.value = true; } }, 'Mark complete'),
    ]);
  },
});
</script>

<template>
  <p>{{ status }}</p>
  <div style="height:400px">
    <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" fit-view
      @selection-change="status = $event.nodes.length + ' selected'">
      <template #node-card="{ data }">
        <Card :title="String(data['title'] ?? '')" />
      </template>
    </GrafloriaFlow>
  </div>
</template>
```
:::

The JavaScript mount includes a Close diagram button above the connected cards.

![JavaScript shows Build and Publish cards with Mark complete buttons, a connecting arrow, and Close diagram.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/305ba9c55380f7834dc5b43bf921bf43.png)

The Angular template fills both cards and leaves the surrounding node outlines and ports visible.

![Angular shows connected Build and Publish cards with Mark complete buttons, orange outlines, and gray port dots.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/e9ecdd6cb5c709ac252151a1faf34f43.png)

The Qwik components draw the card content inside the connected hosts.

![Qwik shows Build and Publish cards with Mark complete buttons below Select a card.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/9e3b916fdaa9ea8ecdff1f3b5fde7293.png)

The React components and Vue slot components produce the same initial card content: Build and Publish, each with a Mark complete button.

Vue slots do not repaint for in-place node-data changes. Put a component in the slot that subscribes to your own reactive source, rather than relying on slot reinvocation. The Vue card above owns a reactive completion flag. Qwik custom nodes occupy separate Qwik containers: keep them self-contained and pass their initial payload through node data, not surrounding app context.

### Use a template for markup-driven cards

For a CMS or static page, place a `<template data-node-type="template-card">` inside the element. This alternative clones the template for the Build node. `data-field="title"` reads the data bag; `data-field="id"` reads the node id. Missing and null values become empty strings. A registered renderer takes precedence over a matching template.

```html title="index.html"
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8"><title>Template card</title></head>
<body>
  <grafloria-flow style="height:400px" fit-view
    nodes='[{"id":"build","type":"template-card","custom":true,"position":{"x":80,"y":90},"size":{"width":220,"height":110},"data":{"title":"Build"}}]'>
    <template data-node-type="template-card">
      <div style="box-sizing:border-box;height:100%;padding:12px;border:1px solid #94a5f0;border-radius:10px;background:white">
        <strong data-field="title"></strong>
        <p data-field="id"></p>
      </div>
    </template>
  </grafloria-flow>
  <script type="module" src="/src/register.ts"></script>
</body>
</html>
```

```ts title="src/register.ts"
import '@grafloria/element';
```

Template substitution uses `textContent`, not HTML injection. Use the same rule in your JavaScript renderers: never interpolate user-supplied text into `innerHTML`. Templates share the mount-once contract; use owned DOM or widget handles when content must change later. The canvas uses light DOM, so your page stylesheet applies to the cloned children.

## 3. Update custom dashboard content through a handle

Use a [`WidgetHandle`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-dashboard-kit-widgethandle#widgethandle) to repaint your custom content without remounting the board; see [Arrange dashboard containers](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/arrange-dashboard-containers) for creating and mounting the board and obtaining its handles.

The kit ships KPI, line, bar, donut, funnel and table painters. Delegate those kinds to [`defaultWidgetRenderer`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-dashboard-kit-constants#defaultwidgetrenderer) before adding your own painter. Type the data with [`DashboardWidgetSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-dashboard-kit-dashboardwidgetspec#dashboardwidgetspec) and the painter with [`WidgetRenderer`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-dashboard-kit-types#widgetrenderer). This board draws a shipped Builds KPI beside a custom Release note. Update note changes the note to Ready to publish without replacing the board or its instance.

```ts title="src/widgets.ts"
import { dashboard, defaultWidgetRenderer, render } from '@grafloria/element';
import type { DashboardWidgetSpec, WidgetRenderer } from '@grafloria/element';

const widgets: DashboardWidgetSpec[] = [
  { id: 'builds', kind: 'kpi', span: 3, rows: 1,
    data: { label: 'Builds', value: '24' } },
  { id: 'note', kind: 'release-note', span: 3, rows: 1,
    title: 'Release note', data: { text: 'Waiting for review' } },
];
const paint: WidgetRenderer = (widget, host) => {
  if (widget.kind !== 'release-note') {
    defaultWidgetRenderer(widget, host);
    return;
  }
  const card = document.createElement('div');
  Object.assign(card.style, {
    boxSizing: 'border-box', height: '100%', padding: '12px',
    background: '#fff', border: '1px solid #94a5f0', borderRadius: '10px',
    fontFamily: 'system-ui', color: '#232a3d',
  });
  const title = document.createElement('strong');
  title.textContent = widget.title ?? '';
  const text = document.createElement('p');
  text.textContent = String(widget.data?.['text'] ?? '');
  card.append(title, text);
  host.replaceChildren(card);
};
const canvas = document.createElement('div');
canvas.style.height = '400px';
const update = document.createElement('button');
update.textContent = 'Update note';
const close = document.createElement('button');
close.textContent = 'Close board';
document.body.append(update, close, canvas);
const spec = dashboard({ columns: 6, widgets, renderWidget: paint });
const instance = render(spec, canvas);
const handle = spec.handle;
update.onclick = () => {
  handle.widget('note')?.update({ data: { text: 'Ready to publish' } });
};
close.onclick = () => {
  handle.dispose();
  instance.dispose();
  canvas.remove();
  update.remove();
  close.remove();
};
```

![The Builds KPI displays 24 beside a Release note reading Waiting for review; Update note and Close board appear above.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/22b0a37cd56cc47c8a7cf22c31baebbf.png)

`WidgetRenderer` receives the declared widget spec and its HTML host. It mounts once during ordinary rendering; the widget handle explicitly invokes it again for content updates.

| Operation or option | Type | Default | What it does |
| --- | --- | --- | --- |
| `renderWidget` | `WidgetRenderer` | Shipped painter | Fills each widget's host |
| `columns` | `number` | `12` | Sets the board's column count |
| `rowHeight` | `number` | `130` | Sets row height in grow mode |
| `update(patch)` | Partial `data`, `title`, `kind` fields | No patch | Replaces supplied fields and repaints; returns `void` |
| `repaint()` | No arguments | No automatic data binding | Reinvokes the painter with the current declared spec; returns `void` |

`update()` replaces `data`; it does not merge keys into the old payload. Supply every data field the painter needs. If your painter reads an external store, change that store and call `handle.widget('note')?.repaint()` to refresh the same host. Neither call recreates the widget node.

The handle calls are shared across bindings. For React widget components, Vue widget slots, Angular widget templates and Qwik widget components, use the complete mounted examples in [Build a dashboard](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/build-a-dashboard).

## Demos and next steps

- [Custom nodes live demo](https://grafloria.com/demos/nodes/custom-nodes.html) and its [source](https://github.com/grafloria/grafloria/blob/cccb505e76c7af26c8bafecaba8146db7d8df928/demos/nodes/custom-nodes.html): drag connected, custom-shaped nodes.
- [HTML nodes live demo](https://grafloria.com/demos/nodes/html-nodes.html): inspect rich content that moves with a node.
- [Dashboard builder live demo](https://grafloria.com/demos/dashboard/dashboard-builder.html): see the shipped painters on a rearrangeable board.
- [Instance and lifecycle](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/instance-and-lifecycle): keep an instance and clean it up with its host.
- [State and event flow](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/state-and-event-flow): choose who owns spec data and return edits to that owner.
- [Validate port connections](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/validate-port-connections): constrain connections around custom content.
