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:
5
.changeset/fn-6170-mobile-nodes-fullscreen.md
Normal file
5
.changeset/fn-6170-mobile-nodes-fullscreen.md
Normal 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.
|
||||||
@@ -605,6 +605,17 @@
|
|||||||
|
|
||||||
/* ── Nodes View Mobile Responsive ──────────────────────────────────────── */
|
/* ── Nodes View Mobile Responsive ──────────────────────────────────────── */
|
||||||
@media (max-width: 768px) {
|
@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 {
|
.nodes-view {
|
||||||
padding-inline: var(--space-sm);
|
padding-inline: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useNodeSettingsSync } from "../../hooks/useNodeSettingsSync";
|
|||||||
import { useManagedDockerNodes } from "../../hooks/useManagedDockerNodes";
|
import { useManagedDockerNodes } from "../../hooks/useManagedDockerNodes";
|
||||||
import { useMeshState } from "../../hooks/useMeshState";
|
import { useMeshState } from "../../hooks/useMeshState";
|
||||||
import type { NodeSettingsSyncStatus } from "../../api-node";
|
import type { NodeSettingsSyncStatus } from "../../api-node";
|
||||||
|
import { loadAllAppCss } from "../../test/cssFixture";
|
||||||
|
|
||||||
vi.mock("../../hooks/useNodes", () => ({
|
vi.mock("../../hooks/useNodes", () => ({
|
||||||
useNodes: vi.fn(),
|
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(() => {
|
beforeEach(() => {
|
||||||
mockUseProjects.mockReturnValue({
|
mockUseProjects.mockReturnValue({
|
||||||
projects: [],
|
projects: [],
|
||||||
@@ -150,6 +181,27 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("NodesView", () => {
|
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", () => {
|
it("renders docker stat and passes docker data to matching node card", () => {
|
||||||
mockUseNodes.mockReturnValue(makeUseNodesResult({
|
mockUseNodes.mockReturnValue(makeUseNodesResult({
|
||||||
nodes: [makeNode({ id: "node-1", name: "Alpha", type: "remote", url: "https://alpha.node" })],
|
nodes: [makeNode({ id: "node-1", name: "Alpha", type: "remote", url: "https://alpha.node" })],
|
||||||
|
|||||||
Reference in New Issue
Block a user