From 5280392fc49d25c211b7e9994a49412107bc063a Mon Sep 17 00:00:00 2001 From: agent_coder Date: Thu, 2 Jul 2026 01:20:43 +0300 Subject: [PATCH] feat(editor): overlay code-block controls, hide language selector until hover (closes #275) The code-block control panel (language selector + copy) took a full row above the code. Move both to an absolute overlay in the top-right corner and hide the language selector until the block is hovered/focused; the copy button stays always visible. In read-only the language selector isn't rendered at all. The
 (editable contentDOM) stays FIRST in the DOM so click hit-testing (#146)
is not regressed; the panel leaves the flow via position:absolute.

Co-Authored-By: Claude Opus 4.8 (1M context) 
---
 .../components/code-block/code-block-view.tsx | 35 +++++++++++--------
 .../code-block/code-block.module.css          | 34 ++++++++++++++----
 .../src/features/editor/styles/code.css       |  7 ++--
 3 files changed, 53 insertions(+), 23 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 1930f182..39b35229 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
@@ -50,10 +50,10 @@ export default function CodeBlockView(props: NodeViewProps) {
       {/* #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 is rendered after it and lifted back above visually via flex
-          `order: -1` (the `.codeBlock` wrapper is a flex column — see
-          code-block.module.css). It stays fully in flow as a full-width row
-          above the code: no overlay/absolute positioning. The second #146
+          menu is rendered after it and floated into the top-right corner as an
+          absolute overlay (see `.menuGroup` in code-block.module.css, anchored
+          to the `position: relative` `.codeBlock` wrapper in code.css). It no
+          longer takes a full-width row above the code. The second #146
           mitigation lives in editor-paste-handler.tsx (reflowAfterPaste). */}
       
-        
+        )}
 
         
           {({ copied, copy }) => (
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 4ecda370..e2cb6faf 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,15 +17,37 @@
     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. */
+/* #146: the menu follows the 
 in the DOM (so the editable contentDOM is
+   FIRST and click hit-testing is correct). Instead of sitting in-flow, it is
+   floated into the top-right corner as an absolute overlay anchored to the
+   `position: relative` .codeBlock wrapper (see code.css), so it no longer
+   takes a full-width row above the code. The Mantine dropdown is portaled, so
+   it is never clipped by the overlay. */
 .menuGroup {
-    order: -1;
+    position: absolute;
+    top: 8px;
+    right: 8px;
+    z-index: 1;
+    gap: 4px;
 
     @media print {
         display: none;
     }
 }
+
+/* The language selector is hidden until the block is hovered, or the selector
+   itself is focused / its dropdown is open. It keeps its width in the flex
+   Group (only opacity toggles) so the copy button never jumps, and
+   `pointer-events: none` while hidden lets clicks fall through to the code.
+   `.codeBlock` is the global NodeViewWrapper class → use :global(). */
+.languageSelect {
+    opacity: 0;
+    pointer-events: none;
+    transition: opacity 150ms ease;
+}
+
+:global(.codeBlock):hover .languageSelect,
+.languageSelect:focus-within {
+    opacity: 1;
+    pointer-events: auto;
+}
diff --git a/apps/client/src/features/editor/styles/code.css b/apps/client/src/features/editor/styles/code.css
index 100e4153..9aa1cdab 100644
--- a/apps/client/src/features/editor/styles/code.css
+++ b/apps/client/src/features/editor/styles/code.css
@@ -1,9 +1,12 @@
 .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`. */
+    /* #146: flex column keeps the editable 
 (first in the DOM so click
+       hit-testing is correct) laid out above any Mermaid diagram. `position:
+       relative` anchors the control panel, which is floated into the top-right
+       corner as an absolute overlay (see `.menuGroup` in code-block.module.css). */
     display: flex;
     flex-direction: column;
+    position: relative;
     padding: 4px;
     border-radius: var(--mantine-radius-default);
     background-color: light-dark(var(--mantine-color-gray-0),  var(--mantine-color-dark-8));