From 38544e2ddc27796d19fb62e38a83efb3571efd37 Mon Sep 17 00:00:00 2001 From: claude code agent 227 Date: Tue, 23 Jun 2026 21:48:21 +0300 Subject: [PATCH] fix(editor): render NodeViewContent first so click hit-testing isn't offset (#146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three editable NodeViews rendered a contentEditable=false "chrome" element IN FLOW BEFORE NodeViewContent. On macOS the browser's click hit-testing (posAtCoords → caretRangeFromPoint) then misses the contentDOM and snaps the caret to the previous node — the caret/selection lands a line (code block) or several lines (footnotes, into the body) above where the user clicked. Fix (the transclusion pattern / issue #146 plan): make the editable NodeViewContent the FIRST child in the DOM and move the non-editable chrome AFTER it, restoring its visual position with CSS: - code-block-view:
first; the language/copy menu follows and is lifted above via flex `order` (.codeBlock is now a flex column). - footnotes-list-view: NodeViewContent first; the "Footnotes" heading follows and is lifted above via flex `order` (.list is a flex column; the separator border stays on the container). - footnote-definition-view: NodeViewContent first; the "N." marker follows with `order:-1` to stay on the left; the ↩ back-link stays on the right. Layout is visually unchanged. Verified in a real browser (Chromium): the contentDOM is now the first child of every editable NodeView wrapper (no contentEditable=false element precedes it), and the menu/heading/marker still render in their original positions. NOTE: the caret-offset itself is macOS-specific text hit-testing and does not reproduce in headless Chromium/WebKit on Linux (verified extensively), so the visible fix is best confirmed on macOS. Co-Authored-By: Claude Opus 4.8 --- .../components/code-block/code-block-view.tsx | 29 +++++++++++-------- .../code-block/code-block.module.css | 7 +++++ .../footnote/footnote-definition-view.tsx | 6 +++- .../components/footnote/footnote.module.css | 8 ++++- .../footnote/footnotes-list-view.tsx | 20 +++++++++---- .../src/features/editor/styles/code.css | 4 +++ 6 files changed, 54 insertions(+), 20 deletions(-) diff --git a/apps/client/src/features/editor/components/code-block/code-block-view.tsx b/apps/client/src/features/editor/components/code-block/code-block-view.tsx index 0ff2fe36..1b931926 100644 --- a/apps/client/src/features/editor/components/code-block/code-block-view.tsx +++ b/apps/client/src/features/editor/components/code-block/code-block-view.tsx @@ -47,6 +47,23 @@ export default function CodeBlockView(props: NodeViewProps) { return ( + {/* #146: the editable
 (contentDOM) MUST come first in the DOM.
+          With the non-editable menu rendered before it, the browser's click
+          hit-testing snapped the caret up one line. Render content first; the
+          menu follows and is positioned as a top-right overlay via CSS (the
+          transclusion pattern), so it no longer sits in-flow above the code. */}
+      
+
       
       
 
-      
-
       {language === "mermaid" && (
         
           
diff --git a/apps/client/src/features/editor/components/code-block/code-block.module.css b/apps/client/src/features/editor/components/code-block/code-block.module.css
index 6e0a5dd3..4ecda370 100644
--- a/apps/client/src/features/editor/components/code-block/code-block.module.css
+++ b/apps/client/src/features/editor/components/code-block/code-block.module.css
@@ -17,7 +17,14 @@
     justify-content: center;
 }
 
+/* #146: the menu now follows the 
 in the DOM (so the editable contentDOM is
+   FIRST and click hit-testing is correct). Lift it back ABOVE the code visually
+   with flex `order` — the .codeBlock wrapper is a flex column (see code.css) —
+   so the menu still reads as a row above the code, exactly as before, without
+   sitting in-flow before the contentDOM. */
 .menuGroup {
+    order: -1;
+
     @media print {
         display: none;
     }
diff --git a/apps/client/src/features/editor/components/footnote/footnote-definition-view.tsx b/apps/client/src/features/editor/components/footnote/footnote-definition-view.tsx
index 2685fbc3..ef4b5eab 100644
--- a/apps/client/src/features/editor/components/footnote/footnote-definition-view.tsx
+++ b/apps/client/src/features/editor/components/footnote/footnote-definition-view.tsx
@@ -29,10 +29,14 @@ export default function FootnoteDefinitionView(props: NodeViewProps) {
       className={classes.definition}
       style={{ ["--footnote-number" as any]: `"${number}"` }}
     >
+      {/* #146: contentDOM MUST be the first child — a non-editable marker before
+          it makes click hit-testing snap the caret above. Content first; the
+          marker + back-link follow in DOM and are placed left/right via CSS
+          flex `order`. */}
+      
       
         {number}.
       
-      
       
-      
-
{t("Footnotes")}
-
+ +
+ {t("Footnotes")} +
); } diff --git a/apps/client/src/features/editor/styles/code.css b/apps/client/src/features/editor/styles/code.css index fba5db91..100e4153 100644 --- a/apps/client/src/features/editor/styles/code.css +++ b/apps/client/src/features/editor/styles/code.css @@ -1,5 +1,9 @@ .ProseMirror { .codeBlock { + /* #146: flex column so the menu (rendered AFTER
 in the DOM, so the
+       editable contentDOM is first) is lifted back above the code via `order`. */
+    display: flex;
+    flex-direction: column;
     padding: 4px;
     border-radius: var(--mantine-radius-default);
     background-color: light-dark(var(--mantine-color-gray-0),  var(--mantine-color-dark-8));