@@ -840,11 +840,12 @@ export function MarchingAntsRect({ width, height, isDoor, color, paused }: {
840840 ) ;
841841}
842842
843- function SelectionOverlay ( { apiRef, selectedId, selectedType, mode } : {
843+ function SelectionOverlay ( { apiRef, selectedId, selectedType, mode, overlayElRef } : {
844844 apiRef : React . RefObject < DockviewApi | null > ;
845845 selectedId : string | null ;
846846 selectedType : 'pane' | 'door' ;
847847 mode : PondMode ;
848+ overlayElRef ?: React . RefObject < HTMLDivElement | null > ;
848849} ) {
849850 const { elements : panelElements , version : panelVersion } = useContext ( PanelElementsContext ) ;
850851 const { elements : doorElements , version : doorVersion } = useContext ( DoorElementsContext ) ;
@@ -907,11 +908,11 @@ function SelectionOverlay({ apiRef, selectedId, selectedType, mode }: {
907908 if ( mode === 'passthrough' ) {
908909 style . borderRadius = isDoor ? '0.375rem 0.375rem 0 0' : '0.5rem' ;
909910 style . border = `1px solid ${ selectionColor } ` ;
910- return < div style = { style } /> ;
911+ return < div ref = { overlayElRef } style = { style } /> ;
911912 }
912913
913914 return (
914- < div style = { style } >
915+ < div ref = { overlayElRef } style = { style } >
915916 < MarchingAntsRect
916917 width = { rect . width }
917918 height = { rect . height }
@@ -1007,6 +1008,7 @@ function orchestrateKill(
10071008 selectPanel : ( id : string ) => void ,
10081009 setSelectedId : ( id : string | null ) => void ,
10091010 killInProgressRef : { current : boolean } ,
1011+ overlayElRef : { current : HTMLElement | null } ,
10101012) : void {
10111013 const panel = api . getPanel ( killedId ) ;
10121014 if ( ! panel ) return ;
@@ -1029,8 +1031,19 @@ function orchestrateKill(
10291031 }
10301032
10311033 // Fade the killed pane in place. Block input on it during the fade.
1034+ // For a last-pane kill (auto-spawn will create a replacement), also shrink
1035+ // the pane toward the bottom-right so the disappearance is visible — a plain
1036+ // fade offers no visual cue since the pane's space is reclaimed by a new one
1037+ // appearing in exactly the same rect from the opposite corner. The focus
1038+ // ring (SelectionOverlay element) gets a matching shrink animation so it
1039+ // scales with the pane rather than sitting over empty space.
1040+ const isLastPane = api . panels . length === 1 ;
1041+ const fadeClass = isLastPane ? 'pane-fading-and-shrinking-to-br' : 'pane-fading-out' ;
1042+ const fadeAnimationName = isLastPane ? 'pane-fade-and-shrink-to-br' : 'pane-fade-out' ;
10321043 killedGroupEl . style . pointerEvents = 'none' ;
1033- killedGroupEl . classList . add ( 'pane-fading-out' ) ;
1044+ killedGroupEl . classList . add ( fadeClass ) ;
1045+ const overlayEl = isLastPane ? overlayElRef . current : null ;
1046+ if ( overlayEl ) overlayEl . classList . add ( 'ring-shrinking-to-br' ) ;
10341047
10351048 let finalized = false ;
10361049 const finalize = ( ) => {
@@ -1077,10 +1090,15 @@ function orchestrateKill(
10771090 pre . el . addEventListener ( 'transitionend' , cleanup , { once : true } ) ;
10781091 setTimeout ( cleanup , 1000 ) ;
10791092 }
1093+
1094+ // Peel the ring-shrink class so the next selection's overlay renders at
1095+ // full scale. The element may have been reused by React for the next
1096+ // selected pane's overlay by the time the animation finishes.
1097+ if ( overlayEl ) overlayEl . classList . remove ( 'ring-shrinking-to-br' ) ;
10801098 } ;
10811099
10821100 killedGroupEl . addEventListener ( 'animationend' , ( ev ) => {
1083- if ( ( ev as AnimationEvent ) . animationName !== 'pane-fade-out' ) return ;
1101+ if ( ( ev as AnimationEvent ) . animationName !== fadeAnimationName ) return ;
10841102 finalize ( ) ;
10851103 } ) ;
10861104 // Safety: if animationend never fires, still finalize.
@@ -1129,6 +1147,10 @@ export function Pond({
11291147 // the in-place fade) so the auto-spawn shouldn't re-delay another 440ms.
11301148 const killInProgressRef = useRef ( false ) ;
11311149
1150+ // Ref to the SelectionOverlay's root element. orchestrateKill uses it to
1151+ // animate the focus ring in sync with the killed pane's shrink (last-pane case).
1152+ const overlayElRef = useRef < HTMLDivElement | null > ( null ) ;
1153+
11321154 // Consumed once in handleReady to restore existing sessions
11331155 const initialPaneIdsRef = useRef ( initialPaneIds ) ;
11341156 const restoredLayoutRef = useRef ( restoredLayout ) ;
@@ -1480,8 +1502,10 @@ export function Pond({
14801502 selectPanel ( id ) ;
14811503 }
14821504 } ;
1483- if ( delay === 0 ) spawn ( ) ;
1484- else setTimeout ( spawn , delay ) ;
1505+ // Always defer via setTimeout — even when delay is 0 — so api.addPanel is
1506+ // not called re-entrantly from inside the onDidRemovePanel handler (dockview
1507+ // silently drops the spawn in that case).
1508+ setTimeout ( spawn , delay ) ;
14851509 } ) ;
14861510
14871511 onApiReady ?.( e . api ) ;
@@ -1591,7 +1615,7 @@ export function Pond({
15911615 // survivors, handling selection updates itself (since the fade is
15921616 // async and selection must wait until the actual removal fires).
15931617 setConfirmKill ( null ) ;
1594- orchestrateKill ( api , ck . id , selectPanel , setSelectedId , killInProgressRef ) ;
1618+ orchestrateKill ( api , ck . id , selectPanel , setSelectedId , killInProgressRef , overlayElRef ) ;
15951619 return ;
15961620 }
15971621 // Wrong key — shake then dismiss
@@ -2010,7 +2034,7 @@ export function Pond({
20102034 theme = { mousetermTheme }
20112035 singleTabMode = "fullwidth"
20122036 />
2013- < SelectionOverlay apiRef = { apiRef } selectedId = { selectedId } selectedType = { selectedType } mode = { mode } />
2037+ < SelectionOverlay apiRef = { apiRef } selectedId = { selectedId } selectedType = { selectedType } mode = { mode } overlayElRef = { overlayElRef } />
20142038 </ div >
20152039 </ div >
20162040
0 commit comments