@darksnow-ui/commander
Version:
Command pattern implementation with React hooks for building command palettes and keyboard-driven UIs
741 lines (604 loc) โข 20 kB
Markdown
# @darksnow-ui/commander
> ๐ **Enterprise-grade command system for React applications with Command Palette integration**
[](https://badge.fury.io/js/%40darksnow-ui%2Fcommander)
[](http://www.typescriptlang.org/)
[](https://opensource.org/licenses/MIT)
Transform your React app into a power-user's dream with a **VS Code-style command system**. Register commands from anywhere, execute them programmatically or via Command Palette, and watch your UX reach new levels.

## โจ Why Commander?
### ๐ฏ **The Problem**
- Scattered actions across your app with no centralized control
- No unified way to execute operations programmatically + via UI
- Command Palettes that require manual registration and maintenance
- Poor discoverability of available actions for power users
### ๐ก **The Solution**
```tsx
// 1. Register commands anywhere in your app
useCustomCommand({
key: 'file:save',
label: 'Save File',
shortcut: 'ctrl+s',
handle: async () => saveCurrentFile()
});
// 2. Execute programmatically with full type safety
const saveFile = useInvoker<SaveInput, SaveResult>('file:save');
const result = await saveFile({ filename: 'document.txt' });
// 3. Users find it instantly in Command Palette (Ctrl+Shift+P)
// 4. Command auto-removes when component unmounts
```
## ๐ Features
### ๐๏ธ **Core Architecture**
- **๐ฏ Command Pattern** - Centralized command management with O(1) lookups
- **๐ Intelligent Search** - Hierarchical scoring algorithm with fuzzy matching
- **โจ๏ธ Keyboard First** - Built-in shortcut support with conflict resolution
- **๐ท๏ธ Smart Organization** - Categories, tags, owners, and priority system
- **๐ Event-Driven** - Comprehensive lifecycle events for monitoring and analytics
### โ๏ธ **React Integration**
- **๐จ Zero Boilerplate** - Context Provider handles everything automatically
- **โก Temporary Commands** - Components register/unregister commands automatically
- **๐ก๏ธ Type Safety** - Full TypeScript with generics for inputs/outputs
- **๐ช Specialized Hooks** - 10 purpose-built hooks for every use case
- **๐ฅ Hot Reload Friendly** - Commands survive React Fast Refresh
### ๐๏ธ **Advanced Features**
- **๐ Execution Tracking** - History, analytics, and recent commands
- **๐ Conditional Availability** - Commands appear/disappear based on context
- **โฑ๏ธ Timeout Handling** - Automatic timeouts with graceful error handling
- **๐ ๏ธ Dev Tools** - Built-in debugging commands and global access
- **๐ Performance** - Optimized for apps with hundreds of commands
## ๐ฆ Installation
```bash
npm install @darksnow-ui/commander
# or
yarn add @darksnow-ui/commander
# or
pnpm add @darksnow-ui/commander
```
## โก Quick Start
### 1. **Setup Provider** (one time in your app root)
```tsx
import { CommanderProvider, Commander } from '@darksnow-ui/commander';
// Create your commander instance (usually in src/core/commander.ts)
const commander = new Commander();
function App() {
return (
<CommanderProvider
commander={commander}
enableDevTools
onReady={(commander) => {
console.log('Commander ready with', commander.commands().length, 'commands');
}}
>
<MyApp />
</CommanderProvider>
);
}
```
### 2. **Register Commands** (in any component)
```tsx
import { useCustomCommand } from '@darksnow-ui/commander';
function FileEditor({ file }) {
// Command automatically appears in Command Palette!
useCustomCommand({
key: `file:save:${file.id}`,
label: `Save ${file.name}`,
icon: '๐พ',
shortcut: 'ctrl+s',
when: () => file.isDirty, // Only when file has changes
handle: async () => {
await saveFile(file);
return { saved: true, filename: file.name };
}
});
return <div>Your file editor UI...</div>;
}
```
### 3. **Execute Commands** (programmatically)
```tsx
import { useInvoker } from '@darksnow-ui/commander';
function ActionButton() {
const saveFile = useInvoker<SaveInput, SaveResult>('file:save');
const handleSave = async () => {
try {
const result = await saveFile({ filename: 'document.txt' });
toast.success(`Saved: ${result.filename}`);
} catch (error) {
toast.error('Save failed');
}
};
return <button onClick={handleSave}>Save File</button>;
}
```
## ๐ฃ React Hooks
### Core Hooks
#### `useCommander()`
Access the complete Commander API:
```tsx
const { commander, search, invoke, commands, has, getCommand } = useCommander();
// Search for commands
const results = await search('save');
// Execute any command
await invoke('file:save', { filename: 'test.txt' });
// Get all commands
const allCommands = commands();
// Check if command exists
if (has('file:save')) {
const saveCommand = getCommand('file:save');
}
```
#### `useCustomCommand()`
Register temporary commands that auto-cleanup:
```tsx
useCustomCommand({
key: 'my-command',
label: 'My Command',
description: 'Does something awesome',
category: 'actions',
icon: '๐',
shortcut: 'ctrl+shift+a',
tags: ['quick', 'action'],
priority: 10,
when: () => isFeatureEnabled(), // Conditional availability
handle: async (input) => {
// Your command logic
return { success: true };
}
});
```
### Execution Hooks
#### `useCommand()`
Full-featured command execution with state tracking:
```tsx
const command = useCommand<Input, Output>('my-command', {
throwOnError: true,
source: 'api',
onSuccess: (result) => console.log('Success:', result),
onError: (error) => console.error('Error:', error)
});
// Execute with various methods
await command.invoke({ data: 'test' }); // throws on error
const result = await command.attempt({ data: 'test' }); // returns ExecutionResult
await command.execute({ data: 'test' }, {
onSuccess: (result) => toast.success('Done!'),
onError: (error) => toast.error(error.message)
});
// Access state
console.log(command.isLoading);
console.log(command.lastResult);
console.log(command.lastError);
console.log(command.executionCount);
// Check availability
if (command.isAvailable) {
// Show UI
}
```
#### `useInvoker()`
Direct function execution (simplified):
```tsx
const saveFile = useInvoker<Input, Output>('file:save');
// Direct invocation - returns a function!
const result = await saveFile({ data: 'test' });
// With options
const saveWithOptions = useInvoker('file:save', {
throwOnError: false,
onSuccess: (result) => toast.success('Saved!')
});
```
### Specialized Hooks
#### `useAction()`
For commands without parameters:
```tsx
const logout = useAction('auth:logout');
const refresh = useAction('app:refresh');
// Simple execution
await logout();
await refresh();
```
#### `useCommandState()`
Always returns the CommandInvoker object (alias for useCommand):
```tsx
const command = useCommandState('my-command');
// Access state and methods
console.log(command.isLoading);
console.log(command.lastResult);
await command.invoke(data);
```
#### `useSafeInvoker()`
Non-throwing execution with ExecutionResult:
```tsx
const saveFile = useSafeInvoker<Input, Output>('file:save');
const result = await saveFile({ filename: 'test.txt' });
if (result.success) {
console.log('Saved:', result.result);
} else {
console.error('Failed:', result.error);
}
```
#### `useBoundInvoker()`
Pre-configured command execution:
```tsx
// Always saves as PDF
const savePdf = useBoundInvoker('file:save', { format: 'pdf' });
await savePdf({ filename: 'document' }); // format is already bound
```
#### `useToggleInvoker()`
For boolean toggle commands:
```tsx
const toggleDarkMode = useToggleInvoker('ui:dark-mode');
const toggleSidebar = useToggleInvoker('ui:sidebar');
// Toggle state
await toggleDarkMode(true);
await toggleSidebar(); // toggles current state
```
#### `useBatchInvoker()`
Sequential execution with progress tracking:
```tsx
const deployPipeline = useBatchInvoker(
['build', 'test', 'deploy'],
{
stopOnError: true,
onProgress: (step, total) => console.log(`${step}/${total}`)
}
);
const results = await deployPipeline([
{ target: 'production' },
{ suite: 'all' },
{ env: 'prod' }
]);
```
#### `useParallelInvoker()`
Parallel execution with Promise.allSettled:
```tsx
const loadDashboard = useParallelInvoker([
'stats:revenue',
'stats:users',
'stats:orders'
]);
const results = await loadDashboard([
{ period: '30d' },
{ status: 'active' },
{ status: 'pending' }
]);
```
## ๐๏ธ Core Concepts
### Commander Instance
The heart of the system - manages all commands:
```tsx
// Create and configure
const commander = new Commander();
commander.maxHistorySize = 200;
commander.maxRecentSize = 15;
// Register system commands
commander.add({
key: 'app:refresh',
label: 'Refresh Application',
handle: async () => location.reload()
});
```
### Command Structure
Commands are the building blocks:
```tsx
interface Command<TInput, TOutput> {
key: CommandKey; // Unique identifier
label: string; // Human-readable name
handle: (input?: TInput) => Promise<TOutput>; // The actual function
description?: string; // Detailed description
category?: CommandCategory; // Organization
icon?: string; // Visual indicator
shortcut?: string; // Keyboard shortcut
when?: () => boolean | Promise<boolean>; // Conditional availability
tags?: string[]; // Search tags
priority?: number; // Search ranking
owner?: string; // Who registered it
source?: CommandSource; // Where it came from
}
```
### Command Builder
Fluent API for command creation:
```tsx
import { CommandBuilder } from '@darksnow-ui/commander';
const saveCommand = CommandBuilder
.create<SaveInput, SaveOutput>('file:save')
.label('Save File')
.description('Save the current file to disk')
.category('file')
.icon('๐พ')
.shortcut('ctrl+s')
.tags(['file', 'save', 'disk'])
.priority(100)
.handle(async (input) => {
const result = await saveFile(input);
return { success: true, path: result.path };
})
.build();
commander.add(saveCommand);
```
### Search System
Intelligent hierarchical scoring:
```tsx
const results = await commander.search('save file', {
category: 'file', // Filter by category
owner: 'editor', // Filter by owner
tags: ['important'], // Filter by tags
limit: 10 // Limit results
});
// Results are scored by:
// 1. Exact key match (highest)
// 2. Label match
// 3. Description match
// 4. Tag match
// 5. Fuzzy match (lowest)
```
### Event System
Monitor command lifecycle:
```tsx
// Listen to events
commander.on('commandRegistered', (command) => {
console.log('New command:', command.key);
});
commander.on('beforeExecute', ({ command, input }) => {
analytics.track('command_execute', { key: command.key });
});
commander.on('afterExecute', ({ command, result, duration }) => {
console.log(`Command ${command.key} took ${duration}ms`);
});
commander.on('executionError', ({ command, error }) => {
errorReporter.log(error);
});
```
## ๐ Documentation
### ๐ **Guides**
- **[Getting Started](./docs/getting-started.md)** - Complete setup guide
- **[Hooks Guide](./docs/hooks-guide.md)** - Deep dive into all React hooks
- **[useCustomCommand Examples](./docs/useCustomCommand-examples.md)** - 30+ real-world examples
- **[useCommand Guide](./docs/useCommand-guide.md)** - Advanced state management
- **[useInvoker Guide](./docs/useInvoker-guide.md)** - Command execution patterns
- **[Commander Algorithm](./docs/commander-algorithm.md)** - Core architecture deep dive
### ๐ฏ **Examples**
<details>
<summary><b>๐ Text Editor with Context Commands</b></summary>
```tsx
function TextEditor({ document }) {
const [content, setContent] = useState(document.content);
const [isDirty, setIsDirty] = useState(false);
// Save command - only available when dirty
useCustomCommand({
key: `doc:save:${document.id}`,
label: `Save ${document.name}`,
category: 'file',
icon: '๐พ',
shortcut: 'ctrl+s',
when: () => isDirty,
handle: async () => {
await saveDocument(document.id, content);
setIsDirty(false);
return { saved: true };
}
});
// Format command
useCustomCommand({
key: `doc:format:${document.id}`,
label: `Format ${document.name}`,
category: 'edit',
icon: 'โจ',
shortcut: 'shift+alt+f',
handle: async () => {
const formatted = await formatText(content);
setContent(formatted);
setIsDirty(true);
return { formatted: true };
}
});
return (
<textarea
value={content}
onChange={(e) => {
setContent(e.target.value);
setIsDirty(true);
}}
/>
);
}
```
</details>
<details>
<summary><b>๐๏ธ E-commerce Admin Dashboard</b></summary>
```tsx
function ProductList({ products }) {
// Register command for each product
products.forEach(product => {
useCustomCommand({
key: `product:edit:${product.id}`,
label: `Edit ${product.name}`,
description: `SKU: ${product.sku}`,
category: 'products',
icon: 'โ๏ธ',
tags: ['product', 'edit', product.category],
searchKeywords: [product.name, product.sku, product.brand],
handle: async () => {
await openProductEditor(product.id);
return { opened: true };
}
});
});
// Bulk operations
const bulkDelete = useInvoker<{ ids: string[] }>('products:bulk-delete');
const handleBulkDelete = async (selectedIds: string[]) => {
const result = await bulkDelete({ ids: selectedIds });
if (result.success) {
toast.success(`Deleted ${result.count} products`);
}
};
return <ProductGrid products={products} onBulkDelete={handleBulkDelete} />;
}
```
</details>
<details>
<summary><b>๐ฎ Game Controls</b></summary>
```tsx
function GameControls() {
const [isPaused, setIsPaused] = useState(false);
// Game actions
useCustomCommand({
key: 'game:pause',
label: isPaused ? 'Resume Game' : 'Pause Game',
icon: isPaused ? 'โถ๏ธ' : 'โธ๏ธ',
shortcut: 'space',
handle: async () => {
setIsPaused(!isPaused);
return { paused: !isPaused };
}
});
useCustomCommand({
key: 'game:save',
label: 'Quick Save',
icon: '๐พ',
shortcut: 'f5',
when: () => !isPaused,
handle: async () => {
const slot = await saveGame();
return { slot };
}
});
useCustomCommand({
key: 'game:load',
label: 'Quick Load',
icon: '๐',
shortcut: 'f9',
handle: async () => {
await loadLastSave();
return { loaded: true };
}
});
return <GameUI paused={isPaused} />;
}
```
</details>
## ๐ Comparison
| Feature | **@darksnow-ui/commander** | cmdk | kbar | Custom Solution |
|---------|----------------------------|------|------|-----------------|
| **TypeScript** | โ
Full generics | โ
Basic | โ
Good | ๐คท Depends |
| **React Hooks** | โ
10 specialized hooks | โ None | โ Limited | ๐คท Depends |
| **Auto Cleanup** | โ
Automatic | โ Manual | โ Manual | โ Manual |
| **Conditional Commands** | โ
Built-in `when()` | โ Manual | โ Manual | ๐คท Depends |
| **State Management** | โ
Built-in tracking | โ None | โ None | ๐คท Depends |
| **Event System** | โ
Full lifecycle | โ None | โ
Limited | ๐คท Depends |
| **Search Algorithm** | โ
Hierarchical scoring | โ
Fuzzy | โ
Fuzzy | ๐คท Depends |
| **Batch Execution** | โ
Built-in hooks | โ None | โ None | ๐คท Depends |
| **Performance** | โ
O(1) operations | โ ๏ธ O(n) | โ ๏ธ O(n) | ๐คท Depends |
| **Bundle Size** | ๐ฆ ~50KB | ๐ฆ ~40KB | ๐ฆ ~45KB | ๐คท Depends |
## ๐ Performance
Built for scale with real-world optimization:
- **โก O(1) Operations**: Command lookup, registration, and removal
- **๐ Efficient Search**: Optimized scoring algorithm
- **๐ง Smart Memoization**: React renders optimized automatically
- **๐ฆ Memory Bounded**: Automatic cleanup of history and listeners
- **๐ฏ Lazy Evaluation**: Commands only checked when needed
- **๐ Battle Tested**: Used in production with 500+ commands
## ๐ก๏ธ TypeScript
First-class TypeScript support with advanced patterns:
```tsx
// Strongly typed commands
interface SaveFileInput {
filename: string;
content: string;
format?: 'utf8' | 'binary';
}
interface SaveFileOutput {
success: boolean;
path: string;
size: number;
}
// Type-safe registration
useCustomCommand<SaveFileInput, SaveFileOutput>({
key: 'file:save',
label: 'Save File',
handle: async (input) => {
// input is typed as SaveFileInput
const result = await fs.writeFile(input.filename, input.content);
// Must return SaveFileOutput
return {
success: true,
path: result.path,
size: input.content.length
};
}
});
// Type-safe execution
const saveFile = useInvoker<SaveFileInput, SaveFileOutput>('file:save');
const result = await saveFile({
filename: 'test.txt',
content: 'Hello world'
}); // result is typed as SaveFileOutput
```
## ๐งช Testing
Comprehensive test coverage with 192 tests:
```tsx
import { renderHook } from '@testing-library/react';
import { CommanderProvider, useCustomCommand } from '@darksnow-ui/commander';
test('command registration and execution', async () => {
const { result } = renderHook(
() => {
useCustomCommand({
key: 'test:command',
label: 'Test Command',
handle: async (input: { value: number }) => {
return { doubled: input.value * 2 };
}
});
return useInvoker<{ value: number }, { doubled: number }>('test:command');
},
{
wrapper: ({ children }) => (
<CommanderProvider commander={new Commander()}>
{children}
</CommanderProvider>
)
}
);
const output = await result.current({ value: 5 });
expect(output.doubled).toBe(10);
});
```
## ๐ง Configuration
### Commander Options
```tsx
const commander = new Commander({
maxHistorySize: 200, // Maximum execution history entries
maxRecentSize: 15, // Maximum recent commands to track
executionTimeout: 30000, // Default timeout for commands (ms)
enableDevTools: true, // Enable debugging features
});
```
### Provider Options
```tsx
<CommanderProvider
commander={commander}
enableDevTools={true}
onReady={(commander) => {
// Called when commander is ready
console.log('Commander initialized');
}}
>
{children}
</CommanderProvider>
```
## ๐ License
MIT ยฉ [Anderson Rosa](https://github.com/andersondrosa)
## ๐ค Contributing
We love contributions! See our [Contributing Guide](./CONTRIBUTING.md) for details.
## ๐ฌ Community & Support
- **๐ Bug Reports**: [GitHub Issues](https://github.com/darksnow-ui/darksnow-ui/issues)
- **๐ก Feature Requests**: [GitHub Discussions](https://github.com/darksnow-ui/darksnow-ui/discussions)
- **๐ Full Documentation**: [View all docs](./docs/)
## โญ Show Your Support
If Commander helps your project, please consider:
- โญ **Starring** this repository
- ๐ฆ **Sharing** on social media
- ๐ **Writing** about your experience
- ๐ค **Contributing** to the project
<div align="center">
**Built with โค๏ธ by [Anderson Rosa](https://github.com/andersondrosa)**
*Part of the [DarkSnow UI](https://github.com/darksnow-ui/darksnow-ui) ecosystem*
[โญ Star on GitHub](https://github.com/darksnow-ui/darksnow-ui) โข [๐ Read the Docs](./docs/) โข [๐ View Examples](./docs/useCustomCommand-examples.md)
</div>