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:
parent
494ae7dc3b
commit
0f1c7234b8
1
engine/assets/css/bootstrap.min.css.map
Normal file
1
engine/assets/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
1
engine/assets/js/bootstrap.bundle.min.js.map
Normal file
1
engine/assets/js/bootstrap.bundle.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -427,10 +427,16 @@ private function autoLinkPageTitles($content) {
|
|||||||
|
|
||||||
if ($hasChildren) {
|
if ($hasChildren) {
|
||||||
$folderId = 'folder-' . str_replace('/', '-', $item['path']);
|
$folderId = 'folder-' . str_replace('/', '-', $item['path']);
|
||||||
$html .= '<span class="nav-link folder-toggle" data-bs-toggle="collapse" data-bs-target="#' . $folderId . '" aria-expanded="false">';
|
|
||||||
|
// Check if this folder contains the active page
|
||||||
|
$containsActive = $this->folderContainsActivePage($item['children']);
|
||||||
|
$ariaExpanded = $containsActive ? 'true' : 'false';
|
||||||
|
$collapseClass = $containsActive ? 'collapse show' : 'collapse';
|
||||||
|
|
||||||
|
$html .= '<span class="nav-link folder-toggle" data-bs-toggle="collapse" data-bs-target="#' . $folderId . '" aria-expanded="' . $ariaExpanded . '">';
|
||||||
$html .= '<i class="arrow bi bi-chevron-right"></i> ' . htmlspecialchars($item['title']);
|
$html .= '<i class="arrow bi bi-chevron-right"></i> ' . htmlspecialchars($item['title']);
|
||||||
$html .= '</span>';
|
$html .= '</span>';
|
||||||
$html .= '<ul class="nav flex-column ms-2 collapse" id="' . $folderId . '">';
|
$html .= '<ul class="nav flex-column ms-2 ' . $collapseClass . '" id="' . $folderId . '">';
|
||||||
$html .= $this->renderMenu($item['children'], $level + 1);
|
$html .= $this->renderMenu($item['children'], $level + 1);
|
||||||
$html .= '</ul>';
|
$html .= '</ul>';
|
||||||
} else {
|
} else {
|
||||||
@ -449,6 +455,21 @@ private function autoLinkPageTitles($content) {
|
|||||||
}
|
}
|
||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function folderContainsActivePage($children) {
|
||||||
|
foreach ($children as $child) {
|
||||||
|
if ($child['type'] === 'folder') {
|
||||||
|
if (!empty($child['children']) && $this->folderContainsActivePage($child['children'])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isset($_GET['page']) && $_GET['page'] === $child['path']) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$cms = new CodePressCMS($config);
|
$cms = new CodePressCMS($config);
|
||||||
|
|||||||
@ -4,9 +4,9 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{{page_title}} - {{site_title}}</title>
|
<title>{{page_title}} - {{site_title}}</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="../engine/assets/favicon.svg">
|
<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.min.css" rel="stylesheet">
|
||||||
<link href="../engine/assets/css/bootstrap-icons.css" rel="stylesheet">
|
<link href="/engine/assets/css/bootstrap-icons.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -22,12 +22,14 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-wrapper {
|
.main-wrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
min-height: calc(100vh - 70px); /* Minus header height */
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
@ -43,34 +45,72 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
position: relative;
|
position: fixed;
|
||||||
z-index: 1000;
|
top: 70px;
|
||||||
|
left: 0;
|
||||||
|
height: calc(100vh - 140px); /* 70px header + 70px footer */
|
||||||
|
z-index: 999;
|
||||||
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
.sidebar.collapsed {
|
.sidebar.collapsed {
|
||||||
transform: translateX(-250px);
|
transform: translateX(-250px);
|
||||||
}
|
}
|
||||||
.sidebar-toggle {
|
.sidebar-toggle {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
top: 80px;
|
top: 15px;
|
||||||
left: 10px;
|
right: 15px;
|
||||||
z-index: 1001;
|
z-index: 1001;
|
||||||
background-color: #0d6efd;
|
background: none;
|
||||||
color: white;
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 5px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
|
||||||
transition: all 0.3s ease;
|
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 {
|
.sidebar-toggle:hover {
|
||||||
background-color: #0a58ca;
|
background-color: #0a58ca;
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
.sidebar-toggle.shifted {
|
.main-content {
|
||||||
left: 270px;
|
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;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,7 +286,7 @@
|
|||||||
<div class="row align-items-center">
|
<div class="row align-items-center">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="d-flex align-items-center">
|
<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>
|
<h1 class="h3 mb-0">{{site_title}}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -262,10 +302,10 @@
|
|||||||
|
|
||||||
<div class="main-wrapper">
|
<div class="main-wrapper">
|
||||||
<div class="content-wrapper">
|
<div class="content-wrapper">
|
||||||
<button class="sidebar-toggle" id="sidebarToggle">
|
|
||||||
<i class="bi bi-list"></i>
|
|
||||||
</button>
|
|
||||||
<nav class="sidebar" id="sidebar">
|
<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">
|
<div class="pt-3">
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
{{menu}}
|
{{menu}}
|
||||||
@ -274,6 +314,9 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
|
<div class="sidebar-toggle sidebar-toggle-outer" id="sidebarToggleOuter" style="display: none;">
|
||||||
|
<i class="bi bi-list"></i>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{breadcrumb}}
|
{{breadcrumb}}
|
||||||
</div>
|
</div>
|
||||||
@ -287,7 +330,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</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="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
@ -304,74 +347,90 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="../engine/assets/js/bootstrap.bundle.min.js"></script>
|
<script src="/engine/assets/js/bootstrap.bundle.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Sidebar toggle functionality
|
// Sidebar toggle functionality
|
||||||
const sidebarToggle = document.getElementById('sidebarToggle');
|
const sidebarToggleInner = document.getElementById('sidebarToggleInner');
|
||||||
|
const sidebarToggleOuter = document.getElementById('sidebarToggleOuter');
|
||||||
const sidebar = document.getElementById('sidebar');
|
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');
|
sidebar.classList.toggle('collapsed');
|
||||||
mainContent.classList.toggle('shifted');
|
|
||||||
sidebarToggle.classList.toggle('shifted');
|
|
||||||
|
|
||||||
// Change icon
|
// Change icons
|
||||||
const icon = this.querySelector('i');
|
|
||||||
if (sidebar.classList.contains('collapsed')) {
|
if (sidebar.classList.contains('collapsed')) {
|
||||||
icon.classList.remove('bi-list');
|
innerIcon.classList.remove('bi-x');
|
||||||
icon.classList.add('bi-chevron-right');
|
innerIcon.classList.add('bi-list');
|
||||||
|
outerIcon.classList.remove('bi-x');
|
||||||
|
outerIcon.classList.add('bi-list');
|
||||||
} else {
|
} else {
|
||||||
icon.classList.remove('bi-chevron-right');
|
innerIcon.classList.remove('bi-list');
|
||||||
icon.classList.add('bi-list');
|
innerIcon.classList.add('bi-x');
|
||||||
|
outerIcon.classList.remove('bi-list');
|
||||||
|
outerIcon.classList.add('bi-x');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Open folders that contain the current active page
|
sidebarToggleInner.addEventListener('click', toggleSidebar);
|
||||||
const activeLink = document.querySelector('.nav-link.active');
|
sidebarToggleOuter.addEventListener('click', toggleSidebar);
|
||||||
if (activeLink) {
|
|
||||||
let parent = activeLink.closest('.collapse');
|
// Folders are now automatically expanded by PHP if they contain the active page
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close other folders when opening a new one
|
// Close other folders when opening a new one
|
||||||
const folderToggles = document.querySelectorAll('.folder-toggle');
|
const folderToggles = document.querySelectorAll('.folder-toggle');
|
||||||
folderToggles.forEach(toggle => {
|
folderToggles.forEach(toggle => {
|
||||||
toggle.addEventListener('click', function(e) {
|
toggle.addEventListener('click', function(e) {
|
||||||
const targetId = this.getAttribute('data-bs-target');
|
const targetId = this.getAttribute('data-bs-target');
|
||||||
|
const targetCollapse = document.querySelector(targetId);
|
||||||
const isExpanded = this.getAttribute('aria-expanded') === 'true';
|
const isExpanded = this.getAttribute('aria-expanded') === 'true';
|
||||||
|
|
||||||
if (!isExpanded) {
|
if (!isExpanded && targetCollapse) {
|
||||||
// Close all other folders
|
// Close all other folders first
|
||||||
folderToggles.forEach(otherToggle => {
|
folderToggles.forEach(otherToggle => {
|
||||||
if (otherToggle !== this) {
|
if (otherToggle !== this) {
|
||||||
const otherTargetId = otherToggle.getAttribute('data-bs-target');
|
const otherTargetId = otherToggle.getAttribute('data-bs-target');
|
||||||
if (otherTargetId) {
|
if (otherTargetId) {
|
||||||
const otherCollapse = document.querySelector(otherTargetId);
|
const otherCollapse = document.querySelector(otherTargetId);
|
||||||
if (otherCollapse) {
|
if (otherCollapse) {
|
||||||
const bsCollapse = bootstrap.Collapse.getInstance(otherCollapse);
|
otherCollapse.classList.remove('show');
|
||||||
if (bsCollapse) {
|
|
||||||
bsCollapse.hide();
|
|
||||||
} else {
|
|
||||||
new bootstrap.Collapse(otherCollapse, {
|
|
||||||
hide: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
otherToggle.setAttribute('aria-expanded', 'false');
|
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 = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
1
public/engine
Symbolic link
1
public/engine
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../engine
|
||||||
@ -438,7 +438,7 @@ class CodePressCMS {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Block direct access to content files
|
// Block direct access to content files
|
||||||
$requestUri = $_SERVER['REQUEST_URI'];
|
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
|
||||||
if (strpos($requestUri, '/content/') !== false) {
|
if (strpos($requestUri, '/content/') !== false) {
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
echo '<h1>403 - Forbidden</h1><p>Direct access to content files is not allowed.</p>';
|
echo '<h1>403 - Forbidden</h1><p>Direct access to content files is not allowed.</p>';
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
// Router file for PHP development server to handle security
|
// Router file for PHP development server to handle security and static files
|
||||||
|
|
||||||
$requestUri = $_SERVER['REQUEST_URI'];
|
$requestUri = $_SERVER['REQUEST_URI'];
|
||||||
$parsedUrl = parse_url($requestUri);
|
$parsedUrl = parse_url($requestUri);
|
||||||
@ -23,9 +23,30 @@ foreach ($sensitiveFiles as $file) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Serve static files from engine/assets
|
// Serve static files from engine/assets
|
||||||
if (strpos($path, '/engine/') === 0 && file_exists(__DIR__ . $path)) {
|
if (strpos($path, '/engine/') === 0) {
|
||||||
return false; // Let PHP server serve the file
|
$filePath = __DIR__ . $path;
|
||||||
|
if (file_exists($filePath)) {
|
||||||
|
// Set appropriate content type
|
||||||
|
$extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
||||||
|
$mimeTypes = [
|
||||||
|
'css' => 'text/css',
|
||||||
|
'js' => 'application/javascript',
|
||||||
|
'svg' => 'image/svg+xml',
|
||||||
|
'woff' => 'font/woff',
|
||||||
|
'woff2' => 'font/woff2',
|
||||||
|
'ttf' => 'font/ttf'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isset($mimeTypes[$extension])) {
|
||||||
|
header('Content-Type: ' . $mimeTypes[$extension]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve the file
|
||||||
|
readfile($filePath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Route all other requests to index.php
|
// Route all other requests to index.php
|
||||||
return false; // Let PHP server handle routing to index.php
|
include __DIR__ . '/index.php';
|
||||||
|
return true;
|
||||||
14
server.log
14
server.log
@ -1,13 +1 @@
|
|||||||
[Wed Nov 19 16:58:13 2025] PHP 8.4.14 Development Server (http://localhost:8080) started
|
[Wed Nov 19 17:58:28 2025] Failed to listen on localhost:8080 (reason: Address already in use)
|
||||||
[Wed Nov 19 16:58:22 2025] [::1]:43444 Accepted
|
|
||||||
[Wed Nov 19 16:58:22 2025] [::1]:43444 [200]: GET /
|
|
||||||
[Wed Nov 19 16:58:22 2025] [::1]:43444 Closing
|
|
||||||
[Wed Nov 19 16:58:26 2025] [::1]:59122 Accepted
|
|
||||||
[Wed Nov 19 16:58:26 2025] [::1]:59122 [200]: GET /content/home.md
|
|
||||||
[Wed Nov 19 16:58:26 2025] [::1]:59122 Closing
|
|
||||||
[Wed Nov 19 16:58:30 2025] [::1]:59138 Accepted
|
|
||||||
[Wed Nov 19 16:58:30 2025] [::1]:59138 [200]: GET /content/home.md
|
|
||||||
[Wed Nov 19 16:58:30 2025] [::1]:59138 Closing
|
|
||||||
[Wed Nov 19 16:59:06 2025] [::1]:38150 Accepted
|
|
||||||
[Wed Nov 19 16:59:06 2025] [::1]:38150 [200]: GET /content/home.md
|
|
||||||
[Wed Nov 19 16:59:06 2025] [::1]:38150 Closing
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user