# Schema Form

Build dynamic settings panels from a declarative schema. Each field type maps to a UI component automatically.

## Basic fields

The most common field types: text, numbers, toggles, colors, and images.

```js
const schema = [
  { type: 'string', name: 'title', title: 'Title', initialValue: 'Hello world' },
  {
    type: 'string',
    name: 'description',
    title: 'Description',
    options: { multiline: 3, placeholder: 'Write something...' },
  },
  { type: 'number', name: 'count', title: 'Count', initialValue: 5 },
  {
    type: 'number',
    name: 'opacity',
    title: 'Opacity',
    initialValue: 0.8,
    options: { min: 0, max: 100, scale: 100, display: 'slider' },
  },
  { type: 'boolean', name: 'visible', title: 'Visible', initialValue: true },
  { type: 'color', name: 'color', title: 'Color' },
  { type: 'image', name: 'image', title: 'Image' },
  { type: 'link', name: 'link', title: 'Link' },
];

const [value, setValue] = React.useState({});

const context = {
  actions: {
    imageLibraryOpen: (cb) => cb({ key: 'demo', url: ipsum.image() }),
    imageLibraryClose: () => {},
  },
};

<div style={{ width: '300px' }}>
  <SilkeFormSchema schema={schema} value={value} onChange={setValue} context={context} />
  <pre>{JSON.stringify(value, null, 2)}</pre>
</div>;
```

## Range slider

A dual-handle slider for selecting a numeric range. Set `display: 'range-slider'` with `min` and `max` bounds (in display space). The value is a `[min, max]` tuple.

```js
const schema = [
  {
    type: 'number',
    name: 'priceRange',
    title: 'Price Range',
    initialValue: [200, 800],
    options: { display: 'range-slider', min: 0, max: 1000 },
  },
  {
    type: 'number',
    name: 'opacity',
    title: 'Opacity Range',
    initialValue: [0.2, 0.8],
    options: { display: 'range-slider', min: 0, max: 100, scale: 100 },
  },
];

const [value, setValue] = React.useState({});

<div style={{ width: '300px' }}>
  <SilkeFormSchema schema={schema} value={value} onChange={setValue} context={{}} />
  <pre>{JSON.stringify(value, null, 2)}</pre>
</div>;
```

## Icon field

Pick shapes from the shape library. Stores the full shape object `{ key, name, path }` where `path` is `[width, height, pathData]`.

```js
const schema = [
  { type: 'icon', name: 'icon', title: 'Icon' },
];

const [value, setValue] = React.useState({});

<div style={{ width: '300px' }}>
  <SilkeFormSchema schema={schema} value={value} onChange={setValue} context={{}} />
  <pre>{JSON.stringify(value, null, 2)}</pre>
</div>;
```

## Select fields

Dropdown, radio, checkbox, and autocomplete selection.

```js
const schema = [
  {
    type: 'select',
    name: 'size',
    title: 'Size',
    options: {
      display: 'dropdown',
      items: [
        { label: 'Small', value: 's' },
        { label: 'Medium', value: 'm' },
        { label: 'Large', value: 'l' },
      ],
    },
  },
  {
    type: 'select',
    name: 'alignment',
    title: 'Alignment',
    options: {
      display: 'radio',
      items: [
        { label: 'Left', value: 'left' },
        { label: 'Center', value: 'center' },
        { label: 'Right', value: 'right' },
      ],
    },
  },
  {
    type: 'select',
    name: 'features',
    title: 'Features',
    options: {
      multiselect: true,
      display: 'checkbox',
      items: [
        { label: 'Animations', value: 'animations' },
        { label: 'Shadows', value: 'shadows' },
        { label: 'Rounded corners', value: 'rounded' },
      ],
    },
  },
];

const [value, setValue] = React.useState({});

<div style={{ width: '300px' }}>
  <SilkeFormSchema schema={schema} value={value} onChange={setValue} context={{}} />
  <pre>{JSON.stringify(value, null, 2)}</pre>
</div>;
```

## Row layout

Place fields side-by-side using `layout` with `display: 'row'`. Use `flex: true` on fields that should stretch to fill available space.

