Skip to content

Commit bbbedc9

Browse files
Add custom title bar like VSCode for Electron
1 parent 2e5e17d commit bbbedc9

5 files changed

Lines changed: 227 additions & 1 deletion

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { useEffect, useState } from 'react';
2+
import { classNames } from '~/utils/classNames';
3+
4+
declare global {
5+
interface Window {
6+
electronAPI?: {
7+
minimize: () => void;
8+
maximize: () => void;
9+
close: () => void;
10+
isMaximized: () => Promise<boolean>;
11+
getPlatform: () => Promise<string>;
12+
onMaximize: (callback: () => void) => () => void;
13+
onUnmaximize: (callback: () => void) => () => void;
14+
offMaximize: (callback: () => void) => void;
15+
offUnmaximize: (callback: () => void) => void;
16+
};
17+
}
18+
}
19+
20+
export function CustomTitleBar() {
21+
const [isMaximized, setIsMaximized] = useState(false);
22+
const [platform, setPlatform] = useState<'win32' | 'darwin' | 'linux'>('darwin');
23+
24+
useEffect(() => {
25+
// Check if running in Electron
26+
if (typeof window !== 'undefined' && window.electronAPI) {
27+
// Get platform info
28+
window.electronAPI.getPlatform().then((plat: string) => {
29+
setPlatform(plat as 'win32' | 'darwin' | 'linux');
30+
});
31+
32+
// Listen for window state changes
33+
const handleMaximize = () => setIsMaximized(true);
34+
const handleUnmaximize = () => setIsMaximized(false);
35+
36+
const cleanupMaximize = window.electronAPI.onMaximize(handleMaximize);
37+
const cleanupUnmaximize = window.electronAPI.onUnmaximize(handleUnmaximize);
38+
39+
// Check initial state
40+
window.electronAPI.isMaximized().then(setIsMaximized);
41+
42+
return () => {
43+
cleanupMaximize();
44+
cleanupUnmaximize();
45+
};
46+
}
47+
}, []);
48+
49+
const handleMinimize = () => {
50+
if (window.electronAPI) {
51+
window.electronAPI.minimize();
52+
}
53+
};
54+
55+
const handleMaximize = () => {
56+
if (window.electronAPI) {
57+
window.electronAPI.maximize();
58+
}
59+
};
60+
61+
const handleClose = () => {
62+
if (window.electronAPI) {
63+
window.electronAPI.close();
64+
}
65+
};
66+
67+
// Only show on Electron platforms
68+
if (typeof window === 'undefined' || !window.electronAPI) {
69+
return null;
70+
}
71+
72+
return (
73+
<div
74+
className={classNames(
75+
'flex items-center justify-between h-10 bg-codinit-elements-background-depth-1 border-b border-codinit-elements-border',
76+
'select-none',
77+
)}
78+
style={
79+
{
80+
WebkitAppRegion: 'drag',
81+
appRegion: 'drag',
82+
} as React.CSSProperties
83+
}
84+
>
85+
{/* Left side - Window controls (macOS style) */}
86+
{platform === 'darwin' && (
87+
<div className="flex items-center pl-4 space-x-2 flex-shrink-0">
88+
<button
89+
onClick={handleClose}
90+
className="w-3 h-3 rounded-full bg-red-500 hover:bg-red-600 transition-colors"
91+
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
92+
title="Close"
93+
/>
94+
<button
95+
onClick={handleMinimize}
96+
className="w-3 h-3 rounded-full bg-yellow-500 hover:bg-yellow-600 transition-colors"
97+
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
98+
title="Minimize"
99+
/>
100+
<button
101+
onClick={handleMaximize}
102+
className="w-3 h-3 rounded-full bg-green-500 hover:bg-green-600 transition-colors"
103+
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
104+
title="Maximize"
105+
/>
106+
</div>
107+
)}
108+
109+
{/* Center - App title */}
110+
<div className="flex-1 flex items-center justify-center min-w-0">
111+
<div className="flex items-center gap-2 px-4">
112+
<img src="/icon-dark.png" alt="CodinIT" className="w-4 h-4 dark:hidden flex-shrink-0" />
113+
<img src="/icon-light.png" alt="CodinIT" className="w-4 h-4 hidden dark:block flex-shrink-0" />
114+
<span className="text-sm font-medium text-codinit-elements-textPrimary truncate">CodinIT.dev</span>
115+
</div>
116+
</div>
117+
118+
{/* Right side - Window controls (Windows/Linux style) */}
119+
{platform !== 'darwin' && (
120+
<div className="flex items-center pr-2 space-x-0 flex-shrink-0">
121+
<button
122+
onClick={handleMinimize}
123+
className="w-12 h-10 flex items-center justify-center hover:bg-codinit-elements-background-depth-2 transition-colors"
124+
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
125+
title="Minimize"
126+
>
127+
<div className="w-3 h-0.5 bg-codinit-elements-textSecondary" />
128+
</button>
129+
<button
130+
onClick={handleMaximize}
131+
className="w-12 h-10 flex items-center justify-center hover:bg-codinit-elements-background-depth-2 transition-colors"
132+
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
133+
title={isMaximized ? 'Restore' : 'Maximize'}
134+
>
135+
{isMaximized ? (
136+
<div className="w-3 h-3 border border-codinit-elements-textSecondary relative">
137+
<div className="absolute -top-0.5 -right-0.5 w-2 h-2 bg-codinit-elements-background-depth-1 border border-codinit-elements-textSecondary" />
138+
</div>
139+
) : (
140+
<div className="w-3 h-3 border border-codinit-elements-textSecondary" />
141+
)}
142+
</button>
143+
<button
144+
onClick={handleClose}
145+
className="w-12 h-10 flex items-center justify-center hover:bg-red-500 transition-colors"
146+
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
147+
title="Close"
148+
>
149+
<div className="w-3 h-0.5 bg-codinit-elements-textSecondary rotate-45 relative">
150+
<div className="w-3 h-0.5 bg-codinit-elements-textSecondary -rotate-45 absolute top-0" />
151+
</div>
152+
</button>
153+
</div>
154+
)}
155+
</div>
156+
);
157+
}

