fix: restore full-height primary sidebar

The full-width titlebar pushed the brand, project switcher, navigation, and account controls down from the window edge. The primary sidebar now spans the full window height while the titlebar belongs only to the right content frame, preserving the docked assistant workbar alignment.

Release notes now use the same full-window backdrop as other application modals, including over the titlebar.

Release note: 恢复左侧主导航的全高布局,并让版本更新说明的遮罩完整覆盖顶栏。
This commit is contained in:
mesalogo
2026-08-21 17:20:34 +08:00
parent 97419f3ef7
commit 63f0f7f0b0
6 changed files with 178 additions and 140 deletions
+10 -1
View File
@@ -8028,8 +8028,17 @@ describe('App', () => {
const assistantTrigger =
screen.getByLabelText('切换助手工作栏')
expect(assistantTrigger.parentElement).toBe(
container.querySelector('.app-content')
)
expect(container.querySelector('.sidebar')?.parentElement).toBe(
container.querySelector('.app-shell')
)
expect(container.querySelector('.app-frame')?.parentElement).toBe(
container.querySelector('.app-shell')
)
expect(container.querySelector('.topbar')?.parentElement).toBe(
container.querySelector('.app-frame')
)
expect(assistantTrigger).toHaveClass('assistant-sidebar-toggle')
expect(assistantTrigger).not.toHaveClass('icon-button--active')
expect(assistantTrigger).toHaveAttribute('aria-expanded', 'false')
@@ -8582,7 +8591,7 @@ describe('App', () => {
name: '助手工作栏'
})
expect(workbar).toHaveClass('assistant-sidebar--open')
expect(workbar.parentElement).toHaveClass('app-shell')
expect(workbar.parentElement).toHaveClass('app-content')
expect(
workbar.parentElement?.querySelector('main.workspace')
).toBeInTheDocument()
+136 -133
View File
@@ -7389,114 +7389,7 @@ function App(): React.JSX.Element {
const backgroundIsolated = mainSidebarOpen
return (
<div className="app-frame">
<header className="topbar">
<button
className="icon-button sidebar-toggle"
type="button"
aria-label={t('sidebar.toggle')}
onClick={() => setSidebarOpen((open) => !open)}
ref={sidebarToggleRef}
>
<PanelLeft aria-hidden="true" size={18} />
</button>
{view === 'chat' && (
<>
<div
className="conversation-title"
title={activeConversation?.title}
>
<span className="conversation-title__text">
{activeConversation
? getConversationDisplayTitle(
activeConversation,
t('conversation.defaultTitle')
)
:
(activeProject?.kind === 'channel'
? t('conversation.remoteTitle')
: t('conversation.defaultTitle'))}
</span>
{activeConversation?.branch && (
<ConversationBranchBadge
sourceTitle={
conversationTitles.get(
activeConversation.branch.sourceConversationId
) ?? activeConversation.branch.sourceTitle
}
/>
)}
{activeConversation?.remote && (
<b className="conversation-source-badge">
{
projectChannelLabels[
activeConversation.remote.channel
]
}
</b>
)}
</div>
<ScopeBadge
scope={
activeProject
? {
kind: 'project',
projectName:
activeProjectDisplayName ?? activeProject.name
}
: {
kind: 'unavailable',
explanation: t('notices.projectNotLoaded')
}
}
/>
</>
)}
<div className="topbar__actions">
<span
className={
runtime?.available
? 'runtime-status runtime-status--online'
: 'runtime-status'
}
title={runtime?.detail}
>
<span className="runtime-status__dot" />
<span className="runtime-status__label">
{runtime?.label ?? t('runtime.detecting')}
</span>
{runtime?.capability === 'image-generation' && (
<span className="runtime-capability-badge">
{t('runtime.imageGeneration')}
</span>
)}
</span>
<button
aria-label={
resolvedAppearanceTheme === 'dark'
? t('topbar.switchLight')
: t('topbar.switchDark')
}
aria-pressed={resolvedAppearanceTheme === 'dark'}
className="icon-button theme-toggle-button"
onClick={toggleAppearanceTheme}
title={
resolvedAppearanceTheme === 'dark'
? t('topbar.switchLight')
: t('topbar.switchDark')
}
type="button"
>
{resolvedAppearanceTheme === 'dark' ? (
<Sun aria-hidden="true" size={18} />
) : (
<Moon aria-hidden="true" size={18} />
)}
</button>
</div>
<WindowControls onError={handleWindowControlError} />
</header>
<div className="app-shell">
<div className="app-shell">
<aside
aria-label={
narrowWindow && sidebarOpen ? t('sidebar.label') : undefined
@@ -8148,32 +8041,141 @@ function App(): React.JSX.Element {
/>
)}
{view === 'chat' && (
<button
aria-controls="assistant-sidebar"
aria-expanded={assistantSidebarOpen}
aria-label={t('topbar.toggleAssistantSidebar')}
className="icon-button assistant-sidebar-toggle"
onClick={() =>
setAssistantSidebarOpen((current) => !current)
}
ref={assistantSidebarToggleRef}
title={t('topbar.toggleAssistantSidebar')}
type="button"
>
{assistantSidebarOpen ? (
<PanelRightClose aria-hidden="true" size={18} />
) : (
<PanelRightOpen aria-hidden="true" size={18} />
<div className="app-frame">
<header className="topbar">
<button
className="icon-button sidebar-toggle"
type="button"
aria-label={t('sidebar.toggle')}
onClick={() => setSidebarOpen((open) => !open)}
ref={sidebarToggleRef}
>
<PanelLeft aria-hidden="true" size={18} />
</button>
{view === 'chat' && (
<>
<div
className="conversation-title"
title={activeConversation?.title}
>
<span className="conversation-title__text">
{activeConversation
? getConversationDisplayTitle(
activeConversation,
t('conversation.defaultTitle')
)
:
(activeProject?.kind === 'channel'
? t('conversation.remoteTitle')
: t('conversation.defaultTitle'))}
</span>
{activeConversation?.branch && (
<ConversationBranchBadge
sourceTitle={
conversationTitles.get(
activeConversation.branch.sourceConversationId
) ?? activeConversation.branch.sourceTitle
}
/>
)}
{activeConversation?.remote && (
<b className="conversation-source-badge">
{
projectChannelLabels[
activeConversation.remote.channel
]
}
</b>
)}
</div>
<ScopeBadge
scope={
activeProject
? {
kind: 'project',
projectName:
activeProjectDisplayName ?? activeProject.name
}
: {
kind: 'unavailable',
explanation: t('notices.projectNotLoaded')
}
}
/>
</>
)}
</button>
)}
<div className="topbar__actions">
<span
className={
runtime?.available
? 'runtime-status runtime-status--online'
: 'runtime-status'
}
title={runtime?.detail}
>
<span className="runtime-status__dot" />
<span className="runtime-status__label">
{runtime?.label ?? t('runtime.detecting')}
</span>
{runtime?.capability === 'image-generation' && (
<span className="runtime-capability-badge">
{t('runtime.imageGeneration')}
</span>
)}
</span>
<button
aria-label={
resolvedAppearanceTheme === 'dark'
? t('topbar.switchLight')
: t('topbar.switchDark')
}
aria-pressed={resolvedAppearanceTheme === 'dark'}
className="icon-button theme-toggle-button"
onClick={toggleAppearanceTheme}
title={
resolvedAppearanceTheme === 'dark'
? t('topbar.switchLight')
: t('topbar.switchDark')
}
type="button"
>
{resolvedAppearanceTheme === 'dark' ? (
<Sun aria-hidden="true" size={18} />
) : (
<Moon aria-hidden="true" size={18} />
)}
</button>
</div>
<WindowControls onError={handleWindowControlError} />
</header>
<main
aria-hidden={backgroundIsolated ? 'true' : undefined}
className="workspace"
inert={backgroundIsolated}
>
<div className="app-content">
{view === 'chat' && (
<button
aria-controls="assistant-sidebar"
aria-expanded={assistantSidebarOpen}
aria-label={t('topbar.toggleAssistantSidebar')}
className="icon-button assistant-sidebar-toggle"
onClick={() =>
setAssistantSidebarOpen((current) => !current)
}
ref={assistantSidebarToggleRef}
title={t('topbar.toggleAssistantSidebar')}
type="button"
>
{assistantSidebarOpen ? (
<PanelRightClose aria-hidden="true" size={18} />
) : (
<PanelRightOpen aria-hidden="true" size={18} />
)}
</button>
)}
<main
aria-hidden={backgroundIsolated ? 'true' : undefined}
className="workspace"
inert={backgroundIsolated}
>
{(view === 'chat' || cachedWorkspaceViewKeys.has('chat')) && (
<KeepAliveRoute
active={view === 'chat'}
@@ -9882,7 +9884,8 @@ function App(): React.JSX.Element {
tab={assistantSidebarTab}
workspaceChanges={workspaceChanges}
workspaceProjectId={activeProjectId || undefined}
/>
/>
</div>
</div>
</div>
)
+10 -1
View File
@@ -199,7 +199,16 @@ describe('WorkspacePrimitives', () => {
/\.assistant-sidebar__tab\s*\{[^}]*white-space:\s*nowrap;/u
)
expect(stylesheet).toMatch(
/\.app-frame\s*\{[^}]*display:\s*grid;[^}]*grid-template-rows:\s*58px minmax\(0,\s*1fr\);/u
/\.app-frame\s*\{[^}]*display:\s*grid;[^}]*flex:\s*1;[^}]*grid-template-rows:\s*58px minmax\(0,\s*1fr\);/u
)
expect(stylesheet).toMatch(
/\.app-shell\s*\{[^}]*display:\s*flex;[^}]*width:\s*100%;[^}]*height:\s*100%;/u
)
expect(stylesheet).toMatch(
/\.release-notes-backdrop\s*\{[^}]*position:\s*fixed;[^}]*background:\s*var\(--overlay-backdrop\);[^}]*inset:\s*0;/u
)
expect(stylesheet).toMatch(
/\.app-content\s*\{[^}]*position:\s*relative;[^}]*display:\s*flex;[^}]*width:\s*100%;[^}]*height:\s*100%;/u
)
expect(stylesheet).toMatch(
/\.workspace\s*\{[^}]*grid-template-rows:\s*minmax\(0,\s*1fr\);/u
+14 -3
View File
@@ -2270,8 +2270,8 @@ textarea:focus-visible {
.app-frame {
position: relative;
display: grid;
width: 100%;
height: 100%;
flex: 1;
min-width: 0;
min-height: 0;
background: var(--surface-canvas);
@@ -2290,6 +2290,17 @@ textarea:focus-visible {
color: var(--text-primary);
}
.app-content {
position: relative;
display: flex;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
background: var(--surface-canvas);
color: var(--text-primary);
}
.sidebar {
display: flex;
flex: 0 0 278px;
@@ -5411,7 +5422,7 @@ button > svg {
display: grid;
padding: var(--space-4);
background: var(--overlay-backdrop);
inset: 38px 0 0;
inset: 0;
place-items: center;
}
@@ -12558,7 +12569,7 @@ details.settings-section > :not(summary) + :not(summary) {
--shadow-dialog: 0 18px 48px rgb(0 4 10 / 48%);
}
:root[data-theme='dark'] :where(.app-frame, .app-shell) {
:root[data-theme='dark'] :where(.app-frame, .app-shell, .app-content) {
background: var(--surface-canvas);
color: var(--text-primary);
}