From 8f065f167327366378792053eb530aa8bc8c2dce Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?=
Date: Thu, 28 May 2026 16:27:16 +0100
Subject: [PATCH] feat(web): stop filename squeeze on changes-panel row hover
(#1114)
---
.../task/changes-panel-file-row.test.tsx | 46 +++++++++++++++++++
.../task/changes-panel-file-row.tsx | 10 ++--
2 files changed, 52 insertions(+), 4 deletions(-)
diff --git a/apps/web/components/task/changes-panel-file-row.test.tsx b/apps/web/components/task/changes-panel-file-row.test.tsx
index 35fa3e4fcc..4001594461 100644
--- a/apps/web/components/task/changes-panel-file-row.test.tsx
+++ b/apps/web/components/task/changes-panel-file-row.test.tsx
@@ -109,6 +109,52 @@ describe("FileRow truncation (regression: path overlaps diff stats in narrow pan
});
});
+describe("FileRow hover swap (stats <-> actions occupy same cell)", () => {
+ // Goal: hovering must not shift the file name. The stats group and the
+ // hover-actions group live in the same CSS grid cell — stats fade out and
+ // actions fade in, so the right cluster's width is stable.
+ it("stats container fades out on hover and never intercepts pointer events", () => {
+ const { container } = renderRow("file.go");
+ const li = container.querySelector("li") as HTMLElement;
+ const gridWrapper = li.children[1] as HTMLElement;
+ const statsLayer = gridWrapper.children[0] as HTMLElement;
+ expect(statsLayer.className).toContain("group-hover:opacity-0");
+ expect(statsLayer.className).toContain("transition-opacity");
+ // Defensive: stats has no interactive children today, but blocking pointer
+ // events on this always-non-interactive layer prevents a future regression
+ // if someone adds a clickable element here.
+ expect(statsLayer.className).toContain("pointer-events-none");
+ });
+
+ it("actions container fades in on hover and only accepts pointer/keyboard events while hovered", () => {
+ const { container } = renderRow("file.go");
+ const li = container.querySelector("li") as HTMLElement;
+ const gridWrapper = li.children[1] as HTMLElement;
+ const actionsLayer = gridWrapper.children[1] as HTMLElement;
+ expect(actionsLayer.className).toContain("opacity-0");
+ expect(actionsLayer.className).toContain("group-hover:opacity-100");
+ expect(actionsLayer.className).toContain("transition-opacity");
+ // Critical: the actions div is the top child in the same grid cell, so
+ // without pointer-events gating it would eat clicks aimed at the
+ // visually-shown stats. (Keyboard Tab focus is a separate concern — a
+ // pre-existing gap not closed by `pointer-events: none`, which only
+ // blocks mouse/touch.)
+ expect(actionsLayer.className).toContain("pointer-events-none");
+ expect(actionsLayer.className).toContain("group-hover:pointer-events-auto");
+ });
+
+ it("wrapper stacks both layers into one grid cell so width is max(stats, actions)", () => {
+ const { container } = renderRow("file.go");
+ const li = container.querySelector("li") as HTMLElement;
+ const gridWrapper = li.children[1] as HTMLElement;
+ expect(gridWrapper.className).toContain("grid");
+ // Arbitrary child selector pins every direct child to row 1 / col 1 so
+ // they overlap instead of laying out side-by-side.
+ expect(gridWrapper.className).toContain("col-start-1");
+ expect(gridWrapper.className).toContain("row-start-1");
+ });
+});
+
describe("FileRow active-tab highlight", () => {
it("renders data-active='true' and bg-accent/60 when isActive", () => {
const { container } = render(
diff --git a/apps/web/components/task/changes-panel-file-row.tsx b/apps/web/components/task/changes-panel-file-row.tsx
index f60c777f61..7a3457519a 100644
--- a/apps/web/components/task/changes-panel-file-row.tsx
+++ b/apps/web/components/task/changes-panel-file-row.tsx
@@ -123,7 +123,11 @@ export function FileRow({
-
+
);
@@ -217,7 +219,7 @@ function FileRowActions({
stageButton?: React.ReactNode;
}) {
return (
-