@clinth/ui-commands
v1.1.0
Published
A general-purpose library for managing UI commands, keyboard shortcuts, MIDI input, and tooltips.
Readme
@clinth/ui-commands
A general-purpose library for managing UI commands, keyboard shortcuts, MIDI input, and tooltips.
Installation
npm install @clinth/ui-commandsQuick Start
import {
CommandRegistry,
createCommandRegistry,
KeyboardManager,
loadKeyBindings,
InputManager,
createInputManager,
UiInputManager,
TooltipManager,
createTooltipManager,
type KeyBindingConfig,
} from '@clinth/ui-commands';
// 1. Create a command registry
interface AppState {
isPlaying: boolean;
}
const registry = createCommandRegistry<AppState>({
onInvoke: (id, args) => console.log(`Command invoked: ${id}`, args),
onCommandNotFound: (id) => console.warn(`Unknown command: ${id}`),
});
// 2. Register commands
registry.register({
id: 'play-stop',
label: 'Play/Stop',
description: 'Toggle playback',
execute: (args) => console.log('Toggling playback', args?.context),
});
registry.register({
id: 'zoom-in',
label: 'Zoom In',
description: 'Increase zoom level',
enabledWhen: (state) => !state.isPlaying,
execute: () => console.log('Zooming in'),
});
// 3. Create keyboard manager and load bindings
type AppMode = 'detail' | 'coarse';
const keyboard = new KeyboardManager<AppMode>();
const bindings: KeyBindingConfig<AppMode>[] = [
{ key: ' ', mode: 'all', command: 'play-stop', description: 'Play/Stop' },
{
key: '=',
mode: 'detail',
command: 'zoom-in',
description: 'Zoom in',
nonExclusive: true,
commandContext: 'panel/detail',
},
{
key: '=',
mode: 'detail',
command: 'zoom-in',
description: 'Zoom in (list)',
nonExclusive: true,
commandContext: 'panel/list*',
},
];
loadKeyBindings(keyboard, bindings);
// 4. Create input manager and initialize
const input = createInputManager(registry, keyboard);
input.setModeGetter(() => 'detail' as AppMode);
input.init();
// 5. Optionally, initialize UI input manager for button clicks
const uiInput = new UiInputManager({ parentContainer: document.body }, input);
uiInput.init();
// 6. Add tooltips for keyboard shortcuts
const tooltip = createTooltipManager(document.body, {
getBindings: () => bindings,
getMode: () => 'detail' as AppMode,
});
tooltip.init();Core Concepts
CommandRegistry
Manages registration and invocation of commands.
const registry = createCommandRegistry({
onInvoke: (id, args) => { /* called on successful command */ },
onInvokeError: (id, error, stage) => { /* called when command throws */ },
onCommandNotFound: (id) => { /* called when command not found */ },
});
registry.register({
id: 'my-command',
label: 'My Command',
description: 'Does something',
execute: (args) => { /* ... */ },
});
// Listen for command invocations
const unsubscribe = registry.addOnInvokeHandler((id, args) => {
console.log(`Command ${id} was invoked`);
});
registry.invoke('my-command', { foo: 'bar' });
unsubscribe(); // Stop listeningCommand invocations from input managers include a context payload with the input source and focused element.
registry.register({
id: 'my-command',
label: 'My Command',
description: 'Does something',
execute: (args) => {
console.log(args?.context?.source);
console.log(args?.context?.commandContext);
console.log(args?.context?.element);
},
});KeyboardManager
Handles keyboard event binding and matching.
Bindings are exclusive by default. Registering a binding with the same key, modifiers, and mode
throws unless nonExclusive: true is set.
When multiple bindings match a gesture, any binding whose commandContext matches the input
context wins. Use prefix* to match by prefix.
const keyboard = new KeyboardManager<AppMode>();
keyboard.bind({
key: 'a',
modifiers: new Set(['ctrl']),
mode: 'detail',
nonExclusive: true,
commandContext: 'panel/*',
commandId: 'my-command',
});
// Get all current bindings
const bindings = keyboard.getBindings();
// Enable/disable keyboard handling
keyboard.setActive(false);InputManager
Coordinates between command registry and keyboard, plus handles button bindings.
const input = createInputManager(registry, keyboard);
// Bind keys directly
input.bindKey('Enter', [], 'my-command', 'detail');
// Bind a button element to a command
const button = document.getElementById('my-button');
input.bindButton(button, 'my-command');
// Set the current app mode
input.setModeGetter(() => currentMode);
// Access the registry
const reg = input.getRegistry();UiInputManager
Automatically binds buttons with data-command attributes.
<button data-command="play-stop">Play</button>
<button data-command="zoom-in" data-command-context="panel/list" data-args='{"level": 2}'>Zoom</button>const uiInput = new UiInputManager({ parentContainer: document.body }, input);
uiInput.init();Context selection favors bindings that match commandContext. Wildcards are supported
using a trailing * to match prefixes.
TooltipManager
Shows keyboard shortcut tooltips on hover.
const tooltip = createTooltipManager(document.body, {
getBindings: () => keyboard.getBindings().map(b => ({
key: b.key,
mode: b.mode,
command: b.commandId,
description: '...',
})),
getMode: () => currentMode,
showDelayMs: 500,
dismissDelayMs: 3000,
});
tooltip.init();MIDI Support
import { MidiManager, midiManager, loadMidiBindings, type MidiBindingConfig } from '@clinth/ui-commands';
const midi = new MidiManager();
if (midi.isSupported()) {
await midi.init();
loadMidiBindings([
{ channel: 1, control: 1, command: 'play-stop', description: 'Play/Stop' },
]);
}API Reference
CommandRegistry
register(command)- Register a command (throws if duplicate)unregister(id)- Unregister a command, returns booleaninvoke(id, args?)- Invoke a command, returns booleanaddOnInvokeHandler(handler)- Add listener for invocations, returns unsubscribeget(id)- Get command by IDgetAll()- Get all registered commandsgetEnabled(state)- Get commands enabled for given statehas(id)- Check if command exists
KeyboardManager
bind(binding)- Add a key bindingunbind(key, modifiers)- Remove a key bindingsetActive(active)- Enable/disable keyboard handlingsetModeGetter(getter)- Set function that returns current modeonKeyEvent(handler)- Add handler for key events, returns unsubscribeattach(element)- Attach keyboard events to elementdetach()- Remove keyboard eventsgetBindings()- Get all bindings
InputManager
init()- Initialize and attach keyboard to document.bodydestroy()- CleanupbindKey(key, modifiers, commandId, mode?)- Bind a keybindButton(element, commandId)- Bind a button clickunbindButton(element)- Remove button bindingsetActive(active)- Enable/disable input handlingsetModeGetter(getter)- Set function that returns current modegetRegistry()- Access the command registry
UiInputManager
init()- Initialize and start listening for button clicksdestroy()- Stop listening for button clicks
TooltipManager
init()- Initialize tooltipsdestroy()- Cleanup
MidiManager
init()- Initialize MIDI accessdestroy()- CleanupisSupported()- Check if Web MIDI is supported
loadKeyBindings(keyboard, bindings)
Load an array of KeyBindingConfig into a KeyboardManager.
loadMidiBindings(bindings)
Load MIDI bindings (future implementation).
Types
Command
{
id: CommandId;
label: string;
icon?: string;
description: string;
enabledWhen?: (state: TState) => boolean;
execute: (args?: Record<string, unknown>) => void;
}KeyBindingConfig
{
key: string;
modifiers?: Array<'ctrl' | 'shift' | 'alt' | 'meta'>;
mode?: TAppModes | 'all';
nonExclusive?: boolean;
commandContext?: string;
command: string;
description: string;
}MidiBindingConfig
{
channel: number;
control?: number;
note?: number;
command: string;
description: string;
}TooltipOptions
{
getBindings: () => Array<KeyBindingConfig<TAppModes>>;
getMode: () => TAppModes;
showDelayMs?: number;
dismissDelayMs?: number;
platform?: 'mac' | 'win';
}UiInputOptions
{
parentContainer: HTMLElement;
bindButtonClicks?: boolean;
onUnboundButton?: (button: HTMLButtonElement) => void;
onUnboundCommandId?: (id: CommandId, button: HTMLButtonElement) => void;
}Utilities
formatKeyboardBinding(platform, key, modifiers?)
Formats a keyboard binding for display. Returns strings like "Cmd + A" or "Ctrl + Shift + B".
detectPlatform()
Returns 'mac' or 'win' based on navigator.platform.
Type Aliases
Platform-'mac' | 'win'ModifierKey-'ctrl' | 'shift' | 'alt' | 'meta'Modifiers-{ ctrl: boolean; shift: boolean; alt: boolean; meta: boolean; }KeyBinding<TAppModes>-{ key: string; modifiers: Set<ModifierKey>; mode?: TAppModes | 'all'; nonExclusive?: boolean; commandContext?: string; commandId: CommandId; }KeyEventHandler-(commandId: CommandId, modifiers: Modifiers, context: InputContext) => voidInputContext-{ source: 'keyboard' | 'pointer' | 'midi'; element: HTMLElement | null; commandContext?: string }
