feat(sidebar): add per-page hide toggle with more options dropdown

- Add ``` button in sidebar header with dropdown menu
- Add "Hide sidebar on this page" option with per-page localStorage state
- Show edge indicator (14px chevron) on left when hidden per-page
- Show brief toast notification when hiding
- Fix container margin not resetting when sidebar is per-page hidden
- Add i18n translations for all 10 locales
This commit is contained in:
Will Miao
2026-06-12 18:27:54 +08:00
parent 84fcdb5f20
commit 1ae2778baa
13 changed files with 357 additions and 6 deletions

View File

@@ -84,6 +84,7 @@
border-bottom: 1px solid var(--border-color);
cursor: pointer;
transition: var(--transition-base);
position: relative;
}
.sidebar-header:hover {
@@ -150,6 +151,120 @@
display: none;
}
/* ===== Sidebar More Options Dropdown ===== */
.sidebar-more-dropdown {
position: absolute;
top: 100%;
right: 8px;
min-width: 190px;
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-xs);
box-shadow: var(--shadow-lg);
z-index: calc(var(--z-overlay) + 20);
display: none;
overflow: hidden;
margin-top: 2px;
}
.sidebar-more-dropdown.open {
display: block;
animation: dropdownFadeIn 0.15s ease;
}
@keyframes dropdownFadeIn {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
.sidebar-dropdown-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
cursor: pointer;
font-size: 0.85em;
color: var(--text-color);
transition: var(--transition-base);
white-space: nowrap;
}
.sidebar-dropdown-item:hover {
background: var(--lora-surface);
}
.sidebar-dropdown-item i {
width: 16px;
text-align: center;
color: var(--text-muted);
font-size: 0.9em;
flex-shrink: 0;
}
.sidebar-dropdown-item:hover i {
color: var(--text-color);
}
.sidebar-dropdown-item.disabled {
opacity: 0.4;
pointer-events: none;
}
/* ===== Sidebar Hidden Indicator (left edge) ===== */
.sidebar-hidden-indicator {
position: fixed;
left: 0;
top: 50%;
transform: translateY(-50%);
z-index: var(--z-overlay);
width: 14px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
background: var(--border-color);
opacity: 0.3;
border-radius: 0 4px 4px 0;
cursor: pointer;
transition: opacity 0.15s ease, background 0.15s ease;
}
.sidebar-hidden-indicator:hover {
opacity: 0.7;
background: var(--lora-accent);
}
.sidebar-hidden-indicator i {
font-size: 9px;
color: var(--text-muted);
transition: color 0.15s ease;
}
.sidebar-hidden-indicator:hover i {
color: white;
}
.sidebar-hidden-indicator-tooltip {
position: absolute;
left: 100%;
top: 50%;
transform: translateY(-50%);
margin-left: 8px;
padding: 4px 10px;
background: var(--text-color);
color: var(--bg-color);
font-size: 0.8em;
border-radius: 4px;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s ease;
}
.sidebar-hidden-indicator:hover .sidebar-hidden-indicator-tooltip {
opacity: 1;
}
.sidebar-content {
flex: 1;
overflow: hidden;

View File

@@ -36,6 +36,8 @@ export class SidebarManager {
this.currentDropTarget = null;
this.lastPageControls = null;
this.isDisabledBySetting = false;
this.isDisabledByPage = false;
this.isMoreDropdownOpen = false;
this.initializationPromise = null;
this.isCreatingFolder = false;
this._pendingDragState = null; // 用于保存拖拽创建文件夹时的状态
@@ -68,6 +70,10 @@ export class SidebarManager {
this.handleSidebarDrop = this.handleSidebarDrop.bind(this);
this.handleCreateFolderSubmit = this.handleCreateFolderSubmit.bind(this);
this.handleCreateFolderCancel = this.handleCreateFolderCancel.bind(this);
this.handleMoreToggle = this.handleMoreToggle.bind(this);
this.handleMoreDropdownItemClick = this.handleMoreDropdownItemClick.bind(this);
this.handleDocumentClickForMore = this.handleDocumentClickForMore.bind(this);
this.getPageDisplayName = this.getPageDisplayName.bind(this);
}
setHostPageControls(pageControls) {
@@ -100,6 +106,8 @@ export class SidebarManager {
this.initializeDragAndDrop();
this.updateSidebarTitle();
this.restoreSidebarState();
// Re-apply DOM visibility now that per-page state is known
this.updateDomVisibility(!this.isDisabledBySetting);
await this.loadFolderTree();
if (this.isDisabledBySetting && !forceInitialize) {
this.cleanup();
@@ -143,6 +151,13 @@ export class SidebarManager {
this.sidebarDragHandlersInitialized = false;
}
const moreDropdown = document.getElementById('sidebarMoreDropdown');
if (moreDropdown) {
moreDropdown.classList.remove('open');
}
this.isMoreDropdownOpen = false;
this.hideSidebarHiddenIndicator();
// Reset state
this.pageControls = null;
this.pageType = null;
@@ -151,6 +166,7 @@ export class SidebarManager {
this.expandedNodes = new Set();
this.openDropdown = null;
this.isHovering = false;
this.isDisabledByPage = false;
this.apiClient = null;
this.isInitialized = false;
this.recursiveSearchEnabled = true;
@@ -217,6 +233,18 @@ export class SidebarManager {
if (recursiveToggleBtn) {
recursiveToggleBtn.removeEventListener('click', this.handleRecursiveToggle);
}
const moreToggle = document.getElementById('sidebarMoreToggle');
if (moreToggle) {
moreToggle.removeEventListener('click', this.handleMoreToggle);
}
const moreDropdown = document.getElementById('sidebarMoreDropdown');
if (moreDropdown) {
moreDropdown.removeEventListener('click', this.handleMoreDropdownItemClick);
}
document.removeEventListener('click', this.handleDocumentClickForMore);
}
initializeDragAndDrop() {
@@ -1045,6 +1073,19 @@ export class SidebarManager {
}
});
}
// More options dropdown
const moreToggle = document.getElementById('sidebarMoreToggle');
if (moreToggle) {
moreToggle.addEventListener('click', this.handleMoreToggle);
}
const moreDropdown = document.getElementById('sidebarMoreDropdown');
if (moreDropdown) {
moreDropdown.addEventListener('click', this.handleMoreDropdownItemClick);
}
document.addEventListener('click', this.handleDocumentClickForMore);
}
handleDocumentClick(event) {
@@ -1066,6 +1107,7 @@ export class SidebarManager {
this.isPinned = !this.isPinned;
this.updateAutoHideState();
this.updatePinButton();
this.updateMoreDropdownLabels();
this.saveSidebarState();
this.updateContainerMargin();
}
@@ -1129,7 +1171,7 @@ export class SidebarManager {
}
updateAutoHideState() {
if (this.isDisabledBySetting) return;
if (this.isDisabledBySetting || this.isDisabledByPage) return;
const sidebar = document.getElementById('folderSidebar');
const hoverArea = document.getElementById('sidebarHoverArea');
@@ -1174,9 +1216,12 @@ export class SidebarManager {
if (!container || !sidebar || this.isDisabledBySetting) return;
// Reset margin to default
// Always reset margin first — needed when transitioning from visible to hidden
container.style.marginLeft = '';
// When per-page disabled, skip adjustment but margin is already reset
if (this.isDisabledByPage) return;
// Only adjust margin if sidebar is visible and pinned
if ((this.isPinned || this.isHovering) && this.isVisible) {
const sidebarWidth = sidebar.offsetWidth;
@@ -1193,20 +1238,29 @@ export class SidebarManager {
}
updateDomVisibility(enabled) {
// Per-page disable adds on top of global setting
const isVisible = enabled && !this.isDisabledByPage;
const sidebar = document.getElementById('folderSidebar');
const hoverArea = document.getElementById('sidebarHoverArea');
if (sidebar) {
sidebar.classList.toggle('hidden-by-setting', !enabled);
sidebar.setAttribute('aria-hidden', (!enabled).toString());
sidebar.classList.toggle('hidden-by-setting', !isVisible);
sidebar.setAttribute('aria-hidden', (!isVisible).toString());
}
if (hoverArea) {
hoverArea.classList.toggle('hidden-by-setting', !enabled);
if (!enabled) {
hoverArea.classList.toggle('hidden-by-setting', !isVisible);
if (!isVisible) {
hoverArea.classList.add('disabled');
}
}
// Show or hide the "sidebar hidden" notification
if (enabled && this.isDisabledByPage) {
this.showSidebarHiddenIndicator();
} else {
this.hideSidebarHiddenIndicator();
}
}
async setSidebarEnabled(enabled) {
@@ -1266,6 +1320,133 @@ export class SidebarManager {
}
}
// ===== More Options Dropdown =====
handleMoreToggle(event) {
event.stopPropagation();
const dropdown = document.getElementById('sidebarMoreDropdown');
if (!dropdown) return;
this.isMoreDropdownOpen = !dropdown.classList.contains('open');
dropdown.classList.toggle('open', this.isMoreDropdownOpen);
this.updateMoreDropdownLabels();
}
handleMoreDropdownItemClick(event) {
const item = event.target.closest('.sidebar-dropdown-item');
if (!item) return;
const action = item.dataset.action;
if (!action) return;
const dropdown = document.getElementById('sidebarMoreDropdown');
if (dropdown) {
dropdown.classList.remove('open');
this.isMoreDropdownOpen = false;
}
switch (action) {
case 'toggle-pin':
this.handlePinToggle(event);
break;
case 'toggle-hide':
this.toggleHideOnThisPage();
break;
}
}
handleDocumentClickForMore(event) {
const dropdown = document.getElementById('sidebarMoreDropdown');
const toggle = document.getElementById('sidebarMoreToggle');
if (!dropdown || !toggle) return;
if (!dropdown.contains(event.target) && !toggle.contains(event.target)) {
dropdown.classList.remove('open');
this.isMoreDropdownOpen = false;
}
}
updateMoreDropdownLabels() {
const pinLabel = document.getElementById('sidebarMorePinLabel');
if (pinLabel) {
pinLabel.textContent = this.isPinned
? translate('sidebar.unpinSidebar')
: translate('sidebar.pinSidebar');
}
const hideItem = document.querySelector('.sidebar-dropdown-item[data-action="toggle-hide"]');
if (hideItem) {
const hideIcon = hideItem.querySelector('i');
const hideLabel = hideItem.querySelector('span');
if (this.isDisabledByPage) {
hideLabel.textContent = translate('sidebar.showSidebar');
if (hideIcon) {
hideIcon.className = 'fas fa-eye';
}
} else {
hideLabel.textContent = translate('sidebar.hideOnThisPage');
if (hideIcon) {
hideIcon.className = 'fas fa-eye-slash';
}
}
}
}
toggleHideOnThisPage() {
this.isDisabledByPage = !this.isDisabledByPage;
setStorageItem(`${this.pageType}_sidebarDisabled`, this.isDisabledByPage);
this.updateDomVisibility(!this.isDisabledBySetting);
this.updateAutoHideState();
this.updateContainerMargin();
this.updateMoreDropdownLabels();
if (!this.isDisabledByPage) {
this.hideSidebarHiddenIndicator();
} else {
showToast(
'sidebar.sidebarHiddenNotification',
{ page: this.getPageDisplayName() },
'info',
`Sidebar hidden on ${this.getPageDisplayName()} page`
);
}
}
getPageDisplayName() {
const names = {
loras: 'LoRAs',
recipes: 'Recipes',
checkpoints: 'Checkpoints',
embeddings: 'Embeddings',
};
return names[this.pageType] || this.pageType;
}
showSidebarHiddenIndicator() {
if (document.getElementById('sidebarHiddenIndicator')) return;
const indicator = document.createElement('div');
indicator.id = 'sidebarHiddenIndicator';
indicator.className = 'sidebar-hidden-indicator';
indicator.innerHTML = `
<i class="fas fa-chevron-right"></i>
<span class="sidebar-hidden-indicator-tooltip">${translate('sidebar.showSidebar')}</span>
`;
indicator.addEventListener('click', () => {
this.toggleHideOnThisPage();
});
document.body.appendChild(indicator);
}
hideSidebarHiddenIndicator() {
const indicator = document.getElementById('sidebarHiddenIndicator');
if (indicator) {
indicator.remove();
}
}
async loadFolderTree() {
try {
if (this.displayMode === 'tree') {
@@ -1911,6 +2092,7 @@ export class SidebarManager {
const expandedPaths = getStorageItem(`${this.pageType}_expandedNodes`, []);
const displayMode = getStorageItem(`${this.pageType}_displayMode`, 'tree'); // 'tree' or 'list', default to 'tree'
const recursiveSearchEnabled = getStorageItem(`${this.pageType}_recursiveSearch`, true);
this.isDisabledByPage = getStorageItem(`${this.pageType}_sidebarDisabled`, false);
this.isPinned = isPinned;
this.expandedNodes = new Set(expandedPaths);