Initial Commit
This commit is contained in:
91
openspec/changes/enterprise-react-hotkeys/proposal.md
Normal file
91
openspec/changes/enterprise-react-hotkeys/proposal.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# OpenSpec Change: enterprise-react-hotkeys
|
||||
|
||||
- **Change ID**: enterprise-react-hotkeys
|
||||
- **Package**: `@suaveplan/react-hotkeys`
|
||||
- **Category**: react
|
||||
- **Target path**: `react/ui/react-hotkeys`
|
||||
- **Status**: PROPOSED
|
||||
- **Date**: 2026-04-24
|
||||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/react-hotkeys.md`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Deliver `@suaveplan/react-hotkeys` on genesis from scratch at enterprise quality. Keyboard-shortcut primitives: `useHotkey`, `useHotkeys`, `HotkeyScope`, `HotkeyCheatSheet`, combo parser/matcher/formatter. Genesis replaces the module-level mutable registry with a `useSyncExternalStore`-backed store, adds sequence combos (e.g. `g g`), platform-aware mod key, conflict detection, `prevent-in-input` guard, and full Storybook + Playwright coverage.
|
||||
|
||||
---
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
Package does not exist on genesis — fresh build.
|
||||
|
||||
---
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@suaveplan/error` (`workspace:^`) | `HotkeyError` |
|
||||
| `@suaveplan/react-translate` (`workspace:^`) | i18n for cheat-sheet chrome strings |
|
||||
| `zod` (`catalog:forms`) | Hotkey binding schemas |
|
||||
|
||||
External: `tslib catalog:build`. React 18 peer.
|
||||
|
||||
---
|
||||
|
||||
## Preserved public API surface
|
||||
|
||||
```ts
|
||||
HotkeyCheatSheet — renders all registered entries with descriptions
|
||||
HotkeyScope — provider routing events to active scope
|
||||
useHotkey(combo, handler, options?)
|
||||
useHotkeys(bindings: HotkeyBinding[], options?)
|
||||
useHotkeyRegistry(): HotkeyRegistryEntry[]
|
||||
|
||||
parseCombo(raw: string): HotkeyCombo
|
||||
matchesCombo(event: KeyboardEvent, combo: HotkeyCombo): boolean
|
||||
formatCombo(combo: HotkeyCombo): string
|
||||
|
||||
// Types
|
||||
HotkeyBinding, HotkeyCombo, HotkeyOptions, HotkeyRegistryEntry, RegistryEntry
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## New additions for genesis
|
||||
|
||||
1. **`useSyncExternalStore`-backed registry** — cheat-sheet re-renders on mount/unmount without parent re-render.
|
||||
2. **Sequence combos** — `g g`, `ctrl+k ctrl+p` parsed as `HotkeyCombo[]` array; matched against a keydown sequence buffer.
|
||||
3. **Platform-aware mod key** — `mod` expands to `cmd` on macOS, `ctrl` on Windows/Linux.
|
||||
4. **Conflict detection** — `useHotkeys` warns (via `@suaveplan/error` `HotkeyError`) when two bindings share the same scope + combo.
|
||||
5. **`prevent-in-input` guard** — `options.preventDefault` skips when target is `INPUT`, `TEXTAREA`, `[contenteditable]`.
|
||||
6. **Stories** — `HotkeyCheatSheet` default + live-update, `HotkeyScope` scoped-routing, `useHotkey` playground.
|
||||
|
||||
---
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
react/ui/react-hotkeys/
|
||||
package.json
|
||||
tsconfig.json
|
||||
vitest.config.ts
|
||||
src/
|
||||
index.ts
|
||||
hooks/
|
||||
use-hotkey/ (ts + types + md + test + stories + index)
|
||||
use-hotkeys/ (ts + types + md + test + index)
|
||||
use-hotkey-registry/ (ts + md + test + index)
|
||||
components/
|
||||
hotkey-scope/ (tsx + types + md + test + stories + index)
|
||||
hotkey-cheat-sheet/ (tsx + types + md + test + stories + index)
|
||||
store/
|
||||
hotkey-store/ (ts + md + test + index)
|
||||
types/
|
||||
hotkey.types.ts
|
||||
utils/
|
||||
combo-parser/ (ts + md + test + index)
|
||||
e2e/
|
||||
hotkeys.spec.ts
|
||||
```
|
||||
106
openspec/changes/enterprise-react-hotkeys/tasks.md
Normal file
106
openspec/changes/enterprise-react-hotkeys/tasks.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Tasks: enterprise-react-hotkeys
|
||||
|
||||
**Change ID**: enterprise-react-hotkeys
|
||||
**Package**: `@suaveplan/react-hotkeys`
|
||||
**Target path**: `react/ui/react-hotkeys`
|
||||
**Prerequisite packages on Verdaccio**: `@suaveplan/error`, `@suaveplan/react-translate`
|
||||
|
||||
---
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||||
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||||
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [ ] Create directory tree per proposal layout.
|
||||
- [ ] Write `package.json` at `0.1.0` with `"sideEffects": false`, exports map.
|
||||
- [ ] Dependencies: `@suaveplan/error workspace:^`, `@suaveplan/react-translate workspace:^`, `zod catalog:forms`, `tslib catalog:build`.
|
||||
- [ ] Write `tsconfig.json`, `vitest.config.ts`, scripts.
|
||||
|
||||
---
|
||||
|
||||
## 2. Error codes and types
|
||||
|
||||
- [ ] Define `HotkeyError` extending `SuaveplanError`; codes: `HOTKEY_CONFLICT`, `HOTKEY_INVALID_COMBO`.
|
||||
- [ ] Define all types: `HotkeyCombo` (single token + modifier flags), `HotkeyBinding` (combo string + handler + description), `HotkeyOptions`, `HotkeyRegistryEntry`, `RegistryEntry`.
|
||||
- [ ] Write `HotkeyBindingSchema` Zod schema.
|
||||
|
||||
---
|
||||
|
||||
## 3. Combo parser (`src/utils/combo-parser/`)
|
||||
|
||||
- [ ] Implement `parseCombo(raw: string): HotkeyCombo | HotkeyCombo[]` — supports `ctrl+shift+k`, `cmd+k`, `mod+k` (platform-aware), `g g` (sequence as array), `option+/`.
|
||||
- [ ] Implement `matchesCombo(event: KeyboardEvent, combo: HotkeyCombo): boolean`.
|
||||
- [ ] Implement `formatCombo(combo: HotkeyCombo): string` — `⌘K` on mac, `Ctrl+K` on other.
|
||||
- [ ] Implement `isModifierKey(event): boolean`, `isMac(): boolean`.
|
||||
- [ ] Write `combo-parser.test.ts` (≤500 lines): all modifier combos, sequence combos, platform-aware mod, edge cases (`/`, ` ` space). `{ timeout: 5000 }`.
|
||||
- [ ] Write `combo-parser.md`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Store (`src/store/hotkey-store/`)
|
||||
|
||||
- [ ] Implement `createHotkeyStore()` — `useSyncExternalStore`-compatible external store holding `Map<scope, HotkeyRegistryEntry[]>`.
|
||||
- [ ] `register(scope, entry): () => void` — adds entry, returns unsubscribe; throws `HotkeyError(HOTKEY_CONFLICT)` if same scope+combo already registered.
|
||||
- [ ] `getEntries(scope): HotkeyRegistryEntry[]`, `subscribe(listener): () => void`.
|
||||
- [ ] Sequence buffer: tracks last N keydowns; matches against sequence combos.
|
||||
- [ ] Write `hotkey-store.test.ts`, `hotkey-store.md`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Hooks
|
||||
|
||||
### `useHotkey`
|
||||
|
||||
- [ ] Registers single combo; installs `keydown` listener on `document` (SSR-safe via `useEffect`).
|
||||
- [ ] `prevent-in-input` guard.
|
||||
- [ ] Returns `{ isPressed: boolean }`.
|
||||
- [ ] Write test (combo fires, in-input blocked, SSR-safe), stories (playground toggle), md.
|
||||
|
||||
### `useHotkeys`
|
||||
|
||||
- [ ] Bulk-registers `HotkeyBinding[]`; detects conflicts via store and emits `HotkeyError(HOTKEY_CONFLICT)` (non-throwing — logs via `console.warn` in dev, captures OTel event).
|
||||
- [ ] Write test, md.
|
||||
|
||||
### `useHotkeyRegistry`
|
||||
|
||||
- [ ] Subscribes to store via `useSyncExternalStore`; returns current entries for active scope.
|
||||
- [ ] Write test (mount/unmount triggers re-render), md.
|
||||
|
||||
---
|
||||
|
||||
## 6. Components
|
||||
|
||||
### HotkeyScope
|
||||
|
||||
- [ ] Provides scope name via context; routes events only when scope is active.
|
||||
- [ ] Write test, stories (scoped-routing — two scopes, only active one fires), md.
|
||||
|
||||
### HotkeyCheatSheet
|
||||
|
||||
- [ ] Renders grouped registry entries (scope → table of combo + description).
|
||||
- [ ] Uses `useTranslation` for chrome strings ("Keyboard Shortcuts", "Description", "Shortcut").
|
||||
- [ ] Re-renders live when entries change (via `useHotkeyRegistry`).
|
||||
- [ ] Write test, stories (default, live-update on mount, playground), md.
|
||||
|
||||
---
|
||||
|
||||
## 7. E2E
|
||||
|
||||
- [ ] `e2e/hotkeys.spec.ts` — Playwright:
|
||||
- Press `ctrl+k` (or `meta+k` on mac), verify handler fires.
|
||||
- Press `g g` sequence, verify handler fires.
|
||||
- Focus `<input>`, press hotkey, verify handler does NOT fire.
|
||||
- Switch `HotkeyScope`, verify only active scope responds.
|
||||
|
||||
---
|
||||
|
||||
## 8. Verify and publish
|
||||
|
||||
- [ ] `bunx turbo lint typecheck test --filter=@suaveplan/react-hotkeys` — green.
|
||||
- [ ] `bunx turbo test:coverage --filter=@suaveplan/react-hotkeys` — 100%.
|
||||
- [ ] `bun scripts/publish.ts --package @suaveplan/react-hotkeys`.
|
||||
- [ ] Archive change folder.
|
||||
Reference in New Issue
Block a user