-
Notifications
You must be signed in to change notification settings - Fork 0
Plugins
Emilio Romero edited this page Jun 3, 2026
·
5 revisions
Plugins allow you to extend Umbra with custom UI components or behavior.
export interface Plugin<T extends HTMLElement = HTMLElement> {
// Optional: Element rendered by the plugin
el?: T;
// Required: Render the plugin UI
render(): void | T;
// Optional: Called when theme changes
onThemeChange?: (theme: string) => void;
// Optional: Called when Umbra is destroyed
onDestroy?: () => void;
}-
Constructor - Plugin is instantiated with the Umbra instance as
host - render() - Called during initialization
- onThemeChange() - Called whenever the theme changes (including sync from other tabs)
-
onDestroy() - Called when
umbra.destroy()is invoked
The host parameter is your Umbra instance. You can call:
class MyPlugin implements Plugin {
public static readonly pluginId = 'u-<something>'; // Required
private host: any;
constructor(host: any) {
this.host = host;
// Access: host.theme, host.toggleTheme(), host.getCurrentTheme()
}
render(): void | HTMLElement {
throw new Error("Method not implemented.");
}
onThemeChange(theme: string): void {
console.log('Theme changed to:', theme);
}
}import type { Plugin } from '@emrocode/umbra';
interface KeyboardShortcutOptions {
key?: string;
ctrl?: boolean;
shift?: boolean;
target?: 'body' | 'input' | 'all';
cooldown?: number;
}
export class KeyboardShortcut implements Plugin {
public static readonly pluginId = 'u-keyboard-shortcut';
private _host: any;
private options: Required<KeyboardShortcutOptions>;
private _lastTriggered: number = 0;
constructor(host: any, options?: KeyboardShortcutOptions) {
this._host = host;
this.options = {
key: options?.key ?? 'd',
ctrl: options?.ctrl ?? false,
shift: options?.shift ?? false,
target: options?.target ?? 'body',
cooldown: options?.cooldown ?? 300,
};
}
private handleKeyDown = (e: KeyboardEvent) => {
if (this.options.target === 'body' && this.isTyping(e)) return;
if (this.options.target === 'input' && !this.isTyping(e)) return;
if (this.matches(e)) {
e.preventDefault();
const now = Date.now();
if (now - this._lastTriggered < this.options.cooldown) return;
this._lastTriggered = now;
this._host.toggleTheme();
}
};
render(): void {
document.addEventListener('keydown', this.handleKeyDown);
}
onDestroy(): void {
document.removeEventListener('keydown', this.handleKeyDown);
}
private matches(e: KeyboardEvent): boolean {
return (
e.key.toLowerCase() === this.options.key.toLowerCase() &&
(!this.options.ctrl || e.ctrlKey || e.metaKey) &&
(!this.options.shift || e.shiftKey) &&
(this.options.ctrl || (!e.ctrlKey && !e.metaKey && !e.altKey))
);
}
private isTyping(e: KeyboardEvent): boolean {
const target = e.target as HTMLElement;
const tagName = target.tagName.toLowerCase();
const isEditable =
target.isContentEditable || tagName === 'input' || tagName === 'textarea';
return isEditable;
}
}