fix(panel): one tap edits the plan, with the caret on the words tapped

Merged
DA
5d ago
+385-308

Summary

The developer tried #4119 on their phone and chose one tap edits: caret and keyboard where the tap lands. They also reported that the caret doesn't go where they tapped, because the page moves. Two commits:

  1. Revert #4119. On touch, the first tap opens the editor again, as with a mouse. This removes the second-tap rule, the title's read-only mode, and their tests.
  2. The caret lands on the words tapped. The editor draws a picture, a table, and a diagram's preview at different heights from the read view, so opening it moves everything below them. On the mobile-audit fake's plan at iPhone 13 width, text moves 59 px after the picture and 243 px after the Mermaid diagram. The caret was placed by the tap's coordinates, so it landed on whatever text had moved under the finger, and the page looked as if it jumped.
    • A click now hands the editor the words it landed on: the section, the text node's words, how many identical nodes came before it in that section, and the offset (PlacedWords in contract.ts). It reads them with caretRangeFromPoint, or Firefox's caretPositionFromPoint.
    • The editor finds the same words in the same section and puts the caret there (wordsAt in plan-editor.ts).
    • The plan's column then scrolls by however much the editor moved them, so the tapped line stays under the finger. It does this again each time the editor's height changes as later drawings land, such as a diagram preview or a chip, until the developer scrolls, presses or types (wordsTop on the handle, a ResizeObserver in PlanDocumentEditor). wordsTop answers only while the editor's text and caret are the ones it opened on the found words with. So the fallback, a merged answer, a typed word, or a moved caret all scroll nothing.
    • Words in code blocks are counted apart from other words, so a diagram's code (shown as a picture in the read view, but kept in the editor) never takes the caret (from review).
    • The scroll correction reads the same placement the editor opens with, so a press on the room while the bundle loads scrolls nothing (from review).
    • The tap's coordinates are still the fallback, for example a tap off any words.

This applies to the desktop too, where a click below a picture or diagram had the same offset.

Evidence

  • pnpm --filter @luke/panel typecheck, pnpm knip and oxlint on the changed files pass. Biome's findings on packages/panel/src are already on main.
  • Panel suites tasks, desktop, settings and plan-editor: 200 files and 2317 tests pass.
  • New tests:
    • placement.test.ts: an editor opened on placed words puts the caret at the second matching node in the right section, at the offset. It fails when the words lookup is disabled.
    • plan-document-editor.test.ts: a click hands over the words it landed on, using a stubbed caretRangeFromPoint as the browser boundary.
    • plan-document-editor.test.ts: the column scrolls by the distance the editor moved the words.
    • placement.test.ts: a paragraph's words aren't matched in a diagram's code that repeats them.
    • placement.test.ts: when the words aren't in their section, the section is empty, or it's past the text's end, the caret falls back to the click's point and wordsTop answers nothing. wordsTop stops answering once the caret is put elsewhere, or once a merged answer replaces the words' paragraph.
    • plan-document-editor.test.ts: the words stay under the finger when the editor grows after opening, and a scroll by the developer lets them go. A click whose words weren't found scrolls nothing.
    • plan-document-editor.test.ts: a press on the room after a click, while the bundle loads, opens at the end and scrolls nothing.
  • Real Chromium with iPhone 13 touch emulation against pnpm --filter @luke/web dev and the mobile-audit fake. Each row is one tap:
TappedWords under the fingerCaret after openingLine before → afterColumn scrolled
"Refresh 60 seconds…" (below the picture)Refre|shRefre|sh331 → 327+59
"Ship behind the…" (below the diagram)Ship |behindShip |behind267 → 263+243

Before this change, the same taps would have put the caret 59 px and 243 px lower: two or more lines down, or in a later block.

Notes

  • Still to check by hand on an iPhone: when the keyboard rises, iOS scrolls on its own so the caret stays visible above it. That shouldn't move the caret, but emulation can't show it.

Made with Luke


Devin Review