From 905cbf35e56aacd32903d4585e5c1a9f51cef107 Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Sun, 29 Mar 2026 16:56:09 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20hover=20preview=20dim=20effect=20?= =?UTF-8?q?=E2=80=94=20overlay=20dims=20page,=20hovered=20tile=20+=20popov?= =?UTF-8?q?er=20stand=20out?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.5s hover delay (up from 350ms) for deliberate preview. When popover appears, a semi-transparent dimmer covers the page (z-index 499). The hovered tile/sidebar-item gets z-index 500 (same as popover) so it appears above the dimmer alongside the preview. Both cleaned up on mouseleave via hidePreview(). --- muxplex/frontend/app.js | 24 ++++++++++++++++++++++-- muxplex/frontend/style.css | 14 ++++++++++++++ muxplex/frontend/tests/test_app.mjs | 17 +++++++++++++++++ muxplex/tests/test_frontend_css.py | 6 ++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index a9534b6..b79e3a8 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -127,6 +127,8 @@ let _notificationPermission = 'default'; let _pollFailCount = 0; let _previewPopover = null; let _previewTimer = null; +var _previewDimmer = null; +var _previewEl = null; // ─── DOM helpers ────────────────────────────────────────────────────────────── function $(id) { @@ -526,6 +528,16 @@ function showPreview(el, name) { hidePreview(); + // Dim layer — behind popover, above everything else + var dimmer = document.createElement('div'); + dimmer.className = 'preview-dimmer'; + document.body.appendChild(dimmer); + _previewDimmer = dimmer; + + // Lift the hovered element above the dimmer + el.classList.add(el.classList.contains('sidebar-item') ? 'item--previewing' : 'tile--previewing'); + _previewEl = el; // track so we can remove the class later + var popover = document.createElement('div'); popover.className = 'preview-popover'; var pre = document.createElement('pre'); @@ -592,6 +604,14 @@ function hidePreview() { _previewPopover.remove(); _previewPopover = null; } + if (_previewDimmer) { + _previewDimmer.remove(); + _previewDimmer = null; + } + if (_previewEl) { + _previewEl.classList.remove('tile--previewing', 'item--previewing'); + _previewEl = null; + } } // ─── Notification permission ──────────────────────────────────────────────── @@ -1103,7 +1123,7 @@ function bindStaticEventListeners() { gridEl.addEventListener('mouseenter', function (e) { var tile = e.target.closest('.session-tile'); if (!tile) return; - _previewTimer = setTimeout(function () { showPreview(tile); }, 350); + _previewTimer = setTimeout(function () { showPreview(tile); }, 1500); }, true); // useCapture: true for delegation with mouseenter gridEl.addEventListener('mouseleave', function (e) { @@ -1121,7 +1141,7 @@ function bindStaticEventListeners() { if (!item) return; var name = item.dataset.session; if (!name) return; - _previewTimer = setTimeout(function () { showPreview(item, name); }, 350); + _previewTimer = setTimeout(function () { showPreview(item, name); }, 1500); }, true); sidebarListEl.addEventListener('mouseleave', function (e) { diff --git a/muxplex/frontend/style.css b/muxplex/frontend/style.css index 58b2871..2e3e6dc 100644 --- a/muxplex/frontend/style.css +++ b/muxplex/frontend/style.css @@ -986,6 +986,20 @@ body { overflow-x: hidden; } +.preview-dimmer { + position: fixed; + inset: 0; + z-index: 499; /* behind popover (500), above everything else */ + background: rgba(0, 0, 0, 0.55); + pointer-events: none; /* clicks pass through — don't block mouseleave */ +} + +.session-tile.tile--previewing, +.sidebar-item.item--previewing { + position: relative; + z-index: 500; /* same as popover — above dimmer */ +} + /* ============================================================ Responsive overlay sidebar at <960px ============================================================ */ diff --git a/muxplex/frontend/tests/test_app.mjs b/muxplex/frontend/tests/test_app.mjs index eeb3fb3..d4b1c0c 100644 --- a/muxplex/frontend/tests/test_app.mjs +++ b/muxplex/frontend/tests/test_app.mjs @@ -2074,4 +2074,21 @@ test('hover preview popover works for both grid tiles and sidebar items', () => 'must handle sidebar items too'); }); +test('hover preview popover has dim overlay and highlights hovered element', () => { + const source = fs.readFileSync( + new URL('../app.js', import.meta.url), 'utf8' + ); + assert.ok(source.includes('preview-dimmer'), 'must create dimmer overlay'); + assert.ok(source.includes('tile--previewing'), 'must lift grid tile above dimmer'); + assert.ok(source.includes('item--previewing'), 'must lift sidebar item above dimmer'); +}); + +test('hover preview delay is 1500ms (not 350ms)', () => { + const source = fs.readFileSync( + new URL('../app.js', import.meta.url), 'utf8' + ); + assert.ok(source.includes('1500'), 'hover delay must be 1500ms'); + assert.ok(!source.includes(', 350)'), 'old 350ms delay must be removed'); +}); + diff --git a/muxplex/tests/test_frontend_css.py b/muxplex/tests/test_frontend_css.py index 03c4cdb..4e60904 100644 --- a/muxplex/tests/test_frontend_css.py +++ b/muxplex/tests/test_frontend_css.py @@ -631,3 +631,9 @@ def test_preview_popover_css_exists(): assert "position: fixed" in block or "position:fixed" in block, ( ".preview-popover must use position: fixed" ) + + +def test_preview_dimmer_css_exists(): + """Dim overlay behind popover must have CSS rules.""" + css = read_css() + assert ".preview-dimmer" in css