@@ -15,16 +15,22 @@ import { useStateManager } from '../../../../stores/StateManager/StateManager.js
1515import './MissionControlUI.css' ;
1616import MissionManager from '../MissionManager/MissionManager.jsx' ;
1717
18- const FADE_DURATION = 300 ; // match CSS fade timing (ms)
19- const SLIDE_DURATION = 300 ; // match wrapper transition (ms)
20- const OPEN_DELAY = 200 ; // new delay before fade (ms)
18+ const FADE_DURATION = 300 ; // CSS fade timing (ms)
19+ const SLIDE_DURATION = 300 ; // overview slide timing (ms)
20+ const OPEN_DELAY = 200 ; // delay before fade (ms)
21+
22+ // Hint configuration
23+ const EDGE_THRESHOLD = 20 ; // px from either edge to trigger hint
24+ const HOVER_DELAY = 300 ; // ms to wait before showing hint (1 s)
25+ const HINT_OFFSET = 10 ; // px to slide as a hint
26+ const HINT_SLIDE_DURATION = 200 ; // ms for hint animation
2127
2228const MissionControlUI = ( ) => {
2329 const {
24- createDesktop, // original: adds & switches
25- addDesktop, // new: adds only
30+ createDesktop, // original: adds & switches
31+ addDesktop, // new: adds only
2632 switchDesktop,
27- deleteDesktop, // newly passed through
33+ deleteDesktop,
2834 reorderDesktops,
2935 activeIndex,
3036 desktops
@@ -34,69 +40,78 @@ const MissionControlUI = () => {
3440 const overlayVisible =
3541 state . groups . missionControl ?. overlayVisible === 'true' ;
3642
37- // New flag: render wallpaper at opacity 0 immediately when opening
43+ // Basic UI state
3844 const [ showWallpaperPlaceholder , setShowWallpaperPlaceholder ] = useState ( false ) ;
39- // track when each portal can mount (initially all false)
4045 const [ portalReady , setPortalReady ] = useState ( ( ) =>
4146 desktops . map ( ( ) => false )
4247 ) ;
4348
49+ // Hint state
50+ const [ showRightHint , setShowRightHint ] = useState ( false ) ;
51+ const [ showLeftHint , setShowLeftHint ] = useState ( false ) ;
52+ const hintTimerRef = useRef ( null ) ;
53+ const hoverSideRef = useRef ( null ) ; // 'left' | 'right' | null
54+ const isMouseDownRef = useRef ( false ) ; // track if a button is held
55+
56+ // Window size for edge detection
57+ const [ viewport , setViewport ] = useState ( {
58+ width : window . innerWidth ,
59+ height : window . innerHeight
60+ } ) ;
61+
62+ // Mission‐Control state
63+ const [ overviewOpen , setOverviewOpen ] = useState ( false ) ;
64+ const [ isFading , setIsFading ] = useState ( false ) ;
65+ const [ disableSlideTransition , setDisableSlideTransition ] = useState ( false ) ;
66+ const [ barExpanded , setBarExpanded ] = useState ( false ) ;
67+ const [ prevIndex , setPrevIndex ] = useState ( 0 ) ;
68+
69+ // Refs for tracking portal readiness
4470 const prevDesktopsRef = useRef ( desktops ) ;
4571 const prevPortalReadyRef = useRef ( portalReady ) ;
72+ const scaleRefs = useRef ( [ ] ) ;
4673
74+ // Sync portalReady when desktops array changes
4775 useEffect ( ( ) => {
48- const prevDesktops = prevDesktopsRef . current ;
76+ const prev = prevDesktopsRef . current ;
4977 const prevReady = prevPortalReadyRef . current ;
50- const prevLen = prevDesktops . length ;
51- const currLen = desktops . length ;
52- let newReady ;
53- if ( currLen > prevLen ) {
54- const diffCount = currLen - prevLen ;
55- newReady = [ ...prevReady , ...Array ( diffCount ) . fill ( false ) ] ;
78+ const delta = desktops . length - prev . length ;
79+ let nextReady ;
80+ if ( delta > 0 ) {
81+ nextReady = [ ...prevReady , ...Array ( delta ) . fill ( false ) ] ;
5682 } else {
57- const idToReady = { } ;
58- prevDesktops . forEach ( ( d , idx ) => {
59- idToReady [ d . id ] = prevReady [ idx ] ;
60- } ) ;
61- newReady = desktops . map ( d => idToReady [ d . id ] || false ) ;
83+ const byId = Object . fromEntries ( prev . map ( ( d , i ) => [ d . id , prevReady [ i ] ] ) ) ;
84+ nextReady = desktops . map ( d => ! ! byId [ d . id ] ) ;
6285 }
63- setPortalReady ( newReady ) ;
86+ setPortalReady ( nextReady ) ;
6487 prevDesktopsRef . current = desktops ;
65- prevPortalReadyRef . current = newReady ;
88+ prevPortalReadyRef . current = nextReady ;
6689 } , [ desktops ] ) ;
6790
68- const scaleRefs = useRef ( [ ] ) ;
91+ // Mark portals ready as soon as their DOM node mounts
6992 useLayoutEffect ( ( ) => {
7093 let changed = false ;
71- const latestReady = prevPortalReadyRef . current . slice ( ) ;
94+ const arr = prevPortalReadyRef . current . slice ( ) ;
7295 desktops . forEach ( ( _ , i ) => {
73- if ( ! latestReady [ i ] && scaleRefs . current [ i ] ) {
74- latestReady [ i ] = true ;
96+ if ( ! arr [ i ] && scaleRefs . current [ i ] ) {
97+ arr [ i ] = true ;
7598 changed = true ;
7699 }
77100 } ) ;
78101 if ( changed ) {
79- setPortalReady ( latestReady ) ;
80- prevPortalReadyRef . current = latestReady ;
102+ setPortalReady ( arr ) ;
103+ prevPortalReadyRef . current = arr ;
81104 }
82105 } ) ;
83106
84- const [ overviewOpen , setOverviewOpen ] = useState ( false ) ;
85- const [ isFading , setIsFading ] = useState ( false ) ;
86- const [ barExpanded , setBarExpanded ] = useState ( false ) ;
87- const [ prevIndex , setPrevIndex ] = useState ( 0 ) ;
88- const [ viewport , setViewport ] = useState ( {
89- width : window . innerWidth ,
90- height : window . innerHeight
91- } ) ;
92- const [ disableSlideTransition , setDisableSlideTransition ] = useState ( false ) ;
93-
107+ // Initialize 'opened' flag
94108 useEffect ( ( ) => {
95109 if ( ! state . groups . missionControl . hasOwnProperty ( 'opened' ) ) {
96110 addState ( 'missionControl' , 'opened' , 'false' ) ;
97111 }
98112 } , [ addState , state . groups . missionControl ] ) ;
99113
114+ // Clear lingering 'opened' on first mount
100115 const initialMount = useRef ( true ) ;
101116 useEffect ( ( ) => {
102117 if ( ! initialMount . current ) return ;
@@ -109,25 +124,113 @@ const MissionControlUI = () => {
109124 // eslint-disable-next-line react-hooks/exhaustive-deps
110125 } , [ ] ) ;
111126
127+ // Track viewport size
112128 useEffect ( ( ) => {
113129 const onResize = ( ) =>
114130 setViewport ( { width : window . innerWidth , height : window . innerHeight } ) ;
115131 window . addEventListener ( 'resize' , onResize ) ;
116132 return ( ) => window . removeEventListener ( 'resize' , onResize ) ;
117133 } , [ ] ) ;
118134
135+ // —— Edge‐hover hint (both sides), ignores when mouse is down ——
136+ useEffect ( ( ) => {
137+ if ( overviewOpen ) return ;
138+
139+ const canRight = desktops . length > activeIndex + 1 ;
140+ const canLeft = activeIndex > 0 ;
141+
142+ const onMouseDown = ( ) => { isMouseDownRef . current = true ; } ;
143+ const onMouseUp = ( ) => { isMouseDownRef . current = false ; } ;
144+
145+ const onMouseMove = e => {
146+ if ( isMouseDownRef . current ) return ; // ignore while dragging/clicking
147+
148+ const x = e . clientX ;
149+ const atRight = canRight && x >= viewport . width - EDGE_THRESHOLD ;
150+ const atLeft = canLeft && x <= EDGE_THRESHOLD ;
151+
152+ if ( atRight && hoverSideRef . current !== 'right' ) {
153+ clearTimeout ( hintTimerRef . current ) ;
154+ setShowLeftHint ( false ) ;
155+ setShowRightHint ( false ) ;
156+ hoverSideRef . current = 'right' ;
157+ hintTimerRef . current = setTimeout ( ( ) => {
158+ setShowRightHint ( true ) ;
159+ } , HOVER_DELAY ) ;
160+ } else if ( atLeft && hoverSideRef . current !== 'left' ) {
161+ clearTimeout ( hintTimerRef . current ) ;
162+ setShowRightHint ( false ) ;
163+ setShowLeftHint ( false ) ;
164+ hoverSideRef . current = 'left' ;
165+ hintTimerRef . current = setTimeout ( ( ) => {
166+ setShowLeftHint ( true ) ;
167+ } , HOVER_DELAY ) ;
168+ } else if ( ! atRight && ! atLeft && hoverSideRef . current ) {
169+ clearTimeout ( hintTimerRef . current ) ;
170+ hintTimerRef . current = null ;
171+ hoverSideRef . current = null ;
172+ setShowRightHint ( false ) ;
173+ setShowLeftHint ( false ) ;
174+ }
175+ } ;
176+
177+ window . addEventListener ( 'mousedown' , onMouseDown ) ;
178+ window . addEventListener ( 'mouseup' , onMouseUp ) ;
179+ window . addEventListener ( 'mousemove' , onMouseMove ) ;
180+ return ( ) => {
181+ window . removeEventListener ( 'mousedown' , onMouseDown ) ;
182+ window . removeEventListener ( 'mouseup' , onMouseUp ) ;
183+ window . removeEventListener ( 'mousemove' , onMouseMove ) ;
184+ clearTimeout ( hintTimerRef . current ) ;
185+ hintTimerRef . current = null ;
186+ hoverSideRef . current = null ;
187+ isMouseDownRef . current = false ;
188+ } ;
189+ } , [
190+ overviewOpen ,
191+ viewport . width ,
192+ activeIndex ,
193+ desktops . length
194+ ] ) ;
195+
196+ // —— Click to complete switch when hint is visible ——
119197 useEffect ( ( ) => {
120- document . body . style . overflow = ( overviewOpen || isFading ) ? 'hidden' : '' ;
198+ if ( ! showRightHint && ! showLeftHint ) return ;
199+
200+ const onClick = e => {
201+ const x = e . clientX ;
202+ if ( showRightHint && x >= viewport . width - EDGE_THRESHOLD ) {
203+ switchDesktop ( activeIndex + 1 ) ;
204+ setShowRightHint ( false ) ;
205+ } else if ( showLeftHint && x <= EDGE_THRESHOLD ) {
206+ switchDesktop ( activeIndex - 1 ) ;
207+ setShowLeftHint ( false ) ;
208+ }
209+ } ;
210+
211+ window . addEventListener ( 'click' , onClick ) ;
212+ return ( ) => window . removeEventListener ( 'click' , onClick ) ;
213+ } , [
214+ showRightHint ,
215+ showLeftHint ,
216+ viewport . width ,
217+ activeIndex ,
218+ switchDesktop
219+ ] ) ;
220+
221+ // Prevent body scroll during overview/fade
222+ useEffect ( ( ) => {
223+ document . body . style . overflow = overviewOpen || isFading ? 'hidden' : '' ;
121224 } , [ overviewOpen , isFading ] ) ;
122225
226+ // Fade → open overview
123227 useEffect ( ( ) => {
124- if ( isFading ) {
125- const timer = setTimeout ( ( ) => {
126- setOverviewOpen ( true ) ;
127- setIsFading ( false ) ;
128- } , FADE_DURATION ) ;
129- return ( ) => clearTimeout ( timer ) ;
130- }
228+ if ( ! isFading ) return ;
229+ const t = setTimeout ( ( ) => {
230+ setOverviewOpen ( true ) ;
231+ setIsFading ( false ) ;
232+ } , FADE_DURATION ) ;
233+ return ( ) => clearTimeout ( t ) ;
131234 } , [ isFading ] ) ;
132235
133236 const openOverview = useCallback ( ( ) => {
@@ -144,17 +247,16 @@ const MissionControlUI = () => {
144247 } , [ activeIndex , editStateValue ] ) ;
145248
146249 const instantSwitchDesktop = useCallback (
147- index => {
250+ i => {
148251 setDisableSlideTransition ( true ) ;
149- switchDesktop ( index ) ;
252+ switchDesktop ( i ) ;
150253 } ,
151254 [ switchDesktop ]
152255 ) ;
153256 useEffect ( ( ) => {
154- if ( disableSlideTransition ) {
155- const t = setTimeout ( ( ) => setDisableSlideTransition ( false ) , 0 ) ;
156- return ( ) => clearTimeout ( t ) ;
157- }
257+ if ( ! disableSlideTransition ) return ;
258+ const t = setTimeout ( ( ) => setDisableSlideTransition ( false ) , 0 ) ;
259+ return ( ) => clearTimeout ( t ) ;
158260 } , [ disableSlideTransition ] ) ;
159261
160262 const exitOverview = useCallback (
@@ -194,20 +296,30 @@ const MissionControlUI = () => {
194296 [ reorderDesktops ]
195297 ) ;
196298
197- const THUMB_H = 90 ;
198- const scale = THUMB_H / viewport . height ;
199- const THUMB_W = viewport . width * scale ;
299+ // Compute transform & transition
300+ const baseX = `calc(-${ activeIndex } * (100vw + 60px))` ;
301+ const rightHintX = `calc(${ baseX } - ${ HINT_OFFSET } px)` ;
302+ const leftHintX = `calc(${ baseX } + ${ HINT_OFFSET } px)` ;
303+ const wrapperTransform = showRightHint
304+ ? `translateX(${ rightHintX } )`
305+ : showLeftHint
306+ ? `translateX(${ leftHintX } )`
307+ : `translateX(${ baseX } )` ;
200308 const wrapperStyle = overviewOpen
201309 ? {
202310 top : 30 ,
203- height : THUMB_H ,
204- marginleft : 0 ,
311+ height : 90 ,
312+ marginLeft : 0 ,
205313 transform : 'none' ,
206314 transition : `top ${ SLIDE_DURATION } ms ease, height ${ SLIDE_DURATION } ms ease, transform ${ SLIDE_DURATION } ms ease`
207315 }
208316 : {
209- transform : `translateX(calc(-${ activeIndex } * (100vw + 60px)))` ,
210- transition : disableSlideTransition ? 'none' : undefined
317+ transform : wrapperTransform ,
318+ ...( disableSlideTransition
319+ ? { transition : 'none' }
320+ : ( showRightHint || showLeftHint )
321+ ? { transition : `transform ${ HINT_SLIDE_DURATION } ms ease` }
322+ : { } )
211323 } ;
212324
213325 return (
@@ -262,8 +374,8 @@ const MissionControlUI = () => {
262374 onDragOver = { onDragOver }
263375 onDrop = { onDrop }
264376 viewport = { viewport }
265- createDesktop = { addDesktop } // +New from bar
266- deleteDesktop = { deleteDesktop } // allow deletion per-panel
377+ createDesktop = { addDesktop }
378+ deleteDesktop = { deleteDesktop }
267379 />
268380 </ div >
269381 ) ;
0 commit comments