import { LitElement, html, css } from 'lit'; import { customElement } from 'lit/decorators.js'; import type { TocEntry } from '../extract-headings.js'; /** * * Renders the table of contents as anchor links. * Active section highlighting is handled by CSS :target pseudo-class. */ @customElement('starlight-toc') export class StarlightToc extends LitElement { static override properties = { entries: { type: Array }, }; static override styles = css` :host { display: block; } nav { position: sticky; top: calc(var(--sl-nav-height, 3.5rem) + 1rem); max-height: calc(100vh - var(--sl-nav-height, 3.5rem) - 2rem); overflow-y: auto; padding: 0 0.5rem; } h2 { font-size: var(--sl-text-xs, 0.75rem); font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: var(--sl-color-gray-4, #757575); margin: 0 0 0.75rem; padding: 0; border: none; } ul { list-style: none; padding: 0; margin: 0; } li { margin: 0; } a { display: block; padding: 0.2rem 0; font-size: var(--sl-text-sm, 0.875rem); color: var(--sl-color-gray-4, #757575); text-decoration: none; transition: color 0.15s; border-left: 2px solid transparent; } a:hover { color: var(--sl-color-text, #23262f); } /* Depth indentation */ .depth-2 a { padding-left: 0.75rem; } .depth-3 a { padding-left: 1.5rem; } .depth-4 a { padding-left: 2.25rem; } /* Active via [aria-current] set by the click handler */ li a[aria-current='true'] { color: var(--sl-color-accent, #7c3aed); border-left-color: var(--sl-color-accent, #7c3aed); } `; entries: TocEntry[] = []; /** * Click handler for TOC anchor links. * * Heading elements rendered via unsafeHTML live inside shadow roots that * native fragment navigation and document.getElementById() cannot reach. * Instead of relying on the browser's default hash navigation (which also * fires popstate and re-renders the page component with no data), we: * 1. Prevent the default navigation. * 2. Walk the shadow tree to find the target element. * 3. Scroll to it smoothly. * 4. Update the URL hash via pushState (does not fire popstate). */ private _handleClick(e: MouseEvent, slug: string) { e.preventDefault(); const target = this._findDeep(document, slug); if (target) target.scrollIntoView({ behavior: 'smooth' }); history.pushState(null, '', `#${slug}`); } /** Recursively searches shadow roots for an element matching the CSS id selector. */ private _findDeep(root: Document | ShadowRoot | Element, id: string): Element | null { const sel = `#${CSS.escape(id)}`; const direct = root.querySelector(sel); if (direct) return direct; for (const el of root.querySelectorAll('*')) { if (el.shadowRoot) { const found = this._findDeep(el.shadowRoot, id); if (found) return found; } } return null; } override render() { if (!this.entries.length) return html``; const currentHash = typeof location !== 'undefined' ? location.hash : ''; return html` `; } } export default StarlightToc;