From 028eb65f7d30c7480b4ec283ab44c22ae130fd43 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Sat, 13 Jun 2026 23:52:56 +0200 Subject: [PATCH] feat(titlebar): selectable window button styles + minimize toggle (#1083) * feat(titlebar): selectable window button styles + minimize toggle Custom title bar (Linux) gains a window-button style picker, mirroring the seekbar style picker pattern. Six form-named styles: dots, dotsGlyph, flat, pill, outline, glyph. All buttons now carry minimize/maximize/close glyphs for clear, colour-blind-friendly iconography; dots reveal glyphs on hover, dotsGlyph always shows them. - New authStore state windowButtonStyle (default dots) + showMinimizeButton, with rehydrate validation falling back to dots on unknown values. - WindowButtonPreview reuses the real .titlebar-btn classes for WYSIWYG tiles. - Picker + minimize toggle render under the Custom title bar setting, gated on the toggle being on. - Monochrome styles use --text-primary glyphs and stronger borders for contrast on dark themes. - Dev-build grey marker scoped to the real title bar so previews show true colours. - i18n keys in all 9 locales; setter and rehydrate tests. * docs(changelog): window button styles (#1083) --- CHANGELOG.md | 9 ++ src/components/TitleBar.tsx | 32 +++-- src/components/WindowButtonPreview.tsx | 69 +++++++++++ src/components/settings/AppearanceTab.tsx | 36 +++++- src/locales/de/settings.ts | 10 ++ src/locales/en/settings.ts | 10 ++ src/locales/es/settings.ts | 10 ++ src/locales/fr/settings.ts | 10 ++ src/locales/nb/settings.ts | 10 ++ src/locales/nl/settings.ts | 10 ++ src/locales/ro/settings.ts | 10 ++ src/locales/ru/settings.ts | 10 ++ src/locales/zh/settings.ts | 10 ++ src/store/authStore.persistence.test.ts | 22 ++++ src/store/authStore.settings.test.ts | 2 + src/store/authStore.ts | 2 + src/store/authStoreRehydrate.ts | 13 ++ src/store/authStoreTypes.ts | 17 +++ src/store/authUiAppearanceActions.ts | 4 + ...title-bar-linux-only-decorations-false.css | 116 ++++++++++++++---- src/styles/layout/dev-build-chrome.css | 21 ++-- 21 files changed, 386 insertions(+), 47 deletions(-) create mode 100644 src/components/WindowButtonPreview.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 53488328..f4e62259 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -131,6 +131,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Play counts are unchanged — still driven by the existing scrobble path. Servers without the extension keep the previous now-playing behaviour. + +### Title bar — selectable window button styles + +**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1083](https://github.com/Psychotoxical/psysonic/pull/1083), suggested by [@PHLAK](https://github.com/PHLAK)** + +* The Linux custom title bar gets a **window button style** picker in **Settings → Appearance → Custom title bar** — choose between dots, dots with icons, flat, pill, outline, and minimal looks. +* All styles now carry minimize/maximize/close icons for clear, colour-blind-friendly buttons, and an optional toggle hides the minimize button (maximize and close only). + + ## Changed ### Dependencies — npm and Rust refresh diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx index d21d5a78..3c858edf 100644 --- a/src/components/TitleBar.tsx +++ b/src/components/TitleBar.tsx @@ -1,11 +1,15 @@ import React from 'react'; import { getCurrentWindow } from '@tauri-apps/api/window'; +import { Minus, Square, X } from 'lucide-react'; import { usePlayerStore } from '../store/playerStore'; +import { useAuthStore } from '../store/authStore'; export default function TitleBar() { const win = getCurrentWindow(); const currentTrack = usePlayerStore(s => s.currentTrack); const isPlaying = usePlayerStore(s => s.isPlaying); + const windowButtonStyle = useAuthStore(s => s.windowButtonStyle); + const showMinimizeButton = useAuthStore(s => s.showMinimizeButton); return (
@@ -20,28 +24,36 @@ export default function TitleBar() { )}
-
- + )}
); diff --git a/src/components/WindowButtonPreview.tsx b/src/components/WindowButtonPreview.tsx new file mode 100644 index 00000000..ee992398 --- /dev/null +++ b/src/components/WindowButtonPreview.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { Minus, Square, X } from 'lucide-react'; +import type { WindowButtonStyle } from '../store/authStoreTypes'; + +interface Props { + style: WindowButtonStyle; + label: string; + selected: boolean; + onClick: () => void; +} + +/** + * Selection tile for the custom-title-bar window-button style picker. Renders + * the real `.titlebar-controls` / `.titlebar-btn` classes inside a mini title + * bar so the preview is exactly what the chosen style produces. + */ +export default function WindowButtonPreview({ style, label, selected, onClick }: Props) { + return ( + + ); +} diff --git a/src/components/settings/AppearanceTab.tsx b/src/components/settings/AppearanceTab.tsx index 9e24b817..9673cfb0 100644 --- a/src/components/settings/AppearanceTab.tsx +++ b/src/components/settings/AppearanceTab.tsx @@ -7,12 +7,13 @@ import { LIBRARY_GRID_MAX_COLUMNS_MAX, LIBRARY_GRID_MAX_COLUMNS_MIN, } from '../../store/authStoreDefaults'; -import type { SeekbarStyle } from '../../store/authStoreTypes'; +import type { SeekbarStyle, WindowButtonStyle } from '../../store/authStoreTypes'; import { useFontStore, FontId } from '../../store/fontStore'; import { useThemeStore } from '../../store/themeStore'; import { IS_LINUX, IS_WINDOWS } from '../../utils/platform'; import SettingsSubSection from '../SettingsSubSection'; import { SeekbarPreview } from '../WaveformSeekPreview'; +import WindowButtonPreview from '../WindowButtonPreview'; export function AppearanceTab() { const { t } = useTranslation(); @@ -170,6 +171,39 @@ export function AppearanceTab() { + {auth.useCustomTitlebar && ( + <> +
+
+
{t('settings.windowButtonStyle')}
+
+ {t('settings.windowButtonStyleDesc')} +
+
+ {(['dots', 'dotsGlyph', 'flat', 'pill', 'outline', 'glyph'] as WindowButtonStyle[]).map(style => ( + auth.setWindowButtonStyle(style)} + /> + ))} +
+
+
+
+
+
{t('settings.showMinimizeButton')}
+
{t('settings.showMinimizeButtonDesc')}
+
+ +
+ + )} )}
diff --git a/src/locales/de/settings.ts b/src/locales/de/settings.ts index 8b4a1f3b..eb1a5692 100644 --- a/src/locales/de/settings.ts +++ b/src/locales/de/settings.ts @@ -228,6 +228,16 @@ export const settings = { discordRichPresenceNotice: 'Achtung: Dies ist die in Psysonic integrierte Discord Rich Presence. Wenn du stattdessen das offizielle Navidrome-Discord-Rich-Presence-Plugin nutzen möchtest, lass diese Funktion deaktiviert und aktiviere stattdessen weiter unten auf dieser Seite „Im Livefenster anzeigen".', useCustomTitlebar: 'Eigene Titelleiste', useCustomTitlebarDesc: 'Ersetzt die System-Titelleiste durch eine eingebaute, die zum App-Theme passt. Deaktivieren, um die native GNOME/GTK-Titelleiste zu verwenden.', + windowButtonStyle: 'Fenster-Buttons', + windowButtonStyleDesc: 'Wähle das Aussehen der Minimieren-, Maximieren- und Schließen-Buttons.', + windowButtonsDots: 'Punkte', + windowButtonsDotsGlyph: 'Punkte + Symbole', + windowButtonsFlat: 'Flach', + windowButtonsPill: 'Pille', + windowButtonsOutline: 'Umriss', + windowButtonsGlyph: 'Minimal', + showMinimizeButton: 'Minimieren-Button anzeigen', + showMinimizeButtonDesc: 'Ausschalten, um nur Maximieren und Schließen anzuzeigen.', linuxWebkitSmoothScroll: 'Sanftes Mausrad (Linux)', linuxWebkitSmoothScrollDesc: 'An: mit Nachlauf. Aus: zeilenweise wie in GTK-Apps.', linuxWebkitInputForceRepaint: 'Eingabefelder beim Fokus neu zeichnen (Linux)', diff --git a/src/locales/en/settings.ts b/src/locales/en/settings.ts index 5fd84cd4..507e96b5 100644 --- a/src/locales/en/settings.ts +++ b/src/locales/en/settings.ts @@ -231,6 +231,16 @@ export const settings = { discordRichPresenceNotice: 'Heads up: this is Psysonic\'s built-in Discord Rich Presence. If you\'d rather use the official Navidrome Discord Rich Presence plugin, leave this switched off and enable "Show in Now Playing" further down this page instead.', useCustomTitlebar: 'Custom title bar', useCustomTitlebarDesc: 'Replace the system title bar with a built-in one that matches the app theme. Disable to use the native GNOME/GTK title bar.', + windowButtonStyle: 'Window buttons', + windowButtonStyleDesc: 'Pick the look of the minimize, maximize and close buttons.', + windowButtonsDots: 'Dots', + windowButtonsDotsGlyph: 'Dots + icons', + windowButtonsFlat: 'Flat', + windowButtonsPill: 'Pill', + windowButtonsOutline: 'Outline', + windowButtonsGlyph: 'Minimal', + showMinimizeButton: 'Show minimize button', + showMinimizeButtonDesc: 'Turn off to show only the maximize and close buttons.', linuxWebkitSmoothScroll: 'Smooth wheel (Linux)', linuxWebkitSmoothScrollDesc: 'On: inertial scroll. Off: line-by-line, GTK-style.', linuxWebkitInputForceRepaint: 'Repaint inputs on focus (Linux)', diff --git a/src/locales/es/settings.ts b/src/locales/es/settings.ts index 56a4d6c1..93496d48 100644 --- a/src/locales/es/settings.ts +++ b/src/locales/es/settings.ts @@ -227,6 +227,16 @@ export const settings = { discordRichPresenceNotice: 'Aviso: esta es la Discord Rich Presence integrada en Psysonic. Si prefieres usar el plugin oficial de Discord Rich Presence de Navidrome, deja esta función desactivada y activa en su lugar «Mostrar en Reproduciendo Ahora» más abajo en esta página.', useCustomTitlebar: 'Barra de título personalizada', useCustomTitlebarDesc: 'Reemplaza la barra de título del sistema con una integrada que coincide con el tema de la app. Desactiva para usar la barra nativa de GNOME/GTK.', + windowButtonStyle: 'Botones de ventana', + windowButtonStyleDesc: 'Elige el aspecto de los botones de minimizar, maximizar y cerrar.', + windowButtonsDots: 'Puntos', + windowButtonsDotsGlyph: 'Puntos + iconos', + windowButtonsFlat: 'Plano', + windowButtonsPill: 'Píldora', + windowButtonsOutline: 'Contorno', + windowButtonsGlyph: 'Minimalista', + showMinimizeButton: 'Mostrar botón de minimizar', + showMinimizeButtonDesc: 'Desactiva para mostrar solo maximizar y cerrar.', linuxWebkitSmoothScroll: 'Rueda suave (Linux)', linuxWebkitSmoothScrollDesc: 'Activado: inercia. Desactivado: pasos por línea (estilo GTK).', linuxWebkitInputForceRepaint: 'Repintar los campos al enfocar (Linux)', diff --git a/src/locales/fr/settings.ts b/src/locales/fr/settings.ts index af19e015..e2894cd7 100644 --- a/src/locales/fr/settings.ts +++ b/src/locales/fr/settings.ts @@ -611,6 +611,16 @@ export const settings = { waveformCacheClearFailed: 'Impossible de vider le cache des formes d’onde.', useCustomTitlebar: 'Barre de titre personnalisée', useCustomTitlebarDesc: 'Remplace la barre de titre système par une barre intégrée assortie au thème de l’app. Désactivez pour utiliser la barre GNOME/GTK native.', + windowButtonStyle: 'Boutons de fenêtre', + windowButtonStyleDesc: 'Choisissez l’apparence des boutons réduire, agrandir et fermer.', + windowButtonsDots: 'Points', + windowButtonsDotsGlyph: 'Points + icônes', + windowButtonsFlat: 'Plat', + windowButtonsPill: 'Pilule', + windowButtonsOutline: 'Contour', + windowButtonsGlyph: 'Minimal', + showMinimizeButton: 'Afficher le bouton réduire', + showMinimizeButtonDesc: 'Désactivez pour n’afficher qu’agrandir et fermer.', analyticsStrategyTitle: 'Stratégies d’analyse', analyticsStrategyDesc: 'Choisissez le style d’analyse pour la bibliothèque.', analyticsStrategyLabel: 'Stratégie', diff --git a/src/locales/nb/settings.ts b/src/locales/nb/settings.ts index 1c534fb2..3ff2de1e 100644 --- a/src/locales/nb/settings.ts +++ b/src/locales/nb/settings.ts @@ -612,6 +612,16 @@ export const settings = { showTrayIconDesc: 'Vis Psysonic-ikonet i systemstatusfeltet / menylinjen.', useCustomTitlebar: 'Tilpasset tittellinje', useCustomTitlebarDesc: 'Erstatt systemets tittellinje med en innebygd som matcher app-temaet. Slå av for å bruke den native GNOME/GTK-tittellinjen.', + windowButtonStyle: 'Vindusknapper', + windowButtonStyleDesc: 'Velg utseendet på knappene for minimer, maksimer og lukk.', + windowButtonsDots: 'Prikker', + windowButtonsDotsGlyph: 'Prikker + ikoner', + windowButtonsFlat: 'Flat', + windowButtonsPill: 'Pille', + windowButtonsOutline: 'Omriss', + windowButtonsGlyph: 'Minimal', + showMinimizeButton: 'Vis minimer-knapp', + showMinimizeButtonDesc: 'Slå av for å vise bare maksimer og lukk.', analyticsStrategyTitle: 'Analysestrategier', analyticsStrategyDesc: 'Velg analysestil for biblioteket.', analyticsStrategyLabel: 'Strategi', diff --git a/src/locales/nl/settings.ts b/src/locales/nl/settings.ts index e18885ab..2be05181 100644 --- a/src/locales/nl/settings.ts +++ b/src/locales/nl/settings.ts @@ -611,6 +611,16 @@ export const settings = { waveformCacheClearFailed: 'Waveform-cache wissen mislukt.', useCustomTitlebar: 'Aangepaste titelbalk', useCustomTitlebarDesc: 'Vervang de systeemtitelbalk door een ingebouwde balk die bij het app-thema past. Schakel uit om de native GNOME/GTK-titelbalk te gebruiken.', + windowButtonStyle: 'Vensterknoppen', + windowButtonStyleDesc: 'Kies het uiterlijk van de knoppen minimaliseren, maximaliseren en sluiten.', + windowButtonsDots: 'Stippen', + windowButtonsDotsGlyph: 'Stippen + iconen', + windowButtonsFlat: 'Plat', + windowButtonsPill: 'Pil', + windowButtonsOutline: 'Omtrek', + windowButtonsGlyph: 'Minimaal', + showMinimizeButton: 'Minimaliseerknop tonen', + showMinimizeButtonDesc: 'Schakel uit om alleen maximaliseren en sluiten te tonen.', analyticsStrategyTitle: 'Analysestrategieën', analyticsStrategyDesc: 'Kies de analysemethode voor de bibliotheek.', analyticsStrategyLabel: 'Strategie', diff --git a/src/locales/ro/settings.ts b/src/locales/ro/settings.ts index 91ca87ef..b2dc5533 100644 --- a/src/locales/ro/settings.ts +++ b/src/locales/ro/settings.ts @@ -230,6 +230,16 @@ export const settings = { discordRichPresenceNotice: 'Atenție: aceasta este Discord Rich Presence integrată în Psysonic. Dacă preferi să folosești pluginul oficial Discord Rich Presence de la Navidrome, lasă această funcție dezactivată și activează în schimb „Se afișează în Now Playing" mai jos pe această pagină.', useCustomTitlebar: 'Bară de titlu personalizată', useCustomTitlebarDesc: 'Înlocuiește bara de titlu a sistemului cu una care corespunde cu tema aplicației. Dezactivează pentru a folosi bara de titlu nativ GNOME/GTK.', + windowButtonStyle: 'Butoane de fereastră', + windowButtonStyleDesc: 'Alege aspectul butoanelor de minimizare, maximizare și închidere.', + windowButtonsDots: 'Puncte', + windowButtonsDotsGlyph: 'Puncte + pictograme', + windowButtonsFlat: 'Plat', + windowButtonsPill: 'Pastilă', + windowButtonsOutline: 'Contur', + windowButtonsGlyph: 'Minimal', + showMinimizeButton: 'Afișează butonul de minimizare', + showMinimizeButtonDesc: 'Dezactivează pentru a afișa doar maximizare și închidere.', linuxWebkitSmoothScroll: 'Rotiță lină (Linux)', linuxWebkitSmoothScrollDesc: 'Pornit: scroll inert. Oprit: linie cu linie, în stil GTK.', linuxWebkitInputForceRepaint: 'Redesenare câmpuri la focus (Linux)', diff --git a/src/locales/ru/settings.ts b/src/locales/ru/settings.ts index 927f4cdf..4dbb9eb1 100644 --- a/src/locales/ru/settings.ts +++ b/src/locales/ru/settings.ts @@ -230,6 +230,16 @@ export const settings = { useCustomTitlebar: 'Своя строка заголовка', useCustomTitlebarDesc: 'Заменить системную строку заголовка встроенной, в стиле темы приложения. Отключите, чтобы использовать родную строку GNOME/GTK и т.д.', + windowButtonStyle: 'Кнопки окна', + windowButtonStyleDesc: 'Выберите вид кнопок свернуть, развернуть и закрыть.', + windowButtonsDots: 'Кружки', + windowButtonsDotsGlyph: 'Кружки + значки', + windowButtonsFlat: 'Плоские', + windowButtonsPill: 'Пилюля', + windowButtonsOutline: 'Контур', + windowButtonsGlyph: 'Минимал', + showMinimizeButton: 'Показывать кнопку «Свернуть»', + showMinimizeButtonDesc: 'Выключите, чтобы показывать только «Развернуть» и «Закрыть».', linuxWebkitSmoothScroll: 'Плавное колесо (Linux)', linuxWebkitSmoothScrollDesc: 'Вкл — инерция. Выкл — по шагам, как в GTK.', linuxWebkitInputForceRepaint: 'Перерисовывать поля ввода при фокусе (Linux)', diff --git a/src/locales/zh/settings.ts b/src/locales/zh/settings.ts index fc0b616f..cbf98356 100644 --- a/src/locales/zh/settings.ts +++ b/src/locales/zh/settings.ts @@ -610,6 +610,16 @@ export const settings = { waveformCacheClearFailed: '清除波形缓存失败。', useCustomTitlebar: '自定义标题栏', useCustomTitlebarDesc: '用与应用主题匹配的内置标题栏替换系统标题栏。关闭后将使用原生 GNOME/GTK 标题栏。', + windowButtonStyle: '窗口按钮', + windowButtonStyleDesc: '选择最小化、最大化和关闭按钮的外观。', + windowButtonsDots: '圆点', + windowButtonsDotsGlyph: '圆点 + 图标', + windowButtonsFlat: '扁平', + windowButtonsPill: '胶囊', + windowButtonsOutline: '描边', + windowButtonsGlyph: '极简', + showMinimizeButton: '显示最小化按钮', + showMinimizeButtonDesc: '关闭后仅显示最大化和关闭按钮。', analyticsStrategyTitle: '分析策略', analyticsStrategyDesc: '选择库的分析方式。', analyticsStrategyLabel: '策略', diff --git a/src/store/authStore.persistence.test.ts b/src/store/authStore.persistence.test.ts index 8faa5666..567129e9 100644 --- a/src/store/authStore.persistence.test.ts +++ b/src/store/authStore.persistence.test.ts @@ -131,6 +131,28 @@ describe('onRehydrate migrations', () => { expect(useAuthStore.getState().seekbarStyle).toBe('neon'); }); + it('falls back an invalid windowButtonStyle to `dots`', async () => { + writePersistedState({ + servers: [], + activeServerId: null, + windowButtonStyle: 'bogus', // not in VALID_WINDOW_BUTTON_STYLES + }); + + await useAuthStore.persist.rehydrate(); + expect(useAuthStore.getState().windowButtonStyle).toBe('dots'); + }); + + it('keeps a valid windowButtonStyle unchanged', async () => { + writePersistedState({ + servers: [], + activeServerId: null, + windowButtonStyle: 'glyph', + }); + + await useAuthStore.persist.rehydrate(); + expect(useAuthStore.getState().windowButtonStyle).toBe('glyph'); + }); + it('strips the removed `animationMode` and `reducedAnimations` legacy fields', async () => { writePersistedState({ servers: [], diff --git a/src/store/authStore.settings.test.ts b/src/store/authStore.settings.test.ts index 7f6b5e9d..6de3ecbb 100644 --- a/src/store/authStore.settings.test.ts +++ b/src/store/authStore.settings.test.ts @@ -58,6 +58,8 @@ describe('trivial pass-through setters', () => { ['setDiscordRichPresence', 'discordRichPresence', true], ['setEnableBandsintown', 'enableBandsintown', true], ['setUseCustomTitlebar', 'useCustomTitlebar', true], + ['setWindowButtonStyle', 'windowButtonStyle', 'flat'], + ['setShowMinimizeButton', 'showMinimizeButton', false], ['setPreloadMiniPlayer', 'preloadMiniPlayer', true], ['setLinuxWebkitKineticScroll', 'linuxWebkitKineticScroll', false], ['setLinuxWaylandTextRenderProfile', 'linuxWaylandTextRenderProfile', 'gpu'], diff --git a/src/store/authStore.ts b/src/store/authStore.ts index 2cb68483..1cbc1029 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -74,6 +74,8 @@ export const useAuthStore = create()( discordTemplateLargeText: '{album}', discordTemplateName: '{title}', useCustomTitlebar: false, + windowButtonStyle: 'dots', + showMinimizeButton: true, preloadMiniPlayer: false, linuxWebkitKineticScroll: true, linuxWaylandTextRenderProfile: 'sharp', diff --git a/src/store/authStoreRehydrate.ts b/src/store/authStoreRehydrate.ts index a62bf44d..fcf2fe02 100644 --- a/src/store/authStoreRehydrate.ts +++ b/src/store/authStoreRehydrate.ts @@ -19,6 +19,7 @@ import type { LyricsSourceConfig, QueueDisplayMode, SeekbarStyle, + WindowButtonStyle, } from './authStoreTypes'; import { migrateLegacyLastfm, sanitizeAccounts } from '../music-network'; @@ -93,6 +94,17 @@ export function computeAuthStoreRehydration(state: AuthState): Partial([ + 'dots', 'dotsGlyph', 'flat', 'pill', 'outline', 'glyph', + ]); + const windowButtonStyleMigrated = VALID_WINDOW_BUTTON_STYLES.has( + (state as { windowButtonStyle?: unknown }).windowButtonStyle as string, + ) + ? {} + : { windowButtonStyle: 'dots' as WindowButtonStyle }; + // Garbage / null / undefined / missing key from a legacy or tampered persist // payload maps back to 'total' so the duration chip never receives an // unknown mode (would render an empty label). @@ -236,6 +248,7 @@ export function computeAuthStoreRehydration(state: AuthState): Partial void; setDiscordTemplateName: (v: string) => void; setUseCustomTitlebar: (v: boolean) => void; + setWindowButtonStyle: (v: WindowButtonStyle) => void; + setShowMinimizeButton: (v: boolean) => void; setPreloadMiniPlayer: (v: boolean) => void; setLinuxWebkitKineticScroll: (v: boolean) => void; setLinuxWaylandTextRenderProfile: (v: LinuxWaylandTextRenderProfile) => void; diff --git a/src/store/authUiAppearanceActions.ts b/src/store/authUiAppearanceActions.ts index 60667c01..f7a6f36f 100644 --- a/src/store/authUiAppearanceActions.ts +++ b/src/store/authUiAppearanceActions.ts @@ -19,6 +19,8 @@ export function createUiAppearanceActions(set: SetState): Pick< | 'setClockFormat' | 'setShowOrbitTrigger' | 'setUseCustomTitlebar' + | 'setWindowButtonStyle' + | 'setShowMinimizeButton' | 'setPreloadMiniPlayer' | 'setLinuxWebkitKineticScroll' | 'setLinuxWaylandTextRenderProfile' @@ -41,6 +43,8 @@ export function createUiAppearanceActions(set: SetState): Pick< setClockFormat: (v) => set({ clockFormat: v }), setShowOrbitTrigger: (v) => set({ showOrbitTrigger: v }), setUseCustomTitlebar: (v) => set({ useCustomTitlebar: v }), + setWindowButtonStyle: (v) => set({ windowButtonStyle: v }), + setShowMinimizeButton: (v) => set({ showMinimizeButton: v }), setPreloadMiniPlayer: (v) => set({ preloadMiniPlayer: v }), setLinuxWebkitKineticScroll: (v) => set({ linuxWebkitKineticScroll: v }), setLinuxWaylandTextRenderProfile: (v) => set({ linuxWaylandTextRenderProfile: v }), diff --git a/src/styles/layout/custom-title-bar-linux-only-decorations-false.css b/src/styles/layout/custom-title-bar-linux-only-decorations-false.css index 73c25502..ea877c41 100644 --- a/src/styles/layout/custom-title-bar-linux-only-decorations-false.css +++ b/src/styles/layout/custom-title-bar-linux-only-decorations-false.css @@ -89,49 +89,117 @@ flex: 0 0 auto; } +/* Shared base — each style below overrides shape, fill and glyph colour. */ .titlebar-btn { display: flex; align-items: center; justify-content: center; - width: 14px; - height: 14px; padding: 0; border: none; - border-radius: 50%; + background: transparent; + color: var(--text-primary); cursor: pointer; - box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18); - transition: filter var(--transition-fast), box-shadow var(--transition-fast); + transition: filter var(--transition-fast), box-shadow var(--transition-fast), + background var(--transition-fast), color var(--transition-fast), + border-color var(--transition-fast), opacity var(--transition-fast); } -.titlebar-btn-close { - background: #ff5f57; +.titlebar-btn svg { + width: 10px; + height: 10px; + display: block; } -.titlebar-btn-minimize { - background: #febc2e; -} - -.titlebar-btn-maximize { - background: #28c840; -} - -.titlebar-btn:hover { - filter: brightness(1.1); -} - -.titlebar-btn-close:hover { box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 6px rgba(255, 95, 87, 0.75); } -.titlebar-btn-minimize:hover { box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 6px rgba(254, 188, 46, 0.75); } -.titlebar-btn-maximize:hover { box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 6px rgba(40, 200, 64, 0.75); } - .titlebar-btn:active { filter: brightness(0.92); } .titlebar-btn:focus-visible { outline: none; - box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 0 2px rgba(255, 255, 255, 0.5); + box-shadow: 0 0 0 2px var(--accent); } +/* ── dots / dotsGlyph — colour-circle window buttons ── */ +[data-btnstyle="dots"] .titlebar-btn, +[data-btnstyle="dotsGlyph"] .titlebar-btn { + width: 14px; + height: 14px; + border-radius: 50%; + color: rgba(0, 0, 0, 0.55); + box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18); +} +[data-btnstyle="dots"] .titlebar-btn svg, +[data-btnstyle="dotsGlyph"] .titlebar-btn svg { + width: 8px; + height: 8px; +} +[data-btnstyle="dots"] .titlebar-btn-close, +[data-btnstyle="dotsGlyph"] .titlebar-btn-close { background: #ff5f57; } +[data-btnstyle="dots"] .titlebar-btn-minimize, +[data-btnstyle="dotsGlyph"] .titlebar-btn-minimize { background: #febc2e; } +[data-btnstyle="dots"] .titlebar-btn-maximize, +[data-btnstyle="dotsGlyph"] .titlebar-btn-maximize { background: #28c840; } +[data-btnstyle="dots"] .titlebar-btn:hover, +[data-btnstyle="dotsGlyph"] .titlebar-btn:hover { filter: brightness(1.1); } +[data-btnstyle="dots"] .titlebar-btn-close:hover, +[data-btnstyle="dotsGlyph"] .titlebar-btn-close:hover { box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 6px rgba(255, 95, 87, 0.75); } +[data-btnstyle="dots"] .titlebar-btn-minimize:hover, +[data-btnstyle="dotsGlyph"] .titlebar-btn-minimize:hover { box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 6px rgba(254, 188, 46, 0.75); } +[data-btnstyle="dots"] .titlebar-btn-maximize:hover, +[data-btnstyle="dotsGlyph"] .titlebar-btn-maximize:hover { box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 6px rgba(40, 200, 64, 0.75); } +/* dots: glyphs reveal on hover (authentic); dotsGlyph: always shown */ +[data-btnstyle="dots"] .titlebar-btn svg { opacity: 0; } +[data-btnstyle="dots"] .titlebar-controls:hover .titlebar-btn svg, +[data-btnstyle="dots"]:hover .titlebar-btn svg { opacity: 1; } +[data-btnstyle="dotsGlyph"] .titlebar-btn svg { opacity: 0.85; } + +/* ── flat — full-height rectangular buttons, red close hover ── */ +.titlebar-controls[data-btnstyle="flat"] { gap: 0; padding: 0; margin-right: -6px; } +[data-btnstyle="flat"] .titlebar-btn { + width: 30px; + height: var(--titlebar-height); + border-radius: 0; +} +[data-btnstyle="flat"] .titlebar-btn:hover { background: var(--bg-hover); color: var(--text-primary); } +[data-btnstyle="flat"] .titlebar-btn-close:hover { background: #e81123; color: #fff; } + +/* ── pill — soft circular monochrome buttons ── */ +[data-btnstyle="pill"] .titlebar-btn { + width: 22px; + height: 22px; + border-radius: 50%; + background: var(--bg-hover); +} +[data-btnstyle="pill"] .titlebar-btn:hover { filter: brightness(1.18); color: var(--text-primary); } +[data-btnstyle="pill"] .titlebar-btn-close:hover { background: #e01b24; color: #fff; filter: none; } + +/* ── outline — square bordered buttons, accent hover ── */ +[data-btnstyle="outline"] .titlebar-btn { + width: 20px; + height: 20px; + border-radius: 4px; + border: 1px solid var(--border); +} +[data-btnstyle="outline"] .titlebar-btn:hover { + border-color: var(--accent); + color: var(--accent); + background: color-mix(in srgb, var(--accent) 10%, transparent); +} +[data-btnstyle="outline"] .titlebar-btn-close:hover { + border-color: #e01b24; + color: #e01b24; + background: color-mix(in srgb, #e01b24 12%, transparent); +} + +/* ── glyph — themed monochrome, no fill, blends with the app ── */ +[data-btnstyle="glyph"] .titlebar-btn { + width: 22px; + height: 22px; + border-radius: 6px; +} +[data-btnstyle="glyph"] .titlebar-btn:hover { background: var(--bg-hover); color: var(--text-primary); } +[data-btnstyle="glyph"] .titlebar-btn-close:hover { background: var(--bg-hover); color: #ff5f57; } + /* Resizer handles must start below the titlebar */ .app-shell[data-titlebar] .resizer { top: var(--titlebar-height); diff --git a/src/styles/layout/dev-build-chrome.css b/src/styles/layout/dev-build-chrome.css index b274824e..8202a8a8 100644 --- a/src/styles/layout/dev-build-chrome.css +++ b/src/styles/layout/dev-build-chrome.css @@ -9,26 +9,23 @@ html[data-dev-build] .sidebar-brand svg { --logo-color-end: #fecaca; } -html[data-dev-build] .titlebar-btn-close, -html[data-dev-build] .titlebar-btn-minimize, -html[data-dev-build] .titlebar-btn-maximize { +/* Scope the dev-grey to the real title bar only — the Settings window-button + * style previews reuse `.titlebar-btn` classes and must show true colours. */ +html[data-dev-build] .titlebar .titlebar-btn-close, +html[data-dev-build] .titlebar .titlebar-btn-minimize, +html[data-dev-build] .titlebar .titlebar-btn-maximize { background: #8b8b8b; } -html[data-dev-build] .titlebar-btn-close:hover, -html[data-dev-build] .titlebar-btn-minimize:hover, -html[data-dev-build] .titlebar-btn-maximize:hover { +html[data-dev-build] .titlebar .titlebar-btn-close:hover, +html[data-dev-build] .titlebar .titlebar-btn-minimize:hover, +html[data-dev-build] .titlebar .titlebar-btn-maximize:hover { background: #a3a3a3; box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18); -} - -html[data-dev-build] .titlebar-btn-close:hover, -html[data-dev-build] .titlebar-btn-minimize:hover, -html[data-dev-build] .titlebar-btn-maximize:hover { filter: none; } -html[data-dev-build] .titlebar-btn:focus-visible { +html[data-dev-build] .titlebar .titlebar-btn:focus-visible { box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 0 2px rgba(255, 255, 255, 0.35); }