mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-07-09 17:08:29 +00:00
Improve settings modal navigation
Add settings sidebar search with filtered section results and clear affordance. Decouple sidebar accordion expansion from the active section so groups can be opened or collapsed manually. Keep the settings menu/search column fixed while the content pane scrolls, and promote Backup & Restore to its own section under Self Update.
This commit is contained in:
parent
f9031b7575
commit
60462eabbd
4 changed files with 201 additions and 21 deletions
|
|
@ -16,9 +16,9 @@
|
|||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#section-update-advanced">
|
||||
<a href="#section-backup-restore">
|
||||
<img src="/public/backup_restore.svg" alt="Backup & Restore" />
|
||||
<span>Advanced Settings</span>
|
||||
<span>Backup & Restore</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -28,18 +28,8 @@
|
|||
<x-component path="settings/backup/self-update.html"></x-component>
|
||||
</div>
|
||||
|
||||
<div id="section-update-advanced" class="section" x-data="{ advOpen: false }">
|
||||
<div class="settings-advanced-section">
|
||||
<button type="button" class="settings-advanced-toggle" @click="advOpen = !advOpen">
|
||||
<span class="material-symbols-outlined settings-advanced-toggle-icon"
|
||||
:style="advOpen ? 'transform:rotate(90deg)' : ''">chevron_right</span>
|
||||
<span>Advanced Settings</span>
|
||||
</button>
|
||||
|
||||
<div class="settings-advanced-body" x-show="advOpen" x-transition.opacity>
|
||||
<x-component path="settings/backup/backup_restore.html"></x-component>
|
||||
</div>
|
||||
</div>
|
||||
<div id="section-backup-restore" class="section">
|
||||
<x-component path="settings/backup/backup_restore.html"></x-component>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ const TAB_ITEMS = Object.freeze([
|
|||
icon: "system_update_alt",
|
||||
sections: [
|
||||
{ id: "section-self-update", label: "Self Update", icon: "system_update_alt" },
|
||||
{ id: "section-update-advanced", label: "Advanced Settings", icon: "tune" },
|
||||
{ id: "section-backup-restore", label: "Backup & Restore", icon: "backup" },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
|
@ -106,6 +106,8 @@ const model = {
|
|||
_paneScrollPane: null,
|
||||
_scrollSyncFrame: null,
|
||||
_updateStatusRefreshedAt: 0,
|
||||
expandedNavGroups: {},
|
||||
searchQuery: "",
|
||||
|
||||
// Tab state
|
||||
_activeTab: DEFAULT_TAB,
|
||||
|
|
@ -130,6 +132,7 @@ const model = {
|
|||
if (saved) this._activeTab = this.normalizeTabId(saved);
|
||||
} catch {}
|
||||
this._activeSection = this.getFirstSectionId(this._activeTab);
|
||||
this.expandedNavGroups = this.createDefaultExpandedNavGroups(this._activeTab);
|
||||
},
|
||||
|
||||
async onOpen() {
|
||||
|
|
@ -176,6 +179,7 @@ const model = {
|
|||
this.additional = null;
|
||||
this.error = null;
|
||||
this.isLoading = false;
|
||||
this.searchQuery = "";
|
||||
},
|
||||
|
||||
// Tab management
|
||||
|
|
@ -189,6 +193,7 @@ const model = {
|
|||
localStorage.setItem(VIEW_MODE_STORAGE_KEY, current);
|
||||
} catch {}
|
||||
|
||||
this.setNavGroupExpanded(current, true);
|
||||
this.bindPaneScroll();
|
||||
},
|
||||
|
||||
|
|
@ -204,6 +209,29 @@ const model = {
|
|||
return TAB_ITEMS;
|
||||
},
|
||||
|
||||
get normalizedSearchQuery() {
|
||||
return String(this.searchQuery || "").trim().toLowerCase();
|
||||
},
|
||||
|
||||
get hasSearchQuery() {
|
||||
return this.normalizedSearchQuery.length > 0;
|
||||
},
|
||||
|
||||
get filteredNavItems() {
|
||||
const query = this.normalizedSearchQuery;
|
||||
if (!query) return TAB_ITEMS;
|
||||
|
||||
return TAB_ITEMS
|
||||
.map((item) => {
|
||||
const itemMatches = this.getNavSearchText(item).includes(query);
|
||||
const sections = itemMatches
|
||||
? item.sections
|
||||
: item.sections.filter((section) => this.getNavSearchText(section).includes(query));
|
||||
return sections.length ? { ...item, sections } : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
},
|
||||
|
||||
get activeTabItem() {
|
||||
return TAB_ITEMS.find((item) => item.id === this.activeTab) || TAB_ITEMS[0];
|
||||
},
|
||||
|
|
@ -217,6 +245,59 @@ const model = {
|
|||
return tab?.sections?.[0]?.id || null;
|
||||
},
|
||||
|
||||
getNavSearchText(item) {
|
||||
return `${item?.label || ""} ${item?.id || ""}`.toLowerCase();
|
||||
},
|
||||
|
||||
createDefaultExpandedNavGroups(activeTab = this.activeTab) {
|
||||
return TAB_ITEMS.reduce((groups, item) => {
|
||||
groups[item.id] = item.id === activeTab;
|
||||
return groups;
|
||||
}, {});
|
||||
},
|
||||
|
||||
isNavGroupExpanded(tabName) {
|
||||
if (this.hasSearchQuery) return true;
|
||||
const tabId = this.normalizeTabId(tabName);
|
||||
return Boolean(this.expandedNavGroups?.[tabId]);
|
||||
},
|
||||
|
||||
setNavGroupExpanded(tabName, expanded) {
|
||||
const tabId = this.normalizeTabId(tabName);
|
||||
this.expandedNavGroups = {
|
||||
...(this.expandedNavGroups || {}),
|
||||
[tabId]: Boolean(expanded),
|
||||
};
|
||||
},
|
||||
|
||||
toggleNavGroup(tabName) {
|
||||
const tabId = this.normalizeTabId(tabName);
|
||||
if (this.hasSearchQuery) {
|
||||
this.enterTab(tabId);
|
||||
return;
|
||||
}
|
||||
if (this.activeTab !== tabId) {
|
||||
this.enterTab(tabId);
|
||||
this.setNavGroupExpanded(tabId, true);
|
||||
return;
|
||||
}
|
||||
this.setNavGroupExpanded(tabId, !this.isNavGroupExpanded(tabId));
|
||||
},
|
||||
|
||||
clearSearch() {
|
||||
this.searchQuery = "";
|
||||
},
|
||||
|
||||
openFirstSearchResult() {
|
||||
const item = this.filteredNavItems[0];
|
||||
const section = item?.sections?.[0];
|
||||
if (section?.id) {
|
||||
this.scrollToSection(section.id);
|
||||
} else if (item?.id) {
|
||||
this.enterTab(item.id);
|
||||
}
|
||||
},
|
||||
|
||||
get browserTimezone() {
|
||||
return getBrowserTimezone();
|
||||
},
|
||||
|
|
@ -281,6 +362,7 @@ const model = {
|
|||
enterTab(tabName) {
|
||||
this.activeTab = tabName;
|
||||
this._activeSection = this.getFirstSectionId(this.activeTab);
|
||||
this.setNavGroupExpanded(this.activeTab, true);
|
||||
this.resetPaneScroll();
|
||||
if (tabName === "backup") this.refreshUpdateStatus();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -29,20 +29,44 @@
|
|||
<div x-show="$store.settings.settings" class="settings-content">
|
||||
<!-- Tab Navigation -->
|
||||
<aside class="settings-tabs-container" aria-label="Settings categories">
|
||||
<div class="settings-search" role="search">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">search</span>
|
||||
<input
|
||||
type="search"
|
||||
x-model.debounce.100ms="$store.settings.searchQuery"
|
||||
@keydown.enter.prevent="$store.settings.openFirstSearchResult()"
|
||||
placeholder="Search"
|
||||
aria-label="Search settings"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="settings-search-clear"
|
||||
x-show="$store.settings.hasSearchQuery"
|
||||
@click="$store.settings.clearSearch()"
|
||||
aria-label="Clear search"
|
||||
>
|
||||
<span class="material-symbols-outlined" aria-hidden="true">close</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="settings-tabs no-scrollbar" role="tablist" aria-orientation="vertical">
|
||||
<template x-for="item in $store.settings.navItems" :key="item.id">
|
||||
<template x-for="item in $store.settings.filteredNavItems" :key="item.id">
|
||||
<div class="settings-nav-group"
|
||||
:class="{'settings-nav-group-active': $store.settings.activeTab === item.id}">
|
||||
:class="{
|
||||
'settings-nav-group-active': $store.settings.activeTab === item.id,
|
||||
'settings-nav-group-open': $store.settings.isNavGroupExpanded(item.id)
|
||||
}">
|
||||
<button type="button"
|
||||
class="settings-tab settings-parent-tab"
|
||||
role="tab"
|
||||
:aria-selected="$store.settings.activeTab === item.id"
|
||||
:aria-expanded="$store.settings.activeTab === item.id"
|
||||
:aria-expanded="$store.settings.isNavGroupExpanded(item.id)"
|
||||
:class="{
|
||||
'active': $store.settings.activeTab === item.id,
|
||||
'settings-tab-attention': $store.settings.navItemHasAttention(item)
|
||||
}"
|
||||
@click="$store.settings.enterTab(item.id)">
|
||||
@click="$store.settings.toggleNavGroup(item.id)">
|
||||
<span class="material-symbols-outlined" aria-hidden="true" x-text="item.icon"></span>
|
||||
<span class="settings-tab-label" x-text="item.label"></span>
|
||||
<span class="settings-tab-meta">
|
||||
|
|
@ -55,7 +79,7 @@
|
|||
</button>
|
||||
|
||||
<div class="settings-section-list"
|
||||
x-show="$store.settings.activeTab === item.id"
|
||||
x-show="$store.settings.isNavGroupExpanded(item.id)"
|
||||
x-transition.opacity.duration.120ms>
|
||||
<template x-for="section in item.sections" :key="section.id">
|
||||
<a class="settings-section-link"
|
||||
|
|
@ -76,6 +100,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="settings-search-empty" x-show="$store.settings.hasSearchQuery && !$store.settings.filteredNavItems.length">
|
||||
No settings found
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
|
|
|||
|
|
@ -196,6 +196,9 @@ select:disabled {
|
|||
.settings-tabs-container {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-self: stretch;
|
||||
z-index: 2;
|
||||
min-width: 0;
|
||||
|
|
@ -207,10 +210,81 @@ select:disabled {
|
|||
background: color-mix(in srgb, var(--color-panel) 44%, transparent);
|
||||
}
|
||||
|
||||
.settings-search {
|
||||
display: grid;
|
||||
grid-template-columns: 20px minmax(0, 1fr) 24px;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 34px;
|
||||
padding: 0 7px;
|
||||
border: 1px solid color-mix(in srgb, var(--color-border) 42%, transparent);
|
||||
border-radius: 7px;
|
||||
background: color-mix(in srgb, var(--color-text) 10%, transparent);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.settings-search:focus-within {
|
||||
border-color: color-mix(in srgb, var(--color-primary) 46%, var(--color-border));
|
||||
background: color-mix(in srgb, var(--color-text) 13%, transparent);
|
||||
}
|
||||
|
||||
.settings-search .material-symbols-outlined {
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.settings-search input[type="search"] {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
font: inherit;
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.settings-search input[type="search"]::-webkit-search-decoration,
|
||||
.settings-search input[type="search"]::-webkit-search-cancel-button,
|
||||
.settings-search input[type="search"]::-webkit-search-results-button,
|
||||
.settings-search input[type="search"]::-webkit-search-results-decoration {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-search input[type="search"]::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.settings-search-clear {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
min-width: 24px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settings-search-clear:hover {
|
||||
background: color-mix(in srgb, var(--color-text) 10%, transparent);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.settings-tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
|
|
@ -262,7 +336,7 @@ select:disabled {
|
|||
transition: transform 0.16s ease;
|
||||
}
|
||||
|
||||
.settings-nav-group-active .settings-tab-chevron {
|
||||
.settings-nav-group-open .settings-tab-chevron {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
|
|
@ -353,6 +427,13 @@ select:disabled {
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
.settings-search-empty {
|
||||
padding: 10px 9px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.settings-pane {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue