FN-7408: normalize task detail tab padding

Normalize task detail tab padding so chat-like views reuse the standard modal body inset.

- Remove Activity and planner Chat body padding overrides while preserving their flex and internal-scroll behavior.
- Strengthen CSS contract tests for desktop, expanded, and mobile task-detail tab spacing.
- Add a patch changeset for the published Fusion package.

Files changed:
 .changeset/fn-7408-task-detail-tab-padding.md      |  7 ++++++
 .../dashboard/app/components/TaskDetailModal.css   | 26 ++++----------------
 .../TaskDetailModal.attachments-and-tabs.test.tsx  | 10 +++++++-
 .../__tests__/TaskDetailModal.css.test.ts          | 28 ++++++++++++++++++++++
 ...etailModal.responsive-and-dependencies.test.tsx | 26 ++++++++++++--------
 5 files changed, 64 insertions(+), 33 deletions(-)

Fusion-Task-Id: FN-7408

Fusion-Task-Lineage: ffff5aed-ebb9-4f81-b90c-8d58d218cd95

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-01 23:36:35 -07:00
parent 9464f31caf
commit b6da7fafd5
5 changed files with 64 additions and 33 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Normalize task-detail tab padding across Activity, Chat, and Plan views.
category: fix
dev: Keeps chat-like task-detail tabs on canonical body padding while preserving internal scroll behavior.

View File

