ResourceManager: schwebendes, nicht-modales Fenster statt Modal
Der Ressourcen-Editor ist von einem blockierenden Modal (dunkler Backdrop, Canvas gesperrt) zu einem frei schwebenden Fenster geworden: verschiebbar ueber die Kopfzeile (Pointer-Capture-Drag), unten rechts resizable, KEIN Backdrop — der Plan bleibt bedienbar, sodass Schraffur-/Linien-Aenderungen live am Modell sichtbar sind. Der Ressourcen-Button bleibt Toggle, Esc schliesst weiter. FloatingPanel-Bausteine wiederverwendet; die interne Tab-/Master-Detail-Struktur unveraendert.
This commit is contained in:
+5
-2
@@ -3082,8 +3082,11 @@ export default function App() {
|
||||
onSnapChange={setSnap}
|
||||
/>
|
||||
|
||||
{/* Schwebendes Ressourcen-Fenster ÜBER allem (hoher z-index, abgedunkelter
|
||||
Hintergrund). Schließt über X / Klick auf den Hintergrund / Esc. */}
|
||||
{/* Frei schwebendes, NICHT-modales Ressourcen-Fenster: kein Backdrop, der
|
||||
Grundriss dahinter bleibt bedienbar (Schraffur-/Linien-Änderungen live
|
||||
am Modell sichtbar). Verschiebbar über die Kopfzeile, größenveränderbar
|
||||
über den Greifer unten rechts. Der „Ressourcen"-Knopf toggelt; Esc
|
||||
schließt (siehe Effekt oben). */}
|
||||
{resourcesOpen && (
|
||||
<ResourceManager
|
||||
project={project}
|
||||
|
||||
@@ -1951,6 +1951,31 @@ body {
|
||||
box-shadow: var(--shadow-3);
|
||||
}
|
||||
|
||||
/* Frei schwebendes, NICHT-modales Ressourcen-Fenster (kein Backdrop): schwebt
|
||||
über dem Grundriss, der dahinter bedienbar bleibt. Position/Größe kommen als
|
||||
Inline-Style; die Kopfzeile (res-head) ist der Verschiebegriff. */
|
||||
.res-window {
|
||||
position: fixed;
|
||||
z-index: 700;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-3, 0 12px 32px rgba(0, 0, 0, 0.45));
|
||||
overflow: hidden;
|
||||
}
|
||||
.res-window .res-head {
|
||||
cursor: move;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
/* Greifer unten rechts: nutzt .floating-resize (siehe unten), sitzt aber im
|
||||
overflow:hidden-Fenster korrekt in der Ecke. */
|
||||
.res-window .res-resize {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Kopf der Schublade. */
|
||||
.res-head {
|
||||
display: flex;
|
||||
|
||||
+111
-8
@@ -17,6 +17,7 @@
|
||||
// (CONVENTIONS.md).
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { PointerEvent as ReactPointerEvent } from "react";
|
||||
import type {
|
||||
CeilingType,
|
||||
Component,
|
||||
@@ -1917,10 +1918,45 @@ export interface ResourceManagerHandlers {
|
||||
onImportHatches: (hatches: Omit<HatchStyle, "id">[]) => void;
|
||||
}
|
||||
|
||||
// ── Schwebendes Fenster (nicht-modal) ──────────────────────────────────────
|
||||
// Der Ressourcen-Manager schwebt frei über dem Grundriss, OHNE abdunkelnden
|
||||
// Hintergrund: der Plan bleibt bedienbar, damit Schraffur-/Linien-Änderungen
|
||||
// live am Modell sichtbar sind. Position/Größe leben lokal im Fenster; die
|
||||
// Kopfzeile (res-head) ist der Verschiebegriff, unten rechts sitzt ein
|
||||
// Größen-Greifer — beide über POINTER-Events mit Pointer-Capture (dieselbe
|
||||
// Technik wie FloatingPanel), damit die Geste auch außerhalb weiterläuft.
|
||||
|
||||
/** Standard- und Mindestgröße des schwebenden Ressourcen-Fensters (CSS-Pixel). */
|
||||
const RES_DEFAULT_W = 760;
|
||||
const RES_DEFAULT_H = 640;
|
||||
const RES_MIN_W = 420;
|
||||
const RES_MIN_H = 320;
|
||||
|
||||
/** Rechteck eines frei positionierten Fensters. */
|
||||
interface WindowRect {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
/** Sinnvolle Startlage: rechts oben unter der Oberleiste, in den Viewport geklemmt. */
|
||||
function initialRect(): WindowRect {
|
||||
const vw = typeof window !== "undefined" ? window.innerWidth : 1280;
|
||||
const vh = typeof window !== "undefined" ? window.innerHeight : 800;
|
||||
const w = Math.min(RES_DEFAULT_W, Math.round(vw * 0.9));
|
||||
const h = Math.min(RES_DEFAULT_H, Math.round(vh * 0.85));
|
||||
const x = Math.max(16, vw - w - 24);
|
||||
const y = 64;
|
||||
return { x, y, w, h };
|
||||
}
|
||||
|
||||
/**
|
||||
* Rechte Schublade mit drei Tabs (Bauteile / Schraffuren / Linien). Wird über
|
||||
* den „Ressourcen"-Eintrag der Oberleiste geöffnet und mutiert das Projekt
|
||||
* live über die gereichten Handler.
|
||||
* Rechte Schublade mit mehreren Tabs (Bauteile / Schraffuren / Linien / …). Wird
|
||||
* über den „Ressourcen"-Eintrag der Oberleiste geöffnet und mutiert das Projekt
|
||||
* live über die gereichten Handler. Rendert als FREI SCHWEBENDES, nicht-modales
|
||||
* Fenster (kein Backdrop) — verschiebbar über die Kopfzeile, größenveränderbar
|
||||
* über den Greifer unten rechts.
|
||||
*/
|
||||
export function ResourceManager({
|
||||
project,
|
||||
@@ -1932,16 +1968,75 @@ export function ResourceManager({
|
||||
handlers: ResourceManagerHandlers;
|
||||
}) {
|
||||
const [tab, setTab] = useState<TabId>("components");
|
||||
const [rect, setRect] = useState<WindowRect>(initialRect);
|
||||
|
||||
// Verschieben über die Kopfzeile: Cursor − linke/obere Kante festhalten und die
|
||||
// Kante der Cursorbewegung folgen lassen (Pointer-Capture hält die Geste).
|
||||
const onTitlePointerDown = (e: ReactPointerEvent<HTMLElement>) => {
|
||||
// Klicks auf Knöpfe (Schließen) nicht als Verschiebe-Start werten.
|
||||
if ((e.target as HTMLElement).closest("button")) return;
|
||||
e.preventDefault();
|
||||
const handle = e.currentTarget;
|
||||
handle.setPointerCapture(e.pointerId);
|
||||
const dx = e.clientX - rect.x;
|
||||
const dy = e.clientY - rect.y;
|
||||
const move = (ev: PointerEvent) => {
|
||||
setRect((r) => ({ ...r, x: ev.clientX - dx, y: ev.clientY - dy }));
|
||||
};
|
||||
const up = (ev: PointerEvent) => {
|
||||
handle.removeEventListener("pointermove", move);
|
||||
handle.removeEventListener("pointerup", up);
|
||||
handle.removeEventListener("pointercancel", up);
|
||||
if (handle.hasPointerCapture(ev.pointerId)) {
|
||||
handle.releasePointerCapture(ev.pointerId);
|
||||
}
|
||||
};
|
||||
handle.addEventListener("pointermove", move);
|
||||
handle.addEventListener("pointerup", up);
|
||||
handle.addEventListener("pointercancel", up);
|
||||
};
|
||||
|
||||
// Größe ändern über den Greifer unten rechts (Deltas ab Startpunkt, Mindestmaß).
|
||||
const onResizePointerDown = (e: ReactPointerEvent<HTMLElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const handle = e.currentTarget;
|
||||
handle.setPointerCapture(e.pointerId);
|
||||
const startX = e.clientX;
|
||||
const startY = e.clientY;
|
||||
const startW = rect.w;
|
||||
const startH = rect.h;
|
||||
const move = (ev: PointerEvent) => {
|
||||
const w = Math.max(RES_MIN_W, Math.round(startW + (ev.clientX - startX)));
|
||||
const h = Math.max(RES_MIN_H, Math.round(startH + (ev.clientY - startY)));
|
||||
setRect((r) => ({ ...r, w, h }));
|
||||
};
|
||||
const up = (ev: PointerEvent) => {
|
||||
handle.removeEventListener("pointermove", move);
|
||||
handle.removeEventListener("pointerup", up);
|
||||
handle.removeEventListener("pointercancel", up);
|
||||
if (handle.hasPointerCapture(ev.pointerId)) {
|
||||
handle.releasePointerCapture(ev.pointerId);
|
||||
}
|
||||
};
|
||||
handle.addEventListener("pointermove", move);
|
||||
handle.addEventListener("pointerup", up);
|
||||
handle.addEventListener("pointercancel", up);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="res-overlay" onClick={onClose}>
|
||||
<aside
|
||||
className="res-drawer"
|
||||
className="res-window"
|
||||
role="dialog"
|
||||
aria-label={t("resources.label")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
left: `${rect.x}px`,
|
||||
top: `${rect.y}px`,
|
||||
width: `${rect.w}px`,
|
||||
height: `${rect.h}px`,
|
||||
}}
|
||||
>
|
||||
<header className="res-head">
|
||||
<header className="res-head" onPointerDown={onTitlePointerDown}>
|
||||
<div className="res-head-title">
|
||||
<span className="res-head-icon">
|
||||
<EyeIcon open />
|
||||
@@ -2012,7 +2107,15 @@ export function ResourceManager({
|
||||
)}
|
||||
{tab === "materials" && <MaterialsTab />}
|
||||
</div>
|
||||
|
||||
{/* Greifer unten rechts (Größe ändern). */}
|
||||
<div
|
||||
className="floating-resize res-resize"
|
||||
role="separator"
|
||||
aria-label={t("dock.resizePanel")}
|
||||
title={t("dock.resizePanel")}
|
||||
onPointerDown={onResizePointerDown}
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user