From d7e14542a9a8962936a969451c494e5db5b87258 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 9 Jun 2026 23:16:11 -0700 Subject: [PATCH] FN-6170: make mobile nodes view fullscreen Make the mobile Nodes screen open as a full-screen overlay above the header and nav. - add a mobile-only fixed fullscreen overlay style for the nodes management panel - preserve safe-area spacing and scroll behavior while keeping the overlay above mobile navigation - add a NodesView regression test that asserts the mobile overlay CSS contract and close control rendering Files changed: .changeset/fn-6170-mobile-nodes-fullscreen.md | 5 +++ packages/dashboard/app/components/NodesView.css | 11 +++++ .../app/components/__tests__/NodesView.test.tsx | 52 ++++++++++++++++++++++ 3 files changed, 68 insertions(+) Fusion-Task-Id: FN-6170 Fusion-Task-Lineage: 16995d9f-6b5e-4f15-9367-7eccbd3153d0 --- .changeset/fn-6170-mobile-nodes-fullscreen.md | 5 ++ .../dashboard/app/components/NodesView.css | 11 ++++ .../components/__tests__/NodesView.test.tsx | 52 +++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 .changeset/fn-6170-mobile-nodes-fullscreen.md diff --git a/.changeset/fn-6170-mobile-nodes-fullscreen.md b/.changeset/fn-6170-mobile-nodes-fullscreen.md new file mode 100644 index 0000000000..7c4e008ba6 --- /dev/null +++ b/.changeset/fn-6170-mobile-nodes-fullscreen.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Make the Nodes screen open as a full-screen mobile overlay so it covers the header while staying above the mobile nav. diff --git a/packages/dashboard/app/components/NodesView.css b/packages/dashboard/app/components/NodesView.css index d9f1f221e1..4af3cc0d18 100644 --- a/packages/dashboard/app/components/NodesView.css +++ b/packages/dashboard/app/components/NodesView.css @@ -605,6 +605,17 @@ /* ── Nodes View Mobile Responsive ──────────────────────────────────────── */ @media (max-width: 768px) { + .nodes-management-overlay { + position: fixed; + inset: 0; + z-index: 50; + background: var(--bg); + padding-top: max(var(--space-sm), env(safe-area-inset-top, 0px)); + padding-bottom: calc(var(--mobile-nav-height, 0px) + max(env(safe-area-inset-bottom, 0px), 12px)); + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } + .nodes-view { padding-inline: var(--space-sm); } diff --git a/packages/dashboard/app/components/__tests__/NodesView.test.tsx b/packages/dashboard/app/components/__tests__/NodesView.test.tsx index c4af65c624..98edc470fd 100644 --- a/packages/dashboard/app/components/__tests__/NodesView.test.tsx +++ b/packages/dashboard/app/components/__tests__/NodesView.test.tsx @@ -8,6 +8,7 @@ import { useNodeSettingsSync } from "../../hooks/useNodeSettingsSync"; import { useManagedDockerNodes } from "../../hooks/useManagedDockerNodes"; import { useMeshState } from "../../hooks/useMeshState"; import type { NodeSettingsSyncStatus } from "../../api-node"; +import { loadAllAppCss } from "../../test/cssFixture"; vi.mock("../../hooks/useNodes", () => ({ useNodes: vi.fn(), @@ -105,6 +106,36 @@ function makeUseNodesResult(overrides: Partial> = {} }; } +function extractMobileMediaBlocks(css: string): string { + const blocks: string[] = []; + const regex = /@media[^{]*\(max-width: 768px\)[^{]*\{/g; + let match: RegExpExecArray | null; + + while ((match = regex.exec(css)) !== null) { + const startIdx = match.index + match[0].length; + let braceCount = 1; + let endIdx = startIdx; + + while (braceCount > 0 && endIdx < css.length) { + if (css[endIdx] === "{") braceCount += 1; + if (css[endIdx] === "}") braceCount -= 1; + endIdx += 1; + } + + if (braceCount === 0) { + blocks.push(css.slice(startIdx, endIdx - 1)); + } + } + + return blocks.join("\n"); +} + +function extractRuleBlock(css: string, selector: string): string { + const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const matches = [...css.matchAll(new RegExp(`${escapedSelector}\\s*\\{([^}]*)\\}`, "g"))]; + return matches.at(-1)?.[1] ?? ""; +} + beforeEach(() => { mockUseProjects.mockReturnValue({ projects: [], @@ -150,6 +181,27 @@ beforeEach(() => { }); describe("NodesView", () => { + it("defines nodes overlay as a fixed fullscreen mobile panel", () => { + const mobileCss = extractMobileMediaBlocks(loadAllAppCss()); + const overlayRule = extractRuleBlock(mobileCss, ".nodes-management-overlay"); + + expect(overlayRule).toContain("position: fixed;"); + expect(overlayRule).toContain("inset: 0;"); + expect(overlayRule).toContain("z-index: 50;"); + expect(overlayRule).toContain("background: var(--bg);"); + expect(overlayRule).toContain("padding-top: max(var(--space-sm), env(safe-area-inset-top, 0px));"); + expect(overlayRule).toContain("padding-bottom: calc(var(--mobile-nav-height, 0px) + max(env(safe-area-inset-bottom, 0px), 12px));"); + expect(overlayRule).toContain("overflow-y: auto;"); + expect(overlayRule).toContain("-webkit-overflow-scrolling: touch;"); + + mockUseNodes.mockReturnValue(makeUseNodesResult({ nodes: [] })); + render(
); + + const overlay = document.querySelector(".nodes-management-overlay"); + expect(overlay).toContainElement(screen.getByTestId("nodes-view")); + expect(screen.getByRole("button", { name: "Close nodes view" })).toBeInTheDocument(); + }); + it("renders docker stat and passes docker data to matching node card", () => { mockUseNodes.mockReturnValue(makeUseNodesResult({ nodes: [makeNode({ id: "node-1", name: "Alpha", type: "remote", url: "https://alpha.node" })],