Dial, A Schema-Driven Menu System

Dial CLI
dial-cliv0.0.22

Dial is a schema-driven menu system that generates UI controls from TypeScript interfaces using JSDoc annotations.

The Dial Annotation Syntax: Dial uses JSDoc annotations to define UI controls:

  • Grouping:
    • @dial <grouping> - Groups related properties (e.g., @dial transform, @dial visibility)
    • @dial <grouping> @dial-no-wrap - Apply group-level configuration (e.g., prevent line wrapping)
  • Properties:
    • @dial-<property> <value> - Sets control properties with hyphen notation
    • @dial-col-<n> - Display in column layout with n columns
    • @dial-dtype <type> - Data type (vector3, euler, boolean, int, etc.)
    • @dial-min <number> - Minimum value
    • @dial-max <number> - Maximum value
    • @dial-step <number> - Step increment
    • @dial-options [...] - Preset values array
    • @dial-icon <name> - Lucide icon name
    • @dial-label <text> - Custom label for the property in UI
    • @dial-ignore - Exclude property from dial schema generation

Here's a complete example showing how to annotate TypeScript types with Dial annotations. The docgen tool will parse these annotations and generate a schema that can be used to automatically create UI controls.

// Define a tuple type with per-element constraints
type ExampleBoxArgs = [
  // @dial-min 0.01 @dial-max 200 @dial-step 0.01
  width: number,
  // @dial-min 0.01 @dial-max 200 @dial-step 0.01
  height: number,
  // @dial-min 0.01 @dial-max 200 @dial-step 0.01
  depth: number,
  // @dial-dtype int @dial-min 0 @dial-max 200 @dial-step 1
  widthSegments: number,
  // @dial-dtype int @dial-min 0 @dial-max 200 @dial-step 1
  heightSegments: number,
  // @dial-dtype int @dial-min 0 @dial-max 200 @dial-step 1
  depthSegments: number,
];

interface ExampleBoxProps {
  /**
   * Arguments for the box geometry
   * @dial geometry
   * @dial-col-3
   * @dial-icon Box
   */
  args: ExampleBoxArgs;

  /**
   * Position of the box in 3D space
   * @dial transform
   * @dial-row
   * @dial-dtype vector3
   * @dial-min -10
   * @dial-max 10
   * @dial-step 0.1
   * @dial-icon Move3d
   */
  position: number[] | null;

  /**
   * Rotation of the box (Euler angles)
   * @dial transform
   * @dial-row
   * @dial-dtype euler
   * @dial-min -180
   * @dial-max 180
   * @dial-step 1
   * @dial-icon RotateCw
   */
  rotation: number[] | null;

  /**
   * Scale of the box
   * @dial transform
   * @dial-row
   * @dial-dtype vector3
   * @dial-min 0.1
   * @dial-max 5
   * @dial-step 0.1
   * @dial-icon Scaling
   */
  scale: number[] | null;

  /**
   * Whether to hide the box
   * @dial visibility
   * @dial-dtype boolean
   * @dial-icon Eye
   */
  hide: boolean;

  /**
   * Alpha test threshold
   * @dial visibility
   * @dial-dtype boolean
   * @dial-icon Opacity
   */
  alphaTest: boolean;

  /**
   * Enable depth testing
   * @dial visibility
   * @dial-dtype boolean
   * @dial-icon Layers
   */
  depthTest: boolean;

  /**
   * Render order for transparency sorting
   * @dial visibility
   * @dial-dtype number-int
   * @dial-min 0
   * @dial-max 100
   * @dial-step 1
   * @dial-icon ListOrdered
   * @dial-label-inline
   */
  renderOrder: number;

