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
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:
rr serve .
rr validate .
rr build .
rr deploy github .
rr docs
You can also run the widget bundler directly:
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.
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.
| Subpath | What’s exported |
|---|---|
@readrun/widgets/primitives | WidgetLayout, Slider, Stat, Btn, Tabs, ToggleRow, Notice, LegendDot, Shell, Panel, SectionLabel |
@readrun/widgets/plot | Frame, Axis, Heatmap, Histogram, scale.linear, scale.log, scale.ordinal, ticks(scale, n) |
@readrun/widgets/diagram | Flow, layout.dag, layout.tree, layout.force, edge.straight, edge.curve, edge.orthogonal, port-based routing helpers |
@readrun/widgets/interaction | useDrag, useRaf, useTrace, usePointer, Handle |
@readrun/widgets/draw | palette (named + perceptual), easing, lerp, tween, polyline, arc |
@readrun/widgets/math | gamma, 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:
[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.tsxexportsMyWidget. The bundler errors otherwise. Acronyms force-fit:is-lm-explorer.tsx→IsLmExplorer, notISLMExplorer. - Named export, not default.
export function PascalName()— required. - Sharp corners + monospace numbers. The toolkit’s CSS uses
--radius: 0and assumes monospace for value displays. Don’t override per-widget unless you mean to. - No
<LineChart>shortcuts.Frameis 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:
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.