FN-8624: fix task detail activity right padding

Keep Task Detail activity insets symmetric while reserving overlay clearance only for first-row content.

- Move Live/Feed overlay clearance from the activity container to first-row elements.
- Preserve responsive overlay spacing and the full-width Interventions timeline.
- Cover modal, pop-out, embedded, and responsive activity layouts with updated tests.
- Add a patch changeset for the Task Detail padding fix.

Files changed:
 .changeset/fn-8624-task-detail-inset.md            |  7 ++++
 .../dashboard/app/components/TaskDetailModal.css   | 41 ++++++++++------------
 .../TaskDetailModal.attachments-and-tabs.test.tsx  |  6 ++--
 .../__tests__/TaskDetailModal.css.test.ts          | 21 ++++++-----
 ...etailModal.responsive-and-dependencies.test.tsx | 25 +++++++------
 5 files changed, 54 insertions(+), 46 deletions(-)

Fusion-Task-Id: FN-8624

Fusion-Task-Lineage: 578cfe86-3021-421d-841b-2670efc10a5d

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-26 19:45:47 -07:00
parent d3c1955866
commit 7715a83e6f
5 changed files with 54 additions and 46 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Fix asymmetric right padding in Task Detail activity content.
category: fix
dev: Overlay clearance now applies only to Activity first rows at every breakpoint.

View File

