Yacine Jernite
commited on
Commit
·
a08c68d
1
Parent(s):
f07b5d3
sticky_nav_prettier
Browse files- js/main.js +7 -1
- js/utils/stickyNavigation.js +88 -1
- styles.css +39 -2
js/main.js
CHANGED
|
@@ -12,7 +12,13 @@ export function scrollToSection(sectionId) {
|
|
| 12 |
if (element) {
|
| 13 |
const elementRect = element.getBoundingClientRect();
|
| 14 |
const absoluteElementTop = elementRect.top + window.pageYOffset;
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
window.scrollTo({
|
| 18 |
top: absoluteElementTop - offset,
|
|
|
|
| 12 |
if (element) {
|
| 13 |
const elementRect = element.getBoundingClientRect();
|
| 14 |
const absoluteElementTop = elementRect.top + window.pageYOffset;
|
| 15 |
+
|
| 16 |
+
// Calculate offset: header height + sticky nav height + padding
|
| 17 |
+
const headerHeight = parseInt(getComputedStyle(document.documentElement)
|
| 18 |
+
.getPropertyValue('--header-height')) || 100;
|
| 19 |
+
const stickyNav = document.querySelector('.page-navigation.sticky-nav');
|
| 20 |
+
const navHeight = stickyNav ? stickyNav.offsetHeight : 0;
|
| 21 |
+
const offset = headerHeight + navHeight + 16; // 16px padding
|
| 22 |
|
| 23 |
window.scrollTo({
|
| 24 |
top: absoluteElementTop - offset,
|
js/utils/stickyNavigation.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
// stickyNavigation.js - Sticky navigation behavior and hamburger menu
|
| 2 |
-
// Handles: sticky positioning on scroll + collapse/expand toggle
|
| 3 |
|
| 4 |
/**
|
| 5 |
* Initializes sticky navigation behavior for a page
|
|
@@ -36,6 +36,93 @@ export function initializeStickyNavigation(navElementId) {
|
|
| 36 |
});
|
| 37 |
|
| 38 |
handleScroll(); // Initial check
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
}
|
| 40 |
|
| 41 |
/**
|
|
|
|
| 1 |
// stickyNavigation.js - Sticky navigation behavior and hamburger menu
|
| 2 |
+
// Handles: sticky positioning on scroll + collapse/expand toggle + active section tracking
|
| 3 |
|
| 4 |
/**
|
| 5 |
* Initializes sticky navigation behavior for a page
|
|
|
|
| 36 |
});
|
| 37 |
|
| 38 |
handleScroll(); // Initial check
|
| 39 |
+
|
| 40 |
+
// Initialize active section tracking
|
| 41 |
+
initializeActiveSectionTracking();
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/**
|
| 45 |
+
* Track visible sections and update active navigation item
|
| 46 |
+
* Uses Intersection Observer for efficient scroll tracking
|
| 47 |
+
*/
|
| 48 |
+
function initializeActiveSectionTracking() {
|
| 49 |
+
// Get all navigation links
|
| 50 |
+
const navLinks = document.querySelectorAll('.page-navigation a[href^="/#"], .page-navigation a[href*="#"]');
|
| 51 |
+
if (navLinks.length === 0) return;
|
| 52 |
+
|
| 53 |
+
// Extract section IDs from navigation links
|
| 54 |
+
const sectionIds = Array.from(navLinks)
|
| 55 |
+
.map(link => {
|
| 56 |
+
const href = link.getAttribute('href');
|
| 57 |
+
const hashIndex = href.indexOf('#');
|
| 58 |
+
return hashIndex >= 0 ? href.substring(hashIndex + 1) : null;
|
| 59 |
+
})
|
| 60 |
+
.filter(id => id);
|
| 61 |
+
|
| 62 |
+
if (sectionIds.length === 0) return;
|
| 63 |
+
|
| 64 |
+
// Get all section elements
|
| 65 |
+
const sections = sectionIds
|
| 66 |
+
.map(id => document.getElementById(id))
|
| 67 |
+
.filter(section => section);
|
| 68 |
+
|
| 69 |
+
if (sections.length === 0) return;
|
| 70 |
+
|
| 71 |
+
// Track which section is most visible
|
| 72 |
+
const observerOptions = {
|
| 73 |
+
root: null,
|
| 74 |
+
rootMargin: '-20% 0px -60% 0px', // Trigger when section is 20% from top
|
| 75 |
+
threshold: [0, 0.1, 0.2, 0.5, 0.8, 1]
|
| 76 |
+
};
|
| 77 |
+
|
| 78 |
+
let activeSection = null;
|
| 79 |
+
|
| 80 |
+
const observer = new IntersectionObserver((entries) => {
|
| 81 |
+
// Find the most visible section
|
| 82 |
+
let mostVisible = null;
|
| 83 |
+
let maxRatio = 0;
|
| 84 |
+
|
| 85 |
+
entries.forEach(entry => {
|
| 86 |
+
if (entry.isIntersecting && entry.intersectionRatio > maxRatio) {
|
| 87 |
+
maxRatio = entry.intersectionRatio;
|
| 88 |
+
mostVisible = entry.target;
|
| 89 |
+
}
|
| 90 |
+
});
|
| 91 |
+
|
| 92 |
+
// Update active section if changed
|
| 93 |
+
if (mostVisible && mostVisible !== activeSection) {
|
| 94 |
+
activeSection = mostVisible;
|
| 95 |
+
updateActiveNavItem(mostVisible.id);
|
| 96 |
+
}
|
| 97 |
+
}, observerOptions);
|
| 98 |
+
|
| 99 |
+
// Observe all sections
|
| 100 |
+
sections.forEach(section => observer.observe(section));
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
/**
|
| 104 |
+
* Update active navigation item based on current section
|
| 105 |
+
* @param {string} sectionId - ID of the currently visible section
|
| 106 |
+
*/
|
| 107 |
+
function updateActiveNavItem(sectionId) {
|
| 108 |
+
const navLinks = document.querySelectorAll('.page-navigation a');
|
| 109 |
+
|
| 110 |
+
navLinks.forEach(link => {
|
| 111 |
+
const href = link.getAttribute('href');
|
| 112 |
+
const linkSectionId = href.substring(href.indexOf('#') + 1);
|
| 113 |
+
|
| 114 |
+
if (linkSectionId === sectionId) {
|
| 115 |
+
// Add active classes
|
| 116 |
+
link.classList.add('text-blue-600', 'font-semibold');
|
| 117 |
+
link.classList.add('underline', 'decoration-2', 'underline-offset-2');
|
| 118 |
+
link.classList.remove('text-gray-700');
|
| 119 |
+
} else {
|
| 120 |
+
// Remove active classes
|
| 121 |
+
link.classList.remove('text-blue-600', 'font-semibold');
|
| 122 |
+
link.classList.remove('underline', 'decoration-2', 'underline-offset-2');
|
| 123 |
+
link.classList.add('text-gray-700');
|
| 124 |
+
}
|
| 125 |
+
});
|
| 126 |
}
|
| 127 |
|
| 128 |
/**
|
styles.css
CHANGED
|
@@ -12,6 +12,11 @@ html, body {
|
|
| 12 |
width: 100%;
|
| 13 |
}
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
/* Apply fonts to base elements */
|
| 16 |
body {
|
| 17 |
font-family: 'Source Sans Pro', sans-serif;
|
|
@@ -164,6 +169,28 @@ p {
|
|
| 164 |
transition: all 0.2s ease;
|
| 165 |
}
|
| 166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
.page-navigation.sticky-nav {
|
| 168 |
position: fixed;
|
| 169 |
top: var(--header-height);
|
|
@@ -219,7 +246,7 @@ p {
|
|
| 219 |
@media (max-width: 767px) {
|
| 220 |
.page-navigation.sticky-nav {
|
| 221 |
top: var(--header-height);
|
| 222 |
-
padding: 0.25rem 0;
|
| 223 |
}
|
| 224 |
|
| 225 |
.page-navigation.sticky-nav #nav-container {
|
|
@@ -228,7 +255,17 @@ p {
|
|
| 228 |
|
| 229 |
.page-navigation.sticky-nav ~ section:first-of-type,
|
| 230 |
.page-navigation.sticky-nav ~ div:first-of-type {
|
| 231 |
-
margin-top: 0.5rem;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
}
|
| 233 |
}
|
| 234 |
|
|
|
|
| 12 |
width: 100%;
|
| 13 |
}
|
| 14 |
|
| 15 |
+
/* Smooth scrolling for the entire page */
|
| 16 |
+
html {
|
| 17 |
+
scroll-behavior: smooth;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
/* Apply fonts to base elements */
|
| 21 |
body {
|
| 22 |
font-family: 'Source Sans Pro', sans-serif;
|
|
|
|
| 169 |
transition: all 0.2s ease;
|
| 170 |
}
|
| 171 |
|
| 172 |
+
/* Navigation link active state transitions */
|
| 173 |
+
.page-navigation a {
|
| 174 |
+
transition: all 0.3s ease;
|
| 175 |
+
position: relative;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
/* Subtle entrance animation for sticky nav */
|
| 179 |
+
@keyframes slideDown {
|
| 180 |
+
from {
|
| 181 |
+
opacity: 0.8;
|
| 182 |
+
transform: translateY(-10px);
|
| 183 |
+
}
|
| 184 |
+
to {
|
| 185 |
+
opacity: 1;
|
| 186 |
+
transform: translateY(0);
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.page-navigation.sticky-nav #nav-container {
|
| 191 |
+
animation: slideDown 0.3s ease-out;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
.page-navigation.sticky-nav {
|
| 195 |
position: fixed;
|
| 196 |
top: var(--header-height);
|
|
|
|
| 246 |
@media (max-width: 767px) {
|
| 247 |
.page-navigation.sticky-nav {
|
| 248 |
top: var(--header-height);
|
| 249 |
+
padding: 0.25rem 0;
|
| 250 |
}
|
| 251 |
|
| 252 |
.page-navigation.sticky-nav #nav-container {
|
|
|
|
| 255 |
|
| 256 |
.page-navigation.sticky-nav ~ section:first-of-type,
|
| 257 |
.page-navigation.sticky-nav ~ div:first-of-type {
|
| 258 |
+
margin-top: 0.5rem;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
/* Auto-collapse navigation on mobile when sticky for better UX */
|
| 262 |
+
.page-navigation.sticky-nav #nav-items {
|
| 263 |
+
font-size: 0.875rem; /* Slightly smaller text */
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
/* Ensure hamburger is more prominent on mobile */
|
| 267 |
+
.page-navigation.sticky-nav #nav-toggle {
|
| 268 |
+
padding: 0.5rem;
|
| 269 |
}
|
| 270 |
}
|
| 271 |
|