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}
|
onSnapChange={setSnap}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Schwebendes Ressourcen-Fenster ÜBER allem (hoher z-index, abgedunkelter
|
{/* Frei schwebendes, NICHT-modales Ressourcen-Fenster: kein Backdrop, der
|
||||||
Hintergrund). Schließt über X / Klick auf den Hintergrund / Esc. */}
|
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 && (
|
{resourcesOpen && (
|
||||||
<ResourceManager
|
<ResourceManager
|
||||||
project={project}
|
project={project}
|
||||||
|
|||||||
@@ -1951,6 +1951,31 @@ body {
|
|||||||
box-shadow: var(--shadow-3);
|
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. */
|
/* Kopf der Schublade. */
|
||||||
.res-head {
|
.res-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
+111
-8
@@ -17,6 +17,7 @@
|
|||||||
// (CONVENTIONS.md).
|
// (CONVENTIONS.md).
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import type { PointerEvent as ReactPointerEvent } from "react";
|
||||||
import type {
|
import type {
|
||||||
CeilingType,
|
CeilingType,
|
||||||
Component,
|
Component,
|
||||||
@@ -1917,10 +1918,45 @@ export interface ResourceManagerHandlers {
|
|||||||
onImportHatches: (hatches: Omit<HatchStyle, "id">[]) => void;
|
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
|
* Rechte Schublade mit mehreren Tabs (Bauteile / Schraffuren / Linien / …). Wird
|
||||||
* den „Ressourcen"-Eintrag der Oberleiste geöffnet und mutiert das Projekt
|
* über den „Ressourcen"-Eintrag der Oberleiste geöffnet und mutiert das Projekt
|
||||||
* live über die gereichten Handler.
|
* 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({
|
export function ResourceManager({
|
||||||
project,
|
project,
|
||||||
@@ -1932,16 +1968,75 @@ export function ResourceManager({
|
|||||||
handlers: ResourceManagerHandlers;
|
handlers: ResourceManagerHandlers;
|
||||||
}) {
|
}) {
|
||||||
const [tab, setTab] = useState<TabId>("components");
|
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 (
|
return (
|
||||||
<div className="res-overlay" onClick={onClose}>
|
|
||||||
<aside
|
<aside
|
||||||
className="res-drawer"
|
className="res-window"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-label={t("resources.label")}
|
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">
|
<div className="res-head-title">
|
||||||
<span className="res-head-icon">
|
<span className="res-head-icon">
|
||||||
<EyeIcon open />
|
<EyeIcon open />
|
||||||
@@ -2012,7 +2107,15 @@ export function ResourceManager({
|
|||||||
)}
|
)}
|
||||||
{tab === "materials" && <MaterialsTab />}
|
{tab === "materials" && <MaterialsTab />}
|
||||||
</div>
|
</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>
|
</aside>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user