diff --git a/web/src/App.svelte b/web/src/App.svelte index a5eae15..b86416f 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -443,6 +443,15 @@ if (cmdline !== null || isTypingTarget(event.target)) return + // While writing, the status line says the editor is listening. A key that + // lands anywhere else — the focus left behind by a prompt, a click on the + // header — hands the focus back to it instead of walking the list unseen. + if (workspace.editing && workspace.pane === 'document' && !event.ctrlKey && !event.metaKey && !event.altKey) { + event.preventDefault() + hooks.focusEditor() + return + } + // Ctrl-P: find anything, in any space — the one search that is not // confined to where you are. if (event.ctrlKey && !event.metaKey && !event.altKey && event.key === 'p') { diff --git a/web/src/lib/SecretPrompt.svelte b/web/src/lib/SecretPrompt.svelte index 3423951..ed53e9f 100644 --- a/web/src/lib/SecretPrompt.svelte +++ b/web/src/lib/SecretPrompt.svelte @@ -10,15 +10,34 @@ * field alone: it is not the login, and their menus redrawn on every key * are a flicker of their own. */ + import { untrack } from 'svelte' + import { hooks } from './commands' import { workspace } from './store.svelte' let value = $state('') let input = $state() + /* + * Where the keys went before the question took them. Putting the field away + * would otherwise leave the focus nowhere, and the next keys — meant for the + * editor the status line still says is listening — would walk the list. + */ + let returnTo: HTMLElement | null = null + let asking = false $effect(() => { if (workspace.secret) { value = '' + if (!asking) returnTo = document.activeElement as HTMLElement | null + asking = true input?.focus() + } else if (asking) { + asking = false + untrack(() => { + if (returnTo?.isConnected && returnTo !== document.body) returnTo.focus() + else if (workspace.editing && workspace.pane === 'document') hooks.focusEditor() + else hooks.focusList() + }) + returnTo = null } }) diff --git a/web/src/lib/store.svelte.ts b/web/src/lib/store.svelte.ts index 2d7c7de..ffdcfa3 100644 --- a/web/src/lib/store.svelte.ts +++ b/web/src/lib/store.svelte.ts @@ -276,6 +276,8 @@ class Workspace { if (this.vault.unlocked) return true const password = await this.askSecret('vault password') if (password === null) return false + // The key is stretched on purpose, which takes a moment worth saying. + this.say('unlocking …') this.vault = await api.unlockVault(password) this.say('vault unlocked') return true @@ -583,7 +585,8 @@ class Workspace { } if (spot && this.parentId === spot.parentId) { if (spot.openId !== null && this.nodes.some((n) => n.id === spot.openId)) { - await this.openNode(spot.openId) + // Reopened under the board, if that is what is on screen. + await this.openNode(spot.openId, false, true) this.pane = spot.pane } else { this.cursor = Math.min(spot.cursor, Math.max(this.nodes.length - 1, 0)) @@ -637,6 +640,8 @@ class Workspace { } async search(query: string, global = this.globalSearch) { + // The results are rows in the list, which the board would hide. + if (query && this.view === 'board') this.closeBoard() this.query = query this.globalSearch = global && query !== '' this.cursor = 0 @@ -648,10 +653,19 @@ class Workspace { this.cursor = Math.min(Math.max(this.cursor + delta, 0), this.nodes.length - 1) } - async openNode(id: number, edit = false) { + async openNode(id: number, edit = false, underBoard = false) { if (this.open?.id !== id) await this.flush() const node = await api.getNode(id) + // What is asked for from the board — `:12`, `T`, a search — is shown, not + // opened behind it; closing a document goes back to the board. + if (this.view === 'board' && !underBoard) { + if (isContainer(node)) this.closeBoard() + else { + this.view = 'tree' + this.returnTo = 'board' + } + } if (isContainer(node)) { await this.goTo(node.id) return