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:
7
.changeset/fn-7773-mobile-settings-footer.md
Normal file
7
.changeset/fn-7773-mobile-settings-footer.md
Normal 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.
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;");
|
||||
|
||||
Reference in New Issue
Block a user