diff --git a/src/App.tsx b/src/App.tsx index 6180e37..c2e5a90 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1598,6 +1598,8 @@ export default function App() { const toggleCategoryVisible = useStore((s) => s.toggleCategoryVisible); const addFloor = useStore((s) => s.addFloor); 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 importDrawings = useStore((s) => s.importDrawings); const addCategory = useStore((s) => s.addCategory); @@ -1906,6 +1908,8 @@ export default function App() { onToggleLevel: toggleLevelVisible, onAddFloor: addFloor, onAddDrawing: addDrawing, + onAddSection: addSection, + onAddElevation: addElevation, onLevelContextMenu: (id, x, y) => setMenu({ kind: "level", id, x, y }), activeCategoryCode, diff --git a/src/panels/DrawingLevelsPanel.tsx b/src/panels/DrawingLevelsPanel.tsx index c8e0fc4..d559a93 100644 --- a/src/panels/DrawingLevelsPanel.tsx +++ b/src/panels/DrawingLevelsPanel.tsx @@ -1,18 +1,20 @@ // Inhalts-Panel „Zeichnungsebenen" — die obere Navigator-Liste als Panel. // -// Zeigt die Zeichnungsebenen (Geschosse + Schnitte/Ansichten/Zeichnungen) -// exakt wie der bisherige Navigator: EyeIcon schaltet die Sichtbarkeit, ein -// Kind-Badge zeigt die Art, die aktive Ebene ist hervorgehoben, Höhen-Readout -// (+OKFF) bei Geschossen. Kopfzeile mit + Geschoss / + Zeichnung. +// Zeigt die Zeichnungsebenen gruppiert in vier einklappbaren Kategorien +// (Geschosse / Schnitte / Ansichten / 2D-Zeichnungen, in dieser Reihenfolge). +// Jede Kategorie hat einen eigenen Kopf mit Titel, Ein-/Ausklapp-Chevron und +// 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 -// liest den gewählten DisplayMode aus dem Host-Context. `itemDisplay` -// entscheidet je Zeile, ob sie gezeichnet und ob sie ausgegraut wird (aktives -// Element = Arbeitsfokus, immer sichtbar). Daten/Handler kommen über -// usePanelHost — kein Prop-Drilling. +// Das Panel deklariert hasDisplayMode und liest den gewählten DisplayMode aus +// dem Host-Context. `itemDisplay` entscheidet je Zeile, ob sie gezeichnet und +// ob sie ausgegraut wird (aktives Element = Arbeitsfokus, immer sichtbar). +// Daten/Handler kommen über usePanelHost — kein Prop-Drilling. // // Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md). +import { useState } from "react"; import { EyeIcon } from "../ui/EyeIcon"; import { itemDisplay } from "./displayMode"; import { usePanelHost } from "./host"; @@ -29,11 +31,49 @@ const KIND_BADGE_KEY: Record = { 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 = { + 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 = { + floor: "levels.addFloor", + section: "levels.addSection", + elevation: "levels.addElevation", + drawing: "levels.addDrawing", +}; + /** Inhalt des Panels „Zeichnungsebenen". */ export function DrawingLevelsPanel() { const host = usePanelHost(); const { project, activeLevelId, displayMode } = host; + // Auf-/Zu-Zustand je Kategorie, lokal — Default: alle aufgeklappt. + const [collapsed, setCollapsed] = useState>( + { floor: false, section: false, elevation: false, drawing: false }, + ); + const toggleCategory = (kind: DrawingLevelKind) => + setCollapsed((c) => ({ ...c, [kind]: !c[kind] })); + + const onAddByKind: Record void> = { + floor: host.onAddFloor, + section: host.onAddSection, + elevation: host.onAddElevation, + drawing: host.onAddDrawing, + }; + // Oberstes Geschoss zuoberst (wie DOSSIER): Anzeige umkehren. const levelsTopDown = [...project.drawingLevels].reverse(); @@ -55,29 +95,58 @@ export function DrawingLevelsPanel() {
{t("nav.drawingLevels")} - - - -
-
- {levelsTopDown.map((level) => ( - host.onSelectLevel(level.id)} - onToggle={() => toggle(level.id)} - onContext={(e) => onRowContext(level.id, e)} - /> - ))} -
+ {CATEGORY_ORDER.map((kind) => { + const levelsOfKind = levelsTopDown.filter((l) => l.kind === kind); + const isOpen = !collapsed[kind]; + return ( +
+
toggleCategory(kind)} + > + + + {t(CATEGORY_TITLE_KEY[kind])} + + { + e.stopPropagation(); + onAddByKind[kind](); + }} + > + + + +
+ + {isOpen && ( +
+ {levelsOfKind.map((level) => ( + host.onSelectLevel(level.id)} + onToggle={() => toggle(level.id)} + onContext={(e) => onRowContext(level.id, e)} + /> + ))} +
+ )} +
+ ); + })}
); } diff --git a/src/panels/host.ts b/src/panels/host.ts index 026475b..ba015bf 100644 --- a/src/panels/host.ts +++ b/src/panels/host.ts @@ -82,6 +82,8 @@ export interface PanelHostValue { onToggleLevel: (id: string) => void; onAddFloor: () => void; onAddDrawing: () => void; + onAddSection: () => void; + onAddElevation: () => void; /** * Rechtsklick auf eine Zeichnungsebenen-Zeile: öffnet das Kontextmenü an der * Cursorposition. App hält das offene Menü und baut die Einträge diff --git a/src/state/projectSlice.ts b/src/state/projectSlice.ts index 42eae50..7e551dd 100644 --- a/src/state/projectSlice.ts +++ b/src/state/projectSlice.ts @@ -41,6 +41,10 @@ export interface ProjectSlice { patchActiveFloor: (patch: Partial) => void; addFloor: () => 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 * 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] }; }), + // 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 // zurückgeben (Ziel-Ebene eines DXF-Imports). Der Name kommt vom Aufrufer // (Dialog); leere Eingabe fällt auf den Default-Namen zurück. diff --git a/src/styles.css b/src/styles.css index 8a35346..115f780 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1110,6 +1110,70 @@ body { 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). */ .nav-row { display: flex;