feat: merge titlebar and tab bar into single header with dynamic workspace alignment
- Merge titlebar and tab bar into a single unified top bar (.titlebar) - Remove redundant floating workspace button that caused overlap on collapsed sidebar - Ensure tab bar left aligns with terminal window when workspace sidebar is expanded - Allow tab bar to sit naturally at leftmost position when workspace sidebar is collapsed - Update test runner suite to cover merged titlebar and alignment behavior
This commit is contained in:
+46
-49
@@ -28,26 +28,47 @@ body {
|
|||||||
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45);
|
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 顶部无边框拖拽标题栏 */
|
/* 顶部无边框拖拽标题栏 (与标签栏合二为一) */
|
||||||
.titlebar {
|
.titlebar {
|
||||||
height: var(--titlebar-height);
|
height: var(--titlebar-height);
|
||||||
background: var(--bg-titlebar);
|
background: var(--bg-titlebar);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
-webkit-app-region: drag; /* 允许窗口拖拽 */
|
-webkit-app-region: drag; /* 允许窗口拖拽 */
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
padding: 0 8px;
|
padding: 0;
|
||||||
|
padding-right: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.titlebar-left {
|
.titlebar-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
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;
|
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 {
|
.app-brand {
|
||||||
@@ -57,7 +78,7 @@ body {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
padding: 0 8px;
|
padding: 0 4px;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
@@ -69,7 +90,7 @@ body {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 2px 10px;
|
padding: 2px 8px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
background: var(--bg-tab-inactive);
|
background: var(--bg-tab-inactive);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
@@ -80,6 +101,9 @@ body {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-app-region: no-drag !important;
|
-webkit-app-region: no-drag !important;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
|
flex-shrink: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.titlebar-workspace-tag:hover {
|
.titlebar-workspace-tag:hover {
|
||||||
@@ -99,9 +123,23 @@ body {
|
|||||||
|
|
||||||
.ws-tag-name {
|
.ws-tag-name {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
max-width: 160px;
|
max-width: 95px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
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 {
|
.titlebar-drag-spacer {
|
||||||
@@ -109,7 +147,7 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
-webkit-app-region: drag; /* 中间大面积空白区域允许拖拽 */
|
-webkit-app-region: drag; /* 中间大面积空白区域允许拖拽 */
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
min-width: 40px;
|
min-width: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-brand-icon {
|
.app-brand-icon {
|
||||||
@@ -193,47 +231,6 @@ body {
|
|||||||
color: #ffffff;
|
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 {
|
.main-viewport {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -385,44 +385,3 @@
|
|||||||
background: var(--btn-hover, rgba(255, 255, 255, 0.08));
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,9 @@
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
flex: 1;
|
flex: 0 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
padding: 0 6px;
|
||||||
-webkit-app-region: no-drag !important;
|
-webkit-app-region: no-drag !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
--titlebar-height: 38px;
|
--titlebar-height: 38px;
|
||||||
--statusbar-height: 26px;
|
--statusbar-height: 26px;
|
||||||
|
--sidebar-width: 250px;
|
||||||
--border-radius-sm: 4px;
|
--border-radius-sm: 4px;
|
||||||
--border-radius-md: 8px;
|
--border-radius-md: 8px;
|
||||||
--border-radius-lg: 12px;
|
--border-radius-lg: 12px;
|
||||||
|
|||||||
+36
-52
@@ -17,13 +17,13 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<!-- 顶部现代拖拽式标题栏 -->
|
<!-- 顶部现代拖拽式标题栏 (顶栏与标签页栏合二为一) -->
|
||||||
<header class="titlebar">
|
<header class="titlebar" id="titlebar">
|
||||||
<div class="titlebar-left">
|
<div class="titlebar-left" id="titlebar-left">
|
||||||
<!-- 侧边栏折叠/展开按钮 -->
|
<!-- 侧边栏折叠/展开按钮 -->
|
||||||
<button class="btn-icon btn-toggle-sidebar" id="btn-toggle-sidebar" title="切换工作区侧边栏 (Ctrl+B)">
|
<button class="btn-icon btn-toggle-sidebar" id="btn-toggle-sidebar" title="切换工作区侧边栏 (Ctrl+B)">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
|
||||||
<line x1="9" y1="3" x2="9" y2="21"></line>
|
<line x1="9" y1="3" x2="9" y2="21"></line>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -43,11 +43,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 标签页栏与新建按钮 -->
|
||||||
|
<div class="tab-bar-container" id="tab-bar-container">
|
||||||
|
<div class="tab-bar" id="tab-bar"></div>
|
||||||
|
<button class="new-tab-btn" id="btn-new-tab" title="新建标签页 (Ctrl+Shift+T)">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 中间空白可拖拽区域 -->
|
<!-- 中间空白可拖拽区域 -->
|
||||||
<div class="titlebar-drag-spacer" id="titlebar-drag-spacer"></div>
|
<div class="titlebar-drag-spacer" id="titlebar-drag-spacer"></div>
|
||||||
|
|
||||||
<!-- 右侧快速操作按钮与窗口控制 -->
|
<!-- 右侧快速操作按钮与窗口控制 -->
|
||||||
<div class="titlebar-actions">
|
<div class="titlebar-actions">
|
||||||
|
<button class="btn-icon" id="btn-split-h" title="向右水平分屏 (Ctrl+Shift+E)">
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
|
||||||
|
<line x1="12" y1="3" x2="12" y2="21"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="btn-icon" id="btn-split-v" title="向下垂直分屏 (Ctrl+Shift+O)">
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
|
||||||
|
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="btn-icon" id="btn-search" title="查找文本 (Ctrl+Shift+F)">
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button class="btn-icon" id="btn-palette" title="快速命令面板 (Ctrl+Shift+P / F1)">
|
<button class="btn-icon" id="btn-palette" title="快速命令面板 (Ctrl+Shift+P / F1)">
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
@@ -83,7 +112,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- 应用主体容器 (含左侧工作区侧边栏与主视口) -->
|
<!-- 应用主体容器 (含左侧工作区侧边栏与终端视口) -->
|
||||||
<div class="app-body">
|
<div class="app-body">
|
||||||
<!-- 左侧工作区侧边栏 -->
|
<!-- 左侧工作区侧边栏 -->
|
||||||
<aside class="workspace-sidebar" id="workspace-sidebar">
|
<aside class="workspace-sidebar" id="workspace-sidebar">
|
||||||
@@ -124,53 +153,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- 折叠时的浮动快捷呼出按钮 -->
|
<!-- 终端主视口 -->
|
||||||
<button class="sidebar-floating-toggle" id="sidebar-floating-toggle" title="展开工作区侧边栏 (Ctrl+B)">
|
<main class="main-viewport" id="main-viewport"></main>
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<polyline points="9 18 15 12 9 6"></polyline>
|
|
||||||
</svg>
|
|
||||||
<span>工作区</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- 右侧工作区终端主舞台 (标签页为工作区子层级,与终端视口对齐) -->
|
|
||||||
<div class="workspace-stage">
|
|
||||||
<!-- 标签页栏与终端操作条 (与终端视口完全对齐) -->
|
|
||||||
<div class="workspace-header">
|
|
||||||
<div class="tab-bar-container">
|
|
||||||
<div class="tab-bar" id="tab-bar"></div>
|
|
||||||
<button class="new-tab-btn" id="btn-new-tab" title="新建标签页 (Ctrl+Shift+T)">
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
||||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stage-actions">
|
|
||||||
<button class="btn-icon" id="btn-split-h" title="向右水平分屏 (Ctrl+Shift+E)">
|
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
|
|
||||||
<line x1="12" y1="3" x2="12" y2="21"></line>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button class="btn-icon" id="btn-split-v" title="向下垂直分屏 (Ctrl+Shift+O)">
|
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
|
|
||||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button class="btn-icon" id="btn-search" title="查找文本 (Ctrl+Shift+F)">
|
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<circle cx="11" cy="11" r="8"></circle>
|
|
||||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 终端主视口 -->
|
|
||||||
<main class="main-viewport" id="main-viewport"></main>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 底部状态栏 -->
|
<!-- 底部状态栏 -->
|
||||||
|
|||||||
@@ -117,6 +117,10 @@ class TerminalApp {
|
|||||||
if (tab) tab.splitTree.splitCurrent('vertical');
|
if (tab) tab.splitTree.splitCurrent('vertical');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('btn-search')?.addEventListener('click', () => {
|
||||||
|
this.toggleSearch();
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('status-theme')?.addEventListener('click', () => {
|
document.getElementById('status-theme')?.addEventListener('click', () => {
|
||||||
this.commandPalette.open();
|
this.commandPalette.open();
|
||||||
this.commandPalette.input.value = '切换主题';
|
this.commandPalette.input.value = '切换主题';
|
||||||
@@ -339,5 +343,5 @@ class TerminalApp {
|
|||||||
|
|
||||||
// 页面加载就绪后启动终端
|
// 页面加载就绪后启动终端
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
new TerminalApp();
|
window.terminalApp = new TerminalApp();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -125,6 +125,9 @@ export class WorkspaceManager {
|
|||||||
this.isCollapsed = localStorage.getItem('hatty:sidebar-collapsed') === 'true';
|
this.isCollapsed = localStorage.getItem('hatty:sidebar-collapsed') === 'true';
|
||||||
if (this.isCollapsed) {
|
if (this.isCollapsed) {
|
||||||
this.sidebar?.classList.add('collapsed');
|
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) {
|
} catch (e) {
|
||||||
console.warn('Failed to load workspace state:', e);
|
console.warn('Failed to load workspace state:', e);
|
||||||
@@ -154,12 +157,6 @@ export class WorkspaceManager {
|
|||||||
this.setCollapsed(true);
|
this.setCollapsed(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 浮动快捷呼出按钮
|
|
||||||
document.getElementById('sidebar-floating-toggle')?.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.setCollapsed(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 新建工作区按钮
|
// 新建工作区按钮
|
||||||
document.getElementById('btn-add-workspace')?.addEventListener('click', () => {
|
document.getElementById('btn-add-workspace')?.addEventListener('click', () => {
|
||||||
this.openCreateModal();
|
this.openCreateModal();
|
||||||
@@ -202,6 +199,14 @@ export class WorkspaceManager {
|
|||||||
if (appBody) {
|
if (appBody) {
|
||||||
appBody.classList.toggle('sidebar-collapsed', collapsed);
|
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');
|
const toggleBtn = document.getElementById('btn-toggle-sidebar');
|
||||||
if (toggleBtn) {
|
if (toggleBtn) {
|
||||||
toggleBtn.classList.toggle('active', !collapsed);
|
toggleBtn.classList.toggle('active', !collapsed);
|
||||||
|
|||||||
+42
-24
@@ -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 () => {
|
await runner.test('标题栏与工作区标识层级正确 (Titlebar Header)', async () => {
|
||||||
const res = await win.webContents.executeJavaScript(`
|
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');
|
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 res = await win.webContents.executeJavaScript(`
|
||||||
(() => {
|
(() => {
|
||||||
const stage = document.querySelector('.workspace-stage');
|
const titlebar = document.querySelector('.titlebar');
|
||||||
const header = document.querySelector('.workspace-header');
|
const tabBarContainer = document.getElementById('tab-bar-container');
|
||||||
const viewport = document.getElementById('main-viewport');
|
|
||||||
const tabBar = document.getElementById('tab-bar');
|
const tabBar = document.getElementById('tab-bar');
|
||||||
const newTabBtn = document.getElementById('btn-new-tab');
|
const newTabBtn = document.getElementById('btn-new-tab');
|
||||||
|
const viewport = document.getElementById('main-viewport');
|
||||||
|
const hasOldStage = !!document.querySelector('.workspace-header');
|
||||||
return {
|
return {
|
||||||
hasStage: !!stage,
|
titlebarHasContainer: titlebar?.contains(tabBarContainer),
|
||||||
hasHeader: !!header,
|
containerHasTabs: tabBarContainer?.contains(tabBar),
|
||||||
|
containerHasBtn: tabBarContainer?.contains(newTabBtn),
|
||||||
hasViewport: !!viewport,
|
hasViewport: !!viewport,
|
||||||
tabsInHeader: header?.contains(tabBar),
|
hasOldHeader: hasOldStage
|
||||||
btnInHeader: header?.contains(newTabBtn),
|
|
||||||
};
|
};
|
||||||
})()
|
})()
|
||||||
`);
|
`);
|
||||||
if (!res.hasStage) throw new Error('Missing .workspace-stage container');
|
if (!res.titlebarHasContainer) throw new Error('#tab-bar-container must be inside .titlebar');
|
||||||
if (!res.hasHeader) throw new Error('Missing .workspace-header');
|
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.hasViewport) throw new Error('Missing #main-viewport');
|
||||||
if (!res.tabsInHeader) throw new Error('#tab-bar must be inside .workspace-header');
|
if (res.hasOldHeader) throw new Error('.workspace-header should be removed (merged into titlebar)');
|
||||||
if (!res.btnInHeader) throw new Error('#btn-new-tab must be inside .workspace-header');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await runner.test('标签栏与终端窗口边缘完全像素级对齐 (100% Alignment)', async () => {
|
await runner.test('工作区展开时标签页和终端窗口左边像素级对齐', async () => {
|
||||||
const res = await win.webContents.executeJavaScript(`
|
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();
|
const vRect = document.getElementById('main-viewport').getBoundingClientRect();
|
||||||
return {
|
return {
|
||||||
headerLeft: hRect.left,
|
tabLeft: tRect.left,
|
||||||
viewportLeft: vRect.left,
|
viewportLeft: vRect.left,
|
||||||
headerWidth: hRect.width,
|
leftDiff: Math.abs(tRect.left - vRect.left)
|
||||||
viewportWidth: vRect.width,
|
|
||||||
leftDiff: Math.abs(hRect.left - vRect.left),
|
|
||||||
widthDiff: Math.abs(hRect.width - vRect.width)
|
|
||||||
};
|
};
|
||||||
})()
|
})()
|
||||||
`);
|
`);
|
||||||
if (res.leftDiff >= 1) {
|
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}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user