spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1'use client';2import { useEffect, useState } from 'react';3import { t } from '@/i18n';4import { cn } from '@/lib/cn';56/** Sticky table of contents (desktop) with the current section highlighted via IntersectionObserver. */7export function Toc({ items }: { items: Array<{ id: string; label: string }> }) {8 const [active, setActive] = useState<string | null>(items[0]?.id ?? null);9 useEffect(() => {10 if (typeof IntersectionObserver === 'undefined') return;11 const els = items.map((i) => document.getElementById(i.id)).filter((e): e is HTMLElement => !!e);12 const io = new IntersectionObserver(13 (entries) => {14 const vis = entries.filter((e) => e.isIntersecting).sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);15 if (vis[0]) setActive(vis[0].target.id);16 },17 { rootMargin: '-96px 0px -60% 0px', threshold: 0 },18 );19 els.forEach((e) => io.observe(e));20 return () => io.disconnect();21 }, [items]);22 return (23 <nav aria-label={t('method.toc')} className="lg:sticky lg:top-20">24 <div className="eyebrow mb-1.5 hidden lg:block">{t('method.toc')}</div>25 <ol className="scrollbar-none -mx-4 flex gap-1 overflow-x-auto px-4 lg:mx-0 lg:flex-col lg:gap-0 lg:overflow-visible lg:px-0">26 {items.map((i) => (27 <li key={i.id} className="shrink-0">28 <a href={`#${i.id}`} aria-current={active === i.id ? 'location' : undefined} className={cn('inline-flex h-11 items-center whitespace-nowrap rounded-sm px-3 text-sm lg:h-8 lg:px-2 lg:text-xs', active === i.id ? 'bg-ink text-paper lg:bg-transparent lg:font-semibold lg:text-ink' : 'text-ink-2 hover:bg-surface-2 hover:text-ink lg:hover:bg-transparent')}>29 {i.label}30 </a>31 </li>32 ))}33 </ol>34 </nav>35 );36}37