22 * UntrackedStatus - Shows untracked files count with interactive tooltip
33 */
44
5- import React , { useState , useEffect , useRef } from "react" ;
5+ import React , { useState , useEffect , useRef , useLayoutEffect } from "react" ;
6+ import { createPortal } from "react-dom" ;
67import { cn } from "@/common/lib/utils" ;
78
89interface UntrackedStatusProps {
@@ -22,10 +23,40 @@ export const UntrackedStatus: React.FC<UntrackedStatusProps> = ({
2223 const [ isLoading , setIsLoading ] = useState ( false ) ;
2324 const [ showTooltip , setShowTooltip ] = useState ( false ) ;
2425 const [ isTracking , setIsTracking ] = useState ( false ) ;
26+ const [ popupPosition , setPopupPosition ] = useState < { top : number ; right : number } | null > ( null ) ;
2527 const containerRef = useRef < HTMLDivElement > ( null ) ;
28+ const popupRef = useRef < HTMLDivElement > ( null ) ;
2629 const hasLoadedOnce = useRef ( false ) ;
2730 const loadingRef = useRef ( false ) ; // Prevent concurrent loads
2831
32+ // Calculate popup position when shown
33+ useLayoutEffect ( ( ) => {
34+ if ( ! showTooltip || ! containerRef . current ) {
35+ setPopupPosition ( null ) ;
36+ return ;
37+ }
38+
39+ const updatePosition = ( ) => {
40+ const container = containerRef . current ;
41+ if ( ! container ) return ;
42+
43+ const rect = container . getBoundingClientRect ( ) ;
44+ setPopupPosition ( {
45+ top : rect . bottom + 8 , // 8px gap below anchor
46+ right : window . innerWidth - rect . right ,
47+ } ) ;
48+ } ;
49+
50+ updatePosition ( ) ;
51+
52+ window . addEventListener ( "resize" , updatePosition ) ;
53+ window . addEventListener ( "scroll" , updatePosition , true ) ;
54+ return ( ) => {
55+ window . removeEventListener ( "resize" , updatePosition ) ;
56+ window . removeEventListener ( "scroll" , updatePosition , true ) ;
57+ } ;
58+ } , [ showTooltip ] ) ;
59+
2960 // Load untracked files
3061 useEffect ( ( ) => {
3162 let cancelled = false ;
@@ -78,7 +109,10 @@ export const UntrackedStatus: React.FC<UntrackedStatusProps> = ({
78109 if ( ! showTooltip ) return ;
79110
80111 const handleClickOutside = ( e : MouseEvent ) => {
81- if ( containerRef . current && ! containerRef . current . contains ( e . target as Node ) ) {
112+ const target = e . target as Node ;
113+ const clickedContainer = containerRef . current ?. contains ( target ) ;
114+ const clickedPopup = popupRef . current ?. contains ( target ) ;
115+ if ( ! clickedContainer && ! clickedPopup ) {
82116 setShowTooltip ( false ) ;
83117 }
84118 } ;
@@ -121,7 +155,7 @@ export const UntrackedStatus: React.FC<UntrackedStatusProps> = ({
121155 const hasUntracked = count > 0 ;
122156
123157 return (
124- < div ref = { containerRef } className = "relative inline-block" >
158+ < div ref = { containerRef } className = "inline-block" >
125159 < div
126160 className = { cn (
127161 "py-1 px-2.5 rounded font-medium text-[11px] whitespace-nowrap transition-all duration-200" ,
@@ -134,35 +168,43 @@ export const UntrackedStatus: React.FC<UntrackedStatusProps> = ({
134168 { isLoading ? "..." : `${ count } Untracked` }
135169 </ div >
136170
137- { showTooltip && hasUntracked && (
138- < div className = "bg-modal-bg border-bg-medium animate-in fade-in slide-in-from-top-1 absolute top-[calc(100%+8px)] right-0 z-[1000] max-w-96 min-w-48 rounded border p-2 shadow-[0_4px_12px_rgba(0,0,0,0.3)] duration-150" >
139- < div className = "text-foreground border-border-light mb-2 border-b pb-1.5 text-[11px] font-semibold" >
140- Untracked Files ({ count } )
141- </ div >
142- < div className = "mb-2 max-h-[200px] overflow-y-auto" >
143- { untrackedFiles . map ( ( file ) => (
144- < div
145- key = { file }
146- className = "text-label hover:bg-bg-subtle truncate px-1 py-0.5 font-mono text-[11px]"
147- >
148- { file }
149- </ div >
150- ) ) }
151- </ div >
152- < button
153- onClick = { ( ) => void handleTrackAll ( ) }
154- disabled = { isTracking }
155- className = { cn (
156- "w-full py-1 px-2 bg-transparent text-muted border border-border-medium rounded text-[11px] cursor-pointer transition-all duration-200 font-primary" ,
157- "hover:bg-white-overlay-light hover:text-foreground hover:border-border-subtle" ,
158- "active:bg-white-overlay" ,
159- "disabled:text-border-darker disabled:border-border disabled:cursor-not-allowed disabled:bg-transparent"
160- ) }
171+ { showTooltip &&
172+ hasUntracked &&
173+ popupPosition &&
174+ createPortal (
175+ < div
176+ ref = { popupRef }
177+ className = "bg-modal-bg border-bg-medium animate-in fade-in slide-in-from-top-1 fixed z-[1000] max-w-96 min-w-48 rounded border p-2 shadow-[0_4px_12px_rgba(0,0,0,0.3)] duration-150"
178+ style = { { top : popupPosition . top , right : popupPosition . right } }
161179 >
162- { isTracking ? "Tracking..." : "Track All" }
163- </ button >
164- </ div >
165- ) }
180+ < div className = "text-foreground border-border-light mb-2 border-b pb-1.5 text-[11px] font-semibold" >
181+ Untracked Files ({ count } )
182+ </ div >
183+ < div className = "mb-2 max-h-[200px] overflow-y-auto" >
184+ { untrackedFiles . map ( ( file ) => (
185+ < div
186+ key = { file }
187+ className = "text-label hover:bg-bg-subtle truncate px-1 py-0.5 font-mono text-[11px]"
188+ >
189+ { file }
190+ </ div >
191+ ) ) }
192+ </ div >
193+ < button
194+ onClick = { ( ) => void handleTrackAll ( ) }
195+ disabled = { isTracking }
196+ className = { cn (
197+ "w-full py-1 px-2 bg-transparent text-muted border border-border-medium rounded text-[11px] cursor-pointer transition-all duration-200 font-primary" ,
198+ "hover:bg-white-overlay-light hover:text-foreground hover:border-border-subtle" ,
199+ "active:bg-white-overlay" ,
200+ "disabled:text-border-darker disabled:border-border disabled:cursor-not-allowed disabled:bg-transparent"
201+ ) }
202+ >
203+ { isTracking ? "Tracking..." : "Track All" }
204+ </ button >
205+ </ div > ,
206+ document . body
207+ ) }
166208 </ div >
167209 ) ;
168210} ;
0 commit comments