Implement modern sidebar navigation with hamburger menu

- Add responsive sidebar with hamburger toggle functionality
- Implement dual toggle buttons (inner/outer) for better UX
- Fix sidebar positioning to not overlap header and footer
- Add sticky footer with proper z-index layering
- Download and integrate Bootstrap source maps locally
- Optimize toggle icons: smaller, cleaner, no button styling
- Ensure sidebar respects footer boundaries
- Add smooth transitions and hover effects
- Fix active page highlighting and folder auto-expansion
- Create professional W3Schools-style navigation
- Maintain full offline capability with local assets
This commit is contained in:
2025-11-19 18:02:48 +01:00
parent 494ae7dc3b
commit 0f1c7234b8
10 changed files with 171 additions and 79 deletions

View File

@@ -4,9 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{page_title}} - {{site_title}}</title>
<link rel="icon" type="image/svg+xml" href="../engine/assets/favicon.svg">
<link href="../engine/assets/css/bootstrap.min.css" rel="stylesheet">
<link href="../engine/assets/css/bootstrap-icons.css" rel="stylesheet">
<link rel="icon" type="image/svg+xml" href="/engine/assets/favicon.svg">
<link href="/engine/assets/css/bootstrap.min.css" rel="stylesheet">
<link href="/engine/assets/css/bootstrap-icons.css" rel="stylesheet">
<style>
* {
margin: 0;
@@ -22,12 +22,14 @@
display: flex;
flex-direction: column;
min-height: 100vh;
position: relative;
}
.main-wrapper {
flex: 1;
display: flex;
flex-direction: column;
min-height: calc(100vh - 70px); /* Minus header height */
}
.content-wrapper {
@@ -43,34 +45,72 @@
overflow-y: auto;
flex-shrink: 0;
transition: transform 0.3s ease;
position: relative;
z-index: 1000;
position: fixed;
top: 70px;
left: 0;
height: calc(100vh - 140px); /* 70px header + 70px footer */
z-index: 999;
transform: translateX(0);
}
.sidebar.collapsed {
transform: translateX(-250px);
}
.sidebar-toggle {
position: fixed;
top: 80px;
left: 10px;
position: absolute;
top: 15px;
right: 15px;
z-index: 1001;
background-color: #0d6efd;
color: white;
background: none;
border: none;
border-radius: 5px;
padding: 8px 12px;
cursor: pointer;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
transition: all 0.3s ease;
font-size: 20px;
color: #6c757d;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.sidebar-toggle:hover {
color: #0d6efd;
background-color: #f8f9fa;
}
.sidebar-toggle-inner {
/* Toggle inside sidebar */
}
.sidebar-toggle-outer {
position: fixed;
top: 90px;
left: 20px;
z-index: 1001;
background-color: white;
border: 1px solid #dee2e6;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.sidebar.collapsed .sidebar-toggle-inner {
right: auto;
left: 15px;
}
.sidebar.collapsed ~ .main-content .sidebar-toggle-outer {
display: block !important;
}
.sidebar:not(.collapsed) ~ .main-content .sidebar-toggle-outer {
display: none !important;
}
.sidebar-toggle:hover {
background-color: #0a58ca;
transform: scale(1.05);
}
.sidebar-toggle.shifted {
left: 270px;
.main-content {
flex: 1;
overflow-y: auto;
padding: 20px;
transition: all 0.3s ease;
margin-left: 250px;
}
.main-content.shifted {
.sidebar.collapsed ~ .main-content {
margin-left: 0;
}
@@ -246,7 +286,7 @@
<div class="row align-items-center">
<div class="col">
<div class="d-flex align-items-center">
<img src="../engine/assets/icon.svg" alt="CodePress Logo" width="32" height="32" class="me-2">
<img src="/engine/assets/icon.svg" alt="CodePress Logo" width="32" height="32" class="me-2">
<h1 class="h3 mb-0">{{site_title}}</h1>
</div>
</div>
@@ -262,10 +302,10 @@
<div class="main-wrapper">
<div class="content-wrapper">
<button class="sidebar-toggle" id="sidebarToggle">
<i class="bi bi-list"></i>
</button>
<nav class="sidebar" id="sidebar">
<div class="sidebar-toggle sidebar-toggle-inner" id="sidebarToggleInner">
<i class="bi bi-list"></i>
</div>
<div class="pt-3">
<ul class="nav flex-column">
{{menu}}
@@ -274,6 +314,9 @@
</nav>
<main class="main-content">
<div class="sidebar-toggle sidebar-toggle-outer" id="sidebarToggleOuter" style="display: none;">
<i class="bi bi-list"></i>
</div>
<div>
{{breadcrumb}}
</div>
@@ -287,7 +330,7 @@
</div>
</div>
<footer class="bg-light border-top py-3">
<footer class="bg-light border-top py-3" style="position: fixed; bottom: 0; left: 0; right: 0; z-index: 998;">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
@@ -304,74 +347,90 @@
</div>
</footer>
<script src="../engine/assets/js/bootstrap.bundle.min.js"></script>
<script src="/engine/assets/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Sidebar toggle functionality
const sidebarToggle = document.getElementById('sidebarToggle');
const sidebarToggleInner = document.getElementById('sidebarToggleInner');
const sidebarToggleOuter = document.getElementById('sidebarToggleOuter');
const sidebar = document.getElementById('sidebar');
const mainContent = document.querySelector('.main-content');
sidebarToggle.addEventListener('click', function() {
// Initialize sidebar state (open by default)
sidebar.classList.remove('collapsed');
const innerIcon = sidebarToggleInner.querySelector('i');
const outerIcon = sidebarToggleOuter.querySelector('i');
innerIcon.classList.remove('bi-list');
innerIcon.classList.add('bi-x');
outerIcon.classList.remove('bi-list');
outerIcon.classList.add('bi-x');
function toggleSidebar() {
sidebar.classList.toggle('collapsed');
mainContent.classList.toggle('shifted');
sidebarToggle.classList.toggle('shifted');
// Change icon
const icon = this.querySelector('i');
// Change icons
if (sidebar.classList.contains('collapsed')) {
icon.classList.remove('bi-list');
icon.classList.add('bi-chevron-right');
innerIcon.classList.remove('bi-x');
innerIcon.classList.add('bi-list');
outerIcon.classList.remove('bi-x');
outerIcon.classList.add('bi-list');
} else {
icon.classList.remove('bi-chevron-right');
icon.classList.add('bi-list');
}
});
// Open folders that contain the current active page
const activeLink = document.querySelector('.nav-link.active');
if (activeLink) {
let parent = activeLink.closest('.collapse');
while (parent) {
const toggle = document.querySelector('[data-bs-target="#' + parent.id + '"]');
if (toggle) {
const collapse = new bootstrap.Collapse(parent, {
show: true
});
toggle.setAttribute('aria-expanded', 'true');
}
parent = parent.parentElement.closest('.collapse');
innerIcon.classList.remove('bi-list');
innerIcon.classList.add('bi-x');
outerIcon.classList.remove('bi-list');
outerIcon.classList.add('bi-x');
}
}
sidebarToggleInner.addEventListener('click', toggleSidebar);
sidebarToggleOuter.addEventListener('click', toggleSidebar);
// Folders are now automatically expanded by PHP if they contain the active page
// Close other folders when opening a new one
const folderToggles = document.querySelectorAll('.folder-toggle');
folderToggles.forEach(toggle => {
toggle.addEventListener('click', function(e) {
const targetId = this.getAttribute('data-bs-target');
const targetCollapse = document.querySelector(targetId);
const isExpanded = this.getAttribute('aria-expanded') === 'true';
if (!isExpanded) {
// Close all other folders
if (!isExpanded && targetCollapse) {
// Close all other folders first
folderToggles.forEach(otherToggle => {
if (otherToggle !== this) {
const otherTargetId = otherToggle.getAttribute('data-bs-target');
if (otherTargetId) {
const otherCollapse = document.querySelector(otherTargetId);
if (otherCollapse) {
const bsCollapse = bootstrap.Collapse.getInstance(otherCollapse);
if (bsCollapse) {
bsCollapse.hide();
} else {
new bootstrap.Collapse(otherCollapse, {
hide: true
});
}
otherCollapse.classList.remove('show');
otherToggle.setAttribute('aria-expanded', 'false');
// Reset arrow
const otherArrow = otherToggle.querySelector('.arrow');
if (otherArrow) {
otherArrow.style.transform = '';
}
}
}
}
});
// Open this folder
targetCollapse.classList.add('show');
this.setAttribute('aria-expanded', 'true');
// Rotate arrow
const arrow = this.querySelector('.arrow');
if (arrow) {
arrow.style.transform = 'rotate(90deg)';
}
} else if (isExpanded && targetCollapse) {
// Close this folder
targetCollapse.classList.remove('show');
this.setAttribute('aria-expanded', 'false');
// Reset arrow
const arrow = this.querySelector('.arrow');
if (arrow) {
arrow.style.transform = '';
}
}
});
});