diff --git a/packages/app-core/src/components/ExcalidrawView.tsx b/packages/app-core/src/components/ExcalidrawView.tsx
index 83729a26..e9670198 100644
--- a/packages/app-core/src/components/ExcalidrawView.tsx
+++ b/packages/app-core/src/components/ExcalidrawView.tsx
@@ -22,6 +22,7 @@ export function ExcalidrawView({ path }: { path: string }): JSX.Element {
// Match the app's light/dark theme.
const themeId = useStore((s) => s.themeId)
+ const setFocusedPanel = useStore((s) => s.setFocusedPanel)
const excalidrawTheme = THEMES.find((t) => t.id === themeId)?.mode === 'dark' ? 'dark' : 'light'
useEffect(() => {
@@ -63,7 +64,13 @@ export function ExcalidrawView({ path }: { path: string }): JSX.Element {
}
return (
-
+
setFocusedPanel('editor')}
+ onMouseDownCapture={() => setFocusedPanel('editor')}
+ >
>()
const nextBufferTimer = useRef>()
const leaderTimer = useRef>()
+ const excalidrawSpacePanTimer = useRef>()
+ const excalidrawSpacePanTarget = useRef(null)
+ const excalidrawSpacePanActive = useRef(false)
// Hint mode needs a render (to mount HintOverlay), so it's state.
const [hintActive, setHintActive] = useState(false)
@@ -285,11 +291,21 @@ export function VimNav(): JSX.Element | null {
useEffect(() => {
if (!vimMode) return
+ const clearExcalidrawSpacePan = (): void => {
+ if (excalidrawSpacePanTimer.current) {
+ clearTimeout(excalidrawSpacePanTimer.current)
+ excalidrawSpacePanTimer.current = undefined
+ }
+ excalidrawSpacePanTarget.current = null
+ excalidrawSpacePanActive.current = false
+ }
+
const handler = (e: KeyboardEvent): void => {
const state = useStore.getState()
const overrides = state.keymapOverrides
const leaderToken = getSequenceTokens(overrides, 'vim.leaderPrefix')[0] ?? 'Space'
const panePrefixToken = getSequenceTokens(overrides, 'vim.panePrefix')[0] ?? 'Ctrl+W'
+ const token = sequenceTokenFromEvent(e)
// Skip when modals / overlays are open
if (
@@ -308,6 +324,8 @@ export function VimNav(): JSX.Element | null {
// Hint mode — handled entirely by HintOverlay's own listener
if (hintRef.current) return
+ if (excalidrawSpacePanActive.current && token === leaderToken) return
+
// `e.target` is only an HTMLElement for real DOM-dispatched events.
// Synthetic events fired at `window`/`document` (e.g. programmatic
// shortcuts) have a non-Element target, so narrow with `instanceof`
@@ -327,7 +345,7 @@ export function VimNav(): JSX.Element | null {
if (
shouldYieldToHomeNav(
target,
- sequenceTokenFromEvent(e) === leaderToken,
+ token === leaderToken,
!!leaderPending.current
)
) {
@@ -340,7 +358,7 @@ export function VimNav(): JSX.Element | null {
if (
target?.closest('[data-zen-db-grid]') &&
!ctrlWPending.current &&
- sequenceTokenFromEvent(e) !== panePrefixToken
+ token !== panePrefixToken
) {
return
}
@@ -612,7 +630,7 @@ export function VimNav(): JSX.Element | null {
}
// ------- Ctrl+w initiation ----------------------------------------
- if (sequenceTokenFromEvent(e) === panePrefixToken) {
+ if (token === panePrefixToken) {
if (isEditorFocused(state.editorViewRef) && isEditorInsertMode(state.editorViewRef, state.vimMode)) return
e.preventDefault()
e.stopImmediatePropagation()
@@ -636,7 +654,7 @@ export function VimNav(): JSX.Element | null {
}
if (
leaderPending.current &&
- sequenceTokenFromEvent(e) === leaderToken
+ token === leaderToken
) {
e.preventDefault()
e.stopImmediatePropagation()
@@ -655,7 +673,7 @@ export function VimNav(): JSX.Element | null {
if (
panelViewActive &&
!leaderPending.current &&
- sequenceTokenFromEvent(e) !== leaderToken
+ token !== leaderToken
) {
return
}
@@ -672,6 +690,11 @@ export function VimNav(): JSX.Element | null {
isEditorFocused(state.editorViewRef) &&
isEditorInsertMode(state.editorViewRef, state.vimMode)
+ if (excalidrawSpacePanTimer.current && token && token !== leaderToken) {
+ clearExcalidrawSpacePan()
+ armLeader('leader', editorNormalMode)
+ }
+
if (leaderPending.current === 'leader') {
if (matchesSequenceToken(e, overrides, 'vim.leaderSearchGroup')) {
e.preventDefault()
@@ -817,12 +840,12 @@ export function VimNav(): JSX.Element | null {
// In the tasks/tags panels, only leader input is handled above; hand
// every other key (including a just-reset leader sequence) back to the
// panel's own capture handler. (#151)
- if (panelViewActive && sequenceTokenFromEvent(e) !== leaderToken) {
+ if (panelViewActive && token !== leaderToken) {
return
}
if (
- sequenceTokenFromEvent(e) === leaderToken &&
+ token === leaderToken &&
!editorInsertMode &&
// While Vim is mid-command in the focused editor (e.g. after f/t/r or an
// operator), Space is the command's argument (r, f), not
@@ -831,6 +854,31 @@ export function VimNav(): JSX.Element | null {
) {
const tag = (e.target as HTMLElement | null)?.tagName
if (tag !== 'INPUT' && tag !== 'TEXTAREA') {
+ if (
+ leaderToken === 'Space' &&
+ isExcalidrawPath(state.selectedPath) &&
+ (state.focusedPanel === 'editor' || !!target?.closest('[data-excalidraw-view]'))
+ ) {
+ e.preventDefault()
+ e.stopImmediatePropagation()
+ if (excalidrawSpacePanTimer.current || e.repeat) return
+ excalidrawSpacePanTarget.current = e.target ?? null
+ excalidrawSpacePanActive.current = false
+ excalidrawSpacePanTimer.current = setTimeout(() => {
+ excalidrawSpacePanTimer.current = undefined
+ excalidrawSpacePanActive.current = true
+ const panEvent = new KeyboardEvent('keydown', {
+ key: ' ',
+ code: 'Space',
+ bubbles: true,
+ cancelable: true
+ })
+ const dispatchTarget = excalidrawSpacePanTarget.current
+ if (dispatchTarget) dispatchTarget.dispatchEvent(panEvent)
+ else window.dispatchEvent(panEvent)
+ }, EXCALIDRAW_SPACE_PAN_DELAY_MS)
+ return
+ }
e.preventDefault()
e.stopImmediatePropagation()
armLeader('leader', editorNormalMode)
@@ -990,13 +1038,36 @@ export function VimNav(): JSX.Element | null {
}
+ const keyupHandler = (e: KeyboardEvent): void => {
+ const state = useStore.getState()
+ const leaderToken = getSequenceTokens(state.keymapOverrides, 'vim.leaderPrefix')[0] ?? 'Space'
+ if (leaderToken !== 'Space' || sequenceTokenFromEvent(e) !== leaderToken) return
+ if (!excalidrawSpacePanTimer.current && !excalidrawSpacePanActive.current) return
+
+ if (excalidrawSpacePanTimer.current) {
+ clearExcalidrawSpacePan()
+ e.preventDefault()
+ e.stopImmediatePropagation()
+ const editorNormalMode =
+ isEditorFocused(state.editorViewRef) &&
+ !isEditorInsertMode(state.editorViewRef, state.vimMode)
+ armLeader('leader', editorNormalMode)
+ return
+ }
+
+ clearExcalidrawSpacePan()
+ }
+
window.addEventListener('keydown', handler, true)
+ window.addEventListener('keyup', keyupHandler, true)
return () => {
window.removeEventListener('keydown', handler, true)
+ window.removeEventListener('keyup', keyupHandler, true)
previousBufferPending.current = 0
nextBufferPending.current = 0
if (previousBufferTimer.current) clearTimeout(previousBufferTimer.current)
if (nextBufferTimer.current) clearTimeout(nextBufferTimer.current)
+ clearExcalidrawSpacePan()
resetLeader()
}
}, [