Files
fusion/packages/desktop/src/renderer/components/TitleBar.tsx
gsxdsm a4a99f31b0 feat(FN-1072): add Electron desktop renderer integration
- Add Electron main-process IPC handlers and preload bridges for API proxying, window controls, update install, and platform lookup
- Introduce a desktop renderer entrypoint with DesktopWrapper and a custom frameless TitleBar component
- Add Electron-aware renderer utilities including API transport selection and hooks for runtime detection, auto-update events, and deep links
- Update dashboard header behavior for Electron mode and expand desktop tests across preload, transport, hooks, and title bar flows
- Document the desktop renderer architecture and update desktop package config/dependencies
2026-04-08 00:16:25 -07:00

126 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useCallback, useEffect, useMemo, useState } from "react";
import "./TitleBar.css";
type WindowControlAction = "minimize" | "maximize" | "close" | "isMaximized";
type Platform = "darwin" | "win32" | "linux";
interface ElectronApiForTitleBar {
windowControl?: (action: WindowControlAction) => Promise<boolean | void>;
getPlatform?: () => Promise<Platform>;
}
function getElectronApi(): ElectronApiForTitleBar | null {
if (typeof window === "undefined") {
return null;
}
const maybeApi = (window as Window & { electronAPI?: ElectronApiForTitleBar }).electronAPI;
return maybeApi ?? null;
}
export function TitleBar() {
const electronApi = getElectronApi();
const [isMaximized, setIsMaximized] = useState(false);
const [platform, setPlatform] = useState<Platform | null>(null);
useEffect(() => {
if (!electronApi?.windowControl) {
return;
}
void electronApi.windowControl("isMaximized").then((value) => {
setIsMaximized(Boolean(value));
}).catch(() => {
setIsMaximized(false);
});
}, [electronApi]);
useEffect(() => {
if (!electronApi?.getPlatform) {
return;
}
void electronApi.getPlatform().then(setPlatform).catch(() => {
setPlatform(null);
});
}, [electronApi]);
const handleWindowControl = useCallback(async (action: Exclude<WindowControlAction, "isMaximized">) => {
if (!electronApi?.windowControl) {
return;
}
const result = await electronApi.windowControl(action);
if (action === "maximize") {
if (typeof result === "boolean") {
setIsMaximized(result);
} else {
setIsMaximized((prev) => !prev);
}
}
}, [electronApi]);
const handleTitleDoubleClick = useCallback(() => {
void handleWindowControl("maximize");
}, [handleWindowControl]);
const maximizeGlyph = useMemo(() => (isMaximized ? "⧉" : "□"), [isMaximized]);
const controlsOnLeft = platform === "darwin";
const controls = (
<div className="desktop-titlebar__controls desktop-titlebar__controls--no-drag">
<button
type="button"
className="desktop-titlebar__control"
onClick={() => void handleWindowControl("minimize")}
aria-label="Minimize window"
title="Minimize"
data-testid="titlebar-minimize"
>
</button>
<button
type="button"
className="desktop-titlebar__control"
onClick={() => void handleWindowControl("maximize")}
aria-label={isMaximized ? "Restore window" : "Maximize window"}
title={isMaximized ? "Restore" : "Maximize"}
data-testid="titlebar-maximize"
>
{maximizeGlyph}
</button>
<button
type="button"
className="desktop-titlebar__control desktop-titlebar__control--close"
onClick={() => void handleWindowControl("close")}
aria-label="Close window"
title="Close"
data-testid="titlebar-close"
>
×
</button>
</div>
);
return (
<header
className="desktop-titlebar desktop-titlebar--drag"
onDoubleClick={handleTitleDoubleClick}
data-testid="desktop-titlebar"
>
{controlsOnLeft ? controls : null}
<div className="desktop-titlebar__brand">
<svg className="desktop-titlebar__logo" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
<circle cx="5" cy="5" r="2.5" fill="currentColor" />
<circle cx="11" cy="5" r="2.5" fill="currentColor" />
<circle cx="5" cy="11" r="2.5" fill="currentColor" />
<circle cx="11" cy="11" r="2.5" fill="currentColor" />
</svg>
<span className="desktop-titlebar__title">Fusion</span>
</div>
{controlsOnLeft ? null : controls}
</header>
);
}