  /**
   * Internal property - not exposed in UI
   * @dial-ignore
   */
  _internalState?: any;
}
geometry
width
height
depth
widthSegments
heightSegments
depthSegments
transform
x
y
z
x
y
z
visibility
RenderOrder
Schema Definition
[
  {
    "name": "args",
    "dtype": "vector",
    "value": [
      1,
      1,
      1,
      1,
      1,
      1
    ],
    "icon": "Box",
    "tags": {
      "grouping": "geometry",
      "col": 3,
      "layout": "column"
    },
    "mins": [
      0.01,
      0.01,
      0.01,
      0,
      0,
      0
    ],
    "maxs": [
      200,
      200,
      200,
      200,
      200,
      200
    ],
    "steps": [
      0.01,
      0.01,
      0.01,
      1,
      1,
      1
    ],
    "dtypes": [
      "number",
      "number",
      "number",
      "int",
      "int",
      "int"
    ],
    "placeholders": [
      "width",
      "height",
      "depth",
      "widthSegments",
      "heightSegments",
      "depthSegments"
    ],
    "tooltips": [
      "Width",
      "Height",
      "Depth",
      "width segments",
      "height segments",
      "depth segments"
    ],
    "typeDefinition": {
      "name": "ExampleBoxArgs",
      "kind": "typeAlias",
      "raw": "type ExampleBoxArgs = [\n  // @dial-min 0.01 @dial-max 200 @dial-step 0.01\n  width: number,\n  // @dial-min 0.01 @dial-max 200 @dial-step 0.01\n  height: number,\n  // @dial-min 0.01 @dial-max 200 @dial-step 0.01\n  depth: number,\n  // @dial-dtype int @dial-min 0 @dial-max 200 @dial-step 1\n  widthSegments: number,\n  // @dial-dtype int @dial-min 0 @dial-max 200 @dial-step 1\n  heightSegments: number,\n  // @dial-dtype int @dial-min 0 @dial-max 200 @dial-step 1\n  depthSegments: number,\n];",
      "type": "tuple",
      "elements": [
        {
          "index": 0,
          "raw": "// @dial-min 0.01 @dial-max 200 @dial-step 0.01\n  width: number",
          "name": "width",
          "type": "number",
          "comment": "@dial-min 0.01 @dial-max 200 @dial-step 0.01",
          "min": 0.01,
          "max": 200,
          "step": 0.01,
          "dtype": "number",
          "dialTags": {
            "min": 0.01,
            "max": 200,
            "step": 0.01
          }
        },
        {
          "index": 1,
          "raw": "// @dial-min 0.01 @dial-max 200 @dial-step 0.01\n  height: number",
          "name": "height",
          "type": "number",
          "comment": "@dial-min 0.01 @dial-max 200 @dial-step 0.01",
          "min": 0.01,
          "max": 200,
          "step": 0.01,
          "dtype": "number",
          "dialTags": {
            "min": 0.01,
            "max": 200,
            "step": 0.01
          }
        },
        {
          "index": 2,
          "raw": "// @dial-min 0.01 @dial-max 200 @dial-step 0.01\n  depth: number",
          "name": "depth",
          "type": "number",
          "comment": "@dial-min 0.01 @dial-max 200 @dial-step 0.01",
          "min": 0.01,
          "max": 200,
          "step": 0.01,
          "dtype": "number",
          "dialTags": {
            "min": 0.01,
            "max": 200,
            "step": 0.01
          }
        },
        {
          "index": 3,
          "raw": "// @dial-dtype int @dial-min 0 @dial-max 200 @dial-step 1\n  widthSegments: number",
          "name": "widthSegments",
          "type": "number",
          "comment": "@dial-dtype int @dial-min 0 @dial-max 200 @dial-step 1",
          "min": 0,
          "max": 200,
          "step": 1,
          "dtype": "int",
          "dialTags": {
            "min": 0,
            "max": 200,
            "step": 1,
            "dtype": "int"
          }
        },
        {
          "index": 4,
          "raw": "// @dial-dtype int @dial-min 0 @dial-max 200 @dial-step 1\n  heightSegments: number",
          "name": "heightSegments",
          "type": "number",
          "comment": "@dial-dtype int @dial-min 0 @dial-max 200 @dial-step 1",
          "min": 0,
          "max": 200,
          "step": 1,
          "dtype": "int",
          "dialTags": {
            "min": 0,
            "max": 200,
            "step": 1,
            "dtype": "int"
          }
        },
        {
          "index": 5,
          "raw": "// @dial-dtype int @dial-min 0 @dial-max 200 @dial-step 1\n  depthSegments: number",
          "name": "depthSegments",
          "type": "number",
          "comment": "@dial-dtype int @dial-min 0 @dial-max 200 @dial-step 1",
          "min": 0,
          "max": 200,
          "step": 1,
          "dtype": "int",
          "dialTags": {
            "min": 0,
            "max": 200,
            "step": 1,
            "dtype": "int"
          }
        }
      ]
    }
  },
  {
    "name": "position",
    "dtype": "vector3",
    "value": [
      0,
      5,
      0
    ],
    "min": -10,
    "max": 10,
    "step": 0.1,
    "icon": "Move3d",
    "tags": {
      "grouping": "transform",
      "noWrap": true
    }
  },
  {
    "name": "rotation",
    "dtype": "euler",
    "value": [
      0,
      0,
      0
    ],
    "min": -180,
    "max": 180,
    "step": 1,
    "icon": "RotateCw",
    "tags": {
      "grouping": "transform",
      "noWrap": true
    }
  },
  {
    "name": "scale",
    "dtype": "vector3",
    "value": [
      1,
      1,
      1
    ],
    "min": 0.1,
    "max": 5,
    "step": 0.1,
    "icon": "Scaling",
    "tags": {
      "grouping": "transform",
      "noWrap": true
    }
  },
  {
    "name": "hide",
    "dtype": "boolean",
    "value": false,
    "icon": "Eye",
    "tags": {
      "grouping": "visibility"
    }
  },
  {
    "name": "alphaTest",
    "dtype": "boolean",
    "value": false,
    "icon": "Opacity",
    "tags": {
      "grouping": "visibility"
    }
  },
  {
    "name": "depthTest",
    "dtype": "boolean",
    "value": true,
    "icon": "Layers",
    "tags": {
      "grouping": "visibility"
    }
  },
  {
    "name": "renderOrder",
    "dtype": "number-int",
    "value": 10,
    "min": 0,
    "max": 100,
    "step": 1,
    "icon": "ListOrdered",
    "tags": {
      "grouping": "visibility",
      "labelPosition": "inline"
    }
  }
]
Groups Configuration
[
  {
    "name": "transform",
    "noWrap": true
  }
]

State

Current Values

{}

🎯 Schema-Driven

UI controls are automatically generated from TypeScript interfaces with JSDoc annotations.

📦 Grouped Layout

Properties are automatically grouped by their @dial grouping tags into organized sections.

🔄 State Management

Built-in state management with value change callbacks for easy integration.

Getting Started

The Dial system provides a powerful way to generate UI controls from TypeScript code. To learn how to use dial-cli and integrate it with your application, check out the Dial Tutorial.