diff --git a/.changeset/fn-6974-planning-tablet-actions.md b/.changeset/fn-6974-planning-tablet-actions.md new file mode 100644 index 0000000000..9c4671a74c --- /dev/null +++ b/.changeset/fn-6974-planning-tablet-actions.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Prevent Planning Mode summary buttons from overlapping on tablet screens. +category: fix +dev: Adds a tablet responsive CSS contract for Planning Mode summary action wrapping. diff --git a/packages/dashboard/app/components/PlanningModeModal.css b/packages/dashboard/app/components/PlanningModeModal.css index 4e32bf328a..c2962178ba 100644 --- a/packages/dashboard/app/components/PlanningModeModal.css +++ b/packages/dashboard/app/components/PlanningModeModal.css @@ -1251,12 +1251,41 @@ An empty footer must NOT reserve vertical space or paint its divider band. When .planning-summary-actions { justify-content: space-between; + min-width: 0; } .planning-summary-actions-right { display: flex; gap: var(--space-sm); align-items: center; + min-width: 0; + max-width: 100%; +} + +/* +FNXC:PlanningMode 2026-06-25-09:20: +Tablet embedded Planning keeps the desktop two-pane shell, so the summary footer must absorb the narrower detail pane instead of relying on the mobile full-width stack. Allow the three labeled actions to wrap inside the pane while preserving the desktop left/right grouping when space is available. +*/ +@media (min-width: 769px) and (max-width: 1024px) { + .planning-summary-actions { + flex-wrap: wrap; + align-items: flex-start; + min-width: 0; + } + + .planning-summary-actions-right { + flex: 1 1 auto; + flex-wrap: wrap; + justify-content: flex-end; + min-width: 0; + max-width: 100%; + } + + .planning-summary-actions .btn { + flex-shrink: 1; + max-width: 100%; + white-space: normal; + } } /* Loading State */ diff --git a/packages/dashboard/app/components/__tests__/PlanningModeModal.css.test.ts b/packages/dashboard/app/components/__tests__/PlanningModeModal.css.test.ts new file mode 100644 index 0000000000..96f5cd0ddd --- /dev/null +++ b/packages/dashboard/app/components/__tests__/PlanningModeModal.css.test.ts @@ -0,0 +1,52 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; +import { getMediaBlocks } from "./PlanningModeModal.test-helpers"; + +const PLANNING_CSS_PATH = resolve(__dirname, "..", "PlanningModeModal.css"); +const TABLET_SUMMARY_ACTIONS_QUERY = "@media (min-width: 769px) and (max-width: 1024px)"; +const MOBILE_ACTIONS_QUERY = "@media (max-width: 768px)"; + +function loadPlanningCss(): string { + return readFileSync(PLANNING_CSS_PATH, "utf-8"); +} + +function findRule(css: string, selector: string): string | undefined { + const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return css.match(new RegExp(`${escapedSelector}\\s*\\{[^}]*\\}`))?.[0]; +} + +function findRules(css: string, selector: string): string[] { + const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return [...css.matchAll(new RegExp(`${escapedSelector}\\s*\\{[^}]*\\}`, "g"))].map((match) => match[0]); +} + +function expectSomeRule(css: string, selector: string, pattern: RegExp): void { + expect(findRules(css, selector).some((rule) => pattern.test(rule))).toBe(true); +} + +describe("PlanningModeModal CSS responsive action contract", () => { + it("FN-6974 keeps the summary action footer from overflowing on tablet while preserving desktop and mobile affordances", () => { + const css = loadPlanningCss(); + const baseSummaryActionsRule = findRule(css, ".planning-summary-actions"); + const baseSummaryRightRule = findRule(css, ".planning-summary-actions-right"); + + expect(baseSummaryActionsRule).toContain("justify-content: space-between;"); + expect(baseSummaryRightRule).toContain("display: flex;"); + + const tabletCss = getMediaBlocks(css, TABLET_SUMMARY_ACTIONS_QUERY).join("\n"); + expect(tabletCss).toBeTruthy(); + expect(findRule(tabletCss, ".planning-summary-actions")).toMatch(/flex-wrap\s*:\s*wrap\s*;/); + expect(findRule(tabletCss, ".planning-summary-actions")).toMatch(/min-width\s*:\s*0\s*;/); + expect(findRule(tabletCss, ".planning-summary-actions-right")).toMatch(/flex-wrap\s*:\s*wrap\s*;/); + expect(findRule(tabletCss, ".planning-summary-actions-right")).toMatch(/min-width\s*:\s*0\s*;/); + expect(findRule(tabletCss, ".planning-summary-actions-right")).toMatch(/max-width\s*:\s*100%\s*;/); + expect(findRule(tabletCss, ".planning-summary-actions .btn")).toMatch(/max-width\s*:\s*100%\s*;/); + expect(findRule(tabletCss, ".planning-summary-actions .btn")).toMatch(/white-space\s*:\s*normal\s*;/); + + const mobileCss = getMediaBlocks(css, MOBILE_ACTIONS_QUERY).join("\n"); + expectSomeRule(mobileCss, ".planning-actions", /flex-direction\s*:\s*column\s*;/); + expectSomeRule(mobileCss, ".planning-summary-actions-right", /flex-direction\s*:\s*column\s*;/); + expectSomeRule(mobileCss, ".planning-summary-actions-right", /width\s*:\s*100%\s*;/); + }); +});