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
This commit is contained in:
cucadmuh
2026-06-04 00:03:24 +03:00
committed by GitHub
parent e8962c21ab
commit c9b2d140d9
7 changed files with 65 additions and 14 deletions
+7
View File
@@ -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)**
+1
View File
@@ -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)',
],
},
{
@@ -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);
});
});
@@ -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,
};
}
+6 -9
View File
@@ -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();
+4
View File
@@ -40,3 +40,7 @@
"sidebar main queue";
}
.app-shell.floating-player .resizer {
bottom: 0;
}
+4 -5
View File
@@ -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);
}