fix(web): only allow line selection via gutter click

Quentin Gliech and Claude Opus 4.6 (1M context) created

Replace ::before pseudo-element with an actual <span> for line numbers,
injected by the Shiki transformer. Only the gutter span has
data-line-number, so clicking code text no longer triggers selection.
Gutter gets a hover state (opacity 0.4 → 0.8) for affordance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Change summary

webui2/src/components/code/file-viewer.module.css |  9 ++++++---
webui2/src/components/code/file-viewer.tsx        | 11 ++++++++++-
2 files changed, 16 insertions(+), 4 deletions(-)

Detailed changes

webui2/src/components/code/file-viewer.module.css 🔗

@@ -28,9 +28,8 @@
   padding-bottom: 0.5rem;
 }
 
-/* Line numbers via ::before pseudo-element */
-.line::before {
-  content: attr(data-line-number);
+/* Line number gutter — injected as a <span> by the Shiki transformer */
+.line-number {
   display: inline-block;
   width: 3rem;
   margin-right: 1rem;
@@ -39,3 +38,7 @@
   user-select: none;
   cursor: pointer;
 }
+
+.line-number:hover {
+  opacity: 0.8;
+}

webui2/src/components/code/file-viewer.tsx 🔗

@@ -96,7 +96,16 @@ function lineNumberTransformer(): ShikiTransformer {
     line(node, line) {
       // Replace Shiki's "line" class with our CSS module class
       node.properties["className"] = [styles["line"]!];
-      node.properties["dataLineNumber"] = line;
+      // Prepend a gutter <span> for the line number — clickable target
+      node.children.unshift({
+        type: "element",
+        tagName: "span",
+        properties: {
+          className: [styles["line-number"]!],
+          dataLineNumber: line,
+        },
+        children: [{ type: "text", value: String(line) }],
+      });
       // Append a \n text node so copy-paste preserves newlines
       // (we strip the inter-element whitespace nodes in code() below).
       node.children.push({ type: "text", value: "\n" });