SortableTab.tsx130 lines · main
| 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 | |
| 8 | import { useEditorType } from '../editors/EditorsLayout.hooks' |
| 9 | import { EntityTypeIcon } from '@/components/ui/EntityTypeIcon' |
| 10 | import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState' |
| 11 | import { 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 | */ |
| 21 | export 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 | } |