Kopfzeile der Zeichenflaeche: Dokument-Tabs statt Geschoss-Eigenschaften

Die alte Eigenschaften-Leiste (Geschosshoehe/Schnitthoehe/OKFF) weicht einer
Tab-Leiste 'Zeichnungen': ein Tab je Geschoss-Grundriss, Klick wechselt die
aktive Zeichnungsebene. Bindet an activeLevelId/setActiveLevelId — dieselbe
Store-Quelle wie das Panel 'Zeichnungsebenen', beide bleiben synchron.
Geschosshoehe/Schnitthoehe werden in den Geschoss-Einstellungen gepflegt.

Der Drawing-Typ (kind: 'plan') und der switch beim Aktivieren lassen spaeter
Schnitt-/Layout-Tabs zu, ohne die Leiste umzubauen.
This commit is contained in:
2026-07-03 21:39:56 +02:00
parent f4e70902d0
commit 5fd0161bc7
5 changed files with 167 additions and 102 deletions
+37 -52
View File
@@ -156,6 +156,8 @@ import {
} from "./state/visibilitySets"; } from "./state/visibilitySets";
// Seiteneffekt: registriert die zwei dockbaren Kern-Panels in der Registry. // Seiteneffekt: registriert die zwei dockbaren Kern-Panels in der Registry.
import { BUILTIN_PANEL_IDS } from "./panels/builtinPanels"; import { BUILTIN_PANEL_IDS } from "./panels/builtinPanels";
import { DrawingTabs, deriveDrawings } from "./DrawingTabs";
import type { Drawing } from "./DrawingTabs";
/** /**
* Offenes Kontextmenü — genau eine Quelle. `x/y` sind Viewport-Pixel (clientX/Y). * Offenes Kontextmenü — genau eine Quelle. `x/y` sind Viewport-Pixel (clientX/Y).
@@ -1593,7 +1595,6 @@ export default function App() {
// Panels/Menüs/Editoren weiter. // Panels/Menüs/Editoren weiter.
const toggleLevelVisible = useStore((s) => s.toggleLevelVisible); const toggleLevelVisible = useStore((s) => s.toggleLevelVisible);
const toggleCategoryVisible = useStore((s) => s.toggleCategoryVisible); const toggleCategoryVisible = useStore((s) => s.toggleCategoryVisible);
const patchActiveFloor = useStore((s) => s.patchActiveFloor);
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 addDrawingLevel = useStore((s) => s.addDrawingLevel); const addDrawingLevel = useStore((s) => s.addDrawingLevel);
@@ -2195,6 +2196,14 @@ export default function App() {
// referenzstabil → triggert sonst keinen Neuaufbau). // referenzstabil → triggert sonst keinen Neuaufbau).
const displayKey = `${drawingLevelsMode}|${layersMode}|${activeLevelId}`; const displayKey = `${drawingLevelsMode}|${layersMode}|${activeLevelId}`;
// Dokument-Tabs der Zeichenflächen-Kopfzeile — ein Tab je Geschoss-
// Grundriss (siehe DrawingTabs.tsx). Teilt sich `activeLevelId`/
// `setActiveLevelId` mit dem Panel „Zeichnungsebenen".
const drawings = useMemo(
() => deriveDrawings(project.drawingLevels),
[project.drawingLevels],
);
// ── Ableitungen für Oberleiste + Status-Leiste ──────────────────────────── // ── Ableitungen für Oberleiste + Status-Leiste ────────────────────────────
// Eine zoom-/cursor-fähige Plan-Ansicht ist nur im Grundriss eines Geschosses // Eine zoom-/cursor-fähige Plan-Ansicht ist nur im Grundriss eines Geschosses
// aktiv. Sonst sind Cursor/Zoom bedeutungslos (PlanView ist nicht montiert → // aktiv. Sonst sind Cursor/Zoom bedeutungslos (PlanView ist nicht montiert →
@@ -2794,7 +2803,8 @@ export default function App() {
view3d={view3d} view3d={view3d}
fov={fov} fov={fov}
mono={planColorMode === "mono"} mono={planColorMode === "mono"}
onPatchFloor={patchActiveFloor} drawings={drawings}
onSelectLevel={setActiveLevelId}
floorDisplay={floorDisplay} floorDisplay={floorDisplay}
categoryDisplay={categoryDisplay} categoryDisplay={categoryDisplay}
displayKey={displayKey} displayKey={displayKey}
@@ -3732,7 +3742,8 @@ function Content({
view3d, view3d,
fov, fov,
mono, mono,
onPatchFloor, drawings,
onSelectLevel,
floorDisplay, floorDisplay,
categoryDisplay, categoryDisplay,
displayKey, displayKey,
@@ -3790,7 +3801,11 @@ function Content({
fov: number; fov: number;
/** Grundriss in Schwarz-Weiss (mono) statt mit Kategorie-/Element-Farben. */ /** Grundriss in Schwarz-Weiss (mono) statt mit Kategorie-/Element-Farben. */
mono: boolean; mono: boolean;
onPatchFloor: (patch: Partial<DrawingLevel>) => void; /** Dokument-Tabs der Kopfzeile — ein Tab je Geschoss-Grundriss. */
drawings: Drawing[];
/** Aktiviert eine Zeichnungsebene (Geschoss) — geteilt mit dem Panel
* „Zeichnungsebenen"; siehe DrawingTabs.tsx. */
onSelectLevel: (id: string) => void;
floorDisplay: DisplayResolver; floorDisplay: DisplayResolver;
categoryDisplay: CategoryDisplayResolver & DisplayResolver; categoryDisplay: CategoryDisplayResolver & DisplayResolver;
displayKey: string; displayKey: string;
@@ -3853,6 +3868,11 @@ function Content({
if (level.kind === "section" || level.kind === "elevation") { if (level.kind === "section" || level.kind === "elevation") {
return ( return (
<main className="content"> <main className="content">
<DrawingTabs
drawings={drawings}
activeLevelId={level.id}
onSelectLevel={onSelectLevel}
/>
<SectionPlanView <SectionPlanView
project={project} project={project}
level={level} level={level}
@@ -3894,6 +3914,11 @@ function Content({
if (level.kind === "drawing") { if (level.kind === "drawing") {
return ( return (
<main className="content"> <main className="content">
<DrawingTabs
drawings={drawings}
activeLevelId={level.id}
onSelectLevel={onSelectLevel}
/>
<LevelPlanView <LevelPlanView
project={project} project={project}
level={level} level={level}
@@ -3932,10 +3957,16 @@ function Content({
); );
} }
// Geschoss: Eigenschaften-Leiste + gewählte Ansicht. // Geschoss: Dokument-Tabs + gewählte Ansicht. Geschosshöhe/Schnitthöhe
// werden in den Geschoss-Einstellungen gepflegt (Rechtsklick auf das
// Geschoss im Panel „Zeichnungsebenen"), nicht mehr hier in der Kopfzeile.
return ( return (
<main className="content"> <main className="content">
<FloorProps level={level} onPatch={onPatchFloor} /> <DrawingTabs
drawings={drawings}
activeLevelId={level.id}
onSelectLevel={onSelectLevel}
/>
{viewType === "perspektive" ? ( {viewType === "perspektive" ? (
<section className="pane"> <section className="pane">
<div className="pane-title"> <div className="pane-title">
@@ -4008,52 +4039,6 @@ function Content({
); );
} }
/** Kompakte Eigenschaften-Leiste eines Geschosses. */
function FloorProps({
level,
onPatch,
}: {
level: DrawingLevel;
onPatch: (patch: Partial<DrawingLevel>) => void;
}) {
return (
<div className="props-strip">
<label className="prop">
<span className="prop-label">{t("props.floorHeight")}</span>
<span className="prop-input">
<input
type="number"
step={0.05}
min={0}
value={level.floorHeight ?? 0}
onChange={(e) => onPatch({ floorHeight: Number(e.target.value) })}
/>
<span className="prop-unit">{t("editor.unit.m")}</span>
</span>
</label>
<label className="prop">
<span className="prop-label">{t("props.cutHeight")}</span>
<span className="prop-input">
<input
type="number"
step={0.05}
min={0}
value={level.cutHeight ?? 0}
onChange={(e) => onPatch({ cutHeight: Number(e.target.value) })}
/>
<span className="prop-unit">{t("editor.unit.m")}</span>
</span>
</label>
<span className="prop prop-readonly">
<span className="prop-label">{t("props.okff")}</span>
<span className="prop-value">
{t("props.okffValue", { value: (level.baseElevation ?? 0).toFixed(2) })}
</span>
</span>
</div>
);
}
/** Abgeleiteter Grundriss eines Geschosses. */ /** Abgeleiteter Grundriss eines Geschosses. */
function LevelPlanView({ function LevelPlanView({
project, project,
+90
View File
@@ -0,0 +1,90 @@
// Dokument-Tabs „Zeichnungen/Blätter" — Kopfzeile über der Zeichenfläche.
//
// Ersetzt die frühere Kopfzeile mit Geschosshöhe/Schnitthöhe (diese Werte
// werden jetzt ausschliesslich in den Geschoss-Einstellungen gepflegt, siehe
// den Editor `editor.floorHeight`/`editor.cutHeight` in App.tsx). Ein Tab
// entspricht heute immer dem Grundriss eines Geschosses; ein Klick aktiviert
// dessen Zeichnungsebene über denselben State, den auch das Panel
// „Zeichnungsebenen" (DrawingLevelsPanel) verwendet — beide bleiben so
// zwangsläufig synchron.
//
// Der Typ `Drawing` ist bewusst schlank gehalten, damit künftige Arten
// (Schnitt-Blatt, Layout) sich später einhängen können, ohne dass die
// Tab-Leiste selbst umgebaut werden muss — siehe `activate()` unten.
//
// Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md).
import type { DrawingLevel } from "./model/types";
import { t } from "./i18n";
/** Art einer Dokument-Zeichnung. Heute nur der Grundriss eines Geschosses;
* Platz für künftige Arten (z. B. "section", "layout") bleibt offen. */
export type DrawingKind = "plan";
/** Ein Tab der Dokument-Leiste leichtgewichtiger Verweis auf die Quelle
* (aktuell immer eine Zeichnungsebene vom Typ "floor"). */
export interface Drawing {
id: string;
title: string;
kind: DrawingKind;
/** Geschoss/Zeichnungsebene, deren Grundriss dieser Tab zeigt. */
levelId: string;
}
/** Leitet die Dokument-Tabs aus den Geschossen des Projekts ab ein Tab je
* Geschoss. Schnitte/Ansichten und freie Zeichnungen bleiben vorerst aussen
* vor (weiterhin nur über das Panel Zeichnungsebenen" erreichbar). */
export function deriveDrawings(levels: DrawingLevel[]): Drawing[] {
return levels
.filter((level): level is DrawingLevel & { kind: "floor" } => level.kind === "floor")
.map((level) => ({
id: level.id,
title: level.name,
kind: "plan",
levelId: level.id,
}));
}
export function DrawingTabs({
drawings,
activeLevelId,
onSelectLevel,
}: {
drawings: Drawing[];
/** Aktuell aktive Zeichnungsebene (globaler App-State, siehe App.tsx). */
activeLevelId: string;
/** Setter derselben Zeichnungsebene identisch mit dem, den das Panel
* Zeichnungsebenen" verwendet (kein paralleler State). */
onSelectLevel: (id: string) => void;
}) {
if (drawings.length === 0) return null;
// Aktivierung nach Art dispatchen. Heute gibt es nur "plan" (→ Geschoss
// aktivieren); künftige Arten hängen hier ihr eigenes Ziel ein, ohne dass
// sich an der Tab-Leiste selbst etwas ändert.
const activate = (drawing: Drawing) => {
switch (drawing.kind) {
case "plan":
onSelectLevel(drawing.levelId);
break;
}
};
return (
<div className="drawing-tabs" role="tablist" aria-label={t("tabs.drawings")}>
{drawings.map((d) => (
<button
key={d.id}
type="button"
role="tab"
aria-selected={d.levelId === activeLevelId}
className={`drawing-tab${d.levelId === activeLevelId ? " active" : ""}`}
onClick={() => activate(d)}
title={d.title}
>
<span className="drawing-tab-label">{d.title}</span>
</button>
))}
</div>
);
}
+3
View File
@@ -191,6 +191,9 @@ export const de = {
"status.rendererWasm": "Engine", "status.rendererWasm": "Engine",
"status.rendererWasmUnavailable": "WebGPU nicht verfügbar", "status.rendererWasmUnavailable": "WebGPU nicht verfügbar",
// ── Dokument-Tabs (Zeichnungen/Blätter) ─────────────────────────────────
"tabs.drawings": "Zeichnungen",
// ── Navigator-Panels ───────────────────────────────────────────────────── // ── Navigator-Panels ─────────────────────────────────────────────────────
"nav.drawingLevels": "Zeichnungsebenen", "nav.drawingLevels": "Zeichnungsebenen",
"nav.layers": "Ebenen", "nav.layers": "Ebenen",
+3
View File
@@ -190,6 +190,9 @@ export const en: Record<TranslationKey, string> = {
"status.rendererWasm": "Engine", "status.rendererWasm": "Engine",
"status.rendererWasmUnavailable": "WebGPU unavailable", "status.rendererWasmUnavailable": "WebGPU unavailable",
// Document tabs (drawings/sheets).
"tabs.drawings": "Drawings",
// Navigator panels. // Navigator panels.
"nav.drawingLevels": "Drawing levels", "nav.drawingLevels": "Drawing levels",
"nav.layers": "Layers", "nav.layers": "Layers",
+34 -50
View File
@@ -1306,66 +1306,50 @@ body {
min-width: 0; min-width: 0;
} }
/* Eigenschaften-Leiste eines Geschosses. */ /* Dokument-Tabs Zeichnungen/Blätter" Kopfzeile über der Zeichenfläche.
.props-strip { Leiste auf Ebene 1 (--panel), der aktive Tab hebt sich auf Ebene 2 an und
verschmilzt unten mit dem Grund der Zeichenfläche darunter (--bg), wie bei
klassischen Dokument-Tabs. */
.drawing-tabs {
display: flex; display: flex;
align-items: center; align-items: flex-end;
gap: 18px; gap: 2px;
padding: 8px 16px; padding: 6px 10px 0;
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
background: var(--panel); background: var(--panel);
overflow-x: auto;
scrollbar-width: thin;
} }
.prop { .drawing-tab {
display: inline-flex; border: 1px solid transparent;
align-items: center; border-bottom: none;
gap: 8px; background: transparent;
} color: var(--muted);
font-family: inherit;
.prop-label { font-size: 12px;
font-size: 10px;
font-weight: 600; font-weight: 600;
letter-spacing: 0.06em; letter-spacing: 0.01em;
text-transform: uppercase; padding: 7px 16px;
color: var(--muted); border-radius: 8px 8px 0 0;
cursor: pointer;
white-space: nowrap;
transition: background 0.14s, color 0.14s, border-color 0.14s;
} }
.drawing-tab:hover {
.prop-input { background: var(--panel-2);
display: inline-flex;
align-items: center;
gap: 4px;
}
.prop-input input {
width: 64px;
font-family: var(--font-mono);
font-size: 12px;
color: var(--ink); color: var(--ink);
background: var(--input);
border: 1px solid var(--border);
border-radius: 999px;
padding: 4px 10px;
text-align: right;
outline: none;
transition: border-color 0.14s, box-shadow 0.14s;
} }
.prop-input input:hover { .drawing-tab.active {
border-color: var(--accent); background: var(--bg);
border-color: var(--line);
color: var(--ink);
/* Nahtloser Übergang: die Unterkante verschwindet im Zeichenflächen-Grund. */
position: relative;
top: 1px;
} }
.prop-input input:focus { .drawing-tab-label {
border-color: var(--accent); pointer-events: none;
box-shadow: 0 0 0 2px var(--accent-dim);
}
.prop-unit {
font-size: 11px;
color: var(--muted);
}
.prop-readonly .prop-value {
font-family: var(--font-mono);
font-size: 12px;
color: var(--ink-2);
} }
/* Eine Ansicht füllt die Restfläche. */ /* Eine Ansicht füllt die Restfläche. */