From c224f9e4f9f04485f9f827cfcef2704b0e75e006 Mon Sep 17 00:00:00 2001 From: Karim Date: Thu, 20 Aug 2026 00:48:34 +0200 Subject: [PATCH] Tab-Drag-Steuerung aus App.tsx in eigenen Hook extrahiert (state/useDockDragState.ts). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/App.tsx | 75 ++++++++---------------------- src/state/useDockDragState.ts | 86 +++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 55 deletions(-) create mode 100644 src/state/useDockDragState.ts diff --git a/src/App.tsx b/src/App.tsx index 09c0d84..9120821 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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(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, diff --git a/src/state/useDockDragState.ts b/src/state/useDockDragState.ts new file mode 100644 index 0000000..d5c47ac --- /dev/null +++ b/src/state/useDockDragState.ts @@ -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(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, + }; +}