1- import React , { useCallback } from 'react' ;
1+ import React , { useCallback , useState } from 'react' ;
22import { Flex , Box , Text , IconButton , Kbd } from '@radix-ui/themes' ;
33import { Cross2Icon } from '@radix-ui/react-icons' ;
44import { useTabStore } from '../stores/tabStore' ;
55import { useHotkeys } from 'react-hotkeys-hook' ;
66
77export function TabBar ( ) {
8- const { tabs, activeTabId, setActiveTab, closeTab } = useTabStore ( ) ;
8+ const { tabs, activeTabId, setActiveTab, closeTab, reorderTabs } = useTabStore ( ) ;
9+ const [ draggedTab , setDraggedTab ] = useState < string | null > ( null ) ;
10+ const [ dragOverTab , setDragOverTab ] = useState < string | null > ( null ) ;
11+ const [ dropPosition , setDropPosition ] = useState < 'left' | 'right' | null > ( null ) ;
912
1013 // Keyboard navigation handlers
1114 const handlePrevTab = useCallback ( ( ) => {
@@ -75,53 +78,135 @@ export function TabBar() {
7578 }
7679 } , [ handlePrevTab , handleNextTab , handleCloseTab , handleSwitchToTab ] ) ;
7780
81+ const handleDragStart = useCallback ( ( e : React . DragEvent , tabId : string ) => {
82+ setDraggedTab ( tabId ) ;
83+ e . dataTransfer . effectAllowed = 'move' ;
84+ e . dataTransfer . setData ( 'text/plain' , tabId ) ;
85+ } , [ ] ) ;
86+
87+ const handleDragOver = useCallback ( ( e : React . DragEvent , tabId : string ) => {
88+ e . preventDefault ( ) ;
89+ e . dataTransfer . dropEffect = 'move' ;
90+
91+ const rect = e . currentTarget . getBoundingClientRect ( ) ;
92+ const midpoint = rect . left + rect . width / 2 ;
93+ const mouseX = e . clientX ;
94+
95+ setDragOverTab ( tabId ) ;
96+ setDropPosition ( mouseX < midpoint ? 'left' : 'right' ) ;
97+ } , [ ] ) ;
98+
99+ const handleDragLeave = useCallback ( ( ) => {
100+ setDragOverTab ( null ) ;
101+ setDropPosition ( null ) ;
102+ } , [ ] ) ;
103+
104+ const handleDrop = useCallback ( ( e : React . DragEvent , targetTabId : string ) => {
105+ e . preventDefault ( ) ;
106+ const sourceTabId = e . dataTransfer . getData ( 'text/plain' ) ;
107+
108+ if ( sourceTabId && sourceTabId !== targetTabId ) {
109+ const sourceIndex = tabs . findIndex ( tab => tab . id === sourceTabId ) ;
110+ let targetIndex = tabs . findIndex ( tab => tab . id === targetTabId ) ;
111+
112+ if ( sourceIndex !== - 1 && targetIndex !== - 1 ) {
113+ // Adjust target index based on drop position
114+ if ( dropPosition === 'right' ) {
115+ targetIndex = targetIndex + 1 ;
116+ }
117+
118+ // If moving to the right, adjust for the source being removed
119+ if ( sourceIndex < targetIndex ) {
120+ targetIndex = targetIndex - 1 ;
121+ }
122+
123+ reorderTabs ( sourceIndex , targetIndex ) ;
124+ }
125+ }
126+
127+ setDraggedTab ( null ) ;
128+ setDragOverTab ( null ) ;
129+ setDropPosition ( null ) ;
130+ } , [ tabs , reorderTabs , dropPosition ] ) ;
131+
132+ const handleDragEnd = useCallback ( ( ) => {
133+ setDraggedTab ( null ) ;
134+ setDragOverTab ( null ) ;
135+ setDropPosition ( null ) ;
136+ } , [ ] ) ;
137+
78138 return (
79139 < Flex className = "drag border-b border-gray-6" height = "40px" >
80140 { /* Spacer for macOS window controls */ }
81141 < Box width = "80px" flexShrink = "0" />
82142
83- { tabs . map ( ( tab , index ) => (
84- < Flex
85- key = { tab . id }
86- className = { `no-drag cursor-pointer border-r border-gray-6 transition-colors group ${ tab . id === activeTabId
87- ? 'bg-accent-3 text-accent-12 border-b-2 border-b-accent-8 font-medium'
88- : 'text-gray-11 hover:bg-gray-3 hover:text-gray-12'
89- } ` }
90- align = "center"
91- px = "4"
92- onClick = { ( ) => setActiveTab ( tab . id ) }
93- >
94- { index < 9 && (
95- < Kbd size = "1" className = "mr-2 opacity-70" >
96- { navigator . platform . includes ( 'Mac' ) ? '⌘' : 'Ctrl+' } { index + 1 }
97- </ Kbd >
98- ) }
99-
100- < Text
101- size = "2"
102- className = { `max-w-[200px] overflow-hidden select-none text-ellipsis whitespace-nowrap ${ tab . id === activeTabId ? 'font-medium' : ''
103- } ` }
104- mr = "2"
143+ { tabs . map ( ( tab , index ) => {
144+ const isDragging = draggedTab === tab . id ;
145+ const isDragOver = dragOverTab === tab . id ;
146+ const showLeftIndicator = isDragOver && dropPosition === 'left' ;
147+ const showRightIndicator = isDragOver && dropPosition === 'right' ;
148+
149+ return (
150+ < Flex
151+ key = { tab . id }
152+ className = { `no-drag cursor-pointer border-r border-gray-6 border-b-2 transition-colors group relative ${ tab . id === activeTabId
153+ ? 'bg-accent-3 text-accent-12 border-b-accent-8'
154+ : 'text-gray-11 hover:bg-gray-3 hover:text-gray-12 border-b-transparent'
155+ } ${ isDragging ? ' opacity-50' : '' } ` }
156+ align = "center"
157+ px = "4"
158+ draggable
159+ onClick = { ( ) => setActiveTab ( tab . id ) }
160+ onDragStart = { ( e ) => handleDragStart ( e , tab . id ) }
161+ onDragOver = { ( e ) => handleDragOver ( e , tab . id ) }
162+ onDragLeave = { handleDragLeave }
163+ onDrop = { ( e ) => handleDrop ( e , tab . id ) }
164+ onDragEnd = { handleDragEnd }
105165 >
106- { tab . title }
107- </ Text >
108-
109- { tabs . length > 1 && (
110- < IconButton
111- size = "1"
112- variant = "ghost"
113- color = { tab . id === activeTabId ? "accent" : "gray" }
114- className = "opacity-0 group-hover:opacity-100 transition-opacity"
115- onClick = { ( e ) => {
116- e . stopPropagation ( ) ;
117- closeTab ( tab . id ) ;
118- } }
166+ { showLeftIndicator && (
167+ < Box
168+ className = "absolute left-0 top-0 bottom-0 w-0.5 bg-accent-8 z-10"
169+ style = { { marginLeft : '-1px' } }
170+ />
171+ ) }
172+
173+ { showRightIndicator && (
174+ < Box
175+ className = "absolute right-0 top-0 bottom-0 w-0.5 bg-accent-8 z-10"
176+ style = { { marginRight : '-1px' } }
177+ />
178+ ) }
179+ { index < 9 && (
180+ < Kbd size = "1" className = "mr-2 opacity-70" >
181+ { navigator . platform . includes ( 'Mac' ) ? '⌘' : 'Ctrl+' } { index + 1 }
182+ </ Kbd >
183+ ) }
184+
185+ < Text
186+ size = "2"
187+ className = "max-w-[200px] overflow-hidden select-none text-ellipsis whitespace-nowrap"
188+ mr = "2"
119189 >
120- < Cross2Icon />
121- </ IconButton >
122- ) }
123- </ Flex >
124- ) ) }
190+ { tab . title }
191+ </ Text >
192+
193+ { tabs . length > 1 && (
194+ < IconButton
195+ size = "1"
196+ variant = "ghost"
197+ color = { tab . id === activeTabId ? "accent" : "gray" }
198+ className = "opacity-0 group-hover:opacity-100 transition-opacity"
199+ onClick = { ( e ) => {
200+ e . stopPropagation ( ) ;
201+ closeTab ( tab . id ) ;
202+ } }
203+ >
204+ < Cross2Icon />
205+ </ IconButton >
206+ ) }
207+ </ Flex >
208+ ) ;
209+ } ) }
125210 </ Flex >
126211 ) ;
127212}
0 commit comments