@@ -3258,20 +3258,27 @@ The body-portaled menu must paint above the owning task-detail modal/popup while
font-weight: 600; font-weight: 600;
} }
/*
FNXC:TaskDetailActivity 2026-07-27-02:15:
FN-8624 requires Task Detail content insets to stay symmetric at every breakpoint,
including modal, pop-out, and embedded surfaces. The fixed Live/Feed overlay can cover
only the first row, so its tokenized clearance belongs there rather than on this container;
this generalizes FN-8166's mobile-only correction to desktop and tablet.
*/
.detail-activity { .detail-activity {
position: relative; position: relative;
padding-inline-end: 0;
}
.detail-activity:not(.detail-activity--interventions) > h4,
.detail-activity:not(.detail-activity--interventions) > .detail-log-truncated,
.detail-activity:not(.detail-activity--interventions) > .detail-log-loading,
.detail-activity:not(.detail-activity--interventions) > .detail-log-empty,
.detail-activity:not(.detail-activity--interventions) > .detail-activity-list > .detail-log-entry:first-child {
padding-inline-end: calc(var(--space-2xl) + var(--space-md)); padding-inline-end: calc(var(--space-2xl) + var(--space-md));
} }
/* /* FNXC:PlannerOversight 2026-07-27-02:15: Retain the explicit zero inset for the overlay-free Interventions segment; it documents the FN-7581 full-width timeline contract even though the shared container is now symmetric. */
FNXC:PlannerOversight 2026-07-05-00:00:
The Interventions Activity segment (FN-7571) renders no `.activity-expand-toggle--overlay`
button, so it must not reserve the shared `.detail-activity` padding-inline-end that exists
solely to keep that overlay toggle from covering Live/Feed content. Without this override the
FN-7519 Intervention Timeline is inset from the right edge (worse on mobile, where the base
reserved padding grows). FN-7581 zeroes the reserved padding for this segment only; Live/Feed/
Raw keep the base rule unchanged.
*/
.detail-activity--interventions { .detail-activity--interventions {
padding-inline-end: 0; padding-inline-end: 0;
} }
@@ -3315,15 +3322,10 @@ Live and Feed Activity expansion overlays the content instead of reserving a too
} }
/* /*
FNXC:TaskDetailActivity 2026-07-16-00:00: FNXC:TaskDetailActivity 2026-07-27-02:15:
FN-8166 removes the residual mobile Feed container right inset so `.detail-body` FN-8624 keeps the base container symmetric; at mobile sizes the smaller fixed
provides equal left and right insets. Reserve overlay-toggle clearance only on overlay needs correspondingly smaller first-row clearance without covering log text.
top-of-panel first-row elements, keeping the first visible row uncovered.
*/ */
.detail-activity {
padding-inline-end: 0;
}
.detail-activity:not(.detail-activity--interventions) > h4, .detail-activity:not(.detail-activity--interventions) > h4,
.detail-activity:not(.detail-activity--interventions) > .detail-log-truncated, .detail-activity:not(.detail-activity--interventions) > .detail-log-truncated,
.detail-activity:not(.detail-activity--interventions) > .detail-log-loading, .detail-activity:not(.detail-activity--interventions) > .detail-log-loading,
@@ -3332,11 +3334,6 @@ Live and Feed Activity expansion overlays the content instead of reserving a too
padding-inline-end: calc(var(--space-2xl) + var(--space-sm)); padding-inline-end: calc(var(--space-2xl) + var(--space-sm));
} }
/* FNXC:PlannerOversight 2026-07-05-00:00: mobile reserves more padding for the overlay toggle; neutralize it for Interventions too so FN-7519's timeline stays full width on mobile per FN-7581. */
.detail-activity--interventions {
padding-inline-end: 0;
}
.activity-expand-toggle { .activity-expand-toggle {
min-inline-size: calc(var(--space-2xl) + var(--space-sm)); min-inline-size: calc(var(--space-2xl) + var(--space-sm));
min-block-size: calc(var(--space-2xl) + var(--space-sm)); min-block-size: calc(var(--space-2xl) + var(--space-sm));

View File

@@ -928,12 +928,14 @@ describe("TaskDetailModal", () => {
expect(css).not.toContain(".activity-toolbar"); expect(css).not.toContain(".activity-toolbar");
expect(css).not.toContain("activity-toolbar--expand-only"); expect(css).not.toContain("activity-toolbar--expand-only");
expect(css).toContain(".detail-activity {\n position: relative;\n padding-inline-end: calc(var(--space-2xl) + var(--space-md));\n}"); expect(css).toContain(".detail-activity {\n position: relative;\n padding-inline-end: 0;\n}");
expect(css).toContain(".detail-activity:not(.detail-activity--interventions) > h4,");
expect(css).toContain("padding-inline-end: calc(var(--space-2xl) + var(--space-md));");
expect(activityOverlayRule).toContain("position: absolute"); expect(activityOverlayRule).toContain("position: absolute");
expect(activityOverlayRule).toContain("top: var(--space-md)"); expect(activityOverlayRule).toContain("top: var(--space-md)");
expect(activityOverlayRule).toContain("right: var(--space-md)"); expect(activityOverlayRule).toContain("right: var(--space-md)");
expect(activityOverlayRule).toContain("z-index: 3"); expect(activityOverlayRule).toContain("z-index: 3");
expect(mobileCss).toContain(" .detail-activity {\n padding-inline-end: 0;\n }"); expect(mobileCss).not.toContain(" .detail-activity {\n padding-inline-end:");
expect(mobileCss).toContain(" .activity-expand-toggle--overlay {\n top: var(--space-sm);\n right: var(--space-sm);\n }"); expect(mobileCss).toContain(" .activity-expand-toggle--overlay {\n top: var(--space-sm);\n right: var(--space-sm);\n }");
expect(expandedTitleRule).not.toContain("display: none"); expect(expandedTitleRule).not.toContain("display: none");
expect(expandedMetaRule).toContain("display: none"); expect(expandedMetaRule).toContain("display: none");

View File

@@ -45,13 +45,12 @@ describe("TaskDetailModal CSS contract", () => {
}); });
/* /*
FNXC:TaskDetailActivity 2026-07-18-07:25: FNXC:TaskDetailActivity 2026-07-27-02:15:
FN-8166 zeroed mobile `.detail-activity` padding-inline-end (equal insets from FN-8624 extends FN-8166's symmetric Activity container inset from mobile to
`.detail-body`); overlay clearance lives only on first-row selectors. Keep the desktop and tablet. Only possible overlay-covered first rows may reserve tokenized
contract aligned with TaskDetailModal.css so full-suite does not assert the clearance, so modal, pop-out, and embedded task-detail renders share this CSS contract.
pre-8166 `var(--space-md)` residual inset.
*/ */
it("FN-8154 keeps the mobile Feed inset narrow while clearing its overlay toggle from first rows", async () => { it("FN-8624 keeps Activity container insets symmetric while clearing the overlay from first rows", async () => {
const css = await loadAllAppCss(); const css = await loadAllAppCss();
const baseCss = await loadAllAppCssBaseOnly(); const baseCss = await loadAllAppCssBaseOnly();
const mobileCss = css.slice(css.indexOf("@media (max-width: 768px)")); const mobileCss = css.slice(css.indexOf("@media (max-width: 768px)"));
@@ -63,13 +62,13 @@ describe("TaskDetailModal CSS contract", () => {
".detail-activity:not(.detail-activity--interventions) > .detail-activity-list > .detail-log-entry:first-child", ".detail-activity:not(.detail-activity--interventions) > .detail-activity-list > .detail-log-entry:first-child",
]; ];
expect(baseCss).toContain(".detail-activity {\n position: relative;\n padding-inline-end: calc(var(--space-2xl) + var(--space-md));\n}"); expect(baseCss).toContain(".detail-activity {\n position: relative;\n padding-inline-end: 0;\n}");
expect(mobileCss).toContain(" .detail-activity {\n padding-inline-end: 0;\n }"); expect(baseCss).toContain(".detail-activity--interventions {\n padding-inline-end: 0;\n}");
expect(mobileCss).not.toContain(" .detail-activity {\n padding-inline-end: calc(var(--space-2xl) + var(--space-lg));\n }"); expect(baseCss).not.toContain(".detail-activity {\n position: relative;\n padding-inline-end: calc(var(--space-2xl) + var(--space-md));\n}");
expect(mobileCss).not.toContain(" .detail-activity {\n padding-inline-end: var(--space-md);\n }"); expect(mobileCss).not.toContain(" .detail-activity {\n padding-inline-end:");
expect(mobileCss).toContain(" .detail-activity--interventions {\n padding-inline-end: 0;\n }");
for (const selector of firstRowSelectors) { for (const selector of firstRowSelectors) {
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
expect(baseCss, selector).toMatch(new RegExp(`${escapedSelector}(?:\\s*,\\s*[^{}]+)*\\s*\\{[^}]*padding-inline-end: calc\\(var\\(--space-2xl\\) \\+ var\\(--space-md\\)\\);`));
expect(mobileCss, selector).toMatch(new RegExp(`${escapedSelector}(?:\\s*,\\s*[^{}]+)*\\s*\\{[^}]*padding-inline-end: calc\\(var\\(--space-2xl\\) \\+ var\\(--space-sm\\)\\);`)); expect(mobileCss, selector).toMatch(new RegExp(`${escapedSelector}(?:\\s*,\\s*[^{}]+)*\\s*\\{[^}]*padding-inline-end: calc\\(var\\(--space-2xl\\) \\+ var\\(--space-sm\\)\\);`));
} }
}); });

View File

@@ -729,23 +729,24 @@ describe("TaskDetailModal", () => {
expect(css).not.toContain(".activity-toolbar"); expect(css).not.toContain(".activity-toolbar");
expect(css).not.toContain("activity-toolbar--expand-only"); expect(css).not.toContain("activity-toolbar--expand-only");
expect(css).toContain(".detail-activity {\n position: relative;\n padding-inline-end: calc(var(--space-2xl) + var(--space-md));\n}"); expect(css).toContain(".detail-activity {\n position: relative;\n padding-inline-end: 0;\n}");
expect(css).toContain(".detail-activity:not(.detail-activity--interventions) > h4,");
expect(css).toContain("padding-inline-end: calc(var(--space-2xl) + var(--space-md));");
expect(overlayBlock).toContain("position: absolute;"); expect(overlayBlock).toContain("position: absolute;");
expect(overlayBlock).toContain("top: var(--space-md);"); expect(overlayBlock).toContain("top: var(--space-md);");
expect(overlayBlock).toContain("right: var(--space-md);"); expect(overlayBlock).toContain("right: var(--space-md);");
expect(mobileBlock).toContain(" .detail-activity {\n padding-inline-end: 0;\n }"); expect(mobileBlock).not.toContain(" .detail-activity {\n padding-inline-end:");
expect(mobileOverlayBlock).toContain("top: var(--space-sm);"); expect(mobileOverlayBlock).toContain("top: var(--space-sm);");
expect(mobileOverlayBlock).toContain("right: var(--space-sm);"); expect(mobileOverlayBlock).toContain("right: var(--space-sm);");
/* /*
FNXC:TaskDetailActivity 2026-07-16-00:00: FNXC:TaskDetailActivity 2026-07-27-02:15:
FN-8166 requires Feed to inherit the symmetric mobile `.detail-body` inset, FN-8624 requires Feed to inherit the symmetric `.detail-body` inset at every
while only its first visible row reserves space for the opaque overlay toggle. breakpoint, while only its first visible row reserves space for the opaque overlay.
This contract also prevents non-Feed task-detail surfaces from adding a This contract covers modal, pop-out, embedded, and mobile task-detail surfaces.
container-level right-only inset or mobile horizontal overflow.
*/ */
const mobileDetailBodyBlock = getExactCssRuleBlock(mobileBlock, ".detail-body"); const mobileDetailBodyBlock = getExactCssRuleBlock(mobileBlock, ".detail-body");
const mobileInterventionsBlock = getExactCssRuleBlock(mobileBlock, ".detail-activity--interventions"); const baseInterventionsBlock = getExactCssRuleBlock(css, ".detail-activity--interventions");
const mobilePrBlock = getExactCssRuleBlock( const mobilePrBlock = getExactCssRuleBlock(
getCssAtRuleBlockContainingExactRule(css, "@media (max-width: 768px)", ".detail-pr-tab"), getCssAtRuleBlockContainingExactRule(css, "@media (max-width: 768px)", ".detail-pr-tab"),
".detail-pr-tab", ".detail-pr-tab",
@@ -758,7 +759,7 @@ describe("TaskDetailModal", () => {
expect(mobileDetailBodyBlock).toContain("padding: calc(var(--space-md) + var(--space-xs) / 2);"); expect(mobileDetailBodyBlock).toContain("padding: calc(var(--space-md) + var(--space-xs) / 2);");
expect(mobileDetailBodyBlock).toContain("overflow-x: hidden;"); expect(mobileDetailBodyBlock).toContain("overflow-x: hidden;");
expect(mobileInterventionsBlock).toContain("padding-inline-end: 0;"); expect(baseInterventionsBlock).toContain("padding-inline-end: 0;");
expect(mobileBlock).toContain(".detail-activity:not(.detail-activity--interventions) > h4,"); expect(mobileBlock).toContain(".detail-activity:not(.detail-activity--interventions) > h4,");
expect(mobileBlock).toContain(".detail-activity:not(.detail-activity--interventions) > .detail-log-loading,"); expect(mobileBlock).toContain(".detail-activity:not(.detail-activity--interventions) > .detail-log-loading,");
expect(mobileBlock).toContain(".detail-activity:not(.detail-activity--interventions) > .detail-log-empty,"); expect(mobileBlock).toContain(".detail-activity:not(.detail-activity--interventions) > .detail-log-empty,");
@@ -797,6 +798,7 @@ describe("TaskDetailModal", () => {
const mobileScrollbarBlock = getExactCssRuleBlock(mobileBlock, ".detail-body::-webkit-scrollbar"); const mobileScrollbarBlock = getExactCssRuleBlock(mobileBlock, ".detail-body::-webkit-scrollbar");
const mobileActivityBlock = getExactCssRuleBlock(mobileBlock, ".detail-activity"); const mobileActivityBlock = getExactCssRuleBlock(mobileBlock, ".detail-activity");
const mobileInterventionsBlock = getExactCssRuleBlock(mobileBlock, ".detail-activity--interventions"); const mobileInterventionsBlock = getExactCssRuleBlock(mobileBlock, ".detail-activity--interventions");
const baseInterventionsBlock = getExactCssRuleBlock(css, ".detail-activity--interventions");
const mobilePrBlock = getExactCssRuleBlock( const mobilePrBlock = getExactCssRuleBlock(
getCssAtRuleBlockContainingExactRule(css, "@media (max-width: 768px)", ".detail-pr-tab"), getCssAtRuleBlockContainingExactRule(css, "@media (max-width: 768px)", ".detail-pr-tab"),
".detail-pr-tab", ".detail-pr-tab",
@@ -815,9 +817,10 @@ describe("TaskDetailModal", () => {
expect(mobileDetailBodyBlock).toContain("scrollbar-width: none;"); expect(mobileDetailBodyBlock).toContain("scrollbar-width: none;");
expect(mobileScrollbarBlock).toContain("display: none;"); expect(mobileScrollbarBlock).toContain("display: none;");
expect(mobileActivityBlock).toContain("padding-inline-end: 0;"); expect(mobileActivityBlock).toBe("");
expect(mobileBlock).toContain("padding-inline-end: calc(var(--space-2xl) + var(--space-sm));"); expect(mobileBlock).toContain("padding-inline-end: calc(var(--space-2xl) + var(--space-sm));");
expect(mobileInterventionsBlock).toContain("padding-inline-end: 0;"); expect(mobileInterventionsBlock).toBe("");
expect(baseInterventionsBlock).toContain("padding-inline-end: 0;");
expect(mobilePrBlock.trim()).toBe("gap: var(--space-md);"); expect(mobilePrBlock.trim()).toBe("gap: var(--space-md);");
expect(allMobileCss).not.toMatch(/\.task-changes-tab\s*\{[^}]*\bpadding(?:-[\w-]+)?\s*:/); expect(allMobileCss).not.toMatch(/\.task-changes-tab\s*\{[^}]*\bpadding(?:-[\w-]+)?\s*:/);
expect(embeddedBodyBlock).toContain("width: 100%;"); expect(embeddedBodyBlock).toContain("width: 100%;");