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:
+6
-2
@@ -333,9 +333,13 @@
|
||||
应用顶栏用于当前会话标题、范围、窗口级状态、侧栏开关和低频全局操作,不承担主要导航。
|
||||
顶栏必须保持紧凑,不能与页面内容争夺注意力。
|
||||
|
||||
- 无边框窗口使用一条紧凑标题栏,保留侧栏开关、当前会话标题、范围、Runtime 状态、主题
|
||||
开关和窗口控制,不得为了新增工作栏入口把这一整组控件下移。普通图标按钮使用
|
||||
- 应用壳层在水平方向分为全高主侧栏和右侧内容框架。主侧栏从窗口顶部贯穿到底部,品牌区、
|
||||
项目切换器、导航和账户区保持同一连续表面;不能用全宽标题栏把整条侧栏向下推移。
|
||||
- 无边框窗口的紧凑标题栏只覆盖右侧内容框架,保留侧栏开关、当前会话标题、范围、Runtime
|
||||
状态、主题开关和窗口控制,不得为了新增工作栏入口把这一整组控件下移。普通图标按钮使用
|
||||
`34px × 34px` 点击区域。
|
||||
- 发布说明 Modal 与其他应用级 Modal 一样使用整窗遮罩;遮罩必须覆盖主侧栏、右侧内容和
|
||||
顶栏,不能保留一条未被遮罩的窗口控制色块。关闭操作由 Modal 自身提供。
|
||||
- 当前会话存在 Task 时,Task 上下文栏位于标题栏下方并承载任务摘要与详情;没有 Task 时
|
||||
整行隐藏。折叠状态保持为 `46px` 单行表面,与右侧工作栏标签栏等高,左右分隔线必须水平
|
||||
对齐。该栏不重复放置新建入口。
|
||||
|
||||
@@ -221,6 +221,8 @@ Runtime
|
||||
- 分栏计算排除最左侧主导航,主工作区与工作栏使用相同的 `300px` 最小宽度。拖动记录两侧
|
||||
在可用分栏宽度中的比例;任一侧达到最小值后停止,窗口缩放后按原比例重新计算并再次
|
||||
应用同一最小值约束。
|
||||
- 最左侧主导航保持全高连续表面;窗口标题栏只覆盖右侧内容框架,不能横跨主导航并把品牌、
|
||||
项目切换器和导航整体向下推移。
|
||||
- 对话内容与输入区始终保留对称的左右安全边距,工作栏边界不得覆盖或裁切输入框、发送
|
||||
按钮及主工作区右侧边距。
|
||||
- 唯一展开与收起开关固定在窗口关闭按钮正下方。工作栏内部不重复关闭入口。开关图标中的
|
||||
|
||||
@@ -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