npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@luckydraw/blex

v0.1.15

Published

Interactive content block rendering library — framework-agnostic, streaming-aware

Downloads

1,292

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/blex

Distribution

Two build outputs from the same source:

  • ESMimport { renderBlock } from '@luckydraw/blex' — for bundled consumers (plastic, janus)
  • IIFE<script src="blex.min.js">window.Blex — for cumulus widget (no build step)
  • Chart standaloneblex-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/chart is fully independent: no side effects, no registry, no DOMPurify. Just renderChart() + 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-loaded

License

MIT