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[];}| Field | Type | Required | Description |
|---|---|---|---|
type | backtick ui:… backtick | Yes | Block type identifier. Must start with ui:. |
schema | Zod-compatible | Yes | Validates the block data payload. Must expose parse and safeParse. |
render | ComponentType | Yes | React component that renders the block. |
themeDefaults | Record<string, string> | No | Default CSS variable values for this component. |
dependencies | string[] | No | Other 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:
- Runtime validation — When the runtime encounters a block with your
ui:*type, it callsschema.safeParse(block.data)to validate the data. If validation fails, the block renders with diagnostic errors. - 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.safeParseis 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;}| Prop | Type | Description |
|---|---|---|
data | T | The validated block data (output of schema.parse). |
block | Block | The full block object, including id, type, position, children, and metadata. |
outgoingRefs | Reference[] | References originating from this block. |
incomingRefs | Reference[] | References targeting this block. |
onNavigate | (ref: Reference) => void | Callback to trigger navigation along a reference. |
onInteraction | (event) => void | Optional callback for interaction events. Only present when interactive: true is set on the block. Components emit events without documentId; the runtime injects it automatically. |
theme | GlyphThemeContext | Current theme context with resolveVar, isDark, and name. |
layout | LayoutHints | Global layout configuration (mode, columns, spacing). |
container | ContainerContext | Container 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 componentregistry.registerComponent(myComponentDefinition);
// Bulk-register multiple componentsregistry.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:
typemust match theui:*format (e.g.ui:chart,ui:graph).schemamust be present withparseandsafeParsemethods (Zod-compatible).rendermust be a function (functional component) or class (class component).
Lookups
// Get a registered component definitionconst def = registry.getRenderer('ui:graph');
// Check if a type is registeredconst exists = registry.has('ui:graph');
// Get all registered type namesconst 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 rendererconst 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) pairInteractionEvent 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.