SortableTab.tsx130 lines · main
1import { useSortable } from '@dnd-kit/sortable'
2import { CSS } from '@dnd-kit/utilities'
3import { AnimatePresence, motion } from 'framer-motion'
4import { X } from 'lucide-react'
5import { useMemo } from 'react'
6import { cn, TabsTrigger_Shadcn_ } from 'ui'
7
8import { useEditorType } from '../editors/EditorsLayout.hooks'
9import { EntityTypeIcon } from '@/components/ui/EntityTypeIcon'
10import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState'
11import { useTabsStateSnapshot, type Tab } from '@/state/tabs'
12
13/**
14 * Individual draggable tab component that handles:
15 * - Drag
16 * - Drop functionality
17 * - Dynamic schema name display
18 * - Tab label animations
19 * - Close button interactions
20 */
21export const SortableTab = ({
22 tab,
23 index,
24 openTabs,
25 onClose,
26}: {
27 tab: Tab
28 index: number
29 openTabs: Tab[]
30 onClose: (id: string) => void
31}) => {
32 const editor = useEditorType()
33 const tabs = useTabsStateSnapshot()
34 const { selectedSchema: currentSchema } = useQuerySchemaState()
35 const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
36 id: tab.id,
37 })
38
39 const style = {
40 transform: CSS.Transform.toString(transform),
41 transition,
42 zIndex: isDragging ? 1 : 0,
43 }
44
45 // Update schema visibility check to include URL param comparison
46 const shouldShowSchema = useMemo(() => {
47 // For both table and schema tabs, show schema if:
48 // Any tab has a different schema than the current schema parameter
49 return openTabs.some((t) => editor === 'table' && t.metadata?.schema !== currentSchema)
50 }, [openTabs, currentSchema, editor])
51
52 // Create a motion version of TabsTrigger while preserving all functionality
53 // const MotionTabsTrigger = motion(TabsTrigger_Shadcn_)
54
55 return (
56 <motion.div
57 ref={setNodeRef}
58 style={style}
59 {...attributes}
60 layoutId={tab.id}
61 transition={{ duration: 0.045 }}
62 animate={{ opacity: isDragging ? 0 : 1 }}
63 className={cn('flex items-center h-(--header-height) first-of-type:border-l')}
64 >
65 <TabsTrigger_Shadcn_
66 value={tab.id}
67 onAuxClick={(e) => {
68 // Middle click closes tab
69 if (e.button === 1) {
70 e.preventDefault()
71 onClose(tab.id)
72 }
73 }}
74 onDoubleClick={() => tabs.makeTabPermanent(tab.id)}
75 className={cn(
76 'flex items-center gap-2 pl-3 pr-2.5 text-xs',
77 'bg-dash-sidebar/50 dark:bg-surface-100/50',
78 'data-[state=active]:bg-dash-sidebar dark:data-[state=active]:bg-surface-100',
79 'border-b border-default',
80 'data-[state=active]:border-b-background-dash-sidebar dark:data-[state=active]:border-b-background-surface-100',
81 'relative group h-full',
82 'hover:bg-surface-300 dark:hover:bg-surface-100',
83 tab.isPreview && 'italic font-light' // Optional: style preview tabs differently
84 )}
85 {...listeners}
86 >
87 <EntityTypeIcon type={tab.type} />
88 <div className="flex items-center gap-0">
89 <AnimatePresence mode="popLayout" initial>
90 {shouldShowSchema && (
91 <motion.span
92 initial={{ opacity: 0, width: 0 }}
93 animate={{ opacity: 1, width: 'auto' }}
94 exit={{ opacity: 0, width: 0 }}
95 transition={{ duration: 0.15 }}
96 className="text-foreground-muted group-data-[state=active]:text-foreground-lighter"
97 >
98 {tab?.metadata?.schema}.
99 </motion.span>
100 )}
101 </AnimatePresence>
102 <span>{tab.label || 'Untitled'}</span>
103 </div>
104 <span
105 role="button"
106 onClick={(e) => {
107 e.preventDefault()
108 e.stopPropagation()
109 }}
110 className="p-0.5 ml-1 opacity-0 group-hover:opacity-100 hover:bg-200 rounded-xs cursor-pointer"
111 onMouseDown={(e) => {
112 e.preventDefault()
113 e.stopPropagation()
114 }}
115 onPointerDown={(e) => {
116 e.preventDefault()
117 e.stopPropagation()
118 onClose(tab.id)
119 }}
120 >
121 <X size={12} className="text-foreground-light" />
122 </span>
123 <div className="absolute w-full top-0 left-0 right-0 h-px bg-foreground opacity-0 group-data-[state=active]:opacity-100" />
124 </TabsTrigger_Shadcn_>
125 {index < openTabs.length && (
126 <div role="separator" className="h-full w-px bg-border" key={`separator-${tab.id}`} />
127 )}
128 </motion.div>
129 )
130}