```js
const schema = [
  {
    type: 'layout',
    options: { display: 'row' },
    fields: [
      { type: 'number', name: 'width', title: 'W', flex: true },
      { type: 'number', name: 'height', title: 'H', flex: true },
    ],
  },
  {
    type: 'layout',
    options: { display: 'row' },
    fields: [
      { type: 'number', name: 'x', title: 'X', flex: true },
      { type: 'number', name: 'y', title: 'Y', flex: true },
    ],
  },
  {
    type: 'layout',
    options: { display: 'row' },
    fields: [
      { type: 'number', name: 'top', title: 'Top', flex: true },
      { type: 'number', name: 'right', title: 'Right', flex: true },
      { type: 'number', name: 'bottom', title: 'Bottom', flex: true },
      { type: 'number', name: 'left', title: 'Left', flex: true },
    ],
  },
];

const [value, setValue] = React.useState({});

<div style={{ width: '300px' }}>
  <SilkeFormSchema schema={schema} value={value} onChange={setValue} context={{}} />
</div>;
```

## Groups

Use `group` to organize fields into collapsible card sections with a bold title header and chevron toggle. Set `collapsed: true` to start closed.

```js
const schema = [
  { type: 'string', name: 'title', title: 'Title', initialValue: 'My Component' },
  {
    type: 'group',
    title: 'Background',
    fields: [
      { type: 'image', name: 'bgImage', title: 'Image' },
      { type: 'color', name: 'bgColor', title: 'Color' },
    ],
  },
  {
    type: 'group',
    title: 'Typography',
    fields: [
      {
        type: 'select',
        name: 'fontWeight',
        title: 'Weight',
        options: {
          display: 'dropdown',
          items: [
            { label: 'Regular', value: '400' },
            { label: 'Medium', value: '500' },
            { label: 'Bold', value: '700' },
          ],
        },
      },
      { type: 'number', name: 'fontSize', title: 'Font Size' },
      { type: 'color', name: 'textColor', title: 'Text Color' },
    ],
  },
  {
    type: 'group',
    title: 'Advanced Settings',
    options: { collapsed: true },
    fields: [
      { type: 'number', name: 'zIndex', title: 'Z-Index' },
      { type: 'boolean', name: 'lazyLoad', title: 'Lazy Load' },
      { type: 'string', name: 'cssClass', title: 'CSS Class' },
    ],
  },
];

const [value, setValue] = React.useState({});

<div style={{ width: '300px' }}>
  <SilkeFormSchema schema={schema} value={value} onChange={setValue} context={{}} />
</div>;
```

## Popover settings

Use `popover` to tuck away extra settings behind a button. Works great inside a `layout` row next to a field.

```js
const schema = [
  {
    type: 'layout', options: { display: 'row' },
    fields: [
      { type: 'string', name: 'url', title: 'API URL', flex: true },
      {
        type: 'popover',
        title: 'Request Options',
        options: { icon: 'settings' },
        fields: [
          {
            type: 'select',
            name: 'method',
            title: 'Method',
            options: {
              display: 'dropdown',
              items: [
                { label: 'GET', value: 'GET' },
                { label: 'POST', value: 'POST' },
                { label: 'PUT', value: 'PUT' },
              ],
            },
          },
          { type: 'number', name: 'timeout', title: 'Timeout (ms)' },
          { type: 'boolean', name: 'cache', title: 'Enable Cache' },
        ],
      },
    ],
  },
  {
    type: 'layout', options: { display: 'row' },
    fields: [
      { type: 'color', name: 'color', title: 'Color', flex: true },
      {
        type: 'popover',
        title: 'Color Options',
        options: { icon: 'settings' },
        fields: [
          {
            type: 'number',
            name: 'opacity',
            title: 'Opacity',
            options: { min: 0, max: 100, scale: 100, display: 'slider' },
          },
          {
            type: 'select',
            name: 'blendMode',
            title: 'Blend Mode',
            options: {
              display: 'dropdown',
              items: [
                { label: 'Normal', value: 'normal' },
                { label: 'Multiply', value: 'multiply' },
                { label: 'Screen', value: 'screen' },
                { label: 'Overlay', value: 'overlay' },
              ],
            },
          },
        ],
      },
    ],
  },
];

const [value, setValue] = React.useState({});

<div style={{ width: '300px' }}>
  <SilkeFormSchema schema={schema} value={value} onChange={setValue} context={{}} />
</div>;
```

