import { Elena, html, unsafeHTML } from '@elenajs/core'; import type { NavItem } from './starlight-header.js'; import type { SidebarGroup } from './starlight-sidebar.js'; import type { TocEntry } from '../extract-headings.js'; // Side-effect imports — registers child custom elements import './starlight-header.js'; import './starlight-sidebar.js'; import './starlight-toc.js'; import './litro-footer.js'; /** * *
...page content...
*
* * Central layout component — three-column grid: sidebar | content | TOC. * Owns the header, sidebar drawer toggle, and backdrop. * Light DOM — children render in place, no slots needed. * * Nav toggle is handled imperatively (class toggles + DOM insert/remove) * to avoid Elena re-rendering the entire tree and destroying child * component state/focus. */ export class StarlightPage extends Elena(HTMLElement) { static tagName = 'starlight-page'; // _navopen is NOT a prop — toggling it must not trigger a re-render static props = ['sitetitle', 'pagetitle', 'nav', 'sidebar', 'toc', 'currentslug', 'currentpath', 'nosidebar']; sitetitle = ''; pagetitle = ''; nav: NavItem[] = []; sidebar: SidebarGroup[] = []; toc: TocEntry[] = []; currentslug = ''; currentpath = ''; nosidebar = false; private _navOpen = false; /** Original innerHTML captured before first render — preserved across re-renders. */ private _contentHtml = ''; override connectedCallback(): void { // Capture the original content before Elena's first render replaces innerHTML if (!this._contentHtml) { this._contentHtml = this.innerHTML; } super.connectedCallback(); this.addEventListener('sl-nav-toggle', this._onNavToggle); this.addEventListener('click', this._onBackdropClick); } override disconnectedCallback(): void { super.disconnectedCallback(); this.removeEventListener('sl-nav-toggle', this._onNavToggle); this.removeEventListener('click', this._onBackdropClick); } /** Toggle nav imperatively — no re-render, just class/DOM manipulation. */ private _setNavOpen(open: boolean) { this._navOpen = open; const sidebar = this.querySelector('.sidebar-wrap') as HTMLElement | null; const header = this.querySelector('starlight-header') as any; if (sidebar) sidebar.classList.toggle('nav-open', open); if (header) header.navopen = open; // Manage backdrop let backdrop = this.querySelector('.nav-backdrop') as HTMLElement | null; if (open && !backdrop) { backdrop = document.createElement('div'); backdrop.className = 'nav-backdrop'; backdrop.setAttribute('data-action', 'close-nav'); // Insert after the header const headerEl = this.querySelector('starlight-header'); headerEl?.after(backdrop); } else if (!open && backdrop) { backdrop.remove(); } } private _onNavToggle = () => { this._setNavOpen(!this._navOpen); }; private _onBackdropClick = (e: Event) => { const target = e.target as HTMLElement; if (target.closest('[data-action="close-nav"]')) { this._setNavOpen(false); } }; override updated(): void { // Elena light DOM — pass properties to child components imperatively const header = this.querySelector('starlight-header') as any; if (header) { header.sitetitle = this.sitetitle; header.nav = this.nav; header.currentpath = this.currentpath; header.navopen = this._navOpen; header.hassidebar = !this.nosidebar; } const sidebar = this.querySelector('starlight-sidebar') as any; if (sidebar) { sidebar.groups = this.sidebar; sidebar.currentslug = this.currentslug; } const toc = this.querySelector('starlight-toc') as any; if (toc) { toc.entries = this.toc; } } render() { // During SSR, connectedCallback() never runs so _contentHtml is empty. // Fall back to this.innerHTML which the SSR adapter sets from children. const contentHtml = this._contentHtml || this.innerHTML || ''; // Serialize props as JSON for child component attributes. // Elena's html tag handles attribute escaping (" → "). const navJson = JSON.stringify(this.nav || []); const sidebarJson = JSON.stringify(this.sidebar || []); const tocJson = JSON.stringify(this.toc || []); const sidebarHtml = !this.nosidebar ? `` : ''; const titleHtml = this.pagetitle ? `

${this.pagetitle}

` : ''; const tocHtml = !this.nosidebar ? `` : ''; const bodyClass = this.nosidebar ? 'body no-sidebar' : 'body'; return html`
${unsafeHTML(sidebarHtml)}
${unsafeHTML(titleHtml)}
${unsafeHTML(contentHtml)}
${unsafeHTML(tocHtml)}
`; } } StarlightPage.define(); export default StarlightPage;