Skip to content

Open rendered code, diff and diagram blocks by clicking or arrowing in - #388

Merged
MaggieAppleton merged 6 commits into
mainfrom
design/render-block-editing
Oct 8, 2026
Merged

MaggieAppleton merged 6 commits into
mainfrom
design/render-block-editing

Conversation

@MaggieAppleton

@MaggieAppleton MaggieAppleton commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Why

Rendered code, diff and diagram blocks could not be entered to edit. Clicking the coloured code focused a non-editable preview: typing did nothing, Space scrolled the pane, and the source appeared as a second copy underneath. ArrowDown from the heading above skipped every collapsed block and landed in the next table. Clicking a diagram put a browser caret inside a Mermaid label, and typing went into the drawing's HTML. The only way in was the small <> toggle.

What changed

  • Click to edit. Clicking a preview opens its source with the caret where you clicked. For code that is the clicked line and column. For a diff it is the matching patch line, and a gutter number goes to the start of its line. A diagram or formula puts the caret at the end. Dragging or double-clicking to select text is left alone, so copying from a preview still works.
  • One copy. While you edit code or a diff, the source replaces the coloured view in the same box, with a short fade (--duration-fast, off under reduced motion). A diagram keeps its drawing above the source, as in Quiet rendered block chrome and steady diagram previews #370.
  • Escape returns to the preview. Focus stays on the preview, and a ring on the whole block shows that it is held. From a focused preview, Enter or Space opens the source with the caret at the end, and any other typed character does the same and is inserted there. ArrowUp and ArrowDown move to the neighbouring block. Space never scrolls the pane. The hint reads "Esc to preview" on blocks that have one. Plain fences keep "Esc then Tab to leave".
  • Arrow keys. On the last or first line of a block, ArrowDown or ArrowUp into a collapsed block puts the caret in the source for code and diffs, on the first or last line. A diagram is held as a block, and Enter, Space or typing edits it. This also works from table cells.
  • Collapsing again. A source the caret opened closes when the caret leaves it. A source opened with the <> toggle stays open until you hide it.
  • Diagram labels can no longer take a caret. The drawing and the block-formula preview are contenteditable=false.

AGENTS.md's code-block rules still hold. The preview is still derived UI owned by one React tree, and no second editor is mounted. Every caret placement is a Lexical range selection inside the canonical code node. Keys aimed at a focused preview are handled before Lexical's own key handling, so they never act on a caret left somewhere else.

Screenshots

Clicking code. Before: the source appears as a second copy underneath, typing does nothing, and Space scrolls away.
Before: click code
After:
After: click code

Arrowing down from the heading. Before: the caret skips the blocks, and "Q" lands in the document title.
Before: arrows
After: into the code, into the diff, Escape back to the preview, onto the diagram, edit it, and out into the table.
After: arrows

Clicking a diagram. Before: "W" is typed into the "Commit" label.
Before: diagram
After:
After: diagram

Verification

  • Six commits remain patch-identical on the refreshed stack, including the prior research composer style integration.
  • Focused source-offset tests: 6 passed. bun run fix, bun run ci, bun run types, and bun run build passed. Reviewed design hashes are current.
  • Earlier manual Chromium checks covered click, key, and arrow entry through code, diff, diagram, and table. Fresh GitHub CI must run after the prepared head is published.
  • Initial JavaScript: 251,414 B raw / 79,327 B gzip, within budget.

🤖 Generated with Claude Code

@MaggieAppleton

Copy link
Copy Markdown
Collaborator Author

Review: Changes needed

Checked in a real browser (headless Chromium, desktop 1440x900 and phone 390x844 with touch, reduced motion, two-user and read-only sessions) plus a code read. Most of the design works well; one blocker and two smaller gaps.

Works

  • Click in code opens source with the caret at the clicked line and column. Past end of line goes to the end of that line. Verified by typing and reading the stored file.
  • Diff: addition, deletion and context lines map to the right patch line and column. A gutter number puts the caret just after the +/- prefix.
  • Focused preview: Enter and Space open without inserting anything, a printable key inserts exactly once ("hello" gives "hello"), Shift+letter works, Tab moves on to the toggle, Escape returns focus to the preview, Cmd+Z undoes the typed text.
  • Arrows from paragraphs, list items and table header or last-row cells enter code in both directions. Table rows 1 to 2 still stay inside the table. A wrapped paragraph's first line does not trigger entry.
  • Two users in one fence: open state is per viewer. Alice opening, typing or pressing Escape does not open or close Bob's source, and both edits land in the file.
  • Read-only viewer (archive repo): click, Enter and typing do nothing, and the file is unchanged.
  • Phone: tap opens the source and typing works, with no horizontal overflow. Reduced motion: animation-name is none on the source.
  • Swap: no duplicated lines. Height changes about 89 to 93px for code and 118 to 160px for the diff (the diff source shows the header lines).

