Add UI button registration API (registerButton, unregisterButton, getButtonsForLocation)

This commit is contained in:
2026-04-22 00:51:57 +10:00
parent 6f506c59a4
commit 6c389381df
20 changed files with 438 additions and 21 deletions

View File

@@ -1 +1 @@
{"version":3,"file":"PluginContext.d.ts","sourceRoot":"","sources":["../src/PluginContext.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,kBAAkB,CAAC;IAEjC;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnE;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,cAAc,GACvB,aAAa,CAqJf"}
{"version":3,"file":"PluginContext.d.ts","sourceRoot":"","sources":["../src/PluginContext.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,kBAAkB,CAAC;IAEjC;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnE;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,cAAc,GACvB,aAAa,CA4Jf"}

View File

@@ -40,6 +40,13 @@ export function createPluginContext(options, registry) {
unregisterRenderer: (type) => {
registry.unregisterRenderer(type);
},
registerButton: (button) => {
registry.registerButton(pluginId, button);
registry.addLog(pluginId, 'log', [`Registered button: ${button.id} at ${button.location}`]);
},
unregisterButton: (buttonId) => {
registry.unregisterButton(pluginId, buttonId);
},
},
settings: {
define: (schema) => {

File diff suppressed because one or more lines are too long

View File

@@ -139,6 +139,8 @@ export interface PluginContext {
ui: {
registerRenderer: (type: string, renderer: CustomRenderer) => void;
unregisterRenderer: (type: string) => void;
registerButton: (button: UIButtonDefinition) => void;
unregisterButton: (buttonId: string) => void;
};
/** Settings API */
settings: {
@@ -188,7 +190,47 @@ export interface PluginSettingsSection {
component: unknown;
}
/** UI locations where plugins can inject content. */
export type UILocation = 'room-header' | 'message-actions' | 'user-menu' | 'room-menu' | 'composer-actions';
export type UILocation = 'room-header' | 'message-actions' | 'user-menu' | 'room-menu' | 'composer-actions' | 'channel-list' | 'direct-messages' | 'home-section' | 'search-notification-section' | 'text-composer-toolbar' | 'sidebar-actions';
/**
* Button positioning configuration for plugins.
* Allows buttons to be placed before or after existing elements or groups.
*/
export interface UIButtonPosition {
/** Place button before this element/group ID */
before?: string;
/** Place button after this element/group ID */
after?: string;
/**
* Optional group ID this button belongs to.
* Buttons in the same group are rendered together.
*/
group?: string;
/** Order within the group (lower numbers appear first) */
order?: number;
}
/**
* Definition for a UI button registered by a plugin.
* Framework-agnostic - the component/element is passed as unknown.
*/
export interface UIButtonDefinition {
/** Unique ID for this button */
id: string;
/** Location where the button should appear */
location: UILocation;
/** Button label or tooltip */
label: string;
/** Optional icon identifier */
icon?: string;
/** Positioning configuration */
position?: UIButtonPosition;
/**
* The actual button component or render function.
* Framework-agnostic - hosts using React should cast to ReactNode.
*/
component: unknown;
/** Optional callback when clicked (if component doesn't handle it) */
onClick?: () => void | Promise<void>;
}
/**
* The main plugin interface that all plugins must implement.
*/

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
import type { Plugin, PluginContext, PluginCommand, MessageInterceptor, MessageContext, CustomRenderer, SettingsSchema, PluginTheme, PluginLogEntry } from './PluginInterfaces.js';
import type { Plugin, PluginContext, PluginCommand, MessageInterceptor, MessageContext, CustomRenderer, SettingsSchema, PluginTheme, PluginLogEntry, UIButtonDefinition, UILocation } from './PluginInterfaces.js';
import type { IPluginStorage } from './interfaces.js';
/**
* Configuration options for the PluginRegistry.
@@ -32,6 +32,7 @@ export declare class PluginRegistry {
private beforeSendInterceptors;
private receiveInterceptors;
private readonly renderers;
private readonly buttons;
private readonly settingsSchemas;
private readonly pluginSettings;
private readonly matrixHandlers;
@@ -52,7 +53,7 @@ export declare class PluginRegistry {
/** Remove a registered command by name. */
unregisterCommand(name: string): void;
/** Execute a command by name, passing raw argument string. */
executeCommand(name: string, rawArgs: string): Promise<string | void>;
executeCommand(name: string, rawArgs: string, roomId?: string): Promise<string | void>;
private parseCommandArgs;
/** Return all registered commands. */
getCommands(): Array<{
@@ -74,6 +75,22 @@ export declare class PluginRegistry {
unregisterRenderer(type: string): void;
/** Get the renderer for a given type, if any. */
getRenderer(type: string): CustomRenderer | undefined;
/** Register a UI button contributed by a plugin. */
registerButton(pluginId: string, button: UIButtonDefinition): void;
/** Remove a registered button by ID. */
unregisterButton(pluginId: string, buttonId: string): void;
/**
* Get all buttons for a specific UI location, sorted by position configuration.
* Returns buttons in the order they should be rendered.
*/
getButtonsForLocation(location: UILocation): UIButtonDefinition[];
/**
* Sort buttons according to their position configuration.
* - Buttons with position.before/after are positioned relative to existing buttons
* - Buttons in groups are kept together
* - Within groups, buttons are sorted by order property
*/
private sortButtonsByPosition;
/** Define the settings schema for a plugin and load persisted values. */
defineSettings(pluginId: string, schema: SettingsSchema): void;
/** Get a plugin setting value. */

View File

@@ -1 +1 @@
{"version":3,"file":"PluginRegistry.d.ts","sourceRoot":"","sources":["../src/PluginRegistry.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"PluginRegistry.d.ts","sourceRoot":"","sources":["../src/PluginRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EACN,aAAa,EACb,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,cAAc,EACd,WAAW,EACX,cAAc,EACd,kBAAkB,EAClB,UAAU,EACX,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,cAAc,EAAsB,MAAM,iBAAiB,CAAC;AAI1E;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;IAEzB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAE9E;;;OAGG;IACH,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACjD;AAED;;;GAGG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiE;IACzF,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmE;IAC5F,OAAO,CAAC,sBAAsB,CAAoE;IAClG,OAAO,CAAC,mBAAmB,CAAoE;IAC/F,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqE;IAC/F,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuE;IAC/F,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqC;IACrE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA8C;IAC7E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAG3B;IACJ,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+B;IACtD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAGnB;IACJ,OAAO,CAAC,IAAI,CAAwB;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAA6C;IAChF,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAA+C;gBAExE,OAAO,GAAE,qBAA0B;IAU/C,sFAAsF;IACtF,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,IAAI;IASxE,uFAAuF;IACjF,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAsDjD,kDAAkD;IAClD,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,IAAI;IAQ/D,2CAA2C;IAC3C,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIrC,8DAA8D;IACxD,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAc5F,OAAO,CAAC,gBAAgB;IA4BxB,sCAAsC;IACtC,WAAW,IAAI,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,aAAa,CAAA;KAAE,CAAC;IAQhF,kDAAkD;IAClD,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,kBAAkB,GAAG,IAAI;IAIjF,8CAA8C;IAC9C,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,kBAAkB,GAAG,IAAI;IAI9E,gFAAgF;IAC1E,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;IAYrE,4EAA4E;IACtE,cAAc,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;IAgBlE,0DAA0D;IAC1D,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,GAAG,IAAI;IAIhF,iCAAiC;IACjC,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAItC,iDAAiD;IACjD,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAQrD,oDAAoD;IACpD,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,IAAI;IASlE,wCAAwC;IACxC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAK1D;;;OAGG;IACH,qBAAqB,CAAC,QAAQ,EAAE,UAAU,GAAG,kBAAkB,EAAE;IAQjE;;;;;OAKG;IACH,OAAO,CAAC,qBAAqB;IA8F7B,yEAAyE;IACzE,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,IAAI;IAmB9D,kCAAkC;IAClC,gBAAgB,CAAC,CAAC,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAI3E,iDAAiD;IACjD,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAOrE,4CAA4C;IAC5C,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAQrE,gDAAgD;IAChD,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI;IAazD,kDAAkD;IAClD,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAQxD,2CAA2C;IAC3C,eAAe,IAAI,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAajG,qDAAqD;IACrD,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,IAAI;IAO7F,iDAAiD;IACjD,kBAAkB,CAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAChC,IAAI;IAQP,kDAAkD;IAClD,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;KAAE,CAAC;IAQnG,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAMjD,yEAAyE;IACzE,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IASpD,wCAAwC;IACxC,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAOhF,oEAAoE;IACpE,OAAO,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE;IAI5C,iEAAiE;IACjE,SAAS,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;IAQlC,iDAAiD;IACjD,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAI3C;;;OAGG;IACH,oBAAoB,IAAI,KAAK,CAAC;QAC5B,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,EAAE,MAAM,CAAC;QACrB,gBAAgB,EAAE,MAAM,CAAC;QACzB,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,OAAO,CAAC;KACtB,CAAC;IAoBF,sEAAsE;IAChE,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAUjG,uDAAuD;IACvD,KAAK,IAAI,IAAI;CAgBd"}

141
dist/PluginRegistry.js vendored
View File

@@ -11,6 +11,7 @@ export class PluginRegistry {
this.beforeSendInterceptors = [];
this.receiveInterceptors = [];
this.renderers = new Map();
this.buttons = new Map();
this.settingsSchemas = new Map();
this.pluginSettings = new Map();
this.matrixHandlers = new Map();
@@ -57,6 +58,10 @@ export class PluginRegistry {
if (renderer.pluginId === id)
this.renderers.delete(type);
}
for (const [buttonId, btn] of this.buttons.entries()) {
if (btn.pluginId === id)
this.buttons.delete(buttonId);
}
for (const [themeId, theme] of this.themes.entries()) {
if (theme.pluginId === id) {
this.onThemeUnregistered?.(themeId);
@@ -93,11 +98,14 @@ export class PluginRegistry {
this.commands.delete(name);
}
/** Execute a command by name, passing raw argument string. */
async executeCommand(name, rawArgs) {
async executeCommand(name, rawArgs, roomId) {
const cmd = this.commands.get(name);
if (!cmd)
throw new Error(`Command /${name} not found`);
const args = this.parseCommandArgs(cmd.command, rawArgs);
if (roomId) {
args.roomId = roomId;
}
const entry = this.plugins.get(cmd.pluginId);
if (!entry)
throw new Error(`Plugin ${cmd.pluginId} not loaded`);
@@ -106,11 +114,25 @@ export class PluginRegistry {
parseCommandArgs(command, rawArgs) {
if (!command.args || command.args.length === 0)
return {};
const parts = rawArgs.trim().split(/\s+/);
const trimmedArgs = rawArgs.trim();
if (!trimmedArgs)
return {};
const argNames = command.args.map(arg => typeof arg === 'string' ? arg : arg.name);
// If only one arg (that's not roomId), give it all the text
const nonRoomIdArgs = argNames.filter(name => name !== 'roomId');
if (nonRoomIdArgs.length === 1) {
const result = {};
result[nonRoomIdArgs[0]] = trimmedArgs;
return result;
}
// Otherwise, split by whitespace
const parts = trimmedArgs.split(/\s+/);
const result = {};
command.args.forEach((arg, index) => {
const argName = typeof arg === 'string' ? arg : arg.name;
result[argName] = parts[index];
if (argName !== 'roomId') {
result[argName] = parts[index];
}
});
return result;
}
@@ -171,6 +193,119 @@ export class PluginRegistry {
return this.renderers.get(type)?.renderer;
}
// ---------------------------------------------------------------------------
// UI Buttons
// ---------------------------------------------------------------------------
/** Register a UI button contributed by a plugin. */
registerButton(pluginId, button) {
const fullId = `${pluginId}:${button.id}`;
if (this.buttons.has(fullId)) {
console.warn(`[PluginRegistry] Button ${fullId} already registered`);
return;
}
this.buttons.set(fullId, { pluginId, button: { ...button, id: fullId } });
}
/** Remove a registered button by ID. */
unregisterButton(pluginId, buttonId) {
const fullId = `${pluginId}:${buttonId}`;
this.buttons.delete(fullId);
}
/**
* Get all buttons for a specific UI location, sorted by position configuration.
* Returns buttons in the order they should be rendered.
*/
getButtonsForLocation(location) {
const buttons = Array.from(this.buttons.values())
.filter(({ button }) => button.location === location)
.map(({ button }) => button);
return this.sortButtonsByPosition(buttons);
}
/**
* Sort buttons according to their position configuration.
* - Buttons with position.before/after are positioned relative to existing buttons
* - Buttons in groups are kept together
* - Within groups, buttons are sorted by order property
*/
sortButtonsByPosition(buttons) {
if (buttons.length === 0)
return [];
const grouped = new Map();
const ungrouped = [];
const positionedButtons = new Map();
for (const button of buttons) {
positionedButtons.set(button.id, button);
if (button.position?.group) {
if (!grouped.has(button.position.group)) {
grouped.set(button.position.group, []);
}
grouped.get(button.position.group).push(button);
}
else {
ungrouped.push(button);
}
}
for (const [_group, groupButtons] of grouped) {
groupButtons.sort((a, b) => {
const orderA = a.position?.order ?? 0;
const orderB = b.position?.order ?? 0;
return orderA - orderB;
});
}
const result = [];
const inserted = new Set();
const insertButton = (button) => {
if (inserted.has(button.id))
return;
const group = button.position?.group ? grouped.get(button.position.group) : [button];
if (!group)
return;
for (const btn of group) {
if (!inserted.has(btn.id)) {
result.push(btn);
inserted.add(btn.id);
}
}
};
for (const button of [...ungrouped, ...Array.from(grouped.values()).flat()]) {
if (inserted.has(button.id))
continue;
if (button.position?.before) {
const beforeButton = positionedButtons.get(button.position.before);
if (beforeButton) {
const idx = result.findIndex(b => b.id === beforeButton.id);
if (idx !== -1) {
const group = button.position.group ? grouped.get(button.position.group) : [button];
if (group) {
for (let i = group.length - 1; i >= 0; i--) {
if (!inserted.has(group[i].id)) {
result.splice(idx, 0, group[i]);
inserted.add(group[i].id);
}
}
}
continue;
}
}
}
if (button.position?.after) {
const afterButton = positionedButtons.get(button.position.after);
if (afterButton) {
const idx = result.findIndex(b => b.id === afterButton.id);
if (idx !== -1) {
insertButton(button);
continue;
}
}
}
insertButton(button);
}
for (const button of buttons) {
if (!inserted.has(button.id)) {
result.push(button);
}
}
return result;
}
// ---------------------------------------------------------------------------
// Settings
// ---------------------------------------------------------------------------
/** Define the settings schema for a plugin and load persisted values. */

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@ export { PluginMarketplaceManager } from './PluginMarketplaceManager.js';
export type { PluginMarketplaceHostAdapter, PluginMarketplaceInstalledRecord, PluginMarketplaceManagerOptions, } from './PluginMarketplaceManager.js';
export type { PluginMetadata, PluginIndex, InstalledPlugin, } from './types.js';
export { PluginTab } from './types.js';
export type { ThemeColorGroup, ThemePaletteGroup, PluginThemeColors, PluginTheme, CommandArg, PluginCommand, MessageInterceptor, MessageContext, CustomRenderer, SettingDefinition, SettingsSchema, NotificationOptions, PluginContext, PluginSettingsSection, UILocation, Plugin, PluginLogEntry, } from './PluginInterfaces.js';
export type { ThemeColorGroup, ThemePaletteGroup, PluginThemeColors, PluginTheme, CommandArg, PluginCommand, MessageInterceptor, MessageContext, CustomRenderer, SettingDefinition, SettingsSchema, NotificationOptions, PluginContext, PluginSettingsSection, UILocation, UIButtonDefinition, UIButtonPosition, Plugin, PluginLogEntry, } from './PluginInterfaces.js';
export { generateThemeCSS } from './theme-css.js';
export { PluginRegistry } from './PluginRegistry.js';
export type { PluginRegistryOptions } from './PluginRegistry.js';

View File

@@ -1 +1 @@
{"version":3,"file":"index.browser.d.ts","sourceRoot":"","sources":["../src/index.browser.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,YAAY,EACV,8BAA8B,EAC9B,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,YAAY,EACV,4BAA4B,EAC5B,gCAAgC,EAChC,+BAA+B,GAChC,MAAM,+BAA+B,CAAC;AAEvC,YAAY,EACV,cAAc,EACd,WAAW,EACX,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,YAAY,EACV,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,UAAU,EACV,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,UAAU,EACV,MAAM,EACN,cAAc,GACf,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,YAAY,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC"}
{"version":3,"file":"index.browser.d.ts","sourceRoot":"","sources":["../src/index.browser.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,YAAY,EACV,8BAA8B,EAC9B,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,YAAY,EACV,4BAA4B,EAC5B,gCAAgC,EAChC,+BAA+B,GAChC,MAAM,+BAA+B,CAAC;AAEvC,YAAY,EACV,cAAc,EACd,WAAW,EACX,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,YAAY,EACV,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,UAAU,EACV,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,UAAU,EACV,kBAAkB,EAClB,gBAAgB,EAChB,MAAM,EACN,cAAc,GACf,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,YAAY,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC"}

View File

@@ -1 +1 @@
{"version":3,"file":"index.browser.js","sourceRoot":"","sources":["../src/index.browser.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAOvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAYzE,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAsBvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC"}
{"version":3,"file":"index.browser.js","sourceRoot":"","sources":["../src/index.browser.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAOvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAYzE,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAwBvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC"}

2
dist/index.d.ts vendored
View File

@@ -6,7 +6,7 @@ export { PluginMarketplaceManager } from './PluginMarketplaceManager.js';
export type { PluginMarketplaceHostAdapter, PluginMarketplaceManagerOptions, } from './PluginMarketplaceManager.js';
export type { PluginMetadata, PluginIndex, InstalledPlugin, } from './types.js';
export { PluginTab } from './types.js';
export type { ThemeColorGroup, ThemePaletteGroup, PluginThemeColors, PluginTheme, CommandArg, PluginCommand, MessageInterceptor, MessageContext, CustomRenderer, SettingDefinition, SettingsSchema, NotificationOptions, PluginContext, PluginSettingsSection, UILocation, Plugin, PluginLogEntry, } from './PluginInterfaces.js';
export type { ThemeColorGroup, ThemePaletteGroup, PluginThemeColors, PluginTheme, CommandArg, PluginCommand, MessageInterceptor, MessageContext, CustomRenderer, SettingDefinition, SettingsSchema, NotificationOptions, PluginContext, PluginSettingsSection, UILocation, UIButtonDefinition, UIButtonPosition, Plugin, PluginLogEntry, } from './PluginInterfaces.js';
export { generateThemeCSS } from './theme-css.js';
export { PluginRegistry } from './PluginRegistry.js';
export type { PluginRegistryOptions } from './PluginRegistry.js';

2
dist/index.d.ts.map vendored
View File

@@ -1 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,YAAY,EACV,8BAA8B,EAC9B,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,YAAY,EACV,4BAA4B,EAC5B,+BAA+B,GAChC,MAAM,+BAA+B,CAAC;AAEvC,YAAY,EACV,cAAc,EACd,WAAW,EACX,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,YAAY,EACV,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,UAAU,EACV,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,UAAU,EACV,MAAM,EACN,cAAc,GACf,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,YAAY,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC"}
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,YAAY,EACV,8BAA8B,EAC9B,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,YAAY,EACV,4BAA4B,EAC5B,+BAA+B,GAChC,MAAM,+BAA+B,CAAC;AAEvC,YAAY,EACV,cAAc,EACd,WAAW,EACX,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,YAAY,EACV,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,UAAU,EACV,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,UAAU,EACV,kBAAkB,EAClB,gBAAgB,EAChB,MAAM,EACN,cAAc,GACf,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,YAAY,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC"}

2
dist/index.js.map vendored
View File

@@ -1 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAOvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAWzE,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAsBvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAGzD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAOvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAWzE,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAwBvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAGzD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}

View File

@@ -72,6 +72,13 @@ export function createPluginContext(
unregisterRenderer: (type) => {
registry.unregisterRenderer(type);
},
registerButton: (button) => {
registry.registerButton(pluginId, button);
registry.addLog(pluginId, 'log', [`Registered button: ${button.id} at ${button.location}`]);
},
unregisterButton: (buttonId) => {
registry.unregisterButton(pluginId, buttonId);
},
},
settings: {

View File

@@ -148,6 +148,8 @@ export interface PluginContext {
ui: {
registerRenderer: (type: string, renderer: CustomRenderer) => void;
unregisterRenderer: (type: string) => void;
registerButton: (button: UIButtonDefinition) => void;
unregisterButton: (buttonId: string) => void;
};
/** Settings API */
@@ -211,7 +213,55 @@ export type UILocation =
| 'message-actions'
| 'user-menu'
| 'room-menu'
| 'composer-actions';
| 'composer-actions'
| 'channel-list'
| 'direct-messages'
| 'home-section'
| 'search-notification-section'
| 'text-composer-toolbar'
| 'sidebar-actions';
/**
* Button positioning configuration for plugins.
* Allows buttons to be placed before or after existing elements or groups.
*/
export interface UIButtonPosition {
/** Place button before this element/group ID */
before?: string;
/** Place button after this element/group ID */
after?: string;
/**
* Optional group ID this button belongs to.
* Buttons in the same group are rendered together.
*/
group?: string;
/** Order within the group (lower numbers appear first) */
order?: number;
}
/**
* Definition for a UI button registered by a plugin.
* Framework-agnostic - the component/element is passed as unknown.
*/
export interface UIButtonDefinition {
/** Unique ID for this button */
id: string;
/** Location where the button should appear */
location: UILocation;
/** Button label or tooltip */
label: string;
/** Optional icon identifier */
icon?: string;
/** Positioning configuration */
position?: UIButtonPosition;
/**
* The actual button component or render function.
* Framework-agnostic - hosts using React should cast to ReactNode.
*/
component: unknown;
/** Optional callback when clicked (if component doesn't handle it) */
onClick?: () => void | Promise<void>;
}
/**
* The main plugin interface that all plugins must implement.

View File

@@ -8,6 +8,8 @@ import type {
SettingsSchema,
PluginTheme,
PluginLogEntry,
UIButtonDefinition,
UILocation,
} from './PluginInterfaces.js';
import type { IPluginStorage, IPluginEventClient } from './interfaces.js';
import { MemoryStorage } from './interfaces.js';
@@ -48,6 +50,7 @@ export class PluginRegistry {
private beforeSendInterceptors: Array<{ pluginId: string; interceptor: MessageInterceptor }> = [];
private receiveInterceptors: Array<{ pluginId: string; interceptor: MessageInterceptor }> = [];
private readonly renderers = new Map<string, { pluginId: string; renderer: CustomRenderer }>();
private readonly buttons = new Map<string, { pluginId: string; button: UIButtonDefinition }>();
private readonly settingsSchemas = new Map<string, SettingsSchema>();
private readonly pluginSettings = new Map<string, Record<string, unknown>>();
private readonly matrixHandlers = new Map<
@@ -109,6 +112,10 @@ export class PluginRegistry {
if (renderer.pluginId === id) this.renderers.delete(type);
}
for (const [buttonId, btn] of this.buttons.entries()) {
if (btn.pluginId === id) this.buttons.delete(buttonId);
}
for (const [themeId, theme] of this.themes.entries()) {
if (theme.pluginId === id) {
this.onThemeUnregistered?.(themeId);
@@ -151,11 +158,14 @@ export class PluginRegistry {
}
/** Execute a command by name, passing raw argument string. */
async executeCommand(name: string, rawArgs: string): Promise<string | void> {
async executeCommand(name: string, rawArgs: string, roomId?: string): Promise<string | void> {
const cmd = this.commands.get(name);
if (!cmd) throw new Error(`Command /${name} not found`);
const args = this.parseCommandArgs(cmd.command, rawArgs);
if (roomId) {
args.roomId = roomId;
}
const entry = this.plugins.get(cmd.pluginId);
if (!entry) throw new Error(`Plugin ${cmd.pluginId} not loaded`);
@@ -164,11 +174,28 @@ export class PluginRegistry {
private parseCommandArgs(command: PluginCommand, rawArgs: string): Record<string, unknown> {
if (!command.args || command.args.length === 0) return {};
const parts = rawArgs.trim().split(/\s+/);
const trimmedArgs = rawArgs.trim();
if (!trimmedArgs) return {};
const argNames = command.args.map(arg => typeof arg === 'string' ? arg : arg.name);
// If only one arg (that's not roomId), give it all the text
const nonRoomIdArgs = argNames.filter(name => name !== 'roomId');
if (nonRoomIdArgs.length === 1) {
const result: Record<string, unknown> = {};
result[nonRoomIdArgs[0]] = trimmedArgs;
return result;
}
// Otherwise, split by whitespace
const parts = trimmedArgs.split(/\s+/);
const result: Record<string, unknown> = {};
command.args.forEach((arg, index) => {
const argName = typeof arg === 'string' ? arg : arg.name;
result[argName] = parts[index];
if (argName !== 'roomId') {
result[argName] = parts[index];
}
});
return result;
}
@@ -237,6 +264,134 @@ export class PluginRegistry {
return this.renderers.get(type)?.renderer;
}
// ---------------------------------------------------------------------------
// UI Buttons
// ---------------------------------------------------------------------------
/** Register a UI button contributed by a plugin. */
registerButton(pluginId: string, button: UIButtonDefinition): void {
const fullId = `${pluginId}:${button.id}`;
if (this.buttons.has(fullId)) {
console.warn(`[PluginRegistry] Button ${fullId} already registered`);
return;
}
this.buttons.set(fullId, { pluginId, button: { ...button, id: fullId } });
}
/** Remove a registered button by ID. */
unregisterButton(pluginId: string, buttonId: string): void {
const fullId = `${pluginId}:${buttonId}`;
this.buttons.delete(fullId);
}
/**
* Get all buttons for a specific UI location, sorted by position configuration.
* Returns buttons in the order they should be rendered.
*/
getButtonsForLocation(location: UILocation): UIButtonDefinition[] {
const buttons = Array.from(this.buttons.values())
.filter(({ button }) => button.location === location)
.map(({ button }) => button);
return this.sortButtonsByPosition(buttons);
}
/**
* Sort buttons according to their position configuration.
* - Buttons with position.before/after are positioned relative to existing buttons
* - Buttons in groups are kept together
* - Within groups, buttons are sorted by order property
*/
private sortButtonsByPosition(buttons: UIButtonDefinition[]): UIButtonDefinition[] {
if (buttons.length === 0) return [];
const grouped = new Map<string, UIButtonDefinition[]>();
const ungrouped: UIButtonDefinition[] = [];
const positionedButtons = new Map<string, UIButtonDefinition>();
for (const button of buttons) {
positionedButtons.set(button.id, button);
if (button.position?.group) {
if (!grouped.has(button.position.group)) {
grouped.set(button.position.group, []);
}
grouped.get(button.position.group)!.push(button);
} else {
ungrouped.push(button);
}
}
for (const [_group, groupButtons] of grouped) {
groupButtons.sort((a, b) => {
const orderA = a.position?.order ?? 0;
const orderB = b.position?.order ?? 0;
return orderA - orderB;
});
}
const result: UIButtonDefinition[] = [];
const inserted = new Set<string>();
const insertButton = (button: UIButtonDefinition) => {
if (inserted.has(button.id)) return;
const group = button.position?.group ? grouped.get(button.position.group) : [button];
if (!group) return;
for (const btn of group) {
if (!inserted.has(btn.id)) {
result.push(btn);
inserted.add(btn.id);
}
}
};
for (const button of [...ungrouped, ...Array.from(grouped.values()).flat()]) {
if (inserted.has(button.id)) continue;
if (button.position?.before) {
const beforeButton = positionedButtons.get(button.position.before);
if (beforeButton) {
const idx = result.findIndex(b => b.id === beforeButton.id);
if (idx !== -1) {
const group = button.position.group ? grouped.get(button.position.group) : [button];
if (group) {
for (let i = group.length - 1; i >= 0; i--) {
if (!inserted.has(group[i].id)) {
result.splice(idx, 0, group[i]);
inserted.add(group[i].id);
}
}
}
continue;
}
}
}
if (button.position?.after) {
const afterButton = positionedButtons.get(button.position.after);
if (afterButton) {
const idx = result.findIndex(b => b.id === afterButton.id);
if (idx !== -1) {
insertButton(button);
continue;
}
}
}
insertButton(button);
}
for (const button of buttons) {
if (!inserted.has(button.id)) {
result.push(button);
}
}
return result;
}
// ---------------------------------------------------------------------------
// Settings
// ---------------------------------------------------------------------------

View File

@@ -41,6 +41,8 @@ export type {
PluginContext,
PluginSettingsSection,
UILocation,
UIButtonDefinition,
UIButtonPosition,
Plugin,
PluginLogEntry,
} from './PluginInterfaces.js';

View File

@@ -40,6 +40,8 @@ export type {
PluginContext,
PluginSettingsSection,
UILocation,
UIButtonDefinition,
UIButtonPosition,
Plugin,
PluginLogEntry,
} from './PluginInterfaces.js';