Dial, A Schema-Driven Menu System
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;
}
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.
Quick Links
- Tutorial - Step-by-step guide to using dial-cli
- Input Types - Complete reference of all supported input types
- Controlled Dials - Advanced usage patterns
- API Notes - Detailed API reference
- CLI Details - Advanced CLI options