diff --git a/src/App.tsx b/src/App.tsx index 4be2e3f..c6eb0a4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -230,6 +230,31 @@ function promptCount(): number | null { return Number.isFinite(n) && n >= 1 ? n : null; } +/** localStorage-Schlüssel der modularen Custom-Bar (Ribbon-Tab „Eigene"). */ +const RIBBON_CUSTOM_KEY = "dossier.ribbon.customItems"; + +/** Persistierte Custom-Bar-Item-Schlüssel lesen (leer bei Fehler/Erststart). */ +function loadCustomItems(): string[] { + if (typeof window === "undefined") return []; + try { + const raw = window.localStorage.getItem(RIBBON_CUSTOM_KEY); + const arr = raw ? JSON.parse(raw) : null; + return Array.isArray(arr) ? arr.filter((x): x is string => typeof x === "string") : []; + } catch { + return []; + } +} + +/** Custom-Bar-Item-Schlüssel persistieren (Fehler still schlucken). */ +function saveCustomItems(items: string[]): void { + if (typeof window === "undefined") return; + try { + window.localStorage.setItem(RIBBON_CUSTOM_KEY, JSON.stringify(items)); + } catch { + // localStorage nicht verfügbar (privater Modus o. ä.) → ignorieren. + } +} + /** Umriss (bbox) aller Drawing2D-Geometrien in Modell-Metern (null = leer). */ function drawingsBBox( drawings: Drawing2D[], @@ -360,6 +385,18 @@ export default function App() { // weil die Tab-Reiter in der TopBar-Zeile sitzen und der Ribbon-Inhalt darunter // — beide teilen sich denselben aktiven Tab. const [ribbonTab, setRibbonTab] = useState("views"); + // Modulare Custom-Bar (Ribbon-Tab „Eigene"): persistierte Item-Schlüssel. Der + // Picker im Tab wählt Werkzeuge/Befehle an/ab (localStorage-persistiert). + const [customItems, setCustomItems] = useState(loadCustomItems); + const onToggleCustomItem = (key: string) => { + setCustomItems((prev) => { + const next = prev.includes(key) + ? prev.filter((k) => k !== key) + : [...prev, key]; + saveCustomItems(next); + return next; + }); + }; const [activeWallTypeId, setActiveWallTypeId] = useState( project.wallTypes[0].id, ); @@ -3231,6 +3268,8 @@ export default function App() { toolsEnabled={activeLevel.kind === "floor"} onSelectTool={onSelectTool} onRunCommand={onRunCommand} + customItems={customItems} + onToggleCustomItem={onToggleCustomItem} tabContent={{ views: ( = { "ribbon.tab.3d": "3D", "ribbon.tab.bim": "BIM", "ribbon.tab.views": "Views", + "ribbon.tab.custom": "Custom", "ribbon.group.draw": "Draw", "ribbon.group.modify": "Modify", "ribbon.group.components": "Components", + "ribbon.group.custom": "Custom bar", + "ribbon.custom.add": "+ Add", + "ribbon.custom.empty": "Pick elements via “+ Add”", "ribbon.empty": "No tools in this tab yet", "tool.select.hint": "Select — click/drag elements", "tool.window.hint": "Window: pick a wall, then set the position", diff --git a/src/styles.css b/src/styles.css index dd9af0b..fc16eea 100644 --- a/src/styles.css +++ b/src/styles.css @@ -5529,6 +5529,18 @@ body { font-style: italic; padding: 0 8px; } +/* Custom-Bar-Picker („+ Hinzufügen") — kompakter, unauffälliger Trigger, der + sich neben die Custom-Buttons stellt. */ +.ribbon-add.tb-dd-trigger { + align-self: center; + height: 24px; + font-size: 11px; + color: var(--muted); + border-style: dashed; +} +.ribbon-add.tb-dd-trigger:hover { + color: var(--label); +} /* „Ansichten"-Tab: die aus der TopBar gemergten Cluster (View-Grid, Kombis, Detail, Zoom, Darstellung) in EINER Zeile — nutzt die bestehenden `.tb-*`- Stile unverändert weiter. */ diff --git a/src/ui/ribbon/RibbonBar.tsx b/src/ui/ribbon/RibbonBar.tsx index 89bb242..94bd360 100644 --- a/src/ui/ribbon/RibbonBar.tsx +++ b/src/ui/ribbon/RibbonBar.tsx @@ -17,10 +17,19 @@ 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 { Dropdown } from "../Dropdown"; +import type { DropdownItem } from "../Dropdown"; +import { RIBBON_TABS, ALL_RIBBON_ITEMS, itemKey, itemFromKey } from "./ribbonItems"; import type { RibbonItem, RibbonTabId } from "./ribbonItems"; import { CommandIcon } from "./CommandIcon"; +/** Aufgelöste Beschriftung eines Items (Werkzeug bzw. Befehl). */ +function itemLabel(item: RibbonItem): string { + if (item.kind === "tool") return t(getTool(item.id).labelKey); + const cmd = getCommand(item.name); + return cmd ? t(cmd.labelKey) : item.name; +} + export interface RibbonBarProps { /** Aktiver Tab (State liegt in App, geteilt mit den Reitern in der TopBar). */ tab: RibbonTabId; @@ -41,6 +50,10 @@ export interface RibbonBarProps { * Gruppen zeigt den Leer-Hinweis. */ tabContent?: Partial>; + /** Persistierte Item-Schlüssel der modularen Custom-Bar (Tab „Eigene"). */ + customItems: string[]; + /** Ein Item in der Custom-Bar an-/abwählen (Picker im „Eigene"-Tab). */ + onToggleCustomItem: (key: string) => void; } /** Aktivierungs-/Highlight-Kontext, den jeder Button-Typ braucht. */ @@ -120,6 +133,53 @@ export function RibbonTabs({ ); } +/** + * Modulare Custom-Bar (Tab „Eigene") — rendert die vom Nutzer gewählten Items + * plus einen „+"-Picker, der ALLE verfügbaren Werkzeuge/Befehle mit Häkchen für + * die bereits enthaltenen listet (Klick = an-/abwählen). Gleiche Item-Typen + + * gleiche Aktivierung wie die normalen Tabs — kein Sonderweg. + */ +function CustomBar({ + customItems, + onToggle, + ctx, +}: { + customItems: string[]; + onToggle: (key: string) => void; + ctx: ButtonCtx; +}) { + const pickerItems: DropdownItem[] = ALL_RIBBON_ITEMS.map((it) => { + const key = itemKey(it); + return { + label: itemLabel(it), + active: customItems.includes(key), + onSelect: () => onToggle(key), + }; + }); + const items = customItems + .map(itemFromKey) + .filter((it): it is RibbonItem => it !== null); + return ( +
+
+ {items.length === 0 && ( +
{t("ribbon.custom.empty")}
+ )} + {items.map((item, i) => ( + + ))} + +
+
{t("ribbon.group.custom")}
+
+ ); +} + export function RibbonBar(props: RibbonBarProps) { const current = RIBBON_TABS.find((tt) => tt.id === props.tab) ?? RIBBON_TABS[0]; const custom = props.tabContent?.[current.id]; @@ -134,9 +194,16 @@ export function RibbonBar(props: RibbonBarProps) { return (
{/* Inhalt des aktiven Tabs. Tabs mit reichem Custom-Inhalt („Ansichten", - „Text") rendern diesen statt der datengetriebenen Item-Gruppen. */} + „Text") rendern diesen; der „Eigene"-Tab die modulare Custom-Bar; sonst + die datengetriebenen Item-Gruppen. */}
- {custom ? ( + {current.id === "custom" ? ( + + ) : custom ? ( custom ) : current.groups.length === 0 ? (
{t("ribbon.empty")}
diff --git a/src/ui/ribbon/ribbonItems.ts b/src/ui/ribbon/ribbonItems.ts index 07f9791..1aa8974 100644 --- a/src/ui/ribbon/ribbonItems.ts +++ b/src/ui/ribbon/ribbonItems.ts @@ -28,7 +28,7 @@ export interface RibbonGroup { } /** Kennung eines Ribbon-Tabs (fest, für Tab-State + i18n). */ -export type RibbonTabId = "views" | "2d" | "3d" | "bim"; +export type RibbonTabId = "views" | "2d" | "3d" | "bim" | "custom"; /** Ein Ribbon-Tab: Beschriftung + gruppierte Elemente. */ export interface RibbonTab { @@ -105,4 +105,35 @@ export const RIBBON_TABS: RibbonTab[] = [ }, ], }, + { + // Modulare Custom-Bar (Phase 4): der Nutzer stellt sich die Elemente selbst + // zusammen (persistiert). Gerendert aus `customItems`, nicht aus `groups`. + id: "custom", + labelKey: "ribbon.tab.custom", + groups: [], + }, ]; + +/** Stabiler Schlüssel eines Items für die Persistenz der Custom-Bar. */ +export function itemKey(item: RibbonItem): string { + return item.kind === "tool" ? `tool:${item.id}` : `command:${item.name}`; +} + +/** Parst einen Item-Schlüssel zurück (oder null bei unbekanntem Präfix). */ +export function itemFromKey(key: string): RibbonItem | null { + const i = key.indexOf(":"); + if (i < 0) return null; + const kind = key.slice(0, i); + const rest = key.slice(i + 1); + if (kind === "tool") return { kind: "tool", id: rest as ToolId }; + if (kind === "command") return { kind: "command", name: rest }; + return null; +} + +/** + * Alle datengetriebenen Items (aus den Werkzeug-/Befehls-Tabs) — die wählbare + * Menge für den Custom-Bar-Picker. „views"/„custom" tragen keine `groups`. + */ +export const ALL_RIBBON_ITEMS: RibbonItem[] = RIBBON_TABS.flatMap((tab) => + tab.groups.flatMap((g) => g.items), +);