From e82d4b084cb1c4c33dbee35251bce2be5ce9ad13 Mon Sep 17 00:00:00 2001 From: Karim Date: Sun, 5 Jul 2026 20:57:13 +0200 Subject: [PATCH] =?UTF-8?q?Ribbon=20Phase=204:=20modulare=20Custom-Bar=20a?= =?UTF-8?q?ls=20eigener=20Tab=20(Eigene)=20mit=20=EF=BC=8B-Picker,=20local?= =?UTF-8?q?Storage-persistiert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PENDENZEN.md | 3 +- src/App.tsx | 39 +++++++++++++++++++ src/i18n/de.ts | 4 ++ src/i18n/en.ts | 4 ++ src/styles.css | 12 ++++++ src/ui/ribbon/RibbonBar.tsx | 73 ++++++++++++++++++++++++++++++++++-- src/ui/ribbon/ribbonItems.ts | 33 +++++++++++++++- 7 files changed, 163 insertions(+), 5 deletions(-) diff --git a/PENDENZEN.md b/PENDENZEN.md index bd0a6e7..21dc96d 100644 --- a/PENDENZEN.md +++ b/PENDENZEN.md @@ -39,7 +39,7 @@ ## ⏭️ Als Nächstes -- [ ] **Ribbon-UI + modulare Bars** (Nutzer-Vision 2026-07-05, bestätigt: Tab-Schema **2D·3D·BIM·Ansichten**). Voller Plan + datengetriebene Architektur: **[docs/design/ribbon-ui-plan.md](docs/design/ribbon-ui-plan.md)**. Ribbon-Oberleiste mit Tabs ersetzt die Werkzeug-Sidebar; datengetriebene Registry (RibbonItem = tool|command|action) ermöglicht auch eine **modulare Custom-Bar** (gleiche Items, vom Nutzer gewählt). Attribute bekommen volle Höhe, Objektinfo darunter gemergt; XYZ-Box oben rechts bleibt. **Phasen:** ~~(1) Gerüst + 2D/BIM-Tab~~ ✅ (`9d6e86d`), ~~(2) TopBar → Ansichten-Tab mergen~~ ✅ (`85011cb`), ~~(2b) Tabs in die TopBar-Zeile~~ ✅ (`456ebc8`), ~~(2c) OCS-Chrome (kleine Wortmarke + Quick-Access-Icons statt Burger, Zeile 26px)~~ ✅ (`4e3b074`), ~~(2d) Text + Ansichten auf eine Leiste, „Ansichten" als Standard-Tab zuerst~~ ✅ (`fe22cbf`), (3) **teilweise** ✅: Werkzeug-Sidebar aus Default-Layout raus + Attribute volle linke Höhe + Wandtyp-/Deckentyp-Picker ins Attribute-Panel verschoben (Nutzer-Entscheid), `LAYOUT_VERSION`→8; **offen:** Objektinfo unter Attribute mergen (großer Panel-Umbau, XYZ-Box-Platzierung — visuell iterieren), (4) modulare Custom-Bar. **Offen:** 3D-Tab füllen (aktuell leer; ggf. 3D-spezifisch statt Doppelung mit Ansichten), Band-Höhe/Abstände + 26px-Zeile visuell im Tauri abnehmen, Dauer-Zoom-Anzeige (Statusleiste?) klären. ✅ Vorarbeit: Eigenschaften-Grid im OCS-Stil (`00733d8`), Kreis+Bogen-Werkzeuge (`e454eab`/`bd2b12b`). **Nächster Schritt: im Tauri visuell prüfen (Band-Höhe, Icons, Aktiv-Highlight), dann Phase 2/3.** +- [ ] **Ribbon-UI + modulare Bars** (Nutzer-Vision 2026-07-05, bestätigt: Tab-Schema **2D·3D·BIM·Ansichten**). Voller Plan + datengetriebene Architektur: **[docs/design/ribbon-ui-plan.md](docs/design/ribbon-ui-plan.md)**. Ribbon-Oberleiste mit Tabs ersetzt die Werkzeug-Sidebar; datengetriebene Registry (RibbonItem = tool|command|action) ermöglicht auch eine **modulare Custom-Bar** (gleiche Items, vom Nutzer gewählt). Attribute bekommen volle Höhe, Objektinfo darunter gemergt; XYZ-Box oben rechts bleibt. **Phasen:** ~~(1) Gerüst + 2D/BIM-Tab~~ ✅ (`9d6e86d`), ~~(2) TopBar → Ansichten-Tab mergen~~ ✅ (`85011cb`), ~~(2b) Tabs in die TopBar-Zeile~~ ✅ (`456ebc8`), ~~(2c) OCS-Chrome (kleine Wortmarke + Quick-Access-Icons statt Burger, Zeile 26px)~~ ✅ (`4e3b074`), ~~(2d) Text + Ansichten auf eine Leiste, „Ansichten" als Standard-Tab zuerst~~ ✅ (`fe22cbf`), (3) **teilweise** ✅: Werkzeug-Sidebar aus Default-Layout raus + Attribute volle linke Höhe + Wandtyp-/Deckentyp-Picker ins Attribute-Panel verschoben (Nutzer-Entscheid), `LAYOUT_VERSION`→8; **offen:** Objektinfo unter Attribute mergen (großer Panel-Umbau, XYZ-Box-Platzierung — visuell iterieren), ~~(4) modulare Custom-Bar~~ ✅ (Tab „Eigene" + +-Picker, localStorage-persistiert). **Offen:** 3D-Tab füllen (aktuell leer; ggf. 3D-spezifisch statt Doppelung mit Ansichten), Band-Höhe/Abstände + 26px-Zeile visuell im Tauri abnehmen, Dauer-Zoom-Anzeige (Statusleiste?) klären. ✅ Vorarbeit: Eigenschaften-Grid im OCS-Stil (`00733d8`), Kreis+Bogen-Werkzeuge (`e454eab`/`bd2b12b`). **Nächster Schritt: im Tauri visuell prüfen (Band-Höhe, Icons, Aktiv-Highlight), dann Phase 2/3.** - [x] ~~**SPIKE — Bild-Texturen in `render3d`**~~ — **erledigt `0ca3b1d`** (verifiziert 2026-07-05): `RenderStyle::Textured` real, prozedurales 256×256-Schachbrett (kein Asset/`image`-Crate), UVs planar in Metern, Textur-Bind-Group group 1, `MESH_TEXTURED_WGSL` (gleiche Beleuchtung, Albedo aus `textureSample`), `spike3d` per `T` umschaltbar. Alt-Vertexpfad `[pos,normal,color]` bitgleich (Regressionstest). `cargo test` 58 grün (59 mit `--features render`, inkl. naga-Test); `spike3d`-Build sauber; keine neuen Deps, Default-Build unverändert. Auftrag: [SPIKE_TEXTUR_render3d.md](SPIKE_TEXTUR_render3d.md). **Lücken bis „richtig gutes" Texturing → siehe 3D-REST unten.** @@ -107,6 +107,7 @@ _Nur jüngste Session; ältere Historie siehe `git log` und HANDOVER-Narrative._ +- [x] 2026-07-05 **Ribbon Phase 4: modulare Custom-Bar** (Tab „Eigene") — datengetrieben auf der bestehenden Registry: neuer `custom`-Tab, `CustomBar` in `RibbonBar.tsx` rendert die vom Nutzer gewählten Items + einen „+ Hinzufügen"-Picker (Dropdown mit Häkchen, listet ALLE Werkzeuge/Befehle aus `ALL_RIBBON_ITEMS`, Klick = an/abwählen). `itemKey`/`itemFromKey` für stabile Persistenz; State in App (`customItems`) via localStorage (`dossier.ribbon.customItems`). Gleiche `RibbonButton`-Aktivierung wie normale Tabs (kein Sonderweg). tsc + Suite 334 grün. - [x] 2026-07-05 **Ribbon Phase 3 (Teil): Werkzeug-Sidebar raus** — `DEFAULT_LEFT_GROUPS` nur noch `attributes` (volle linke Höhe), `LAYOUT_VERSION` 7→8 (gespeicherte Layouts fallen auf neuen Default zurück → sichtbar). Wandtyp-/Deckentyp-Picker von `ToolsPanel` ins `AttributesPanel` verschoben (Nutzer-Entscheid „ins Attribute-Panel") — erscheint bei aktivem Wand-/Decken-Werkzeug (Sektion „Neues Bauteil", auch ohne Auswahl), Host-State unverändert. `ToolsPanel` bleibt registriert (per Fenster-Menü andockbar), `Dropdown`-Import dort entfernt. tsc + Suite 334 grün. **Offen:** Objektinfo unter Attribute mergen. - [x] 2026-07-05 **Fix: gezeichnete/importierte Kreise+Bögen im WebGL unsichtbar** — der WebGL-Compiler (`glPlan/glPlanCompile.ts`, Default-Renderer) dispatchte `polygon`/`line`/`arc`, aber NICHT `drawingCircle`/`drawingArc` → seit `4ac99d3` (Kreise als echtes `drawingCircle`-Primitiv statt 64-Eck-Polygon) fielen sie im GL still raus (SVG-/WASM-Pfad hatten sie, GL nicht). Zweig ergänzt: bildschirm-adaptive Tessellierung wie beim `arc`-Primitiv (Kreis geschlossen + optionale Vollton-Füllung, Bogen a0..a1 offen). tsc + Suite 334 grün. **Nutzer-Report** („zeichne Kreis, bleibt nicht sichtbar"). - [x] 2026-07-05 **Textur-Spike verifiziert erledigt** (`0ca3b1d`) — bei der Backlog-Abarbeitung festgestellt, dass die render3d-Textur-Spike (`RenderStyle::Textured`, Schachbrett, planare Meter-UVs, `MESH_TEXTURED_WGSL` group 1, `spike3d`-`T`-Toggle) bereits vollständig umgesetzt + committet war; `cargo test` 58/59 grün, keine neuen Deps, Alt-Pfad bitgleich. „Als Nächstes"-Eintrag war veraltet → abgehakt; PBR-Restlückenliste (~18–29 PT) in 3D-REST übernommen. 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), +);