diff --git a/.changeset/settings-footer-update-banner-mobile.md b/.changeset/settings-footer-update-banner-mobile.md
new file mode 100644
index 0000000000..ecb8044f44
--- /dev/null
+++ b/.changeset/settings-footer-update-banner-mobile.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": patch
+---
+
+summary: Fix the Settings footer update notice and buttons being cut off on mobile.
+category: fix
+dev: On mobile the update-check result renders in a new `.settings-modal-footer-update-row` above the nowrap `.modal-actions` rail; desktop/tablet keep it inline next to the version button.
diff --git a/packages/dashboard/app/components/SettingsModal.css b/packages/dashboard/app/components/SettingsModal.css
index b9f1885333..8b37617bca 100644
--- a/packages/dashboard/app/components/SettingsModal.css
+++ b/packages/dashboard/app/components/SettingsModal.css
@@ -402,6 +402,32 @@ Fix the invariant for BOTH presentations (standalone modal + embedded SettingsVi
white-space: normal;
}
+ /*
+ FNXC:SettingsUpdate 2026-07-25-19:40:
+ Mobile-only row that carries the update-check result above the footer rail (rendered by SettingsModal.tsx when
+ viewportMode === "mobile"). The rail below is a deliberate nowrap scroller; the banner is not, so it wraps and
+ centers here at full width and can no longer clip itself or shove Import/Export/Reset/Close out of view.
+ It owns the footer's top divider and the adjacent rail drops its own so the two rows read as one footer block.
+ */
+ .settings-modal .settings-modal-footer-update-row {
+ display: flex;
+ flex-shrink: 0;
+ justify-content: center;
+ text-align: center;
+ padding: var(--space-sm) var(--modal-padding) 0;
+ border-top: 1px solid var(--border);
+ background: color-mix(in srgb, var(--text) 5%, transparent);
+ }
+
+ .settings-modal .settings-modal-footer-update-row + .modal-actions {
+ border-top: none;
+ }
+
+ .settings-modal .settings-modal-footer-update-row .settings-update-result {
+ justify-content: center;
+ white-space: normal;
+ }
+
.settings-modal .settings-footer-help-btn {
align-items: center;
flex-shrink: 0;
diff --git a/packages/dashboard/app/components/SettingsModal.tsx b/packages/dashboard/app/components/SettingsModal.tsx
index ebae8be6f8..ca09101351 100644
--- a/packages/dashboard/app/components/SettingsModal.tsx
+++ b/packages/dashboard/app/components/SettingsModal.tsx
@@ -2160,6 +2160,30 @@ export function SettingsModal({
return t("settings.general.upToDate", "You're up to date โ");
}, [handleInstallUpdate, handleRestart, restartError, restartLoading, restartScheduled, restartSupported, t, updateCheckResult, updateInstallLoading, updateInstallResult]);
+ /*
+ FNXC:SettingsUpdate 2026-07-25-19:40:
+ The update-check result ("vX available ยท Learn more" plus the Update now / Restart controls) is one node rendered
+ in two places: inline next to the version button on desktop/tablet, and on its OWN full-width row above the footer
+ rail on mobile. The mobile footer is a single nowrap horizontally-scrolling rail (FN-7752); once an update banner
+ joined that rail its intrinsic width exceeded the viewport, so the banner itself was clipped mid-sentence and
+ Import/Export/Reset/Close were pushed off-screen behind a scroll affordance operators do not see. Giving the banner
+ its own row keeps the rail to the controls it was sized for, and the banner wraps normally instead of clipping.
+ */
+ const updateCheckResultNode = updateCheckResult ? (
+
+ {renderUpdateCheckResultContent()}
+
+ ) : null;
+
// Load auth status when the authentication section is active
const loadAuthStatus = useCallback(async () => {
try {
@@ -4916,6 +4940,9 @@ export function SettingsModal({
)}
+ {viewportMode === "mobile" && updateCheckResultNode && (
+
{updateCheckResultNode}
+ )}
)}
- {updateCheckResult && (
-
- {renderUpdateCheckResultContent()}
-
- )}
+ {viewportMode !== "mobile" && updateCheckResultNode}
diff --git a/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx b/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx
index de4b88544e..1ed7a9b85a 100644
--- a/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx
+++ b/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx
@@ -397,12 +397,40 @@ describe("SettingsModal mobile adaptations", () => {
await user.click(within(modalActions as HTMLElement).getByRole("button", { name: "Check for updates" }));
const updateNow = await findByRole("button", { name: "Update now" });
- expect((modalActions as HTMLElement).contains(updateNow)).toBe(true);
+
+ /*
+ The mobile footer rail is a single nowrap horizontal scroller, so the update banner must live in its own
+ full-width row directly above it โ inside the rail it clipped itself and pushed Import/Export/Reset/Close
+ off-screen. Assert both halves of that invariant: banner outside the rail, banner present in the footer row.
+ */
+ const updateRow = container.querySelector(".settings-modal-footer-update-row");
+ expect(updateRow).toBeTruthy();
+ expect((updateRow as HTMLElement).contains(updateNow)).toBe(true);
+ expect((modalActions as HTMLElement).contains(updateNow)).toBe(false);
+ expect(modalActions?.querySelector(".settings-update-result")).toBeNull();
+ expect(updateRow?.nextElementSibling).toBe(modalActions);
await user.click(updateNow);
expect(await findByText("Updated to v2.0.0 โ restart Fusion to apply")).toBeTruthy();
});
+ it("keeps the update result inline in the footer rail on desktop", async () => {
+ mockSettingsViewport(false);
+ const user = userEvent.setup();
+ const { container, findByRole } = render();
+ await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
+
+ const modalActions = container.querySelector(".modal-actions");
+ expect(modalActions).toBeTruthy();
+
+ await user.click(within(modalActions as HTMLElement).getByRole("button", { name: "Check for updates" }));
+ const updateNow = await findByRole("button", { name: "Update now" });
+
+ expect(container.querySelector(".settings-modal-footer-update-row")).toBeNull();
+ expect((modalActions as HTMLElement).contains(updateNow)).toBe(true);
+ expect(container.querySelector(".settings-update-check .settings-update-result")).toBeTruthy();
+ });
+
it("preserves the mobile section picker accessible name without rendering a visible label", async () => {
mockSettingsViewport(true);
const { container, getByLabelText, queryByText } = render();
@@ -811,6 +839,10 @@ describe("SettingsModal mobile adaptations", () => {
expectMobileRule(css, ".settings-modal .settings-modal-footer-version", "margin-right: 0;");
expectMobileRule(css, ".settings-modal .settings-update-check", "align-items: center;");
expectMobileRule(css, ".settings-modal .settings-update-check", "flex-wrap: wrap;");
+ // The update banner owns a full-width row above the nowrap footer rail, and the rail drops its duplicate divider.
+ expectMobileRule(css, ".settings-modal .settings-modal-footer-update-row", "border-top: 1px solid var(--border);");
+ expectMobileRule(css, ".settings-modal .settings-modal-footer-update-row", "justify-content: center;");
+ expectMobileRule(css, ".settings-modal .settings-modal-footer-update-row + .modal-actions", "border-top: none;");
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;");