Skip to content

Navigation Menu

Sign in
Sign up

Plugins

Emilio Romero edited this page Jun 3, 2026 · 13 revisions

Create custom plugins for Umbra

Plugins allow you to extend Umbra with custom UI components or behavior.

Interface

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;
}

Lifecycle

  1. Constructor - Plugin is instantiated with the Umbra instance as host
  2. render() - Called during initialization
  3. onThemeChange() - Called whenever the theme changes (including sync from other tabs)
  4. onDestroy() - Called when umbra.destroy() is invoked

Accessing methods

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);
 }
}

KeyboardShortcut Plugin

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;
 }
}

Clone this wiki locally

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /