feat: refine and finalize redesign

This commit is contained in:
2026-01-10 19:33:24 +00:00
parent be51ec4831
commit fb95f72e03
77 changed files with 12571 additions and 5019 deletions

View File

@@ -0,0 +1,64 @@
export interface ActiveSectionTrackerOptions {
linkSelector: string;
sectionIdAttr?: string;
headerOffset?: number;
defaultToFirst?: boolean;
}
export function initActiveSectionTracker(
options: ActiveSectionTrackerOptions,
): void {
const {
linkSelector,
sectionIdAttr = "data-section-id",
headerOffset = 100,
defaultToFirst = true,
} = options;
const links = document.querySelectorAll(linkSelector);
const sections: { id: string; element: Element }[] = [];
// Collect unique section IDs
const seenIds = new Set<string>();
links.forEach((link) => {
const id = link.getAttribute(sectionIdAttr);
if (id && !seenIds.has(id)) {
seenIds.add(id);
const section = document.getElementById(id);
if (section) {
sections.push({ id, element: section });
}
}
});
function updateActiveSection(): void {
let activeId: string | null = defaultToFirst ? sections[0]?.id : null;
for (const { id, element } of sections) {
const rect = element.getBoundingClientRect();
if (rect.top <= headerOffset) {
activeId = id;
}
}
links.forEach((link) => {
const linkId = link.getAttribute(sectionIdAttr);
link.classList.toggle("active", linkId === activeId);
});
}
// Update on scroll with throttling
let ticking = false;
window.addEventListener("scroll", () => {
if (!ticking) {
requestAnimationFrame(() => {
updateActiveSection();
ticking = false;
});
ticking = true;
}
});
// Initial update
updateActiveSection();
}

View File

@@ -0,0 +1,75 @@
/**
* Highlights clause elements when navigating to them via anchor links.
* Works on both initial page load with hash and when clicking anchor links.
*/
const HIGHLIGHT_DURATION = 2000;
const HIGHLIGHT_CLASS = "clause-highlight";
/**
* Highlight a clause element briefly
*/
function highlightClause(element: Element): void {
// Remove any existing highlight
element.classList.remove(HIGHLIGHT_CLASS);
// Force reflow to restart animation if needed
void (element as HTMLElement).offsetWidth;
// Add highlight class
element.classList.add(HIGHLIGHT_CLASS);
// Remove after animation completes
setTimeout(() => {
element.classList.remove(HIGHLIGHT_CLASS);
}, HIGHLIGHT_DURATION);
}
/**
* Handle hash change and highlight target clause
*/
function handleHashChange(): void {
const hash = window.location.hash;
if (!hash || !hash.startsWith("#clause-")) return;
const targetId = hash.slice(1);
const element = document.getElementById(targetId);
if (element) {
// Small delay to let scroll complete
setTimeout(() => highlightClause(element), 100);
}
}
/**
* Initialize clause highlight behavior
*/
export function initClauseHighlight(): void {
// Handle clicks on clause links
document.addEventListener("click", (e) => {
const link = (e.target as Element).closest('a[href^="#clause-"]');
if (!link) return;
const href = link.getAttribute("href");
if (!href) return;
const targetId = href.slice(1);
const element = document.getElementById(targetId);
if (element) {
// Small delay to let scroll complete
setTimeout(() => highlightClause(element), 100);
}
});
// Handle hash changes (back/forward navigation)
window.addEventListener("hashchange", handleHashChange);
// Handle initial page load with hash
if (window.location.hash?.startsWith("#clause-")) {
// Wait for page to be fully ready
if (document.readyState === "complete") {
handleHashChange();
} else {
window.addEventListener("load", handleHashChange);
}
}
}