Files
tolaria/src/hooks/useAppKeyboard.ts
Luca Rossi 98f0e68e38 feat: back/forward navigation between opened notes (#75)
* feat: add useNavigationHistory hook for browser-style back/forward

Pure state management hook that tracks a navigation stack of note paths
with cursor-based back/forward traversal. Handles edge cases: duplicate
pushes (no-op), forward stack cleared on new push, invalid path skipping,
and path removal when tabs close.

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

* feat: integrate back/forward navigation into UI and keyboard shortcuts

- Add Back/Forward arrow buttons to TabBar (left of tabs)
- Wire useNavigationHistory through App → Editor → TabBar
- Add Cmd+[ and Cmd+] keyboard shortcuts via useAppKeyboard
- Register Go Back/Go Forward in command palette
- History tracks active tab changes, skips closed tabs gracefully
- Navigation from history doesn't re-push to stack (ref guard)

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

* feat: add mouse button 3/4 and trackpad swipe for back/forward

- Mouse buttons 3 (back) and 4 (forward) trigger navigation
- macOS trackpad two-finger horizontal swipe triggers back/forward
  using accumulated wheel deltaX with a 120px threshold
- Debounced reset after 300ms of inactivity

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

* design: add back-forward-nav.pen with 3 state frames

Shows: default (both disabled), one note visited (back disabled),
two notes visited (back enabled). Matches tab bar button placement.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 18:39:12 +00:00

98 lines
3.1 KiB
TypeScript

import { useEffect } from 'react'
import type { ViewMode } from './useViewMode'
interface KeyboardActions {
onQuickOpen: () => void
onCommandPalette: () => void
onSearch: () => void
onCreateNote: () => void
onSave: () => void
onOpenSettings: () => void
onTrashNote: (path: string) => void
onArchiveNote: (path: string) => void
onSetViewMode: (mode: ViewMode) => void
onGoBack?: () => void
onGoForward?: () => void
activeTabPathRef: React.MutableRefObject<string | null>
handleCloseTabRef: React.MutableRefObject<(path: string) => void>
}
type ShortcutHandler = () => void
const TEXT_EDITING_KEYS = new Set(['Backspace', 'Delete'])
function isTextInputFocused(): boolean {
const tag = document.activeElement?.tagName
return tag === 'INPUT' || tag === 'TEXTAREA'
}
const VIEW_MODE_KEYS: Record<string, ViewMode> = {
'1': 'editor-only',
'2': 'editor-list',
'3': 'all',
}
function isCmdOnly(e: KeyboardEvent): boolean {
return (e.metaKey || e.ctrlKey) && !e.altKey
}
function handleViewModeKey(e: KeyboardEvent, onSetViewMode: (m: ViewMode) => void): boolean {
if (!isCmdOnly(e)) return false
const mode = VIEW_MODE_KEYS[e.key]
if (!mode) return false
e.preventDefault()
onSetViewMode(mode)
return true
}
function handleCmdKey(e: KeyboardEvent, keyMap: Record<string, ShortcutHandler>): boolean {
const mod = e.metaKey || e.ctrlKey
if (!mod) return false
const handler = keyMap[e.key]
if (!handler) return false
if (TEXT_EDITING_KEYS.has(e.key) && isTextInputFocused()) return false
e.preventDefault()
handler()
return true
}
export function useAppKeyboard({
onQuickOpen, onCommandPalette, onSearch, onCreateNote, onSave, onOpenSettings, onTrashNote, onArchiveNote,
onSetViewMode, onGoBack, onGoForward, activeTabPathRef, handleCloseTabRef,
}: KeyboardActions) {
useEffect(() => {
const withActiveTab = (fn: (path: string) => void): ShortcutHandler => () => {
const path = activeTabPathRef.current
if (path) fn(path)
}
const cmdKeyMap: Record<string, ShortcutHandler> = {
k: onCommandPalette,
p: onQuickOpen,
n: onCreateNote,
s: onSave,
',': onOpenSettings,
e: withActiveTab(onArchiveNote),
w: withActiveTab((path) => handleCloseTabRef.current(path)),
Backspace: withActiveTab(onTrashNote),
Delete: withActiveTab(onTrashNote),
'[': () => onGoBack?.(),
']': () => onGoForward?.(),
}
const handleKeyDown = (e: KeyboardEvent) => {
// Cmd+Shift+F: full-text search (distinct from Cmd+F browser find)
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'f') {
e.preventDefault()
onSearch()
return
}
if (!handleViewModeKey(e, onSetViewMode)) {
handleCmdKey(e, cmdKeyMap)
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [onQuickOpen, onCommandPalette, onSearch, onCreateNote, onSave, onOpenSettings, onTrashNote, onArchiveNote, activeTabPathRef, handleCloseTabRef, onSetViewMode, onGoBack, onGoForward])
}