Skip to content

Plugin API

The Glyph JS plugin system lets you register custom ui:* component types that integrate with the compiler, renderer, and theming layers. Plugins are defined as GlyphComponentDefinition objects and registered via the PluginRegistry. All plugin types live in @glyphjs/types; the registry implementation lives in @glyphjs/runtime.

GlyphComponentDefinition

Every plugin provides a GlyphComponentDefinition that declares its block type, validation schema, render function, and optional theme defaults:

interface GlyphComponentDefinition<T = unknown> {
type: `ui:${string}`;
schema: {
parse: (data: unknown) => T;
safeParse: (data: unknown) => { success: boolean; data?: T; error?: unknown };
};
render: ComponentType<GlyphComponentProps<T>>;
themeDefaults?: Record<string, string>;
dependencies?: string[];
}
FieldTypeRequiredDescription
typebacktick ui:… backtickYesBlock type identifier. Must start with ui:.
schemaZod-compatibleYesValidates the block data payload. Must expose parse and safeParse.
renderComponentTypeYesReact component that renders the block.
themeDefaultsRecord<string, string>NoDefault CSS variable values for this component.
dependenciesstring[]NoOther ui:* types this component depends on.

Schema requirements (Zod)

The schema field must be a Zod-compatible object that exposes parse and safeParse methods. This is typed loosely in @glyphjs/types to avoid a hard dependency on Zod, but in practice you should use Zod schemas:

import { z } from 'zod';
const mySchema = z.object({
title: z.string(),
items: z.array(z.object({
label: z.string(),
value: z.number(),
})),
});

The schema serves two purposes:

  1. Runtime validation — When the runtime encounters a block with your ui:* type, it calls schema.safeParse(block.data) to validate the data. If validation fails, the block renders with diagnostic errors.
  2. JSON Schema generation — Zod schemas can be converted to JSON Schema for external consumers, LLM prompts, and documentation via zod-to-json-schema.

Validation behavior

  • schema.safeParse is called during rendering, not during compilation.
  • If validation fails, the block is preserved with the original data and diagnostic errors are attached.
  • Successful validation returns the parsed (and potentially transformed) data to the render function.

Render function props (GlyphComponentProps)

The render component receives GlyphComponentProps<T> where T is the validated data type from the schema:

interface GlyphComponentProps<T = unknown> {
data: T;
block: Block;
outgoingRefs: Reference[];
incomingRefs: Reference[];
onNavigate: (ref: Reference) => void;
onInteraction?: (event: Omit<InteractionEvent, 'documentId'>) => void;
theme: GlyphThemeContext;
layout: LayoutHints;
container: ContainerContext;
}
PropTypeDescription
dataTThe validated block data (output of schema.parse).
blockBlockThe full block object, including id, type, position, children, and metadata.
outgoingRefsReference[]References originating from this block.
incomingRefsReference[]References targeting this block.
onNavigate(ref: Reference) => voidCallback to trigger navigation along a reference.
onInteraction(event) => voidOptional callback for interaction events. Only present when interactive: true is set on the block. Components emit events without documentId; the runtime injects it automatically.
themeGlyphThemeContextCurrent theme context with resolveVar, isDark, and name.
layoutLayoutHintsGlobal layout configuration (mode, columns, spacing).
containerContainerContextContainer tier and sizing context for adaptive rendering.

Registration via PluginRegistry

The PluginRegistry class in @glyphjs/runtime manages component registration, validation, theme defaults merging, and change notification.

Registering components

import { PluginRegistry } from '@glyphjs/runtime';
const registry = new PluginRegistry();
// Register a single component
registry.registerComponent(myComponentDefinition);
// Bulk-register multiple components
registry.registerAll([graphComponent, tableComponent, chartComponent]);

Registration validates the definition before accepting it. If validation fails, an error is thrown with details about what is wrong.

Validation rules

When you call registerComponent, the registry validates:

  1. type must match the ui:* format (e.g. ui:chart, ui:graph).
  2. schema must be present with parse and safeParse methods (Zod-compatible).
  3. render must be a function (functional component) or class (class component).

Lookups

// Get a registered component definition
const def = registry.getRenderer('ui:graph');
// Check if a type is registered
const exists = registry.has('ui:graph');
// Get all registered type names
const types: string[] = registry.getRegisteredTypes();

Overrides

You can override the renderer for any block type (including standard Markdown types) without replacing the full component definition:

registry.setOverrides({
'heading': MyCustomHeadingRenderer,
'ui:graph': MyCustomGraphRenderer,
});
// Get an override renderer
const override = registry.getOverride('heading');

Overrides take priority over registered component renderers during rendering.

Theme defaults

themeDefaults on the definition ships default CSS variable values for the component. Lower priority than existing theme variables.

Change notification

registry.subscribe(fn) returns an unsubscribe function. Fires on registration and override changes.

Dependencies

Optional dependencies field declares other ui:* types this component needs.

Runtime integration

createGlyphRuntime({ components }) auto-registers plugins. runtime.registerComponent(def) adds after creation. Unknown types render a fallback.


Interaction Events

Components can emit interaction events when the block has interactive: true set in its YAML. The runtime gates the onInteraction prop — components only receive it when block.metadata.interactive === true.

Configuring the callback

Pass onInteraction to the runtime config:

import { createGlyphRuntime, debounceInteractions } from '@glyphjs/runtime';
import type { InteractionEvent } from '@glyphjs/types';
const runtime = createGlyphRuntime({
ir: compiledDocument,
components: builtinComponents,
onInteraction: (event: InteractionEvent) => {
console.log(`[${event.kind}] block=${event.blockId}`, event.payload);
},
});

debounceInteractions

A per-stream debounce helper that groups events by blockId + kind. Rapid repeated interactions (e.g., clicking sort headers) only fire the callback once after the delay:

import { debounceInteractions } from '@glyphjs/runtime';
const handler = debounceInteractions((event) => {
sendToAnalytics(event);
}, 300); // 300ms debounce per (blockId, kind) pair

InteractionEvent types

All event types are exported from @glyphjs/types:

import type {
InteractionEvent,
InteractionKind,
QuizSubmitEvent,
TableSortEvent,
TableFilterEvent,
TabSelectEvent,
AccordionToggleEvent,
FileTreeSelectEvent,
GraphNodeClickEvent,
ChartSelectEvent,
ComparisonSelectEvent,
CustomInteractionEvent,
} from '@glyphjs/types';

The InteractionEvent union is discriminated by the kind field. Use a switch on event.kind for exhaustive handling:

function handleEvent(event: InteractionEvent): void {
switch (event.kind) {
case 'chart-select':
console.log('Chart data point:', event.payload.label, event.payload.value);
break;
case 'quiz-submit':
console.log('Score:', event.payload.score.correct, '/', event.payload.score.total);
break;
case 'table-sort':
console.log('Sorted by:', event.payload.state.sortColumn, event.payload.state.sortDirection);
break;
// ... handle other event kinds
}
}

Emitting events from custom plugins

If you build a custom ui:* component, check for onInteraction in your props and call it with your event:

function MyWidget({ data, block, onInteraction }: GlyphComponentProps<MyData>) {
const handleClick = (item: string) => {
onInteraction?.({
kind: 'custom',
timestamp: new Date().toISOString(),
blockId: block.id,
blockType: block.type,
payload: { action: 'item-click', detail: item },
});
};
return <div onClick={() => handleClick('example')}>...</div>;
}

Custom plugins should use kind: 'custom' and place their event-specific data in payload.