@luckydraw/blex
v0.1.15
Published
Interactive content block rendering library — framework-agnostic, streaming-aware
Downloads
1,292
Maintainers
Readme
@luckydraw/blex
A TypeScript library for rendering interactive content blocks from JSON. Framework-agnostic, streaming-aware, with lazy-loaded heavy renderers.
Used by cumulus (AI chat) to display rich content inline in conversations, and by plastic (app platform) for charts, dashboards, and application view components.
Install
npm install @luckydraw/blexDistribution
Two build outputs from the same source:
- ESM —
import { renderBlock } from '@luckydraw/blex'— for bundled consumers (plastic, janus) - IIFE —
<script src="blex.min.js">→window.Blex— for cumulus widget (no build step) - Chart standalone —
blex-chart.min.js/import { renderChart } from '@luckydraw/blex/chart'
Target: ~11KB core (gzipped), heavy renderers lazy-loaded on first use.
Quick Start
import { renderBlock, registerBlockType } from '@luckydraw/blex';
// Render a block into a container
const handle = await renderBlock(
{ type: 'confirm', id: 'c1', data: { message: 'Deploy to production?' } },
document.getElementById('container')
);
// Listen for interactions
handle.onInteraction((interaction) => {
console.log(interaction.type); // 'click'
console.log(interaction.serialized); // 'User confirmed: Deploy to production?'
});
// Clean up
handle.destroy();Chart Subpath Export
A standalone chart API shared between cumulus and plastic. Fully independent — no side effects, no DOMPurify, no registry setup:
import { renderChart } from '@luckydraw/blex/chart';
const chart = renderChart(container, {
type: 'bar',
data: {
labels: ['Q1', 'Q2', 'Q3', 'Q4'],
datasets: [{ label: 'Revenue', values: [10, 25, 15, 30] }]
},
options: { title: 'Quarterly Revenue', legend: true }
});
chart.update({ data: { labels: ['Q1', 'Q2'], datasets: [{ label: 'Revenue', values: [10, 25] }] } });
chart.destroy();API
renderBlock(block, container, options?): Promise<BlockHandle>
Resolves the block type from the registry, loads the renderer (lazy if needed), renders into the container, and wires up interaction callbacks.
Options:
onReady?: () => void— called when the block finishes initial render
renderPlaceholder(type, container): HTMLElement
Renders a typed skeleton/spinner for a block that hasn't finished streaming yet. Returns the placeholder element for later replacement. Used by cumulus while buffering ~~~blex:TYPE fence content.
registerBlockType(type, renderer): void
Register a custom block type renderer. Overrides built-in renderers if the type matches.
registerBlockType('my-widget', {
render(block, container) { /* ... */ },
update(block) { /* streaming updates */ },
onInteraction(callback) { /* wire up events */ },
destroy() { /* cleanup */ }
});Types
interface ContentBlock<T = unknown> {
type: string;
data: T;
id: string; // unique per block instance, stable across re-renders
streaming?: boolean; // true while data is still arriving
}
interface BlockRenderer<T = unknown> {
render(block: ContentBlock<T>, container: HTMLElement): void;
update?(block: ContentBlock<T>): void;
onInteraction?(callback: (interaction: BlockInteraction) => void): void;
destroy(): void;
}
interface BlockHandle {
update(block: ContentBlock): void;
onInteraction(callback: (interaction: BlockInteraction) => void): void;
destroy(): void;
}
interface BlockInteraction {
blockId: string;
type: string; // 'select', 'click', 'submit', 'apply', 'reject', 'move'
payload: unknown;
serialized: string; // pre-formatted text for chat input injection
}Chart Types
interface ChartConfig {
type: 'bar' | 'line' | 'pie' | 'doughnut';
data: { labels: string[]; datasets: DataSet[] };
options?: { title?: string; legend?: boolean; responsive?: boolean };
}
interface DataSet {
label: string;
values: number[];
color?: string;
}
interface ChartInstance {
update(config: Partial<ChartConfig>): void;
destroy(): void;
}Built-in Block Types
Core Blocks (bundled, <5KB each)
| Type | Description | Interaction Types |
|------|-------------|-------------------|
| confirm | Yes/No/Cancel buttons | click |
| poll | Single/multi choice with optional write-in | submit |
| status | Key/value pairs with status indicators (ok/warning/error) | — |
| metric | Single big number with label and trend indicator | — |
| image | URL/base64 image with caption, zoom/download | click |
| svg | Sanitized inline SVG with download/copy | click |
| table | Sortable columns, row/cell/column selection | select |
| code | Syntax-highlighted code with line selection | select |
| diff | Unified or side-by-side diff | apply, reject |
| file-tree | Expandable directory tree | select |
| form | JSON schema → dynamic form | submit |
| progress | Live-updating step tracker | — |
| terminal | Collapsible command output with exit code | click |
| timeline | Vertical event timeline with timestamps | — |
Lazy-loaded Blocks
| Type | Description | Loaded Size |
|------|-------------|-------------|
| mermaid | Diagram rendering via mermaid.js | ~200KB |
| chart | Bar/line/pie/doughnut via chart library | ~60KB |
| kanban | Drag-and-drop columns via HTML5 DnD | ~8KB |
| calendar | Month/week/day event views | ~12KB |
| gallery | Image grid/carousel with lazy loading | ~5KB |
Heavy renderers are loaded on first use. Subsequent renders of the same type reuse the loaded module.
Unknown Types
Any unrecognized block type renders as raw JSON inside a collapsible <details> element, so content is never lost.
Theming
blex uses CSS custom properties with sensible defaults. Consumers override them to match their theme:
/* blex defaults — override in your app */
--blex-bg: #ffffff;
--blex-text: #1a1a1a;
--blex-border: #e5e5e5;
--blex-accent: #3b82f6;
--blex-success: #22c55e;
--blex-warning: #f59e0b;
--blex-error: #ef4444;
--blex-chart-bg: var(--blex-bg);
--blex-chart-text: var(--blex-text);
--blex-animation-duration: 200ms;Dark mode: Set data-theme="dark" on a parent element. blex adjusts defaults automatically.
Test mode: Set data-test-mode="true" or --blex-animation-duration: 0ms to disable all animations for puppet/browser testing.
Streaming
Renderers that implement update() receive incremental data without a full destroy/re-render cycle. The streaming flag on ContentBlock indicates data is still arriving.
const handle = await renderBlock(
{ type: 'table', id: 't1', data: { rows: [] }, streaming: true },
container
);
// As data arrives:
handle.update({ type: 'table', id: 't1', data: { rows: newRows }, streaming: true });
// When complete:
handle.update({ type: 'table', id: 't1', data: { rows: allRows }, streaming: false });When streaming: false (e.g., historical message replay), blocks render immediately with no transition/animation.
Interaction Serialization
Every BlockInteraction includes a serialized field — a pre-formatted text string suitable for injecting into a chat input. Each block type controls its own serialization format.
// Table selection might serialize as:
{
blockId: 't1',
type: 'select',
payload: { rows: [0, 2], columns: ['name', 'status'] },
serialized: 'Selected from table:\n| name | status |\n| Alice | active |\n| Charlie | pending |'
}Consumers receive interactions via the BlockHandle.onInteraction() callback and can forward serialized text directly to the chat input.
Cleanup Contract
destroy() is a hard guarantee: after calling it, zero references remain. This includes:
- Event listeners (click, resize, keyboard)
- Animation frames (RAF callbacks)
- Canvas contexts
- Resize/intersection/mutation observers
- Timers (setTimeout, setInterval)
- Pending lazy-load promises
- DOM references
Enforced via BaseRenderer abstract class with cleanup tracking. All built-in renderers extend it.
Design Decisions
- Vanilla JS/TS — no React dependency. Consumers wrap as needed.
- Dual output — ESM for bundled consumers, IIFE (
window.Blex) for cumulus widget. - DOMPurify — the one required runtime dependency, used for SVG/HTML sanitization.
- Lazy loading — heavy renderers (mermaid, chart.js, kanban, calendar, gallery) loaded on first use.
- Subpath export —
@luckydraw/blex/chartis fully independent: no side effects, no registry, no DOMPurify. JustrenderChart()+ Chart.js lazy load. - CSS custom properties —
--blex-*variables for theming. No hardcoded colors. data-testid— on all interactive elements for puppet/browser automation.- BaseRenderer — abstract class enforcing
destroy()cleanup contract.
Package Structure
src/
├── index.ts # main entry: renderBlock, registerBlockType, renderPlaceholder
├── types.ts # ContentBlock, BlockRenderer, BlockInteraction, BlockHandle
├── registry.ts # block type registry + lazy loader
├── base-renderer.ts # BaseRenderer abstract class with cleanup tracking
├── fallback.ts # unknown type fallback renderer
├── placeholder.ts # streaming skeleton/placeholder utility
├── theme.ts # CSS custom property defaults + dark mode
├── chart/
│ └── index.ts # subpath export: renderChart (fully independent)
└── renderers/
├── confirm.ts
├── poll.ts
├── status.ts
├── metric.ts
├── image.ts
├── mermaid.ts # lazy-loaded
├── svg.ts
├── table.ts
├── chart.ts # lazy-loaded, wraps chart/index.ts
├── code.ts
├── diff.ts
├── file-tree.ts
├── form.ts
├── progress.ts
├── terminal.ts
├── timeline.ts
├── kanban.ts # lazy-loaded
├── calendar.ts # lazy-loaded
└── gallery.ts # lazy-loadedLicense
MIT