app/routes/_index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ClientOnly } from 'remix-utils/client-only';
33
import { BaseChat } from '~/components/chat/BaseChat';
44
import { Chat } from '~/components/chat/Chat.client';
55
import { Header } from '~/components/header/Header';
6+
import { CustomTitleBar } from '~/components/ui/CustomTitleBar';
67
import BackgroundRays from '~/components/ui/BackgroundRays';
78

89
export const meta: MetaFunction = () => {
@@ -17,6 +18,7 @@ export const loader = () => json({});
1718
export default function Index() {
1819
return (
1920
<div className="flex flex-col h-full w-full bg-codinit-elements-background-depth-1">
21+
<ClientOnly>{() => <CustomTitleBar />}</ClientOnly>
2022
<BackgroundRays />
2123
<Header />
2224
<ClientOnly fallback={<BaseChat />}>{() => <Chat />}</ClientOnly>

electron/main/index.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,40 @@ declare global {
292292
}
293293
});
294294

295+
// Window control handlers
296+
ipcMain.handle('window-minimize', () => {
297+
win.minimize();
298+
});
299+
300+
ipcMain.handle('window-maximize', () => {
301+
if (win.isMaximized()) {
302+
win.unmaximize();
303+
} else {
304+
win.maximize();
305+
}
306+
});
307+
308+
ipcMain.handle('window-close', () => {
309+
win.close();
310+
});
311+
312+
ipcMain.handle('window-is-maximized', () => {
313+
return win.isMaximized();
314+
});
315+
316+
ipcMain.handle('window-get-platform', () => {
317+
return process.platform;
318+
});
319+
320+
// Window state change listeners
321+
win.on('maximize', () => {
322+
win.webContents.send('window-maximized');
323+
});
324+
325+
win.on('unmaximize', () => {
326+
win.webContents.send('window-unmaximized');
327+
});
328+
295329
return win;
296330
})
297331
.then((win) => {

electron/main/ui/window.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function createWindow(rendererURL: string) {
1515
height: 800,
1616
...bounds,
1717
},
18-
titleBarStyle: 'hidden',
18+
frame: false,
1919
vibrancy: 'under-window',
2020
visualEffectState: 'active',
2121
webPreferences: {

electron/preload/index.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,38 @@ const cookies = {
3434
},
3535
};
3636

37+
const windowControls = {
38+
minimize: () => {
39+
ipcRenderer.invoke('window-minimize');
40+
},
41+
maximize: () => {
42+
ipcRenderer.invoke('window-maximize');
43+
},
44+
close: () => {
45+
ipcRenderer.invoke('window-close');
46+
},
47+
isMaximized: () => {
48+
return ipcRenderer.invoke('window-is-maximized');
49+
},
50+
getPlatform: () => {
51+
return ipcRenderer.invoke('window-get-platform');
52+
},
53+
onMaximize: (callback: () => void) => {
54+
ipcRenderer.on('window-maximized', callback);
55+
return () => ipcRenderer.removeListener('window-maximized', callback);
56+
},
57+
onUnmaximize: (callback: () => void) => {
58+
ipcRenderer.on('window-unmaximized', callback);
59+
return () => ipcRenderer.removeListener('window-unmaximized', callback);
60+
},
61+
offMaximize: (callback: () => void) => {
62+
ipcRenderer.removeListener('window-maximized', callback);
63+
},
64+
offUnmaximize: (callback: () => void) => {
65+
ipcRenderer.removeListener('window-unmaximized', callback);
66+
},
67+
};
68+
3769
contextBridge.exposeInMainWorld('ipc', ipc);
3870
contextBridge.exposeInMainWorld('electronCookies', cookies);
71+
contextBridge.exposeInMainWorld('electronAPI', windowControls);

0 commit comments

Comments
 (0)