diff --git a/apps/website/src/components/docs/DocsSidebar.tsx b/apps/website/src/components/docs/DocsSidebar.tsx index 38845eb88..9be40e2f7 100644 --- a/apps/website/src/components/docs/DocsSidebar.tsx +++ b/apps/website/src/components/docs/DocsSidebar.tsx @@ -3,7 +3,6 @@ import { useState, useRef, useEffect } from 'react'; import Link from 'next/link'; import { usePathname, useRouter } from 'next/navigation'; import { docsConfig, getLibraryConfig, specialDocsPages, type DocsSection, type LibraryId } from '../../lib/docs-config'; -import { Pill } from '../ui/Pill'; import { LibraryMark } from './LibraryMark'; interface Props { @@ -78,6 +77,52 @@ function LibraryDropdown({ activeLibrary }: { activeLibrary: LibraryId }) { ); } +/** Small consistent-stroke glyphs keyed by section id (premium-polish pass). */ +function SectionGlyph({ id }: { id: string }) { + const common = { + width: 15, + height: 15, + viewBox: '0 0 16 16', + fill: 'none', + stroke: 'currentColor', + strokeWidth: 1.5, + strokeLinecap: 'round' as const, + strokeLinejoin: 'round' as const, + 'aria-hidden': true, + }; + switch (id) { + case 'getting-started': + return ( + + ); + case 'guides': + return ( + + ); + case 'concepts': + return ( + + ); + case 'components': + return ( + + ); + case 'a2ui': + return ( + + ); + case 'api': + case 'reference': + return ( + + ); + default: + return ( + + ); + } +} + function SectionGroup({ section, activeLibrary, @@ -97,11 +142,14 @@ function SectionGroup({ onClick={() => setOpen(!open)} className="w-full text-left px-4 py-1.5 flex items-center justify-between docs-sidebar-section-toggle" > - - {section.title} + + + + {section.title} + ▾ @@ -142,8 +190,14 @@ export function DocsSidebar({ activeLibrary, activeSection, activeSlug }: Props) className="w-full text-left px-3 py-2 rounded-lg text-sm flex items-center justify-between docs-sidebar-search-trigger" onClick={() => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true }))} > - Search docs... - ⌘K + + + Search docs... + + ⌘K diff --git a/apps/website/src/components/docs/DocsTOC.tsx b/apps/website/src/components/docs/DocsTOC.tsx index 2b3b883b8..1f3cec13a 100644 --- a/apps/website/src/components/docs/DocsTOC.tsx +++ b/apps/website/src/components/docs/DocsTOC.tsx @@ -6,23 +6,36 @@ export function DocsTOC({ headings }: { headings: DocHeading[] }) { const [activeId, setActiveId] = useState(''); useEffect(() => { - const observer = new IntersectionObserver( - (entries) => { - for (const entry of entries) { - if (entry.isIntersecting) { - setActiveId(entry.target.id); - } - } - }, - { rootMargin: '-80px 0px -80% 0px' }, - ); + // "Current section" = the last heading above the reading line. Unlike an + // IntersectionObserver band, this always yields an active item after a + // jump-scroll or hash navigation, not only when a heading crosses the band. + const els = headings + .map((h) => document.getElementById(h.id)) + .filter((el): el is HTMLElement => el !== null); + if (els.length === 0) return undefined; - for (const heading of headings) { - const el = document.getElementById(heading.id); - if (el) observer.observe(el); - } - - return () => observer.disconnect(); + let frame = 0; + const update = () => { + frame = 0; + const line = window.scrollY + window.innerHeight * 0.25; + let current = ''; + for (const el of els) { + if (el.offsetTop <= line) current = el.id; + else break; + } + setActiveId(current); + }; + const onScroll = () => { + if (!frame) frame = requestAnimationFrame(update); + }; + update(); + window.addEventListener('scroll', onScroll, { passive: true }); + window.addEventListener('resize', onScroll, { passive: true }); + return () => { + if (frame) cancelAnimationFrame(frame); + window.removeEventListener('scroll', onScroll); + window.removeEventListener('resize', onScroll); + }; }, [headings]); if (headings.length === 0) return null; @@ -30,7 +43,7 @@ export function DocsTOC({ headings }: { headings: DocHeading[] }) { return (