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.
|
After Width: | Height: | Size: 701 KiB |
|
After Width: | Height: | Size: 701 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 914 B |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
@@ -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:
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||