@gentleduck/vim
v0.1.12
Published
A package for creating variants of components, providing a simple and efficient way to create variants of components.
Readme
@gentleduck/vim
A tiny, framework‑agnostic keyboard command engine with optional React bindings. Think “vim‑style” key sequences like g+d, plus single‑key combos such as ctrl+shift+k — all with a simple, strongly‑typed API.
- Core is DOM‑only and framework‑agnostic (
src/command/). - Optional React bindings (
src/react/) provide a provider + hook for ergonomic usage. - Supports multi‑step sequences with timeout and prefix matching.
- Written in TypeScript and ships types.
Table of contents
- Why this exists
- Installation
- Quick start (vanilla)
- Quick start (React)
- Concepts
- API (core)
- API (React bindings)
- Advanced usage
- Accessibility & UX
- Testing
- Limitations
- Roadmap
- License
Why this exists
Many apps need a small, predictable keyboard command system without bringing a large dependency. This project provides a tiny, strongly‑typed engine that:
- Works in any web framework (only depends on the DOM)
- Supports multi‑step sequences (e.g.
g+d) - Gives simple React ergonomics when you want them
Installation
npm install @gentleduck/vim// ESM (core)
import { Registry, KeyHandler, type Command } from '@gentleduck/vim/command'
// React bindings
import { KeyProvider, useKeyCommands } from '@gentleduck/vim/react'If consuming from source in a monorepo, import via your configured workspace alias or relative path.
Quick start (vanilla TS/JS)
import { Registry, KeyHandler, type Command } from '@gentleduck/vim/command'
const registry = new Registry(true) // enable debug logs
const handler = new KeyHandler(registry, 600)
const openPalette: Command = {
name: 'Open Command Palette',
execute: () => console.log('palette!'),
}
const goDashboard: Command = {
name: 'Go Dashboard',
execute: () => console.log('navigate to /dashboard'),
}
registry.register('ctrl+k', openPalette)
registry.register('g+d', goDashboard) // press `g`, then `d`
handler.attach(document)
// later: handler.detach(document)Quick start (React)
import React from 'react'
import { KeyProvider, useKeyCommands } from '@gentleduck/vim/react'
function App() {
useKeyCommands({
'g+d': { name: 'Go Dashboard', execute: () => console.log('dash') },
'ctrl+k': { name: 'Open Palette', execute: () => console.log('palette') },
})
return <div>Press g then d, or Ctrl+K</div>
}
export default function Root() {
return (
<KeyProvider debug timeoutMs={600}>
<App />
</KeyProvider>
)
}Concepts
- Key descriptor — built from a
KeyboardEventasctrl?+alt?+meta?+shift?+key. Always normalized to lowercase. Aliases:' '→space,escape→esc,control→ctrl. - Sequence — concatenation of descriptors separated by
+between steps (e.g.g+d). Each step may include modifiers:ctrl+shift+k. - Prefixes — every registered sequence contributes progressive prefixes. Registering
g+dwill markgas a valid prefix while waiting for completion. - Timeout — when a prefix is active, the internal sequence resets after
timeoutMsunless the sequence completes.
API (core)
interface Command
interface Command {
name: string
description?: string
execute: <T>(args?: T) => void | Promise<void>
}class Registry
constructor(debug?: boolean)
register(key: string, cmd: Command): void
hasCommand(key: string): boolean
getCommand(key: string): Command | undefined
isPrefix(key: string): booleanRegistry holds the mapping from sequence strings to Command and tracks prefixes for multi‑step sequences.
class KeyHandler
constructor(registry: Registry, timeoutMs = 600)
attach(target: HTMLElement | Document = document): void
detach(target: HTMLElement | Document = document): void- Ignores pure modifier presses (
Shift,Control,Alt,Meta). - Matching strategy: try full sequence → if prefix, wait → otherwise retry last descriptor → reset.
API (React bindings)
KeyProvider— mounts aRegistryandKeyHandler, attaches on mount, detaches on unmount.- Props:
{ debug?: boolean; timeoutMs?: number; children: ReactNode }
- Props:
useKeyCommands(commands: Record<string, Command>)— registers a set of key→command mappings using the provider'sregistry. Must be used insideKeyProvider.KeyContext— advanced: exposes{ registry: Registry; handler: KeyHandler }for programmatic usage.
Advanced usage
- Scoped listeners — call
handler.attach(element)to scope keyboard handling to a specific DOM subtree. - Multiple registries — create separate
Registry/KeyHandlerinstances per feature for isolation. - Programmatic execution —
registry.getCommand('ctrl+k')?.execute(). - Debugging — enable
debugto log state transitions and matches.
Accessibility & UX
- Avoid shadowing essential browser shortcuts unless necessary.
- Provide discoverability (help modal or command palette listing registered shortcuts).
- Offer alternative mouse/UI paths for critical actions.
- Test on various keyboard layouts and IME-enabled environments —
e.keycan vary by layout.
Testing
The repo uses vitest + JSDOM. Test Registry directly for DOM‑less behavior. For KeyHandler, dispatch keydown events on a JSDOM document or element to simulate user input.
Limitations
- No built‑in
unregisterAPI yet — consumers must manage cleanup or reinstantiate registries. preventDefault/stopPropagationare not applied automatically — left to integrators for fine control.- Timing‑based sequences (
timeoutMs) can conflict with IME and accessibility tools. TweaktimeoutMsas needed.
