Skip to content
Prev Previous commit
Next Next commit
fix: overlay vtab drop indicator to remove accent gap
Co-authored-by: sawka <2722291+sawka@users.noreply.github.com>
  • Loading branch information
Copilot and sawka committed Mar 3, 2026
commit bbac1af47265f05d8ec5680e48595b3e497b61b6
91 changes: 49 additions & 42 deletions frontend/app/tab/vtabbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function VTabBar({ tabs, activeTabId, width, className, onSelectTab, onCl
const [orderedTabs, setOrderedTabs] = useState<VTabItem[]>(tabs);
const [dragTabId, setDragTabId] = useState<string | null>(null);
const [dropIndex, setDropIndex] = useState<number | null>(null);
const [dropLineTop, setDropLineTop] = useState<number | null>(null);
const dragSourceRef = useRef<string | null>(null);

useEffect(() => {
Expand All @@ -46,6 +47,7 @@ export function VTabBar({ tabs, activeTabId, width, className, onSelectTab, onCl
dragSourceRef.current = null;
setDragTabId(null);
setDropIndex(null);
setDropLineTop(null);
};

const reorder = (targetIndex: number) => {
Expand All @@ -69,24 +71,18 @@ export function VTabBar({ tabs, activeTabId, width, className, onSelectTab, onCl
onReorderTabs?.(nextTabs.map((tab) => tab.id));
};

const getDropLineClass = (index: number) =>
cn(
"h-0 border-t-2",
dragTabId != null && dropIndex === index ? "border-accent/80" : "border-transparent",
index > 0 && index < orderedTabs.length && "my-px"
);

return (
<div
className={cn("flex h-full min-w-[100px] max-w-[400px] flex-col overflow-hidden border-r border-border bg-panel", className)}
style={{ width: barWidth }}
>
<div
className="flex min-h-0 flex-1 flex-col overflow-y-auto"
className="relative flex min-h-0 flex-1 flex-col overflow-y-auto"
onDragOver={(event) => {
event.preventDefault();
if (event.target === event.currentTarget) {
setDropIndex(orderedTabs.length);
setDropLineTop(event.currentTarget.scrollHeight);
}
}}
onDrop={(event) => {
Expand All @@ -97,42 +93,53 @@ export function VTabBar({ tabs, activeTabId, width, className, onSelectTab, onCl
clearDragState();
}}
>
<div className={getDropLineClass(0)} />
{orderedTabs.map((tab, index) => (
<div key={tab.id} className="flex flex-col">
<VTab
tab={tab}
active={tab.id === activeTabId}
isDragging={dragTabId === tab.id}
isReordering={dragTabId != null}
onSelect={() => onSelectTab?.(tab.id)}
onClose={onCloseTab ? () => onCloseTab(tab.id) : undefined}
onRename={onRenameTab ? (newName) => onRenameTab(tab.id, newName) : undefined}
onDragStart={(event) => {
dragSourceRef.current = tab.id;
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", tab.id);
setDragTabId(tab.id);
setDropIndex(index);
}}
onDragOver={(event) => {
event.preventDefault();
const rect = event.currentTarget.getBoundingClientRect();
const insertBefore = event.clientY < rect.top + rect.height / 2;
setDropIndex(insertBefore ? index : index + 1);
}}
onDrop={(event) => {
event.preventDefault();
if (dropIndex != null) {
reorder(dropIndex);
}
clearDragState();
}}
onDragEnd={clearDragState}
/>
<div className={getDropLineClass(index + 1)} />
</div>
<VTab
key={tab.id}
tab={tab}
active={tab.id === activeTabId}
isDragging={dragTabId === tab.id}
isReordering={dragTabId != null}
onSelect={() => onSelectTab?.(tab.id)}
onClose={onCloseTab ? () => onCloseTab(tab.id) : undefined}
onRename={onRenameTab ? (newName) => onRenameTab(tab.id, newName) : undefined}
onDragStart={(event) => {
dragSourceRef.current = tab.id;
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", tab.id);
setDragTabId(tab.id);
setDropIndex(index);
setDropLineTop(event.currentTarget.offsetTop);
}}
onDragOver={(event) => {
event.preventDefault();
const rect = event.currentTarget.getBoundingClientRect();
const relativeY = event.clientY - rect.top;
const midpoint = event.currentTarget.offsetHeight / 2;
const insertBefore = relativeY < midpoint;
setDropIndex(insertBefore ? index : index + 1);
setDropLineTop(
insertBefore
? event.currentTarget.offsetTop
: event.currentTarget.offsetTop + event.currentTarget.offsetHeight
);
}}
onDrop={(event) => {
event.preventDefault();
if (dropIndex != null) {
reorder(dropIndex);
}
clearDragState();
}}
onDragEnd={clearDragState}
/>
))}
{dragTabId != null && dropIndex != null && dropLineTop != null && (
<div
className="pointer-events-none absolute left-0 right-0 border-t-2 border-accent/80"
style={{ top: dropLineTop, transform: "translateY(-1px)" }}
/>
)}
</div>
</div>
);
Expand Down