From d24be4a2d662d8929a897d52c2f1e3c3889ae1ba Mon Sep 17 00:00:00 2001 From: Johan Sandoval Date: Tue, 6 Oct 2026 16:58:05 +0000 Subject: [PATCH] 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 --- web/demo/demo.css | 3 +-- web/index.html | 8 +++--- web/layout.css | 62 +++++++++++++++++++++++++++++++++++++---------- web/layout.js | 25 +++++++++++++------ 4 files changed, 73 insertions(+), 25 deletions(-) diff --git a/web/demo/demo.css b/web/demo/demo.css index 9b73e1a..fd711db 100644 --- a/web/demo/demo.css +++ b/web/demo/demo.css @@ -5,8 +5,7 @@ font-family: 'Segoe UI', sans-serif; box-shadow: 0 -4px 16px rgba(0,0,0,0.35); } -body { padding-bottom: 40px; } -body.usmt-term-open { padding-bottom: calc(var(--usmt-term-h, 340px) + 72px); } +/* Page bottom padding for the dock is set in layout.css from its measured height */ .usmt-term-resize { height: 5px; cursor: ns-resize; background: transparent; } .usmt-term.collapsed .usmt-term-resize { display: none; } diff --git a/web/index.html b/web/index.html index 6099eed..129bc96 100755 --- a/web/index.html +++ b/web/index.html @@ -459,7 +459,7 @@
- Missing Software + All Devices
-
Select a document from the left...
+
Select an article from the list to read it.
@@ -880,6 +880,8 @@ let swVis = Array.from(swRows).filter(tr => tr.style.display !== 'none').length; let sc = document.getElementById('softwareCount'); if(sc) sc.innerText = `(${swVis})`; + let sgc = document.getElementById('softwareGroupCount'); + if(sgc) sgc.innerText = `(${swVis})`; } let globalDataLastUpdated = "Never"; @@ -2242,7 +2244,7 @@ if (res.ok) { 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('docEditor').style.display = 'none'; currentDocTitle = ""; diff --git a/web/layout.css b/web/layout.css index e111d63..922634f 100644 --- a/web/layout.css +++ b/web/layout.css @@ -10,25 +10,45 @@ * 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). + * through that group, then hands over to the next group's header. Beneath it the table's + * header (column names + filters on desktop, the filter panel on phones) sticks as one + * 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; } +.master-table:not(#table-custom) thead { position: sticky; top: var(--sticky-top, 112px); z-index: 20; } +.master-table:not(#table-custom) thead tr.filter-row th { + /* 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); +} + @media (min-width: 1101px) { - /* An overflowing wrapper would capture the sticky rows; these tables never need to + /* 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; } - .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); - } + /* 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 */ table.master-table thead tr.filter-row { 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 { 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); } + /* 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 */ #table-custom thead tr.card-label-row { display: flex; flex-wrap: wrap; gap: 6px; padding: 10px 12px; @@ -134,6 +161,15 @@ @media (min-width: 1101px) { .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) { table.master-table tbody td { padding-left: 7.8em; } table.master-table tbody td::before { width: 7.2em; } diff --git a/web/layout.js b/web/layout.js index ed2bf58..7ab4c4a 100644 --- a/web/layout.js +++ b/web/layout.js @@ -87,16 +87,19 @@ return null; } function updateSticky() { + const root = document.documentElement; const nav = document.querySelector('.top-nav'); - const navH = nav ? nav.offsetHeight : 60; - setVar(document.documentElement, '--nav-h', navH + 'px'); + 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 ? 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'); + const ghH = gh && getComputedStyle(gh).position === 'sticky' ? gh.getBoundingClientRect().height : 0; + // 1px overlap tucks the table header under the group header: no hairline gap + setVar(table, '--sticky-top', Math.max(0, navH + ghH - 1) + 'px'); }); } @@ -121,6 +124,14 @@ // 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 }); + // 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);