Ribbon-UI Phase 1: datengetriebene Tab-Leiste (2D/3D/BIM/Ansichten), additiv unter TopBar
This commit is contained in:
+22
@@ -70,6 +70,7 @@ import { ResourceManager } from "./ui/ResourceManager";
|
|||||||
import type { ResourceManagerHandlers } from "./ui/ResourceManager";
|
import type { ResourceManagerHandlers } from "./ui/ResourceManager";
|
||||||
import { HatchSwatch } from "./ui/hatchPreview";
|
import { HatchSwatch } from "./ui/hatchPreview";
|
||||||
import { TopBar } from "./ui/TopBar";
|
import { TopBar } from "./ui/TopBar";
|
||||||
|
import { RibbonBar } from "./ui/ribbon/RibbonBar";
|
||||||
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";
|
||||||
@@ -810,6 +811,16 @@ export default function App() {
|
|||||||
setActiveTool(id);
|
setActiveTool(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Ribbon-Befehl (Ändern-Gruppe u. a.): startet einen Engine-Befehl OHNE
|
||||||
|
// eigenes Werkzeug (kein ToolId). Läuft über DIESELBE Engine wie das Tippen des
|
||||||
|
// Befehls; das Werkzeug-Highlight fällt auf „Auswahl" zurück, das Aktiv-
|
||||||
|
// Highlight des Ribbon-Befehls spiegelt der laufende Befehl (engineView).
|
||||||
|
const onRunCommand = (name: string) => {
|
||||||
|
engine.start(name);
|
||||||
|
setActiveTool("select");
|
||||||
|
requestAnimationFrame(() => commandLineRef.current?.focus());
|
||||||
|
};
|
||||||
|
|
||||||
// 3D-Viewport als Erstellungs-Fläche: der Viewport projiziert den Cursor auf
|
// 3D-Viewport als Erstellungs-Fläche: der Viewport projiziert den Cursor auf
|
||||||
// die Geschoss-Arbeitsebene und liefert einen rohen Modell-`Vec2`; wir füttern
|
// die Geschoss-Arbeitsebene und liefert einen rohen Modell-`Vec2`; wir füttern
|
||||||
// ihn in DIESELBE ansichts-agnostische CommandEngine wie PlanView (move/pick).
|
// ihn in DIESELBE ansichts-agnostische CommandEngine wie PlanView (move/pick).
|
||||||
@@ -3244,6 +3255,17 @@ export default function App() {
|
|||||||
textTarget={textTarget}
|
textTarget={textTarget}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Ribbon-Oberleiste (2D · 3D · BIM · Ansichten) — additiv unter der TopBar.
|
||||||
|
Teilt sich denselben Aktivierungs-Pfad wie die Werkzeug-Sidebar; die
|
||||||
|
Sidebar bleibt vorerst erhalten (Plan-Doc Phase 3 entfernt sie). */}
|
||||||
|
<RibbonBar
|
||||||
|
activeTool={activeTool}
|
||||||
|
activeCommand={engineView.commandName}
|
||||||
|
toolsEnabled={activeLevel.kind === "floor"}
|
||||||
|
onSelectTool={onSelectTool}
|
||||||
|
onRunCommand={onRunCommand}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="body">
|
<div className="body">
|
||||||
{/* Linkes Dock: gestapelte Gruppen (Werkzeuge oben, Zeichnungsebenen/
|
{/* Linkes Dock: gestapelte Gruppen (Werkzeuge oben, Zeichnungsebenen/
|
||||||
Ebenen unten). Jede Gruppe baut über buildHost ihren eigenen
|
Ebenen unten). Jede Gruppe baut über buildHost ihren eigenen
|
||||||
|
|||||||
@@ -120,6 +120,15 @@ export const de = {
|
|||||||
"tool.circle.hint": "Kreis: Mittelpunkt setzen, dann Radius",
|
"tool.circle.hint": "Kreis: Mittelpunkt setzen, dann Radius",
|
||||||
"tool.arc": "Bogen",
|
"tool.arc": "Bogen",
|
||||||
"tool.arc.hint": "Bogen: Mittelpunkt, Startpunkt, dann Endpunkt (CCW)",
|
"tool.arc.hint": "Bogen: Mittelpunkt, Startpunkt, dann Endpunkt (CCW)",
|
||||||
|
// Ribbon-Oberleiste (Tabs + Gruppen)
|
||||||
|
"ribbon.tab.2d": "2D",
|
||||||
|
"ribbon.tab.3d": "3D",
|
||||||
|
"ribbon.tab.bim": "BIM",
|
||||||
|
"ribbon.tab.views": "Ansichten",
|
||||||
|
"ribbon.group.draw": "Zeichnen",
|
||||||
|
"ribbon.group.modify": "Ändern",
|
||||||
|
"ribbon.group.components": "Bauteile",
|
||||||
|
"ribbon.empty": "Noch keine Werkzeuge in diesem Tab",
|
||||||
"tool.select.hint": "Auswahl — Elemente klicken/aufziehen",
|
"tool.select.hint": "Auswahl — Elemente klicken/aufziehen",
|
||||||
"tool.window.hint": "Fenster: Wand wählen, dann Position setzen",
|
"tool.window.hint": "Fenster: Wand wählen, dann Position setzen",
|
||||||
"tool.door.hint": "Tür: Wand wählen, dann Position setzen",
|
"tool.door.hint": "Tür: Wand wählen, dann Position setzen",
|
||||||
|
|||||||
@@ -119,6 +119,15 @@ export const en: Record<TranslationKey, string> = {
|
|||||||
"tool.circle.hint": "Circle: set center, then radius",
|
"tool.circle.hint": "Circle: set center, then radius",
|
||||||
"tool.arc": "Arc",
|
"tool.arc": "Arc",
|
||||||
"tool.arc.hint": "Arc: center, start point, then end point (CCW)",
|
"tool.arc.hint": "Arc: center, start point, then end point (CCW)",
|
||||||
|
// Ribbon top bar (tabs + groups)
|
||||||
|
"ribbon.tab.2d": "2D",
|
||||||
|
"ribbon.tab.3d": "3D",
|
||||||
|
"ribbon.tab.bim": "BIM",
|
||||||
|
"ribbon.tab.views": "Views",
|
||||||
|
"ribbon.group.draw": "Draw",
|
||||||
|
"ribbon.group.modify": "Modify",
|
||||||
|
"ribbon.group.components": "Components",
|
||||||
|
"ribbon.empty": "No tools in this tab yet",
|
||||||
"tool.select.hint": "Select — click/drag elements",
|
"tool.select.hint": "Select — click/drag elements",
|
||||||
"tool.window.hint": "Window: pick a wall, then set the position",
|
"tool.window.hint": "Window: pick a wall, then set the position",
|
||||||
"tool.door.hint": "Door: pick a wall, then set the position",
|
"tool.door.hint": "Door: pick a wall, then set the position",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { Dropdown } from "../ui/Dropdown";
|
|||||||
// Keine echten Asset-Icons vorhanden: schlichte Inline-SVG-Glyphen je Werkzeug
|
// Keine echten Asset-Icons vorhanden: schlichte Inline-SVG-Glyphen je Werkzeug
|
||||||
// (~16×16, stroke=currentColor, fill=none), damit das Icon die Textfarbe der
|
// (~16×16, stroke=currentColor, fill=none), damit das Icon die Textfarbe der
|
||||||
// Zeile übernimmt.
|
// Zeile übernimmt.
|
||||||
function ToolIcon({ id }: { id: ToolId }) {
|
export function ToolIcon({ id }: { id: ToolId }) {
|
||||||
const common = {
|
const common = {
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
|
|||||||
+132
@@ -5340,3 +5340,135 @@ body {
|
|||||||
background: #e5484d;
|
background: #e5484d;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Ribbon-Oberleiste (2D · 3D · BIM · Ansichten) ─────────────────────────
|
||||||
|
Datengetriebene Tab-Leiste unter der TopBar (RibbonBar). Kompakte Band-Höhe:
|
||||||
|
Tab-Reiter oben, darunter die Werkzeug-/Befehls-Gruppen des aktiven Tabs. */
|
||||||
|
.ribbon {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--panel);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.ribbon-tabs {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.ribbon-tab {
|
||||||
|
appearance: none;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 7px 12px 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
transition: color 0.12s, border-color 0.12s;
|
||||||
|
}
|
||||||
|
.ribbon-tab:hover {
|
||||||
|
color: var(--label);
|
||||||
|
}
|
||||||
|
.ribbon-tab.active {
|
||||||
|
color: var(--ink);
|
||||||
|
border-bottom-color: var(--accent-border, var(--accent));
|
||||||
|
}
|
||||||
|
.ribbon-body {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: transparent transparent;
|
||||||
|
min-height: 56px;
|
||||||
|
}
|
||||||
|
.ribbon-body::-webkit-scrollbar {
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
.ribbon-body::-webkit-scrollbar-thumb {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.ribbon-body:hover::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
/* Eine Gruppe: Buttons oben, kleiner Gruppentitel darunter; rechts ein dezenter
|
||||||
|
Trenner (wie OCS-Ribbon-Gruppen). */
|
||||||
|
.ribbon-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-right: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.ribbon-group:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
.ribbon-items {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 2px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.ribbon-group-title {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 9px;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
/* Ein Ribbon-Button: Icon oben, Label darunter (kompakt, wie eine Palette). */
|
||||||
|
.ribbon-btn {
|
||||||
|
appearance: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--label);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
width: 52px;
|
||||||
|
padding: 4px 2px 3px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.15;
|
||||||
|
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
||||||
|
}
|
||||||
|
.ribbon-btn span {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.ribbon-btn:hover:not(:disabled) {
|
||||||
|
background: var(--panel-2);
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
.ribbon-btn.active {
|
||||||
|
background: var(--accent-dim, var(--panel-2));
|
||||||
|
border-color: var(--accent-border, var(--accent));
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.ribbon-btn:disabled {
|
||||||
|
opacity: 0.35;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.ribbon-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// Kleine Inline-SVG-Glyphen für Befehls-Ribbon-Elemente (Ändern-Gruppe u. a.),
|
||||||
|
// die keine ToolId haben und daher nicht über `ToolIcon` abgedeckt sind. Gleicher
|
||||||
|
// Stil wie `ToolIcon` (~16×16, stroke=currentColor, fill=none), damit das Icon
|
||||||
|
// die Textfarbe der Zeile übernimmt. Unbekannte Namen → kein Icon (null).
|
||||||
|
|
||||||
|
const common = {
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
viewBox: "0 0 16 16",
|
||||||
|
fill: "none",
|
||||||
|
stroke: "currentColor",
|
||||||
|
strokeWidth: 1.4,
|
||||||
|
strokeLinecap: "round" as const,
|
||||||
|
strokeLinejoin: "round" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CommandIcon({ name }: { name: string }) {
|
||||||
|
switch (name) {
|
||||||
|
case "move":
|
||||||
|
// Vier-Wege-Verschiebepfeil.
|
||||||
|
return (
|
||||||
|
<svg {...common}>
|
||||||
|
<line x1="8" y1="2.5" x2="8" y2="13.5" />
|
||||||
|
<line x1="2.5" y1="8" x2="13.5" y2="8" />
|
||||||
|
<polyline points="6,4.5 8,2.5 10,4.5" />
|
||||||
|
<polyline points="6,11.5 8,13.5 10,11.5" />
|
||||||
|
<polyline points="4.5,6 2.5,8 4.5,10" />
|
||||||
|
<polyline points="11.5,6 13.5,8 11.5,10" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
case "copy":
|
||||||
|
// Zwei überlappende Rechtecke (Duplikat).
|
||||||
|
return (
|
||||||
|
<svg {...common}>
|
||||||
|
<rect x="2.5" y="2.5" width="8" height="8" />
|
||||||
|
<rect x="5.5" y="5.5" width="8" height="8" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
case "mirror":
|
||||||
|
// Spiegelachse (gestrichelt) mit zwei gespiegelten Dreiecken.
|
||||||
|
return (
|
||||||
|
<svg {...common}>
|
||||||
|
<line x1="8" y1="2" x2="8" y2="14" strokeDasharray="1.6 1.6" />
|
||||||
|
<path d="M6.5 4 L2.5 8 L6.5 12 Z" />
|
||||||
|
<path d="M9.5 4 L13.5 8 L9.5 12 Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
case "offset":
|
||||||
|
// Zwei parallele Winkel (Ursprung + Versatz).
|
||||||
|
return (
|
||||||
|
<svg {...common}>
|
||||||
|
<polyline points="3,12 3,4 11,4" />
|
||||||
|
<polyline points="6,12 6,7 14,7" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
case "trim":
|
||||||
|
// Schere (Stutzen).
|
||||||
|
return (
|
||||||
|
<svg {...common}>
|
||||||
|
<line x1="13.5" y1="3" x2="6.5" y2="9" />
|
||||||
|
<circle cx="4" cy="11" r="2" />
|
||||||
|
<line x1="13.5" y1="13" x2="6.5" y2="7" />
|
||||||
|
<circle cx="4" cy="5" r="2" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
case "join":
|
||||||
|
// Zwei Segmente, die sich an einer Ecke treffen (Verbinden).
|
||||||
|
return (
|
||||||
|
<svg {...common}>
|
||||||
|
<polyline points="2.5,13 8,13 8,3" />
|
||||||
|
<circle cx="8" cy="13" r="1.4" fill="currentColor" stroke="none" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
// Ribbon-Oberleiste (docs/design/ribbon-ui-plan.md, Phase 1) — datengetriebene
|
||||||
|
// Tab-Leiste unter der TopBar. Zeigt je Tab (2D · 3D · BIM · Ansichten) die
|
||||||
|
// gruppierten Werkzeuge/Befehle aus `RIBBON_TABS`. ADDITIV: die Werkzeug-Sidebar
|
||||||
|
// (ToolsPanel) bleibt vorerst funktionsfähig — beide teilen sich denselben
|
||||||
|
// Aktivierungs-Pfad (onSelectTool / engine.start), es gibt keine Duplikat-Logik.
|
||||||
|
//
|
||||||
|
// Werkzeug-Elemente laufen über `onSelectTool` (gekoppelte Befehle inklusive,
|
||||||
|
// wie in der Sidebar); Befehls-Elemente über `onRunCommand` (engine.start). Das
|
||||||
|
// Aktiv-Highlight spiegelt für Werkzeuge `activeTool`, für Befehle den laufenden
|
||||||
|
// Befehl (`activeCommand`).
|
||||||
|
//
|
||||||
|
// Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md).
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { t } from "../../i18n";
|
||||||
|
import { ToolIcon } from "../../panels/ToolsPanel";
|
||||||
|
import { getTool } from "../../tools/tools";
|
||||||
|
import type { ToolId } from "../../tools/types";
|
||||||
|
import { getCommand } from "../../commands/registry";
|
||||||
|
import { RIBBON_TABS } from "./ribbonItems";
|
||||||
|
import type { RibbonItem, RibbonTabId } from "./ribbonItems";
|
||||||
|
import { CommandIcon } from "./CommandIcon";
|
||||||
|
|
||||||
|
export interface RibbonBarProps {
|
||||||
|
/** Aktives Werkzeug (Highlight der `tool`-Elemente). */
|
||||||
|
activeTool: ToolId;
|
||||||
|
/** Name des laufenden Engine-Befehls (Highlight der `command`-Elemente). */
|
||||||
|
activeCommand: string | null;
|
||||||
|
/** Ob Boden-gebundene (`floorOnly`) Werkzeuge nutzbar sind (aktives Geschoss). */
|
||||||
|
toolsEnabled: boolean;
|
||||||
|
/** Werkzeug aktivieren (gleicher Pfad wie die Sidebar). */
|
||||||
|
onSelectTool: (id: ToolId) => void;
|
||||||
|
/** Engine-Befehl starten (Ändern-Gruppe u. a.). */
|
||||||
|
onRunCommand: (name: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ein einzelner Ribbon-Button (Werkzeug ODER Befehl). */
|
||||||
|
function RibbonButton({
|
||||||
|
item,
|
||||||
|
activeTool,
|
||||||
|
activeCommand,
|
||||||
|
toolsEnabled,
|
||||||
|
onSelectTool,
|
||||||
|
onRunCommand,
|
||||||
|
}: {
|
||||||
|
item: RibbonItem;
|
||||||
|
} & Omit<RibbonBarProps, "activeCommand"> & { activeCommand: string | null }) {
|
||||||
|
if (item.kind === "tool") {
|
||||||
|
const tool = getTool(item.id);
|
||||||
|
// Nur Boden-gebundene Werkzeuge sind ohne aktives Geschoss gesperrt; freie
|
||||||
|
// 2D-Werkzeuge (Linie/Kreis/…) bleiben auch auf Zeichnungsebenen nutzbar.
|
||||||
|
const disabled = !!tool.floorOnly && !toolsEnabled;
|
||||||
|
const active = activeTool === item.id;
|
||||||
|
const label = t(tool.labelKey);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`ribbon-btn${active ? " active" : ""}`}
|
||||||
|
disabled={disabled}
|
||||||
|
title={disabled ? t("tool.floorOnlyDisabled") : label}
|
||||||
|
onClick={() => onSelectTool(item.id)}
|
||||||
|
>
|
||||||
|
<ToolIcon id={item.id} />
|
||||||
|
<span>{label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Befehls-Element: Label aus der Befehls-Registry ableiten.
|
||||||
|
const cmd = getCommand(item.name);
|
||||||
|
const label = cmd ? t(cmd.labelKey) : item.name;
|
||||||
|
const active = activeCommand === item.name;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`ribbon-btn${active ? " active" : ""}`}
|
||||||
|
title={label}
|
||||||
|
onClick={() => onRunCommand(item.name)}
|
||||||
|
>
|
||||||
|
<CommandIcon name={item.name} />
|
||||||
|
<span>{label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RibbonBar(props: RibbonBarProps) {
|
||||||
|
const [tab, setTab] = useState<RibbonTabId>("2d");
|
||||||
|
const current = RIBBON_TABS.find((tt) => tt.id === tab) ?? RIBBON_TABS[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ribbon">
|
||||||
|
{/* Tab-Reiter */}
|
||||||
|
<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={() => setTab(tt.id)}
|
||||||
|
>
|
||||||
|
{t(tt.labelKey)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gruppen des aktiven Tabs */}
|
||||||
|
<div className="ribbon-body">
|
||||||
|
{current.groups.length === 0 ? (
|
||||||
|
<div className="ribbon-empty">{t("ribbon.empty")}</div>
|
||||||
|
) : (
|
||||||
|
current.groups.map((g) => (
|
||||||
|
<div key={g.titleKey} className="ribbon-group">
|
||||||
|
<div className="ribbon-items">
|
||||||
|
{g.items.map((item) => (
|
||||||
|
<RibbonButton
|
||||||
|
key={item.kind === "tool" ? `t:${item.id}` : `c:${item.name}`}
|
||||||
|
item={item}
|
||||||
|
activeTool={props.activeTool}
|
||||||
|
activeCommand={props.activeCommand}
|
||||||
|
toolsEnabled={props.toolsEnabled}
|
||||||
|
onSelectTool={props.onSelectTool}
|
||||||
|
onRunCommand={props.onRunCommand}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="ribbon-group-title">{t(g.titleKey)}</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// Ribbon-Registry (docs/design/ribbon-ui-plan.md §Architektur) — EINE
|
||||||
|
// datengetriebene Beschreibung aller Ribbon-Elemente. Die Ribbon-Tabs UND eine
|
||||||
|
// spätere modulare Custom-Bar (Phase 4) sind bloß zwei Ansichten über dieselbe
|
||||||
|
// Registry, ohne Sonderweg pro Element.
|
||||||
|
//
|
||||||
|
// Ein `RibbonItem` ist entweder ein Werkzeug (`tool` → onSelectTool, wie die
|
||||||
|
// Werkzeug-Sidebar) oder ein Engine-Befehl (`command` → engine.start). Beide
|
||||||
|
// münden in exakt DENSELBEN Pfad wie Klick in der alten Sidebar bzw. das Tippen
|
||||||
|
// des Befehlsnamens — keine Duplikate.
|
||||||
|
//
|
||||||
|
// Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md). Sichtbare
|
||||||
|
// Texte kommen als i18n-Schlüssel und werden erst in der Komponente über t(...)
|
||||||
|
// aufgelöst.
|
||||||
|
|
||||||
|
import type { ToolId } from "../../tools/types";
|
||||||
|
|
||||||
|
/** Ein aktivierbares Ribbon-Element. Bei `command` wird die Beschriftung aus der
|
||||||
|
* Befehls-Registry (`getCommand(name).labelKey`) abgeleitet — kein doppeltes
|
||||||
|
* Pflegen von Labels. */
|
||||||
|
export type RibbonItem =
|
||||||
|
| { kind: "tool"; id: ToolId }
|
||||||
|
| { kind: "command"; name: string };
|
||||||
|
|
||||||
|
/** Eine benannte Gruppe von Elementen innerhalb eines Tabs. */
|
||||||
|
export interface RibbonGroup {
|
||||||
|
titleKey: string;
|
||||||
|
items: RibbonItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kennung eines Ribbon-Tabs (fest, für Tab-State + i18n). */
|
||||||
|
export type RibbonTabId = "2d" | "3d" | "bim" | "views";
|
||||||
|
|
||||||
|
/** Ein Ribbon-Tab: Beschriftung + gruppierte Elemente. */
|
||||||
|
export interface RibbonTab {
|
||||||
|
id: RibbonTabId;
|
||||||
|
labelKey: string;
|
||||||
|
groups: RibbonGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = (id: ToolId): RibbonItem => ({ kind: "tool", id });
|
||||||
|
const c = (name: string): RibbonItem => ({ kind: "command", name });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Ribbon-Tabs. Phase 1 füllt 2D (Zeichnen + Ändern) und BIM (Bauteile) mit
|
||||||
|
* den vorhandenen Werkzeugen/Befehlen; 3D und Ansichten bleiben vorerst leer
|
||||||
|
* (Platzhalter, in einer späteren Phase gefüllt — siehe Plan-Doc).
|
||||||
|
*/
|
||||||
|
export const RIBBON_TABS: RibbonTab[] = [
|
||||||
|
{
|
||||||
|
id: "2d",
|
||||||
|
labelKey: "ribbon.tab.2d",
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
titleKey: "ribbon.group.draw",
|
||||||
|
items: [
|
||||||
|
t("select"),
|
||||||
|
t("line"),
|
||||||
|
t("polyline"),
|
||||||
|
t("rect"),
|
||||||
|
t("circle"),
|
||||||
|
t("arc"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titleKey: "ribbon.group.modify",
|
||||||
|
items: [
|
||||||
|
c("move"),
|
||||||
|
c("copy"),
|
||||||
|
c("mirror"),
|
||||||
|
c("offset"),
|
||||||
|
c("trim"),
|
||||||
|
c("join"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bim",
|
||||||
|
labelKey: "ribbon.tab.bim",
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
titleKey: "ribbon.group.components",
|
||||||
|
items: [
|
||||||
|
t("wall"),
|
||||||
|
t("window"),
|
||||||
|
t("door"),
|
||||||
|
t("stair"),
|
||||||
|
t("ceiling"),
|
||||||
|
t("room"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3d",
|
||||||
|
labelKey: "ribbon.tab.3d",
|
||||||
|
groups: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "views",
|
||||||
|
labelKey: "ribbon.tab.views",
|
||||||
|
groups: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user