Ribbon-Tabs in die TopBar-Zeile verlegt: eine Leiste (Chrome + Tabs), Inhalt darunter

This commit is contained in:
2026-07-05 18:04:48 +02:00
parent 13c3294f4d
commit 456ebc8098
4 changed files with 103 additions and 60 deletions
+8
View File
@@ -71,6 +71,7 @@ import type { ResourceManagerHandlers } from "./ui/ResourceManager";
import { HatchSwatch } from "./ui/hatchPreview"; import { HatchSwatch } from "./ui/hatchPreview";
import { TopBar, ViewRibbonTab } from "./ui/TopBar"; import { TopBar, ViewRibbonTab } from "./ui/TopBar";
import { RibbonBar } from "./ui/ribbon/RibbonBar"; import { RibbonBar } from "./ui/ribbon/RibbonBar";
import type { RibbonTabId } from "./ui/ribbon/ribbonItems";
import { Dropdown } from "./ui/Dropdown"; import { Dropdown } from "./ui/Dropdown";
import type { DropdownItem } from "./ui/Dropdown"; import type { DropdownItem } from "./ui/Dropdown";
import { TextEditorDialog } from "./ui/TextEditorDialog"; import { TextEditorDialog } from "./ui/TextEditorDialog";
@@ -355,6 +356,10 @@ export default function App() {
// Einstellungen sowie der laufende Werkzeug-Zustand (Ref, kein Re-Render je // Einstellungen sowie der laufende Werkzeug-Zustand (Ref, kein Re-Render je
// Mausbewegung) und die Live-Vorschau (Draft → Overlay in PlanView). // Mausbewegung) und die Live-Vorschau (Draft → Overlay in PlanView).
const [activeTool, setActiveTool] = useState<ToolId>("select"); const [activeTool, setActiveTool] = useState<ToolId>("select");
// Aktiver Ribbon-Tab (2D/3D/BIM/Ansichten). Liegt hier (nicht in der RibbonBar),
// weil die Tab-Reiter in der TopBar-Zeile sitzen und der Ribbon-Inhalt darunter
// — beide teilen sich denselben aktiven Tab.
const [ribbonTab, setRibbonTab] = useState<RibbonTabId>("2d");
const [activeWallTypeId, setActiveWallTypeId] = useState<string>( const [activeWallTypeId, setActiveWallTypeId] = useState<string>(
project.wallTypes[0].id, project.wallTypes[0].id,
); );
@@ -3196,6 +3201,8 @@ export default function App() {
in den „Ansichten"-Ribbon-Tab gewandert (ViewRibbonTab, unten). */} in den „Ansichten"-Ribbon-Tab gewandert (ViewRibbonTab, unten). */}
<TopBar <TopBar
project={project} project={project}
ribbonTab={ribbonTab}
onRibbonTab={setRibbonTab}
zoomEnabled={planActive} zoomEnabled={planActive}
onExportPdf={() => setExportOpen(true)} onExportPdf={() => setExportOpen(true)}
onExportDxf={() => setExportDxfOpen(true)} onExportDxf={() => setExportDxfOpen(true)}
@@ -3219,6 +3226,7 @@ export default function App() {
die aus der TopBar gemergte Ansichts-/Zoom-/Darstellungs-Steuerung die aus der TopBar gemergte Ansichts-/Zoom-/Darstellungs-Steuerung
(ViewRibbonTab); die Werkzeug-Sidebar bleibt vorerst erhalten. */} (ViewRibbonTab); die Werkzeug-Sidebar bleibt vorerst erhalten. */}
<RibbonBar <RibbonBar
tab={ribbonTab}
activeTool={activeTool} activeTool={activeTool}
activeCommand={engineView.commandName} activeCommand={engineView.commandName}
toolsEnabled={activeLevel.kind === "floor"} toolsEnabled={activeLevel.kind === "floor"}
+11 -7
View File
@@ -5341,9 +5341,10 @@ body {
color: #fff; color: #fff;
} }
/* Ribbon-Oberleiste (2D · 3D · BIM · Ansichten) /* Ribbon (2D · 3D · BIM · Ansichten)
Datengetriebene Tab-Leiste unter der TopBar (RibbonBar). Kompakte Band-Höhe: Die Tab-REITER (`.ribbon-tabs`) sitzen in der TopBar-Zeile; hier folgt nur der
Tab-Reiter oben, darunter die Werkzeug-/Befehls-Gruppen des aktiven Tabs. */ INHALT (`.ribbon`/`.ribbon-body`) des aktiven Tabs darunter. So gibt es genau
zwei Bänder: TopBar-Zeile (Chrome + Tabs) oben, Ribbon-Inhalt darunter. */
.ribbon { .ribbon {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -5353,12 +5354,13 @@ body {
z-index: 1; z-index: 1;
flex: 0 0 auto; flex: 0 0 auto;
} }
/* Tab-Reiter leben in der TopBar-Zeile: volle Zeilenhöhe, Aktiv-Unterstrich
sitzt an der Unterkante (verbindet optisch mit dem Inhalt darunter). */
.ribbon-tabs { .ribbon-tabs {
display: flex; display: flex;
align-items: flex-end; align-self: stretch;
align-items: stretch;
gap: 2px; gap: 2px;
padding: 0 10px;
border-bottom: 1px solid var(--line);
} }
.ribbon-tab { .ribbon-tab {
appearance: none; appearance: none;
@@ -5369,7 +5371,9 @@ body {
font-weight: 600; font-weight: 600;
letter-spacing: 0.04em; letter-spacing: 0.04em;
text-transform: uppercase; text-transform: uppercase;
padding: 7px 12px 6px; padding: 0 12px;
display: flex;
align-items: center;
cursor: pointer; cursor: pointer;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
transition: color 0.12s, border-color 0.12s; transition: color 0.12s, border-color 0.12s;
+17 -1
View File
@@ -15,6 +15,8 @@ import { useEffect, useRef, useState } from "react";
import { Dropdown } from "./Dropdown"; import { Dropdown } from "./Dropdown";
import type { DropdownItem, DropdownOption } from "./Dropdown"; import type { DropdownItem, DropdownOption } from "./Dropdown";
import { WindowControls } from "./WindowControls"; import { WindowControls } from "./WindowControls";
import { RibbonTabs } from "./ribbon/RibbonBar";
import type { RibbonTabId } from "./ribbon/ribbonItems";
import { import {
applyMark, applyMark,
applyPreset, applyPreset,
@@ -84,9 +86,14 @@ const ZOOM_CLUSTER_WIDTH = 140;
export interface TopBarProps { export interface TopBarProps {
project: Project; project: Project;
// Ribbon-Tab-Reiter (2D/3D/BIM/Ansichten) sitzen in DIESER Zeile — der Ribbon-
// Inhalt darunter (RibbonBar). Der aktive Tab liegt in App (geteilter State).
ribbonTab: RibbonTabId;
onRibbonTab: (id: RibbonTabId) => void;
// Ansichts-/Zoom-/Darstellungs-Steuerung ist in den „Ansichten"-Ribbon-Tab // Ansichts-/Zoom-/Darstellungs-Steuerung ist in den „Ansichten"-Ribbon-Tab
// gewandert (siehe ViewRibbonTab). Die TopBar ist nur noch die schmale globale // gewandert (siehe ViewRibbonTab). Die TopBar ist nur noch die schmale globale
// Leiste: Marke/Ressourcen · Text-Formatierung · Datei/Export/Einstellungen. // Leiste: Marke/Ressourcen · Tabs · Text-Formatierung · Datei/Export/Einstell.
/** Ob Zoom/Plan aktiv ist — steuert nur noch, ob Export im Datei-Menü geht. */ /** Ob Zoom/Plan aktiv ist — steuert nur noch, ob Export im Datei-Menü geht. */
zoomEnabled: boolean; zoomEnabled: boolean;
@@ -1198,6 +1205,8 @@ export function ViewRibbonTab({
export function TopBar({ export function TopBar({
project, project,
ribbonTab,
onRibbonTab,
zoomEnabled, zoomEnabled,
onExportPdf, onExportPdf,
onExportDxf, onExportDxf,
@@ -1259,6 +1268,13 @@ export function TopBar({
<Sep /> <Sep />
{/* Ribbon-Tab-Reiter (2D/3D/BIM/Ansichten) sitzen in dieser Zeile, der
Ribbon-Inhalt folgt darunter (RibbonBar). So gibt es nur ZWEI Bänder:
diese Leiste (Chrome + Tabs) und den Inhalt. */}
<RibbonTabs tab={ribbonTab} onTab={onRibbonTab} />
<Sep />
{/* Text-Gruppe (kontextabhängig) Stil/Schrift/Grösse + B/I/U · L/C/R + {/* Text-Gruppe (kontextabhängig) Stil/Schrift/Grösse + B/I/U · L/C/R +
Zeilenhöhe. Formatiert das aktuelle Text-Ziel (Raumstempel) live. Bleibt Zeilenhöhe. Formatiert das aktuelle Text-Ziel (Raumstempel) live. Bleibt
in der schmalen globalen Leiste, weil sie eine kontextuelle Formatier- in der schmalen globalen Leiste, weil sie eine kontextuelle Formatier-
+67 -52
View File
@@ -1,17 +1,16 @@
// Ribbon-Oberleiste (docs/design/ribbon-ui-plan.md, Phase 1) — datengetriebene // Ribbon-Inhaltsleiste (docs/design/ribbon-ui-plan.md) — rendert den INHALT des
// Tab-Leiste unter der TopBar. Zeigt je Tab (2D · 3D · BIM · Ansichten) die // aktiven Ribbon-Tabs. Die Tab-REITER selbst sitzen in der TopBar-Zeile (eine
// gruppierten Werkzeuge/Befehle aus `RIBBON_TABS`. ADDITIV: die Werkzeug-Sidebar // Leiste: globales Chrome + Tabs), damit es nur ZWEI Bänder gibt: TopBar-Zeile
// (ToolsPanel) bleibt vorerst funktionsfähig — beide teilen sich denselben // (mit Tabs) oben, Ribbon-Inhalt darunter. Der aktive Tab wird als `tab`-Prop
// Aktivierungs-Pfad (onSelectTool / engine.start), es gibt keine Duplikat-Logik. // hereingereicht (State liegt in App, geteilt mit der TopBar).
// //
// Werkzeug-Elemente laufen über `onSelectTool` (gekoppelte Befehle inklusive, // Werkzeug-Elemente laufen über `onSelectTool` (gekoppelte Befehle inklusive, wie
// wie in der Sidebar); Befehls-Elemente über `onRunCommand` (engine.start). Das // in der Sidebar); Befehls-Elemente über `onRunCommand` (engine.start). Der
// Aktiv-Highlight spiegelt für Werkzeuge `activeTool`, für Befehle den laufenden // „Ansichten"-Tab trägt reichen Custom-Inhalt (`viewsContent`, aus der TopBar
// Befehl (`activeCommand`). // gemergt) statt datengetriebener Item-Gruppen.
// //
// Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md). // Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md).
import { useState } from "react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { t } from "../../i18n"; import { t } from "../../i18n";
import { ToolIcon } from "../../panels/ToolsPanel"; import { ToolIcon } from "../../panels/ToolsPanel";
@@ -23,6 +22,8 @@ import type { RibbonItem, RibbonTabId } from "./ribbonItems";
import { CommandIcon } from "./CommandIcon"; import { CommandIcon } from "./CommandIcon";
export interface RibbonBarProps { export interface RibbonBarProps {
/** Aktiver Tab (State liegt in App, geteilt mit den Reitern in der TopBar). */
tab: RibbonTabId;
/** Aktives Werkzeug (Highlight der `tool`-Elemente). */ /** Aktives Werkzeug (Highlight der `tool`-Elemente). */
activeTool: ToolId; activeTool: ToolId;
/** Name des laufenden Engine-Befehls (Highlight der `command`-Elemente). */ /** Name des laufenden Engine-Befehls (Highlight der `command`-Elemente). */
@@ -36,29 +37,28 @@ export interface RibbonBarProps {
/** /**
* Reicher Inhalt des Ansichten"-Tabs (aus der TopBar gemergte Ansichts-/Zoom-/ * Reicher Inhalt des Ansichten"-Tabs (aus der TopBar gemergte Ansichts-/Zoom-/
* Darstellungs-Steuerung, `ViewRibbonTab`). Von App geliefert, damit dessen * Darstellungs-Steuerung, `ViewRibbonTab`). Von App geliefert, damit dessen
* Setter/State dort bleiben analog dazu, wie die TopBar das Layout-Menü als * Setter/State dort bleiben. Fehlt er, zeigt der Tab den Leer-Hinweis.
* Node bekommt. Fehlt er, zeigt der Tab den Leer-Hinweis.
*/ */
viewsContent?: ReactNode; viewsContent?: ReactNode;
} }
/** Aktivierungs-/Highlight-Kontext, den jeder Button-Typ braucht. */
interface ButtonCtx {
activeTool: ToolId;
activeCommand: string | null;
toolsEnabled: boolean;
onSelectTool: (id: ToolId) => void;
onRunCommand: (name: string) => void;
}
/** Ein einzelner Ribbon-Button (Werkzeug ODER Befehl). */ /** Ein einzelner Ribbon-Button (Werkzeug ODER Befehl). */
function RibbonButton({ function RibbonButton({ item, ctx }: { item: RibbonItem; ctx: ButtonCtx }) {
item,
activeTool,
activeCommand,
toolsEnabled,
onSelectTool,
onRunCommand,
}: {
item: RibbonItem;
} & Omit<RibbonBarProps, "activeCommand"> & { activeCommand: string | null }) {
if (item.kind === "tool") { if (item.kind === "tool") {
const tool = getTool(item.id); const tool = getTool(item.id);
// Nur Boden-gebundene Werkzeuge sind ohne aktives Geschoss gesperrt; freie // Nur Boden-gebundene Werkzeuge sind ohne aktives Geschoss gesperrt; freie
// 2D-Werkzeuge (Linie/Kreis/…) bleiben auch auf Zeichnungsebenen nutzbar. // 2D-Werkzeuge (Linie/Kreis/…) bleiben auch auf Zeichnungsebenen nutzbar.
const disabled = !!tool.floorOnly && !toolsEnabled; const disabled = !!tool.floorOnly && !ctx.toolsEnabled;
const active = activeTool === item.id; const active = ctx.activeTool === item.id;
const label = t(tool.labelKey); const label = t(tool.labelKey);
return ( return (
<button <button
@@ -66,7 +66,7 @@ function RibbonButton({
className={`ribbon-btn${active ? " active" : ""}`} className={`ribbon-btn${active ? " active" : ""}`}
disabled={disabled} disabled={disabled}
title={disabled ? t("tool.floorOnlyDisabled") : label} title={disabled ? t("tool.floorOnlyDisabled") : label}
onClick={() => onSelectTool(item.id)} onClick={() => ctx.onSelectTool(item.id)}
> >
<ToolIcon id={item.id} /> <ToolIcon id={item.id} />
<span>{label}</span> <span>{label}</span>
@@ -76,13 +76,13 @@ function RibbonButton({
// Befehls-Element: Label aus der Befehls-Registry ableiten. // Befehls-Element: Label aus der Befehls-Registry ableiten.
const cmd = getCommand(item.name); const cmd = getCommand(item.name);
const label = cmd ? t(cmd.labelKey) : item.name; const label = cmd ? t(cmd.labelKey) : item.name;
const active = activeCommand === item.name; const active = ctx.activeCommand === item.name;
return ( return (
<button <button
type="button" type="button"
className={`ribbon-btn${active ? " active" : ""}`} className={`ribbon-btn${active ? " active" : ""}`}
title={label} title={label}
onClick={() => onRunCommand(item.name)} onClick={() => ctx.onRunCommand(item.name)}
> >
<CommandIcon name={item.name} /> <CommandIcon name={item.name} />
<span>{label}</span> <span>{label}</span>
@@ -90,30 +90,49 @@ function RibbonButton({
); );
} }
/**
* Die Tab-REITER als eigene Komponente exportiert, weil sie in der TopBar-Zeile
* gerendert werden (nicht hier). Rein gesteuert über `tab`/`onTab`.
*/
export function RibbonTabs({
tab,
onTab,
}: {
tab: RibbonTabId;
onTab: (id: RibbonTabId) => void;
}) {
return (
<div className="ribbon-tabs" role="tablist">
{RIBBON_TABS.map((tt) => (
<button
key={tt.id}
type="button"
role="tab"
aria-selected={tt.id === tab}
className={`ribbon-tab${tt.id === tab ? " active" : ""}`}
onClick={() => onTab(tt.id)}
>
{t(tt.labelKey)}
</button>
))}
</div>
);
}
export function RibbonBar(props: RibbonBarProps) { export function RibbonBar(props: RibbonBarProps) {
const [tab, setTab] = useState<RibbonTabId>("2d"); const current = RIBBON_TABS.find((tt) => tt.id === props.tab) ?? RIBBON_TABS[0];
const current = RIBBON_TABS.find((tt) => tt.id === tab) ?? RIBBON_TABS[0]; const ctx: ButtonCtx = {
activeTool: props.activeTool,
activeCommand: props.activeCommand,
toolsEnabled: props.toolsEnabled,
onSelectTool: props.onSelectTool,
onRunCommand: props.onRunCommand,
};
return ( return (
<div className="ribbon"> <div className="ribbon">
{/* Tab-Reiter */} {/* Inhalt des aktiven Tabs. Der Ansichten"-Tab trägt reichen Custom-Inhalt
<div className="ribbon-tabs" role="tablist"> (ViewRibbonTab) statt der datengetriebenen Item-Gruppen. */}
{RIBBON_TABS.map((tt) => (
<button
key={tt.id}
type="button"
role="tab"
aria-selected={tt.id === tab}
className={`ribbon-tab${tt.id === tab ? " active" : ""}`}
onClick={() => setTab(tt.id)}
>
{t(tt.labelKey)}
</button>
))}
</div>
{/* Gruppen des aktiven Tabs. Der Ansichten"-Tab trägt reichen Custom-
Inhalt (ViewRibbonTab) statt der datengetriebenen Item-Gruppen. */}
<div className="ribbon-body"> <div className="ribbon-body">
{current.id === "views" && props.viewsContent ? ( {current.id === "views" && props.viewsContent ? (
props.viewsContent props.viewsContent
@@ -127,11 +146,7 @@ export function RibbonBar(props: RibbonBarProps) {
<RibbonButton <RibbonButton
key={item.kind === "tool" ? `t:${item.id}` : `c:${item.name}`} key={item.kind === "tool" ? `t:${item.id}` : `c:${item.name}`}
item={item} item={item}
activeTool={props.activeTool} ctx={ctx}
activeCommand={props.activeCommand}
toolsEnabled={props.toolsEnabled}
onSelectTool={props.onSelectTool}
onRunCommand={props.onRunCommand}
/> />
))} ))}
</div> </div>