Sticky status headers; fix floating details-view button on phones

- Status group headers ("Not Started (37)") stick under the top nav while
  scrolling through their group, on every screen size
- On desktop the column header and filter rows stick beneath the group
  header (table wrappers no longer clip them; these fixed-layout tables
  never scroll sideways at desktop widths)
- layout.js measures the nav, group header and header row heights so the
  sticky rows stack without gaps, re-measuring on re-render/resize/scroll
- Phones: the Advanced/Simple View button sits on its own line under the
  device name instead of floating over the dialog content
- Rename mobile.css/js to layout.css/js now that they hold desktop rules
This commit is contained in:
Johan Sandoval committed 2026-10-06 16:43:49 +00:00
1 parent 2cbb03894b
commit a85c22b2b9
3 files changed
+77 -11

No files matched your search

+2 -2
View File
@@ -351,8 +351,8 @@
.status-btn:hover { color: white; background: rgba(255,255,255,0.1); }
}
</style>
<link rel="stylesheet" href="/mobile.css">
<script src="/mobile.js"></script>
<link rel="stylesheet" href="/layout.css">
<script src="/layout.js"></script>
</head>
<body>
+34 -3
View File
@@ -1,9 +1,36 @@
/*
* Small-screen layout for the dashboard (phones, tablets, narrow windows).
* Loaded after the page's own <style> block. Desktop layouts are untouched: every rule
* here is inside a max-width media query.
* Layout additions for the dashboard, loaded after the page's own <style> block:
* - sticky status-group headers (all sizes) and sticky column headers (desktop)
* - card view for tables on phones and tablets
* - small-screen fixes for forms, docs and dialogs
* layout.js measures the sticky offsets and labels card cells.
*/
/* ---------------------------------------------------------------------------------------
* Sticky headers
*
* A status group's header ("Not Started (37)") sticks under the top nav while you scroll
* through that group, then hands over to the next group's header. On desktop the column
* header and filter rows stick beneath it. layout.js sets --nav-h (top nav height) and,
* per table, --sticky-top (nav + group header) and --thead-r1 (header row height).
* ------------------------------------------------------------------------------------- */
.group-header { position: sticky; top: var(--nav-h, 60px); z-index: 30; }
@media (min-width: 1101px) {
/* An overflowing wrapper would capture the sticky rows; these tables never need to
scroll sideways at desktop widths (they're fixed-layout, 100% wide) */
#masterList .table-wrapper, #software .table-wrapper, .stack-box.table-wrapper { overflow: visible !important; }
.master-table:not(#table-custom) thead th {
position: sticky; top: var(--sticky-top, 112px); z-index: 20;
}
.master-table:not(#table-custom) thead tr.filter-row th {
top: calc(var(--sticky-top, 112px) + var(--thead-r1, 38px) - 2px); z-index: 19; /* -2px: no seam from collapsed borders */
/* the original header tint, but opaque so rows don't show through */
background: linear-gradient(rgba(0,0,0,0.2), rgba(0,0,0,0.2)), var(--bg-card);
}
}
/* ---------------------------------------------------------------------------------------
* Card view for data tables (phones and tablets)
*
@@ -149,6 +176,10 @@
.modal-header { padding: 14px 14px 8px; }
.modal-header h2 { font-size: 1.2em; word-break: break-all; }
/* Details title: the Advanced/Simple View button gets its own line under the device
name instead of floating over the content */
#detailsTitle button { float: none !important; display: block; margin: 10px 0 0 !important; font-size: 0.65em !important; }
/* Device details: Advanced View's source / overview / destination columns stack,
and the whole dialog scrolls instead of each column scrolling separately */
#detailsModal .modal-content { height: auto !important; overflow-y: auto !important; display: block !important; }
+41 -6
View File
@@ -1,11 +1,15 @@
/*
* Card layout support for small screens.
* Layout helpers (see layout.css).
*
* On phones and tablets (see mobile.css) 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 labels are reapplied (once per frame at most) whenever the
* DOM changes. On wide screens it does nothing.
* 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';
@@ -70,9 +74,36 @@
if (btn.innerHTML !== html) btn.innerHTML = html;
}
// --- 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 nav = document.querySelector('.top-nav');
const navH = nav ? nav.offsetHeight : 60;
setVar(document.documentElement, '--nav-h', navH + 'px');
document.querySelectorAll('table.master-table').forEach(table => {
const gh = groupHeaderFor(table);
const ghH = gh ? gh.offsetHeight : 0;
const firstRow = table.tHead && table.tHead.rows[0];
if (!firstRow || !firstRow.offsetHeight) return; // hidden page/group: keep old values
setVar(table, '--sticky-top', (navH + ghH) + 'px');
setVar(table, '--thead-r1', firstRow.offsetHeight + 'px');
});
}
let queued = false;
function run() {
queued = false;
updateSticky();
if (!cardsQuery.matches) return;
document.querySelectorAll('table.master-table').forEach(labelTable);
}
@@ -86,6 +117,10 @@
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 });
schedule();
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);