From c9b2d140d96f141e10445c9e045da12dc15fb716 Mon Sep 17 00:00:00 2001 From: cucadmuh <49571317+cucadmuh@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:03:24 +0300 Subject: [PATCH] fix(ui): shrink-wrap floating player bar instead of full-width strip (#969) * fix(ui): shrink-wrap floating player bar instead of full-width strip The bar used fixed left and right insets, which stretched its background across the whole main column. Center it with max-content width so only the pill-shaped controls are painted, and soften the drop shadow. * docs: note PR #969 in changelog and settings credits --- CHANGELOG.md | 7 ++++++ src/config/settingsCredits.ts | 1 + .../computeFloatingPlayerBarStyle.test.ts | 19 +++++++++++++++ src/hooks/computeFloatingPlayerBarStyle.ts | 24 +++++++++++++++++++ src/hooks/useFloatingPlayerBar.ts | 15 +++++------- src/styles/layout/_intro.css | 4 ++++ src/styles/layout/player-bar.css | 9 ++++--- 7 files changed, 65 insertions(+), 14 deletions(-) create mode 100644 src/hooks/computeFloatingPlayerBarStyle.test.ts create mode 100644 src/hooks/computeFloatingPlayerBarStyle.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ae901880..ecae8d99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -491,6 +491,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Nonsense queries no longer return unrelated fuzzy matches (e.g. long repeated letters). +### Floating player bar — remove full-width background strip + +**By [@cucadmuh](https://github.com/cucadmuh), reported by Asra on the Psysonic Discord, PR [#969](https://github.com/Psychotoxical/psysonic/pull/969)** + +* Floating mode no longer stretches the player bar between sidebar and queue with fixed `left`/`right` — only the centered pill is painted over the page instead of a full-width black band behind the rounded corners. + + ### In-page browse — virtual scroll and cover-art priority **By [@cucadmuh](https://github.com/cucadmuh), PR [#783](https://github.com/Psychotoxical/psysonic/pull/783)** diff --git a/src/config/settingsCredits.ts b/src/config/settingsCredits.ts index ea97939a..07dc3d2c 100644 --- a/src/config/settingsCredits.ts +++ b/src/config/settingsCredits.ts @@ -154,6 +154,7 @@ const CONTRIBUTOR_ENTRIES = [ 'Player: persist volume/repeat/queue visibility/Last.fm cache outside quota-bound queue blob (report: norp on Psysonic Discord) (PR #958)', 'Player transport: custom delay input capped to browser timer limit; preview/countdown aligned for fractional minutes (report: zunoz on Psysonic Discord) (PR #967)', 'Settings: in-page search indexes AudioMuse + shortcut rows; rejects junk fuzzy matches (report: zunoz on Psysonic Discord) (PR #968)', + 'Floating player bar: center shrink-wrapped pill instead of full-width background strip (report: Asra on Psysonic Discord) (PR #969)', ], }, { diff --git a/src/hooks/computeFloatingPlayerBarStyle.test.ts b/src/hooks/computeFloatingPlayerBarStyle.test.ts new file mode 100644 index 00000000..938970d8 --- /dev/null +++ b/src/hooks/computeFloatingPlayerBarStyle.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; +import { computeFloatingPlayerBarStyle } from './computeFloatingPlayerBarStyle'; + +describe('computeFloatingPlayerBarStyle', () => { + it('centers in the main column and shrink-wraps instead of stretching', () => { + const style = computeFloatingPlayerBarStyle(220, 1600, 1920); + expect(style.left).toBe(910); + expect(style.right).toBe('auto'); + expect(style.transform).toBe('translateX(-50%)'); + expect(style.width).toBe('max-content'); + expect(style.maxWidth).toBe(1332); + }); + + it('accounts for a hidden queue panel', () => { + const style = computeFloatingPlayerBarStyle(220, null, 1920); + expect(style.left).toBe(1070); + expect(style.maxWidth).toBe(1652); + }); +}); diff --git a/src/hooks/computeFloatingPlayerBarStyle.ts b/src/hooks/computeFloatingPlayerBarStyle.ts new file mode 100644 index 00000000..8919cf77 --- /dev/null +++ b/src/hooks/computeFloatingPlayerBarStyle.ts @@ -0,0 +1,24 @@ +import type { CSSProperties } from 'react'; + +const HORIZONTAL_MARGIN_PX = 24; + +/** Center the floating bar in the main column; shrink-wrap width instead of stretching. */ +export function computeFloatingPlayerBarStyle( + sidebarRight: number, + queueLeft: number | null, + viewportWidth: number, +): CSSProperties { + const contentLeft = sidebarRight; + const contentRight = queueLeft ?? viewportWidth; + const contentWidth = Math.max(0, contentRight - contentLeft); + const maxWidth = Math.max(100, contentWidth - HORIZONTAL_MARGIN_PX * 2); + const centerX = contentLeft + contentWidth / 2; + + return { + left: centerX, + right: 'auto', + transform: 'translateX(-50%)', + width: 'max-content', + maxWidth, + }; +} diff --git a/src/hooks/useFloatingPlayerBar.ts b/src/hooks/useFloatingPlayerBar.ts index 3e36d160..f7b3d068 100644 --- a/src/hooks/useFloatingPlayerBar.ts +++ b/src/hooks/useFloatingPlayerBar.ts @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { computeFloatingPlayerBarStyle } from './computeFloatingPlayerBarStyle'; /** Computes the floating player-bar position based on the current sidebar + * queue panel widths. Returns an inline-style object (left/right/width); only @@ -14,17 +15,13 @@ export function useFloatingPlayerBar( if (!floatingPlayerBar) return; const updatePosition = () => { - const sidebar = document.querySelector('.sidebar') as HTMLElement; - const queue = document.querySelector('.queue-panel') as HTMLElement; + const sidebar = document.querySelector('.sidebar') as HTMLElement | null; + const queue = document.querySelector('.queue-panel') as HTMLElement | null; - const leftOffset = sidebar ? sidebar.getBoundingClientRect().right : 0; - const rightOffset = queue ? window.innerWidth - queue.getBoundingClientRect().left : 0; + const sidebarRight = sidebar ? sidebar.getBoundingClientRect().right : 0; + const queueLeft = queue ? queue.getBoundingClientRect().left : null; - setFloatingStyle({ - left: leftOffset + 24, - right: rightOffset + 24, - width: 'auto', - }); + setFloatingStyle(computeFloatingPlayerBarStyle(sidebarRight, queueLeft, window.innerWidth)); }; updatePosition(); diff --git a/src/styles/layout/_intro.css b/src/styles/layout/_intro.css index 322ec64f..9eb8287c 100644 --- a/src/styles/layout/_intro.css +++ b/src/styles/layout/_intro.css @@ -40,3 +40,7 @@ "sidebar main queue"; } +.app-shell.floating-player .resizer { + bottom: 0; +} + diff --git a/src/styles/layout/player-bar.css b/src/styles/layout/player-bar.css index beb7958d..daf0a7c3 100644 --- a/src/styles/layout/player-bar.css +++ b/src/styles/layout/player-bar.css @@ -21,19 +21,18 @@ .player-bar.floating { position: fixed; bottom: 12px; - /* left/right handled dynamically by JS */ + /* left / max-width set by useFloatingPlayerBar — center + shrink-wrap */ z-index: 1000; border-radius: 50px; - width: auto; - min-width: 100px; - max-width: none; + width: max-content; + max-width: min(100%, calc(100vw - 48px)); grid-area: unset; padding: 0 24px 0 8px; gap: 16px; background: var(--bg-player); border: 1px solid rgba(255, 255, 255, 0.12); box-shadow: - 0 8px 32px rgba(0, 0, 0, 0.8), + 0 8px 32px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.08); }