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" })],