refactor sidebar header for improved dropdown functionality
- Replace dropdownOpen state management with store-based methods for better state handling. - Update event listeners for click and keydown actions to utilize the sidebar store. - Adjust dropdown visibility and close actions to ensure consistent behavior across menu items.
Alessandro committed
Mar 24, 2026 at 20:27 UTC
c7b97c17f015cd68285f89cf241e4bb0a2b24eed
3 files changed
+65
-29
webui/components/sidebar/sidebar-store.js
+28
-3
@@ -3,6 +3,7 @@ import { createStore } from "/js/AlpineStore.js";
3
// This store manages the visibility and state of the main sidebar panel.
4
const model = {
5
isOpen: true,
6
+ menuOpen: false,
7
_initialized: false,
8
9
// Centralized collapse state for all sidebar sections (persisted in localStorage)
@@ -79,7 +80,10 @@ const model = {
80
81
// Handle browser resize to show/hide sidebar based on viewport width
82
handleResize() {
82
- this.isOpen = !this.isMobile();
83
+ if (this.isMobile()) {
84
+ this.isOpen = false;
85
+ }
86
+ this.menuClose();
87
},
88
89
// Check if the current viewport is mobile
@@ -89,8 +93,29 @@ const model = {
93
94
// Dropdown positioning for quick-actions (fixed position to escape overflow:hidden)
95
dropdownStyle: {},
92
-
93
- updateDropdownPosition(triggerElement) {
96
+
97
+ headOpen() {
98
+ return this.isOpen || this.menuOpen;
99
+ },
100
+
101
+ menuToggle(triggerElement) {
102
+ this.menuOpen = !this.menuOpen;
103
+ if (this.menuOpen) {
104
+ this.menuPos(triggerElement);
105
+ }
106
+ },
107
+
108
+ menuClose() {
109
+ this.menuOpen = false;
110
+ },
111
+
112
+ menuClick(event, panelElement) {
113
+ if (this.menuOpen && panelElement && !panelElement.contains(event.target)) {
114
+ this.menuClose();
115
+ }
116
+ },
117
+
118
+ menuPos(triggerElement) {
119
if (!triggerElement) return;
120
const rect = triggerElement.getBoundingClientRect();
121
const menuWidth = Math.max(rect.width, 180);
webui/components/sidebar/top-section/header-icons.html
+33
-26
@@ -9,13 +9,17 @@
9
<body>
10
<div
11
class="sidebar-header-stack"
12
- x-data="{ dropdownOpen: false }"
13
- @click.outside="dropdownOpen = false"
14
- @keydown.escape.window="dropdownOpen = false"
15
- x-init="$watch('dropdownOpen', open => open && $nextTick(() => $store.sidebar.updateDropdownPosition($refs.quickActionsToggle)))"
12
+ x-data
13
+ @click.window="$store.sidebar.menuClick($event, $refs.headerPanel)"
14
+ @keydown.escape.window="$store.sidebar.menuClose()"
15
>
16
<!-- Fixed panel that expands on hover -->
18
- <div id="sidebar-header-panel" class="sidebar-header-panel" :class="{ 'sidebar-header-panel-open': $store.sidebar.isOpen }">
17
+ <div
18
+ id="sidebar-header-panel"
19
+ class="sidebar-header-panel"
20
+ x-ref="headerPanel"
21
+ :class="{ 'sidebar-header-panel-open': $store.sidebar.headOpen() }"
22
+ >
23
<div id="logo-bar">
24
<a href="https://github.com/agent0ai/agent-zero" target="_blank" rel="noopener noreferrer" aria-label="Agent Zero on GitHub">
25
<div class="logo-container">
@@ -47,37 +51,37 @@
51
<button
52
class="config-button header-action-button dropdown-toggle"
53
id="header-more"
50
- x-ref="quickActionsToggle"
51
- @click="dropdownOpen = !dropdownOpen"
54
+ x-ref="menuButton"
55
+ @click="$store.sidebar.menuToggle($refs.menuButton)"
56
title="More options"
57
aria-label="More options"
58
tabindex="-1"
59
>
56
- <span class="material-symbols-outlined" aria-hidden="true" :class="dropdownOpen ? 'rotated' : ''">expand_more</span>
60
+ <span class="material-symbols-outlined" aria-hidden="true" :class="$store.sidebar.menuOpen ? 'rotated' : ''">expand_more</span>
61
</button>
62
</div>
63
60
- <div class="dropdown-menu quick-actions-dropdown" x-show="dropdownOpen" :style="$store.sidebar.dropdownStyle">
64
+ <div class="dropdown-menu quick-actions-dropdown" x-show="$store.sidebar.menuOpen" :style="$store.sidebar.dropdownStyle">
65
<div id="sidebar-quick-actions-dropdown-slot-start"></div>
66
67
<div class="dropdown-header">Navigation</div>
68
65
- <button class="dropdown-item" @click="deselectChat(); dropdownOpen = false">
69
+ <button class="dropdown-item" @click="deselectChat(); $store.sidebar.menuClose()">
70
<span class="material-symbols-outlined">home</span>
71
<span>Dashboard</span>
72
</button>
73
70
- <button class="dropdown-item" @click="$store.projects.openProjectsModal(); dropdownOpen = false">
74
+ <button class="dropdown-item" @click="$store.projects.openProjectsModal(); $store.sidebar.menuClose()">
75
<span class="material-symbols-outlined">snippet_folder</span>
76
<span>Projects</span>
77
</button>
78
75
- <button class="dropdown-item" @click="openModal('components/modals/scheduler/scheduler-modal.html'); dropdownOpen = false">
79
+ <button class="dropdown-item" @click="openModal('components/modals/scheduler/scheduler-modal.html'); $store.sidebar.menuClose()">
80
<span class="material-symbols-outlined">schedule</span>
81
<span>Scheduler</span>
82
</button>
83
80
- <button class="dropdown-item" @click="openModal('settings/settings.html'); dropdownOpen = false">
84
+ <button class="dropdown-item" @click="openModal('settings/settings.html'); $store.sidebar.menuClose()">
85
<span class="material-symbols-outlined">settings</span>
86
<span>Settings</span>
87
</button>
@@ -85,34 +89,34 @@
89
<div class="dropdown-separator"></div>
90
<div class="dropdown-header">Chat Actions</div>
91
88
- <button class="dropdown-item" @click="$store.chats.newChat(); dropdownOpen = false">
92
+ <button class="dropdown-item" @click="$store.chats.newChat(); $store.sidebar.menuClose()">
93
<span class="material-symbols-outlined">chat_add_on</span>
94
<span>New Chat</span>
95
</button>
96
93
- <button class="dropdown-item" @click="$store.chats.loadChats(); dropdownOpen = false">
97
+ <button class="dropdown-item" @click="$store.chats.loadChats(); $store.sidebar.menuClose()">
98
<span class="material-symbols-outlined">folder_open</span>
99
<span>Load Chat</span>
100
</button>
101
98
- <button class="dropdown-item" @click="$store.chats.saveChat(); dropdownOpen = false" :disabled="!$store.chats.selected">
102
+ <button class="dropdown-item" @click="$store.chats.saveChat(); $store.sidebar.menuClose()" :disabled="!$store.chats.selected">
103
<span class="material-symbols-outlined">save</span>
104
<span>Save Chat</span>
105
</button>
106
103
- <button class="dropdown-item" @click="$confirmClick($event, () => { $store.chats.resetChat(); dropdownOpen = false })" :disabled="!$store.chats.selected">
107
+ <button class="dropdown-item" @click="$confirmClick($event, () => { $store.chats.resetChat(); $store.sidebar.menuClose() })" :disabled="!$store.chats.selected">
108
<span class="material-symbols-outlined">delete_sweep</span>
109
<span>Clear Chat</span>
110
</button>
111
112
<div class="dropdown-separator"></div>
113
110
- <button class="dropdown-item" @click="$confirmClick($event, () => { $store.chats.logout(); dropdownOpen = false })" :disabled="!$store.chats.loggedIn">
114
+ <button class="dropdown-item" @click="$confirmClick($event, () => { $store.chats.logout(); $store.sidebar.menuClose() })" :disabled="!$store.chats.loggedIn">
115
<span class="material-symbols-outlined">logout</span>
116
<span>Logout</span>
117
</button>
118
115
- <button class="dropdown-item" @click="$confirmClick($event, () => { $store.chats.restart(); dropdownOpen = false })">
119
+ <button class="dropdown-item" @click="$confirmClick($event, () => { $store.chats.restart(); $store.sidebar.menuClose() })">
120
<span class="material-symbols-outlined">restart_alt</span>
121
<span>Restart A0</span>
122
</button>
@@ -333,26 +337,29 @@
337
}
338
}
339
336
- /* --- Mobile Always Expanded --- */
340
+ /* --- Mobile: open only when header state says so --- */
341
@media (max-width: 768px) {
342
.sidebar-header-panel {
339
- width: var(--header-panel-expanded-width);
343
transition: all 0.3s ease;
344
}
342
-
343
- #a0-logo {
345
+
346
+ .sidebar-header-panel.sidebar-header-panel-open {
347
+ width: var(--header-panel-expanded-width);
348
+ }
349
+
350
+ .sidebar-header-panel.sidebar-header-panel-open #a0-logo {
351
opacity: 0.8;
352
}
353
347
- #logo-bar a:hover #a0-logo {
354
+ .sidebar-header-panel.sidebar-header-panel-open #logo-bar a:hover #a0-logo {
355
opacity: 1;
356
}
357
351
- #a0-logo-collapsed {
358
+ .sidebar-header-panel.sidebar-header-panel-open #a0-logo-collapsed {
359
opacity: 0;
360
}
361
355
- .sidebar-header-panel #hide-button .config-button {
362
+ .sidebar-header-panel.sidebar-header-panel-open #hide-button .config-button {
363
opacity: 0.8 !important;
364
transform: translateX(0) !important;
365
pointer-events: auto !important;
webui/index.css
+4
@@ -1022,6 +1022,10 @@ input:checked + .slider:before {
1022
/* Media Queries */
1023
@media (max-width: 640px) {
1024
/* Responsive tiles for mobile */
1025
+ #time-date {
1026
+ display: none;
1027
+ }
1028
+
1029
.preview-section {
1030
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
1031
gap: 8px;