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:
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user