(function () { const NAV_ENDPOINT = "/navigation/sidebar"; const SIDEBAR_SELECTOR = ".sidebar-nav"; const DATA_SOURCE_ATTR = "navigationSource"; const ROLE_ATTR = "navigationRoles"; function onReady(callback) { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", callback, { once: true }); } else { callback(); } } function isActivePath(pathname, matchPrefix) { if (!matchPrefix) { return false; } if (matchPrefix === "/") { return pathname === "/"; } return pathname.startsWith(matchPrefix); } function createAnchor({ href, label, matchPrefix, tooltip, isExternal, isActive, className, }) { const anchor = document.createElement("a"); anchor.href = href; anchor.className = className + (isActive ? " is-active" : ""); anchor.dataset.matchPrefix = matchPrefix || href; if (tooltip) { anchor.title = tooltip; } if (isExternal) { anchor.target = "_blank"; anchor.rel = "noopener noreferrer"; anchor.classList.add("is-external"); } anchor.textContent = label; return anchor; } function buildLinkBlock(link, pathname) { if (!link || !link.href) { return null; } const matchPrefix = link.match_prefix || link.matchPrefix || link.href; const isActive = isActivePath(pathname, matchPrefix); const block = document.createElement("div"); block.className = "sidebar-link-block"; if (typeof link.id === "number") { block.dataset.linkId = String(link.id); } const anchor = createAnchor({ href: link.href, label: link.label, matchPrefix, tooltip: link.tooltip, isExternal: Boolean(link.is_external ?? link.isExternal), isActive, className: "sidebar-link", }); block.appendChild(anchor); const children = Array.isArray(link.children) ? link.children : []; if (children.length > 0) { const container = document.createElement("div"); container.className = "sidebar-sublinks"; for (const child of children) { if (!child || !child.href) { continue; } const childMatch = child.match_prefix || child.matchPrefix || child.href; const childActive = isActivePath(pathname, childMatch); const childAnchor = createAnchor({ href: child.href, label: child.label, matchPrefix: childMatch, tooltip: child.tooltip, isExternal: Boolean(child.is_external ?? child.isExternal), isActive: childActive, className: "sidebar-sublink", }); container.appendChild(childAnchor); } if (container.children.length > 0) { block.appendChild(container); } } return block; } function buildGroupSection(group, pathname) { if (!group) { return null; } const links = Array.isArray(group.links) ? group.links : []; if (links.length === 0) { return null; } const section = document.createElement("div"); section.className = "sidebar-section"; if (typeof group.id === "number") { section.dataset.groupId = String(group.id); } const label = document.createElement("div"); label.className = "sidebar-section-label"; label.textContent = group.label; section.appendChild(label); const linksContainer = document.createElement("div"); linksContainer.className = "sidebar-section-links"; for (const link of links) { const block = buildLinkBlock(link, pathname); if (block) { linksContainer.appendChild(block); } } if (linksContainer.children.length === 0) { return null; } section.appendChild(linksContainer); return section; } function buildEmptyState() { const section = document.createElement("div"); section.className = "sidebar-section sidebar-empty-state"; const label = document.createElement("div"); label.className = "sidebar-section-label"; label.textContent = "Navigation"; section.appendChild(label); const copyWrapper = document.createElement("div"); copyWrapper.className = "sidebar-section-links"; const copy = document.createElement("p"); copy.className = "sidebar-empty-copy"; copy.textContent = "Navigation is unavailable."; copyWrapper.appendChild(copy); section.appendChild(copyWrapper); return section; } function renderSidebar(navContainer, payload) { const pathname = window.location.pathname; const groups = Array.isArray(payload?.groups) ? payload.groups : []; navContainer.replaceChildren(); const rendered = []; for (const group of groups) { const section = buildGroupSection(group, pathname); if (section) { rendered.push(section); } } if (rendered.length === 0) { navContainer.appendChild(buildEmptyState()); navContainer.dataset[DATA_SOURCE_ATTR] = "client-empty"; delete navContainer.dataset[ROLE_ATTR]; return; } for (const section of rendered) { navContainer.appendChild(section); } navContainer.dataset[DATA_SOURCE_ATTR] = "client"; const roles = Array.isArray(payload?.roles) ? payload.roles : []; if (roles.length > 0) { navContainer.dataset[ROLE_ATTR] = roles.join(","); } else { delete navContainer.dataset[ROLE_ATTR]; } } async function hydrateSidebar(navContainer) { try { const response = await fetch(NAV_ENDPOINT, { method: "GET", credentials: "include", headers: { Accept: "application/json", }, }); if (!response.ok) { if (response.status !== 401 && response.status !== 403) { console.warn( "Navigation sidebar hydration failed with status", response.status ); } return; } const payload = await response.json(); renderSidebar(navContainer, payload); } catch (error) { console.warn("Navigation sidebar hydration failed", error); } } onReady(() => { const navContainer = document.querySelector(SIDEBAR_SELECTOR); if (!navContainer) { return; } hydrateSidebar(navContainer); }); })();