DebugManager
Create a lazy debug pane and control when it appears.
Demo
xyzdebug=absent'use client'
import { DebugManager, bool, ref } from '@joycostudio/xyz/debug'
import { useEffect, useRef, useState } from 'react'
import { Pane } from 'tweakpane'
import { Button } from '@/components/ui/button'
import { Kbd, KbdGroup } from '@/components/ui/kbd'
import { DemoPanel, ValueRow, ValueRows } from '@/components/demos/demo-ui'
export default function ManagerDemo() {
const host = useRef<HTMLDivElement>(null)
const manager = useRef<DebugManager | null>(null)
const [enabled, setEnabled] = useState(false)
useEffect(() => {
if (!host.current) return
const instance = new DebugManager({
parameter: 'xyzdebug',
createPane: () => new Pane({ container: host.current ?? undefined, title: 'DebugManager' }),
})
instance.bind({
title: 'Runtime',
expanded: true,
schema: () => ({
active: bool(
ref(
() => true,
() => undefined
)
),
}),
})
manager.current = instance
setEnabled(instance.enabled)
const sync = () => queueMicrotask(() => setEnabled(instance.enabled))
window.addEventListener('keydown', sync)
return () => {
window.removeEventListener('keydown', sync)
instance.dispose()
manager.current = null
}
}, [])
const toggle = () => {
manager.current?.toggle()
setEnabled(manager.current?.enabled ?? false)
}
return (
<DemoPanel>
<Button onClick={toggle} className="pr-2">
{enabled ? 'Disable manager' : 'Enable manager'}
<KbdGroup className="text-primary-foreground [&_kbd]:bg-primary-foreground/20 [&_kbd]:border-primary-foreground/20 [&_kbd]:text-primary-foreground">
<Kbd>⌥</Kbd>
<Kbd>D</Kbd>
</KbdGroup>
</Button>
<div ref={host} className="debug-host mt-5 max-w-sm" />
<ValueRows className="mt-gap">
<ValueRow label="URL parameter">xyzdebug={enabled ? 'true' : 'absent'}</ValueRow>
</ValueRows>
</DemoPanel>
)
}
Usage
import { DebugManager, property, slider } from '@joycostudio/xyz/debug'
import { Pane } from 'tweakpane'
const state = { speed: 1 }
const debug = new DebugManager({ createPane: () => new Pane() })
debug.bind({
title: 'Motion',
schema: () => ({ speed: slider(property(state, 'speed'), { min: 0, max: 3 }) }),
})
debug.enable() // Show the pane; dragging the slider updates state.speed.
// Option+D toggles it. Opening the page with ?debug=true also enables it.
// When the owner is removed:
debug.dispose()
API
new DebugManager
new DebugManager(options: DebugManagerOptions)
Owns URL/keyboard activation and lazy pane creation. Registrations survive disable/enable cycles and mount again from their definitions.
| Argument | Description |
|---|---|
options.createPane | Required factory returning a pane, or a promise of one. The pane must provide addFolder() and dispose(). Called when debug is enabled. |
options.parameter? | URL parameter controlling activation. Defaults to 'debug'. Presence enables debug unless the value is 0, false, no, or off (case-insensitive). |
options.shortcuts? | One shortcut object, an array of them, or false to disable keyboard activation. Defaults to { code: 'KeyD', altKey: true }. Shortcut fields are described below. |
options.binding? | Shared binding options applied whenever a definition mounts. |
options.binding.registerFrame? | Receives a refresh callback to attach to your frame loop and returns its teardown. Required for automatic refresh of controls with live: true. |
options.binding.copyText? | Receives serialized values and returns void or a promise. Defaults to navigator.clipboard.writeText. |
options.onError? | Receives (error, context) for pane, binding, action, or cleanup failures. Defaults to console logging. |
options.window? | Browser window to use. Defaults to the global window when available. |
For a single shortcut, use these fields on options.shortcuts; for an array, use them on each entry. Modifier flags must match exactly.
| Argument | Description |
|---|---|
code | Required keyboard event code, such as 'KeyD'. |
altKey? | Require Alt (Option on Mac). Defaults to false. |
ctrlKey? | Require Control. Defaults to false. |
metaKey? | Require Meta (Command on Mac). Defaults to false. |
shiftKey? | Require Shift. Defaults to false. |
const debug = new DebugManager({
createPane: () => new Pane(),
parameter: 'debug',
shortcuts: { code: 'KeyD', altKey: true },
onError: (error, context) => console.error(context, error),
})
register
debug.register(source: Debuggable): Disposable
Registers an object that supplies debug folders. Returns a disposable registration; disposing it removes its controls and prevents future remounts.
Debuggable is exported from @joycostudio/xyz/debug. A class implements it by exposing debug(), which returns one titled folder definition or a readonly array of them. Each folder supplies a schema() factory; its other options are described under bind.
Call debug.register(instance) to register the class instance. Implementing the interface checks its shape at compile time; the manager does not discover instances automatically. The manager calls debug() when the pane mounts and again after each disable/enable cycle.
| Argument | Description |
|---|---|
source | An object with a debug() method returning one titled folder definition or an array of them. Called again on each mount. |
import { DebugManager, type Debuggable, property, slider } from '@joycostudio/xyz/debug'
import { Pane } from 'tweakpane'
class Motion implements Debuggable {
speed = 1
debug() {
return {
title: 'Motion',
schema: () => ({
speed: slider(property(this, 'speed'), { min: 0, max: 3 }),
}),
}
}
}
const motion = new Motion()
const debug = new DebugManager({ createPane: () => new Pane() })
const registration = debug.register(motion)
debug.enable()
// When the Motion owner is removed:
registration.dispose() // Remove its controls and prevent future remounts.
// When the application is removed:
debug.dispose()
bind
debug.bind(definition: DebugFolder | (() => DebugFolder)): Disposable
Registers one folder and calls bindDebugSchema internally when mounting it. Returns a disposable registration. If debug is disabled, control creation waits until activation.
| Argument | Description |
|---|---|
definition | A titled folder definition or a factory returning one. Factories run on each mount. |
definition.title | Required folder title. |
definition.expanded? | Initial expanded state; when omitted, uses Tweakpane's default. |
definition.schema | Required factory returning the 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 unmounts. |
definition.extend? | Receives the Tweakpane folder and { fields } to add custom controls; may return cleanup. |
definition.sources? | Source descriptors with a label and value: () => object. Collected by the underlying binding. |
For a factory, the fields above describe its returned object.
const state = { speed: 1 }
const registration = debug.bind(() => ({
title: 'Motion',
expanded: true,
schema: () => ({
speed: slider(property(state, 'speed'), { min: 0, max: 3 }),
}),
}))
debug.enable() // Mount controls; the factory runs again after disable/enable.
registration.dispose()
enable
debug.enable(): void
Enables debug, adds the configured URL parameter with value true, and creates the pane lazily. Takes no arguments; does nothing if already enabled.
disable
debug.disable(): void
Disables debug, removes the configured URL parameter, and disposes the pane and mounted controls. Takes no arguments; registrations remain available for the next enable cycle.
toggle
debug.toggle(): void
Switches between enabled and disabled, updating the URL. Takes no arguments.
sync
debug.sync(): void
Re-reads activation from the current URL without rewriting it. Takes no arguments. The manager also calls it on popstate.
enabled
debug.enabled // boolean
Read-only current activation state. Pane creation may still be pending when this is true.
dispose
debug.dispose(): void
Unmounts the pane, clears registrations, and removes browser listeners. Takes no arguments and leaves the URL unchanged.