UNPKG

@darksnow-ui/commander

Version:

Command pattern implementation with React hooks for building command palettes and keyboard-driven UIs

741 lines (604 loc) โ€ข 20 kB
# @darksnow-ui/commander > ๐Ÿš€ **Enterprise-grade command system for React applications with Command Palette integration** [![npm version](https://badge.fury.io/js/%40darksnow-ui%2Fcommander.svg)](https://badge.fury.io/js/%40darksnow-ui%2Fcommander) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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. ![Commander](https://github.com/andersondrosa/andersondrosa/blob/main/images/commander-gracious-robot.jpeg?raw=true) ## โœจ 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>