feat: frameless title bar for Windows/Linux with native overlay controls

Use titleBarStyle: 'hidden' + titleBarOverlay on Win/Linux to remove the
ugly native title bar and menu bar while keeping OS-rendered min/max/close
buttons. Hidden Edit menu preserves keyboard shortcuts (Ctrl+C/V/X/Z/A).
Title bar drag region and separator line rendered via web frontend, with
colors matching the Aether Drift design system.

Also adds electron-builder metadata (description, author, homepage,
artifactName) and multi-size icons for cross-platform builds.
This commit is contained in:
Jannis Braun
2026-03-16 06:16:43 +01:00
parent cd2f1e52ef
commit 7a86cde67e
15 changed files with 41 additions and 9 deletions
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 701 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

+1
View File
@@ -1,5 +1,6 @@
appId: com.backspace.desktop appId: com.backspace.desktop
productName: Backspace productName: Backspace
artifactName: "${productName}-${version}-${arch}.${ext}"
directories: directories:
output: dist-electron output: dist-electron
files: files:
+6
View File
@@ -2,6 +2,12 @@
"name": "@backspace/desktop", "name": "@backspace/desktop",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"description": "Backspace — a self-hosted, open-source chat platform",
"author": {
"name": "youruser",
"email": "backspace@backspace.chat"
},
"homepage": "https://github.com/youruser/backspace",
"main": "dist/main.js", "main": "dist/main.js",
"scripts": { "scripts": {
"build:ts": "tsc", "build:ts": "tsc",
+25 -1
View File
@@ -186,7 +186,14 @@ function createWindow(): void {
minHeight: 500, minHeight: 500,
title: 'Backspace', title: 'Backspace',
icon: path.join(__dirname, '..', 'build', 'icon.png'), icon: path.join(__dirname, '..', 'build', 'icon.png'),
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default', titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'hidden',
...(process.platform !== 'darwin' ? {
titleBarOverlay: {
color: '#0b0b10',
symbolColor: '#d8d8de',
height: 32,
},
} : {}),
backgroundColor: '#313338', backgroundColor: '#313338',
show: false, show: false,
webPreferences: { webPreferences: {
@@ -561,6 +568,23 @@ if (!gotTheLock) {
}, },
]); ]);
Menu.setApplicationMenu(appMenu); Menu.setApplicationMenu(appMenu);
} else {
// Win/Linux: frameless window has no menu bar, but we still need an
// application menu so keyboard accelerators (Ctrl+C/V/X/Z/A) work.
Menu.setApplicationMenu(Menu.buildFromTemplate([
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'selectAll' },
],
},
]));
} }
// Purge ALL stale caches so Electron always loads fresh code on launch // Purge ALL stale caches so Electron always loads fresh code on launch
+5 -4
View File
@@ -7,7 +7,7 @@ import { JoinPage } from './components/JoinPage';
import { SwAutoUpdate } from './components/ui/SwUpdatePrompt'; import { SwAutoUpdate } from './components/ui/SwUpdatePrompt';
import { ScreenSharePicker } from './components/voice/ScreenSharePicker'; import { ScreenSharePicker } from './components/voice/ScreenSharePicker';
import { useAuthStore } from './stores/authStore'; import { useAuthStore } from './stores/authStore';
import { isElectronMac } from './platform/platform'; import { isElectron } from './platform/platform';
function ProtectedRoute({ children }: { children: React.ReactNode }) { function ProtectedRoute({ children }: { children: React.ReactNode }) {
const token = useAuthStore((s) => s.token); const token = useAuthStore((s) => s.token);
@@ -29,13 +29,14 @@ function AuthRedirect({ children }: { children: React.ReactNode }) {
} }
export function App() { export function App() {
const showTitleBar = isElectronMac(); const showTitleBar = isElectron();
return ( return (
<div className={`flex flex-col ${showTitleBar ? 'h-screen' : 'contents'}`}> <div className={`flex flex-col ${showTitleBar ? 'h-screen' : 'contents'}`}>
{showTitleBar && ( {showTitleBar && <>
<div className="h-8 flex-shrink-0 bg-surface-base titlebar-drag" /> <div className="h-8 flex-shrink-0 bg-surface-base titlebar-drag" />
)} <div className="h-px flex-shrink-0 bg-border-hard" />
</>}
<div className={showTitleBar ? 'flex-1 min-h-0' : 'contents'}> <div className={showTitleBar ? 'flex-1 min-h-0' : 'contents'}>
<SwAutoUpdate /> <SwAutoUpdate />
<ScreenSharePicker /> <ScreenSharePicker />
@@ -13,7 +13,7 @@ import { TransferOwnershipModal } from '../modals/TransferOwnershipModal';
import type { SpaceLayoutItem, SpaceFolder } from '@backspace/shared'; import type { SpaceLayoutItem, SpaceFolder } from '@backspace/shared';
import { getSpaceGradient, HOME_GRADIENT } from '../../utils/gradients'; import { getSpaceGradient, HOME_GRADIENT } from '../../utils/gradients';
import { isElectronMac } from '../../platform/platform'; import { isElectron } from '../../platform/platform';
import { useFloatingPosition } from '../../hooks/useFloatingPosition'; import { useFloatingPosition } from '../../hooks/useFloatingPosition';
// ─── Resolved layout types ───────────────────────────────────────────────── // ─── Resolved layout types ─────────────────────────────────────────────────
@@ -79,7 +79,7 @@ function SidebarItem({ id, name, icon, avatarColor, active, onClick, onContextMe
} }
if (type === 'dm') { if (type === 'dm') {
return { background: HOME_GRADIENT.gradient }; return undefined;
} }
// Space type — if it has a custom icon image, no gradient needed // Space type — if it has a custom icon image, no gradient needed
@@ -110,7 +110,7 @@ function SidebarItem({ id, name, icon, avatarColor, active, onClick, onContextMe
const buttonContent = ( const buttonContent = (
<button onClick={onClick} className={`${getButtonClasses()} ${dimmed ? 'opacity-40 saturate-50' : ''}`} style={backgroundStyle} title={tooltipText ? undefined : name}> <button onClick={onClick} className={`${getButtonClasses()} ${dimmed ? 'opacity-40 saturate-50' : ''}`} style={backgroundStyle} title={tooltipText ? undefined : name}>
{type === 'dm' ? ( {type === 'dm' ? (
<span className="text-[17px] font-bold">B</span> <img src="/icons/logo.png" alt="Backspace" className="w-full h-full object-cover" />
) : type === 'action' ? ( ) : type === 'action' ? (
actionType === 'add' ? ( actionType === 'add' ? (
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
@@ -1248,7 +1248,7 @@ export function SpaceSidebar() {
}, [openFolderId, resolvedLayout]); }, [openFolderId, resolvedLayout]);
return ( return (
<nav data-pip-obstacle="left" className="w-[72px] bg-surface-base flex flex-col items-center py-3 overflow-y-auto flex-shrink-0 no-scrollbar select-none md:fixed md:inset-y-0 md:left-0 md:z-[100] md:glass-strip" style={{ paddingBottom: floatingPanelHeight + 24, ...(isElectronMac() ? { top: '32px' } : {}) }} onDragOver={(e) => { if (dragState) e.preventDefault(); }} onDrop={handleDrop}> <nav data-pip-obstacle="left" className="w-[72px] bg-surface-base flex flex-col items-center py-3 overflow-y-auto flex-shrink-0 no-scrollbar select-none md:fixed md:inset-y-0 md:left-0 md:z-[100] md:glass-strip" style={{ paddingBottom: floatingPanelHeight + 24, ...(isElectron() ? { top: '33px' } : {}) }} onDragOver={(e) => { if (dragState) e.preventDefault(); }} onDrop={handleDrop}>
<SidebarItem <SidebarItem
id="@me" id="@me"
name="Direct Messages" name="Direct Messages"