# Addon UI

Addons add UI without shipping JavaScript to the main app. There are three ways to do it:

- **Native pages** (`ui.pages[].body`): a JSON tree of shadcn components that the frontend renders. The allowed node types and their fields are in the [UI schema](ui.md).
- **Iframe pages** (`ui.pages[].frame`): a sandboxed HTML file from the addon's `assets/`. This needs `"iframe"` in `capabilities.ui`, and the admin must approve it explicitly at install.
- **Slots** (`ui.slots[]`): small contributions in named places of existing pages, such as a toolbar, the dashboard or the layout banner.

## Example

_extensions/examples/addon-guide/addon.json_

```json
{
  "id": "integration-guide",
  "name": "Addon guide example",
  "version": "0.1.0",
  "kind": "integration",
  "engine": "wasm",
  "provides": "guide",
  "abi": "0.3.0",
  "wasm": "addon.wasm",
  "description": "addons.desc.integration.guide",
  "author": "MythicalSystems",
  "permissions": [],
  "capabilities": {
    "kv": true,
    "http": ["example.com"],
    "secrets": ["api_token"],
    "events": ["invoice.paid", "user.registered"],
    "emits": ["integration-guide:greeted"],
    "hooks": [
      { "event": "client.before_register", "priority": "normal" },
      { "event": "login.before", "priority": "high", "fail_open": true, "timeout_ms": 500 },
      { "event": "before:integration-guide:greeted" },
      { "event": "order.before_place", "priority": "highest", "ignore_cancelled": true },
      { "event": "after:PUT /api/v1/client/profile", "priority": "monitor" }
    ],
    "mixins": [
      { "target": "GET /api/v1/client/profile", "at": "after" }
    ],
    "jobs": [{ "op": "cleanup", "every_secs": 3600 }],
    "routes": [
      { "method": "GET", "path": "/hello", "access": "public" },
      { "method": "POST", "path": "/greet", "access": "client" },
      { "method": "GET", "path": "/stats", "access": "admin", "permission": "reports.read" }
    ],
    "ui": ["iframe"]
  },
  "ui": {
    "pages": [
      {
        "id": "stats", "area": "admin", "title": "plugin.integration-guide.stats_title", "icon": "ChartBar", "nav": true,
        "data": { "stats": { "get": "/api/v1/addons/integration-guide/stats" } },
        "body": { "type": "stack", "children": [
          { "type": "text", "text": "{{t:plugin.integration-guide.stats_hint}}" },
          { "type": "table", "rows": "{{data.stats.rows}}", "columns": [
            { "header": "plugin.integration-guide.key", "value": "{{row.key}}" },
            { "header": "plugin.integration-guide.value", "value": "{{row.value}}" }
          ] }
        ] }
      },
      { "id": "frame", "area": "client", "title": "plugin.integration-guide.frame_title", "icon": "AppWindow", "nav": true, "frame": { "src": "assets/ui.html", "height": 200 } }
    ],
    "slots": [
      { "slot": "client.dashboard", "body": { "type": "alert", "text": "{{t:plugin.integration-guide.hello}}" } },
      { "slot": "admin.invoices.toolbar", "order": 5, "body": { "type": "badge", "text": "{{t:plugin.integration-guide.badge}}" } }
    ]
  },
  "settings": [
    { "key": "greeting", "label": "Greeting text", "kind": "text", "required": false },
    { "key": "api_token", "label": "API token", "kind": "secret", "required": false }
  ],
  "i18n": {
    "en": { "stats_title": "Guide stats", "stats_hint": "Counters kept by the guide addon.", "key": "Counter", "value": "Value", "frame_title": "Guide frame", "hello": "Hello from the guide addon.", "badge": "Guide" },
    "ro": { "stats_title": "Statistici ghid", "stats_hint": "Contoare ținute de addon-ul ghid.", "key": "Contor", "value": "Valoare", "frame_title": "Cadru ghid", "hello": "Salut de la addon-ul ghid.", "badge": "Ghid" }
  }
}
```

Strings that start with `plugin.<addon-id>.` are translated from the manifest's `i18n` map. `{{data.<name>}}` reads a data source declared in `data`, and `{{row.<field>}}` reads the current table row.

## Slots and positions

Every page and its exact slot names are listed in [Widgets and slots](widgets.md).

Positions an addon can fill on every page. A slot name is `<area>.<route>.<position>`: `area` is `admin` or `client`, `route` is the page's route key (path segments without ids joined by `-`, e.g. `/admin/invoices/42` → `invoices`, `/admin/settings/tax` → `settings-tax`) or `*` for every page. `replace:<section>` swaps out a named section.

| Kind | Values |
|---|---|
| Page positions | `before`, `after`, `toolbar`, `row-actions`, `tabs`, `detail-panel` |
| Layout positions | `header`, `sidebar`, `footer`, `banner` |

Slots a plugin can fill with a node tree. Context keys available as `{{ctx.*}}` are listed next to each slot.

| Slot | Context keys |
|---|---|
| `admin.dashboard` |  |
| `client.dashboard` |  |
| `admin.service.detail` | `ctx.service_id` |
| `admin.customer.detail` | `ctx.customer_id` |
| `admin.order.detail` | `ctx.order_id` |
| `admin.invoice.detail` | `ctx.invoice_id` |
| `client.home` |  |