## Array fields

Repeatable lists with drag-and-drop sorting.

- `of: 'string'` — simple string list
- `of: 'object'` with `fields` — structured items with a fixed schema
- `of: [...]` — multiple item types the user can pick from when adding

```js
const schema = [
  {
    type: 'array',
    name: 'tags',
    title: 'Tags',
    of: 'string',
  },
  {
    type: 'array',
    name: 'links',
    title: 'Links',
    of: 'object',
    fields: [
      { type: 'string', name: 'label', title: 'Label' },
      { type: 'string', name: 'url', title: 'URL', options: { type: 'url' } },
      { type: 'boolean', name: 'external', title: 'Open in new tab' },
    ],
  },
  {
    type: 'array',
    name: 'blocks',
    title: 'Content Blocks',
    of: [
      {
        type: 'object',
        name: 'text',
        title: 'Text Block',
        fields: [
          { type: 'string', name: 'heading', title: 'Heading' },
          { type: 'string', name: 'body', title: 'Body', options: { multiline: 3 } },
        ],
      },
      {
        type: 'object',
        name: 'image',
        title: 'Image Block',
        fields: [
          { type: 'image', name: 'src', title: 'Image' },
          { type: 'string', name: 'alt', title: 'Alt Text' },
        ],
      },
      {
        type: 'object',
        name: 'cta',
        title: 'CTA Block',
        fields: [
          { type: 'string', name: 'label', title: 'Button Label' },
          { type: 'link', name: 'link', title: 'Link' },
        ],
      },
    ],
  },
];

const [value, setValue] = React.useState({
  tags: ['Design', 'Code'],
  links: [
    { label: 'Docs', url: 'https://docs.example.com', external: true },
  ],
  blocks: [
    { text: { heading: 'Welcome', body: 'Hello world' } },
    { image: { alt: 'Hero image' } },
  ],
});

<div style={{ width: '300px' }}>
  <SilkeFormSchema schema={schema} value={value} onChange={setValue} context={{}} />
  <pre>{JSON.stringify(value, null, 2)}</pre>
</div>;
```

## Combining everything

A realistic component settings panel using groups, layouts, and popovers together.

