@darksnow-ui/commander
v1.1.0
Published
Command pattern implementation with React hooks for building command palettes and keyboard-driven UIs
Maintainers
Readme
@darksnow-ui/commander
🚀 Enterprise-grade command system for React applications with Command Palette integration
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?
- Scattered actions across your app? → Centralize with Commander
- No programmatic execution? → Full API + keyboard shortcuts
- Manual Command Palette maintenance? → Auto-register/cleanup
- Poor discoverability? → Intelligent search with fuzzy matching
// 1. Register commands anywhere
useCustomCommand({
key: "file:save",
label: "Save File",
shortcut: "ctrl+s",
handle: async () => saveCurrentFile(),
});
// 2. Execute with type safety
const saveFile = useInvoker<SaveInput, SaveResult>("file:save");
await saveFile({ filename: "document.txt" });
// 3. Users find it in Command Palette (Ctrl+Shift+P)
// 4. Auto-cleanup on component unmount📦 Installation
pnpm add @darksnow-ui/commander
# or
npm install @darksnow-ui/commander⚡ Quick Start
1. Setup Provider
import { CommanderProvider, Commander } from "@darksnow-ui/commander";
const commander = new Commander();
function App() {
return (
<CommanderProvider commander={commander} enableDevTools>
<MyApp />
</CommanderProvider>
);
}2. Register Commands
import { useCustomCommand } from "@darksnow-ui/commander";
function FileEditor({ file }) {
useCustomCommand({
key: `file:save:${file.id}`,
label: `Save ${file.name}`,
shortcut: "ctrl+s",
when: () => file.isDirty,
handle: async () => {
await saveFile(file);
return { saved: true };
},
});
return <Editor />;
}3. Execute Commands
import { useInvoker } from "@darksnow-ui/commander";
function SaveButton() {
const saveFile = useInvoker("file:save");
return <button onClick={() => saveFile()}>Save</button>;
}🎣 Hooks API
Core
| Hook | Purpose |
|------|---------|
| useCommander() | Access Commander instance and methods |
| useCustomCommand(config) | Register temporary command (auto-cleanup) |
Execution
| Hook | Purpose |
|------|---------|
| useCommand(key, options) | Full-featured with state tracking |
| useInvoker(key, options) | Direct function execution |
| useAction(key) | For commands without parameters |
| useSafeInvoker(key) | Non-throwing, returns ExecutionResult |
Specialized
| Hook | Purpose |
|------|---------|
| useBoundInvoker(key, defaults) | Pre-configured parameters |
| useToggleInvoker(key) | Boolean toggle commands |
| useBatchInvoker(keys, options) | Sequential execution |
| useParallelInvoker(keys) | Parallel execution |
🔧 Command Options
useCustomCommand({
// Required
key: "namespace:action",
label: "Human Readable Name",
handle: async (input) => result,
// Optional
description: "What this command does",
category: "file" | "edit" | "view" | "tools" | "debug" | "system" | "custom",
icon: "💾",
shortcut: "ctrl+s",
tags: ["save", "file"],
priority: 10,
timeout: 5000,
owner: "my-component",
// Conditional availability
when: () => canExecute,
// Input validation (works with Zod, Yup, etc.)
inputValidator: (input) => {
if (!input?.email) {
return [{ path: "email", message: "Required", code: "required" }];
}
return true;
},
});🛡️ Input Validation
Commands support input validation with any library:
// With Zod
import { z } from "zod";
const schema = z.object({
email: z.string().email(),
name: z.string().min(2),
});
useCustomCommand({
key: "user:create",
label: "Create User",
inputValidator: (input) => {
const result = schema.safeParse(input);
if (result.success) return true;
return result.error.issues.map((i) => ({
path: i.path.join("."),
message: i.message,
code: i.code,
}));
},
handle: async (input) => createUser(input),
});Handle validation errors:
import { isInputValidationError } from "@darksnow-ui/commander";
try {
await commander.invoke("user:create", { email: "" });
} catch (error) {
if (isInputValidationError(error)) {
console.log(error.getMissingFields()); // ["name"]
console.log(error.errors); // Full error details
}
}🎯 useCommand vs useInvoker
// useInvoker - Simple, direct execution
const save = useInvoker("file:save");
await save({ filename: "doc.txt" });
// useCommand - Full state tracking
const cmd = useCommand("file:save");
cmd.isLoading; // boolean
cmd.lastResult; // last successful result
cmd.lastError; // last error
cmd.executionCount; // number of executions
await cmd.invoke({ filename: "doc.txt" });📚 Documentation
| Document | Description | |----------|-------------| | Architecture | Core internals, types, algorithms | | useInvoker Guide | Deep dive into execution hooks | | useCustomCommand Examples | 30+ real-world examples |
🏆 Comparison
| Feature | Commander | cmdk | kbar |
|---------|-----------|------|------|
| TypeScript generics | ✅ Full | ✅ Basic | ✅ Good |
| React hooks | ✅ 10 specialized | ❌ | ❌ Limited |
| Auto cleanup | ✅ | ❌ Manual | ❌ Manual |
| Conditional commands | ✅ when() | ❌ | ❌ |
| Input validation | ✅ Built-in | ❌ | ❌ |
| State tracking | ✅ | ❌ | ❌ |
| Event system | ✅ Full lifecycle | ❌ | ✅ Limited |
📄 License
MIT © Anderson Rosa
Built with ❤️ by Anderson Rosa
Part of the DarkSnow UI ecosystem
