diff --git a/.changeset/config.json b/.changeset/config.json index 8bc7e8a..24b29b5 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -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/*"] } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1786004 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c088622 --- /dev/null +++ b/.github/workflows/release.yml @@ -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 }} diff --git a/README.md b/README.md index 007e7ee..bcb6cda 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,102 @@ # Keybindy -**A lightweight, fast, and framework-agnostic TypeScript library for managing keyboard shortcuts in modern web applications.** +

+ The lightweight, framework-agnostic keyboard shortcut engine for modern web apps.
+ Fast, tiny, type-safe, and zero dependencies. +

-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) +

+ MIT License + TypeScript + pnpm +

--- -## 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`, ``). | [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) diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 10358fe..32f904c 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -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 ``, ` + diff --git a/tests/sample-project/src/main.js b/tests/sample-project/src/main.js index 5b3c16a..0cb320d 100644 --- a/tests/sample-project/src/main.js +++ b/tests/sample-project/src/main.js @@ -17,7 +17,7 @@ manager.register( manager.register( ['D'], - e => { + () => { console.log('D key released!'); }, { @@ -27,7 +27,7 @@ manager.register( manager.register( ['D'], - e => { + () => { console.log('D key pressed!'); }, { @@ -82,7 +82,35 @@ manager.register( scope: 'lg', } ); +manager.register( + ['l', 'g'], + () => { + console.log('Sequential LG shortcut triggered from default!'); + }, + { + sequential: true, + sequenceDelay: 3000, + } +); + +manager.register( + ['Ctrl', 'C'], + () => { + console.log('CTRL+C shortcut triggered!'); + }, + { + ignoreInputs: true, + } +); + +// manager.setActiveScope('nono'); -manager.setActiveScope('global'); // manager.disableAll(); // Get all methods (including inherited ones) +manager.setScopeMode('cascade'); +manager.setScopePriority(scope => ({ ...scope, global: 10 })); +// manager.setScopePriority(scope => ({ ...scope, global: 10 })); +// console.log(manager.getScopesInfo()); +// console.log(manager.getScopePriority()); + +console.log(manager.getScopePriority()); diff --git a/tests/sample-react/src/App.jsx b/tests/sample-react/src/App.jsx index ffd9a03..461d537 100644 --- a/tests/sample-react/src/App.jsx +++ b/tests/sample-react/src/App.jsx @@ -1,16 +1,14 @@ import { useEffect, useState } from 'react'; -import { Keybindy, ShortcutLabel, useKeybindy } from '@keybindy/react'; +import { Keybindy, useKeybindy } from '@keybindy/react'; function App() { const [isOpen, setIsOpen] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); const s = useKeybindy(); useEffect(() => { - s.register(['Enter'], () => { - console.log('Enter key pressed!'); - }); - console.log(s.getCheatSheet()) - }, [s]); + console.log(s.getScopePriority()) + }, [s]) return ( @@ -63,6 +61,11 @@ function App() { variable++; console.log('Z pressed:', variable); }, + options: { + repeat: true, + // ignoreInputs: true + + } }, { keys: ['X'], @@ -74,22 +77,6 @@ function App() { ]; }}> -
- { - // console.log(keys) - // return keys.map((key) => ( - // - // {key} - // - // )); - // }} - /> -

Vite + React

{isOpen && ( @@ -128,9 +115,24 @@ function App() { Edit src/App.jsx and save to test HMR

Click on the Vite and React logos to learn more

+ + + + {isModalOpen && } + +
); } export default App; + +const Modal = ({ setIsModalOpen }) => { + return ( { console.log("x pressed from Modal") } }]}> +
+

Open

+ +
+
) +} \ No newline at end of file