Headless-Titelleiste (Tauri decorations:false): Fensterknöpfe links + Drag-Region, Tauri-Fenstersteuerung + Window-Capabilities

This commit is contained in:
2026-07-05 19:42:48 +02:00
parent c40764a25f
commit 456bf29984
5 changed files with 103 additions and 33 deletions
+16
View File
@@ -0,0 +1,16 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Basis-Permissions + Fenstersteuerung fuer die randlose (decorations:false) Titelleiste: Minimieren/Maximieren/Schliessen + Ziehen der eigenen Titelleiste (data-tauri-drag-region).",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:allow-minimize",
"core:window:allow-maximize",
"core:window:allow-unmaximize",
"core:window:allow-toggle-maximize",
"core:window:allow-is-maximized",
"core:window:allow-close",
"core:window:allow-start-dragging"
]
}
+3 -1
View File
@@ -10,10 +10,12 @@
"app": { "app": {
"windows": [ "windows": [
{ {
"label": "main",
"title": "cad", "title": "cad",
"width": 1400, "width": 1400,
"height": 900, "height": 900,
"dragDropEnabled": false "dragDropEnabled": false,
"decorations": false
} }
], ],
"security": { "security": {
+2 -1
View File
@@ -5317,7 +5317,8 @@ body {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 2px; gap: 2px;
margin-left: 6px; /* Ganz links in der randlosen Titelleiste (macOS-Konvention). */
margin-right: 4px;
flex: 0 0 auto; flex: 0 0 auto;
} }
.win-btn { .win-btn {
+12 -4
View File
@@ -1299,7 +1299,15 @@ export function TopBar({
}, []); }, []);
return ( return (
<header className="topbar" ref={headerRef}> // `data-tauri-drag-region`: die randlose (decorations:false) Titelleiste ist
// selbst der Fenster-Ziehbereich. Kind-Buttons/Dropdowns bleiben klickbar;
// nur die leeren Zonen ziehen das Fenster.
<header className="topbar" ref={headerRef} data-tauri-drag-region>
{/* Fensterknöpfe (Schliessen/Minimieren/Maximieren) GANZ LINKS die
randlose Titelleiste beginnt mit ihnen (macOS-Konvention). Nur unter
Electron/Tauri sichtbar (sonst null). */}
<WindowControls />
{/* Kleine Wortmarke dossier" (OCS-Stil, klein/kleingeschrieben) + Quick- {/* Kleine Wortmarke dossier" (OCS-Stil, klein/kleingeschrieben) + Quick-
Access-Leiste: alle Datei-/Export-Aktionen + Ressourcen/Einstellungen Access-Leiste: alle Datei-/Export-Aktionen + Ressourcen/Einstellungen
als kleine Icons direkt in der Zeile (ersetzt das Burger-Menü). */} als kleine Icons direkt in der Zeile (ersetzt das Burger-Menü). */}
@@ -1328,12 +1336,12 @@ export function TopBar({
Bänder: diese schmale Leiste (Chrome + Tabs) und den Inhalt. */} Bänder: diese schmale Leiste (Chrome + Tabs) und den Inhalt. */}
<RibbonTabs tab={ribbonTab} onTab={onRibbonTab} /> <RibbonTabs tab={ribbonTab} onTab={onRibbonTab} />
{/* Rechte Gruppe: Layout-Menü · Projektname · Fensterknöpfe. Datei-/Export- {/* Rechte Gruppe: Layout-Menü · Projektname. Die Fensterknöpfe sitzen jetzt
Aktionen liegen jetzt in der Quick-Access-Leiste links (kein Burger). */} ganz links (macOS-Titelleisten-Konvention); Datei-/Export-Aktionen in
der Quick-Access-Leiste (kein Burger). */}
<div className="topbar-right"> <div className="topbar-right">
{layoutMenu} {layoutMenu}
<span className="project-name">{project.name}</span> <span className="project-name">{project.name}</span>
<WindowControls />
</div> </div>
</header> </header>
); );
+70 -27
View File
@@ -1,33 +1,75 @@
// Eigene Fenstersteuerung (Minimieren/Maximieren/Schliessen) für das randlose // Eigene Fenstersteuerung (Minimieren/Maximieren/Schliessen) für das RANDLOSE
// Electron-Fenster (frame:false, s. scripts/electron-main.cjs) — ohne native // Fenster — sowohl Electron (frame:false, s. scripts/electron-main.cjs) ALS AUCH
// Titelleiste gibt es sonst keine Möglichkeit, das Fenster zu steuern. // Tauri (decorations:false, s. tauri.conf.json). Ohne native Titelleiste gibt es
// sonst keine Möglichkeit, das Fenster zu steuern.
// //
// Rendert NUR, wenn `window.dossierWindow` existiert (vom Preload-Skript // Rendert NUR, wenn eine Fenster-API vorhanden ist: unter Electron
// injiziert) — also nur unter Electron. In Tauri (native Fensterdekoration) // `window.dossierWindow` (Preload), unter Tauri `getCurrentWindow()`
// und im Browser-Dev-Modus (?vite) bleibt die Komponente unsichtbar (`null`), // (@tauri-apps/api/window, dynamisch geladen wie in nativeSync.ts). Im reinen
// dort steuert das Betriebssystem bzw. der Browser das Fenster. // Browser-Dev-Modus (?vite) bleibt die Komponente unsichtbar (`null`) — dort
// steuert der Browser das Fenster. Die Tauri-Permissions liegen in
// src-tauri/capabilities/default.json (core:window:allow-minimize/…/close).
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
/** Von electron-preload.cjs via contextBridge injiziert (nur unter Electron). */ /** Gemeinsame, minimale Fenster-API (Electron-Preload ODER Tauri-Adapter). */
interface DossierWindowApi { interface WindowApi {
minimize: () => void; minimize: () => void;
toggleMaximize: () => void; toggleMaximize: () => void;
close: () => void; close: () => void;
isMaximized: () => Promise<boolean>; isMaximized: () => Promise<boolean>;
/** Abo auf Maximiert-Wechsel; liefert eine Abmelde-Funktion. */
onMaximizedChange: (callback: (isMaximized: boolean) => void) => () => void; onMaximizedChange: (callback: (isMaximized: boolean) => void) => () => void;
} }
declare global { declare global {
interface Window { interface Window {
dossierWindow?: DossierWindowApi; dossierWindow?: WindowApi;
} }
} }
/** Tauri-Laufzeit erkannt (analog nativeSync.ts). */
function isTauri(): boolean {
return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
}
export function WindowControls() { export function WindowControls() {
const api = window.dossierWindow; const [api, setApi] = useState<WindowApi | null>(null);
const [maximized, setMaximized] = useState(false); const [maximized, setMaximized] = useState(false);
// Passende Fenster-API einmalig auflösen: Electron synchron, Tauri asynchron
// (dynamischer Import, damit Browser-/Build-Pfad ohne @tauri-apps grün bleibt).
useEffect(() => {
if (window.dossierWindow) {
setApi(window.dossierWindow);
return;
}
if (!isTauri()) return;
let disposed = false;
import(/* @vite-ignore */ "@tauri-apps/api/window")
.then(({ getCurrentWindow }) => {
if (disposed) return;
const w = getCurrentWindow();
setApi({
minimize: () => void w.minimize(),
toggleMaximize: () => void w.toggleMaximize(),
close: () => void w.close(),
isMaximized: () => w.isMaximized(),
onMaximizedChange: (cb) => {
// Bei jeder Größenänderung den Maximiert-Zustand neu melden.
const unlisten = w.onResized(() => {
void w.isMaximized().then(cb);
});
return () => void unlisten.then((fn) => fn());
},
});
})
.catch(() => {});
return () => {
disposed = true;
};
}, []);
useEffect(() => { useEffect(() => {
if (!api) return; if (!api) return;
let cancelled = false; let cancelled = false;
@@ -43,8 +85,25 @@ export function WindowControls() {
if (!api) return null; if (!api) return null;
// Reihenfolge (macOS-Titelleiste links): Schliessen · Minimieren · Maximieren.
return ( return (
<div className="win-controls" role="group" aria-label="Fenster"> <div className="win-controls" role="group" aria-label="Fenster">
<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>
<button <button
type="button" type="button"
className="win-btn" className="win-btn"
@@ -86,22 +145,6 @@ export function WindowControls() {
</svg> </svg>
)} )}
</button> </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> </div>
); );
} }