Skip to main content
Glama
toc.tsx5.06 kB
"use client"; import { useEffect, useRef, useState } from "react"; type TocSection = { name: string; id: string; active: boolean }; export default function TableOfContents() { const [sections, setSections] = useState<TocSection[]>([ { name: "Getting Started", id: "getting-started", active: true }, // { name: "Integration Guides", id: "integration-guides", active: false }, { name: "Available Skills", id: "skills", active: false }, // { name: "Available Prompts", id: "prompts", active: false }, // { name: "Available Resources", id: "resources", active: false }, { name: "More Information", id: "more-information", active: false }, ]); // live set of elements currently intersecting const inViewRef = useRef<Set<HTMLElement>>(new Set()); const rafRef = useRef<number | null>(null); const observerRef = useRef<IntersectionObserver | null>(null); const currentActiveIdRef = useRef<string | null>(null); useEffect(() => { const biasPx = 6; // tiny bias so a section "counts" as soon as it peeks in const computeAndSetActive = () => { if (inViewRef.current.size === 0) return; let bottomMost: HTMLElement | null = null; let maxTop = Number.NEGATIVE_INFINITY; const vh = window.innerHeight; // biome-ignore lint/complexity/noForEach: <explanation: vibes> inViewRef.current.forEach((el) => { const r = el.getBoundingClientRect(); // truly visible in viewport (with a small bias) const visible = r.bottom > biasPx && r.top < vh - biasPx; if (!visible) return; // pick the one closest to the bottom of the viewport if (r.top > maxTop) { maxTop = r.top; bottomMost = el as HTMLElement; } }); if (!bottomMost) return; const id = (bottomMost as HTMLElement).id; if (id === currentActiveIdRef.current) return; // skip redundant state updates currentActiveIdRef.current = id; setSections((prev) => prev.map((s) => s.id === id ? { ...s, active: true } : { ...s, active: false }, ), ); }; const cb: IntersectionObserverCallback = (entries) => { for (const entry of entries) { const el = entry.target as HTMLElement; if (entry.isIntersecting) { inViewRef.current.add(el); } else { inViewRef.current.delete(el); } } if (rafRef.current) cancelAnimationFrame(rafRef.current); rafRef.current = requestAnimationFrame(computeAndSetActive); }; observerRef.current = new IntersectionObserver(cb, { root: null, rootMargin: "-120px 0px 0px 0px", threshold: 0, }); const els = Array.from( document.querySelectorAll<HTMLElement>("section[id]"), ); // biome-ignore lint/complexity/noForEach: <explanation: vibes> els.forEach((el) => observerRef.current!.observe(el)); // initial calculation on load/refresh requestAnimationFrame(() => { const vh = window.innerHeight; // biome-ignore lint/complexity/noForEach: <explanation: vibes> els.forEach((el) => { const r = el.getBoundingClientRect(); const visible = r.bottom > biasPx && r.top < vh - biasPx; if (visible) inViewRef.current.add(el); }); computeAndSetActive(); }); return () => { if (observerRef.current) { // biome-ignore lint/complexity/noForEach: <explanation: vibes> els.forEach((el) => observerRef.current!.unobserve(el)); observerRef.current.disconnect(); } observerRef.current = null; if (rafRef.current) cancelAnimationFrame(rafRef.current); inViewRef.current.clear(); }; }, []); // run once return ( <div className="group pointer-events-none sticky top-20 px-12 text-white/60"> <div className="pointer-events-auto flex flex-col py-2"> <b className="-ml-5 mb-2 font-mono text-xs text-white"> [table of contents] </b> {sections.map((section) => ( <a key={section.id} href={`#${section.id}`} onClick={(e) => { e.preventDefault(); document .getElementById(section.id) ?.scrollIntoView({ behavior: "smooth", block: "start" }); // preserve current query string, only change the hash const url = new URL(window.location.href); url.hash = section.id; window.history.pushState( window.history.state, "", url.toString(), ); }} className={`-ml-[calc(1rem+1px)] border-l py-1.5 pl-3 duration-75 max-xl:lg:opacity-20 max-xl:lg:group-hover:opacity-100 ${ section.active ? "border-violet-300 text-violet-300" : "border-neutral-400/30 hover:border-white hover:text-white" }`} > {section.name} </a> ))} </div> </div> ); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/getsentry/sentry-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server