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;