diff --git a/static/js/modalSnap.js b/static/js/modalSnap.js index 305c829..9dd2158 100644 --- a/static/js/modalSnap.js +++ b/static/js/modalSnap.js @@ -426,11 +426,16 @@ function _applyDockInternal(modal, side, dockClass) { // its padding-right. if (!modal._dockCloseWatcher && typeof MutationObserver !== 'undefined') { const onGone = () => _onDockedModalGone(modal, dockClass); - // Watch the modal itself for hidden-class flips and parent removal. - const obs = new MutationObserver(() => { - if (!modal.isConnected || modal.classList.contains('hidden')) onGone(); - }); - obs.observe(modal, { attributes: true, attributeFilter: ['class'] }); + // Watch the modal for: the `.hidden` class flip, an inline + // `display:none` (how the draggable modals — calendar, plan, workspace, + // etc. — actually close), and parent removal. Without the `style` filter + // a display:none close left the body's dock padding on, so the chat + // stayed shifted after the docked modal was closed. + const _isGone = () => !modal.isConnected + || modal.classList.contains('hidden') + || modal.style.display === 'none'; + const obs = new MutationObserver(() => { if (_isGone()) onGone(); }); + obs.observe(modal, { attributes: true, attributeFilter: ['class', 'style'] }); // A second observer catches DOM removal — childList on the parent // is the reliable signal for `.remove()` / `.removeChild()` calls. if (modal.parentNode) { @@ -475,6 +480,25 @@ function _onDockedModalGone(modal, dockClass) { } modal.classList.remove('modal-right-docked'); modal.classList.remove('modal-left-docked'); + // Clear the content's docked inline geometry. Singleton modals (plan, + // workspace, calendar, …) reuse the same element across open/close, so if we + // only drop the body push the element stays positioned (position:fixed; + // right:0; fixed width) on the next open — floating over the chat with no + // push. We deliberately do NOT restore the pre-dock snapshot here: that + // snapshot is the drag position from when the user pulled the window to the + // edge (near the side), so restoring it would reopen the modal off to the + // side, still overlapping. Clearing the inline styles lets the modal reopen + // at its CSS default (centered). Drag-to-undock still uses clearRightDock, + // which DOES restore the snapshot for the peel-off feel. + if (_c) { + for (const prop of ['position', 'inset', 'left', 'top', 'right', 'bottom', + 'width', 'maxWidth', 'height', 'maxHeight', + 'borderRadius', 'transform', 'margin']) { + _c.style[prop] = ''; + } + delete _c._preDockSnapshot; + delete _c._dockSide; + } } function _expandSidebarFromRail() { diff --git a/static/style.css b/static/style.css index dcda5bf..7ee6aee 100644 --- a/static/style.css +++ b/static/style.css @@ -87,6 +87,11 @@ html, body { overflow-x: hidden; height: 100%; margin: 0; overscroll-behavior: n body { background-color: var(--bg); color: var(--fg); + /* Animate the dock push BOTH ways. Keeping the transition on the base body + (not on .right/left-dock-active) means removing the class on undock also + animates padding back to 0 — otherwise the chat snapped back instantly. */ + transition: padding-left 160ms cubic-bezier(0.22, 0.61, 0.36, 1), + padding-right 160ms cubic-bezier(0.22, 0.61, 0.36, 1); font-family: var(--font-family, 'Fira Code', monospace); display: flex; height: 100%; @@ -14546,11 +14551,9 @@ body [data-act="from-sender"] { fit instead of being hidden behind the panel. */ body.right-dock-active { padding-right: var(--right-dock-w, 0px); - transition: padding-right 160ms cubic-bezier(0.22, 0.61, 0.36, 1); } body.left-dock-active { padding-left: var(--left-dock-w, 0px); - transition: padding-left 160ms cubic-bezier(0.22, 0.61, 0.36, 1); } .modal.modal-right-docked { align-items: stretch;