diff --git a/changelogs/CHANGELOG-annotation-panel-overlay-fix.md b/changelogs/CHANGELOG-annotation-panel-overlay-fix.md new file mode 100644 index 0000000..2a709ab --- /dev/null +++ b/changelogs/CHANGELOG-annotation-panel-overlay-fix.md @@ -0,0 +1,18 @@ +# Fix: Annotation Thread Panel Renders With Empty Overlay Box + +- Fixed the annotation Q&A panel opening with its content squeezed to the left and a large empty white box covering the right portion +- Root cause: the right-side Threads dock was created and left visible (320px wide) even when no thread was docked, painting an invisible overlay over every floating panel. Dock visibility was managed via inconsistent inline `display` styles. +- Fix: the dock is now `display:none` by default in CSS and shown only via `body.ai-tag-dock-open`, which is added exclusively when a panel is actually docked (and removed when the dock empties) +- Also: messages area sizes to content (`flex:0 1 auto`) instead of growing to fill an oversized panel; floating-panel positioning now hard-clamps to the viewport on all four edges; removed `backdrop-filter: blur()` (compositing artifacts on a fixed/draggable element) for a solid background; mobile panel height floored (`min-height`, `max-height:70vh`) instead of a collapsing `60vh` + +--- + +## Summary + +A regression in the new reading-companion thread panels: an empty dock element overlaid the right side of every floating Q&A panel, clipping the conversation into a narrow left strip. The dock is now hidden unless it actually holds a docked thread, and several panel-sizing/positioning robustness fixes were made. Verified at desktop resolution via Playwright (the live preview iframe is 1px tall and can't validate layout heights). + +--- + +## Testing +- Verified at 1280×800 via Playwright: panel renders full-width with header/toolbar/messages/input visible; hit-test at the previously-empty region now returns a message element, not the dock; docking still reflows the document (1280 → 960) and shows the dock only when used. +- Smoke suite 22/22; build clean. diff --git a/css/ai-tags.css b/css/ai-tags.css index 69a803d..3cf19ca 100644 --- a/css/ai-tags.css +++ b/css/ai-tags.css @@ -160,11 +160,12 @@ body.ai-tags-hidden .ai-tag-pill-anchor { z-index: 10001; width: 400px; max-height: 520px; - background: rgba(13, 17, 23, 0.96); + /* Solid background — backdrop-filter:blur on a fixed, draggable element caused + a compositing artifact that clipped content and painted an empty overlay box. */ + background: #161b22; border: 1px solid rgba(99, 110, 123, 0.3); border-radius: 14px; - box-shadow: 0 12px 48px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.03); - backdrop-filter: blur(16px); + box-shadow: 0 12px 48px rgba(0, 0, 0, 0.5); display: flex; flex-direction: column; overflow: hidden; @@ -175,7 +176,7 @@ body.ai-tags-hidden .ai-tag-pill-anchor { to { opacity: 1; transform: translateY(0) scale(1); } } [data-theme="light"] .ai-tag-thread-panel { - background: rgba(255, 255, 255, 0.97); + background: #ffffff; border-color: rgba(0, 0, 0, 0.12); box-shadow: 0 12px 48px rgba(0, 0, 0, 0.15); } @@ -280,13 +281,15 @@ body.ai-tags-hidden .ai-tag-pill-anchor { /* Thread messages area */ .ai-tag-thread-messages { - flex: 1; + /* Size to content (scroll past 300px) rather than flex-growing to fill an + oversized panel — otherwise a short thread leaves a large empty void. */ + flex: 0 1 auto; overflow-y: auto; padding: 10px 14px; display: flex; flex-direction: column; gap: 10px; - min-height: 80px; + min-height: 60px; max-height: 300px; } .ai-tag-thread-msg { @@ -558,12 +561,17 @@ body.ai-tags-hidden .ai-tag-pill-anchor { width: var(--ai-tag-dock-width); height: 100vh; z-index: 9000; - display: flex; + /* Hidden unless the dock actually holds a thread — body.ai-tag-dock-open shows it. + This prevents an empty dock from painting an overlay box over the document. */ + display: none; flex-direction: column; - background: rgba(13, 17, 23, 0.98); + background: #161b22; border-left: 1px solid rgba(99, 110, 123, 0.3); box-shadow: -8px 0 32px rgba(0, 0, 0, 0.35); } +body.ai-tag-dock-open .ai-tag-thread-dock { + display: flex; +} [data-theme="light"] .ai-tag-thread-dock { background: rgba(250, 251, 252, 0.99); border-left-color: rgba(0, 0, 0, 0.1); @@ -632,8 +640,9 @@ body.ai-tag-dock-open .app-container { @media (max-width: 640px) { .ai-tag-thread-panel { width: 100vw; - height: 60vh; - max-height: 60vh; + height: auto; + max-height: 70vh; + min-height: 240px; bottom: 0; left: 0 !important; right: 0; diff --git a/js/ai-tags.js b/js/ai-tags.js index 9609b41..d907dea 100644 --- a/js/ai-tags.js +++ b/js/ai-tags.js @@ -1115,22 +1115,32 @@ } // Place a floating panel near its anchor, cascading so multiples don't stack - // exactly on top of each other. + // exactly on top of each other. Clamps to the viewport on all four edges so a + // panel never opens partly off-screen or taller than the window. function positionFloatingPanel(panel, anchorEl) { + var vw = window.innerWidth, vh = window.innerHeight; + var panelWidth = Math.min(400, vw - 24); + var panelHeight = Math.min(520, vh - 24); var n = Object.keys(openPanels).length; var offset = ((n - 1) % 6) * 28; - var panelWidth = 400, panelHeight = 520; + var left, top; if (anchorEl) { var rect = anchorEl.getBoundingClientRect(); - left = Math.min(rect.right + 8, window.innerWidth - panelWidth - 16); - top = Math.min(rect.top - 20, window.innerHeight - panelHeight - 16); + // Prefer to the right of the anchor; if no room, sit to its left. + left = rect.right + 8; + if (left + panelWidth > vw - 16) left = rect.left - panelWidth - 8; + top = rect.top - 20; } else { - left = (window.innerWidth - panelWidth) / 2; - top = (window.innerHeight - panelHeight) / 2; + left = (vw - panelWidth) / 2; + top = (vh - panelHeight) / 2; } - left = Math.max(16, left - offset); - top = Math.max(16, top + offset); + // Apply cascade, then hard-clamp inside the viewport on every edge. + left = left - offset; + top = top + offset; + left = Math.max(12, Math.min(left, vw - panelWidth - 12)); + top = Math.max(12, Math.min(top, vh - panelHeight - 12)); + panel.style.position = 'fixed'; panel.style.left = left + 'px'; panel.style.top = top + 'px'; @@ -1238,18 +1248,16 @@ var total = Object.keys(openPanels).length; var countEl = threadDock.querySelector('.ai-tag-dock-count'); if (countEl) countEl.textContent = total; - // hide the dock chrome when nothing is docked - threadDock.style.display = docked > 0 ? '' : 'none'; - if (docked > 0) document.body.classList.add('ai-tag-dock-open'); + // Visibility is driven purely by the body class (CSS shows the dock only then). + document.body.classList.toggle('ai-tag-dock-open', docked > 0); } - // Remove the dock (and un-reflow the document) once it has no docked panels. + // Un-reflow the document once the dock has no docked panels. function maybeCollapseDock(force) { if (!threadDock) return; var docked = threadDock.querySelectorAll('.ai-tag-thread-panel').length; if (docked === 0 || force) { document.body.classList.remove('ai-tag-dock-open'); - if (docked === 0) { threadDock.style.display = 'none'; } } }