Drag & drop tab reordering

Tabs are draggable within the strip: HTML5 DnD with the WebKit setData quirk
handled, an inset primary insertion line at the drop position (no layout
shift), and the dragged tab dimmed. Reordering only touches the workspaces
array order; ids, sessions, and the active tab are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jokin
2026-07-11 17:33:52 +02:00
co-authored by Claude Fable 5
parent c95087c079
commit 3e33bac426
2 changed files with 68 additions and 1 deletions
+56 -1
View File
@@ -26,10 +26,17 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
[class.pl-1.5]="!isMacChrome"
[class.pt-1]="!isMacChrome"
>
@for (ws of panels.workspaces(); track ws.id) {
@for (ws of panels.workspaces(); track ws.id; let i = $index) {
<div
class="group relative flex max-w-52 items-center rounded-md electrobun-webkit-app-region-no-drag"
[class.bg-accent]="ws.id === panels.activeWorkspaceId()"
[class.opacity-50]="draggedId() === ws.id"
[style.box-shadow]="dropIndicatorShadow(i)"
draggable="true"
(dragstart)="onDragStart($event, ws.id)"
(dragover)="onDragOver($event, i)"
(drop)="onDrop($event, i)"
(dragend)="onDragEnd()"
>
<button
(click)="panels.setActiveWorkspace(ws.id)"
@@ -190,8 +197,56 @@ export class TabBar {
readonly transferModes = TRANSFER_MODES;
readonly concurrencyChoices = CONCURRENCY_CHOICES;
// drag & drop reordering
readonly draggedId = signal<number | null>(null);
readonly dropTarget = signal<{ index: number; before: boolean } | null>(null);
close(event: MouseEvent, id: number): void {
event.stopPropagation();
this.panels.closeWorkspace(id);
}
/** Insertion line at the drop position (inset shadow: no layout shift). */
dropIndicatorShadow(index: number): string | null {
const target = this.dropTarget();
if (!target || target.index !== index || this.draggedId() === null) return null;
return target.before
? 'inset 2px 0 0 var(--primary)'
: 'inset -2px 0 0 var(--primary)';
}
onDragStart(event: DragEvent, id: number): void {
this.draggedId.set(id);
// WebKit refuses to start a drag without data
event.dataTransfer?.setData('text/plain', String(id));
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
}
onDragOver(event: DragEvent, index: number): void {
if (this.draggedId() === null) return;
event.preventDefault();
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
const el = event.currentTarget as HTMLElement;
const before = event.clientX < el.getBoundingClientRect().left + el.offsetWidth / 2;
this.dropTarget.set({ index, before });
}
onDrop(event: DragEvent, index: number): void {
event.preventDefault();
const id = this.draggedId();
const target = this.dropTarget();
this.onDragEnd();
if (id === null || !target) return;
const list = this.panels.workspaces();
const fromIndex = list.findIndex((w) => w.id === id);
if (fromIndex < 0) return;
let toIndex = target.before ? index : index + 1;
if (fromIndex < toIndex) toIndex--; // account for removal before insert
if (toIndex !== fromIndex) this.panels.moveWorkspace(id, toIndex);
}
onDragEnd(): void {
this.draggedId.set(null);
this.dropTarget.set(null);
}
}