diff --git a/packages/dashboard/app/components/SettingsModal.css b/packages/dashboard/app/components/SettingsModal.css index 66adb15de3..b9f1885333 100644 --- a/packages/dashboard/app/components/SettingsModal.css +++ b/packages/dashboard/app/components/SettingsModal.css @@ -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); } diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.navResize.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.navResize.test.tsx index 685e507ba0..e5567298e8 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.navResize.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.navResize.test.tsx @@ -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); diff --git a/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx b/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx index 3e0d46680e..b7687621bf 100644 --- a/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx +++ b/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx @@ -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; diff --git a/packages/dashboard/app/styles.css b/packages/dashboard/app/styles.css index fa63466adf..062c5848c8 100644 --- a/packages/dashboard/app/styles.css +++ b/packages/dashboard/app/styles.css @@ -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;