fix: hover preview — bottom-anchor, flexible width, sidebar support, longer delay
- Auto-scroll to bottom (prompt area is the valuable part) - Dynamic width based on available space (fixes 3-column layout) - 350ms delay (prevents brush-over flicker) - Extend hover preview to sidebar items (popover to the right) - Remove fixed min/max-width from CSS, calculate in JS
This commit is contained in:
+57
-12
@@ -515,8 +515,10 @@ function renderGrid(sessions) {
|
|||||||
// Hover preview popover (desktop only — no hover on touch devices)
|
// Hover preview popover (desktop only — no hover on touch devices)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function showPreview(tile) {
|
function showPreview(el, name) {
|
||||||
var name = tile.dataset.session;
|
// el = the DOM element to position relative to
|
||||||
|
// name = session name (optional — reads from el.dataset.session if omitted)
|
||||||
|
if (!name) name = el.dataset.session;
|
||||||
if (!name || !_currentSessions) return;
|
if (!name || !_currentSessions) return;
|
||||||
|
|
||||||
var session = _currentSessions.find(function (s) { return s.name === name; });
|
var session = _currentSessions.find(function (s) { return s.name === name; });
|
||||||
@@ -531,20 +533,42 @@ function showPreview(tile) {
|
|||||||
popover.appendChild(pre);
|
popover.appendChild(pre);
|
||||||
document.body.appendChild(popover);
|
document.body.appendChild(popover);
|
||||||
|
|
||||||
// Position: right of tile if room, else left
|
// Position: try right of element, then left
|
||||||
var rect = tile.getBoundingClientRect();
|
var rect = el.getBoundingClientRect();
|
||||||
var popW = popover.offsetWidth;
|
var left;
|
||||||
var popH = popover.offsetHeight;
|
|
||||||
var left, top;
|
|
||||||
|
|
||||||
if (rect.right + popW + 12 < window.innerWidth) {
|
if (rect.right + 288 < window.innerWidth) {
|
||||||
left = rect.right + 8;
|
left = rect.right + 8;
|
||||||
|
} else if (rect.left > 288) {
|
||||||
|
left = rect.left - 8; // will be adjusted after width is set
|
||||||
} else {
|
} else {
|
||||||
left = rect.left - popW - 8;
|
// Fallback: center horizontally
|
||||||
|
left = Math.max(8, (window.innerWidth - 400) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vertically: align top with tile, clamp to viewport
|
// Calculate available width for the popover
|
||||||
top = rect.top;
|
var availW;
|
||||||
|
if (left >= rect.right) {
|
||||||
|
availW = window.innerWidth - left - 16;
|
||||||
|
} else if (left < rect.left) {
|
||||||
|
availW = rect.left - 16;
|
||||||
|
} else {
|
||||||
|
availW = window.innerWidth - 32;
|
||||||
|
}
|
||||||
|
// Clamp between 280px (minimum readable) and 640px (comfortable max)
|
||||||
|
var popWidth = Math.min(Math.max(availW, 280), 640);
|
||||||
|
popover.style.width = popWidth + 'px';
|
||||||
|
|
||||||
|
// If positioned to the left, adjust left edge now that we know width
|
||||||
|
var fallback = Math.max(8, (window.innerWidth - 400) / 2);
|
||||||
|
if (left < rect.left && left !== fallback) {
|
||||||
|
left = rect.left - popWidth - 8;
|
||||||
|
if (left < 8) left = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertically: align top with element, clamp to viewport
|
||||||
|
var popH = popover.offsetHeight;
|
||||||
|
var top = rect.top;
|
||||||
if (top + popH > window.innerHeight - 16) {
|
if (top + popH > window.innerHeight - 16) {
|
||||||
top = window.innerHeight - popH - 16;
|
top = window.innerHeight - popH - 16;
|
||||||
}
|
}
|
||||||
@@ -553,6 +577,9 @@ function showPreview(tile) {
|
|||||||
popover.style.left = left + 'px';
|
popover.style.left = left + 'px';
|
||||||
popover.style.top = top + 'px';
|
popover.style.top = top + 'px';
|
||||||
|
|
||||||
|
// Auto-scroll to bottom — the prompt/cursor area is the valuable part
|
||||||
|
popover.scrollTop = popover.scrollHeight;
|
||||||
|
|
||||||
_previewPopover = popover;
|
_previewPopover = popover;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1076,7 +1103,7 @@ function bindStaticEventListeners() {
|
|||||||
gridEl.addEventListener('mouseenter', function (e) {
|
gridEl.addEventListener('mouseenter', function (e) {
|
||||||
var tile = e.target.closest('.session-tile');
|
var tile = e.target.closest('.session-tile');
|
||||||
if (!tile) return;
|
if (!tile) return;
|
||||||
_previewTimer = setTimeout(function () { showPreview(tile); }, 100);
|
_previewTimer = setTimeout(function () { showPreview(tile); }, 350);
|
||||||
}, true); // useCapture: true for delegation with mouseenter
|
}, true); // useCapture: true for delegation with mouseenter
|
||||||
|
|
||||||
gridEl.addEventListener('mouseleave', function (e) {
|
gridEl.addEventListener('mouseleave', function (e) {
|
||||||
@@ -1085,6 +1112,24 @@ function bindStaticEventListeners() {
|
|||||||
hidePreview();
|
hidePreview();
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hover preview — delegated on sidebar list (items are re-rendered each poll)
|
||||||
|
var sidebarListEl = $('sidebar-list');
|
||||||
|
if (sidebarListEl && !('ontouchstart' in window)) { // desktop only
|
||||||
|
sidebarListEl.addEventListener('mouseenter', function (e) {
|
||||||
|
var item = e.target.closest('.sidebar-item');
|
||||||
|
if (!item) return;
|
||||||
|
var name = item.dataset.session;
|
||||||
|
if (!name) return;
|
||||||
|
_previewTimer = setTimeout(function () { showPreview(item, name); }, 350);
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
sidebarListEl.addEventListener('mouseleave', function (e) {
|
||||||
|
var item = e.target.closest('.sidebar-item');
|
||||||
|
if (!item) return;
|
||||||
|
hidePreview();
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Test-only helpers ────────────────────────────────────────────────────────
|
// ─── Test-only helpers ────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -970,8 +970,6 @@ body {
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
min-width: 480px;
|
|
||||||
max-width: 640px;
|
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||||
|
|||||||
@@ -2062,4 +2062,16 @@ test('app.js has hover preview popover with desktop-only guard', () => {
|
|||||||
assert.ok(source.includes('hidePreview'), 'must have cleanup on mouseleave');
|
assert.ok(source.includes('hidePreview'), 'must have cleanup on mouseleave');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('hover preview popover works for both grid tiles and sidebar items', () => {
|
||||||
|
const source = fs.readFileSync(
|
||||||
|
new URL('../app.js', import.meta.url), 'utf8'
|
||||||
|
);
|
||||||
|
assert.ok(source.includes('preview-popover'), 'must create popover');
|
||||||
|
assert.ok(source.includes('ontouchstart'), 'desktop-only guard');
|
||||||
|
assert.ok(source.includes('session.snapshot'), 'uses full snapshot');
|
||||||
|
assert.ok(source.includes('scrollHeight'), 'auto-scrolls to bottom');
|
||||||
|
assert.ok(source.includes('sidebar-list') || source.includes('sidebar-item'),
|
||||||
|
'must handle sidebar items too');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user