feat(titlebar): theme-colored window bar on macOS (#1199)

* feat(titlebar): theme-colored window bar on macOS

macOS showed the grey native title bar that ignores the app theme
(#1198). Enable titleBarStyle: Overlay so the webview reaches the top
edge and the native traffic lights float over the existing in-page
title bar, which already paints with the active theme.

Reuses the Linux title-bar grid: the bar is shown on macOS too (hidden
in native fullscreen), without the custom window buttons since the
traffic lights stay native. The GTK resize grips are now Linux-only.
Renamed the stylesheet to drop the misleading linux-only name.

* docs(changelog): macOS themed title bar

* fix(titlebar): crisp macOS bar text — drop transform layer and shadow

* fix(titlebar): drop now-playing text from the macOS bar
This commit is contained in:
Psychotoxical
2026-06-27 14:17:55 +02:00
committed by GitHub
parent c609beddfa
commit ed02ba4e1d
7 changed files with 49 additions and 13 deletions
+6
View File
@@ -156,6 +156,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Fifty strings that still fell back to English in the Russian UI are now translated — macOS in-place updater, device sync file migration, fullscreen lyrics, and statistics share-image export. * Fifty strings that still fell back to English in the Russian UI are now translated — macOS in-place updater, device sync file migration, fullscreen lyrics, and statistics share-image export.
* User-facing descriptions in Russian and English no longer mention WebKitGTK or FisherYates internals; several Russian labels and section titles read more naturally (settings casing, smart playlists, track transitions, and home rails). * User-facing descriptions in Russian and English no longer mention WebKitGTK or FisherYates internals; several Russian labels and section titles read more naturally (settings casing, smart playlists, track transitions, and home rails).
### macOS — themed window title bar
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1199](https://github.com/Psychotoxical/psysonic/pull/1199)**, suggested by [@bcorporaal](https://github.com/bcorporaal)
* On macOS the window's title bar now follows the active theme instead of the grey system bar; the native macOS window buttons stay in place, floating over the themed bar.
## Fixed ## Fixed
+1
View File
@@ -22,6 +22,7 @@
"fullscreen": false, "fullscreen": false,
"decorations": true, "decorations": true,
"transparent": false, "transparent": false,
"titleBarStyle": "Overlay",
"visible": false, "visible": false,
"backgroundColor": "#1e1e2e", "backgroundColor": "#1e1e2e",
"dragDropEnabled": false "dragDropEnabled": false
+14 -4
View File
@@ -56,7 +56,7 @@ import { useNowPlayingPrewarm } from '../hooks/useNowPlayingPrewarm';
import { useOfflineAutoNav } from '../hooks/useOfflineAutoNav'; import { useOfflineAutoNav } from '../hooks/useOfflineAutoNav';
import { useOfflineLibraryFilterSuspend } from '../hooks/useOfflineLibraryFilterSuspend'; import { useOfflineLibraryFilterSuspend } from '../hooks/useOfflineLibraryFilterSuspend';
import { AppShellQueueResizerSeam } from '../components/AppShellQueueResizerSeam'; import { AppShellQueueResizerSeam } from '../components/AppShellQueueResizerSeam';
import { IS_LINUX } from '../utils/platform'; import { IS_LINUX, IS_MACOS } from '../utils/platform';
import { useConnectionStatus } from '../hooks/useConnectionStatus'; import { useConnectionStatus } from '../hooks/useConnectionStatus';
import { useIdlePlayQueuePull } from '../hooks/useIdlePlayQueuePull'; import { useIdlePlayQueuePull } from '../hooks/useIdlePlayQueuePull';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
@@ -75,7 +75,7 @@ import {
} from '../utils/componentHelpers/appShellHelpers'; } from '../utils/componentHelpers/appShellHelpers';
/** /**
* The main webview's persistent layout: titlebar (Linux only) + sidebar + * The main webview's persistent layout: titlebar (Linux + macOS) + sidebar +
* main content area (header + route host + offline banner) + queue panel + * main content area (header + route host + offline banner) + queue panel +
* player bar + fullscreen overlay + global modals + tray-tooltip / title * player bar + fullscreen overlay + global modals + tray-tooltip / title
* sync. Mounted under `<RequireAuth>` and shared across all routes. * sync. Mounted under `<RequireAuth>` and shared across all routes.
@@ -226,12 +226,22 @@ export function AppShell() {
const isMobilePlayer = isMobile && location.pathname === '/now-playing'; const isMobilePlayer = isMobile && location.pathname === '/now-playing';
// Custom in-page titlebar. Linux: opt-in, native decorations off. macOS:
// always on — `titleBarStyle: Overlay` lets the webview reach the top edge
// with the native traffic lights floating over our themed bar, so the bar
// follows the active theme instead of the grey system titlebar (#1198).
// Hidden in native fullscreen (the OS chrome is gone there anyway).
const showLinuxTitlebar = IS_LINUX && useCustomTitlebar && !isWindowFullscreen && !isTilingWm;
const showMacTitlebar = IS_MACOS && !isWindowFullscreen;
const showTitlebar = showLinuxTitlebar || showMacTitlebar;
return ( return (
<div <div
className={`app-shell ${floatingPlayerBar ? 'floating-player' : ''}`} className={`app-shell ${floatingPlayerBar ? 'floating-player' : ''}`}
data-mobile={isMobile || undefined} data-mobile={isMobile || undefined}
data-mobile-player={isMobilePlayer || undefined} data-mobile-player={isMobilePlayer || undefined}
data-titlebar={(IS_LINUX && useCustomTitlebar && !isWindowFullscreen && !isTilingWm) || undefined} data-titlebar={showTitlebar || undefined}
data-titlebar-platform={showMacTitlebar ? 'macos' : showLinuxTitlebar ? 'linux' : undefined}
data-fullscreen={isWindowFullscreen || undefined} data-fullscreen={isWindowFullscreen || undefined}
style={{ style={{
'--sidebar-width': isMobile ? '0px' : (isSidebarCollapsed ? '72px' : 'clamp(200px, 15vw, 220px)'), '--sidebar-width': isMobile ? '0px' : (isSidebarCollapsed ? '72px' : 'clamp(200px, 15vw, 220px)'),
@@ -241,7 +251,7 @@ export function AppShell() {
} as React.CSSProperties} } as React.CSSProperties}
onContextMenu={e => e.preventDefault()} onContextMenu={e => e.preventDefault()}
> >
{IS_LINUX && useCustomTitlebar && !isWindowFullscreen && !isTilingWm && <TitleBar />} {showTitlebar && <TitleBar />}
{import.meta.env.DEV && isMobile && ( {import.meta.env.DEV && isMobile && (
<span className="dev-build-badge" aria-hidden>DEV</span> <span className="dev-build-badge" aria-hidden>DEV</span>
)} )}
+11
View File
@@ -3,6 +3,7 @@ import { getCurrentWindow } from '@tauri-apps/api/window';
import { Minus, Square, X } from 'lucide-react'; import { Minus, Square, X } from 'lucide-react';
import { usePlayerStore } from '../store/playerStore'; import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
import { IS_MACOS } from '../utils/platform';
export default function TitleBar() { export default function TitleBar() {
const win = getCurrentWindow(); const win = getCurrentWindow();
@@ -13,6 +14,11 @@ export default function TitleBar() {
return ( return (
<div className="titlebar" data-tauri-drag-region> <div className="titlebar" data-tauri-drag-region>
{/* macOS drops the now-playing label: subpixel antialiasing is disabled
in the Overlay title-bar zone, so small text renders frayed. The bar
stays a clean themed strip with the native traffic lights (#1198);
the track is already shown in the player bar. */}
{!IS_MACOS && (
<div className="titlebar-track" data-tauri-drag-region> <div className="titlebar-track" data-tauri-drag-region>
{currentTrack && ( {currentTrack && (
<> <>
@@ -23,7 +29,11 @@ export default function TitleBar() {
</> </>
)} )}
</div> </div>
)}
{/* macOS keeps its native traffic lights (floating over the bar via
titleBarStyle: Overlay); only Linux draws in-page window buttons. */}
{!IS_MACOS && (
<div className="titlebar-controls" data-btnstyle={windowButtonStyle}> <div className="titlebar-controls" data-btnstyle={windowButtonStyle}>
{showMinimizeButton && ( {showMinimizeButton && (
<button <button
@@ -55,6 +65,7 @@ export default function TitleBar() {
<X size={10} strokeWidth={2.5} aria-hidden /> <X size={10} strokeWidth={2.5} aria-hidden />
</button> </button>
</div> </div>
)}
</div> </div>
); );
} }
+1
View File
@@ -392,6 +392,7 @@ const CONTRIBUTOR_ENTRIES = [
'Compact buttons — appearance toggle that switches action and toolbar buttons to icon-only across detail headers and browse views, app-wide (PR #1189)', 'Compact buttons — appearance toggle that switches action and toolbar buttons to icon-only across detail headers and browse views, app-wide (PR #1189)',
'Playlist sort — sort a playlist by date added (newest/oldest) or any track column from a visible sort dropdown (PR #1191)', 'Playlist sort — sort a playlist by date added (newest/oldest) or any track column from a visible sort dropdown (PR #1191)',
'Configurable artist backdrops — per-place source order + on/off (mainstage hero, artist page, fullscreen), with the mainstage hero showing the artist backdrop and prefetching upcoming ones (PR #1193)', 'Configurable artist backdrops — per-place source order + on/off (mainstage hero, artist page, fullscreen), with the mainstage hero showing the artist backdrop and prefetching upcoming ones (PR #1193)',
'Themed window title bar on macOS — follows the active theme instead of the grey system bar, with the native window buttons floating over it (PR #1199)',
], ],
}, },
{ {
@@ -1,4 +1,4 @@
/* ─── Custom title bar (Linux only — decorations: false) ─── */ /* ─── Custom title bar Linux (decorations: false) + macOS (titleBarStyle: Overlay) ─── */
:root { :root {
--titlebar-height: 32px; --titlebar-height: 32px;
} }
@@ -19,9 +19,10 @@
"sidebar main queue"; "sidebar main queue";
} }
/* Resize grips — replace the native GTK grips lost when decorations: false */ /* Resize grips Linux only; replace the native GTK grips lost when
.app-shell[data-titlebar]::before, decorations: false. macOS keeps its native window-edge resize. */
.app-shell[data-titlebar]::after { .app-shell[data-titlebar-platform='linux']::before,
.app-shell[data-titlebar-platform='linux']::after {
content: ''; content: '';
position: fixed; position: fixed;
bottom: 0; bottom: 0;
@@ -33,8 +34,8 @@
opacity: 0.35; opacity: 0.35;
z-index: 9999; z-index: 9999;
} }
.app-shell[data-titlebar]::before { left: 0; } .app-shell[data-titlebar-platform='linux']::before { left: 0; }
.app-shell[data-titlebar]::after { right: 0; } .app-shell[data-titlebar-platform='linux']::after { right: 0; }
.titlebar { .titlebar {
grid-area: titlebar; grid-area: titlebar;
@@ -50,8 +51,14 @@
.titlebar-track { .titlebar-track {
position: absolute; position: absolute;
left: 50%; /* Centre without translateX: a transform promotes the label to its own
transform: translateX(-50%); compositing layer, which drops macOS subpixel antialiasing and lands it
on half-pixels the text ends up frayed/grainy. Auto-margins between
left:0/right:0 keep it centred and pixel-snapped on both platforms. */
left: 0;
right: 0;
width: fit-content;
margin-inline: auto;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
+1 -1
View File
@@ -1,6 +1,6 @@
@import './_intro.css'; @import './_intro.css';
@import './dev-build-chrome.css'; @import './dev-build-chrome.css';
@import './custom-title-bar-linux-only-decorations-false.css'; @import './custom-title-bar.css';
@import './resizer-handles.css'; @import './resizer-handles.css';
@import './sidebar.css'; @import './sidebar.css';
@import './update-toast.css'; @import './update-toast.css';