```js
const schema = [
  { type: 'string', name: 'heading', title: 'Heading', initialValue: 'Welcome' },
  {
    type: 'string',
    name: 'body',
    title: 'Body Text',
    options: { multiline: 3, placeholder: 'Enter body text...' },
  },
  { type: 'link', name: 'ctaLink', title: 'CTA Link' },
  {
    type: 'array',
    name: 'buttons',
    title: 'Buttons',
    of: 'object',
    fields: [
      { type: 'string', name: 'label', title: 'Label' },
      { type: 'link', name: 'link', title: 'Link' },
    ],
  },
  { type: 'divider' },
  {
    type: 'group',
    title: 'Layout',
    fields: [
      {
        type: 'layout', options: { display: 'row' },
        fields: [
          { type: 'number', name: 'columns', title: 'Cols', flex: true },
          { type: 'number', name: 'gap', title: 'Gap', flex: true },
        ],
      },
      {
        type: 'layout', options: { display: 'row' },
        fields: [
          {
            type: 'select',
            name: 'align',
            title: 'Align',
            flex: true,
            options: {
              display: 'dropdown',
              items: [
                { label: 'Left', value: 'left' },
                { label: 'Center', value: 'center' },
                { label: 'Right', value: 'right' },
              ],
            },
          },
          {
            type: 'popover',
            title: 'Spacing',
            options: { icon: 'settings' },
            fields: [
              {
                type: 'layout', options: { display: 'row' },
                fields: [
                  { type: 'number', name: 'padTop', title: 'Top', flex: true },
                  { type: 'number', name: 'padRight', title: 'Right', flex: true },
                ],
              },
              {
                type: 'layout', options: { display: 'row' },
                fields: [
                  { type: 'number', name: 'padBottom', title: 'Bottom', flex: true },
                  { type: 'number', name: 'padLeft', title: 'Left', flex: true },
                ],
              },
            ],
          },
        ],
      },
    ],
  },
  {
    type: 'group',
    title: 'Background',
    fields: [
      { type: 'image', name: 'bgImage', title: 'Image' },
      { type: 'color', name: 'bgColor', title: 'Color' },
      {
        type: 'number',
        name: 'bgOpacity',
        title: 'Opacity',
        options: { min: 0, max: 100, scale: 100, display: 'slider' },
      },
      {
        type: 'array',
        name: 'overlays',
        title: 'Overlays',
        of: 'object',
        fields: [
          { type: 'color', name: 'color', title: 'Color' },
          { type: 'number', name: 'opacity', title: 'Opacity', options: { min: 0, max: 100, scale: 100, display: 'slider' } },
        ],
      },
    ],
  },
  {
    type: 'group',
    title: 'Advanced',
    options: { collapsed: true },
    fields: [
      { type: 'boolean', name: 'lazyLoad', title: 'Lazy Load' },
      { type: 'string', name: 'anchor', title: 'Anchor ID' },
      { type: 'string', name: 'ariaLabel', title: 'Aria Label' },
    ],
  },
];

const [value, setValue] = React.useState({
  buttons: [{ label: 'Get Started' }, { label: 'Learn More' }],
  overlays: [{ color: '#00000080', opacity: 0.5 }],
});

<div style={{ width: '300px' }}>
  <SilkeFormSchema schema={schema} value={value} onChange={setValue} context={{
    actions: {
      imageLibraryOpen: (cb) => cb({ key: 'demo', url: ipsum.image() }),
      imageLibraryClose: () => {},
    },
    currentPage: { title: 'Home', key: 'home', path: '/' },
    pages: [
      { title: 'Home', key: 'home', path: '/' },
      { title: 'About', key: 'about', path: '/about' },
    ],
  }} />
</div>;
```

## Conditional visibility

Use the `hidden` property to show or hide fields based on other field values.

```js
const schema = [
  {
    type: 'select',
    name: 'mode',
    title: 'Mode',
    initialValue: 'simple',
    options: {
      display: 'radio',
      items: [
        { label: 'Simple', value: 'simple' },
        { label: 'Advanced', value: 'advanced' },
      ],
    },
  },
  { type: 'string', name: 'title', title: 'Title' },
  {
    type: 'string',
    name: 'subtitle',
    title: 'Subtitle',
    hidden: (ctx) => ctx.value.mode !== 'advanced',
  },
  {
    type: 'number',
    name: 'maxItems',
    title: 'Max Items',
    hidden: (ctx) => ctx.value.mode !== 'advanced',
  },
  {
    type: 'boolean',
    name: 'debug',
    title: 'Debug Mode',
    hidden: (ctx) => ctx.value.mode !== 'advanced',
  },
];

const [value, setValue] = React.useState({});

<div style={{ width: '300px' }}>
  <SilkeFormSchema schema={schema} value={value} onChange={setValue} context={{}} />
</div>;
```

## Async and initial values

Fields support static defaults, functions, and eval callbacks for dynamic initialization.

```js
const schema = [
  {
    type: 'string',
    title: 'Static default',
    name: 'field1',
    initialValue: 'Hello',
  },
  {
    type: 'string',
    title: 'Function default',
    name: 'field2',
    initialValue: () => 'Computed at render',
  },
  {
    type: 'string',
    title: 'Eval callback',
    name: 'field3',
    initialValue: { type: '__eval__', value: `"Evaluated"` },
  },
];

const [value, setValue] = React.useState({});

<div style={{ width: '300px' }}>
  <SilkeFormSchema schema={schema} value={value} context={{}} onChange={setValue} />
  <pre>{JSON.stringify(value, null, 2)}</pre>
</div>;
```
