Initial Commit
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
# OpenSpec Change: enterprise-browser-react-command-palette
|
||||
|
||||
- **Change ID**: enterprise-browser-react-command-palette
|
||||
- **Package**: `@suaveplan/react-command-palette`
|
||||
- **Category**: browser
|
||||
- **Target path**: `browser/react/react-command-palette`
|
||||
- **Status**: IN PROGRESS
|
||||
- **Date**: 2026-04-24
|
||||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/react-command-palette.md`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Deliver `@suaveplan/react-command-palette` on the genesis branch at enterprise quality. The package provides a headless `useCommandPalette` hook with `CommandHistory` (injectable storage adapter) and `fuzzySearch`/`fuzzyScore`. Genesis adds a reference `CommandPalette` component with Storybook stories, injects a `StorageAdapter` into `CommandHistory`, adds Zod schemas for `CommandItem`, OTel metrics, i18n, and accessibility attributes — all with no `react-dom` runtime dep and no `@ts-expect-error` in source.
|
||||
|
||||
---
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
Fresh build — does not exist on genesis yet.
|
||||
|
||||
---
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@suaveplan/error` (`workspace:^`) | Typed errors for history storage failures |
|
||||
| `@suaveplan/events` (`workspace:^`) | Optional event bus for palette lifecycle |
|
||||
| `@suaveplan/logger` (`workspace:^`) | Structured logging |
|
||||
| `@suaveplan/schema-registry` (`workspace:^`) | `CommandItem` schema registration |
|
||||
| `zod` (`catalog:forms`) | `CommandItem` Zod schema |
|
||||
| `@opentelemetry/api` (`catalog:observability`) | Palette open/execute/history metrics |
|
||||
| `tslib` (`catalog:build`) | Runtime helpers |
|
||||
|
||||
Dev: `vitest`, `@vitest/coverage-istanbul`, `happy-dom`, `@testing-library/react`, `@suaveplan/testing`, `@suaveplan/testing-dom`.
|
||||
|
||||
No `react-dom` in runtime `dependencies` — peer only.
|
||||
|
||||
---
|
||||
|
||||
## Preserved public API surface
|
||||
|
||||
```ts
|
||||
// Hook
|
||||
function useCommandPalette(config: CommandPaletteConfig): [CommandPaletteState, CommandPaletteActions, CommandPaletteInputProps]
|
||||
|
||||
// History (with injectable storage)
|
||||
class CommandHistory {
|
||||
constructor(config: HistoryConfig)
|
||||
add(item: CommandItem): void
|
||||
get(): CommandItem[]
|
||||
clear(): void
|
||||
}
|
||||
|
||||
// Fuzzy search
|
||||
function fuzzySearch(items: CommandItem[], query: string): FuzzyMatch[]
|
||||
function fuzzyScore(item: CommandItem, query: string): number
|
||||
|
||||
// Reference component (new)
|
||||
function CommandPalette(props: CommandPaletteProps): JSX.Element
|
||||
|
||||
// Types
|
||||
interface CommandItem { id: string; title: string; description?: string; group?: string; keywords?: string[] }
|
||||
interface CommandPaletteConfig { triggerKey?: string; placeholder?: string; maxResults?: number; history?: HistoryConfig }
|
||||
interface CommandPaletteState { isOpen: boolean; query: string; results: FuzzyMatch[]; selectedIndex: number }
|
||||
interface CommandPaletteActions { open(): void; close(): void; setQuery(q: string): void; execute(item: CommandItem): void }
|
||||
interface CommandPaletteInputProps { value: string; onChange(e: ChangeEvent): void; role: string; 'aria-expanded': boolean; 'aria-activedescendant': string }
|
||||
```
|
||||
|
||||
Subpath exports: `"."`, `"./utils/fuzzy-search"`, `"./core/history/history"`, `"./core/command-palette/command-palette"`, `"./components/command-palette"`.
|
||||
|
||||
---
|
||||
|
||||
## New additions for genesis
|
||||
|
||||
### 1. Reference `CommandPalette` component + stories
|
||||
|
||||
`src/components/command-palette/command-palette.tsx` — reference visual component built on `useCommandPalette`. Ships `.stories.tsx` (default + playground + variants: groups, async commands, history). Required by CLAUDE.md for every React component.
|
||||
|
||||
### 2. Injectable `StorageAdapter` for `CommandHistory`
|
||||
|
||||
`CommandHistory` accepts `StorageAdapter` interface; default implementation wraps `localStorage`. Consumers can pass an `IndexedDBAdapter` backed by `@suaveplan/storage-browser`.
|
||||
|
||||
### 3. Zod schema for `CommandItem`
|
||||
|
||||
`CommandItemSchema` registered in `@suaveplan/schema-registry`. Runtime validation on `history.add()` and `fuzzySearch()` inputs.
|
||||
|
||||
### 4. OTel metrics
|
||||
|
||||
Counter `palette.open_total`, counter `palette.execute_total`, counter `palette.history_hit_total`, histogram `palette.filter_ms`.
|
||||
|
||||
### 5. i18n — no hard-coded strings in reference component
|
||||
|
||||
Reference component accepts `placeholder` prop; falls through to `@suaveplan/i18n` message resolver hook for default text.
|
||||
|
||||
### 6. Accessibility attributes
|
||||
|
||||
`CommandPaletteInputProps` includes `role="combobox"`, `aria-expanded`, `aria-activedescendant`, `aria-autocomplete`. Reference component passes axe-core gate.
|
||||
|
||||
### 7. Error codes
|
||||
|
||||
`HISTORY_STORAGE_FAILED`, `COMMAND_INVALID`.
|
||||
|
||||
---
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
browser/react/react-command-palette/
|
||||
package.json
|
||||
tsconfig.json
|
||||
vitest.config.ts
|
||||
src/
|
||||
index.ts
|
||||
error-codes.ts
|
||||
core/
|
||||
command-palette/
|
||||
command-palette.ts
|
||||
command-palette.test.ts (split: open/close, filtering, navigation, history, config)
|
||||
command-palette.md
|
||||
command-palette.types.ts
|
||||
index.ts
|
||||
history/
|
||||
history.ts
|
||||
history.test.ts
|
||||
history.md
|
||||
index.ts
|
||||
storage-adapter/
|
||||
storage-adapter.ts (StorageAdapter interface + LocalStorageAdapter)
|
||||
storage-adapter.test.ts
|
||||
storage-adapter.md
|
||||
index.ts
|
||||
components/
|
||||
command-palette/
|
||||
command-palette.tsx
|
||||
command-palette.stories.tsx
|
||||
command-palette.test.tsx
|
||||
command-palette.md
|
||||
index.ts
|
||||
utils/
|
||||
fuzzy-search/
|
||||
fuzzy-search.ts
|
||||
fuzzy-search.test.ts
|
||||
fuzzy-search.md
|
||||
index.ts
|
||||
schema/
|
||||
command-item-schema/
|
||||
command-item-schema.ts
|
||||
command-item-schema.md
|
||||
index.ts
|
||||
types/
|
||||
common.ts
|
||||
common.md
|
||||
constants/
|
||||
index.ts
|
||||
tests/
|
||||
integration.test.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Impact
|
||||
|
||||
**Downstream**: any Suaveplan browser app using a command palette.
|
||||
|
||||
**Breaking changes**: None — new factory + component are additive.
|
||||
|
||||
**Prerequisites**: `@suaveplan/error`, `@suaveplan/schema-registry` on Verdaccio.
|
||||
@@ -0,0 +1,109 @@
|
||||
# Tasks: enterprise-browser-react-command-palette
|
||||
|
||||
**Change ID**: enterprise-browser-react-command-palette
|
||||
**Package**: `@suaveplan/react-command-palette`
|
||||
**Target path**: `browser/react/react-command-palette`
|
||||
**Prerequisite packages on Verdaccio**: `@suaveplan/error`, `@suaveplan/schema-registry`
|
||||
|
||||
---
|
||||
|
||||
## 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.md`.
|
||||
- [ ] `package.json` v0.0.0, `"sideEffects": false`.
|
||||
- `dependencies`: `@suaveplan/error workspace:^`, `@suaveplan/events workspace:^`, `@suaveplan/logger workspace:^`, `@suaveplan/schema-registry workspace:^`, `zod catalog:forms`, `@opentelemetry/api catalog:observability`, `tslib catalog:build`, `react catalog:react`
|
||||
- `peerDependencies`: `react`, `react-dom`
|
||||
- `devDependencies`: `vitest catalog:testing`, `@vitest/coverage-istanbul catalog:testing`, `happy-dom catalog:testing`, `@testing-library/react catalog:testing`, `@suaveplan/testing workspace:^`, `@suaveplan/testing-dom workspace:^`
|
||||
- Subpath exports: `"."`, `"./utils/fuzzy-search"`, `"./core/history/history"`, `"./core/command-palette/command-palette"`, `"./components/command-palette"`
|
||||
- [ ] `tsconfig.json`, `vitest.config.ts` (pool: forks, happy-dom environment).
|
||||
|
||||
---
|
||||
|
||||
## 2. Error codes
|
||||
|
||||
- [ ] `HISTORY_STORAGE_FAILED`, `COMMAND_INVALID`.
|
||||
- [ ] `registerCommandPaletteErrorCodes()` — call eagerly.
|
||||
|
||||
---
|
||||
|
||||
## 3. Types (`src/types/common.ts`)
|
||||
|
||||
- [ ] `CommandItem`, `CommandPaletteConfig`, `CommandPaletteState`, `CommandPaletteActions`, `CommandPaletteInputProps`, `FuzzyMatch`, `HistoryConfig`.
|
||||
- [ ] Write `common.md`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Fuzzy search (`src/utils/fuzzy-search/`)
|
||||
|
||||
- [ ] `fuzzy-search.ts` — `fuzzySearch(items, query)` returning ranked `FuzzyMatch[]`; `fuzzyScore(item, query)` with configurable weights (exact-match, prefix, subsequence).
|
||||
- [ ] Write `fuzzy-search.test.ts` (exact match, prefix, subsequence, empty, special chars). Write `fuzzy-search.md`.
|
||||
|
||||
---
|
||||
|
||||
## 5. `StorageAdapter` interface (`src/core/storage-adapter/`)
|
||||
|
||||
- [ ] `storage-adapter.ts` — `StorageAdapter` interface: `get(): CommandItem[]`, `set(items: CommandItem[]): void`, `clear(): void`.
|
||||
- [ ] `LocalStorageAdapter` default implementation; `MemoryAdapter` for tests.
|
||||
- [ ] Write `storage-adapter.test.ts`. Write `storage-adapter.md`.
|
||||
|
||||
---
|
||||
|
||||
## 6. `CommandHistory` (`src/core/history/`)
|
||||
|
||||
- [ ] `history.ts` — `CommandHistory(config)`: injectable `StorageAdapter`, `maxSize`, `add(item)`, `get()`, `clear()`.
|
||||
- [ ] Validate `CommandItem` via `CommandItemSchema` on `add()`; throw `COMMAND_INVALID` on failure.
|
||||
- [ ] Throw `HISTORY_STORAGE_FAILED` on storage adapter error.
|
||||
- [ ] Write `history.test.ts` (add, evict over max, clear, storage failure). Write `history.md`.
|
||||
|
||||
---
|
||||
|
||||
## 7. `useCommandPalette` hook (`src/core/command-palette/`)
|
||||
|
||||
- [ ] `command-palette.ts` — hook: open/close, input binding, keyboard navigation, result filtering, selection.
|
||||
- [ ] `CommandPaletteInputProps` includes `role="combobox"`, `aria-expanded`, `aria-activedescendant`, `aria-autocomplete`.
|
||||
- [ ] Wire OTel counters + histogram `palette.filter_ms`.
|
||||
- [ ] Split tests into files ≤ 500 lines by responsibility: open/close, filtering, navigation, history integration, config.
|
||||
- [ ] Write `command-palette.md`.
|
||||
|
||||
---
|
||||
|
||||
## 8. `CommandItem` schema (`src/schema/command-item-schema/`)
|
||||
|
||||
- [ ] `command-item-schema.ts` — Zod `CommandItemSchema`; `registerCommandItemSchema()` in `@suaveplan/schema-registry`.
|
||||
- [ ] Write `command-item-schema.md`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Reference `CommandPalette` component (`src/components/command-palette/`)
|
||||
|
||||
- [ ] `command-palette.tsx` — consumes `useCommandPalette`; renders input + results list.
|
||||
- [ ] `placeholder` from props; passes through to i18n resolver for default.
|
||||
- [ ] Axe-core clean: `role="dialog"`, focus trap, `aria-live` step announcement.
|
||||
- [ ] `command-palette.stories.tsx` — default story, playground, variant: grouped commands, async commands, history.
|
||||
- [ ] `command-palette.test.tsx` — render, keyboard navigation, a11y. Write `command-palette.md`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Integration test (`tests/integration.test.ts`)
|
||||
|
||||
- [ ] Full round-trip: render component, open, type query, navigate with arrows, execute, verify history.
|
||||
- [ ] Storage adapter swap: `MemoryAdapter` replaces `LocalStorageAdapter` without code change.
|
||||
|
||||
---
|
||||
|
||||
## 11. Docs + verify
|
||||
|
||||
- [ ] Write `.md` per module (≥200 words, API reference, examples).
|
||||
- [ ] Write `README.md` with subpath table, hook API, storage-adapter guide, a11y notes.
|
||||
- [ ] `bunx turbo lint typecheck test --filter=@suaveplan/react-command-palette` — green.
|
||||
- [ ] `bunx turbo test:coverage` — 100%.
|
||||
- [ ] No `@ts-expect-error` in `src/` or tests. No `react-dom` in runtime deps.
|
||||
- [ ] Reference component passes axe-core.
|
||||
- [ ] `bun scripts/publish.ts --package @suaveplan/react-command-palette`.
|
||||
- [ ] Archive to `openspec/archive/2026-04-24-enterprise-browser-react-command-palette/`.
|
||||
Reference in New Issue
Block a user