FN-7751: move mobile settings search toggle inline with section picker
Relocates the mobile-only settings search icon toggle so it sits inline next to the section dropdown instead of its own row above it, tightening the mobile Settings header. - Move the settings-search-toggle button out of the standalone .settings-search row into a new .settings-mobile-section-picker-control-row alongside the mobile section <select>. - Preserve FN-7713 expand/hide toggle behavior and active-query preservation; desktop/tablet layout unaffected. - Add CSS for .settings-mobile-section-picker-control-row to lay out the select and toggle button inline. - Update settings-mobile tests to assert the toggle renders next to the section picker. - Add changeset (patch) documenting the fix. Files changed: .../fn-7751-mobile-settings-search-inline.md | 7 ++ docs/dashboard-guide.md | 4 +- .../dashboard/app/components/SettingsModal.css | 23 ++++-- .../dashboard/app/components/SettingsModal.tsx | 89 +++++++++++----------- .../components/__tests__/settings-mobile.test.tsx | 38 ++++++++- 5 files changed, 105 insertions(+), 56 deletions(-) Fusion-Task-Id: FN-7751 Fusion-Task-Lineage: 8bb9a32b-80f4-4b73-a366-d23e97106b73 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7751-mobile-settings-search-inline.md
Normal file
7
.changeset/fn-7751-mobile-settings-search-inline.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Mobile Settings search icon now sits inline next to the section dropdown.
|
||||
category: fix
|
||||
dev: SettingsModal moves .settings-search-toggle into the .settings-mobile-section-picker row; desktop unchanged.
|
||||
@@ -13,8 +13,8 @@ When Fusion detects a newer `@runfusion/fusion` release, the Settings modal foot
|
||||
<!-- FNXC:SettingsSearchDocs 2026-07-04-00:00: Settings search is section-discovery, not a global command palette. Document that it filters visible Settings sections by section names and setting keywords while preserving feature-gated hidden sections. -->
|
||||
Use **Search settings** at the top of Settings to find the section that contains a setting by name or keyword. The same search works in the Settings modal and embedded Settings page, filters both the desktop section list and mobile section picker, and only searches sections currently visible for enabled feature flags.
|
||||
|
||||
<!-- FNXC:Settings 2026-07-09-00:00: FN-7713 collapses the mobile search row by default behind a toggle icon to reclaim vertical space above the section picker; desktop/tablet keep the row always visible with no toggle. -->
|
||||
On mobile, the search row starts collapsed behind a compact toggle icon (top of the Settings navigation) to save vertical space; tap it to reveal the search input and tap again to hide it. An in-progress search query is preserved across collapse/expand. Desktop and tablet always show the search row with no toggle.
|
||||
<!-- FNXC:Settings 2026-07-09-12:00: FN-7751 keeps FN-7713's collapsed mobile search row but moves the toggle inline beside the section dropdown so mobile Settings exposes one compact section/search control row; desktop/tablet keep the row always visible with no toggle. -->
|
||||
On mobile, the search row starts collapsed behind a compact toggle icon beside the **Settings Section** dropdown to save vertical space; tap it to reveal the search input and tap again to hide it. An in-progress search query is preserved across collapse/expand. Desktop and tablet always show the search row with no toggle.
|
||||
|
||||
<!-- FNXC:SettingsDefaults 2026-07-04-00:00: FN-7505 requires every user-editable setting's help text to state its own default value, so operators reading a field's description know what it defaults to without checking the reference doc. -->
|
||||
Every user-editable setting's help text (the `.settings-description`/`<small>` hint under a field) states its own default value — for example “Default: 3.”, “Default: enabled.”, or “No default — unset (inherits the global setting).” for values that fall back to another scope. Canonical default values come from `DEFAULT_GLOBAL_SETTINGS` / `DEFAULT_PROJECT_SETTINGS` in `packages/core/src/settings-schema.ts`; the dashboard copy never invents a number. A guard test (`settings-default-descriptions.test.tsx`) enforces that every surfaced setting states its default and that every `DEFAULT_SETTINGS` key is either documented or explicitly allowlisted as not surfaced in the Settings UI.
|
||||
|
||||
@@ -558,9 +558,7 @@ The embedded title reads like other embedded-view titles (Planning modal-header-
|
||||
border-bottom: var(--btn-border-width) solid var(--border);
|
||||
}
|
||||
|
||||
/* FNXC:Settings 2026-07-09-00:00: the row wraps label + input + results; on desktop it always
|
||||
renders (see .settings-search-toggle below, hidden here). On mobile the toggle in SettingsModal.tsx
|
||||
controls whether this wrapper mounts at all. */
|
||||
/* FNXC:Settings 2026-07-09-12:00: the row wraps label + input + results; on desktop it always renders (see .settings-search-toggle below, hidden here). On mobile the inline section-picker toggle controls whether this wrapper mounts below the picker, avoiding an empty padded shell above the picker while collapsed. */
|
||||
.settings-search-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -2193,14 +2191,12 @@ The header row wraps so the badge drops below the heading on narrow widths inste
|
||||
padding: var(--space-md) var(--space-lg) var(--space-sm);
|
||||
}
|
||||
|
||||
/* FNXC:Settings 2026-07-09-00:00: mobile-only collapse-by-default toggle. The row starts
|
||||
collapsed (only the icon button renders) and expands to the full search row on tap. */
|
||||
/* FNXC:Settings 2026-07-09-12:00: mobile-only collapse-by-default toggle sits in the section picker control row so search and section navigation share one horizontal affordance row. The row starts collapsed and expands to the full search row below the picker on tap. */
|
||||
.settings-search-toggle {
|
||||
--settings-search-toggle-touch-target: calc(var(--space-lg) + var(--space-lg) + var(--space-xs));
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-self: flex-start;
|
||||
width: var(--settings-search-toggle-touch-target);
|
||||
height: var(--settings-search-toggle-touch-target);
|
||||
flex-shrink: 0;
|
||||
@@ -2233,7 +2229,22 @@ The header row wraps so the badge drops below the heading on narrow widths inste
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.settings-mobile-section-picker-control-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.settings-mobile-section-picker select {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-mobile-section-picker .settings-search-empty-hint {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -3651,26 +3651,51 @@ export function SettingsModal({
|
||||
) : (
|
||||
<div className="settings-layout">
|
||||
<aside className="settings-navigation" aria-label={t("settings.search.navigationLabel", "Settings navigation")}>
|
||||
<div className="settings-search" data-testid="settings-search">
|
||||
{isMobileSettingsSearch && (
|
||||
// FNXC:Settings 2026-07-09-00:00: mobile-only compact icon toggle; hidden entirely on
|
||||
// desktop/tablet via `isMobileSettingsSearch`, which always shows the row instead.
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-icon settings-search-toggle"
|
||||
onClick={() => setMobileSearchRowExpanded((expanded) => !expanded)}
|
||||
aria-expanded={settingsSearchRowVisible}
|
||||
aria-controls="settings-search-row-region"
|
||||
aria-label={
|
||||
settingsSearchRowVisible
|
||||
? t("settings.search.toggleHide", "Hide search")
|
||||
: t("settings.search.toggleShow", "Show search")
|
||||
}
|
||||
>
|
||||
{settingsSearchRowVisible ? <SearchToggleCloseIcon size={16} /> : <Search size={16} />}
|
||||
</button>
|
||||
)}
|
||||
{settingsSearchRowVisible && (
|
||||
{showMobileSectionPicker && (
|
||||
<div className="settings-mobile-section-picker">
|
||||
<label htmlFor="settings-mobile-section">{t("settings.mobileNav.label", "Settings Section")}</label>
|
||||
<div className="settings-mobile-section-picker-control-row">
|
||||
{hasSettingsSearchResults ? (
|
||||
<select
|
||||
id="settings-mobile-section"
|
||||
className="select touch-target"
|
||||
value={activeSection}
|
||||
onChange={(event) => setActiveSection(event.target.value as SectionId)}
|
||||
>
|
||||
{searchableSectionOptions.map((section) => {
|
||||
const label = t(section.labelKey, section.label);
|
||||
return (
|
||||
<option key={section.id} value={section.id}>
|
||||
{resolveSettingsSectionOptionLabel(section, label)}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
) : (
|
||||
<p className="settings-search-empty-hint">{t("settings.search.noMobileOptions", "No sections match this search.")}</p>
|
||||
)}
|
||||
{isMobileSettingsSearch && (
|
||||
// FNXC:Settings 2026-07-09-12:00: mobile-only search toggle now lives inline beside the section picker control so section navigation and search read as one compact row; FN-7713 expand/hide behavior and active-query preservation remain unchanged.
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-icon settings-search-toggle"
|
||||
onClick={() => setMobileSearchRowExpanded((expanded) => !expanded)}
|
||||
aria-expanded={settingsSearchRowVisible}
|
||||
aria-controls="settings-search-row-region"
|
||||
aria-label={
|
||||
settingsSearchRowVisible
|
||||
? t("settings.search.toggleHide", "Hide search")
|
||||
: t("settings.search.toggleShow", "Show search")
|
||||
}
|
||||
>
|
||||
{settingsSearchRowVisible ? <SearchToggleCloseIcon size={16} /> : <Search size={16} />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{settingsSearchRowVisible && (
|
||||
<div className="settings-search" data-testid="settings-search">
|
||||
<div id="settings-search-row-region" className="settings-search-row">
|
||||
<label className="settings-search-label" htmlFor="settings-search-input">
|
||||
{t("settings.search.label", "Search settings")}
|
||||
@@ -3709,30 +3734,6 @@ export function SettingsModal({
|
||||
: t("settings.search.allSections", "Showing all settings sections")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showMobileSectionPicker && (
|
||||
<div className="settings-mobile-section-picker">
|
||||
<label htmlFor="settings-mobile-section">{t("settings.mobileNav.label", "Settings Section")}</label>
|
||||
{hasSettingsSearchResults ? (
|
||||
<select
|
||||
id="settings-mobile-section"
|
||||
className="select touch-target"
|
||||
value={activeSection}
|
||||
onChange={(event) => setActiveSection(event.target.value as SectionId)}
|
||||
>
|
||||
{searchableSectionOptions.map((section) => {
|
||||
const label = t(section.labelKey, section.label);
|
||||
return (
|
||||
<option key={section.id} value={section.id}>
|
||||
{resolveSettingsSectionOptionLabel(section, label)}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
) : (
|
||||
<p className="settings-search-empty-hint">{t("settings.search.noMobileOptions", "No sections match this search.")}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<nav className="settings-sidebar">
|
||||
|
||||
@@ -505,6 +505,10 @@ 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-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;");
|
||||
expectMobileRule(css, ".settings-mobile-section-picker .settings-search-empty-hint", "flex: 1 1 auto;");
|
||||
expectMobileRule(css, ".settings-navigation", "width: 100%;");
|
||||
expectMobileRule(css, ".settings-search", "padding: var(--space-md) var(--space-lg) var(--space-sm);");
|
||||
expectMobileRule(css, ".settings-sidebar", "display: none;");
|
||||
@@ -605,16 +609,24 @@ describe("SettingsModal mobile adaptations", () => {
|
||||
const toggle = getByLabelText("Show search");
|
||||
expect(toggle.getAttribute("aria-expanded")).toBe("false");
|
||||
expect(toggle.getAttribute("aria-controls")).toBe("settings-search-row-region");
|
||||
const picker = toggle.closest(".settings-mobile-section-picker") as HTMLElement;
|
||||
expect(picker).toBeTruthy();
|
||||
expect(within(picker).getByLabelText("Settings Section")).toBeTruthy();
|
||||
expect(queryByTestId("settings-search")).toBeNull();
|
||||
|
||||
await user.click(toggle);
|
||||
expect(await findByLabelText("Hide search")).toBeTruthy();
|
||||
expect(getByLabelText("Hide search").getAttribute("aria-expanded")).toBe("true");
|
||||
const hideToggle = await findByLabelText("Hide search");
|
||||
expect(hideToggle).toBeTruthy();
|
||||
expect(hideToggle.getAttribute("aria-expanded")).toBe("true");
|
||||
expect(hideToggle.closest(".settings-mobile-section-picker")).toBe(picker);
|
||||
expect(document.getElementById("settings-search-row-region")).toBeTruthy();
|
||||
expect(picker.nextElementSibling?.classList.contains("settings-search")).toBe(true);
|
||||
|
||||
await user.click(getByLabelText("Hide search"));
|
||||
await waitFor(() => expect(queryByTestId("settings-search-input")).toBeNull());
|
||||
expect(getByLabelText("Show search").getAttribute("aria-expanded")).toBe("false");
|
||||
// No leftover shell for the results region while collapsed.
|
||||
// No leftover shell for the search row or results region while collapsed.
|
||||
expect(queryByTestId("settings-search")).toBeNull();
|
||||
expect(document.getElementById("settings-search-row-region")).toBeNull();
|
||||
expect(document.getElementById("settings-search-results")).toBeNull();
|
||||
});
|
||||
@@ -642,18 +654,36 @@ describe("SettingsModal mobile adaptations", () => {
|
||||
|
||||
it("keeps the desktop search row always visible with no toggle rendered", async () => {
|
||||
mockSettingsViewport(false);
|
||||
const { queryByLabelText, getByTestId } = render(<SettingsModal onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
const { container, queryByLabelText, getByTestId } = render(<SettingsModal onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
expect(getByTestId("settings-search-input")).toBeTruthy();
|
||||
expect(queryByLabelText("Show search")).toBeNull();
|
||||
expect(queryByLabelText("Hide search")).toBeNull();
|
||||
expect(container.querySelector(".settings-mobile-section-picker .settings-search-toggle")).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps the inline toggle reachable when mobile search has no section results", async () => {
|
||||
mockSettingsViewport(true);
|
||||
const user = userEvent.setup();
|
||||
const { getByLabelText, getByTestId, findByText } = render(<SettingsModal onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
await user.click(getByLabelText("Show search"));
|
||||
await user.type(getByTestId("settings-search-input"), "zzzzzz-no-match");
|
||||
|
||||
const emptyHint = await findByText("No sections match this search.");
|
||||
const picker = emptyHint.closest(".settings-mobile-section-picker") as HTMLElement;
|
||||
expect(picker).toBeTruthy();
|
||||
expect(within(picker).getByLabelText("Hide search")).toBeTruthy();
|
||||
expect(picker.querySelector("#settings-mobile-section")).toBeNull();
|
||||
});
|
||||
|
||||
it("contains the mobile-only toggle CSS override and hides it on desktop", () => {
|
||||
const css = loadAllAppCss();
|
||||
|
||||
expectMobileRule(css, ".settings-search-toggle", "display: inline-flex;");
|
||||
expectMobileRule(css, ".settings-search-toggle", "flex-shrink: 0;");
|
||||
expectBaseRule(css, ".settings-search-toggle", "display: none;");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user