feat(KB-165): add responsive mobile layout for settings modal

- Add mobile CSS in @media query to stack settings sidebar horizontally above content
- Style nav items as horizontal scrollable tabs with bottom-border active indicator
- Adjust settings content, form groups, and auth provider rows for narrow viewports
- Add SettingsModal tests verifying .settings-layout, .settings-sidebar, and .settings-content structure
- Add test for active nav item class toggling and auth provider row DOM structure
This commit is contained in:
Dustin Byrne
2026-03-28 09:32:50 -04:00
parent cf7b385549
commit 85ea8fb545
2 changed files with 133 additions and 0 deletions

View File

@@ -563,4 +563,80 @@ describe("SettingsModal", () => {
expect(screen.getByText("Anthropic")).toBeTruthy();
expect(screen.getByText("GitHub")).toBeTruthy();
});
// --- Mobile structure tests (DOM classes that responsive CSS targets) ---
it("has .settings-layout wrapping sidebar and content", async () => {
const { container } = render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
const layout = container.querySelector(".settings-layout");
expect(layout).toBeTruthy();
expect(layout!.querySelector(".settings-sidebar")).toBeTruthy();
expect(layout!.querySelector(".settings-content")).toBeTruthy();
});
it("has .settings-sidebar with 7 .settings-nav-item buttons for all sections", async () => {
const { container } = render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
const sidebar = container.querySelector(".settings-sidebar");
expect(sidebar).toBeTruthy();
const navItems = sidebar!.querySelectorAll(".settings-nav-item");
expect(navItems.length).toBe(7);
const labels = Array.from(navItems).map((el) => el.textContent);
expect(labels).toEqual(["General", "Model", "Scheduling", "Worktrees", "Commands", "Merge", "Authentication"]);
});
it("has .settings-content as sibling of .settings-sidebar", async () => {
const { container } = render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
const layout = container.querySelector(".settings-layout");
const children = Array.from(layout!.children);
const sidebar = children.find((el) => el.classList.contains("settings-sidebar"));
const content = children.find((el) => el.classList.contains("settings-content"));
expect(sidebar).toBeTruthy();
expect(content).toBeTruthy();
});
it("marks the active nav item with .active class", async () => {
const { container } = render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
// Default active section is General
const activeItems = container.querySelectorAll(".settings-nav-item.active");
expect(activeItems.length).toBe(1);
expect(activeItems[0].textContent).toBe("General");
// Switch to Scheduling
fireEvent.click(screen.getByText("Scheduling"));
const newActive = container.querySelectorAll(".settings-nav-item.active");
expect(newActive.length).toBe(1);
expect(newActive[0].textContent).toBe("Scheduling");
});
it("auth provider rows contain .auth-provider-info and action button", async () => {
(fetchAuthStatus as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: true },
{ id: "github", name: "GitHub", authenticated: false },
],
});
const { container } = render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Authentication"));
await waitFor(() => expect(fetchAuthStatus).toHaveBeenCalled());
const rows = container.querySelectorAll(".auth-provider-row");
expect(rows.length).toBe(2);
for (const row of rows) {
expect(row.querySelector(".auth-provider-info")).toBeTruthy();
expect(row.querySelector("button")).toBeTruthy();
}
});
});

View File

@@ -1343,4 +1343,61 @@ html, body {
flex: 1;
text-align: center;
}
/* Settings modal: stack sidebar above content for mobile */
.settings-layout {
flex-direction: column;
min-height: 0;
}
.settings-sidebar {
width: auto;
min-width: 0;
flex-direction: row;
border-right: none;
border-bottom: 1px solid var(--border);
overflow-x: auto;
-webkit-overflow-scrolling: touch;
padding: 6px 8px;
gap: 2px;
}
.settings-nav-item {
border-left: none;
border-bottom: 2px solid transparent;
border-radius: var(--radius) var(--radius) 0 0;
padding: 6px 12px;
white-space: nowrap;
text-align: center;
}
.settings-nav-item:hover {
border-left-color: transparent;
border-bottom-color: var(--border);
}
.settings-nav-item.active {
border-left-color: transparent;
border-bottom-color: var(--todo);
}
.settings-content {
flex: 1;
min-height: 0;
}
.settings-section-heading {
padding: 12px 14px 10px;
margin-right: 14px;
}
.form-group {
padding: 0 14px;
}
.auth-provider-row {
flex-wrap: wrap;
padding: 12px 14px;
gap: 8px;
}
}