From 1ba588d7075296ee2be3c10c2390693b0b118089 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 10 Jul 2026 07:30:08 -0700 Subject: [PATCH] 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) --- .changeset/fn-7773-mobile-settings-footer.md | 7 ++ .../app/components/SettingsModal.css | 59 +++++++++++++---- .../__tests__/settings-mobile.test.tsx | 64 +++++++++++++++---- 3 files changed, 106 insertions(+), 24 deletions(-) create mode 100644 .changeset/fn-7773-mobile-settings-footer.md 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;");