board: F jumps to a card by letter

`F` on the board puts a letter or two over every card in view, as it does
over links in the reader, and typing one puts the cursor on that card;
Enter then opens its notes as before.

Hints are pinned where their targets are when they appear, so `F` pressed
while a pane was still sliding in — the board just opened, a space just
switched to — put them where the targets were going to be. They now wait
for the pane to settle.
This commit is contained in:
nvrl committed 2026-10-20 15:42:12 +02:00
1 parent 9fce0112e2
commit 439d5b083b
3 files changed
+25 -2

No files matched your search

+1
View File
@@ -214,6 +214,7 @@ you were. On the board, `o` or `c` puts a card on the column under the cursor.
| `D` | due date — `fri`, `tm`, `+3d`, `+2w`, `10-31`, `2026-10-31` | | `D` | due date — `fri`, `tm`, `+3d`, `+2w`, `10-31`, `2026-10-31` |
| `R` | rename the card, or the column when it has none | | `R` | rename the card, or the column when it has none |
| `z` | show done cards older than a week | | `z` | show done cards older than a week |
| `F` | a letter over every card in view; typing it puts the cursor on that card |
| `x`, `dd` | delete the card | | `x`, `dd` | delete the card |
| `q`, `Esc` | back to the tree | | `q`, `Esc` | back to the tree |
+23 -2
View File
@@ -45,6 +45,7 @@
let importRoot = false let importRoot = false
let shell = $state<HTMLDivElement>() let shell = $state<HTMLDivElement>()
let overlayEl = $state<HTMLDivElement>() let overlayEl = $state<HTMLDivElement>()
let boardEl = $state<HTMLDivElement>()
/** The first half of `gg`, `dd`, `]]` …, shown until the second comes. */ /** The first half of `gg`, `dd`, `]]` …, shown until the second comes. */
let pending = $state('') let pending = $state('')
/** `F`: the pane whose links and buttons are wearing hints right now. */ /** `F`: the pane whose links and buttons are wearing hints right now. */
@@ -390,6 +391,22 @@
} }
} }
/*
* `F`: the hints are pinned where the targets are, so a pane still sliding
* in — a space switch, the board just opened — is let settle first.
*/
async function showHints(root: HTMLElement) {
// What moves the root may be an animation on a pane around it. A blinking
// cursor never finishes, so only what ends is waited for.
const settling = document.getAnimations().filter((animation) => {
const target = (animation.effect as KeyframeEffect | null)?.target
if (!target || animation.effect?.getComputedTiming().endTime === Infinity) return false
return target.contains(root) || root.contains(target)
})
await Promise.all(settling.map((animation) => animation.finished.catch(() => {})))
if (root.isConnected) hintRoot = root
}
/** `gx`: the file as the browser shows it, in a tab of its own. */ /** `gx`: the file as the browser shows it, in a tab of its own. */
function openInTab() { function openInTab() {
const file = workspace.target?.file const file = workspace.target?.file
@@ -487,6 +504,10 @@
case 'z': case 'z':
workspace.showOldDone = !workspace.showOldDone workspace.showOldDone = !workspace.showOldDone
return true return true
case 'F':
// A letter over every card in view: typing it puts the cursor there.
if (boardEl) void showHints(boardEl)
return true
case ' ': case ' ':
void workspace.guard(() => workspace.toggleDone()) void workspace.guard(() => workspace.toggleDone())
return true return true
@@ -772,7 +793,7 @@
// on its other key. // on its other key.
event.preventDefault() event.preventDefault()
const root = reading ? hooks.hintRoot() : null const root = reading ? hooks.hintRoot() : null
if (root) hintRoot = root if (root) void showHints(root)
break break
} }
case 'm': case 'm':
@@ -906,7 +927,7 @@
{#if workspace.view === 'board'} {#if workspace.view === 'board'}
<!-- The board takes the whole width: its columns are the two panes. --> <!-- The board takes the whole width: its columns are the two panes. -->
<div class="panes board switch" style="--dir: {workspace.switchDirection}"> <div class="panes board switch" style="--dir: {workspace.switchDirection}">
<div class="window focused"><Board /></div> <div class="window focused" bind:this={boardEl}><Board /></div>
</div> </div>
{:else} {:else}
<div <div
+1
View File
@@ -62,6 +62,7 @@
['D', 'due date: fri, tm, +3d, 10-31 …'], ['D', 'due date: fri, tm, +3d, 10-31 …'],
['R', 'rename the card, or the list when it is empty'], ['R', 'rename the card, or the list when it is empty'],
['z', 'show done cards older than a week'], ['z', 'show done cards older than a week'],
['F', 'a letter over every card in view — type it to put the cursor there'],
['r', 'read the board again, for changes made elsewhere'], ['r', 'read the board again, for changes made elsewhere'],
['x · dd', 'delete the card (asks y/N)'], ['x · dd', 'delete the card (asks y/N)'],
['q · Esc', 'back to the tree'], ['q · Esc', 'back to the tree'],