From cde5be0874976f22d9ad7bba719bab67a8b49670 Mon Sep 17 00:00:00 2001 From: mroxso <24775431+mroxso@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:09:00 +0200 Subject: [PATCH] fix: keep a window in place when focus switches (#16) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raising a window made it visibly jump to the top-left corner and animate back. Two things combined: the enter keyframes animated `transform`, which overrode the inline `translate3d` a window's position is applied with, and the window layer rendered the z-sorted list, so focusing a window moved its DOM node and made the browser replay that animation. Animate the standalone `scale` property instead — it composes with the inline transform rather than replacing it — scope the animation to `.os-window` (it was hitting every child of the layer, including the snap preview, which is positioned the same way), and render windows in their stable creation order, since the inline `z-index` already handles stacking. Fixes #13 Claude-Session: https://claude.ai/code/session_01Trku191Ww2a2YmDWQF3fTS Co-authored-by: Claude Opus 5 --- src/components/os/WindowLayer.tsx | 10 ++++++++-- src/index.css | 15 +++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/components/os/WindowLayer.tsx b/src/components/os/WindowLayer.tsx index 4f3d517..ce76720 100644 --- a/src/components/os/WindowLayer.tsx +++ b/src/components/os/WindowLayer.tsx @@ -24,11 +24,17 @@ function SnapPreview({ zone }: { zone: Exclude }) { } export function WindowLayer() { - const { windows, focusedId } = useWindowManager(); + // Deliberately the unsorted list. `windows` from the context is sorted by + // z-order, so raising a window reorders the DOM nodes; the browser then + // replays the enter animation on the moved element. Stacking is already + // handled by the inline `z-index` each frame sets, so render order is free + // to stay stable. + const { state, focusedId } = useWindowManager(); + const windows = state.windows; const [snap, setSnap] = useState(null); return ( -
+
{snap && } {windows.map((win) => { diff --git a/src/index.css b/src/index.css index 6432f9a..936ea5b 100644 --- a/src/index.css +++ b/src/index.css @@ -308,14 +308,20 @@ body.os-dragging .os-window-content { /* ------------------------------------------------------------- Animations */ +/* + * Windows carry their position in an inline `transform: translate3d(...)`, so + * the opening animation must not touch `transform` — animating it would pin + * the window at 0,0 for the duration. The standalone `scale` property + * composes with the inline transform instead of replacing it. + */ @keyframes os-window-in { from { opacity: 0; - transform: scale(0.96); + scale: 0.96; } to { opacity: 1; - transform: scale(1); + scale: 1; } } @@ -328,7 +334,7 @@ body.os-dragging .os-window-content { } } -.os-window-enter > * { +.os-window { animation: os-window-in 160ms cubic-bezier(0.22, 1, 0.36, 1); } @@ -339,9 +345,6 @@ body.os-dragging .os-window-content { @media (prefers-reduced-motion: reduce) { .os-window { transition: none; - } - - .os-window-enter > * { animation: os-fade-in 120ms ease-out; } }