feat(ui): add custom title bar window controls for Windows and Linux

Hide the native title bar on win32/linux (titleBarStyle: 'hidden', frame
untouched so native resize borders and snapping keep working) and render
minimize / maximize-restore / close buttons in the renderer, mirroring the
existing macOS traffic-light setup.

- New WINDOW:* IPC contract (minimize, toggle-maximize, close, get-state)
  handled in window.events.ts, resolved from the sender WebContents;
  close goes through win.close() so window-bounds persistence still runs.
- WINDOW:STATE_CHANGED pushed on maximize/unmaximize/fullscreen so the
  maximize/restore glyph stays correct for OS-triggered changes; controls
  hide while fullscreen.
- WindowControlsComponent mounts once in app-root as a manual popover so
  it stays in the browser top layer above CDK overlays (dialogs,
  multi-EPG) - same behavior as macOS traffic lights.
- Theme-aware via CSS vars (--app-on-surface, --app-hover-overlay);
  Windows-red close hover. Drag regions get right padding through a
  body-level frameless-platform class.
- Gated by RuntimeCapabilitiesService.usesCustomWindowControls; PWA and
  macOS never mount the controls.

Includes unit specs for the component and IPC handlers, an Electron E2E
suite (window-controls.e2e.ts), and a window-chrome section in
docs/architecture/workspace-shell.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-06-10 20:46:21 +02:00
1 parent 2be42257ab
commit 24c2da48ae
18 files changed
+939 -25

No files matched your search

@@ -135,6 +135,11 @@ export interface ElectronBridgeDialogFilter {
extensions: string[];
}
export interface ElectronBridgeWindowState {
isMaximized: boolean;
isFullScreen: boolean;
}
export interface ElectronBridgeAiSettings {
aiProvider: string;
aiModelName: string;
@@ -425,6 +430,13 @@ export interface ElectronBridgeApi {
) => () => void;
getAppVersion: () => Promise<string>;
platform: string;
minimizeWindow: () => Promise<void>;
toggleMaximizeWindow: () => Promise<ElectronBridgeWindowState>;
closeWindow: () => Promise<void>;
getWindowState: () => Promise<ElectronBridgeWindowState>;
onWindowStateChange: (
callback: (state: ElectronBridgeWindowState) => void
) => () => void;
fetchPlaylistByUrl: (url: string, title?: string) => Promise<Playlist>;
updatePlaylistFromFilePath: (
filePath: string,
@@ -75,3 +75,10 @@ export const DELETE_ALL_PLAYLISTS = 'DELETE_ALL_PLAYLISTS';
// Remote Control
export const REMOTE_CONTROL_CHANGE_CHANNEL = 'REMOTE_CONTROL_CHANGE_CHANNEL';
// Window controls (custom title bar on Windows/Linux)
export const WINDOW_MINIMIZE = 'WINDOW:MINIMIZE';
export const WINDOW_TOGGLE_MAXIMIZE = 'WINDOW:TOGGLE_MAXIMIZE';
export const WINDOW_CLOSE = 'WINDOW:CLOSE';
export const WINDOW_GET_STATE = 'WINDOW:GET_STATE';
export const WINDOW_STATE_CHANGED = 'WINDOW:STATE_CHANGED';