Blocker

  1. Text selection in a code or diff preview opens the source, so selecting code to copy it is not possible. render-blocks.tsx, the update listener guard near closest("[data-plan-preview]"). On mousedown the browser puts the DOM selection in the block's hidden source text (not in the preview), so the guard misses it and the listener opens the block with "editing". Reproduced: pressing and dragging across a line leaves data-plan-swapped set and getSelection().toString() empty. Double and triple click after that select source text, not the preview. The moved > 4 check in click is too late because the open already happened on mousedown. Suggested fix: ignore selection updates while a pointer press that started in a preview is in flight (set a flag in down, clear it after click or mouseup), and let click alone decide opening. Add a drag-select e2e case asserting [data-plan-source] stays hidden.

Should fix
2. Arrows cannot enter or leave code nested in a callout or list item. In a callout containing code, ArrowDown from the text before the code skipped it and landed in the paragraph after the callout (stored "AfterQ"). arrow() only looks at the top-level sibling of the caret's top ancestor. Either handle the immediate sibling inside the container or note it as a known gap. Click on nested code works in callout and list.
3. Focus ring on the block has a visible notch at the top-right corner (visible on code and diagram focus, styles.css focus rule on .planCode). Likely the clipped corner or the toolbar painting over the outline. Worth a look.

Minor

  • The dropped expectFocusIndicator in syntax-contrast.e2e.ts now only checks outline-style, which weakens that test.
  • Edit state shows a tinted background on every source line. Check whether that is intended in the swapped state.

CI (gh pr checks 388): format, lint, types, tests fails, only on design-contract hash reviews ("reviewed dynamic owner changed" for code-view.tsx:137, language-menu.tsx:175 and :180), which are acceptable and not to be edited. container passes. e2e passes.

