Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .changeset/config.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"linked": [
["@keybindy/core", "@keybindy/react"]
],
"access": "public",
"baseBranch": "main",
"changelog": false,
"updateInternalDependencies": "patch",
"ignore": ["@tests/*"]
}
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
validate:
name: Test & Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.16.1

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build Packages
run: pnpm build

- name: Run Tests
run: pnpm test

- name: Run Lint
run: pnpm lint
57 changes: 57 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Release

on:
push:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

jobs:
release:
name: Release & Publish
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.16.1

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run Tests & Build
run: |
pnpm test
pnpm build

- name: Create Release PR or Publish to NPM
id: changesets
uses: changesets/action@v1
with:
publish: pnpm release
version: pnpm version
title: 'chore(release): Version Packages'
commit: 'chore(release): Version Packages'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
131 changes: 66 additions & 65 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,101 +1,102 @@
# Keybindy

**A lightweight, fast, and framework-agnostic TypeScript library for managing keyboard shortcuts in modern web applications.**
<p align="center">
<strong>The lightweight, framework-agnostic keyboard shortcut engine for modern web apps.</strong><br />
<em>Fast, tiny, type-safe, and zero dependencies.</em>
</p>

Keybindy provides a simple yet powerful API to register, manage, and scope keyboard shortcuts with a tiny footprint and zero dependencies. It is designed to be flexible, tree-shakeable, and easy to integrate into any project, from vanilla JavaScript to React.

