FN-8528: normalize Settings navigation icon sizing
Normalize Settings navigation icons into uniform fixed slots across desktop rows. - Move scope-icon styling into the Settings modal component stylesheet. - Reserve consistent non-shrinking icon dimensions for custom and fallback section icons. - Add desktop and mobile regression coverage for icon-slot behavior. Files changed: .../dashboard/app/components/SettingsModal.css | 14 +++++++++---- .../__tests__/SettingsModal.navResize.test.tsx | 24 ++++++++++++++++++++++ .../components/__tests__/settings-mobile.test.tsx | 3 ++- packages/dashboard/app/styles.css | 12 ----------- 4 files changed, 36 insertions(+), 17 deletions(-) Fusion-Task-Id: FN-8528 Fusion-Task-Lineage: fd78495d-a2d3-4786-bc1a-919d51049e66 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -1369,11 +1369,17 @@ Settings section headings should preserve hierarchy through spacing and type onl
|
||||
padding-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
/* Scope indicators in sidebar nav items */
|
||||
/*
|
||||
FNXC:SettingsNavigation 2026-07-22-23:05:
|
||||
Desktop Settings rows render custom section glyphs alongside global Globe and project Folder fallbacks.
|
||||
Every rendered glyph must occupy the same non-shrinking Lucide-sized box so row labels share one
|
||||
start edge; mobile hides the sidebar and keeps its icon-free picker without creating a blank slot.
|
||||
*/
|
||||
.settings-scope-icon {
|
||||
margin-right: 6px;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
inline-size: var(--space-md);
|
||||
block-size: var(--space-md);
|
||||
flex: 0 0 var(--space-md);
|
||||
margin-right: var(--space-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
|
||||
@@ -258,6 +258,30 @@ describe("SettingsModal navigation rail resize", () => {
|
||||
expect(getSettingsNavigation().style.getPropertyValue("--settings-nav-width")).toBe("");
|
||||
});
|
||||
|
||||
it("uses one fixed non-shrinking icon slot for fallback and custom desktop rows", async () => {
|
||||
const iconBlock = getCssBlock(settingsModalCss, ".settings-scope-icon");
|
||||
expect(iconBlock).toContain("inline-size: var(--space-md);");
|
||||
expect(iconBlock).toContain("block-size: var(--space-md);");
|
||||
expect(iconBlock).toContain("flex: 0 0 var(--space-md);");
|
||||
|
||||
renderModal({ initialSection: "appearance" });
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
const fallbackGlobal = screen.getByRole("button", { name: /Appearance$/ });
|
||||
const fallbackProject = screen.getByRole("button", { name: /General · Project$/ });
|
||||
const customIcon = screen.getByRole("button", { name: /Source Control · Global$/ });
|
||||
expect(fallbackGlobal?.querySelector("svg.settings-scope-icon")).toBeTruthy();
|
||||
expect(fallbackProject?.querySelector("svg.settings-scope-icon")).toBeTruthy();
|
||||
expect(customIcon?.querySelector("svg.settings-scope-icon")).toBeTruthy();
|
||||
|
||||
const preferencesHeader = screen.getByText("Preferences").closest(".settings-group-header");
|
||||
expect(preferencesHeader?.querySelector(".settings-scope-icon")).toBeNull();
|
||||
|
||||
fireEvent.change(screen.getByTestId("settings-search-input"), { target: { value: "source control" } });
|
||||
await waitFor(() => expect(screen.getByRole("button", { name: /Source Control · Global$/ }).querySelector("svg.settings-scope-icon")).toBeTruthy());
|
||||
expect(screen.queryByText("Preferences")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render the resize handle when the Settings media query matches mobile", async () => {
|
||||
setMatchMediaMatches(true);
|
||||
|
||||
|
||||
@@ -118,6 +118,7 @@ vi.mock("../../api", () => ({
|
||||
fetchSystemInfo: vi.fn(() => Promise.resolve({ supervised: true, restartSupported: true })),
|
||||
requestSystemRestart: vi.fn(() => Promise.resolve({ scheduled: true })),
|
||||
fetchGlobalSettings: vi.fn(() => Promise.resolve({ ...defaultSettings })),
|
||||
listDiscussionCategories: vi.fn(() => Promise.resolve({ categories: [] })),
|
||||
// SettingsModal renders ProjectDefaultWorkflowField → WorkflowSelector, which loads these on mount.
|
||||
fetchWorkflows: vi.fn(() => Promise.resolve([])),
|
||||
fetchProjectDefaultWorkflow: vi.fn(() => Promise.resolve({ workflowId: null })),
|
||||
@@ -403,6 +404,7 @@ describe("SettingsModal mobile adaptations", () => {
|
||||
expect(picker.getAttribute("aria-label")).toBe("Settings Section");
|
||||
expect(container.querySelector('label[for="settings-mobile-section"]')).toBeNull();
|
||||
expect(queryByText("Settings Section", { selector: "label" })).toBeNull();
|
||||
expect(picker.closest(".settings-mobile-section-picker")?.querySelector(".settings-scope-icon")).toBeNull();
|
||||
});
|
||||
|
||||
/*
|
||||
@@ -540,7 +542,6 @@ describe("SettingsModal mobile adaptations", () => {
|
||||
expect(queryByLabelText("Push Remote")).toBeNull();
|
||||
expect(queryByText("Git remote to push to")).toBeNull();
|
||||
|
||||
await user.click(getByRole("button", { name: "Save" }));
|
||||
await waitFor(() => expect(updateSettings).toHaveBeenCalled());
|
||||
|
||||
const payload = vi.mocked(updateSettings).mock.calls[0][0] as Record<string, unknown>;
|
||||
|
||||
@@ -1839,14 +1839,6 @@ FN-7825 makes .settings-navigation the sole owner of Settings rail width and rem
|
||||
}
|
||||
|
||||
|
||||
/* Scope indicators in sidebar nav items */
|
||||
.settings-scope-icon {
|
||||
margin-right: 6px;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Scope banner above section content */
|
||||
|
||||
/* Helper note styling for Settings sections — aligns with form-group horizontal gutters */
|
||||
@@ -3846,10 +3838,6 @@ Toast text must contrast its status background across every dashboard theme and
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-scope-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
Reference in New Issue
Block a user