# Custom modules

AsukaDesigner v1 can be extended at runtime through a module registry.
Built-in icon names and direct font usage are documented in
[the icon library guide](./icons.md).

Custom modules can be used to:

- add new editor tools
- create integration-specific panels
- expose internal business actions
- embed forms and workflows inside the editor
- open modules from layout zones or the contextual selection menu

## Registering a module

Use `designer.registerModule(...)` to register a custom module after the editor has been created.

```ts
const designer = createAsukaDesigner('#app');

designer.registerModule({
  id: 'my-module',
  label: 'My module',
  icon: 'puzzle',
  displayMode: 'modal',
  keepMounted: true,
  render: ({ ui }) => ui.el('div', null, 'Hello from a custom module'),
});
```

You can register the module and add its layout button in one call:

```ts
designer.registerModule({
  id: 'my-module',
  label: 'My module',
  icon: 'puzzle',
  displayMode: 'modal',
  supportedEngines: ['fabric', '3d'],
  render: ({ ui }) => ui.el('div', null, 'Hello from a custom module'),
}, {
  addToLayout: true,
  layoutZone: 'left',
});
```

## Declare a small module directly in the initial layout

For small integration-specific modules, the executable definition can be
declared directly on a layout item. `kind` becomes the module id and no explicit
`registerModule(...)` call is required.

```js
const designer = createAsukaDesigner('#app', {
  layout: {
    left: {
      items: [
        {
          kind: 'customer-text',
          label: 'Customer text',
          icon: 'puzzle',
          type: 'drawer',
          module: {
            supportedEngines: ['fabric', '3d'],
            keepMounted: true,
            render: ({ ui }) => {
              const { Button } = ui.components;

              return ui.el(
                Button,
                {
                  onClick: () => {
                    void designer.api.addText({ text: 'Customer text' });
                  },
                },
                'Add text',
              );
            },
          },
        },
      ],
    },
  },
});
```

The runtime registers `module` before the first render and removes that property
from the normalized layout. Functions such as `render`, `loader`, and `action`
therefore never enter exported or persisted layout state. Use
`designer.registerModule(...)` instead when the module is loaded after startup,
shared by several placements, or installed from an optional add-on chunk.

## Ship a customer module as an optional chunk

Keep the add-on independent from the AsukaDesigner bundle. Export an installer and use the `designer` instance passed by the host application instead of importing the runtime again.

```js
// customer-tools-addon.js
export function install(designer) {
  designer.registerModule({
    id: 'customer-tools',
    label: 'Customer tools',
    icon: 'puzzle',
    displayMode: 'drawer',
    licenseFeature: 'customerTools',
    supportedEngines: ['fabric', '3d'],
    render: ({ ui }) => {
      const { Card, CardContent, Button } = ui.components;
      return ui.el(
        Card,
        null,
        ui.el(
          CardContent,
          null,
          ui.el(Button, { onClick: () => designer.api.addText({ text: 'Customer text' }) }, 'Add text'),
        ),
      );
    },
  }, {
    addToLayout: true,
    layoutZone: 'left',
  });
}
```

Load it only for the relevant customer:

```js
if (designer.util.license.canUse('customerTools')) {
  try {
    const addon = await import('/customer-addons/customer-tools-addon.js');
    await addon.install(designer);
  } catch {
    // No registration means no menu entry is rendered.
  }
}
```

Resolve the license before running this loader and include
`features.customerTools: true` only in the signed entitlement of customers who
own the add-on. Repeating `licenseFeature` in the module definition also hides an
already-loaded module after a license downgrade.

For TypeScript authoring, `import type { AsukaDesigner } from 'asuka-designer'` is safe because the type-only import is erased. Do not import `asukadesigner.min.js` from the add-on itself; the host already owns that runtime.

## Module definition

A custom module definition can include:

```ts
{
  id: string;
  label?: string;
  title?: string;
  icon?: string;
  group?: string;
  displayMode?: 'floating' | 'popover' | 'panel-left' | 'panel-right' | 'panel-bottom' | 'panel-top' | 'modal' | 'inline' | 'drawer' | 'overlay';
  licenseFeature?: string;
  supportedEngines?: Array<'fabric' | '3d'>;
  requiredCapabilities?: string[];
  requiresView?: boolean;
  mountTarget?: string;
  keepMounted?: boolean;
  render?: AsukaModuleComponent;
  loader?: AsukaModuleLoader;
  action?: (context) => void | Promise<void>;
  isAvailable?: boolean;
}
```

### Required field

`id` is required and must be stable.

### `render`

Use `render` for a UI module. It receives the module context and UI helpers.

### `loader`

Use `loader` when you want a lazy module.

### `action`

Use `action` for modules that perform an operation without opening a UI container.

### `keepMounted`

Set `keepMounted` to `true` when you want the module state to persist between openings.

## Runtime context

Custom modules receive a runtime context that includes:

- `getEngine()` to access the active engine-neutral renderer facade
- `ui` to access reusable vanilla-friendly helpers and components

```ts
render: ({ context, ui }) => {
  const engine = context.getEngine();
  return ui.el('div', null, engine ? 'Ready' : 'No active engine');
}
```

Use the `designer` variable captured when registering the module for public document, layout, theme, and licensing APIs.

## UI helpers

The instance exposes reusable UI helpers through `designer.ui`.

The same helpers are injected into custom modules.

Available helpers include:

- `h`
- `el`
- `components`
- `icon(name, options?)`

Typical reusable components include:

- `Button`
- `Card`
- `CardHeader`
- `CardTitle`
- `CardContent`
- `Field`
- `Form`
- `FormWithHelpers`
- `Input`
- `Select`
- `SegmentedControl`
- `Tabs`
- `Tooltip`
- `List`
- `Carousel`
- `IconButton`
- `Checkbox`

## Add a module to the layout

A module can be registered first and added to the layout afterward.

```ts
designer.registerModule({
  id: 'my-module',
  label: 'My module',
  icon: 'puzzle',
  displayMode: 'floating',
  render: ({ ui }) => ui.el('div', null, 'Custom content'),
});

designer.layout.addItem('left', {
  kind: 'my-module',
  label: 'My module',
  icon: 'puzzle',
  type: 'floating',
});
```

Some integrations also add the layout entry immediately after registration as part of their own bootstrap flow.

## Example: form-based custom module

```ts
const designer = createAsukaDesigner('#app');
const { components } = designer.ui;

designer.registerModule({
  id: 'test',
  label: 'Test',
  icon: 'puzzle',
  displayMode: 'modal',
  keepMounted: true,
  render: ({ context, ui }) => {
    const $ui = ui || context.ui;
    const { Card, CardHeader, CardTitle, CardContent, Button, Input, Form, Checkbox } = $ui.components || components;

    const onSubmit = (values: { text: string; fontSize: number; bold: boolean }) => {
      void designer.api.addText({
        text: values.text,
        fontSize: Number(values.fontSize) || 48,
        fontWeight: values.bold ? '700' : '400',
      });
    };

    return $ui.el(
      Card,
      null,
      $ui.el(CardHeader, null, $ui.el(CardTitle, null, 'Test')),
      $ui.el(
        CardContent,
        null,
        $ui.el(
          Form,
          { initialValues: { text: 'Hello', fontSize: 48, bold: false }, onSubmit },
          $ui.el(Form.Item, { name: 'text', label: 'Text', required: true }, $ui.el(Input, { placeholder: 'Type something…' })),
          $ui.el(Form.Item, {
            name: 'fontSize',
            label: 'Font size',
            required: true,
            validate: (value) => {
              const number = Number(value);
              return Number.isFinite(number) && number >= 8 && number <= 240
                ? null
                : 'Font size must be between 8 and 240';
            },
          }, $ui.el(Input, { type: 'number', min: 8, max: 240 })),
          $ui.el(Form.Item, { name: 'bold', label: 'Bold' }, $ui.el(Checkbox, { label: 'Bold' })),
          $ui.el(Button, { type: 'submit' }, 'Add text')
        )
      )
    );
  },
});
```

This pattern matches the built-in example shipped in the project and demonstrates:

- runtime registration
- UI helper usage
- built-in and callback validation
- reusable core components
- interaction with the designer API

## Required fields DX

For simple required fields, use `required` directly on `Form.Item`. The label automatically shows a required marker, the child receives `required` / `aria-required`, and the form displays the default `form.validation.required` message on submit.

```tsx
<Form initialValues={{ name: '' }} onSubmit={save}>
  <Form.Item name="name" label="Name" required>
    <Input placeholder="Product name" />
  </Form.Item>
</Form>
```

Pass a string to customize the required message for one field:

```tsx
<Form.Item name="sku" label="SKU" required="SKU is required">
  <Input />
</Form.Item>
```

The reusable form wrappers also display the same marker when you pass `required`, for example `<TextField required />`, `<NumberField required />`, `<SelectField required />`, `<MultiSelectField required />`, `<CheckboxField required />`, and `<DimensionFields required />`.

For cross-field rules, pass `validate` to `Form` and return messages keyed by field path:

```tsx
<Form
  initialValues={{ start: 0, end: 10 }}
  validate={(values) => values.end > values.start ? {} : { end: 'End must be greater than start' }}
  onSubmit={save}
>
  {/* fields */}
</Form>
```

## Action-only modules

A module can be action-only when it should trigger behavior without rendering a panel or overlay.

```ts
designer.registerModule({
  id: 'duplicate-selection',
  label: 'Duplicate',
  icon: 'copy',
  action: () => {
    const selected = designer.api.getSelectedObjects();
    if (selected[0]) void designer.api.duplicateObject(selected[0].id);
  },
});
```

## Lazy modules

Use `loader` to defer heavy module code.

```ts
designer.registerModule({
  id: 'advanced-catalog',
  label: 'Advanced catalog',
  icon: 'puzzle',
  displayMode: 'floating',
  keepMounted: true,
  loader: () => import('./AdvancedCatalogModule'),
});
```

The registry can keep a module declared even before its implementation has been loaded.

## Change display mode at runtime

The module registry supports runtime display mode overrides.

```ts
designer.util.modules.setDisplayMode?.('my-module', 'drawer');
```

Depending on the integration layer, you may also choose the opening mode through layout configuration.

## Mounting into a custom target

Some modules can be mounted into a specific target.

```ts
designer.registerModule({
  id: 'inspector',
  label: 'Inspector',
  icon: 'puzzle',
  displayMode: 'inline',
  mountTarget: '#external-inspector-host',
  render: ({ ui }) => ui.el('div', null, 'Inspector content'),
});
```

## Best practices

- Keep module ids stable.
- Use `keepMounted` only when persistent state is actually useful.
- Prefer `popover` for small contextual tools.
- Prefer `floating` or `panel-*` for larger editing surfaces.
- Reuse `designer.ui` rather than creating ad hoc UI primitives.
- Use lazy loaders for heavy integrations.
- Keep action-only modules focused and predictable.

## Next step

Continue with the [Fonts guide](./fonts.md).

## See also

- [Guides index](./index.md)
- [API overview](./api-overview.md)
- [Layout](./layout.md)
- [Examples](./examples.md)
