/** * zen-nav.js — Zen dashboard navigation bar Web Component * * Usage: * * * * Node * Orchestrator * * * Each child becomes a nav link. The current page is * highlighted automatically based on the href. * * Links may be added or removed dynamically — the component * re-renders automatically via MutationObserver. */ class ZenNav extends HTMLElement { connectedCallback() { if (!this.shadowRoot) this.attachShadow({ mode: 'open' }); this._render(); this._observer = new MutationObserver(() => this._render()); this._observer.observe(this, { childList: true }); } disconnectedCallback() { if (this._observer) { this._observer.disconnect(); this._observer = null; } } _render() { const currentPath = window.location.pathname; const currentSearch = window.location.search; const items = Array.from(this.querySelectorAll(':scope > a')); const currentParams = new URLSearchParams(currentSearch); let spacerInserted = false; const links = items.map(a => { const href = a.getAttribute('href') || ''; const label = a.textContent.trim(); const alignRight = a.hasAttribute('data-align') && a.getAttribute('data-align') === 'right'; let active = false; try { const linkUrl = new URL(href, window.location.origin); if (linkUrl.pathname === currentPath || currentPath.endsWith(href)) { // All of the link's query params must be present in the current URL const linkParams = linkUrl.searchParams; active = Array.from(linkParams.entries()).every( ([k, v]) => currentParams.get(k) === v ); // A bare path with no params only matches when the current URL also has no page param if (linkParams.toString() === '' && currentParams.has('page')) { active = false; } } } catch (e) { active = currentPath.endsWith(href); } let prefix = ''; if (alignRight && !spacerInserted) { prefix = ''; spacerInserted = true; } return `${prefix}${label}`; }).join(''); this.shadowRoot.innerHTML = ` `; } } customElements.define('zen-nav', ZenNav);