Skip to main content

MetabindUI Inspector Grammar with Zod Compatibility

Overview

The inspector function defines component properties for both UI editing controls and type validation. This grammar is compatible with Zod schemas and can be used to auto-generate TypeScript types.

Basic Structure

const inspector = () => ({
  propertyName: {
    title: 'Human-readable title',
    type: 'string',
    description: 'Optional description of this property',
    // Additional type-specific fields
  },
  // Additional properties...
});

Mapping to Zod

import { z } from 'zod/v4';

function createZodSchema(inspectorDefinition) {
  const schemaObj = {};

  for (const [key, def] of Object.entries(inspectorDefinition)) {
    let schema;

    switch (def.type) {
      case 'string':
        schema = z.string();
        if (def.minLength !== undefined) schema = schema.min(def.minLength);
        if (def.maxLength !== undefined) schema = schema.max(def.maxLength);
        break;

      case 'number':
        schema = z.number();
        if (def.min !== undefined) schema = schema.min(def.min);
        if (def.max !== undefined) schema = schema.max(def.max);
        break;

      case 'boolean':
        schema = z.boolean();
        break;

      case 'color':
        schema = z.string().regex(/^#/, { message: 'Must be a valid color string' });
        break;

      case 'enum':
        const values = def.options.map(([v]) => v);
        schema = z.enum([...values]);
        break;

      case 'json':
        schema = z.any();
        break;

      case 'array':
        const itemSchema = createZodSchema({ item: def.itemType }).item;
        schema = z.array(itemSchema);
        if (def.minItems !== undefined) schema = schema.min(def.minItems);
        if (def.maxItems !== undefined) schema = schema.max(def.maxItems);
        break;

      case 'object':
        schema = z.object(createZodSchema(def.properties));
        break;

      default:
        schema = z.any();
    }

    if (def.default !== undefined) {
      schema = schema.default(def.default);
    } else if (!def.required) {
      schema = schema.optional();
    }

    schemaObj[key] = schema;
  }

  return schemaObj;
}

// Usage
const zodSchema = z.object(createZodSchema(inspector()));
type MyComponentProps = z.infer<typeof zodSchema>;

Property Types

TypeUI ControlZod Equivalent
stringText inputz.string()
numberNumber inputz.number()
booleanTogglez.boolean()
colorColor pickerz.string().regex(/^#/))
enumDropdownz.enum([...])
jsonJSON editorz.any()
arrayListz.array(...)
objectObject editorz.object({...})
assetMedia browserz.string() or z.array(z.string())
componentsComponent listz.array(...)

Common Fields

All property types may include:
  • title: Human-readable name
  • description: Description or tooltip
  • default: Default value
  • required: Whether the field is mandatory
  • hidden: Hide from UI editor
  • deprecated: Mark as deprecated with optional message
  • group: Group for UI organization
  • condition: Conditional display rule { key, eq }

Type-Specific Fields

String

{
  type: 'string',
  default: 'Default text',
  placeholder: 'Enter text here...',
  multiline: false,
  minLength: 0,
  maxLength: 100
}

Number

{
  type: 'number',
  default: 0,
  min: 0,
  max: 100,
  step: 1,
  unit: 'px',
  slider: true
}

Boolean

{
  type: 'boolean',
  default: false,
  labels: {
    true: 'Enabled',
    false: 'Disabled'
  }
}

Color

{
  type: 'color',
  default: '#ffffff',
  format: 'hex',
  alpha: true
}

Enum

{
  type: 'enum',
  options: [
    ['value1', 'Display 1'],
    ['value2', 'Display 2']
  ],
  default: 'value1'
}

JSON

{
  type: 'json',
  default: { key: 'value' }
}

Array

{
  type: 'array',
  itemType: { type: 'string' },
  default: [],
  minItems: 0,
  maxItems: 10
}

Object

{
  type: 'object',
  properties: {
    field1: { type: 'string', title: 'Field 1' },
    field2: { type: 'number', title: 'Field 2' }
  },
  default: { field1: '', field2: 0 }
}

Advanced Features

Conditional Properties

{
  type: {
    type: 'enum',
    options: [['simple', 'Simple'], ['advanced', 'Advanced']],
    default: 'simple'
  },
  advancedSetting: {
    type: 'string',
    condition: { key: 'type', eq: 'advanced' }
  }
}

Asset

{
  type: 'asset',
  title: 'Background Image',
  assetTypes: ['image', 'video'], // Optional - defaults to ['image'] if not provided
  multiSelect: false, // Optional - defaults to false if not provided
  default: '' // Optional - ID of default asset
}

// For multi-selection
{
  type: 'asset',
  title: 'Gallery Images',
  assetTypes: ['image'],
  multiSelect: true,
  default: [] // Optional - array of asset IDs
}

Custom Controls

{
  type: 'custom',
  control: 'imagePicker',
  controlProps: {
    source: 'library'
  }
}

Computed Defaults

{
  width: {
    type: 'number',
    default: 100
  },
  height: {
    type: 'number',
    default: '@{width}'
  }
}

TypeScript Type Generation

interface MyComponentProps {
  /** Main heading text */
  title?: string;
  size?: 'small' | 'medium' | 'large';
  showIcon?: boolean;
  iconName?: string;
  colors?: {
    background?: string;
    text?: string;
  };
  padding?: {
    top?: number;
    right?: number;
    bottom?: number;
    left?: number;
  };
  items?: Array<{
    label?: string;
    value?: string;
  }>;
}

Best Practices

  1. Use descriptive title and description.
  2. Group related properties with group.
  3. Set sensible default values.
  4. Use condition to improve UI relevance.
  5. Avoid unused fields.
  6. Maintain property declaration order.
  7. Favor predictable, flat object shapes.
  8. Avoid optional fields unless semantically needed.