diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 6b87f1692..788e95d38 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -15,6 +15,10 @@ "postinstall": "electron-builder install-app-deps" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", "@multica/core": "workspace:*", @@ -28,8 +32,8 @@ "tw-animate-css": "^1.4.0" }, "devDependencies": { - "@multica/tsconfig": "workspace:*", "@electron-toolkit/tsconfig": "^2.0.0", + "@multica/tsconfig": "workspace:*", "@tailwindcss/vite": "^4", "@types/node": "catalog:", "@types/react": "catalog:", diff --git a/apps/desktop/src/renderer/src/components/tab-bar.tsx b/apps/desktop/src/renderer/src/components/tab-bar.tsx index a42b97951..0bd4f388b 100644 --- a/apps/desktop/src/renderer/src/components/tab-bar.tsx +++ b/apps/desktop/src/renderer/src/components/tab-bar.tsx @@ -10,6 +10,24 @@ import { Plus, type LucideIcon, } from "lucide-react"; +import { + DndContext, + PointerSensor, + useSensor, + useSensors, + closestCenter, + type DragEndEvent, +} from "@dnd-kit/core"; +import { + SortableContext, + horizontalListSortingStrategy, + useSortable, +} from "@dnd-kit/sortable"; +import { + restrictToHorizontalAxis, + restrictToParentElement, +} from "@dnd-kit/modifiers"; +import { CSS } from "@dnd-kit/utilities"; import { cn } from "@multica/ui/lib/utils"; import { useTabStore, resolveRouteIcon, type Tab } from "@/stores/tab-store"; @@ -23,12 +41,28 @@ const TAB_ICONS: Record = { Settings, }; -function TabItem({ tab, isActive, isOnly }: { tab: Tab; isActive: boolean; isOnly: boolean }) { +function SortableTabItem({ tab, isActive, isOnly }: { tab: Tab; isActive: boolean; isOnly: boolean }) { const setActiveTab = useTabStore((s) => s.setActiveTab); const closeTab = useTabStore((s) => s.closeTab); + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: tab.id }); + const Icon = TAB_ICONS[tab.icon]; + const style = { + transform: CSS.Transform.toString(transform), + transition, + WebkitAppRegion: "no-drag", + zIndex: isDragging ? 10 : undefined, + } as React.CSSProperties; + const handleClick = () => { if (isActive) return; setActiveTab(tab.id); @@ -41,16 +75,25 @@ function TabItem({ tab, isActive, isOnly }: { tab: Tab; isActive: boolean; isOnl // No navigate() — store handles activeTabId switch }; + // Stop pointer down on close so it doesn't start a drag on the parent button. + const stopDragOnClose = (e: React.PointerEvent) => { + e.stopPropagation(); + }; + return (