diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index f82020d..e811a80 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -523,30 +523,37 @@ function renderGrid(sessions) { // Hover preview popover (desktop only — no hover on touch devices) // --------------------------------------------------------------------------- +// Click handler registered while preview is showing — navigates to the previewed session +function _previewClickHandler(e) { + e.preventDefault(); + e.stopPropagation(); + var name = _previewSessionName; + hidePreview(); + if (name) openSession(name); +} + function showPreview(name) { if (!name || !_currentSessions) return; var session = _currentSessions.find(function (s) { return s.name === name; }); if (!session || !session.snapshot) return; - // If already showing this session's preview, just update content + position + // If already showing this session, just update content if (_previewPopover && _previewSessionName === name) { var pre = _previewPopover.querySelector('pre'); if (pre) pre.textContent = session.snapshot; - repositionPreview(); return; } - // New preview — clean slate (DOM only, no render trigger) hidePreviewDOM(); _previewSessionName = name; - // Dimmer layer — behind popover, above everything else + // Dimmer var dimmer = document.createElement('div'); dimmer.className = 'preview-dimmer'; document.body.appendChild(dimmer); _previewDimmer = dimmer; - // Popover + // Full-window overlay var popover = document.createElement('div'); popover.className = 'preview-popover'; var pre = document.createElement('pre'); @@ -555,67 +562,14 @@ function showPreview(name) { document.body.appendChild(popover); _previewPopover = popover; - // Lift the tile/item above the dimmer + position + // Lift the original tile above dimmer liftHoveredTile(); - repositionPreview(); - // Auto-scroll to bottom — the prompt/cursor area is the valuable part + // Auto-scroll to bottom (prompt area) popover.scrollTop = popover.scrollHeight; -} -function repositionPreview() { - if (!_previewPopover || !_previewSessionName) return; - - // Re-query DOM for the current element (may have been rebuilt by a render cycle) - var el = document.querySelector( - '.session-tile[data-session="' + _previewSessionName + '"], ' + - '.sidebar-item[data-session="' + _previewSessionName + '"]' - ); - if (!el) return; - - var rect = el.getBoundingClientRect(); - var popover = _previewPopover; - var left; - - if (rect.right + 288 < window.innerWidth) { - left = rect.right + 8; - } else if (rect.left > 288) { - left = rect.left - 8; // will be adjusted after width is set - } else { - // Fallback: center horizontally - left = Math.max(8, (window.innerWidth - 400) / 2); - } - - // Calculate available width for the popover - 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) { - top = window.innerHeight - popH - 16; - } - if (top < 8) top = 8; - - popover.style.left = left + 'px'; - popover.style.top = top + 'px'; + // Click anywhere navigates to previewed session + document.addEventListener('click', _previewClickHandler, true); } function liftHoveredTile() { @@ -636,6 +590,7 @@ function liftHoveredTile() { // hidePreviewDOM: removes the visual elements only (no render trigger) function hidePreviewDOM() { + document.removeEventListener('click', _previewClickHandler, true); if (_previewPopover) { _previewPopover.remove(); _previewPopover = null; diff --git a/muxplex/frontend/style.css b/muxplex/frontend/style.css index 2e3e6dc..961ee2e 100644 --- a/muxplex/frontend/style.css +++ b/muxplex/frontend/style.css @@ -968,21 +968,26 @@ body { z-index: 500; background: var(--bg-secondary); border: 1px solid var(--border); - border-radius: 6px; - padding: 10px 12px; - max-height: 80vh; + border-radius: 8px; + padding: 16px 20px; + /* Cover most of the viewport — centered with margin */ + top: 5vh; + left: 5vw; + right: 5vw; + bottom: 5vh; overflow-y: auto; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); - pointer-events: none; /* don't interfere with mouse leaving tile */ + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); + pointer-events: none; /* mouse events pass through to tiles below */ } .preview-popover pre { margin: 0; font-family: var(--font-mono); - font-size: 11px; - line-height: 1.4; - color: var(--text-muted); - white-space: pre; + font-size: 12px; + line-height: 1.5; + color: var(--text-dim); + white-space: pre-wrap; /* WRAP text instead of horizontal overflow */ + word-break: break-all; /* break long lines at viewport edge */ overflow-x: hidden; } diff --git a/muxplex/frontend/tests/test_app.mjs b/muxplex/frontend/tests/test_app.mjs index c92a590..975cbb2 100644 --- a/muxplex/frontend/tests/test_app.mjs +++ b/muxplex/frontend/tests/test_app.mjs @@ -13,6 +13,7 @@ globalThis.document = { querySelectorAll: () => [], createElement: () => ({ style: {}, classList: { add: () => {}, remove: () => {} } }), addEventListener: () => {}, + removeEventListener: () => {}, }; // Stubs for functions called by pollSessions (implemented in later tasks) @@ -2091,16 +2092,18 @@ test('hover preview delay is 1500ms (not 350ms)', () => { assert.ok(!source.includes(', 350)'), 'old 350ms delay must be removed'); }); -test('hover preview uses session name tracking instead of DOM element reference', () => { +test('hover preview uses full-window overlay with text wrapping', () => { const source = fs.readFileSync( new URL('../app.js', import.meta.url), 'utf8' ); - assert.ok(source.includes('_previewSessionName'), 'must track by session name, not DOM element'); - assert.ok(source.includes('repositionPreview'), 'must have repositionPreview for re-anchoring'); - assert.ok(source.includes('liftHoveredTile'), 'must re-lift tile after render cycles'); - assert.ok(source.includes('scrollHeight'), 'must auto-scroll popover to bottom'); + assert.ok(source.includes('_previewSessionName'), 'must track by session name'); + assert.ok(source.includes('liftHoveredTile'), 'must re-lift tile after renders'); + assert.ok(source.includes('scrollHeight'), 'must auto-scroll to bottom'); assert.ok(source.includes('preview-dimmer'), 'must have dim overlay'); assert.ok(source.includes('ontouchstart'), 'must be desktop-only'); + assert.ok(source.includes('_previewClickHandler'), 'must have click-to-navigate handler'); + // Must NOT have repositionPreview (old side-positioned approach) + assert.ok(!source.includes('repositionPreview'), 'must NOT have repositionPreview (old approach)'); }); test('renderGrid and renderSidebar re-lift hovered tile after innerHTML rebuild', () => {