Debugclass

DebugManager

Create a lazy debug pane and control when it appears.

Demo

URL parameterxyzdebug=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.

ArgumentDescription
options.createPaneRequired 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.

ArgumentDescription
codeRequired 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.

ArgumentDescription
sourceAn 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.

ArgumentDescription
definitionA titled folder definition or a factory returning one. Factories run on each mount.
definition.titleRequired folder title.
definition.expanded?Initial expanded state; when omitted, uses Tweakpane's default.
definition.schemaRequired 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.