Zeichnungsebenen-Panel: vier einklappbare Kategorien
Die flache Ebenenliste ist in vier einklappbare Kategorien gegliedert — Geschosse/Schnitte/Ansichten/2D-Zeichnungen nach drawingLevel.kind. Jeder Kopf trägt ein Chevron und rechtsbündig ein blankes +-Glyph (kein Button-Chrome), das je Kategorie eine neue Ebene der passenden Art anlegt; leere Kategorien bleiben mit Kopf und + sichtbar. Für Schnitt und Ansicht sind dafür neue Platzhalter-Aktionen addSection/addElevation ergänzt (analog addFloor/addDrawing, noch ohne Schnittlinie).
This commit is contained in:
@@ -1598,6 +1598,8 @@ export default function App() {
|
|||||||
const toggleCategoryVisible = useStore((s) => s.toggleCategoryVisible);
|
const toggleCategoryVisible = useStore((s) => s.toggleCategoryVisible);
|
||||||
const addFloor = useStore((s) => s.addFloor);
|
const addFloor = useStore((s) => s.addFloor);
|
||||||
const addDrawing = useStore((s) => s.addDrawing);
|
const addDrawing = useStore((s) => s.addDrawing);
|
||||||
|
const addSection = useStore((s) => s.addSection);
|
||||||
|
const addElevation = useStore((s) => s.addElevation);
|
||||||
const addDrawingLevel = useStore((s) => s.addDrawingLevel);
|
const addDrawingLevel = useStore((s) => s.addDrawingLevel);
|
||||||
const importDrawings = useStore((s) => s.importDrawings);
|
const importDrawings = useStore((s) => s.importDrawings);
|
||||||
const addCategory = useStore((s) => s.addCategory);
|
const addCategory = useStore((s) => s.addCategory);
|
||||||
@@ -1906,6 +1908,8 @@ export default function App() {
|
|||||||
onToggleLevel: toggleLevelVisible,
|
onToggleLevel: toggleLevelVisible,
|
||||||
onAddFloor: addFloor,
|
onAddFloor: addFloor,
|
||||||
onAddDrawing: addDrawing,
|
onAddDrawing: addDrawing,
|
||||||
|
onAddSection: addSection,
|
||||||
|
onAddElevation: addElevation,
|
||||||
onLevelContextMenu: (id, x, y) =>
|
onLevelContextMenu: (id, x, y) =>
|
||||||
setMenu({ kind: "level", id, x, y }),
|
setMenu({ kind: "level", id, x, y }),
|
||||||
activeCategoryCode,
|
activeCategoryCode,
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
// Inhalts-Panel „Zeichnungsebenen" — die obere Navigator-Liste als Panel.
|
// Inhalts-Panel „Zeichnungsebenen" — die obere Navigator-Liste als Panel.
|
||||||
//
|
//
|
||||||
// Zeigt die Zeichnungsebenen (Geschosse + Schnitte/Ansichten/Zeichnungen)
|
// Zeigt die Zeichnungsebenen gruppiert in vier einklappbaren Kategorien
|
||||||
// exakt wie der bisherige Navigator: EyeIcon schaltet die Sichtbarkeit, ein
|
// (Geschosse / Schnitte / Ansichten / 2D-Zeichnungen, in dieser Reihenfolge).
|
||||||
// Kind-Badge zeigt die Art, die aktive Ebene ist hervorgehoben, Höhen-Readout
|
// Jede Kategorie hat einen eigenen Kopf mit Titel, Ein-/Ausklapp-Chevron und
|
||||||
// (+OKFF) bei Geschossen. Kopfzeile mit + Geschoss / + Zeichnung.
|
// einem blanken „+"-Glyph zum Anlegen einer neuen Zeichnungsebene dieser Art.
|
||||||
|
// Innerhalb einer Kategorie: EyeIcon schaltet die Sichtbarkeit, die aktive
|
||||||
|
// Ebene ist hervorgehoben, Höhen-Readout (+OKFF) bei Geschossen.
|
||||||
//
|
//
|
||||||
// Neu gegenüber dem Inline-Navigator: das Panel deklariert hasDisplayMode und
|
// Das Panel deklariert hasDisplayMode und liest den gewählten DisplayMode aus
|
||||||
// liest den gewählten DisplayMode aus dem Host-Context. `itemDisplay`
|
// dem Host-Context. `itemDisplay` entscheidet je Zeile, ob sie gezeichnet und
|
||||||
// entscheidet je Zeile, ob sie gezeichnet und ob sie ausgegraut wird (aktives
|
// ob sie ausgegraut wird (aktives Element = Arbeitsfokus, immer sichtbar).
|
||||||
// Element = Arbeitsfokus, immer sichtbar). Daten/Handler kommen über
|
// Daten/Handler kommen über usePanelHost — kein Prop-Drilling.
|
||||||
// usePanelHost — kein Prop-Drilling.
|
|
||||||
//
|
//
|
||||||
// Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md).
|
// Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md).
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
import { EyeIcon } from "../ui/EyeIcon";
|
import { EyeIcon } from "../ui/EyeIcon";
|
||||||
import { itemDisplay } from "./displayMode";
|
import { itemDisplay } from "./displayMode";
|
||||||
import { usePanelHost } from "./host";
|
import { usePanelHost } from "./host";
|
||||||
@@ -29,11 +31,49 @@ const KIND_BADGE_KEY: Record<DrawingLevelKind, string> = {
|
|||||||
drawing: "badge.drawing",
|
drawing: "badge.drawing",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Kategorien der Zeichnungsebenen, in Anzeigereihenfolge. */
|
||||||
|
const CATEGORY_ORDER: DrawingLevelKind[] = [
|
||||||
|
"floor",
|
||||||
|
"section",
|
||||||
|
"elevation",
|
||||||
|
"drawing",
|
||||||
|
];
|
||||||
|
|
||||||
|
/** i18n-Key des Kategorie-Titels. */
|
||||||
|
const CATEGORY_TITLE_KEY: Record<DrawingLevelKind, string> = {
|
||||||
|
floor: "levelCat.floor",
|
||||||
|
section: "levelCat.section",
|
||||||
|
elevation: "levelCat.elevation",
|
||||||
|
drawing: "levelCat.drawing",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** i18n-Key der Beschriftung des „+"-Glyphs (Tooltip/aria-label). */
|
||||||
|
const CATEGORY_ADD_KEY: Record<DrawingLevelKind, string> = {
|
||||||
|
floor: "levels.addFloor",
|
||||||
|
section: "levels.addSection",
|
||||||
|
elevation: "levels.addElevation",
|
||||||
|
drawing: "levels.addDrawing",
|
||||||
|
};
|
||||||
|
|
||||||
/** Inhalt des Panels „Zeichnungsebenen". */
|
/** Inhalt des Panels „Zeichnungsebenen". */
|
||||||
export function DrawingLevelsPanel() {
|
export function DrawingLevelsPanel() {
|
||||||
const host = usePanelHost();
|
const host = usePanelHost();
|
||||||
const { project, activeLevelId, displayMode } = host;
|
const { project, activeLevelId, displayMode } = host;
|
||||||
|
|
||||||
|
// Auf-/Zu-Zustand je Kategorie, lokal — Default: alle aufgeklappt.
|
||||||
|
const [collapsed, setCollapsed] = useState<Record<DrawingLevelKind, boolean>>(
|
||||||
|
{ floor: false, section: false, elevation: false, drawing: false },
|
||||||
|
);
|
||||||
|
const toggleCategory = (kind: DrawingLevelKind) =>
|
||||||
|
setCollapsed((c) => ({ ...c, [kind]: !c[kind] }));
|
||||||
|
|
||||||
|
const onAddByKind: Record<DrawingLevelKind, () => void> = {
|
||||||
|
floor: host.onAddFloor,
|
||||||
|
section: host.onAddSection,
|
||||||
|
elevation: host.onAddElevation,
|
||||||
|
drawing: host.onAddDrawing,
|
||||||
|
};
|
||||||
|
|
||||||
// Oberstes Geschoss zuoberst (wie DOSSIER): Anzeige umkehren.
|
// Oberstes Geschoss zuoberst (wie DOSSIER): Anzeige umkehren.
|
||||||
const levelsTopDown = [...project.drawingLevels].reverse();
|
const levelsTopDown = [...project.drawingLevels].reverse();
|
||||||
|
|
||||||
@@ -55,29 +95,58 @@ export function DrawingLevelsPanel() {
|
|||||||
<section className="nav-group">
|
<section className="nav-group">
|
||||||
<header className="nav-group-head">
|
<header className="nav-group-head">
|
||||||
<span className="nav-group-title">{t("nav.drawingLevels")}</span>
|
<span className="nav-group-title">{t("nav.drawingLevels")}</span>
|
||||||
<span className="nav-group-actions">
|
|
||||||
<button className="nav-add" onClick={host.onAddFloor}>
|
|
||||||
{t("levels.addFloor")}
|
|
||||||
</button>
|
|
||||||
<button className="nav-add" onClick={host.onAddDrawing}>
|
|
||||||
{t("levels.addDrawing")}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="nav-list">
|
{CATEGORY_ORDER.map((kind) => {
|
||||||
{levelsTopDown.map((level) => (
|
const levelsOfKind = levelsTopDown.filter((l) => l.kind === kind);
|
||||||
<LevelRow
|
const isOpen = !collapsed[kind];
|
||||||
key={level.id}
|
return (
|
||||||
level={level}
|
<div className="level-cat" key={kind}>
|
||||||
active={level.id === activeLevelId}
|
<header
|
||||||
mode={displayMode.mode}
|
className="level-cat-head"
|
||||||
onSelect={() => host.onSelectLevel(level.id)}
|
onClick={() => toggleCategory(kind)}
|
||||||
onToggle={() => toggle(level.id)}
|
>
|
||||||
onContext={(e) => onRowContext(level.id, e)}
|
<span
|
||||||
/>
|
className={`level-cat-chevron${isOpen ? " open" : ""}`}
|
||||||
))}
|
aria-hidden="true"
|
||||||
</div>
|
>
|
||||||
|
▸
|
||||||
|
</span>
|
||||||
|
<span className="level-cat-title">
|
||||||
|
{t(CATEGORY_TITLE_KEY[kind])}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="level-cat-add"
|
||||||
|
role="button"
|
||||||
|
aria-label={t(CATEGORY_ADD_KEY[kind])}
|
||||||
|
title={t(CATEGORY_ADD_KEY[kind])}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onAddByKind[kind]();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="nav-list">
|
||||||
|
{levelsOfKind.map((level) => (
|
||||||
|
<LevelRow
|
||||||
|
key={level.id}
|
||||||
|
level={level}
|
||||||
|
active={level.id === activeLevelId}
|
||||||
|
mode={displayMode.mode}
|
||||||
|
onSelect={() => host.onSelectLevel(level.id)}
|
||||||
|
onToggle={() => toggle(level.id)}
|
||||||
|
onContext={(e) => onRowContext(level.id, e)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ export interface PanelHostValue {
|
|||||||
onToggleLevel: (id: string) => void;
|
onToggleLevel: (id: string) => void;
|
||||||
onAddFloor: () => void;
|
onAddFloor: () => void;
|
||||||
onAddDrawing: () => void;
|
onAddDrawing: () => void;
|
||||||
|
onAddSection: () => void;
|
||||||
|
onAddElevation: () => void;
|
||||||
/**
|
/**
|
||||||
* Rechtsklick auf eine Zeichnungsebenen-Zeile: öffnet das Kontextmenü an der
|
* Rechtsklick auf eine Zeichnungsebenen-Zeile: öffnet das Kontextmenü an der
|
||||||
* Cursorposition. App hält das offene Menü und baut die Einträge
|
* Cursorposition. App hält das offene Menü und baut die Einträge
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ export interface ProjectSlice {
|
|||||||
patchActiveFloor: (patch: Partial<DrawingLevel>) => void;
|
patchActiveFloor: (patch: Partial<DrawingLevel>) => void;
|
||||||
addFloor: () => void;
|
addFloor: () => void;
|
||||||
addDrawing: () => void;
|
addDrawing: () => void;
|
||||||
|
/** Legt eine neue Schnitt-Zeichnungsebene (kind:"section") als Platzhalter an. */
|
||||||
|
addSection: () => void;
|
||||||
|
/** Legt eine neue Ansichts-Zeichnungsebene (kind:"elevation") als Platzhalter an. */
|
||||||
|
addElevation: () => void;
|
||||||
/**
|
/**
|
||||||
* Legt eine neue freie Zeichnungsebene (kind:"drawing") mit dem gegebenen
|
* Legt eine neue freie Zeichnungsebene (kind:"drawing") mit dem gegebenen
|
||||||
* Namen an und liefert ihre ID zurück (z. B. als Ziel eines DXF-Imports).
|
* Namen an und liefert ihre ID zurück (z. B. als Ziel eines DXF-Imports).
|
||||||
@@ -309,6 +313,34 @@ export function createProjectSlice(
|
|||||||
return { ...p, drawingLevels: [...p.drawingLevels, drawing] };
|
return { ...p, drawingLevels: [...p.drawingLevels, drawing] };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// Schnitt/Ansicht sind vorerst Platzhalter ohne Schnittlinie (linePoints
|
||||||
|
// bleibt undefined, bis die Schnitt-UI existiert — siehe toSection.ts).
|
||||||
|
addSection: () =>
|
||||||
|
setProject((p) => {
|
||||||
|
const n = p.drawingLevels.filter((z) => z.kind === "section").length + 1;
|
||||||
|
const section: DrawingLevel = {
|
||||||
|
id: `section-${Date.now()}`,
|
||||||
|
name: t("default.sectionName", { n }),
|
||||||
|
kind: "section",
|
||||||
|
visible: true,
|
||||||
|
locked: false,
|
||||||
|
};
|
||||||
|
return { ...p, drawingLevels: [...p.drawingLevels, section] };
|
||||||
|
}),
|
||||||
|
|
||||||
|
addElevation: () =>
|
||||||
|
setProject((p) => {
|
||||||
|
const n = p.drawingLevels.filter((z) => z.kind === "elevation").length + 1;
|
||||||
|
const elevation: DrawingLevel = {
|
||||||
|
id: `elevation-${Date.now()}`,
|
||||||
|
name: t("default.elevationName", { n }),
|
||||||
|
kind: "elevation",
|
||||||
|
visible: true,
|
||||||
|
locked: false,
|
||||||
|
};
|
||||||
|
return { ...p, drawingLevels: [...p.drawingLevels, elevation] };
|
||||||
|
}),
|
||||||
|
|
||||||
// Neue freie Zeichnungsebene mit explizitem Namen anlegen und ihre ID
|
// Neue freie Zeichnungsebene mit explizitem Namen anlegen und ihre ID
|
||||||
// zurückgeben (Ziel-Ebene eines DXF-Imports). Der Name kommt vom Aufrufer
|
// zurückgeben (Ziel-Ebene eines DXF-Imports). Der Name kommt vom Aufrufer
|
||||||
// (Dialog); leere Eingabe fällt auf den Default-Namen zurück.
|
// (Dialog); leere Eingabe fällt auf den Default-Namen zurück.
|
||||||
|
|||||||
@@ -1110,6 +1110,70 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Zeichnungsebenen: einklappbare Kategorien (Geschosse/Schnitte/
|
||||||
|
Ansichten/2D-Zeichnungen) ──────────────────────────────────────────── */
|
||||||
|
.level-cat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-cat-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 3px 10px 3px 6px;
|
||||||
|
background: var(--panel-2);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.level-cat-head:hover {
|
||||||
|
background: var(--accent-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-cat-chevron {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--muted);
|
||||||
|
transition: transform 0.12s;
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
.level-cat-chevron.open {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-cat-title {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blankes „+"-Glyph: reines Zeichen, keine Button-Chrome. */
|
||||||
|
.level-cat-add {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.level-cat-add:hover {
|
||||||
|
color: var(--accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
/* Zeile (Zeichnungsebene). */
|
/* Zeile (Zeichnungsebene). */
|
||||||
.nav-row {
|
.nav-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user