SortableTab.tsx 4.4 KB

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