Ribbon Phase 4: modulare Custom-Bar als eigener Tab (Eigene) mit +-Picker, localStorage-persistiert
This commit is contained in:
+2
-1
@@ -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.
|
||||
|
||||
+39
@@ -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<RibbonTabId>("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<string[]>(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<string>(
|
||||
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: (
|
||||
<ViewRibbonTab
|
||||
|
||||
@@ -125,9 +125,13 @@ export const de = {
|
||||
"ribbon.tab.3d": "3D",
|
||||
"ribbon.tab.bim": "BIM",
|
||||
"ribbon.tab.views": "Ansichten",
|
||||
"ribbon.tab.custom": "Eigene",
|
||||
"ribbon.group.draw": "Zeichnen",
|
||||
"ribbon.group.modify": "Ändern",
|
||||
"ribbon.group.components": "Bauteile",
|
||||
"ribbon.group.custom": "Eigene Leiste",
|
||||
"ribbon.custom.add": "+ Hinzufügen",
|
||||
"ribbon.custom.empty": "Elemente über + Hinzufügen wählen",
|
||||
"ribbon.empty": "Noch keine Werkzeuge in diesem Tab",
|
||||
"tool.select.hint": "Auswahl — Elemente klicken/aufziehen",
|
||||
"tool.window.hint": "Fenster: Wand wählen, dann Position setzen",
|
||||
|
||||
@@ -124,9 +124,13 @@ export const en: Record<TranslationKey, string> = {
|
||||
"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",
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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<Record<RibbonTabId, ReactNode>>;
|
||||
/** 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 (
|
||||
<div className="ribbon-group">
|
||||
<div className="ribbon-items">
|
||||
{items.length === 0 && (
|
||||
<div className="ribbon-empty">{t("ribbon.custom.empty")}</div>
|
||||
)}
|
||||
{items.map((item, i) => (
|
||||
<RibbonButton key={`${itemKey(item)}-${i}`} item={item} ctx={ctx} />
|
||||
))}
|
||||
<Dropdown
|
||||
items={pickerItems}
|
||||
placeholder={t("ribbon.custom.add")}
|
||||
title={t("ribbon.custom.add")}
|
||||
triggerClassName="ribbon-add"
|
||||
/>
|
||||
</div>
|
||||
<div className="ribbon-group-title">{t("ribbon.group.custom")}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="ribbon">
|
||||
{/* 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. */}
|
||||
<div className="ribbon-body">
|
||||
{custom ? (
|
||||
{current.id === "custom" ? (
|
||||
<CustomBar
|
||||
customItems={props.customItems}
|
||||
onToggle={props.onToggleCustomItem}
|
||||
ctx={ctx}
|
||||
/>
|
||||
) : custom ? (
|
||||
custom
|
||||
) : current.groups.length === 0 ? (
|
||||
<div className="ribbon-empty">{t("ribbon.empty")}</div>
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user