Panels
Create a panel, mount it into your layout, and control it at runtime.
A panel is the root container for your controls. You create one with
createPanel, then call builder methods on it to add sliders, folders, monitors,
and everything else.
createPanel
function createPanel(config?: PanelConfig): ParamPanel;import { createPanel } from "param-panel";
const pp = createPanel({
id: "edge-detector-panel",
title: "Edge Detector",
position: "top-right",
density: "compact",
});PanelConfig
Every field is optional.
interface PanelConfig {
/** Title shown at the top of the panel. */
title?: string;
/** Mount target. Defaults to document.body. */
container?: HTMLElement;
/** Initial position when mounted to the document. */
position?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
/**
* Drag a floating panel by its header; on release it snaps to the nearest
* window corner. Defaults to true for panels mounted to document.body.
*/
draggable?: boolean;
/** Initial collapsed state. Defaults to false. */
collapsed?: boolean;
/** Visual density. */
density?: "compact" | "comfortable" | "touch";
/** Additional root CSS class. */
className?: string;
/** Optional stable ID for serialization and diagnostics. */
id?: string;
}Mounting into your layout
By default a panel mounts to document.body, positions itself in a corner of the
viewport, and is draggable by its header — drag it anywhere and it snaps to
the nearest corner, staying pinned there when the window resizes. Pass
draggable: false to disable this. To place the panel inside your own layout,
pass a container; a contained panel is not draggable and ParamPanel does not
force viewport positioning:
const sidebar = document.querySelector<HTMLElement>("#sidebar");
if (!sidebar) {
throw new Error("Missing #sidebar");
}
const pp = createPanel({
title: "Inspector",
container: sidebar,
});The ParamPanel interface
A panel is itself a control container (so it exposes every builder) and a control
(so it has configure, destroy, and friends). It adds panel-level actions:
interface ParamPanel extends Control<PanelRuntimeConfig>, ControlContainer {
setTitle(title: string): this;
expand(): this;
collapse(): this;
setExpanded(expanded: boolean): this;
isExpanded(): boolean;
expandAll(): this;
collapseAll(): this;
refresh(): this;
onChange(listener: PanelChangeListener): Unsubscribe;
getState(): PanelState;
setState(state: PanelState, options?: SetStateOptions): this;
}Collapsing
pp.collapse();
pp.expand();
pp.setExpanded(window.innerWidth > 800);expandAll() and collapseAll() recursively open or close every folder in the
panel — handy for large inspectors.
Runtime configuration
Because the panel is a control, configure() accepts a PanelRuntimeConfig:
interface PanelRuntimeConfig {
title?: string;
disabled?: boolean;
visible?: boolean;
density?: "compact" | "comfortable" | "touch";
className?: string;
}pp.configure({ density: "touch", title: "Inspector (mobile)" });Refresh
pp.refresh();refresh() re-reads values and redraws. You only need it for
getter/setter bindings that cannot
notify ParamPanel about external mutations — reactive bindings update
automatically.
Observing all changes
onChange observes every value-control change in the panel from one place —
instead of subscribing to each control by hand. It is the seam that the
persistence and URL
adapters build on.
interface PanelChange {
id: string; // path-based id of the changed control
control: ValueControl<unknown, unknown>;
value: unknown;
details: ChangeDetails<unknown>;
}
type PanelChangeListener = (change: PanelChange) => void;Only controls with a stable id are reported (the id is what makes a change
addressable). For example, log every committed change:
const stop = pp.onChange((change) => {
if (change.details.final) {
console.log(`${change.id} = ${change.value}`);
}
});It returns an Unsubscribe function.
Adding controls
Every builder lives on the panel (and on folders). Full details are in the Controls and Visualizations sections, but here is the container surface at a glance:
interface ControlContainer {
slider(config: SliderConfig): SliderControl;
number(config: NumberConfig): NumberControl;
checkbox(config: CheckboxConfig): CheckboxControl;
text(config: TextConfig): TextControl;
textarea(config: TextareaConfig): TextareaControl;
select<T>(config: SelectConfig<T>): SelectControl<T>;
color(config: ColorConfig): ColorControl;
button(config: ButtonConfig): ButtonControl;
folder(config: FolderConfig): FolderControl;
row(config?: RowConfig): RowControl;
tabs(config?: TabsConfig): TabsControl;
monitor<T>(config: MonitorConfig<T>): MonitorControl<T>;
graph(config: GraphConfig): GraphControl;
canvas(config: CanvasConfig): CanvasControl;
}A complete small panel
import { createPanel } from "param-panel";
const pp = createPanel({
title: "Demo Parameters",
});
const speed = pp.slider({
label: "Speed",
value: 1,
min: 0,
max: 10,
step: 0.1,
});
const enabled = pp.checkbox({
label: "Enabled",
value: true,
});
speed.subscribe((value) => {
simulation.speed = value;
});
enabled.subscribe((value) => {
simulation.enabled = value;
});