@@ -128,24 +128,16 @@ Task detail workflow badges share the board badge's slight token-based icon-to-l
overflow-y: hidden;
}
/* Activity → Live and expanded Activity mirror the Raw Logs fill-height layout: the modal body does not scroll;
each segment owns internal scrolling while composers and controls stay visible. */
/*
FNXC:TaskDetailPadding 2026-07-01-12:00:
Task-detail tabs share the `.detail-body` outer content inset so switching Activity, planner Chat, Plan, or standard sections does not shift the shell padding. Chat-like tabs may only change flex and internal-scroll ownership here; transcript/composer spacing stays inside their own surfaces.
*/
.detail-body--chat,
.detail-body--planner-chat {
display: flex;
flex-direction: column;
min-height: 0;
overflow-y: hidden;
/* FNXC:TaskDetailChat 2026-06-13-19:55: Chat box should sit in the chat view without excess vertical spacing around the composer (FN-6418), so only the chat body tightens vertical padding while shared detail tab padding remains unchanged. */
padding-block: var(--space-md);
}
/*
FNXC:TaskDetailPlannerChat 2026-06-30-23:58:
Planner Chat expand/collapse may reallocate height or hide mobile chrome, but it must not change the chat surface spacing. Keep body padding on the collapsed base selector and let expanded selectors own only flex/height behavior.
*/
.detail-body--planner-chat {
padding: var(--space-md);
}
.detail-title {
@@ -853,7 +845,6 @@ The same expanded-mode chrome collapse now applies to all Activity segments: Liv
.task-detail-content--chat-expanded .detail-body--chat {
flex: 1;
min-height: 0;
padding: var(--space-md);
}
.task-detail-content--chat-expanded .detail-section--chat {
@@ -899,7 +890,6 @@ Planner Chat should not open expanded by default. When the user expands it, the
.task-detail-content--planner-chat-expanded .detail-body--planner-chat {
flex: 1;
min-height: 0;
padding: var(--space-md);
}
.task-detail-content--planner-chat-expanded .detail-section--planner-chat {
@@ -1191,7 +1181,6 @@ FN-6500 fixes a tablet regression from FN-5599: the task-detail overlay offset a
flex-direction: column;
min-height: 0;
overflow-y: hidden;
padding-block: var(--space-sm);
}
.detail-section--chat {
@@ -1200,10 +1189,6 @@ FN-6500 fixes a tablet regression from FN-5599: the task-detail overlay offset a
margin-top: 0;
}
.task-detail-content--chat-expanded .detail-body--chat {
padding: var(--space-sm);
}
.task-detail-content--chat-expanded .detail-tabs {
display: flex;
}
@@ -1212,9 +1197,6 @@ FN-6500 fixes a tablet regression from FN-5599: the task-detail overlay offset a
display: none;
}
.detail-body--planner-chat {
padding: var(--space-sm);
}
}
.detail-actions-menu-item-danger {

View File

@@ -870,12 +870,19 @@ describe("TaskDetailModal", () => {
describe("Chat full-height layout", () => {
it("FN-6347 defines chat modal-body and section fill-height CSS for desktop and mobile", () => {
const css = readDashboardStylesSource();
const bodyRule = getCssRuleBlock(css, ".detail-body--chat");
const bodyRuleStart = css.indexOf("Task-detail tabs share the `.detail-body` outer content inset");
const bodyRuleCss = css.slice(bodyRuleStart, css.indexOf(".detail-title", bodyRuleStart));
const bodyRule = bodyRuleCss;
const sectionRule = getCssRuleBlock(css, ".detail-section--chat");
const mobileCss = css.slice(css.indexOf("@media (max-width: 768px)"));
const mobileBodyRule = getCssRuleBlock(mobileCss, ".detail-body--chat");
const mobileSectionRule = getCssRuleBlock(mobileCss, ".detail-section--chat");
expect(bodyRule).toContain("display: flex");
expect(bodyRule).toContain("flex-direction: column");
expect(bodyRule).toContain("min-height: 0");
expect(bodyRule).toContain("overflow-y: hidden");
expect(bodyRule).not.toMatch(/\bpadding(?:-[\w-]+)?:/);
expectBaseRule(css, ".detail-body--planner-chat", "display: flex");
expectBaseRule(css, ".detail-body--planner-chat", "flex-direction: column");
expectBaseRule(css, ".detail-body--planner-chat", "min-height: 0");
@@ -886,6 +893,7 @@ describe("TaskDetailModal", () => {
expect(sectionRule).toContain("min-height: 0");
expectBaseRule(mobileCss, ".detail-body--chat", "overflow-y: hidden");
expectBaseRule(mobileCss, ".detail-body--chat", "min-height: 0");
expect(mobileBodyRule).not.toMatch(/\bpadding(?:-[\w-]+)?:/);
expect(mobileSectionRule).toContain("flex: 1");
expect(mobileSectionRule).toContain("min-height: 0");
});

View File

@@ -1,6 +1,18 @@
import { describe, expect, it } from "vitest";
import { loadAllAppCssBaseOnly } from "../../test/cssFixture";
function getCssRuleBlock(css: string, selector: string): string {
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const ruleMatch = css.match(new RegExp(`(?:^|[}\n])\\s*(?:[^{}]*,\\s*)?${escapedSelector}(?:\\s*,[^{}]*)?\\s*\\{([^}]*)\\}`));
return ruleMatch?.[1] ?? "";
}
function expectNoOuterPaddingOverride(css: string, selector: string): void {
const ruleBlock = getCssRuleBlock(css, selector);
expect(ruleBlock, `${selector} rule`).not.toBe("");
expect(ruleBlock, `${selector} padding`).not.toMatch(/\bpadding(?:-[\w-]+)?\s*:/);
}
describe("TaskDetailModal CSS contract", () => {
it("FN-4183 keeps detail source headers top-aligned so the disclosure toggle stays on the first row", async () => {
const css = await loadAllAppCssBaseOnly();
@@ -16,6 +28,22 @@ describe("TaskDetailModal CSS contract", () => {
expect(css).toMatch(/\.detail-tab\s*\{[^}]*flex-shrink\s*:\s*0\s*;/);
});
it("FN-7408 keeps task-detail tab body padding canonical across Activity, planner Chat, and Plan surfaces", async () => {
const css = await loadAllAppCssBaseOnly();
const detailBodyBlock = getCssRuleBlock(css, ".detail-body");
const rawBodyBlock = getCssRuleBlock(css, ".detail-body--agent-log");
const planBlock = getCssRuleBlock(css, ".detail-section--plan-prompt");
expect(detailBodyBlock).toContain("padding: calc(var(--space-lg) + var(--space-xs));");
expectNoOuterPaddingOverride(css, ".detail-body--chat");
expectNoOuterPaddingOverride(css, ".detail-body--planner-chat");
expectNoOuterPaddingOverride(css, ".task-detail-content--chat-expanded .detail-body--chat");
expectNoOuterPaddingOverride(css, ".task-detail-content--planner-chat-expanded .detail-body--planner-chat");
expect(rawBodyBlock).not.toMatch(/\bpadding(?:-[\w-]+)?\s*:/);
expect(planBlock).toContain("width: 100%;");
expect(planBlock).toContain("max-width: 100%;");
});
it("FN-7351/FN-7375 keeps the Activity tab dropdown portal-safe on narrow task-detail surfaces", async () => {
const css = await loadAllAppCssBaseOnly();

View File

@@ -170,36 +170,42 @@ describe("TaskDetailModal", () => {
expect(plannerExpandedMetaBlock).toContain("flex: 0 0 auto;");
});
it("keeps Planner Chat expand spacing identical across desktop and mobile", () => {
it("keeps task-detail outer padding canonical while Planner Chat owns only internal spacing", () => {
const css = readDashboardStylesSource();
const plannerBodySpacingStart = css.indexOf("Planner Chat expand/collapse may reallocate height");
expect(plannerBodySpacingStart).toBeGreaterThanOrEqual(0);
const plannerBodySpacingCss = css.slice(plannerBodySpacingStart, css.indexOf(".detail-title", plannerBodySpacingStart));
const plannerBodyBlock = getExactCssRuleBlock(plannerBodySpacingCss, ".detail-body--planner-chat");
const paddingContractStart = css.indexOf("Task-detail tabs share the `.detail-body` outer content inset");
expect(paddingContractStart).toBeGreaterThanOrEqual(0);
const detailBodyBlock = getExactCssRuleBlock(css, ".detail-body");
const activityBodyBlock = getCssRuleBlock(css, ".detail-body--chat");
const plannerBodyBlock = getCssRuleBlock(css, ".detail-body--planner-chat");
const plannerPanelBlock = getExactCssRuleBlock(css, ".task-planner-chat");
const plannerTranscriptBlock = getExactCssRuleBlock(css, ".task-planner-chat-transcript");
const plannerComposerBlock = getExactCssRuleBlock(css, ".task-planner-chat-composer");
const expandedPlannerBodyBlock = getExactCssRuleBlock(css, ".task-detail-content--planner-chat-expanded .detail-body--planner-chat");
const expandedPlannerSectionBlock = getExactCssRuleBlock(css, ".task-detail-content--planner-chat-expanded .detail-section--planner-chat");
const mobilePlannerBlock = getCssAtRuleBlockContaining(css, "@media (max-width: 768px)", ".detail-body--planner-chat");
const mobileBodyBlock = getCssAtRuleBlockContainingExactRule(css, "@media (max-width: 768px)", ".detail-body");
const mobileDetailBodyBlock = getExactCssRuleBlock(mobileBodyBlock, ".detail-body");
const mobilePlannerBlock = getCssAtRuleBlockContaining(css, "@media (max-width: 768px)", ".detail-body--chat");
const mobilePlannerBodyBlock = getStandaloneCssRuleBlock(mobilePlannerBlock, ".detail-body--planner-chat");
const mobileExpandedPlannerBodyBlock = getExactCssRuleBlock(mobilePlannerBlock, ".task-detail-content--planner-chat-expanded .detail-body--planner-chat");
expect(plannerBodyBlock).toContain("padding: var(--space-md);");
expect(detailBodyBlock).toContain("padding: calc(var(--space-lg) + var(--space-xs));");
expect(mobileDetailBodyBlock).toContain("padding: calc(var(--space-md) + var(--space-xs) / 2);");
expectNoSpacingOverrides(activityBodyBlock, "desktop Activity body modifier");
expectNoSpacingOverrides(plannerBodyBlock, "desktop planner body modifier");
expect(expandedPlannerBodyBlock).toContain("flex: 1;");
expect(expandedPlannerBodyBlock).toContain("min-height: 0;");
expect(expandedPlannerBodyBlock).toContain("padding: var(--space-md);");
expectNoSpacingOverrides(expandedPlannerBodyBlock, "desktop expanded planner body");
expect(expandedPlannerSectionBlock).toContain("flex: 1;");
expect(expandedPlannerSectionBlock).toContain("min-height: 0;");
expectNoSpacingOverrides(expandedPlannerSectionBlock, "desktop expanded planner section");
expect(mobilePlannerBodyBlock).toContain("padding: var(--space-sm);");
expect(mobilePlannerBodyBlock).toBe("");
expect(mobileExpandedPlannerBodyBlock).toBe("");
expect(plannerPanelBlock).toContain("gap: var(--space-md);");
expect(plannerTranscriptBlock).toContain("padding: var(--space-md);");
expect(plannerTranscriptBlock).toContain("gap: var(--space-md);");
expect(plannerComposerBlock).toContain("gap: var(--space-sm);");
expect(css).not.toMatch(/task-detail-content--planner-chat-expanded[^{]+\.(?:task-planner-chat|task-planner-chat-transcript|task-planner-chat-composer)\s*\{[^}]*(?:padding|margin|gap)\s*:/);
expect(css).not.toMatch(/task-detail-content--planner-chat-expanded[^{]+\.detail-body--planner-chat\s*\{[^}]*(?:margin|gap)\s*:/);
expect(css).not.toMatch(/task-detail-content--planner-chat-expanded[^{]+\.detail-body--planner-chat\s*\{[^}]*(?:padding|margin|gap)\s*:/);
});
it("keeps detail metadata as a single wrapping flex row without mobile column fallbacks", () => {