Files
tolaria/src/hooks/useNavigationHistory.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

74 lines
2.4 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react'
interface HistoryState {
stack: string[]
cursor: number
}
const EMPTY: HistoryState = { stack: [], cursor: -1 }
const MAX_HISTORY = 200
/**
* Manages a browser-style back/forward navigation stack of note paths.
*
* - `push(path)` adds a path, clearing any forward history.
* - `goBack()` / `goForward()` return the target path (or null).
* - Deleted/invalid paths are skipped transparently.
*/
export function useNavigationHistory() {
const [state, setState] = useState<HistoryState>(EMPTY)
const stateRef = useRef(state)
useEffect(() => { stateRef.current = state })
const push = useCallback((path: string) => {
setState((prev) => {
if (prev.cursor >= 0 && prev.stack[prev.cursor] === path) return prev
const truncated = prev.stack.slice(0, prev.cursor + 1)
const stack = truncated.length >= MAX_HISTORY
? [...truncated.slice(truncated.length - MAX_HISTORY + 1), path]
: [...truncated, path]
return { stack, cursor: stack.length - 1 }
})
}, [])
const canGoBack = state.cursor > 0
const canGoForward = state.cursor < state.stack.length - 1
const goBack = useCallback((isValid?: (path: string) => boolean): string | null => {
const { stack, cursor } = stateRef.current
for (let i = cursor - 1; i >= 0; i--) {
if (!isValid || isValid(stack[i])) {
setState({ stack, cursor: i })
return stack[i]
}
}
return null
}, [])
const goForward = useCallback((isValid?: (path: string) => boolean): string | null => {
const { stack, cursor } = stateRef.current
for (let i = cursor + 1; i < stack.length; i++) {
if (!isValid || isValid(stack[i])) {
setState({ stack, cursor: i })
return stack[i]
}
}
return null
}, [])
/** Remove a path from history (e.g. when a tab is closed). */
const removePath = useCallback((path: string) => {
setState((prev) => {
const idx = prev.stack.indexOf(path)
if (idx === -1) return prev
const stack = prev.stack.filter((p) => p !== path)
const cursor = prev.cursor > idx ? prev.cursor - 1
: prev.cursor === idx ? Math.min(prev.cursor, stack.length - 1)
: prev.cursor
return { stack, cursor: Math.max(cursor, stack.length > 0 ? 0 : -1) }
})
}, [])
return { canGoBack, canGoForward, push, goBack, goForward, removePath }
}