Polish sticky headers, Custom View, docs page and dialogs

- Status tables: the whole header (column names + filters) sticks as one
  block, tucked 1px under the group header, so no gap can open between
  the rows; offsets use fractional heights
- Phones/tablets: the filter panel sticks under the group header too, and
  is opaque and compact (three filters per row on phones)
- Custom View: fix the group header being pushed over the column header
  (it lives inside the view's own scroll box); the column header now pins
  to the top of that box on desktop, and the header strip sticks on phones
- All Devices: group title is "All Devices (N)" instead of "Missing
  Software", matching the page title count
- Documentation fits the window on desktop: only the article list and the
  article scroll; placeholder no longer says "from the left"
- Dialogs sit above the terminal dock so it can't cover their last rows;
  page bottom padding follows the dock's measured height
This commit is contained in:
Johan Sandoval committed 2026-10-06 16:58:05 +00:00
1 parent a85c22b2b9
commit d24be4a2d6
4 files changed
+73 -25

No files matched your search

+1 -2
View File
@@ -5,8 +5,7 @@
font-family: 'Segoe UI', sans-serif; font-family: 'Segoe UI', sans-serif;
box-shadow: 0 -4px 16px rgba(0,0,0,0.35); box-shadow: 0 -4px 16px rgba(0,0,0,0.35);
} }
body { padding-bottom: 40px; } /* Page bottom padding for the dock is set in layout.css from its measured height */
body.usmt-term-open { padding-bottom: calc(var(--usmt-term-h, 340px) + 72px); }
.usmt-term-resize { height: 5px; cursor: ns-resize; background: transparent; } .usmt-term-resize { height: 5px; cursor: ns-resize; background: transparent; }
.usmt-term.collapsed .usmt-term-resize { display: none; } .usmt-term.collapsed .usmt-term-resize { display: none; }
+5 -3
View File
@@ -459,7 +459,7 @@
<div class="table-wrapper" style="overflow-x: auto; background: var(--bg-card); border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.3); transition: padding-bottom 0.2s;"> <div class="table-wrapper" style="overflow-x: auto; background: var(--bg-card); border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.3); transition: padding-bottom 0.2s;">
<div class="group-header" style="margin-top: 0; border-radius: 8px 8px 0 0;"> <div class="group-header" style="margin-top: 0; border-radius: 8px 8px 0 0;">
<span>Missing Software</span> <span>All Devices <span id="softwareGroupCount"></span></span>
<div style="display: flex; gap: 8px;"> <div style="display: flex; gap: 8px;">
<button class="copy-table-btn no-copy" onclick="copyLocationInfo('software', this)"> <button class="copy-table-btn no-copy" onclick="copyLocationInfo('software', this)">
<svg class="icon-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M215.7 499.2C267 435 384 279.4 384 192C384 86 298 0 192 0S0 86 0 192c0 87.4 117 243 168.3 307.2c12.3 15.3 35.1 15.3 47.4 0zM192 128a64 64 0 1 1 0 128 64 64 0 1 1 0-128z"/></svg> Copy Location Info <svg class="icon-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M215.7 499.2C267 435 384 279.4 384 192C384 86 298 0 192 0S0 86 0 192c0 87.4 117 243 168.3 307.2c12.3 15.3 35.1 15.3 47.4 0zM192 128a64 64 0 1 1 0 128 64 64 0 1 1 0-128z"/></svg> Copy Location Info
@@ -595,7 +595,7 @@
<button class="nav-btn" id="btn-edit-mode" onclick="setDocMode('edit')" title="Switch to Edit Mode" style="padding: 6px 10px; border: 1px solid var(--border-color);"></button> <button class="nav-btn" id="btn-edit-mode" onclick="setDocMode('edit')" title="Switch to Edit Mode" style="padding: 6px 10px; border: 1px solid var(--border-color);"></button>
</div> </div>
<div id="docContent" style="flex: 1; overflow-y: auto; line-height: 1.6; min-height: 0; padding-right: 10px;">Select a document from the left...</div> <div id="docContent" style="flex: 1; overflow-y: auto; line-height: 1.6; min-height: 0; padding-right: 10px;">Select an article from the list to read it.</div>
<textarea id="docEditor" style="display: none; flex: 1; width: 100%; background: var(--input-bg); color: var(--text-main); border: 1px solid var(--border-color); border-radius: 4px; padding: 15px; font-family: monospace; font-size: 1.05em; resize: none; min-height: 0; box-sizing: border-box;"></textarea> <textarea id="docEditor" style="display: none; flex: 1; width: 100%; background: var(--input-bg); color: var(--text-main); border: 1px solid var(--border-color); border-radius: 4px; padding: 15px; font-family: monospace; font-size: 1.05em; resize: none; min-height: 0; box-sizing: border-box;"></textarea>
</div> </div>
@@ -880,6 +880,8 @@
let swVis = Array.from(swRows).filter(tr => tr.style.display !== 'none').length; let swVis = Array.from(swRows).filter(tr => tr.style.display !== 'none').length;
let sc = document.getElementById('softwareCount'); let sc = document.getElementById('softwareCount');
if(sc) sc.innerText = `(${swVis})`; if(sc) sc.innerText = `(${swVis})`;
let sgc = document.getElementById('softwareGroupCount');
if(sgc) sgc.innerText = `(${swVis})`;
} }
let globalDataLastUpdated = "Never"; let globalDataLastUpdated = "Never";
@@ -2242,7 +2244,7 @@
if (res.ok) { if (res.ok) {
showToast("Article archived successfully!"); showToast("Article archived successfully!");
document.getElementById('docContent').innerHTML = "Select a document from the left..."; document.getElementById('docContent').innerHTML = "Select an article from the list to read it.";
document.getElementById('docToolbar').style.display = 'none'; document.getElementById('docToolbar').style.display = 'none';
document.getElementById('docEditor').style.display = 'none'; document.getElementById('docEditor').style.display = 'none';
currentDocTitle = ""; currentDocTitle = "";
+49 -13
View File
@@ -10,25 +10,45 @@
* Sticky headers * Sticky headers
* *
* A status group's header ("Not Started (37)") sticks under the top nav while you scroll * 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 * through that group, then hands over to the next group's header. Beneath it the table's
* header and filter rows stick beneath it. layout.js sets --nav-h (top nav height) and, * header (column names + filters on desktop, the filter panel on phones) sticks as one
* per table, --sticky-top (nav + group header) and --thead-r1 (header row height). * block, so the rows inside it can never drift apart. layout.js sets --nav-h and, per
* table, --sticky-top (nav + group header).
* ------------------------------------------------------------------------------------- */ * ------------------------------------------------------------------------------------- */
.group-header { position: sticky; top: var(--nav-h, 60px); z-index: 30; } .group-header { position: sticky; top: var(--nav-h, 60px); z-index: 30; }
@media (min-width: 1101px) { .master-table:not(#table-custom) thead { position: sticky; top: var(--sticky-top, 112px); z-index: 20; }
/* 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 { .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 */ /* 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); background: linear-gradient(rgba(0,0,0,0.2), rgba(0,0,0,0.2)), var(--bg-card);
} }
@media (min-width: 1101px) {
/* An overflowing wrapper would capture the sticky header; 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; }
/* Custom View scrolls inside its own box (it can be wider than the page), so its
group header stays put and its column header sticks to the top of that box */
#wrapper-custom .group-header { position: static; }
#scroll-custom thead th { position: sticky; top: 0; z-index: 20; }
}
/* ---------------------------------------------------------------------------------------
* Bottom terminal dock: keep page content and dialogs clear of it.
* layout.js sets --dock-h to the dock's current height (bar only, or bar + terminal).
* ------------------------------------------------------------------------------------- */
body { padding-bottom: var(--dock-h, 40px) !important; }
/* Dialogs sit above the dock (below the top nav), so it can't cover their bottom rows */
.modal-overlay { z-index: 1950 !important; }
#modal { z-index: 1960 !important; }
#syncModal { z-index: 1970 !important; }
#confirmModal, #promptModal { z-index: 1980 !important; }
/* Documentation fits the window on desktop: only the article list and the article scroll */
@media (min-width: 901px) {
#docs > div { height: calc(100vh - var(--nav-h, 60px) - 50px - var(--dock-h, 40px)) !important; }
} }
/* --------------------------------------------------------------------------------------- /* ---------------------------------------------------------------------------------------
@@ -56,7 +76,9 @@
/* Filters: a wrapping panel of labeled controls */ /* Filters: a wrapping panel of labeled controls */
table.master-table thead tr.filter-row { table.master-table thead tr.filter-row {
display: flex; flex-wrap: wrap; gap: 8px 10px; padding: 10px 12px; display: flex; flex-wrap: wrap; gap: 8px 10px; padding: 10px 12px;
background: rgba(0,0,0,0.15); border-bottom: 1px solid var(--border-color); /* opaque: the panel sticks over the cards while scrolling */
background: linear-gradient(rgba(0,0,0,0.15), rgba(0,0,0,0.15)), var(--bg-card);
border-bottom: 1px solid var(--border-color); box-shadow: 0 4px 8px rgba(0,0,0,0.3);
} }
table.master-table thead tr.filter-row th { table.master-table thead tr.filter-row th {
display: block; flex: 1 1 150px; min-width: 0; width: auto !important; display: block; flex: 1 1 150px; min-width: 0; width: auto !important;
@@ -69,6 +91,11 @@
font-size: 0.78em; font-weight: 600; color: var(--text-muted); font-size: 0.78em; font-weight: 600; color: var(--text-muted);
} }
/* Custom View: cards don't need the sideways-scrolling box, so it scrolls with the
page like the other tables and its header strip sticks under the group header */
#wrapper-custom, #scroll-custom { overflow: visible !important; max-height: none !important; }
#table-custom thead { position: sticky; top: var(--sticky-top, 112px); z-index: 20; }
/* Custom View keeps its header (sort / filter / remove buttons) as a strip of chips */ /* Custom View keeps its header (sort / filter / remove buttons) as a strip of chips */
#table-custom thead tr.card-label-row { #table-custom thead tr.card-label-row {
display: flex; flex-wrap: wrap; gap: 6px; padding: 10px 12px; display: flex; flex-wrap: wrap; gap: 6px; padding: 10px 12px;
@@ -134,6 +161,15 @@
@media (min-width: 1101px) { @media (min-width: 1101px) {
.card-filter-toggle { display: none; } .card-filter-toggle { display: none; }
} }
@media (max-width: 600px) {
/* Compact filter panel on phones: three per row, shorter controls */
table.master-table thead tr.filter-row { gap: 6px 8px; padding: 8px 10px; }
table.master-table thead tr.filter-row th { flex: 1 1 96px; }
table.master-table thead tr.filter-row th::before { font-size: 0.7em; margin-bottom: 2px; }
table.master-table thead tr.filter-row input[type="text"],
table.master-table thead tr.filter-row .select-box { height: 32px; padding: 3px 6px; }
table.master-table thead tr.filter-row .select-box { font-size: 13px; }
}
@media (max-width: 420px) { @media (max-width: 420px) {
table.master-table tbody td { padding-left: 7.8em; } table.master-table tbody td { padding-left: 7.8em; }
table.master-table tbody td::before { width: 7.2em; } table.master-table tbody td::before { width: 7.2em; }
+18 -7
View File
@@ -87,16 +87,19 @@
return null; return null;
} }
function updateSticky() { function updateSticky() {
const root = document.documentElement;
const nav = document.querySelector('.top-nav'); const nav = document.querySelector('.top-nav');
const navH = nav ? nav.offsetHeight : 60; const navH = nav ? nav.getBoundingClientRect().height : 60;
setVar(document.documentElement, '--nav-h', navH + 'px'); 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 => { document.querySelectorAll('table.master-table').forEach(table => {
if (!table.offsetParent) return; // hidden page/group: keep old values
const gh = groupHeaderFor(table); const gh = groupHeaderFor(table);
const ghH = gh ? gh.offsetHeight : 0; const ghH = gh && getComputedStyle(gh).position === 'sticky' ? gh.getBoundingClientRect().height : 0;
const firstRow = table.tHead && table.tHead.rows[0]; // 1px overlap tucks the table header under the group header: no hairline gap
if (!firstRow || !firstRow.offsetHeight) return; // hidden page/group: keep old values setVar(table, '--sticky-top', Math.max(0, navH + ghH - 1) + 'px');
setVar(table, '--sticky-top', (navH + ghH) + 'px');
setVar(table, '--thead-r1', firstRow.offsetHeight + 'px');
}); });
} }
@@ -121,6 +124,14 @@
// Groups and pages are shown by toggling display (not a DOM change), so also // 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. // re-measure as the user scrolls; writes only happen when a value changes.
window.addEventListener('scroll', schedule, { passive: true }); window.addEventListener('scroll', schedule, { passive: true });
// 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(); schedule();
} }
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);