1- import { useMemo } from 'react' ;
1+ import { useMemo , useRef , useState , useEffect , useCallback } from 'react' ;
2+ import { ChevronLeftIcon , ChevronRightIcon } from 'lucide-react' ;
23import { cn } from '../../lib/utils' ;
34import { ResultsTable } from './ResultsTable' ;
45import type { ResultTab } from './types' ;
@@ -28,11 +29,41 @@ export function ResultsTabs({
2829 onTabClose,
2930 isLoading,
3031} : ResultsTabsProps ) {
32+ const tabsContainerRef = useRef < HTMLDivElement > ( null ) ;
33+ const [ canScrollLeft , setCanScrollLeft ] = useState ( false ) ;
34+ const [ canScrollRight , setCanScrollRight ] = useState ( false ) ;
35+
3136 const activeTab = useMemo (
3237 ( ) => tabs . find ( ( tab ) => tab . id === activeTabId ) ,
3338 [ tabs , activeTabId ]
3439 ) ;
3540
41+ const updateScrollButtons = useCallback ( ( ) => {
42+ const container = tabsContainerRef . current ;
43+ if ( ! container ) return ;
44+
45+ const { scrollLeft, scrollWidth, clientWidth } = container ;
46+ setCanScrollLeft ( scrollLeft > 0 ) ;
47+ setCanScrollRight ( scrollLeft + clientWidth < scrollWidth - 1 ) ;
48+ } , [ ] ) ;
49+
50+ useEffect ( ( ) => {
51+ updateScrollButtons ( ) ;
52+ window . addEventListener ( 'resize' , updateScrollButtons ) ;
53+ return ( ) => window . removeEventListener ( 'resize' , updateScrollButtons ) ;
54+ } , [ updateScrollButtons , tabs ] ) ;
55+
56+ const scroll = ( direction : 'left' | 'right' ) => {
57+ const container = tabsContainerRef . current ;
58+ if ( ! container ) return ;
59+
60+ const scrollAmount = 150 ;
61+ container . scrollBy ( {
62+ left : direction === 'left' ? - scrollAmount : scrollAmount ,
63+ behavior : 'smooth' ,
64+ } ) ;
65+ } ;
66+
3667 // Loading state (no tabs yet)
3768 if ( isLoading && tabs . length === 0 ) {
3869 return (
@@ -56,44 +87,79 @@ export function ResultsTabs({
5687 return (
5788 < div className = "space-y-2" >
5889 { /* Tab bar */ }
59- < div className = "flex items-center gap-1 border-b border-border overflow-x-auto overflow-y-hidden" >
60- { tabs . map ( ( tab ) => (
90+ < div className = "relative flex items-center border-b border-border" >
91+ { /* Left scroll button */ }
92+ { canScrollLeft && (
6193 < button
62- key = { tab . id }
6394 type = "button"
64- onClick = { ( ) => onTabSelect ( tab . id ) }
65- className = { cn (
66- 'group flex items-center gap-1.5 px-3 py-1.5 text-sm whitespace-nowrap' ,
67- 'border-b-2 -mb-px transition-colors cursor-pointer' ,
68- tab . id === activeTabId
69- ? 'border-primary text-foreground'
70- : 'border-transparent text-muted-foreground hover:text-foreground'
71- ) }
95+ onClick = { ( ) => scroll ( 'left' ) }
96+ className = "absolute left-0 z-10 flex items-center justify-center w-6 h-full bg-background hover:bg-muted cursor-pointer"
97+ aria-label = "Scroll tabs left"
7298 >
73- < span > { formatTimestamp ( tab . timestamp ) } </ span >
74- { tab . error && (
75- < span className = "w-1.5 h-1.5 rounded-full bg-destructive" aria-label = "Error" />
76- ) }
77- < span
78- role = "button"
79- tabIndex = { 0 }
80- aria-label = "Close tab"
81- onClick = { ( e ) => {
82- e . stopPropagation ( ) ;
83- onTabClose ( tab . id ) ;
84- } }
85- onKeyDown = { ( e ) => {
86- if ( e . key === 'Enter' || e . key === ' ' ) {
99+ < ChevronLeftIcon className = "w-4 h-4 text-muted-foreground" />
100+ </ button >
101+ ) }
102+
103+ { /* Tabs container */ }
104+ < div
105+ ref = { tabsContainerRef }
106+ onScroll = { updateScrollButtons }
107+ className = { cn (
108+ "flex items-center gap-1 overflow-hidden" ,
109+ canScrollLeft && "pl-6" ,
110+ canScrollRight && "pr-6"
111+ ) }
112+ >
113+ { tabs . map ( ( tab ) => (
114+ < button
115+ key = { tab . id }
116+ type = "button"
117+ onClick = { ( ) => onTabSelect ( tab . id ) }
118+ className = { cn (
119+ 'group flex items-center gap-1.5 px-3 py-1.5 text-sm whitespace-nowrap' ,
120+ 'border-b-2 -mb-px transition-colors cursor-pointer' ,
121+ tab . id === activeTabId
122+ ? 'border-primary text-foreground'
123+ : 'border-transparent text-muted-foreground hover:text-foreground'
124+ ) }
125+ >
126+ < span > { formatTimestamp ( tab . timestamp ) } </ span >
127+ { tab . error && (
128+ < span className = "w-1.5 h-1.5 rounded-full bg-destructive" aria-label = "Error" />
129+ ) }
130+ < span
131+ role = "button"
132+ tabIndex = { 0 }
133+ aria-label = "Close tab"
134+ onClick = { ( e ) => {
87135 e . stopPropagation ( ) ;
88136 onTabClose ( tab . id ) ;
89- }
90- } }
91- className = "opacity-0 group-hover:opacity-100 hover:bg-muted rounded p-0.5 transition-opacity"
92- >
93- < XIcon className = "w-3 h-3" />
94- </ span >
137+ } }
138+ onKeyDown = { ( e ) => {
139+ if ( e . key === 'Enter' || e . key === ' ' ) {
140+ e . stopPropagation ( ) ;
141+ onTabClose ( tab . id ) ;
142+ }
143+ } }
144+ className = "opacity-0 group-hover:opacity-100 hover:bg-muted rounded p-0.5 transition-opacity"
145+ >
146+ < XIcon className = "w-3 h-3" />
147+ </ span >
148+ </ button >
149+ ) ) }
150+ </ div >
151+
152+ { /* Right scroll button */ }
153+ { canScrollRight && (
154+ < button
155+ type = "button"
156+ onClick = { ( ) => scroll ( 'right' ) }
157+ className = "absolute right-0 z-10 flex items-center justify-center w-6 h-full bg-background hover:bg-muted cursor-pointer"
158+ aria-label = "Scroll tabs right"
159+ >
160+ < ChevronRightIcon className = "w-4 h-4 text-muted-foreground" />
95161 </ button >
96- ) ) }
162+ ) }
97163 </ div >
98164
99165 { /* Active tab content */ }
0 commit comments