feat: hover preview dim effect — overlay dims page, hovered tile + popover stand out

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().
This commit is contained in:
Brian Krabach
2026-03-29 16:56:09 -07:00
parent 40ec20f4ba
commit 905cbf35e5
4 changed files with 59 additions and 2 deletions
+22 -2
View File
@@ -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) {
+14
View File
@@ -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
============================================================ */
+17
View File
@@ -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');
});
+6
View File
@@ -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