Native macOS-Titelleiste: titleBarStyle Overlay (echte Ampeln + gerundete Ecken), Zeile links eingerückt; WindowControls Electron-only

This commit is contained in:
2026-07-05 20:14:10 +02:00
parent 456bf29984
commit a750e87517
4 changed files with 36 additions and 61 deletions
+2 -1
View File
@@ -15,7 +15,8 @@
"width": 1400, "width": 1400,
"height": 900, "height": 900,
"dragDropEnabled": false, "dragDropEnabled": false,
"decorations": false "titleBarStyle": "Overlay",
"hiddenTitle": true
} }
], ],
"security": { "security": {
+6
View File
@@ -174,6 +174,12 @@ body {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: transparent transparent; scrollbar-color: transparent transparent;
} }
/* Tauri/macOS: native Ampeln (titleBarStyle "Overlay") liegen oben links über
dem Inhalt → Zeile links einrücken, damit `dossier.` frei bleibt (~3 Ampeln +
Rand). */
.topbar.topbar-native-inset {
padding-left: 78px;
}
/* Sehr ruhige eigene Scrollleiste (WebKit/Blink): Thumb nur beim Scrollen /* Sehr ruhige eigene Scrollleiste (WebKit/Blink): Thumb nur beim Scrollen
(`is-scrolling`, von TopBar gesetzt) oder beim Überfahren sichtbar. */ (`is-scrolling`, von TopBar gesetzt) oder beim Überfahren sichtbar. */
.topbar::-webkit-scrollbar { .topbar::-webkit-scrollbar {
+17 -7
View File
@@ -1298,14 +1298,24 @@ export function TopBar({
}; };
}, []); }, []);
// Unter Tauri (native macOS-Ampeln oben links via titleBarStyle "Overlay")
// braucht die Zeile links Platz, damit `dossier.` nicht darunter liegt.
const tauriNativeTitlebar =
typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
return ( return (
// `data-tauri-drag-region`: die randlose (decorations:false) Titelleiste ist // `data-tauri-drag-region`: die Titelleiste ist selbst der Fenster-Ziehbereich
// selbst der Fenster-Ziehbereich. Kind-Buttons/Dropdowns bleiben klickbar; // (macOS titleBarStyle "Overlay"). Kind-Buttons/Dropdowns bleiben klickbar;
// nur die leeren Zonen ziehen das Fenster. // nur die leeren Zonen ziehen das Fenster. Unter Tauri bekommt die Zeile links
<header className="topbar" ref={headerRef} data-tauri-drag-region> // einen Inset, damit `dossier.` nicht unter den nativen macOS-Ampeln liegt.
{/* Fensterknöpfe (Schliessen/Minimieren/Maximieren) GANZ LINKS die <header
randlose Titelleiste beginnt mit ihnen (macOS-Konvention). Nur unter className={`topbar${tauriNativeTitlebar ? " topbar-native-inset" : ""}`}
Electron/Tauri sichtbar (sonst null). */} ref={headerRef}
data-tauri-drag-region
>
{/* Fensterknöpfe NUR unter Electron (randloses frame:false-Fenster) unter
Tauri liefern die NATIVEN macOS-Ampeln Schliessen/Minimieren/Maximieren,
daher rendert WindowControls dort null. */}
<WindowControls /> <WindowControls />
{/* Kleine Wortmarke dossier" (OCS-Stil, klein/kleingeschrieben) + Quick- {/* Kleine Wortmarke dossier" (OCS-Stil, klein/kleingeschrieben) + Quick-
+11 -53
View File
@@ -1,75 +1,33 @@
// Eigene Fenstersteuerung (Minimieren/Maximieren/Schliessen) für das RANDLOSE // Eigene Fenstersteuerung (Minimieren/Maximieren/Schliessen) für das randlose
// Fenster — sowohl Electron (frame:false, s. scripts/electron-main.cjs) ALS AUCH // ELECTRON-Fenster (frame:false, s. scripts/electron-main.cjs) — dort gibt es
// Tauri (decorations:false, s. tauri.conf.json). Ohne native Titelleiste gibt es // ohne native Titelleiste sonst keine Fenstersteuerung.
// sonst keine Möglichkeit, das Fenster zu steuern.
// //
// Rendert NUR, wenn eine Fenster-API vorhanden ist: unter Electron // Unter TAURI werden die NATIVEN macOS-Ampeln verwendet (tauri.conf.json:
// `window.dossierWindow` (Preload), unter Tauri `getCurrentWindow()` // titleBarStyle "Overlay" + hiddenTitle, native Dekoration mit gerundeten Ecken),
// (@tauri-apps/api/window, dynamisch geladen wie in nativeSync.ts). Im reinen // daher rendert diese Komponente dort NICHT (kein `window.dossierWindow`) — sonst
// Browser-Dev-Modus (?vite) bleibt die Komponente unsichtbar (`null`) — dort // gäbe es doppelte Knöpfe. Im Browser-Dev-Modus ebenfalls unsichtbar (`null`).
// 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";
/** Gemeinsame, minimale Fenster-API (Electron-Preload ODER Tauri-Adapter). */ /** Von electron-preload.cjs via contextBridge injiziert (nur unter Electron). */
interface WindowApi { interface DossierWindowApi {
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?: WindowApi; dossierWindow?: DossierWindowApi;
} }
} }
/** 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, setApi] = useState<WindowApi | null>(null); const api = window.dossierWindow;
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;