/* * Layout helpers (see layout.css). * * Sticky headers: measures the top nav and each table's group header so the column * header rows can stick directly beneath them (--nav-h, --sticky-top, --thead-r1). * * Card view: on phones and tablets each table row is shown as a card, with every cell * rendered as "Column name: value". CSS can't look up a cell's column header, so this * copies each header's text onto its cells as data-label. * * The dashboard re-renders its tables constantly, so both are reapplied (once per frame * at most) whenever the DOM changes. */ (function () { 'use strict'; const cardsQuery = window.matchMedia('(max-width: 1100px)'); const clean = text => text.replace(/[\u2195\u25B2\u25BC\u00D7\u2715]/g, '').replace(/\s+/g, ' ').trim(); function labelTable(table) { const thead = table.tHead; if (!thead) return; // The header row is the one with the most text (Custom View has a drag-handle row above it) let headerRow = null, best = -1; for (const row of thead.rows) { if (row.classList.contains('filter-row')) continue; const named = Array.from(row.cells).filter(c => clean(c.textContent)).length; if (named > best) { best = named; headerRow = row; } } if (!headerRow) return; for (const row of thead.rows) { if (row === headerRow) row.classList.add('card-label-row'); else if (!row.classList.contains('filter-row')) row.classList.add('card-aux-row'); } const labels = Array.from(headerRow.cells).map(c => clean(c.textContent)); const apply = row => { Array.from(row.cells).forEach((cell, i) => { const label = cell.colSpan > 1 ? '' : (labels[i] || ''); if (cell.dataset.label !== label) cell.dataset.label = label; }); }; thead.querySelectorAll('tr.filter-row').forEach(apply); for (const body of table.tBodies) for (const row of body.rows) apply(row); } // --- Sticky offsets --------------------------------------------------------------- function setVar(el, name, value) { if (el.style.getPropertyValue(name) !== value) el.style.setProperty(name, value); } function groupHeaderFor(table) { // The group header is a sibling of the table or of its wrapper for (let el = table.parentElement, depth = 0; el && depth < 3; el = el.parentElement, depth++) { const gh = el.querySelector(':scope > .group-header'); if (gh) return gh; } return null; } function updateSticky() { const root = document.documentElement; const nav = document.querySelector('.top-nav'); const navH = nav ? nav.getBoundingClientRect().height : 60; setVar(root, '--nav-h', navH + 'px'); const dock = document.getElementById('usmt-term'); setVar(root, '--dock-h', (dock ? Math.ceil(dock.getBoundingClientRect().height) : 0) + 'px'); document.querySelectorAll('table.master-table').forEach(table => { if (!table.offsetParent) return; // hidden page/group: keep old values const gh = groupHeaderFor(table); const ghH = gh && getComputedStyle(gh).position === 'sticky' ? gh.getBoundingClientRect().height : 0; // 1px overlap tucks the table header under the group header: no hairline gap const top = Math.max(0, navH + ghH - 1) + 'px'; setVar(table, '--sticky-top', top); if (table.parentElement) setVar(table.parentElement, '--sticky-top', top); // for the filter chip bar }); pinCustomHeader(); } // Custom View can be wider than the page, so it scrolls sideways in its own box, and a // sideways-scrolling box can't contain a position:sticky header. Instead its header is // shifted down by exactly the amount needed to stay pinned under the group header. function pinCustomHeader() { const table = document.getElementById('table-custom'); if (!table) return; if (cardsQuery.matches || !table.offsetParent || !table.tHead) { setVar(table, '--pin-y', '0px'); return; } const top = parseFloat(table.style.getPropertyValue('--sticky-top')) || 0; const r = table.getBoundingClientRect(); const headH = table.tHead.getBoundingClientRect().height; const y = Math.max(0, Math.min(top - r.top, r.height - headH)); setVar(table, '--pin-y', y + 'px'); } let queued = false; function run() { queued = false; updateSticky(); if (!cardsQuery.matches) return; document.querySelectorAll('table.master-table').forEach(labelTable); } function schedule() { if (queued) return; queued = true; requestAnimationFrame(run); } function start() { new MutationObserver(schedule).observe(document.body, { childList: true, subtree: true }); if (cardsQuery.addEventListener) cardsQuery.addEventListener('change', schedule); else cardsQuery.addListener(schedule); window.addEventListener('resize', schedule); // Groups and pages are shown by toggling display (not a DOM change), so also // re-measure as the user scrolls; writes only happen when a value changes. window.addEventListener('scroll', schedule, { passive: true }); window.addEventListener('scroll', pinCustomHeader, { passive: true }); // same frame: no lag // The terminal dock opens, closes and resizes without a page resize if (window.ResizeObserver) { const watchDock = () => { const dock = document.getElementById('usmt-term'); if (dock) new ResizeObserver(schedule).observe(dock); else setTimeout(watchDock, 500); }; watchDock(); } schedule(); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start(); })();