use universal window controls across platforms

This commit is contained in:
talksik
2026-04-15 08:03:32 -07:00
parent 637342cb92
commit 07f02ecfeb
7 changed files with 78 additions and 32 deletions
+59 -28
View File
@@ -1,33 +1,64 @@
import { useEffect, useState } from "react";
import { Copy, Minus, Square, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export function WindowControls() {
const [isMaximized, setIsMaximized] = useState(false);
useEffect(() => {
return window.electronWindow.onMaximizeChange(setIsMaximized);
}, []);
const minimize = (
<Button
key="minimize"
variant="ghost"
size="icon-sm"
onClick={() => window.electronWindow.minimize()}
aria-label="Minimize"
className="dark:hover:bg-white/10 rounded text-white/50"
>
<Minus />
</Button>
);
const maximize = (
<Button
key="maximize"
variant="ghost"
size="icon-sm"
onClick={() => window.electronWindow.maximize()}
aria-label={isMaximized ? "Restore" : "Maximize"}
className="dark:hover:bg-white/10 rounded text-white/50"
>
{isMaximized ? <Copy /> : <Square />}
</Button>
);
const close = (
<Button
key="close"
variant="ghost"
size="icon-sm"
onClick={() => window.electronWindow.close()}
aria-label="Close"
className="hover:text-destructive dark:hover:bg-white/10 rounded text-white/50"
>
<X />
</Button>
);
const buttons = [close, maximize, minimize];
return (
<div className="no-drag flex items-center gap-2">
<button
onClick={() => window.electronWindow.close()}
className="group flex h-3 w-3 items-center justify-center rounded-full bg-[#FF5F57] transition-colors hover:bg-[#FF5F57]/80"
aria-label="Close"
>
<svg className="h-1.5 w-1.5 opacity-0 group-hover:opacity-100" viewBox="0 0 6 6" fill="none">
<path d="M1 1L5 5M5 1L1 5" stroke="rgba(0,0,0,0.5)" strokeWidth="1.2" strokeLinecap="round" />
</svg>
</button>
<button
onClick={() => window.electronWindow.minimize()}
className="group flex h-3 w-3 items-center justify-center rounded-full bg-[#FEBC2E] transition-colors hover:bg-[#FEBC2E]/80"
aria-label="Minimize"
>
<svg className="h-1.5 w-1.5 opacity-0 group-hover:opacity-100" viewBox="0 0 6 6" fill="none">
<path d="M1 3H5" stroke="rgba(0,0,0,0.5)" strokeWidth="1.2" strokeLinecap="round" />
</svg>
</button>
<button
onClick={() => window.electronWindow.fullscreen()}
className="group flex h-3 w-3 items-center justify-center rounded-full bg-[#28C840] transition-colors hover:bg-[#28C840]/80"
aria-label="Maximize"
>
<svg className="h-1.5 w-1.5 opacity-0 group-hover:opacity-100" viewBox="0 0 6 6" fill="none">
<path d="M1 1.5L3 0.5L5 1.5V4.5L3 5.5L1 4.5V1.5Z" stroke="rgba(0,0,0,0.5)" strokeWidth="0.8" />
</svg>
</button>
<div
className={cn(
"no-drag flex items-center gap-0.5"
)}
>
{buttons}
</div>
);
}
+2
View File
@@ -17,6 +17,8 @@ declare global {
close: () => void;
openHuddle: (data: { token: string; serverUrl: string }) => void;
closeHuddle: () => void;
platform: NodeJS.Platform;
onMaximizeChange: (callback: (isMaximized: boolean) => void) => () => void;
};
electronHuddle: {
getScreenSources: () => Promise<ScreenSource[]>;
+2 -2
View File
@@ -92,10 +92,10 @@ function TopBar() {
const { data: particle } = useParticle(path);
return (
<div className="drag-region flex items-center gap-3 border-b px-3 py-1 bg-muted">
<div className="drag-region flex items-center gap-1 border-b bg-muted p-1">
<WindowControls />
<Breadcrumb className="no-drag">
<Breadcrumb className="no-drag px-2">
<BreadcrumbList>
<BreadcrumbItem className="text-xs">
{segments.length === 0 ? (
+1 -1
View File
@@ -77,7 +77,7 @@ export function TopBar({ networkId, particle, streamParticle }: TopBarProps) {
};
return (
<div className="drag-region flex flex-row px-4 gap-1 items-center">
<div className="drag-region flex flex-row px-2 gap-1 items-center">
<WindowControls />
<Breadcrumb className="no-drag rounded-full bg-black/30 backdrop-blur-sm px-3 py-1 mx-auto">
+1 -1
View File
@@ -419,7 +419,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
<div className="pointer-events-none absolute inset-x-0 top-0 z-[5] h-32 bg-gradient-to-b from-black/60 to-transparent" />
{/* TopBar — always visible */}
<div className="z-10 absolute left-0 right-0 mt-3">
<div className="z-10 absolute left-0 right-0 pt-2">
<TopBar networkId={networkId} particle={currentParticle} streamParticle={streamParticle} />
</div>
+7
View File
@@ -124,6 +124,13 @@ const createWindow = () => {
});
hardenWindow(mainWindow);
mainWindow.on('maximize', () => {
mainWindow?.webContents.send('window:maximize-changed', true);
});
mainWindow.on('unmaximize', () => {
mainWindow?.webContents.send('window:maximize-changed', false);
});
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
mainWindow.webContents.openDevTools();
+6
View File
@@ -9,6 +9,12 @@ contextBridge.exposeInMainWorld('electronWindow', {
close: () => ipcRenderer.send('window:close'),
openHuddle: (data: { token: string; serverUrl: string }) => ipcRenderer.send('window:open-huddle', data),
closeHuddle: () => ipcRenderer.send('window:close-huddle'),
platform: process.platform,
onMaximizeChange: (callback: (isMaximized: boolean) => void) => {
const handler = (_event: Electron.IpcRendererEvent, value: boolean) => callback(value);
ipcRenderer.on('window:maximize-changed', handler);
return () => { ipcRenderer.removeListener('window:maximize-changed', handler); };
},
});
contextBridge.exposeInMainWorld('electronHuddle', {