1// Instant anchor scroll - no smooth scrolling for better UX on long pages.
2// `behavior: 'instant'` explicitly overrides any CSS `scroll-behavior: smooth`
3// from a stylesheet we don't own; `behavior: 'auto'` would defer to CSS.
4export function initAnchorScroll() {
5 document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
6 anchor.addEventListener("click", (e) => {
7 e.preventDefault();
8 const target = document.querySelector(anchor.getAttribute("href"));
9 if (target) {
10 const offset = 40;
11 const targetPosition = target.getBoundingClientRect().top + window.scrollY - offset;
12 window.scrollTo({ top: targetPosition, behavior: 'instant' });
13 }
14 });
15 });
16}
17
18export function initHashTracking() {
19 const sections = document.querySelectorAll('section[id]');
20 if (!sections.length) return;
21
22 let currentHash = window.location.hash.slice(1) || '';
23 let ticking = false;
24
25 function updateHash() {
26 // Don't override command deep links while user is in the commands section
27 if (currentHash.startsWith('cmd-')) {
28 const cmdEl = document.getElementById(currentHash);
29 if (cmdEl) {
30 const rect = cmdEl.getBoundingClientRect();
31 // Only clear the cmd hash if user scrolled well away from commands section
32 if (rect.top > window.innerHeight * 2 || rect.bottom < -window.innerHeight) {
33 currentHash = '';
34 } else {
35 ticking = false;
36 return;
37 }
38 }
39 }
40
41 const scrollY = window.scrollY;
42 const viewportHeight = window.innerHeight;
43 const triggerPoint = scrollY + viewportHeight * 0.3;
44
45 let activeSection = '';
46
47 sections.forEach(section => {
48 const rect = section.getBoundingClientRect();
49 const sectionTop = scrollY + rect.top;
50 const sectionBottom = sectionTop + rect.height;
51
52 if (triggerPoint >= sectionTop && triggerPoint < sectionBottom) {
53 activeSection = section.id;
54 }
55 });
56
57 // Don't set #hero — it's the default state, no hash needed
58 if (activeSection === 'hero') activeSection = '';
59
60 if (activeSection !== currentHash) {
61 currentHash = activeSection;
62 if (activeSection) {
63 history.replaceState(null, '', `#${activeSection}`);
64 } else {
65 history.replaceState(null, '', window.location.pathname);
66 }
67 }
68
69 ticking = false;
70 }
71
72 window.addEventListener('scroll', () => {
73 if (!ticking) {
74 requestAnimationFrame(updateHash);
75 ticking = true;
76 }
77 }, { passive: true });
78
79 // Handle initial hash on page load — instant jump, retried on
80 // fonts.ready and window `load`. A fixed setTimeout is unreliable
81 // because async-loaded display fonts reflow the page by hundreds of
82 // pixels when they swap in; computing target position before that
83 // lands the user several sections above the right spot.
84 if (window.location.hash) {
85 const hash = window.location.hash.slice(1);
86 const target = document.getElementById(hash);
87 if (target) {
88 currentHash = hash;
89 let clicked = false;
90 const jump = () => {
91 const offset = 40;
92 const targetPosition = target.getBoundingClientRect().top + window.scrollY - offset;
93 window.scrollTo({ top: targetPosition, behavior: 'instant' });
94 if (!clicked && hash.startsWith('cmd-') && target.classList.contains('manual-entry')) {
95 target.click();
96 clicked = true;
97 }
98 };
99 jump();
100 if (document.fonts?.ready) document.fonts.ready.then(jump).catch(() => {});
101 window.addEventListener('load', jump, { once: true });
102 }
103 } else {
104 // No hash — don't set one on initial load
105 }
106}
107