bindDebugSchema
Describe values, controls, groups, and actions once, then bind them to Tweakpane.
Demo
{
"amount": 0.42,
"speed": 1.2,
"enabled": true,
"visible": false,
"mode": "balanced",
"cssColor": "#7c5cff",
"hex": "#ffb454",
"actions": 0,
"tint": {
"r": 0.3,
"g": 0.7,
"b": 1
}
}'use client'
import { bindDebugSchema } from '@joycostudio/xyz/debug'
import { useEffect, useRef, useState } from 'react'
import { Pane } from 'tweakpane'
import { DemoPanel } from '@/components/demos/demo-ui'
import { JsonOutput } from './json-output'
import { definitionFor, makeState } from './schema'
export default function PaneDemo() {
const host = useRef<HTMLDivElement>(null)
const [, render] = useState(0)
const [state] = useState(() => makeState(() => render((value) => value + 1)))
useEffect(() => {
if (!host.current) return
const pane = new Pane({ container: host.current, title: 'bindDebugSchema' })
const bound = bindDebugSchema(
pane,
definitionFor(state, () => render((value) => value + 1)),
{
sectionName: 'bindDebugSchema',
copyText: async () => undefined,
}
)
return () => {
bound.dispose()
pane.dispose()
}
}, [state])
const { tint, ...snapshot } = state
return (
<DemoPanel>
<div className="grid min-w-0 items-start gap-gap sm:grid-cols-2">
<div ref={host} className="debug-host min-w-0" />
<JsonOutput value={{ ...snapshot, tint: { r: tint.r, g: tint.g, b: tint.b } }} />
</div>
</DemoPanel>
)
}
Usage
import { bindDebugSchema, bool, property, slider } from '@joycostudio/xyz/debug'
import { Pane } from 'tweakpane'
const state = { speed: 1, enabled: true }
const pane = new Pane()
const controls = bindDebugSchema(pane, {
schema: () => ({
speed: slider(property(state, 'speed'), { min: 0, max: 3 }),
enabled: bool(property(state, 'enabled')),
}),
})
// Controls write to state. Refresh after changing state from code.
state.speed = 2
controls.refresh()
controls.values() // { speed: 2, enabled: true }
// When the owner is removed:
controls.dispose()
pane.dispose()
Binding
bindDebugSchema
bindDebugSchema(host: FolderApi, definition: DebugDefinition, options?: BindDebugOptions): BoundDebug
Creates controls in an existing Tweakpane pane or folder. Use this directly when you own the pane's lifecycle. DebugManager.bind() calls it internally when mounting a registered definition, so you do not need to call both.
| Argument | Description |
|---|---|
host | The pane or folder that receives the controls. The caller owns and disposes this host. |
definition | An object containing the schema factory and optional behavior described below. |
definition.schema | Required factory returning a record of control fields. |
definition.copy? | Adds a copy-values button when true. Off when omitted. |
definition.dispose? | Cleanup function, disposable object, or array of either, run when the binding is disposed. |
definition.extend? | Receives the Tweakpane folder and { fields } (see fields() below) after the schema is bound, to add custom controls; may return cleanup. |
definition.sources? | Source descriptors with label: string and value: () => object, collected in controls.sources. The built-in copy button reads schema values instead. |
options.sectionName? | Wraps copied values under this name. |
options.registerFrame? | Receives a refresh callback to register with your frame loop and returns its teardown. Required for automatic updates of controls with live: true. |
options.copyText? | Receives serialized values and returns void or a promise. Defaults to navigator.clipboard.writeText. |
options.onError? | Receives (error, context) when a control, action, or copy operation fails. Defaults to logging to the console. |
Returns a BoundDebug controller:
apply(): Writes the controls' current values to their sources, then refreshes.refresh(): Pulls current source values into the controls, including nested folders.values(): Reads the current nested value object, excluding buttons.fields(): Lists every bound schema field, nested folders' fields included, in schema order. Each entry haspath(schema keys from this definition's root, never labels),field(the boundDebugField), andblade(theBindingApi,FolderApi, orButtonApicreated for it). The list is fixed at bind time and excludes the copy button and anything added byextend.sources: Collects declaredDebugSourceentries from this definition and its children.dispose(): Removes created controls, nested folders, frame callbacks, and extension teardowns, and runs the definition's cleanup. The host remains owned by the caller.
References
ref
ref<T>(read: () => T, write: (value: T) => void): ValueRef<T>
Adapts getter and setter functions into a mutable reference.
| Argument | Description |
|---|---|
read | Returns the current source value whenever the reference is read. |
write | Updates the source whenever the reference is assigned a value. |
const state = { speed: 1 }
const speed = ref(
() => state.speed,
(value) => {
state.speed = value
}
)
property
property<TObject, TKey extends keyof TObject>(
object: TObject,
key: TKey,
changed?: () => void
): ValueRef<TObject[TKey]>
Creates a typed reference to one object property.
| Argument | Description |
|---|---|
object | The object to read from and write to. |
key | A key of that object. The reference keeps the property's value type. |
changed? | A callback with no arguments, called after the reference writes to the property. Direct changes to the object do not trigger it. |
const state = { speed: 1 }
const speed = property(state, 'speed', () => console.log(state.speed))
Number and boolean controls
Control helpers return a DebugField to place in a schema. Their optional label overrides the schema key displayed beside the control. live defaults to off; set it to true and provide registerFrame in the binding options to reflect source changes automatically.
slider
slider(store: ValueRef<number>, options: SliderOptions): DebugField
Creates one numeric slider.
| Argument | Description |
|---|---|
store | A numeric reference, usually from property or ref. |
options.min | Required lower bound. |
options.max | Required upper bound. |
options.step? | Numeric increment. Omitted by default. |
options.label? | Display label. |
options.live? | Refresh from the source on registered frames. |
const state = { speed: 1 }
slider(property(state, 'speed'), { min: 0, max: 3, step: 0.1, label: 'Speed' })
sliders
sliders(object, specs, changed?): DebugSchema
Creates several sliders for numeric properties of one object. Spread the result into your schema.
| Argument | Description |
|---|---|
object | The object whose numeric properties will be edited. |
specs | A map of numeric property keys to SliderSpec tuples: [min, max, step?, label?]. Only listed keys get controls. |
changed? | A callback with no arguments, called after any generated control writes to the object. |
const state = { speed: 1, intensity: 0.5 }
const schema = () => ({
...sliders(state, {
speed: [0, 3, 0.1, 'Speed'],
intensity: [0, 1],
}),
})
bool
bool(store: ValueRef<boolean>, options?: { label?: string; live?: boolean }): DebugField
Creates one boolean toggle.
| Argument | Description |
|---|---|
store | A boolean reference. |
options.label? | Display label. |
options.live? | Refresh from the source on registered frames. |
const state = { enabled: true }
bool(property(state, 'enabled'), { label: 'Enabled' })
toggles
toggles(object, specs, changed?): DebugSchema
Creates several toggles for boolean properties of one object. Spread the result into your schema.
| Argument | Description |
|---|---|
object | The object whose boolean properties will be edited. |
specs | A map of boolean property keys to a label string or true. Use true to keep the property key as the label. Only listed keys get controls. |
changed? | A callback with no arguments, called after any generated control writes to the object. |
const state = { enabled: true, wireframe: false }
const schema = () => ({
...toggles(state, { enabled: true, wireframe: 'Show wireframe' }),
})
Choices and colors
select
select<T extends string | number>(
store: ValueRef<T>,
options: Record<string, T>,
config?: { label?: string; live?: boolean }
): DebugField
Creates a dropdown for string or number values.
| Argument | Description |
|---|---|
store | A reference to the selected value. |
options | A map from displayed option labels to stored values. |
config.label? | Display label for the control. |
config.live? | Refresh from the source on registered frames. |
const state = { quality: 'high' }
select(property(state, 'quality'), { Low: 'low', High: 'high' }, { label: 'Quality' })
enumField
enumField(store, options, config?): DebugField
Alias of select, with the same arguments and behavior. Pass the selected-value reference, the label-to-value map, and optional label / live configuration.
const state = { quality: 'high' }
enumField(property(state, 'quality'), { Low: 'low', High: 'high' })
color
color<T extends RgbColor>(store: ValueRef<T>, options?: { label?: string; live?: boolean }): DebugField
Creates an RGB color control configured for floating-point channels. Edits call the existing object's setRGB method, preserving its identity.
| Argument | Description |
|---|---|
store | A reference to an object with r, g, and b numeric channels and a setRGB(r, g, b) method. |
options.label? | Display label. |
options.live? | Refresh from the source on registered frames. |
const state = {
tint: {
r: 1,
g: 0.5,
b: 0,
setRGB(r: number, g: number, b: number) {
Object.assign(this, { r, g, b })
},
},
}
color(property(state, 'tint'), { label: 'Tint' })
colorString
colorString(store: ValueRef<string>, options?: { label?: string; live?: boolean }): DebugField
Creates a color picker backed by a CSS color string.
| Argument | Description |
|---|---|
store | A string reference containing a color value supported by Tweakpane, such as '#ff8800'. |
options.label? | Display label. |
options.live? | Refresh from the source on registered frames. |
const state = { tint: '#ff8800' }
colorString(property(state, 'tint'), { label: 'Tint' })
hexColor
hexColor(store: ValueRef<string>, options?: { label?: string; live?: boolean }): DebugField
Alias of colorString, with the same string reference and optional label / live arguments. It does not add hex-only validation.
const state = { tint: '#ff8800' }
hexColor(property(state, 'tint'))
Structure and actions
folder
folder(definition: DebugDefinition & { title?: string; expanded?: boolean }): DebugField
Nests a debug definition inside the current schema. Also accepts an existing DebugFolder, such as a single definition returned by an object's debug() method or passed to DebugManager.bind().
| Argument | Description |
|---|---|
definition | The nested schema factory and optional copy, extension, source, and cleanup behavior. |
definition.title? | Folder title. Defaults to the schema key. |
definition.expanded? | Initial expanded state. When omitted, uses Tweakpane's default. |
const state = { speed: 1 }
const schema = () => ({
motion: folder({
title: 'Motion',
expanded: true,
schema: () => ({ speed: slider(property(state, 'speed'), { min: 0, max: 3 }) }),
}),
})
section
section(schema: () => DebugSchema, options?: SectionOptions): DebugField
Creates a nested folder that is collapsed and includes a copy-values button by default.
| Argument | Description |
|---|---|
schema | Factory returning the nested fields. |
options.label? | Folder title. Defaults to the schema key. |
options.expanded? | Initial expanded state. Defaults to false. |
options.copy? | Show the copy-values button. Defaults to true. |
options.dispose? | Teardown to run when the section is disposed. |
options.extend? | Receives the section's Tweakpane folder and { fields } with paths relative to the section; may return a teardown. |
options.sources? | Source descriptors included in the bound controller's sources array. |
const state = { speed: 1 }
const schema = () => ({
motion: section(() => ({ speed: slider(property(state, 'speed'), { min: 0, max: 3 }) }), { label: 'Motion' }),
})
button
button(title: string, run: () => void | Promise<void>, feedback?: string): DebugField
Creates an action button. After the action completes successfully, the bound controls refresh from their sources.
| Argument | Description |
|---|---|
title | Button text. |
run | Callback invoked on click. May be synchronous or return a promise. |
feedback? | Temporary button text shown for 1.2 seconds after success. |
const state = { speed: 1 }
button(
'Reset speed',
() => {
state.speed = 1
},
'Reset ✓'
)
schemaValues
schemaValues(schema: DebugSchema): Record<string, unknown>
Reads current values without creating any Tweakpane controls.
| Argument | Description |
|---|---|
schema | The field record to read. Nested folder factories are evaluated recursively, and buttons are omitted from the result. |
const state = { speed: 1 }
schemaValues({ speed: slider(property(state, 'speed'), { min: 0, max: 3 }) })
// { speed: 1 }