Phoundry UI

Keyboard Shortcuts

Headless shortcut management with registration, conflict detection, user overrides, and platform-aware display formatting.

import { createShortcutManager } from 'phoundry-ui';

Basic Registration & Display

Create a manager, register shortcuts, and display formatted key combos.

Save: S New: N

The manager listens to keyboard events via handleKeyDown(). Wire it up in your layout's onkeydown.

Show code
<script lang="ts">
  import { createShortcutManager } from 'phoundry-ui';

  const shortcuts = createShortcutManager();

  shortcuts.register({
    id: 'app.save',
    label: 'Save',
    defaults: ['CmdOrCtrl+S'],
    handler: () => console.log('Saved!'),
  });

  shortcuts.register({
    id: 'app.new',
    label: 'New Document',
    defaults: ['CmdOrCtrl+N'],
    handler: () => console.log('New document'),
    section: 'Documents',
  });

  // Display the formatted shortcut
  const display = shortcuts.getDisplayShortcut('app.save');
  // → "⌘S" on Mac, "Ctrl+S" on Windows/Linux

  await shortcuts.init();
</script>

Platform-Specific Shortcuts

Use per-platform objects for shortcuts that differ across operating systems.

Mac: Win/Linux: K
Show code
shortcuts.register({
  id: 'editor.delete-line',
  label: 'Delete Line',
  defaults: [
    { mac: 'Cmd+Backspace', default: 'Ctrl+Shift+K' }
  ],
  handler: () => deleteLine(),
});

User Overrides & Conflict Detection

Users can rebind shortcuts. The manager validates conflicts and persists overrides via callbacks.

Show code
// User wants to rebind Save to Ctrl+Shift+S
await shortcuts.setOverride('app.save', ['Ctrl+Shift+S']);

// Check for conflicts before setting
const conflict = shortcuts.checkConflict('Ctrl+N', 'app.new');
if (conflict) {
  console.warn(`Conflicts with "${conflict.existingActionLabel}"`);
}

// Reset a single action or all overrides
await shortcuts.resetToDefault('app.save');
await shortcuts.resetAll();

Standalone Utilities

Low-level utilities for parsing, formatting, and matching shortcuts outside the manager.

Show code
import { parseShortcut, formatShortcut, matchesEvent } from 'phoundry-ui';

const parsed = parseShortcut('CmdOrCtrl+Shift+P', 'mac');
// → { key: 'p', ctrl: false, meta: true, alt: false, shift: true }

const display = formatShortcut('CmdOrCtrl+S', 'mac');
// → "⌘S"

document.addEventListener('keydown', (e) => {
  if (matchesEvent(parsed, e)) {
    console.log('Matched!');
  }
});

Factory vs getter

createShortcutManager installs the singleton; subsequent modules can call getShortcutManager() without threading instances through every import.

Show code
import { createShortcutManager, getShortcutManager } from 'phoundry-ui';

// Typically once at startup:
export const shortcuts = createShortcutManager({ loadOverrides, saveOverrides });

// Elsewhere (after startup):
const same = getShortcutManager();

Construction

PropTypeDefaultDescription
createShortcutManager(options?) → ShortcutManager-Construct (or replace) the process singleton - must run before <code>getShortcutManager()</code>.
getShortcutManager() → ShortcutManager-Throws if no manager was created yet - useful for modules that only consume shortcuts.

ShortcutManager (state)

PropTypeDefaultDescription
platform ShortcutPlatform-Resolved platform key (<code>mac</code>, <code>windows</code>, <code>linux</code>) used when parsing definitions.
isReady boolean-Becomes true after <code>init()</code> resolves (safe to read shortcuts with overrides applied).

ShortcutManagerOptions

PropTypeDefaultDescription
loadOverrides () => Promise<ShortcutOverrides>-Callback to load persisted shortcut overrides at init.
saveOverrides (overrides) => Promise<void>-Callback to persist overrides after every change.
shouldIgnore (event: KeyboardEvent) => boolean-Filter to skip events. Defaults to ignoring inputs, textareas, and open modals.

ShortcutRegistration

PropTypeDefaultDescription
id requiredstring-Unique identifier for the shortcut action.
label requiredstring-Human-readable label shown in settings UI.
defaults ShortcutDefinition[]-Default shortcut(s). Strings like "CmdOrCtrl+S" or per-platform objects.
handler required(event) => void | boolean-Called when the shortcut fires. Return false to allow propagation.
when () => boolean-Guard - shortcut only fires when this returns true.
section string"General"Group in settings UI.
subsection string"General"Subgroup in settings UI.
priority number0Higher priority shortcuts are checked first.
allowDefault booleanfalseIf true, does not call preventDefault().

ShortcutManager Methods

PropTypeDefaultDescription
register(reg) ShortcutRegistration-Register a keyboard shortcut with id, label, defaults, and handler.
registerAll(regs) ShortcutRegistration[]-Register multiple shortcuts at once.
unregister(id) string-Remove a shortcut registration by id.
unregisterByPrefix(prefix) string-Remove all registrations whose id starts with the given prefix.
has(id) string → boolean-Check if a shortcut id is registered.
get(id) string → ShortcutRegistration | undefined-Get a registration by id.
getShortcutsForAction(id) string → string[]-Get the active shortcut strings for an action (respects overrides).
getDisplayShortcut(id) string → string | null-Get the formatted display string for the primary shortcut.
getDisplayShortcuts(id) string → string[]-Get formatted display strings for all shortcuts of an action.
setOverride(id, shortcuts) async-Set user-customized shortcut(s) for an action. Validates conflicts.
resetToDefault(id) async-Remove the user override for an action, restoring defaults.
resetAll() async-Clear all user overrides.
hasOverride(id) string → boolean-Check if an action has a user override.
checkConflict(shortcut, excludeId?) ShortcutConflict | null-Check if a shortcut string conflicts with existing registrations.
handleKeyDown(event) KeyboardEvent → boolean-Process a keyboard event. Returns true if a shortcut matched.
getSections() ShortcutSection[]-Get all registrations grouped by section/subsection for settings UI.
init() async-Load persisted overrides. Call once after creation.
destroy() void-Clear all registrations and caches.

Usage tips

  • Use CmdOrCtrl as a modifier prefix - it resolves to Cmd on Mac and Ctrl elsewhere.
  • Call init() after creation to load persisted overrides before registering shortcuts.
  • The default shouldIgnore skips events in inputs, textareas, contenteditable, and when a modal is open.
  • Use when guards for context-dependent shortcuts (e.g., only active when a panel is focused).
  • Up to 3 shortcut bindings per action are supported.
  • getSections() returns grouped registrations - useful for building a keyboard shortcuts settings panel.