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,158 @@ 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+
169201 return (
170202 < Flex
171203 className = "drag border-gray-6 border-b"
172204 height = "40px"
173205 minHeight = "40px"
206+ position = "relative"
174207 >
175208 { /* Spacer for macOS window controls */ }
176209 < Box width = "80px" flexShrink = "0" />
177210
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"
211+ < Flex
212+ ref = { scrollContainerRef }
213+ className = "scrollbar-hide overflow-x-auto"
214+ flexGrow = "1"
215+ style = { {
216+ scrollbarWidth : "none" ,
217+ msOverflowStyle : "none" ,
218+ } }
219+ >
220+ { tabs . map ( ( tab , index ) => {
221+ const isDragging = draggedTab === tab . id ;
222+ const isDragOver = dragOverTab === tab . id ;
223+ const showLeftIndicator = isDragOver && dropPosition === "left" ;
224+ const showRightIndicator = isDragOver && dropPosition === "right" ;
225+
226+ return (
227+ < ContextMenu . Root key = { tab . id } >
228+ < ContextMenu . Trigger >
229+ < Flex
230+ className = { `no-drag group relative cursor-pointer border-gray-6 border-r border-b-2 transition-colors ${
231+ tab . id === activeTabId
232+ ? "border-b-accent-8 bg-accent-3 text-accent-12"
233+ : "border-b-transparent text-gray-11 hover:bg-gray-3 hover:text-gray-12"
234+ } ${ isDragging ? "opacity-50" : "" } `}
235+ align = "center"
236+ px = "4"
237+ draggable
238+ onClick = { ( ) => setActiveTab ( tab . id ) }
239+ onDragStart = { ( e ) => handleDragStart ( e , tab . id ) }
240+ onDragOver = { ( e ) => handleDragOver ( e , tab . id ) }
241+ onDragLeave = { handleDragLeave }
242+ onDrop = { ( e ) => handleDrop ( e , tab . id ) }
243+ onDragEnd = { handleDragEnd }
227244 >
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- } }
245+ { showLeftIndicator && (
246+ < Box
247+ className = "absolute top-0 bottom-0 left-0 z-10 w-0.5 bg-accent-8"
248+ style = { { marginLeft : "-1px" } }
249+ />
250+ ) }
251+
252+ { showRightIndicator && (
253+ < Box
254+ className = "absolute top-0 right-0 bottom-0 z-10 w-0.5 bg-accent-8"
255+ style = { { marginRight : "-1px" } }
256+ />
257+ ) }
258+ { index < 9 && (
259+ < Kbd size = "1" className = "mr-2 opacity-70" >
260+ { navigator . platform . includes ( "Mac" ) ? "⌘" : "Ctrl+" }
261+ { index + 1 }
262+ </ Kbd >
263+ ) }
264+
265+ < Text
266+ size = "2"
267+ className = "max-w-[200px] select-none overflow-hidden text-ellipsis whitespace-nowrap"
268+ mr = "2"
241269 >
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- } ) }
270+ { tab . title }
271+ </ Text >
272+
273+ { tabs . length > 1 && (
274+ < IconButton
275+ size = "1"
276+ variant = "ghost"
277+ color = { tab . id !== activeTabId ? "gray" : undefined }
278+ className = "opacity-0 transition-opacity group-hover:opacity-100"
279+ onClick = { ( e ) => {
280+ e . stopPropagation ( ) ;
281+ closeTab ( tab . id ) ;
282+ } }
283+ >
284+ < Cross2Icon />
285+ </ IconButton >
286+ ) }
287+ </ Flex >
288+ </ ContextMenu . Trigger >
289+ < ContextMenu . Content >
290+ < ContextMenu . Item
291+ disabled = { tabs . length === 1 }
292+ onSelect = { ( ) => closeOtherTabs ( tab . id ) }
293+ >
294+ Close other tabs
295+ </ ContextMenu . Item >
296+ < ContextMenu . Item
297+ disabled = { index === tabs . length - 1 }
298+ onSelect = { ( ) => closeTabsToRight ( tab . id ) }
299+ >
300+ Close tabs to the right
301+ </ ContextMenu . Item >
302+ </ ContextMenu . Content >
303+ </ ContextMenu . Root >
304+ ) ;
305+ } ) }
306+ </ Flex >
307+
308+ { showScrollGradient && (
309+ < Box
310+ position = "absolute"
311+ top = "0"
312+ right = "0"
313+ height = "40px"
314+ width = "80px"
315+ className = "pointer-events-none"
316+ style = { {
317+ background :
318+ "linear-gradient(to left, var(--color-background) 0%, transparent 100%)" ,
319+ zIndex : 10 ,
320+ } }
321+ />
322+ ) }
264323 </ Flex >
265324 ) ;
266325}
0 commit comments