# UI schema

Declarative plugin UI: plugins describe pages and slot contributions as a JSON node tree that the frontend renders natively with shadcn components and the active theme. No plugin JavaScript ever runs in the app; `embed` (a sandboxed iframe) is a documented last resort behind the `ui.embed` capability. 

## Node types

Every node type the renderer understands. Unknown types are rejected at manifest load so a plugin never renders half-broken.

- `stack`
- `row`
- `grid`
- `card`
- `section`
- `separator`
- `tabs`
- `tab`
- `accordion`
- `accordion_item`
- `dialog`
- `heading`
- `text`
- `badge`
- `alert`
- `stat`
- `kv`
- `code`
- `image`
- `avatar`
- `icon`
- `progress`
- `skeleton`
- `empty`
- `tooltip`
- `money`
- `date`
- `link`
- `table`
- `list`
- `each`
- `if`
- `chart`
- `button`
- `form`
- `input`
- `textarea`
- `select`
- `switch`
- `checkbox`
- `radio`
- `collapsible`
- `popover`
- `sheet`
- `dropdown`
- `breadcrumb`
- `pagination`
- `scroll_area`
- `label`
- `kbd`
- `blockquote`
- `list_item`
- `frame`
- `embed`

Limits: at most 2000 nodes and depth 32 per tree.

## Slots

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` |  |

## UiPage

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | `String` | yes | `[a-z0-9-]`, unique per plugin; route `/admin/p/<plugin>/<id>` or `/p/<plugin>/<id>`. |
| `area` | `String` | yes | `admin` or `client`. |
| `title` | `String` | yes | Translation key in `plugin.<id>.*`. |
| `icon` | `Option<String>` | no | Phosphor icon name (PascalCase without `Icon`), e.g. `HardDrives`. |
| `nav` | `bool` | no | Show in the sidebar. |
| `hub` | `Option<String>` | no | Nav category whose hub lists the page (`billing`, `addons`, …). |
| `permission` | `Option<String>` | no |  |
| `data` | `BTreeMap<String, DataSource>` | no |  |
| `body` | `Value` | no | Declarative node tree (or `null` when `frame` is set). |
| `frame` | `Option<Frame>` | no |  |

Source: `crates/pelty-extensions/src/ui_schema.rs` (`UiPage`).

## UiSlotContribution

| Field | Type | Required | Description |
|---|---|---|---|
| `slot` | `String` | yes |  |
| `permission` | `Option<String>` | no |  |
| `data` | `BTreeMap<String, DataSource>` | no |  |
| `body` | `Value` | no |  |
| `frame` | `Option<Frame>` | no |  |
| `order` | `i32` | no | Order among contributions to the same slot (lower first). |

Source: `crates/pelty-extensions/src/ui_schema.rs` (`UiSlotContribution`).

## Frame

A sandboxed iframe served from the addon's own `assets/` folder.

| Field | Type | Required | Description |
|---|---|---|---|
| `src` | `String` | yes | Package path, e.g. `assets/ui/index.html` (served at `/api/v1/addons/<id>/assets/ui/index.html`). |
| `height` | `Option<u32>` | no | Initial height in px (the frame auto-resizes via the bridge). |

Source: `crates/pelty-extensions/src/ui_schema.rs` (`Frame`).

## DataSource

| Field | Type | Required | Description |
|---|---|---|---|
| `get` | `String` | yes | GET path under `/api/v1/`; may contain `{{ctx.*}}` / `{{params.*}}`. |
| `refresh_secs` | `Option<u32>` | no | Auto-refresh interval (5..=3600 s). |

Source: `crates/pelty-extensions/src/ui_schema.rs` (`DataSource`).

## Example

See [Addon UI](ui-guide.md).

