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
This commit is contained in:
gsxdsm
2026-06-16 19:49:45 -07:00
parent 21c4d3e5ca
commit 98cb80d88d
5 changed files with 94 additions and 9 deletions

View File

@@ -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.

View File

@@ -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", () => {

View File

@@ -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));
}
}

View File

@@ -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(
<TaskDetailModal

View File

@@ -81,7 +81,10 @@ describe("core modals mobile css coverage", () => {
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;");