FN-6997: widen task changes diff panel

Reclaims task detail padding so inline diffs have more readable width on compact screens.

- Expand the compact Changes file list to consume detail-body padding without page overflow.
- Add mobile breakpoint coverage that matches the detail-body padding contract.
- Add regression tests for the phone-width inline diff surface and CSS rules.
- Add a patch changeset for the published Fusion CLI package.

Files changed:
 .changeset/fn-6997-diff-panel-width.md             |  7 ++
 .../dashboard/app/components/TaskChangesTab.css    | 20 ++++-
 .../components/__tests__/TaskChangesTab.test.tsx   | 98 +++++++++++++++++++++-
 3 files changed, 121 insertions(+), 4 deletions(-)

Fusion-Task-Id: FN-6997

Fusion-Task-Lineage: a7f21c0d-89a8-430e-a84c-341a10af4a4a
This commit is contained in:
gsxdsm
2026-06-24 22:29:19 -07:00
parent 29530b5fdb
commit e473ba68fb
3 changed files with 121 additions and 4 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Make the task Changes tab inline diff panel wider on narrow screens.
category: fix
dev: Reclaims task-detail body padding for compact inline diff lists with mobile CSS contract coverage.

View File

@@ -2,10 +2,24 @@
gap: var(--space-xs);
}
/* Pull the file list wider to reclaim horizontal space from .detail-body padding */
/*
FNXC:TaskChangesDiff 2026-06-25-22:12:
The inline task Changes diff should reclaim safe horizontal padding from the task detail body, especially on Android-width viewports, without changing the expanded diff modal or allowing page-level horizontal overflow.
Keep the negative margins matched to the active .detail-body padding at desktop/tablet and mobile breakpoints.
*/
.task-changes-tab .changes-file-list.task-changes-file-list--compact {
margin-left: calc(-1 * var(--space-md));
margin-right: calc(-1 * var(--space-md));
margin-left: calc(-1 * calc(var(--space-lg) + var(--space-xs)));
margin-right: calc(-1 * calc(var(--space-lg) + var(--space-xs)));
max-width: calc(100% + (calc(var(--space-lg) + var(--space-xs)) * 2));
box-sizing: border-box;
}
@media (max-width: 768px) {
.task-changes-tab .changes-file-list.task-changes-file-list--compact {
margin-left: calc(-1 * calc(var(--space-md) + var(--space-xs) / 2));
margin-right: calc(-1 * calc(var(--space-md) + var(--space-xs) / 2));
max-width: calc(100% + (calc(var(--space-md) + var(--space-xs) / 2) * 2));
}
}
.task-changes-file-list--compact .changes-file-header {

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { loadAllAppCss } from "../../test/cssFixture";
import { loadAllAppCss, loadAllAppCssBaseOnly } from "../../test/cssFixture";
import { TaskChangesTab } from "../TaskChangesTab";
import type { MergeDetails, Column } from "@fusion/core";
@@ -1115,6 +1115,102 @@ describe("TaskChangesTab — expand button", () => {
});
describe("TaskChangesTab — compact spacing class", () => {
it("reproduces the mobile inline diff surface inside a phone-width detail body", async () => {
const longPath = "packages/dashboard/app/components/task-detail/mobile/VeryLongInlineDiffPanelRegressionFile.tsx";
mockFetchTaskDiff.mockResolvedValue({
files: [
{
path: longPath,
status: "modified",
additions: 47,
deletions: 5,
patch: [
"diff --git a/packages/dashboard/app/components/task-detail/mobile/VeryLongInlineDiffPanelRegressionFile.tsx b/packages/dashboard/app/components/task-detail/mobile/VeryLongInlineDiffPanelRegressionFile.tsx",
"@@ -1,3 +1,6 @@",
"-const cramped = true;",
"+const inlineChangesPanelReclaimsTaskDetailPaddingWithoutPageOverflow = true;",
"+export const androidChromeWidthRepresentativeLine = inlineChangesPanelReclaimsTaskDetailPaddingWithoutPageOverflow;",
].join("\n"),
},
{
path: "packages/dashboard/app/components/TaskChangesTab.css",
status: "modified",
additions: 3,
deletions: 0,
patch: "@@ -1 +1,3 @@\n+.task-changes-tab {}",
},
],
stats: { filesChanged: 2, additions: 50, deletions: 5 },
});
const { container } = render(
<div className="detail-body" data-testid="mobile-detail-body" style={{ maxInlineSize: "430px", overflowX: "hidden" }}>
<TaskChangesTab
taskId="FN-6997"
worktree="/path/to/worktree"
column="in-progress"
/>
</div>,
);
await waitFor(() => {
expect(screen.getByText(longPath)).toBeTruthy();
});
const mobileDetailBody = screen.getByTestId("mobile-detail-body");
const taskTab = container.querySelector(".detail-body > .task-changes-tab");
const fileList = taskTab?.querySelector(".changes-file-list.task-changes-file-list--compact");
const diffPatch = fileList?.querySelector(".changes-diff-patch.changes-diff-patch--wrap");
expect(mobileDetailBody.style.maxInlineSize).toBe("430px");
expect(mobileDetailBody.style.overflowX).toBe("hidden");
expect(taskTab).toBeTruthy();
expect(fileList).toBeTruthy();
expect(diffPatch).toBeTruthy();
expect(diffPatch?.textContent).toContain("inlineChangesPanelReclaimsTaskDetailPaddingWithoutPageOverflow");
expect(fileList?.getAttribute("style")).toBeNull();
expect(diffPatch?.getAttribute("style")).toBeNull();
});
it("widens the desktop inline file list by reclaiming detail-body padding without inline styles", () => {
const css = loadAllAppCssBaseOnly();
const ruleMatch = css.match(/\.task-changes-tab\s+\.changes-file-list\.task-changes-file-list--compact\s*\{([^}]*)\}/);
expect(ruleMatch).toBeTruthy();
const rule = ruleMatch![1];
expect(rule).toContain("margin-left: calc(-1 * calc(var(--space-lg) + var(--space-xs)));");
expect(rule).toContain("margin-right: calc(-1 * calc(var(--space-lg) + var(--space-xs)));");
expect(rule).toContain("max-width: calc(100% + (calc(var(--space-lg) + var(--space-xs)) * 2));");
});
it("uses a 768px mobile breakpoint to reclaim the narrower mobile detail-body padding safely", () => {
const css = loadAllAppCss();
const mobileRuleMatch = css.match(/@media\s*\(max-width:\s*768px\)\s*\{\s*\.task-changes-tab\s+\.changes-file-list\.task-changes-file-list--compact\s*\{([\s\S]*?)\}/);
expect(mobileRuleMatch).toBeTruthy();
const rule = mobileRuleMatch![1];
expect(rule).toContain("margin-left: calc(-1 * calc(var(--space-md) + var(--space-xs) / 2));");
expect(rule).toContain("margin-right: calc(-1 * calc(var(--space-md) + var(--space-xs) / 2));");
expect(rule).toContain("max-width: calc(100% + (calc(var(--space-md) + var(--space-xs) / 2) * 2));");
});
it("keeps mobile widening equal and opposite to detail-body padding while preserving overflow containment", () => {
const css = loadAllAppCss();
const detailBodyMobileRuleMatch = css.match(/\.detail-body\s*\{\s*padding:\s*calc\(var\(--space-md\) \+ var\(--space-xs\) \/ 2\);([\s\S]*?)\}/);
const compactListMobileRuleMatch = css.match(/@media\s*\(max-width:\s*768px\)\s*\{\s*\.task-changes-tab\s+\.changes-file-list\.task-changes-file-list--compact\s*\{([\s\S]*?)\}/);
expect(detailBodyMobileRuleMatch).toBeTruthy();
expect(compactListMobileRuleMatch).toBeTruthy();
const mobilePadding = "calc(var(--space-md) + var(--space-xs) / 2)";
expect(css).toContain("@media (max-width: 768px)");
expect(detailBodyMobileRuleMatch![0]).toContain(`padding: ${mobilePadding};`);
expect(detailBodyMobileRuleMatch![1]).toContain("overflow-x: hidden;");
expect(compactListMobileRuleMatch![1]).toContain(`margin-left: calc(-1 * ${mobilePadding});`);
expect(compactListMobileRuleMatch![1]).toContain(`margin-right: calc(-1 * ${mobilePadding});`);
expect(compactListMobileRuleMatch![1]).toContain(`max-width: calc(100% + (${mobilePadding} * 2));`);
});
it("renders the file list with the compact modifier class", async () => {
mockFetchTaskDiff.mockResolvedValue(DONE_TASK_DIFF);