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:
5
.changeset/fn-6500-tablet-task-detail-modal.md
Normal file
5
.changeset/fn-6500-tablet-task-detail-modal.md
Normal 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.
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;");
|
||||
|
||||
Reference in New Issue
Block a user