Files
the-other-dude/docs/website/script.js
Jason Staack e2813116da fix(docs): stagger bullet throb so only one pulses at a time per list
Each list gets a dynamically generated keyframe where only 1/N of the
cycle is active. Bullets are staggered 0.8s apart so they take turns
pulsing in sequence, looping forever.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 23:08:11 -05:00

291 lines
9.3 KiB
JavaScript

/* TOD Documentation Website — Shared JavaScript */
(function () {
'use strict';
/* -------------------------------------------------- */
/* 1. Scroll Spy (docs page) */
/* -------------------------------------------------- */
function initScrollSpy() {
const sidebar = document.querySelector('.sidebar-nav');
if (!sidebar) return;
const links = Array.from(document.querySelectorAll('.sidebar-link'));
const sections = links
.map(function (link) {
var id = link.getAttribute('data-section');
return id ? document.getElementById(id) : null;
})
.filter(Boolean);
if (!sections.length) return;
var current = null;
var observer = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
var id = entry.target.id;
if (id !== current) {
current = id;
links.forEach(function (l) {
l.classList.toggle(
'sidebar-link--active',
l.getAttribute('data-section') === id
);
});
/* keep active link visible in sidebar */
var active = sidebar.querySelector('.sidebar-link--active');
if (active) {
active.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
}
}
});
},
{ rootMargin: '-80px 0px -60% 0px', threshold: 0 }
);
sections.forEach(function (s) {
observer.observe(s);
});
}
/* -------------------------------------------------- */
/* 2. Docs Search */
/* -------------------------------------------------- */
function initDocsSearch() {
var input = document.getElementById('docs-search-input');
if (!input) return;
var content = document.getElementById('docs-content');
if (!content) return;
var sections = Array.from(content.querySelectorAll('section[id]'));
var links = Array.from(document.querySelectorAll('.sidebar-link'));
input.addEventListener('input', function () {
var q = input.value.trim().toLowerCase();
if (!q) {
sections.forEach(function (s) { s.style.display = ''; });
links.forEach(function (l) { l.style.display = ''; });
return;
}
sections.forEach(function (s) {
var text = s.textContent.toLowerCase();
var match = text.indexOf(q) !== -1;
s.style.display = match ? '' : 'none';
});
links.forEach(function (l) {
var sectionId = l.getAttribute('data-section');
var section = sectionId ? document.getElementById(sectionId) : null;
if (section) {
l.style.display = section.style.display;
}
});
});
}
/* -------------------------------------------------- */
/* 3. Back to Top */
/* -------------------------------------------------- */
function initBackToTop() {
var btn = document.getElementById('back-to-top');
if (!btn) return;
window.addEventListener('scroll', function () {
btn.classList.toggle('back-to-top--visible', window.scrollY > 400);
}, { passive: true });
}
window.scrollToTop = function () {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
/* -------------------------------------------------- */
/* 4. Sidebar Toggle (mobile) */
/* -------------------------------------------------- */
window.toggleSidebar = function () {
var sidebar = document.getElementById('docs-sidebar');
if (!sidebar) return;
sidebar.classList.toggle('docs-sidebar--open');
};
function initSidebarClose() {
var sidebar = document.getElementById('docs-sidebar');
if (!sidebar) return;
/* close on outside click */
document.addEventListener('click', function (e) {
if (
sidebar.classList.contains('docs-sidebar--open') &&
!sidebar.contains(e.target) &&
!e.target.closest('.docs-hamburger')
) {
sidebar.classList.remove('docs-sidebar--open');
}
});
/* close on link click (mobile) */
sidebar.addEventListener('click', function (e) {
if (e.target.closest('.sidebar-link')) {
sidebar.classList.remove('docs-sidebar--open');
}
});
}
/* -------------------------------------------------- */
/* 5. Reveal Animation (landing page) */
/* -------------------------------------------------- */
function initReveal() {
var els = document.querySelectorAll('.reveal');
if (!els.length) return;
var observer = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add('reveal--visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1 }
);
els.forEach(function (el) {
observer.observe(el);
});
}
/* -------------------------------------------------- */
/* 6. Smooth scroll for anchor links */
/* -------------------------------------------------- */
function initSmoothScroll() {
document.addEventListener('click', function (e) {
var link = e.target.closest('a[href^="#"]');
if (!link) return;
var id = link.getAttribute('href').slice(1);
var target = document.getElementById(id);
if (!target) return;
e.preventDefault();
var offset = 80;
var top = target.getBoundingClientRect().top + window.pageYOffset - offset;
window.scrollTo({ top: top, behavior: 'smooth' });
/* update URL without jump */
history.pushState(null, '', '#' + id);
});
}
/* -------------------------------------------------- */
/* 7. Active nav link (landing page) */
/* -------------------------------------------------- */
function initActiveNav() {
var navLinks = document.querySelectorAll('.nav-link[href^="index.html#"]');
if (!navLinks.length) return;
/* only run on landing page */
if (document.body.classList.contains('docs-page')) return;
var sectionIds = [];
navLinks.forEach(function (l) {
var hash = l.getAttribute('href').split('#')[1];
if (hash) sectionIds.push({ id: hash, link: l });
});
if (!sectionIds.length) return;
var observer = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
sectionIds.forEach(function (item) {
item.link.classList.toggle(
'nav-link--active',
item.id === entry.target.id
);
});
}
});
},
{ rootMargin: '-80px 0px -60% 0px', threshold: 0 }
);
sectionIds.forEach(function (item) {
var el = document.getElementById(item.id);
if (el) observer.observe(el);
});
}
/* -------------------------------------------------- */
/* 8. Bullet throb — one at a time per list */
/* -------------------------------------------------- */
function initBulletThrob() {
var lists = document.querySelectorAll('.content-list');
lists.forEach(function (list) {
var items = list.querySelectorAll('li');
var count = items.length;
if (!count) return;
/* each bullet gets 0.8s of the cycle; total = count * 0.8s */
var step = 0.8;
var total = count * step;
/*
* The keyframe runs 0→50%→100% as throb.
* We shrink the active window so only ~1/count of the cycle is the throb.
* Rewrite: 0-X% is the throb, X-100% is idle.
*/
var pct = (1 / count) * 100;
var halfPct = pct / 2;
/* inject a scoped keyframe for this list size */
var name = 'bt-' + count;
if (!document.querySelector('[data-bt="' + name + '"]')) {
var style = document.createElement('style');
style.setAttribute('data-bt', name);
style.textContent =
'@keyframes ' + name + ' {' +
'0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(42,157,143,0); }' +
halfPct.toFixed(1) + '% { transform: scale(1.5); box-shadow: 0 0 8px 2px rgba(42,157,143,0.3); }' +
pct.toFixed(1) + '% { transform: scale(1); box-shadow: 0 0 0 0 rgba(42,157,143,0); }' +
'100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(42,157,143,0); }' +
'}';
document.head.appendChild(style);
}
items.forEach(function (li, i) {
li.style.setProperty('--bt-animation', name + ' ' + total + 's ease-in-out infinite ' + (i * step) + 's');
});
});
/* apply via a single rule */
var rule = document.createElement('style');
rule.textContent = '.content-list li::before { animation: var(--bt-animation, none); }';
document.head.appendChild(rule);
}
/* -------------------------------------------------- */
/* Init on DOMContentLoaded */
/* -------------------------------------------------- */
document.addEventListener('DOMContentLoaded', function () {
initScrollSpy();
initDocsSearch();
initBackToTop();
initSidebarClose();
initReveal();
initSmoothScroll();
initActiveNav();
initBulletThrob();
});
})();