[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
<p align="center">
<a href="https://github.com/keybindyjs/keybindy/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat&colorA=18181B&colorB=6366F1" alt="MIT License" /></a>
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-Ready-blue?style=flat&colorA=18181B&colorB=3178C6" alt="TypeScript" /></a>
<a href="https://pnpm.io/"><img src="https://img.shields.io/badge/pnpm-monorepo-orange?style=flat&colorA=18181B&colorB=F69220" alt="pnpm" /></a>
</p>

---

## About Keybindy

Keyboard shortcuts are essential for a productive user experience, but managing them across different components, contexts, and frameworks can be complex. Keybindy solves this by providing a robust, centralized, and framework-agnostic solution.
## 🎯 About Keybindy

- **Tiny & Dependency-Free**: Approximately 2KB gzipped, with no external dependencies.
- **Framework-Agnostic**: Works with Vanilla JS, React, Vue, Svelte, and any other framework.
- **Simple yet Powerful API**: Clean and intuitive methods to register, scope, and manage shortcuts.
- **Type-Safe**: Written entirely in TypeScript for a great developer experience.
- **SSR-Safe**: Designed to work flawlessly in server-side rendering environments like Next.js.
Handling keyboard shortcuts in modern web applications shouldn't require messy event listeners, broken modal state, or bulky dependencies.

## Packages
**Keybindy** is a centralized, high-performance keyboard shortcut engine (~2KB gzipped) designed to handle everything from simple hotkeys to complex Figma/Vim-style layered tools with ease:

This repository is a monorepo managed by [pnpm](https://pnpm.io/). It contains the following packages:
- **Simultaneous, Hold, & Sequential Chains**: Support for chords (`Ctrl+Shift+P`), holdable push-to-talk keys, and Vim sequences (`G` then `D`).
- **Modal Isolation & Cascading Tools**: Strict scope isolation (`default` mode) or layered priority overrides (`cascade` mode).
- **Smart Input Detection**: Automatically ignores keystrokes when typing in inputs, textareas, and contenteditables (with per-shortcut overrides).
- **Lifecycle Safety in React**: Stable ref architecture that completely eliminates hook "blinking" and stale closures.
- **Zero Dependencies & 100% Type-Safe**: Written in TypeScript with rich autocomplete.

| Package | Version | Description | Docs |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------- |
| [`@keybindy/core`](./packages/core) | [![npm version](https://badge.fury.io/js/@keybindy%2Fcore.svg)](https://www.npmjs.com/package/@keybindy/core) | The core, framework-agnostic shortcut management library. | [Docs](./packages/core/README.md) |
| [`@keybindy/react`](./packages/react) | [![npm version](https://badge.fury.io/js/@keybindy%2Freact.svg)](https://www.npmjs.com/package/@keybindy/react) | React components and hooks for seamless integration with React applications. | [Docs](./packages/react/README.md) |
---

## Installation
## 📦 Packages

Install the package you need using your preferred package manager:
| Package | Version | Description | Docs |
| :--- | :--- | :--- | :--- |
| [`@keybindy/core`](./packages/core) | [![npm version](https://img.shields.io/npm/v/@keybindy/core.svg?style=flat&colorA=18181B&colorB=3B82F6)](https://www.npmjs.com/package/@keybindy/core) | Core framework-agnostic shortcut engine. | [Core Docs](./packages/core/README.md) |
| [`@keybindy/react`](./packages/react) | [![npm version](https://img.shields.io/npm/v/@keybindy/react.svg?style=flat&colorA=18181B&colorB=3B82F6)](https://www.npmjs.com/package/@keybindy/react) | Modern React hooks & components (`useShortcut`, `useShortcuts`, `<Keybindy />`). | [React Docs](./packages/react/README.md) |

```bash
# For the core library
pnpm add @keybindy/core

# For the React integration
pnpm add @keybindy/react
```
---

## Getting Started
## 🚀 Quick Look

Here is a quick example of how to use `@keybindy/core` in a vanilla JavaScript project.
### Vanilla JS / Framework-Agnostic (`@keybindy/core`)

```ts
import ShortcutManager from '@keybindy/core';

// 1. Initialize the manager
const manager = new ShortcutManager();
const shortcuts = new ShortcutManager({ ignoreInputs: true });

// 2. Define a handler
const saveDocument = () => {
console.log('Document saved!');
};
// Register shortcut
shortcuts.register(['Ctrl', 'S'], () => {
saveDocument();
}, { preventDefault: true });

// 3. Register the shortcut
manager.register(['Ctrl', 'S'], saveDocument, {
// Optional: prevent the browser's default save action
preventDefault: true,
});
// Sequential Vim-style shortcut (G then D)
shortcuts.register(['G', 'D'], () => {
navigateToDashboard();
}, { sequential: true });
```

For framework-specific usage, please see the `README.md` file in the corresponding package directory.
### React (`@keybindy/react`)

## Development and Contributing
```tsx
import { useShortcut, useShortcuts } from '@keybindy/react';

This project is a monorepo, and we welcome contributions! To get started with local development, follow these steps:
function Canvas() {
const [scale, setScale] = useState(1);

1. **Clone the repository:**
// ⚡️ Always accesses latest state with ZERO re-registration flickering
useShortcut(['Ctrl', '+'], () => setScale(s => s + 0.1), {
preventDefault: true,
});

```bash
git clone https://github.com/keybindyjs/keybindy.git
cd keybindy
```

2. **Install dependencies:**
useShortcuts([
{ keys: ['Space'], handler: (e, state) => setPanning(state === 'down'), options: { hold: true } },
{ keys: ['Esc'], handler: deselectAll, options: { enableInInput: true } },
]);
}
```

This project uses `pnpm` for workspace management. Install it if you haven't already (`npm install -g pnpm`), then run:
---

```bash
pnpm install
```
## 🛠 Local Development & Testing

3. **Run tests:**
```bash
# 1. Clone repo
git clone https://github.com/keybindyjs/keybindy.git
cd keybindy

You can run tests for all packages or for a specific one.
# 2. Install dependencies
pnpm install

```bash
# Run all tests
pnpm test
# 3. Run all unit tests
pnpm test

# Run tests for a specific package (e.g., @keybindy/react)
pnpm --filter @keybindy/react test
```
# 4. Build packages
pnpm build
```

We encourage you to open an issue or a pull request if you have ideas for improvement or have found a bug.
---

## License
## 📄 License

This project is licensed under the MIT License. See the [LICENSE](./LICENSE.md) file for details.
MIT © [Keybindy Contributors](https://github.com/keybindyjs/keybindy)
18 changes: 18 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# @keybindy/core

## [2.0.0] - 2026-08-30

### Major Features & Improvements

- **Cascading Scope Mode & Priority Weights**:
- Added `scopeMode: 'default' | 'cascade'`. In `'default'` mode, only the topmost active scope is enabled (ideal for strict modal isolation). In `'cascade'` mode, all active scopes work together.
- Added dynamic numeric priority weighting (`setScopePriority(scope, priority)`, `removeScopePriority(scope)`). Common key collisions are resolved by priority weight (e.g. `100 > 10 > 0`) or stack order, while non-colliding parent shortcuts continue executing.
- **Guard & Interceptor Middlewares**:
- Added `beforeEach((shortcut, event) => boolean | void, options?)` guard hook. Returning `false` safely cancels shortcut execution.
- Added `afterEach((shortcut, event) => void, options?)` interceptor hook for analytics, telemetry, and side-effects.
- **Smart Input Target Handling**:
- Added automatic ignoring of keystrokes when typing inside `<input>`, `<textarea>`, `<select>`, and `[contenteditable="true"]` via global `ignoreInputs: true` and per-shortcut `ignoreInputs: true`.
- Added `enableInInput: true` option to allow specific shortcuts (like `Esc`) to trigger even while focused on an input.
- **Forgiving Cross-Platform Aliases**:
- Expanded key alias normalization to seamlessly recognize `Cmd`, `Command`, `Meta`, `Option`, `Alt`, `Return`, `Enter`, `Esc`, and `Escape`.
- **Enhanced Types & TypeScript DX**:
- Introduced `Key` and `ShortcutBinding = (Key | Key[])[]` types, completely eliminating TypeScript union array inference bugs (`never[]`).

### [1.1.7] - 2025-08-08

### Changes
Expand Down
Loading
Loading