feat: MVP-5 P2 - Export/Import, Settings, Tests y Validaciones

- Ticket 10: Navegación completa de listas por teclado (↑↓ Enter E F P)
- Ticket 13: Historial de navegación contextual con recent-context-list
- Ticket 17: Exportación mejorada a Markdown con frontmatter
- Ticket 18: Exportación HTML simple y legible
- Ticket 19: Importador Markdown mejorado con frontmatter, tags, wiki links
- Ticket 20: Importador Obsidian-compatible (wiki links, #tags inline)
- Ticket 21: Centro de respaldo y portabilidad en Settings
- Ticket 22: Configuración visible de feature flags
- Ticket 24: Tests de command palette y captura externa
- Ticket 25: Harden de validaciones y límites (50MB backup, 10K notas, etc)
This commit is contained in:
2026-03-22 19:39:55 -03:00
parent 8d56f34d68
commit e66a678160
24 changed files with 1286 additions and 42 deletions

View File

@@ -0,0 +1,87 @@
import { useEffect, useCallback, useState } from 'react'
import { useRouter } from 'next/navigation'
import { Note } from '@/types/note'
interface UseNoteListKeyboardOptions {
notes: Note[]
onEdit?: (noteId: string) => void
onFavorite?: (noteId: string) => void
onPin?: (noteId: string) => void
}
export function useNoteListKeyboard({
notes,
onEdit,
onFavorite,
onPin,
}: UseNoteListKeyboardOptions) {
const [selectedIndex, setSelectedIndex] = useState(-1)
const router = useRouter()
// Reset selection when notes change
useEffect(() => {
setSelectedIndex(-1)
}, [notes.length])
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
// Ignore if in input/textarea/contenteditable
const target = e.target as HTMLElement
if (
target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.isContentEditable
) {
return
}
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
setSelectedIndex((prev) =>
prev < notes.length - 1 ? prev + 1 : notes.length - 1
)
break
case 'ArrowUp':
e.preventDefault()
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : -1))
break
case 'Enter':
e.preventDefault()
if (selectedIndex >= 0 && notes[selectedIndex]) {
router.push(`/notes/${notes[selectedIndex].id}`)
}
break
case 'e':
case 'E':
if (selectedIndex >= 0 && notes[selectedIndex] && onEdit) {
e.preventDefault()
onEdit(notes[selectedIndex].id)
}
break
case 'f':
case 'F':
if (selectedIndex >= 0 && notes[selectedIndex] && onFavorite) {
e.preventDefault()
onFavorite(notes[selectedIndex].id)
}
break
case 'p':
case 'P':
if (selectedIndex >= 0 && notes[selectedIndex] && onPin) {
e.preventDefault()
onPin(notes[selectedIndex].id)
}
break
}
},
[notes, selectedIndex, router, onEdit, onFavorite, onPin]
)
useEffect(() => {
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [handleKeyDown])
return { selectedIndex, setSelectedIndex }
}