diff --git a/src/renderer/css/app.css b/src/renderer/css/app.css
index 64dd3d8..6a4ba5e 100644
--- a/src/renderer/css/app.css
+++ b/src/renderer/css/app.css
@@ -28,26 +28,47 @@ body {
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45);
}
-/* 顶部无边框拖拽标题栏 */
+/* 顶部无边框拖拽标题栏 (与标签栏合二为一) */
.titlebar {
height: var(--titlebar-height);
background: var(--bg-titlebar);
display: flex;
align-items: center;
- justify-content: space-between;
border-bottom: 1px solid var(--border-color);
-webkit-app-region: drag; /* 允许窗口拖拽 */
position: relative;
z-index: 100;
- padding: 0 8px;
+ padding: 0;
+ padding-right: 8px;
+ box-sizing: border-box;
}
.titlebar-left {
display: flex;
align-items: center;
gap: 6px;
+ height: 100%;
+ padding: 0 8px;
+ width: var(--sidebar-width, 250px);
+ min-width: var(--sidebar-width, 250px);
+ max-width: var(--sidebar-width, 250px);
+ border-right: 1px solid var(--border-color);
+ -webkit-app-region: no-drag;
overflow: hidden;
- -webkit-app-region: no-drag; /* 允许子元素正常响应鼠标交互 */
+ box-sizing: border-box;
+ flex-shrink: 0;
+ transition: width 0.22s cubic-bezier(0.4, 0, 0.2, 1),
+ min-width 0.22s cubic-bezier(0.4, 0, 0.2, 1),
+ max-width 0.22s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+#app.sidebar-collapsed .titlebar-left,
+.titlebar-left.sidebar-collapsed {
+ width: auto;
+ min-width: 0;
+ max-width: none;
+ border-right: none;
+ padding: 0 8px;
}
.app-brand {
@@ -57,7 +78,7 @@ body {
font-size: 13px;
font-weight: 700;
color: var(--accent-color);
- padding: 0 8px;
+ padding: 0 4px;
letter-spacing: 0.5px;
-webkit-app-region: drag;
cursor: grab;
@@ -69,7 +90,7 @@ body {
display: inline-flex;
align-items: center;
gap: 6px;
- padding: 2px 10px;
+ padding: 2px 8px;
height: 22px;
background: var(--bg-tab-inactive);
border: 1px solid var(--border-color);
@@ -80,6 +101,9 @@ body {
user-select: none;
-webkit-app-region: no-drag !important;
transition: all 0.15s ease;
+ flex-shrink: 1;
+ min-width: 0;
+ overflow: hidden;
}
.titlebar-workspace-tag:hover {
@@ -99,9 +123,23 @@ body {
.ws-tag-name {
font-weight: 500;
- max-width: 160px;
+ max-width: 95px;
overflow: hidden;
text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* 标签页栏容器 (位于顶栏中央,工作区展开时与终端左侧像素对齐) */
+.tab-bar-container {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ min-width: 0;
+ max-width: calc(100vw - 360px);
+ padding: 0;
+ height: 100%;
+ -webkit-app-region: no-drag !important;
+ box-sizing: border-box;
}
.titlebar-drag-spacer {
@@ -109,7 +147,7 @@ body {
height: 100%;
-webkit-app-region: drag; /* 中间大面积空白区域允许拖拽 */
cursor: grab;
- min-width: 40px;
+ min-width: 20px;
}
.app-brand-icon {
@@ -193,47 +231,6 @@ body {
color: #ffffff;
}
-/* 工作区终端主舞台与标签页栏 (工作区子层级,与终端严格对齐) */
-.workspace-stage {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- position: relative;
- overflow: hidden;
- background: transparent;
-}
-
-.workspace-header {
- height: 38px;
- min-height: 38px;
- background: var(--bg-titlebar);
- border-bottom: 1px solid var(--border-color);
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0 8px;
- gap: 8px;
- user-select: none;
- z-index: 20;
-}
-
-.tab-bar-container {
- display: flex;
- align-items: center;
- gap: 6px;
- flex: 1;
- min-width: 0;
- overflow: hidden;
-}
-
-.stage-actions {
- display: flex;
- align-items: center;
- gap: 4px;
- flex-shrink: 0;
-}
-
/* 主内容区终端视口 */
.main-viewport {
flex: 1;
diff --git a/src/renderer/css/sidebar.css b/src/renderer/css/sidebar.css
index 61a6a6a..1c78cf0 100644
--- a/src/renderer/css/sidebar.css
+++ b/src/renderer/css/sidebar.css
@@ -385,44 +385,3 @@
background: var(--btn-hover, rgba(255, 255, 255, 0.08));
}
-/* 侧边栏折叠时的浮动快捷呼出按钮 */
-.sidebar-floating-toggle {
- display: none;
- position: absolute;
- top: 10px;
- left: 10px;
- z-index: 35;
- background: var(--bg-titlebar);
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius-sm);
- color: var(--text-muted);
- font-size: 11px;
- padding: 4px 10px;
- cursor: pointer;
- align-items: center;
- gap: 5px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
- opacity: 0;
- pointer-events: none;
- transform: translateX(-15px);
-}
-
-.app-body.sidebar-collapsed .sidebar-floating-toggle,
-.workspace-sidebar.collapsed ~ .sidebar-floating-toggle {
- display: flex;
- opacity: 0.9;
- pointer-events: auto;
- transform: translateX(0);
-}
-
-.sidebar-floating-toggle:hover {
- opacity: 1;
- background: var(--bg-tab-hover);
- color: var(--accent-color);
- border-color: var(--border-active);
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5), 0 0 10px var(--accent-glow);
-}
-
diff --git a/src/renderer/css/tabs.css b/src/renderer/css/tabs.css
index c8c57ab..5217ecc 100644
--- a/src/renderer/css/tabs.css
+++ b/src/renderer/css/tabs.css
@@ -5,8 +5,9 @@
gap: 4px;
overflow-x: auto;
scrollbar-width: none;
- flex: 1;
+ flex: 0 1 auto;
min-width: 0;
+ padding: 0 6px;
-webkit-app-region: no-drag !important;
}
diff --git a/src/renderer/css/variables.css b/src/renderer/css/variables.css
index 55dcc25..646d738 100644
--- a/src/renderer/css/variables.css
+++ b/src/renderer/css/variables.css
@@ -18,6 +18,7 @@
--titlebar-height: 38px;
--statusbar-height: 26px;
+ --sidebar-width: 250px;
--border-radius-sm: 4px;
--border-radius-md: 8px;
--border-radius-lg: 12px;
diff --git a/src/renderer/index.html b/src/renderer/index.html
index 9dc6186..28463df 100644
--- a/src/renderer/index.html
+++ b/src/renderer/index.html
@@ -17,13 +17,13 @@
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/src/renderer/js/app.js b/src/renderer/js/app.js
index f9dd59a..10629f6 100644
--- a/src/renderer/js/app.js
+++ b/src/renderer/js/app.js
@@ -117,6 +117,10 @@ class TerminalApp {
if (tab) tab.splitTree.splitCurrent('vertical');
});
+ document.getElementById('btn-search')?.addEventListener('click', () => {
+ this.toggleSearch();
+ });
+
document.getElementById('status-theme')?.addEventListener('click', () => {
this.commandPalette.open();
this.commandPalette.input.value = '切换主题';
@@ -339,5 +343,5 @@ class TerminalApp {
// 页面加载就绪后启动终端
window.addEventListener('DOMContentLoaded', () => {
- new TerminalApp();
+ window.terminalApp = new TerminalApp();
});
diff --git a/src/renderer/js/workspace-manager.js b/src/renderer/js/workspace-manager.js
index 6645fea..ab3133c 100644
--- a/src/renderer/js/workspace-manager.js
+++ b/src/renderer/js/workspace-manager.js
@@ -125,6 +125,9 @@ export class WorkspaceManager {
this.isCollapsed = localStorage.getItem('hatty:sidebar-collapsed') === 'true';
if (this.isCollapsed) {
this.sidebar?.classList.add('collapsed');
+ document.querySelector('.app-body')?.classList.add('sidebar-collapsed');
+ document.getElementById('app')?.classList.add('sidebar-collapsed');
+ document.getElementById('titlebar-left')?.classList.add('sidebar-collapsed');
}
} catch (e) {
console.warn('Failed to load workspace state:', e);
@@ -154,12 +157,6 @@ export class WorkspaceManager {
this.setCollapsed(true);
});
- // 浮动快捷呼出按钮
- document.getElementById('sidebar-floating-toggle')?.addEventListener('click', (e) => {
- e.stopPropagation();
- this.setCollapsed(false);
- });
-
// 新建工作区按钮
document.getElementById('btn-add-workspace')?.addEventListener('click', () => {
this.openCreateModal();
@@ -202,6 +199,14 @@ export class WorkspaceManager {
if (appBody) {
appBody.classList.toggle('sidebar-collapsed', collapsed);
}
+ const app = document.getElementById('app');
+ if (app) {
+ app.classList.toggle('sidebar-collapsed', collapsed);
+ }
+ const titlebarLeft = document.getElementById('titlebar-left');
+ if (titlebarLeft) {
+ titlebarLeft.classList.toggle('sidebar-collapsed', collapsed);
+ }
const toggleBtn = document.getElementById('btn-toggle-sidebar');
if (toggleBtn) {
toggleBtn.classList.toggle('active', !collapsed);
diff --git a/test/test-runner.cjs b/test/test-runner.cjs
index 235b867..2770db1 100644
--- a/test/test-runner.cjs
+++ b/test/test-runner.cjs
@@ -150,9 +150,9 @@ app.whenReady().then(async () => {
`);
// ==========================================
- // Suite 2: Workspace Hierarchy & Stage Alignment
+ // Suite 2: Merged Titlebar & Workspace Alignment
// ==========================================
- runner.suite('2. 工作区层级结构与视口对齐验证 (UI Architecture)');
+ runner.suite('2. 顶栏与标签页栏合二为一与工作区对齐验证 (UI Architecture)');
await runner.test('标题栏与工作区标识层级正确 (Titlebar Header)', async () => {
const res = await win.webContents.executeJavaScript(`
@@ -172,50 +172,68 @@ app.whenReady().then(async () => {
if (!res.wsName) throw new Error('Active workspace tag name is empty');
});
- await runner.test('工作区终端舞台包含独立头部标签条与视口 (.workspace-stage)', async () => {
+ await runner.test('顶栏和标签页栏合二为一 (Merged Titlebar and Tab Bar)', async () => {
const res = await win.webContents.executeJavaScript(`
(() => {
- const stage = document.querySelector('.workspace-stage');
- const header = document.querySelector('.workspace-header');
- const viewport = document.getElementById('main-viewport');
+ const titlebar = document.querySelector('.titlebar');
+ const tabBarContainer = document.getElementById('tab-bar-container');
const tabBar = document.getElementById('tab-bar');
const newTabBtn = document.getElementById('btn-new-tab');
+ const viewport = document.getElementById('main-viewport');
+ const hasOldStage = !!document.querySelector('.workspace-header');
return {
- hasStage: !!stage,
- hasHeader: !!header,
+ titlebarHasContainer: titlebar?.contains(tabBarContainer),
+ containerHasTabs: tabBarContainer?.contains(tabBar),
+ containerHasBtn: tabBarContainer?.contains(newTabBtn),
hasViewport: !!viewport,
- tabsInHeader: header?.contains(tabBar),
- btnInHeader: header?.contains(newTabBtn),
+ hasOldHeader: hasOldStage
};
})()
`);
- if (!res.hasStage) throw new Error('Missing .workspace-stage container');
- if (!res.hasHeader) throw new Error('Missing .workspace-header');
+ if (!res.titlebarHasContainer) throw new Error('#tab-bar-container must be inside .titlebar');
+ if (!res.containerHasTabs) throw new Error('#tab-bar must be inside #tab-bar-container');
+ if (!res.containerHasBtn) throw new Error('#btn-new-tab must be inside #tab-bar-container');
if (!res.hasViewport) throw new Error('Missing #main-viewport');
- if (!res.tabsInHeader) throw new Error('#tab-bar must be inside .workspace-header');
- if (!res.btnInHeader) throw new Error('#btn-new-tab must be inside .workspace-header');
+ if (res.hasOldHeader) throw new Error('.workspace-header should be removed (merged into titlebar)');
});
- await runner.test('标签栏与终端窗口边缘完全像素级对齐 (100% Alignment)', async () => {
+ await runner.test('工作区展开时标签页和终端窗口左边像素级对齐', async () => {
const res = await win.webContents.executeJavaScript(`
(() => {
- const hRect = document.querySelector('.workspace-header').getBoundingClientRect();
+ const tRect = document.getElementById('tab-bar-container').getBoundingClientRect();
const vRect = document.getElementById('main-viewport').getBoundingClientRect();
return {
- headerLeft: hRect.left,
+ tabLeft: tRect.left,
viewportLeft: vRect.left,
- headerWidth: hRect.width,
- viewportWidth: vRect.width,
- leftDiff: Math.abs(hRect.left - vRect.left),
- widthDiff: Math.abs(hRect.width - vRect.width)
+ leftDiff: Math.abs(tRect.left - vRect.left)
};
})()
`);
if (res.leftDiff >= 1) {
- throw new Error(`Left mismatch: header.left=${res.headerLeft}, viewport.left=${res.viewportLeft}`);
+ throw new Error(`Left mismatch: tabLeft=${res.tabLeft}, viewportLeft=${res.viewportLeft}`);
}
- if (res.widthDiff >= 1) {
- throw new Error(`Width mismatch: header.width=${res.headerWidth}, viewport.width=${res.viewportWidth}`);
+ });
+
+ await runner.test('工作区关闭时标签页位于左侧且不强求与终端左边对齐', async () => {
+ const res = await win.webContents.executeJavaScript(`
+ (() => {
+ // 折叠侧边栏
+ window.terminalApp?.workspaceManager?.setCollapsed(true);
+ const tRect = document.getElementById('tab-bar-container').getBoundingClientRect();
+ const titlebarLeft = document.getElementById('titlebar-left').getBoundingClientRect();
+ const vRect = document.getElementById('main-viewport').getBoundingClientRect();
+ // 恢复展开状态供后续测试
+ window.terminalApp?.workspaceManager?.setCollapsed(false);
+ return {
+ tabLeft: tRect.left,
+ controlsRight: titlebarLeft.right,
+ viewportLeft: vRect.left,
+ distFromControls: Math.abs(tRect.left - titlebarLeft.right)
+ };
+ })()
+ `);
+ if (res.distFromControls >= 2) {
+ throw new Error(`Tab bar not immediately beside titlebar-left controls when collapsed: tabLeft=${res.tabLeft}, controlsRight=${res.controlsRight}`);
}
});