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?) → ShortcutManagerConstruct (or replace) the process singleton — must run before <code>getShortcutManager()</code>.
getShortcutManager() → ShortcutManagerThrows if no manager was created yet — useful for modules that only consume shortcuts.

ShortcutManager (state)

PropTypeDefaultDescription
platform ShortcutPlatformResolved platform key (<code>mac</code>, <code>windows</code>, <code>linux</code>) used when parsing definitions.
isReady booleanBecomes 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) => booleanFilter to skip events. Defaults to ignoring inputs, textareas, and open modals.

ShortcutRegistration

PropTypeDefaultDescription
id requiredstringUnique identifier for the shortcut action.
label requiredstringHuman-readable label shown in settings UI.
defaults ShortcutDefinition[]Default shortcut(s). Strings like "CmdOrCtrl+S" or per-platform objects.
handler required(event) => void | booleanCalled when the shortcut fires. Return false to allow propagation.
when () => booleanGuard — 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) ShortcutRegistrationRegister a keyboard shortcut with id, label, defaults, and handler.
registerAll(regs) ShortcutRegistration[]Register multiple shortcuts at once.
unregister(id) stringRemove a shortcut registration by id.
unregisterByPrefix(prefix) stringRemove all registrations whose id starts with the given prefix.
has(id) string → booleanCheck if a shortcut id is registered.
get(id) string → ShortcutRegistration | undefinedGet a registration by id.
getShortcutsForAction(id) string → string[]Get the active shortcut strings for an action (respects overrides).
getDisplayShortcut(id) string → string | nullGet 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) asyncSet user-customized shortcut(s) for an action. Validates conflicts.
resetToDefault(id) asyncRemove the user override for an action, restoring defaults.
resetAll() asyncClear all user overrides.
hasOverride(id) string → booleanCheck if an action has a user override.
checkConflict(shortcut, excludeId?) ShortcutConflict | nullCheck if a shortcut string conflicts with existing registrations.
handleKeyDown(event) KeyboardEvent → booleanProcess a keyboard event. Returns true if a shortcut matched.
getSections() ShortcutSection[]Get all registrations grouped by section/subsection for settings UI.
init() asyncLoad persisted overrides. Call once after creation.
destroy() voidClear 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.