88 Text ,
99} from "@radix-ui/themes" ;
1010import type React from "react" ;
11- import { useCallback , useState } from "react" ;
11+ import { useCallback , useEffect , useRef , useState } from "react" ;
1212import { useHotkeys } from "react-hotkeys-hook" ;
1313import { useTabStore } from "../stores/tabStore" ;
1414
@@ -27,6 +27,8 @@ export function TabBar() {
2727 const [ dropPosition , setDropPosition ] = useState < "left" | "right" | null > (
2828 null ,
2929 ) ;
30+ const [ showScrollGradient , setShowScrollGradient ] = useState ( false ) ;
31+ const scrollContainerRef = useRef < HTMLDivElement > ( null ) ;
3032
3133 // Keyboard navigation handlers
3234 const handlePrevTab = useCallback ( ( ) => {
@@ -166,101 +168,175 @@ export function TabBar() {
166168 setDropPosition ( null ) ;
167169 } , [ ] ) ;
168170
171+ const checkScrollGradient = useCallback ( ( ) => {
172+ const container = scrollContainerRef . current ;
173+ if ( ! container ) return ;
174+
175+ const canScrollRight =
176+ container . scrollWidth > container . clientWidth &&
177+ container . scrollLeft + container . clientWidth < container . scrollWidth - 1 ;
178+
179+ setShowScrollGradient ( canScrollRight ) ;
180+ } , [ ] ) ;
181+
182+ useEffect ( ( ) => {
183+ checkScrollGradient ( ) ;
184+
185+ const container = scrollContainerRef . current ;
186+ if ( ! container ) return ;
187+
188+ container . addEventListener ( "scroll" , checkScrollGradient ) ;
189+ window . addEventListener ( "resize" , checkScrollGradient ) ;
190+
191+ return ( ) => {
192+ container . removeEventListener ( "scroll" , checkScrollGradient ) ;
193+ window . removeEventListener ( "resize" , checkScrollGradient ) ;
194+ } ;
195+ } , [ checkScrollGradient ] ) ;
196+
197+ useEffect ( ( ) => {
198+ checkScrollGradient ( ) ;
199+ } , [ checkScrollGradient ] ) ;
200+
201+ useEffect ( ( ) => {
202+ const container = scrollContainerRef . current ;
203+ if ( ! container ) return ;
204+
205+ const activeTabElement = container . querySelector (
206+ `[data-tab-id="${ activeTabId } "]` ,
207+ ) ;
208+ if ( activeTabElement ) {
209+ activeTabElement . scrollIntoView ( {
210+ behavior : "smooth" ,
211+ block : "nearest" ,
212+ inline : "nearest" ,
213+ } ) ;
214+ }
215+ } , [ activeTabId ] ) ;
216+
169217 return (
170218 < Flex
171219 className = "drag border-gray-6 border-b"
172220 height = "40px"
173221 minHeight = "40px"
222+ position = "relative"
174223 >
175224 { /* Spacer for macOS window controls */ }
176225 < Box width = "80px" flexShrink = "0" />
177226
178- { tabs . map ( ( tab , index ) => {
179- const isDragging = draggedTab === tab . id ;
180- const isDragOver = dragOverTab === tab . id ;
181- const showLeftIndicator = isDragOver && dropPosition === "left" ;
182- const showRightIndicator = isDragOver && dropPosition === "right" ;
183-
184- return (
185- < ContextMenu . Root key = { tab . id } >
186- < ContextMenu . Trigger >
187- < Flex
188- className = { `no-drag group relative cursor-pointer border-gray-6 border-r border-b-2 transition-colors ${
189- tab . id === activeTabId
190- ? "border-b-accent-8 bg-accent-3 text-accent-12"
191- : "border-b-transparent text-gray-11 hover:bg-gray-3 hover:text-gray-12"
192- } ${ isDragging ? "opacity-50" : "" } `}
193- align = "center"
194- px = "4"
195- draggable
196- onClick = { ( ) => setActiveTab ( tab . id ) }
197- onDragStart = { ( e ) => handleDragStart ( e , tab . id ) }
198- onDragOver = { ( e ) => handleDragOver ( e , tab . id ) }
199- onDragLeave = { handleDragLeave }
200- onDrop = { ( e ) => handleDrop ( e , tab . id ) }
201- onDragEnd = { handleDragEnd }
202- >
203- { showLeftIndicator && (
204- < Box
205- className = "absolute top-0 bottom-0 left-0 z-10 w-0.5 bg-accent-8"
206- style = { { marginLeft : "-1px" } }
207- />
208- ) }
209-
210- { showRightIndicator && (
211- < Box
212- className = "absolute top-0 right-0 bottom-0 z-10 w-0.5 bg-accent-8"
213- style = { { marginRight : "-1px" } }
214- />
215- ) }
216- { index < 9 && (
217- < Kbd size = "1" className = "mr-2 opacity-70" >
218- { navigator . platform . includes ( "Mac" ) ? "⌘" : "Ctrl+" }
219- { index + 1 }
220- </ Kbd >
221- ) }
222-
223- < Text
224- size = "2"
225- className = "max-w-[200px] select-none overflow-hidden text-ellipsis whitespace-nowrap"
226- mr = "2"
227+ < Flex
228+ ref = { scrollContainerRef }
229+ className = "scrollbar-hide overflow-x-auto"
230+ flexGrow = "1"
231+ style = { {
232+ scrollbarWidth : "none" ,
233+ msOverflowStyle : "none" ,
234+ } }
235+ >
236+ { tabs . map ( ( tab , index ) => {
237+ const isDragging = draggedTab === tab . id ;
238+ const isDragOver = dragOverTab === tab . id ;
239+ const showLeftIndicator = isDragOver && dropPosition === "left" ;
240+ const showRightIndicator = isDragOver && dropPosition === "right" ;
241+
242+ return (
243+ < ContextMenu . Root key = { tab . id } >
244+ < ContextMenu . Trigger >
245+ < Flex
246+ data-tab-id = { tab . id }
247+ className = { `no-drag group relative cursor-pointer border-gray-6 border-r border-b-2 transition-colors ${
248+ tab . id === activeTabId
249+ ? "border-b-accent-8 bg-accent-3 text-accent-12"
250+ : "border-b-transparent text-gray-11 hover:bg-gray-3 hover:text-gray-12"
251+ } ${ isDragging ? "opacity-50" : "" } `}
252+ align = "center"
253+ px = "4"
254+ draggable
255+ onClick = { ( ) => setActiveTab ( tab . id ) }
256+ onDragStart = { ( e ) => handleDragStart ( e , tab . id ) }
257+ onDragOver = { ( e ) => handleDragOver ( e , tab . id ) }
258+ onDragLeave = { handleDragLeave }
259+ onDrop = { ( e ) => handleDrop ( e , tab . id ) }
260+ onDragEnd = { handleDragEnd }
227261 >
228- { tab . title }
229- </ Text >
230-
231- { tabs . length > 1 && (
232- < IconButton
233- size = "1"
234- variant = "ghost"
235- color = { tab . id !== activeTabId ? "gray" : undefined }
236- className = "opacity-0 transition-opacity group-hover:opacity-100"
237- onClick = { ( e ) => {
238- e . stopPropagation ( ) ;
239- closeTab ( tab . id ) ;
240- } }
262+ { showLeftIndicator && (
263+ < Box
264+ className = "absolute top-0 bottom-0 left-0 z-10 w-0.5 bg-accent-8"
265+ style = { { marginLeft : "-1px" } }
266+ />
267+ ) }
268+
269+ { showRightIndicator && (
270+ < Box
271+ className = "absolute top-0 right-0 bottom-0 z-10 w-0.5 bg-accent-8"
272+ style = { { marginRight : "-1px" } }
273+ />
274+ ) }
275+ { index < 9 && (
276+ < Kbd size = "1" className = "mr-2 opacity-70" >
277+ { navigator . platform . includes ( "Mac" ) ? "⌘" : "Ctrl+" }
278+ { index + 1 }
279+ </ Kbd >
280+ ) }
281+
282+ < Text
283+ size = "2"
284+ className = "max-w-[200px] select-none overflow-hidden text-ellipsis whitespace-nowrap"
285+ mr = "2"
241286 >
242- < Cross2Icon />
243- </ IconButton >
244- ) }
245- </ Flex >
246- </ ContextMenu . Trigger >
247- < ContextMenu . Content >
248- < ContextMenu . Item
249- disabled = { tabs . length === 1 }
250- onSelect = { ( ) => closeOtherTabs ( tab . id ) }
251- >
252- Close other tabs
253- </ ContextMenu . Item >
254- < ContextMenu . Item
255- disabled = { index === tabs . length - 1 }
256- onSelect = { ( ) => closeTabsToRight ( tab . id ) }
257- >
258- Close tabs to the right
259- </ ContextMenu . Item >
260- </ ContextMenu . Content >
261- </ ContextMenu . Root >
262- ) ;
263- } ) }
287+ { tab . title }
288+ </ Text >
289+
290+ { tabs . length > 1 && (
291+ < IconButton
292+ size = "1"
293+ variant = "ghost"
294+ color = { tab . id !== activeTabId ? "gray" : undefined }
295+ className = "opacity-0 transition-opacity group-hover:opacity-100"
296+ onClick = { ( e ) => {
297+ e . stopPropagation ( ) ;
298+ closeTab ( tab . id ) ;
299+ } }
300+ >
301+ < Cross2Icon />
302+ </ IconButton >
303+ ) }
304+ </ Flex >
305+ </ ContextMenu . Trigger >
306+ < ContextMenu . Content >
307+ < ContextMenu . Item
308+ disabled = { tabs . length === 1 }
309+ onSelect = { ( ) => closeOtherTabs ( tab . id ) }
310+ >
311+ Close other tabs
312+ </ ContextMenu . Item >
313+ < ContextMenu . Item
314+ disabled = { index === tabs . length - 1 }
315+ onSelect = { ( ) => closeTabsToRight ( tab . id ) }
316+ >
317+ Close tabs to the right
318+ </ ContextMenu . Item >
319+ </ ContextMenu . Content >
320+ </ ContextMenu . Root >
321+ ) ;
322+ } ) }
323+ </ Flex >
324+
325+ { showScrollGradient && (
326+ < Box
327+ position = "absolute"
328+ top = "0"
329+ right = "0"
330+ height = "40px"
331+ width = "80px"
332+ className = "pointer-events-none"
333+ style = { {
334+ background :
335+ "linear-gradient(to left, var(--color-background) 0%, transparent 100%)" ,
336+ zIndex : 10 ,
337+ } }
338+ />
339+ ) }
264340 </ Flex >
265341 ) ;
266342}
0 commit comments