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,86 @@
'use client'
import { useCallback } from 'react'
import { Note } from '@/types/note'
import { NoteCard } from './note-card'
import { useNoteListKeyboard } from '@/hooks/use-note-list-keyboard'
import { toast } from 'sonner'
interface KeyboardNavigableNoteListProps {
notes: Note[]
onEdit?: (noteId: string) => void
}
export function KeyboardNavigableNoteList({
notes,
onEdit,
}: KeyboardNavigableNoteListProps) {
const handleFavorite = useCallback(async (noteId: string) => {
try {
const res = await fetch(`/api/notes/${noteId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ isFavorite: true }),
})
if (res.ok) {
toast.success('Añadido a favoritos')
window.location.reload()
}
} catch {
toast.error('Error al añadir a favoritos')
}
}, [])
const handlePin = useCallback(async (noteId: string) => {
try {
const res = await fetch(`/api/notes/${noteId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ isPinned: true }),
})
if (res.ok) {
toast.success('Nota fijada')
window.location.reload()
}
} catch {
toast.error('Error al fijar nota')
}
}, [])
const { selectedIndex } = useNoteListKeyboard({
notes,
onEdit,
onFavorite: handleFavorite,
onPin: handlePin,
})
if (notes.length === 0) {
return (
<div className="text-center py-12 text-gray-500">
<p className="text-lg">No hay notas todavía</p>
<p className="text-sm">Crea tu primera nota para comenzar</p>
</div>
)
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{notes.map((note, index) => (
<div
key={note.id}
className={`relative ${index === selectedIndex ? 'ring-2 ring-primary ring-offset-2 rounded-lg' : ''}`}
data-selected={index === selectedIndex}
>
<NoteCard note={note} />
{index === selectedIndex && (
<div className="absolute bottom-2 right-2 flex gap-1">
<span className="px-1.5 py-0.5 bg-muted text-xs rounded text-muted-foreground">
Enter: abrir | E: editar | F: favoritar | P: fijar
</span>
</div>
)}
</div>
))}
</div>
)
}