@MaggieAppleton

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. All five points are addressed in e185e74 and eb108f7. CI: e2e and container pass. format, lint, types, tests fails only on the inherited #370 design-contract hashes (code-view.tsx:137, language-menu.tsx:175, language-menu.tsx:180).

  1. Drag-select (blocker). Two causes. The update listener opened the block on mousedown. Separately, Chromium answers any press on a preview inside the editable root by putting the caret in the hidden source, so a drag never selected preview text, even when nothing opened. Fixes:
    • New widgets/preview-selection.ts takes over presses on a preview. It cancels the default and drives the selection from caretPositionFromPoint inside the renderer's shadow roots. Double and triple presses select a word and a line, but for a writer the first click has already opened the source, so in practice those serve readers.
    • While a press that started on a preview is in progress, selection-driven opening is suspended, and click decides (moved more than 4px means it doesn't open).
    • COPY_COMMAND at CRITICAL leaves a copy from a focused preview to the browser, so Lexical can't substitute its own stale selection.
    • New e2e: dragging across a line keeps the preview, selects the line text, and the copy event is not defaultPrevented and carries that text.
  2. Nested code.
    • arrow() now finds the nearest sibling of the caret's line or any ancestor. Inline siblings are skipped, table rows and cells are skipped, and it then descends to that sibling's first or last block. This covers code in callouts and list items (code under a list item's text, and ArrowUp from the next item). $leave uses the same lookup, so leaving nested code lands on the right neighbour.
    • It now measures from the DOM selection rather than Lexical's, which lags a click by a task.
    • New e2e: callout and list item.
  3. Focus-ring notch. The block inherited the document's inset --focus-ring-offset: -2px, so the corner controls painted over it. The ring now sits on the border (outline-offset: 0).
  4. expectFocusIndicator restored. It takes an optional ring locator, and syntax-contrast asserts the focused preview with the ring on the block.
  5. Teal tint. I couldn't reproduce it on desktop, on phone with touch, or in the diff. This PR sets the swapped source's background to --color-page and adds no line tint. The only tints in edit state come from other features. With DEV_COMMENTS and DEV_QUESTIONS, the comment and related-prose highlights (plan-related) are teal washes over their passages, which is the likely source. If you still see it, a screenshot or the document URL would help.

Also fixed: with a comment preview open on the same passage, Escape from a source also closed the comment preview and sent focus to its marker. The Escape that returns to the preview now stops there.

@MaggieAppleton

Copy link
Copy Markdown
Collaborator Author

Review: Looks good

Re-verified at eb108f7 in a fresh build (headless Chromium, desktop).

Fixed

  • Drag-select in a code preview selects the text and does not open the source. Cmd+C copied exactly the selection ("let alpha = 1;\nlet beta = 2;\nlet ga").
  • Drag-select in a diff preview works and does not open the source.
  • Double click selects a word, and triple click selects a line, in the preview. A writer's double click opens the source, then selects in the source as usual.
  • Click-to-edit lands the caret at the clicked column.
  • Cross-block drag from prose through code into the next paragraph gives one sane selection, and the source stays closed. Backspace deletes the selected range cleanly.
  • Nested arrows enter code in both directions in a callout and in a list item. Arrowing out of the code reaches the paragraphs before and after it.
  • Focus ring corners are now smooth (checked on a focused code preview). Escape returns focus to the preview.

Remaining nits, not blocking

  1. Shift+click inside a preview collapses the selection instead of extending it (preview-selection.ts, down: a shift press falls through to selection.collapse). Extending from the existing anchor with setBaseAndExtent would fix it.
  2. Touch drag-select and long-press selection were not tested, because Playwright cannot synthesise touch selection handles. Tap-to-edit was verified earlier on a 390px viewport. Worth a quick check on a real phone.
  3. The pale teal per-line tint on source text in edit state is still there. The selection is collapsed, no CSS highlight is registered, and the span has a transparent background, so I could not find its source. It looks like a selection and may be unintended. Check the .planCode [data-plan-source] styles near styles.css:2176.

@MaggieAppleton

Copy link
Copy Markdown
Collaborator Author

Both follow-ups:

  1. Teal per-line tint in edit state. This is the plan-related CSS highlight (::highlight(plan-related), --color-brand-wash, styles.css:1029), not something this PR adds. I reproduced your exact document (Before + let alpha/let beta fence + diagram). CSS.highlights holds one entry, plan-related: "let alpha = 1;\nlet beta = 2;". A DEV_QUESTIONS sample decision has linked that block as its related prose. The DOM selection is a collapsed caret, and nothing selected in the preview's shadow root is drawn.
    • The highlight's ranges are in the Lexical source text, so you only see them once the source is on screen. Before this PR the <> toggle showed the same wash.
    • In my seeded documents the same wash sits on the H1, wherever the sample decision points.
    • I've left it as is. It's the decision-to-prose link working as designed. Hiding it inside code sources would be a change to related-prose behaviour, not to this PR.
    • A related gap, outside this PR: the coloured preview doesn't show the related-prose mark at all, so a reader only sees the link once the source is open. Worth a follow-up if we want related code blocks marked at rest.
  2. Shift+click extends. In 409a77a, a Shift-press inside a preview that already holds a selection now extends from its anchor with setBaseAndExtent, and dragging continues from there. The click still doesn't open the source. The drag e2e now drags, Shift-clicks further along, and asserts that the selection grew from the same start, the source stayed hidden, and copy is native.

CI on 409a77a: e2e pass, container pass. format, lint, types, tests fails only on the inherited #370 design-contract hashes.

@MaggieAppleton
MaggieAppleton force-pushed the design/rendered-block-chrome branch from 658a58d to c2d2b34 Compare October 8, 2026 16:12
Base automatically changed from design/rendered-block-chrome to main October 8, 2026 16:23
MaggieAppleton and others added 6 commits October 8, 2026 18:55
Clicking a preview opens its source with the caret where the click was
(the clicked line and column for code, the matching patch line for a diff,
the end for a diagram or formula). Code and diffs swap the coloured copy for
the source instead of showing both. Escape returns to the preview and holds
focus on it; Enter, Space or typing on a focused preview opens the source,
and ArrowUp/ArrowDown move on. Arrowing from a neighbouring block enters a
collapsed block instead of skipping it: the caret goes into code, and a
diagram is held as a block. A source the caret opened closes again when the
caret leaves; one opened with the toggle stays open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A press on a preview no longer lets the browser drop the caret into the
  hidden source. The selection follows the pointer inside the renderer's
  shadow roots, so code can be dragged over and copied without opening it.
  Copy from a preview is left to the browser. While the press lasts, the
  caret-follows-selection opening is suspended; the click decides.
- Arrow keys find a folded block across callouts and list items, not only
  top-level siblings, measuring from the DOM selection so a key pressed
  right after a click starts from the click.
- Escape that returns to a preview is not also taken by a comment preview.
- The focus ring sits on the block's border, clear of the corner controls.
- The syntax-contrast test checks the ring with expectFocusIndicator again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaggieAppleton
MaggieAppleton force-pushed the design/render-block-editing branch from c4c4578 to 86cb5be Compare October 8, 2026 17:55
@MaggieAppleton
MaggieAppleton merged commit e85b69a into main Oct 8, 2026
5 checks passed
@MaggieAppleton
MaggieAppleton deleted the design/render-block-editing branch October 8, 2026 18:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant