Prototype · quotes lens

Spatial arrow navigation

Today moves to the next quote in DOM order — which in a multi-column grid renders to the right. This prototype makes the arrows geometric and lets you feel the three candidate scoring rules against a real masonry grid.

Click any quote to start, then use . j/k stay bound to DOM order so you can feel the difference. x toggles selection. Resize the window to change the column count.

Artefact — real pixels, shipped tokens Commentary — reasoning, not product
Rule Palette
quotes lens

Getting set up

Making sense of the results

Commentary

Finding — 4 Aug 2026

All three rules felt equivalent on this sample. The predicted drift under Nearest centre was not perceptible in use. That makes Nearest centre the default choice on parsimony: it is the least code by some margin — no lane detection, no remembered-y state machine, no reset rules — and it is the only one of the three that needs nothing beyond a half-plane filter and a distance sort.

Caveat worth keeping. The drift is real mathematically; it just needs more height variance, or a longer run of consecutive horizontal moves, than this sample produces before it accumulates into something a person notices. So this is a finding about the sample as much as about the rules. All three implementations are kept below rather than pruned to the winner, so revisiting is a one-line change to rule rather than a rewrite.

Amended on review. Plain nearest-centre had a defect this sample also missed: a vertical press could answer with a sideways card, because align-items: start gives same-row cards different centres once their heights differ. Not drift — a single keypress in the wrong direction, and present on the rectangular grid too, not just masonry. The shipped rule adds a 2× cross-axis penalty; Nearest centre above now includes it, so this page still shows what the product does. Two things follow: the felt-equivalence finding stands for horizontal moves and was under-tested vertically, and the recorded tell gains a second symptom — "↓ moved right", distinct from the "focus wanders / won't come back" non-reversibility symptom.

The tell to watch for, if it ever comes back: a report that arrow-key focus "wanders" or "won't go back where it was" — particularly from someone working at a wide window with many lanes, or on a study whose quotes vary a lot in length. That symptom is specifically non-reversibility, and Line of sight is the fix. Anything else (focus landing somewhere odd once, then behaving) is not this.

What to feel for

The drift test. Focus a card in the left lane, then press three times and three times. Line of sight returns you to the start by construction — it holds the entry y and only releases it on a vertical move. Nearest centre has no such guarantee: each hop re-centres on a card of a different height, so the error can accumulate. On this sample it doesn't accumulate enough to feel (see the finding above) — the test is kept because it is the thing to re-run against a study with wider height variance.

The tall-card test. Find a tall card with two or three short ones beside it and press . Max overlap has no principled answer here — all the short cards sit entirely within the tall one's span, so they score identically and the tiebreak decides. Line of sight picks by the height you were looking at. Both land somewhere defensible; the difference is whether the choice is principled or arbitrary-but-stable, which only matters if the arbitrary one later proves unstable.

The rule

Line of sight is the goal column from text editors, rotated 90°. In an editor you sit on a long line, press through short ones, and land back at your original column — the editor remembers the column and only resets it when you move horizontally. Here we remember the row (a y coordinate) and only reset it on a vertical move, a click, or j/k. Nobody has ever noticed the editor version, which is the sign it matches perception; the only time it's visible is when it's missing.

Turn on the diagnostic overlay to see the remembered y as a dashed line and the detected lane index on each card. Note the lane index is derived — cards in a lane share a left edge because every lane is 1fr off the same minmax, so nothing needs to know the column count, whether masonry is active, or where the sections start.

Why this is measured, not computed

Every complication dissolves by reading getBoundingClientRect() off the laid-out DOM at keypress time: masonry lanes, the variable column count from auto-fill, the stack of separate .quote-group grids with full-span headings between them, and hidden/filtered/searched quotes. Crossing a section boundary isn't special-cased below — it falls out of "nearest thing in that direction". Cost is one batched rect read per keypress, a single reflow rather than N.

Open questions