From 98cb80d88d98d200a6dd3578aadba9600219e657 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 16 Jun 2026 19:49:45 -0700 Subject: [PATCH] FN-6500: fix tablet task detail modal sizing Keeps tablet task detail modals wide enough and within the viewport.\n\n- Reconcile the tablet overlay offset with modal max-height so actions remain visible.\n- Widen the tablet task detail modal to use more horizontal viewport space.\n- Add CSS-focused coverage for tablet sizing plus unchanged desktop and mobile guards.\n- Add a patch changeset for the published Fusion package.\n\nFiles changed:\n .changeset/fn-6500-tablet-task-detail-modal.md | 5 ++\n .../task-detail-modal-tablet-width.test.ts | 14 +++--\n .../dashboard/app/components/TaskDetailModal.css | 15 +++--\n ...etailModal.responsive-and-dependencies.test.tsx | 64 ++++++++++++++++++++++\n .../__tests__/core-modals-mobile.test.tsx | 5 +-\n 5 files changed, 94 insertions(+), 9 deletions(-) Fusion-Task-Id: FN-6500 Fusion-Task-Lineage: 016a2839-29f9-48de-b644-a5fcd27b35e3 --- .../fn-6500-tablet-task-detail-modal.md | 5 ++ .../task-detail-modal-tablet-width.test.ts | 14 ++-- .../app/components/TaskDetailModal.css | 15 +++-- ...Modal.responsive-and-dependencies.test.tsx | 64 +++++++++++++++++++ .../__tests__/core-modals-mobile.test.tsx | 5 +- 5 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 .changeset/fn-6500-tablet-task-detail-modal.md diff --git a/.changeset/fn-6500-tablet-task-detail-modal.md b/.changeset/fn-6500-tablet-task-detail-modal.md new file mode 100644 index 0000000000..01a4c2a87e --- /dev/null +++ b/.changeset/fn-6500-tablet-task-detail-modal.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Fix the tablet task detail modal sizing so the action footer remains on-screen and the modal uses more viewport width. diff --git a/packages/dashboard/app/__tests__/task-detail-modal-tablet-width.test.ts b/packages/dashboard/app/__tests__/task-detail-modal-tablet-width.test.ts index 9d3486e9ab..25a3027dc7 100644 --- a/packages/dashboard/app/__tests__/task-detail-modal-tablet-width.test.ts +++ b/packages/dashboard/app/__tests__/task-detail-modal-tablet-width.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest"; import { readFileSync } from "fs"; import { resolve } from "path"; -describe("task detail modal tablet width (FN-5599)", () => { +describe("task detail modal tablet width (FN-5599, FN-6500)", () => { const detailModalCss = readFileSync( resolve(__dirname, "../components/TaskDetailModal.css"), "utf-8", @@ -14,17 +14,23 @@ describe("task detail modal tablet width (FN-5599)", () => { expect(baseRuleMatch![0]).toContain("width: min(95vw, 800px);"); }); - it("defines a tablet breakpoint override for task detail modal width", () => { + it("defines a tablet breakpoint override for task detail modal width and height coupling", () => { const tabletBlockMatch = detailModalCss.match( /@media\s*\(min-width:\s*769px\)\s*and\s*\(max-width:\s*1024px\)\s*\{([\s\S]*?)\n\}/, ); expect(tabletBlockMatch).toBeTruthy(); const tabletBlock = tabletBlockMatch![1]; + const overlayRuleMatch = tabletBlock.match(/\.modal-overlay:has\(\.task-detail-modal\)\s*\{[^}]*\}/s); const modalRuleMatch = tabletBlock.match(/\.modal\.task-detail-modal\s*\{[^}]*\}/s); + const overlayOffset = overlayRuleMatch?.[0].match(/--overlay-padding-top:\s*([^;]+);/)?.[1]?.trim(); + const maxHeightOffset = modalRuleMatch?.[0].match(/max-height:\s*calc\(100dvh - var\(--overlay-padding-top,\s*([^)]+)\) - var\(--space-md\)\);/)?.[1]?.trim(); + + expect(overlayRuleMatch).toBeTruthy(); expect(modalRuleMatch).toBeTruthy(); - expect(modalRuleMatch![0]).toContain("width: min(96vw, 1024px);"); - expect(modalRuleMatch![0]).toContain("max-width: 96vw;"); + expect(maxHeightOffset).toBe(overlayOffset); + expect(modalRuleMatch![0]).toContain("width: 98vw;"); + expect(modalRuleMatch![0]).toContain("max-width: 98vw;"); }); it("keeps mobile full-screen sheet width behavior", () => { diff --git a/packages/dashboard/app/components/TaskDetailModal.css b/packages/dashboard/app/components/TaskDetailModal.css index 2835cb93f1..b065e54850 100644 --- a/packages/dashboard/app/components/TaskDetailModal.css +++ b/packages/dashboard/app/components/TaskDetailModal.css @@ -962,13 +962,20 @@ The task-detail modal metadata must keep priority, execution mode, provenance, P font-size: calc(var(--space-sm) + var(--space-xs) * 0.75); } -/* FN-5599: widen task detail modal on tablet viewports. */ +/* +FNXC:TaskDetailModalResponsive 2026-06-16-19:13: +FN-6500 fixes a tablet regression from FN-5599: the task-detail overlay offset and modal max-height subtraction must use the same `--overlay-padding-top` value so the `.modal-actions` footer remains on-screen, while the modal uses more of the tablet viewport to avoid a cramped layout. +*/ @media (min-width: 769px) and (max-width: 1024px) { + .modal-overlay:has(.task-detail-modal) { + --overlay-padding-top: 6vh; + } + .modal.task-detail-modal { - width: min(96vw, 1024px); - max-width: 96vw; + width: 98vw; + max-width: 98vw; height: 92vh; - max-height: calc(100dvh - var(--overlay-padding-top, 6vh) - 16px); + max-height: calc(100dvh - var(--overlay-padding-top, 6vh) - var(--space-md)); } } 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 06cfa5c8dc..d6ee36072a 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 @@ -13,6 +13,7 @@ import { mockConfirmWithCheckbox, mockUsePluginUiSlots, expectBaseRule, + getCssRuleBlock, readDashboardStylesSource, setupTaskDetailModalHooks, } from "./TaskDetailModal.test-helpers"; @@ -20,6 +21,38 @@ import { TaskDetailModal, TaskDetailContent } from "../TaskDetailModal"; setupTaskDetailModalHooks(); +function getCssAtRuleBlock(css: string, atRule: string, startAt = 0): { block: string; endIndex: number } { + const atRuleStart = css.indexOf(atRule, startAt); + expect(atRuleStart).toBeGreaterThanOrEqual(0); + const openingBrace = css.indexOf("{", atRuleStart); + expect(openingBrace).toBeGreaterThanOrEqual(0); + + let depth = 0; + for (let index = openingBrace; index < css.length; index += 1) { + const char = css[index]; + if (char === "{") depth += 1; + if (char === "}") depth -= 1; + if (depth === 0) { + return { block: css.slice(openingBrace + 1, index), endIndex: index + 1 }; + } + } + + throw new Error(`Missing closing brace for ${atRule}`); +} + +function getCssAtRuleBlockContaining(css: string, atRule: string, selector: string): string { + let startAt = 0; + while (startAt < css.length) { + const { block, endIndex } = getCssAtRuleBlock(css, atRule, startAt); + if (block.includes(selector)) { + return block; + } + startAt = endIndex; + } + + throw new Error(`Missing ${atRule} block containing ${selector}`); +} + describe("TaskDetailModal", () => { describe("mobile responsive structure", () => { it("keeps detail metadata as a single wrapping flex row without mobile column fallbacks", () => { @@ -52,6 +85,37 @@ describe("TaskDetailModal", () => { expect(css).not.toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.detail-timestamps\s*\{[^}]*flex-direction:\s*column;/); expect(css).not.toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.detail-timestamp-separator\s*\{[^}]*display:\s*none;/); }); + it("keeps desktop and mobile modal sizing guards unchanged", () => { + const css = readDashboardStylesSource(); + const mobileBlock = getCssAtRuleBlockContaining(css, "@media (max-width: 768px)", ".modal-overlay:has(.task-detail-modal)"); + const mobileOverlayBlock = getCssRuleBlock(mobileBlock, ".modal-overlay:has(.task-detail-modal)"); + const mobileModalBlock = getCssRuleBlock(mobileBlock, ".modal.task-detail-modal"); + + expectBaseRule(css, ".modal.task-detail-modal", "width: min(95vw, 800px);"); + expectBaseRule(css, ".modal.task-detail-modal", "height: 85vh;"); + expect(mobileOverlayBlock).toContain("padding-top: 0;"); + expect(mobileOverlayBlock).toContain("align-items: stretch;"); + expect(mobileModalBlock).toContain("width: 100vw;"); + expect(mobileModalBlock).toContain("height: 100dvh;"); + }); + + it("reconciles tablet overlay offset with task-detail max-height and widens the modal", () => { + const css = readDashboardStylesSource(); + const tabletBlock = getCssAtRuleBlockContaining(css, "@media (min-width: 769px) and (max-width: 1024px)", ".modal.task-detail-modal"); + const tabletOverlayBlock = getCssRuleBlock(tabletBlock, ".modal-overlay:has(.task-detail-modal)"); + const tabletModalBlock = getCssRuleBlock(tabletBlock, ".modal.task-detail-modal"); + const overlayOffset = tabletOverlayBlock.match(/--overlay-padding-top:\s*([^;]+);/)?.[1]?.trim(); + const maxHeightOffset = tabletModalBlock.match(/max-height:\s*calc\(100dvh - var\(--overlay-padding-top,\s*([^)]+)\) - var\(--space-md\)\);/)?.[1]?.trim(); + + expect(overlayOffset).toBeTruthy(); + expect(maxHeightOffset).toBe(overlayOffset); + expect(tabletModalBlock).toContain("width: 98vw;"); + expect(tabletModalBlock).toContain("max-width: 98vw;"); + expect(tabletModalBlock).toContain("height: 92vh;"); + expect(tabletModalBlock).not.toContain("width: min(96vw, 1024px);"); + expect(tabletModalBlock).not.toContain("16px"); + }); + it("renders responsive structural classes (modal-lg, overlay, spacer, tabs, detail-body)", () => { const { container } = render( { const tabletRule = getLastRuleBlock(tabletBlock, ".modal.task-detail-modal"); expect(tabletRule).toContain("height: 92vh;"); expect(extractVhHeight(tabletRule)).toBeGreaterThan(extractVhHeight(baseRule)); - expect(tabletRule).toContain("max-height: calc(100dvh - var(--overlay-padding-top, 6vh) - 16px);"); + expect(tabletRule).toContain("width: 98vw;"); + expect(tabletRule).toContain("max-width: 98vw;"); + expect(tabletBlock).toContain("--overlay-padding-top: 6vh;"); + expect(tabletRule).toContain("max-height: calc(100dvh - var(--overlay-padding-top, 6vh) - var(--space-md));"); const mobileRule = getLastRuleBlock(mobileBlock, ".modal.task-detail-modal"); expect(mobileRule).toContain("height: 100dvh;");