Tab-Drag-Steuerung aus App.tsx in eigenen Hook extrahiert (state/useDockDragState.ts).

Reorder/Einreihen/Stapeln/Lösen (usePanelDrag-Wrapper) + Float-Titel-Drag über
Rand-Andockzonen. Reine Verschiebung, keine Verhaltensänderung.

App.tsx: 3690 → 3655 Zeilen.

tsc -b sauber, vitest 891/891 grün, vite build ok.
This commit is contained in:
2026-08-20 00:48:34 +02:00
parent b6d47b433d
commit c224f9e4f9
2 changed files with 106 additions and 55 deletions
+20 -55
View File
@@ -118,6 +118,7 @@ import { useDrawingToolState } from "./state/useDrawingToolState";
import { useSelectionState } from "./state/useSelectionState";
import { useGripEditing } from "./state/useGripEditing";
import { usePanelHostBase } from "./state/usePanelHostBase";
import { useDockDragState } from "./state/useDockDragState";
import { useProjectFileActions } from "./state/useProjectFileActions";
import { useProjectActions } from "./state/useProjectActions";
import {
@@ -162,10 +163,9 @@ import { reconcileLayout } from "./state/layoutSlice";
// Panel-System.
import { Dock } from "./panels/Dock";
import { FloatingPanel } from "./panels/FloatingPanel";
import { DragOverlay, usePanelDrag } from "./panels/panelDrag";
import { DragOverlay } from "./panels/panelDrag";
import { itemDisplay } from "./panels/displayMode";
import { PanelHostContext } from "./panels/types";
import type { DockId } from "./panels/types";
import { hostWithMode } from "./panels/host";
import { hitTestWall } from "./viewport/planHitTest";
import { deriveSelection } from "./state/selectionInfo";
@@ -1785,61 +1785,26 @@ export default function App() {
const redockFloat = useStore((s) => s.redockFloat);
const dockFloatTo = useStore((s) => s.dockFloatTo);
// ── Tab-Drag-Steuerung (Reorder / Einreihen / Stapeln / Lösen) ────────────
// Der Controller (usePanelDrag) klärt anhand der Bewegung Klick vs. Ziehen
// und löst beim Loslassen das Ziel auf; die Auflösung selbst macht der Slice.
const { drag, startTabDrag } = usePanelDrag({
onActivate: activateTab,
onJoinGroup: joinGroup,
onNewGroup: newGroup,
onDropFloat: dropFloat,
// Tab-Drag-Steuerung (Reorder/Einreihen/Stapeln/Lösen) + Float-Titel-Drag
// über Rand-Andockzonen — siehe state/useDockDragState.ts.
const {
drag,
startTabDrag,
onFloatTitleDrop,
onFloatTitleDrag,
leftHasTabs,
rightHasTabs,
dragActive,
activeZone,
} = useDockDragState({
layout,
activateTab,
joinGroup,
newGroup,
dropFloat,
dockFloatTo,
});
// Zustand für die Rand-Zonen-Hervorhebung beim Titel-Drag eines Fensters.
// `floatDragging` schaltet die Zonen ein (sobald die Titelleiste gezogen
// wird), `floatDragHover` markiert die gerade überfahrene Zone.
const [floatDragging, setFloatDragging] = useState(false);
const [floatDragHover, setFloatDragHover] = useState<DockId | null>(null);
// Titel-Drag eines schwebenden Fensters über eine Rand-Zone: liefert true,
// wenn das Fenster angedockt wurde (dann unterbleibt das reine Verschieben).
// Trefferprüfung über die Datenattribute der Rand-Zonen (data-drop-zone).
const onFloatTitleDrop = (panelId: string, clientX: number, clientY: number): boolean => {
setFloatDragging(false);
setFloatDragHover(null);
const zone = document
.elementsFromPoint(clientX, clientY)
.find((el) => (el as HTMLElement).dataset?.dropZone !== undefined) as
| HTMLElement
| undefined;
if (!zone) return false;
const dock = zone.dataset.dropZone as DockId;
dockFloatTo(panelId, dock);
return true;
};
const onFloatTitleDrag = (_panelId: string, clientX: number, clientY: number) => {
if (!floatDragging) setFloatDragging(true);
const zone = document
.elementsFromPoint(clientX, clientY)
.find((el) => (el as HTMLElement).dataset?.dropZone !== undefined) as
| HTMLElement
| undefined;
setFloatDragHover(zone ? (zone.dataset.dropZone as DockId) : null);
};
// Ein Dock wird nur gerendert, wenn es Tabs hat — ein leeres Dock soll die
// Mitte nicht stauchen. (Rechtes Dock ist standardmäßig leer: Ressourcen ist
// ein schwebendes Fenster.)
const leftHasTabs = layout.left.groups.length > 0;
const rightHasTabs = layout.right.groups.length > 0;
// Rand-Andock-Zonen sind aktiv, solange irgendeine Drag-Geste läuft (Tab-Drag
// ODER Titel-Drag eines schwebenden Fensters). Die gerade überfahrene Zone
// (für die Hervorhebung) kommt aus dem Tab-Drag-Hover bzw. dem Float-Hover.
const dragActive = drag !== null || floatDragging;
const activeZone: DockId | null =
drag && drag.hover.kind === "zone" ? drag.hover.dock : floatDragHover;
// Handler-Bündel für das schwebende Ressourcen-Fenster (ResourceManager).
const resourceHandlers: ResourceManagerHandlers = {
onPatchComponent: patchComponent,
+86
View File
@@ -0,0 +1,86 @@
// Tab-Drag-Steuerung (Reorder / Einreihen / Stapeln / Lösen) + Titel-Drag eines
// schwebenden Fensters über eine Rand-Andockzone. Der eigentliche Drag-vs-Klick-
// Controller ist `usePanelDrag` (panels/panelDrag.tsx); dieser Hook bündelt nur
// den App-lokalen Float-Titel-Drag-Zustand + die abgeleiteten Rand-Zonen-Werte.
//
// Extrahiert aus App.tsx. Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
import { useState } from "react";
import { usePanelDrag } from "../panels/panelDrag";
import type { DockId, LayoutState } from "../panels/types";
export function useDockDragState(opts: {
layout: LayoutState;
activateTab: (panelId: string) => void;
joinGroup: (panelId: string, dock: DockId, group: number, index?: number) => void;
newGroup: (panelId: string, dock: DockId, at: number) => void;
dropFloat: (panelId: string, x: number, y: number) => void;
dockFloatTo: (panelId: string, dock: DockId) => void;
}) {
const { layout, activateTab, joinGroup, newGroup, dropFloat, dockFloatTo } = opts;
// Der Controller (usePanelDrag) klärt anhand der Bewegung Klick vs. Ziehen
// und löst beim Loslassen das Ziel auf; die Auflösung selbst macht der Slice.
const { drag, startTabDrag } = usePanelDrag({
onActivate: activateTab,
onJoinGroup: joinGroup,
onNewGroup: newGroup,
onDropFloat: dropFloat,
});
// Zustand für die Rand-Zonen-Hervorhebung beim Titel-Drag eines Fensters.
// `floatDragging` schaltet die Zonen ein (sobald die Titelleiste gezogen
// wird), `floatDragHover` markiert die gerade überfahrene Zone.
const [floatDragging, setFloatDragging] = useState(false);
const [floatDragHover, setFloatDragHover] = useState<DockId | null>(null);
// Titel-Drag eines schwebenden Fensters über eine Rand-Zone: liefert true,
// wenn das Fenster angedockt wurde (dann unterbleibt das reine Verschieben).
// Trefferprüfung über die Datenattribute der Rand-Zonen (data-drop-zone).
const onFloatTitleDrop = (panelId: string, clientX: number, clientY: number): boolean => {
setFloatDragging(false);
setFloatDragHover(null);
const zone = document
.elementsFromPoint(clientX, clientY)
.find((el) => (el as HTMLElement).dataset?.dropZone !== undefined) as
| HTMLElement
| undefined;
if (!zone) return false;
const dock = zone.dataset.dropZone as DockId;
dockFloatTo(panelId, dock);
return true;
};
const onFloatTitleDrag = (_panelId: string, clientX: number, clientY: number) => {
if (!floatDragging) setFloatDragging(true);
const zone = document
.elementsFromPoint(clientX, clientY)
.find((el) => (el as HTMLElement).dataset?.dropZone !== undefined) as
| HTMLElement
| undefined;
setFloatDragHover(zone ? (zone.dataset.dropZone as DockId) : null);
};
// Ein Dock wird nur gerendert, wenn es Tabs hat — ein leeres Dock soll die
// Mitte nicht stauchen. (Rechtes Dock ist standardmäßig leer: Ressourcen ist
// ein schwebendes Fenster.)
const leftHasTabs = layout.left.groups.length > 0;
const rightHasTabs = layout.right.groups.length > 0;
// Rand-Andock-Zonen sind aktiv, solange irgendeine Drag-Geste läuft (Tab-Drag
// ODER Titel-Drag eines schwebenden Fensters). Die gerade überfahrene Zone
// (für die Hervorhebung) kommt aus dem Tab-Drag-Hover bzw. dem Float-Hover.
const dragActive = drag !== null || floatDragging;
const activeZone: DockId | null =
drag && drag.hover.kind === "zone" ? drag.hover.dock : floatDragHover;
return {
drag,
startTabDrag,
onFloatTitleDrop,
onFloatTitleDrag,
leftHasTabs,
rightHasTabs,
dragActive,
activeZone,
};
}