diff --git a/.changeset/fn-7012-task-detail-mobile-tabs-scroll.md b/.changeset/fn-7012-task-detail-mobile-tabs-scroll.md new file mode 100644 index 0000000000..5b0056bce3 --- /dev/null +++ b/.changeset/fn-7012-task-detail-mobile-tabs-scroll.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Restore horizontal scrolling for mobile task detail tabs. +category: fix +dev: Keeps the task-detail tab strip scrollable across Board modal and List embedded surfaces. diff --git a/packages/dashboard/app/components/TaskDetailModal.css b/packages/dashboard/app/components/TaskDetailModal.css index ff94507f4f..f0a6b76987 100644 --- a/packages/dashboard/app/components/TaskDetailModal.css +++ b/packages/dashboard/app/components/TaskDetailModal.css @@ -3,6 +3,7 @@ display: flex; flex-direction: column; height: 100%; + min-width: 0; min-height: 0; } @@ -69,6 +70,7 @@ The gray top header band (task id + column badge) was over-padded. Trim its vert .detail-body { padding: calc(var(--space-lg) + var(--space-xs)); + min-width: 0; overflow-x: hidden; overflow-y: auto; scrollbar-color: var(--border) transparent; @@ -1012,6 +1014,17 @@ FN-6500 fixes a tablet regression from FN-5599: the task-detail overlay offset a height: 92vh; max-height: calc(100dvh - var(--overlay-padding-top, 6vh) - var(--space-md)); } + + .detail-tabs { + width: 100%; + max-inline-size: 100%; + min-width: 0; + overflow-x: auto; + overflow-y: hidden; + overscroll-behavior-inline: contain; + touch-action: pan-x pan-y; + -webkit-overflow-scrolling: touch; + } } @media (max-width: 768px) { @@ -1112,6 +1125,14 @@ Embedded in the full-width board-card panel the detail content must be width-bou max-width: 100%; } +.task-detail-content--embedded .detail-tabs { + overflow-x: auto; + overflow-y: hidden; + overscroll-behavior-inline: contain; + touch-action: pan-x pan-y; + -webkit-overflow-scrolling: touch; +} + /* The gray header row must wrap (task id left, Back-to-board right) instead of overflowing on narrow panels. */ .task-detail-content--embedded .modal-header { flex-wrap: wrap; @@ -1917,8 +1938,8 @@ The overflowing task-detail tab strip must keep horizontal touch panning enabled FNXC:TaskDetailTabs 2026-06-22-18:00: Remove the divider between the tab selector and the detail area below. Keep the active-tab underline as the only local selection affordance. -FNXC:TaskDetailTabs 2026-06-23-20:25: -Narrow mobile task detail surfaces from both Board and List must allow horizontal tab scrolling. The tab strip is the horizontal scroller, not the detail body; bound it to the available inline size and preserve pan-x so parent overflow clipping does not trap hidden tabs. +FNXC:TaskDetailTabs 2026-06-25-16:55: +Task-detail tabs must remain reachable as a one-row horizontal scroller on mobile across the Board modal and List embedded pane. The tab strip is the scroller, opts out of the global mobile pan-y lock with pan-x pan-y, and keeps tab children non-shrinking so conditional and plugin tabs overflow-scroll instead of clipping or compressing. */ .detail-tabs { display: flex; diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx index 668855b134..6280bee47f 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx @@ -57,6 +57,33 @@ function getCssAtRuleBlockContaining(css: string, atRule: string, selector: stri throw new Error(`Missing ${atRule} block containing ${selector}`); } +function getExactCssRuleBlock(css: string, selector: string): string { + const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const ruleMatch = css.match(new RegExp(`(?:^|[}\\n])\\s*${escapedSelector}\\s*\\{([^}]*)\\}`)); + return ruleMatch?.[1] ?? ""; +} + +function getCssAtRuleBlockContainingExactRule(css: string, atRule: string, selector: string): string { + let startAt = 0; + while (startAt < css.length) { + const { block, endIndex } = getCssAtRuleBlock(css, atRule, startAt); + if (getExactCssRuleBlock(block, selector)) { + return block; + } + startAt = endIndex; + } + + throw new Error(`Missing ${atRule} block containing exact ${selector}`); +} + +function expectHorizontalTabScroller(ruleBlock: string, surface: string): void { + expect(ruleBlock, `${surface} overflow-x`).toContain("overflow-x: auto;"); + expect(ruleBlock, `${surface} overflow-y`).toContain("overflow-y: hidden;"); + expect(ruleBlock, `${surface} overscroll`).toContain("overscroll-behavior-inline: contain;"); + expect(ruleBlock, `${surface} touch-action`).toContain("touch-action: pan-x pan-y;"); + expect(ruleBlock, `${surface} momentum-scroll`).toContain("-webkit-overflow-scrolling: touch;"); +} + describe("TaskDetailModal", () => { describe("mobile responsive structure", () => { it("keeps detail metadata as a single wrapping flex row without mobile column fallbacks", () => { @@ -120,6 +147,32 @@ describe("TaskDetailModal", () => { expect(tabletModalBlock).not.toContain("16px"); }); + it("keeps task-detail tabs as horizontal scrollers across modal, embedded, mobile, and tablet surfaces", () => { + const css = readDashboardStylesSource(); + const baseTabsBlock = getExactCssRuleBlock(css, ".detail-tabs"); + const mobileBlock = getCssAtRuleBlockContainingExactRule(css, "@media (max-width: 768px)", ".detail-tabs"); + const mobileTabsBlock = getExactCssRuleBlock(mobileBlock, ".detail-tabs"); + const tabletBlock = getCssAtRuleBlockContainingExactRule(css, "@media (min-width: 769px) and (max-width: 1024px)", ".detail-tabs"); + const tabletTabsBlock = getExactCssRuleBlock(tabletBlock, ".detail-tabs"); + const embeddedTabsBlock = getExactCssRuleBlock(css, ".task-detail-content--embedded .detail-tabs"); + const detailContentBlock = getCssRuleBlock(css, ".task-detail-content"); + const detailBodyBlock = getCssRuleBlock(css, ".detail-body"); + const detailTabBlock = getCssRuleBlock(css, ".detail-tab"); + + expectHorizontalTabScroller(baseTabsBlock, "base .detail-tabs"); + expectHorizontalTabScroller(mobileTabsBlock, "mobile .detail-tabs"); + expectHorizontalTabScroller(tabletTabsBlock, "tablet .detail-tabs"); + expectHorizontalTabScroller(embeddedTabsBlock, "embedded .detail-tabs"); + expect(baseTabsBlock).toContain("min-width: 0;"); + expect(mobileTabsBlock).toContain("min-width: 0;"); + expect(detailTabBlock).toContain("flex-shrink: 0;"); + expect(detailContentBlock).toContain("min-height: 0;"); + expect(detailContentBlock).toContain("min-width: 0;"); + expect(detailBodyBlock).toContain("min-width: 0;"); + expect(detailBodyBlock).not.toContain("overflow-x: auto;"); + expect(detailBodyBlock).not.toContain("overflow: hidden;"); + }); + it("renders responsive structural classes (modal-lg, overlay, spacer, tabs, detail-body)", () => { const { container } = render( { expect(container.querySelector(".detail-timestamps")).toBeTruthy(); expect(container.querySelectorAll(".detail-timestamp-item").length).toBe(2); const tabs = container.querySelectorAll(".detail-tab"); - expect(tabs.length).toBe(11); + expect(Array.from(tabs).map((tab) => tab.textContent?.trim())).toEqual([ + "Chat", + "Definition", + "Logs", + "Changes", + "Review", + "Comments", + "Artifacts", + "Model", + "Workflow", + "Stats", + "Routing", + ]); expect(tabs[0].classList.contains("detail-tab-active")).toBe(true); expect(Array.from(tabs).slice(1).every((t) => !t.classList.contains("detail-tab-active"))).toBe(true); // Responsive CSS controls sizing — no inline padding/fontSize/borderBottom leaks