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
This commit is contained in:
gsxdsm
2026-04-08 00:16:25 -07:00
parent 95bc081ed2
commit f3c463cb5e
27 changed files with 1325 additions and 28 deletions

View File

@@ -0,0 +1,37 @@
import React, { useEffect } from "react";
import type { PropsWithChildren } from "react";
import { TitleBar } from "./TitleBar";
function detectElectron(): boolean {
if (typeof window === "undefined") {
return false;
}
return Boolean((window as Window & { electronAPI?: unknown }).electronAPI);
}
export function DesktopWrapper({ children }: PropsWithChildren) {
const isElectron = detectElectron();
useEffect(() => {
if (!isElectron) {
return;
}
document.body.classList.add("fusion-desktop");
return () => {
document.body.classList.remove("fusion-desktop");
};
}, [isElectron]);
if (!isElectron) {
return <>{children}</>;
}
return (
<div className="desktop-app-shell" data-testid="desktop-wrapper">
<TitleBar />
<div className="desktop-app-content">{children}</div>
</div>
);
}

View File

@@ -0,0 +1,102 @@
:root {
--desktop-titlebar-height: 38px;
}
.desktop-titlebar {
height: var(--desktop-titlebar-height);
min-height: var(--desktop-titlebar-height);
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 0 10px;
border-bottom: 1px solid var(--border);
background: var(--surface);
color: var(--text);
user-select: none;
flex-shrink: 0;
}
.desktop-titlebar--drag {
-webkit-app-region: drag;
}
.desktop-titlebar__brand {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.desktop-titlebar__logo {
width: 16px;
height: 16px;
color: var(--logo-accent, var(--todo));
flex-shrink: 0;
}
.desktop-titlebar__title {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.01em;
color: var(--text);
}
.desktop-titlebar__controls {
display: inline-flex;
align-items: center;
gap: 4px;
}
.desktop-titlebar__controls--no-drag,
.desktop-titlebar__control {
-webkit-app-region: no-drag;
}
.desktop-titlebar__control {
width: 28px;
height: 24px;
border: none;
border-radius: 6px;
background: transparent;
color: var(--text-muted);
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 13px;
line-height: 1;
transition: background-color 0.15s ease, color 0.15s ease;
}
.desktop-titlebar__control:hover {
background: var(--card);
color: var(--text);
}
.desktop-titlebar__control:focus-visible {
outline: 2px solid var(--todo);
outline-offset: 1px;
}
.desktop-titlebar__control--close:hover {
background: color-mix(in srgb, var(--color-error) 20%, transparent);
color: var(--color-error);
}
.desktop-app-shell {
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
}
.desktop-app-content {
flex: 1;
min-height: 0;
overflow: hidden;
}
body.fusion-desktop {
overflow: hidden;
}

View File

@@ -0,0 +1,125 @@
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>
);
}