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:
@@ -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
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user