TopBar-Redesign: Marke, Zoom-Cluster, Datei-Menü, Fenstersteuerung, Dark-Theme fest
- Marke (DOSSIER-Wortmarke) + Ressourcen-Icon ganz links, wie im Rhino-Plugin; Settings-Icon folgt über den neuen Einstellungs-Dialog. - Massstab/Zoom-Cluster bereinigt: doppelte Massstab-Dropdown entfernt, Zoom-Aktionen unter dem Massstab-Dropdown gestapelt (gleiche Breite), Ring bei "am Massstab" statt Flächen-Aufhellung. - PDF/DXF-Export aus der Leiste raus, zusammen mit Speichern/Öffnen/ Import in einem neuen Datei-Burger-Menü ganz rechts. Speichern/Öffnen sind echte JSON-Download/-Upload-Handler fürs ganze Projekt. - Einstellungs-Fenster (neu): Accent-Palette, Auswahlrahmen-/Snap-Farbe editierbar (freier Picker + Presets), Projekt-Referenzhöhe (m ü. M.) als Feld. - LayoutMenu auf die eigene Dropdown-Komponente umgestellt (war noch natives <select>). - Custom Fenstersteuerung (_/□/X) für die randlose Electron-Shell via Preload+IPC, eigener Look statt OS-Chrome; Oberleiste als Drag-Region. - Dark-Theme ist jetzt fester Standard (vorher an OS-Präferenz gekoppelt, zeigte je nach Umgebung fälschlich Light) — Light bleibt als Opt-in (`[data-theme="light"]`) für einen künftigen Umschalter. Alle Oberleisten-Pillen (Dropdown-Trigger, Segmente, Icon-Knöpfe, Ansichts-Icons) auf dieselbe dunkle Fläche wie das Kontextmenü vereinheitlicht. Panel-Köpfe nicht mehr separat aufgehellt, feste Höhe (40px) unabhängig von optionalem Darstellungsmodus-Dropdown. - 2D-Zoom-Grenzen deutlich erweitert (20000x/0.005x statt 50x/0.2x).
This commit is contained in:
@@ -4,10 +4,14 @@
|
||||
// (render2d-WASM-Engine, ?engine=wasm) zuverlässig läuft. Kein Rust-Backend
|
||||
// nötig: compute_joins hat einen TS-Fallback (src/compute/index.ts).
|
||||
|
||||
const { app, BrowserWindow, Menu } = require("electron");
|
||||
const { app, BrowserWindow, Menu, ipcMain } = require("electron");
|
||||
const path = require("node:path");
|
||||
|
||||
// Randloses App-Fenster wie chromium-shell.sh (--app=…): keine native
|
||||
// Menüleiste (File/Edit/View/Window), kein Fensterrahmen.
|
||||
// Menüleiste (File/Edit/View/Window), kein Fensterrahmen. Minimieren/
|
||||
// Maximieren/Schliessen laufen darum über eine eigene Fenstersteuerung im
|
||||
// Renderer (WindowControls.tsx) + IPC hier unten — ohne frame gibt es sonst
|
||||
// gar keine Möglichkeit, das Fenster zu steuern.
|
||||
Menu.setApplicationMenu(null);
|
||||
|
||||
// Gleiche Flags wie scripts/chromium-shell.sh — WebGPU liegt unter Linux
|
||||
@@ -27,9 +31,16 @@ function createWindow() {
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
preload: path.join(__dirname, "electron-preload.cjs"),
|
||||
},
|
||||
});
|
||||
|
||||
const notifyMaximized = () => {
|
||||
win.webContents.send("window:maximized-changed", win.isMaximized());
|
||||
};
|
||||
win.on("maximize", notifyMaximized);
|
||||
win.on("unmaximize", notifyMaximized);
|
||||
|
||||
if (isDev) {
|
||||
win.loadURL(DEV_URL);
|
||||
} else {
|
||||
@@ -37,6 +48,24 @@ function createWindow() {
|
||||
}
|
||||
}
|
||||
|
||||
// Fenstersteuerung vom Renderer (WindowControls.tsx über electron-preload.cjs).
|
||||
// Wirkt auf das Fenster, aus dem das Event kam (mehrfachfenster-sicher).
|
||||
ipcMain.on("window:minimize", (event) => {
|
||||
BrowserWindow.fromWebContents(event.sender)?.minimize();
|
||||
});
|
||||
ipcMain.on("window:toggle-maximize", (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender);
|
||||
if (!win) return;
|
||||
if (win.isMaximized()) win.unmaximize();
|
||||
else win.maximize();
|
||||
});
|
||||
ipcMain.on("window:close", (event) => {
|
||||
BrowserWindow.fromWebContents(event.sender)?.close();
|
||||
});
|
||||
ipcMain.handle("window:is-maximized", (event) => {
|
||||
return BrowserWindow.fromWebContents(event.sender)?.isMaximized() ?? false;
|
||||
});
|
||||
|
||||
app.whenReady().then(createWindow);
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// electron-preload.cjs — Bruecke zwischen dem isolierten Renderer (contextIsolation)
|
||||
// und dem Electron-Hauptprozess. Braucht es NUR fuer die custom Fenstersteuerung
|
||||
// (Minimieren/Maximieren/Schliessen) im randlosen Fenster (frame:false, siehe
|
||||
// electron-main.cjs) — sonst gaebe es keine Moeglichkeit, das Fenster ueberhaupt
|
||||
// zu steuern.
|
||||
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
contextBridge.exposeInMainWorld("dossierWindow", {
|
||||
minimize: () => ipcRenderer.send("window:minimize"),
|
||||
toggleMaximize: () => ipcRenderer.send("window:toggle-maximize"),
|
||||
close: () => ipcRenderer.send("window:close"),
|
||||
isMaximized: () => ipcRenderer.invoke("window:is-maximized"),
|
||||
onMaximizedChange: (callback) => {
|
||||
const listener = (_event, isMaximized) => callback(isMaximized);
|
||||
ipcRenderer.on("window:maximized-changed", listener);
|
||||
return () => ipcRenderer.removeListener("window:maximized-changed", listener);
|
||||
},
|
||||
});
|
||||
+154
-48
@@ -53,6 +53,7 @@ import { savePlanPdf } from "./export/exportPdf";
|
||||
import { ExportDxfDialog } from "./ui/ExportDxfDialog";
|
||||
import type { ExportDxfDecision } from "./ui/ExportDxfDialog";
|
||||
import { savePlanDxf } from "./export/exportDxf";
|
||||
import { SettingsDialog } from "./ui/SettingsDialog";
|
||||
import { PlanView } from "./plan/PlanView";
|
||||
import type {
|
||||
PlanViewHandle,
|
||||
@@ -66,6 +67,8 @@ import { ResourceManager } from "./ui/ResourceManager";
|
||||
import type { ResourceManagerHandlers } from "./ui/ResourceManager";
|
||||
import { HatchSwatch } from "./ui/hatchPreview";
|
||||
import { TopBar } from "./ui/TopBar";
|
||||
import { Dropdown } from "./ui/Dropdown";
|
||||
import type { DropdownItem } from "./ui/Dropdown";
|
||||
import { TextEditorDialog } from "./ui/TextEditorDialog";
|
||||
import { RoomStampEditor } from "./panels/RoomStampEditor";
|
||||
import { defaultRoomStamp } from "./model/roomStamp";
|
||||
@@ -223,6 +226,8 @@ export default function App() {
|
||||
// Endlosschleife). Actions leben ebenfalls im Zustand und sind referenzstabil.
|
||||
const project = useStore((s) => s.project);
|
||||
const setProject = useStore((s) => s.setProject);
|
||||
const undo = useStore((s) => s.undo);
|
||||
const redo = useStore((s) => s.redo);
|
||||
const activeLevelId = useStore((s) => s.activeLevelId);
|
||||
const setActiveLevelId = useStore((s) => s.setActiveLevelId);
|
||||
const viewType = useStore((s) => s.viewType);
|
||||
@@ -246,6 +251,8 @@ export default function App() {
|
||||
// Modaler PDF-Export-Dialog (Grundriss → Vektor-PDF). true = offen.
|
||||
const [exportOpen, setExportOpen] = useState(false);
|
||||
const [exportDxfOpen, setExportDxfOpen] = useState(false);
|
||||
// Modales Einstellungs-Fenster (Darstellungsfarben + Projekt-Felder).
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
// Visuelles Drop-Feedback (dezenter Rahmen), solange eine Datei über die App
|
||||
// gezogen wird. Zählt dragenter/dragleave aus, um Flackern bei Kind-Elementen
|
||||
// zu vermeiden.
|
||||
@@ -268,6 +275,10 @@ export default function App() {
|
||||
const setReferenceLines = useStore((s) => s.setReferenceLines);
|
||||
const lineMode = useStore((s) => s.lineMode);
|
||||
const setLineMode = useStore((s) => s.setLineMode);
|
||||
const marqueeColor = useStore((s) => s.marqueeColor);
|
||||
const setMarqueeColor = useStore((s) => s.setMarqueeColor);
|
||||
const snapColor = useStore((s) => s.snapColor);
|
||||
const setSnapColor = useStore((s) => s.setSnapColor);
|
||||
const scaleDenominator = useStore((s) => s.scaleDenominator);
|
||||
const setScaleDenominator = useStore((s) => s.setScaleDenominator);
|
||||
// LIVE Papier-Massstab-Nenner aus der View-Transform (PlanView meldet ihn);
|
||||
@@ -387,6 +398,8 @@ export default function App() {
|
||||
// Der Klick auf „DXF importieren" triggert .click() dieses Inputs; onChange
|
||||
// liest die Datei als Text und reicht sie durch parseDxf → addContextObjects.
|
||||
const dxfInputRef = useRef<HTMLInputElement>(null);
|
||||
// Verstecktes Datei-Input zum Laden eines Projekts (Datei-Burger „Öffnen").
|
||||
const projectFileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Den DXF-Datei-Dialog-Öffner für den „import"-Befehl registrieren (entkoppelt
|
||||
// über den Modul-Hook). Beim Unmount wieder abmelden.
|
||||
@@ -791,6 +804,29 @@ export default function App() {
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, []);
|
||||
|
||||
// Globaler Undo/Redo-Shortcut: Ctrl+Z / Cmd+Z, Ctrl+Shift+Z / Cmd+Shift+Z
|
||||
// bzw. Ctrl+Y für redo. Gleicher Eingabe-Guard wie die übrigen globalen
|
||||
// keydown-Handler (Tab-Zyklus, Zahlen-Shortcuts).
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
const mod = e.ctrlKey || e.metaKey;
|
||||
const key = e.key.toLowerCase();
|
||||
if (!mod || (key !== "z" && key !== "y")) return;
|
||||
const el = e.target as HTMLElement | null;
|
||||
if (
|
||||
el &&
|
||||
(el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
if (key === "y" || (key === "z" && e.shiftKey)) redo();
|
||||
else undo();
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [undo, redo]);
|
||||
|
||||
// Nummern-Shortcuts für Zeichenwerkzeuge (Vectorworks-Stil): 2 Linie, 4
|
||||
// Rechteck, 5 Polylinie, 6 Wand, 7 Decke, 8 Fenster, 9 Türe, 0 Raum. Nur auf
|
||||
// Geschoss-Tabs, nicht beim Tippen in Feldern und nicht mit Modifikator
|
||||
@@ -1981,6 +2017,41 @@ export default function App() {
|
||||
r.readAsText(file);
|
||||
};
|
||||
|
||||
// Projekt als Datei speichern/laden (Datei-Burger-Menü, TopBar/AppMenu).
|
||||
// Reines JSON des ProjectSlice-Zustands (gleiche Blob+Anchor-Download-Technik
|
||||
// wie savePlanDxf/savePlanPdf); Laden liest die Datei als Text, parst sie und
|
||||
// ersetzt das Projekt — nur ein grobes Shape-Sanity-Check (walls/layers als
|
||||
// Array), damit ein fremdes/kaputtes JSON nicht die App zerlegt.
|
||||
const onSaveProject = () => {
|
||||
const json = JSON.stringify(project, null, 2);
|
||||
const blob = new Blob([json], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `${project.name || "projekt"}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
const onOpenProject = () => projectFileInputRef.current?.click();
|
||||
const onProjectFileChosen = (file: File) => {
|
||||
const r = new FileReader();
|
||||
r.onload = () => {
|
||||
try {
|
||||
const parsed = JSON.parse(String(r.result ?? "")) as Partial<Project>;
|
||||
if (!Array.isArray(parsed.walls) || !Array.isArray(parsed.layers)) {
|
||||
console.warn("Projekt-Datei hat kein gültiges Projekt-Format (walls/layers fehlen).");
|
||||
return;
|
||||
}
|
||||
setProject(parsed as Project);
|
||||
} catch (err) {
|
||||
console.error("Projekt-Import fehlgeschlagen:", err);
|
||||
}
|
||||
};
|
||||
r.readAsText(file);
|
||||
};
|
||||
|
||||
// Bestätigte Import-Entscheidung ausführen: ggf. neue Zeichnung anlegen, die
|
||||
// 2D-Konturen als Drawing2D anhängen, optional die Meshes als Kontext
|
||||
// übernehmen. Reihenfolge: erst Ebene (echte ID), dann Drawings/Kontext.
|
||||
@@ -2917,6 +2988,18 @@ export default function App() {
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
{/* Verstecktes Projekt-Datei-Input (Datei-Burger „Öffnen"). */}
|
||||
<input
|
||||
ref={projectFileInputRef}
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
style={{ display: "none" }}
|
||||
onChange={(e) => {
|
||||
const f = e.target.files?.[0];
|
||||
if (f) onProjectFileChosen(f);
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<TopBar
|
||||
project={project}
|
||||
viewToggleEnabled={activeLevel.kind === "floor"}
|
||||
@@ -2967,6 +3050,10 @@ export default function App() {
|
||||
}}
|
||||
resourcesOpen={resourcesOpen}
|
||||
onToggleResources={() => setResourcesOpen((v) => !v)}
|
||||
onOpenSettings={() => setSettingsOpen(true)}
|
||||
onImport={() => dxfInputRef.current?.click()}
|
||||
onSaveProject={onSaveProject}
|
||||
onOpenProject={onOpenProject}
|
||||
layoutMenu={
|
||||
<LayoutMenu
|
||||
layout={layout}
|
||||
@@ -3008,6 +3095,8 @@ export default function App() {
|
||||
detail={detail}
|
||||
referenceLines={referenceLines}
|
||||
hairline={lineMode === "display"}
|
||||
marqueeColor={marqueeColor}
|
||||
snapColor={snapColor}
|
||||
renderMode={renderMode}
|
||||
view3d={view3d}
|
||||
fov={fov}
|
||||
@@ -3243,6 +3332,18 @@ export default function App() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{settingsOpen && (
|
||||
<SettingsDialog
|
||||
marqueeColor={marqueeColor}
|
||||
onMarqueeColorChange={setMarqueeColor}
|
||||
snapColor={snapColor}
|
||||
onSnapColorChange={setSnapColor}
|
||||
project={project}
|
||||
setProject={setProject}
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Text-Editor (schwebendes Fenster): Doppelklick auf einen Raumstempel bzw.
|
||||
Button in der Objekt-Info öffnet ihn. „Übernehmen" schreibt das
|
||||
bearbeitete Rich-Text-Dokument über setRoomStampDoc. Die Formatier-
|
||||
@@ -3908,58 +4009,39 @@ function LayoutMenu({
|
||||
refresh();
|
||||
};
|
||||
|
||||
const onPick = (value: string) => {
|
||||
if (value === "__save__") {
|
||||
onSave();
|
||||
return;
|
||||
// Aktions-Menü (analog ComboMenu in TopBar.tsx): „Aktuelles speichern" oben,
|
||||
// dann je eine Gruppe „Laden" und „Löschen" (Trenner statt <optgroup>, die
|
||||
// die Dropdown-Komponente nicht kennt).
|
||||
const items: DropdownItem[] = [];
|
||||
items.push({ label: t("layout.saveCurrent"), onSelect: onSave });
|
||||
if (names.length > 0) {
|
||||
items.push({ divider: true });
|
||||
for (const n of names) {
|
||||
items.push({
|
||||
label: n,
|
||||
onSelect: () => {
|
||||
const loaded = loadNamedLayout(n);
|
||||
if (loaded) onApply(loaded);
|
||||
},
|
||||
});
|
||||
}
|
||||
if (value.startsWith("__del__:")) {
|
||||
const name = value.slice("__del__:".length);
|
||||
deleteNamedLayout(name);
|
||||
refresh();
|
||||
return;
|
||||
items.push({ divider: true });
|
||||
for (const n of names) {
|
||||
items.push({
|
||||
label: `✕ ${n}`,
|
||||
danger: true,
|
||||
onSelect: () => {
|
||||
deleteNamedLayout(n);
|
||||
refresh();
|
||||
},
|
||||
});
|
||||
}
|
||||
const loaded = loadNamedLayout(value);
|
||||
if (loaded) onApply(loaded);
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<select
|
||||
className="layout-menu"
|
||||
title={t("layout.title")}
|
||||
aria-label={t("layout.title")}
|
||||
value=""
|
||||
onFocus={refresh}
|
||||
onMouseDown={refresh}
|
||||
onChange={(e) => {
|
||||
onPick(e.target.value);
|
||||
// Auswahl-Anzeige zurücksetzen (Menü ist ein Aktions-Trigger).
|
||||
e.currentTarget.value = "";
|
||||
}}
|
||||
>
|
||||
<option value="" disabled>
|
||||
{t("layout.placeholder")}
|
||||
</option>
|
||||
<option value="__save__">{t("layout.saveCurrent")}</option>
|
||||
{names.length > 0 && (
|
||||
<optgroup label={t("layout.loadGroup")}>
|
||||
{names.map((n) => (
|
||||
<option key={n} value={n}>
|
||||
{n}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
)}
|
||||
{names.length > 0 && (
|
||||
<optgroup label={t("layout.deleteGroup")}>
|
||||
{names.map((n) => (
|
||||
<option key={`del-${n}`} value={`__del__:${n}`}>
|
||||
✕ {n}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
)}
|
||||
</select>
|
||||
<span onPointerDownCapture={refresh}>
|
||||
<Dropdown items={items} placeholder={t("layout.placeholder")} title={t("layout.title")} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3974,6 +4056,8 @@ function Content({
|
||||
detail,
|
||||
referenceLines,
|
||||
hairline,
|
||||
marqueeColor,
|
||||
snapColor,
|
||||
renderMode,
|
||||
view3d,
|
||||
fov,
|
||||
@@ -4029,6 +4113,10 @@ function Content({
|
||||
detail: DetailLevel;
|
||||
referenceLines: boolean;
|
||||
hairline: boolean;
|
||||
/** Farbe des Auswahlrahmens (Marquee) im Grundriss. */
|
||||
marqueeColor: string;
|
||||
/** Farbe der Snap-/Endpunkt-Hervorhebung im Grundriss. */
|
||||
snapColor: string;
|
||||
renderMode: RenderMode;
|
||||
/** Kanonischer 3D-Blickwinkel (Preset) der Perspektive. */
|
||||
view3d: View3d;
|
||||
@@ -4111,6 +4199,8 @@ function Content({
|
||||
project={project}
|
||||
level={level}
|
||||
hairline={hairline}
|
||||
marqueeColor={marqueeColor}
|
||||
snapColor={snapColor}
|
||||
mono={mono}
|
||||
planViewRef={planViewRef}
|
||||
onCursor={onCursor}
|
||||
@@ -4160,6 +4250,8 @@ function Content({
|
||||
detail={detail}
|
||||
referenceLines={referenceLines}
|
||||
hairline={hairline}
|
||||
marqueeColor={marqueeColor}
|
||||
snapColor={snapColor}
|
||||
mono={mono}
|
||||
planViewRef={planViewRef}
|
||||
onCursor={onCursor}
|
||||
@@ -4240,6 +4332,8 @@ function Content({
|
||||
detail={detail}
|
||||
referenceLines={referenceLines}
|
||||
hairline={hairline}
|
||||
marqueeColor={marqueeColor}
|
||||
snapColor={snapColor}
|
||||
mono={mono}
|
||||
planViewRef={planViewRef}
|
||||
onCursor={onCursor}
|
||||
@@ -4279,6 +4373,8 @@ function LevelPlanView({
|
||||
detail,
|
||||
referenceLines,
|
||||
hairline,
|
||||
marqueeColor,
|
||||
snapColor,
|
||||
mono,
|
||||
planViewRef,
|
||||
onCursor,
|
||||
@@ -4311,6 +4407,8 @@ function LevelPlanView({
|
||||
detail: DetailLevel;
|
||||
referenceLines: boolean;
|
||||
hairline: boolean;
|
||||
marqueeColor: string;
|
||||
snapColor: string;
|
||||
mono: boolean;
|
||||
planViewRef: React.RefObject<PlanViewHandle>;
|
||||
onCursor: (model: Vec2 | null) => void;
|
||||
@@ -4406,6 +4504,8 @@ function LevelPlanView({
|
||||
grips={grips}
|
||||
edgeGrips={edgeGrips}
|
||||
hairline={hairline}
|
||||
marqueeColor={marqueeColor}
|
||||
snapColor={snapColor}
|
||||
selectedDrawingId={selectedDrawingId}
|
||||
selectedDrawingIds={selectedDrawingIds}
|
||||
gripHandlers={gripHandlers}
|
||||
@@ -4429,6 +4529,8 @@ function SectionPlanView({
|
||||
project,
|
||||
level,
|
||||
hairline,
|
||||
marqueeColor,
|
||||
snapColor,
|
||||
mono,
|
||||
planViewRef,
|
||||
onCursor,
|
||||
@@ -4457,6 +4559,8 @@ function SectionPlanView({
|
||||
project: Project;
|
||||
level: DrawingLevel;
|
||||
hairline: boolean;
|
||||
marqueeColor: string;
|
||||
snapColor: string;
|
||||
mono: boolean;
|
||||
planViewRef: React.RefObject<PlanViewHandle>;
|
||||
onCursor: (model: Vec2 | null) => void;
|
||||
@@ -4562,6 +4666,8 @@ function SectionPlanView({
|
||||
grips={grips}
|
||||
edgeGrips={edgeGrips}
|
||||
hairline={hairline}
|
||||
marqueeColor={marqueeColor}
|
||||
snapColor={snapColor}
|
||||
selectedDrawingId={selectedDrawingId}
|
||||
selectedDrawingIds={selectedDrawingIds}
|
||||
gripHandlers={gripHandlers}
|
||||
|
||||
@@ -802,6 +802,30 @@ export const de = {
|
||||
"exportDxf.unitsNote":
|
||||
"Modell-Space in Metern (1:1). Layer entsprechen den Ebenen des Projekts; beim Plot beliebig skalierbar.",
|
||||
|
||||
// ── Datei-Menü (Burger, ganz rechts der Oberleiste) ──────────────────────
|
||||
"file.menu": "Datei",
|
||||
"file.save": "Projekt speichern",
|
||||
"file.open": "Projekt öffnen",
|
||||
"file.import": "DXF/DWG importieren…",
|
||||
"file.exportPdf": "Als PDF exportieren…",
|
||||
"file.exportDxf": "Als DXF exportieren…",
|
||||
|
||||
// ── Einstellungs-Fenster ──────────────────────────────────────────────────
|
||||
"topbar.settings": "Einstellungen",
|
||||
"topbar.settings.hint": "Einstellungen öffnen (Darstellungsfarben, Projekt)",
|
||||
"settings.title": "Einstellungen",
|
||||
"settings.section.display": "Darstellung",
|
||||
"settings.marqueeColor": "Auswahlrahmen-Farbe",
|
||||
"settings.marqueeColor.hint":
|
||||
"Farbe des Auswahlrahmens beim Aufziehen einer Auswahl (Marquee) im Grundriss.",
|
||||
"settings.snapColor": "Snap-/Endpunkt-Farbe",
|
||||
"settings.snapColor.hint":
|
||||
"Farbe der Snap-/Endpunkt-Hervorhebung beim Zeichnen/Editieren im Grundriss.",
|
||||
"settings.section.project": "Projekt",
|
||||
"settings.referenceElevation": "Referenzhöhe EG (m ü. M.)",
|
||||
"settings.referenceElevation.hint":
|
||||
"Nur gespeichert — wird noch nicht verwendet. Folgt für Terrain-Draping/reale Höhen relativ zum Erdgeschoss.",
|
||||
|
||||
// ── Drag & Drop (App-weiter Datei-Drop) ──────────────────────────────────
|
||||
"drop.hint": "DXF/DWG-Datei hier ablegen",
|
||||
|
||||
|
||||
@@ -793,6 +793,30 @@ export const en: Record<TranslationKey, string> = {
|
||||
"exportDxf.unitsNote":
|
||||
"Model space in meters (1:1). Layers mirror the project's categories; scale freely on plot.",
|
||||
|
||||
// File menu (burger, far right of the top bar).
|
||||
"file.menu": "File",
|
||||
"file.save": "Save project",
|
||||
"file.open": "Open project",
|
||||
"file.import": "Import DXF/DWG…",
|
||||
"file.exportPdf": "Export as PDF…",
|
||||
"file.exportDxf": "Export as DXF…",
|
||||
|
||||
// Settings dialog.
|
||||
"topbar.settings": "Settings",
|
||||
"topbar.settings.hint": "Open settings (display colors, project)",
|
||||
"settings.title": "Settings",
|
||||
"settings.section.display": "Display",
|
||||
"settings.marqueeColor": "Selection box color",
|
||||
"settings.marqueeColor.hint":
|
||||
"Color of the marquee selection box drawn while dragging a selection in the plan view.",
|
||||
"settings.snapColor": "Snap/endpoint color",
|
||||
"settings.snapColor.hint":
|
||||
"Color of the snap/endpoint highlight while drawing/editing in the plan view.",
|
||||
"settings.section.project": "Project",
|
||||
"settings.referenceElevation": "Ground floor reference elevation (m a.s.l.)",
|
||||
"settings.referenceElevation.hint":
|
||||
"Stored only — not used yet. Terrain draping / real-world elevations relative to the ground floor will follow.",
|
||||
|
||||
// Drag & drop (app-wide file drop).
|
||||
"drop.hint": "Drop a DXF/DWG file here",
|
||||
|
||||
|
||||
@@ -1144,6 +1144,15 @@ export interface Project {
|
||||
* aufgelöst — generiert Wall[]-Objekte bei Bedarf.
|
||||
*/
|
||||
parametricWalls?: ParametricWall[];
|
||||
/**
|
||||
* Referenzhöhe des Erdgeschosses in Metern über Meer (m ü. M.), editierbar
|
||||
* im Einstellungs-Fenster. Optional, damit bestehende Projekte ohne diesen
|
||||
* Wert gültig bleiben (Default: unbestimmt/0). NUR Speicherfeld — die
|
||||
* eigentliche Verwendung (Terrain-Draping/reale Höhen relativ dazu, siehe
|
||||
* HANDOVER GEO-BLOCK) ist ein separater, späterer Task und liest dieses
|
||||
* Feld noch nicht.
|
||||
*/
|
||||
referenceElevationMasl?: number;
|
||||
}
|
||||
|
||||
// ── Helfer ───────────────────────────────────────────────────────────────
|
||||
|
||||
+47
-7
@@ -22,6 +22,7 @@ import { useWasmPlanRenderer } from "./useWasmPlanRenderer";
|
||||
import { quantizePen } from "../export/sceneToPrintSvg";
|
||||
import { buildRandomHatchRuns, zigzagPoints, motifPoints, DASH_MM_TO_M } from "./glPlan/glPlanHatch";
|
||||
import { dashHasDot } from "../ui/lineSegments";
|
||||
import { DEFAULT_MARQUEE_COLOR, DEFAULT_SNAP_COLOR } from "../theme/accents";
|
||||
|
||||
const PX_PER_M = 90; // viewBox-Einheiten je Meter (Modell → SVG-Benutzerraum)
|
||||
const PAD = 60; // Rand in viewBox-Einheiten
|
||||
@@ -79,8 +80,11 @@ const mmToPx = (mm: number): number => (mm / 25.4) * dpi();
|
||||
|
||||
// Zoom-Grenzen relativ zur eingepassten Default-Ansicht. Kleinere viewBox-
|
||||
// Breite = stärkerer Zoom. ZOOM_MAX → max. Hineinzoomen, ZOOM_MIN → Herauszoomen.
|
||||
const ZOOM_MAX = 50; // bis 50× hinein
|
||||
const ZOOM_MIN = 0.2; // bis 5× heraus
|
||||
// Grosszügig bemessen (CAD-Anspruch: vom Milimeter-Detail bis zum Siedlungs-
|
||||
// Kontext) statt der früheren 250×-Spanne (50/0.2), die für Detailarbeit an
|
||||
// einem Gebäude deutlich zu eng war.
|
||||
const ZOOM_MAX = 20000; // bis 20 000× hinein (Sub-Millimeter-Details)
|
||||
const ZOOM_MIN = 0.005; // bis 200× heraus (Siedlungs-/Site-Kontext)
|
||||
const WHEEL_STEP = 0.0015; // Empfindlichkeit des Mausrads
|
||||
|
||||
// Schwelle (Bildschirm-Pixel), ab der ein Links-Ziehen als Marquee gilt; darunter
|
||||
@@ -320,6 +324,16 @@ export interface PlanViewProps {
|
||||
selectedDrawingIds?: string[];
|
||||
/** Griff-Zieh-Treiber: Live-Bewegung (gesnappt) + Abschluss. */
|
||||
gripHandlers?: GripHandlers;
|
||||
/**
|
||||
* Farbe des Auswahlrahmens (Marquee-Ziehauswahl). Einstellbar im
|
||||
* Einstellungs-Fenster (State in ViewSlice); Default = Akzent „Yuyake".
|
||||
*/
|
||||
marqueeColor?: string;
|
||||
/**
|
||||
* Farbe der Snap-/Endpunkt-Hervorhebung (Werkzeug-Overlay). Einstellbar im
|
||||
* Einstellungs-Fenster (State in ViewSlice); Default = Akzent „Sora".
|
||||
*/
|
||||
snapColor?: string;
|
||||
}
|
||||
|
||||
/** Treiber für das Ziehen von Editier-Griffen (von App bereitgestellt). */
|
||||
@@ -370,6 +384,8 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
||||
selectedDrawingId,
|
||||
selectedDrawingIds,
|
||||
gripHandlers,
|
||||
marqueeColor = DEFAULT_MARQUEE_COLOR,
|
||||
snapColor = DEFAULT_SNAP_COLOR,
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
@@ -1884,6 +1900,7 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
||||
y={marqueeRect.y}
|
||||
width={marqueeRect.w}
|
||||
height={marqueeRect.h}
|
||||
style={{ stroke: marqueeColor, fill: marqueeColor }}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
)}
|
||||
@@ -1965,6 +1982,7 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
||||
draft={toolHandlers.draft}
|
||||
toScreen={toScreen}
|
||||
vbPerPx={1 / (meetScale(view, svgRef.current) ?? 1)}
|
||||
snapColor={snapColor}
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
@@ -1983,10 +2001,12 @@ function DraftOverlay({
|
||||
draft,
|
||||
toScreen,
|
||||
vbPerPx,
|
||||
snapColor,
|
||||
}: {
|
||||
draft: { preview: DraftShape[]; vertices: Vec2[]; snap?: SnapResult | null };
|
||||
toScreen: (v: Vec2) => Vec2;
|
||||
vbPerPx: number;
|
||||
snapColor: string;
|
||||
}) {
|
||||
const vertSize = 4.5 * vbPerPx; // halbe Kantenlänge eines Stützpunkt-Quadrats (klein + einheitlich)
|
||||
const markSize = 4.5 * vbPerPx; // halbe Kantenlänge des Snap-Markers (gleich groß wie Stützpunkt)
|
||||
@@ -2030,7 +2050,7 @@ function DraftOverlay({
|
||||
})}
|
||||
{/* Snap-Marker + optionale Ortho-Hilfslinie. */}
|
||||
{draft.snap && (
|
||||
<SnapMarker snap={draft.snap} toScreen={toScreen} size={markSize} />
|
||||
<SnapMarker snap={draft.snap} toScreen={toScreen} size={markSize} color={snapColor} />
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
@@ -2041,13 +2061,22 @@ function SnapMarker({
|
||||
snap,
|
||||
toScreen,
|
||||
size,
|
||||
color,
|
||||
}: {
|
||||
snap: SnapResult;
|
||||
toScreen: (v: Vec2) => Vec2;
|
||||
size: number;
|
||||
/** Farbe des Snap-Glyphs (einstellbar, Default = Akzent „Sora"). */
|
||||
color: string;
|
||||
}) {
|
||||
const s = toScreen(snap.point);
|
||||
const r = size;
|
||||
// Inline `style` überschreibt gezielt die CSS-Klassenfarbe (eine am Element
|
||||
// selbst deklarierte Klassenregel gewinnt sonst gegen eine geerbte Farbe von
|
||||
// einer umschließenden Gruppe — daher pro Glyph statt einmal an der Gruppe).
|
||||
const glyphStyle = { stroke: color };
|
||||
const helperStyle = { stroke: color };
|
||||
const fillStyle = { stroke: color, fill: color };
|
||||
return (
|
||||
<g className="snap-marker">
|
||||
{(snap.kind === "ortho" || snap.kind === "extension") && snap.refA && (
|
||||
@@ -2057,11 +2086,19 @@ function SnapMarker({
|
||||
y1={toScreen(snap.refA).y}
|
||||
x2={s.x}
|
||||
y2={s.y}
|
||||
style={helperStyle}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
)}
|
||||
{snap.kind === "grid" && (
|
||||
<circle className="snap-glyph snap-fill" cx={s.x} cy={s.y} r={r * 0.5} vectorEffect="non-scaling-stroke" />
|
||||
<circle
|
||||
className="snap-glyph snap-fill"
|
||||
cx={s.x}
|
||||
cy={s.y}
|
||||
r={r * 0.5}
|
||||
style={fillStyle}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
)}
|
||||
{snap.kind === "midpoint" && (
|
||||
<line
|
||||
@@ -2070,22 +2107,24 @@ function SnapMarker({
|
||||
y1={s.y}
|
||||
x2={s.x + r}
|
||||
y2={s.y}
|
||||
style={glyphStyle}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
)}
|
||||
{snap.kind === "intersection" && (
|
||||
<>
|
||||
<line className="snap-glyph" x1={s.x - r} y1={s.y - r} x2={s.x + r} y2={s.y + r} vectorEffect="non-scaling-stroke" />
|
||||
<line className="snap-glyph" x1={s.x - r} y1={s.y + r} x2={s.x + r} y2={s.y - r} vectorEffect="non-scaling-stroke" />
|
||||
<line className="snap-glyph" x1={s.x - r} y1={s.y - r} x2={s.x + r} y2={s.y + r} style={glyphStyle} vectorEffect="non-scaling-stroke" />
|
||||
<line className="snap-glyph" x1={s.x - r} y1={s.y + r} x2={s.x + r} y2={s.y - r} style={glyphStyle} vectorEffect="non-scaling-stroke" />
|
||||
</>
|
||||
)}
|
||||
{(snap.kind === "center" || snap.kind === "quadrant") && (
|
||||
<circle className="snap-glyph" cx={s.x} cy={s.y} r={r} vectorEffect="non-scaling-stroke" />
|
||||
<circle className="snap-glyph" cx={s.x} cy={s.y} r={r} style={glyphStyle} vectorEffect="non-scaling-stroke" />
|
||||
)}
|
||||
{snap.kind === "onEdge" && (
|
||||
<polygon
|
||||
className="snap-glyph"
|
||||
points={`${s.x},${s.y - r} ${s.x + r},${s.y} ${s.x},${s.y + r} ${s.x - r},${s.y}`}
|
||||
style={glyphStyle}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
)}
|
||||
@@ -2095,6 +2134,7 @@ function SnapMarker({
|
||||
cx={s.x}
|
||||
cy={s.y}
|
||||
r={r}
|
||||
style={glyphStyle}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { sampleProject } from "../model/sampleProject";
|
||||
import type { DetailLevel, RenderMode, View3d, ViewType } from "../ui/TopBar";
|
||||
import type { DisplayMode } from "../panels/types";
|
||||
import { DEFAULT_MARQUEE_COLOR, DEFAULT_SNAP_COLOR } from "../theme/accents";
|
||||
import type { StoreApi } from "./store";
|
||||
|
||||
/** Standard-Darstellungsmodus je Panel (wie bisher: alles sichtbar normal). */
|
||||
@@ -45,6 +46,13 @@ export interface ViewSlice {
|
||||
activeCategoryCode: string;
|
||||
// Darstellungsmodus je Panel-ID (z. B. "drawing-levels", "layers").
|
||||
displayModes: Record<string, DisplayMode>;
|
||||
// Farbe des Auswahlrahmens (Marquee-Ziehauswahl) im Grundriss. Einstellbar
|
||||
// im Einstellungs-Fenster; Default = Akzent „Yuyake" (theme/accents.ts).
|
||||
marqueeColor: string;
|
||||
// Farbe der Snap-/Endpunkt-Hervorhebung im Grundriss (Zeichnen/Editieren).
|
||||
// Einstellbar im Einstellungs-Fenster; Default = Akzent „Sora" (vorläufig,
|
||||
// siehe HANDOVER-Rückfrage „aki").
|
||||
snapColor: string;
|
||||
|
||||
setActiveLevelId: (id: string) => void;
|
||||
setViewType: (v: ViewType) => void;
|
||||
@@ -61,6 +69,8 @@ export interface ViewSlice {
|
||||
modeFor: (id: string | null) => DisplayMode;
|
||||
/** Darstellungsmodus eines Panels setzen (No-op bei null id). */
|
||||
setModeFor: (id: string | null, mode: DisplayMode) => void;
|
||||
setMarqueeColor: (hex: string) => void;
|
||||
setSnapColor: (hex: string) => void;
|
||||
}
|
||||
|
||||
export function createViewSlice(api: StoreApi<ViewSlice>): ViewSlice {
|
||||
@@ -80,6 +90,8 @@ export function createViewSlice(api: StoreApi<ViewSlice>): ViewSlice {
|
||||
activeCategoryCode:
|
||||
sampleProject.walls[0]?.categoryCode ?? sampleProject.layers[0].code,
|
||||
displayModes: {},
|
||||
marqueeColor: DEFAULT_MARQUEE_COLOR,
|
||||
snapColor: DEFAULT_SNAP_COLOR,
|
||||
|
||||
setActiveLevelId: (id) =>
|
||||
set((s) => ({
|
||||
@@ -105,5 +117,7 @@ export function createViewSlice(api: StoreApi<ViewSlice>): ViewSlice {
|
||||
if (!id) return;
|
||||
set((s) => ({ displayModes: { ...s.displayModes, [id]: mode } }));
|
||||
},
|
||||
setMarqueeColor: (hex) => set({ marqueeColor: hex }),
|
||||
setSnapColor: (hex) => set({ snapColor: hex }),
|
||||
};
|
||||
}
|
||||
|
||||
+291
-154
@@ -3,10 +3,69 @@
|
||||
das DOSSIER-Look: aktive Zeile als Petrol-Pille, nicht-aktive gedimmt,
|
||||
ausgeblendete grau; Augen-Schalter, Farbtupfer, eingerückte Kinder. */
|
||||
|
||||
/* Dark ist der feste Standard (Nutzer-Wunsch, unabhängig von OS/Browser-
|
||||
Präferenz — vorher hing das an `prefers-color-scheme` und zeigte je nach
|
||||
Umgebung fälschlich Light). Light bleibt als Opt-in erhalten (`[data-theme=
|
||||
"light"]`) für ein späteres manuelles Umschalt-Feature. */
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
/* Gestufte Elevations-Palette (Wunsch: dunkler + mehr Tiefe). Tiefster
|
||||
Grund hinten, Flächen werden nach vorn schrittweise heller — leicht
|
||||
kühl-neutral, harmoniert mit dem Petrol-Akzent.
|
||||
--bg Ebene 0 — App-Grund / Rinnen hinter allem (am dunkelsten)
|
||||
--panel Ebene 1 — Oberleiste, Docks, Panel-Körper
|
||||
--panel-2 Ebene 2 — angehobene Flächen: Panel-Köpfe, Hover-Zeilen
|
||||
--input versenkt — Eingabefelder (dunkler als die Fläche → Inset) */
|
||||
/* Zwei-Ton-Grundlage (Nutzer-Vorgabe): Fast-Schwarz hinten, dunkles
|
||||
Neutralgrau für die Flächen. Neutral (kein Blaustich), mit dezenten
|
||||
Elevations-Schritten für Tiefe. */
|
||||
--bg: #0e0e0e;
|
||||
--panel: #1d1d1d;
|
||||
--panel-2: #262626;
|
||||
--input: #0a0a0a;
|
||||
--line: #2b2b2b;
|
||||
--border: #383838;
|
||||
|
||||
/* --sheet bleibt bewusst hell (#f0f0f0): das Zeichenblatt ist immer
|
||||
„Papier", unabhängig vom UI-Theme (Wunsch des Nutzers). */
|
||||
--sheet: #f0f0f0;
|
||||
|
||||
--ink: #ededed;
|
||||
--ink-2: #a8a8a8;
|
||||
--muted: #6e6e6e;
|
||||
--label: #c8c8c8;
|
||||
|
||||
/* Akzent NEUTRAL (kein Petrol-Grün, Nutzer-Vorgabe): helles Neutralgrau für
|
||||
aktive/ausgewählte Flächen; weiße Schrift darauf bleibt lesbar. Hover/Dim
|
||||
sind dezente weiße Aufhellungen. */
|
||||
--accent: #4d4d4d;
|
||||
--accent-light: #5e5e5e;
|
||||
--accent-dim: rgba(255, 255, 255, 0.07);
|
||||
--accent-border: rgba(255, 255, 255, 0.22);
|
||||
--active: #6a6a6a;
|
||||
--active-light: #7d7d7d;
|
||||
--active-dim: rgba(255, 255, 255, 0.1);
|
||||
|
||||
--wall: #c8ccd2;
|
||||
--swing: #7e8794;
|
||||
--danger: #c85050;
|
||||
--danger-light: #d86060;
|
||||
/* Tiefere Schatten für klare Schichtung auf dunklem Grund. */
|
||||
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
--shadow-2: 0 3px 12px rgba(0, 0, 0, 0.55);
|
||||
--shadow-3: 0 12px 34px rgba(0, 0, 0, 0.62);
|
||||
--overlay: rgba(0, 0, 0, 0.62);
|
||||
|
||||
--font: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
|
||||
sans-serif;
|
||||
--font-mono: "DM Mono", ui-monospace, "SF Mono", monospace;
|
||||
}
|
||||
|
||||
/* Light-Palette als Opt-in (aktuell von nichts gesetzt — bereit für einen
|
||||
künftigen manuellen Theme-Umschalter in den Einstellungen). */
|
||||
:root[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
|
||||
/* Flächen */
|
||||
--bg: #e0dbd4;
|
||||
--panel: #e0dbd4;
|
||||
--panel-2: #d4cfc8;
|
||||
@@ -14,17 +73,13 @@
|
||||
--line: #d4cfc8;
|
||||
--border: #c8c2ba;
|
||||
|
||||
/* Zeichenblatt-Hintergrund (Plan) — bewusst fix, unabhängig von der Panel-
|
||||
Farbe: helles „Papier" im Light-Mode. */
|
||||
--sheet: #f0f0f0;
|
||||
|
||||
/* Schrift */
|
||||
--ink: #1a1a18;
|
||||
--ink-2: #555550;
|
||||
--muted: #8a8580;
|
||||
--label: #1a1a18;
|
||||
|
||||
/* Petrol-Akzent + aktiver Marker */
|
||||
--accent: #2f5d54;
|
||||
--accent-light: #4a8a7c;
|
||||
--accent-dim: #e6efed;
|
||||
@@ -33,11 +88,9 @@
|
||||
--active-light: #2f8275;
|
||||
--active-dim: rgba(26, 101, 90, 0.12);
|
||||
|
||||
/* Plan-/3D-Töne */
|
||||
--wall: #2b3039;
|
||||
--swing: #9aa3b2;
|
||||
|
||||
/* Warn-/Gefahr-Akzente (z. B. Löschen). */
|
||||
--danger: #8a1a1a;
|
||||
--danger-light: #b03030;
|
||||
|
||||
@@ -45,61 +98,6 @@
|
||||
--shadow-2: 0 2px 8px rgba(26, 26, 24, 0.14);
|
||||
--shadow-3: 0 6px 24px rgba(26, 26, 24, 0.18);
|
||||
--overlay: rgba(26, 26, 24, 0.36);
|
||||
|
||||
--font: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
|
||||
sans-serif;
|
||||
--font-mono: "DM Mono", ui-monospace, "SF Mono", monospace;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
/* Gestufte Elevations-Palette (Wunsch: dunkler + mehr Tiefe). Tiefster
|
||||
Grund hinten, Flächen werden nach vorn schrittweise heller — leicht
|
||||
kühl-neutral, harmoniert mit dem Petrol-Akzent.
|
||||
--bg Ebene 0 — App-Grund / Rinnen hinter allem (am dunkelsten)
|
||||
--panel Ebene 1 — Oberleiste, Docks, Panel-Körper
|
||||
--panel-2 Ebene 2 — angehobene Flächen: Panel-Köpfe, Hover-Zeilen
|
||||
--input versenkt — Eingabefelder (dunkler als die Fläche → Inset) */
|
||||
/* Zwei-Ton-Grundlage (Nutzer-Vorgabe): Fast-Schwarz hinten, dunkles
|
||||
Neutralgrau für die Flächen. Neutral (kein Blaustich), mit dezenten
|
||||
Elevations-Schritten für Tiefe. */
|
||||
--bg: #0e0e0e;
|
||||
--panel: #1d1d1d;
|
||||
--panel-2: #262626;
|
||||
--input: #0a0a0a;
|
||||
--line: #2b2b2b;
|
||||
--border: #383838;
|
||||
|
||||
/* --sheet bleibt bewusst hell (#f0f0f0) wie im Light-Mode: das Zeichenblatt
|
||||
ist immer „Papier", unabhängig vom UI-Theme (Wunsch des Nutzers). */
|
||||
|
||||
--ink: #ededed;
|
||||
--ink-2: #a8a8a8;
|
||||
--muted: #6e6e6e;
|
||||
--label: #c8c8c8;
|
||||
|
||||
/* Akzent NEUTRAL (kein Petrol-Grün mehr, Nutzer-Vorgabe): helles Neutralgrau
|
||||
für aktive/ausgewählte Flächen; weiße Schrift darauf bleibt lesbar. Hover/
|
||||
Dim sind dezente weiße Aufhellungen. */
|
||||
--accent: #4d4d4d;
|
||||
--accent-light: #5e5e5e;
|
||||
--accent-dim: rgba(255, 255, 255, 0.07);
|
||||
--accent-border: rgba(255, 255, 255, 0.22);
|
||||
--active: #6a6a6a;
|
||||
--active-light: #7d7d7d;
|
||||
--active-dim: rgba(255, 255, 255, 0.1);
|
||||
|
||||
--wall: #c8ccd2;
|
||||
--swing: #7e8794;
|
||||
--danger: #c85050;
|
||||
--danger-light: #d86060;
|
||||
/* Tiefere Schatten für klare Schichtung auf dunklem Grund. */
|
||||
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
--shadow-2: 0 3px 12px rgba(0, 0, 0, 0.55);
|
||||
--shadow-3: 0 12px 34px rgba(0, 0, 0, 0.62);
|
||||
--overlay: rgba(0, 0, 0, 0.62);
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -155,7 +153,7 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 14px;
|
||||
padding: 4px 14px;
|
||||
height: 56px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
/* Oberleiste auf Ebene 1 (angehoben über dem App-Grund) + weicher Schatten
|
||||
@@ -254,15 +252,17 @@ body {
|
||||
.tb-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--input);
|
||||
color: var(--ink);
|
||||
/* Gleiche stets-dunkle Fläche wie .tb-dd-trigger — einheitlicher Pillen-
|
||||
Look über die ganze Oberleiste. */
|
||||
border: 1px solid #4a4a4a;
|
||||
background: #2c2c2c;
|
||||
color: #e8e8e8;
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: border-color 0.14s, background-color 0.14s, color 0.14s,
|
||||
@@ -270,7 +270,7 @@ body {
|
||||
}
|
||||
.tb-btn:hover:not(:disabled) {
|
||||
border-color: var(--accent-border);
|
||||
background-color: var(--accent-dim);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: var(--accent);
|
||||
}
|
||||
.tb-btn:focus-visible {
|
||||
@@ -278,7 +278,7 @@ body {
|
||||
box-shadow: 0 0 0 2px var(--accent-dim);
|
||||
}
|
||||
.tb-btn:disabled {
|
||||
color: var(--muted);
|
||||
color: #8a8a8a;
|
||||
opacity: 0.55;
|
||||
cursor: default;
|
||||
}
|
||||
@@ -320,20 +320,20 @@ body {
|
||||
|
||||
/* Pillen-Toggle der Oberleiste (Referenzlinien). */
|
||||
.tb-toggle {
|
||||
border: 1px solid var(--border);
|
||||
background: var(--input);
|
||||
color: var(--ink-2);
|
||||
border: 1px solid #4a4a4a;
|
||||
background: #2c2c2c;
|
||||
color: #e8e8e8;
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.14s, color 0.14s, border-color 0.14s;
|
||||
}
|
||||
.tb-toggle:hover {
|
||||
background: var(--accent-dim);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: var(--accent-border);
|
||||
color: var(--accent);
|
||||
}
|
||||
@@ -346,16 +346,17 @@ body {
|
||||
/* ── DOSSIER-Grundprimitive: Segmentpille · Icon-Knopf · Zoom-Cluster ──────── */
|
||||
|
||||
/* Segmentpille (3er/4er): Aussencontainer rund, Zellen gleich hoch, interne
|
||||
Trenner über border-left. Genutzt für Zoom-Aktionen, B/I/U, L/C/R. */
|
||||
Trenner über border-left. Genutzt für Zoom-Aktionen, B/I/U, L/C/R. Gleiche
|
||||
stets-dunkle Fläche wie .tb-dd-trigger/.tb-btn (einheitlicher Pillen-Look). */
|
||||
.tb-seg {
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
height: 24px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--input);
|
||||
background: #2c2c2c;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
/* Akzent-Rand, wenn ein Text-Ziel selektiert ist (Auswahl-Bewusstsein). */
|
||||
@@ -370,9 +371,9 @@ body {
|
||||
min-width: 26px;
|
||||
padding: 0 6px;
|
||||
border: none;
|
||||
border-left: 1px solid var(--border);
|
||||
border-left: 1px solid #4a4a4a;
|
||||
background: transparent;
|
||||
color: var(--ink-2);
|
||||
color: #b0b0b0;
|
||||
cursor: pointer;
|
||||
transition: background 0.14s, color 0.14s;
|
||||
}
|
||||
@@ -380,8 +381,8 @@ body {
|
||||
border-left: none;
|
||||
}
|
||||
.tb-seg-cell:hover:not(:disabled) {
|
||||
background: var(--accent-dim);
|
||||
color: var(--accent);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #f0f0f0;
|
||||
}
|
||||
.tb-seg-cell.active {
|
||||
background: var(--accent);
|
||||
@@ -392,7 +393,7 @@ body {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Quadratischer Icon-Knopf (BarButton) — Export-Pillen (PDF/DXF). */
|
||||
/* Quadratischer Icon-Knopf (BarButton). */
|
||||
.tb-iconbtn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -401,15 +402,15 @@ body {
|
||||
height: 24px;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
background: var(--input);
|
||||
color: var(--ink-2);
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 8px;
|
||||
background: #2c2c2c;
|
||||
color: #b0b0b0;
|
||||
cursor: pointer;
|
||||
transition: background 0.14s, color 0.14s, border-color 0.14s;
|
||||
}
|
||||
.tb-iconbtn:hover:not(:disabled) {
|
||||
background: var(--accent-dim);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: var(--accent-border);
|
||||
color: var(--accent);
|
||||
}
|
||||
@@ -422,7 +423,7 @@ body {
|
||||
.tb-zoomcluster {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
gap: 4px 6px;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
.tb-stat {
|
||||
@@ -437,6 +438,10 @@ body {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
background: var(--input);
|
||||
/* Ring (Box-Shadow statt Border) für „am Massstab" — startet unsichtbar
|
||||
(Alpha 0) und blendet weich ein/aus, statt die Fläche aufzuhellen. */
|
||||
box-shadow: 0 0 0 1.5px rgba(255, 255, 255, 0);
|
||||
transition: box-shadow 0.25s ease;
|
||||
}
|
||||
.tb-stat-scale {
|
||||
font-family: var(--font-mono);
|
||||
@@ -456,20 +461,16 @@ body {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
/* „am Massstab" (Live == gewählter Massstab): Akzent-Hervorhebung. */
|
||||
/* „am Massstab" (Live == gewählter Massstab): weisser Ring statt aufgehellter
|
||||
Fläche — blendet dynamisch ein/aus (s. transition oben), Fläche bleibt
|
||||
unverändert. */
|
||||
.tb-stat-atscale {
|
||||
background: var(--accent-dim);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.tb-stat-atscale .tb-stat-scale {
|
||||
border-bottom-color: var(--accent-border);
|
||||
}
|
||||
.tb-stat-atscale .tb-stat-zoom {
|
||||
color: var(--accent);
|
||||
box-shadow: 0 0 0 1.5px rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
.tb-zoomcluster-right {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
@@ -477,7 +478,7 @@ body {
|
||||
.tb-textgroup {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
}
|
||||
.tb-textgroup-row {
|
||||
display: inline-flex;
|
||||
@@ -517,27 +518,71 @@ body {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.brand {
|
||||
/* Marken-Gruppe: Wortmarke + kleiner Icon-Stapel daneben (Settings/Ressourcen),
|
||||
wie im DOSSIER-Rhino-Plugin (ToolbarApp.jsx: Logo + vertikal gestapelte
|
||||
Settings-Icons direkt daneben, vor dem ersten Trenner). */
|
||||
.tb-brandgroup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 650;
|
||||
font-size: 16px;
|
||||
letter-spacing: 0.2px;
|
||||
gap: 6px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
.brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 3px;
|
||||
line-height: 1;
|
||||
}
|
||||
.brand-word {
|
||||
font-weight: 800;
|
||||
font-size: 15px;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--ink);
|
||||
}
|
||||
.brand-dot {
|
||||
color: var(--accent);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
padding: 2px 8px;
|
||||
padding: 1px 7px;
|
||||
}
|
||||
|
||||
/* Icon-Stapel neben der Wortmarke (Settings/Ressourcen) — schmal, dezent. */
|
||||
.tb-brandicons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.tb-brandicon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 17px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
transition: background 0.14s, color 0.14s;
|
||||
}
|
||||
.tb-brandicon .tb-ico {
|
||||
font-size: 13px;
|
||||
}
|
||||
.tb-brandicon:hover {
|
||||
background: var(--accent-dim);
|
||||
color: var(--ink);
|
||||
}
|
||||
.tb-brandicon.active {
|
||||
background: var(--accent-dim);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.project-name {
|
||||
@@ -625,9 +670,9 @@ body {
|
||||
grid-template-columns: repeat(4, auto);
|
||||
}
|
||||
.view-icon {
|
||||
border: 1px solid var(--border);
|
||||
background: var(--input);
|
||||
color: var(--ink-2);
|
||||
border: 1px solid #4a4a4a;
|
||||
background: #2c2c2c;
|
||||
color: #b0b0b0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -639,8 +684,8 @@ body {
|
||||
transition: background 0.14s, color 0.14s, border-color 0.14s;
|
||||
}
|
||||
.view-icon:hover:not(:disabled) {
|
||||
background: var(--accent-dim);
|
||||
color: var(--ink);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #f0f0f0;
|
||||
border-color: var(--accent-border);
|
||||
}
|
||||
.view-icon.active {
|
||||
@@ -681,7 +726,7 @@ body {
|
||||
.tb-group.tb-stack {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
}
|
||||
.tb-field-row {
|
||||
justify-content: space-between;
|
||||
@@ -2852,10 +2897,18 @@ body {
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
flex: 0 0 auto;
|
||||
padding: 9px 12px;
|
||||
/* Feste Höhe statt nur Padding: Panels OHNE Darstellungsmodus-Dropdown
|
||||
(nur Titel) hatten sonst einen niedrigeren Kopf als Panels MIT Dropdown
|
||||
— beim Wechseln/Stapeln "sprang" die Kopfzeile sichtbar. box-sizing
|
||||
zieht das Padding von der festen Höhe ab, align-items:center zentriert
|
||||
Titel bzw. Titel+Dropdown gleichermassen. */
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
/* Panel-Kopf leicht angehoben (Ebene 2) → setzt sich vom Körper ab. */
|
||||
background: var(--panel-2);
|
||||
/* Uniform mit dem Panel-Körper (Nutzer-Wunsch): kein separater hellerer
|
||||
Kopf-Balken mehr, nur die Trennlinie grenzt Kopf/Körper ab. */
|
||||
background: var(--panel);
|
||||
}
|
||||
.panel-title {
|
||||
min-width: 0;
|
||||
@@ -2863,7 +2916,7 @@ body {
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
color: #f0f0f0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -2876,38 +2929,15 @@ body {
|
||||
padding: 10px 8px 16px;
|
||||
}
|
||||
|
||||
/* ── Darstellungsmodus-Dropdown (kompaktes Pill, vgl. .res-select) ────────── */
|
||||
/* ── Darstellungsmodus-Dropdown (Panel-Kopf) ──────────────────────────────
|
||||
Nutzt jetzt die gemeinsame Dropdown-Komponente (.tb-dd-trigger) für den
|
||||
dunklen, abgerundeten Look; hier nur noch die Breitenbegrenzung + kompakte
|
||||
Panel-Kopf-Typografie, die dem generischen Trigger fehlen. */
|
||||
.display-mode {
|
||||
flex: 0 0 auto;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
max-width: 160px;
|
||||
font-family: var(--font);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--ink-2);
|
||||
background-color: var(--input);
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M0 0l5 6 5-6z' fill='%23888'/></svg>");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
padding: 4px 22px 4px 10px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
text-overflow: ellipsis;
|
||||
transition: border-color 0.14s, background-color 0.14s, color 0.14s,
|
||||
box-shadow 0.14s;
|
||||
}
|
||||
.display-mode:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
background-color: var(--panel);
|
||||
}
|
||||
.display-mode:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px var(--accent-dim);
|
||||
}
|
||||
|
||||
/* ── Layout-Menü (Oberleiste: benannte Layouts sichern/laden) ─────────────── */
|
||||
@@ -2952,14 +2982,18 @@ body {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--input);
|
||||
color: var(--ink);
|
||||
/* Trigger trägt dieselbe stets-dunkle Fläche wie sein Popover (.ctx-menu) —
|
||||
beide sollen wie EIN durchgehendes Element wirken statt heller Knopf +
|
||||
dunkles Menü darunter. .ctx-menu hängt per Portal ausserhalb dieses
|
||||
DOM-Zweigs, darum hier dieselben Werte dupliziert statt geerbt. */
|
||||
border: 1px solid #4a4a4a;
|
||||
background: #2c2c2c;
|
||||
color: #e8e8e8;
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 3px 8px 3px 8px;
|
||||
border-radius: 999px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: border-color 0.14s, background-color 0.14s, color 0.14s,
|
||||
@@ -2967,7 +3001,7 @@ body {
|
||||
}
|
||||
.tb-dd-trigger:hover:not(:disabled) {
|
||||
border-color: var(--accent-border);
|
||||
background-color: var(--accent-dim);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: var(--accent);
|
||||
}
|
||||
.tb-dd-trigger.active,
|
||||
@@ -2976,7 +3010,7 @@ body {
|
||||
box-shadow: 0 0 0 2px var(--accent-dim);
|
||||
}
|
||||
.tb-dd-trigger:disabled {
|
||||
color: var(--muted);
|
||||
color: #8a8a8a;
|
||||
opacity: 0.55;
|
||||
cursor: default;
|
||||
}
|
||||
@@ -2989,7 +3023,7 @@ body {
|
||||
}
|
||||
.tb-dd-chevron {
|
||||
flex: 0 0 auto;
|
||||
color: var(--muted);
|
||||
color: #8a8a8a;
|
||||
}
|
||||
.tb-dd-trigger:hover:not(:disabled) .tb-dd-chevron {
|
||||
color: var(--accent);
|
||||
@@ -4212,6 +4246,59 @@ body {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ── Einstellungs-Fenster (SettingsDialog): Farbfelder + Akzent-Presets ────
|
||||
Nutzt sonst dieselben imp-*-Klassen wie die anderen Dialoge. */
|
||||
.settings-field-label {
|
||||
font-size: 12px;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
.settings-color-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
}
|
||||
.settings-color-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
.settings-accent-presets {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.settings-accent-btn {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--line);
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s, border-color 0.12s;
|
||||
}
|
||||
.settings-accent-btn:hover {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
.settings-accent-btn.active {
|
||||
border: 2px solid var(--accent);
|
||||
box-shadow: 0 0 0 2px var(--accent-dim);
|
||||
}
|
||||
.settings-num-input {
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
padding: 6px 9px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
background: var(--input);
|
||||
color: var(--ink);
|
||||
width: 120px;
|
||||
}
|
||||
.settings-num-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-border);
|
||||
}
|
||||
|
||||
/* ── App-weites Drop-Feedback ──────────────────────────────────────────────
|
||||
Dezenter Rahmen + Hinweis, solange eine Datei über die App gezogen wird. */
|
||||
.drop-feedback {
|
||||
@@ -5122,3 +5209,53 @@ body {
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* ── Fenster-Drag-Region (randloses Electron-Fenster) ────────────────────────
|
||||
Ohne native Titelleiste (frame:false) muss die Oberleiste als Drag-Handle
|
||||
dienen; alle interaktiven Elemente darin bekommen `no-drag` zurück, sonst
|
||||
liesse sich nichts mehr anklicken. Wirkt nur unter Electron/Chromium
|
||||
(WebKit/Firefox ignorieren `-webkit-app-region` ergebnislos, harmlos). */
|
||||
.topbar {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
.topbar button,
|
||||
.topbar select,
|
||||
.topbar input,
|
||||
.topbar a,
|
||||
.topbar [role="button"],
|
||||
.topbar [role="listbox"] {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* ── Custom Fenstersteuerung (WindowControls.tsx) — eigener Look statt OS-
|
||||
Fensterknöpfen, ganz rechts in der Oberleiste (nur sichtbar unter
|
||||
Electron/randlosem Fenster). */
|
||||
.win-controls {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
margin-left: 6px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.win-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.14s, color 0.14s;
|
||||
}
|
||||
.win-btn:hover {
|
||||
background: var(--accent-dim);
|
||||
color: var(--ink);
|
||||
}
|
||||
.win-btn-close:hover {
|
||||
background: #e5484d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// Accent-Farbpalette (Schnellauswahl) für einstellbare UI-Akzente wie den
|
||||
// Auswahlrahmen (Marquee) oder die Snap-/Endpunkt-Hervorhebung im Grundriss.
|
||||
// Zehn benannte Töne (japanische Wörter, wie im HANDOVER-Backlog vorgesehen);
|
||||
// jede Einstellung bleibt trotzdem ein FREIER Farbpicker — diese Liste liefert
|
||||
// nur die Schnellauswahl-Buttons daneben.
|
||||
//
|
||||
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
|
||||
|
||||
/** Ein Akzent-Farbton der Schnellauswahl-Palette. */
|
||||
export interface AccentSwatch {
|
||||
/** Stabiler Schlüssel (React-key, Vergleich). */
|
||||
id: string;
|
||||
/** Anzeigename (japanisch, wie im Backlog benannt). */
|
||||
name: string;
|
||||
/** Hex-Farbwert. */
|
||||
hex: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Die zehn benannten Akzenttöne. Reihenfolge wie im HANDOVER-Backlog genannt.
|
||||
* Nur `yuyake` hat einen vom Nutzer vorgegebenen Wert (#CEB188); die übrigen
|
||||
* sind plausible, klar unterscheidbare Töne passend zum Namen — bei Bedarf
|
||||
* später anpassbar, ohne dass Aufrufer sich ändern (Verweis über `id`).
|
||||
*/
|
||||
export const ACCENT_SWATCHES: AccentSwatch[] = [
|
||||
{ id: "ajisai", name: "Ajisai", hex: "#7C86C4" }, // 紫陽花 Hortensie — Blau-Violett
|
||||
{ id: "sakura", name: "Sakura", hex: "#E3A8B8" }, // 桜 Kirschblüte — zartes Rosa
|
||||
{ id: "suna", name: "Suna", hex: "#CBB994" }, // 砂 Sand — helles Beige
|
||||
{ id: "ichigo", name: "Ichigo", hex: "#C2485C" }, // 苺 Erdbeere — Rot
|
||||
{ id: "yuyake", name: "Yuyake", hex: "#CEB188" }, // 夕焼け Abendrot — warmes Gold-Beige
|
||||
{ id: "sora", name: "Sora", hex: "#5FA1C9" }, // 空 Himmel — kühles Blau
|
||||
{ id: "kusa", name: "Kusa", hex: "#7FA65A" }, // 草 Gras — Grün
|
||||
{ id: "kori", name: "Kori", hex: "#A8D4D8" }, // 氷 Eis — blasses Zyan
|
||||
{ id: "amagumo", name: "Amagumo", hex: "#6E7C8C" }, // 雨雲 Regenwolke — Blaugrau
|
||||
{ id: "yuki", name: "Yuki", hex: "#E7E5DE" }, // 雪 Schnee — nahezu Weiss
|
||||
];
|
||||
|
||||
/** Auswahlrahmen-Default (Marquee, 2D-Planansicht): Akzent „Yuyake". */
|
||||
export const DEFAULT_MARQUEE_COLOR = "#CEB188";
|
||||
|
||||
/**
|
||||
* Snap-/Endpunkt-Default: Akzent „Sora" (vorläufig — die offene Rückfrage
|
||||
* „aki" im HANDOVER-Backlog ist noch nicht geklärt). Bewusst ein kühler,
|
||||
* heller Ton: hebt sich klar vom warmen Yuyake-Auswahlrahmen UND von den
|
||||
* meist neutral-dunklen Wand-/Linienfarben ab, bleibt aber (anders als das
|
||||
* sehr blasse Kori) auch auf hellem Papier-Hintergrund gut sichtbar.
|
||||
*/
|
||||
export const DEFAULT_SNAP_COLOR = "#5FA1C9";
|
||||
|
||||
/** Liefert einen Swatch nach `id`, oder `undefined`. */
|
||||
export function findAccentSwatch(id: string): AccentSwatch | undefined {
|
||||
return ACCENT_SWATCHES.find((s) => s.id === id);
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// Einstellungs-Fenster (modal). HANDOVER-Backlog Punkt 4: Default-Farbe des
|
||||
// Auswahlrahmens (Marquee) + der Snap-/Endpunkt-Hervorhebung im Grundriss,
|
||||
// beide mit freiem Farbpicker + Akzent-Schnellauswahl; dazu ein Projekt-Feld
|
||||
// für die Referenzhöhe des Erdgeschosses (m ü. M.).
|
||||
//
|
||||
// Rein gesteuert: Marquee-/Snap-Farbe kommen aus der ViewSlice (App reicht
|
||||
// Wert + Setter durch), das MüM-Feld schreibt direkt ins Projekt über die
|
||||
// bestehende `setProject`-Aktion. Muster wie ExportDxfDialog (abgedunkeltes
|
||||
// Overlay, Klick auf Hintergrund + Esc schliessen).
|
||||
// Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md).
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { t } from "../i18n";
|
||||
import { ACCENT_SWATCHES } from "../theme/accents";
|
||||
import type { Project } from "../model/types";
|
||||
|
||||
export interface SettingsDialogProps {
|
||||
marqueeColor: string;
|
||||
onMarqueeColorChange: (hex: string) => void;
|
||||
snapColor: string;
|
||||
onSnapColorChange: (hex: string) => void;
|
||||
project: Project;
|
||||
setProject: (next: Project | ((p: Project) => Project)) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function SettingsDialog({
|
||||
marqueeColor,
|
||||
onMarqueeColorChange,
|
||||
snapColor,
|
||||
onSnapColorChange,
|
||||
project,
|
||||
setProject,
|
||||
onClose,
|
||||
}: SettingsDialogProps) {
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [onClose]);
|
||||
|
||||
// Lokaler Textzustand fürs MüM-Feld: erlaubt Zwischenzustände beim Tippen
|
||||
// (leer, "412." etc.), ohne bei jedem Tastendruck einen ungültigen/
|
||||
// gerundeten Projektwert zu schreiben. Synchronisiert sich, wenn sich das
|
||||
// Projekt von aussen ändert (z. B. Undo/Import).
|
||||
const [elevationText, setElevationText] = useState(
|
||||
project.referenceElevationMasl != null ? String(project.referenceElevationMasl) : "",
|
||||
);
|
||||
useEffect(() => {
|
||||
setElevationText(
|
||||
project.referenceElevationMasl != null ? String(project.referenceElevationMasl) : "",
|
||||
);
|
||||
}, [project.referenceElevationMasl]);
|
||||
|
||||
const commitElevation = (raw: string) => {
|
||||
setElevationText(raw);
|
||||
const trimmed = raw.trim();
|
||||
if (trimmed === "") {
|
||||
setProject((p) => ({ ...p, referenceElevationMasl: undefined }));
|
||||
return;
|
||||
}
|
||||
const n = Number(trimmed);
|
||||
if (Number.isFinite(n)) {
|
||||
setProject((p) => ({ ...p, referenceElevationMasl: n }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="imp-overlay" onClick={onClose}>
|
||||
<div
|
||||
className="imp-dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={t("settings.title")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<header className="imp-head">
|
||||
<span className="imp-head-title">{t("settings.title")}</span>
|
||||
<button className="imp-close" onClick={onClose} aria-label={t("export.close")}>
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="imp-body">
|
||||
<section className="imp-section">
|
||||
<div className="imp-section-title">{t("settings.section.display")}</div>
|
||||
<AccentColorField
|
||||
label={t("settings.marqueeColor")}
|
||||
hint={t("settings.marqueeColor.hint")}
|
||||
value={marqueeColor}
|
||||
onChange={onMarqueeColorChange}
|
||||
/>
|
||||
<AccentColorField
|
||||
label={t("settings.snapColor")}
|
||||
hint={t("settings.snapColor.hint")}
|
||||
value={snapColor}
|
||||
onChange={onSnapColorChange}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="imp-section">
|
||||
<div className="imp-section-title">{t("settings.section.project")}</div>
|
||||
<label className="settings-field-label" htmlFor="settings-reference-elevation">
|
||||
{t("settings.referenceElevation")}
|
||||
</label>
|
||||
<input
|
||||
id="settings-reference-elevation"
|
||||
className="settings-num-input"
|
||||
type="number"
|
||||
step="0.1"
|
||||
inputMode="decimal"
|
||||
value={elevationText}
|
||||
onChange={(e) => commitElevation(e.target.value)}
|
||||
placeholder="0.0"
|
||||
/>
|
||||
<div className="imp-note">{t("settings.referenceElevation.hint")}</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer className="imp-foot">
|
||||
<button className="imp-btn primary" onClick={onClose}>
|
||||
{t("export.close")}
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Farbfeld: freier Picker (Swatch + Hex) + Akzent-Presets als Schnellauswahl. */
|
||||
function AccentColorField({
|
||||
label,
|
||||
hint,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
label: string;
|
||||
hint: string;
|
||||
value: string;
|
||||
onChange: (hex: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="settings-color-field">
|
||||
<div className="settings-color-head">
|
||||
<span className="settings-field-label">{label}</span>
|
||||
<label className="attr-color">
|
||||
<span className="attr-color-swatch" style={{ background: value }} />
|
||||
<input type="color" value={value} onChange={(e) => onChange(e.target.value)} />
|
||||
<span className="attr-color-hex">{value.toUpperCase()}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="settings-accent-presets" role="group" aria-label={label}>
|
||||
{ACCENT_SWATCHES.map((a) => (
|
||||
<button
|
||||
key={a.id}
|
||||
type="button"
|
||||
className={`settings-accent-btn${
|
||||
value.toLowerCase() === a.hex.toLowerCase() ? " active" : ""
|
||||
}`}
|
||||
style={{ background: a.hex }}
|
||||
title={a.name}
|
||||
aria-label={a.name}
|
||||
aria-pressed={value.toLowerCase() === a.hex.toLowerCase()}
|
||||
onClick={() => onChange(a.hex)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="imp-note">{hint}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+106
-54
@@ -14,6 +14,7 @@ import { t } from "../i18n";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Dropdown } from "./Dropdown";
|
||||
import type { DropdownItem, DropdownOption } from "./Dropdown";
|
||||
import { WindowControls } from "./WindowControls";
|
||||
import {
|
||||
applyMark,
|
||||
applyPreset,
|
||||
@@ -73,6 +74,12 @@ export type PlanColorMode = "color" | "mono";
|
||||
*/
|
||||
export const SCALE_PRESETS: number[] = [1, 5, 10, 20, 50, 100, 200, 500, 1000];
|
||||
|
||||
/**
|
||||
* Gemeinsame Breite (px) von Massstab-Dropdown und Zoom-Segmentpille im
|
||||
* Zoom-Cluster — beide sollen exakt gleich breit sein (Nutzer-Wunsch).
|
||||
*/
|
||||
const ZOOM_CLUSTER_WIDTH = 140;
|
||||
|
||||
export interface TopBarProps {
|
||||
project: Project;
|
||||
|
||||
@@ -143,9 +150,20 @@ export interface TopBarProps {
|
||||
resourcesOpen: boolean;
|
||||
onToggleResources: () => void;
|
||||
|
||||
/** Öffnet das Einstellungs-Fenster (Darstellungsfarben, Projekt-Felder). */
|
||||
onOpenSettings: () => void;
|
||||
|
||||
/** Layout-Menü (von App geliefert, damit dessen Logik dort bleibt). */
|
||||
layoutMenu: ReactNode;
|
||||
|
||||
// ── Datei-Menü (Burger, ganz rechts) ───────────────────────────────────────
|
||||
/** Öffnet den DXF/DWG-Import-Datei-Dialog. */
|
||||
onImport: () => void;
|
||||
/** Projekt als Datei speichern (JSON-Export des gesamten Projekts). */
|
||||
onSaveProject: () => void;
|
||||
/** Projekt aus einer Datei laden. */
|
||||
onOpenProject: () => void;
|
||||
|
||||
// ── Text-Gruppe (Stempel-/Text-Formatierung) ───────────────────────────────
|
||||
/**
|
||||
* Ziel der Text-Formatierung: `null` = nichts Text-artiges selektiert (die
|
||||
@@ -813,6 +831,51 @@ function touchedParagraphs(doc: RichTextDoc, range: TextRange | null): number[]
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Datei-Menü der Oberleiste (Burger, ganz rechts): Speichern/Öffnen des
|
||||
* Projekts sowie Import (DXF/DWG) und Export (PDF/DXF) — vorher eigene
|
||||
* Icon-Pillen im Zoom-Bereich, jetzt gebündelt hinter einem Menü, damit die
|
||||
* Leiste ruhiger bleibt (GTK-Burger-Stil).
|
||||
*/
|
||||
function AppMenu({
|
||||
onImport,
|
||||
onExportPdf,
|
||||
onExportDxf,
|
||||
onSaveProject,
|
||||
onOpenProject,
|
||||
onOpenSettings,
|
||||
exportEnabled,
|
||||
}: {
|
||||
onImport: () => void;
|
||||
onExportPdf: () => void;
|
||||
onExportDxf: () => void;
|
||||
onSaveProject: () => void;
|
||||
onOpenProject: () => void;
|
||||
onOpenSettings: () => void;
|
||||
exportEnabled: boolean;
|
||||
}) {
|
||||
const items: DropdownItem[] = [
|
||||
{ label: t("file.save"), onSelect: onSaveProject },
|
||||
{ label: t("file.open"), onSelect: onOpenProject },
|
||||
{ divider: true },
|
||||
{ label: t("file.import"), onSelect: onImport },
|
||||
{ divider: true },
|
||||
{ label: t("file.exportPdf"), onSelect: onExportPdf, disabled: !exportEnabled },
|
||||
{ label: t("file.exportDxf"), onSelect: onExportDxf, disabled: !exportEnabled },
|
||||
{ divider: true },
|
||||
{ label: t("topbar.settings"), onSelect: onOpenSettings },
|
||||
];
|
||||
return (
|
||||
<Dropdown
|
||||
items={items}
|
||||
placeholder=""
|
||||
title={t("file.menu")}
|
||||
triggerClassName="tb-burger"
|
||||
triggerSuffix={<TbIcon name="menu" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TopBar({
|
||||
project,
|
||||
viewToggleEnabled,
|
||||
@@ -843,7 +906,11 @@ export function TopBar({
|
||||
combos,
|
||||
resourcesOpen,
|
||||
onToggleResources,
|
||||
onOpenSettings,
|
||||
layoutMenu,
|
||||
onImport,
|
||||
onSaveProject,
|
||||
onOpenProject,
|
||||
textTarget,
|
||||
onAddText,
|
||||
}: TopBarProps) {
|
||||
@@ -899,10 +966,28 @@ export function TopBar({
|
||||
|
||||
return (
|
||||
<header className="topbar" ref={headerRef}>
|
||||
{/* Marke/Logo. */}
|
||||
<div className="brand">
|
||||
<span className="logo">▦</span> cad{" "}
|
||||
<span className="tag">{t("brand.tag")}</span>
|
||||
{/* Marke (DOSSIER-Wortmarke) + Settings/Ressourcen-Icons daneben,
|
||||
gestapelt wie im Rhino-Plugin (ToolbarApp.jsx: Wortmarke links,
|
||||
rechts daneben ein schmaler Icon-Stapel). */}
|
||||
<div className="tb-brandgroup">
|
||||
<div className="brand">
|
||||
<span className="brand-word">
|
||||
DOSSIER<span className="brand-dot">.</span>
|
||||
</span>
|
||||
<span className="tag">{t("brand.tag")}</span>
|
||||
</div>
|
||||
<div className="tb-brandicons">
|
||||
<button
|
||||
type="button"
|
||||
className={`tb-brandicon${resourcesOpen ? " active" : ""}`}
|
||||
onClick={onToggleResources}
|
||||
aria-pressed={resourcesOpen}
|
||||
aria-label={t("topbar.resources")}
|
||||
title={`${t("topbar.resources")} — ${t("topbar.resources.hint")}`}
|
||||
>
|
||||
<TbIcon name="widgets" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Sep />
|
||||
@@ -1017,9 +1102,9 @@ export function TopBar({
|
||||
{/* Detailgrad (grob/mittel/fein) — wirkt im Grundriss auf Symbole +
|
||||
Schraffuren/Linien. In der Perspektive (noch) ohne Wirkung → dort
|
||||
deaktiviert mit Tooltip statt stiller No-Op. */}
|
||||
{/* Detailgrad + Massstab gestapelt — eine Spalte, platzsparend (wie die
|
||||
Ebenen-/Zeichnungs-Kombis). */}
|
||||
<div className="tb-group tb-stack" role="group" aria-label={t("topbar.detailGroup")}>
|
||||
{/* Detailgrad — Massstab lebt nur noch EINMAL im Zoom-Cluster (die
|
||||
Dropdown hier war ein redundantes Relikt der TopBar-Reorg). */}
|
||||
<div className="tb-group" role="group" aria-label={t("topbar.detailGroup")}>
|
||||
<label className="tb-field tb-field-row">
|
||||
<span className="tb-label">{t("topbar.detail")}</span>
|
||||
<Dropdown
|
||||
@@ -1038,17 +1123,6 @@ export function TopBar({
|
||||
]}
|
||||
/>
|
||||
</label>
|
||||
<label className="tb-field tb-field-row">
|
||||
<span className="tb-label">{t("topbar.scale")}</span>
|
||||
<Dropdown
|
||||
value={inPresets ? String(scaleDenominator) : "__current__"}
|
||||
disabled={!zoomEnabled}
|
||||
onChange={onScaleChange}
|
||||
title={t("topbar.scale.apply")}
|
||||
triggerClassName="tb-dd-mono"
|
||||
options={scaleOptions}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Sep />
|
||||
@@ -1081,11 +1155,12 @@ export function TopBar({
|
||||
onChange={onScaleChange}
|
||||
title={t("topbar.scale.apply")}
|
||||
triggerClassName="tb-dd-mono"
|
||||
width={140}
|
||||
width={ZOOM_CLUSTER_WIDTH}
|
||||
options={scaleOptions}
|
||||
/>
|
||||
<Segment
|
||||
ariaLabel={t("topbar.scaleGroup")}
|
||||
style={{ width: ZOOM_CLUSTER_WIDTH }}
|
||||
cells={[
|
||||
{
|
||||
key: "zoom100",
|
||||
@@ -1113,31 +1188,6 @@ export function TopBar({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Export als eigene kleine Pillen-Reihe (nicht mehr Teil des Zoom-Blocks,
|
||||
damit der Cluster ruhig bleibt). */}
|
||||
<div className="tb-group tb-btns" role="group" aria-label={t("topbar.exportPdf")}>
|
||||
<button
|
||||
type="button"
|
||||
className="tb-iconbtn"
|
||||
onClick={onExportPdf}
|
||||
disabled={!zoomEnabled}
|
||||
title={t("topbar.exportPdf.hint")}
|
||||
aria-label={t("topbar.exportPdf")}
|
||||
>
|
||||
<TbIcon name="picture_as_pdf" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="tb-iconbtn"
|
||||
onClick={onExportDxf}
|
||||
disabled={!zoomEnabled}
|
||||
title={t("topbar.exportDxf.hint")}
|
||||
aria-label={t("topbar.exportDxf")}
|
||||
>
|
||||
<TbIcon name="download" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Sep />
|
||||
|
||||
{/* Darstellungsart (Vectorworks-Stil) — EIN Dropdown, dessen Optionen vom
|
||||
@@ -1177,19 +1227,21 @@ export function TopBar({
|
||||
|
||||
<Sep />
|
||||
|
||||
{/* Rechte Gruppe: Layout-Menü · Ressourcen · Projektname. */}
|
||||
{/* Rechte Gruppe: Layout-Menü · Projektname · Datei-Burger (ganz rechts,
|
||||
GTK-Stil) — Ressourcen sitzt jetzt bei der Marke (s. o.). */}
|
||||
<div className="topbar-right">
|
||||
{layoutMenu}
|
||||
<button
|
||||
className={`view-icon${resourcesOpen ? " active" : ""}`}
|
||||
onClick={onToggleResources}
|
||||
aria-pressed={resourcesOpen}
|
||||
aria-label={t("topbar.resources")}
|
||||
title={`${t("topbar.resources")} — ${t("topbar.resources.hint")}`}
|
||||
>
|
||||
<TbIcon name="widgets" />
|
||||
</button>
|
||||
<span className="project-name">{project.name}</span>
|
||||
<AppMenu
|
||||
onImport={onImport}
|
||||
onExportPdf={onExportPdf}
|
||||
onExportDxf={onExportDxf}
|
||||
onSaveProject={onSaveProject}
|
||||
onOpenProject={onOpenProject}
|
||||
onOpenSettings={onOpenSettings}
|
||||
exportEnabled={zoomEnabled}
|
||||
/>
|
||||
<WindowControls />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// Eigene Fenstersteuerung (Minimieren/Maximieren/Schliessen) für das randlose
|
||||
// Electron-Fenster (frame:false, s. scripts/electron-main.cjs) — ohne native
|
||||
// Titelleiste gibt es sonst keine Möglichkeit, das Fenster zu steuern.
|
||||
//
|
||||
// Rendert NUR, wenn `window.dossierWindow` existiert (vom Preload-Skript
|
||||
// injiziert) — also nur unter Electron. In Tauri (native Fensterdekoration)
|
||||
// und im Browser-Dev-Modus (?vite) bleibt die Komponente unsichtbar (`null`),
|
||||
// dort steuert das Betriebssystem bzw. der Browser das Fenster.
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
/** Von electron-preload.cjs via contextBridge injiziert (nur unter Electron). */
|
||||
interface DossierWindowApi {
|
||||
minimize: () => void;
|
||||
toggleMaximize: () => void;
|
||||
close: () => void;
|
||||
isMaximized: () => Promise<boolean>;
|
||||
onMaximizedChange: (callback: (isMaximized: boolean) => void) => () => void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
dossierWindow?: DossierWindowApi;
|
||||
}
|
||||
}
|
||||
|
||||
export function WindowControls() {
|
||||
const api = window.dossierWindow;
|
||||
const [maximized, setMaximized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!api) return;
|
||||
let cancelled = false;
|
||||
api.isMaximized().then((v) => {
|
||||
if (!cancelled) setMaximized(v);
|
||||
});
|
||||
const unsubscribe = api.onMaximizedChange((v) => setMaximized(v));
|
||||
return () => {
|
||||
cancelled = true;
|
||||
unsubscribe();
|
||||
};
|
||||
}, [api]);
|
||||
|
||||
if (!api) return null;
|
||||
|
||||
return (
|
||||
<div className="win-controls" role="group" aria-label="Fenster">
|
||||
<button
|
||||
type="button"
|
||||
className="win-btn"
|
||||
onClick={() => api.minimize()}
|
||||
title="Minimieren"
|
||||
aria-label="Minimieren"
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
||||
<path d="M1 5 L9 5" stroke="currentColor" strokeWidth="1.2" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="win-btn"
|
||||
onClick={() => api.toggleMaximize()}
|
||||
title={maximized ? "Wiederherstellen" : "Maximieren"}
|
||||
aria-label={maximized ? "Wiederherstellen" : "Maximieren"}
|
||||
>
|
||||
{maximized ? (
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
||||
<path
|
||||
d="M2.5 1.5 H8.5 V7.5 H2.5 Z M2.5 1.5 V3 M1.5 3 H2.5 M1.5 3 V9 H7.5 V7.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
||||
<rect
|
||||
x="1.5"
|
||||
y="1.5"
|
||||
width="7"
|
||||
height="7"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="win-btn win-btn-close"
|
||||
onClick={() => api.close()}
|
||||
title="Schliessen"
|
||||
aria-label="Schliessen"
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
||||
<path
|
||||
d="M1 1 L9 9 M9 1 L1 9"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user