1- import { useMemo } from 'react' ;
1+ import { useMemo , useRef , useState , useEffect , useCallback } from 'react' ;
22import { cn } from '../../lib/utils' ;
33import { ResultsTable } from './ResultsTable' ;
44import type { ResultTab } from './types' ;
@@ -21,18 +21,64 @@ function formatTimestamp(date: Date): string {
2121 } ) ;
2222}
2323
24+ function ChevronLeftIcon ( { className } : { className ?: string } ) {
25+ return (
26+ < svg className = { className } fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
27+ < path strokeLinecap = "round" strokeLinejoin = "round" d = "M15 19l-7-7 7-7" />
28+ </ svg >
29+ ) ;
30+ }
31+
32+ function ChevronRightIcon ( { className } : { className ?: string } ) {
33+ return (
34+ < svg className = { className } fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
35+ < path strokeLinecap = "round" strokeLinejoin = "round" d = "M9 5l7 7-7 7" />
36+ </ svg >
37+ ) ;
38+ }
39+
2440export function ResultsTabs ( {
2541 tabs,
2642 activeTabId,
2743 onTabSelect,
2844 onTabClose,
2945 isLoading,
3046} : ResultsTabsProps ) {
47+ const tabsContainerRef = useRef < HTMLDivElement > ( null ) ;
48+ const [ canScrollLeft , setCanScrollLeft ] = useState ( false ) ;
49+ const [ canScrollRight , setCanScrollRight ] = useState ( false ) ;
50+
3151 const activeTab = useMemo (
3252 ( ) => tabs . find ( ( tab ) => tab . id === activeTabId ) ,
3353 [ tabs , activeTabId ]
3454 ) ;
3555
56+ const updateScrollButtons = useCallback ( ( ) => {
57+ const container = tabsContainerRef . current ;
58+ if ( ! container ) return ;
59+
60+ const { scrollLeft, scrollWidth, clientWidth } = container ;
61+ setCanScrollLeft ( scrollLeft > 0 ) ;
62+ setCanScrollRight ( scrollLeft + clientWidth < scrollWidth - 1 ) ;
63+ } , [ ] ) ;
64+
65+ useEffect ( ( ) => {
66+ updateScrollButtons ( ) ;
67+ window . addEventListener ( 'resize' , updateScrollButtons ) ;
68+ return ( ) => window . removeEventListener ( 'resize' , updateScrollButtons ) ;
69+ } , [ updateScrollButtons , tabs ] ) ;
70+
71+ const scroll = ( direction : 'left' | 'right' ) => {
72+ const container = tabsContainerRef . current ;
73+ if ( ! container ) return ;
74+
75+ const scrollAmount = 150 ;
76+ container . scrollBy ( {
77+ left : direction === 'left' ? - scrollAmount : scrollAmount ,
78+ behavior : 'smooth' ,
79+ } ) ;
80+ } ;
81+
3682 // Loading state (no tabs yet)
3783 if ( isLoading && tabs . length === 0 ) {
3884 return (
@@ -56,44 +102,79 @@ export function ResultsTabs({
56102 return (
57103 < div className = "space-y-2" >
58104 { /* Tab bar */ }
59- < div className = "flex items-center gap-1 border-b border-border overflow-x-auto overflow-y-hidden" >
60- { tabs . map ( ( tab ) => (
105+ < div className = "relative flex items-center border-b border-border" >
106+ { /* Left scroll button */ }
107+ { canScrollLeft && (
61108 < button
62- key = { tab . id }
63109 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- ) }
110+ onClick = { ( ) => scroll ( 'left' ) }
111+ className = "absolute left-0 z-10 flex items-center justify-center w-6 h-full bg-background hover:bg-muted cursor-pointer"
112+ aria-label = "Scroll tabs left"
72113 >
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 === ' ' ) {
114+ < ChevronLeftIcon className = "w-4 h-4 text-muted-foreground" />
115+ </ button >
116+ ) }
117+
118+ { /* Tabs container */ }
119+ < div
120+ ref = { tabsContainerRef }
121+ onScroll = { updateScrollButtons }
122+ className = { cn (
123+ "flex items-center gap-1 overflow-hidden" ,
124+ canScrollLeft && "pl-6" ,
125+ canScrollRight && "pr-6"
126+ ) }
127+ >
128+ { tabs . map ( ( tab ) => (
129+ < button
130+ key = { tab . id }
131+ type = "button"
132+ onClick = { ( ) => onTabSelect ( tab . id ) }
133+ className = { cn (
134+ 'group flex items-center gap-1.5 px-3 py-1.5 text-sm whitespace-nowrap' ,
135+ 'border-b-2 -mb-px transition-colors cursor-pointer' ,
136+ tab . id === activeTabId
137+ ? 'border-primary text-foreground'
138+ : 'border-transparent text-muted-foreground hover:text-foreground'
139+ ) }
140+ >
141+ < span > { formatTimestamp ( tab . timestamp ) } </ span >
142+ { tab . error && (
143+ < span className = "w-1.5 h-1.5 rounded-full bg-destructive" aria-label = "Error" />
144+ ) }
145+ < span
146+ role = "button"
147+ tabIndex = { 0 }
148+ aria-label = "Close tab"
149+ onClick = { ( e ) => {
87150 e . stopPropagation ( ) ;
88151 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 >
152+ } }
153+ onKeyDown = { ( e ) => {
154+ if ( e . key === 'Enter' || e . key === ' ' ) {
155+ e . stopPropagation ( ) ;
156+ onTabClose ( tab . id ) ;
157+ }
158+ } }
159+ className = "opacity-0 group-hover:opacity-100 hover:bg-muted rounded p-0.5 transition-opacity"
160+ >
161+ < XIcon className = "w-3 h-3" />
162+ </ span >
163+ </ button >
164+ ) ) }
165+ </ div >
166+
167+ { /* Right scroll button */ }
168+ { canScrollRight && (
169+ < button
170+ type = "button"
171+ onClick = { ( ) => scroll ( 'right' ) }
172+ className = "absolute right-0 z-10 flex items-center justify-center w-6 h-full bg-background hover:bg-muted cursor-pointer"
173+ aria-label = "Scroll tabs right"
174+ >
175+ < ChevronRightIcon className = "w-4 h-4 text-muted-foreground" />
95176 </ button >
96- ) ) }
177+ ) }
97178 </ div >
98179
99180 { /* Active tab content */ }
0 commit comments