fix: tighten collapsed sidebar layout (#834)

This commit is contained in:
Zhicheng Han
2026-05-19 02:33:53 +02:00
committed by GitHub
parent d2cbce2f13
commit 3d74d78698
11 changed files with 131 additions and 16 deletions
@@ -22,6 +22,12 @@ const logoPath = '/logo.png';
const collapsedGroups = reactive<Record<string, boolean>>({}); const collapsedGroups = reactive<Record<string, boolean>>({});
type SidebarGroupKey = "Conversation" | "Agent" | "Monitoring" | "System";
function groupLabel(key: SidebarGroupKey) {
return t(`sidebar.group${key}${appStore.sidebarCollapsed ? "Short" : ""}`);
}
function toggleGroup(key: string) { function toggleGroup(key: string) {
collapsedGroups[key] = !collapsedGroups[key]; collapsedGroups[key] = !collapsedGroups[key];
} }
@@ -79,12 +85,12 @@ function openChangelog() {
<!-- Conversation --> <!-- Conversation -->
<div class="nav-group"> <div class="nav-group">
<div class="nav-group-label" @click="toggleGroup('conversation')"> <div class="nav-group-label" @click="toggleGroup('conversation')">
<span>{{ t("sidebar.groupConversation") }}</span> <span>{{ groupLabel("Conversation") }}</span>
<svg class="nav-group-arrow" :class="{ collapsed: isGroupCollapsed('conversation') }" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg class="nav-group-arrow" :class="{ collapsed: isGroupCollapsed('conversation') }" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9" /> <polyline points="6 9 12 15 18 9" />
</svg> </svg>
</div> </div>
<div v-show="!isGroupCollapsed('conversation')"> <div v-show="!isGroupCollapsed('conversation')" class="nav-group-items">
<button class="nav-item" :class="{ active: selectedKey === 'hermes.chat' }" @click="handleNav('hermes.chat')"> <button class="nav-item" :class="{ active: selectedKey === 'hermes.chat' }" @click="handleNav('hermes.chat')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" /> <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
@@ -124,12 +130,12 @@ function openChangelog() {
<!-- Agent --> <!-- Agent -->
<div class="nav-group"> <div class="nav-group">
<div class="nav-group-label" @click="toggleGroup('agent')"> <div class="nav-group-label" @click="toggleGroup('agent')">
<span>{{ t("sidebar.groupAgent") }}</span> <span>{{ groupLabel("Agent") }}</span>
<svg class="nav-group-arrow" :class="{ collapsed: isGroupCollapsed('agent') }" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg class="nav-group-arrow" :class="{ collapsed: isGroupCollapsed('agent') }" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9" /> <polyline points="6 9 12 15 18 9" />
</svg> </svg>
</div> </div>
<div v-show="!isGroupCollapsed('agent')"> <div v-show="!isGroupCollapsed('agent')" class="nav-group-items">
<button class="nav-item" :class="{ active: selectedKey === 'hermes.jobs' }" @click="handleNav('hermes.jobs')"> <button class="nav-item" :class="{ active: selectedKey === 'hermes.jobs' }" @click="handleNav('hermes.jobs')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" /> <rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
@@ -196,12 +202,12 @@ function openChangelog() {
<!-- Monitoring --> <!-- Monitoring -->
<div class="nav-group"> <div class="nav-group">
<div class="nav-group-label" @click="toggleGroup('monitoring')"> <div class="nav-group-label" @click="toggleGroup('monitoring')">
<span>{{ t("sidebar.groupMonitoring") }}</span> <span>{{ groupLabel("Monitoring") }}</span>
<svg class="nav-group-arrow" :class="{ collapsed: isGroupCollapsed('monitoring') }" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg class="nav-group-arrow" :class="{ collapsed: isGroupCollapsed('monitoring') }" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9" /> <polyline points="6 9 12 15 18 9" />
</svg> </svg>
</div> </div>
<div v-show="!isGroupCollapsed('monitoring')"> <div v-show="!isGroupCollapsed('monitoring')" class="nav-group-items">
<button class="nav-item" :class="{ active: selectedKey === 'hermes.logs' }" @click="handleNav('hermes.logs')"> <button class="nav-item" :class="{ active: selectedKey === 'hermes.logs' }" @click="handleNav('hermes.logs')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
@@ -232,12 +238,12 @@ function openChangelog() {
<!-- System --> <!-- System -->
<div class="nav-group"> <div class="nav-group">
<div class="nav-group-label" @click="toggleGroup('system')"> <div class="nav-group-label" @click="toggleGroup('system')">
<span>{{ t("sidebar.groupSystem") }}</span> <span>{{ groupLabel("System") }}</span>
<svg class="nav-group-arrow" :class="{ collapsed: isGroupCollapsed('system') }" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg class="nav-group-arrow" :class="{ collapsed: isGroupCollapsed('system') }" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9" /> <polyline points="6 9 12 15 18 9" />
</svg> </svg>
</div> </div>
<div v-show="!isGroupCollapsed('system')"> <div v-show="!isGroupCollapsed('system')" class="nav-group-items">
<button class="nav-item" :class="{ active: selectedKey === 'hermes.gateways' }" @click="handleNav('hermes.gateways')"> <button class="nav-item" :class="{ active: selectedKey === 'hermes.gateways' }" @click="handleNav('hermes.gateways')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="2" width="20" height="8" rx="2" ry="2" /> <rect x="2" y="2" width="20" height="8" rx="2" ry="2" />
@@ -423,6 +429,12 @@ function openChangelog() {
} }
} }
.nav-group-items {
display: flex;
flex-direction: column;
gap: 2px;
}
.nav-group-label { .nav-group-label {
font-size: 10px; font-size: 10px;
font-weight: 600; font-weight: 600;
@@ -549,14 +561,20 @@ function openChangelog() {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 8px; gap: 8px;
overflow: hidden;
} }
.version-links { .version-links {
display: flex; display: flex;
align-items: center; align-items: center;
flex-shrink: 0;
gap: 8px; gap: 8px;
} }
:deep(.theme-switch-container) {
flex-shrink: 0;
}
.github-link, .github-link,
.website-link { .website-link {
color: $text-muted; color: $text-muted;
@@ -576,6 +594,9 @@ function openChangelog() {
.version-text { .version-text {
flex: 1; flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
cursor: pointer; cursor: pointer;
transition: color 0.2s; transition: color 0.2s;
@@ -660,7 +681,18 @@ function openChangelog() {
} }
.nav-group-label { .nav-group-label {
display: none; justify-content: center;
gap: 2px;
padding: 8px 0 4px;
letter-spacing: 0;
span {
max-width: 36px;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
} }
.nav-item { .nav-item {
@@ -677,13 +709,6 @@ function openChangelog() {
} }
} }
// Keep group children visible — user can still see icons
.nav-group > div {
display: flex !important;
flex-direction: column;
gap: 2px;
}
// Hide selectors and footer text, keep theme switch // Hide selectors and footer text, keep theme switch
:deep(.profile-selector), :deep(.profile-selector),
:deep(.model-selector) { :deep(.model-selector) {
@@ -691,6 +716,12 @@ function openChangelog() {
} }
.sidebar-footer { .sidebar-footer {
.logout-item {
margin: 0;
padding: 10px 4px;
border-radius: $radius-sm;
}
.logout-item span { .logout-item span {
display: none; display: none;
} }
+7
View File
@@ -85,6 +85,13 @@ export default {
files: 'Dateien', files: 'Dateien',
groupChat: 'Gruppenchat', groupChat: 'Gruppenchat',
groupConversation: 'Konversation', groupConversation: 'Konversation',
groupConversationShort: 'Konv',
groupAgent: 'Agent',
groupAgentShort: 'Agent',
groupSystem: 'System',
groupSystemShort: 'Sys',
groupMonitoring: 'Monitoring',
groupMonitoringShort: 'Mon',
settings: 'Einstellungen', settings: 'Einstellungen',
connected: 'Verbunden', connected: 'Verbunden',
disconnected: 'Getrennt', disconnected: 'Getrennt',
+4
View File
@@ -90,10 +90,14 @@ export default {
groupChat: 'Group Chat', groupChat: 'Group Chat',
files: 'Files', files: 'Files',
groupConversation: 'Conversation', groupConversation: 'Conversation',
groupConversationShort: 'Conv',
groupPlatform: 'Platform', groupPlatform: 'Platform',
groupAgent: 'Agent', groupAgent: 'Agent',
groupAgentShort: 'Agent',
groupSystem: 'System', groupSystem: 'System',
groupSystemShort: 'Sys',
groupMonitoring: 'Monitoring', groupMonitoring: 'Monitoring',
groupMonitoringShort: 'Mon',
groupTools: 'Tools', groupTools: 'Tools',
settings: 'Settings', settings: 'Settings',
connected: 'Connected', connected: 'Connected',
+7
View File
@@ -85,6 +85,13 @@ export default {
files: 'Archivos', files: 'Archivos',
groupChat: 'Chat grupal', groupChat: 'Chat grupal',
groupConversation: 'Conversación', groupConversation: 'Conversación',
groupConversationShort: 'Conv',
groupAgent: 'Agente',
groupAgentShort: 'Ag.',
groupSystem: 'Sistema',
groupSystemShort: 'Sist',
groupMonitoring: 'Monitoreo',
groupMonitoringShort: 'Mon',
settings: 'Configuracion', settings: 'Configuracion',
connected: 'Conectado', connected: 'Conectado',
disconnected: 'Desconectado', disconnected: 'Desconectado',
+7
View File
@@ -85,6 +85,13 @@ export default {
files: 'Fichiers', files: 'Fichiers',
groupChat: 'Chat de groupe', groupChat: 'Chat de groupe',
groupConversation: 'Conversation', groupConversation: 'Conversation',
groupConversationShort: 'Conv',
groupAgent: 'Agent',
groupAgentShort: 'Agent',
groupSystem: 'Systeme',
groupSystemShort: 'Sys',
groupMonitoring: 'Suivi',
groupMonitoringShort: 'Suivi',
settings: 'Parametres', settings: 'Parametres',
connected: 'Connecte', connected: 'Connecte',
disconnected: 'Deconnecte', disconnected: 'Deconnecte',
+7
View File
@@ -85,6 +85,13 @@ export default {
files: 'ファイル', files: 'ファイル',
groupChat: 'グループチャット', groupChat: 'グループチャット',
groupConversation: '会話', groupConversation: '会話',
groupConversationShort: '会話',
groupAgent: 'エージェント',
groupAgentShort: '代理',
groupSystem: 'システム',
groupSystemShort: 'シス',
groupMonitoring: '監視',
groupMonitoringShort: '監視',
settings: '設定', settings: '設定',
connected: '接続済み', connected: '接続済み',
disconnected: '未接続', disconnected: '未接続',
+7
View File
@@ -85,6 +85,13 @@ export default {
files: '파일', files: '파일',
groupChat: '그룹 채팅', groupChat: '그룹 채팅',
groupConversation: '대화', groupConversation: '대화',
groupConversationShort: '대화',
groupAgent: '에이전트',
groupAgentShort: '에전',
groupSystem: '시스템',
groupSystemShort: '시스템',
groupMonitoring: '모니터링',
groupMonitoringShort: '모니터',
settings: '설정', settings: '설정',
connected: '연결됨', connected: '연결됨',
disconnected: '연결 끊김', disconnected: '연결 끊김',
+7
View File
@@ -85,6 +85,13 @@ export default {
files: 'Arquivos', files: 'Arquivos',
groupChat: 'Chat em grupo', groupChat: 'Chat em grupo',
groupConversation: 'Conversa', groupConversation: 'Conversa',
groupConversationShort: 'Conv',
groupAgent: 'Agente',
groupAgentShort: 'Ag.',
groupSystem: 'Sistema',
groupSystemShort: 'Sist',
groupMonitoring: 'Monitoramento',
groupMonitoringShort: 'Mon',
settings: 'Configuracoes', settings: 'Configuracoes',
connected: 'Conectado', connected: 'Conectado',
disconnected: 'Desconectado', disconnected: 'Desconectado',
@@ -90,10 +90,14 @@ export default {
groupChat: '群聊', groupChat: '群聊',
files: '檔案', files: '檔案',
groupConversation: '對話', groupConversation: '對話',
groupConversationShort: '對話',
groupPlatform: '平台', groupPlatform: '平台',
groupAgent: '代理', groupAgent: '代理',
groupAgentShort: '代理',
groupSystem: '系統', groupSystem: '系統',
groupSystemShort: '系統',
groupMonitoring: '監控', groupMonitoring: '監控',
groupMonitoringShort: '監控',
groupTools: '工具', groupTools: '工具',
settings: '設定', settings: '設定',
connected: '已連線', connected: '已連線',
+4
View File
@@ -90,10 +90,14 @@ export default {
groupChat: '群聊', groupChat: '群聊',
files: '文件', files: '文件',
groupConversation: '对话', groupConversation: '对话',
groupConversationShort: '对话',
groupPlatform: '平台', groupPlatform: '平台',
groupAgent: '代理', groupAgent: '代理',
groupAgentShort: '代理',
groupSystem: '系统', groupSystem: '系统',
groupSystemShort: '系统',
groupMonitoring: '监控', groupMonitoring: '监控',
groupMonitoringShort: '监控',
groupTools: '工具', groupTools: '工具',
settings: '设置', settings: '设置',
connected: '已连接', connected: '已连接',
+30
View File
@@ -82,6 +82,7 @@ describe('AppSidebar search entry', () => {
mockAppStore.updateAvailable = false mockAppStore.updateAvailable = false
mockAppStore.clientOutdated = false mockAppStore.clientOutdated = false
mockAppStore.updating = false mockAppStore.updating = false
mockAppStore.sidebarCollapsed = false
mockAppStore.reloadClient.mockClear() mockAppStore.reloadClient.mockClear()
}) })
@@ -127,4 +128,33 @@ describe('AppSidebar search entry', () => {
await reloadButton!.trigger('click') await reloadButton!.trigger('click')
expect(mockAppStore.reloadClient).toHaveBeenCalledTimes(1) expect(mockAppStore.reloadClient).toHaveBeenCalledTimes(1)
}) })
it('uses short group labels and keeps group folding active when collapsed', async () => {
mockAppStore.sidebarCollapsed = true
const wrapper = mount(AppSidebar, {
global: {
stubs: {
ProfileSelector: true,
ModelSelector: true,
LanguageSwitch: true,
ThemeSwitch: true,
NButton: true,
},
},
})
expect(wrapper.classes()).toContain('collapsed')
expect(wrapper.findAll('.nav-group-label span').map(node => node.text())).toEqual([
'sidebar.groupConversationShort',
'sidebar.groupAgentShort',
'sidebar.groupMonitoringShort',
'sidebar.groupSystemShort',
])
const agentGroup = wrapper.findAll('.nav-group')[1]
expect(agentGroup.find('.nav-group-items').attributes('style')).toBeUndefined()
await agentGroup.find('.nav-group-label').trigger('click')
expect(agentGroup.find('.nav-group-items').attributes('style')).toContain('display: none')
})
}) })