Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/homepage/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/style.css">
<style>
.nav-link-active {
font-weight: 700;
text-decoration: underline;
text-underline-offset: 4px;
text-decoration-thickness: 2px;
color: #62bfe7;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/keycloak-js@25.0.6/dist/keycloak.min.js"></script>
</head>
<body class="text-gray-100 font-sans">
Expand Down
50 changes: 48 additions & 2 deletions apps/homepage/public/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -335,9 +335,55 @@ async function init() {
}
}

function initScrollSpy() {
if (typeof IntersectionObserver === 'undefined') return;

const links = Array.from(document.querySelectorAll('nav a[href^="#"]'));
const sections = [];
for (const link of links) {
const section = document.getElementById(link.getAttribute('href').slice(1));
if (section) sections.push(section);
}
if (!sections.length) return;

let activeId = null;
const setActive = (id) => {
if (id === activeId) return;
activeId = id;
for (const link of links) {
const isActive = link.getAttribute('href') === '#' + id;
link.classList.toggle('nav-link-active', isActive);
if (isActive) link.setAttribute('aria-current', 'page');
else link.removeAttribute('aria-current');
}
};

const visible = new Set();
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) visible.add(entry.target.id);
else visible.delete(entry.target.id);
}
// Prefer the first section (in document order) currently inside the middle band.
const firstVisible = sections.find((s) => visible.has(s.id));
if (firstVisible) {
setActive(firstVisible.id);
return;
}
// Fallback: scrolled past the last section — keep the last nav link active.
const scrollBottom = window.scrollY + window.innerHeight;
if (scrollBottom >= document.documentElement.scrollHeight - 4) {
setActive(sections[sections.length - 1].id);
Comment on lines +374 to +376
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear active nav when no section is in the spy band

The scroll-spy callback only updates state when a section is intersecting or when the page is at the bottom, so when users scroll back into the hero area (above #features) the previously active link remains highlighted even though no tracked section is in the middle band. This produces an incorrect active state after upward scrolling and contradicts the intended “active section in viewport band” behavior; add an else path here to unset the active link when visible is empty and bottom fallback does not apply.

Useful? React with 👍 / 👎.

}
}, { rootMargin: '-40% 0px -40% 0px', threshold: 0 });

for (const section of sections) observer.observe(section);
}

// Run when DOM is ready
const onReady = () => { init(); initScrollSpy(); };
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
document.addEventListener('DOMContentLoaded', onReady);
} else {
init();
onReady();
}
Loading