@darksnow-ui/commander
Version:
Command pattern implementation with React hooks for building command palettes and keyboard-driven UIs
1,232 lines (1,099 loc) • 30.9 kB
Markdown
# useCustomCommand - Exemplos Práticos
> 🎯 **Comandos temporários que existem apenas enquanto o componente está montado**
O `useCustomCommand` é o hook principal para criar comandos contextuais que aparecem automaticamente no Command Palette (Ctrl+Shift+P) e desaparecem quando o componente é desmontado.
## 📋 Índice
1. [Modal com Comandos](#modal-com-comandos)
2. [Editor de Texto](#editor-de-texto)
3. [Formulário com Ações](#formulário-com-ações)
4. [Lista com Ações Contextuais](#lista-com-ações-contextuais)
5. [Player de Mídia](#player-de-mídia)
6. [Dashboard com Widgets](#dashboard-com-widgets)
7. [Tabela de Dados](#tabela-de-dados)
8. [Integração com Rotas](#integração-com-rotas)
---
## Modal com Comandos
```tsx
import { useState } from "react";
import { useCustomCommand } from "@darksnow-ui/commander";
function UserProfileModal({ user, isOpen, onClose }) {
const [isEditing, setIsEditing] = useState(false);
const [formData, setFormData] = useState(user);
// Comando para fechar o modal (ESC)
useCustomCommand({
key: `modal:close:${user.id}`,
label: "Fechar Modal",
icon: "❌",
shortcut: "escape",
handle: async () => {
onClose();
},
});
// Comando para editar/salvar
const saveCommand = useCustomCommand({
key: `user:save:${user.id}`,
label: isEditing ? "Salvar Alterações" : "Editar Perfil",
icon: isEditing ? "💾" : "✏️",
shortcut: isEditing ? "ctrl+s" : "ctrl+e",
category: "user",
when: () => isOpen, // Só disponível quando modal está aberto
handle: async () => {
if (isEditing) {
await api.updateUser(user.id, formData);
setIsEditing(false);
toast.success("Perfil atualizado!");
} else {
setIsEditing(true);
}
},
});
// Comando para deletar usuário
useCustomCommand({
key: `user:delete:${user.id}`,
label: "Deletar Usuário",
icon: "🗑️",
category: "user",
tags: ["danger", "delete"],
priority: -1, // Baixa prioridade na busca
when: () => isOpen && user.role !== "admin",
handle: async () => {
if (confirm("Tem certeza?")) {
await api.deleteUser(user.id);
onClose();
}
},
});
// Comando para enviar mensagem
useCustomCommand({
key: `user:message:${user.id}`,
label: `Enviar mensagem para ${user.name}`,
icon: "💬",
shortcut: "ctrl+m",
category: "communication",
handle: async () => {
openMessageComposer(user);
},
});
if (!isOpen) return null;
return <Modal onClose={onClose}>{/* UI do modal */}</Modal>;
}
```
---
## Editor de Texto
```tsx
import { useCustomCommand, useAction } from "@darksnow-ui/commander";
function RichTextEditor({ documentId, initialContent, onSave }) {
const [content, setContent] = useState(initialContent);
const [isDirty, setIsDirty] = useState(false);
const [selectedText, setSelectedText] = useState("");
const [undoStack, setUndoStack] = useState([]);
const [redoStack, setRedoStack] = useState([]);
// Comando de salvar
const saveCommand = useCustomCommand({
key: `doc:save:${documentId}`,
label: "Salvar Documento",
icon: "💾",
shortcut: "ctrl+s",
category: "file",
when: () => isDirty, // Só aparece quando há mudanças
handle: async () => {
const result = await onSave(content);
setIsDirty(false);
return result;
},
});
// Comandos de formatação
useCustomCommand({
key: `format:bold:${documentId}`,
label: "Negrito",
icon: "🅱️",
shortcut: "ctrl+b",
category: "format",
when: () => selectedText.length > 0,
handle: async () => {
document.execCommand("bold");
},
});
useCustomCommand({
key: `format:italic:${documentId}`,
label: "Itálico",
icon: "𝐼",
shortcut: "ctrl+i",
category: "format",
when: () => selectedText.length > 0,
handle: async () => {
document.execCommand("italic");
},
});
// Comandos de edição
useAction(
`edit:undo:${documentId}`,
"Desfazer",
() => {
if (undoStack.length > 0) {
const previous = undoStack[undoStack.length - 1];
setRedoStack([...redoStack, content]);
setContent(previous);
setUndoStack(undoStack.slice(0, -1));
}
},
{
shortcut: "ctrl+z",
icon: "↩️",
when: () => undoStack.length > 0,
},
);
useAction(
`edit:redo:${documentId}`,
"Refazer",
() => {
if (redoStack.length > 0) {
const next = redoStack[redoStack.length - 1];
setUndoStack([...undoStack, content]);
setContent(next);
setRedoStack(redoStack.slice(0, -1));
}
},
{
shortcut: "ctrl+y",
icon: "↪️",
when: () => redoStack.length > 0,
},
);
// Comandos avançados
useCustomCommand({
key: `edit:find:${documentId}`,
label: "Buscar e Substituir",
icon: "🔍",
shortcut: "ctrl+f",
category: "edit",
handle: async () => {
const searchTerm = prompt("Buscar por:");
if (searchTerm) {
const replaceTerm = prompt("Substituir por:");
if (replaceTerm !== null) {
const newContent = content.replaceAll(searchTerm, replaceTerm);
setContent(newContent);
setIsDirty(true);
return { replaced: content.split(searchTerm).length - 1 };
}
}
},
});
// Comando de exportação
useCustomCommand({
key: `export:pdf:${documentId}`,
label: "Exportar como PDF",
icon: "📄",
shortcut: "ctrl+shift+e",
category: "file",
tags: ["export", "pdf", "download"],
handle: async () => {
const blob = await generatePDF(content);
downloadBlob(blob, `document-${documentId}.pdf`);
},
});
return (
<div className="editor">
<div className="toolbar">
<button onClick={() => saveCommand.invoke()}>
Salvar {isDirty && "*"}
</button>
</div>
<textarea
value={content}
onChange={(e) => {
setContent(e.target.value);
setIsDirty(true);
setUndoStack([...undoStack, content].slice(-50)); // Max 50 undos
}}
onSelect={(e) => {
const target = e.target as HTMLTextAreaElement;
setSelectedText(
target.value.substring(target.selectionStart, target.selectionEnd),
);
}}
/>
</div>
);
}
```
---
## Formulário com Ações
```tsx
import { useCustomCommand, useToggleCommand } from "@darksnow-ui/commander";
function ContactForm() {
const [formData, setFormData] = useState({
name: "",
email: "",
message: "",
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [autoSave, setAutoSave] = useState(true);
// Toggle de auto-save
useToggleCommand("form:autosave", "Auto-save", autoSave, setAutoSave, {
icon: autoSave ? "✅" : "❌",
category: "settings",
});
// Comando para limpar formulário
useCustomCommand({
key: "form:clear",
label: "Limpar Formulário",
icon: "🧹",
shortcut: "ctrl+shift+delete",
category: "form",
when: () => Object.values(formData).some((v) => v !== ""),
handle: async () => {
if (confirm("Limpar todos os campos?")) {
setFormData({ name: "", email: "", message: "" });
}
},
});
// Comando para preencher com dados de teste
useCustomCommand({
key: "form:fill-test",
label: "Preencher com Dados de Teste",
icon: "🧪",
category: "debug",
tags: ["test", "development"],
when: () => process.env.NODE_ENV === "development",
handle: async () => {
setFormData({
name: "João Silva",
email: "joao@example.com",
message: "Mensagem de teste para desenvolvimento",
});
},
});
// Comando para enviar
const submitCommand = useCustomCommand({
key: "form:submit",
label: "Enviar Formulário",
icon: "📤",
shortcut: "ctrl+enter",
category: "form",
when: () =>
formData.name && formData.email && formData.message && !isSubmitting,
handle: async () => {
setIsSubmitting(true);
try {
await api.submitContact(formData);
toast.success("Mensagem enviada!");
setFormData({ name: "", email: "", message: "" });
} catch (error) {
toast.error("Erro ao enviar");
} finally {
setIsSubmitting(false);
}
},
});
// Auto-save com debounce
useEffect(() => {
if (!autoSave) return;
const timer = setTimeout(() => {
if (Object.values(formData).some((v) => v !== "")) {
localStorage.setItem("contact-form-draft", JSON.stringify(formData));
}
}, 1000);
return () => clearTimeout(timer);
}, [formData, autoSave]);
return (
<form
onSubmit={(e) => {
e.preventDefault();
submitCommand.invoke();
}}
>
{/* Campos do formulário */}
</form>
);
}
```
---
## Lista com Ações Contextuais
```tsx
import {
useContextualCommands,
useCustomCommand,
} from "@darksnow-ui/commander";
function TaskList({ projectId }) {
const [tasks, setTasks] = useState([]);
const [selectedTasks, setSelectedTasks] = useState([]);
const [filter, setFilter] = useState("all");
// Comandos para cada task individual
useContextualCommands(tasks, (task) => ({
key: `task:toggle:${task.id}`,
label: task.completed
? `Reabrir: ${task.title}`
: `Completar: ${task.title}`,
icon: task.completed ? "↩️" : "✅",
category: "task",
tags: ["task", task.priority],
priority: task.priority === "high" ? 10 : 5,
handle: async () => {
await api.updateTask(task.id, { completed: !task.completed });
setTasks(
tasks.map((t) =>
t.id === task.id ? { ...t, completed: !t.completed } : t,
),
);
},
}));
// Comandos para seleção múltipla
useCustomCommand({
key: "tasks:select-all",
label: "Selecionar Todas",
icon: "☑️",
shortcut: "ctrl+a",
category: "selection",
when: () => tasks.length > 0,
handle: async () => {
setSelectedTasks(tasks.map((t) => t.id));
},
});
useCustomCommand({
key: "tasks:complete-selected",
label: `Completar ${selectedTasks.length} tarefas`,
icon: "✅",
category: "bulk",
when: () => selectedTasks.length > 0,
handle: async () => {
await api.bulkUpdate(selectedTasks, { completed: true });
setTasks(
tasks.map((t) =>
selectedTasks.includes(t.id) ? { ...t, completed: true } : t,
),
);
setSelectedTasks([]);
},
});
// Comandos de filtro
useCustomCommand({
key: "filter:active",
label: "Mostrar Apenas Ativas",
icon: "🔵",
shortcut: "alt+1",
category: "view",
when: () => filter !== "active",
handle: async () => setFilter("active"),
});
useCustomCommand({
key: "filter:completed",
label: "Mostrar Apenas Completas",
icon: "🟢",
shortcut: "alt+2",
category: "view",
when: () => filter !== "completed",
handle: async () => setFilter("completed"),
});
// Comando para adicionar nova task
useCustomCommand({
key: "task:add",
label: "Nova Tarefa",
icon: "➕",
shortcut: "ctrl+n",
category: "task",
handle: async () => {
const title = prompt("Título da tarefa:");
if (title) {
const newTask = await api.createTask({ projectId, title });
setTasks([...tasks, newTask]);
}
},
});
const filteredTasks = tasks.filter((task) => {
if (filter === "active") return !task.completed;
if (filter === "completed") return task.completed;
return true;
});
return (
<div>
<div className="filters">
<button onClick={() => setFilter("all")}>Todas</button>
<button onClick={() => setFilter("active")}>Ativas</button>
<button onClick={() => setFilter("completed")}>Completas</button>
</div>
{filteredTasks.map((task) => (
<TaskItem
key={task.id}
task={task}
isSelected={selectedTasks.includes(task.id)}
onToggleSelect={() => {
setSelectedTasks(
selectedTasks.includes(task.id)
? selectedTasks.filter((id) => id !== task.id)
: [...selectedTasks, task.id],
);
}}
/>
))}
</div>
);
}
```
---
## Player de Mídia
```tsx
import { useCustomCommand, useAction } from "@darksnow-ui/commander";
function VideoPlayer({ video }) {
const videoRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
const [volume, setVolume] = useState(1);
const [playbackRate, setPlaybackRate] = useState(1);
const [currentTime, setCurrentTime] = useState(0);
// Play/Pause
useCustomCommand({
key: `video:playpause:${video.id}`,
label: isPlaying ? "Pausar" : "Reproduzir",
icon: isPlaying ? "⏸️" : "▶️",
shortcut: "space",
category: "playback",
handle: async () => {
if (isPlaying) {
videoRef.current?.pause();
} else {
videoRef.current?.play();
}
setIsPlaying(!isPlaying);
},
});
// Controles de velocidade
const speeds = [0.5, 0.75, 1, 1.25, 1.5, 2];
speeds.forEach((speed) => {
useAction(
`video:speed:${speed}:${video.id}`,
`Velocidade ${speed}x`,
() => {
if (videoRef.current) {
videoRef.current.playbackRate = speed;
setPlaybackRate(speed);
}
},
{
category: "playback",
icon: "⚡",
when: () => playbackRate !== speed,
},
);
});
// Volume
useCustomCommand({
key: `video:mute:${video.id}`,
label: volume > 0 ? "Silenciar" : "Ativar Som",
icon: volume > 0 ? "🔇" : "🔊",
shortcut: "m",
category: "audio",
handle: async () => {
if (videoRef.current) {
const newVolume = volume > 0 ? 0 : 1;
videoRef.current.volume = newVolume;
setVolume(newVolume);
}
},
});
// Navegação
useAction(
`video:skip-forward:${video.id}`,
"Avançar 10s",
() => {
if (videoRef.current) {
videoRef.current.currentTime += 10;
}
},
{
shortcut: "right",
icon: "⏩",
category: "navigation",
},
);
useAction(
`video:skip-backward:${video.id}`,
"Voltar 10s",
() => {
if (videoRef.current) {
videoRef.current.currentTime -= 10;
}
},
{
shortcut: "left",
icon: "⏪",
category: "navigation",
},
);
// Fullscreen
useCustomCommand({
key: `video:fullscreen:${video.id}`,
label: "Tela Cheia",
icon: "🖥️",
shortcut: "f",
category: "view",
handle: async () => {
if (videoRef.current) {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
videoRef.current.requestFullscreen();
}
}
},
});
// Screenshot
useCustomCommand({
key: `video:screenshot:${video.id}`,
label: "Capturar Tela",
icon: "📸",
shortcut: "s",
category: "tools",
handle: async () => {
const canvas = document.createElement("canvas");
canvas.width = videoRef.current.videoWidth;
canvas.height = videoRef.current.videoHeight;
const ctx = canvas.getContext("2d");
ctx.drawImage(videoRef.current, 0, 0);
canvas.toBlob((blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `screenshot-${video.id}-${Date.now()}.png`;
a.click();
});
},
});
return (
<div className="video-player">
<video
ref={videoRef}
src={video.url}
onTimeUpdate={(e) => setCurrentTime(e.currentTarget.currentTime)}
/>
{/* Controles do player */}
</div>
);
}
```
---
## Dashboard com Widgets
```tsx
import { useCustomCommand, useModalCommand } from "@darksnow-ui/commander";
function Dashboard() {
const [widgets, setWidgets] = useState([]);
const [layout, setLayout] = useState("grid");
const [refreshInterval, setRefreshInterval] = useState(30000);
// Comando para adicionar widget
const addWidgetCommand = useModalCommand(
"dashboard:add-widget",
"Adicionar Widget",
async () => {
const widgetType = await showWidgetPicker();
if (widgetType) {
const newWidget = await createWidget(widgetType);
setWidgets([...widgets, newWidget]);
return newWidget;
}
},
{
icon: "➕",
shortcut: "ctrl+shift+w",
category: "dashboard",
},
);
// Comandos para cada widget
widgets.forEach((widget) => {
// Refresh individual
useCustomCommand({
key: `widget:refresh:${widget.id}`,
label: `Atualizar: ${widget.title}`,
icon: "🔄",
category: "widget",
tags: ["refresh", widget.type],
handle: async () => {
await widget.refresh();
toast.success(`${widget.title} atualizado`);
},
});
// Configurar widget
useModalCommand(
`widget:config:${widget.id}`,
`Configurar: ${widget.title}`,
async () => {
const config = await showWidgetConfig(widget);
if (config) {
await updateWidget(widget.id, config);
setWidgets(
widgets.map((w) => (w.id === widget.id ? { ...w, ...config } : w)),
);
}
},
{
icon: "⚙️",
category: "widget",
},
);
// Remover widget
useCustomCommand({
key: `widget:remove:${widget.id}`,
label: `Remover: ${widget.title}`,
icon: "🗑️",
category: "widget",
priority: -1,
handle: async () => {
if (confirm(`Remover ${widget.title}?`)) {
setWidgets(widgets.filter((w) => w.id !== widget.id));
}
},
});
});
// Comandos de layout
useCustomCommand({
key: "dashboard:layout:grid",
label: "Layout em Grade",
icon: "⚏",
category: "view",
when: () => layout !== "grid",
handle: async () => setLayout("grid"),
});
useCustomCommand({
key: "dashboard:layout:list",
label: "Layout em Lista",
icon: "☰",
category: "view",
when: () => layout !== "list",
handle: async () => setLayout("list"),
});
// Refresh global
useCustomCommand({
key: "dashboard:refresh-all",
label: "Atualizar Todos os Widgets",
icon: "🔄",
shortcut: "ctrl+r",
category: "dashboard",
handle: async () => {
await Promise.all(widgets.map((w) => w.refresh()));
toast.success("Dashboard atualizado");
},
});
// Auto-refresh toggle
useToggleCommand(
"dashboard:auto-refresh",
"Auto-refresh",
refreshInterval > 0,
(enabled) => setRefreshInterval(enabled ? 30000 : 0),
{
category: "settings",
icon: "⏱️",
},
);
// Exportar dashboard
useCustomCommand({
key: "dashboard:export",
label: "Exportar Dashboard",
icon: "📊",
shortcut: "ctrl+e",
category: "dashboard",
tags: ["export", "download"],
handle: async () => {
const format = await showExportOptions();
if (format) {
const data = await exportDashboard(widgets, format);
downloadFile(data, `dashboard-${Date.now()}.${format}`);
}
},
});
return (
<div className={`dashboard layout-${layout}`}>
<button onClick={() => addWidgetCommand.invoke()}>Add Widget</button>
{widgets.map((widget) => (
<Widget key={widget.id} {...widget} />
))}
</div>
);
}
```
---
## Tabela de Dados
```tsx
import {
useCustomCommand,
useContextualCommands,
} from "@darksnow-ui/commander";
function DataTable({ data, columns, onUpdate }) {
const [selectedRows, setSelectedRows] = useState([]);
const [sortConfig, setSortConfig] = useState(null);
const [filters, setFilters] = useState({});
const [editingCell, setEditingCell] = useState(null);
// Comandos para linhas selecionadas
useCustomCommand({
key: "table:export-selected",
label: `Exportar ${selectedRows.length} linhas`,
icon: "📥",
shortcut: "ctrl+shift+e",
category: "table",
when: () => selectedRows.length > 0,
handle: async () => {
const selectedData = data.filter((row) => selectedRows.includes(row.id));
const csv = convertToCSV(selectedData);
downloadFile(csv, "export.csv");
},
});
useCustomCommand({
key: "table:delete-selected",
label: `Deletar ${selectedRows.length} linhas`,
icon: "🗑️",
shortcut: "delete",
category: "table",
when: () => selectedRows.length > 0,
priority: -1,
handle: async () => {
if (confirm(`Deletar ${selectedRows.length} linhas?`)) {
await onUpdate("delete", selectedRows);
setSelectedRows([]);
}
},
});
// Comandos de ordenação para cada coluna
columns.forEach((column) => {
if (column.sortable) {
useCustomCommand({
key: `sort:${column.key}:asc`,
label: `Ordenar por ${column.label} (A-Z)`,
icon: "⬆️",
category: "sort",
when: () =>
sortConfig?.key !== column.key || sortConfig?.direction !== "asc",
handle: async () => {
setSortConfig({ key: column.key, direction: "asc" });
},
});
useCustomCommand({
key: `sort:${column.key}:desc`,
label: `Ordenar por ${column.label} (Z-A)`,
icon: "⬇️",
category: "sort",
when: () =>
sortConfig?.key !== column.key || sortConfig?.direction !== "desc",
handle: async () => {
setSortConfig({ key: column.key, direction: "desc" });
},
});
}
});
// Comandos contextuais para cada linha
useContextualCommands(data, (row) => ({
key: `row:edit:${row.id}`,
label: `Editar: ${row.name || row.title || row.id}`,
icon: "✏️",
category: "row",
handle: async () => {
const updatedRow = await showRowEditor(row);
if (updatedRow) {
await onUpdate("update", row.id, updatedRow);
}
},
}));
// Comandos de filtro
useCustomCommand({
key: "table:clear-filters",
label: "Limpar Filtros",
icon: "🧹",
category: "filter",
when: () => Object.keys(filters).length > 0,
handle: async () => {
setFilters({});
},
});
// Comando de busca global
useCustomCommand({
key: "table:search",
label: "Buscar na Tabela",
icon: "🔍",
shortcut: "ctrl+f",
category: "table",
handle: async () => {
const searchTerm = prompt("Buscar por:");
if (searchTerm) {
setFilters({ ...filters, global: searchTerm });
}
},
});
// Copiar dados selecionados
useCustomCommand({
key: "table:copy-selected",
label: "Copiar Seleção",
icon: "📋",
shortcut: "ctrl+c",
category: "table",
when: () => selectedRows.length > 0,
handle: async () => {
const selectedData = data.filter((row) => selectedRows.includes(row.id));
const text = selectedData
.map((row) => columns.map((col) => row[col.key]).join("\t"))
.join("\n");
await navigator.clipboard.writeText(text);
toast.success("Copiado para área de transferência");
},
});
const sortedAndFilteredData = useMemo(() => {
let result = [...data];
// Apply filters
Object.entries(filters).forEach(([key, value]) => {
if (key === "global") {
result = result.filter((row) =>
Object.values(row).some((v) =>
String(v).toLowerCase().includes(value.toLowerCase()),
),
);
} else {
result = result.filter((row) =>
String(row[key]).toLowerCase().includes(value.toLowerCase()),
);
}
});
// Apply sorting
if (sortConfig) {
result.sort((a, b) => {
const aVal = a[sortConfig.key];
const bVal = b[sortConfig.key];
if (sortConfig.direction === "asc") {
return aVal > bVal ? 1 : -1;
} else {
return aVal < bVal ? 1 : -1;
}
});
}
return result;
}, [data, filters, sortConfig]);
return (
<div className="data-table">
<table>
<thead>
<tr>
{columns.map((column) => (
<th key={column.key}>
{column.label}
{column.sortable && (
<button
onClick={() => {
const newDir =
sortConfig?.key === column.key &&
sortConfig?.direction === "asc"
? "desc"
: "asc";
setSortConfig({ key: column.key, direction: newDir });
}}
>
{sortConfig?.key === column.key
? sortConfig.direction === "asc"
? "⬆️"
: "⬇️"
: "↕️"}
</button>
)}
</th>
))}
</tr>
</thead>
<tbody>
{sortedAndFilteredData.map((row) => (
<tr
key={row.id}
className={selectedRows.includes(row.id) ? "selected" : ""}
onClick={() => {
setSelectedRows(
selectedRows.includes(row.id)
? selectedRows.filter((id) => id !== row.id)
: [...selectedRows, row.id],
);
}}
>
{columns.map((column) => (
<td key={column.key}>{row[column.key]}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
```
---
## Integração com Rotas
```tsx
import { useNavigationCommand, useCustomCommand } from "@darksnow-ui/commander";
import { useRouter, useParams } from "next/router";
function ProductPage() {
const router = useRouter();
const { productId } = useParams();
const [product, setProduct] = useState(null);
const [relatedProducts, setRelatedProducts] = useState([]);
// Navegação entre produtos
useNavigationCommand(
"nav:products",
"Voltar para Produtos",
"/products",
router.push,
{
icon: "🏪",
shortcut: "alt+p",
},
);
// Navegação para produtos relacionados
relatedProducts.forEach((related) => {
useNavigationCommand(
`nav:product:${related.id}`,
`Ver: ${related.name}`,
`/products/${related.id}`,
router.push,
{
icon: "🔗",
category: "related",
tags: ["product", related.category],
},
);
});
// Ações do produto
useCustomCommand({
key: `product:share:${productId}`,
label: "Compartilhar Produto",
icon: "🔗",
shortcut: "ctrl+shift+s",
category: "product",
handle: async () => {
const url = `${window.location.origin}/products/${productId}`;
if (navigator.share) {
await navigator.share({
title: product.name,
text: product.description,
url,
});
} else {
await navigator.clipboard.writeText(url);
toast.success("Link copiado!");
}
},
});
// Adicionar ao carrinho
const addToCartCommand = useCustomCommand({
key: `product:cart:${productId}`,
label: "Adicionar ao Carrinho",
icon: "🛒",
shortcut: "ctrl+enter",
category: "product",
priority: 10,
when: () => product?.inStock,
handle: async () => {
await addToCart(productId);
toast.success("Adicionado ao carrinho!");
},
});
// Admin commands
const isAdmin = useIsAdmin();
useCustomCommand({
key: `product:edit:${productId}`,
label: "Editar Produto",
icon: "✏️",
category: "admin",
when: () => isAdmin,
handle: async () => {
router.push(`/admin/products/${productId}/edit`);
},
});
useCustomCommand({
key: `product:analytics:${productId}`,
label: "Ver Analytics",
icon: "📊",
category: "admin",
when: () => isAdmin,
handle: async () => {
const analytics = await fetchProductAnalytics(productId);
showAnalyticsModal(analytics);
},
});
return (
<div>
{/* UI do produto */}
<button onClick={() => addToCartCommand.invoke()}>
Adicionar ao Carrinho
</button>
</div>
);
}
```
---
## 💡 Dicas e Melhores Práticas
### 1. **Use IDs únicos nas keys**
```tsx
// ✅ Bom - ID único por instância
useCustomCommand({
key: `modal:save:${modalId}`,
// ...
});
// ❌ Evite - Pode causar conflitos
useCustomCommand({
key: "modal:save",
// ...
});
```
### 2. **Aproveite a função `when`**
```tsx
// Comando só aparece quando relevante
useCustomCommand({
key: "form:submit",
label: "Enviar",
when: () => isFormValid && !isSubmitting,
handle: async () => {
/* ... */
},
});
```
### 3. **Use categorias para organização**
```tsx
// Agrupe comandos relacionados
category: "file"; // Operações de arquivo
category: "edit"; // Edição
category: "view"; // Visualização
category: "tools"; // Ferramentas
category: "debug"; // Desenvolvimento
```
### 4. **Retorne valores úteis**
```tsx
// Retorne informações sobre a operação
handle: async () => {
const result = await saveFile();
return {
success: true,
fileSize: result.size,
savedAt: new Date(),
};
};
```
### 5. **Use os hooks especializados**
```tsx
// Para ações simples
useAction("toggle:sidebar", "Toggle Sidebar", () => {
setSidebarOpen(!sidebarOpen);
});
// Para toggles
useToggleCommand("dark:mode", "Dark Mode", isDark, setIsDark);
// Para modais
useModalCommand("user:edit", "Edit User", async () => {
return await showUserEditModal();
});
```
### 6. **Integre com o estado do componente**
```tsx
// Comandos que refletem o estado atual
const [filter, setFilter] = useState("all")[
("all", "active", "completed")
].forEach((filterType) => {
useCustomCommand({
key: `filter:${filterType}`,
label: `Show ${filterType}`,
when: () => filter !== filterType,
handle: async () => setFilter(filterType),
});
});
```
---
## 🎯 Conclusão
O `useCustomCommand` transforma qualquer componente em uma interface poderosa e acessível. Os comandos aparecem automaticamente no Command Palette, permitindo que usuários avançados naveguem e controlem sua aplicação com eficiência.
**Lembre-se**: Os comandos são temporários e existem apenas enquanto o componente está montado, garantindo que o Command Palette sempre reflita o contexto atual da aplicação.