FN-7773: fix mobile Settings footer spacing to prevent version overlap

Tightens the mobile Settings modal footer rail so Version/Help, Import/Export/Reset, and Save no longer overlap or crowd on small viewports.

- Split modal-header and modal-actions padding rules so the mobile footer uses tighter --space-xs block padding independently of the header
- Center-align modal-actions-left/right, footer version, update-check, and version-check-button on the mobile footer rail
- Make the version text and version-check button non-wrapping inline elements with normalized line-height so they stop overlapping neighboring controls
- Add a settings-modal-version rule as a non-wrapping inline-flex box
- Extend settings-mobile.test.tsx with regression coverage for the tightened mobile footer layout
- Add a patch changeset documenting the fix

Files changed:
 .changeset/fn-7773-mobile-settings-footer.md       |  7 +++
 packages/dashboard/app/components/SettingsModal.css | 55 +++++++++++++++----
 packages/dashboard/app/components/__tests__/settings-mobile.test.tsx | 62 +++++++++++++++++-----
 3 files changed, 103 insertions(+), 21 deletions(-)

Fusion-Task-Id: FN-7773

Fusion-Task-Lineage: ea45e7bb-2560-4d17-89dd-9dbc7a307351

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-10 07:30:08 -07:00
parent df8ad460af
commit 1ba588d707
3 changed files with 106 additions and 24 deletions

View File

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

View File

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

View File

@@ -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(<SettingsModal onClose={vi.fn()} addToast={vi.fn()} />);
const standalone = render(<SettingsModal onClose={vi.fn()} addToast={vi.fn()} />);
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(<SettingsView onClose={vi.fn()} addToast={vi.fn()} />);
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(<SettingsModal onClose={vi.fn()} addToast={vi.fn()} />);
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;");