feat: MVP-5 Sprint 2 - Command Palette and Global Shortcuts

- Add Command Palette (Ctrl+K / Cmd+K) with search and navigation
- Add global keyboard shortcuts: g h (dashboard), g n (notes), n (new note)
- Add keyboard shortcuts help dialog (?)
- Add shortcuts provider component
- Shortcuts ignore inputs/textareas for proper UX
This commit is contained in:
2026-03-22 18:22:28 -03:00
parent 8c80a12b81
commit cde0a143a5
6 changed files with 215 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import { Dialog, DialogContent } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { commands, CommandItem } from '@/lib/command-items'
import { Search, FileText, Settings, Home, Plus } from 'lucide-react'
export function CommandPalette() {
const [open, setOpen] = useState(false)
const [query, setQuery] = useState('')
const [selectedIndex, setSelectedIndex] = useState(0)
const router = useRouter()
// Listen for Ctrl+K / Cmd+K
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault()
setOpen(true)
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [])
// Filter commands by query
const filtered = commands.filter(cmd =>
cmd.label.toLowerCase().includes(query.toLowerCase()) ||
cmd.keywords?.some(k => k.includes(query.toLowerCase()))
)
// Keyboard navigation
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') setOpen(false)
if (e.key === 'ArrowDown') setSelectedIndex(i => Math.min(i + 1, filtered.length - 1))
if (e.key === 'ArrowUp') setSelectedIndex(i => Math.max(i - 1, 0))
if (e.key === 'Enter' && filtered[selectedIndex]) {
executeCommand(filtered[selectedIndex])
}
}
// Execute command
const executeCommand = (cmd: CommandItem) => {
router.push(cmd.id === 'action-new' ? '/new' : `/${cmd.id.split('-')[1]}`)
setOpen(false)
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-md p-0 gap-0">
<div className="flex items-center border-b px-3">
<Search className="h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar comandos..."
className="border-0 focus-visible:ring-0"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
/>
</div>
<div className="max-h-80 overflow-y-auto">
{filtered.map((cmd, i) => (
<button
key={cmd.id}
onClick={() => executeCommand(cmd)}
className={`w-full px-3 py-2 text-left flex items-center gap-2 ${
i === selectedIndex ? 'bg-muted' : 'hover:bg-muted/50'
}`}
>
<span className="text-sm">{cmd.label}</span>
<span className="text-xs text-muted-foreground ml-auto">{cmd.group}</span>
</button>
))}
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,39 @@
'use client'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Keyboard } from 'lucide-react'
const shortcuts = [
{ keys: ['n'], description: 'Nueva nota' },
{ keys: ['g', 'h'], description: 'Ir al Dashboard' },
{ keys: ['g', 'n'], description: 'Ir a Notas' },
{ keys: ['/'], description: 'Enfocar búsqueda' },
{ keys: ['?'], description: 'Mostrar atajos' },
{ keys: ['Ctrl', 'K'], description: 'Command Palette' },
]
export function KeyboardShortcutsDialog({ open, onOpenChange }: { open: boolean, onOpenChange: (o: boolean) => void }) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Keyboard className="h-5 w-5" />
Atajos de teclado
</DialogTitle>
</DialogHeader>
<div className="divide-y">
{shortcuts.map((s) => (
<div key={s.description} className="flex justify-between py-2">
<span className="text-sm">{s.description}</span>
<div className="flex gap-1">
{s.keys.map((k) => (
<kbd key={k} className="px-2 py-1 bg-muted rounded text-xs">{k}</kbd>
))}
</div>
</div>
))}
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,12 @@
'use client'
import { useGlobalShortcuts } from '@/hooks/use-global-shortcuts'
import { KeyboardShortcutsDialog } from '@/components/keyboard-shortcuts-dialog'
export function ShortcutsProvider() {
const { showHelp, setShowHelp } = useGlobalShortcuts()
return (
<KeyboardShortcutsDialog open={showHelp} onOpenChange={setShowHelp} />
)
}