Private/Get-DhJsNav.ps1

function Get-DhJsNav {
    # Auto-split fragment of the dashboard runtime JS (see Get-DhJsContent).
    return @'
  /* =========================================================================
     NAVIGATION (v2.0.0) — one depth-agnostic menu tree

     A section is visible IFF it carries panel-active. NOTHING else ever touches
     style.display on a section. State is RECOMPUTED WHOLE from the selected
     path, never patched incrementally.

     This replaces the 1.x showGroup / showSubPanel / showSubGroup /
     _showFlatPanel quartet, which mutated overlapping subsets of the state
     using two competing mechanisms (an inline style.display AND the
     panel-active class). That is what made a block declared with
     -NavGroup + -NavSubGroup permanently unreachable (readme-dev gotcha #26):
     the hide branch cleared both, the show branch restored only one.

     There are no show/hide branches left to keep symmetrical. Re-applying the
     same path is a no-op, and the result depends only on the final path, never
     on the route taken to reach it.

     Nothing below mentions a number of levels. Depth is data.
     ========================================================================= */

  /* ── Sync nav sticky top to actual header height ────────────────── */
  function syncNavTop() {
    var header = document.querySelector('.report-header');
    var nav = document.getElementById('report-nav');
    if (!header || !nav) return;
    document.documentElement.style.setProperty('--header-height', header.offsetHeight + 'px');
  }

  /* ── Collapsible table chrome (unchanged from 1.x) ──────────────── */
  function initTableCollapsibles() {
    document.querySelectorAll('.table-section-collapsible .table-collapse-toggle').forEach(function (btn) {
      if (btn._dhWired) return;
      btn._dhWired = true;
      btn.addEventListener('click', function () {
        var sec = btn.closest('.table-section');
        if (!sec) return;
        var open = sec.classList.toggle('table-collapsed');
        btn.setAttribute('aria-expanded', open ? 'false' : 'true');
      });
    });
  }

  /* ─────────────────────────────────────────────────────────────────
     THE MATCHER — mirrors Test-DhNavMatch in PowerShell exactly.
     Extracted verbatim by Tests/js/nav.test.js via these sentinels, so the
     two copies cannot drift apart unnoticed. Keep them.
     ───────────────────────────────────────────────────────────────── */
  /* ==== DH-NAV-MATCHER-START ==== */
  function navPathOf(sec) {
    try { return JSON.parse(sec.dataset.navpath || '[]'); } catch (e) { return []; }
  }
  function navMatches(sec, path) {
    var scope = (sec.dataset.navscope || 'exact').toLowerCase();
    if (scope === 'global') return true;
    var p = navPathOf(sec);
    if (p.length > path.length) return false; /* declared deeper than selected */
    for (var i = 0; i < p.length; i++) { if (p[i] !== path[i]) return false; }
    return scope === 'subtree' || p.length === path.length;
  }
  /* ==== DH-NAV-MATCHER-END ==== */

  var currentNavPath = [];

  function navSections() { return document.querySelectorAll('[data-navpath]'); }

  /* ── Tree, derived from the DOM ──────────────────────────────────
     Built from the same data-navpath attributes the matcher reads, so a menu
     strip can never describe a view the content does not have, nor miss one it
     does. That drift was defect #2 in the proposal. */
  function buildNavTree() {
    var root = { seg: '', path: [], label: '', children: {}, order: [], hasContent: false };
    navSections().forEach(function (sec) {
      var p = navPathOf(sec);
      if (!p.length) return; /* global: no menu node */
      var scope = (sec.dataset.navscope || 'exact').toLowerCase();
      var node = root;
      for (var i = 0; i < p.length; i++) {
        if (!node.children[p[i]]) {
          node.children[p[i]] = { seg: p[i], path: p.slice(0, i + 1), label: p[i],
                                  children: {}, order: [], hasContent: false, tableId: null,
                                  seq: Infinity, navOrder: 0 };
          node.order.push(p[i]);
        }
        node = node.children[p[i]];
        /* A node inherits the EARLIEST declaration that passes through it, so an
           ancestor created late by a deep path still sorts by its first mention. */
        var seq = parseInt(sec.dataset.navseq || '0', 10);
        if (seq < node.seq) node.seq = seq;
        var ord = parseInt(sec.dataset.navorder || '0', 10);
        if (ord !== 0 && node.navOrder === 0) node.navOrder = ord;
      }
      if (scope === 'exact') node.hasContent = true;
      if (sec.dataset.navlabel) node.label = sec.dataset.navlabel;
      /* A table section sitting exactly here lets its chip carry a row-count
         badge, replacing the 1.x .nav-badge that lived in the flat nav link. */
      if (scope === 'exact' && sec.id && sec.id.indexOf('section-') === 0) {
        node.tableId = sec.id.slice('section-'.length);
      }
    });
    /* Sort every level: explicit -NavOrder first, then declaration order.
       Mirrors Get-DhNavChildOrder in PowerShell. */
    (function sortTree(node) {
      node.order.sort(function (a, b) {
        var A = node.children[a], B = node.children[b];
        var ao = A.navOrder !== 0 ? 0 : 1, bo = B.navOrder !== 0 ? 0 : 1;
        if (ao !== bo) return ao - bo;
        if (A.navOrder !== B.navOrder) return A.navOrder - B.navOrder;
        return A.seq - B.seq;
      });
      node.order.forEach(function (k) { sortTree(node.children[k]); });
    })(root);
    return root;
  }

  function navNodeAt(tree, path) {
    var node = tree;
    for (var i = 0; i < path.length; i++) {
      if (!node.children[path[i]]) return null;
      node = node.children[path[i]];
    }
    return node;
  }

  /* Descend to the first node that holds content of its own, then STOP.
     At depth <= 3 this is identical to descend-to-first-leaf; deeper, it stops
     one click teleporting the reader several levels down. */
  function navAutoDescend(tree, path) {
    var out = path.slice();
    var node = navNodeAt(tree, out);
    if (!node) return out;
    while (!node.hasContent && node.order.length) {
      var next = node.order[0];
      out.push(next);
      node = node.children[next];
    }
    return out;
  }

  /* ── THE ONE RULE ────────────────────────────────────────────────── */
  function applyNav(path) {
    currentNavPath = (path || []).slice();
    navSections().forEach(function (sec) {
      sec.classList.toggle('panel-active', navMatches(sec, currentNavPath));
    });
    renderNavMenu(currentNavPath);
    if (typeof URLState !== 'undefined' && URLState.save) URLState.save();
    if (typeof updateNavBadges === 'function') updateNavBadges();
  }

  /* ── Menu rendering: one tree, two presentations ─────────────────
     Both read the SAME tree and emit the SAME chip semantics; only the
     arrangement differs. Nothing below is aware of a level count. */
  function navLayout() {
    var nav = document.getElementById('report-nav');
    return (nav && nav.dataset.navlayout) ? nav.dataset.navlayout : 'strips';
  }

  function renderNavMenu(path) {
    if (navLayout() === 'sidebar') { renderNavSidebar(path); renderNavStrips(path, true); }
    else { renderNavSidebar(path, true); renderNavStrips(path); }
    renderNavCrumb(path);
  }

  /* Sidebar rail — the whole tree at once, recovering the ~190px of sticky
     vertical chrome the strips consume. Required at depth 4+ (plan-v2.md D6). */
  function renderNavSidebar(path, hide) {
    var host = document.getElementById('nav-sidebar');
    if (!host) return;
    if (hide) { host.style.display = 'none'; host.innerHTML = ''; return; }
    host.style.display = '';
    host.innerHTML = '';
    document.body.classList.add('has-nav-sidebar');

    var tree = buildNavTree();
    (function walk(node, depth, container) {
      node.order.forEach(function (seg) {
        var child = node.children[seg];
        var onPath = path[depth] === seg;
        var selected = onPath && path.length === child.path.length;

        var a = document.createElement('a');
        a.className = 'nav-rail-item nav-rail-d' + depth +
                      (selected ? ' nav-rail-active' : '') +
                      (onPath ? ' nav-rail-onpath' : '');
        a.href = '#';
        a.style.paddingLeft = (10 + depth * 14) + 'px';
        a.textContent = child.label;
        a.setAttribute('role', 'treeitem');
        a.setAttribute('aria-selected', selected ? 'true' : 'false');
        if (child.tableId) {
          var b = document.createElement('span');
          b.className = 'nav-badge';
          b.dataset.table = child.tableId;
          a.appendChild(b);
        }
        a.addEventListener('click', function (e) {
          e.preventDefault();
          applyNav(navAutoDescend(buildNavTree(), child.path));
        });
        container.appendChild(a);

        /* Only expand the branch the reader is actually on - a fully expanded
           deep tree is as unreadable as four stacked strips. */
        if (onPath && child.order.length) { walk(child, depth + 1, container); }
      });
    })(tree, 0, host);
  }

  /* ── Strips, rendered from a loop ────────────────────────────────
     N strips, no fixed element ids and no L1/L2/L3 naming anywhere. This is
     what makes a 4th level a config change rather than a rewrite. */
  function renderNavStrips(path, hide) {
    var host = document.getElementById('nav-strips');
    if (!host) return;
    var topHostEl = document.getElementById('nav-strips-top');
    if (hide) {
      host.style.display = 'none'; host.innerHTML = '';
      if (topHostEl) { topHostEl.style.display = 'none'; topHostEl.innerHTML = ''; }
      return;
    }
    host.style.display = '';
    if (topHostEl) topHostEl.style.display = '';
    document.body.classList.remove('has-nav-sidebar');
    var tree = buildNavTree();
    host.innerHTML = '';

    /* Depth 0 renders INSIDE .nav-inner, on the same line as the Top / density /
       theme buttons - that is where the 1.x group tabs lived, and moving them to
       their own row cost a whole line of vertical chrome for no benefit.
       Levels 1+ get their own strips below. */
    var topHost = document.getElementById('nav-strips-top');
    if (topHost) topHost.innerHTML = '';

    var node = tree;
    var depth = 0;
    while (node && node.order.length) {
      var strip = null, inner;
      if (depth === 0 && topHost) {
        inner = topHost; /* chips go straight into nav-inner */
      } else {
        strip = document.createElement('div');
        strip.className = 'nav-strip nav-strip-' + depth; /* depth, not a level name */
        strip.setAttribute('role', 'tablist');
        strip.setAttribute('aria-label', 'Subsections');
        inner = document.createElement('div');
        inner.className = 'nav-strip-inner';
      }

      (function (currentNode, currentDepth) {
        currentNode.order.forEach(function (seg) {
          var child = currentNode.children[seg];
          var selected = path[currentDepth] === seg;
          var a = document.createElement('a');
          a.className = 'nav-chip' + (selected ? ' nav-chip-active' : '');
          a.href = '#';
          a.textContent = child.label;
          a.setAttribute('role', 'tab');
          a.setAttribute('aria-selected', selected ? 'true' : 'false');
          a.dataset.navto = JSON.stringify(child.path);
          if (child.tableId) {
            var badge = document.createElement('span');
            badge.className = 'nav-badge';
            badge.dataset.table = child.tableId;
            a.appendChild(badge);
          }
          a.addEventListener('click', function (e) {
            e.preventDefault();
            applyNav(navAutoDescend(buildNavTree(), child.path));
          });
          inner.appendChild(a);
        });
      })(node, depth);

      if (strip) { strip.appendChild(inner); host.appendChild(strip); }

      if (path[depth] === undefined || !node.children[path[depth]]) break;
      node = node.children[path[depth]];
      depth++;
    }
  }

  /* Breadcrumb — mandatory once a tree is 4+ deep, harmless shallower. */
  function renderNavCrumb(path) {
    var host = document.getElementById('nav-crumb');
    if (!host) return;
    if (path.length < 4) { host.innerHTML = ''; host.style.display = 'none'; return; }
    host.style.display = '';
    host.innerHTML = '';
    var tree = buildNavTree();
    path.forEach(function (seg, i) {
      var node = navNodeAt(tree, path.slice(0, i + 1));
      var a = document.createElement('a');
      a.className = 'nav-crumb-item';
      a.href = '#';
      a.textContent = node ? node.label : seg;
      a.addEventListener('click', function (e) {
        e.preventDefault();
        applyNav(path.slice(0, i + 1));
      });
      host.appendChild(a);
      if (i < path.length - 1) {
        var sep = document.createElement('span');
        sep.className = 'nav-crumb-sep';
        sep.textContent = '›';
        host.appendChild(sep);
      }
    });
  }

  /* ── Init ────────────────────────────────────────────────────────── */
  function initNav() {
    var tree = buildNavTree();

    /* Restore from the URL, validating segment by segment and truncating at the
       first miss, so a renamed node degrades to its parent instead of a blank
       page. Old 1.x {group,panel} hashes are shimmed to a path. */
    var saved = (typeof URLState !== 'undefined' && URLState.load) ? URLState.load() : {};
    var wanted = [];
    if (saved && saved.nav && saved.nav.length) {
      wanted = saved.nav;
    } else if (saved && saved.group) {
      wanted = [saved.group]; /* 1.x hash shim */
      if (saved.panel) wanted.push(saved.panel);
    }

    var valid = [];
    var node = tree;
    for (var i = 0; i < wanted.length; i++) {
      if (!node.children[wanted[i]]) break;
      node = node.children[wanted[i]];
      valid.push(wanted[i]);
    }

    applyNav(navAutoDescend(tree, valid));
    initTableCollapsibles();

    /* Compat surface for anything that called into the 1.x internals. */
    window._applyNav = applyNav;
    window._navPath = function () { return currentNavPath.slice(); };
    window._showPanel = function (id) {
      var sec = document.getElementById('section-' + id) || document.getElementById('bsection-' + id);
      if (sec) applyNav(navPathOf(sec));
    };
    window._showGroup = function (g) { applyNav(navAutoDescend(buildNavTree(), [g])); };
    window._showSubGroup = function (g, sg) { applyNav(navAutoDescend(buildNavTree(), sg ? [g, sg] : [g])); };
  }

  /* Update nav row-count badge — shows filtered/total e.g. "12/47" when filtered */
  function updateNavBadge(id) {
    var badge = document.querySelector('.nav-badge[data-table="'+id+'"]');
    if (!badge || !engines[id]) return;
    var filtered = engines[id]._getFiltered().length;
    var total = engines[id].allData.length;
    badge.textContent = (filtered < total) ? filtered+'/'+total : total;
  }

  function updateNavBadges() {
    Object.keys(engines).forEach(function (id) { updateNavBadge(id); });
  }

'@

}