Files
pbakaus_impeccable/site/scripts/utils/scroll.js
T
Paul BakausandCursor 7648af0095 Apply neo-kinpaku design system and improve live picker UX
Restyle the live picker to match the site kinpaku kit, persist pick mode
in localStorage, fix DESIGN.md color swatches in the parser, and land the
neo-kinpaku site refresh with new tokens, assets, palette script, and
detector rules.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 10:24:25 -07:00

119 lines
3.7 KiB
JavaScript

// Instant anchor scroll - no smooth scrolling for better UX on long pages.
// `behavior: 'instant'` explicitly overrides any CSS `scroll-behavior: smooth`
// from a stylesheet we don't own; `behavior: 'auto'` would defer to CSS.
export function initAnchorScroll() {
const getAnchorOffset = () => {
const header = document.querySelector('[data-site-header]');
return (header?.getBoundingClientRect().height || 0) + 28;
};
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
anchor.addEventListener("click", (e) => {
e.preventDefault();
const target = document.querySelector(anchor.getAttribute("href"));
if (target) {
const offset = getAnchorOffset();
const targetPosition = target.getBoundingClientRect().top + window.scrollY - offset;
window.scrollTo({ top: targetPosition, behavior: 'instant' });
}
});
});
return getAnchorOffset;
}
export function initHashTracking() {
const sections = document.querySelectorAll('section[id]');
if (!sections.length) return;
const getAnchorOffset = () => {
const header = document.querySelector('[data-site-header]');
return (header?.getBoundingClientRect().height || 0) + 28;
};
let currentHash = window.location.hash.slice(1) || '';
let ticking = false;
function updateHash() {
// Don't override command deep links while user is in the commands section
if (currentHash.startsWith('cmd-')) {
const cmdEl = document.getElementById(currentHash);
if (cmdEl) {
const rect = cmdEl.getBoundingClientRect();
// Only clear the cmd hash if user scrolled well away from commands section
if (rect.top > window.innerHeight * 2 || rect.bottom < -window.innerHeight) {
currentHash = '';
} else {
ticking = false;
return;
}
}
}
const scrollY = window.scrollY;
const viewportHeight = window.innerHeight;
const triggerPoint = scrollY + viewportHeight * 0.3;
let activeSection = '';
sections.forEach(section => {
const rect = section.getBoundingClientRect();
const sectionTop = scrollY + rect.top;
const sectionBottom = sectionTop + rect.height;
if (triggerPoint >= sectionTop && triggerPoint < sectionBottom) {
activeSection = section.id;
}
});
// Don't set #hero — it's the default state, no hash needed
if (activeSection === 'hero') activeSection = '';
if (activeSection !== currentHash) {
currentHash = activeSection;
if (activeSection) {
history.replaceState(null, '', `#${activeSection}`);
} else {
history.replaceState(null, '', window.location.pathname);
}
}
ticking = false;
}
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(updateHash);
ticking = true;
}
}, { passive: true });
// Handle initial hash on page load — instant jump, retried on
// fonts.ready and window `load`. A fixed setTimeout is unreliable
// because async-loaded display fonts reflow the page by hundreds of
// pixels when they swap in; computing target position before that
// lands the user several sections above the right spot.
if (window.location.hash) {
const hash = window.location.hash.slice(1);
const target = document.getElementById(hash);
if (target) {
currentHash = hash;
let clicked = false;
const jump = () => {
const offset = getAnchorOffset();
const targetPosition = target.getBoundingClientRect().top + window.scrollY - offset;
window.scrollTo({ top: targetPosition, behavior: 'instant' });
if (!clicked && hash.startsWith('cmd-') && target.classList.contains('manual-entry')) {
target.click();
clicked = true;
}
};
jump();
if (document.fonts?.ready) document.fonts.ready.then(jump).catch(() => {});
window.addEventListener('load', jump, { once: true });
}
} else {
// No hash — don't set one on initial load
}
}