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
This commit is contained in:
gsxdsm
2026-06-09 23:16:11 -07:00
parent 3f6e1c6e40
commit d7e14542a9
3 changed files with 68 additions and 0 deletions

View File

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

View File

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

View File

@@ -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<ReturnType<typeof useNodes>> = {}
};
}
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(<div className="nodes-management-overlay"><NodesView addToast={vi.fn()} onClose={vi.fn()} /></div>);
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" })],