@darksnow-ui/commander
Version:
Command pattern implementation with React hooks for building command palettes and keyboard-driven UIs
1,219 lines (1,092 loc) • 30.5 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.