Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5bef0fa
fix: docked queue scroll-to-now-playing never fired on track/group ch…
May 5, 2026
50cef71
style: add gradient background to docked queue sidebar
May 5, 2026
9e70183
style: transparent docked queue background — inherits app gradient
May 6, 2026
d88a804
style: boost purple/blue radial gradient prominence in app shell
May 6, 2026
75c7f72
style: remove docked queue border-left — borderless floating scrollbar
May 6, 2026
dc53246
style: wrap docked queue window controls in a frosted pill
May 6, 2026
10decd6
style: fatter pill scrollbar on queue content area
May 6, 2026
d747cba
style: resize handle highlight follows mouse as a centred 200px pill
May 6, 2026
85e4848
style: resize handle glows and bulges with a purple lens on hover
May 6, 2026
8179740
style: remove outer box-shadow glow from resize handle
May 6, 2026
2cc386d
style: scrollbar — top gap, shorter thumb, wider hit box
May 6, 2026
77ffe97
style: scrollbar thumb taller (52px) and thinner (4px)
May 6, 2026
f4c1887
style: scrollbar 6px wide, 10px hit box, 124px thumb
May 6, 2026
435255b
style: scrollbar thumb capped at 20% of container height
May 6, 2026
8190648
style: scrollbar thumb max-height 300px
May 6, 2026
d084dde
style: global scrollbar — 10px hit box, 6px visual pill, top gap
May 6, 2026
6188bf5
feat: hide queue button in player bar and header when queue is docked
May 6, 2026
7f91be3
feat: centre player bar over main content area when queue is docked
May 6, 2026
122f49f
feat: centre top nav pill over main content area when queue is docked
May 6, 2026
3db71ea
feat: player bar and nav pill recentre live during queue resize drag
May 6, 2026
f4a081f
feat: queue max width derived from player bar width + padding
May 6, 2026
79e401b
feat: enforce minimum window width of 1144px (864 + min queue width)
May 6, 2026
4cc913b
style: resize handle glow emanates right only into queue
May 6, 2026
140df31
style: resize handle glow — larger (280x400) and more diffuse
May 6, 2026
4eb0b7e
style: Refine QueueSidebar resize handle appearance and usability
May 6, 2026
a6fb041
feat: scroll-sync panel-colour gradient and docked queue visual polish
May 6, 2026
70e400a
fix: restore main content scroll broken by routes wrapper div
May 6, 2026
50d3479
fix: simplify gradient — set directly on .body, no pseudo-element or JS
May 6, 2026
8f0de96
fix: prevent song change from clobbering body gradient colour
May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,12 @@ function MainApp() {
const [queueMode, setQueueMode] = useState<'floating' | 'docked'>('floating');
const [queueDockedWidth, setQueueDockedWidth] = useState<number>(380);
const queueSidebarRef = useRef<QueueSidebarHandle>(null);
const shellRef = useRef<HTMLDivElement>(null);
const handleResizeWidthLive = useCallback((width: number) => {
shellRef.current?.style.setProperty('--docked-queue-w', `${width}px`);
}, []);

useEffect(() => {
useEffect(() => {
window.sonos.getDisplayName().then(setDisplayName);
window.sonos.getQueueMode().then(setQueueMode).catch(() => {});
window.sonos.getQueueDockedWidth().then(setQueueDockedWidth).catch(() => {});
Expand All @@ -99,7 +103,7 @@ function MainApp() {
useEffect(() => {
if (queueMode !== 'docked') return;
function clamp() {
const max = Math.min(700, window.innerWidth - 320);
const max = window.innerWidth - (800 + 64);
setQueueDockedWidth((w) => (w > max ? max : w));
}
window.addEventListener('resize', clamp);
Expand Down Expand Up @@ -436,7 +440,11 @@ function MainApp() {
const splashReady = isAuthed && groups.length > 0 && !ytmLoading && !histLoading;

return (
<div className={styles.shell}>
<div
ref={shellRef}
className={styles.shell}
style={queueMode === 'docked' ? { '--docked-queue-w': `${queueDockedWidth}px` } as React.CSSProperties : undefined}
>
<Splash ready={splashReady} />
<TopNav
isAuthed={isAuthed}
Expand Down Expand Up @@ -510,6 +518,7 @@ function MainApp() {
onAddToQueue={handleAddToQueue}
dockedWidth={queueDockedWidth}
onResizeWidth={handleSetQueueDockedWidth}
onResizeWidthLive={handleResizeWidthLive}
/>
)}
</div>
Expand Down
8 changes: 5 additions & 3 deletions renderer/src/components/PlayerBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,11 @@ export function PlayerBar({ isAuthed, playback, onToggleQueue, onShuffle, queueM
>
<MicVocal size={14} />
</button>
<button className={styles.ctrl} onClick={onToggleQueue} title={queueMode === 'docked' ? 'Jump to now playing' : 'Queue'}>
<List size={14} />
</button>
{queueMode !== 'docked' && (
<button className={styles.ctrl} onClick={onToggleQueue} title="Queue">
<List size={14} />
</button>
)}
<button
className={styles.ctrl}
onClick={() => window.sonos.openMiniPlayer()}
Expand Down
16 changes: 9 additions & 7 deletions renderer/src/components/TopNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -306,13 +306,15 @@ export function TopNav({
</button>
)}

<button
className={`${styles.iconBtn}${queueMode === 'floating' && queueOpen ? ' ' + styles.active : ''}`}
onClick={onToggleQueue}
title={queueMode === 'docked' ? 'Jump to now playing' : 'Queue'}
>
<List size={15} />
</button>
{queueMode !== 'docked' && (
<button
className={`${styles.iconBtn}${queueOpen ? ' ' + styles.active : ''}`}
onClick={onToggleQueue}
title="Queue"
>
<List size={15} />
</button>
)}
</div>
</nav>
</div>
Expand Down
2 changes: 1 addition & 1 deletion renderer/src/components/album/AlbumPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function AlbumPanel({ onAddToQueue, queueOpen }: Props) {
const artist = data?.artist ?? ((item as Record<string, unknown>)?.['subtitle'] as string) ?? '';
const artUrl = data?.artUrl ?? (item ? getItemArt(item) : null);
const cachedArt = useImage(artUrl);
const dominantColor = useDominantColor(cachedArt);
const dominantColor = useDominantColor(cachedArt, { setGlobal: true });

useEffect(() => {
setSelected(new Set());
Expand Down
2 changes: 1 addition & 1 deletion renderer/src/components/artist/ArtistHero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function ArtistHero({
});

const cachedArt = useImage(getItemArt(artist));
const dominantColor = useDominantColor(cachedArt);
const dominantColor = useDominantColor(cachedArt, { setGlobal: true });
const name = (artist.title ?? artist.name ?? '') as string;

const [selected, setSelected] = useState<Set<number>>(new Set());
Expand Down
2 changes: 1 addition & 1 deletion renderer/src/components/artist/ArtistPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export function ArtistPanel({ onAddToQueue }: Props) {
}

const cachedArt = useImage(imageUrl);
const dominantColor = useDominantColor(cachedArt);
const dominantColor = useDominantColor(cachedArt, { setGlobal: true });

const artistRadio = data?.playlists.find(p => (p.title as string)?.toLowerCase().includes('radio'));
const latestAlbum = data?.albums[0] ?? null;
Expand Down
5 changes: 5 additions & 0 deletions renderer/src/components/queue/DraggableQueueRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export function DraggableQueueRow({
].filter(Boolean).join(' ')}
data-playing={isPlaying ? 'true' : undefined}
draggable
onPointerMove={e => {
const r = e.currentTarget.getBoundingClientRect();
e.currentTarget.style.setProperty('--mx', `${e.clientX - r.left}px`);
e.currentTarget.style.setProperty('--my', `${e.clientY - r.top}px`);
}}
onClick={e => onRowClick(index, e)}
onDoubleClick={() => getActiveProvider().skipToTrack(index + 1)}
onDragStart={e => onDragStart(index, e)}
Expand Down
22 changes: 14 additions & 8 deletions renderer/src/components/queue/QueueSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,16 @@ interface Props {
onAddToQueue: (item: SonosItem, position: number) => void;
dockedWidth?: number;
onResizeWidth?: (width: number) => void;
onResizeWidthLive?: (width: number) => void;
}

export interface QueueSidebarHandle {
scrollToNowPlaying: () => void;
}

const MIN_DOCKED_WIDTH = 280;
const MAX_DOCKED_WIDTH = 700;
const MIN_ROUTES_WIDTH = 320;
const PLAYER_BAR_W = 800; // matches .inner width in PlayerBar.module.css
const PLAYER_BAR_PADDING = 64; // 32px breathing room each side

export const QueueSidebar = forwardRef<QueueSidebarHandle, Props>(function QueueSidebar(
{
Expand All @@ -58,6 +59,7 @@ export const QueueSidebar = forwardRef<QueueSidebarHandle, Props>(function Queue
onAddToQueue,
dockedWidth,
onResizeWidth,
onResizeWidthLive,
},
ref
) {
Expand Down Expand Up @@ -122,16 +124,16 @@ export const QueueSidebar = forwardRef<QueueSidebarHandle, Props>(function Queue
return () => clearTimeout(id);
}, [isActive]);

// Track change while queue is open
// Track change while queue is visible
useEffect(() => {
if (!open) return;
if (!isActive) return;
const id = setTimeout(scrollToNowPlaying, 50);
return () => clearTimeout(id);
}, [currentQueueItemId]); // eslint-disable-line react-hooks/exhaustive-deps

// Group switch — queue reloads async so use a longer delay
useEffect(() => {
if (!open) return;
if (!isActive) return;
const id = setTimeout(scrollToNowPlaying, 400);
return () => clearTimeout(id);
}, [groupName]); // eslint-disable-line react-hooks/exhaustive-deps
Expand Down Expand Up @@ -160,11 +162,12 @@ export const QueueSidebar = forwardRef<QueueSidebarHandle, Props>(function Queue
handle.setPointerCapture(e.pointerId);
document.documentElement.classList.add('resizingQueue');

const max = () => Math.min(MAX_DOCKED_WIDTH, window.innerWidth - MIN_ROUTES_WIDTH);
const max = () => window.innerWidth - (PLAYER_BAR_W + PLAYER_BAR_PADDING);

const onMove = (ev: PointerEvent) => {
const next = Math.max(MIN_DOCKED_WIDTH, Math.min(max(), startWidth + (startX - ev.clientX)));
setLiveWidth(next);
onResizeWidthLive?.(next);
};
const onUp = () => {
document.documentElement.classList.remove('resizingQueue');
Expand Down Expand Up @@ -283,14 +286,17 @@ export const QueueSidebar = forwardRef<QueueSidebarHandle, Props>(function Queue
<div
className={styles.resizeHandle}
onPointerDown={handleResizePointerDown}
onPointerMove={e => e.currentTarget.style.setProperty('--mouse-y', `${e.nativeEvent.offsetY}px`)}
role="separator"
aria-orientation="vertical"
aria-label="Resize queue"
/>
)}
{isDocked && (
<div className={styles.dockedTopBar}>
<WindowControls />
<div className={styles.winPill}>
<WindowControls />
</div>
</div>
)}
<div className={styles.header}>
Expand Down Expand Up @@ -325,7 +331,7 @@ export const QueueSidebar = forwardRef<QueueSidebarHandle, Props>(function Queue
<div
className={styles.content}
ref={contentRef}
onClick={handleContentClick}
onClick={handleContentClick}
onDragOver={e => {
e.preventDefault();
e.dataTransfer.dropEffect = e.dataTransfer.types.includes('application/sonos-item-list') ? 'copy' : 'move';
Expand Down
12 changes: 11 additions & 1 deletion renderer/src/hooks/useDominantColor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';

export function useDominantColor(src: string | null): string | null {
export function useDominantColor(src: string | null, { setGlobal = false } = {}): string | null {
const [color, setColor] = useState<string | null>(null);

useEffect(() => {
Expand Down Expand Up @@ -38,5 +38,15 @@ export function useDominantColor(src: string | null): string | null {
img.src = src;
}, [src]);

useEffect(() => {
if (!setGlobal) return;
if (color) {
document.documentElement.style.setProperty('--panel-color', color);
} else {
document.documentElement.style.removeProperty('--panel-color');
}
return () => { document.documentElement.style.removeProperty('--panel-color'); };
}, [color, setGlobal]);

return color;
}
14 changes: 11 additions & 3 deletions renderer/src/styles/App.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
flex-direction: column;
overflow: hidden;
background:
radial-gradient(ellipse at 20% 50%, rgba(70, 45, 110, 0.45) 0%, transparent 60%),
radial-gradient(ellipse at 80% 15%, rgba(25, 55, 95, 0.35) 0%, transparent 55%),
radial-gradient(ellipse at 55% 85%, rgba(20, 55, 85, 0.3) 0%, transparent 50%),
radial-gradient(ellipse at 20% 50%, rgba(90, 50, 150, 0.7) 0%, transparent 60%),
radial-gradient(ellipse at 80% 15%, rgba(25, 70, 130, 0.55) 0%, transparent 55%),
radial-gradient(ellipse at 55% 85%, rgba(20, 65, 110, 0.45) 0%, transparent 50%),
#0f0f14;
}

Expand All @@ -15,6 +15,14 @@
min-height: 0;
overflow: hidden;
display: flex;
position: relative;
background: linear-gradient(
180deg,
rgba(var(--panel-color, 80, 60, 120), 0.55) 0%,
rgba(var(--panel-color, 80, 60, 120), 0.2) 20%,
transparent 40%
);
transition: background 0.8s ease;
}

.toast {
Expand Down
2 changes: 1 addition & 1 deletion renderer/src/styles/PlayerBar.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
.bar {
position: fixed;
bottom: 24px;
left: 50%;
left: calc((100vw - var(--docked-queue-w, 0px)) / 2);
transform: translateX(-50%) translateZ(0);
z-index: 150;
border-radius: 22px;
Expand Down
Loading
Loading