fix: keep a window in place when focus switches (#16)

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 <noreply@anthropic.com>
This commit is contained in:
mroxso
2026-09-06 12:09:00 +02:00
committed by GitHub
parent 949a551492
commit cde5be0874
2 changed files with 17 additions and 8 deletions

View File

@@ -24,11 +24,17 @@ function SnapPreview({ zone }: { zone: Exclude<SnapZone, null> }) {
}
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<SnapZone>(null);
return (
<div className="os-window-enter pointer-events-none absolute inset-0">
<div className="pointer-events-none absolute inset-0">
{snap && <SnapPreview zone={snap} />}
{windows.map((win) => {

View File

@@ -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;
}
}