/**
* 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);