diff --git a/.changeset/fn-7773-mobile-settings-footer.md b/.changeset/fn-7773-mobile-settings-footer.md
new file mode 100644
index 0000000000..50d79115e4
--- /dev/null
+++ b/.changeset/fn-7773-mobile-settings-footer.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": patch
+---
+
+summary: Fix mobile Settings footer spacing so version text no longer overlaps actions.
+category: fix
+dev: Tightens the Settings modal mobile footer rail and adds CSS regression coverage.
diff --git a/packages/dashboard/app/components/SettingsModal.css b/packages/dashboard/app/components/SettingsModal.css
index 57161eec41..3bc8fee04b 100644
--- a/packages/dashboard/app/components/SettingsModal.css
+++ b/packages/dashboard/app/components/SettingsModal.css
@@ -246,13 +246,20 @@ The embedded title reads like other embedded-view titles (Planning modal-header-
/*
FNXC:Settings 2026-07-09-00:00:
FN-7752 makes the mobile Settings header and footer shorter at ≤768px only. Keep the FN-4375 one-line header invariant while trimming vertical padding in both standalone modal and embedded SettingsView presentations.
+
+ FNXC:Settings 2026-07-10-00:00:
+ FN-7773 further tightens only the mobile footer rail so Version, Help, Import/Export, Reset, and Save read as one contained row without affecting desktop/tablet sizing.
*/
- .settings-modal .modal-header,
- .settings-modal .modal-actions {
+ .settings-modal .modal-header {
padding-block: var(--space-sm);
padding-inline: var(--space-md);
}
+ .settings-modal .modal-actions {
+ padding-block: var(--space-xs);
+ padding-inline: var(--space-md);
+ }
+
.settings-modal--embedded .modal-header--embedded {
min-height: 0;
}
@@ -304,6 +311,9 @@ The embedded title reads like other embedded-view titles (Planning modal-header-
/*
FNXC:Settings 2026-07-09-00:00:
FN-7752 requires the mobile Settings footer to remain a single horizontal row in both standalone and embedded presentations. Keep Version/Help, Export/Import/Reset, and Cancel/Save on one nowrap rail that can scroll horizontally instead of stacking into multiple rows.
+
+ FNXC:Settings 2026-07-10-00:00:
+ FN-7773 fixes the observed mobile overlap by centering every footer sub-region on the rail and making the Version text a non-wrapping inline box. Keep these selectors under the mobile media query so desktop/tablet footer flow remains unchanged.
*/
.settings-modal .modal-actions {
flex-wrap: nowrap;
@@ -313,31 +323,58 @@ The embedded title reads like other embedded-view titles (Planning modal-header-
overflow-y: hidden;
}
- .settings-modal .modal-actions-left,
- .settings-modal .modal-actions-right {
+ .settings-modal .modal-actions-left {
+ align-items: center;
flex-shrink: 0;
gap: var(--space-xs);
}
- .settings-modal-footer-version {
- flex: 0 1 auto;
- margin-right: 0;
+ .settings-modal .modal-actions-right {
+ align-items: center;
+ flex-shrink: 0;
+ gap: var(--space-xs);
}
- .settings-update-check {
+ .settings-modal .settings-modal-footer-version {
+ align-items: center;
+ align-self: center;
+ flex: 0 0 auto;
+ margin-right: 0;
+ min-width: max-content;
+ }
+
+ .settings-modal .settings-update-check {
+ align-items: center;
flex-wrap: nowrap;
row-gap: 0;
}
- .settings-update-result {
+ .settings-modal .settings-update-result {
+ align-self: center;
flex: 0 1 auto;
+ line-height: 1;
white-space: nowrap;
}
- .settings-footer-help-btn,
- .settings-version-check-btn {
+ .settings-modal .settings-footer-help-btn {
+ align-items: center;
flex-shrink: 0;
}
+
+ .settings-modal .settings-version-check-btn {
+ align-items: center;
+ flex-shrink: 0;
+ line-height: 1;
+ white-space: nowrap;
+ }
+
+ .settings-modal .settings-modal-version {
+ align-items: center;
+ display: inline-flex;
+ flex-shrink: 0;
+ line-height: 1;
+ white-space: nowrap;
+ }
}
.settings-version-check-btn {
diff --git a/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx b/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx
index 59fc7d2e0c..54e803bf93 100644
--- a/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx
+++ b/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx
@@ -4,7 +4,7 @@ import path from "node:path";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
-import { SettingsModal } from "../SettingsModal";
+import { SettingsModal, SettingsView } from "../SettingsModal";
import type { Settings } from "@fusion/core";
@@ -150,7 +150,7 @@ vi.mock("../../hooks/useMemoryBackendStatus", () => ({
})),
}));
-import { fetchSettings, updateSettings } from "../../api";
+import { fetchDashboardHealth, fetchSettings, updateSettings } from "../../api";
function mockSettingsViewport(matches: boolean): void {
Object.defineProperty(window, "matchMedia", {
@@ -240,19 +240,45 @@ describe("SettingsModal mobile adaptations", () => {
expect(modalHeader?.contains(version)).toBe(false);
});
- it("keeps update-check button clickable from the footer", async () => {
+ it("keeps update-check button clickable from the standalone and embedded mobile footers", async () => {
mockSettingsViewport(true);
const user = userEvent.setup();
- const { container } = render();
+ const standalone = render();
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
- const modalActions = container.querySelector(".modal-actions");
+ const standaloneActions = standalone.container.querySelector(".settings-modal:not(.settings-modal--embedded) .modal-actions");
+ expect(standaloneActions).toBeTruthy();
+
+ const standaloneUpdateButton = within(standaloneActions as HTMLElement).getByRole("button", { name: "Check for updates" });
+ await user.click(standaloneUpdateButton);
+ expect(standaloneUpdateButton.closest(".settings-modal-footer-version")).toBeTruthy();
+
+ standalone.unmount();
+ vi.clearAllMocks();
+
+ const embedded = render();
+ await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
+
+ const embeddedActions = embedded.container.querySelector(".settings-modal--embedded .modal-actions");
+ expect(embeddedActions).toBeTruthy();
+ const embeddedUpdateButton = within(embeddedActions as HTMLElement).getByRole("button", { name: "Check for updates" });
+ await user.click(embeddedUpdateButton);
+ expect(embeddedUpdateButton.closest(".settings-modal-footer-version")).toBeTruthy();
+ });
+
+ it("omits the version button when appVersion is unavailable without removing the footer rail", async () => {
+ vi.mocked(fetchDashboardHealth).mockResolvedValueOnce({ status: "ok", version: "", uptime: 120 });
+ mockSettingsViewport(true);
+ const { container, queryByRole } = render();
+ await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
+ await waitFor(() => expect(fetchDashboardHealth).toHaveBeenCalled());
+
+ const modalActions = container.querySelector(".settings-modal:not(.settings-modal--embedded) .modal-actions");
expect(modalActions).toBeTruthy();
-
- const updateButton = within(modalActions as HTMLElement).getByRole("button", { name: "Check for updates" });
- await user.click(updateButton);
-
- expect(updateButton).toBeTruthy();
+ expect(within(modalActions as HTMLElement).getByRole("link", { name: "Help and discussions" })).toBeTruthy();
+ expect(queryByRole("button", { name: "Check for updates" })).toBeNull();
+ expect(container.querySelector(".settings-modal-footer-version")).toBeTruthy();
+ expect(container.querySelector(".settings-update-check")).toBeTruthy();
});
it("keeps update-now button reachable from the mobile footer", async () => {
@@ -540,11 +566,23 @@ describe("SettingsModal mobile adaptations", () => {
expectMobileRule(css, ".settings-preset-item", "flex-direction: column;");
expectMobileRule(css, ".settings-preset-item-actions", "justify-content: flex-start;");
expectMobileRule(css, ".settings-preset-size-grid", "grid-template-columns: 1fr;");
+ expectMobileRule(css, ".settings-modal .modal-actions", "padding-block: var(--space-xs);");
expectMobileRule(css, ".settings-modal .modal-actions", "flex-wrap: nowrap;");
+ expectMobileRule(css, ".settings-modal .modal-actions", "align-items: center;");
expectMobileRule(css, ".settings-modal .modal-actions", "overflow-x: auto;");
- expectMobileRule(css, ".settings-update-check", "flex-wrap: nowrap;");
- expect(css).toContain(".settings-modal .modal-header,\n .settings-modal .modal-actions");
- expect(css).toContain("padding-block: var(--space-sm);");
+ expectMobileRule(css, ".settings-modal .modal-actions-left", "align-items: center;");
+ expectMobileRule(css, ".settings-modal .modal-actions-right", "align-items: center;");
+ expectMobileRule(css, ".settings-modal .settings-modal-footer-version", "align-self: center;");
+ expectMobileRule(css, ".settings-modal .settings-modal-footer-version", "flex: 0 0 auto;");
+ expectMobileRule(css, ".settings-modal .settings-modal-footer-version", "min-width: max-content;");
+ expectMobileRule(css, ".settings-modal .settings-update-check", "align-items: center;");
+ expectMobileRule(css, ".settings-modal .settings-update-check", "flex-wrap: nowrap;");
+ expectMobileRule(css, ".settings-modal .settings-version-check-btn", "line-height: 1;");
+ expectMobileRule(css, ".settings-modal .settings-version-check-btn", "white-space: nowrap;");
+ expectMobileRule(css, ".settings-modal .settings-modal-version", "display: inline-flex;");
+ expectMobileRule(css, ".settings-modal .settings-modal-version", "line-height: 1;");
+ expectMobileRule(css, ".settings-modal .settings-modal-version", "white-space: nowrap;");
+ expect(css).toContain(".settings-modal .modal-header {\n padding-block: var(--space-sm);");
expectMobileRule(css, ".auth-provider-header > div:not(.auth-provider-info):not(.auth-apikey-section)", "margin-left: auto;");
expectMobileRule(css, ".auth-apikey-section", "align-items: flex-end;");
expectMobileRule(css, ".auth-apikey-input-row", "justify-content: flex-end;");