# React KeyHub

A lightweight, scalable keyboard shortcut manager for React applications with TypeScript support.

[![npm version](https://img.shields.io/npm/v/react-keyhub.svg)](https://www.npmjs.com/package/react-keyhub)
[![npm downloads](https://img.shields.io/npm/dm/react-keyhub.svg)](https://www.npmjs.com/package/react-keyhub)
[![license](https://img.shields.io/npm/l/react-keyhub.svg)](https://github.com/xenral/react-keyhub/blob/main/LICENSE)
[![CI](https://github.com/xenral/react-keyhub/actions/workflows/ci.yml/badge.svg)](https://github.com/xenral/react-keyhub/actions/workflows/ci.yml)

## Demo 

- 🔗 **Live Shortcut Sheet Demo**: [KeyHub Example App](https://xenral.github.io/keyhub-example)
- 💻 **Example Source Code**: [GitHub Repository](https://github.com/xenral/keyhub-example)

## Features

- 🔑 **Central Configuration**: Define all keyboard shortcuts in one place
- 🔄 **Type Safety**: Full TypeScript support for shortcut definitions and hooks
- 🎯 **Optimized Performance**: Single event listener with efficient lookup
- 🧩 **Modular API**: Subscribe to shortcuts from any component
- 📋 **Built-in Shortcut Sheet**: Display all registered shortcuts in a user-friendly format
- 🔌 **Zero Dependencies**: No external dependencies (aside from React)
- 🔄 **Dynamic Updates**: Enable, disable, or modify shortcuts at runtime
- 🌐 **Context Awareness**: Define shortcuts that only work in specific contexts
- 🔢 **Sequence Support**: Create shortcuts that require a sequence of key presses
- 🎨 **Theming Support**: Light, dark, and auto themes for the shortcut sheet
- 📱 **Responsive Layouts**: Modal and sidebar layouts for the shortcut sheet
- 💡 **Type Suggestions**: Enhanced hooks with autocomplete for registered shortcuts

## Installation

```bash
npm install react-keyhub
# or
yarn add react-keyhub
```

## Quick Start

```tsx
import React, { useState } from 'react';
import { 
  KeyHubProvider, 
  useShortcut,
  useKeyboardShortcut,
  ShortcutSheet,
  defaultShortcuts 
} from 'react-keyhub';

// Define your shortcuts (or use the default ones)
const myShortcuts = {
  ...defaultShortcuts,
  customAction: {
    keyCombo: 'ctrl+k',
    name: 'Custom Action',
    description: 'Perform a custom action',
    scope: 'global',
    priority: 100,
    status: 'enabled',
    group: 'Custom',
    type: 'regular'
  },
};

// Your app component
function App() {
  const [isShortcutSheetOpen, setShortcutSheetOpen] = useState(false);
  
  // Use the enhanced hook with type suggestions
  const isSaveRegistered = useShortcut('save', (e) => {
    console.log('Save triggered!');
    // Your save logic here
  });
  
  // You can also use the backward compatibility hooks
  useKeyboardShortcut('customAction', (e) => {
    console.log('Custom action triggered!');
    // Your custom action logic here
  });
  
  // Toggle the shortcut sheet
  useShortcut('showShortcuts', () => {
    setShortcutSheetOpen(prev => !prev);
  });
  
  return (
    <div>
      <h1>My App</h1>
      <p>Press Ctrl+/ to see all shortcuts</p>
      <p>Save shortcut registered: {isSaveRegistered ? 'Yes' : 'No'}</p>
      
      {/* Shortcut Sheet */}
      <ShortcutSheet 
        isOpen={isShortcutSheetOpen} 
        onClose={() => setShortcutSheetOpen(false)} 
      />
    </div>
  );
}

// Wrap your app with the provider
function Root() {
  return (
    <KeyHubProvider shortcuts={myShortcuts}>
      <App />
    </KeyHubProvider>
  );
}

export default Root;
```

## API Reference

### `KeyHubProvider`

The provider component that makes shortcuts available throughout your application.

```tsx
<KeyHubProvider 
  shortcuts={myShortcuts} 
  options={{ 
    preventDefault: true,
    stopPropagation: true,
    debounceTime: 0,
    sequenceTimeout: 1000,
    ignoreInputFields: true,
    ignoreModifierOnlyEvents: true
  }}
>
  {children}
</KeyHubProvider>
```

#### Props

- `shortcuts`: A record of shortcut configurations
- `options` (optional):
  - `preventDefault`: Whether to prevent the default browser behavior (default: `true`)
  - `stopPropagation`: Whether to stop event propagation (default: `true`)
  - `target`: The element to attach the event listener to (default: `document`)
  - `debounceTime`: Debounce time in milliseconds (default: `0`)
  - `sequenceTimeout`: Timeout for sequence shortcuts in milliseconds (default: `1000`)
  - `ignoreInputFields`: Whether to ignore keyboard events from input fields (default: `true`)
  - `ignoreModifierOnlyEvents`: Whether to ignore keyboard events that only contain modifier keys (default: `true`)

### `useShortcut`

A hook to subscribe to a keyboard shortcut with type suggestions.

```tsx
// The shortcutId will have type suggestions for all registered shortcuts
// TypeScript will show an error for non-existent shortcuts
const isSaveRegistered = useShortcut('save', (e) => {
  console.log('Save shortcut triggered!');
  // Your save logic here
});

// The hook returns a boolean indicating if the shortcut is registered
console.log('Is save shortcut registered?', isSaveRegistered);
```

#### Parameters

- `shortcutId`: The ID of the shortcut to subscribe to (with type suggestions based on provider shortcuts)
- `callback`: The callback to execute when the shortcut is triggered

#### Return Value

- `boolean`: Indicates if the shortcut is registered

#### Type Safety

The hook uses the actual shortcuts provided to the KeyHubProvider for type checking:

```tsx
import { 
  ShortcutScope, 
  ShortcutStatus, 
  ShortcutType, 
  ShortcutSettings 
} from 'react-keyhub';

// Define custom shortcuts
const myShortcuts = {
  ...defaultShortcuts,
  customAction: {
    keyCombo: 'ctrl+shift+c',
    name: 'Custom Action',
    description: 'A custom action shortcut',
    scope: ShortcutScope.GLOBAL,
    priority: 100,
    status: ShortcutStatus.ENABLED,
    group: 'Custom',
    type: ShortcutType.REGULAR
  }
} as ShortcutSettings;

// In your component
function MyComponent() {
  // This will work fine
  useShortcut('customAction', () => {});
  
  // This will cause a TypeScript error
  useShortcut('nonExistentShortcut', () => {});
}
```

#### Error Handling

The hook checks if the shortcut is registered and provides a warning if it's not:

```tsx
// This will log a warning if 'nonExistentShortcut' is not registered
useShortcut('nonExistentShortcut', (e) => {});
// Warning: Shortcut "nonExistentShortcut" is not registered. Available shortcuts: save, saveAs, print, ...
```

### `useKeyboardShortcut` and `useKey`

For backward compatibility, `useKeyboardShortcut` and `useKey` are also available as aliases for `useShortcut`:

```tsx
// These are all equivalent
useShortcut('save', callback);
useKeyboardShortcut('save', callback);
useKey('save', callback);
```

### `AvailableShortcuts`

A type that provides suggestions for all registered shortcuts based on what's provided to the KeyHubProvider:

```tsx
import { AvailableShortcuts } from 'react-keyhub';

// This will have type suggestions for all registered shortcuts
// based on what's provided to the KeyHubProvider
const shortcutId: AvailableShortcuts = 'save';

// If you've added a custom shortcut, it will be included in the suggestions
const customShortcutId: AvailableShortcuts = 'customAction'; // Works if customAction is registered
```

### `getRegisteredShortcuts`

A function to get all registered shortcuts from the current provider:

```tsx
import { getRegisteredShortcuts } from 'react-keyhub';

function MyComponent() {
  // This will return the shortcuts from the current provider
  const shortcuts = getRegisteredShortcuts();
  
  return (
    <div>
      <h2>Registered Shortcuts</h2>
      <ul>
        {Object.entries(shortcuts).map(([id, config]) => (
          <li key={id}>
            {id}: {config.name} - {config.type === 'regular' ? config.keyCombo : config.sequence}
          </li>
        ))}
      </ul>
    </div>
  );
}
```

### `useShortcutSheet`

A hook to get all registered shortcuts.

```tsx
const shortcuts = useShortcutSheet();
```

### `useShortcutStatus`

A hook to enable or disable a shortcut.

```tsx
useShortcutStatus('save', true); // Enable the "save" shortcut
useShortcutStatus('save', false); // Disable the "save" shortcut
```

### `useShortcutUpdate`

A hook to update a shortcut configuration.

```tsx
useShortcutUpdate('save', {
  keyCombo: 'ctrl+shift+s',
  priority: 200,
});
```

### `useShortcutRegister`

A hook to register a new shortcut dynamically.

```tsx
useShortcutRegister('myDynamicShortcut', {
  keyCombo: 'ctrl+d',
  name: 'Dynamic Shortcut',
  description: 'A dynamically registered shortcut',
  scope: 'global',
  priority: 100,
  status: 'enabled',
  group: 'Dynamic',
  type: 'regular'
});
```

### `useShortcutContext`

A hook to set the active context.

```tsx
useShortcutContext('editor'); // Set the active context to "editor"
useShortcutContext(null); // Clear the active context
```

### `useShortcutPause`

A hook to pause and resume the event bus.

```tsx
useShortcutPause(true); // Pause all shortcuts
useShortcutPause(false); // Resume all shortcuts
```

### `useShortcutGroups`

A hook to get all shortcut groups.

```tsx
const groups = useShortcutGroups(); // Returns an array of group names
```

### `useShortcutsByGroup`

A hook to get shortcuts by group.

```tsx
const fileShortcuts = useShortcutsByGroup('File'); // Returns all shortcuts in the "File" group
```

### `useKeyHub`

A hook to access the KeyHub event bus directly.

```tsx
const eventBus = useKeyHub();

// Now you can use the event bus methods
eventBus.on('save', callback);
eventBus.off(subscriptionId);
eventBus.enableShortcut('save');
eventBus.disableShortcut('save');
eventBus.updateShortcut('save', { priority: 200 });
eventBus.registerShortcut('myShortcut', { ... });
eventBus.unregisterShortcut('myShortcut');
eventBus.setContext('editor');
eventBus.getContext();
eventBus.pause();
eventBus.resume();
eventBus.getShortcuts();
eventBus.getShortcutsByGroup('File');
eventBus.getShortcutGroups();
```

### `ShortcutSheet`

A component to display all registered shortcuts.

```tsx
<ShortcutSheet 
  isOpen={isOpen} 
  onClose={handleClose} 
  theme="light" // 'light', 'dark', or 'auto'
  layout="modal" // 'modal' or 'sidebar'
  filter={{ 
    scope: 'global',
    search: 'save',
    group: 'File',
    context: 'editor'
  }}
/>
```

#### Props

- `isOpen`: Whether to show the shortcut sheet
- `onClose`: Callback to close the shortcut sheet
- `theme` (optional): Theme for the shortcut sheet (`'light'`, `'dark'`, or `'auto'`)
- `layout` (optional): Layout for the shortcut sheet (`'modal'` or `'sidebar'`)
- `filter` (optional): Filter for the shortcuts to display
  - `scope` (optional): Filter by scope (`'global'` or `'local'`)
  - `search` (optional): Filter by search term
  - `group` (optional): Filter by group
  - `context` (optional): Filter by context
- `className` (optional): Custom class name for the shortcut sheet

### `ShortcutSheetStyles`

A string of CSS styles for the ShortcutSheet component.

```tsx
import { ShortcutSheetStyles } from 'react-keyhub';

// Add the styles to your app
const App = () => (
  <>
    <style>{ShortcutSheetStyles}</style>
    {/* Your app content */}
  </>
);
```

## Shortcut Configuration

Each shortcut is defined with the following properties:

### Regular Shortcut

```tsx
{
  keyCombo: 'ctrl+s',
  name: 'Save',
  description: 'Save the current document',
  scope: 'global',
  priority: 100,
  status: 'enabled',
  group: 'File',
  context: 'editor', // Optional
  type: 'regular',
  action: (e) => { /* Optional default action */ }
}
```

### Sequence Shortcut

```tsx
{
  sequence: 'g c', // 'g' followed by 'c'
  name: 'Git Commands',
  description: 'Show git commands menu',
  scope: 'global',
  priority: 100,
  status: 'enabled',
  group: 'Git',
  context: 'editor', // Optional
  type: 'sequence',
  action: (e) => { /* Optional default action */ }
}
```

### Properties

- `keyCombo` (for regular shortcuts): The key combination (e.g., `'ctrl+s'`, `'ctrl+shift+n'`)
- `sequence` (for sequence shortcuts): The sequence of key combinations (e.g., `'g c'` for "g" followed by "c")
- `name`: A human-readable name for the shortcut
- `description`: A detailed description of what the shortcut does
- `scope`: Either `'global'` or `'local'`
- `priority`: The priority of the shortcut (higher numbers take precedence)
- `status`: Either `'enabled'` or `'disabled'`
- `group`: A group for the shortcut (used for organizing in the shortcut sheet)
- `context` (optional): A context for the shortcut (only active when the context matches)
- `type`: Either `'regular'` or `'sequence'`
- `action` (optional): A default action to execute when the shortcut is triggered

## Default Shortcuts

React KeyHub comes with a set of default shortcuts organized by groups:

### File Operations
- `save`: Ctrl+S
- `saveAs`: Ctrl+Shift+S
- `print`: Ctrl+P
- `newWindow`: Ctrl+Shift+N

### Edit Operations
- `find`: Ctrl+F
- `replace`: Ctrl+H
- `undo`: Ctrl+Z
- `redo`: Ctrl+Y
- `cut`: Ctrl+X
- `copy`: Ctrl+C
- `paste`: Ctrl+V
- `selectAll`: Ctrl+A

### Navigation
- `goToLine`: Ctrl+G
- `goToFile`: Ctrl+P (lower priority than print)

### Help
- `help`: F1
- `showShortcuts`: Ctrl+/

### Git (Sequence Shortcuts)
- `gitCommands`: g c (press "g" then "c")
- `gitStatus`: g s (press "g" then "s")

### Vim Navigation (Context-Specific)
- `vimUp`: k (only active in "vim" context)
- `vimDown`: j (only active in "vim" context)
- `vimLeft`: h (only active in "vim" context)
- `vimRight`: l (only active in "vim" context)

You can use these as a starting point and override or extend them as needed.

## Advanced Usage

### Context-Aware Shortcuts

```tsx
// Define shortcuts with contexts
const myShortcuts = {
  ...defaultShortcuts,
  editorSave: {
    keyCombo: 'ctrl+s',
    name: 'Save',
    description: 'Save the current document',
    scope: 'global',
    priority: 100,
    status: 'enabled',
    group: 'File',
    context: 'editor', // Only active in "editor" context
    type: 'regular'
  },
  terminalClear: {
    keyCombo: 'ctrl+l',
    name: 'Clear Terminal',
    description: 'Clear the terminal',
    scope: 'global',
    priority: 100,
    status: 'enabled',
    group: 'Terminal',
    context: 'terminal', // Only active in "terminal" context
    type: 'regular'
  }
};

// In your component, set the active context
function EditorComponent() {
  // Set the active context to "editor"
  useShortcutContext('editor');
  
  // ...
}

function TerminalComponent() {
  // Set the active context to "terminal"
  useShortcutContext('terminal');
  
  // ...
}
```

### Sequence Shortcuts

```tsx
// Define sequence shortcuts
const myShortcuts = {
  ...defaultShortcuts,
  gitCommit: {
    sequence: 'g c', // Press "g" then "c"
    name: 'Git Commit',
    description: 'Open git commit dialog',
    scope: 'global',
    priority: 100,
    status: 'enabled',
    group: 'Git',
    type: 'sequence'
  }
};

// Subscribe to the sequence shortcut
function MyComponent() {
  useShortcut('gitCommit', () => {
    console.log('Git commit dialog opened');
  });
  
  // ...
}
```

### Dynamic Shortcut Registration

```tsx
function MyComponent() {
  // Register a dynamic shortcut
  useShortcutRegister('dynamicShortcut', {
    keyCombo: 'ctrl+d',
    name: 'Dynamic Shortcut',
    description: 'A dynamically registered shortcut',
    scope: 'global',
    priority: 100,
    status: 'enabled',
    group: 'Dynamic',
    type: 'regular',
    action: () => {
      console.log('Dynamic shortcut triggered!');
    }
  });
  
  // The shortcut will be automatically unregistered when the component unmounts
  
  // ...
}
```

### Pausing and Resuming Shortcuts

```tsx
function MyComponent() {
  const [isPaused, setIsPaused] = useState(false);
  
  // Pause or resume all shortcuts
  useShortcutPause(isPaused);
  
  return (
    <div>
      <button onClick={() => setIsPaused(!isPaused)}>
        {isPaused ? 'Resume Shortcuts' : 'Pause Shortcuts'}
      </button>
    </div>
  );
}
```

### Themed Shortcut Sheet

```tsx
function MyComponent() {
  const [isOpen, setIsOpen] = useState(false);
  const [theme, setTheme] = useState<'light' | 'dark' | 'auto'>('auto');
  
  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Show Shortcuts</button>
      
      <select value={theme} onChange={(e) => setTheme(e.target.value as any)}>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
        <option value="auto">Auto (System)</option>
      </select>
      
      <ShortcutSheet 
        isOpen={isOpen} 
        onClose={() => setIsOpen(false)} 
        theme={theme}
      />
    </div>
  );
}
```

## Browser Support

React KeyHub works in all modern browsers that support React.

## License

MIT 