Sub-issue of #13158 (dock layouts). Reported by the operator on the Workstation, 2026-09-02 15:35Z: "rails slide in animations are nice now, however, the team forgot about the opposite direction => slide out on focus leave". The entry motion is #18074 / PR #18079, which scoped this out explicitly ("Exit motion — the hidden state is display: none; a two-phase hide is its own leaf if wanted"). This is that leaf.
The Problem
Neo.dashboard.dock.interaction.RevealOverlay#syncSnapshot adds neo-dashboard-dock-reveal-overlay-hidden the moment visible turns false, and that class is display: none (resources/scss/src/dashboard/Container.scss). The reveal's content therefore emerges from its strip on entry and vanishes in one frame on dismissal — a dismissal by focus leave, Escape, an outside click, or the pointer leaving after dwell all cut. The asymmetry reads as a glitch next to the entry, and it is the more frequent of the two motions on a rail-heavy layout.
The Architectural Reality
RevealOverlay#syncRevealMotion opens a MotionSignal window on the hidden→visible boundary and closes it on visible→hidden; onMotionAnimationEnd closes it on the ROOT's own animationend (target identity; the children's ends bubble past). destroy() settles without an end event.
The entry keyframes (neo-dock-reveal-fade on the root, neo-dock-reveal-from-<edge> on the header and pane slot, 100cqw/100cqh travel against the root's size container) read --dock-transition-duration-reveal, which reduced motion collapses to 0ms at the token layer.
The iframe shield (:has(.neo-dashboard-dock-reveal-overlay:not(…-hidden))) and the rail's outside-pointer listener key on the hidden class and on the machine state respectively.
The Fix
A two-phase hide, owned by the overlay:
On a visible→hidden boundary with a mounted DOM, the overlay stamps neo-dashboard-dock-reveal-overlay-leaving instead of -hidden and keeps the motion window open. The leaving rules run the reverse of the entry — root fades out, header and slot travel back to the edge — with animation-fill-mode: forwards, so the content holds its end state until the hidden class lands one round trip later, and pointer-events: none, so the departing panel never takes a click meant for what is under it.
On the root's animationend while leaving, the overlay applies -hidden, drops -leaving, and settles the motion window — the same seam the entry already uses. Reduced motion collapses the duration to 0ms, which still fires the end event.
A re-entry while leaving cancels the leave (drop -leaving, stay visible); teardown while leaving hides and settles without an end event, as today. A stuck exit (no end event — a consumer sheet that sets animation: none) hides after the motion signal's settle grace rather than never.
The iframe shield excludes a leaving overlay, so a click into a frame during the exit lands in the frame.
Acceptance Criteria
AC-1 Unit (DockRevealOverlay.spec.mjs): a visible→hidden flip on a mounted overlay stamps -leaving, not -hidden, and keeps the motion window open; the root's animationend then stamps -hidden, drops -leaving, and settles; a hidden→visible flip while leaving cancels the leave; destroy while leaving settles. Red on dev for the first.
AC-2 Component (dock-rail-origin or dock-rail fixture): reveal, dismiss with Escape, and the overlay carries -leaving with a running animation on its pane slot before it becomes hidden; red on dev (hidden lands at once).
AC-3 The Workstation's rails, read by eye on the served app: the panel slides back under its strip on focus leave and on Escape, both neo themes.
AC-4 The existing reveal, rail, iframe-dismissal and motion-signal suites stay green (unit/dashboard, component/dashboard).
Out of Scope
A reverse slide-in from the current position on a re-entry during a leave. What happens instead, and it is load-bearing: dropping the leaving class changes animation-name back, so the browser restarts the ENTRY keyframes from the edge, and that restart's end event is what settles the still-open motion window (corrected after PR #18118's review — the original line claimed a snap-back with no restart).
Any change to the entry motion or its tokens.
Live latest-open sweep at 2026-09-02T15:36Z: no reveal-exit ticket (#18115 is the native-drop hold, #18074 is the entry, closed by PR #18079); A2A: no claim; Knowledge Base: the motion contract tickets (#13280, #14779, #14780), no leaf.
Authored by Mnemosyne (@neo-fable, Claude Fable 5.1, Claude Code) · session 37bbc610-f0cd-4abb-95c2-eb70336a86f3
tobiu referenced in commit 9360b3b - "feat(dock): a dismissed rail reveal slides back under its strip before it hides (#18116) (#18118) on Sep 2, 2026, 6:21 PM
Sub-issue of #13158 (dock layouts). Reported by the operator on the Workstation, 2026-09-02 15:35Z: "rails slide in animations are nice now, however, the team forgot about the opposite direction => slide out on focus leave". The entry motion is #18074 / PR #18079, which scoped this out explicitly ("Exit motion — the hidden state is
display: none; a two-phase hide is its own leaf if wanted"). This is that leaf.The Problem
Neo.dashboard.dock.interaction.RevealOverlay#syncSnapshotaddsneo-dashboard-dock-reveal-overlay-hiddenthe momentvisibleturns false, and that class isdisplay: none(resources/scss/src/dashboard/Container.scss). The reveal's content therefore emerges from its strip on entry and vanishes in one frame on dismissal — a dismissal by focus leave, Escape, an outside click, or the pointer leaving after dwell all cut. The asymmetry reads as a glitch next to the entry, and it is the more frequent of the two motions on a rail-heavy layout.The Architectural Reality
RevealOverlay#syncRevealMotionopens aMotionSignalwindow on the hidden→visible boundary and closes it on visible→hidden;onMotionAnimationEndcloses it on the ROOT's ownanimationend(target identity; the children's ends bubble past).destroy()settles without an end event.neo-dock-reveal-fadeon the root,neo-dock-reveal-from-<edge>on the header and pane slot,100cqw/100cqhtravel against the root's size container) read--dock-transition-duration-reveal, which reduced motion collapses to0msat the token layer.:has(.neo-dashboard-dock-reveal-overlay:not(…-hidden))) and the rail's outside-pointer listener key on the hidden class and on the machine state respectively.The Fix
A two-phase hide, owned by the overlay:
neo-dashboard-dock-reveal-overlay-leavinginstead of-hiddenand keeps the motion window open. The leaving rules run the reverse of the entry — root fades out, header and slot travel back to the edge — withanimation-fill-mode: forwards, so the content holds its end state until the hidden class lands one round trip later, andpointer-events: none, so the departing panel never takes a click meant for what is under it.animationendwhile leaving, the overlay applies-hidden, drops-leaving, and settles the motion window — the same seam the entry already uses. Reduced motion collapses the duration to0ms, which still fires the end event.-leaving, stay visible); teardown while leaving hides and settles without an end event, as today. A stuck exit (no end event — a consumer sheet that setsanimation: none) hides after the motion signal's settle grace rather than never.Acceptance Criteria
DockRevealOverlay.spec.mjs): a visible→hidden flip on a mounted overlay stamps-leaving, not-hidden, and keeps the motion window open; the root'sanimationendthen stamps-hidden, drops-leaving, and settles; a hidden→visible flip while leaving cancels the leave; destroy while leaving settles. Red ondevfor the first.dock-rail-originordock-railfixture): reveal, dismiss with Escape, and the overlay carries-leavingwith a running animation on its pane slot before it becomes hidden; red ondev(hidden lands at once).unit/dashboard,component/dashboard).Out of Scope
animation-nameback, so the browser restarts the ENTRY keyframes from the edge, and that restart's end event is what settles the still-open motion window (corrected after PR #18118's review — the original line claimed a snap-back with no restart).Live latest-open sweep at 2026-09-02T15:36Z: no reveal-exit ticket (#18115 is the native-drop hold, #18074 is the entry, closed by PR #18079); A2A: no claim; Knowledge Base: the motion contract tickets (#13280, #14779, #14780), no leaf.
Authored by Mnemosyne (@neo-fable, Claude Fable 5.1, Claude Code) · session 37bbc610-f0cd-4abb-95c2-eb70336a86f3