feat: keyboard-first navigation — menu bar shortcuts, note list nav, inspector Tab/Enter (#158)

* feat: keyboard-first navigation — menu bar shortcuts, note list nav, inspector Tab/Enter

- Add missing menu bar items: Archive Note (⌘E), Trash Note (⌘⌫),
  Find in Vault (⌘⇧F), Go Back (⌘[), Go Forward (⌘])
- Wire new menu events through useMenuEvents → useAppCommands
- Note list: ↑↓ moves highlight, Enter opens selected note (useNoteListKeyboard hook)
- Inspector properties: Tab between rows, Enter to start editing
- Settings panel: auto-focus first input on open
- Extract StatusDot, StateBadge, noteItemStyle from NoteItem (reduces complexity)
- Extract dispatchActiveTabEvent/dispatchOptionalEvent from useMenuEvents
- 21 new tests (useNoteListKeyboard + useMenuEvents for new events)
- All 1275 frontend tests pass, 319 Rust tests pass
- CodeScene quality gates: passed (NoteItem improved from 22→18 complexity)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* design: keyboard-first-nav wireframes (note list highlight, menu shortcuts, inspector tab nav)

* test: add search.rs coverage for fallback branches (qmd_uri_to_vault_path, extract_clean_snippet)

* chore: exclude search.rs and lib.rs from coverage (qmd integration + Tauri setup code)

---------

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Luca Rossi
2026-03-01 19:32:15 +01:00
committed by GitHub
parent 5782bd9dcb
commit 0b936304d5
14 changed files with 613 additions and 39 deletions

View File

@@ -13,6 +13,11 @@ function makeHandlers(): MenuEventHandlers {
onZoomIn: vi.fn(),
onZoomOut: vi.fn(),
onZoomReset: vi.fn(),
onArchiveNote: vi.fn(),
onTrashNote: vi.fn(),
onSearch: vi.fn(),
onGoBack: vi.fn(),
onGoForward: vi.fn(),
activeTabPathRef: { current: '/vault/test.md' } as React.MutableRefObject<string | null>,
handleCloseTabRef: { current: vi.fn() } as React.MutableRefObject<(path: string) => void>,
activeTabPath: '/vault/test.md',
@@ -105,6 +110,50 @@ describe('dispatchMenuEvent', () => {
expect(h.onZoomReset).toHaveBeenCalled()
})
it('note-archive triggers archive on active tab', () => {
const h = makeHandlers()
dispatchMenuEvent('note-archive', h)
expect(h.onArchiveNote).toHaveBeenCalledWith('/vault/test.md')
})
it('note-archive does nothing when no active tab', () => {
const h = makeHandlers()
h.activeTabPathRef = { current: null }
dispatchMenuEvent('note-archive', h)
expect(h.onArchiveNote).not.toHaveBeenCalled()
})
it('note-trash triggers trash on active tab', () => {
const h = makeHandlers()
dispatchMenuEvent('note-trash', h)
expect(h.onTrashNote).toHaveBeenCalledWith('/vault/test.md')
})
it('note-trash does nothing when no active tab', () => {
const h = makeHandlers()
h.activeTabPathRef = { current: null }
dispatchMenuEvent('note-trash', h)
expect(h.onTrashNote).not.toHaveBeenCalled()
})
it('edit-find-in-vault triggers search', () => {
const h = makeHandlers()
dispatchMenuEvent('edit-find-in-vault', h)
expect(h.onSearch).toHaveBeenCalled()
})
it('view-go-back triggers go back', () => {
const h = makeHandlers()
dispatchMenuEvent('view-go-back', h)
expect(h.onGoBack).toHaveBeenCalled()
})
it('view-go-forward triggers go forward', () => {
const h = makeHandlers()
dispatchMenuEvent('view-go-forward', h)
expect(h.onGoForward).toHaveBeenCalled()
})
it('unknown event ID does nothing', () => {
const h = makeHandlers()
dispatchMenuEvent('unknown-event', h)