Widgets — the @readrun/widgets toolkit

readrun ships a small React-based component library for building interactive visualisations. Author .tsx widgets in your content folder; rr bundles each one to a self-contained .jsx file and the existing JSX runtime mounts them through the same [jsx=...] block syntax used elsewhere.

Where files live

text
my-content/
  .readrun/
    widgets/              # source — author here
      my-widget.tsx
    .widgets-out/         # output — written by rr, do not edit by hand
      my-widget.jsx       # generated, includes a banner

readrun builds widget sources before serving, validating, building, or deploying content:

bash
rr serve .
rr validate .
rr build .
rr deploy github .
rr docs

You can also run the widget bundler directly:

bash
rr widgets-build .          # bundles every .tsx in ./.readrun/widgets now
rr widgets-build path/to/content

During rr serve, changes under .readrun/widgets/ trigger a rebuild and page reload. Generated files under .readrun/.widgets-out/ are left alone unless the compiled widget body actually changes.

Authoring a widget

A widget is a kebab-named .tsx file with a single named export whose PascalCase matches the file name.

tsx
import React, { useState } from "react";
import { WidgetLayout, Slider, Stat } from "@readrun/widgets/primitives";
import { Frame, scale } from "@readrun/widgets/plot";

export function MyWidget() {
  const [n, setN] = useState(50);
  return (
    <WidgetLayout
      title="My widget"
      subtitle="Move the slider, watch the bar grow."
      arrangement="visual-left"
    >
      <WidgetLayout.Visual>
        <svg viewBox="0 0 320 240" width="100%" height={240}>
          <rect x={0} y={100} width={n * 3} height={40} fill="var(--accent)" />
        </svg>
      </WidgetLayout.Visual>
      <WidgetLayout.Controls>
        <Slider label="n" value={n} min={0} max={100} step={1} onChange={setN} />
        <Stat label="value" value={n} />
      </WidgetLayout.Controls>
    </WidgetLayout>
  );
}

File my-widget.tsx → export MyWidget → bundled as my-widget.jsx.

Public API surface

All subpaths resolve via the bundler — no separate widget package is involved.

table
2 cols · 6 rows
col width
28ch
SubpathWhat’s exported
@readrun/widgets/primitivesWidgetLayout, Slider, Stat, Btn, Tabs, ToggleRow, Notice, LegendDot, Shell, Panel, SectionLabel
@readrun/widgets/plotFrame, Axis, Heatmap, Histogram, scale.linear, scale.log, scale.ordinal, ticks(scale, n)
@readrun/widgets/diagramFlow, layout.dag, layout.tree, layout.force, edge.straight, edge.curve, edge.orthogonal, port-based routing helpers
@readrun/widgets/interactionuseDrag, useRaf, useTrace, usePointer, Handle
@readrun/widgets/drawpalette (named + perceptual), easing, lerp, tween, polyline, arc
@readrun/widgets/mathgamma, random, linalg (decompose, eig2, Mat2), force (pairwise repulsion + springs)

Deep subpaths also resolve, e.g. @readrun/widgets/math/linalg.

Embedding a widget on a page

In your markdown:

markdown
[jsx=my-widget.jsx]

readrun finds .readrun/.widgets-out/my-widget.jsx, embeds it inline, and mounts it on page load through Babel-standalone.

Conventions

  • Kebab → Pascal must match. my-widget.tsx exports MyWidget. The bundler errors otherwise. Acronyms force-fit: is-lm-explorer.tsxIsLmExplorer, not ISLMExplorer.
  • Named export, not default. export function PascalName() — required.
  • Sharp corners + monospace numbers. The toolkit’s CSS uses --radius: 0 and assumes monospace for value displays. Don’t override per-widget unless you mean to.
  • No <LineChart> shortcuts. Frame is a scaffold with a render-prop; hand-draw series with inline SVG inside it.
  • Theme tokens. Use var(--accent), var(--bg), var(--text), var(--border), var(--muted) rather than hex.

Conflict rule

The bundler refuses to overwrite a hand-written .readrun/.widgets-out/<name>.jsx that does not carry the // generated by @readrun/widgets banner. If you have a hand-written foo.jsx and a foo.tsx widget, rename one. This prevents rr serve from silently clobbering hand-authored output.

Reference widgets

The docs/.readrun/widgets/ folder contains toolkit demos and reference widgets that exercise every part of the toolkit (distribution-explorer, force-graph, is-lm-explorer, kmeans-clustering, and more). Open the demo:

bash
rr docs

…and browse Widget Library from the sidebar. Their source is fair game for copying into your own content folder as a starting point.