22
33import { useState , useCallback , useRef } from 'react'
44import { Icon } from '@iconify/react'
5- import { useEditor } from '@/context/editor-context'
5+ import { PREVIEW_TAB_ID , useEditor } from '@/context/editor-context'
66import { useView } from '@/context/view-context'
77import { formatShortcut } from '@/lib/platform'
88
@@ -49,7 +49,16 @@ function getFileIcon(path: string) {
4949}
5050
5151export function EditorTabs ( { onTabSelect } : { onTabSelect ?: ( path : string ) => void } ) {
52- const { files, activeFile, setActiveFile, closeFile, reorderFiles } = useEditor ( )
52+ const {
53+ tabs,
54+ files,
55+ activeFile,
56+ setActiveFile,
57+ closeFile,
58+ reorderTabs,
59+ closePreviewTab,
60+ openPreviewTab,
61+ } = useEditor ( )
5362 const { activeView, setView } = useView ( )
5463 const previewActive = activeView === 'preview'
5564 const [ dragIndex , setDragIndex ] = useState < number | null > ( null )
@@ -60,7 +69,6 @@ export function EditorTabs({ onTabSelect }: { onTabSelect?: (path: string) => vo
6069 setDragIndex ( index )
6170 dragNode . current = e . currentTarget as HTMLDivElement
6271 e . dataTransfer . effectAllowed = 'move'
63- // Make drag image semi-transparent
6472 requestAnimationFrame ( ( ) => {
6573 if ( dragNode . current ) dragNode . current . style . opacity = '0.4'
6674 } )
@@ -69,32 +77,38 @@ export function EditorTabs({ onTabSelect }: { onTabSelect?: (path: string) => vo
6977 const handleDragEnd = useCallback ( ( ) => {
7078 if ( dragNode . current ) dragNode . current . style . opacity = '1'
7179 if ( dragIndex !== null && dropTarget !== null && dragIndex !== dropTarget ) {
72- reorderFiles ( dragIndex , dropTarget )
80+ reorderTabs ( dragIndex , dropTarget )
7381 }
7482 setDragIndex ( null )
7583 setDropTarget ( null )
7684 dragNode . current = null
77- } , [ dragIndex , dropTarget , reorderFiles ] )
85+ } , [ dragIndex , dropTarget , reorderTabs ] )
7886
7987 const handleDragOver = useCallback ( ( e : React . DragEvent , index : number ) => {
8088 e . preventDefault ( )
8189 e . dataTransfer . dropEffect = 'move'
8290 setDropTarget ( index )
8391 } , [ ] )
8492
85- if ( files . length === 0 ) return null
93+ if ( tabs . length === 0 ) return null
8694
8795 return (
8896 < div className = "relative flex items-center border-b border-[var(--border)] bg-[var(--bg)] overflow-x-auto no-scrollbar shrink-0 h-[42px]" >
89- { files . map ( ( file , index ) => {
90- const name = file . path . split ( '/' ) . pop ( ) ?? file . path
91- const isActive = ! previewActive && file . path === activeFile
97+ { tabs . map ( ( tab , index ) => {
98+ const isPreview = tab . type === 'preview'
99+ const tabPath = tab . type === 'file' ? tab . path : null
100+ const isActive = isPreview ? previewActive : ! previewActive && tabPath === activeFile
92101 const isDragTarget = dropTarget === index && dragIndex !== null && dragIndex !== index
93- const { icon, color } = getFileIcon ( file . path )
102+
103+ const label = isPreview ? 'Preview' : ( tabPath ?. split ( '/' ) . pop ( ) ?? '' )
104+ const dirty = tabPath ? files . find ( ( file ) => file . path === tabPath ) ?. dirty : false
105+ const iconMeta = isPreview
106+ ? { icon : 'lucide:eye' , color : 'var(--brand)' }
107+ : getFileIcon ( tabPath ?? '' )
94108
95109 return (
96110 < div
97- key = { file . path }
111+ key = { tab . id }
98112 draggable
99113 onDragStart = { ( e ) => handleDragStart ( e , index ) }
100114 onDragEnd = { handleDragEnd }
@@ -110,90 +124,72 @@ export function EditorTabs({ onTabSelect }: { onTabSelect?: (path: string) => vo
110124 ${ isDragTarget ? 'border-b-[var(--brand)] bg-[var(--bg-subtle)]' : '' }
111125 ` }
112126 onClick = { ( ) => {
113- setActiveFile ( file . path )
127+ if ( isPreview ) {
128+ openPreviewTab ( )
129+ setView ( 'preview' )
130+ return
131+ }
132+ if ( ! tabPath ) return
133+ setActiveFile ( tabPath )
114134 setView ( 'editor' )
115- onTabSelect ?.( file . path )
135+ onTabSelect ?.( tabPath )
116136 } }
117137 >
118- { /* Active indicator */ }
119138 { isActive && (
120139 < div className = "absolute bottom-0 left-0 right-0 h-[3px]" >
121140 < div className = "h-full bg-[var(--brand)] rounded-t-full" />
122141 </ div >
123142 ) }
124143
125- { /* File icon */ }
126144 < Icon
127- icon = { icon }
145+ icon = { iconMeta . icon }
128146 width = { 17 }
129147 height = { 17 }
130- style = { { color : isActive ? color : undefined } }
148+ style = { { color : isActive ? iconMeta . color : undefined } }
131149 className = { `transition-colors duration-150 ${ isActive ? '' : 'text-[var(--text-tertiary)]' } ` }
132150 />
133151
134- { /* File name */ }
135- < span className = "text-[13px] font-medium truncate max-w-[140px]" title = { file . path } >
136- { name }
152+ < span
153+ className = "text-[13px] font-medium truncate max-w-[140px]"
154+ title = { isPreview ? 'Preview' : ( tabPath ?? '' ) }
155+ >
156+ { label }
137157 </ span >
138158
139- { /* Dirty indicator */ }
140- { file . dirty && (
159+ { dirty && (
141160 < span
142161 className = "h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--brand)]"
143162 title = "Unsaved changes"
144163 />
145164 ) }
146165
147- { /* Close button — show dot when dirty and not hovered */ }
148166 < button
149167 onClick = { ( e ) => {
150168 e . stopPropagation ( )
151- closeFile ( file . path )
169+ if ( isPreview ) {
170+ closePreviewTab ( )
171+ if ( previewActive ) setView ( 'editor' )
172+ return
173+ }
174+ if ( tabPath ) closeFile ( tabPath )
152175 } }
153176 className = "ml-1 cursor-pointer rounded-lg p-1.5 opacity-0 transition-colors hover:bg-[var(--bg)] group-hover:opacity-100 focus:opacity-100"
154177 title = { `Close (${ formatShortcut ( 'meta+W' ) } )` }
155178 >
156179 < Icon icon = "lucide:x" width = { 14 } height = { 14 } />
157180 </ button >
158181
159- { /* Separator */ }
160182 { ! isActive && (
161183 < div className = "absolute right-0 top-[6px] bottom-[6px] w-px bg-[var(--border)] opacity-30" />
162184 ) }
163185 </ div >
164186 )
165187 } ) }
166188
167- < div
168- className = { `
169- group relative flex items-center gap-2.5 px-4 h-full cursor-pointer transition-colors duration-150 select-none min-w-0 shrink-0
170- ${
171- previewActive
172- ? 'bg-[var(--bg-elevated)] text-[var(--text-primary)]'
173- : 'text-[var(--text-secondary)] hover:bg-[var(--bg-subtle)] hover:text-[var(--text-primary)]'
174- }
175- ` }
176- onClick = { ( ) => setView ( 'preview' ) }
177- >
178- { previewActive && (
179- < div className = "absolute bottom-0 left-0 right-0 h-[3px]" >
180- < div className = "h-full bg-[var(--brand)] rounded-t-full" />
181- </ div >
182- ) }
183- < Icon
184- icon = "lucide:eye"
185- width = { 17 }
186- height = { 17 }
187- className = { `transition-colors duration-150 ${ previewActive ? 'text-[var(--brand)]' : 'text-[var(--text-tertiary)]' } ` }
188- />
189- < span className = "text-[13px] font-medium truncate max-w-[140px]" > Preview</ span >
190- </ div >
191-
192- { /* Tab count indicator when many tabs are open */ }
193- { files . length > 6 && (
189+ { tabs . length > 6 && (
194190 < div className = "sticky right-0 flex items-center px-2 h-full bg-gradient-to-l from-[var(--bg)] via-[var(--bg)] to-transparent shrink-0" >
195191 < span className = "text-[11px] font-mono font-bold text-[var(--text-tertiary)] bg-[var(--bg-subtle)] px-2.5 py-1 rounded-full border-[1.5px] border-[var(--border)]" >
196- { files . length }
192+ { tabs . length }
197193 </ span >
198194 </ div >
199195 ) }
0 commit comments