235 lines
6.7 KiB
Markdown
235 lines
6.7 KiB
Markdown
---
|
|
sidebar_position: 11
|
|
---
|
|
|
|
# UI API
|
|
|
|
The UI API lets plugins add pages, settings pages, side panels, channel sections, actions, embed renderers, and controlled DOM mounts.
|
|
|
|
Prefer registered UI contributions over direct DOM mounting. Contribution APIs let Angular render the plugin UI when the matching app surface exists. Direct DOM mounting runs immediately and throws if the target selector is not present.
|
|
|
|
## Required Capabilities
|
|
|
|
| Method | Capability |
|
|
| --- | --- |
|
|
| `ui.registerAppPage(id, contribution)` | `ui.pages` |
|
|
| `ui.registerSettingsPage(id, contribution)` | `ui.settings` |
|
|
| `ui.registerSidePanel(id, contribution)` | `ui.sidePanel` |
|
|
| `ui.registerChannelSection(id, contribution)` | `ui.channelsSection` |
|
|
| `ui.registerComposerAction(id, contribution)` | `ui.pages` |
|
|
| `ui.registerProfileAction(id, contribution)` | `ui.pages` |
|
|
| `ui.registerToolbarAction(id, contribution)` | `ui.pages` |
|
|
| `ui.registerEmbedRenderer(id, contribution)` | `ui.embeds` |
|
|
| `ui.mountElement(id, request)` | `ui.dom` |
|
|
|
|
Every registration returns a disposable. Push it into `context.subscriptions`.
|
|
|
|
## App Page
|
|
|
|
```js
|
|
export function activate(context) {
|
|
context.subscriptions.push(context.api.ui.registerAppPage('dashboard', {
|
|
label: 'Raid Dashboard',
|
|
path: '/plugins/example.raid-helper/dashboard',
|
|
render: () => {
|
|
const root = document.createElement('section');
|
|
root.innerHTML = '<h1>Raid Dashboard</h1><p>Tonight: dungeon practice.</p>';
|
|
return root;
|
|
}
|
|
}));
|
|
}
|
|
```
|
|
|
|
The page is hosted by `/plugins/:pluginId/:pageId`.
|
|
|
|
## Settings Page
|
|
|
|
```js
|
|
export function activate(context) {
|
|
context.subscriptions.push(context.api.ui.registerSettingsPage('preferences', {
|
|
label: 'Raid Helper',
|
|
settingsKey: 'raid-helper',
|
|
order: 20,
|
|
render: () => {
|
|
const wrapper = document.createElement('section');
|
|
const label = document.createElement('label');
|
|
const checkbox = document.createElement('input');
|
|
|
|
checkbox.type = 'checkbox';
|
|
checkbox.checked = true;
|
|
label.append(checkbox, ' Enable ready-check reminders');
|
|
wrapper.append(label);
|
|
return wrapper;
|
|
}
|
|
}));
|
|
}
|
|
```
|
|
|
|
## Side Panel
|
|
|
|
Use `ui.registerSidePanel` for content that belongs in the server sidebar plugin area. Do not mount directly into `[data-testid="plugin-room-side-panel"]`; that host is route-specific and may not exist during plugin activation.
|
|
|
|
```js
|
|
export function activate(context) {
|
|
context.subscriptions.push(context.api.ui.registerSidePanel('soundboard', {
|
|
label: 'Soundboard',
|
|
order: 10,
|
|
render: () => {
|
|
const panel = document.createElement('div');
|
|
const button = document.createElement('button');
|
|
|
|
button.type = 'button';
|
|
button.textContent = 'Play chime';
|
|
button.onclick = () => context.api.media.playAudioClip({
|
|
url: 'https://cdn.example.com/chime.wav',
|
|
volume: 0.6
|
|
});
|
|
panel.append(button);
|
|
return panel;
|
|
}
|
|
}));
|
|
}
|
|
```
|
|
|
|
Capabilities required: `ui.sidePanel` and `media.playAudio`.
|
|
|
|
## Channel Section
|
|
|
|
```js
|
|
export function activate(context) {
|
|
context.subscriptions.push(context.api.ui.registerChannelSection('events', {
|
|
label: 'Event Rooms',
|
|
type: 'custom',
|
|
order: 50
|
|
}));
|
|
}
|
|
```
|
|
|
|
## Composer Action
|
|
|
|
```js
|
|
export function activate(context) {
|
|
context.subscriptions.push(context.api.ui.registerComposerAction('insert-standup', {
|
|
icon: 'ST',
|
|
label: 'Insert standup prompt',
|
|
run: (actionContext) => {
|
|
context.api.messages.send(
|
|
'Standup: yesterday I..., today I..., blocked by...',
|
|
actionContext.textChannel?.id
|
|
);
|
|
}
|
|
}));
|
|
}
|
|
```
|
|
|
|
Capabilities required: `ui.pages` and `messages.send`.
|
|
|
|
## Profile Action
|
|
|
|
```js
|
|
export function activate(context) {
|
|
context.subscriptions.push(context.api.ui.registerProfileAction('wave', {
|
|
label: 'Wave',
|
|
run: (actionContext) => {
|
|
context.api.messages.send(`Waving at ${actionContext.user?.displayName ?? 'someone'}!`);
|
|
}
|
|
}));
|
|
}
|
|
```
|
|
|
|
## Toolbar Action
|
|
|
|
```js
|
|
export function activate(context) {
|
|
context.subscriptions.push(context.api.ui.registerToolbarAction('open-dashboard', {
|
|
label: 'Raid Helper',
|
|
run: () => {
|
|
context.api.logger.info('Open the Raid Helper plugin page from /plugins/example.raid-helper/dashboard');
|
|
}
|
|
}));
|
|
}
|
|
```
|
|
|
|
## Embed Renderer
|
|
|
|
```js
|
|
export function activate(context) {
|
|
context.subscriptions.push(context.api.ui.registerEmbedRenderer('raid-card', {
|
|
embedType: 'raid.card',
|
|
render: (payload) => {
|
|
const card = document.createElement('article');
|
|
const title = document.createElement('h3');
|
|
const body = document.createElement('p');
|
|
|
|
title.textContent = payload?.title ?? 'Raid';
|
|
body.textContent = payload?.description ?? 'No description provided.';
|
|
card.append(title, body);
|
|
return card;
|
|
}
|
|
}));
|
|
}
|
|
```
|
|
|
|
Example message content for this embed:
|
|
|
|
```text
|
|
toju:embed:raid.card:{"title":"Friday Raid","description":"Meet in Lobby at 20:00."}
|
|
```
|
|
|
|
## DOM Mount
|
|
|
|
Use DOM mounting only when normal UI contribution points are not enough. `ui.mountElement` resolves its target immediately. If the target does not exist, plugin activation fails with `Plugin mount target not found: <selector>`.
|
|
|
|
Safe uses:
|
|
|
|
- Mounting a global overlay, badge, or modal into `body` during activation.
|
|
- Mounting into a route-specific element only after checking that element exists.
|
|
|
|
Avoid:
|
|
|
|
- Mounting sidebar content into `[data-testid="plugin-room-side-panel"]`. Use `ui.registerSidePanel`.
|
|
- Mounting chat content into `app-chat-messages` during activation without checking for the element.
|
|
|
|
```js
|
|
export function activate(context) {
|
|
const badge = document.createElement('div');
|
|
badge.textContent = 'Raid helper active';
|
|
badge.style.position = 'fixed';
|
|
badge.style.right = '16px';
|
|
badge.style.bottom = '16px';
|
|
badge.style.padding = '8px 10px';
|
|
badge.style.background = '#111827';
|
|
badge.style.color = 'white';
|
|
badge.style.borderRadius = '6px';
|
|
|
|
context.subscriptions.push(context.api.ui.mountElement('active-badge', {
|
|
target: 'body',
|
|
position: 'beforeend',
|
|
element: badge
|
|
}));
|
|
}
|
|
```
|
|
|
|
Route-specific mount example with a guard:
|
|
|
|
```js
|
|
export function activate(context) {
|
|
const target = document.querySelector('app-chat-messages');
|
|
|
|
if (!target) {
|
|
context.api.logger.warn('Chat messages host is not rendered yet; skipping chat mount');
|
|
return;
|
|
}
|
|
|
|
const banner = document.createElement('div');
|
|
banner.textContent = 'Raid helper active in this chat.';
|
|
|
|
context.subscriptions.push(context.api.ui.mountElement('chat-banner', {
|
|
target,
|
|
position: 'afterbegin',
|
|
element: banner
|
|
}));
|
|
}
|
|
```
|
|
|
|
The runtime tags plugin-owned DOM and removes it on unload, but plugins should still keep mounts minimal and accessible. |