FN-7752: tighten mobile Settings modal layout
Squashes FN-7752, improving the mobile Settings modal layout to reduce chrome and simplify section navigation on small screens. - Replace the mobile section-picker button row with a compact dropdown-only selector, keeping an accessible aria-label - Slim down the header and footer, collapsing footer actions to a single row on mobile - Tighten SettingsModal.css breakpoint rules (≤768px) for header/footer sizing and spacing - Update settings-mobile tests to cover the new dropdown-only picker and single-row footer behavior - Add changeset documenting the mobile layout fix Files changed: .changeset/fn-7752-mobile-settings-layout.md | 7 +++ .../dashboard/app/components/SettingsModal.css | 62 ++++++++++++++++------ .../dashboard/app/components/SettingsModal.tsx | 19 +++++-- .../components/__tests__/settings-mobile.test.tsx | 26 +++++++-- 4 files changed, 90 insertions(+), 24 deletions(-) Fusion-Task-Id: FN-7752 Fusion-Task-Lineage: 5dea597c-19f7-49c4-979a-7528d84fa6c5 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7752-mobile-settings-layout.md
Normal file
7
.changeset/fn-7752-mobile-settings-layout.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Tighten the mobile Settings layout — dropdown-only section picker, single-row footer, and slimmer header/footer.
|
||||
category: fix
|
||||
dev: SettingsModal mobile (≤768px) CSS/JSX only; section-picker label removed, aria-label preserves accessible name.
|
||||
@@ -243,6 +243,20 @@ The embedded title reads like other embedded-view titles (Planning modal-header-
|
||||
edge, after the Star/Discord actions, without displacing them. Desktop/tablet embedded and the standalone modal
|
||||
presentation are untouched by this rule (scoped to .settings-modal--embedded .modal-header--embedded).
|
||||
*/
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
.settings-modal .modal-header,
|
||||
.settings-modal .modal-actions {
|
||||
padding-block: var(--space-sm);
|
||||
padding-inline: var(--space-md);
|
||||
}
|
||||
|
||||
.settings-modal--embedded .modal-header--embedded {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.settings-modal--embedded .modal-header--embedded .settings-embedded-mobile-close {
|
||||
--settings-embedded-mobile-close-touch-target: calc(var(--space-lg) + var(--space-lg) + var(--space-xs));
|
||||
|
||||
@@ -287,21 +301,41 @@ The embedded title reads like other embedded-view titles (Planning modal-header-
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
.settings-modal .modal-actions {
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.settings-modal .modal-actions-left,
|
||||
.settings-modal .modal-actions-right {
|
||||
flex-shrink: 0;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.settings-modal-footer-version {
|
||||
flex: 1 1 100%;
|
||||
flex: 0 1 auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.settings-update-check {
|
||||
flex-wrap: wrap;
|
||||
row-gap: var(--space-xs);
|
||||
flex-wrap: nowrap;
|
||||
row-gap: 0;
|
||||
}
|
||||
|
||||
.settings-update-result {
|
||||
flex: 1 1 100%;
|
||||
flex: 0 1 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.settings-footer-help-btn {
|
||||
.settings-footer-help-btn,
|
||||
.settings-version-check-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
@@ -2214,21 +2248,19 @@ The header row wraps so the badge drops below the heading on narrow widths inste
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:Settings 2026-07-09-00:00:
|
||||
FN-7752 trims mobile-only Settings section chrome: after removing the visible picker label, use smaller horizontal padding for the picker and internal section text so narrow viewports devote more width to controls without changing desktop/tablet rules.
|
||||
*/
|
||||
.settings-mobile-section-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-sm) var(--space-lg) var(--space-md);
|
||||
padding: var(--space-sm) var(--space-md) var(--space-md);
|
||||
border-bottom: none;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.settings-mobile-section-picker label {
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.settings-mobile-section-picker-control-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2320,7 +2352,7 @@ The header row wraps so the badge drops below the heading on narrow widths inste
|
||||
}
|
||||
|
||||
.settings-section-heading {
|
||||
padding: var(--space-lg) var(--space-lg) var(--space-md);
|
||||
padding: var(--space-lg) var(--space-md) var(--space-md);
|
||||
margin: 0 0 var(--space-md);
|
||||
}
|
||||
|
||||
@@ -2345,7 +2377,7 @@ The header row wraps so the badge drops below the heading on narrow widths inste
|
||||
}
|
||||
|
||||
.settings-scope-banner {
|
||||
padding: var(--space-sm) var(--space-lg);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
}
|
||||
|
||||
.memory-editor-frame {
|
||||
@@ -2371,7 +2403,7 @@ The header row wraps so the badge drops below the heading on narrow widths inste
|
||||
}
|
||||
|
||||
.settings-description {
|
||||
padding: 0 var(--space-lg);
|
||||
padding: 0 var(--space-md);
|
||||
}
|
||||
|
||||
.settings-model-lane-actions {
|
||||
|
||||
@@ -967,10 +967,15 @@ export function SettingsModal({
|
||||
const [activePluginsSubsection, setActivePluginsSubsection] = useState<PluginsSubsectionId>(() =>
|
||||
initialSection === "pi-extensions" ? "pi-extensions" : "fusion-plugins",
|
||||
);
|
||||
/*
|
||||
FNXC:Settings 2026-07-09-00:00:
|
||||
Mobile Settings navigation is controlled by both the viewport hook and the CSS media query because tests and embedded shells can mock one surface independently. Treat either mobile signal as sufficient so the compact picker/search-toggle path stays available whenever Settings is in mobile mode.
|
||||
*/
|
||||
const [showMobileSectionPicker, setShowMobileSectionPicker] = useState(() =>
|
||||
typeof window !== "undefined" && typeof window.matchMedia === "function"
|
||||
viewportMode === "mobile" ||
|
||||
(typeof window !== "undefined" && typeof window.matchMedia === "function"
|
||||
? window.matchMedia(MOBILE_SETTINGS_MEDIA_QUERY)?.matches === true
|
||||
: false,
|
||||
: false),
|
||||
);
|
||||
const [settingsSearchQuery, setSettingsSearchQuery] = useState("");
|
||||
/*
|
||||
@@ -1210,13 +1215,13 @@ export function SettingsModal({
|
||||
return;
|
||||
}
|
||||
const updateMobilePicker = (event?: MediaQueryListEvent) => {
|
||||
setShowMobileSectionPicker(event ? event.matches : mediaQuery.matches);
|
||||
setShowMobileSectionPicker(viewportMode === "mobile" || (event ? event.matches : mediaQuery.matches));
|
||||
};
|
||||
|
||||
updateMobilePicker();
|
||||
mediaQuery.addEventListener("change", updateMobilePicker);
|
||||
return () => mediaQuery.removeEventListener("change", updateMobilePicker);
|
||||
}, []);
|
||||
}, [viewportMode]);
|
||||
|
||||
/*
|
||||
FNXC:SettingsReset 2026-07-04-00:15:
|
||||
@@ -3653,11 +3658,15 @@ export function SettingsModal({
|
||||
<aside className="settings-navigation" aria-label={t("settings.search.navigationLabel", "Settings navigation")}>
|
||||
{showMobileSectionPicker && (
|
||||
<div className="settings-mobile-section-picker">
|
||||
<label htmlFor="settings-mobile-section">{t("settings.mobileNav.label", "Settings Section")}</label>
|
||||
{/**
|
||||
* FNXC:Settings 2026-07-09-00:00:
|
||||
* FN-7752 removes the visible mobile section-picker label to reclaim vertical space on narrow Settings screens. The select keeps "Settings Section" as its aria-label so screen readers and getByLabelText tests keep the same accessible name without an empty label shell.
|
||||
*/}
|
||||
<div className="settings-mobile-section-picker-control-row">
|
||||
{hasSettingsSearchResults ? (
|
||||
<select
|
||||
id="settings-mobile-section"
|
||||
aria-label={t("settings.mobileNav.label", "Settings Section")}
|
||||
className="select touch-target"
|
||||
value={activeSection}
|
||||
onChange={(event) => setActiveSection(event.target.value as SectionId)}
|
||||
|
||||
@@ -272,6 +272,18 @@ describe("SettingsModal mobile adaptations", () => {
|
||||
expect(await findByText("Updated to v2.0.0 — restart Fusion to apply")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("preserves the mobile section picker accessible name without rendering a visible label", async () => {
|
||||
mockSettingsViewport(true);
|
||||
const { container, getByLabelText, queryByText } = render(<SettingsModal onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
const picker = getByLabelText("Settings Section") as HTMLSelectElement;
|
||||
expect(picker.id).toBe("settings-mobile-section");
|
||||
expect(picker.getAttribute("aria-label")).toBe("Settings Section");
|
||||
expect(container.querySelector('label[for="settings-mobile-section"]')).toBeNull();
|
||||
expect(queryByText("Settings Section", { selector: "label" })).toBeNull();
|
||||
});
|
||||
|
||||
it("excludes research sections from mobile picker when researchView is disabled", async () => {
|
||||
mockSettingsViewport(true);
|
||||
const user = userEvent.setup();
|
||||
@@ -505,6 +517,7 @@ describe("SettingsModal mobile adaptations", () => {
|
||||
|
||||
expectMobileRule(css, ".settings-layout", "flex-direction: column;");
|
||||
expectMobileRule(css, ".settings-mobile-section-picker", "display: flex;");
|
||||
expectMobileRule(css, ".settings-mobile-section-picker", "padding: var(--space-sm) var(--space-md) var(--space-md);");
|
||||
expectMobileRule(css, ".settings-mobile-section-picker-control-row", "display: flex;");
|
||||
expectMobileRule(css, ".settings-mobile-section-picker-control-row", "align-items: center;");
|
||||
expectMobileRule(css, ".settings-mobile-section-picker select", "flex: 1 1 auto;");
|
||||
@@ -517,16 +530,21 @@ describe("SettingsModal mobile adaptations", () => {
|
||||
expectMobileRule(css, ".settings-nav-item", "justify-content: center;");
|
||||
expectMobileRule(css, ".settings-nav-item", "gap: 4px;");
|
||||
expectMobileRule(css, ".settings-content textarea", "font-size: 16px;");
|
||||
expectMobileRule(css, ".settings-section-heading", "padding: var(--space-lg) 0 var(--space-md);");
|
||||
expectMobileRule(css, ".settings-section-heading", "margin: 0;");
|
||||
expectMobileRule(css, ".settings-section-heading", "padding: var(--space-lg) var(--space-md) var(--space-md);");
|
||||
expectMobileRule(css, ".settings-section-heading", "margin: 0 0 var(--space-md);");
|
||||
expectMobileRule(css, ".settings-scope-icon", "margin-right: 0;");
|
||||
expectMobileRule(css, ".settings-scope-banner", "padding: var(--space-sm) var(--space-lg);");
|
||||
expectMobileRule(css, ".settings-scope-banner", "padding: var(--space-sm) var(--space-md);");
|
||||
expectMobileRule(css, ".settings-empty-state", "padding: 12px 14px;");
|
||||
expectMobileRule(css, ".settings-description", "padding: 0 var(--space-lg);");
|
||||
expectMobileRule(css, ".settings-description", "padding: 0 var(--space-md);");
|
||||
expectMobileRule(css, ".theme-selector", "padding: 0 14px 14px;");
|
||||
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", "flex-wrap: nowrap;");
|
||||
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, ".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