From ad6b20658bfb2b8c66f1d4759c716149acaf87d6 Mon Sep 17 00:00:00 2001 From: Dhruv Suthar Date: Thu, 12 Nov 2026 17:17:08 +0530 Subject: [PATCH 1/2] chore: trim redundant comments and overlapping tests Drop comments that restate the code and merge test pairs that covered the same behavior. --- bench/dispatch.bench.mjs | 2 -- site/src/home/demos.tsx | 16 ---------------- site/src/tests/cases.ts | 8 -------- site/src/ui.tsx | 1 - src/core.ts | 11 +---------- test/microkeys.test.ts | 37 ++++++++++--------------------------- 6 files changed, 11 insertions(+), 64 deletions(-) diff --git a/bench/dispatch.bench.mjs b/bench/dispatch.bench.mjs index 4bb003f..38f4b91 100644 --- a/bench/dispatch.bench.mjs +++ b/bench/dispatch.bench.mjs @@ -1,5 +1,4 @@ // Compares the built microkeys bundle against upstream tinykeys. -// Run with `npm run bench` (builds first). import { Window } from "happy-dom"; import { Bench } from "tinybench"; @@ -9,7 +8,6 @@ console.warn = () => {}; // silence tinykeys conflict warnings const { createKeybindingsHandler: microHandler } = await import("../dist/index.js"); const { createKeybindingsHandler: tinyHandler } = await import("tinykeys"); -// 104 realistic bindings: single keys, modifiers, $mod, and sequences. const map = {}; for (const k of "abcdefghijklmnopqrstuvwxyz") { map[k] = () => {}; diff --git a/site/src/home/demos.tsx b/site/src/home/demos.tsx index 92fc58a..ad4c89d 100644 --- a/site/src/home/demos.tsx +++ b/site/src/home/demos.tsx @@ -20,10 +20,6 @@ function plainKey(e: KeyboardEvent): string | null | undefined { return eventModifiers(e) === 0 ? e.key.toLowerCase() : null; } -// --------------------------------------------------------------------------- -// Command palette: $mod+k, then arrows/enter/escape bound to the input itself -// --------------------------------------------------------------------------- - const COMMANDS = ["New file", "Open recent", "Toggle sidebar", "Settings"]; export function PaletteDemo() { @@ -122,10 +118,6 @@ export function PaletteDemo() { ); } -// --------------------------------------------------------------------------- -// Sequences: g i / g s / g d -// --------------------------------------------------------------------------- - // Visitors press G, then look at the keycaps before the second key, so this // demo allows more than microkeys' default 1s between presses. const GOTO_TIMEOUT = 2000; @@ -194,10 +186,6 @@ export function SequenceDemo() { ); } -// --------------------------------------------------------------------------- -// Live modifier visualizer -// --------------------------------------------------------------------------- - const APPLE_MODS = [ ["⌃", 8, "Control"], ["⌥", 4, "Alt"], @@ -278,10 +266,6 @@ export function ModifiersDemo() { ); } -// --------------------------------------------------------------------------- -// Konami code: a 10-press sequence -// --------------------------------------------------------------------------- - const KONAMI = [ "ArrowUp", "ArrowUp", diff --git a/site/src/tests/cases.ts b/site/src/tests/cases.ts index 3649d8d..67934c1 100644 --- a/site/src/tests/cases.ts +++ b/site/src/tests/cases.ts @@ -72,7 +72,6 @@ function typed(keys: string, last: string[] = []): Step[] { } export const CASES: Case[] = [ - // ------------------------------------------------------------- keys { id: "key", group: "Keys", @@ -135,7 +134,6 @@ export const CASES: Case[] = [ steps: [{ press: p("+", "shift"), expect: ["Shift++"] }], }, - // ------------------------------------------------------------- modifiers { id: "mods-each", group: "Modifiers", @@ -235,7 +233,6 @@ export const CASES: Case[] = [ ], }, - // ------------------------------------------------------------- altgraph { id: "altgraph", group: "AltGraph", @@ -270,7 +267,6 @@ export const CASES: Case[] = [ steps: [{ press: p("€", "altGraph alt", { code: "KeyE" }), expect: [] }], }, - // ------------------------------------------------------------- sequences { id: "seq", group: "Sequences", @@ -370,7 +366,6 @@ export const CASES: Case[] = [ ], }, - // ------------------------------------------------------------- config { id: "prevent", group: "Binding options", @@ -469,7 +464,6 @@ export const CASES: Case[] = [ ], }, - // ------------------------------------------------------------- layouts { id: "layout-non-latin", group: "Layouts", @@ -498,7 +492,6 @@ export const CASES: Case[] = [ steps: [{ press: p("a", "", { code: "KeyQ" }), expect: ["a"] }], }, - // ------------------------------------------------------------- ignore { id: "ignore-fields", group: "Ignore", @@ -551,7 +544,6 @@ export const CASES: Case[] = [ ], }, - // ------------------------------------------------------------- errors { id: "error-modifier", group: "Errors", diff --git a/site/src/ui.tsx b/site/src/ui.tsx index 4aaf53f..5b83061 100644 --- a/site/src/ui.tsx +++ b/site/src/ui.tsx @@ -266,7 +266,6 @@ export function ThemeToggle() { ); } -/** Mounts the global key sounds for the page. */ export function KeySounds() { useEffect(() => installKeySounds(), []); return null; diff --git a/src/core.ts b/src/core.ts index 21d0bdf..3286099 100644 --- a/src/core.ts +++ b/src/core.ts @@ -1,4 +1,3 @@ -/** Platform string, preferring the modern `userAgentData` API. */ const PLATFORM: string = typeof navigator === "object" ? ((navigator as Navigator & { userAgentData?: { platform?: string } }).userAgentData @@ -29,7 +28,6 @@ const STRICT_MASK = SHIFT | META | ALT | CONTROL; */ const ALT_GRAPH_ALIASES = IS_WINDOWS ? ALT | CONTROL : ALT; -/** Lower-cased modifier spelling → bit. */ const MODIFIER_LOOKUP: { readonly [name: string]: number } = { shift: SHIFT, meta: META, @@ -44,7 +42,6 @@ const MODIFIER_LOOKUP: { readonly [name: string]: number } = { $mod: IS_APPLE ? META : CONTROL, }; -/** Returns the modifier bit for a name, or `0` if it isn't a modifier. */ export function modifierBit(name: string): number { return MODIFIER_LOOKUP[name.toLowerCase()] ?? 0; } @@ -65,9 +62,7 @@ export function eventModifiers(event: KeyboardEvent): number { return mask; } -/** - * A single parsed press of a keybinding sequence. - */ +/** A single parsed press of a keybinding sequence. */ export interface KeybindingPress { /** Bitmask of modifiers that must be held. */ readonly required: number; @@ -183,9 +178,6 @@ export function eventInfo(event: KeyboardEvent): EventInfo { }; } -/** - * Tests if a normalized event matches a single press. - */ export function matchPress(info: EventInfo, press: KeybindingPress): boolean { const { key, required, optional, keyBit } = press; @@ -214,7 +206,6 @@ export function matchPress(info: EventInfo, press: KeybindingPress): boolean { return false; } - // Unexpected strict modifiers change the meaning of the press. let allowed = required | optional | keyBit; if (required & ALT_GRAPH) allowed |= ALT_GRAPH_ALIASES; return (info.mods & STRICT_MASK & ~allowed) === 0; diff --git a/test/microkeys.test.ts b/test/microkeys.test.ts index 801c963..0382a65 100644 --- a/test/microkeys.test.ts +++ b/test/microkeys.test.ts @@ -117,18 +117,13 @@ describe("single press", () => { }); describe("sequences", () => { - test("full sequence", () => { - const cb = vi.fn(); - bind({ "y e e t": cb }); - type("yeet"); - expect(cb).toHaveBeenCalledOnce(); - }); - - test("partial sequence", () => { + test("fires only on the last press", () => { const cb = vi.fn(); bind({ "y e e t": cb }); type("yee"); expect(cb).not.toHaveBeenCalled(); + type("t"); + expect(cb).toHaveBeenCalledOnce(); }); test("extra whitespace is tolerated", () => { @@ -172,18 +167,12 @@ describe("sequences", () => { const short = vi.fn(); const long = vi.fn(); const onConflict = vi.fn(); - bind({ "g i": long, i: short }, { onConflict }); - press("g"); - press("i"); - expect(long).toHaveBeenCalledOnce(); - expect(short).not.toHaveBeenCalled(); - }); - - test("conflict callback", () => { - const onConflict = vi.fn(); - bind({ "a b": () => {}, a: () => {} }, { onConflict }); + bind({ "a b": long, a: short }, { onConflict }); press("a"); expect(onConflict).toHaveBeenCalledWith("a", ["a b"]); + press("b"); + expect(long).toHaveBeenCalledOnce(); + expect(short).not.toHaveBeenCalled(); }); test("reset() clears pending sequences", () => { @@ -278,18 +267,12 @@ describe("modifiers", () => { }); describe("optional modifiers", () => { - test("fires without optional modifier", () => { + test("fires with or without the optional modifier", () => { const cb = vi.fn(); bind({ "[Shift]+a": cb }); press("a"); - expect(cb).toHaveBeenCalledOnce(); - }); - - test("fires with optional modifier", () => { - const cb = vi.fn(); - bind({ "[Shift]+a": cb }); press("a", { shiftKey: true }); - expect(cb).toHaveBeenCalledOnce(); + expect(cb).toHaveBeenCalledTimes(2); }); test("optional $mod", () => { @@ -562,7 +545,7 @@ describe("ignore", () => { }); describe("binding config", () => { - test("preventDefault / stopPropagation per binding", () => { + test("preventDefault per binding", () => { bind({ a: { handler: () => {}, preventDefault: true } }); expect(press("a").defaultPrevented).toBe(true); expect(press("b").defaultPrevented).toBe(false); From edc27741b9e4c938f467531b8f57a2c99381f165 Mon Sep 17 00:00:00 2001 From: Dhruv Suthar Date: Thu, 12 Nov 2026 16:39:20 +0530 Subject: [PATCH 2/2] build: add a bundle size check and stop tracking the tool cache - Fail CI when the core bundle grows past 3.25 KB gzipped (3,087 B today) - Correct the README size claim, which said ~2 KB - Stop tracking the .impeccable tool cache - Add npm keywords --- .github/workflows/check.yml | 3 +++ .gitignore | 4 ++++ README.md | 2 +- package.json | 10 +++++++++- scripts/size.mjs | 17 +++++++++++++++++ site/.impeccable/hook.cache.json | 17 ----------------- 6 files changed, 34 insertions(+), 19 deletions(-) create mode 100644 scripts/size.mjs delete mode 100644 site/.impeccable/hook.cache.json diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 137daff..7beb3c5 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -23,5 +23,8 @@ jobs: - name: Check run: pnpm check + - name: Size + run: node scripts/size.mjs + - name: Build site run: pnpm site:build diff --git a/.gitignore b/.gitignore index d8ef2f3..df1a8ff 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ dist coverage *.log .vitest/ + +# Claude Code per-user settings (may contain approved commands with secrets) +.claude/settings.local.json +.impeccable/ diff --git a/README.md b/README.md index 3a4114a..6a00296 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A tiny, fast, type-safe keybinding library, inspired by [tinykeys](https://github.com/jamiebuilds/tinykeys). -- **~2 KB gzipped**, zero dependencies, ESM + CJS +- **~3 KB gzipped**, zero dependencies, ESM + CJS - **Type-checked bindings**: `"Hyper+k"` or `"Shift+"` fail to compile - **Fast dispatch**: bitmask modifiers, no per-event timers - Per-binding `preventDefault`, `stopPropagation`, `repeat`, `when`, `once` diff --git a/package.json b/package.json index b0b22cc..c1d5ca8 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,17 @@ "description": "A tiny, fast, type-safe keybinding library.", "keywords": [ "hotkeys", + "key-sequences", "keybindings", "keyboard", + "keyboard-shortcuts", + "react", + "react-hooks", "shortcuts", - "typescript" + "tinykeys", + "type-safe", + "typescript", + "zero-dependencies" ], "homepage": "https://microkeys.dhrv.pw", "bugs": { @@ -57,6 +64,7 @@ "test": "vp test run", "test:watch": "vp test", "bench": "pnpm build && node bench/dispatch.bench.mjs", + "size": "pnpm build && node scripts/size.mjs", "check": "vp check && vp test run && vp pack", "prepublishOnly": "pnpm check", "site:dev": "pnpm --filter microkeys-site dev", diff --git a/scripts/size.mjs b/scripts/size.mjs new file mode 100644 index 0000000..e5b9fea --- /dev/null +++ b/scripts/size.mjs @@ -0,0 +1,17 @@ +import { readFileSync } from "node:fs"; +import { gzipSync } from "node:zlib"; + +const LIMIT = 3.25 * 1024; +const dist = new URL("../dist/", import.meta.url); + +function bundle(file, seen = new Set()) { + if (seen.has(file)) return ""; + seen.add(file); + const code = readFileSync(new URL(file, dist), "utf8"); + const imports = [...code.matchAll(/from\s*"\.\/([^"]+)"/g)].map((m) => m[1]); + return code + imports.map((dep) => bundle(dep, seen)).join(""); +} + +const bytes = gzipSync(bundle("index.js"), { level: 9 }).length; +console.log(`microkeys: ${bytes} B gzipped (limit ${LIMIT} B)`); +if (bytes > LIMIT) process.exit(1); diff --git a/site/.impeccable/hook.cache.json b/site/.impeccable/hook.cache.json deleted file mode 100644 index 0153a48..0000000 --- a/site/.impeccable/hook.cache.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": 1, - "sessions": { - "166d9312-9080-4595-bb50-dbeb3b559ca8": { - "updatedAt": 1792539368808, - "files": { - "/Users/dhrv/Projects/PER/microkeys/site/src/tests/tests.css": { - "editCount": 1, - "findings": ["side-tab:288"], - "stopBaseline": { "version": 1, "engine": "0.1.15", "counts": {} }, - "cleanAcked": true - } - }, - "footerShown": true - } - } -}