fix: group conversation search and standardize sidebar spacing

Move search to the top of the conversation area, remove its visible heading, and use a white surface in the light theme. Keep the list independently scrollable and sidebar controls reachable in short windows. Standardize footer divider spacing to 16px on both sides and update layout tests and design guidance.

Validation: 307 focused tests, typecheck, targeted ESLint, and four Electron layout scenarios passed.

Release note: Conversation search now sits above the conversation list in a distinct themed area, with consistent divider spacing and improved short-window scrolling.
This commit is contained in:
mesalogo
2026-09-28 19:06:06 +08:00
parent d8f0ef1301
commit 120076a13d
5 changed files with 91 additions and 37 deletions
+4
View File
@@ -3054,6 +3054,10 @@ describe("App", () => {
]);
const { container } = render(<App />);
const search = await screen.findByLabelText("搜索对话");
const conversationSection = search.closest(".sidebar-conversations");
expect(conversationSection).toBeInTheDocument();
expect(search.closest(".conversation-list")).toBeNull();
expect(conversationSection?.querySelector(".conversation-list")).toBeInTheDocument();
const searchControls = within(search.parentElement!);
expect(
searchControls.queryByRole("button", { name: "清除搜索" }),
+29 -26
View File
@@ -8716,31 +8716,6 @@ function App(): React.JSX.Element {
</button>
)}
<div className="sidebar-search">
<Search size={15} />
<input
aria-label={t("sidebar.searchLabel")}
ref={searchInputRef}
onChange={(event) => setSearchQuery(event.target.value)}
placeholder={t("sidebar.searchPlaceholder")}
value={searchQuery}
/>
{searchQuery.length > 0 && (
<button
aria-label={t("conversation.clearSearch")}
className="icon-button sidebar-search__clear"
onClick={() => {
setSearchQuery("");
searchInputRef.current?.focus();
}}
title={t("conversation.clearSearch")}
type="button"
>
<X aria-hidden="true" size={14} />
</button>
)}
</div>
<nav className="primary-nav" aria-label={t("navigation.label")}>
<button
aria-current={view === "chat" ? "page" : undefined}
@@ -8800,8 +8775,34 @@ function App(): React.JSX.Element {
</button>
</nav>
<section className="sidebar-conversations" aria-label={t("sidebar.recent")}>
<div className="sidebar-conversations__header">
<div className="sidebar-search">
<Search aria-hidden="true" size={15} />
<input
aria-label={t("sidebar.searchLabel")}
ref={searchInputRef}
onChange={(event) => setSearchQuery(event.target.value)}
placeholder={t("sidebar.searchPlaceholder")}
value={searchQuery}
/>
{searchQuery.length > 0 && (
<button
aria-label={t("conversation.clearSearch")}
className="icon-button sidebar-search__clear"
onClick={() => {
setSearchQuery("");
searchInputRef.current?.focus();
}}
title={t("conversation.clearSearch")}
type="button"
>
<X aria-hidden="true" size={14} />
</button>
)}
</div>
</div>
<div className="conversation-list">
<p className="section-label">{t("sidebar.recent")}</p>
{!conversationLoadError &&
filteredConversations.map((conversation) => {
const conversationTasks =
@@ -9328,6 +9329,8 @@ function App(): React.JSX.Element {
) : null}
</div>
</section>
<div className="sidebar-footer sidebar-footer--applications">
<button
className="nav-item"
+17
View File
@@ -8,6 +8,23 @@ const stylesheet = readFileSync(
).replaceAll('\r\n', '\n')
describe('primary sidebar layout', () => {
it('keeps search above an independently scrolling conversation list', () => {
const section = stylesheet.match(/\.sidebar-conversations\s*\{([^}]*)\}/u)?.[1]
const header = stylesheet.match(/\.sidebar-conversations__header\s*\{([^}]*)\}/u)?.[1]
const list = stylesheet.match(/\.conversation-list\s*\{([^}]*)\}/u)?.[1]
expect(section).toContain('min-height: 0;')
expect(section).toContain('flex-direction: column;')
expect(section).toContain('background: var(--surface-raised);')
expect(header).toContain('flex: 0 0 auto;')
expect(list).toContain('min-width: 0;')
expect(list).toContain('min-height: 0;')
expect(list).toContain('overflow: auto;')
const shortWindow = stylesheet.split('@media (max-height: 600px) {')[1]?.split('@media')[0]
expect(shortWindow).toContain('overflow-y: auto;')
expect(shortWindow).toContain('flex-shrink: 0;')
expect(shortWindow).toContain('min-height: 200px;')
})
it('floats conversation actions outside list layout with viewport limits', () => {
const rule = stylesheet.match(/\.conversation-actions\s*\{([^}]*)\}/u)?.[1]
expect(rule).toContain('position: fixed;')
+38 -10
View File
@@ -2587,9 +2587,8 @@ textarea:focus-visible {
.sidebar > .brand,
.sidebar > .project-switcher,
.sidebar > .new-chat,
.sidebar > .sidebar-search,
.sidebar > .primary-nav,
.sidebar > .conversation-list,
.sidebar > .sidebar-conversations,
.sidebar > .sidebar-footer {
min-width: 0;
}
@@ -3527,14 +3526,13 @@ textarea:focus-visible {
.sidebar-search {
position: relative;
display: flex;
min-width: 248px;
min-width: 0;
align-items: center;
padding: 9px 11px;
padding-right: 40px;
border: 1px solid var(--border-default);
border: 1px solid var(--border-control);
border-radius: var(--radius-control);
margin-bottom: var(--space-4);
background: var(--surface-raised);
background: transparent;
color: var(--text-muted);
gap: var(--space-2);
}
@@ -3648,13 +3646,43 @@ textarea:focus-visible {
place-items: center;
}
.sidebar-conversations {
display: flex;
min-height: 0;
flex: 1;
flex-direction: column;
padding: var(--space-2);
margin-top: var(--space-4);
border-radius: var(--radius-control);
background: var(--surface-raised);
}
.sidebar-conversations__header {
flex: 0 0 auto;
padding: var(--space-2) var(--space-1) var(--space-3);
}
.conversation-list {
min-width: 248px;
min-width: 0;
min-height: 0;
flex: 1;
padding-top: var(--space-4);
overflow: auto;
}
@media (max-height: 600px) {
.sidebar {
overflow-y: auto;
}
.sidebar > * {
flex-shrink: 0;
}
.sidebar-conversations {
min-height: 200px;
}
}
@media (forced-colors: none) and (pointer: fine) {
.conversation-list {
scrollbar-width: thin;
@@ -3813,7 +3841,8 @@ textarea:focus-visible {
.sidebar-footer {
min-width: 248px;
padding-top: 12px;
padding-top: var(--space-4);
margin-top: var(--space-4);
border-top: 1px solid var(--border-default);
}
@@ -16427,7 +16456,6 @@ textarea.field-control {
}
:root[data-theme='dark'] :where(
.sidebar-search,
.settings-section,
.settings-tabs,
.assistant-sidebar__row,