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>
2026-03-01 19:32:15 +01:00
|
|
|
import { useState, useCallback, useEffect, useRef } from 'react'
|
|
|
|
|
import type { VirtuosoHandle } from 'react-virtuoso'
|
|
|
|
|
import type { VaultEntry } from '../types'
|
2026-03-09 13:05:18 +01:00
|
|
|
import { prefetchNoteContent } from './useTabManagement'
|
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>
2026-03-01 19:32:15 +01:00
|
|
|
|
|
|
|
|
interface NoteListKeyboardOptions {
|
|
|
|
|
items: VaultEntry[]
|
|
|
|
|
selectedNotePath: string | null
|
|
|
|
|
onOpen: (entry: VaultEntry) => void
|
|
|
|
|
enabled: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useNoteListKeyboard({
|
|
|
|
|
items, selectedNotePath, onOpen, enabled,
|
|
|
|
|
}: NoteListKeyboardOptions) {
|
|
|
|
|
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
|
|
|
|
const virtuosoRef = useRef<VirtuosoHandle>(null)
|
|
|
|
|
|
|
|
|
|
// Reset highlight when items change (filter/sort/selection changed)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setHighlightedIndex(-1) // eslint-disable-line react-hooks/set-state-in-effect -- reset on data change
|
|
|
|
|
}, [items])
|
|
|
|
|
|
|
|
|
|
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
|
|
|
if (!enabled || items.length === 0) return
|
|
|
|
|
if (e.metaKey || e.ctrlKey || e.altKey) return
|
|
|
|
|
|
|
|
|
|
if (e.key === 'ArrowDown') {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
setHighlightedIndex(prev => {
|
|
|
|
|
const next = Math.min((prev < 0 ? -1 : prev) + 1, items.length - 1)
|
|
|
|
|
virtuosoRef.current?.scrollToIndex({ index: next, behavior: 'auto' })
|
2026-03-09 13:05:18 +01:00
|
|
|
if (next >= 0 && next < items.length) prefetchNoteContent(items[next].path)
|
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>
2026-03-01 19:32:15 +01:00
|
|
|
return next
|
|
|
|
|
})
|
|
|
|
|
} else if (e.key === 'ArrowUp') {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
setHighlightedIndex(prev => {
|
|
|
|
|
const next = Math.max((prev < 0 ? items.length : prev) - 1, 0)
|
|
|
|
|
virtuosoRef.current?.scrollToIndex({ index: next, behavior: 'auto' })
|
2026-03-09 13:05:18 +01:00
|
|
|
if (next >= 0 && next < items.length) prefetchNoteContent(items[next].path)
|
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>
2026-03-01 19:32:15 +01:00
|
|
|
return next
|
|
|
|
|
})
|
|
|
|
|
} else if (e.key === 'Enter' && highlightedIndex >= 0 && highlightedIndex < items.length) {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
onOpen(items[highlightedIndex])
|
|
|
|
|
}
|
|
|
|
|
}, [enabled, items, highlightedIndex, onOpen])
|
|
|
|
|
|
|
|
|
|
const handleFocus = useCallback(() => {
|
|
|
|
|
if (highlightedIndex >= 0 || items.length === 0) return
|
|
|
|
|
const activeIdx = selectedNotePath
|
|
|
|
|
? items.findIndex(n => n.path === selectedNotePath)
|
|
|
|
|
: -1
|
|
|
|
|
setHighlightedIndex(activeIdx >= 0 ? activeIdx : 0)
|
|
|
|
|
}, [highlightedIndex, items, selectedNotePath])
|
|
|
|
|
|
|
|
|
|
const highlightedPath = (highlightedIndex >= 0 && highlightedIndex < items.length)
|
|
|
|
|
? items[highlightedIndex].path
|
|
|
|
|
: null
|
|
|
|
|
|
|
|
|
|
return { highlightedPath, handleKeyDown, handleFocus, virtuosoRef }
|
|
|
|
|
}
|