feat: Implement auto-hide functionality for sidebar and update controls layout

This commit is contained in:
Will Miao
2025-08-26 17:57:59 +08:00
parent 522a3ea88b
commit a98e26139f
5 changed files with 240 additions and 135 deletions

View File

@@ -1,7 +1,7 @@
.folder-sidebar { .folder-sidebar {
position: fixed; position: fixed;
top: 68px; /* Below header */ top: 68px; /* Below header */
left: 0px; left: 10px;
width: 230px; width: 230px;
height: calc(100vh - 88px); height: calc(100vh - 88px);
background: var(--bg-color); background: var(--bg-color);
@@ -17,12 +17,41 @@
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
} }
/* Auto-hide states */
.folder-sidebar.auto-hide {
transform: translateX(-100%);
opacity: 0;
pointer-events: none;
}
.folder-sidebar.auto-hide.hover-active {
transform: translateX(0);
opacity: 1;
pointer-events: all;
}
.folder-sidebar.collapsed { .folder-sidebar.collapsed {
transform: translateX(-100%); transform: translateX(-100%);
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
} }
/* Hover detection area for auto-hide */
.sidebar-hover-area {
position: fixed;
top: 68px;
left: 0;
width: 20px;
height: calc(100vh - 88px);
z-index: calc(var(--z-overlay) - 1);
background: transparent;
pointer-events: all;
}
.sidebar-hover-area.disabled {
pointer-events: none;
}
.sidebar-header { .sidebar-header {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -54,9 +83,17 @@
align-items: center; align-items: center;
gap: 8px; gap: 8px;
font-weight: 500; font-weight: 500;
flex: 1;
pointer-events: none;
} }
.sidebar-toggle-close { .sidebar-header-actions {
display: flex;
align-items: center;
gap: 4px;
}
.sidebar-action-btn {
background: none; background: none;
border: none; border: none;
color: var(--text-muted); color: var(--text-muted);
@@ -72,12 +109,23 @@
justify-content: center; justify-content: center;
} }
.sidebar-toggle-close:hover { .sidebar-action-btn:hover {
opacity: 1; opacity: 1;
background: var(--lora-surface); background: var(--lora-surface);
color: var(--text-color); color: var(--text-color);
} }
.sidebar-action-btn.active {
opacity: 1;
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.15);
color: var(--lora-accent);
}
/* Remove old close button styles */
.sidebar-toggle-close {
display: none;
}
.sidebar-content { .sidebar-content {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
@@ -328,38 +376,6 @@
} }
} }
/* Sidebar Toggle Button */
.sidebar-toggle-container {
margin-left: auto;
}
.sidebar-toggle-btn {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--card-bg);
border: 1px solid var(--border-color);
color: var(--text-color);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.sidebar-toggle-btn:hover {
background: var(--lora-accent);
color: white;
transform: translateY(-2px);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1);
}
.sidebar-toggle-btn.active {
background: var(--lora-accent);
color: white;
}
/* Empty State */ /* Empty State */
.sidebar-tree-placeholder { .sidebar-tree-placeholder {
padding: 24px 16px; padding: 24px 16px;

View File

@@ -224,33 +224,6 @@
display: none !important; display: none !important;
} }
/* Sidebar Toggle Button */
.sidebar-toggle-container {
margin-left: auto;
}
.sidebar-toggle-btn {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--card-bg);
border: 1px solid var(--border-color);
color: var(--text-color);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.sidebar-toggle-btn:hover {
background: var(--lora-accent);
color: white;
transform: translateY(-2px);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1);
}
/* Icon-only button style */ /* Icon-only button style */
.icon-only { .icon-only {
min-width: unset !important; min-width: unset !important;
@@ -298,7 +271,6 @@
/* Prevent text selection in control and header areas */ /* Prevent text selection in control and header areas */
.control-group button, .control-group button,
.control-group select, .control-group select,
.sidebar-toggle-btn,
.bulk-operations-panel, .bulk-operations-panel,
.app-header, .app-header,
.header-branding, .header-branding,
@@ -306,8 +278,7 @@
.main-nav, .main-nav,
.nav-item, .nav-item,
.header-actions button, .header-actions button,
.header-controls, .header-controls {
.sidebar-toggle-container button {
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
@@ -391,14 +362,6 @@
justify-content: flex-end; justify-content: flex-end;
margin-top: 8px; margin-top: 8px;
} }
.sidebar-toggle-container {
margin-left: 0;
}
.sidebar-toggle-btn:hover {
transform: none; /* Disable hover effects on mobile */
}
.control-group button:hover { .control-group button:hover {
transform: none; /* Disable hover effects on mobile */ transform: none; /* Disable hover effects on mobile */

View File

@@ -12,15 +12,23 @@ export class SidebarManager {
this.selectedPath = ''; this.selectedPath = '';
this.expandedNodes = new Set(); this.expandedNodes = new Set();
this.isVisible = true; this.isVisible = true;
this.isPinned = false;
this.apiClient = null; this.apiClient = null;
this.openDropdown = null; this.openDropdown = null;
this.hoverTimeout = null;
this.isHovering = false;
// Bind methods // Bind methods
this.handleTreeClick = this.handleTreeClick.bind(this); this.handleTreeClick = this.handleTreeClick.bind(this);
this.handleBreadcrumbClick = this.handleBreadcrumbClick.bind(this); this.handleBreadcrumbClick = this.handleBreadcrumbClick.bind(this);
this.toggleSidebar = this.toggleSidebar.bind(this);
this.closeSidebar = this.closeSidebar.bind(this);
this.handleDocumentClick = this.handleDocumentClick.bind(this); this.handleDocumentClick = this.handleDocumentClick.bind(this);
this.handleSidebarHeaderClick = this.handleSidebarHeaderClick.bind(this);
this.handlePinToggle = this.handlePinToggle.bind(this);
this.handleCollapseAll = this.handleCollapseAll.bind(this);
this.handleMouseEnter = this.handleMouseEnter.bind(this);
this.handleMouseLeave = this.handleMouseLeave.bind(this);
this.handleHoverAreaEnter = this.handleHoverAreaEnter.bind(this);
this.handleHoverAreaLeave = this.handleHoverAreaLeave.bind(this);
this.init(); this.init();
} }
@@ -32,6 +40,7 @@ export class SidebarManager {
this.restoreSidebarState(); this.restoreSidebarState();
await this.loadFolderTree(); await this.loadFolderTree();
this.restoreSelectedFolder(); this.restoreSelectedFolder();
this.updateAutoHideState();
} }
updateSidebarTitle() { updateSidebarTitle() {
@@ -42,22 +51,22 @@ export class SidebarManager {
} }
setupEventHandlers() { setupEventHandlers() {
// Sidebar toggle button // Sidebar header (root selection) - only trigger on title area
const toggleBtn = document.querySelector('.sidebar-toggle-btn');
if (toggleBtn) {
toggleBtn.addEventListener('click', this.toggleSidebar);
}
// Sidebar header (root selection)
const sidebarHeader = document.getElementById('sidebarHeader'); const sidebarHeader = document.getElementById('sidebarHeader');
if (sidebarHeader) { if (sidebarHeader) {
sidebarHeader.addEventListener('click', () => this.selectFolder('')); sidebarHeader.addEventListener('click', this.handleSidebarHeaderClick);
} }
// Sidebar close button // Pin toggle button
const closeBtn = document.getElementById('sidebarToggleClose'); const pinToggleBtn = document.getElementById('sidebarPinToggle');
if (closeBtn) { if (pinToggleBtn) {
closeBtn.addEventListener('click', this.closeSidebar); pinToggleBtn.addEventListener('click', this.handlePinToggle);
}
// Collapse all button
const collapseAllBtn = document.getElementById('sidebarCollapseAll');
if (collapseAllBtn) {
collapseAllBtn.addEventListener('click', this.handleCollapseAll);
} }
// Tree click handler // Tree click handler
@@ -72,27 +81,34 @@ export class SidebarManager {
sidebarBreadcrumbNav.addEventListener('click', this.handleBreadcrumbClick); sidebarBreadcrumbNav.addEventListener('click', this.handleBreadcrumbClick);
} }
// Hover detection for auto-hide
const sidebar = document.getElementById('folderSidebar');
const hoverArea = document.getElementById('sidebarHoverArea');
if (sidebar) {
sidebar.addEventListener('mouseenter', this.handleMouseEnter);
sidebar.addEventListener('mouseleave', this.handleMouseLeave);
}
if (hoverArea) {
hoverArea.addEventListener('mouseenter', this.handleHoverAreaEnter);
hoverArea.addEventListener('mouseleave', this.handleHoverAreaLeave);
}
// Close sidebar when clicking outside on mobile // Close sidebar when clicking outside on mobile
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
if (window.innerWidth <= 1024 && this.isVisible) { if (window.innerWidth <= 1024 && this.isVisible) {
const sidebar = document.getElementById('folderSidebar'); const sidebar = document.getElementById('folderSidebar');
const toggleBtn = document.querySelector('.sidebar-toggle-btn');
if (sidebar && !sidebar.contains(e.target) && if (sidebar && !sidebar.contains(e.target)) {
toggleBtn && !toggleBtn.contains(e.target)) { this.hideSidebar();
this.closeSidebar();
} }
} }
}); });
// Handle window resize // Handle window resize
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
if (window.innerWidth > 1024 && this.isVisible) { this.updateAutoHideState();
const sidebar = document.getElementById('folderSidebar');
if (sidebar) {
sidebar.classList.remove('collapsed');
}
}
}); });
// Add document click handler for closing dropdowns // Add document click handler for closing dropdowns
@@ -106,10 +122,115 @@ export class SidebarManager {
} }
} }
closeDropdown() { handleSidebarHeaderClick(event) {
if (this.openDropdown) { // Only trigger root selection if clicking on the title area, not the buttons
this.openDropdown.classList.remove('open'); if (!event.target.closest('.sidebar-header-actions')) {
this.openDropdown = null; this.selectFolder('');
}
}
handlePinToggle(event) {
event.stopPropagation();
this.isPinned = !this.isPinned;
this.updateAutoHideState();
this.updatePinButton();
this.saveSidebarState();
}
handleCollapseAll(event) {
event.stopPropagation();
this.expandedNodes.clear();
this.renderTree();
this.saveExpandedState();
}
handleMouseEnter() {
this.isHovering = true;
if (this.hoverTimeout) {
clearTimeout(this.hoverTimeout);
this.hoverTimeout = null;
}
if (!this.isPinned) {
this.showSidebar();
}
}
handleMouseLeave() {
this.isHovering = false;
if (!this.isPinned) {
this.hoverTimeout = setTimeout(() => {
if (!this.isHovering) {
this.hideSidebar();
}
}, 300);
}
}
handleHoverAreaEnter() {
if (!this.isPinned) {
this.showSidebar();
}
}
handleHoverAreaLeave() {
// Let the sidebar's mouse leave handler deal with hiding
}
showSidebar() {
const sidebar = document.getElementById('folderSidebar');
if (sidebar && !this.isPinned) {
sidebar.classList.add('hover-active');
this.isVisible = true;
}
}
hideSidebar() {
const sidebar = document.getElementById('folderSidebar');
if (sidebar && !this.isPinned) {
sidebar.classList.remove('hover-active');
this.isVisible = false;
}
}
updateAutoHideState() {
const sidebar = document.getElementById('folderSidebar');
const hoverArea = document.getElementById('sidebarHoverArea');
if (!sidebar || !hoverArea) return;
if (window.innerWidth <= 1024) {
// Mobile: always use collapsed state
sidebar.classList.remove('auto-hide', 'hover-active');
sidebar.classList.add('collapsed');
hoverArea.classList.add('disabled');
this.isVisible = false;
} else if (this.isPinned) {
// Desktop pinned: always visible
sidebar.classList.remove('auto-hide', 'collapsed', 'hover-active');
hoverArea.classList.add('disabled');
this.isVisible = true;
} else {
// Desktop auto-hide: use hover detection
sidebar.classList.remove('collapsed');
sidebar.classList.add('auto-hide');
hoverArea.classList.remove('disabled');
if (this.isHovering) {
sidebar.classList.add('hover-active');
this.isVisible = true;
} else {
sidebar.classList.remove('hover-active');
this.isVisible = false;
}
}
}
updatePinButton() {
const pinBtn = document.getElementById('sidebarPinToggle');
if (pinBtn) {
pinBtn.classList.toggle('active', this.isPinned);
pinBtn.title = this.isPinned ? 'Unpin Sidebar' : 'Pin Sidebar';
} }
} }
@@ -255,15 +376,12 @@ export class SidebarManager {
this.pageControls.pageState.activeFolder = path || null; this.pageControls.pageState.activeFolder = path || null;
setStorageItem(`${this.pageType}_activeFolder`, path || null); setStorageItem(`${this.pageType}_activeFolder`, path || null);
// Always show breadcrumb container
// Removed hiding breadcrumb container code
// Reload models with new filter // Reload models with new filter
await this.pageControls.resetAndReload(); await this.pageControls.resetAndReload();
// Auto-close sidebar on mobile after selection // Auto-hide sidebar on mobile after selection
if (window.innerWidth <= 1024) { if (window.innerWidth <= 1024) {
this.closeSidebar(); this.hideSidebar();
} }
} }
@@ -487,22 +605,13 @@ export class SidebarManager {
} }
restoreSidebarState() { restoreSidebarState() {
const isVisible = getStorageItem(`${this.pageType}_sidebarVisible`, true); const isPinned = getStorageItem(`${this.pageType}_sidebarPinned`, false);
const expandedPaths = getStorageItem(`${this.pageType}_expandedNodes`, []); const expandedPaths = getStorageItem(`${this.pageType}_expandedNodes`, []);
this.isVisible = isVisible; this.isPinned = isPinned;
this.expandedNodes = new Set(expandedPaths); this.expandedNodes = new Set(expandedPaths);
const sidebar = document.getElementById('folderSidebar'); this.updatePinButton();
const toggleBtn = document.querySelector('.sidebar-toggle-btn');
if (sidebar) {
sidebar.classList.toggle('collapsed', !this.isVisible);
}
if (toggleBtn) {
toggleBtn.classList.toggle('active', this.isVisible);
}
} }
restoreSelectedFolder() { restoreSelectedFolder() {
@@ -520,7 +629,7 @@ export class SidebarManager {
} }
saveSidebarState() { saveSidebarState() {
setStorageItem(`${this.pageType}_sidebarVisible`, this.isVisible); setStorageItem(`${this.pageType}_sidebarPinned`, this.isPinned);
} }
saveExpandedState() { saveExpandedState() {
@@ -533,18 +642,25 @@ export class SidebarManager {
} }
destroy() { destroy() {
// Clear any pending timeouts
if (this.hoverTimeout) {
clearTimeout(this.hoverTimeout);
}
// Clean up event handlers // Clean up event handlers
const toggleBtn = document.querySelector('.sidebar-toggle-btn'); const pinToggleBtn = document.getElementById('sidebarPinToggle');
const closeBtn = document.getElementById('sidebarToggleClose'); const collapseAllBtn = document.getElementById('sidebarCollapseAll');
const folderTree = document.getElementById('sidebarFolderTree'); const folderTree = document.getElementById('sidebarFolderTree');
const sidebarBreadcrumbNav = document.getElementById('sidebarBreadcrumbNav'); const sidebarBreadcrumbNav = document.getElementById('sidebarBreadcrumbNav');
const sidebarHeader = document.getElementById('sidebarHeader'); const sidebarHeader = document.getElementById('sidebarHeader');
const sidebar = document.getElementById('folderSidebar');
const hoverArea = document.getElementById('sidebarHoverArea');
if (toggleBtn) { if (pinToggleBtn) {
toggleBtn.removeEventListener('click', this.toggleSidebar); pinToggleBtn.removeEventListener('click', this.handlePinToggle);
} }
if (closeBtn) { if (collapseAllBtn) {
closeBtn.removeEventListener('click', this.closeSidebar); collapseAllBtn.removeEventListener('click', this.handleCollapseAll);
} }
if (folderTree) { if (folderTree) {
folderTree.removeEventListener('click', this.handleTreeClick); folderTree.removeEventListener('click', this.handleTreeClick);
@@ -553,7 +669,15 @@ export class SidebarManager {
sidebarBreadcrumbNav.removeEventListener('click', this.handleBreadcrumbClick); sidebarBreadcrumbNav.removeEventListener('click', this.handleBreadcrumbClick);
} }
if (sidebarHeader) { if (sidebarHeader) {
sidebarHeader.removeEventListener('click', () => this.selectFolder('')); sidebarHeader.removeEventListener('click', this.handleSidebarHeaderClick);
}
if (sidebar) {
sidebar.removeEventListener('mouseenter', this.handleMouseEnter);
sidebar.removeEventListener('mouseleave', this.handleMouseLeave);
}
if (hoverArea) {
hoverArea.removeEventListener('mouseenter', this.handleHoverAreaEnter);
hoverArea.removeEventListener('mouseleave', this.handleHoverAreaLeave);
} }
// Remove document click handler // Remove document click handler

View File

@@ -65,12 +65,6 @@
</div> </div>
<div class="controls-right"> <div class="controls-right">
<div class="sidebar-toggle-container">
<button class="sidebar-toggle-btn icon-only" title="Toggle folder sidebar">
<i class="fas fa-folder-tree"></i>
</button>
</div>
<div class="keyboard-nav-hint tooltip"> <div class="keyboard-nav-hint tooltip">
<i class="fas fa-keyboard"></i> <i class="fas fa-keyboard"></i>
<span class="tooltiptext"> <span class="tooltiptext">

View File

@@ -1,10 +1,18 @@
<!-- Hover detection area -->
<div class="sidebar-hover-area" id="sidebarHoverArea"></div>
<!-- Folder Navigation Sidebar --> <!-- Folder Navigation Sidebar -->
<div class="folder-sidebar" id="folderSidebar"> <div class="folder-sidebar" id="folderSidebar">
<div class="sidebar-header" id="sidebarHeader"> <div class="sidebar-header" id="sidebarHeader">
<h3><i class="fas fa-home"></i> <span id="sidebarTitle">Model Root</span></h3> <h3><i class="fas fa-home"></i> <span id="sidebarTitle">Model Root</span></h3>
<button class="sidebar-toggle-close" id="sidebarToggleClose"> <div class="sidebar-header-actions">
<i class="fas fa-times"></i> <button class="sidebar-action-btn" id="sidebarCollapseAll" title="Collapse All Folders">
</button> <i class="fas fa-compress-alt"></i>
</button>
<button class="sidebar-action-btn" id="sidebarPinToggle" title="Pin/Unpin Sidebar">
<i class="fas fa-thumbtack"></i>
</button>
</div>
</div> </div>
<div class="sidebar-content"> <div class="sidebar-content">
<div class="sidebar-tree-container"> <div class="sidebar-tree-container">