diff --git a/src/renderer/components/layout/TabBar.tsx b/src/renderer/components/layout/TabBar.tsx
index 29f00e9..989414f 100644
--- a/src/renderer/components/layout/TabBar.tsx
+++ b/src/renderer/components/layout/TabBar.tsx
@@ -1,12 +1,92 @@
import { X } from 'lucide-react';
-import { useFileStore } from '@/stores/file-store';
+import {
+ DndContext,
+ PointerSensor,
+ useSensor,
+ useSensors,
+ closestCenter,
+ type DragEndEvent,
+} from '@dnd-kit/core';
+import {
+ SortableContext,
+ horizontalListSortingStrategy,
+ useSortable,
+} from '@dnd-kit/sortable';
+import { CSS } from '@dnd-kit/utilities';
+import { useFileStore, type OpenTab } from '@/stores/file-store';
import { cn } from '@/lib/utils';
+function SortableTab({
+ tab,
+ isActive,
+ onSelect,
+ onClose,
+}: {
+ tab: OpenTab;
+ isActive: boolean;
+ onSelect: (id: string) => void;
+ onClose: (id: string) => void;
+}) {
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
+ id: tab.id,
+ });
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ opacity: isDragging ? 0.5 : 1,
+ };
+
+ return (
+
onSelect(tab.id)}
+ >
+ {tab.dirty && (
+
+ )}
+ {tab.title}
+
+
+ );
+}
+
export function TabBar() {
const openTabs = useFileStore((s) => s.openTabs);
const activeTabId = useFileStore((s) => s.activeTabId);
const setActiveTab = useFileStore((s) => s.setActiveTab);
const closeTab = useFileStore((s) => s.closeTab);
+ const reorderTabs = useFileStore((s) => s.reorderTabs);
+
+ // Require a small drag distance before activating — so a click on the tab
+ // body still triggers `onClick` (dnd-kit's PointerSensor default is 0).
+ const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }));
if (openTabs.length === 0) {
return (
@@ -16,43 +96,32 @@ export function TabBar() {
);
}
+ function handleDragEnd(e: DragEndEvent) {
+ const { active, over } = e;
+ if (!over || active.id === over.id) return;
+ const fromIndex = openTabs.findIndex((t) => t.id === active.id);
+ const toIndex = openTabs.findIndex((t) => t.id === over.id);
+ if (fromIndex === -1 || toIndex === -1) return;
+ reorderTabs(fromIndex, toIndex);
+ }
+
return (
-
- {openTabs.map((tab) => {
- const isActive = tab.id === activeTabId;
- return (
-
setActiveTab(tab.id)}
- >
- {tab.dirty && (
-
- )}
- {tab.title}
-
-
- );
- })}
-
+
+ t.id)} strategy={horizontalListSortingStrategy}>
+
+ {openTabs.map((tab) => (
+
+ ))}
+
+
+
);
}
diff --git a/tests/component/layout/TabBar.test.tsx b/tests/component/layout/TabBar.test.tsx
index 94ef08b..7748b14 100644
--- a/tests/component/layout/TabBar.test.tsx
+++ b/tests/component/layout/TabBar.test.tsx
@@ -90,4 +90,27 @@ describe('TabBar', () => {
const dirtyDot = screen.getByLabelText('Unsaved changes');
expect(dirtyDot).toBeInTheDocument();
});
+
+ it('reorders tabs when reorderTabs is called (drag end handler integration)', () => {
+ useFileStore.setState({
+ openTabs: [
+ { id: '/tmp/a.md', path: '/tmp/a.md', title: 'a.md', dirty: false },
+ { id: '/tmp/b.md', path: '/tmp/b.md', title: 'b.md', dirty: false },
+ { id: '/tmp/c.md', path: '/tmp/c.md', title: 'c.md', dirty: false },
+ ],
+ activeTabId: '/tmp/a.md',
+ });
+ render();
+ // Initial DOM order
+ const initialOrder = screen.getAllByRole('tab').map((el) => el.getAttribute('data-testid'));
+ expect(initialOrder).toEqual(['tab-/tmp/a.md', 'tab-/tmp/b.md', 'tab-/tmp/c.md']);
+
+ // Simulate what dnd-kit's onDragEnd would do: move a.md (idx 0) to idx 2
+ act(() => {
+ useFileStore.getState().reorderTabs(0, 2);
+ });
+
+ const newOrder = screen.getAllByRole('tab').map((el) => el.getAttribute('data-testid'));
+ expect(newOrder).toEqual(['tab-/tmp/b.md', 'tab-/tmp/c.md', 'tab-/tmp/a.md']);
+ });
});